23 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
end 52cd3c6e8d add preventable and reversible event helper system 2026-04-20 19:15:00 -07:00
end 7e6fdc4071 Dialog: pass state to close snippet 2026-04-15 17:06:08 -07:00
end 9f19a36994 Combobox: allow null value 2026-04-15 17:04:49 -07:00
end 740e038382 error: extend ErrorMessage & helpers for improved ergonomics
- added isRawError helper
- added catchErrorString helper to convert any error to a string
- added checkGraphResponse to throw if error is present or data missing
- ErrorMessage now supports wrapping multiple errors ergonomically
2026-04-15 17:04:32 -07:00
end c991ac044f Combobox: fix icon padding issue with compact mode 2026-04-12 22:52:19 -07:00
end e6fc8e8b37 floating: add position reference element override 2026-04-12 17:52:08 -07:00
end ab352b217a floating: add constraint size option 2026-04-12 17:34:02 -07:00
end 18b0b8963a floating: add click & keydown helpers 2026-04-12 17:03:29 -07:00
end 337c2a50b3 Combobox: fix primary icon visibility
The primary icon for a Combobox was never visible if a value was
selected because we were always attempting to use the icon for that item
even if it didn't have one.
2026-04-08 17:39:04 -07:00
end e994d8e232 add .nvmrc 2026-04-08 17:37:39 -07:00
end d108a74aaf 1.1.3 2026-03-15 22:10:14 -07:00
end 2c99c16b48 tabs: more configurable content padding options
choice between margin and padding. ngl, this feels kind of dumb, it
wasn't be problem and I don't know when it would ever be a problem.
perhaps i'll simplify or revert this later.
2026-03-15 22:03:59 -07:00
end ffe9c0369b tabs: separate controls for header and content padding 2026-03-15 21:52:01 -07:00
end 622481a1ca dialog: add autofreeze to sync loading state
Enabled by default, autofreeze freezes the dialog whenever loading state
is enabled.
2026-03-15 15:58:44 -07:00
end ebdca97527 dialog: fix DialogState export 2026-03-15 13:03:39 -07:00
end bb27d0c9d7 dialog: pass state to snippet overrides 2026-03-15 12:58:28 -07:00
11 changed files with 339 additions and 87 deletions
+1
View File
@@ -0,0 +1 @@
24.12.0
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "git",
"url": "https://gitea.auvem.com/svelte-toolkit/sui.git"
},
"version": "1.1.2",
"version": "1.3.1",
"scripts": {
"dev": "vite dev",
"build": "vite build && pnpm run prepack",
+8 -11
View File
@@ -29,10 +29,11 @@
};
/** returns option label, falling back to value or 'Undefined Option' if no option provided */
const getLabel = (opt: ComboboxOption | undefined): string =>
const getLabel = (opt: ComboboxOption | undefined | null): string =>
opt ? (opt.label ?? opt.value) : 'Undefined Option';
/** returns option preview, falling back to getLabel if missing */
const getPreview = (opt: ComboboxOption | undefined): string => opt?.preview ?? getLabel(opt);
const getPreview = (opt: ComboboxOption | undefined | null): string =>
opt?.preview ?? getLabel(opt);
</script>
<script lang="ts">
@@ -41,7 +42,7 @@
import Label from './Label.svelte';
import StyledRawInput from './StyledRawInput.svelte';
import { InputValidatorEvent, validate, type ValidatorOptions } from '@svelte-toolkit/validate';
import { untrack, type Snippet } from 'svelte';
import { type Snippet } from 'svelte';
import { scale } from 'svelte/transition';
import { generateIdentifier, type IconDef } from './util';
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
@@ -78,7 +79,7 @@
stateless?: boolean;
/** Bindable value of the combobox, the currently selected option */
value?: ComboboxOption;
value?: ComboboxOption | null;
/** Array of ComboboxOptions for the picker */
options: ComboboxOption[];
/**
@@ -271,7 +272,7 @@
});
/** currently highlighted option, updated by keyboard navigation or defaults to first item */
let highlighted = $derived.by((): ComboboxOption | undefined => {
let highlighted = $derived.by((): ComboboxOption | undefined | null => {
if (!searching) return undefined; // otherwise, the first item is highlighted on first open
if (filteredItems.length === 0) return undefined;
if (value !== undefined && filteredItems.find((v) => v.value === value?.value)) return value;
@@ -314,12 +315,8 @@
easing: cubicOut
});
$effect(() => {
if (iconWidth >= 0) {
untrack(() => {
inputPadding.target = calculatePadding();
});
}
});
/*** HELPER FUNCTIONS ***/
@@ -654,9 +651,9 @@
>
{#if loading}
<Spinner class="stroke-sui-accent! -mt-0.5" size="1em" />
{:else if useHighlighted && highlighted}
{:else if useHighlighted && highlighted && highlighted.icon}
{@render optionIcon(highlighted)}
{:else if value}
{:else if value && value.icon}
{@render optionIcon(value)}
{:else if icon}
{#if typeof icon === 'function'}
+22 -15
View File
@@ -76,9 +76,9 @@
/**
* Stores internal state of the dialog, everything necessary to render
* internal snippets.
* internal and override capable snippets.
*/
type DialogState = {
export type DialogState = {
frozen: boolean;
loading: boolean;
disabled: boolean;
@@ -112,9 +112,9 @@
/** Bindable open state of the dialog */
open?: boolean;
/** Title of the dialog */
title: string | Snippet;
title: string | Snippet<[state: DialogState]>;
/** Description of the dialog, optionally rendered below the title */
description?: string | Snippet;
description?: string | Snippet<[state: DialogState]>;
/** Size of the dialog (default: 'sm') */
size?: 'sm' | 'md' | 'lg' | 'max';
/** Additional classes for the dialog */
@@ -122,11 +122,11 @@
/** Content of the dialog */
children?: Snippet;
/** Bottom controls for the dialog */
controls?: Snippet | DialogControls;
controls?: Snippet<[state: DialogState]> | DialogControls;
/** Sets bottom alignment of controls (default: end) */
controlsAlign?: 'start' | 'center' | 'end';
/** Top-right close control */
close?: Snippet | Omit<DialogControlButton, 'label'> | null;
close?: Snippet<[state: DialogState]> | Omit<DialogControlButton, 'label'> | null;
/**
* Callback when the dialog is opened
* @deprecated use onopenchange instead and check the open parameter
@@ -141,8 +141,10 @@
onopenchange?: ({ open, dialog }: { open: boolean; dialog: DialogAPI }) => void;
/** If default controls are used, controls loading state of submit button */
loading?: boolean;
/** If default controls are used, freezes all interactions */
/** If default controls are used, freezes all interactions preventing user input */
frozen?: boolean;
/** If enabled, automatically freezes dialog when loading (default: true) */
autoFreeze?: boolean;
/** If default controls are used, disables submit button */
disabled?: boolean;
}
@@ -162,6 +164,7 @@
onopenchange,
loading = $bindable(false),
frozen = $bindable(false),
autoFreeze = true,
disabled = $bindable(false)
}: Props = $props();
@@ -180,6 +183,8 @@
let stackIndex = $state(-1);
const zIndex = $derived(1000 + stackIndex * 100);
const reallyFrozen = $derived(autoFreeze ? frozen || loading : frozen);
/** handles open change */
const handleOpenChange = (localOpen: boolean) => {
if (localOpen) {
@@ -244,7 +249,7 @@
/** Returns the current state of the dialog */
export const getState = (): DialogState => {
return {
frozen,
frozen: reallyFrozen,
loading,
disabled,
api: dialogAPI
@@ -271,11 +276,11 @@
transition:fade={{ duration: 150 }}
onclick={(e) => {
const target = e.target as HTMLElement;
if (open && !frozen && !dialogContainer?.contains(target) && target !== dialogContainer)
if (open && !reallyFrozen && !dialogContainer?.contains(target) && target !== dialogContainer)
open = false;
}}
onkeydown={(e) => {
if (e.key === 'Escape' && !frozen) {
if (e.key === 'Escape' && !reallyFrozen) {
if (stackIndex === dialogStack.length - 1) {
// only close if this dialog is the topmost dialog
open = false;
@@ -314,7 +319,7 @@
<!-- Close Button -->
{#if close !== null}
{#if typeof close === 'function'}
{@render close()}
{@render close(getState())}
{:else}
{@render dialogCloseButton(getState(), close)}
{/if}
@@ -342,7 +347,9 @@
controlsAlign === 'end' && 'justify-end'
]}
>
{#if controls && typeof controls === 'function'}{@render controls()}{:else if controls?.flip}
{#if controls && typeof controls === 'function'}{@render controls(
getState()
)}{:else if controls?.flip}
{#if controls.ok !== null}
{@render dialogOkButton(getState(), controls.ok)}
{/if}
@@ -372,7 +379,7 @@
state.api.close();
}
}}
disabled={state.frozen}
disabled={state.frozen || state.disabled}
>
{opts?.label || 'Cancel'}
</Button>
@@ -417,10 +424,10 @@
</button>
{/snippet}
{#snippet stringOrSnippet(val: string | Snippet)}
{#snippet stringOrSnippet(val: string | Snippet<[state: DialogState]>)}
{#if typeof val === 'string'}
{val}
{:else}
{@render val()}
{@render val(getState())}
{/if}
{/snippet}
+8 -9
View File
@@ -50,8 +50,7 @@
const options: ComboboxOption[] = countries.map((country) => ({
value: country.isoCode,
label: `${country.name} (+${country.phonecode})`,
preview: `+${country.phonecode}`,
icon: renderIcon
preview: `+${country.phonecode}`
}));
let phonecode: string = $derived.by(() => {
@@ -118,12 +117,6 @@
});
</script>
{#snippet renderIcon(item: ComboboxOption)}
{#if countrycodeMap[item.value]?.flag}
{countrycodeMap[item.value].flag}
{/if}
{/snippet}
<div class={classValue}>
{#if label}
<Label for={id}>{label}</Label>
@@ -149,7 +142,13 @@
countriesValid = e.detail.valid;
}}
invalidMessage={null}
/>
>
{#snippet iconRender(opt)}
{#if countrycodeMap[opt.value]?.flag}
{countrycodeMap[opt.value].flag}
{/if}
{/snippet}
</Combobox>
</div>
<div class="w-full">
+9 -4
View File
@@ -23,8 +23,13 @@
activeIndex?: number;
/** Callback fired when the active tab changes */
onchange?: (event: { index: number; tab: TabPage }) => void;
/** Applies layout padding to the tab header & content areas (default: false) */
padded?: boolean;
/**
* Controls padding of content areas. True applies padding to content
* and header, false applies no padding (default), and 'content' and
* 'header' apply padding to their respective areas only.
*/
padded?: boolean | 'content' | 'header';
/** Additional classes applied to the outer container */
class?: ClassValue | null;
}
@@ -99,7 +104,7 @@
bind:this={tabContainerEl}
class={[
'border-sui-text/15 relative mb-4 flex items-center gap-5 border-b-2',
padded && 'px-layout'
padded === true || padded === 'header' ? 'px-layout' : ''
]}
>
{#each pages as page, i (page.title)}
@@ -134,7 +139,7 @@
{#key activeIndex}
<div
class={[padded && 'px-layout']}
class={[padded === true || padded === 'content' ? 'px-layout' : '']}
in:flyX={{ direction: activeIndex > prevIndex ? 1 : -1, duration: 180, delay: 181 }}
out:flyX={{ direction: activeIndex > prevIndex ? -1 : 1, duration: 180 }}
onoutrostart={lockHeight}
+125 -31
View File
@@ -3,26 +3,134 @@
*/
export interface GraphError {
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 */
export type RawError = Error | string | GraphError[];
export type RawError = ErrorMessage | Error | string | GraphError[];
/**
* Type guard to check if an error is a RawError
* @param error The error to check
* @returns true if the error is a RawError, false otherwise
*/
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 (
error instanceof ErrorMessage ||
error instanceof Error ||
typeof error === 'string' ||
(Array.isArray(error) && error.every(isGraphError))
);
};
/**
* Converts any error (including GraphQL errors, standard Errors, strings, and
* ErrorMessages) into a consistent string format for display.
* @param error The error to convert to a string.
* @returns A string representation of the error, suitable for display to users.
*/
export const catchErrorString = (error: unknown): string => {
if (error instanceof ErrorMessage) {
return error.toString();
} else if (isRawError(error)) {
return new ErrorMessage(error).toString();
} else {
return String(error);
}
};
/**
* Checks a typical response from a GraphQL server for error and missing data,
* throwing an appropriate ErrorMessage if an error is found or if no data is
* returned.
* @param resourceName A human-readable name for the resource being fetched,
* used in error messages.
* @param response The response from the GraphQL server, which may contain an
* 'errors' array and a 'data' field.
* @throws An ErrorMessage if the response contains errors or if the data field
* is missing (undefined or null).
*/
export const checkGraphResponse = (
resourceName: string,
response: { errors?: GraphError[] | null; data?: unknown }
): void => {
if (response.errors && response.errors.length > 0) {
throw new ErrorMessage(`Error fetching ${resourceName}`, response.errors);
}
if (!response.data) {
throw new ErrorMessage(`No data returned for ${resourceName}`);
}
};
/**
* A class that represents an error message, which can be constructed from various types of raw errors
* and provides methods to convert those errors into a consistent format (lines of text) for display.
* It also supports wrapping existing error messages with additional context.
*/
export class ErrorMessage {
private _lines: string[] = [];
/**
* Converts a RawError to an array of lines and stores it for later access,
* or initializes without any errors if the input is null or undefined.
* @param raw The raw error to convert and store, or null/undefined for no error.
* @throws If the raw error is of an unsupported type.
*/
constructor(raw: RawError | null | undefined) {
if (raw) {
this._lines = ErrorMessage.rawErrorToLines(raw);
/** 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.
* @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.
*/
constructor(...errors: (unknown | null | undefined)[]) {
if (errors.length === 0) return;
this._lines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e));
}
/**
* Creates a new ErrorMessage only if the provided errors are not null or
* undefined. If no errors are provided, returns null.
*/
static from(...errors: (unknown | null | undefined)[]): ErrorMessage | null {
if (errors.length === 0) return null;
return new ErrorMessage(...errors);
}
/**
* Wraps this ErrorMessage inside another error, nesting the original 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.
*/
wrap(...errors: (unknown | null | undefined)[]): ErrorMessage {
if (errors.length === 0) return this;
const newLines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e));
return new ErrorMessage(...newLines, ...this._lines);
}
/** returns true if there are any error lines */
hasError(): boolean {
return this._lines.length > 0;
}
/** returns the stored lines */
get lines(): string[] {
@@ -37,28 +145,14 @@ export class ErrorMessage {
return this._lines.join('<br />');
}
/** returns true if there are any error lines */
hasError(): boolean {
return this._lines.length > 0;
}
/** adds a new line to the error message */
addLine(line: string): void {
this._lines.push(line);
}
/** optionally returns a new ErrorMessage only if the RawError is not empty */
static from(raw: RawError | null | undefined): ErrorMessage | null {
if (!raw) return null;
return new ErrorMessage(raw);
}
/** converts a RawError to an array of lines */
static rawErrorToLines(raw: RawError | null | undefined): string[] {
if (!raw) return ['No error'];
static rawErrorToLines(raw: unknown | null | undefined): string[] {
if (!raw) return [];
let errorLines: string[];
if (typeof raw === 'string') {
if (raw instanceof ErrorMessage) {
errorLines = raw.lines;
} else if (typeof raw === 'string') {
errorLines = [raw];
} else if (raw instanceof Error) {
errorLines = [raw.message];
@@ -66,7 +160,7 @@ export class ErrorMessage {
errorLines = raw.map((e) => {
const messageString = e.message || 'Unknown error';
if (e.path && e.path.length > 0) {
return `"${messageString}" at ${e.path.join('.')}`;
return `"${messageString}" at ${ErrorMessage.formatGraphPath(e.path)}`;
}
return messageString;
});
+77 -4
View File
@@ -4,7 +4,7 @@
* for more details, examples, and original source.
*/
import { computePosition, autoUpdate, flip, offset, type Placement } from '@floating-ui/dom';
import { computePosition, autoUpdate, flip, offset, type Placement, size } from '@floating-ui/dom';
import { createAttachmentKey } from 'svelte/attachments';
/**
@@ -17,6 +17,13 @@ export interface PopoverOptions {
placement?: Placement;
/** Offset distance between the reference and floating elements (default: 8) */
offset?: number;
/**
* Constraints the width and height of the popover to prevent it from
* overflowing the viewport. If true, the popover will adjust its max-width
* and max-height to fit. Scrolling is the responsibility of the parent
* element. Default is true.
*/
constrainSize?: boolean;
/** Callback when the popover is opened or closed */
ontoggle?: (open: boolean) => void;
}
@@ -29,10 +36,12 @@ export class Popover {
private options: PopoverOptions = {
interaction: 'click',
placement: 'bottom-start',
offset: 8
offset: 8,
constrainSize: true
};
private referenceElement: HTMLElement | undefined = $state();
private floatingElement: HTMLElement | undefined = $state();
private positionElement: HTMLElement | undefined = $state();
private open = $state(false);
/**
@@ -87,6 +96,23 @@ export class Popover {
return attrs;
}
/**
* Optionally sets the positioning element for the popover, which is used
* for calculating the position of the floating element. If not set, the
* reference element is used by default.
* @returns An object containing an attachment key for the positioning element.
*/
positionReference() {
return {
[createAttachmentKey()]: (node: HTMLElement) => {
this.positionElement = node;
return () => {
this.positionElement = undefined;
};
}
};
}
/** Returns whether the popover is open */
isOpen() {
return this.open;
@@ -122,6 +148,38 @@ export class Popover {
};
}
/**
* Callback function that closes the popover when the Escape key is pressed.
* Can be applied to the window, typically via <svelte:window onkeydown={popover.handleEscape} />,
* or to any other element that will receive keyboard events while the popover is open.
* @param event - The keyboard event to handle.
*/
handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
this.setOpen(false);
}
}
/**
* Callback function that closes the popover when a click occurs outside of the
* reference and floating elements. Should be applied to the window, typically
* via <svelte:window onclick={popover.handleClickOutside} />.
* @param event - The click event to handle.
*/
handleClickOutside(event: MouseEvent) {
if (!this.isOpen() || !this.referenceElement || !this.floatingElement) {
return;
}
if (
event.target instanceof Node &&
!this.referenceElement.contains(event.target) &&
!this.floatingElement.contains(event.target)
) {
this.setOpen(false);
}
}
/**
* Updates the position of the floating element based on the reference element using
* the computePosition function from floating-ui. It applies the calculated
@@ -131,9 +189,24 @@ export class Popover {
if (!this.referenceElement || !this.floatingElement) {
return;
}
const position = await computePosition(this.referenceElement, this.floatingElement, {
const reference = this.positionElement || this.referenceElement;
const position = await computePosition(reference, this.floatingElement, {
placement: this.options.placement,
middleware: [flip(), offset(this.options.offset)]
middleware: [
flip(),
offset(this.options.offset),
size({
apply: ({ availableWidth, availableHeight, elements }) => {
if (this.options.constrainSize) {
elements.floating.style.maxWidth = `${availableWidth}px`;
elements.floating.style.maxHeight = `${availableHeight}px`;
} else {
elements.floating.style.maxWidth = '';
elements.floating.style.maxHeight = '';
}
}
})
]
});
const { x, y } = position;
Object.assign(this.floatingElement.style, {
+10 -1
View File
@@ -10,6 +10,7 @@ export {
default as Dialog,
type DialogAPI,
type DialogControls,
type DialogState,
dialogCancelButton,
dialogCloseButton,
dialogOkButton
@@ -30,6 +31,7 @@ export { default as Label } from './Label.svelte';
export { default as Link, rewriteHref } from './Link.svelte';
export { default as PhoneInput } from './PhoneInput.svelte';
export { default as PinInput } from './PinInput.svelte';
export { type Preventable, checkPreventer, type Reversible, checkReversible } from './prevent';
export { default as RadioGroup } from './RadioGroup.svelte';
export { default as ScrollBox } from './ScrollBox.svelte';
export { default as Spinner } from './Spinner.svelte';
@@ -66,7 +68,14 @@ export {
ToolbarGroup,
Toolbar
} from './Toolbar';
export { type GraphError, type RawError, ErrorMessage } from './error';
export {
type GraphError,
type RawError,
isRawError,
catchErrorString,
checkGraphResponse,
ErrorMessage
} from './error';
export {
NavigationItem,
NavigationManager,
+63
View File
@@ -0,0 +1,63 @@
/**
* A type for a function that can be prevented from executing its default behavior.
* See checkPreventer for easy usage of this type.
*/
export type Preventable<T> = (ev: T & { prevent: () => void }) => Promise<void> | void;
/**
* Calls the provided function with a preventer object, and if the preventer is not
* prevented, calls the callback function.
* @param fn The function to call with the preventer.
* @param ev The event object to pass to the function.
* @param cb The callback function to call if the preventer is not prevented.
* @returns A promise that resolves to true if the action was not prevented, or false if it was prevented.
*/
export const checkPreventer = async <T>(
fn: Preventable<T>,
ev: T,
cb?: () => void
): Promise<boolean> => {
let prevented = false;
const preventer = {
prevent: () => {
prevented = true;
}
};
await fn({ ...ev, ...preventer });
if (!prevented && cb) {
cb();
}
return !prevented;
};
/**
* A type for a function that can be reversed after executing its behavior.
* See checkReversible for easy usage of this type.
*/
export type Reversible<T> = (ev: T & { reverse: () => void }) => Promise<void> | void;
/**
* Calls the provided function with a reverser object, and if the reverser is
* reversed, calls the callback function.
* @param fn The function to call with the reverser.
* @param ev The event object to pass to the function.
* @param cb The callback function to call if the reverser is reversed.
* @returns A promise that resolves to true if the action was not reversed, or false if it was reversed.
*/
export const checkReversible = async <T>(
fn: Reversible<T>,
ev: T,
cb?: () => void
): Promise<boolean> => {
let reversed = false;
const reverser = {
reverse: () => {
reversed = true;
}
};
await fn({ ...ev, ...reverser });
if (reversed && cb) {
cb();
}
return !reversed;
};
+14 -10
View File
@@ -448,7 +448,7 @@
<p class="title">Tabs</p>
<Tabs
padded={true}
padded
pages={[
{
title: 'Dashboard',
@@ -537,15 +537,6 @@
bind:open={dialogOpen}
title="Dialog Title"
size="sm"
controls={{
ok: {
action: (dialog) => {
dialog.close();
alert('Dialog submitted!');
}
},
cancel: null
}}
onopen={(dialog) => {
dialog.error('Example error message!');
dialog.loading();
@@ -554,6 +545,19 @@
}, 2000);
}}
>
{#snippet controls(state)}
<Button onclick={() => state.api.close()}>Cancel</Button>
<Button
onclick={() => {
state.api.close();
alert('Dialog submitted!');
}}
loading={state.loading}
>
Submit
</Button>
{/snippet}
<p>This is a dialog content area.</p>
<Button onclick={() => (nestedDialogOpen = true)}>Open Nested Dialog</Button>