@joewinke/jatui 0.38.6 → 0.40.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.
Files changed (98) hide show
  1. package/CLAUDE.md +54 -5
  2. package/THIRD_PARTY_NOTICES.md +67 -0
  3. package/dist/actions/scrollEdge.d.ts +55 -0
  4. package/dist/actions/scrollEdge.js +144 -0
  5. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
  6. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
  7. package/dist/components/BottomSheet.svelte +7 -6
  8. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  9. package/dist/components/BulkActionBar.svelte +32 -2
  10. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  11. package/dist/components/CloseButton.svelte +75 -0
  12. package/dist/components/CloseButton.svelte.d.ts +29 -0
  13. package/dist/components/Drawer.svelte +10 -11
  14. package/dist/components/Drawer.svelte.d.ts +2 -8
  15. package/dist/components/FloatingActionBar.svelte +102 -34
  16. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  17. package/dist/components/IconButton.svelte +200 -0
  18. package/dist/components/IconButton.svelte.d.ts +46 -0
  19. package/dist/components/LinkButton.svelte +117 -0
  20. package/dist/components/LinkButton.svelte.d.ts +32 -0
  21. package/dist/components/MediaExplorer.svelte +249 -0
  22. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  23. package/dist/components/MediaLightbox.svelte +2 -7
  24. package/dist/components/Modal.svelte +2 -10
  25. package/dist/components/NotificationCenter.svelte +481 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  27. package/dist/components/PinInput.svelte +94 -31
  28. package/dist/components/PinInput.svelte.d.ts +4 -0
  29. package/dist/components/SearchDropdown.svelte +54 -8
  30. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  31. package/dist/components/TextArea.svelte +67 -5
  32. package/dist/components/TextArea.svelte.d.ts +4 -6
  33. package/dist/components/ThemeSelector.svelte +29 -9
  34. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  35. package/dist/components/ThemeToggle.svelte +28 -3
  36. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  37. package/dist/components/agent/AgentLog.svelte +217 -0
  38. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  39. package/dist/components/agent/AgentLogRow.svelte +281 -0
  40. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  41. package/dist/components/agent/AgentTaskList.svelte +607 -0
  42. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  43. package/dist/components/agent/AgentThinking.svelte +280 -0
  44. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  45. package/dist/components/agent/ComposerGlow.svelte +265 -0
  46. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  47. package/dist/components/agent/ContextMeter.svelte +277 -0
  48. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  49. package/dist/components/agent/Questionnaire.svelte +767 -0
  50. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  51. package/dist/components/agent/ShimmerText.svelte +100 -0
  52. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  53. package/dist/components/agent/StreamedText.svelte +135 -0
  54. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  55. package/dist/components/agent/agentLog.d.ts +22 -0
  56. package/dist/components/agent/agentLog.js +26 -0
  57. package/dist/components/agent/agentTaskList.d.ts +90 -0
  58. package/dist/components/agent/agentTaskList.js +93 -0
  59. package/dist/components/agent/agentThinking.d.ts +46 -0
  60. package/dist/components/agent/agentThinking.js +78 -0
  61. package/dist/components/agent/contextMeter.d.ts +97 -0
  62. package/dist/components/agent/contextMeter.js +156 -0
  63. package/dist/components/agent/questionnaire.d.ts +104 -0
  64. package/dist/components/agent/questionnaire.js +225 -0
  65. package/dist/components/agent/revealCore.d.ts +74 -0
  66. package/dist/components/agent/revealCore.js +160 -0
  67. package/dist/components/agent/revealTicker.d.ts +34 -0
  68. package/dist/components/agent/revealTicker.js +73 -0
  69. package/dist/components/agent/streamedText.d.ts +28 -0
  70. package/dist/components/agent/streamedText.js +59 -0
  71. package/dist/components/basic/Breadcrumb.svelte +226 -11
  72. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  73. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  74. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  75. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  76. package/dist/components/messaging/MessageInput.svelte +34 -4
  77. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  78. package/dist/components/messaging/MessageItem.svelte +2 -2
  79. package/dist/components/notificationCenter.d.ts +65 -0
  80. package/dist/components/notificationCenter.js +110 -0
  81. package/dist/components/progress/Steps.svelte +269 -5
  82. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  83. package/dist/components/progress/stepsSummary.d.ts +14 -0
  84. package/dist/components/progress/stepsSummary.js +32 -0
  85. package/dist/index.d.ts +48 -1
  86. package/dist/index.js +39 -1
  87. package/dist/selection/clickSelection.d.ts +89 -0
  88. package/dist/selection/clickSelection.js +152 -0
  89. package/dist/selection/createClickSelection.d.ts +79 -0
  90. package/dist/selection/createClickSelection.js +134 -0
  91. package/dist/styles/tokens.css +80 -1
  92. package/dist/utils/pinInput.d.ts +33 -0
  93. package/dist/utils/pinInput.js +63 -0
  94. package/dist/utils/textareaRows.d.ts +23 -0
  95. package/dist/utils/textareaRows.js +13 -0
  96. package/dist/utils/themeWipe.d.ts +36 -17
  97. package/dist/utils/themeWipe.js +145 -35
  98. package/package.json +3 -2
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
4
+ * needs. An IconButton with a built-in X glyph whose stroke is a true CSS
5
+ * pixel width at every size (`vector-effect: non-scaling-stroke`), so the
6
+ * cross never turns hairline at xs or chunky at lg.
7
+ *
8
+ * <CloseButton onclick={close} />
9
+ * <CloseButton label="Close drawer" size="md" onclick={close} />
10
+ * <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
11
+ *
12
+ * Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
13
+ */
14
+ import IconButton, { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
15
+
16
+ interface Props {
17
+ /** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
18
+ label?: string;
19
+ size?: IconButtonSize;
20
+ variant?: IconButtonVariant;
21
+ shape?: 'square' | 'circle';
22
+ disabled?: boolean;
23
+ /** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
24
+ tooltip?: boolean | string;
25
+ class?: string;
26
+ onclick?: (e: MouseEvent) => void;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ let {
31
+ label = 'Close',
32
+ size = 'sm',
33
+ variant = 'ghost',
34
+ shape = 'circle',
35
+ disabled = false,
36
+ tooltip = false,
37
+ class: className = '',
38
+ onclick,
39
+ ...rest
40
+ }: Props = $props();
41
+
42
+ /** Glyph box per size — the X itself is inset to ~58% of it. */
43
+ const GLYPH: Record<IconButtonSize, number> = { xs: 12, sm: 16, md: 18, lg: 22 };
44
+ const STROKE: Record<IconButtonSize, number> = { xs: 1.5, sm: 1.75, md: 2, lg: 2 };
45
+ const px = $derived(GLYPH[size] ?? 16);
46
+ </script>
47
+
48
+ <IconButton
49
+ {...rest}
50
+ {label}
51
+ {size}
52
+ {variant}
53
+ {shape}
54
+ {disabled}
55
+ {tooltip}
56
+ {onclick}
57
+ class="jatui-close-btn {className}"
58
+ >
59
+ <svg
60
+ width={px}
61
+ height={px}
62
+ viewBox="0 0 24 24"
63
+ fill="none"
64
+ stroke="currentColor"
65
+ stroke-linecap="round"
66
+ aria-hidden="true"
67
+ focusable="false"
68
+ >
69
+ <path
70
+ d="M17 7L7 17M7 7l10 10"
71
+ stroke-width={STROKE[size] ?? 1.75}
72
+ vector-effect="non-scaling-stroke"
73
+ />
74
+ </svg>
75
+ </IconButton>
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
3
+ * needs. An IconButton with a built-in X glyph whose stroke is a true CSS
4
+ * pixel width at every size (`vector-effect: non-scaling-stroke`), so the
5
+ * cross never turns hairline at xs or chunky at lg.
6
+ *
7
+ * <CloseButton onclick={close} />
8
+ * <CloseButton label="Close drawer" size="md" onclick={close} />
9
+ * <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
10
+ *
11
+ * Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
12
+ */
13
+ import { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
14
+ interface Props {
15
+ /** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
16
+ label?: string;
17
+ size?: IconButtonSize;
18
+ variant?: IconButtonVariant;
19
+ shape?: 'square' | 'circle';
20
+ disabled?: boolean;
21
+ /** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
22
+ tooltip?: boolean | string;
23
+ class?: string;
24
+ onclick?: (e: MouseEvent) => void;
25
+ [key: string]: unknown;
26
+ }
27
+ declare const CloseButton: import("svelte").Component<Props, {}, "">;
28
+ type CloseButton = ReturnType<typeof CloseButton>;
29
+ export default CloseButton;
@@ -8,6 +8,8 @@
8
8
  * class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
9
9
  */
10
10
 
11
+ import CloseButton from './CloseButton.svelte';
12
+ import { scrollEdge as scrollEdgeAction } from '../actions/scrollEdge';
11
13
  import { untrack, type Snippet } from 'svelte';
12
14
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
13
15
 
@@ -22,6 +24,8 @@
22
24
  width?: string;
23
25
  showCloseButton?: boolean;
24
26
  contentClass?: string;
27
+ /** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
28
+ scrollEdge?: boolean;
25
29
  /**
26
30
  * When true (default), keydown events fired inside the drawer panel are
27
31
  * stopped from propagating to window — preventing page-level list/keyboard
@@ -59,6 +63,7 @@
59
63
  width,
60
64
  showCloseButton = true,
61
65
  contentClass = '',
66
+ scrollEdge = true,
62
67
  trapKeys = true,
63
68
  shortcutRegistry,
64
69
  shortcutContext = 'drawer',
@@ -150,22 +155,16 @@
150
155
  {@render headerActions()}
151
156
  {/if}
152
157
  {#if showCloseButton}
153
- <button
154
- type="button"
155
- class="btn btn-sm btn-circle btn-ghost"
156
- onclick={handleClose}
157
- aria-label="Close drawer"
158
- >
159
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
160
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
161
- </svg>
162
- </button>
158
+ <CloseButton label="Close drawer" onclick={handleClose} />
163
159
  {/if}
164
160
  </div>
165
161
  </div>
166
162
  {/if}
167
163
 
168
- <div class="flex-1 overflow-y-auto {contentClass}">
164
+ <div
165
+ class="flex-1 overflow-y-auto {contentClass}"
166
+ use:scrollEdgeAction={{ enabled: scrollEdge, surface: 'var(--color-base-200)' }}
167
+ >
169
168
  {#if open && children}
170
169
  {@render children()}
171
170
  {/if}
@@ -1,11 +1,3 @@
1
- /**
2
- * Drawer Component
3
- *
4
- * Sliding panel overlay with backdrop, escape key, and header actions.
5
- * Supports a `footer` snippet (pinned below the scrollable body —
6
- * matches mardukos DrawerShell) and an arbitrary Tailwind `width`
7
- * class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
8
- */
9
1
  import { type Snippet } from 'svelte';
10
2
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
11
3
  interface Props {
@@ -19,6 +11,8 @@ interface Props {
19
11
  width?: string;
20
12
  showCloseButton?: boolean;
21
13
  contentClass?: string;
14
+ /** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
15
+ scrollEdge?: boolean;
22
16
  /**
23
17
  * When true (default), keydown events fired inside the drawer panel are
24
18
  * stopped from propagating to window — preventing page-level list/keyboard
@@ -20,6 +20,22 @@
20
20
  * Per-action `disabled` renders the button dimmed and non-interactive;
21
21
  * per-action `loadingText` disables it and swaps the label for a
22
22
  * spinner + that text (mardukos fork) while other actions stay active.
23
+ *
24
+ * `placement="inline"` puts the bar in the normal flow at its container's
25
+ * full width (for a host with its own sticky bar); the default floats
26
+ * bottom-centre. Either way it wraps rather than run off a narrow screen.
27
+ *
28
+ * The root class is `fab-bar`, not `fab`: daisyUI 5 ships a `.fab`
29
+ * (speed-dial) component whose rules hid every button of this bar in a
30
+ * daisyUI 5 host (qimg-x9ifp.1).
31
+ *
32
+ * Theming: every colour is a custom property with the original dark value
33
+ * as its fallback, so a host can map the bar onto its theme without
34
+ * overriding internals: --fab-bg, --fab-fg, --fab-muted, --fab-border,
35
+ * --fab-shadow, --fab-radius, --fab-btn-bg/-fg/-border/-hover/-radius,
36
+ * --fab-primary-bg/-fg/-border/-hover, --fab-danger-bg/-fg/-border/-hover,
37
+ * --fab-menu-bg/-radius/-shadow, --fab-focus, --fab-spinner. Set them on
38
+ * any ancestor (the bar inherits them).
23
39
  */
24
40
 
25
41
  import { fade } from 'svelte/transition';
@@ -49,6 +65,8 @@
49
65
  label: string;
50
66
  /** Optional colored dot (oklch string) */
51
67
  dotColor?: string;
68
+ /** Optional key hint shown at the item's right edge (display only; the host binds it). */
69
+ hint?: string;
52
70
  /** Optional SVG snippet rendered before the label */
53
71
  iconSnippet?: import('svelte').Snippet;
54
72
  /** Called when this item is clicked */
@@ -86,6 +104,13 @@
86
104
  onClear?: () => void;
87
105
  /** Label for the selection count (default: "selected") */
88
106
  countLabel?: string;
107
+ /**
108
+ * 'floating' (default): fixed, bottom-centre over the page.
109
+ * 'inline': in the normal flow, full width of its container — for a
110
+ * host that already has a sticky bar to put it in. Both wrap onto more
111
+ * lines rather than run off a narrow screen.
112
+ */
113
+ placement?: 'floating' | 'inline';
89
114
  }
90
115
 
91
116
  let {
@@ -96,6 +121,7 @@
96
121
  loadingMessage = null,
97
122
  onClear,
98
123
  countLabel = 'selected',
124
+ placement = 'floating',
99
125
  }: Props = $props();
100
126
 
101
127
  let openDropdownId = $state<string | null>(null);
@@ -129,7 +155,7 @@
129
155
  </script>
130
156
 
131
157
  {#if count > 0}
132
- <div class="fab" transition:fade={{ duration: 150 }}>
158
+ <div class="fab-bar" class:fab-inline={placement === 'inline'} role="group" aria-label="{count} {countLabel}" transition:fade={{ duration: 150 }}>
133
159
  <span class="fab-count">{count} {countLabel}</span>
134
160
 
135
161
  {#if loadingMessage}
@@ -176,13 +202,22 @@
176
202
  class="fab-dropdown-wrapper"
177
203
  role="group"
178
204
  onclick={(e) => e.stopPropagation()}
179
- onkeydown={(e) => e.stopPropagation()}
205
+ onkeydown={(e) => {
206
+ e.stopPropagation();
207
+ if (e.key === 'Escape' && openDropdownId === dropdown.id) {
208
+ e.preventDefault();
209
+ openDropdownId = null;
210
+ (e.currentTarget.querySelector('.fab-btn') as HTMLElement | null)?.focus();
211
+ }
212
+ }}
180
213
  >
181
214
  <button
182
215
  type="button"
183
216
  class="fab-btn {variantClass(dropdown.variant)}"
184
217
  onclick={() => openDropdownId = openDropdownId === dropdown.id ? null : dropdown.id}
185
218
  disabled={loading}
219
+ aria-haspopup="true"
220
+ aria-expanded={openDropdownId === dropdown.id}
186
221
  >
187
222
  {#if dropdown.icon}
188
223
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
@@ -205,7 +240,10 @@
205
240
  {#if item.iconSnippet}
206
241
  {@render item.iconSnippet()}
207
242
  {/if}
208
- {item.label}
243
+ <span class="fab-item-label">{item.label}</span>
244
+ {#if item.hint}
245
+ <kbd class="fab-key fab-item-hint">{item.hint}</kbd>
246
+ {/if}
209
247
  </button>
210
248
  {/each}
211
249
  </div>
@@ -229,8 +267,11 @@
229
267
  {/if}
230
268
 
231
269
  <style>
232
- .fab {
270
+ .fab-bar {
233
271
  position: fixed;
272
+ max-width: calc(100vw - 1rem);
273
+ flex-wrap: wrap;
274
+ justify-content: center;
234
275
  bottom: 1.5rem;
235
276
  left: 50%;
236
277
  transform: translateX(-50%);
@@ -239,23 +280,35 @@
239
280
  align-items: center;
240
281
  gap: 0.5rem;
241
282
  padding: 0.5rem 0.75rem;
242
- background: oklch(0.16 0.02 250);
243
- border: 1px solid oklch(0.30 0.02 250);
244
- border-radius: 0.75rem;
245
- box-shadow: 0 8px 32px oklch(0.05 0 0 / 0.6), 0 2px 8px oklch(0.05 0 0 / 0.3);
283
+ background: var(--fab-bg, oklch(0.16 0.02 250));
284
+ border: 1px solid var(--fab-border, oklch(0.30 0.02 250));
285
+ border-radius: var(--fab-radius, 0.75rem);
286
+ box-shadow: var(--fab-shadow, 0 8px 32px oklch(0.05 0 0 / 0.6), 0 2px 8px oklch(0.05 0 0 / 0.3));
287
+ color: var(--fab-fg, oklch(0.85 0.02 250));
288
+ }
289
+ /* Inline: in the host's own bar, so no float, no shadow, its full width. */
290
+ .fab-bar.fab-inline {
291
+ position: static;
292
+ left: auto;
293
+ bottom: auto;
294
+ transform: none;
295
+ max-width: none;
296
+ width: 100%;
297
+ justify-content: flex-start;
298
+ box-shadow: var(--fab-shadow-inline, none);
246
299
  }
247
300
 
248
301
  .fab-count {
249
302
  font-size: 0.8125rem;
250
303
  font-weight: 600;
251
- color: oklch(0.85 0.02 250);
304
+ color: var(--fab-fg, oklch(0.85 0.02 250));
252
305
  white-space: nowrap;
253
306
  }
254
307
 
255
308
  .fab-divider {
256
309
  width: 1px;
257
310
  height: 1.25rem;
258
- background: oklch(0.30 0.02 250);
311
+ background: var(--fab-border, oklch(0.30 0.02 250));
259
312
  }
260
313
 
261
314
  .fab-btn {
@@ -264,7 +317,7 @@
264
317
  gap: 0.375rem;
265
318
  padding: 0.375rem 0.625rem;
266
319
  border: 1px solid transparent;
267
- border-radius: 0.375rem;
320
+ border-radius: var(--fab-btn-radius, 0.375rem);
268
321
  font-size: 0.75rem;
269
322
  font-weight: 500;
270
323
  cursor: pointer;
@@ -278,25 +331,25 @@
278
331
 
279
332
  /* Variants */
280
333
  .fab-btn-default {
281
- background: oklch(0.25 0.02 250 / 0.5);
282
- color: oklch(0.75 0.02 250);
283
- border-color: oklch(0.35 0.02 250);
334
+ background: var(--fab-btn-bg, oklch(0.25 0.02 250 / 0.5));
335
+ color: var(--fab-btn-fg, oklch(0.75 0.02 250));
336
+ border-color: var(--fab-btn-border, oklch(0.35 0.02 250));
284
337
  }
285
- .fab-btn-default:hover { background: oklch(0.30 0.02 250); }
338
+ .fab-btn-default:hover { background: var(--fab-btn-hover, oklch(0.30 0.02 250)); }
286
339
 
287
340
  .fab-btn-primary {
288
- background: oklch(0.65 0.18 250 / 0.15);
289
- color: oklch(0.82 0.14 250);
290
- border-color: oklch(0.65 0.18 250 / 0.3);
341
+ background: var(--fab-primary-bg, oklch(0.65 0.18 250 / 0.15));
342
+ color: var(--fab-primary-fg, oklch(0.82 0.14 250));
343
+ border-color: var(--fab-primary-border, oklch(0.65 0.18 250 / 0.3));
291
344
  }
292
- .fab-btn-primary:hover { background: oklch(0.65 0.18 250 / 0.25); }
345
+ .fab-btn-primary:hover { background: var(--fab-primary-hover, oklch(0.65 0.18 250 / 0.25)); }
293
346
 
294
347
  .fab-btn-danger {
295
- background: oklch(0.55 0.18 30 / 0.15);
296
- color: oklch(0.80 0.15 30);
297
- border-color: oklch(0.55 0.18 30 / 0.3);
348
+ background: var(--fab-danger-bg, oklch(0.55 0.18 30 / 0.15));
349
+ color: var(--fab-danger-fg, oklch(0.80 0.15 30));
350
+ border-color: var(--fab-danger-border, oklch(0.55 0.18 30 / 0.3));
298
351
  }
299
- .fab-btn-danger:hover { background: oklch(0.55 0.18 30 / 0.25); }
352
+ .fab-btn-danger:hover { background: var(--fab-danger-hover, oklch(0.55 0.18 30 / 0.25)); }
300
353
 
301
354
  .fab-btn-warning {
302
355
  background: oklch(0.75 0.15 85 / 0.12);
@@ -328,19 +381,23 @@
328
381
 
329
382
  .fab-btn-clear {
330
383
  background: transparent;
331
- color: oklch(0.65 0.02 250);
384
+ color: var(--fab-muted, oklch(0.65 0.02 250));
332
385
  border-color: transparent;
333
386
  }
334
387
  .fab-btn-clear:hover {
335
- background: oklch(0.25 0.02 250);
336
- color: oklch(0.80 0.02 250);
388
+ background: var(--fab-btn-hover, oklch(0.25 0.02 250));
389
+ color: var(--fab-fg, oklch(0.80 0.02 250));
390
+ }
391
+ .fab-btn:focus-visible {
392
+ outline: 2px solid var(--fab-focus, currentColor);
393
+ outline-offset: 1px;
337
394
  }
338
395
 
339
396
  .fab-spinner {
340
397
  width: 14px;
341
398
  height: 14px;
342
- border: 2px solid oklch(0.35 0.02 250);
343
- border-top-color: oklch(0.70 0.15 240);
399
+ border: 2px solid var(--fab-border, oklch(0.35 0.02 250));
400
+ border-top-color: var(--fab-spinner, oklch(0.70 0.15 240));
344
401
  border-radius: 50%;
345
402
  animation: fab-spin 0.6s linear infinite;
346
403
  flex-shrink: 0;
@@ -389,13 +446,17 @@
389
446
  left: 50%;
390
447
  transform: translateX(-50%);
391
448
  min-width: 160px;
392
- background: oklch(0.18 0.02 250);
393
- border: 1px solid oklch(0.30 0.02 250);
394
- border-radius: 0.5rem;
449
+ background: var(--fab-menu-bg, oklch(0.18 0.02 250));
450
+ border: 1px solid var(--fab-border, oklch(0.30 0.02 250));
451
+ border-radius: var(--fab-menu-radius, 0.5rem);
395
452
  padding: 0.375rem;
396
- box-shadow: 0 8px 24px oklch(0.05 0 0 / 0.5);
453
+ box-shadow: var(--fab-menu-shadow, 0 8px 24px oklch(0.05 0 0 / 0.5));
397
454
  z-index: 60;
398
455
  }
456
+ .fab-inline .fab-dropdown {
457
+ left: 0;
458
+ transform: none;
459
+ }
399
460
  .fab-dropdown-item {
400
461
  display: flex;
401
462
  align-items: center;
@@ -404,7 +465,7 @@
404
465
  padding: 0.375rem 0.625rem;
405
466
  border: none;
406
467
  background: transparent;
407
- color: oklch(0.80 0.02 250);
468
+ color: var(--fab-fg, oklch(0.80 0.02 250));
408
469
  font-size: 0.75rem;
409
470
  text-align: left;
410
471
  border-radius: 0.375rem;
@@ -412,8 +473,15 @@
412
473
  transition: background 0.1s;
413
474
  white-space: nowrap;
414
475
  }
415
- .fab-dropdown-item:hover { background: oklch(0.25 0.02 250); }
476
+ .fab-dropdown-item:hover,
477
+ .fab-dropdown-item:focus-visible { background: var(--fab-btn-hover, oklch(0.25 0.02 250)); outline: none; }
416
478
 
479
+ .fab-item-label {
480
+ flex: 1;
481
+ }
482
+ .fab-item-hint {
483
+ margin-left: 1rem;
484
+ }
417
485
  .fab-dot {
418
486
  width: 8px;
419
487
  height: 8px;
@@ -22,6 +22,8 @@ export interface DropdownItem {
22
22
  label: string;
23
23
  /** Optional colored dot (oklch string) */
24
24
  dotColor?: string;
25
+ /** Optional key hint shown at the item's right edge (display only; the host binds it). */
26
+ hint?: string;
25
27
  /** Optional SVG snippet rendered before the label */
26
28
  iconSnippet?: import('svelte').Snippet;
27
29
  /** Called when this item is clicked */
@@ -57,6 +59,13 @@ interface Props {
57
59
  onClear?: () => void;
58
60
  /** Label for the selection count (default: "selected") */
59
61
  countLabel?: string;
62
+ /**
63
+ * 'floating' (default): fixed, bottom-centre over the page.
64
+ * 'inline': in the normal flow, full width of its container — for a
65
+ * host that already has a sticky bar to put it in. Both wrap onto more
66
+ * lines rather than run off a narrow screen.
67
+ */
68
+ placement?: 'floating' | 'inline';
60
69
  }
61
70
  declare const FloatingActionBar: import("svelte").Component<Props, {}, "">;
62
71
  type FloatingActionBar = ReturnType<typeof FloatingActionBar>;
@@ -0,0 +1,200 @@
1
+ <script module lang="ts">
2
+ export type IconButtonVariant =
3
+ | 'ghost'
4
+ | 'outline'
5
+ | 'soft'
6
+ | 'primary'
7
+ | 'secondary'
8
+ | 'error'
9
+ | 'overlay';
10
+ export type IconButtonSize = 'xs' | 'sm' | 'md' | 'lg';
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ /**
15
+ * IconButton — an icon-only button with a REQUIRED accessible name.
16
+ *
17
+ * The square/circle `btn` that consumers hand-write hundreds of times
18
+ * (`btn btn-sm btn-circle btn-ghost` + an inline svg + an aria-label that is
19
+ * sometimes forgotten). `label` is required by the types, becomes the
20
+ * aria-label, and — with `tooltip` — the hover/focus hint too.
21
+ *
22
+ * <IconButton label="Edit" onclick={edit}><PencilIcon size={16} /></IconButton>
23
+ * <IconButton label="Pin" pressed={pinned} tooltip onclick={toggle}>…</IconButton>
24
+ * <IconButton label="Open" href="/x" shape="circle" variant="outline">…</IconButton>
25
+ *
26
+ * `variant="overlay"` is light-on-dark translucent chrome for controls that
27
+ * sit over media (lightboxes, video stages) where a ghost button vanishes.
28
+ *
29
+ * Idea from BoardUI's icon-button (MIT); written fresh for Svelte 5 + jatui
30
+ * tokens.
31
+ */
32
+ import type { Snippet } from 'svelte';
33
+ import type { HTMLButtonAttributes } from 'svelte/elements';
34
+ import { tooltip as tooltipAction, type TooltipOptions } from '../actions/tooltip';
35
+
36
+ interface Props
37
+ extends Omit<
38
+ HTMLButtonAttributes,
39
+ 'type' | 'disabled' | 'class' | 'onclick' | 'children' | 'aria-label'
40
+ > {
41
+ /** Accessible name — required. Rendered as aria-label (and the tooltip text). */
42
+ label: string;
43
+ variant?: IconButtonVariant;
44
+ size?: IconButtonSize;
45
+ shape?: 'square' | 'circle';
46
+ /** Render an `<a>` instead of a `<button>`. */
47
+ href?: string;
48
+ type?: 'button' | 'submit' | 'reset';
49
+ disabled?: boolean;
50
+ /** Toggle semantics — renders aria-pressed and the pressed surface. */
51
+ pressed?: boolean;
52
+ /** Show a tooltip: `true` uses `label`; a string shows that text instead (e.g. with a shortcut). */
53
+ tooltip?: boolean | string;
54
+ tooltipPlacement?: TooltipOptions['placement'];
55
+ class?: string;
56
+ onclick?: (e: MouseEvent) => void;
57
+ /** The icon. Size it to the button (16px for sm/md is the house default). */
58
+ children?: Snippet;
59
+ }
60
+
61
+ let {
62
+ label,
63
+ variant = 'ghost',
64
+ size = 'md',
65
+ shape = 'square',
66
+ href,
67
+ type = 'button',
68
+ disabled = false,
69
+ pressed,
70
+ tooltip = false,
71
+ tooltipPlacement = 'top',
72
+ class: className = '',
73
+ onclick,
74
+ children,
75
+ ...rest
76
+ }: Props = $props();
77
+
78
+ const VARIANT: Record<IconButtonVariant, string> = {
79
+ ghost: 'btn-ghost',
80
+ outline: 'btn-outline',
81
+ soft: 'btn-soft',
82
+ primary: 'btn-primary',
83
+ secondary: 'btn-secondary',
84
+ error: 'btn-error',
85
+ overlay: 'jatui-icon-btn--overlay'
86
+ };
87
+ const SIZE: Record<IconButtonSize, string> = {
88
+ xs: 'btn-xs',
89
+ sm: 'btn-sm',
90
+ md: '',
91
+ lg: 'btn-lg'
92
+ };
93
+
94
+ const classes = $derived(
95
+ [
96
+ 'btn jatui-icon-btn',
97
+ shape === 'circle' ? 'btn-circle' : 'btn-square',
98
+ VARIANT[variant] ?? 'btn-ghost',
99
+ SIZE[size],
100
+ pressed ? 'jatui-icon-btn--pressed' : '',
101
+ className
102
+ ]
103
+ .filter(Boolean)
104
+ .join(' ')
105
+ );
106
+
107
+ const ariaPressed = $derived(pressed === undefined ? undefined : pressed ? 'true' : 'false');
108
+ const tipText = $derived(typeof tooltip === 'string' ? tooltip : tooltip ? label : '');
109
+
110
+ /** use:tooltip only when asked for — no aria-describedby noise otherwise. */
111
+ function maybeTooltip(node: HTMLElement, text: string) {
112
+ let handle: ReturnType<typeof tooltipAction> | undefined;
113
+ const apply = (t: string) => {
114
+ if (t && !handle) handle = tooltipAction(node, { text: t, placement: tooltipPlacement });
115
+ else if (t && handle) handle.update?.({ text: t, placement: tooltipPlacement });
116
+ else if (!t && handle) {
117
+ handle.destroy?.();
118
+ handle = undefined;
119
+ }
120
+ };
121
+ apply(text);
122
+ return { update: apply, destroy: () => handle?.destroy?.() };
123
+ }
124
+ </script>
125
+
126
+ {#if href && !disabled}
127
+ <a
128
+ {...rest as Record<string, unknown>}
129
+ {href}
130
+ class={classes}
131
+ aria-label={label}
132
+ aria-pressed={ariaPressed}
133
+ {onclick}
134
+ use:maybeTooltip={tipText}
135
+ >
136
+ {#if children}{@render children()}{/if}
137
+ </a>
138
+ {:else}
139
+ <button
140
+ {...rest}
141
+ {type}
142
+ {disabled}
143
+ class={classes}
144
+ aria-label={label}
145
+ aria-pressed={ariaPressed}
146
+ {onclick}
147
+ use:maybeTooltip={tipText}
148
+ >
149
+ {#if children}{@render children()}{/if}
150
+ </button>
151
+ {/if}
152
+
153
+ <style>
154
+ /* Flat (no DaisyUI v5 variant shadow), press squish, token focus ring. */
155
+ .jatui-icon-btn {
156
+ --btn-shadow: 0 0 0 0 transparent;
157
+ transition:
158
+ transform 500ms var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
159
+ background-color var(--motion-fast, 150ms) var(--motion-glide, ease-out),
160
+ color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
161
+ }
162
+ .jatui-icon-btn:active:not(:disabled) {
163
+ transform: scale(0.92);
164
+ transition: transform var(--motion-instant, 80ms) ease-out;
165
+ }
166
+ .jatui-icon-btn:focus-visible {
167
+ outline: var(--jatui-focus-ring-width, 2px) solid
168
+ var(--jatui-focus-ring, color-mix(in oklab, var(--color-primary) 70%, transparent));
169
+ outline-offset: var(--jatui-focus-ring-offset, 2px);
170
+ }
171
+ .jatui-icon-btn--pressed:not(.btn-primary):not(.btn-secondary):not(.btn-error) {
172
+ background-color: var(
173
+ --jatui-selection-bg,
174
+ color-mix(in oklab, var(--color-primary) 14%, transparent)
175
+ );
176
+ color: var(--color-primary);
177
+ }
178
+
179
+ /* Over media: translucent light chrome that reads on any image or video. */
180
+ .jatui-icon-btn--overlay {
181
+ --btn-color: color-mix(in oklab, white 10%, transparent);
182
+ --btn-fg: color-mix(in oklab, white 95%, transparent);
183
+ --btn-border: color-mix(in oklab, white 15%, transparent);
184
+ --btn-noise: none;
185
+ background-color: color-mix(in oklab, white 10%, transparent);
186
+ border: 1px solid color-mix(in oklab, white 15%, transparent);
187
+ color: color-mix(in oklab, white 95%, transparent);
188
+ }
189
+ .jatui-icon-btn--overlay:hover {
190
+ background-color: color-mix(in oklab, white 20%, transparent);
191
+ }
192
+
193
+ @media (prefers-reduced-motion: reduce) {
194
+ .jatui-icon-btn,
195
+ .jatui-icon-btn:active:not(:disabled) {
196
+ transform: none;
197
+ transition: none;
198
+ }
199
+ }
200
+ </style>