@heyramzi/ui 2.0.1 → 2.2.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/package.json +48 -24
- package/scripts/sync-shadcn.mjs +3 -4
- package/src/agents/agent-avatar.svelte +604 -575
- package/src/agents/agent-cast.svelte +179 -183
- package/src/agents/demo.svelte +125 -113
- package/src/agents/helpers.ts +23 -17
- package/src/agents/index.ts +4 -4
- package/src/agents/page-state.ts +6 -6
- package/src/agents/types.ts +6 -6
- package/src/ai/ai-button/AiButton.svelte +4 -12
- package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
- package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
- package/src/ai/response/Response.css +2 -3
- package/src/animations/avatar-group-hover.css +2 -16
- package/src/animations/card-tilt.css +3 -6
- package/src/animations/digit-pop-in.css +2 -15
- package/src/animations/dropdown.css +4 -17
- package/src/animations/input-clear-dissolve.css +5 -18
- package/src/animations/input-shake.css +9 -41
- package/src/animations/learn-more-hover.css +17 -9
- package/src/animations/like-button.css +2 -4
- package/src/animations/page-side-by-side.css +2 -10
- package/src/animations/plus-menu-morph.css +2 -5
- package/src/animations/shimmer-text.css +5 -18
- package/src/animations/spinning-counter.css +2 -6
- package/src/animations/success-check.css +2 -15
- package/src/animations/thinking-states.css +2 -7
- package/src/brand/brand.test.ts +21 -5
- package/src/brand/index.ts +4 -10
- package/src/canvas/README.md +9 -12
- package/src/canvas/card/card-frame.svelte +43 -39
- package/src/canvas/card/resize-handles.svelte +89 -80
- package/src/canvas/chrome/chrome-zoom.svelte +101 -100
- package/src/canvas/chrome/lock-badge.svelte +8 -8
- package/src/canvas/chrome/side-handles.svelte +24 -19
- package/src/canvas/chrome/size-chip.svelte +30 -30
- package/src/canvas/chrome/snap-guides.svelte +20 -20
- package/src/canvas/components/icons/ToolIcon.svelte +8 -8
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
- package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
- package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
- package/src/canvas/edges/bend-handle.svelte +46 -46
- package/src/canvas/edges/edge-gradient.svelte +10 -10
- package/src/canvas/edges/gradient-edge.svelte +45 -29
- package/src/canvas/editor/lock.test.ts +6 -1
- package/src/canvas/editor/style.ts +7 -1
- package/src/canvas/headless/svg-figure.test.ts +5 -1
- package/src/canvas/lod/lod-switch.svelte +111 -106
- package/src/canvas/pen/PenField.svelte +87 -81
- package/src/canvas/pen/PenOverlay.svelte +127 -127
- package/src/canvas/pen/ShapeNode.svelte +31 -32
- package/src/canvas/pen/TextNode.svelte +54 -53
- package/src/canvas/pen/host-field.test.svelte +7 -7
- package/src/canvas/pen/shapes.ts +7 -1
- package/src/canvas/screen/GlyphIcon.svelte +22 -17
- package/src/canvas/screen/PartView.svelte +138 -72
- package/src/canvas/screen/ScreenBody.svelte +81 -60
- package/src/canvas/screen/ScreenCard.svelte +20 -20
- package/src/canvas/ui/CanvasToolbar.svelte +173 -85
- package/src/canvas/ui/FrameLayer.svelte +59 -59
- package/src/canvas/ui/PenLayer.svelte +39 -30
- package/src/canvas/ui/StylePanel.svelte +119 -102
- package/src/canvas/ui/canvas-grid.svelte +137 -129
- package/src/charts/animated-value.svelte +1 -1
- package/src/charts/area-chart.css +2 -5
- package/src/charts/area-chart.svelte +1 -1
- package/src/charts/bar-chart.css +2 -3
- package/src/charts/bar-chart.svelte +1 -1
- package/src/charts/charts.css +5 -17
- package/src/charts/crosshair.svelte +1 -1
- package/src/charts/dashboard/dashboard-empty.svelte +25 -25
- package/src/charts/dashboard/dashboard-section.svelte +24 -24
- package/src/charts/dashboard/data-panel.svelte +50 -50
- package/src/charts/dashboard/data-sort-head.svelte +56 -56
- package/src/charts/dashboard/data-table.svelte +138 -138
- package/src/charts/dashboard/panel-grid.svelte +30 -30
- package/src/charts/dashboard/period-filter.svelte +25 -25
- package/src/charts/dashboard/rating-chip.svelte +19 -19
- package/src/charts/dashboard/stat-band.svelte +32 -32
- package/src/charts/dashboard/stat-tile.svelte +106 -99
- package/src/charts/donut-chart.svelte +1 -1
- package/src/charts/edge-fade.svelte +1 -1
- package/src/charts/figure.svelte +1 -1
- package/src/charts/gauge-chart.css +4 -7
- package/src/charts/gauge-chart.svelte +1 -1
- package/src/charts/hover-focus.svelte +1 -1
- package/src/charts/legend.css +1 -3
- package/src/charts/legend.svelte +1 -1
- package/src/charts/line-chart.svelte +1 -1
- package/src/charts/number-chart.css +6 -9
- package/src/charts/number-chart.svelte +1 -1
- package/src/charts/reference-line.svelte +19 -19
- package/src/charts/reveal.svelte +1 -1
- package/src/charts/ring-chart.css +2 -4
- package/src/charts/ring-chart.svelte +1 -1
- package/src/charts/sparkline.svelte +5 -2
- package/src/charts/stat-card.svelte +1 -1
- package/src/charts/time-series-chart.css +2 -5
- package/src/charts/trend-pill.css +2 -4
- package/src/charts/trend-pill.svelte +1 -1
- package/src/components/copy/copy-button.svelte +93 -98
- package/src/components/copy/copy-button.test.ts +10 -2
- package/src/components/data-table/infinite-scroll.svelte +42 -42
- package/src/components/data-table/pagination.svelte +34 -22
- package/src/components/data-table/row-arrow.svelte +19 -16
- package/src/components/data-table/row-context-menu.svelte +52 -52
- package/src/components/data-table/sortable-head.svelte +25 -25
- package/src/components/data-table/table-skeleton.svelte +41 -36
- package/src/components/docs/doc-page.svelte +18 -9
- package/src/components/docs/on-this-page.svelte +16 -3
- package/src/components/docs/scroll-spy.svelte.ts +2 -3
- package/src/components/field-row/field-row.svelte +10 -8
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
- package/src/components/icon/icon.svelte +14 -14
- package/src/components/language-flag/language-flag.svelte +3 -1
- package/src/components/list-page/detail-empty.svelte +34 -34
- package/src/components/list-page/detail-header.svelte +55 -47
- package/src/components/list-page/detail-panel.svelte +51 -59
- package/src/components/list-page/detail-row.svelte +19 -19
- package/src/components/list-page/detail-stack.svelte +21 -21
- package/src/components/list-page/empty-state.svelte +30 -30
- package/src/components/list-page/filter-item.svelte +20 -20
- package/src/components/list-page/filter-popover.svelte +60 -55
- package/src/components/list-page/grouped-list.svelte +4 -2
- package/src/components/list-page/index.ts +8 -4
- package/src/components/list-page/list-page-shell.svelte +2 -5
- package/src/components/list-page/master-detail.svelte +87 -88
- package/src/components/list-page/notice-bar.svelte +38 -38
- package/src/components/list-page/prev-next.svelte +63 -63
- package/src/components/list-page/rail-body.svelte +18 -18
- package/src/components/list-page/rail-empty.svelte +23 -30
- package/src/components/list-page/rail-head.svelte +24 -24
- package/src/components/list-page/rail-row.svelte +77 -73
- package/src/components/list-page/section-host.ts +2 -3
- package/src/components/locale-switch/locale-switch.svelte +41 -41
- package/src/components/mark/mark.svelte +34 -38
- package/src/components/open-in/index.ts +1 -5
- package/src/components/open-in/open-in.svelte +40 -40
- package/src/components/picker/mark-picker.svelte +49 -49
- package/src/components/picker/picker.svelte +5 -3
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.svelte +1 -1
- package/src/components/section-header/section-header.test.ts +7 -1
- package/src/components/section-header/section-label.svelte +39 -0
- package/src/components/segmented/animated-tabs.svelte +31 -2
- package/src/components/segmented/collapse.test.ts +2 -1
- package/src/components/segmented/index.ts +2 -0
- package/src/components/segmented/segment.svelte +52 -49
- package/src/components/segmented/segmented-filter.svelte +79 -80
- package/src/components/segmented/segmented-nav.svelte +34 -40
- package/src/components/segmented/segmented-overflow.svelte +53 -50
- package/src/components/segmented/segmented-track.svelte +62 -46
- package/src/components/segmented/segmented.ts +2 -3
- package/src/components/star-rating-input/star-rating-input.svelte +103 -103
- package/src/components/svg/svg.svelte +4 -4
- package/src/components/table-view/table-view.svelte +17 -2
- package/src/components/table-view/table-view.test.ts +7 -1
- package/src/components/table-view/types.ts +11 -11
- package/src/components/tags-input/index.ts +13 -0
- package/src/components/tags-input/tags-input.svelte +305 -0
- package/src/components/tags-input/tags.test.ts +64 -0
- package/src/components/tags-input/tags.ts +142 -0
- package/src/components/theme/index.ts +2 -4
- package/src/components/theme/theme-provider.svelte +1 -3
- package/src/components/toolbar/index.ts +2 -0
- package/src/components/{list-page → toolbar}/toolbar-search.svelte +25 -25
- package/src/components/toolbar/toolbar.svelte +13 -9
- package/src/components/toolbar/toolbar.test.ts +10 -1
- package/src/design/index.css +21 -78
- package/src/design/polish.css +26 -35
- package/src/editor/editor-toolbar.svelte +104 -99
- package/src/editor/editor.svelte +296 -266
- package/src/editor/image-toolbar.svelte +20 -22
- package/src/editor/index.ts +1 -4
- package/src/editor/inline-editor.svelte +280 -238
- package/src/editor/markdown.ts +15 -15
- package/src/editor/renderer.svelte +71 -71
- package/src/editor/slash-menu.svelte +24 -27
- package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
- package/src/effects/image-comparison/ImageComparison.svelte +11 -1
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
- package/src/effects/magic-card/MagicCard.css +14 -7
- package/src/effects/marquee/Marquee.svelte +3 -5
- package/src/effects/reveal/observe.ts +0 -3
- package/src/effects/spotlight/Spotlight.svelte +1 -1
- package/src/effects/theme-toggle/ThemeToggle.css +25 -22
- package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
- package/src/gallery.test.ts +14 -4
- package/src/inbox/README.md +27 -0
- package/src/inbox/inbox.test.ts +214 -0
- package/src/inbox/index.ts +31 -0
- package/src/inbox/logic.ts +169 -0
- package/src/inbox/thread-inbox.svelte +439 -0
- package/src/inbox/types.ts +161 -0
- package/src/lib/theme-transition.ts +2 -4
- package/src/lib/tone.ts +2 -4
- package/src/lib/utils.ts +2 -2
- package/src/modules.test.ts +105 -0
- package/src/onboarding/define-tours.test.ts +55 -40
- package/src/onboarding/define-tours.ts +23 -23
- package/src/onboarding/demo.svelte +24 -5
- package/src/onboarding/index.ts +1 -8
- package/src/onboarding/onboarding-provider.svelte +19 -7
- package/src/onboarding/onboarding.svelte.ts +16 -16
- package/src/onboarding/pick.test.ts +61 -52
- package/src/onboarding/pick.ts +16 -13
- package/src/onboarding/stores.test.ts +80 -80
- package/src/onboarding/stores.ts +41 -39
- package/src/onboarding/tour-card.svelte +30 -5
- package/src/onboarding/tour-runner.svelte +12 -3
- package/src/onboarding/types.ts +20 -12
- package/src/onboarding/wait.ts +22 -22
- package/src/panel/README.md +5 -0
- package/src/panel/index.ts +2 -0
- package/src/panel/side-panel.svelte +194 -0
- package/src/record/README.md +3 -0
- package/src/record/demo-data.ts +352 -0
- package/src/record/field-section.svelte +57 -0
- package/src/record/fields.ts +194 -0
- package/src/record/format.ts +94 -0
- package/src/{tasks/record → record}/index.ts +49 -35
- package/src/{tasks/record → record}/info-list.svelte +145 -114
- package/src/record/kinds.ts +42 -0
- package/src/record/logic.ts +326 -0
- package/src/record/next-up.svelte +81 -0
- package/src/{tasks/record → record}/project-list.svelte +56 -50
- package/src/{tasks/record → record}/record-demo.svelte +130 -110
- package/src/{tasks/record → record}/record-header.svelte +37 -28
- package/src/{tasks/record → record}/record-shell.svelte +54 -54
- package/src/record/record-stats.svelte +48 -0
- package/src/record/record-timeline.svelte +251 -0
- package/src/record/record.test.ts +471 -0
- package/src/{tasks/record → record}/steps-rail.svelte +225 -216
- package/src/record/types.ts +299 -0
- package/src/record/where-we-are.svelte +125 -0
- package/src/shadcn/button/button.svelte +1 -1
- package/src/shadcn/card/card-title.svelte +1 -5
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
- package/src/shadcn/sheet/sheet-content.svelte +2 -4
- package/src/shadcn/table/table-cell.svelte +2 -3
- package/src/shadcn/utilities.css +3 -8
- package/src/slides/Controls.svelte +150 -124
- package/src/slides/Deck.svelte +243 -238
- package/src/slides/Fragment.svelte +64 -57
- package/src/slides/Overview.svelte +106 -101
- package/src/slides/Presenter.svelte +300 -266
- package/src/slides/Slide.svelte +185 -170
- package/src/slides/SlideList.svelte +16 -12
- package/src/slides/ambient.d.ts +1 -1
- package/src/slides/bridges.ts +42 -23
- package/src/slides/deck-controller.svelte.ts +47 -35
- package/src/slides/demo-slides.svelte +330 -306
- package/src/slides/demo.svelte +164 -160
- package/src/slides/export/capture.ts +20 -16
- package/src/slides/export/ffmpeg.ts +43 -38
- package/src/slides/export/index.ts +5 -5
- package/src/slides/export/launch.ts +3 -1
- package/src/slides/export/pdf.ts +30 -27
- package/src/slides/export/types.ts +10 -8
- package/src/slides/index.ts +11 -11
- package/src/slides/labels.ts +22 -19
- package/src/slides/layout.ts +9 -2
- package/src/slides/model.ts +4 -4
- package/src/slides/navigation.ts +43 -31
- package/src/slides/theme.css +5 -7
- package/src/slides/types.ts +9 -10
- package/src/status/README.md +18 -0
- package/src/status/define-statuses.ts +25 -0
- package/src/status/index.ts +11 -0
- package/src/{tasks → status}/status-badge.svelte +3 -1
- package/src/{tasks → status}/status-dot.svelte +1 -3
- package/src/{tasks → status}/status-icon.svelte +1 -1
- package/src/status/status-types.ts +40 -0
- package/src/tasks/README.md +1 -1
- package/src/tasks/board-order.ts +2 -4
- package/src/tasks/demo.svelte +192 -34
- package/src/tasks/detail/detail-demo.svelte +75 -15
- package/src/tasks/detail/detail-types.ts +45 -45
- package/src/tasks/detail/task-detail.svelte +40 -10
- package/src/tasks/detail/task-property.svelte +8 -2
- package/src/tasks/detail/task-thread.svelte +53 -12
- package/src/tasks/gantt/gantt-bar.svelte +17 -6
- package/src/tasks/gantt/gantt-grid.svelte +40 -9
- package/src/tasks/gantt/gantt-group-row.svelte +8 -2
- package/src/tasks/gantt/gantt-header.svelte +9 -3
- package/src/tasks/gantt/gantt-task-row.svelte +26 -6
- package/src/tasks/gantt/gantt.css +2 -1
- package/src/tasks/gantt/gantt.test.ts +4 -2
- package/src/tasks/gantt/logic.test.ts +110 -20
- package/src/tasks/gantt/logic.ts +42 -6
- package/src/tasks/inbox/inbox-card.svelte +15 -7
- package/src/tasks/inbox/inbox-draft.svelte +1 -1
- package/src/tasks/inbox/inbox-logic.ts +6 -40
- package/src/tasks/inbox/inbox-statuses.ts +24 -24
- package/src/tasks/inbox/inbox-trace.svelte +9 -3
- package/src/tasks/inbox/inbox-types.ts +54 -58
- package/src/tasks/inbox/index.ts +22 -21
- package/src/tasks/index.ts +52 -53
- package/src/tasks/kanban-board.svelte +16 -12
- package/src/tasks/kanban-types.ts +2 -3
- package/src/tasks/presets/sales.ts +7 -7
- package/src/tasks/task-gantt.svelte +28 -8
- package/src/tasks/task-list.svelte +41 -17
- package/src/tasks/time/duration.ts +40 -40
- package/src/tasks/time/time-chip.svelte +9 -2
- package/src/tasks/time/time-dot.svelte +1 -1
- package/src/tasks/time/time-ring.svelte +11 -3
- package/src/tasks/time/time-tracker.svelte +57 -15
- package/src/tasks/time/time-types.ts +57 -57
- package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
- package/src/thread/README.md +31 -0
- package/src/thread/index.ts +12 -0
- package/src/thread/logic.ts +61 -0
- package/src/thread/thread.svelte +432 -0
- package/src/thread/thread.test.ts +73 -0
- package/src/thread/types.ts +93 -0
- package/src/tokens/craft.css +17 -53
- package/src/tokens/index.css +3 -13
- package/src/tokens/palette.css +13 -11
- package/src/tokens/primitives.css +10 -23
- package/src/components/list-page/section-label.svelte +0 -39
- package/src/tasks/define-statuses.ts +0 -32
- package/src/tasks/inbox/inbox.svelte +0 -203
- package/src/tasks/record/demo-data.ts +0 -286
- package/src/tasks/record/field-section.svelte +0 -53
- package/src/tasks/record/fields.ts +0 -142
- package/src/tasks/record/format.ts +0 -87
- package/src/tasks/record/kinds.ts +0 -42
- package/src/tasks/record/logic.ts +0 -324
- package/src/tasks/record/next-up.svelte +0 -70
- package/src/tasks/record/record-stats.svelte +0 -48
- package/src/tasks/record/record-timeline.svelte +0 -231
- package/src/tasks/record/record.test.ts +0 -350
- package/src/tasks/record/types.ts +0 -280
- package/src/tasks/record/where-we-are.svelte +0 -121
- package/src/tasks/side-panel.svelte +0 -193
- package/src/tasks/status-types.ts +0 -40
- /package/src/components/{list-page → toolbar}/toolbar-trigger.ts +0 -0
- /package/src/{tasks → panel}/side-panel-types.ts +0 -0
- /package/src/{tasks/record → record}/gutter.ts +0 -0
|
@@ -3,101 +3,101 @@ import { localStorageStore, memoryStore } from "./stores.js";
|
|
|
3
3
|
import type { SeenStore } from "./types.js";
|
|
4
4
|
|
|
5
5
|
function fakeStorage(): Storage {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
const data = new Map<string, string>();
|
|
7
|
+
return {
|
|
8
|
+
get length() {
|
|
9
|
+
return data.size;
|
|
10
|
+
},
|
|
11
|
+
clear: () => data.clear(),
|
|
12
|
+
getItem: (key) => data.get(key) ?? null,
|
|
13
|
+
key: (i) => [...data.keys()][i] ?? null,
|
|
14
|
+
removeItem: (key) => void data.delete(key),
|
|
15
|
+
setItem: (key, value) => void data.set(key, String(value)),
|
|
16
|
+
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function behavesLikeAStore(make: () => SeenStore) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
it("starts empty", async () => {
|
|
21
|
+
expect(await make().load()).toEqual({});
|
|
22
|
+
});
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
it("remembers the version it saw", async () => {
|
|
25
|
+
const store = make();
|
|
26
|
+
await store.markSeen("home", 1, "completed");
|
|
27
|
+
await store.markSeen("board", 2, "dismissed");
|
|
28
|
+
expect(await store.load()).toEqual({ home: 1, board: 2 });
|
|
29
|
+
});
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
it("never goes back to an older version", async () => {
|
|
32
|
+
const store = make();
|
|
33
|
+
await store.markSeen("home", 3, "completed");
|
|
34
|
+
await store.markSeen("home", 2, "completed");
|
|
35
|
+
expect((await store.load()).home).toBe(3);
|
|
36
|
+
});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
it("resets one tour or all of them", async () => {
|
|
39
|
+
const store = make();
|
|
40
|
+
await store.markSeen("home", 1, "completed");
|
|
41
|
+
await store.markSeen("board", 1, "completed");
|
|
42
|
+
await store.reset("home");
|
|
43
|
+
expect(await store.load()).toEqual({ board: 1 });
|
|
44
|
+
await store.reset();
|
|
45
|
+
expect(await store.load()).toEqual({});
|
|
46
|
+
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
describe("memoryStore", () => {
|
|
50
|
-
|
|
50
|
+
behavesLikeAStore(() => memoryStore());
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
it("takes a starting state and keeps its own copy", async () => {
|
|
53
|
+
const initial = { home: 1 };
|
|
54
|
+
const store = memoryStore(initial);
|
|
55
|
+
await store.markSeen("board", 1, "completed");
|
|
56
|
+
expect(initial).toEqual({ home: 1 });
|
|
57
|
+
expect(await store.load()).toEqual({ home: 1, board: 1 });
|
|
58
|
+
});
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
describe("localStorageStore", () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.stubGlobal("localStorage", fakeStorage());
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.unstubAllGlobals();
|
|
67
|
+
});
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
behavesLikeAStore(() => localStorageStore());
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
it("shares state between two stores on one key and keeps keys apart", async () => {
|
|
72
|
+
await localStorageStore("a").markSeen("home", 1, "completed");
|
|
73
|
+
expect(await localStorageStore("a").load()).toEqual({ home: 1 });
|
|
74
|
+
expect(await localStorageStore("b").load()).toEqual({});
|
|
75
|
+
});
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
it("reads corrupt or foreign data as empty", async () => {
|
|
78
|
+
localStorage.setItem("heyramzi:onboarding", "{nope");
|
|
79
|
+
expect(await localStorageStore().load()).toEqual({});
|
|
80
|
+
localStorage.setItem("heyramzi:onboarding", JSON.stringify(["x"]));
|
|
81
|
+
expect(await localStorageStore().load()).toEqual({});
|
|
82
|
+
localStorage.setItem("heyramzi:onboarding", JSON.stringify({ home: 1, bad: "yes" }));
|
|
83
|
+
expect(await localStorageStore().load()).toEqual({ home: 1 });
|
|
84
|
+
});
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
it("doesn't throw when storage is blocked", async () => {
|
|
87
|
+
vi.stubGlobal("localStorage", {
|
|
88
|
+
getItem: () => {
|
|
89
|
+
throw new Error("blocked");
|
|
90
|
+
},
|
|
91
|
+
setItem: () => {
|
|
92
|
+
throw new Error("blocked");
|
|
93
|
+
},
|
|
94
|
+
removeItem: () => {
|
|
95
|
+
throw new Error("blocked");
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const store = localStorageStore();
|
|
99
|
+
expect(await store.load()).toEqual({});
|
|
100
|
+
await expect(store.markSeen("home", 1, "completed")).resolves.toBeUndefined();
|
|
101
|
+
await expect(store.reset()).resolves.toBeUndefined();
|
|
102
|
+
});
|
|
103
103
|
});
|
package/src/onboarding/stores.ts
CHANGED
|
@@ -5,54 +5,56 @@ type Seen = Record<string, number>;
|
|
|
5
5
|
|
|
6
6
|
/** Holds seen-state in memory. Tests and previews. */
|
|
7
7
|
export function memoryStore(initial: Seen = {}): SeenStore {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
let seen: Seen = { ...initial };
|
|
9
|
+
return {
|
|
10
|
+
async load() {
|
|
11
|
+
return { ...seen };
|
|
12
|
+
},
|
|
13
|
+
async markSeen(tourId, version, _outcome: Outcome) {
|
|
14
|
+
seen[tourId] = Math.max(seen[tourId] ?? 0, version);
|
|
15
|
+
},
|
|
16
|
+
async reset(tourId) {
|
|
17
|
+
if (tourId === undefined) seen = {};
|
|
18
|
+
else delete seen[tourId];
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function readSeen(key: string): Seen {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const read = tryCatch(() => JSON.parse(localStorage.getItem(key) ?? "{}") as unknown);
|
|
25
|
+
const parsed = read.ok ? read.value : null;
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.entries(parsed).filter(
|
|
29
|
+
(entry): entry is [string, number] => typeof entry[1] === "number",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
// Blocked storage (private mode, a full disk) means the tour shows again next visit, which beats an error.
|
|
33
35
|
function writeSeen(key: string, seen: Seen): void {
|
|
34
|
-
|
|
36
|
+
tryCatch(() => localStorage.setItem(key, JSON.stringify(seen)));
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** Keeps seen-state in this browser. Demos and signed-out pages. */
|
|
38
40
|
export function localStorageStore(key = "heyramzi:onboarding"): SeenStore {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
return {
|
|
42
|
+
async load() {
|
|
43
|
+
return readSeen(key);
|
|
44
|
+
},
|
|
45
|
+
async markSeen(tourId, version, _outcome: Outcome) {
|
|
46
|
+
const seen = readSeen(key);
|
|
47
|
+
seen[tourId] = Math.max(seen[tourId] ?? 0, version);
|
|
48
|
+
writeSeen(key, seen);
|
|
49
|
+
},
|
|
50
|
+
async reset(tourId) {
|
|
51
|
+
if (tourId === undefined) {
|
|
52
|
+
tryCatch(() => localStorage.removeItem(key));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const seen = readSeen(key);
|
|
56
|
+
delete seen[tourId];
|
|
57
|
+
writeSeen(key, seen);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
58
60
|
}
|
|
@@ -40,12 +40,28 @@
|
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
{#if image}
|
|
43
|
-
<img
|
|
43
|
+
<img
|
|
44
|
+
src={image}
|
|
45
|
+
alt=""
|
|
46
|
+
class="aspect-video w-full rounded-t-[inherit] object-cover outline -outline-offset-1 outline-black/10 dark:outline-white/10"
|
|
47
|
+
/>
|
|
44
48
|
{/if}
|
|
45
49
|
<div class="flex flex-col gap-1.5 p-4 pb-3">
|
|
46
50
|
<div class="flex items-start justify-between gap-3">
|
|
47
|
-
<h2
|
|
48
|
-
|
|
51
|
+
<h2
|
|
52
|
+
{...titleAttrs}
|
|
53
|
+
class={["font-semibold leading-snug text-balance", intro ? "text-base" : "text-sm"]}
|
|
54
|
+
>
|
|
55
|
+
{title}
|
|
56
|
+
</h2>
|
|
57
|
+
<Button
|
|
58
|
+
onclick={onclose}
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size="sm"
|
|
61
|
+
icon
|
|
62
|
+
class="-mt-1 -mr-1.5 text-muted-foreground"
|
|
63
|
+
aria-label={strings.skip}
|
|
64
|
+
>
|
|
49
65
|
<X weight="bold" />
|
|
50
66
|
</Button>
|
|
51
67
|
</div>
|
|
@@ -53,9 +69,18 @@
|
|
|
53
69
|
</div>
|
|
54
70
|
<div class="flex items-center justify-between gap-3 px-4 pb-4">
|
|
55
71
|
{#if total > 1}
|
|
56
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
role="img"
|
|
74
|
+
aria-label={strings.progress(index + 1, total)}
|
|
75
|
+
class="flex items-center gap-1.5"
|
|
76
|
+
>
|
|
57
77
|
{#each { length: total } as _, i (i)}
|
|
58
|
-
<span
|
|
78
|
+
<span
|
|
79
|
+
class={[
|
|
80
|
+
"h-1.5 rounded-full transition-[width,background-color] duration-(--duration-fast) ease-(--ease-smooth-out) motion-reduce:transition-none",
|
|
81
|
+
i === index ? "bg-foreground w-4" : "bg-border w-1.5",
|
|
82
|
+
]}
|
|
83
|
+
></span>
|
|
59
84
|
{/each}
|
|
60
85
|
</div>
|
|
61
86
|
{:else}
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
// dismissal waits a microtask and the completion wins.
|
|
48
48
|
function onStatusChange({ status }: zag.StatusChangeDetails) {
|
|
49
49
|
if (status === "completed") finish("completed");
|
|
50
|
-
else if (status === "dismissed" || status === "skipped")
|
|
50
|
+
else if (status === "dismissed" || status === "skipped")
|
|
51
|
+
queueMicrotask(() => finish("dismissed"));
|
|
51
52
|
else if (status === "not-found") finish(null);
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -107,13 +108,21 @@
|
|
|
107
108
|
</script>
|
|
108
109
|
|
|
109
110
|
{#if step}
|
|
110
|
-
<div
|
|
111
|
+
<div
|
|
112
|
+
use:portal
|
|
113
|
+
{...api.getBackdropProps()}
|
|
114
|
+
class="tour-backdrop"
|
|
115
|
+
data-instant={instant || undefined}
|
|
116
|
+
></div>
|
|
111
117
|
<div use:portal {...api.getSpotlightProps()} class="tour-spotlight"></div>
|
|
112
118
|
<div use:portal {...api.getPositionerProps()} class="tour-positioner">
|
|
113
119
|
<div
|
|
114
120
|
{...api.getContentProps()}
|
|
115
121
|
bind:this={card}
|
|
116
|
-
class={[
|
|
122
|
+
class={[
|
|
123
|
+
"tour-card bg-popover text-popover-foreground ring-foreground/10 shadow-overlay pointer-events-auto max-w-full text-sm ring-1 outline-none",
|
|
124
|
+
intro ? "w-96 rounded-2xl" : "w-80 rounded-xl",
|
|
125
|
+
]}
|
|
117
126
|
>
|
|
118
127
|
<TourCard
|
|
119
128
|
title={String(step.title)}
|
package/src/onboarding/types.ts
CHANGED
|
@@ -9,26 +9,34 @@ export type Step = { target: string; title: string; body: string; placement?: Pl
|
|
|
9
9
|
* `version` starts at 1. Raise it and everyone who saw the old one sees the tour again.
|
|
10
10
|
*/
|
|
11
11
|
export type Tour =
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
| { id: string; version?: number; route: string; kind: "steps"; steps: Step[] }
|
|
13
|
+
| {
|
|
14
|
+
id: string;
|
|
15
|
+
version?: number;
|
|
16
|
+
route: string;
|
|
17
|
+
kind: "intro";
|
|
18
|
+
title: string;
|
|
19
|
+
body: string;
|
|
20
|
+
image?: string;
|
|
21
|
+
};
|
|
14
22
|
|
|
15
23
|
export type Outcome = "completed" | "dismissed";
|
|
16
24
|
|
|
17
25
|
/** Where seen-state lives. `load` runs once per session. */
|
|
18
26
|
export interface SeenStore {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
/** Tour id to the version seen. */
|
|
28
|
+
load(): Promise<Record<string, number>>;
|
|
29
|
+
markSeen(tourId: string, version: number, outcome: Outcome): Promise<void>;
|
|
30
|
+
/** One tour, or all of them when no id is given. */
|
|
31
|
+
reset(tourId?: string): Promise<void>;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export type Strings = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
next: string;
|
|
36
|
+
back: string;
|
|
37
|
+
done: string;
|
|
38
|
+
skip: string;
|
|
39
|
+
progress: (n: number, total: number) => string;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export const MAX_STEPS = 6;
|
package/src/onboarding/wait.ts
CHANGED
|
@@ -2,28 +2,28 @@ const find = (selector: string) => document.querySelector(selector);
|
|
|
2
2
|
|
|
3
3
|
/** Resolves with the element once the selector matches, or null after `timeoutMs` or an abort. */
|
|
4
4
|
export function waitForElement(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
selector: string,
|
|
6
|
+
timeoutMs: number,
|
|
7
|
+
signal?: AbortSignal,
|
|
8
8
|
): Promise<Element | null> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const now = find(selector);
|
|
11
|
+
if (now) return resolve(now);
|
|
12
|
+
if (signal?.aborted) return resolve(null);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
const finish = (el: Element | null) => {
|
|
15
|
+
observer.disconnect();
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
signal?.removeEventListener("abort", onAbort);
|
|
18
|
+
resolve(el);
|
|
19
|
+
};
|
|
20
|
+
const onAbort = () => finish(null);
|
|
21
|
+
const observer = new MutationObserver(() => {
|
|
22
|
+
const el = find(selector);
|
|
23
|
+
if (el) finish(el);
|
|
24
|
+
});
|
|
25
|
+
const timer = setTimeout(() => finish(find(selector)), timeoutMs);
|
|
26
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
27
|
+
signal?.addEventListener("abort", onAbort);
|
|
28
|
+
});
|
|
29
29
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @heyramzi/ui/panel
|
|
2
|
+
|
|
3
|
+
`SidePanel`, a sheet for one record: badges, links out to other tools, a copy-a-prompt handoff and a stats strip. `PanelStat` types one stat. It used to live in `@heyramzi/ui/tasks`, which still re-exports both, and `tasks/side-panel` still resolves.
|
|
4
|
+
|
|
5
|
+
Exports: `SidePanel` and the types `PanelStat`, `SidePanelWidth`.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
|
|
3
|
+
export type { PanelStat, SidePanelWidth };
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
// =========================
|
|
8
|
+
// IMPORTS
|
|
9
|
+
// =========================
|
|
10
|
+
import type { Snippet } from "svelte";
|
|
11
|
+
import * as Sheet from "../shadcn/sheet/index.js";
|
|
12
|
+
import { OpenIn, type OpenInTool } from "../components/open-in/index.js";
|
|
13
|
+
import { CopyButton } from "../components/copy/index.js";
|
|
14
|
+
import type { Tone } from "../lib/tone.js";
|
|
15
|
+
import { cn } from "../lib/utils.js";
|
|
16
|
+
import StatusBadge from "../status/status-badge.svelte";
|
|
17
|
+
|
|
18
|
+
// =========================
|
|
19
|
+
// PROPS
|
|
20
|
+
// =========================
|
|
21
|
+
interface Props {
|
|
22
|
+
open: boolean;
|
|
23
|
+
/** Names the dialog for screen readers and heads the panel. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Merged onto the title. A short path is set `font-mono`, a subject is not. */
|
|
26
|
+
titleClass?: string;
|
|
27
|
+
/** The facts the record is filed under, read out under the title. */
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Status chips, left of the action row. */
|
|
30
|
+
badges?: { label: string; tone: Tone }[];
|
|
31
|
+
/** Doors out to another tool. `text` replaces the tool's name on the button. */
|
|
32
|
+
openIn?: { tool: OpenInTool; href: string; label: string; text?: string }[];
|
|
33
|
+
/** A prompt for an AI about this record, copied on click. */
|
|
34
|
+
handoff?: { value: () => string | Promise<string>; label: string; copiedLabel: string };
|
|
35
|
+
/** The record's numbers, as a strip under the badges. Same place in every panel. */
|
|
36
|
+
stats?: PanelStat[];
|
|
37
|
+
/** Anything else the record can do, drawn last in the action row. */
|
|
38
|
+
actions?: Snippet;
|
|
39
|
+
/** Pinned actions. The body scrolls above it; the caller aligns the row. */
|
|
40
|
+
footer?: Snippet;
|
|
41
|
+
children: Snippet;
|
|
42
|
+
width?: SidePanelWidth;
|
|
43
|
+
/** The body brings its own header: the title is read out but not drawn, and
|
|
44
|
+
the body is neither padded nor scrolled by the panel. */
|
|
45
|
+
bare?: boolean;
|
|
46
|
+
/** Fires once when the panel closes, from the close button, Escape or the overlay. */
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
}
|
|
49
|
+
let {
|
|
50
|
+
open = $bindable(),
|
|
51
|
+
title,
|
|
52
|
+
titleClass,
|
|
53
|
+
description,
|
|
54
|
+
badges = [],
|
|
55
|
+
openIn = [],
|
|
56
|
+
handoff,
|
|
57
|
+
stats = [],
|
|
58
|
+
actions,
|
|
59
|
+
footer,
|
|
60
|
+
children,
|
|
61
|
+
width = "sm",
|
|
62
|
+
bare = false,
|
|
63
|
+
onClose,
|
|
64
|
+
}: Props = $props();
|
|
65
|
+
|
|
66
|
+
// =========================
|
|
67
|
+
// STATE
|
|
68
|
+
// =========================
|
|
69
|
+
let panel = $state<HTMLElement | null>(null);
|
|
70
|
+
|
|
71
|
+
// =========================
|
|
72
|
+
// DERIVED
|
|
73
|
+
// =========================
|
|
74
|
+
let hasRow = $derived(badges.length > 0 || openIn.length > 0 || Boolean(handoff || actions));
|
|
75
|
+
const STAT_TONE = {
|
|
76
|
+
default: "",
|
|
77
|
+
muted: "text-muted-foreground",
|
|
78
|
+
good: "text-success",
|
|
79
|
+
bad: "text-destructive",
|
|
80
|
+
} as const;
|
|
81
|
+
const WIDTH: Record<SidePanelWidth, string> = {
|
|
82
|
+
sm: "sm:max-w-sm",
|
|
83
|
+
md: "sm:max-w-md",
|
|
84
|
+
lg: "sm:max-w-xl",
|
|
85
|
+
xl: "sm:max-w-2xl",
|
|
86
|
+
"2xl": "sm:max-w-3xl",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// =========================
|
|
90
|
+
// HANDLERS
|
|
91
|
+
// =========================
|
|
92
|
+
// Focus goes to the panel unless a text field comes first, or the first control draws
|
|
93
|
+
// a ring on a record nobody touched.
|
|
94
|
+
const FIRST_CONTROL =
|
|
95
|
+
'a[href], button:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
|
|
96
|
+
function onOpenAutoFocus(event: Event) {
|
|
97
|
+
if (!panel || panel.querySelector(FIRST_CONTROL)?.matches("input, textarea, select")) return;
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
panel.focus();
|
|
100
|
+
}
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<!-- =========================== -->
|
|
104
|
+
<!-- MARKUP -->
|
|
105
|
+
<!-- =========================== -->
|
|
106
|
+
<Sheet.Root
|
|
107
|
+
bind:open
|
|
108
|
+
onOpenChange={(next) => {
|
|
109
|
+
if (!next) onClose?.();
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Sheet.Content
|
|
113
|
+
side="right"
|
|
114
|
+
bind:ref={panel}
|
|
115
|
+
tabindex={-1}
|
|
116
|
+
class={cn("flex w-full flex-col gap-0 p-0", WIDTH[width], bare && "overflow-hidden")}
|
|
117
|
+
{onOpenAutoFocus}
|
|
118
|
+
>
|
|
119
|
+
{#if bare}
|
|
120
|
+
<Sheet.Header class="sr-only">
|
|
121
|
+
<Sheet.Title>{title}</Sheet.Title>
|
|
122
|
+
</Sheet.Header>
|
|
123
|
+
{@render children()}
|
|
124
|
+
{:else}
|
|
125
|
+
<Sheet.Header class="gap-1 border-b border-border">
|
|
126
|
+
<!-- WHY the right padding: the close button sits over this corner and
|
|
127
|
+
a long title runs straight under it. -->
|
|
128
|
+
<Sheet.Title class={cn("pr-9 text-base leading-snug", titleClass)}>{title}</Sheet.Title>
|
|
129
|
+
{#if description}
|
|
130
|
+
<Sheet.Description>{description}</Sheet.Description>
|
|
131
|
+
{/if}
|
|
132
|
+
{#if hasRow}
|
|
133
|
+
<div class="flex items-center justify-between gap-3 pt-1.5">
|
|
134
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
|
135
|
+
{#each badges as badge (`${badge.tone}:${badge.label}`)}
|
|
136
|
+
<StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
140
|
+
{#each openIn as door (door.href)}
|
|
141
|
+
<OpenIn
|
|
142
|
+
tool={door.tool}
|
|
143
|
+
href={door.href}
|
|
144
|
+
title={door.label}
|
|
145
|
+
label={door.text}
|
|
146
|
+
variant="button"
|
|
147
|
+
size="sm"
|
|
148
|
+
/>
|
|
149
|
+
{/each}
|
|
150
|
+
{#if handoff}
|
|
151
|
+
<CopyButton
|
|
152
|
+
agent
|
|
153
|
+
value={handoff.value}
|
|
154
|
+
label={handoff.label}
|
|
155
|
+
copiedLabel={handoff.copiedLabel}
|
|
156
|
+
/>
|
|
157
|
+
{/if}
|
|
158
|
+
{@render actions?.()}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
{#if stats.length > 0}
|
|
163
|
+
<dl class="flex flex-wrap gap-x-6 gap-y-3 pt-3">
|
|
164
|
+
{#each stats as stat (stat.label)}
|
|
165
|
+
<div class="min-w-0">
|
|
166
|
+
<dt class="text-xs text-muted-foreground">{stat.label}</dt>
|
|
167
|
+
<dd
|
|
168
|
+
class={cn("text-sm font-medium tabular-nums", STAT_TONE[stat.tone ?? "default"])}
|
|
169
|
+
>
|
|
170
|
+
{stat.value}
|
|
171
|
+
{#if stat.sub}
|
|
172
|
+
<span class="text-xs font-normal text-muted-foreground">{stat.sub}</span>
|
|
173
|
+
{/if}
|
|
174
|
+
</dd>
|
|
175
|
+
</div>
|
|
176
|
+
{/each}
|
|
177
|
+
</dl>
|
|
178
|
+
{/if}
|
|
179
|
+
</Sheet.Header>
|
|
180
|
+
|
|
181
|
+
<!-- WHY the body pads and spaces its sections itself: a panel that lets the
|
|
182
|
+
caller choose drifts, one with a grey band and one with a white page. -->
|
|
183
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
184
|
+
{@render children()}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{#if footer}
|
|
188
|
+
<Sheet.Footer class="mt-0 flex-row items-center justify-end border-t border-border">
|
|
189
|
+
{@render footer()}
|
|
190
|
+
</Sheet.Footer>
|
|
191
|
+
{/if}
|
|
192
|
+
{/if}
|
|
193
|
+
</Sheet.Content>
|
|
194
|
+
</Sheet.Root>
|