@joewinke/jatui 0.40.0 → 0.41.1
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 +29 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/dist/components/AnimatedNumber.svelte +60 -1
- package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
- package/dist/components/SegmentedControl.svelte +263 -0
- package/dist/components/SegmentedControl.svelte.d.ts +48 -0
- 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 +22 -0
- package/dist/index.js +21 -0
- package/dist/showcase/Globe.svelte +1 -1
- package/package.json +5 -1
package/CLAUDE.md
CHANGED
|
@@ -24,6 +24,7 @@ Then import:
|
|
|
24
24
|
| `TextInput` | Text input with label, error, icon slots | `label`, `value` (bindable), `placeholder`, `error`, `disabled` |
|
|
25
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` |
|
|
28
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` |
|
|
29
30
|
| `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
|
|
@@ -62,6 +63,12 @@ Then import:
|
|
|
62
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` |
|
|
63
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` |
|
|
64
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?` |
|
|
65
72
|
| `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
|
|
66
73
|
| `Card` | Content card container | `title`, `children` (snippet) |
|
|
67
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?` |
|
|
@@ -219,6 +226,25 @@ import type {
|
|
|
219
226
|
import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui/meet'
|
|
220
227
|
```
|
|
221
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
|
+
|
|
222
248
|
### Layout
|
|
223
249
|
|
|
224
250
|
| Component | Purpose | Key Props |
|
|
@@ -231,6 +257,9 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
|
|
|
231
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) |
|
|
232
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) |
|
|
233
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?` |
|
|
234
263
|
|
|
235
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`.
|
|
236
265
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -65,3 +65,18 @@ follow ideas from BoardUI's free items:
|
|
|
65
65
|
- `themeWipe` fixes and `ThemeToggle`/`ThemeSelector` `reveal`: BoardUI theme
|
|
66
66
|
toggle.
|
|
67
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.
|
|
@@ -1,11 +1,63 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { cubicOut } from 'svelte/easing';
|
|
4
|
+
import { getMotionContext, motionDuration } from '../motion/level';
|
|
5
|
+
|
|
2
6
|
interface Props {
|
|
3
7
|
value: number;
|
|
4
8
|
format?: Intl.NumberFormatOptions;
|
|
9
|
+
/**
|
|
10
|
+
* 'roll' (default): per-digit odometer. 'tween': counts from the previous
|
|
11
|
+
* value to the new one (dashboards, a headline driven by chart hover).
|
|
12
|
+
*/
|
|
13
|
+
mode?: 'roll' | 'tween';
|
|
14
|
+
/** Tween duration in ms at the standard motion level. Default: the --motion-move token. */
|
|
15
|
+
duration?: number;
|
|
5
16
|
class?: string;
|
|
6
17
|
}
|
|
7
18
|
|
|
8
|
-
let { value, format = {}, class: className = '' }: Props = $props();
|
|
19
|
+
let { value, format = {}, mode = 'roll', duration, class: className = '' }: Props = $props();
|
|
20
|
+
|
|
21
|
+
// ── Tween mode ─────────────────────────────────────────────────────────
|
|
22
|
+
let tweenEl: HTMLSpanElement | undefined = $state();
|
|
23
|
+
// A hand-rolled rAF tween, NOT svelte/motion's Tween: importing svelte/motion
|
|
24
|
+
// creates a prefers-reduced-motion MediaQuery at MODULE load, which throws
|
|
25
|
+
// 'window.matchMedia is not a function' in any jsdom test that merely
|
|
26
|
+
// imports 'jatui' (jatui-oozkb). Reduced motion comes from motionDuration,
|
|
27
|
+
// which is 0 under prefers-reduced-motion.
|
|
28
|
+
let tweenCurrent = $state(untrack(() => value));
|
|
29
|
+
$effect(() => {
|
|
30
|
+
if (mode !== 'tween') return;
|
|
31
|
+
const target = value;
|
|
32
|
+
// Level-aware (data-motion) and instant under prefers-reduced-motion.
|
|
33
|
+
const ms = motionDuration(duration ?? 'move', getMotionContext(tweenEl ?? null));
|
|
34
|
+
const from = untrack(() => tweenCurrent);
|
|
35
|
+
if (ms <= 0 || from === target || typeof requestAnimationFrame !== 'function') {
|
|
36
|
+
tweenCurrent = target;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let frame = 0;
|
|
40
|
+
let start: number | null = null;
|
|
41
|
+
const step = (now: number) => {
|
|
42
|
+
start ??= now;
|
|
43
|
+
const t = Math.min(1, (now - start) / ms);
|
|
44
|
+
tweenCurrent = from + (target - from) * cubicOut(t);
|
|
45
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
46
|
+
};
|
|
47
|
+
frame = requestAnimationFrame(step);
|
|
48
|
+
return () => cancelAnimationFrame(frame);
|
|
49
|
+
});
|
|
50
|
+
/** Decimals to show mid-tween: the target's own, unless `format` decides. */
|
|
51
|
+
const tweenDecimals = $derived.by(() => {
|
|
52
|
+
if (format.maximumFractionDigits != null) return null;
|
|
53
|
+
const s = String(value);
|
|
54
|
+
return s.includes('.') && !s.includes('e') ? s.split('.')[1].length : 0;
|
|
55
|
+
});
|
|
56
|
+
const tweenText = $derived.by(() => {
|
|
57
|
+
const v = tweenCurrent;
|
|
58
|
+
const shown = tweenDecimals == null ? v : Number(v.toFixed(tweenDecimals));
|
|
59
|
+
return formatNumber(shown, format);
|
|
60
|
+
});
|
|
9
61
|
|
|
10
62
|
type Digit = { prev: string; next: string; animating: boolean; id: number };
|
|
11
63
|
|
|
@@ -54,6 +106,12 @@
|
|
|
54
106
|
});
|
|
55
107
|
</script>
|
|
56
108
|
|
|
109
|
+
{#if mode === 'tween'}
|
|
110
|
+
<span
|
|
111
|
+
class="animated-number inline-flex tabular-nums {className}"
|
|
112
|
+
bind:this={tweenEl}
|
|
113
|
+
><span class="sr-only">{formatNumber(value, format)}</span><span aria-hidden="true">{tweenText}</span></span>
|
|
114
|
+
{:else}
|
|
57
115
|
<span
|
|
58
116
|
class="animated-number inline-flex tabular-nums {className}"
|
|
59
117
|
aria-live="polite"
|
|
@@ -80,6 +138,7 @@
|
|
|
80
138
|
{/if}
|
|
81
139
|
{/each}
|
|
82
140
|
</span>
|
|
141
|
+
{/if}
|
|
83
142
|
|
|
84
143
|
<style>
|
|
85
144
|
.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, {}, "">;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SegmentedControl: single-select toggle group that SETS A VALUE
|
|
3
|
+
(jatui-6kitr). Use Tabs to switch panels; use this for "Week / Month /
|
|
4
|
+
Year", view modes and filter scopes.
|
|
5
|
+
|
|
6
|
+
Radio semantics (role="radiogroup" + role="radio"), roving tabindex, arrow
|
|
7
|
+
keys move AND select (as native radios do), Home/End. A sliding thumb marks
|
|
8
|
+
the selection on motion tokens; it jumps under reduced motion. Variants
|
|
9
|
+
after BoardUI's segmented control (MIT, © 2026 Mertcan Dundar Esmergul):
|
|
10
|
+
`solid` (track + raised thumb) and `plain` (tinted thumb, no track).
|
|
11
|
+
-->
|
|
12
|
+
<script lang="ts" module>
|
|
13
|
+
export interface SegmentedOption<V extends string | number = string> {
|
|
14
|
+
value: V;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Accessible name when `label` is visually hidden by an `option` snippet. */
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts" generics="V extends string | number = string">
|
|
23
|
+
import type { Snippet } from 'svelte';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Options, or plain strings (value = label). */
|
|
27
|
+
options: readonly (SegmentedOption<V> | V)[];
|
|
28
|
+
value?: V;
|
|
29
|
+
size?: 'xs' | 'sm' | 'md';
|
|
30
|
+
variant?: 'solid' | 'plain';
|
|
31
|
+
/** Stretch to the container, segments share the width. */
|
|
32
|
+
fullWidth?: boolean;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
ariaLabel?: string;
|
|
35
|
+
/** Custom segment content (icon + label, counts…). */
|
|
36
|
+
option?: Snippet<[SegmentedOption<V>, { selected: boolean }]>;
|
|
37
|
+
onChange?: (value: V) => void;
|
|
38
|
+
class?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
options,
|
|
43
|
+
value = $bindable(),
|
|
44
|
+
size = 'sm',
|
|
45
|
+
variant = 'solid',
|
|
46
|
+
fullWidth = false,
|
|
47
|
+
disabled = false,
|
|
48
|
+
ariaLabel,
|
|
49
|
+
option,
|
|
50
|
+
onChange,
|
|
51
|
+
class: className = ''
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
const items = $derived(
|
|
55
|
+
options.map((o): SegmentedOption<V> =>
|
|
56
|
+
typeof o === 'object' ? o : { value: o, label: String(o) }
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
const selectedIndex = $derived(items.findIndex((o) => o.value === value));
|
|
60
|
+
const tabIndexOf = $derived(selectedIndex >= 0 ? selectedIndex : items.findIndex((o) => !o.disabled));
|
|
61
|
+
|
|
62
|
+
let trackEl: HTMLDivElement | undefined = $state();
|
|
63
|
+
const buttons: HTMLButtonElement[] = $state([]);
|
|
64
|
+
let thumb = $state<{ x: number; w: number } | null>(null);
|
|
65
|
+
let ready = $state(false);
|
|
66
|
+
|
|
67
|
+
function measure() {
|
|
68
|
+
const b = buttons[selectedIndex];
|
|
69
|
+
if (!b || !trackEl) {
|
|
70
|
+
thumb = null;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
thumb = { x: b.offsetLeft, w: b.offsetWidth };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
void selectedIndex;
|
|
78
|
+
void items.length;
|
|
79
|
+
measure();
|
|
80
|
+
// Enable the slide only after the first placement (no fly-in on mount).
|
|
81
|
+
const id = requestAnimationFrame(() => (ready = true));
|
|
82
|
+
return () => cancelAnimationFrame(id);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (!trackEl || typeof ResizeObserver === 'undefined') return;
|
|
87
|
+
const ro = new ResizeObserver(() => measure());
|
|
88
|
+
ro.observe(trackEl);
|
|
89
|
+
return () => ro.disconnect();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function choose(i: number) {
|
|
93
|
+
const o = items[i];
|
|
94
|
+
if (!o || o.disabled || disabled) return;
|
|
95
|
+
if (o.value !== value) {
|
|
96
|
+
value = o.value;
|
|
97
|
+
onChange?.(o.value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function onKeydown(e: KeyboardEvent, i: number) {
|
|
102
|
+
const step = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0;
|
|
103
|
+
let next = -1;
|
|
104
|
+
const n = items.length;
|
|
105
|
+
if (step) {
|
|
106
|
+
for (let k = 1; k <= n; k++) {
|
|
107
|
+
const j = (((i + step * k) % n) + n) % n;
|
|
108
|
+
if (!items[j].disabled) {
|
|
109
|
+
next = j;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (e.key === 'Home') next = items.findIndex((o) => !o.disabled);
|
|
114
|
+
else if (e.key === 'End') next = items.findLastIndex((o) => !o.disabled);
|
|
115
|
+
else return;
|
|
116
|
+
if (next < 0) return;
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
choose(next);
|
|
119
|
+
buttons[next]?.focus();
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<div
|
|
124
|
+
bind:this={trackEl}
|
|
125
|
+
class="jatui-segmented seg-{size} seg-{variant} {className}"
|
|
126
|
+
class:seg-full={fullWidth}
|
|
127
|
+
class:seg-ready={ready}
|
|
128
|
+
class:seg-disabled={disabled}
|
|
129
|
+
role="radiogroup"
|
|
130
|
+
aria-label={ariaLabel}
|
|
131
|
+
aria-disabled={disabled || undefined}
|
|
132
|
+
>
|
|
133
|
+
{#if thumb}
|
|
134
|
+
<span class="seg-thumb" aria-hidden="true" style:--x="{thumb.x}px" style:--w="{thumb.w}px"></span>
|
|
135
|
+
{/if}
|
|
136
|
+
{#each items as o, i (o.value)}
|
|
137
|
+
{@const sel = i === selectedIndex}
|
|
138
|
+
<button
|
|
139
|
+
bind:this={buttons[i]}
|
|
140
|
+
type="button"
|
|
141
|
+
role="radio"
|
|
142
|
+
class="seg-item"
|
|
143
|
+
class:is-selected={sel}
|
|
144
|
+
aria-checked={sel}
|
|
145
|
+
aria-label={o.ariaLabel}
|
|
146
|
+
disabled={o.disabled || disabled}
|
|
147
|
+
tabindex={i === tabIndexOf ? 0 : -1}
|
|
148
|
+
onclick={() => choose(i)}
|
|
149
|
+
onkeydown={(e) => onKeydown(e, i)}
|
|
150
|
+
>
|
|
151
|
+
{#if option}{@render option(o, { selected: sel })}{:else}{o.label}{/if}
|
|
152
|
+
</button>
|
|
153
|
+
{/each}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<style>
|
|
157
|
+
.jatui-segmented {
|
|
158
|
+
--seg-h: 2rem;
|
|
159
|
+
--seg-px: 0.75rem;
|
|
160
|
+
--seg-pad: 3px;
|
|
161
|
+
position: relative;
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: stretch;
|
|
164
|
+
gap: 2px;
|
|
165
|
+
padding: var(--seg-pad);
|
|
166
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
isolation: isolate;
|
|
169
|
+
}
|
|
170
|
+
.seg-solid {
|
|
171
|
+
background: color-mix(in oklch, var(--color-base-content) 7%, var(--color-base-100));
|
|
172
|
+
border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
173
|
+
}
|
|
174
|
+
.seg-xs {
|
|
175
|
+
--seg-h: 1.5rem;
|
|
176
|
+
--seg-px: 0.5rem;
|
|
177
|
+
--seg-pad: 2px;
|
|
178
|
+
font-size: 0.75rem;
|
|
179
|
+
}
|
|
180
|
+
.seg-sm {
|
|
181
|
+
--seg-h: 1.75rem;
|
|
182
|
+
--seg-px: 0.625rem;
|
|
183
|
+
font-size: 0.8125rem;
|
|
184
|
+
}
|
|
185
|
+
.seg-full {
|
|
186
|
+
display: flex;
|
|
187
|
+
width: 100%;
|
|
188
|
+
}
|
|
189
|
+
.seg-full .seg-item {
|
|
190
|
+
flex: 1 1 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.seg-item {
|
|
194
|
+
position: relative;
|
|
195
|
+
z-index: 1;
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
gap: var(--jatui-space-sm, 4px);
|
|
200
|
+
min-height: var(--seg-h);
|
|
201
|
+
padding: 0 var(--seg-px);
|
|
202
|
+
border: 0;
|
|
203
|
+
border-radius: calc(var(--jatui-radius-field, 0.25rem) - 1px);
|
|
204
|
+
background: transparent;
|
|
205
|
+
color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
|
|
206
|
+
font: inherit;
|
|
207
|
+
font-weight: 500;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
transition: color var(--motion-fast, 150ms) var(--motion-standard, ease);
|
|
211
|
+
}
|
|
212
|
+
.seg-item:hover:not(:disabled):not(.is-selected) {
|
|
213
|
+
color: var(--color-base-content);
|
|
214
|
+
}
|
|
215
|
+
.seg-item.is-selected {
|
|
216
|
+
color: var(--color-base-content);
|
|
217
|
+
}
|
|
218
|
+
.seg-plain .seg-item.is-selected {
|
|
219
|
+
color: var(--color-primary);
|
|
220
|
+
}
|
|
221
|
+
.seg-item:focus-visible {
|
|
222
|
+
outline: 2px solid var(--color-primary);
|
|
223
|
+
outline-offset: 1px;
|
|
224
|
+
}
|
|
225
|
+
.seg-item:disabled {
|
|
226
|
+
opacity: 0.45;
|
|
227
|
+
cursor: not-allowed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.seg-thumb {
|
|
231
|
+
position: absolute;
|
|
232
|
+
z-index: 0;
|
|
233
|
+
top: var(--seg-pad);
|
|
234
|
+
bottom: var(--seg-pad);
|
|
235
|
+
left: 0;
|
|
236
|
+
width: var(--w);
|
|
237
|
+
transform: translateX(var(--x));
|
|
238
|
+
border-radius: calc(var(--jatui-radius-field, 0.25rem) - 1px);
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
}
|
|
241
|
+
.seg-solid .seg-thumb {
|
|
242
|
+
background: var(--color-base-100);
|
|
243
|
+
border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
244
|
+
}
|
|
245
|
+
.seg-plain .seg-thumb {
|
|
246
|
+
background: color-mix(in oklch, var(--color-primary) 14%, transparent);
|
|
247
|
+
}
|
|
248
|
+
.seg-ready .seg-thumb {
|
|
249
|
+
transition:
|
|
250
|
+
transform var(--motion-move, 250ms) var(--motion-glide, cubic-bezier(0.22, 1, 0.36, 1)),
|
|
251
|
+
width var(--motion-move, 250ms) var(--motion-glide, cubic-bezier(0.22, 1, 0.36, 1));
|
|
252
|
+
}
|
|
253
|
+
.seg-disabled {
|
|
254
|
+
opacity: 0.6;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media (prefers-reduced-motion: reduce) {
|
|
258
|
+
.seg-ready .seg-thumb,
|
|
259
|
+
.seg-item {
|
|
260
|
+
transition: none;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface SegmentedOption<V extends string | number = string> {
|
|
2
|
+
value: V;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
/** Accessible name when `label` is visually hidden by an `option` snippet. */
|
|
6
|
+
ariaLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
declare function $$render<V extends string | number = string>(): {
|
|
10
|
+
props: {
|
|
11
|
+
/** Options, or plain strings (value = label). */
|
|
12
|
+
options: readonly (SegmentedOption<V> | V)[];
|
|
13
|
+
value?: V;
|
|
14
|
+
size?: "xs" | "sm" | "md";
|
|
15
|
+
variant?: "solid" | "plain";
|
|
16
|
+
/** Stretch to the container, segments share the width. */
|
|
17
|
+
fullWidth?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
/** Custom segment content (icon + label, counts…). */
|
|
21
|
+
option?: Snippet<[SegmentedOption<V>, {
|
|
22
|
+
selected: boolean;
|
|
23
|
+
}]>;
|
|
24
|
+
onChange?: (value: V) => void;
|
|
25
|
+
class?: string;
|
|
26
|
+
};
|
|
27
|
+
exports: {};
|
|
28
|
+
bindings: "value";
|
|
29
|
+
slots: {};
|
|
30
|
+
events: {};
|
|
31
|
+
};
|
|
32
|
+
declare class __sveltets_Render<V extends string | number = string> {
|
|
33
|
+
props(): ReturnType<typeof $$render<V>>['props'];
|
|
34
|
+
events(): ReturnType<typeof $$render<V>>['events'];
|
|
35
|
+
slots(): ReturnType<typeof $$render<V>>['slots'];
|
|
36
|
+
bindings(): "value";
|
|
37
|
+
exports(): {};
|
|
38
|
+
}
|
|
39
|
+
interface $$IsomorphicComponent {
|
|
40
|
+
new <V extends string | number = string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
|
|
41
|
+
$$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
|
|
42
|
+
} & ReturnType<__sveltets_Render<V>['exports']>;
|
|
43
|
+
<V extends string | number = string>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
|
|
44
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
45
|
+
}
|
|
46
|
+
declare const SegmentedControl: $$IsomorphicComponent;
|
|
47
|
+
type SegmentedControl<V extends string | number = string> = InstanceType<typeof SegmentedControl<V>>;
|
|
48
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SettingsRow: label + description on the start side, the control on the
|
|
3
|
+
end side (jatui-6kitr). Stacks when its container is narrow. The row wires
|
|
4
|
+
ids: pass `for` (your control's id) or let it generate one and put
|
|
5
|
+
`ctx.id` on your control; `ctx.descriptionId` goes on aria-describedby.
|
|
6
|
+
|
|
7
|
+
<SettingsRow label="Email digests" description="One summary a day">
|
|
8
|
+
{#snippet control(ctx)}
|
|
9
|
+
<Switch id={ctx.id} aria-describedby={ctx.descriptionId} bind:checked />
|
|
10
|
+
{/snippet}
|
|
11
|
+
</SettingsRow>
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts" module>
|
|
14
|
+
export interface SettingsRowContext {
|
|
15
|
+
/** Id to put on the control (the label points at it). */
|
|
16
|
+
id: string;
|
|
17
|
+
labelId: string;
|
|
18
|
+
/** Undefined when there is no description. */
|
|
19
|
+
descriptionId: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import type { Snippet } from 'svelte';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
label: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Id of the control the label is for. Default: generated, passed as `ctx.id`. */
|
|
30
|
+
for?: string;
|
|
31
|
+
/** 'inline' (default) puts the control beside the text; 'stacked' always below. */
|
|
32
|
+
layout?: 'inline' | 'stacked';
|
|
33
|
+
/** Unsaved-change dot. */
|
|
34
|
+
dirty?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Validation / warning text under the description. */
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Anchor id for deep links. */
|
|
39
|
+
id?: string;
|
|
40
|
+
control?: Snippet<[SettingsRowContext]>;
|
|
41
|
+
/** Extra content under the row (an expanded sub-form). */
|
|
42
|
+
children?: Snippet;
|
|
43
|
+
/** Custom label content (badge, icon). It becomes the accessible name, so include the text. */
|
|
44
|
+
labelContent?: Snippet;
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let {
|
|
49
|
+
label,
|
|
50
|
+
description,
|
|
51
|
+
for: forId,
|
|
52
|
+
layout = 'inline',
|
|
53
|
+
dirty = false,
|
|
54
|
+
disabled = false,
|
|
55
|
+
error,
|
|
56
|
+
id,
|
|
57
|
+
control,
|
|
58
|
+
children,
|
|
59
|
+
labelContent,
|
|
60
|
+
class: className = ''
|
|
61
|
+
}: Props = $props();
|
|
62
|
+
|
|
63
|
+
const uid = $props.id();
|
|
64
|
+
const ctx = $derived<SettingsRowContext>({
|
|
65
|
+
id: forId ?? `${uid}-control`,
|
|
66
|
+
labelId: `${uid}-label`,
|
|
67
|
+
descriptionId: description || error ? `${uid}-desc` : undefined
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
{id}
|
|
73
|
+
class="jatui-settings-row srow-{layout} {className}"
|
|
74
|
+
class:is-disabled={disabled}
|
|
75
|
+
class:has-error={!!error}
|
|
76
|
+
>
|
|
77
|
+
<div class="srow-main">
|
|
78
|
+
<div class="srow-text">
|
|
79
|
+
<label class="srow-label" id={ctx.labelId} for={ctx.id}>
|
|
80
|
+
{#if labelContent}{@render labelContent()}{:else}{label}{/if}
|
|
81
|
+
{#if dirty}
|
|
82
|
+
<span class="srow-dirty" aria-hidden="true"></span><span class="sr-only">(unsaved)</span>
|
|
83
|
+
{/if}
|
|
84
|
+
</label>
|
|
85
|
+
{#if description || error}
|
|
86
|
+
<p class="srow-desc" id={ctx.descriptionId}>
|
|
87
|
+
{#if description}<span>{description}</span>{/if}
|
|
88
|
+
{#if error}<span class="srow-error" role="alert">{error}</span>{/if}
|
|
89
|
+
</p>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
{#if control}
|
|
93
|
+
<div class="srow-control">{@render control(ctx)}</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
{#if children}
|
|
97
|
+
<div class="srow-extra">{@render children()}</div>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.jatui-settings-row {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: var(--jatui-space-base, 8px);
|
|
106
|
+
min-height: 3.25rem;
|
|
107
|
+
padding-block: var(--jatui-space-lg, 12px);
|
|
108
|
+
justify-content: center;
|
|
109
|
+
}
|
|
110
|
+
.srow-main {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
gap: var(--jatui-space-xl, 16px);
|
|
115
|
+
}
|
|
116
|
+
.srow-stacked .srow-main {
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
align-items: stretch;
|
|
119
|
+
gap: var(--jatui-space-base, 8px);
|
|
120
|
+
}
|
|
121
|
+
@container (max-width: 28rem) {
|
|
122
|
+
.srow-main {
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
align-items: stretch;
|
|
125
|
+
gap: var(--jatui-space-base, 8px);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
.srow-text {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 2px;
|
|
132
|
+
min-width: 0;
|
|
133
|
+
flex: 1 1 auto;
|
|
134
|
+
}
|
|
135
|
+
.srow-label {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: var(--jatui-space-md, 6px);
|
|
139
|
+
font-size: 0.875rem;
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
color: var(--color-base-content);
|
|
142
|
+
}
|
|
143
|
+
.srow-dirty {
|
|
144
|
+
width: 0.4375rem;
|
|
145
|
+
height: 0.4375rem;
|
|
146
|
+
border-radius: 9999px;
|
|
147
|
+
background: var(--color-warning);
|
|
148
|
+
}
|
|
149
|
+
.srow-desc {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 2px;
|
|
153
|
+
margin: 0;
|
|
154
|
+
font-size: 0.8125rem;
|
|
155
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
156
|
+
}
|
|
157
|
+
.srow-error {
|
|
158
|
+
color: var(--color-error);
|
|
159
|
+
}
|
|
160
|
+
.srow-control {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: flex-end;
|
|
164
|
+
gap: var(--jatui-space-base, 8px);
|
|
165
|
+
flex: none;
|
|
166
|
+
max-width: 100%;
|
|
167
|
+
}
|
|
168
|
+
.srow-stacked .srow-control {
|
|
169
|
+
justify-content: flex-start;
|
|
170
|
+
}
|
|
171
|
+
.is-disabled .srow-text {
|
|
172
|
+
opacity: 0.55;
|
|
173
|
+
}
|
|
174
|
+
.srow-extra {
|
|
175
|
+
min-width: 0;
|
|
176
|
+
}
|
|
177
|
+
</style>
|