@spaethtech/svelte-ui 0.3.1-dev.9.88889c6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,7 +101,9 @@ examples):
101
101
  - **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`
102
102
  - **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
103
103
  (query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`).
104
- - **Overlays:** `Dialog` `Popup` `Menu` · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
104
+ - **Overlays:** `Dialog` (pure modal shell BYO content; `width`/`height` accept any CSS value)
105
+ · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog) · `Popup` `Menu` ·
106
+ `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
105
107
  - **Navigation:** `TabStrip` `SideBarMenu`
106
108
  - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
107
109
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter`
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ import Dialog from "./Dialog.svelte";
3
+ import Card from "./Card.svelte";
4
+ import CardHeader from "./CardHeader.svelte";
5
+ import CardFooter from "./CardFooter.svelte";
6
+ import Button from "./Button.svelte";
7
+ import type { Snippet } from "svelte";
8
+ import type { Size } from "../types/sizes.js";
9
+ import type { Responsive } from "../types/responsive.js";
10
+
11
+ /**
12
+ * ConfirmDialog — the common title + message + Confirm/Cancel modal, built on the `Dialog` shell.
13
+ * It composes a `Card` (`CardHeader` title, body, `CardFooter` actions) inside `Dialog`, so it stays
14
+ * consistent with every other surface. Pass `children` to replace the message body with custom
15
+ * content (the header + footer stay). For a fully custom modal, use `Dialog` directly.
16
+ */
17
+ interface Props {
18
+ isOpen: boolean;
19
+ title?: string;
20
+ message?: string;
21
+ confirmText?: string;
22
+ cancelText?: string;
23
+ onConfirm?: () => void;
24
+ onCancel?: () => void;
25
+ variant?: "danger" | "normal";
26
+ /** Density tier — forwarded to the Card / CardHeader / CardFooter. */
27
+ size?: Responsive<Size>;
28
+ showConfirm?: boolean;
29
+ showCancel?: boolean;
30
+ /** Allow dismissing by backdrop / Escape (default true). */
31
+ dismissible?: boolean;
32
+ /** Forwarded to the Dialog shell — any CSS value (default: content-sized, capped at 32rem). */
33
+ width?: string;
34
+ height?: string;
35
+ /** Custom body between header and footer (replaces `message`). */
36
+ children?: Snippet;
37
+ }
38
+
39
+ let {
40
+ isOpen = $bindable(),
41
+ title = "Dialog",
42
+ message,
43
+ confirmText = "Confirm",
44
+ cancelText = "Cancel",
45
+ onConfirm,
46
+ onCancel,
47
+ variant = "normal",
48
+ size = "md",
49
+ showConfirm = true,
50
+ showCancel = true,
51
+ dismissible = true,
52
+ width,
53
+ height,
54
+ children,
55
+ }: Props = $props();
56
+
57
+ function handleConfirm() {
58
+ onConfirm?.();
59
+ isOpen = false;
60
+ }
61
+ function handleCancel() {
62
+ onCancel?.();
63
+ isOpen = false;
64
+ }
65
+ </script>
66
+
67
+ <Dialog
68
+ bind:isOpen
69
+ {dismissible}
70
+ {width}
71
+ {height}
72
+ onClose={onCancel}
73
+ aria-label={title}
74
+ >
75
+ <Card {size} class="w-full shadow-xl">
76
+ <CardHeader {size}>
77
+ <h3 class="text-lg font-medium [color:var(--ui-color-text)]">{title}</h3>
78
+ </CardHeader>
79
+
80
+ {#if children}
81
+ {@render children()}
82
+ {:else if message}
83
+ <p class="text-sm [color:color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]">
84
+ {message}
85
+ </p>
86
+ {/if}
87
+
88
+ {#if showConfirm || showCancel}
89
+ <CardFooter {size}>
90
+ {#if showCancel}
91
+ <Button text={cancelText} onclick={handleCancel} variant="secondary" />
92
+ {/if}
93
+ {#if showConfirm}
94
+ <Button
95
+ text={confirmText}
96
+ onclick={handleConfirm}
97
+ variant={variant === "danger" ? "danger" : "primary"}
98
+ />
99
+ {/if}
100
+ </CardFooter>
101
+ {/if}
102
+ </Card>
103
+ </Dialog>
@@ -0,0 +1,33 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ /**
5
+ * ConfirmDialog — the common title + message + Confirm/Cancel modal, built on the `Dialog` shell.
6
+ * It composes a `Card` (`CardHeader` title, body, `CardFooter` actions) inside `Dialog`, so it stays
7
+ * consistent with every other surface. Pass `children` to replace the message body with custom
8
+ * content (the header + footer stay). For a fully custom modal, use `Dialog` directly.
9
+ */
10
+ interface Props {
11
+ isOpen: boolean;
12
+ title?: string;
13
+ message?: string;
14
+ confirmText?: string;
15
+ cancelText?: string;
16
+ onConfirm?: () => void;
17
+ onCancel?: () => void;
18
+ variant?: "danger" | "normal";
19
+ /** Density tier — forwarded to the Card / CardHeader / CardFooter. */
20
+ size?: Responsive<Size>;
21
+ showConfirm?: boolean;
22
+ showCancel?: boolean;
23
+ /** Allow dismissing by backdrop / Escape (default true). */
24
+ dismissible?: boolean;
25
+ /** Forwarded to the Dialog shell — any CSS value (default: content-sized, capped at 32rem). */
26
+ width?: string;
27
+ height?: string;
28
+ /** Custom body between header and footer (replaces `message`). */
29
+ children?: Snippet;
30
+ }
31
+ declare const ConfirmDialog: import("svelte").Component<Props, {}, "isOpen">;
32
+ type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
33
+ export default ConfirmDialog;
@@ -1,57 +1,58 @@
1
1
  <script lang="ts">
2
- import Button from "./Button.svelte";
3
- import Card from "./Card.svelte";
4
- import CardHeader from "./CardHeader.svelte";
5
- import CardFooter from "./CardFooter.svelte";
6
2
  import type { Snippet } from "svelte";
7
- import type { Size } from "../types/sizes.js";
8
- import type { Responsive } from "../types/responsive.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
9
4
 
10
- interface Props {
5
+ /**
6
+ * Dialog — a PURE modal shell (layout container). It owns only the modal mechanics: portals itself
7
+ * to <body>, locks background scroll, renders a dismissable backdrop, traps focus, and exposes the
8
+ * ARIA dialog role. It imposes NO content structure — put a `Card` (or anything) inside via
9
+ * `children`. For the common title + message + Confirm/Cancel pattern, use `ConfirmDialog`, which is
10
+ * built on this.
11
+ *
12
+ * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
13
+ * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
14
+ */
15
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
16
+ /** Whether the dialog is shown (bindable). */
11
17
  isOpen: boolean;
12
- title?: string;
13
- message?: string;
14
- confirmText?: string;
15
- cancelText?: string;
16
- onConfirm?: () => void;
17
- onCancel?: () => void;
18
- variant?: "danger" | "normal";
19
- /** Density tier — forwarded to the underlying Card / CardHeader / CardFooter. */
20
- size?: Responsive<Size>;
21
- showConfirm?: boolean;
22
- showCancel?: boolean;
23
- /** Allow dismissing by clicking the backdrop OR pressing Escape (default true). Set `false` for a
24
- * must-decide dialog (e.g. a destructive confirm) so only the buttons close it. */
18
+ /** Panel width — any CSS value (`80%`, `80vw`, `400px`). Default: auto, capped at `min(32rem, …)`. */
19
+ width?: string;
20
+ /** Panel height — any CSS value (`60%`, `60vh`, `300px`). Default: auto (content height). */
21
+ height?: string;
22
+ /** Allow dismissing via backdrop click / Escape (default true). Set false for a must-decide modal. */
25
23
  dismissible?: boolean;
24
+ /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
25
+ onClose?: () => void;
26
+ /** Extra classes on the panel wrapper. */
27
+ class?: string;
26
28
  children?: Snippet;
27
29
  }
28
30
 
29
31
  let {
30
32
  isOpen = $bindable(),
31
- title = "Dialog",
32
- message,
33
- confirmText = "Confirm",
34
- cancelText = "Cancel",
35
- onConfirm,
36
- onCancel,
37
- variant = "normal",
38
- size = "md",
39
- showConfirm = true,
40
- showCancel = true,
33
+ width,
34
+ height,
41
35
  dismissible = true,
36
+ onClose,
37
+ class: cls = "",
42
38
  children,
39
+ ...rest
43
40
  }: Props = $props();
44
41
 
42
+ const FOCUSABLE =
43
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
44
+
45
+ let panelEl: HTMLElement | undefined = $state();
46
+ let previouslyFocused: HTMLElement | null = null;
47
+
45
48
  // Move the overlay to <body> so it's a real top-level layer — never a sibling of the trigger (which
46
- // would perturb surrounding layout, e.g. flip a space-y `:last-child`) and never clipped by an
47
- // ancestor's overflow/stacking context. This is what makes it an actual modal, not an inline box.
49
+ // would perturb surrounding layout) and never clipped by an ancestor's overflow/stacking context.
48
50
  function portal(node: HTMLElement) {
49
51
  document.body.appendChild(node);
50
52
  return { destroy: () => node.remove() };
51
53
  }
52
54
 
53
- // Lock background scroll while open; pad by the scrollbar width so hiding it doesn't shift the page
54
- // sideways. Restore exactly what was there on close/destroy.
55
+ // Lock background scroll while open; pad by the scrollbar width so hiding it doesn't shift the page.
55
56
  $effect(() => {
56
57
  if (!isOpen) return;
57
58
  const sbw = window.innerWidth - document.documentElement.clientWidth;
@@ -65,68 +66,86 @@
65
66
  };
66
67
  });
67
68
 
68
- function handleConfirm() {
69
- onConfirm?.();
70
- isOpen = false;
71
- }
72
- function handleCancel() {
73
- onCancel?.();
69
+ // Focus management: on open, remember what was focused and move focus into the panel; on close,
70
+ // restore it. (Tab-wrapping is handled in onKeydown.)
71
+ $effect(() => {
72
+ if (!isOpen) return;
73
+ previouslyFocused = document.activeElement as HTMLElement | null;
74
+ queueMicrotask(() => {
75
+ const first = panelEl?.querySelector<HTMLElement>(FOCUSABLE);
76
+ (first ?? panelEl)?.focus();
77
+ });
78
+ return () => previouslyFocused?.focus?.();
79
+ });
80
+
81
+ const panelStyle = $derived(
82
+ [
83
+ width ? `width:${width}` : "width:auto",
84
+ `max-width:${width ? "calc(100vw - 2rem)" : "min(32rem, calc(100vw - 2rem))"}`,
85
+ height ? `height:${height}` : null,
86
+ "max-height:calc(100vh - 2rem)",
87
+ ]
88
+ .filter(Boolean)
89
+ .join(";"),
90
+ );
91
+
92
+ function close() {
93
+ if (!dismissible) return;
94
+ onClose?.();
74
95
  isOpen = false;
75
96
  }
76
- // Backdrop click / Escape only dismiss when allowed.
77
- function dismiss() {
78
- if (dismissible) handleCancel();
79
- }
80
- function handleBackdropClick(event: MouseEvent) {
81
- if (event.target === event.currentTarget) dismiss();
97
+ function onBackdrop(event: MouseEvent) {
98
+ if (event.target === event.currentTarget) close();
82
99
  }
83
- function handleKeydown(event: KeyboardEvent) {
84
- if (isOpen && event.key === "Escape") dismiss();
100
+ function onKeydown(event: KeyboardEvent) {
101
+ if (!isOpen) return;
102
+ if (event.key === "Escape") {
103
+ close();
104
+ return;
105
+ }
106
+ if (event.key === "Tab" && panelEl) {
107
+ const items = [...panelEl.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(
108
+ (el) => el.offsetParent !== null,
109
+ );
110
+ if (items.length === 0) {
111
+ event.preventDefault();
112
+ panelEl.focus();
113
+ return;
114
+ }
115
+ const first = items[0];
116
+ const last = items[items.length - 1];
117
+ const active = document.activeElement;
118
+ if (event.shiftKey && (active === first || active === panelEl)) {
119
+ event.preventDefault();
120
+ last.focus();
121
+ } else if (!event.shiftKey && active === last) {
122
+ event.preventDefault();
123
+ first.focus();
124
+ }
125
+ }
85
126
  }
86
127
  </script>
87
128
 
88
- <svelte:window on:keydown={handleKeydown} />
129
+ <svelte:window on:keydown={onKeydown} />
89
130
 
90
131
  {#if isOpen}
132
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
91
133
  <div
92
134
  use:portal
93
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]"
94
- onclick={handleBackdropClick}
95
- onkeydown={handleKeydown}
96
- role="dialog"
97
- aria-modal="true"
98
- tabindex="-1"
135
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999] p-4"
136
+ onclick={onBackdrop}
137
+ role="presentation"
99
138
  >
100
- <!-- The panel IS a Card — border + --ui-border-radius + padding + background come from Card, so a
101
- Dialog stays consistent with every other surface; CardHeader/CardFooter give the title + action
102
- rows their (inset) separators and spacing. `size` flows to all three. -->
103
- <Card {size} class="max-w-md w-full mx-4 shadow-xl z-[10000]">
104
- <CardHeader {size}>
105
- <h3 class="text-lg font-medium [color:var(--ui-color-text)]">{title}</h3>
106
- </CardHeader>
107
-
108
- {#if children}
109
- {@render children()}
110
- {:else if message}
111
- <p class="text-sm [color:color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]">
112
- {message}
113
- </p>
114
- {/if}
115
-
116
- {#if showConfirm || showCancel}
117
- <CardFooter {size}>
118
- {#if showCancel}
119
- <Button text={cancelText} onclick={handleCancel} variant="secondary" />
120
- {/if}
121
- {#if showConfirm}
122
- <Button
123
- text={confirmText}
124
- onclick={handleConfirm}
125
- variant={variant === "danger" ? "danger" : "primary"}
126
- />
127
- {/if}
128
- </CardFooter>
129
- {/if}
130
- </Card>
139
+ <div
140
+ bind:this={panelEl}
141
+ class="overflow-auto z-[10000] {cls}"
142
+ style={panelStyle}
143
+ role="dialog"
144
+ aria-modal="true"
145
+ tabindex="-1"
146
+ {...rest}
147
+ >
148
+ {@render children?.()}
149
+ </div>
131
150
  </div>
132
151
  {/if}
@@ -1,22 +1,28 @@
1
1
  import type { Snippet } from "svelte";
2
- import type { Size } from "../types/sizes.js";
3
- import type { Responsive } from "../types/responsive.js";
4
- interface Props {
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * Dialog — a PURE modal shell (layout container). It owns only the modal mechanics: portals itself
5
+ * to <body>, locks background scroll, renders a dismissable backdrop, traps focus, and exposes the
6
+ * ARIA dialog role. It imposes NO content structure — put a `Card` (or anything) inside via
7
+ * `children`. For the common title + message + Confirm/Cancel pattern, use `ConfirmDialog`, which is
8
+ * built on this.
9
+ *
10
+ * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
11
+ * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
12
+ */
13
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
14
+ /** Whether the dialog is shown (bindable). */
5
15
  isOpen: boolean;
6
- title?: string;
7
- message?: string;
8
- confirmText?: string;
9
- cancelText?: string;
10
- onConfirm?: () => void;
11
- onCancel?: () => void;
12
- variant?: "danger" | "normal";
13
- /** Density tier — forwarded to the underlying Card / CardHeader / CardFooter. */
14
- size?: Responsive<Size>;
15
- showConfirm?: boolean;
16
- showCancel?: boolean;
17
- /** Allow dismissing by clicking the backdrop OR pressing Escape (default true). Set `false` for a
18
- * must-decide dialog (e.g. a destructive confirm) so only the buttons close it. */
16
+ /** Panel width — any CSS value (`80%`, `80vw`, `400px`). Default: auto, capped at `min(32rem, …)`. */
17
+ width?: string;
18
+ /** Panel height — any CSS value (`60%`, `60vh`, `300px`). Default: auto (content height). */
19
+ height?: string;
20
+ /** Allow dismissing via backdrop click / Escape (default true). Set false for a must-decide modal. */
19
21
  dismissible?: boolean;
22
+ /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
23
+ onClose?: () => void;
24
+ /** Extra classes on the panel wrapper. */
25
+ class?: string;
20
26
  children?: Snippet;
21
27
  }
22
28
  declare const Dialog: import("svelte").Component<Props, {}, "isOpen">;
@@ -14,7 +14,7 @@
14
14
  import Input from "./Input.svelte";
15
15
  import Button from "./Button.svelte";
16
16
  import Popup from "./Popup.svelte";
17
- import Dialog from "./Dialog.svelte";
17
+ import ConfirmDialog from "./ConfirmDialog.svelte";
18
18
  import type { DataSet } from "../data/dataset.js";
19
19
  import type { Size } from "../types/sizes.js";
20
20
  import type { Responsive } from "../types/responsive.js";
@@ -227,7 +227,7 @@
227
227
  </ul>
228
228
  </Popup>
229
229
 
230
- <Dialog bind:isOpen={helpOpen} title="Query syntax" showConfirm={false} showCancel={false}>
230
+ <ConfirmDialog bind:isOpen={helpOpen} title="Query syntax" showConfirm={false} showCancel={false}>
231
231
  <div class="max-h-[70vh] space-y-4 overflow-y-auto text-sm [color:var(--ui-color-text)]">
232
232
  <p>
233
233
  A filter is one or more conditions, <code class="font-mono">$column operator value</code>.
@@ -281,4 +281,4 @@ $ip|inet |= [10.0.0.0|inet..10.255.255.255|inet]
281
281
  values. Applies on <kbd class="rounded border px-1 {bc}">Enter</kbd>.
282
282
  </p>
283
283
  </div>
284
- </Dialog>
284
+ </ConfirmDialog>
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { default as CardFooter } from "./components/CardFooter.svelte";
10
10
  export { default as Toaster } from "./components/Toast/Toaster.svelte";
11
11
  export { toast, getToasts, dismissToast, type ToastVariant, type ToastItem, } from "./components/Toast/toast.svelte.js";
12
12
  export { default as Dialog } from "./components/Dialog.svelte";
13
+ export { default as ConfirmDialog } from "./components/ConfirmDialog.svelte";
13
14
  export { default as Input } from "./components/Input.svelte";
14
15
  export { default as NotesEditor } from "./components/NotesEditor.svelte";
15
16
  export { default as Rating } from "./components/Rating.svelte";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as CardFooter } from "./components/CardFooter.svelte";
12
12
  export { default as Toaster } from "./components/Toast/Toaster.svelte";
13
13
  export { toast, getToasts, dismissToast, } from "./components/Toast/toast.svelte.js";
14
14
  export { default as Dialog } from "./components/Dialog.svelte";
15
+ export { default as ConfirmDialog } from "./components/ConfirmDialog.svelte";
15
16
  export { default as Input } from "./components/Input.svelte";
16
17
  export { default as NotesEditor } from "./components/NotesEditor.svelte";
17
18
  export { default as Rating } from "./components/Rating.svelte";
@@ -142,12 +142,27 @@ dialog.
142
142
 
143
143
  ### Dialog
144
144
 
145
- Modal dialog with confirm/cancel and optional custom content.
145
+ **Pure modal shell** the mechanics only, no imposed content. Put a `Card` (or anything) inside via
146
+ `children`. For the common confirm pattern use `ConfirmDialog` (below).
146
147
 
147
148
  - **Location**: `src/lib/components/Dialog.svelte`
149
+ - **Props**: `isOpen` (bindable), `width` (any CSS value — `'80%'`, `'80vw'`, `'400px'`; default
150
+ auto, capped at `min(32rem, …)`), `height` (any CSS value; default auto), `dismissible` (default
151
+ `true`), `onClose`, `class`, `children` (snippet). Extra attributes (e.g. `aria-label`) spread onto
152
+ the panel.
153
+ - **Features**: portals to `<body>`, background scroll-lock (scrollbar-width compensated), backdrop
154
+ + Escape dismiss, **focus trap** (initial focus in, Tab wraps, focus restored on close),
155
+ `role="dialog"` + `aria-modal`.
156
+
157
+ ### ConfirmDialog
158
+
159
+ The common title + message + Confirm/Cancel modal, composed from a `Card` on top of the `Dialog`
160
+ shell.
161
+
162
+ - **Location**: `src/lib/components/ConfirmDialog.svelte`
148
163
  - **Props**: `isOpen` (bindable), `title`, `message`, `confirmText`, `cancelText`, `onConfirm`,
149
- `onCancel`, `variant` (`'normal' | 'danger'`), `showConfirm`, `showCancel`, `children` (snippet)
150
- - **Features**: backdrop-click + ESC dismiss, theme-aware
164
+ `onCancel`, `variant` (`'normal' | 'danger'`), `size`, `showConfirm`, `showCancel`, `dismissible`,
165
+ `width`/`height` (forwarded to the shell), `children` (snippet — replaces `message` as the body).
151
166
 
152
167
  ### Popup
153
168
 
package/docs/usage.md CHANGED
@@ -212,41 +212,64 @@ into a component's `icon` snippet — the library sizes and colors it:
212
212
  <Checkbox disabled />
213
213
  ```
214
214
 
215
- ### Dialog
215
+ ### ConfirmDialog
216
+
217
+ The common title + message + Confirm/Cancel modal (composed from a `Card` on the `Dialog` shell).
216
218
 
217
219
  ```svelte
218
220
  <script>
219
- import { Dialog, Button, Input } from "@spaethtech/svelte-ui";
221
+ import { ConfirmDialog, Button } from "@spaethtech/svelte-ui";
220
222
 
221
- let showDialog = $state(false);
223
+ let showConfirm = $state(false);
222
224
  let showDanger = $state(false);
223
225
  </script>
224
226
 
225
227
  <!-- Basic confirmation -->
226
- <Button text="Show Dialog" onclick={() => (showDialog = true)} />
227
- <Dialog
228
- bind:isOpen={showDialog}
228
+ <Button text="Show Dialog" onclick={() => (showConfirm = true)} />
229
+ <ConfirmDialog
230
+ bind:isOpen={showConfirm}
229
231
  title="Confirm Action"
230
232
  message="Are you sure you want to proceed?"
231
233
  onConfirm={() => console.log("Confirmed")}
232
234
  />
233
235
 
234
- <!-- Danger variant -->
235
- <Dialog
236
+ <!-- Danger + must-decide (no backdrop/Escape dismiss) -->
237
+ <ConfirmDialog
236
238
  bind:isOpen={showDanger}
237
239
  title="Delete Item"
238
240
  message="This action cannot be undone!"
239
241
  variant="danger"
240
242
  confirmText="Delete"
243
+ dismissible={false}
241
244
  onConfirm={() => console.log("Deleted")}
242
245
  />
246
+ ```
243
247
 
244
- <!-- Custom content -->
245
- <Dialog bind:isOpen={showDialog} title="Custom Content">
246
- <div class="space-y-2">
247
- <p>This is custom content</p>
248
- <Input placeholder="Enter value" />
249
- </div>
248
+ ### Dialog (shell)
249
+
250
+ A pure modal shell — you supply the content. Use it for anything beyond the confirm pattern; size it
251
+ with `width` / `height` (any CSS value).
252
+
253
+ ```svelte
254
+ <script>
255
+ import { Dialog, Card, CardHeader, CardFooter, Button, Input } from "@spaethtech/svelte-ui";
256
+
257
+ let open = $state(false);
258
+ </script>
259
+
260
+ <Button text="Open" onclick={() => (open = true)} />
261
+
262
+ <Dialog bind:isOpen={open} width="80vw" height="60vh">
263
+ <Card class="w-full h-full flex flex-col">
264
+ <CardHeader><h3 class="text-lg font-medium">Custom modal</h3></CardHeader>
265
+ <div class="flex-1 overflow-auto space-y-2">
266
+ <p>You own the Card — control its separator, variant, sizing, and body.</p>
267
+ <Input placeholder="Enter value" />
268
+ </div>
269
+ <CardFooter separator={false}>
270
+ <Button text="Close" variant="secondary" onclick={() => (open = false)} />
271
+ </CardFooter>
272
+ </Card>
250
273
  </Dialog>
251
274
  ```
252
275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.3.1-dev.9.88889c6",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"