@urbicon-ui/blocks 6.28.0 → 6.29.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.
@@ -1,19 +1,19 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const tooltipVariants: ((props?: {
3
3
  open?: boolean | undefined;
4
- intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
4
+ intent?: "info" | "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
5
5
  size?: "sm" | "md" | "lg" | undefined;
6
6
  } | undefined) => {
7
7
  base: (props?: ({
8
8
  open?: boolean | undefined;
9
- intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
9
+ intent?: "info" | "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
10
10
  size?: "sm" | "md" | "lg" | undefined;
11
11
  } & {
12
12
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
13
13
  }) | undefined) => string;
14
14
  arrow: (props?: ({
15
15
  open?: boolean | undefined;
16
- intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
16
+ intent?: "info" | "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
17
17
  size?: "sm" | "md" | "lg" | undefined;
18
18
  } & {
19
19
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
package/docs/GUIDE.md ADDED
@@ -0,0 +1,480 @@
1
+ # Guide System
2
+
3
+ The **`Guide`** component family is a bidirectional in-app help system: a non-modal help
4
+ panel, contextual hints, UI↔guide links, and an opt-in guided tour — all over *one* headless
5
+ engine. Shipped in **v5.8.0**, stability **beta**. Lives on `@urbicon-ui/blocks` (no separate
6
+ package).
7
+
8
+ This is the living architecture/usage reference, shipped in the npm tarball
9
+ (`docs/GUIDE.md`). The monorepo's broader conventions (token system, component
10
+ families, decision matrices, Svelte 5 patterns) live in the repo's `docs/`
11
+ directory; where a decision below depends on one of them, the relevant rule is
12
+ restated in place.
13
+
14
+ ---
15
+
16
+ ## 1. What it is
17
+
18
+ `Guide` is the library's first **sequential, system-driven** overlay pattern. Every other
19
+ overlay (Tooltip, Popover, Dialog, Menu) is *spatial and event-driven* — the user clicks,
20
+ something appears *there*. `Guide` inverts that: the system decides *when*, *in what order*,
21
+ and *in which mode* help appears.
22
+
23
+ The value is **not** the tour mechanic (product tours are rightly distrusted) but three design
24
+ principles:
25
+
26
+ 1. **Helpful, not intrusive.** The defaults are the *callable* help panel and the *waiting*
27
+ hint — not a modal forced tour. The scrim-modal guided tour is the most aggressive mode and
28
+ stays **opt-in**.
29
+ 2. **One shared namespace.** Every UI spot with a help identity carries `data-guide="<topic-id>"`.
30
+ Tours, markers, hints, and article references all resolve through that one namespace — it
31
+ connects the modes instead of duplicating them.
32
+ 3. **Bidirectional.** UI ↔ Guide is linkable in both directions (§4) — the actual
33
+ differentiator versus Joyride/driver.js/Shepherd.
34
+
35
+ ### Modes (surfaces over one engine)
36
+
37
+ | Mode | Intrusiveness | Purpose |
38
+ |---|---|---|
39
+ | **Sidebar help panel** (`GuidePanel`) | callable, self-directed | Structured in-app reference (the main focus) |
40
+ | **Contextual hint** (`GuideHint`) | waiting, non-blocking | Micro-help at the right element at the right time |
41
+ | **Bidirectional link** (`GuideMarker` ↔ `GuideMention`) | passive | UI explains itself; the article points back at the UI |
42
+ | **Guided tour + spotlight** (`Guide`, `GuideBeacon`) | modal/blocking | Critical first-run flows, **opt-in** |
43
+
44
+ ---
45
+
46
+ ## 2. Architecture
47
+
48
+ ### Headless engine + swappable surfaces
49
+
50
+ ```
51
+ ┌──────────────────────────────────────────────┐
52
+ │ GuideController (utils/guide.svelte.ts) │
53
+ │ Class · $state · untrack · SvelteMap/Set │
54
+ │ │
55
+ │ #targets: SvelteMap<id, RegisteredTarget> │
56
+ │ activeTour · stepIndex · highlightedId │
57
+ │ panelOpen · activeArticle · panelId · #seen │
58
+ │ │
59
+ │ registerTarget()/target() · resolveTarget() │
60
+ │ startTour() next() prev() skip() finish() │
61
+ │ stopTour() · reapplyStepHighlight() │
62
+ │ highlight(id) / clearHighlight() │
63
+ │ openPanel(article?) / closePanel() │
64
+ │ hasSeen()/markSeen()/resetSeen() ← Storage │
65
+ └───────────────────────┬──────────────────────┘
66
+ createOptionalContext<GuideController>()
67
+
68
+ Surfaces (consume the controller via context):
69
+ • GuidePanel + GuideArticle — non-modal help panel (main focus)
70
+ • GuideMarker — direction A: UI → panel (ⓘ trigger)
71
+ • GuideMention — direction B: article → UI highlight (additive ring)
72
+ • GuideHint — contextual, waiting hint
73
+ • Guide + GuideBeacon — guided tour + spotlight (opt-in)
74
+ ```
75
+
76
+ - **Engine** (`packages/blocks/src/lib/utils/guide.svelte.ts`) is a `class` modeled on
77
+ `overlay-stack.svelte.ts` (`$state` + `untrack`, no string context keys). **Completely
78
+ UI-free** and unit-tested in isolation (`guide.svelte.test.ts`, node env). This is the
79
+ valuable part.
80
+ - **Surfaces** are thin views in `packages/blocks/src/lib/components/Guide/*` that consume the
81
+ engine via `createOptionalContext` — a surface used **without** a `GuideProvider` renders
82
+ inert (or as plain text for `GuideMention`) instead of throwing `missing_context`.
83
+ - **No separate package.** The UI depends fundamentally on `floating`/`Portal`/`overlay-stack`
84
+ (all in `blocks`); a separate package would only create peer-dependency friction (same reason
85
+ `CommandPalette` stayed in `blocks`).
86
+
87
+ ### Boundary: engine/UI in the library, content in the consumer app
88
+
89
+ The library ships **engine + presentation**. The guide *definition* (which steps, which texts,
90
+ which selectors, which articles) belongs in the **consumer app**. This keeps consumer i18n
91
+ content out of the library and prevents over-engineering. `GuideTour` / `GuideStep` /
92
+ `GuideArticle` content is authored by the consumer.
93
+
94
+ ### Reused building blocks (zero-dependency contract)
95
+
96
+ `packages/blocks` has no runtime dependencies, so an external tour library (driver.js, Shepherd)
97
+ is out — and unnecessary, because the expensive parts already exist:
98
+
99
+ | Guide needs… | Existing building block |
100
+ |---|---|
101
+ | Anchor a bubble/hint to a target (flip/shift/arrow) | `utils/floating.ts` (`computePosition` + `autoUpdate`) |
102
+ | Coexist with / pause around foreign overlays | `utils/overlay-stack.svelte.ts` (`depth`, `isTop`, `register`) |
103
+ | Position the panel as a sidebar | `Sidebar` mechanics + `Drawer` styling (D1) |
104
+ | Localize next/prev/skip/done | `@urbicon-ui/i18n` (runes-based, keys under `guide.*`) |
105
+ | Engine state (class + `$state` + `untrack`) | model: `overlay-stack.svelte.ts` |
106
+
107
+ **Genuinely new:** only the **engine/registry** (state machine + topic resolution + persistence)
108
+ and the **spotlight mask** for the tour. Everything else is composition.
109
+
110
+ ---
111
+
112
+ ## 3. The `data-guide` namespace
113
+
114
+ Two ways to mark an element as a guide target, both feeding the **same** registry — so a tour
115
+ step, a hint, a marker, and a mention can all point at one id:
116
+
117
+ 1. **`data-guide="topic-id"`** — declarative, framework-agnostic, robust, placeable even on
118
+ elements the consumer doesn't render itself. Resolved via `querySelector` fallback.
119
+ 2. **`{@attach controller.target('topic-id', { label, article, direction })}`** — programmatic,
120
+ carries metadata, reactive binding, auto-cleanup (`{@attach}`, **not** `use:`).
121
+
122
+ A single `highlight(topicId)` in the engine serves **both** tour steps **and** the Mention→UI
123
+ hover — one mechanism, multiple surfaces.
124
+
125
+ **Two layers (D3).** The `data-guide` anchors are the **anchor layer** (*where* an element is —
126
+ necessarily declarative & distributed). On top sits an optional, typed **structure layer**: a
127
+ `defineGuide([...])` manifest describing *which* tours/articles exist in *what* order, referencing
128
+ `data-guide` ids as a string union. The manifest does **not** resolve targets itself — that
129
+ separation is what makes the DEV-mode warning valuable (manifest names id X, engine can't find X
130
+ in the DOM → warning). Repo precedent: `TypedColumnBuilder` (table columns), `createPackageI18n`
131
+ (i18n).
132
+
133
+ **Resilience.** In DEV, any tour step / mention / highlight pointing at an id that can't be
134
+ resolved logs a warning instead of failing silently. A lazily-rendered target is observed and
135
+ re-anchored once it appears; a vanished one falls back gracefully (§6).
136
+
137
+ ---
138
+
139
+ ## 4. Bidirectional link (the differentiator)
140
+
141
+ Both directions share the `data-guide` namespace and the same `highlight()` primitive.
142
+ Per topic/surface configurable: `direction?: 'to-guide' | 'to-ui' | 'both'` (default `both`) —
143
+ so uni- or bidirectional is a deliberate option, resolved by `resolveDirection(id, override?)`
144
+ (a surface-level override wins over the topic's registered direction).
145
+
146
+ **Direction A — UI → Guide** (`GuideMarker`): a discreet "ⓘ" trigger on a UI element. Activation
147
+ (click/Enter/Space) opens the `GuidePanel` at the matching article. A real `<button>` with
148
+ `aria-controls` (the panel's published id) + `aria-expanded`. Inert when the direction is
149
+ `'to-ui'`. → *"I see this element and want to know what it does."*
150
+
151
+ **Direction B — Guide → UI** (`GuideMention`): an inline reference inside a `GuideArticle`. Hover
152
+ **or focus** (keyboard parity) highlights the real element (`[data-guide="…"]`) with a light
153
+ ring/glow + optional scroll-into-view; clicking scrolls it into view. Degrades to plain text when
154
+ the direction is `'to-guide'`. → *"I'm reading the article and want to see where the element is."*
155
+
156
+ **Article → article** (`GuideRef`): an inline link inside a `GuideArticle` body that navigates the
157
+ panel to another article (`setArticle`) — the help-internal analogue of `GuideMention`. It is *not*
158
+ a `data-guide`/highlight link: it resolves against the panel's article registry (`hasArticle`), so
159
+ it degrades to plain text for an unknown article id (or without a provider/panel). → *"I'm reading
160
+ this article and want to jump to a related one."*
161
+
162
+ **Highlight principle (D5):** **Tour = subtractive** (full scrim, the surroundings disappear)
163
+ · **Highlight = additive** (an `outline` ring + soft glow, the target is emphasized, nothing is
164
+ dimmed). Direction B uses only the additive ring via `outline` (not `border` → no layout shift at
165
+ the target), `prefers-reduced-motion`-aware. The full scrim is reserved for the spotlight tour.
166
+
167
+ ---
168
+
169
+ ## 5. Component inventory
170
+
171
+ | Name | `@tag`s | Role |
172
+ |---|---|---|
173
+ | `GuideProvider` | overlay, feedback | Context root; instantiates `GuideController`, injects the StorageAdapter (analogous to `BlocksProvider`) |
174
+ | `GuidePanel` | overlay, navigation | Callable, **non-modal** sidebar help panel |
175
+ | `GuideArticle` | display | Structured article in the panel (contains `GuideMention`s); optional `group` buckets it under a section header in the index |
176
+ | `GuideMention` | navigation | Inline article→UI reference (direction B) |
177
+ | `GuideRef` | navigation | Inline article→article link inside an article body (panel-internal navigation) |
178
+ | `GuideMarker` | action, feedback | "ⓘ" UI→panel trigger (direction A) — *not* a status `Badge` |
179
+ | `GuideHint` | overlay, feedback | Contextual, non-blocking hint at an element |
180
+ | `Guide` | overlay, feedback | Tour renderer: bubble (`floating.ts`) + spotlight mask |
181
+ | `GuideBeacon` | feedback, action | Waiting hotspot — unobtrusive tour entry point |
182
+
183
+ Plus `controller.target(id, meta?)` — the attachment that registers/binds an element
184
+ programmatically.
185
+
186
+ Every `*Props` interface carries the mandatory JSDoc (`@description`, `@tag`, `@related`,
187
+ `@stability beta`) — the single source for the MCP server, `llms.txt`, and the docs site.
188
+ The eight surfaces additionally carry `@standalone`, which gives each its own MCP-catalog
189
+ entry and `llm.txt` (`find_components("guide")` lists all nine; `get_component("guide-panel")`
190
+ etc. work) despite the family sharing one `index.ts` and one docs page — unlike compound
191
+ subcomponents (TabItem, MenuItem), which stay folded into their directory component's entry.
192
+
193
+ **Family classification (D6):** Guide is a standalone **component family** (like `CommandPalette`,
194
+ `DatePicker`) — deliberately **not** a row in the six-family primitives taxonomy (the monorepo's
195
+ COMPONENT-FAMILIES.md, which assigns border-token source + tier behaviour per family). The
196
+ surfaces carry `@tag`s; the architecture lives here.
197
+
198
+ ---
199
+
200
+ ## 6. Tokens, z-index & top-layer discipline
201
+
202
+ `Dialog` uses a native `<dialog>` → a real top layer. A portaled tour bubble with an ordinary
203
+ z-index would vanish *behind* an open dialog. Therefore:
204
+
205
+ - Active tours/spotlight register in the `overlayStack`.
206
+ - The engine **pauses/dims** (`paused` getter) when `overlayStack.depth` grows from a *foreign*
207
+ overlay; non-modal surfaces (`GuideHint`) hide while `overlayDepth > 0`.
208
+ - z-index is token-only. **`--z-guide: 1550`** (D4), between `--z-popover` (1500) and
209
+ `--z-skiplink` (1600), drives the spotlight tour. `GuidePanel` inherits `--z-sidebar` (1350),
210
+ `GuideHint` `--z-popover` (1500). The stacking guarantee versus a native `<dialog>` (browser
211
+ top layer) comes from the `overlayStack.depth` pause, **not** from z-index.
212
+
213
+ Tunable tokens (defined in `blocks/src/lib/style/interaction.css`, the `[data-guide-highlight]`
214
+ rule in `index.css`):
215
+
216
+ - `--blocks-guide-scrim` (default `oklch(0 0 0 / 0.5)`) — the tour's dimming backdrop.
217
+ - `--blocks-guide-highlight-ring` (default `--color-primary`) — the additive Mention→UI ring.
218
+
219
+ ---
220
+
221
+ ## 7. Key decisions (D1–D6)
222
+
223
+ - **D1 — Panel placement: a standalone, *non-modal* `GuidePanel`.** It takes the `Sidebar`
224
+ mechanics (coexists with the app) and only the `Drawer` styling/`slotClasses` pattern. `Drawer`
225
+ is modal (`<dialog>.showModal()`, backdrop, focus trap) — that would break direction B (§4),
226
+ which must hover a mention *and simultaneously* highlight the element behind the panel.
227
+ - **D2 — Marker naming: `GuideMarker`.** The interactive "ⓘ" is **not** a "Badge" (collision with
228
+ the non-interactive status `Badge` primitive). It may still *look* like "ⓘ".
229
+ - **D3 — Topic definition: declarative anchors + a typed manifest (both, layered).** Anchors stay
230
+ declarative & distributed; structure (tours/articles/order) comes from an optional typed
231
+ `defineGuide([...])` manifest referencing `data-guide` ids (§3).
232
+ - **D4 — z-index: token `--z-guide: 1550`** for the spotlight bubble; stacking vs. native
233
+ `<dialog>` comes from the `overlayStack.depth` pause, not z-index (§6).
234
+ - **D5 — Direction-B highlight: additive `outline` ring, no scrim.** Tour = subtractive, highlight
235
+ = additive; `outline` (not `border`) → no layout shift; reduced-motion-aware (§4).
236
+ - **D6 — Family: standalone component family, not a row in the primitives table** (§5).
237
+
238
+ ---
239
+
240
+ ## 8. As-built contract (deviations now part of the API)
241
+
242
+ The build deviated from the original plan in ways that are now the intended contract:
243
+
244
+ - **`createOptionalContext`** (not `createContext`): surfaces render inert without a provider.
245
+ - **Controller-driven panel visibility** (`panelOpen`), not `bind:open` — single source of truth.
246
+ The panel uses `inert` (not `aria-hidden`) when closed, and focus-sensitive Escape (non-modal
247
+ courtesy). Closing from *within* returns focus to the opener (`aria-labelledby` on the header).
248
+ - **Native Popover API top-layer** (not `Portal`) for `GuideHint` and `Guide` — mirrors
249
+ `Tooltip`/`Popover`. The tour is one `popover="manual"` shell (`pointer-events:none`) with an
250
+ SVG even-odd scrim **and** the bubble inside.
251
+ - **`GuideMarker` opens on activation** (click/Enter/Space), not on bare focus (tab-friendly).
252
+ `GuideMention` has **no** `aria-describedby` (the visible text is the accessible name).
253
+ - **Tour progress = dot row + "Step X of Y"** (not the `Stepper` primitive — too heavy for a
254
+ ~22rem bubble). Step announcement via an always-present polite `aria-live` region (a sibling of
255
+ the popover, so the first step is announced as a content *change*). Interactive steps use a
256
+ two-zone Tab cycle (bubble ⇄ target) so the spotlit target stays keyboard-reachable; `aria-modal`
257
+ is dropped on those steps.
258
+ - **Spotlight = SVG even-odd hole** following the target via `autoUpdate`; a non-interactive step
259
+ lays a transparent blocker `<rect>` over the hole, an interactive step leaves it click-through.
260
+ - **Analytics hooks** on `GuideTour`: `onStep` / `onComplete` / `onSkip` (the real business value —
261
+ the onboarding funnel + drop-off). Fired from the engine, defensively (a throwing callback can't
262
+ corrupt tour state). `stopTour()` is analytics-silent (programmatic teardown, e.g. route change).
263
+ - **Action-gated steps** (`GuideStep.advance: 'action'`, default `'user'`): learning-by-doing —
264
+ the footer's Next is rendered `aria-disabled` (still focusable, with an `aria-describedby`
265
+ screen-reader hint, i18n `guide.actionRequired`) and ArrowRight is inert; the app advances
266
+ imperatively via `controller.next()` once the user performed the real action (pair with
267
+ `interactive: true`). The gate lives purely in the `Guide` renderer — the engine's `next()`
268
+ is never blocked. Back/Skip stay available; the live region announces the gate per step.
269
+ - **Phase-8 hardening:** `observeTargetResolution` (rAF-coalesced MutationObserver) +
270
+ `reapplyStepHighlight()` re-anchor and re-spotlight a target that renders *after* a step begins,
271
+ and fall back to the centered scrim when one vanishes. The tour exit fades via a
272
+ `view = liveView ?? heldView` snapshot that holds the last step's content through the popover
273
+ transition. `GuidePanel` returns focus to its opener on close.
274
+ - **Declarative cross-route touring** (`GuideStep.route` + `GuideControllerOptions.navigate`,
275
+ also forwarded by `GuideProvider.navigate`): a step can name the route it lives on, and the
276
+ controller navigates there (via the injected, framework-agnostic hook) **before** the spotlight,
277
+ re-anchoring through the same `reapplyStepHighlight` once the target appears. All the logic lives
278
+ in the engine — the renderer is unchanged. A tour-internal navigation (tracked via an internal
279
+ `expectedRoute`) keeps the tour running; a foreign one stops it analytics-silent (`stopTour`), so
280
+ the manual-`goto` recipe is preserved by subscribing to navigation **only** for tours that declare
281
+ a `route`. The current path + navigation events come from an injectable `navigationSource`
282
+ (default: Navigation API with a `popstate` fallback; the escape hatch for a base path / custom
283
+ router). `prev()` navigates back symmetrically; a missing hook or a never-appearing target DEV-warns
284
+ without hanging or crashing (§9).
285
+ - **Panel index scales with the catalog:** `GuideArticle.group` buckets the index under section
286
+ headers (a `<ul aria-labelledby>` per section in first-occurrence order; ungrouped articles in
287
+ one headerless block; a flat list when no article sets a group — unchanged for existing
288
+ consumers). `GuidePanel.searchable` adds an opt-in case-insensitive title filter that runs
289
+ *before* grouping (non-empty sections keep their headers, empty ones disappear; an empty result
290
+ shows the i18n `guide.noResults` status). `GuideRef` (§4) is the inline article→article link,
291
+ resolving against the panel's article registry (`hasArticle`) so it degrades to plain text for an
292
+ unknown id — hydration-safe because the registry is empty during SSR and the first client render
293
+ alike, then reactively upgrades the `<span>` to a `<button>`.
294
+
295
+ ---
296
+
297
+ ## 9. Cross-route tours
298
+
299
+ Tours survive client-side navigation by construction: the controller lives in the layout's
300
+ `GuideProvider` and outlives route components; an unresolved step target falls back gracefully to
301
+ the centered, full-scrim bubble; and the `MutationObserver` in `Guide.svelte` re-anchors as soon
302
+ as the new route's `data-guide` element appears (the lazy-target hardening, §8). On top of that,
303
+ a step can declare the route it lives on and let the library drive the navigation.
304
+
305
+ ### Declarative (`GuideStep.route` + `navigate` hook) — the supported pattern
306
+
307
+ Give the step its `route` and wire a `navigate` hook once (UIB stays framework-agnostic, so it
308
+ never imports your router). When a step becomes active and its `route` differs from the current
309
+ location, the controller navigates **before** the spotlight, then re-anchors once the target
310
+ appears on the new page:
311
+
312
+ ```svelte
313
+ <script lang="ts">
314
+ import { goto } from '$app/navigation';
315
+ import { GuideProvider, Guide, GuideController, type GuideTour } from '@urbicon-ui/blocks';
316
+
317
+ // Wire the router once. (Equivalently: <GuideProvider navigate={(route) => goto(route)}>.)
318
+ const guide = new GuideController({ navigate: (route) => goto(route) });
319
+
320
+ const tour: GuideTour = {
321
+ id: 'cross-route-onboarding',
322
+ steps: [
323
+ { target: 'dash-overview', route: '/dashboard', title: 'Your dashboard', body: '…' },
324
+ { target: 'dash-filter', route: '/dashboard', title: 'Filter', body: '…' },
325
+ { target: 'billing-plan', route: '/settings/billing', title: 'Your plan', body: '…' }
326
+ ]
327
+ };
328
+ </script>
329
+
330
+ <GuideProvider controller={guide}>
331
+ <Guide />
332
+ <!-- … app … -->
333
+ </GuideProvider>
334
+ ```
335
+
336
+ How it behaves:
337
+
338
+ - **Tour-internal vs. foreign navigation.** A navigation the tour itself triggers (a `step.route`)
339
+ keeps it running. A *foreign* navigation — the user leaving the flow on their own, or a
340
+ youngest-gesture-wins race during a pending step navigation — stops it via `stopTour()`
341
+ (analytics-silent, so it can surface again; no `onSkip`). The engine subscribes to navigation
342
+ only for tours that declare at least one `route`, so a tour that does its own routing (below) is
343
+ never second-guessed.
344
+ - **`prev()` is symmetric.** Stepping back across a route boundary navigates back too — the target
345
+ step carries its own `route`.
346
+ - **The navigation gap is covered.** Between the `navigate(...)` call and the new route's
347
+ `data-guide` element mounting, the step renders centered over the full scrim; once the target
348
+ appears, the bubble re-anchors and the spotlight ring lands (via the existing
349
+ `reapplyStepHighlight`). If the target never appears, the step still shows centered (it never
350
+ hangs) and DEV warns.
351
+ - **Missing hook is safe.** A `route` step with no `navigate` wired DEV-warns and stays on the
352
+ current route — no crash.
353
+ - **Path comparison.** `route` is compared against `window.location.pathname` by default, so use a
354
+ normalized path (no query/hash, preferably the router's *actual* landed path). An asynchronously
355
+ observed landing that is merely a normalized form of the step's route (trailing slash, base/locale
356
+ prefix) is recognized as the tour's own — the tour keeps running, DEV-warned so you can make the
357
+ route exact; an unrelated landing (e.g. an auth redirect) stops the tour as foreign. A
358
+ synchronous, re-entrant `navigationSource` (the `afterNavigate` one below) doesn't need the
359
+ heuristic — a same-tick report is recognized as the tour's own by *timing*, not by path.
360
+
361
+ > **Reliable foreign-navigation detection.** The default `navigationSource` observes navigations via
362
+ > the Navigation API, which catches link clicks, `goto`, and back/forward in Chromium and recent
363
+ > Safari. In a browser without it the source falls back to `popstate` — back/forward only, **not**
364
+ > `pushState` — so a foreign *forward* navigation (an in-app link click) may go unobserved and the
365
+ > tour won't stop (it DEV-warns once when it takes the fallback). For detection that works in every
366
+ > browser, inject a `navigationSource` backed by your router. SvelteKit (`afterNavigate` fires for
367
+ > every client navigation):
368
+ >
369
+ > ```svelte
370
+ > <script lang="ts">
371
+ > import { afterNavigate, goto } from '$app/navigation';
372
+ > import { page } from '$app/state';
373
+ > import { GuideController, type GuideNavigationSource } from '@urbicon-ui/blocks';
374
+ >
375
+ > let notify: ((path: string) => void) | null = null;
376
+ > const navigationSource: GuideNavigationSource = {
377
+ > current: () => page.url.pathname,
378
+ > subscribe: (cb) => {
379
+ > notify = cb;
380
+ > return () => {
381
+ > notify = null;
382
+ > };
383
+ > }
384
+ > };
385
+ > afterNavigate(() => notify?.(page.url.pathname));
386
+ >
387
+ > const guide = new GuideController({ navigate: goto, navigationSource });
388
+ > </script>
389
+ > ```
390
+ >
391
+ > This source fires **synchronously inside `goto`** — re-entrant, while the controller's own
392
+ > `navigate` hook is still on the stack. The engine treats such a same-tick report as the tour's own
393
+ > navigation (recognized by *timing*, not by path), so a plain synchronous `notify` is correct — no
394
+ > `queueMicrotask` deferral is needed. Because the match is by timing, a router-normalized landing
395
+ > (trailing slash, base/locale prefix) keeps the tour running; a synchronous *redirect* off the
396
+ > step's route also keeps it running (DEV-warned, not stopped), so stop the tour explicitly from a
397
+ > router guard if a redirect should end it.
398
+
399
+ ### Manual (`onStep` + `goto`) — the escape hatch
400
+
401
+ For routing that the declarative `route` can't express — chosen at runtime from tour state or user
402
+ answers — navigate imperatively in `onStep` instead. A tour with no `step.route` is never
403
+ subscribed to navigation, so its own `goto` is not mistaken for a foreign navigation:
404
+
405
+ ```svelte
406
+ <script lang="ts">
407
+ import { goto } from '$app/navigation';
408
+ import { page } from '$app/state';
409
+ import type { GuideTour } from '@urbicon-ui/blocks';
410
+
411
+ const stepRoutes: Record<number, string> = { 0: '/dashboard', 2: '/settings/billing' };
412
+
413
+ const tour: GuideTour = {
414
+ id: 'cross-route-onboarding',
415
+ steps: [
416
+ { target: 'dash-overview', title: 'Your dashboard', body: '…' },
417
+ { target: 'dash-filter', title: 'Filter', body: '…' },
418
+ { target: 'billing-plan', title: 'Your plan', body: '…' }
419
+ ],
420
+ onStep: ({ index }) => {
421
+ const route = stepRoutes[index];
422
+ if (route && route !== page.url.pathname) goto(route);
423
+ }
424
+ };
425
+ </script>
426
+ ```
427
+
428
+ Rules of thumb (both patterns):
429
+
430
+ - **Mount `<Guide />` (and the provider) in the layout, not in a route.** A route-local renderer
431
+ unmounts on navigation, and its unmount cleanup calls `stopTour()` — the tour would silently end
432
+ mid-flight before it could re-anchor on the destination.
433
+ - **`stopTour()` when navigation invalidates the tour** (logout, the user abandons the flow) — it
434
+ tears down without marking the tour seen and stays analytics-silent, so it can surface again. The
435
+ declarative pattern does this for foreign navigation automatically.
436
+ - Steps that land right after a navigation pair well with `advance: 'action'` (§8) when the
437
+ navigation itself *is* the action being taught — together they yield learning-by-doing across
438
+ routes.
439
+
440
+ ---
441
+
442
+ ## 10. Non-goals / v2
443
+
444
+ Deliberately **out** of the first cut (avoiding over-engineering):
445
+
446
+ - Branching / conditional steps (branched tours by user answer)
447
+ - Cross-route **resume state** (surviving a full page reload / deep-link into the middle of a
448
+ tour) — client-side cross-route touring *is* supported now, both declaratively (`GuideStep.route`
449
+ + the `navigate` hook) and imperatively (the `onStep` recipe), §9; persisting progress across a
450
+ hard reload is what stays out
451
+ - WYSIWYG tour editor / no-code authoring
452
+ - Built-in analytics persistence (we ship only the hooks, no backend)
453
+ - Server-driven remote guides (content from a CMS) — an adapter is conceivable, not v1
454
+
455
+ ---
456
+
457
+ ## 11. Where things live & how it's tested
458
+
459
+ | Concern | Location |
460
+ |---|---|
461
+ | Engine (state machine, registry, persistence, analytics) | `packages/blocks/src/lib/utils/guide.svelte.ts` |
462
+ | Lazy/vanishing-target watcher | `packages/blocks/src/lib/utils/observe-target.ts` |
463
+ | Surfaces | `packages/blocks/src/lib/components/Guide/*.svelte` |
464
+ | Context + panel context | `components/Guide/guide.context.ts`, `guide-panel.context.ts` |
465
+ | Variants | `components/Guide/guide.variants.ts` |
466
+ | Tokens / highlight CSS | `blocks/src/lib/style/{interaction,index,foundation}.css` |
467
+ | i18n keys (`guide.*`) | `blocks/src/lib/translations/{en,de}.ts` |
468
+ | Doc page (live examples) | `apps/docs/src/routes/blocks/components/guide/` |
469
+ | Recipe (Onboarding Flow + live analytics log) | `apps/docs/src/routes/recipes/onboarding-flow/` |
470
+ | MCP pattern | `design-system/patterns/onboarding-guide.md` |
471
+
472
+ **Tests.** Engine logic is node-unit-tested (`utils/guide.svelte.test.ts`,
473
+ `utils/observe-target.test.ts`). Component behaviour + a11y (axe) + light/dark visual snapshots
474
+ run via Playwright against `apps/docs/src/routes/test-fixtures/guide/`
475
+ (`e2e/guide.spec.ts`); the visual baselines are CI-optional (`e2e/snapshots/guide.spec.ts-snapshots/`).
476
+
477
+ **docs-gen caveat.** docs-gen extracts only local `*Props`/`*Variants`, not types imported from
478
+ `utils`. So `GuideController` / `GuideTour` / `GuideStep` / the analytics-event payloads are **not**
479
+ auto-expanded into the generated `api.ts` / `llms.txt`; the hand-authored API tables on the doc
480
+ page are the source of truth for them until docs-gen does cross-file type resolution.
@@ -0,0 +1,118 @@
1
+ # Migration Guide: v4 → v5
2
+
3
+ The v5.0.0 release ("Lighter design refactor") introduces a quieter visual
4
+ language across the library: hairline borders, surface-tinted in-page zones,
5
+ fewer chrome layers. Most consumer code is unaffected — the renames listed
6
+ below are the only typed-API breakages.
7
+
8
+ > Released `2026-05-21` as commit `8ac07ca` (`feat!`). All four breaking
9
+ > blocks below were declared in the commit's `BREAKING CHANGE:` footers but
10
+ > are surfaced here because git-cliff's default template renders only the
11
+ > commit subject in the CHANGELOG.
12
+
13
+ ## 1. Card variants
14
+
15
+ | Before | After | Notes |
16
+ | ------------------- | -------------------- | --------------------------------------------- |
17
+ | `variant="elevated"` (default) | `variant="quiet"` (default) | New default — surface-quiet tint, no shadow |
18
+ | `variant="filled"` | _removed_ | Replaced by `quiet` |
19
+ | `variant="glass"` | _removed_ | No drop-in replacement; build with `unstyled` |
20
+ | `variant="outlined"` | _unchanged_ | — |
21
+ | `variant="elevated"` | _unchanged_ | Still available, no longer the default |
22
+ | — | `variant="floating"` | New — popover-family weight (shadow-lg) |
23
+
24
+ The `dividers` prop now defaults to **false** (was implicit `true` via
25
+ header/footer borders). Set `dividers={true}` to restore traditional
26
+ card-with-header look.
27
+
28
+ ```svelte
29
+ <!-- v4 -->
30
+ <Card variant="elevated">…</Card>
31
+ <!-- v5 — keeps the elevated look explicitly -->
32
+ <Card variant="elevated">…</Card>
33
+ <!-- v5 — pick up the new default, no shadow -->
34
+ <Card>…</Card>
35
+ ```
36
+
37
+ ## 2. Alert + Accordion variant renames
38
+
39
+ | Component | Before | After |
40
+ | ----------- | --------------------- | ---------------------- |
41
+ | `Alert` | `variant="outlined"` | `variant="inline"` |
42
+ | `Accordion` | `variant="bordered"` | `variant="separated"` |
43
+
44
+ `Alert.inline` is a memo-style with a left accent border (`border-l-2`),
45
+ no surrounding box. `Accordion.separated` renders items as a stacked
46
+ card list with `surface-quiet` tint and `space-y-2`.
47
+
48
+ ```svelte
49
+ <!-- v4 -->
50
+ <Alert variant="outlined" intent="warning">…</Alert>
51
+ <Accordion variant="bordered">…</Accordion>
52
+ <!-- v5 -->
53
+ <Alert variant="inline" intent="warning">…</Alert>
54
+ <Accordion variant="separated">…</Accordion>
55
+ ```
56
+
57
+ ## 3. Table chrome
58
+
59
+ The Table no longer renders a frame by default. The new `appearance` prop
60
+ controls the container chrome:
61
+
62
+ | `appearance` | Effect |
63
+ | ------------ | ------------------------------------------------------------ |
64
+ | `flush` (default) | No frame; the table sits inline in the reading flow. |
65
+ | `surface` | Quiet tinted zone — `bg-surface-quiet`. |
66
+ | `framed` | Border + `rounded-contain` + shadow — closest to v4 default. |
67
+
68
+ The dead `tableVariants` export is removed (was unused in any consumer).
69
+
70
+ ```svelte
71
+ <!-- v4 — implicit frame -->
72
+ <Table data={rows} columns={cols} />
73
+ <!-- v5 — explicit equivalent -->
74
+ <Table data={rows} columns={cols} appearance="framed" />
75
+ <!-- v5 — new editorial default -->
76
+ <Table data={rows} columns={cols} />
77
+ ```
78
+
79
+ ## 4. Dialog + Drawer intent
80
+
81
+ Dialog and Drawer no longer draw a coloured `border-t-[3px]` accent for
82
+ non-neutral intents. Intent is exposed on the panel as a data attribute:
83
+
84
+ ```html
85
+ <div data-intent="warning" …>…</div>
86
+ ```
87
+
88
+ Consumers can re-introduce a coloured accent via CSS, a preset, or a
89
+ custom snippet on `header`/`title`:
90
+
91
+ ```css
92
+ [data-intent='warning'] {
93
+ border-top: 3px solid var(--color-warning);
94
+ }
95
+ ```
96
+
97
+ No prop renames — `intent="warning"` still works; only the default visual
98
+ changed.
99
+
100
+ ## Container-component border tokens
101
+
102
+ Many container surfaces (Card, Alert, Dialog, Drawer, Popover, Menu, Toast,
103
+ Sidebar, Collapsible, FileUpload, Sankey, CompositionBar, CommandPalette,
104
+ SidebarLayout, Calendar) migrated from `border-subtle` to
105
+ `border-hairline` (~8 % alpha at light, 6 % at dark). Form primitives
106
+ (Input, Select, Combobox, Textarea, Checkbox, RadioGroup) kept
107
+ `border-subtle` as an input affordance — consumers see no API change but
108
+ the visual difference is real. Override per-component via `class` /
109
+ `slotClasses` if a heavier border is desired.
110
+
111
+ ## New tokens consumers can use directly
112
+
113
+ - `bg-surface-quiet` — softly tinted in-page zone (was `bg-surface-subtle`-equivalent)
114
+ - `border-hairline` — container divider (auto-upgrades under
115
+ `prefers-contrast: more`)
116
+
117
+ Both are exposed as semantic Tailwind utilities and via CSS custom
118
+ properties (`--color-surface-quiet`, `--color-border-hairline`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.28.0",
3
+ "version": "6.29.1",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,6 +35,8 @@
35
35
  },
36
36
  "files": [
37
37
  "LICENSE",
38
+ "docs/GUIDE.md",
39
+ "docs/MIGRATION-v5.md",
38
40
  "dist",
39
41
  "!dist/**/*.test.*",
40
42
  "!dist/**/*.spec.*",
@@ -92,8 +94,8 @@
92
94
  "@sveltejs/package": "^2.5.8",
93
95
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
94
96
  "@tailwindcss/vite": "^4.3.1",
95
- "@urbicon-ui/i18n": "6.28.0",
96
- "@urbicon-ui/shared-types": "6.28.0",
97
+ "@urbicon-ui/i18n": "6.29.1",
98
+ "@urbicon-ui/shared-types": "6.29.1",
97
99
  "prettier": "^3.8.4",
98
100
  "prettier-plugin-svelte": "^4.1.1",
99
101
  "prettier-plugin-tailwindcss": "^0.8.0",