@joewinke/jatui 0.38.6 → 0.39.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 (62) hide show
  1. package/CLAUDE.md +31 -0
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/components/BulkActionBar.svelte +32 -2
  4. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  5. package/dist/components/FloatingActionBar.svelte +102 -34
  6. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  7. package/dist/components/MediaExplorer.svelte +249 -0
  8. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  9. package/dist/components/NotificationCenter.svelte +481 -0
  10. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  11. package/dist/components/SearchDropdown.svelte +54 -8
  12. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  13. package/dist/components/agent/AgentLog.svelte +217 -0
  14. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  15. package/dist/components/agent/AgentLogRow.svelte +281 -0
  16. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  17. package/dist/components/agent/AgentTaskList.svelte +607 -0
  18. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  19. package/dist/components/agent/AgentThinking.svelte +280 -0
  20. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  21. package/dist/components/agent/ComposerGlow.svelte +265 -0
  22. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  23. package/dist/components/agent/ContextMeter.svelte +277 -0
  24. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  25. package/dist/components/agent/Questionnaire.svelte +767 -0
  26. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  27. package/dist/components/agent/ShimmerText.svelte +100 -0
  28. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  29. package/dist/components/agent/StreamedText.svelte +135 -0
  30. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  31. package/dist/components/agent/agentLog.d.ts +22 -0
  32. package/dist/components/agent/agentLog.js +26 -0
  33. package/dist/components/agent/agentTaskList.d.ts +90 -0
  34. package/dist/components/agent/agentTaskList.js +93 -0
  35. package/dist/components/agent/agentThinking.d.ts +46 -0
  36. package/dist/components/agent/agentThinking.js +78 -0
  37. package/dist/components/agent/contextMeter.d.ts +97 -0
  38. package/dist/components/agent/contextMeter.js +156 -0
  39. package/dist/components/agent/questionnaire.d.ts +104 -0
  40. package/dist/components/agent/questionnaire.js +225 -0
  41. package/dist/components/agent/revealCore.d.ts +74 -0
  42. package/dist/components/agent/revealCore.js +160 -0
  43. package/dist/components/agent/revealTicker.d.ts +34 -0
  44. package/dist/components/agent/revealTicker.js +73 -0
  45. package/dist/components/agent/streamedText.d.ts +28 -0
  46. package/dist/components/agent/streamedText.js +59 -0
  47. package/dist/components/messaging/MessageInput.svelte +34 -4
  48. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  49. package/dist/components/messaging/MessageItem.svelte +2 -2
  50. package/dist/components/notificationCenter.d.ts +65 -0
  51. package/dist/components/notificationCenter.js +110 -0
  52. package/dist/components/progress/Steps.svelte +269 -5
  53. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  54. package/dist/components/progress/stepsSummary.d.ts +14 -0
  55. package/dist/components/progress/stepsSummary.js +32 -0
  56. package/dist/index.d.ts +34 -0
  57. package/dist/index.js +29 -0
  58. package/dist/selection/clickSelection.d.ts +89 -0
  59. package/dist/selection/clickSelection.js +152 -0
  60. package/dist/selection/createClickSelection.d.ts +79 -0
  61. package/dist/selection/createClickSelection.js +134 -0
  62. package/package.json +3 -2
package/CLAUDE.md CHANGED
@@ -81,6 +81,37 @@ Then import:
81
81
  | `VariantMatrixPicker` | Category/option chip grid for building a combinatorial variant matrix (harvested from vid-gen's FrameVariantGenerator, presets stripped — caller-supplied `categories`). Each category tab shows a live selection count and an additive/multiplicative badge: multiplicative selections cartesian-multiply the total, additive ones stack flat onto every combination. Live combination count in the footer; algebra exported separately as pure functions (`generateCombinations`, `countCombinations`, `variantCategoryMode`, `findVariantOption`, `generateVariantFilename`). | `categories: VariantCategory[]`, `selected` (bindable `VariantSelection`), `forceMode?`, `showCount?` (true), `countLabel?`, `onChange?`, `class?` |
82
82
  | `BottomSheet` | Mobile bottom sheet (upstreamed from the JAT IDE — jatui-rl7w6.2): drag handle with pointer capture, backdrop close, optional header. `snap='dual'` enables the inspector pattern — peek → expanded snap heights with flick-momentum release (fast flick ≥ 0.5 px/ms wins over position; flick down from peek dismisses) and `visualViewport` keyboard avoidance so content clears the soft keyboard. | `open` (bindable), `title?`, `zIndex?` (70), `fullHeight?`, `snap?` ('single'\|'dual'), `peekFraction?` (0.4), `expandedFraction?` (0.9), `onclose?`, `children` (snippet) |
83
83
 
84
+ ### Agent surfaces (BoardUI wave 2)
85
+
86
+ "Agent is working" UI. Free BoardUI items (agent-thinking, agent-log, composer-loader, notification-center) were ported under MIT — see `THIRD_PARTY_NOTICES.md`; AgentTaskList, Questionnaire and ContextMeter are clean-room. Demos: `/components/agent-thinking`, `agent-log`, `agent-task-list`, `questionnaire`, `context-meter`, `notification-center`, `composer-glow`, `streamed-text`.
87
+
88
+ | Component | Purpose | Key Props |
89
+ |-----------|---------|-----------|
90
+ | `ShimmerText` | A highlight that sweeps across text. Every gradient stop comes from ONE tone via `color-mix`, so any token colour shimmers the same way. Decorative (`.jatui-motion-ambient`): solid text in the tone under `data-motion="calm"`, reduced motion or `active={false}`. | `text?` / `children`, `tone?` ('subtle'\|'default'\|'strong'\|'primary' or any CSS colour), `duration?` (2600 ms), `active?`, `as?`, `class?` |
91
+ | `AgentThinking` | The "agent is working" status line: an indicator, a shimmering label and an elapsed timer. Variants: `wave` / `spin` (3×3 dot comet, pure CSS), `stars` (sparkles), `infinity` (comet on a figure-eight). One `role="status"`; the timer is `aria-hidden`, so screen readers hear the label once. The indicator keeps running on calm (it carries meaning) but stops under reduced motion; the shimmer stops on calm. `startedAt` keeps the true elapsed time when a row re-mounts. | `variant?`, `label?` ('Thinking'), `tone?`, `shimmer?`, `showTimer?`, `startedAt?` (epoch ms), `size?` ('sm'\|'md'), `class?` |
92
+ | `AgentLog` | Streaming agent transcript. Rows grow in: height 0→auto, a brief blur and lift, and a soft fading clip edge (a registered `--jatui-reveal-fade` mask, removed once the row settles). A curved tree guide draws beside each row; the elbow is fixed at 14px so wrapped labels branch at their first line, the trunk is 1px, and it mirrors in RTL. An optional `working` line (AgentThinking "stars") sits at the tail, off the guide. Durations come from `--motion-*` with glide easing. Rows present on first render don't animate, and nothing animates under reduced motion. Fill it with `items` + a `row` snippet, or with `AgentLogRow` children. `bridge` connects a nested list to its parent row's glyph. Pace it with `createRevealTicker`. | `items`, `row` (snippet), `key?`, `working?`, `workingTimer?`, `guide?`, `bridge?`, `appear?`, `live?` (role="log"), `ariaLabel?`, `children?` |
93
+ | `AgentLogRow` | One row of an `AgentLog`: grow-in, soft clip edge and its piece of the guide. Use it directly in children mode. | `first?`, `last?`, `guide?`, `animate?`, `as?` ('li'\|'div'), `class?`, `children` |
94
+ | `AgentTaskList` | An agent's plan as it runs, built on `AgentLog`: tasks → steps. Each task header and each step is one reveal unit. With `revealed` unset a timer paces them (`startDelay` / `stepInterval` / `delayFor`); set it to drive from real events (`Infinity` shows everything). A running task's title shimmers; a done or failed task still reads as running until its steps have appeared. Steps take truncating resource `chips` (icon component or glyph, optional `href`). A step with `sources` branches into a "Sources" row of overlapping site marks: `maxSources` (6) then "+N", a title + domain tooltip on hover/focus, a link when `href` is set, the host's `favicon` URL or a letter tile (no bundled logos). `collapseOnComplete`: `true` folds each finished task once the log moves past it, `'all'` folds them together at the end; failed tasks stay open. The header is a real button (`aria-expanded` / `aria-controls`). `onComplete` fires once. | `tasks: AgentTask[]`, `revealed?`, `stepInterval?`, `startDelay?`, `delayFor?`, `collapseOnComplete?: boolean \| 'all'`, `working?`, `maxSources?`, `sourcesLabel?`, `statusLabels?`, `foldedSummary?`, `onComplete?`, `onToggle?`, `ariaLabel?` |
95
+ | `Questionnaire` | Plan-mode question card shaped like Claude Code's AskUserQuestion: pass a tool call's `questions` (`{question, header?, options: {label, description?, preview?}[], multiSelect?}[]`) straight through; answers come back aligned as `{ values: string[] (option labels), other? }[]`. One question per step, each a `fieldset` / `legend`. Single = radiogroup with roving tabindex (arrows move focus, Space/Enter/click pick, auto-advance after `advanceDelay`); multi = checkbox group. Digit keys 1..N pick and N+1 ticks Other (a component-scoped keydown, never a window listener, ignored while typing). Free-text "Other" row: typing ticks it, Enter sends in single mode. An option's `preview` shows as a monospace pane while it is focused or selected. Footer step pills (jump to any; `aria-current="step"`; answered pills marked) + Previous / Next / Done → `onComplete`. "Question 2 of 4" in the header and in a polite live region. Card height eases between steps on `--motion-move` and content glides in the direction of travel; neither under reduced motion. Pure logic in `agent/questionnaire.ts`. | `questions`, `answers?` (bindable), `step?` (bindable), `onComplete?`, `onChange?`, `allowOther?` (true), `otherLabel?`, `otherPlaceholder?`, `advanceDelay?` (180, `false` disables), `submitLabel?` ('Done'), `nextLabel?`, `previousLabel?`, `title?`, `requireAll?` (true), `disabled?`, `class?` |
96
+ | `ContextMeter` | Context-window meter split by token bucket. Stacked bar coloured from a token palette (primary / secondary / accent / info / success / warning, then `color-mix` variants), with free space as the track. Total line reads "820k / 1M (82%)". `role="meter"` carries valuenow/min/max/valuetext. An `aria-expanded` button reveals a `<table>` breakdown with a "Free space" row; segments with `group` fold under a collapsible header row. Deferred buckets are listed with a dash and left out of both the bar and the total. Warning tint at `warnAt`, error at ≥100% (bar widths rescale to fill exactly 100%). Pairs with `QuotaMeter` for plan limits. | `max`, `segments: {id?, label, tokens, deferred?, color?, group?}[]`, `groups?: {id, label, defaultExpanded?}[]`, `label?` ('Context'), `unit?` ('tokens'), `expanded` (bindable), `warnAt?` (0.8), `collapsible?` (true), `class?` |
97
+ | `NotificationCenter` | Inbox panel: title + "N unread" + "Mark all read"; a tab list with count chips (default All / Mentions / System by `category`; `bind:tab` or uncontrolled `defaultTab`; arrows / Home / End switch tabs). Items are newest first under Today / Yesterday / This week / Earlier headers (`groupNotificationsByDate`, `now` injectable). Roving focus via `createListNav`, scoped to the list: j/k/arrows move, Enter opens → `onOpen(item)` or follows `href`. Opening an item or clicking one of its actions marks it read. The host owns read state (`read`); the component also tracks read ids locally, so it works uncontrolled. The unread dot is announced (sr-only "Unread"). Status glyph or `Avatar`; `<time>` via `formatRelativeTime`, refreshed each minute. Empty state via `Empty` ("You're all caught up"). | `items: NotificationItem[]`, `tabs?: {id, label, filter?}[]`, `tab` (bindable), `defaultTab?`, `onTabChange?`, `onOpen?`, `onMarkRead?(ids)`, `onMarkAllRead?` (omitted → `onMarkRead` gets every unread id), `groupByDate?` (true), `now?`, `maxHeight?` ('32rem'), `title?`, `emptyTitle?`, `emptyDescription?` |
98
+ | `ComposerGlow` | Opt-in ambient rim light around a composer while the agent works: an SVG `<rect rx>` stroke with a pathLength=100 dash laps the box in three layers (crisp line, glow, bloom clipped inside; SVG `feGaussianBlur`, so iOS stays soft); a ResizeObserver keeps the viewBox equal to the px box. Token colours only (info → primary → `color-mix` midpoint → secondary → accent). Fades in/out on `active`; the lap runs only while visible. Under `data-motion="calm"` / reduced motion there is no frozen arc: it shows a static primary focus-ring tint. Paints a `--color-base-100` surface, so the wrapped composer must be transparent. | `active`, `speed?` (4.5 s/lap), `intensity?` ('subtle'\|'normal'\|'vivid'), `radius?` (px or CSS; default box radius), `class?`, `children` |
99
+ | `StreamedText` | Streamed assistant text revealed per paragraph/line, never per token: an append-only splitter (`splitStreamedText`, index keys, code fences kept whole) mounts each block once with a short fade + un-blur on motion tokens, after which it only grows. History doesn't animate; reduced motion is static; a subtle caret shows while `streaming`. `renderBlock` lets the host render markdown per block. Recipe: show `AgentThinking` until the first *text* part exists, not while the request status is "submitted". | `text`, `streaming?`, `split?` ('paragraph'\|'line'), `as?`, `class?`, `renderBlock?: Snippet<[string, number]>` |
100
+
101
+ **Upgrades in this wave (all additive):**
102
+ - **`Steps`** (`progress/Steps.svelte`) — `collapsible?`, `collapsed?` (bindable), `summary?: string | Snippet` (default "3 steps left" / "All steps done", live while folded; the header is a real `aria-expanded` / `aria-controls` button), `orientation?` ('horizontal'\|'vertical'; vertical also shows descriptions), `activeProgress?` (0–1, a mini ProgressRing on the active node and in the folded header), `completionDelay?` + `onFinished?` (fires once after `current` passes the last step; re-arms if `current` drops back). Unset, the markup is unchanged. Helpers: `stepsSummary`, `stepsRemaining`, `isStepsFinished`, `clampStepProgress`.
103
+ - **`MessageInput`** — `busy?` + `onStop?` swap the send button for a same-size "Stop generating" button; Escape in the input stops (an open mention list still takes Escape first); while busy, Enter doesn't send and the draft is kept. `statusTab?: Snippet` renders a slim strip attached to the input's top edge.
104
+ - **`MessageItem`** — the action toolbar and timestamp also appear on keyboard focus (`group-focus-within`), not only on hover.
105
+
106
+ ```typescript
107
+ // Reveal pacing for AgentLog / AgentTaskList. Every option may be a value or a getter.
108
+ import { createRevealTicker } from "jatui"
109
+ // const t = createRevealTicker({ total: () => n, stepInterval?, startDelay?, revealed?: () => count | undefined,
110
+ // delayFor?: (i) => ms, run?, onComplete }) → t.revealed, t.complete, t.replay()
111
+ // `revealed` defined = event-driven (no timer). onComplete fires once per completion.
112
+ // The timing core is also exported as a plain class: RevealCore (injectable clock).
113
+ ```
114
+
84
115
  ### Tables (subpath: `jatui/table`)
85
116
 
86
117
  Requires peer `@tanstack/table-core`. `import { DataTable, flexRender, columnResize } from 'jatui/table'`.
@@ -0,0 +1,51 @@
1
+ # Third-party notices
2
+
3
+ jatui is MIT licensed (see `LICENSE`). Some files adapt code from the projects
4
+ below; each such file also carries the notice in its header comment.
5
+
6
+ ## BoardUI (free tier)
7
+
8
+ <https://github.com/BoardUI/boardui> — MIT License
9
+
10
+ ```
11
+ MIT License
12
+
13
+ Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ ```
33
+
34
+ Files adapted from BoardUI free components:
35
+
36
+ - `src/lib/components/agent/agentThinking.ts`, `AgentThinking.svelte`,
37
+ `ShimmerText.svelte` — from `agent-thinking` (dot-grid comet curve, sparkle
38
+ and figure-eight geometry, single-tone shimmer gradient).
39
+ - `src/lib/components/agent/AgentLog.svelte`, `AgentLogRow.svelte`, `agentLog.ts`,
40
+ `revealCore.ts` — from `agent-log` (grow-in row, soft reveal mask, one-pen
41
+ tree guide, reveal ticker).
42
+ - `src/lib/components/agent/ComposerGlow.svelte` — from `composer-loader`
43
+ (perimeter/dash math, layered SVG blur).
44
+ - `src/lib/components/agent/StreamedText.svelte` — the per-block reveal idea from
45
+ `agent-chat`.
46
+ - `src/lib/components/NotificationCenter.svelte`, `notificationCenter.ts` — from
47
+ `notification-center`.
48
+
49
+ BoardUI Pro items are not covered by the MIT licence. `AgentTaskList`,
50
+ `Questionnaire` and `ContextMeter` were built from public behaviour only; no Pro
51
+ source was read or copied.
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * BulkActionBar — floating action bar for listNav selections.
3
+ * BulkActionBar — floating action bar for listNav or clickSelection selections.
4
4
  *
5
5
  * Simpler API than `FloatingActionBar`: takes a `Set` or `Array` of
6
6
  * selected ids and a flat `actions[]`. Renders nothing when the
@@ -47,6 +47,27 @@
47
47
  * onClear={() => nav.clearSelection()}
48
48
  * />
49
49
  *
50
+ * Usage — with the mouse (createClickSelection): click selects one,
51
+ * Ctrl/⌘-click toggles, Shift-click selects a range in display order,
52
+ * Shift+arrows extend, Ctrl/⌘ A selects all, Esc clears. Feed its
53
+ * `ordered` (display order) or `picked` (selection order) to the bar:
54
+ *
55
+ * const sel = createClickSelection({ ids: () => rows.map((r) => r.id) });
56
+ *
57
+ * {#each rows as r (r.id)}
58
+ * <button class:selected={sel.has(r.id)} onclick={(e) => sel.click(r.id, e)}>…</button>
59
+ * {/each}
60
+ * <svelte:window onkeydown={(e) => sel.handleKeydown(e, { next: neighbour })} />
61
+ *
62
+ * <BulkActionBar
63
+ * selection={sel.ordered}
64
+ * actions={[{ label: 'Delete', onSelect: () => del(sel.ordered), destructive: true }]}
65
+ * onClear={() => sel.clear()}
66
+ * />
67
+ *
68
+ * listNav is the keyboard-only alternative (x / V / Shift+J/K). Pick ONE
69
+ * selection owner per surface; see createClickSelection for combining them.
70
+ *
50
71
  * Usage — grouped:
51
72
  *
52
73
  * <BulkActionBar
@@ -93,7 +114,9 @@
93
114
  label: string;
94
115
  /** Optional SVG path string for a 24×24 icon on the group button. */
95
116
  icon?: string;
96
- /** Actions rendered inside the dropdown. `destructive` on a nested action
117
+ /** Actions rendered inside the dropdown; a nested action's `key` shows as a
118
+ * hint at the item's right edge but is NOT bound (bind it in the host).
119
+ * `destructive` on a nested action
97
120
  * currently only affects semantics for readers — the dropdown item row
98
121
  * is styled uniformly. */
99
122
  actions: BulkAction[];
@@ -120,6 +143,8 @@
120
143
  loadingMessage?: string | null;
121
144
  /** Label for the count. Default "selected". */
122
145
  countLabel?: string;
146
+ /** 'floating' (default, fixed bottom-centre) or 'inline' (in the host's own bar). */
147
+ placement?: 'floating' | 'inline';
123
148
  }
124
149
 
125
150
  let {
@@ -130,6 +155,7 @@
130
155
  loading = false,
131
156
  loadingMessage = null,
132
157
  countLabel = 'selected',
158
+ placement = 'floating',
133
159
  }: Props = $props();
134
160
 
135
161
  const count = $derived(
@@ -179,6 +205,9 @@
179
205
  items: g.actions.map((a) => ({
180
206
  label: a.label,
181
207
  onclick: a.onSelect,
208
+ // Inside a group a key is a hint only: the host binds it (BulkActionBar
209
+ // registers hotkeys for flat actions alone).
210
+ hint: a.key,
182
211
  })),
183
212
  })),
184
213
  );
@@ -192,4 +221,5 @@
192
221
  {loadingMessage}
193
222
  {onClear}
194
223
  {countLabel}
224
+ {placement}
195
225
  />
@@ -27,7 +27,9 @@ export interface BulkActionGroup {
27
27
  label: string;
28
28
  /** Optional SVG path string for a 24×24 icon on the group button. */
29
29
  icon?: string;
30
- /** Actions rendered inside the dropdown. `destructive` on a nested action
30
+ /** Actions rendered inside the dropdown; a nested action's `key` shows as a
31
+ * hint at the item's right edge but is NOT bound (bind it in the host).
32
+ * `destructive` on a nested action
31
33
  * currently only affects semantics for readers — the dropdown item row
32
34
  * is styled uniformly. */
33
35
  actions: BulkAction[];
@@ -53,6 +55,8 @@ interface Props {
53
55
  loadingMessage?: string | null;
54
56
  /** Label for the count. Default "selected". */
55
57
  countLabel?: string;
58
+ /** 'floating' (default, fixed bottom-centre) or 'inline' (in the host's own bar). */
59
+ placement?: 'floating' | 'inline';
56
60
  }
57
61
  declare const BulkActionBar: import("svelte").Component<Props, {}, "">;
58
62
  type BulkActionBar = ReturnType<typeof BulkActionBar>;
@@ -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>;