@heyramzi/ui 2.1.0 → 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 +7 -8
- 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/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 +5 -1
- 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/section-header.test.ts +7 -1
- package/src/components/section-header/section-label.svelte +20 -20
- 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/toolbar-search.svelte +25 -25
- package/src/components/toolbar/toolbar.svelte +11 -7
- 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 +1 -3
- package/src/inbox/inbox.test.ts +181 -138
- package/src/inbox/index.ts +20 -20
- package/src/inbox/logic.ts +114 -111
- package/src/inbox/thread-inbox.svelte +227 -151
- package/src/inbox/types.ts +130 -108
- 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 +79 -69
- 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/side-panel.svelte +94 -93
- package/src/record/demo-data.ts +291 -225
- package/src/record/field-section.svelte +38 -34
- package/src/record/fields.ts +148 -96
- package/src/record/format.ts +63 -56
- package/src/record/index.ts +49 -35
- package/src/record/info-list.svelte +145 -114
- package/src/record/kinds.ts +31 -31
- package/src/record/logic.ts +216 -214
- package/src/record/next-up.svelte +45 -34
- package/src/record/project-list.svelte +56 -50
- package/src/record/record-demo.svelte +130 -110
- package/src/record/record-header.svelte +37 -28
- package/src/record/record-shell.svelte +54 -54
- package/src/record/record-stats.svelte +25 -25
- package/src/record/record-timeline.svelte +134 -114
- package/src/record/record.test.ts +430 -309
- package/src/record/steps-rail.svelte +225 -216
- package/src/record/types.ts +234 -215
- package/src/record/where-we-are.svelte +62 -58
- 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/define-statuses.ts +17 -24
- package/src/status/index.ts +5 -5
- package/src/status/status-badge.svelte +3 -1
- package/src/status/status-dot.svelte +1 -3
- package/src/status/status-icon.svelte +1 -1
- package/src/status/status-types.ts +21 -21
- package/src/tasks/board-order.ts +2 -4
- package/src/tasks/demo.svelte +189 -31
- package/src/tasks/detail/detail-demo.svelte +73 -13
- 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 +3 -1
- package/src/tasks/gantt/logic.test.ts +110 -20
- package/src/tasks/gantt/logic.ts +42 -6
- package/src/tasks/inbox/inbox-card.svelte +13 -5
- 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 +23 -23
- 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 +49 -52
- package/src/tasks/kanban-board.svelte +16 -12
- package/src/tasks/kanban-types.ts +2 -3
- package/src/tasks/presets/sales.ts +6 -6
- 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/index.ts +7 -7
- package/src/thread/logic.ts +36 -33
- package/src/thread/thread.svelte +82 -16
- package/src/thread/thread.test.ts +58 -47
- package/src/thread/types.ts +68 -68
- 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/tasks/inbox/inbox.svelte +0 -203
|
@@ -20,9 +20,24 @@
|
|
|
20
20
|
route: "/demo/board",
|
|
21
21
|
kind: "steps",
|
|
22
22
|
steps: [
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
{
|
|
24
|
+
target: '[data-tour="demo-new"]',
|
|
25
|
+
title: "Start here",
|
|
26
|
+
body: "A new project takes a name and a client. That's all.",
|
|
27
|
+
placement: "bottom-end",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
target: '[data-tour="demo-filter"]',
|
|
31
|
+
title: "Narrow it down",
|
|
32
|
+
body: "Late shows what slipped. Mine shows what's yours.",
|
|
33
|
+
placement: "bottom-start",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
target: '[data-tour="demo-list"]',
|
|
37
|
+
title: "Open a row",
|
|
38
|
+
body: "Click a project to see its tasks, files and notes.",
|
|
39
|
+
placement: "top",
|
|
40
|
+
},
|
|
26
41
|
],
|
|
27
42
|
},
|
|
28
43
|
]);
|
|
@@ -55,8 +70,12 @@
|
|
|
55
70
|
|
|
56
71
|
<div class="flex flex-col gap-4">
|
|
57
72
|
<div class="flex flex-wrap gap-2">
|
|
58
|
-
<Button variant="outline" size="sm" onclick={() => start("/demo/board")}
|
|
59
|
-
|
|
73
|
+
<Button variant="outline" size="sm" onclick={() => start("/demo/board")}
|
|
74
|
+
>Show the page tour</Button
|
|
75
|
+
>
|
|
76
|
+
<Button variant="outline" size="sm" onclick={() => start("/demo/intro")}
|
|
77
|
+
>Show the intro card</Button
|
|
78
|
+
>
|
|
60
79
|
</div>
|
|
61
80
|
|
|
62
81
|
<div class="bg-card ring-foreground/10 flex flex-col gap-4 rounded-xl p-4 ring-1">
|
package/src/onboarding/index.ts
CHANGED
|
@@ -2,11 +2,4 @@ export { default as OnboardingProvider } from "./onboarding-provider.svelte";
|
|
|
2
2
|
export { onboarding } from "./onboarding.svelte.js";
|
|
3
3
|
export { defineTours } from "./define-tours.js";
|
|
4
4
|
export { localStorageStore, memoryStore } from "./stores.js";
|
|
5
|
-
export type {
|
|
6
|
-
Outcome,
|
|
7
|
-
Placement,
|
|
8
|
-
SeenStore,
|
|
9
|
-
Step,
|
|
10
|
-
Strings,
|
|
11
|
-
Tour,
|
|
12
|
-
} from "./types.js";
|
|
5
|
+
export type { Outcome, Placement, SeenStore, Step, Strings, Tour } from "./types.js";
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
import { tryCatchAsync } from "../lib/result.js";
|
|
3
3
|
import { untrack } from "svelte";
|
|
4
4
|
import { onboarding } from "./onboarding.svelte.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
keepFoundSteps,
|
|
7
|
+
pickForReplay,
|
|
8
|
+
pickTour,
|
|
9
|
+
toursForRoute,
|
|
10
|
+
type SeenMap,
|
|
11
|
+
} from "./pick.js";
|
|
6
12
|
import TourRunner from "./tour-runner.svelte";
|
|
7
13
|
import type { Outcome, SeenStore, Step, Strings, Tour } from "./types.js";
|
|
8
14
|
import { waitForElement } from "./wait.js";
|
|
@@ -71,17 +77,23 @@
|
|
|
71
77
|
controller = mine;
|
|
72
78
|
void loadSeen().then((map) => {
|
|
73
79
|
if (!map || mine.signal.aborted) return;
|
|
74
|
-
const tour = pickTour(
|
|
80
|
+
const tour = pickTour(
|
|
81
|
+
untrack(() => tours),
|
|
82
|
+
id,
|
|
83
|
+
map,
|
|
84
|
+
);
|
|
75
85
|
if (tour) void launch(tour, false, mine.signal);
|
|
76
86
|
});
|
|
77
87
|
return () => mine.abort();
|
|
78
88
|
});
|
|
79
89
|
|
|
80
|
-
$effect(() =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
$effect(() =>
|
|
91
|
+
onboarding.bind(async (tourId) => {
|
|
92
|
+
const tour = pickForReplay(tours, routeId, tourId);
|
|
93
|
+
if (!tour || !controller || controller.signal.aborted) return false;
|
|
94
|
+
return launch(tour, true, controller.signal);
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
85
97
|
|
|
86
98
|
$effect(() => {
|
|
87
99
|
onboarding.running = active !== null;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
type Replay = (tourId?: string) => Promise<boolean>;
|
|
2
2
|
|
|
3
3
|
class Onboarding {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
/** True when the current page has a tour, so a menu item can hide itself. */
|
|
5
|
+
hasTour = $state(false);
|
|
6
|
+
/** True while a tour is on screen. */
|
|
7
|
+
running = $state(false);
|
|
8
|
+
#replay: Replay | null = null;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/** The provider registers itself here. Returns the undo for cleanup. */
|
|
11
|
+
bind(replay: Replay): () => void {
|
|
12
|
+
this.#replay = replay;
|
|
13
|
+
return () => {
|
|
14
|
+
if (this.#replay === replay) this.#replay = null;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/** Restarts this page's tour, or the one named. Writes no seen-state. False when nothing started. */
|
|
19
|
+
replay(tourId?: string): Promise<boolean> {
|
|
20
|
+
return this.#replay ? this.#replay(tourId) : Promise.resolve(false);
|
|
21
|
+
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const onboarding = new Onboarding();
|
|
@@ -1,79 +1,88 @@
|
|
|
1
1
|
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isSeen,
|
|
4
|
+
keepFoundSteps,
|
|
5
|
+
pickForReplay,
|
|
6
|
+
pickTour,
|
|
7
|
+
toursForRoute,
|
|
8
|
+
} from "./pick.js";
|
|
3
9
|
import type { Tour } from "./types.js";
|
|
4
10
|
|
|
5
11
|
const step = { target: "#a", title: "A", body: "a" };
|
|
6
12
|
const tours: Tour[] = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
{ id: "home", route: "/(app)", kind: "intro", title: "Hi", body: "Welcome." },
|
|
14
|
+
{ id: "board", route: "/(app)/projects", kind: "steps", steps: [step] },
|
|
15
|
+
{ id: "board-extra", route: "/(app)/projects", kind: "steps", steps: [step], version: 2 },
|
|
16
|
+
{ id: "record", route: "/(app)/projects/[id]", kind: "steps", steps: [step] },
|
|
11
17
|
];
|
|
12
18
|
|
|
13
19
|
describe("isSeen", () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
it("counts a tour as seen once the stored version reaches its own", () => {
|
|
21
|
+
expect(isSeen(tours[1], {})).toBe(false);
|
|
22
|
+
expect(isSeen(tours[1], { board: 1 })).toBe(true);
|
|
23
|
+
expect(isSeen(tours[2], { "board-extra": 1 })).toBe(false);
|
|
24
|
+
expect(isSeen(tours[2], { "board-extra": 2 })).toBe(true);
|
|
25
|
+
});
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
it("shows a tour again after its version is bumped", () => {
|
|
28
|
+
const v1: Tour = { id: "t", route: "/", kind: "steps", steps: [step] };
|
|
29
|
+
const v2: Tour = { ...v1, version: 2 };
|
|
30
|
+
expect(isSeen(v1, { t: 1 })).toBe(true);
|
|
31
|
+
expect(isSeen(v2, { t: 1 })).toBe(false);
|
|
32
|
+
});
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
describe("pickTour", () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
it("matches the route id exactly, so a params route never borrows its parent's tour", () => {
|
|
37
|
+
expect(pickTour(tours, "/(app)", {})?.id).toBe("home");
|
|
38
|
+
expect(pickTour(tours, "/(app)/projects/[id]", {})?.id).toBe("record");
|
|
39
|
+
expect(pickTour(tours, "/(app)/settings", {})).toBeUndefined();
|
|
40
|
+
});
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
it("takes the first unseen tour on a route, one at a time", () => {
|
|
43
|
+
expect(pickTour(tours, "/(app)/projects", {})?.id).toBe("board");
|
|
44
|
+
expect(pickTour(tours, "/(app)/projects", { board: 1 })?.id).toBe("board-extra");
|
|
45
|
+
expect(pickTour(tours, "/(app)/projects", { board: 1, "board-extra": 2 })).toBeUndefined();
|
|
46
|
+
});
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
it("picks nothing without a route", () => {
|
|
49
|
+
expect(pickTour(tours, null, {})).toBeUndefined();
|
|
50
|
+
expect(pickTour(tours, undefined, {})).toBeUndefined();
|
|
51
|
+
});
|
|
46
52
|
});
|
|
47
53
|
|
|
48
54
|
describe("toursForRoute", () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
it("lists every tour on the route in definition order", () => {
|
|
56
|
+
expect(toursForRoute(tours, "/(app)/projects").map((t) => t.id)).toEqual([
|
|
57
|
+
"board",
|
|
58
|
+
"board-extra",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
52
61
|
});
|
|
53
62
|
|
|
54
63
|
describe("pickForReplay", () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
it("restarts the page's first tour even when it was seen", () => {
|
|
65
|
+
expect(pickForReplay(tours, "/(app)/projects")?.id).toBe("board");
|
|
66
|
+
});
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
it("finds a tour by id on any route", () => {
|
|
69
|
+
expect(pickForReplay(tours, "/(app)", "record")?.id).toBe("record");
|
|
70
|
+
expect(pickForReplay(tours, "/(app)", "missing")).toBeUndefined();
|
|
71
|
+
});
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
describe("keepFoundSteps", () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
const steps = [
|
|
76
|
+
{ ...step, target: "#a" },
|
|
77
|
+
{ ...step, target: "#b" },
|
|
78
|
+
{ ...step, target: "#c" },
|
|
79
|
+
];
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
it("drops the steps whose target isn't on the page", () => {
|
|
82
|
+
expect(keepFoundSteps(steps, (s) => s !== "#b").map((s) => s.target)).toEqual(["#a", "#c"]);
|
|
83
|
+
});
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
it("returns nothing when no target is found, so no empty spotlight starts", () => {
|
|
86
|
+
expect(keepFoundSteps(steps, () => false)).toEqual([]);
|
|
87
|
+
});
|
|
79
88
|
});
|
package/src/onboarding/pick.ts
CHANGED
|
@@ -4,33 +4,36 @@ export type SeenMap = Record<string, number>;
|
|
|
4
4
|
|
|
5
5
|
/** The seen version has to reach the tour's own, so bumping `version` shows it again. */
|
|
6
6
|
export function isSeen(tour: Tour, seen: SeenMap): boolean {
|
|
7
|
-
|
|
7
|
+
return (seen[tour.id] ?? 0) >= (tour.version ?? 1);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function toursForRoute(tours: readonly Tour[], routeId: string | null | undefined): Tour[] {
|
|
11
|
-
|
|
11
|
+
return routeId ? tours.filter((tour) => tour.route === routeId) : [];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** The first unseen tour on a route, in the order they were defined. One per page view. */
|
|
15
15
|
export function pickTour(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
tours: readonly Tour[],
|
|
17
|
+
routeId: string | null | undefined,
|
|
18
|
+
seen: SeenMap,
|
|
19
19
|
): Tour | undefined {
|
|
20
|
-
|
|
20
|
+
return toursForRoute(tours, routeId).find((tour) => !isSeen(tour, seen));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** What replay starts: a named tour, or the page's first one whether or not it was seen. */
|
|
24
24
|
export function pickForReplay(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
tours: readonly Tour[],
|
|
26
|
+
routeId: string | null | undefined,
|
|
27
|
+
tourId?: string,
|
|
28
28
|
): Tour | undefined {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (tourId) return tours.find((tour) => tour.id === tourId);
|
|
30
|
+
return toursForRoute(tours, routeId)[0];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Keeps the steps whose target is on the page. A tour with none left starts nothing. */
|
|
34
|
-
export function keepFoundSteps(
|
|
35
|
-
|
|
34
|
+
export function keepFoundSteps(
|
|
35
|
+
steps: readonly Step[],
|
|
36
|
+
exists: (selector: string) => boolean,
|
|
37
|
+
): Step[] {
|
|
38
|
+
return steps.filter((step) => exists(step.target));
|
|
36
39
|
}
|
|
@@ -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
|
}
|