@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
package/CLAUDE.md CHANGED
@@ -22,9 +22,10 @@ Then import:
22
22
  | Component | Purpose | Key Props |
23
23
  |-----------|---------|-----------|
24
24
  | `TextInput` | Text input with label, error, icon slots | `label`, `value` (bindable), `placeholder`, `error`, `disabled` |
25
- | `TextArea` | Multiline text input | `label`, `value` (bindable), `placeholder`, `rows`, `error`, `maxlength` |
25
+ | `TextArea` | Multiline text input. Auto-grow (`autoResize`, or implied by `maxRows`) grows a whole line at a time from `minRows` (default `rows`) to `maxRows`, then scrolls — CSS `field-sizing: content` where supported, row-quantized JS fallback (`textareaRowsHeight`) elsewhere. | `label`, `value` (bindable), `placeholder`, `rows`, `minRows?`, `maxRows?`, `autoResize?`, `error`, `maxlength` |
26
26
  | `SelectInput` | Dropdown select | `label`, `value` (bindable), `options: SelectOption[]`, `placeholder` |
27
27
  | `PhoneInput` | Intl phone (country dropdown + tel input, outputs E.164; caps national digits per country). Fires `onChange(value)` on every change. | `value` (bindable), `name`, `defaultCountry`, `required`, `onChange` |
28
+ | `PinInput` | One box per character for PINs and one-time codes. Every cell is `autocomplete="one-time-code"` with no maxlength, so SMS autofill / WebOTP (which drop the whole code into the focused cell) fill all cells from the first; paste distributes from the caret; `dir="ltr"`, `inputmode`/`pattern` numeric. Pure helpers: `applyPinCellInput`, `distributePin`, `isPinComplete`. | `value` (bindable), `length` (6), `numeric`, `masked`, `name?`, `autocomplete?` (`one-time-code`), `error`, `label`, `oncomplete` |
28
29
  | `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
29
30
  | `SearchDropdown` | Searchable dropdown with grouped results, keyboard nav, avatar/initials options. `appendToBody` escapes table/overflow stacking contexts; `variant='chip'` renders a compact pill; `size='sm'` a smaller trigger; `dropup` opens upward; `clearable` shows an × when a value is set. `onSearch` turns the built-in search box into a debounced server-side search hook (fires on open/clear with `''`; internal filtering is bypassed — the consumer updates `groups`). Exposes `openDropdown()`. | `groups: SearchDropdownGroup[]`, `value`, `onChange`, `placeholder`, `variant`, `size`, `dropup`, `appendToBody`, `clearable`, `colorFn`, `onSearch`, `searchDebounceMs` |
30
31
  | `FilterDropdown` | Multi-select filter dropdown | `label`, `options: string[]`, `selected: Set<string>` (bindable) |
@@ -56,13 +57,16 @@ Then import:
56
57
  | Component | Purpose | Key Props |
57
58
  |-----------|---------|-----------|
58
59
  | `Button` | Styled button with variants | `variant`, `size`, `loading`, `disabled`, `onclick` |
60
+ | `IconButton` | Icon-only button with a **required** `label` (the aria-label; also the tooltip text with `tooltip`). Square/circle, toggle `pressed` (aria-pressed + selection surface), `href` → `<a>`, `variant="overlay"` for chrome over media. Use instead of hand-written `btn btn-sm btn-circle btn-ghost`. | `label`, `variant?` (ghost\|outline\|soft\|primary\|secondary\|error\|overlay), `size?`, `shape?` ('square'\|'circle'), `pressed?`, `tooltip?: boolean \| string`, `href?`, `children` (icon) |
61
+ | `CloseButton` | The one ✕ for dialogs, drawers, sheets, toasts and lightboxes — an IconButton with a built-in X whose stroke is a true CSS pixel at every size. Modal, Drawer, BottomSheet and MediaLightbox use it. | `label?` ('Close'), `size?` ('sm'), `variant?`, `shape?` ('circle'), `tooltip?`, `onclick` |
62
+ | `LinkButton` | Text action styled as a link: `<a>` with `href`, `<button>` without; underline on hover (or always). `external` adds new-tab + rel=noopener + an SR hint. | `href?`, `tone?` (inherit\|primary\|muted\|error), `size?`, `underline?`, `external?`, `disabled?`, `onclick?`, `children` |
59
63
  | `ProgressButton` | Multi-phase async submit button (upstreamed from the JAT IDE): walks through the **named phases** of the work (`phases: [{key,label}]` in order) instead of a blank spinner — spinner + current phase label with shimmer, a fill bar that advances to `(index+1)/N`, disabled while busy, green check-flash on `success`. Caller owns the state machine (`phase` / `success`). Renders through the shared `Button`, so `class` composes with join patterns. | `phase?` (null = idle), `phases?: Phase[]`, `idleLabel?`, `success?`, `successLabel?`, `variant?`, `size?`, `disabled?`, `type?`, `class?`, `onclick?`, `children` |
60
64
  | `Badge` | Status/label badge | `variant`, `size`, `children` (snippet) |
61
65
  | `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
62
66
  | `Card` | Content card container | `title`, `children` (snippet) |
63
67
  | `Modal` | Dialog overlay. `zIndex` accepts `number` OR any CSS `z-index` string, including `"var(--z-lightbox)"`. **Escape is owned by the dialog element itself** (not the window handler) — Modal focuses the dialog on open, so `trapKeys`' stopPropagation would otherwise swallow Esc before the window listener sees it; a Modal opened inside a host Drawer closes before the drawer. The component ships its own open-state `.modal-box` opacity rule, so it renders under DaisyUI 5 with no host global override. | `open` (bindable), `title`, `children` (snippet), `zIndex?: string \| number`, `trapKeys?`, `closeOnEscape?` |
64
68
  | `ConfirmModal` | Yes/no confirmation dialog | `open` (bindable), `title`, `message`, `onConfirm`, `onCancel` |
65
- | `Drawer` | Slide-out side panel | `open` (bindable), `side`, `children` (snippet) |
69
+ | `Drawer` | Slide-out side panel. Header ✕ is a `CloseButton`; the body carries `use:scrollEdge` (opt out with `scrollEdge={false}`). | `open` (bindable), `side`, `scrollEdge?`, `children` (snippet) |
66
70
  | `LoadingSpinner` | Spinner animation | `size`, `color` |
67
71
  | `Sparkline` | Inline mini chart | `data: number[]`, `width`, `height`, `color` |
68
72
  | `LazyImage` | Image with lazy loading + placeholder | `src`, `alt`, `placeholder` |
@@ -81,6 +85,37 @@ Then import:
81
85
  | `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
86
  | `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
87
 
88
+ ### Agent surfaces (BoardUI wave 2)
89
+
90
+ "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`.
91
+
92
+ | Component | Purpose | Key Props |
93
+ |-----------|---------|-----------|
94
+ | `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?` |
95
+ | `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?` |
96
+ | `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?` |
97
+ | `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` |
98
+ | `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?` |
99
+ | `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?` |
100
+ | `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?` |
101
+ | `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?` |
102
+ | `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` |
103
+ | `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]>` |
104
+
105
+ **Upgrades in this wave (all additive):**
106
+ - **`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`.
107
+ - **`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.
108
+ - **`MessageItem`** — the action toolbar and timestamp also appear on keyboard focus (`group-focus-within`), not only on hover.
109
+
110
+ ```typescript
111
+ // Reveal pacing for AgentLog / AgentTaskList. Every option may be a value or a getter.
112
+ import { createRevealTicker } from "jatui"
113
+ // const t = createRevealTicker({ total: () => n, stepInterval?, startDelay?, revealed?: () => count | undefined,
114
+ // delayFor?: (i) => ms, run?, onComplete }) → t.revealed, t.complete, t.replay()
115
+ // `revealed` defined = event-driven (no timer). onComplete fires once per completion.
116
+ // The timing core is also exported as a plain class: RevealCore (injectable clock).
117
+ ```
118
+
84
119
  ### Tables (subpath: `jatui/table`)
85
120
 
86
121
  Requires peer `@tanstack/table-core`. `import { DataTable, flexRender, columnResize } from 'jatui/table'`.
@@ -189,11 +224,13 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
189
224
  | Component | Purpose | Key Props |
190
225
  |-----------|---------|-----------|
191
226
  | `PageHeader` | The standard route header (upstreamed from the JAT IDE): uppercase/tracked "instrument label" title + slots for a count chip, subtitle, leading `icon` snippet, right-aligned `actions` cluster, `meta` metrics strip and content below the header row. `sticky` pins it to the top of the scroll container; `size='lg'` for landing screens. All color via `--color-*` tokens; entrance animation reduced-motion gated (`animate`, default off). | `title`, `subtitle?`, `count?`, `size?` ('md'\|'lg'), `sticky?`, `animate?`, `icon?`/`actions?`/`meta?`/`children` (snippets) |
227
+ | `Breadcrumb` | Location trail in a `<nav aria-label>`; the last item is `aria-current="page"`. `maxItems` keeps the root + the last `maxItems - 1` and folds the middle into a `…` button whose menu lists the hidden levels (arrows/Home/End/Escape, click-outside; pure `collapseBreadcrumb`). Leading icons via `item.icon` (component taking `{size,class}`) or the `icon` snippet. | `items: BreadcrumbItem[]`, `maxItems?`, `icon?` (snippet `(item, i)`), `ariaLabel?` |
192
228
  | `ResizablePanel` | Resizable container | `minWidth`, `maxWidth`, `defaultWidth` |
193
229
  | `ResizableDivider` | Drag handle between panels | `onResize` |
194
230
  | `InfiniteGrid` | Cursor-paged infinite-scroll grid (from chimaro). `loadMore(cursor, {signal})` resolves `{items, nextCursor, total?}` (`nextCursor: null` = end); cells render through the `renderItem(item, i)` snippet. Change `resetKey` to start over — the in-flight page is aborted and a late page for the old key is dropped. One request in flight; items de-duped by `getKey`; short pages auto-chain until the sentinel leaves the screen; distinct skeleton / empty / exhausted / error(+Retry) states; non-item rows are `overflow-anchor: none` so scroll anchoring can't runaway-load. Scroll root = `root` or nearest scrollable ancestor. Exposes `reset()`/`retry()`. Not virtualized. Pure core: `createInfiniteLoader`. | `loadMore`, `renderItem` (snippet), `getKey?`, `resetKey?`, `columns?` / `minItemWidth?`, `gap?`, `root?`, `loadAhead?` (400px), `skeleton?`/`empty?`/`end?`/`error?` (snippets), `emptyText?`, `endText?`, `onStateChange?` |
195
231
  | `InfiniteSearchGrid` | Search box + `InfiniteGrid`. Query is debounced, then becomes the grid's `resetKey` (Enter commits now, Escape clears). Below `minQueryLength` no request is made and `prompt` shows. All other `InfiniteGrid` props pass through. | `search(query, cursor, {signal})`, `renderItem` (snippet), `query` (bindable), `debounceMs?` (300), `minQueryLength?` (0), `placeholder?`, `noResultsText?`/`noResults?`, `prompt?`/`promptText?`, `toolbar?` (snippet) |
196
232
  | `SectionNav` | Sticky in-page section index for long single-column pages — vertical rail beside the content on xl+, wrapping chip row above it below xl. Built-in scroll-spy (omit `activeId` and it just works; `bind:activeId` to observe/control — the spy writes back to it): on every observer callback + rAF-throttled scroll/resize it measures ALL sections and activates the last one whose top is at/above the activation line (`activationRatio`, default 0.3 of the scroller height); at the bottom of the page the last section wins. Clicking an item activates it immediately and holds the spy off until the smooth scroll lands (`scrollend`, or 800ms after the last scroll event); reduced-motion scrolls instantly. Pure decision fn exported as `computeActiveSection(rects, viewport, scroll, ratio?)`. Amber dot per `dirtyIds` marks sections holding unsaved edits. `go()` scrolls AND moves focus so keyboard/SR users land where the click did. j/k/Enter via `listNav`. Layout contract: parent is a `flex flex-wrap gap-8 items-start` row, content column `flex-1 min-w-0`; sections need `id` + `scroll-mt-*`. | `sections: {id,label}[]`, `activeId?` (bindable), `dirtyIds?: Set<string>`, `ariaLabel?`, `spy?`, `activationRatio?` (0.3) |
233
+ | `use:scrollEdge` (action) | Progressive-blur fade on a scroll region's edge once content scrolls under it: a stationary `aria-hidden` sticky strip inside the scroller with two masked backdrop-blur layers + a surface→transparent gradient, each ramping opacity 0→1 over the first `size` px of scroll distance (nothing drawn at rest). Keep headers OUTSIDE the scroller; chat threads (stick-to-bottom) want `edges: 'bottom'`. Opacity-only, reduced-motion safe. Pure: `edgeOpacity`, `edgeDistances`. | `edges?` ('top'\|'bottom'\|'both'), `size?` (24), `surface?` (`--jatui-surface`), `blur?` (true), `enabled?` |
197
234
 
198
235
  **Windowing (subpath: `jatui/virtual`, optional peer `@tanstack/virtual-core`).** `createRowWindow(opts)` is headless variable-height windowing built on **spacer rows**, not absolute positioning, so it works inside a `<table>` and keeps `table-layout: fixed` and sticky `<thead>`s. It returns reactive `items` (`{index, key, start, size, end, gapBefore, sticky, pinned}`), `paddingTop`, `paddingBottom`, `totalSize` and `windowed`, plus `use:win.anchor` (on the `<tbody>` or list container, whose top is item 0's top) and `use:win.measure={item.key}` (on each item; a shared ResizeObserver re-measures it). Reading a row's height for the first time, or later re-measuring one above the viewport, shifts `scrollTop` by the delta so nothing jumps. The anchor gets `overflow-anchor: none` so native scroll anchoring doesn't correct the shift a second time. The options are reactive getters: `count`, `getKey`, `estimateSize`, `pinned()` (keys kept rendered, e.g. an open editor or exiting row), `stickyIndices()` (a flat list, or one array per level `[projectHdrs, epicHdrs]`; the active header of each level stays rendered, and an inner header never outlives its outer group), and `threshold` (render everything below N; pass it as a getter to make it reactive). Also `scrollRoot` (`'auto'` = the nearest ancestor that really scrolls, else the window; it re-resolves on resize), `overscan` (6), `scrollPaddingStart/End` and `initialViewportSize` (800; used for SSR and first paint). Methods: `scrollToIndex`/`scrollToKey(…, {align})` (queued until mount), `indexOf`, `remeasure()`. Render `item.gapBefore` as a spacer before the item; it is non-zero only when a pinned or sticky item sits outside the slice. Pure policy helpers are exported too (`extractWindowIndices`, `activeStickyIndices`, `layoutWindow`, `findScrollRoot`, `RowWindowEngine`). Demo: `/components/row-window`.
199
236
 
@@ -332,8 +369,8 @@ const callbacks: AnnotationCallbacks = {
332
369
 
333
370
  | Component | Purpose | Key Props |
334
371
  |-----------|---------|-----------|
335
- | `ThemeSelector` | DaisyUI theme switcher with preview dots | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange` |
336
- | `ThemeToggle` | Icon-only sun<->moon light/dark toggle with an animated morph (transform+opacity on motion tokens; instant under reduced-motion; spring flattens on `data-motion="calm"`). A pure control — flips the bindable `theme` and fires `onchange`; storage stays with the consumer. | `theme` (bindable), `lightTheme`, `darkTheme`, `onchange`, `size` |
372
+ | `ThemeSelector` | DaisyUI theme switcher with preview dots. `reveal` wipes the new theme in from the clicked item (`themeWipe`). | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange`, `reveal?: boolean \| 'hard' \| 'soft'` |
373
+ | `ThemeToggle` | Icon-only sun<->moon light/dark toggle with an animated morph (transform+opacity on motion tokens; instant under reduced-motion; spring flattens on `data-motion="calm"`). A pure control — flips the bindable `theme` and fires `onchange`; storage stays with the consumer. `reveal` runs the flip inside a `themeWipe` circular View Transition from the button (keyboard activation grows from its centre). | `theme` (bindable), `lightTheme`, `darkTheme`, `onchange`, `size`, `reveal?: boolean \| 'hard' \| 'soft'` |
337
374
 
338
375
  ### Keyboard Navigation
339
376
 
@@ -495,6 +532,12 @@ import { groupRows, groupPathKey, type GroupLevel, type GroupedItem } from "jatu
495
532
  // groupRows(rows, [{ key: t => t.project }, { key: t => t.epic }], { getRowId, collapsed, label? })
496
533
  // → { kind:'group', key, path, depth, label, rows, count, collapsed } | { kind:'row', row, id, depth, groupPath, position }
497
534
 
535
+ // Theme change reveal — circular View Transition from the click (or, for a
536
+ // keyboard activation, the control's centre). Single-flight, level-scaled
537
+ // duration, reduced-motion = instant. ThemeToggle/ThemeSelector `reveal` use it.
538
+ import { themeWipe } from "jatui"
539
+ // themeWipe(event, () => setTheme(next), { edge: 'soft' }) // edge: 'hard' (default) | 'soft'
540
+
498
541
  // Date formatting
499
542
  import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
500
543
 
@@ -614,7 +657,13 @@ Three rules keep DaisyUI swappable instead of load-bearing:
614
657
  internal skin, like vendored CSS — never part of a component's public
615
658
  props/API.
616
659
  3. **jatui owns the token layer.** Components consume `--jatui-*`
617
- (`styles/tokens.css`: radius/spacing/border) and `--motion-*`
660
+ (`styles/tokens.css`: radius/spacing/border, plus the colour-intent layer —
661
+ `--jatui-focus-ring{,-width,-offset}`, `--jatui-surface`, `--jatui-hover-bg`,
662
+ `--jatui-pressed-bg`, `--jatui-separator`, `--jatui-selection-{bg,edge,solid,solid-fg}`,
663
+ `--jatui-chart-{1..5,grid,axis}`, `--jatui-event-{1..6}{,-bg,-fg}`,
664
+ `--jatui-status-{positive,caution,negative,neutral}`; all `color-mix` over DaisyUI
665
+ semantic vars, re-declared on `[data-theme]` so nested themes recompute — use
666
+ them with an inline fallback) and `--motion-*`
618
667
  (`styles/motion.css`: easing/duration) custom properties. DaisyUI theme
619
668
  variables feed INTO those tokens (as fallback-chained inputs), never
620
669
  appear directly in component styles for values the tokens cover.
@@ -0,0 +1,67 @@
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.
52
+
53
+ ### Wave 1 (jatui-owg2z) — ideas only, written fresh for Svelte 5
54
+
55
+ None of these files is a close port, so none carries the BoardUI notice. They
56
+ follow ideas from BoardUI's free items:
57
+
58
+ - `IconButton`, `CloseButton`, `LinkButton`: BoardUI icon-button, close-button
59
+ and link-button.
60
+ - `use:scrollEdge`: BoardUI's scroll-region progressive-blur guidance.
61
+ - Colour-intent tokens in `styles/tokens.css`: BoardUI's semantic theme vars,
62
+ reduced to a thin `color-mix` layer over DaisyUI.
63
+ - `PinInput` one-time-code autofill: BoardUI input-otp.
64
+ - `TextArea` `minRows`/`maxRows`: BoardUI textarea.
65
+ - `themeWipe` fixes and `ThemeToggle`/`ThemeSelector` `reveal`: BoardUI theme
66
+ toggle.
67
+ - `Breadcrumb` `maxItems` and icons: BoardUI breadcrumb.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * scrollEdge — progressive-blur fade on the edge of a scroll region.
3
+ *
4
+ * <div class="overflow-y-auto" use:scrollEdge>…</div> top edge
5
+ * <div class="overflow-y-auto" use:scrollEdge={{ edges: 'both' }}>…</div>
6
+ * <div use:scroller.action use:scrollEdge={{ edges: 'bottom' }}>…</div> chat (stick-to-bottom)
7
+ *
8
+ * When content has scrolled under an edge, that edge softens: a stationary,
9
+ * `pointer-events: none`, `aria-hidden` overlay with three layers —
10
+ *
11
+ * 1. a tall band of light backdrop blur (1px), masked to fade out,
12
+ * 2. a shorter band of heavier blur (4px), masked the same way,
13
+ * 3. a surface → transparent gradient.
14
+ *
15
+ * Each layer's opacity ramps 0 → 1 over the first `size` px of scroll
16
+ * distance from that edge, so at rest (scrollTop 0) nothing is drawn and
17
+ * the first line of content is never dimmed. Opacity goes on the LAYERS,
18
+ * never their parent: a translucent parent becomes a backdrop root and the
19
+ * blur would sample nothing.
20
+ *
21
+ * The overlay lives inside the scroller as a `position: sticky` strip with a
22
+ * negative margin, so it needs no positioned wrapper and takes no layout
23
+ * space. Keep headers OUTSIDE the scroller — the edge belongs to the scroll
24
+ * area, not the chrome above it. Opacity-only, so it is reduced-motion safe.
25
+ *
26
+ * Idea from BoardUI's scroll-region guidance (MIT); written fresh.
27
+ */
28
+ export type ScrollEdgeSide = 'top' | 'bottom' | 'both';
29
+ export interface ScrollEdgeOptions {
30
+ /** Which edge(s) fade. Default 'top'. Chat threads that stick to the bottom want 'bottom'. */
31
+ edges?: ScrollEdgeSide;
32
+ /** Height of the fade band and the scroll distance the ramp runs over, px. Default 24. */
33
+ size?: number;
34
+ /** Colour the gradient blends into. Default the `--jatui-surface` token (base-100). */
35
+ surface?: string;
36
+ /** Backdrop blur layers. false = gradient only (cheaper on huge lists). Default true. */
37
+ blur?: boolean;
38
+ /** Turn it off without unmounting the action. Default true. */
39
+ enabled?: boolean;
40
+ }
41
+ /** Opacity for an edge given how far content has scrolled past it. Pure. */
42
+ export declare function edgeOpacity(distance: number, size: number): number;
43
+ /** Scroll distance past the top and bottom edges of a scroller. Pure. */
44
+ export declare function edgeDistances(m: {
45
+ scrollTop: number;
46
+ scrollHeight: number;
47
+ clientHeight: number;
48
+ }): {
49
+ top: number;
50
+ bottom: number;
51
+ };
52
+ export declare function scrollEdge(node: HTMLElement, options?: ScrollEdgeOptions): {
53
+ update(next?: ScrollEdgeOptions): void;
54
+ destroy(): void;
55
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * scrollEdge — progressive-blur fade on the edge of a scroll region.
3
+ *
4
+ * <div class="overflow-y-auto" use:scrollEdge>…</div> top edge
5
+ * <div class="overflow-y-auto" use:scrollEdge={{ edges: 'both' }}>…</div>
6
+ * <div use:scroller.action use:scrollEdge={{ edges: 'bottom' }}>…</div> chat (stick-to-bottom)
7
+ *
8
+ * When content has scrolled under an edge, that edge softens: a stationary,
9
+ * `pointer-events: none`, `aria-hidden` overlay with three layers —
10
+ *
11
+ * 1. a tall band of light backdrop blur (1px), masked to fade out,
12
+ * 2. a shorter band of heavier blur (4px), masked the same way,
13
+ * 3. a surface → transparent gradient.
14
+ *
15
+ * Each layer's opacity ramps 0 → 1 over the first `size` px of scroll
16
+ * distance from that edge, so at rest (scrollTop 0) nothing is drawn and
17
+ * the first line of content is never dimmed. Opacity goes on the LAYERS,
18
+ * never their parent: a translucent parent becomes a backdrop root and the
19
+ * blur would sample nothing.
20
+ *
21
+ * The overlay lives inside the scroller as a `position: sticky` strip with a
22
+ * negative margin, so it needs no positioned wrapper and takes no layout
23
+ * space. Keep headers OUTSIDE the scroller — the edge belongs to the scroll
24
+ * area, not the chrome above it. Opacity-only, so it is reduced-motion safe.
25
+ *
26
+ * Idea from BoardUI's scroll-region guidance (MIT); written fresh.
27
+ */
28
+ /** Opacity for an edge given how far content has scrolled past it. Pure. */
29
+ export function edgeOpacity(distance, size) {
30
+ if (!(size > 0) || !Number.isFinite(distance))
31
+ return 0;
32
+ return Math.min(1, Math.max(0, distance / size));
33
+ }
34
+ /** Scroll distance past the top and bottom edges of a scroller. Pure. */
35
+ export function edgeDistances(m) {
36
+ return {
37
+ top: Math.max(0, m.scrollTop),
38
+ bottom: Math.max(0, m.scrollHeight - m.clientHeight - m.scrollTop)
39
+ };
40
+ }
41
+ const DEFAULT_SURFACE = 'var(--jatui-surface, var(--color-base-100, Canvas))';
42
+ function strip(side, o) {
43
+ const el = document.createElement('div');
44
+ el.setAttribute('aria-hidden', 'true');
45
+ el.dataset.scrollEdge = side;
46
+ const s = el.style;
47
+ s.position = 'sticky';
48
+ s.display = 'block';
49
+ s[side] = '0';
50
+ s.height = `${o.size}px`;
51
+ s[side === 'top' ? 'marginBottom' : 'marginTop'] = `-${o.size}px`;
52
+ s.pointerEvents = 'none';
53
+ s.zIndex = '1';
54
+ s.gridColumn = '1 / -1'; // a grid scroller must not give the strip its own cell
55
+ s.flexShrink = '0';
56
+ s.alignSelf = 'stretch';
57
+ const dir = side === 'top' ? 'to bottom' : 'to top';
58
+ const anchor = side === 'top' ? 'top: 0' : 'bottom: 0';
59
+ const layers = [];
60
+ if (o.blur) {
61
+ layers.push(`${anchor}; height: 100%; backdrop-filter: blur(1px); -webkit-backdrop-filter: blur(1px); mask-image: linear-gradient(${dir}, #000, transparent); -webkit-mask-image: linear-gradient(${dir}, #000, transparent)`, `${anchor}; height: 66%; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); mask-image: linear-gradient(${dir}, #000, transparent); -webkit-mask-image: linear-gradient(${dir}, #000, transparent)`);
62
+ }
63
+ layers.push(`${anchor}; height: 100%; background: linear-gradient(${dir}, ${o.surface}, transparent)`);
64
+ for (const css of layers) {
65
+ const layer = document.createElement('div');
66
+ layer.style.cssText = `position: absolute; left: 0; right: 0; ${css}; opacity: var(--jatui-scroll-edge-o, 0); transition: opacity var(--motion-instant, 80ms) linear;`;
67
+ el.appendChild(layer);
68
+ }
69
+ return el;
70
+ }
71
+ export function scrollEdge(node, options = {}) {
72
+ let opts = resolve(options);
73
+ let top = null;
74
+ let bottom = null;
75
+ let frame = 0;
76
+ let ro = null;
77
+ function resolve(o) {
78
+ return {
79
+ edges: o.edges ?? 'top',
80
+ size: o.size ?? 24,
81
+ surface: o.surface ?? DEFAULT_SURFACE,
82
+ blur: o.blur ?? true,
83
+ enabled: o.enabled ?? true
84
+ };
85
+ }
86
+ function measure() {
87
+ frame = 0;
88
+ const d = edgeDistances(node);
89
+ top?.style.setProperty('--jatui-scroll-edge-o', String(edgeOpacity(d.top, opts.size)));
90
+ bottom?.style.setProperty('--jatui-scroll-edge-o', String(edgeOpacity(d.bottom, opts.size)));
91
+ }
92
+ function schedule() {
93
+ if (frame)
94
+ return;
95
+ frame = requestAnimationFrame(measure);
96
+ }
97
+ function mount() {
98
+ unmount();
99
+ if (!opts.enabled)
100
+ return;
101
+ if (opts.edges !== 'bottom') {
102
+ top = strip('top', opts);
103
+ node.prepend(top);
104
+ }
105
+ if (opts.edges !== 'top') {
106
+ bottom = strip('bottom', opts);
107
+ node.append(bottom);
108
+ }
109
+ measure();
110
+ }
111
+ function unmount() {
112
+ top?.remove();
113
+ bottom?.remove();
114
+ top = bottom = null;
115
+ }
116
+ node.addEventListener('scroll', schedule, { passive: true });
117
+ if (typeof ResizeObserver !== 'undefined') {
118
+ ro = new ResizeObserver(schedule);
119
+ ro.observe(node);
120
+ }
121
+ // Content growth changes the bottom distance without a scroll event.
122
+ const mo = typeof MutationObserver !== 'undefined' ? new MutationObserver(schedule) : null;
123
+ mo?.observe(node, { childList: true, subtree: true, characterData: true });
124
+ mount();
125
+ return {
126
+ update(next = {}) {
127
+ const prev = opts;
128
+ opts = resolve(next);
129
+ const same = Object.keys(opts).every((k) => opts[k] === prev[k]);
130
+ if (same)
131
+ schedule();
132
+ else
133
+ mount();
134
+ },
135
+ destroy() {
136
+ node.removeEventListener('scroll', schedule);
137
+ ro?.disconnect();
138
+ mo?.disconnect();
139
+ if (frame)
140
+ cancelAnimationFrame(frame);
141
+ unmount();
142
+ }
143
+ };
144
+ }
@@ -6,7 +6,7 @@ declare const DiagramBlock: import("svelte").Component<BlockProps<{
6
6
  id: string;
7
7
  label: string;
8
8
  sublabel?: string | undefined;
9
- kind?: "data" | "input" | "output" | "note" | "default" | "decision" | "external" | "terminal" | "process" | "subroutine" | undefined;
9
+ kind?: "data" | "input" | "output" | "note" | "default" | "external" | "decision" | "terminal" | "process" | "subroutine" | undefined;
10
10
  tone?: "error" | "warning" | "info" | "success" | "primary" | "secondary" | "neutral" | "accent" | undefined;
11
11
  shape?: "circle" | "rect" | "stadium" | "pill" | "diamond" | undefined;
12
12
  }[];
@@ -39,8 +39,8 @@ export declare const diagramParamsSchema: z.ZodObject<{
39
39
  output: "output";
40
40
  note: "note";
41
41
  default: "default";
42
- decision: "decision";
43
42
  external: "external";
43
+ decision: "decision";
44
44
  terminal: "terminal";
45
45
  process: "process";
46
46
  subroutine: "subroutine";
@@ -26,6 +26,8 @@
26
26
  settles on the snap nearest the post-drag height.
27
27
  -->
28
28
  <script lang="ts">
29
+ import CloseButton from './CloseButton.svelte';
30
+ import { scrollEdge as scrollEdgeAction } from '../actions/scrollEdge';
29
31
  import { onMount, untrack } from 'svelte';
30
32
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
31
33
  import type { Snippet } from 'svelte';
@@ -49,6 +51,7 @@
49
51
  onclose = () => {},
50
52
  shortcutRegistry,
51
53
  shortcutContext = 'bottom-sheet',
54
+ scrollEdge = true,
52
55
  children
53
56
  }: {
54
57
  open?: boolean;
@@ -70,6 +73,8 @@
70
73
  shortcutRegistry?: ShortcutRegistry;
71
74
  /** Context name pushed while open. Default `'bottom-sheet'`. */
72
75
  shortcutContext?: string;
76
+ /** Soft progressive-blur fade at the top of the content once it scrolls (`use:scrollEdge`). Ignored with `fullHeight` (the slot owns scrolling). Default true. */
77
+ scrollEdge?: boolean;
73
78
  children?: Snippet;
74
79
  } = $props();
75
80
 
@@ -254,12 +259,7 @@
254
259
  {#if title}
255
260
  <div class="flex-shrink-0 flex items-center justify-between px-4 py-2 border-b border-base-200">
256
261
  <span class="text-sm font-semibold">{title}</span>
257
- <button
258
- type="button"
259
- class="btn btn-sm btn-ghost btn-circle"
260
- aria-label="Close"
261
- onclick={dismiss}
262
- >✕</button>
262
+ <CloseButton label="Close" onclick={dismiss} />
263
263
  </div>
264
264
  {/if}
265
265
 
@@ -273,6 +273,7 @@
273
273
  : keyboardInset > 0
274
274
  ? `padding-bottom: ${keyboardInset}px;`
275
275
  : ''}
276
+ use:scrollEdgeAction={{ enabled: scrollEdge && !(fullHeight && !isDual) }}
276
277
  >
277
278
  {@render children?.()}
278
279
  </div>
@@ -20,6 +20,8 @@ type $$ComponentProps = {
20
20
  shortcutRegistry?: ShortcutRegistry;
21
21
  /** Context name pushed while open. Default `'bottom-sheet'`. */
22
22
  shortcutContext?: string;
23
+ /** Soft progressive-blur fade at the top of the content once it scrolls (`use:scrollEdge`). Ignored with `fullHeight` (the slot owns scrolling). Default true. */
24
+ scrollEdge?: boolean;
23
25
  children?: Snippet;
24
26
  };
25
27
  declare const BottomSheet: import("svelte").Component<$$ComponentProps, {}, "open">;
@@ -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>;