7 Commits

Author SHA1 Message Date
end 5461061e7e 1.3.1 2026-07-14 18:01:12 -07:00
end 39e1f9e9ad fix(error): GraphQL-aligned GraphError type
Expand GraphError.path to accept strings and numbers for field names
and list indexes, rather than strings only. Constrain isRawError to
check this assumption. Add custom path formatter to use dot notation for
fields and brackets for indexes.
2026-07-14 18:01:03 -07:00
end d75b8333e5 1.3.0 2026-05-06 19:10:58 -07:00
end a2097567a0 improve tab padding controls 2026-05-06 19:10:48 -07:00
end 7386bff7be fix PhoneInput combobox icon override 2026-05-06 19:06:41 -07:00
end e8440bc106 1.2.0 2026-05-04 03:34:14 -07:00
end d1019659f5 ErrorMessage: accept unknown in constructor 2026-04-21 18:08:57 -07:00
5 changed files with 53 additions and 35 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "git", "type": "git",
"url": "https://gitea.auvem.com/svelte-toolkit/sui.git" "url": "https://gitea.auvem.com/svelte-toolkit/sui.git"
}, },
"version": "1.1.3", "version": "1.3.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && pnpm run prepack", "build": "vite build && pnpm run prepack",
+8 -9
View File
@@ -50,8 +50,7 @@
const options: ComboboxOption[] = countries.map((country) => ({ const options: ComboboxOption[] = countries.map((country) => ({
value: country.isoCode, value: country.isoCode,
label: `${country.name} (+${country.phonecode})`, label: `${country.name} (+${country.phonecode})`,
preview: `+${country.phonecode}`, preview: `+${country.phonecode}`
icon: renderIcon
})); }));
let phonecode: string = $derived.by(() => { let phonecode: string = $derived.by(() => {
@@ -118,12 +117,6 @@
}); });
</script> </script>
{#snippet renderIcon(item: ComboboxOption)}
{#if countrycodeMap[item.value]?.flag}
{countrycodeMap[item.value].flag}
{/if}
{/snippet}
<div class={classValue}> <div class={classValue}>
{#if label} {#if label}
<Label for={id}>{label}</Label> <Label for={id}>{label}</Label>
@@ -149,7 +142,13 @@
countriesValid = e.detail.valid; countriesValid = e.detail.valid;
}} }}
invalidMessage={null} invalidMessage={null}
/> >
{#snippet iconRender(opt)}
{#if countrycodeMap[opt.value]?.flag}
{countrycodeMap[opt.value].flag}
{/if}
{/snippet}
</Combobox>
</div> </div>
<div class="w-full"> <div class="w-full">
+9 -14
View File
@@ -23,22 +23,17 @@
activeIndex?: number; activeIndex?: number;
/** Callback fired when the active tab changes */ /** Callback fired when the active tab changes */
onchange?: (event: { index: number; tab: TabPage }) => void; onchange?: (event: { index: number; tab: TabPage }) => void;
/** Applies layout padding to the tab header (default: false) */ /**
padHeader?: boolean; * Controls padding of content areas. True applies padding to content
/** Applies layout padding to the content areas (default: false) */ * and header, false applies no padding (default), and 'content' and
padContent?: 'padding' | 'margin' | 'none'; * 'header' apply padding to their respective areas only.
*/
padded?: boolean | 'content' | 'header';
/** Additional classes applied to the outer container */ /** Additional classes applied to the outer container */
class?: ClassValue | null; class?: ClassValue | null;
} }
let { let { pages, activeIndex = 0, onchange, padded = false, class: classValue }: Props = $props();
pages,
activeIndex = 0,
onchange,
padHeader = false,
padContent = 'none',
class: classValue
}: Props = $props();
let primaryContainerEl: HTMLDivElement; let primaryContainerEl: HTMLDivElement;
let tabContainerEl: HTMLDivElement; let tabContainerEl: HTMLDivElement;
@@ -109,7 +104,7 @@
bind:this={tabContainerEl} bind:this={tabContainerEl}
class={[ class={[
'border-sui-text/15 relative mb-4 flex items-center gap-5 border-b-2', 'border-sui-text/15 relative mb-4 flex items-center gap-5 border-b-2',
padHeader && 'px-layout' padded === true || padded === 'header' ? 'px-layout' : ''
]} ]}
> >
{#each pages as page, i (page.title)} {#each pages as page, i (page.title)}
@@ -144,7 +139,7 @@
{#key activeIndex} {#key activeIndex}
<div <div
class={[padContent === 'padding' && 'px-layout', padContent === 'margin' && 'mx-layout']} class={[padded === true || padded === 'content' ? 'px-layout' : '']}
in:flyX={{ direction: activeIndex > prevIndex ? 1 : -1, duration: 180, delay: 181 }} in:flyX={{ direction: activeIndex > prevIndex ? 1 : -1, duration: 180, delay: 181 }}
out:flyX={{ direction: activeIndex > prevIndex ? -1 : 1, duration: 180 }} out:flyX={{ direction: activeIndex > prevIndex ? -1 : 1, duration: 180 }}
onoutrostart={lockHeight} onoutrostart={lockHeight}
+34 -10
View File
@@ -3,7 +3,8 @@
*/ */
export interface GraphError { export interface GraphError {
message: string; message: string;
path?: string[]; /** GraphQL response path - field names and list indexes. */
path?: ReadonlyArray<string | number>;
} }
/** RawError is an error that can be converted to a string by ErrorMessage */ /** RawError is an error that can be converted to a string by ErrorMessage */
@@ -15,14 +16,22 @@ export type RawError = ErrorMessage | Error | string | GraphError[];
* @returns true if the error is a RawError, false otherwise * @returns true if the error is a RawError, false otherwise
*/ */
export const isRawError = (error: unknown): error is RawError => { export const isRawError = (error: unknown): error is RawError => {
const isGraphError = (entry: unknown): entry is GraphError => {
if (!entry || typeof entry !== 'object') return false;
const candidate = entry as { message?: unknown; path?: unknown };
if (typeof candidate.message !== 'string') return false;
if (candidate.path === undefined) return true;
return (
Array.isArray(candidate.path) &&
candidate.path.every((p) => typeof p === 'string' || typeof p === 'number')
);
};
return ( return (
error instanceof ErrorMessage || error instanceof ErrorMessage ||
error instanceof Error || error instanceof Error ||
typeof error === 'string' || typeof error === 'string' ||
(Array.isArray(error) && (Array.isArray(error) && error.every(isGraphError))
error.every(
(e) => typeof e.message === 'string' && (e.path === undefined || Array.isArray(e.path))
))
); );
}; };
@@ -73,12 +82,27 @@ export const checkGraphResponse = (
export class ErrorMessage { export class ErrorMessage {
private _lines: string[] = []; private _lines: string[] = [];
/** formats a GraphQL path using dot notation for fields and brackets for indexes */
private static formatGraphPath(path: ReadonlyArray<string | number>): string {
let formatted = '';
for (const segment of path) {
if (typeof segment === 'number') {
formatted += `[${segment}]`;
} else if (formatted.length === 0) {
formatted = segment;
} else {
formatted += `.${segment}`;
}
}
return formatted;
}
/** /**
* Always creates a new ErrorMessage instance, even if there are no errors. * Always creates a new ErrorMessage instance, even if there are no errors.
* @param errors The raw errors to convert and store, or null/undefined for no error. * @param errors The raw errors to convert and store, or null/undefined for no error.
* @throws If any of the raw errors are of an unsupported type. * @throws If any of the raw errors are of an unsupported type.
*/ */
constructor(...errors: (RawError | null | undefined)[]) { constructor(...errors: (unknown | null | undefined)[]) {
if (errors.length === 0) return; if (errors.length === 0) return;
this._lines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e)); this._lines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e));
} }
@@ -87,7 +111,7 @@ export class ErrorMessage {
* Creates a new ErrorMessage only if the provided errors are not null or * Creates a new ErrorMessage only if the provided errors are not null or
* undefined. If no errors are provided, returns null. * undefined. If no errors are provided, returns null.
*/ */
static from(...errors: (RawError | null | undefined)[]): ErrorMessage | null { static from(...errors: (unknown | null | undefined)[]): ErrorMessage | null {
if (errors.length === 0) return null; if (errors.length === 0) return null;
return new ErrorMessage(...errors); return new ErrorMessage(...errors);
} }
@@ -97,7 +121,7 @@ export class ErrorMessage {
* @param errors The raw errors to wrap around this error, or null/undefined for no additional error. * @param errors The raw errors to wrap around this error, or null/undefined for no additional error.
* @returns A new ErrorMessage instance that includes the original error and any new errors. * @returns A new ErrorMessage instance that includes the original error and any new errors.
*/ */
wrap(...errors: (RawError | null | undefined)[]): ErrorMessage { wrap(...errors: (unknown | null | undefined)[]): ErrorMessage {
if (errors.length === 0) return this; if (errors.length === 0) return this;
const newLines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e)); const newLines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e));
return new ErrorMessage(...newLines, ...this._lines); return new ErrorMessage(...newLines, ...this._lines);
@@ -122,7 +146,7 @@ export class ErrorMessage {
} }
/** converts a RawError to an array of lines */ /** converts a RawError to an array of lines */
static rawErrorToLines(raw: RawError | null | undefined): string[] { static rawErrorToLines(raw: unknown | null | undefined): string[] {
if (!raw) return []; if (!raw) return [];
let errorLines: string[]; let errorLines: string[];
@@ -136,7 +160,7 @@ export class ErrorMessage {
errorLines = raw.map((e) => { errorLines = raw.map((e) => {
const messageString = e.message || 'Unknown error'; const messageString = e.message || 'Unknown error';
if (e.path && e.path.length > 0) { if (e.path && e.path.length > 0) {
return `"${messageString}" at ${e.path.join('.')}`; return `"${messageString}" at ${ErrorMessage.formatGraphPath(e.path)}`;
} }
return messageString; return messageString;
}); });
+1 -1
View File
@@ -448,7 +448,7 @@
<p class="title">Tabs</p> <p class="title">Tabs</p>
<Tabs <Tabs
padded={true} padded
pages={[ pages={[
{ {
title: 'Dashboard', title: 'Dashboard',