@joewinke/jatui 0.39.0 → 0.41.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.
- package/CLAUDE.md +52 -5
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/AnimatedNumber.svelte +42 -1
- package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SegmentedControl.svelte +263 -0
- package/dist/components/SegmentedControl.svelte.d.ts +48 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/settings/SettingsRow.svelte +177 -0
- package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
- package/dist/components/settings/SettingsSection.svelte +139 -0
- package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
- package/dist/components/stats/ActivityHeatmap.svelte +412 -0
- package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
- package/dist/components/stats/BarList.svelte +262 -0
- package/dist/components/stats/BarList.svelte.d.ts +57 -0
- package/dist/components/stats/ChartCard.svelte +230 -0
- package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
- package/dist/components/stats/StatCard.svelte +387 -0
- package/dist/components/stats/StatCard.svelte.d.ts +37 -0
- package/dist/components/stats/StatGroup.svelte +67 -0
- package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
- package/dist/components/stats/heatmap.d.ts +76 -0
- package/dist/components/stats/heatmap.js +151 -0
- package/dist/components/tree/TreeView.svelte +680 -0
- package/dist/components/tree/TreeView.svelte.d.ts +123 -0
- package/dist/components/tree/flattenTree.d.ts +57 -0
- package/dist/components/tree/flattenTree.js +91 -0
- package/dist/components/tree/treeKeys.d.ts +46 -0
- package/dist/components/tree/treeKeys.js +80 -0
- package/dist/data/grid/cells.d.ts +40 -0
- package/dist/data/grid/cells.js +89 -0
- package/dist/data/grid/clipboard.d.ts +46 -0
- package/dist/data/grid/clipboard.js +131 -0
- package/dist/data/grid/edits.d.ts +148 -0
- package/dist/data/grid/edits.js +222 -0
- package/dist/data/grid/index.d.ts +12 -0
- package/dist/data/grid/index.js +12 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +31 -1
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- package/package.json +5 -1
package/CLAUDE.md
CHANGED
|
@@ -22,9 +22,11 @@ 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
|
+
| `SegmentedControl` | Single-select toggle group that SETS A VALUE (Tabs switch panels): `role="radiogroup"` + `role="radio"`, roving tabindex, arrows move AND select, Home/End, disabled options skipped; sliding thumb on motion tokens (jumps under reduced motion). `solid` (track + raised thumb) or `plain`. Options may be plain strings. | `options: (SegmentedOption \| string)[]`, `value` (bindable), `size?` (`xs\|sm\|md`), `variant?`, `fullWidth?`, `disabled?`, `ariaLabel?`, `option?` (snippet), `onChange?` |
|
|
27
28
|
| `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` |
|
|
29
|
+
| `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
30
|
| `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
|
|
29
31
|
| `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
32
|
| `FilterDropdown` | Multi-select filter dropdown | `label`, `options: string[]`, `selected: Set<string>` (bindable) |
|
|
@@ -56,13 +58,22 @@ Then import:
|
|
|
56
58
|
| Component | Purpose | Key Props |
|
|
57
59
|
|-----------|---------|-----------|
|
|
58
60
|
| `Button` | Styled button with variants | `variant`, `size`, `loading`, `disabled`, `onclick` |
|
|
61
|
+
| `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) |
|
|
62
|
+
| `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` |
|
|
63
|
+
| `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
64
|
| `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
65
|
| `Badge` | Status/label badge | `variant`, `size`, `children` (snippet) |
|
|
66
|
+
| `AnimatedNumber` | Number that animates to its new value. `mode='roll'` (default) is the per-digit odometer; `mode='tween'` counts from the previous value (`svelte/motion` Tween, `--motion-move` duration scaled by `data-motion`, instant under reduced motion; screen readers get the final value only). | `value`, `format?: Intl.NumberFormatOptions`, `mode?`, `duration?` (ms) |
|
|
67
|
+
| `StatCard` | KPI tile: label, a value that tweens to its new number, delta chip toned by sign (success/error/neutral tokens; `invertDelta` for metrics where down is good), caption, icon tile (`tone`), info tooltip, inline SVG sparkline (no d3). `variant='footer'` moves caption + delta into a footer band; `href` makes the card a link; `loading` skeleton. A numeric `delta` is a FRACTION (0.125 → "+12.5%"). | `label`, `value`, `format?`, `animate?` (`tween\|roll\|false`), `delta?`, `deltaFormat?`, `deltaTone?`, `invertDelta?`, `caption?`, `info?`, `sparkline?: number[]`, `variant?`, `tone?`, `size?`, `href?`, `loading?`, `icon?`/`children?` (snippets) |
|
|
68
|
+
| `StatGroup` | Lays out StatCards: `cards` (responsive auto-fit grid) or `joined` (one bordered panel with hairline dividers) — the replacement for raw DaisyUI `stats` markup. | `columns?`, `minWidth?` ('12rem'), `variant?`, `ariaLabel?`, `children` |
|
|
69
|
+
| `ChartCard` | "Headline + period switch + chart" shell that draws NO chart (bring layerchart / d3 / SVG). The `chart` snippet gets `{ period, hover, hovered }`; call `hover({value, label, delta?})` as the pointer moves and the headline tweens to that point and the caption shows its label; `hover(null)` (or pointer leave) restores the total. Periods use SegmentedControl. | `label`, `value`, `format?`, `delta?`, `invertDelta?`, `caption?`, `periods?`, `period` (bindable), `onPeriodChange?`, `chartHeight?`, `loading?`, `chart` (snippet), `actions?`/`footer?` (snippets) |
|
|
70
|
+
| `BarList` | Ranked horizontal bars ("top pages / top callers"): label + value as text, a decorative token-tinted bar sized against the max, optional `href` or `onSelect` per row, `hint`, per-row `tone`, `limit` + "Show N more". Pure HTML/CSS, no chart lib. | `items: BarListItem[]`, `sort?` (`desc`), `limit?`, `format?`, `formatValue?(item, total)`, `labelHeader?`/`valueHeader?`, `tone?`, `size?`, `onSelect?`, `label?` (snippet) |
|
|
71
|
+
| `ActivityHeatmap` | GitHub-style activity-per-day calendar (calls/day, tasks closed/day): 7 weekday rows × week columns, default range = last 12 months, `weekStart`, 4-step `color-mix` ramp from one theme `tone` (no hex), one floating tooltip, legend + summary ("1,204 calls in the last year"). `role="grid"` with ONE tab stop: arrows move by day (↑↓) / week (←→), PgUp/PgDn 4 weeks, Home/End row ends, mod+Home/End range ends, Enter/Space select. Pure layout helpers exported (`buildHeatmap`, `moveHeatFocus`). | `data` (`{date, count}[]` or `getDate`/`getCount`), `start?`/`end?`, `weekStart?`, `unit?` ('call' or ['call','calls']), `formatLabel?`, `tone?`, `levels?`, `levelOf?`, `compact?`, `showLegend?`/`showWeekdays?`/`showMonths?`, `summary?`, `selected` (bindable), `onSelectDay?(date, count)`, `locale?` |
|
|
61
72
|
| `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
|
|
62
73
|
| `Card` | Content card container | `title`, `children` (snippet) |
|
|
63
74
|
| `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
75
|
| `ConfirmModal` | Yes/no confirmation dialog | `open` (bindable), `title`, `message`, `onConfirm`, `onCancel` |
|
|
65
|
-
| `Drawer` | Slide-out side panel | `open` (bindable), `side`, `children` (snippet) |
|
|
76
|
+
| `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
77
|
| `LoadingSpinner` | Spinner animation | `size`, `color` |
|
|
67
78
|
| `Sparkline` | Inline mini chart | `data: number[]`, `width`, `height`, `color` |
|
|
68
79
|
| `LazyImage` | Image with lazy loading + placeholder | `src`, `alt`, `placeholder` |
|
|
@@ -215,16 +226,40 @@ import type {
|
|
|
215
226
|
import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui/meet'
|
|
216
227
|
```
|
|
217
228
|
|
|
229
|
+
### Grid helpers (subpath: `jatui/grid`, also on the root barrel)
|
|
230
|
+
|
|
231
|
+
Pure, dependency-free helpers for spreadsheet-style editing (MOS tables, IDE tables, a future DataTable editable mode):
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import {
|
|
235
|
+
parseClipboardTsv, serializeTsv, // Excel/Sheets clipboard: quoted tabs/newlines, "" escapes, CRLF, ragged rows
|
|
236
|
+
serializeCsv, serializeRowsToCsv, // RFC 4180 + BOM + formula-injection escaping ('=…', '+…', '-…', '@…' strings get a leading ')
|
|
237
|
+
escapeCsvFormula,
|
|
238
|
+
columnLetter, columnIndex, parseCellRef, // 0 ↔ "A", 26 ↔ "AA", "B3" → {col:1,row:2}
|
|
239
|
+
parseCellValue, // typed parse + human error: number ($1,234.50, 12%, (7)), select, boolean, text; min/max/required
|
|
240
|
+
applyEdits, diffEdits, deleteRows, planPaste,
|
|
241
|
+
createEditHistory // diff-based undo/redo (limit 100) with a STALE GUARD: if the data changed underneath, history clears instead of reverting someone else's change
|
|
242
|
+
} from 'jatui/grid'
|
|
243
|
+
// const h = createEditHistory({ getRowId: (r) => r.id })
|
|
244
|
+
// rows = h.commit(rows, diffEdits(rows, planPaste(parseClipboardTsv(text), target).values, { getRowId }))
|
|
245
|
+
// const step = h.undo(rows); if (step.ok) rows = step.data // step.reason: 'empty' | 'stale'
|
|
246
|
+
```
|
|
247
|
+
|
|
218
248
|
### Layout
|
|
219
249
|
|
|
220
250
|
| Component | Purpose | Key Props |
|
|
221
251
|
|-----------|---------|-----------|
|
|
222
252
|
| `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) |
|
|
253
|
+
| `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?` |
|
|
223
254
|
| `ResizablePanel` | Resizable container | `minWidth`, `maxWidth`, `defaultWidth` |
|
|
224
255
|
| `ResizableDivider` | Drag handle between panels | `onResize` |
|
|
225
256
|
| `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?` |
|
|
226
257
|
| `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) |
|
|
227
258
|
| `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) |
|
|
259
|
+
| `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?` |
|
|
260
|
+
| `TreeView` | Data-driven tree (`nodes` + `getId`/`getChildren`/`getLabel`, defaults `id`/`children`/`label ?? name`). Renders `flattenTree`'s FLAT row list as `role="tree"` / `role="treeitem"` with `aria-level`/`posinset`/`setsize`/`expanded`/`selected`, so it windows with `jatui/virtual` (`virtual={{ createRowWindow }}`). Connector guides (trunks only while an ancestor has a later sibling, elbow, tail, bridge), full APG keys (↑↓, → open then step INTO the first child, ← close then parent, Home/End, `*` opens siblings, Enter = `onAction`, Space selects, typeahead, mod+A in multi), `selectionMode` none/single/multiple (Ctrl toggle, Shift range), lazy folders via `hasChildren` + `onExpand`, leaving rows go `inert` during the collapse animation (off under reduced motion / while windowed). `bind:this` → `expandAll()`, `collapseAll()`, `reveal(id)`, `focus(id?)`, `visibleIds()`. Pure helpers: `flattenTree`, `collectBranchIds`, `pathToNode`, `resolveTreeKey`, `treeTypeahead`. | `nodes`, `expanded` (bindable Set), `selected` (bindable Set), `selectionMode?`, `size?`, `showGuides?`, `icons?`, `expandOnClick?`, `animate?`, `maxHeight?`, `virtual?`, `isDisabled?`, `hasChildren?`, `label?`/`icon?`/`trailing?`/`empty?` (snippets), `onExpand?`, `onAction?`, `onRowContextMenu?`, `onSelectionChange?` |
|
|
261
|
+
| `SettingsSection` | Titled group of SettingsRows with hairline dividers; `card` (framed) or `plain`; header `actions`, `footer`, amber `dirty` dot (matches SectionNav `dirtyIds`); `id` makes it a SectionNav target. | `title?`, `description?`, `id?`, `variant?`, `headingLevel?`, `dirty?`, `actions?`/`footer?`/`children` (snippets) |
|
|
262
|
+
| `SettingsRow` | Label + description on the start side, control on the end; stacks when its container is narrow (or `layout='stacked'`). Wires ids for you: the `control` snippet gets `{ id, labelId, descriptionId }` — put `id` on the input and `descriptionId` on `aria-describedby`. `error`, `dirty`, `disabled`, children render under the row. | `label`, `description?`, `for?`, `layout?`, `dirty?`, `disabled?`, `error?`, `id?`, `control?` (snippet), `children?`, `labelContent?` |
|
|
228
263
|
|
|
229
264
|
**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`.
|
|
230
265
|
|
|
@@ -363,8 +398,8 @@ const callbacks: AnnotationCallbacks = {
|
|
|
363
398
|
|
|
364
399
|
| Component | Purpose | Key Props |
|
|
365
400
|
|-----------|---------|-----------|
|
|
366
|
-
| `ThemeSelector` | DaisyUI theme switcher with preview dots | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange` |
|
|
367
|
-
| `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` |
|
|
401
|
+
| `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'` |
|
|
402
|
+
| `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'` |
|
|
368
403
|
|
|
369
404
|
### Keyboard Navigation
|
|
370
405
|
|
|
@@ -526,6 +561,12 @@ import { groupRows, groupPathKey, type GroupLevel, type GroupedItem } from "jatu
|
|
|
526
561
|
// groupRows(rows, [{ key: t => t.project }, { key: t => t.epic }], { getRowId, collapsed, label? })
|
|
527
562
|
// → { kind:'group', key, path, depth, label, rows, count, collapsed } | { kind:'row', row, id, depth, groupPath, position }
|
|
528
563
|
|
|
564
|
+
// Theme change reveal — circular View Transition from the click (or, for a
|
|
565
|
+
// keyboard activation, the control's centre). Single-flight, level-scaled
|
|
566
|
+
// duration, reduced-motion = instant. ThemeToggle/ThemeSelector `reveal` use it.
|
|
567
|
+
import { themeWipe } from "jatui"
|
|
568
|
+
// themeWipe(event, () => setTheme(next), { edge: 'soft' }) // edge: 'hard' (default) | 'soft'
|
|
569
|
+
|
|
529
570
|
// Date formatting
|
|
530
571
|
import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
|
|
531
572
|
|
|
@@ -645,7 +686,13 @@ Three rules keep DaisyUI swappable instead of load-bearing:
|
|
|
645
686
|
internal skin, like vendored CSS — never part of a component's public
|
|
646
687
|
props/API.
|
|
647
688
|
3. **jatui owns the token layer.** Components consume `--jatui-*`
|
|
648
|
-
(`styles/tokens.css`: radius/spacing/border
|
|
689
|
+
(`styles/tokens.css`: radius/spacing/border, plus the colour-intent layer —
|
|
690
|
+
`--jatui-focus-ring{,-width,-offset}`, `--jatui-surface`, `--jatui-hover-bg`,
|
|
691
|
+
`--jatui-pressed-bg`, `--jatui-separator`, `--jatui-selection-{bg,edge,solid,solid-fg}`,
|
|
692
|
+
`--jatui-chart-{1..5,grid,axis}`, `--jatui-event-{1..6}{,-bg,-fg}`,
|
|
693
|
+
`--jatui-status-{positive,caution,negative,neutral}`; all `color-mix` over DaisyUI
|
|
694
|
+
semantic vars, re-declared on `[data-theme]` so nested themes recompute — use
|
|
695
|
+
them with an inline fallback) and `--motion-*`
|
|
649
696
|
(`styles/motion.css`: easing/duration) custom properties. DaisyUI theme
|
|
650
697
|
variables feed INTO those tokens (as fallback-chained inputs), never
|
|
651
698
|
appear directly in component styles for values the tokens cover.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -49,3 +49,34 @@ Files adapted from BoardUI free components:
|
|
|
49
49
|
BoardUI Pro items are not covered by the MIT licence. `AgentTaskList`,
|
|
50
50
|
`Questionnaire` and `ContextMeter` were built from public behaviour only; no Pro
|
|
51
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.
|
|
68
|
+
|
|
69
|
+
### Wave 3 (jatui-6kitr) — re-implemented for Svelte 5 after BoardUI free components
|
|
70
|
+
|
|
71
|
+
- `src/lib/components/tree/TreeView.svelte`, `flattenTree.ts`, `treeKeys.ts` — from
|
|
72
|
+
`tree-view` (connector-guide rule, ArrowRight steps into the first child).
|
|
73
|
+
- `src/lib/data/grid/` — from `data-grid` utils (TSV clipboard round-trip, CSV
|
|
74
|
+
formula-injection escaping, stale-guarded edit history).
|
|
75
|
+
- `src/lib/components/stats/StatCard.svelte` — from `stat-cards` (plain/footer looks).
|
|
76
|
+
- `src/lib/components/SegmentedControl.svelte` — from `segmented-control`
|
|
77
|
+
(solid/plain variants).
|
|
78
|
+
- `src/lib/components/settings/` — the row recipe from `settings-rows`.
|
|
79
|
+
|
|
80
|
+
`BarList` and `ActivityHeatmap` follow BoardUI Pro ideas built from public
|
|
81
|
+
behaviour only; no Pro source was read or copied. No BoardUI images, logos or
|
|
82
|
+
palettes are included.
|
|
@@ -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" | "
|
|
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";
|
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { Tween } from 'svelte/motion';
|
|
4
|
+
import { cubicOut } from 'svelte/easing';
|
|
5
|
+
import { getMotionContext, motionDuration } from '../motion/level';
|
|
6
|
+
|
|
2
7
|
interface Props {
|
|
3
8
|
value: number;
|
|
4
9
|
format?: Intl.NumberFormatOptions;
|
|
10
|
+
/**
|
|
11
|
+
* 'roll' (default): per-digit odometer. 'tween': counts from the previous
|
|
12
|
+
* value to the new one (dashboards, a headline driven by chart hover).
|
|
13
|
+
*/
|
|
14
|
+
mode?: 'roll' | 'tween';
|
|
15
|
+
/** Tween duration in ms at the standard motion level. Default: the --motion-move token. */
|
|
16
|
+
duration?: number;
|
|
5
17
|
class?: string;
|
|
6
18
|
}
|
|
7
19
|
|
|
8
|
-
let { value, format = {}, class: className = '' }: Props = $props();
|
|
20
|
+
let { value, format = {}, mode = 'roll', duration, class: className = '' }: Props = $props();
|
|
21
|
+
|
|
22
|
+
// ── Tween mode ─────────────────────────────────────────────────────────
|
|
23
|
+
let tweenEl: HTMLSpanElement | undefined = $state();
|
|
24
|
+
const tween = new Tween(untrack(() => value), { easing: cubicOut, duration: 0 });
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (mode !== 'tween') return;
|
|
27
|
+
const target = value;
|
|
28
|
+
// Level-aware (data-motion) and instant under prefers-reduced-motion.
|
|
29
|
+
const ms = motionDuration(duration ?? 'move', getMotionContext(tweenEl ?? null));
|
|
30
|
+
void tween.set(target, { duration: ms });
|
|
31
|
+
});
|
|
32
|
+
/** Decimals to show mid-tween: the target's own, unless `format` decides. */
|
|
33
|
+
const tweenDecimals = $derived.by(() => {
|
|
34
|
+
if (format.maximumFractionDigits != null) return null;
|
|
35
|
+
const s = String(value);
|
|
36
|
+
return s.includes('.') && !s.includes('e') ? s.split('.')[1].length : 0;
|
|
37
|
+
});
|
|
38
|
+
const tweenText = $derived.by(() => {
|
|
39
|
+
const v = tween.current;
|
|
40
|
+
const shown = tweenDecimals == null ? v : Number(v.toFixed(tweenDecimals));
|
|
41
|
+
return formatNumber(shown, format);
|
|
42
|
+
});
|
|
9
43
|
|
|
10
44
|
type Digit = { prev: string; next: string; animating: boolean; id: number };
|
|
11
45
|
|
|
@@ -54,6 +88,12 @@
|
|
|
54
88
|
});
|
|
55
89
|
</script>
|
|
56
90
|
|
|
91
|
+
{#if mode === 'tween'}
|
|
92
|
+
<span
|
|
93
|
+
class="animated-number inline-flex tabular-nums {className}"
|
|
94
|
+
bind:this={tweenEl}
|
|
95
|
+
><span class="sr-only">{formatNumber(value, format)}</span><span aria-hidden="true">{tweenText}</span></span>
|
|
96
|
+
{:else}
|
|
57
97
|
<span
|
|
58
98
|
class="animated-number inline-flex tabular-nums {className}"
|
|
59
99
|
aria-live="polite"
|
|
@@ -80,6 +120,7 @@
|
|
|
80
120
|
{/if}
|
|
81
121
|
{/each}
|
|
82
122
|
</span>
|
|
123
|
+
{/if}
|
|
83
124
|
|
|
84
125
|
<style>
|
|
85
126
|
.digit-inner {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: number;
|
|
3
3
|
format?: Intl.NumberFormatOptions;
|
|
4
|
+
/**
|
|
5
|
+
* 'roll' (default): per-digit odometer. 'tween': counts from the previous
|
|
6
|
+
* value to the new one (dashboards, a headline driven by chart hover).
|
|
7
|
+
*/
|
|
8
|
+
mode?: 'roll' | 'tween';
|
|
9
|
+
/** Tween duration in ms at the standard motion level. Default: the --motion-move token. */
|
|
10
|
+
duration?: number;
|
|
4
11
|
class?: string;
|
|
5
12
|
}
|
|
6
13
|
declare const AnimatedNumber: import("svelte").Component<Props, {}, "">;
|
|
@@ -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
|
-
<
|
|
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">;
|
|
@@ -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;
|