@heyramzi/ui 1.54.2 → 2.0.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/COMPONENTS.md +3 -2
- package/README.md +34 -9
- package/package.json +205 -13
- package/scripts/sync-shadcn.mjs +2 -0
- package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
- package/src/ambient.d.ts +8 -0
- package/src/brand/brand.test.ts +67 -0
- package/src/brand/index.ts +137 -0
- package/src/canvas/README.md +236 -0
- package/src/canvas/card/card-frame.svelte +61 -0
- package/src/canvas/card/card.css +39 -0
- package/src/canvas/card/resize-handles.svelte +119 -0
- package/src/canvas/card/resize.ts +33 -0
- package/src/canvas/chrome/chrome-zoom.svelte +120 -0
- package/src/canvas/chrome/chrome.css +231 -0
- package/src/canvas/chrome/lock-badge.svelte +18 -0
- package/src/canvas/chrome/side-handles.svelte +30 -0
- package/src/canvas/chrome/size-chip.svelte +45 -0
- package/src/canvas/chrome/snap-guides.svelte +69 -0
- package/src/canvas/components/icons/ToolIcon.svelte +30 -0
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
- package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
- package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
- package/src/canvas/edges/bend-handle.svelte +76 -0
- package/src/canvas/edges/edge-gradient.svelte +30 -0
- package/src/canvas/edges/edge-path.test.ts +58 -0
- package/src/canvas/edges/edge-path.ts +293 -0
- package/src/canvas/edges/gradient-edge.svelte +43 -0
- package/src/canvas/editor/context-menu.test.ts +65 -0
- package/src/canvas/editor/context-menu.ts +48 -0
- package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
- package/src/canvas/editor/dispatch.svelte.ts +205 -0
- package/src/canvas/editor/editor-context.ts +62 -0
- package/src/canvas/editor/editor-mode.ts +67 -0
- package/src/canvas/editor/frame.ts +98 -0
- package/src/canvas/editor/hand-context.ts +15 -0
- package/src/canvas/editor/history-core.test.ts +239 -0
- package/src/canvas/editor/history-core.ts +247 -0
- package/src/canvas/editor/keymap.test.ts +161 -0
- package/src/canvas/editor/keymap.ts +151 -0
- package/src/canvas/editor/layout-actions.test.ts +156 -0
- package/src/canvas/editor/layout-actions.ts +219 -0
- package/src/canvas/editor/lock.test.ts +68 -0
- package/src/canvas/editor/lock.ts +87 -0
- package/src/canvas/editor/rebase.test.ts +208 -0
- package/src/canvas/editor/rebase.ts +272 -0
- package/src/canvas/editor/style.ts +184 -0
- package/src/canvas/editor/text-field-context.ts +41 -0
- package/src/canvas/editor/z-order.test.ts +75 -0
- package/src/canvas/editor/z-order.ts +87 -0
- package/src/canvas/headless/alignment.test.ts +59 -0
- package/src/canvas/headless/alignment.ts +23 -0
- package/src/canvas/headless/break-apart-find.ts +418 -0
- package/src/canvas/headless/break-apart-pieces.ts +104 -0
- package/src/canvas/headless/break-apart-plan.ts +439 -0
- package/src/canvas/headless/break-apart-style.ts +106 -0
- package/src/canvas/headless/break-apart.ts +244 -0
- package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
- package/src/canvas/headless/canvas-cleanup.ts +102 -0
- package/src/canvas/headless/edge-anchors.test.ts +70 -0
- package/src/canvas/headless/edge-anchors.ts +143 -0
- package/src/canvas/headless/edge-memo.test.ts +58 -0
- package/src/canvas/headless/edge-memo.ts +32 -0
- package/src/canvas/headless/edge-serialization.test.ts +55 -0
- package/src/canvas/headless/edge-serialization.ts +32 -0
- package/src/canvas/headless/figure-geometry.ts +293 -0
- package/src/canvas/headless/fit-viewport.test.ts +85 -0
- package/src/canvas/headless/fit-viewport.ts +137 -0
- package/src/canvas/headless/grid.test.ts +34 -0
- package/src/canvas/headless/grid.ts +94 -0
- package/src/canvas/headless/icon-node.test.ts +23 -0
- package/src/canvas/headless/icon-node.ts +66 -0
- package/src/canvas/headless/index.ts +9 -0
- package/src/canvas/headless/linkify.test.ts +23 -0
- package/src/canvas/headless/linkify.ts +25 -0
- package/src/canvas/headless/pen-marks.ts +133 -0
- package/src/canvas/headless/sketch.test.ts +99 -0
- package/src/canvas/headless/sketch.ts +302 -0
- package/src/canvas/headless/snap-guides.test.ts +61 -0
- package/src/canvas/headless/snap-guides.ts +146 -0
- package/src/canvas/headless/svg-figure.test.ts +49 -0
- package/src/canvas/headless/svg-figure.ts +176 -0
- package/src/canvas/headless/table-layout.test.ts +158 -0
- package/src/canvas/headless/table-layout.ts +309 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/lod/lod-mode.svelte.ts +30 -0
- package/src/canvas/lod/lod-switch.svelte +140 -0
- package/src/canvas/lod/render-all.svelte.ts +52 -0
- package/src/canvas/ops/arrange.ts +108 -0
- package/src/canvas/ops/ops.test.ts +98 -0
- package/src/canvas/ops/ops.ts +347 -0
- package/src/canvas/pen/PenField.svelte +128 -0
- package/src/canvas/pen/PenField.test.ts +87 -0
- package/src/canvas/pen/PenOverlay.svelte +172 -0
- package/src/canvas/pen/ShapeNode.svelte +64 -0
- package/src/canvas/pen/TextNode.svelte +97 -0
- package/src/canvas/pen/colors.ts +28 -0
- package/src/canvas/pen/eraser.ts +46 -0
- package/src/canvas/pen/geometry.ts +148 -0
- package/src/canvas/pen/host-field.test.svelte +13 -0
- package/src/canvas/pen/index.ts +40 -0
- package/src/canvas/pen/ops.ts +16 -0
- package/src/canvas/pen/pen-session.ts +177 -0
- package/src/canvas/pen/pen.css +149 -0
- package/src/canvas/pen/plugin.ts +139 -0
- package/src/canvas/pen/shapes.ts +104 -0
- package/src/canvas/pen/stroke-path.ts +54 -0
- package/src/canvas/pen/types.ts +96 -0
- package/src/canvas/pen/ui.ts +26 -0
- package/src/canvas/plugin.test.ts +45 -0
- package/src/canvas/plugin.ts +159 -0
- package/src/canvas/screen/GlyphIcon.svelte +25 -0
- package/src/canvas/screen/PartView.svelte +176 -0
- package/src/canvas/screen/ScreenBody.svelte +142 -0
- package/src/canvas/screen/ScreenCard.svelte +32 -0
- package/src/canvas/screen/glyphs/button.svg +1 -0
- package/src/canvas/screen/glyphs/close.svg +1 -0
- package/src/canvas/screen/glyphs/down.svg +1 -0
- package/src/canvas/screen/glyphs/header.svg +1 -0
- package/src/canvas/screen/glyphs/hills.svg +1 -0
- package/src/canvas/screen/glyphs/image.svg +1 -0
- package/src/canvas/screen/glyphs/input.svg +1 -0
- package/src/canvas/screen/glyphs/list.svg +1 -0
- package/src/canvas/screen/glyphs/modal.svg +1 -0
- package/src/canvas/screen/glyphs/plus.svg +1 -0
- package/src/canvas/screen/glyphs/up.svg +1 -0
- package/src/canvas/screen/glyphs.ts +40 -0
- package/src/canvas/screen/labels.ts +41 -0
- package/src/canvas/screen/parts.ts +164 -0
- package/src/canvas/screen/plugin.ts +67 -0
- package/src/canvas/screen/screen.css +593 -0
- package/src/canvas/screen/types.ts +47 -0
- package/src/canvas/screen/ui.ts +13 -0
- package/src/canvas/test-utils.ts +18 -0
- package/src/canvas/types.ts +53 -0
- package/src/canvas/ui/CanvasToolbar.svelte +169 -0
- package/src/canvas/ui/FrameLayer.svelte +116 -0
- package/src/canvas/ui/PenLayer.svelte +39 -0
- package/src/canvas/ui/StylePanel.svelte +268 -0
- package/src/canvas/ui/canvas-grid.svelte +156 -0
- package/src/canvas/ui/canvas-ui.css +350 -0
- package/src/canvas/ui/forward-wheel.ts +29 -0
- package/src/canvas/ui/icons.ts +124 -0
- package/src/canvas/ui/place-panel.ts +80 -0
- package/src/canvas/ui/shortcut-tip.test.ts +16 -0
- package/src/canvas/ui/shortcut-tip.ts +17 -0
- package/src/canvas/ui/tools.ts +146 -0
- package/src/charts/README.md +104 -0
- package/src/charts/ambient.d.ts +4 -0
- package/src/charts/animated-value.css +7 -0
- package/src/charts/animated-value.svelte +110 -0
- package/src/charts/area-chart.css +29 -0
- package/src/charts/area-chart.svelte +204 -0
- package/src/charts/bar-chart.css +119 -0
- package/src/charts/bar-chart.svelte +275 -0
- package/src/charts/charts.css +201 -0
- package/src/charts/crosshair.svelte +35 -0
- package/src/charts/dashboard/chart.ts +5 -0
- package/src/charts/dashboard/dashboard-empty.svelte +55 -0
- package/src/charts/dashboard/dashboard-section.svelte +58 -0
- package/src/charts/dashboard/data-panel.svelte +81 -0
- package/src/charts/dashboard/data-sort-head.svelte +102 -0
- package/src/charts/dashboard/data-table.svelte +278 -0
- package/src/charts/dashboard/data-table.ts +26 -0
- package/src/charts/dashboard/index.ts +25 -0
- package/src/charts/dashboard/panel-grid.svelte +43 -0
- package/src/charts/dashboard/period-filter.svelte +53 -0
- package/src/charts/dashboard/rating-chip.svelte +35 -0
- package/src/charts/dashboard/stat-band.svelte +47 -0
- package/src/charts/dashboard/stat-tile.svelte +176 -0
- package/src/charts/dashboard/windows.ts +8 -0
- package/src/charts/donut-chart.css +56 -0
- package/src/charts/donut-chart.svelte +158 -0
- package/src/charts/edge-fade.svelte +43 -0
- package/src/charts/edge-fade.ts +16 -0
- package/src/charts/figure.css +100 -0
- package/src/charts/figure.svelte +122 -0
- package/src/charts/gauge-chart.css +94 -0
- package/src/charts/gauge-chart.svelte +302 -0
- package/src/charts/hover-band.test.ts +30 -0
- package/src/charts/hover-band.ts +21 -0
- package/src/charts/hover-focus.svelte +85 -0
- package/src/charts/index.ts +31 -0
- package/src/charts/legend.css +136 -0
- package/src/charts/legend.svelte +112 -0
- package/src/charts/line-chart.css +26 -0
- package/src/charts/line-chart.svelte +256 -0
- package/src/charts/motion.ts +19 -0
- package/src/charts/number-chart.css +179 -0
- package/src/charts/number-chart.svelte +187 -0
- package/src/charts/primitives/chart-container.svelte +57 -0
- package/src/charts/primitives/chart-style.svelte +36 -0
- package/src/charts/primitives/chart-tooltip.svelte +161 -0
- package/src/charts/primitives/chart-utils.ts +54 -0
- package/src/charts/primitives/index.ts +11 -0
- package/src/charts/reference-line.css +18 -0
- package/src/charts/reference-line.svelte +31 -0
- package/src/charts/reveal.svelte +61 -0
- package/src/charts/ring-chart.css +104 -0
- package/src/charts/ring-chart.svelte +166 -0
- package/src/charts/sparkline.css +45 -0
- package/src/charts/sparkline.svelte +141 -0
- package/src/charts/stat-card.css +105 -0
- package/src/charts/stat-card.svelte +267 -0
- package/src/charts/time-series-chart.css +28 -0
- package/src/charts/time-series-chart.svelte +179 -0
- package/src/charts/trend-pill.css +38 -0
- package/src/charts/trend-pill.svelte +48 -0
- package/src/components/copy/copy-button.svelte +1 -1
- package/src/components/docs/doc-page.svelte +1 -1
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/list-page/empty-state.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/segmented/segment.svelte +4 -4
- package/src/components/theme/theme-provider.svelte +6 -4
- package/src/components/toolbar/index.ts +5 -0
- package/src/components/toolbar/toolbar.svelte +168 -0
- package/src/components/toolbar/toolbar.test.ts +66 -0
- package/src/editor/README.md +58 -0
- package/src/editor/ambient.d.ts +1 -0
- package/src/editor/block-id.ts +77 -0
- package/src/editor/block-ops.test.ts +237 -0
- package/src/editor/block-ops.ts +157 -0
- package/src/editor/content.css +418 -0
- package/src/editor/editor-toolbar.svelte +303 -0
- package/src/editor/editor.svelte +469 -0
- package/src/editor/extensions.ts +145 -0
- package/src/editor/image-toolbar.svelte +123 -0
- package/src/editor/index.ts +17 -0
- package/src/editor/inline-editor.svelte +261 -0
- package/src/editor/markdown-escaping.test.ts +158 -0
- package/src/editor/markdown-marks.test.ts +89 -0
- package/src/editor/markdown-test-helpers.ts +13 -0
- package/src/editor/markdown-youtube.test.ts +93 -0
- package/src/editor/markdown.test.ts +250 -0
- package/src/editor/markdown.ts +511 -0
- package/src/editor/renderer.svelte +92 -0
- package/src/editor/serializer.test.ts +133 -0
- package/src/editor/serializer.ts +370 -0
- package/src/editor/slash-items.ts +115 -0
- package/src/editor/slash-menu.svelte +159 -0
- package/src/editor/youtube-url.ts +35 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
- package/src/gallery.test.ts +20 -1
- package/src/lib/result.ts +19 -0
- package/src/onboarding/README.md +120 -0
- package/src/onboarding/define-tours.test.ts +50 -0
- package/src/onboarding/define-tours.ts +31 -0
- package/src/onboarding/demo.svelte +86 -0
- package/src/onboarding/index.ts +12 -0
- package/src/onboarding/onboarding-provider.svelte +120 -0
- package/src/onboarding/onboarding.svelte.ts +24 -0
- package/src/onboarding/pick.test.ts +79 -0
- package/src/onboarding/pick.ts +36 -0
- package/src/onboarding/stores.test.ts +103 -0
- package/src/onboarding/stores.ts +58 -0
- package/src/onboarding/tour-card.svelte +70 -0
- package/src/onboarding/tour-runner.svelte +197 -0
- package/src/onboarding/types.ts +34 -0
- package/src/onboarding/wait.ts +29 -0
- package/src/shadcn/command/command-input.svelte +1 -1
- package/src/slides/Controls.svelte +138 -0
- package/src/slides/Deck.svelte +271 -0
- package/src/slides/Fragment.svelte +71 -0
- package/src/slides/Overview.svelte +113 -0
- package/src/slides/Presenter.svelte +292 -0
- package/src/slides/README.md +89 -0
- package/src/slides/Slide.svelte +179 -0
- package/src/slides/SlideList.svelte +19 -0
- package/src/slides/ambient.d.ts +1 -0
- package/src/slides/bridges.ts +69 -0
- package/src/slides/deck-controller.svelte.ts +292 -0
- package/src/slides/demo-slides.svelte +358 -0
- package/src/slides/demo.svelte +176 -0
- package/src/slides/export/capture.ts +113 -0
- package/src/slides/export/ffmpeg.ts +120 -0
- package/src/slides/export/index.ts +9 -0
- package/src/slides/export/launch.ts +10 -0
- package/src/slides/export/pdf.ts +152 -0
- package/src/slides/export/types.ts +48 -0
- package/src/slides/index.ts +19 -0
- package/src/slides/labels.ts +48 -0
- package/src/slides/layout.ts +51 -0
- package/src/slides/model.ts +47 -0
- package/src/slides/navigation.ts +100 -0
- package/src/slides/theme.css +205 -0
- package/src/slides/types.ts +86 -0
- package/src/tasks/README.md +312 -0
- package/src/tasks/ambient.d.ts +3 -0
- package/src/tasks/board-order.ts +137 -0
- package/src/tasks/dates.ts +31 -0
- package/src/tasks/define-statuses.ts +32 -0
- package/src/tasks/demo.svelte +389 -0
- package/src/tasks/detail/detail-demo.svelte +186 -0
- package/src/tasks/detail/detail-types.ts +54 -0
- package/src/tasks/detail/task-chip.svelte +28 -0
- package/src/tasks/detail/task-detail.svelte +315 -0
- package/src/tasks/detail/task-property.svelte +17 -0
- package/src/tasks/detail/task-thread.svelte +258 -0
- package/src/tasks/gantt/gantt-bar.svelte +243 -0
- package/src/tasks/gantt/gantt-grid.svelte +94 -0
- package/src/tasks/gantt/gantt-group-row.svelte +55 -0
- package/src/tasks/gantt/gantt-header.svelte +71 -0
- package/src/tasks/gantt/gantt-task-row.svelte +186 -0
- package/src/tasks/gantt/gantt.css +22 -0
- package/src/tasks/gantt/gantt.test.ts +77 -0
- package/src/tasks/gantt/logic.test.ts +163 -0
- package/src/tasks/gantt/logic.ts +248 -0
- package/src/tasks/inbox/inbox-card.svelte +283 -0
- package/src/tasks/inbox/inbox-draft.svelte +84 -0
- package/src/tasks/inbox/inbox-logic.ts +55 -0
- package/src/tasks/inbox/inbox-statuses.ts +48 -0
- package/src/tasks/inbox/inbox-trace.svelte +45 -0
- package/src/tasks/inbox/inbox-types.ts +108 -0
- package/src/tasks/inbox/inbox.svelte +203 -0
- package/src/tasks/inbox/index.ts +23 -0
- package/src/tasks/index.ts +85 -0
- package/src/tasks/kanban-board.svelte +561 -0
- package/src/tasks/kanban-column.svelte +117 -0
- package/src/tasks/kanban-drag.ts +38 -0
- package/src/tasks/kanban-types.ts +65 -0
- package/src/tasks/presets/sales.ts +16 -0
- package/src/tasks/record/demo-data.ts +286 -0
- package/src/tasks/record/field-section.svelte +53 -0
- package/src/tasks/record/fields.ts +142 -0
- package/src/tasks/record/format.ts +87 -0
- package/src/tasks/record/gutter.ts +2 -0
- package/src/tasks/record/index.ts +54 -0
- package/src/tasks/record/info-list.svelte +316 -0
- package/src/tasks/record/kinds.ts +42 -0
- package/src/tasks/record/logic.ts +324 -0
- package/src/tasks/record/next-up.svelte +70 -0
- package/src/tasks/record/project-list.svelte +133 -0
- package/src/tasks/record/record-demo.svelte +324 -0
- package/src/tasks/record/record-header.svelte +70 -0
- package/src/tasks/record/record-shell.svelte +138 -0
- package/src/tasks/record/record-stats.svelte +48 -0
- package/src/tasks/record/record-timeline.svelte +231 -0
- package/src/tasks/record/record.test.ts +350 -0
- package/src/tasks/record/steps-rail.svelte +474 -0
- package/src/tasks/record/types.ts +280 -0
- package/src/tasks/record/where-we-are.svelte +121 -0
- package/src/tasks/reorder.ts +19 -0
- package/src/tasks/side-panel-types.ts +16 -0
- package/src/tasks/side-panel.svelte +193 -0
- package/src/tasks/status-badge.svelte +63 -0
- package/src/tasks/status-dot.svelte +228 -0
- package/src/tasks/status-icon.svelte +30 -0
- package/src/tasks/status-types.ts +40 -0
- package/src/tasks/task-gantt.svelte +230 -0
- package/src/tasks/task-list.svelte +470 -0
- package/src/tasks/task-types.ts +33 -0
- package/src/tasks/time/duration.ts +69 -0
- package/src/tasks/time/time-chip.svelte +62 -0
- package/src/tasks/time/time-dot.svelte +58 -0
- package/src/tasks/time/time-ring.svelte +74 -0
- package/src/tasks/time/time-tracker.svelte +565 -0
- package/src/tasks/time/time-types.ts +74 -0
- package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
- package/src/tokens/base.css +5 -5
- package/src/tokens/palette.css +115 -120
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { SlashItem } from "./slash-items.js";
|
|
6
|
+
|
|
7
|
+
// =========================
|
|
8
|
+
// TYPES
|
|
9
|
+
// =========================
|
|
10
|
+
interface Props {
|
|
11
|
+
items: SlashItem[];
|
|
12
|
+
pos: { top: number; left: number };
|
|
13
|
+
selectedIndex: number;
|
|
14
|
+
/** The menu element, so the parent can close it on an outside click. */
|
|
15
|
+
el?: HTMLDivElement;
|
|
16
|
+
onselect: (item: SlashItem) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// =========================
|
|
20
|
+
// PROPS
|
|
21
|
+
// =========================
|
|
22
|
+
let { items, pos, selectedIndex = $bindable(), el = $bindable(), onselect }: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!-- =========================== -->
|
|
26
|
+
<!-- MARKUP -->
|
|
27
|
+
<!-- =========================== -->
|
|
28
|
+
<div
|
|
29
|
+
bind:this={el}
|
|
30
|
+
class="slash-menu"
|
|
31
|
+
style="top: {pos.top}px; left: {pos.left}px;"
|
|
32
|
+
role="listbox"
|
|
33
|
+
>
|
|
34
|
+
{#each items as item, i}
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="slash-item"
|
|
38
|
+
class:selected={i === selectedIndex}
|
|
39
|
+
onclick={() => onselect(item)}
|
|
40
|
+
onpointerenter={() => selectedIndex = i}
|
|
41
|
+
role="option"
|
|
42
|
+
aria-selected={i === selectedIndex}
|
|
43
|
+
>
|
|
44
|
+
<span class="slash-icon">
|
|
45
|
+
<item.icon size={18} />
|
|
46
|
+
</span>
|
|
47
|
+
<span class="slash-text">
|
|
48
|
+
<span class="slash-label">{item.label}</span>
|
|
49
|
+
<span class="slash-desc">{item.description}</span>
|
|
50
|
+
</span>
|
|
51
|
+
</button>
|
|
52
|
+
{/each}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- =========================== -->
|
|
56
|
+
<!-- STYLE -->
|
|
57
|
+
<!-- =========================== -->
|
|
58
|
+
<style>
|
|
59
|
+
/* ---- SLASH COMMAND MENU ---- */
|
|
60
|
+
.slash-menu {
|
|
61
|
+
position: absolute;
|
|
62
|
+
z-index: 50;
|
|
63
|
+
min-width: 260px;
|
|
64
|
+
max-height: 340px;
|
|
65
|
+
overflow-y: auto;
|
|
66
|
+
padding: 6px;
|
|
67
|
+
background: var(--color-popover);
|
|
68
|
+
border: 1px solid var(--color-border);
|
|
69
|
+
border-radius: var(--radius-xl);
|
|
70
|
+
box-shadow: var(--elev-overlay);
|
|
71
|
+
animation: slash-enter var(--duration-fast) var(--ease-out);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes slash-enter {
|
|
75
|
+
from {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: translateY(-3px) scale(0.98);
|
|
78
|
+
}
|
|
79
|
+
to {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
transform: translateY(0) scale(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.slash-item {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 0.75rem;
|
|
89
|
+
width: 100%;
|
|
90
|
+
padding: 0.5rem 0.625rem;
|
|
91
|
+
border: none;
|
|
92
|
+
border-radius: var(--radius-md);
|
|
93
|
+
background: transparent;
|
|
94
|
+
color: var(--color-foreground);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
text-align: left;
|
|
97
|
+
transition: background-color var(--duration-micro) var(--ease-smooth-out);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.slash-item.selected {
|
|
101
|
+
background: var(--color-accent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (hover: hover) and (pointer: fine) {
|
|
105
|
+
.slash-item:hover {
|
|
106
|
+
background: var(--color-accent);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.slash-icon {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
width: 2rem;
|
|
115
|
+
height: 2rem;
|
|
116
|
+
border-radius: var(--radius-sm);
|
|
117
|
+
background: var(--color-muted);
|
|
118
|
+
color: var(--color-muted-foreground);
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
transition: background-color var(--duration-micro) var(--ease-smooth-out), color var(--duration-micro) var(--ease-smooth-out);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.slash-item.selected .slash-icon {
|
|
124
|
+
background: oklch(from var(--color-brand) l c h / 0.1);
|
|
125
|
+
color: var(--color-brand);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@media (hover: hover) and (pointer: fine) {
|
|
129
|
+
.slash-item:hover .slash-icon {
|
|
130
|
+
background: oklch(from var(--color-brand) l c h / 0.1);
|
|
131
|
+
color: var(--color-brand);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.slash-text {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 0;
|
|
139
|
+
min-width: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.slash-label {
|
|
143
|
+
font-size: 0.8125rem;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
line-height: 1.3;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.slash-desc {
|
|
149
|
+
font-size: 0.6875rem;
|
|
150
|
+
color: var(--color-muted-foreground);
|
|
151
|
+
line-height: 1.3;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (prefers-reduced-motion: reduce) {
|
|
155
|
+
.slash-menu {
|
|
156
|
+
animation: none;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// No markdown-it here, so the editor can import it without the markdown engine.
|
|
2
|
+
|
|
3
|
+
export const YOUTUBE_ID_RE = /^[A-Za-z0-9_-]{11}$/;
|
|
4
|
+
|
|
5
|
+
export type YoutubeRef = { videoId: string; start: number };
|
|
6
|
+
|
|
7
|
+
function toUrl(url: string): URL | null {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function videoIdOf(parsed: URL): string | null {
|
|
16
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
17
|
+
if (host === "youtu.be") return parsed.pathname.slice(1);
|
|
18
|
+
if (host !== "youtube.com" && host !== "m.youtube.com") return null;
|
|
19
|
+
if (parsed.pathname === "/watch") return parsed.searchParams.get("v");
|
|
20
|
+
if (parsed.pathname.startsWith("/embed/")) return parsed.pathname.slice("/embed/".length);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reads youtube.com/watch?v=ID, youtu.be/ID and youtube.com/embed/ID. A `t`
|
|
26
|
+
* param in seconds becomes `start`.
|
|
27
|
+
*/
|
|
28
|
+
export function parseYoutubeUrl(url: string): YoutubeRef | null {
|
|
29
|
+
const parsed = toUrl(url);
|
|
30
|
+
if (!parsed) return null;
|
|
31
|
+
const videoId = videoIdOf(parsed);
|
|
32
|
+
if (!videoId || !YOUTUBE_ID_RE.test(videoId)) return null;
|
|
33
|
+
const t = parseInt(parsed.searchParams.get("t") ?? "", 10);
|
|
34
|
+
return { videoId, start: Number.isFinite(t) && t > 0 ? t : 0 };
|
|
35
|
+
}
|
|
@@ -233,13 +233,13 @@ let classes = $derived(
|
|
|
233
233
|
transition: opacity var(--duration-fast) var(--ease-smooth-out), transform var(--duration-fast) var(--ease-smooth-out);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/*
|
|
237
|
-
label, and
|
|
236
|
+
/* signal-600, not signal-400: the dot becomes the hover fill under a paper
|
|
237
|
+
label, and paper on signal-400 fails AA. It works in both themes. */
|
|
238
238
|
.cta-hover__dot {
|
|
239
239
|
width: 8px;
|
|
240
240
|
height: 8px;
|
|
241
241
|
border-radius: 50%;
|
|
242
|
-
background: var(--color-
|
|
242
|
+
background: var(--color-signal-600);
|
|
243
243
|
flex-shrink: 0;
|
|
244
244
|
transition: transform var(--duration-fast) var(--ease-smooth-out);
|
|
245
245
|
}
|
|
@@ -264,9 +264,9 @@ let classes = $derived(
|
|
|
264
264
|
transition:
|
|
265
265
|
transform var(--duration-fast) var(--ease-smooth-out),
|
|
266
266
|
opacity var(--duration-fast) var(--ease-smooth-out);
|
|
267
|
-
/* The reveal label always lands on the
|
|
267
|
+
/* The reveal label always lands on the signal-600 dot fill, where paper
|
|
268
268
|
clears AA at button-label sizes. */
|
|
269
|
-
color: var(--color-
|
|
269
|
+
color: var(--color-paper);
|
|
270
270
|
pointer-events: none;
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -279,16 +279,16 @@ let classes = $derived(
|
|
|
279
279
|
/* Coral fill, heavier than the ink default. It comes after the .dark rules
|
|
280
280
|
so one set of colors covers both themes. */
|
|
281
281
|
.cta-hover--coral {
|
|
282
|
-
/*
|
|
283
|
-
color: var(--color-
|
|
282
|
+
/* Paper label on signal-600: on signal-400 it fails AA. */
|
|
283
|
+
color: var(--color-paper);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
.cta-hover--coral::after {
|
|
287
|
-
background: var(--color-
|
|
287
|
+
background: var(--color-signal-600);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
.cta-hover--coral.cta-hover--no-beam {
|
|
291
|
-
background: var(--color-
|
|
291
|
+
background: var(--color-signal-600);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
.cta-hover--coral::before,
|
|
@@ -296,17 +296,17 @@ let classes = $derived(
|
|
|
296
296
|
background: conic-gradient(
|
|
297
297
|
transparent 0deg,
|
|
298
298
|
transparent 220deg,
|
|
299
|
-
var(--color-
|
|
300
|
-
var(--color-
|
|
299
|
+
var(--color-paper) 280deg,
|
|
300
|
+
var(--color-signal-200) 320deg,
|
|
301
301
|
transparent 360deg
|
|
302
302
|
);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
/* Ink dot on coral, so hover turns the button black under a
|
|
305
|
+
/* Ink dot on coral, so hover turns the button black under a paper label
|
|
306
306
|
instead of washing it out to cream. */
|
|
307
307
|
.cta-hover--coral .cta-hover__dot,
|
|
308
308
|
:global(.dark) .cta-hover--coral .cta-hover__dot {
|
|
309
|
-
background: var(--color-
|
|
309
|
+
background: var(--color-ink);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
@media (hover: hover) and (pointer: fine) {
|
package/src/gallery.test.ts
CHANGED
|
@@ -28,7 +28,12 @@ const SITE = join(PKG, "..", "..", "site", "src", "lib");
|
|
|
28
28
|
|
|
29
29
|
describe("site gallery coverage", () => {
|
|
30
30
|
it("gives every animation a stage", () => {
|
|
31
|
-
|
|
31
|
+
// Each animation's stage is its own file under demos/motion/.
|
|
32
|
+
const stages = join(SITE, "demos", "motion");
|
|
33
|
+
const section = readdirSync(stages)
|
|
34
|
+
.filter((file) => file.endsWith(".svelte"))
|
|
35
|
+
.map((file) => readFileSync(join(stages, file), "utf8"))
|
|
36
|
+
.join("\n");
|
|
32
37
|
const missing = readdirSync(join(PKG, "src", "animations"))
|
|
33
38
|
.filter((file) => file.endsWith(".css") && file !== "index.css")
|
|
34
39
|
.filter((file) => !section.includes(`animations/${file.replace(".css", "")}`));
|
|
@@ -43,6 +48,20 @@ describe("site gallery coverage", () => {
|
|
|
43
48
|
expect(vendored.filter((name) => !manifest.includes(`name: "${name}"`))).toEqual([]);
|
|
44
49
|
});
|
|
45
50
|
|
|
51
|
+
it("gives every component, AI part and effect a place on the site", () => {
|
|
52
|
+
const walk = (dir: string): string[] =>
|
|
53
|
+
readdirSync(dir, { withFileTypes: true }).flatMap((d) =>
|
|
54
|
+
d.isDirectory() ? walk(join(dir, d.name)) : /\.(svelte|ts)$/.test(d.name) ? [join(dir, d.name)] : [],
|
|
55
|
+
);
|
|
56
|
+
const site = walk(join(SITE, "..")).map((file) => readFileSync(file, "utf8")).join("\n");
|
|
57
|
+
const parts = ["components", "ai", "effects"].flatMap((top) =>
|
|
58
|
+
readdirSync(join(PKG, "src", top), { withFileTypes: true })
|
|
59
|
+
.filter((d) => d.isDirectory())
|
|
60
|
+
.map((d) => `${top}/${d.name}`),
|
|
61
|
+
);
|
|
62
|
+
expect(parts.filter((part) => !site.includes(`${part}/`) && !site.includes(`${part}"`))).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
46
65
|
it("keeps a demo file behind every listed primitive", () => {
|
|
47
66
|
const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
|
|
48
67
|
const listed = [...manifest.matchAll(/name: "([a-z-]+)"/g)].map((m) => m[1]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A small Result type, so a throw becomes a value the caller has to look at.
|
|
2
|
+
|
|
3
|
+
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
|
4
|
+
|
|
5
|
+
export function tryCatch<T>(fn: () => T): Result<T> {
|
|
6
|
+
try {
|
|
7
|
+
return { ok: true, value: fn() };
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T>> {
|
|
14
|
+
try {
|
|
15
|
+
return { ok: true, value: await fn() };
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @heyramzi/ui/onboarding
|
|
2
|
+
|
|
3
|
+
Svelte 5 page tours. A dimmed page, one lit element and a short card per step, shown once per page and
|
|
4
|
+
remembered per person. Live at [ui.heyramzi.com/onboarding](https://ui.heyramzi.com/onboarding).
|
|
5
|
+
|
|
6
|
+
It builds on [`@zag-js/tour`](https://zagjs.com/components/svelte/tour) (MIT) for spotlight geometry, focus and
|
|
7
|
+
keys, and draws the card itself with the rest of `@heyramzi/ui`, so it looks like the rest of your app.
|
|
8
|
+
`phosphor-svelte` and `svelte` are peers.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pnpm add @heyramzi/ui phosphor-svelte
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Define the tours
|
|
15
|
+
|
|
16
|
+
A tour belongs to a SvelteKit route id, so params never matter. `steps` points at elements by CSS selector. `intro`
|
|
17
|
+
is one centred card with no target, for a page's first visit.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { defineTours } from "@heyramzi/ui/onboarding";
|
|
21
|
+
|
|
22
|
+
export const tours = defineTours([
|
|
23
|
+
{
|
|
24
|
+
id: "home",
|
|
25
|
+
route: "/(app)",
|
|
26
|
+
kind: "intro",
|
|
27
|
+
title: "The week in one place",
|
|
28
|
+
body: "Open work sits on the left. Anything late floats to the top.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "projects-board",
|
|
32
|
+
version: 1,
|
|
33
|
+
route: "/(app)/projects",
|
|
34
|
+
kind: "steps",
|
|
35
|
+
steps: [
|
|
36
|
+
{ target: '[data-tour="new-project"]', title: "Start here", body: "A name and a client. That's all." },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`defineTours` throws on a repeated id, a route that isn't a route id, and a `steps` tour with fewer than 1 or more
|
|
43
|
+
than 6 steps. Past 6, people skip.
|
|
44
|
+
|
|
45
|
+
## Mount it once
|
|
46
|
+
|
|
47
|
+
Put the provider in the root layout, next to the page slot.
|
|
48
|
+
|
|
49
|
+
```svelte
|
|
50
|
+
<script lang="ts">
|
|
51
|
+
import { page } from "$app/state";
|
|
52
|
+
import { OnboardingProvider, localStorageStore } from "@heyramzi/ui/onboarding";
|
|
53
|
+
import { tours } from "$lib/tours";
|
|
54
|
+
|
|
55
|
+
const strings = {
|
|
56
|
+
next: "Next",
|
|
57
|
+
back: "Back",
|
|
58
|
+
done: "Got it",
|
|
59
|
+
skip: "Skip the tour",
|
|
60
|
+
progress: (n: number, total: number) => `Step ${n} of ${total}`,
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<OnboardingProvider {tours} store={localStorageStore()} {strings} routeId={page.route.id} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On every navigation it picks the first unseen tour for the route, waits up to 2 seconds for the first step's target,
|
|
68
|
+
drops steps whose target never shows, and starts. A tour with no target on the page starts nothing. One tour per page
|
|
69
|
+
view, never two stacked. Strings are yours, so translate them before you pass them in.
|
|
70
|
+
|
|
71
|
+
| Prop | Default | |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `tours` | | From `defineTours`. |
|
|
74
|
+
| `store` | | A `SeenStore`. |
|
|
75
|
+
| `strings` | | `next`, `back`, `done`, `skip`, `progress(n, total)`. |
|
|
76
|
+
| `routeId` | | `page.route.id`. |
|
|
77
|
+
| `enabled` | `true` | Set `false` while signed out. |
|
|
78
|
+
| `replayKey` | `true` | Press `?` to replay. |
|
|
79
|
+
| `waitMs` | `2000` | How long to wait for the first target. |
|
|
80
|
+
|
|
81
|
+
## Replay
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { onboarding } from "@heyramzi/ui/onboarding";
|
|
85
|
+
|
|
86
|
+
onboarding.replay(); // this page's tour
|
|
87
|
+
onboarding.replay("home"); // a named tour
|
|
88
|
+
onboarding.hasTour; // true when this page has one
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replay writes no seen-state. `onboarding.hasTour` is reactive, so a "Show me around" menu item can hide itself on
|
|
92
|
+
pages without a tour.
|
|
93
|
+
|
|
94
|
+
## Seen-state
|
|
95
|
+
|
|
96
|
+
`SeenStore` is three methods. `load` runs once per session and returns tour id to the version seen.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
interface SeenStore {
|
|
100
|
+
load(): Promise<Record<string, number>>;
|
|
101
|
+
markSeen(tourId: string, version: number, outcome: "completed" | "dismissed"): Promise<void>;
|
|
102
|
+
reset(tourId?: string): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The store keeps a version, not a boolean. Raise a tour's `version` after the page changes and everyone sees it
|
|
107
|
+
again. `localStorageStore(key?)` and `memoryStore(initial?)` come with the package and also live at
|
|
108
|
+
`@heyramzi/ui/onboarding/stores`. Server stores belong in the app: a table keyed by user and tour id, and a `fetch`
|
|
109
|
+
adapter.
|
|
110
|
+
|
|
111
|
+
If `load` rejects, no tour shows that session.
|
|
112
|
+
|
|
113
|
+
## Phones and motion
|
|
114
|
+
|
|
115
|
+
Under 640px every step is a bottom sheet over the page, and the target scrolls above it. With
|
|
116
|
+
`prefers-reduced-motion` the spotlight and card stop animating.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { defineTours } from "./define-tours.js";
|
|
3
|
+
import type { Step, Tour } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const step = (n: number): Step => ({ target: `[data-tour="s${n}"]`, title: `Title ${n}`, body: `Body ${n}` });
|
|
6
|
+
const steps = (count: number) => Array.from({ length: count }, (_, i) => step(i + 1));
|
|
7
|
+
const board = (over: Partial<Extract<Tour, { kind: "steps" }>> = {}): Tour => ({
|
|
8
|
+
id: "board",
|
|
9
|
+
route: "/(app)/projects",
|
|
10
|
+
kind: "steps",
|
|
11
|
+
steps: steps(3),
|
|
12
|
+
...over,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("defineTours", () => {
|
|
16
|
+
it("returns the tours it was given", () => {
|
|
17
|
+
const list = [board(), { id: "home", route: "/(app)", kind: "intro", title: "Hi", body: "Welcome." } as const];
|
|
18
|
+
expect(defineTours(list)).toBe(list);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects a repeated id", () => {
|
|
22
|
+
expect(() => defineTours([board(), board({ route: "/(app)/other" })])).toThrow(/"board".*used twice/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("takes 1 to 6 steps", () => {
|
|
26
|
+
expect(() => defineTours([board({ steps: steps(1) })])).not.toThrow();
|
|
27
|
+
expect(() => defineTours([board({ steps: steps(6) })])).not.toThrow();
|
|
28
|
+
expect(() => defineTours([board({ steps: [] })])).toThrow(/0 steps/);
|
|
29
|
+
expect(() => defineTours([board({ steps: steps(7) })])).toThrow(/7 steps/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("rejects a route that isn't a route id", () => {
|
|
33
|
+
expect(() => defineTours([board({ route: "projects" })])).toThrow(/not a route id/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects a bad version", () => {
|
|
37
|
+
expect(() => defineTours([board({ version: 0 })])).toThrow(/version/);
|
|
38
|
+
expect(() => defineTours([board({ version: 1.5 })])).toThrow(/version/);
|
|
39
|
+
expect(() => defineTours([board({ version: 2 })])).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects a step with no target or no words", () => {
|
|
43
|
+
expect(() => defineTours([board({ steps: [{ ...step(1), target: " " }] })])).toThrow(/step 1 has no target/);
|
|
44
|
+
expect(() => defineTours([board({ steps: [{ ...step(1), body: "" }] })])).toThrow(/title and a body/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects an intro with no title", () => {
|
|
48
|
+
expect(() => defineTours([{ id: "home", route: "/(app)", kind: "intro", title: "", body: "x" }])).toThrow(/intro/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MAX_STEPS, type Tour } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function fail(id: string, problem: string): never {
|
|
4
|
+
throw new Error(`tour "${id}": ${problem}`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Returns the tours as written, after checking what a typo would hide until a user hits it. */
|
|
8
|
+
export function defineTours<const T extends readonly Tour[]>(tours: T): T {
|
|
9
|
+
const ids = new Set<string>();
|
|
10
|
+
for (const tour of tours) {
|
|
11
|
+
const { id } = tour;
|
|
12
|
+
if (!id || !id.trim()) throw new Error("a tour needs an id");
|
|
13
|
+
if (ids.has(id)) fail(id, "its id is used twice");
|
|
14
|
+
ids.add(id);
|
|
15
|
+
if (!tour.route.startsWith("/")) fail(id, `route "${tour.route}" is not a route id`);
|
|
16
|
+
const version = tour.version ?? 1;
|
|
17
|
+
if (!Number.isInteger(version) || version < 1) fail(id, "version is a whole number from 1");
|
|
18
|
+
if (tour.kind === "intro") {
|
|
19
|
+
if (!tour.title || !tour.body) fail(id, "an intro needs a title and a body");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (tour.steps.length < 1 || tour.steps.length > MAX_STEPS) {
|
|
23
|
+
fail(id, `has ${tour.steps.length} steps, and a tour takes 1 to ${MAX_STEPS}`);
|
|
24
|
+
}
|
|
25
|
+
tour.steps.forEach((step, i) => {
|
|
26
|
+
if (!step.target.trim()) fail(id, `step ${i + 1} has no target`);
|
|
27
|
+
if (!step.title || !step.body) fail(id, `step ${i + 1} needs a title and a body`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return tours;
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "../shadcn/button/index.js";
|
|
3
|
+
import { Badge } from "../shadcn/badge/index.js";
|
|
4
|
+
import { Plus } from "phosphor-svelte";
|
|
5
|
+
import OnboardingProvider from "./onboarding-provider.svelte";
|
|
6
|
+
import { defineTours } from "./define-tours.js";
|
|
7
|
+
import { memoryStore } from "./stores.js";
|
|
8
|
+
import type { Strings } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const tours = defineTours([
|
|
11
|
+
{
|
|
12
|
+
id: "demo:intro",
|
|
13
|
+
route: "/demo/intro",
|
|
14
|
+
kind: "intro",
|
|
15
|
+
title: "The week in one place",
|
|
16
|
+
body: "Open work sits on the left. Anything late floats to the top.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "demo:board",
|
|
20
|
+
route: "/demo/board",
|
|
21
|
+
kind: "steps",
|
|
22
|
+
steps: [
|
|
23
|
+
{ target: '[data-tour="demo-new"]', title: "Start here", body: "A new project takes a name and a client. That's all.", placement: "bottom-end" },
|
|
24
|
+
{ target: '[data-tour="demo-filter"]', title: "Narrow it down", body: "Late shows what slipped. Mine shows what's yours.", placement: "bottom-start" },
|
|
25
|
+
{ target: '[data-tour="demo-list"]', title: "Open a row", body: "Click a project to see its tasks, files and notes.", placement: "top" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const strings: Strings = {
|
|
31
|
+
next: "Next",
|
|
32
|
+
back: "Back",
|
|
33
|
+
done: "Got it",
|
|
34
|
+
skip: "Skip the tour",
|
|
35
|
+
progress: (n, total) => `Step ${n} of ${total}`,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const rows = [
|
|
39
|
+
{ name: "Brand refresh", client: "Northwind", tone: "success" as const, label: "On track" },
|
|
40
|
+
{ name: "Pricing page", client: "Acme", tone: "warning" as const, label: "Late" },
|
|
41
|
+
{ name: "Launch email", client: "Globex", tone: "neutral" as const, label: "Draft" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let routeId = $state<string | null>(null);
|
|
45
|
+
let run = $state(0);
|
|
46
|
+
let store = $state(memoryStore());
|
|
47
|
+
|
|
48
|
+
// A fresh store each time, so the button always has an unseen tour to show.
|
|
49
|
+
function start(route: string) {
|
|
50
|
+
store = memoryStore();
|
|
51
|
+
routeId = route;
|
|
52
|
+
run += 1;
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="flex flex-col gap-4">
|
|
57
|
+
<div class="flex flex-wrap gap-2">
|
|
58
|
+
<Button variant="outline" size="sm" onclick={() => start("/demo/board")}>Show the page tour</Button>
|
|
59
|
+
<Button variant="outline" size="sm" onclick={() => start("/demo/intro")}>Show the intro card</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="bg-card ring-foreground/10 flex flex-col gap-4 rounded-xl p-4 ring-1">
|
|
63
|
+
<div class="flex items-center justify-between gap-3">
|
|
64
|
+
<h2 class="text-sm font-semibold">Projects</h2>
|
|
65
|
+
<Button size="sm" data-tour="demo-new"><Plus weight="bold" />New project</Button>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="flex gap-1.5" data-tour="demo-filter">
|
|
68
|
+
<Badge variant="outline">All</Badge>
|
|
69
|
+
<Badge variant="outline">Late</Badge>
|
|
70
|
+
<Badge variant="outline">Mine</Badge>
|
|
71
|
+
</div>
|
|
72
|
+
<ul class="divide-border border-border divide-y rounded-lg border" data-tour="demo-list">
|
|
73
|
+
{#each rows as row (row.name)}
|
|
74
|
+
<li class="flex items-center justify-between gap-3 px-3 py-2.5 text-sm">
|
|
75
|
+
<span class="min-w-0 truncate font-medium">{row.name}</span>
|
|
76
|
+
<span class="text-muted-foreground hidden sm:block">{row.client}</span>
|
|
77
|
+
<Badge variant="outline">{row.label}</Badge>
|
|
78
|
+
</li>
|
|
79
|
+
{/each}
|
|
80
|
+
</ul>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{#key run}
|
|
85
|
+
<OnboardingProvider {tours} {store} {strings} {routeId} replayKey={false} />
|
|
86
|
+
{/key}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as OnboardingProvider } from "./onboarding-provider.svelte";
|
|
2
|
+
export { onboarding } from "./onboarding.svelte.js";
|
|
3
|
+
export { defineTours } from "./define-tours.js";
|
|
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";
|