@burtson-labs/ui 0.8.1 → 0.9.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.
@@ -0,0 +1,367 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Button } from "./button.js";
3
+ import { surfaceClasses } from "./popover.js";
4
+ import { Popover, Slot } from "radix-ui";
5
+ import * as React from "react";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import X from "@burtson-labs/icons/react/x";
8
+ //#region src/components/tour.tsx
9
+ /** Marks an element as a tour target. Merges onto its one child. */
10
+ function TourAnchor({ id, children }) {
11
+ return /* @__PURE__ */ jsx(Slot.Root, {
12
+ "data-tour-target": id,
13
+ children
14
+ });
15
+ }
16
+ /** The same marker as TourAnchor, for elements you render yourself. */
17
+ var tourTarget = (id) => ({ "data-tour-target": id });
18
+ var isVisible = (el) => Boolean(el?.isConnected && el.getClientRects().length > 0);
19
+ function findTourTarget(target) {
20
+ if (!target) return null;
21
+ const el = typeof target === "function" ? target() : Array.from(document.querySelectorAll("[data-tour-target]")).find((node) => node.getAttribute("data-tour-target") === target) ?? null;
22
+ return isVisible(el) ? el : null;
23
+ }
24
+ /**
25
+ * Resolves when the target is mounted and visible, or with null after
26
+ * `timeoutMs` or when `signal` aborts. Handles targets that render late
27
+ * (lazy panels, async data) without polling forever.
28
+ */
29
+ function waitForTourTarget(target, timeoutMs, signal) {
30
+ return new Promise((resolve) => {
31
+ const now = findTourTarget(target);
32
+ if (now || !target || timeoutMs <= 0 || signal.aborted) return resolve(now);
33
+ let done = false;
34
+ const finish = (el) => {
35
+ if (done) return;
36
+ done = true;
37
+ observer.disconnect();
38
+ clearInterval(poll);
39
+ clearTimeout(timer);
40
+ signal.removeEventListener("abort", abort);
41
+ resolve(el);
42
+ };
43
+ const check = () => {
44
+ const el = findTourTarget(target);
45
+ if (el) finish(el);
46
+ };
47
+ const abort = () => finish(null);
48
+ const observer = new MutationObserver(check);
49
+ observer.observe(document.body, {
50
+ childList: true,
51
+ subtree: true,
52
+ attributes: true
53
+ });
54
+ const poll = setInterval(check, 150);
55
+ const timer = setTimeout(() => finish(null), timeoutMs);
56
+ signal.addEventListener("abort", abort);
57
+ });
58
+ }
59
+ /**
60
+ * Escape belongs to whatever the person is typing in (a field, CodeMirror's
61
+ * contenteditable, xterm's hidden textarea), not to the tour.
62
+ */
63
+ var typingElsewhere = (container) => {
64
+ const active = document.activeElement;
65
+ if (!(active instanceof HTMLElement) || container?.contains(active)) return false;
66
+ return active.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName);
67
+ };
68
+ var prefersReducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
69
+ /** Follows an element's box across scroll and resize; null until measured. */
70
+ function useRect(el) {
71
+ const [measured, setMeasured] = React.useState(null);
72
+ React.useEffect(() => {
73
+ if (!el) return;
74
+ let frame = 0;
75
+ const update = () => {
76
+ cancelAnimationFrame(frame);
77
+ frame = requestAnimationFrame(() => setMeasured({
78
+ el,
79
+ rect: el.getBoundingClientRect()
80
+ }));
81
+ };
82
+ update();
83
+ const ro = new ResizeObserver(update);
84
+ ro.observe(el);
85
+ window.addEventListener("scroll", update, true);
86
+ window.addEventListener("resize", update);
87
+ return () => {
88
+ cancelAnimationFrame(frame);
89
+ ro.disconnect();
90
+ window.removeEventListener("scroll", update, true);
91
+ window.removeEventListener("resize", update);
92
+ };
93
+ }, [el]);
94
+ return measured && measured.el === el ? measured.rect : null;
95
+ }
96
+ /** A ring around the target, optionally dimming the rest of the page. Never blocks clicks. */
97
+ function TourHighlight({ rect, dim }) {
98
+ const pad = 4;
99
+ return /* @__PURE__ */ jsx("div", {
100
+ "aria-hidden": true,
101
+ "data-slot": "tour-highlight",
102
+ className: cn("pointer-events-none fixed z-40 rounded-md ring-2 ring-brand motion-safe:transition-[top,left,width,height] motion-safe:duration-200", dim && "shadow-[0_0_0_9999px_rgb(10_8_14/0.45)]"),
103
+ style: {
104
+ top: rect.top - pad,
105
+ left: rect.left - pad,
106
+ width: rect.width + 8,
107
+ height: rect.height + 8
108
+ }
109
+ });
110
+ }
111
+ /** A zero-size box a third of the way down the viewport, for steps without a target. */
112
+ var centerRect = () => new DOMRect(window.innerWidth / 2, Math.max(80, window.innerHeight / 3), 0, 0);
113
+ var contentClasses = cn(surfaceClasses, "w-80 max-w-[calc(100vw-2rem)] p-4 text-[13px] outline-none origin-(--radix-popover-content-transform-origin)");
114
+ var defaultLabels = {
115
+ next: "Next",
116
+ back: "Back",
117
+ skip: "Skip tour",
118
+ done: "Done",
119
+ close: "Close tour",
120
+ progress: (step, total) => `${step} of ${total}`
121
+ };
122
+ /**
123
+ * A guided sequence of anchored steps. It is non-modal: the page stays
124
+ * usable and outside clicks do not end it. Focus moves into each step,
125
+ * Escape ends the tour (unless the person is typing in a field or editor
126
+ * outside it), and focus returns to where it was when the tour started.
127
+ * The app owns the copy, the eligibility rules and what it persists.
128
+ */
129
+ function Tour({ steps, open, step, onStepChange, onEnd, missingTarget = "center", waitMs = 2e3, dim = true, labels: labelOverrides, fallbackFocus }) {
130
+ const labels = {
131
+ ...defaultLabels,
132
+ ...labelOverrides
133
+ };
134
+ const current = steps[step];
135
+ const stepKey = current ? `${step}:${current.id}` : "";
136
+ const [resolved, setResolved] = React.useState(null);
137
+ const ready = open && resolved?.key === stepKey;
138
+ const target = ready ? resolved?.el ?? null : null;
139
+ const rect = useRect(target);
140
+ const contentRef = React.useRef(null);
141
+ const [contentEl, setContentEl] = React.useState(null);
142
+ const returnFocus = React.useRef(null);
143
+ const direction = React.useRef(1);
144
+ const titleId = React.useId();
145
+ const bodyId = React.useId();
146
+ React.useLayoutEffect(() => {
147
+ if (!open) return;
148
+ const active = document.activeElement;
149
+ returnFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;
150
+ return () => {
151
+ const back = returnFocus.current;
152
+ (back && isVisible(back) ? back : fallbackFocus?.() ?? null)?.focus();
153
+ };
154
+ }, [open]);
155
+ React.useEffect(() => {
156
+ if (!open || !current) return;
157
+ const controller = new AbortController();
158
+ const { signal } = controller;
159
+ (async () => {
160
+ let prepared = true;
161
+ try {
162
+ await current.prepare?.(signal);
163
+ } catch {
164
+ prepared = false;
165
+ }
166
+ if (signal.aborted) return;
167
+ const el = prepared ? await waitForTourTarget(current.target, waitMs, signal) : null;
168
+ if (signal.aborted) return;
169
+ if (!el && current.target && missingTarget === "skip") {
170
+ const next = step + direction.current;
171
+ if (next >= 0 && next < steps.length) return onStepChange(next);
172
+ }
173
+ if (el) el.scrollIntoView?.({
174
+ block: "nearest",
175
+ behavior: prefersReducedMotion() ? "auto" : "smooth"
176
+ });
177
+ setResolved({
178
+ key: stepKey,
179
+ el
180
+ });
181
+ })();
182
+ return () => controller.abort();
183
+ }, [
184
+ open,
185
+ stepKey,
186
+ waitMs,
187
+ missingTarget
188
+ ]);
189
+ const shown = ready && (!target || rect !== null);
190
+ React.useEffect(() => {
191
+ if (shown) contentEl?.focus();
192
+ }, [
193
+ shown,
194
+ step,
195
+ contentEl
196
+ ]);
197
+ if (!open || !current) return null;
198
+ const last = step === steps.length - 1;
199
+ const go = (to) => {
200
+ direction.current = to < step ? -1 : 1;
201
+ onStepChange(to);
202
+ };
203
+ if (!shown && ready) return null;
204
+ const anchor = rect ?? centerRect();
205
+ return /* @__PURE__ */ jsxs(Fragment, { children: [ready && rect && /* @__PURE__ */ jsx(TourHighlight, {
206
+ rect,
207
+ dim
208
+ }), /* @__PURE__ */ jsxs(Popover.Root, {
209
+ open: shown,
210
+ modal: false,
211
+ children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: { current: { getBoundingClientRect: () => anchor } } }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
212
+ ref: (el) => {
213
+ contentRef.current = el;
214
+ setContentEl(el);
215
+ },
216
+ "data-slot": "tour",
217
+ "data-step": current.id,
218
+ tabIndex: -1,
219
+ "aria-labelledby": titleId,
220
+ "aria-describedby": bodyId,
221
+ side: rect ? current.side ?? "bottom" : "bottom",
222
+ align: rect ? current.align ?? "center" : "center",
223
+ sideOffset: 10,
224
+ collisionPadding: 12,
225
+ className: contentClasses,
226
+ onOpenAutoFocus: (event) => event.preventDefault(),
227
+ onCloseAutoFocus: (event) => event.preventDefault(),
228
+ onInteractOutside: (event) => event.preventDefault(),
229
+ onEscapeKeyDown: (event) => {
230
+ if (typingElsewhere(contentRef.current)) event.preventDefault();
231
+ else onEnd("escape", step);
232
+ },
233
+ children: [
234
+ /* @__PURE__ */ jsxs("div", {
235
+ className: "flex items-start gap-3",
236
+ children: [/* @__PURE__ */ jsxs("div", {
237
+ className: "min-w-0 flex-1",
238
+ children: [/* @__PURE__ */ jsx("p", {
239
+ className: "text-xs text-muted-foreground",
240
+ children: labels.progress(step + 1, steps.length)
241
+ }), /* @__PURE__ */ jsx("h2", {
242
+ id: titleId,
243
+ className: "mt-0.5 text-sm font-semibold text-foreground",
244
+ children: current.title
245
+ })]
246
+ }), /* @__PURE__ */ jsx("button", {
247
+ type: "button",
248
+ "aria-label": labels.close,
249
+ onClick: () => onEnd("close", step),
250
+ className: "-me-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:shadow-[inset_0_0_0_1px_var(--ring)]",
251
+ children: /* @__PURE__ */ jsx(X, {
252
+ "aria-hidden": true,
253
+ className: "size-4"
254
+ })
255
+ })]
256
+ }),
257
+ /* @__PURE__ */ jsx("div", {
258
+ id: bodyId,
259
+ className: "mt-2 text-muted-foreground",
260
+ children: current.content
261
+ }),
262
+ /* @__PURE__ */ jsxs("div", {
263
+ className: "mt-4 flex items-center gap-2",
264
+ children: [!last && /* @__PURE__ */ jsx(Button, {
265
+ variant: "ghost",
266
+ size: "sm",
267
+ className: "-ms-2",
268
+ onClick: () => onEnd("skip", step),
269
+ children: labels.skip
270
+ }), /* @__PURE__ */ jsxs("div", {
271
+ className: "ms-auto flex gap-2",
272
+ children: [step > 0 && /* @__PURE__ */ jsx(Button, {
273
+ variant: "outline",
274
+ size: "sm",
275
+ onClick: () => go(step - 1),
276
+ children: labels.back
277
+ }), /* @__PURE__ */ jsx(Button, {
278
+ size: "sm",
279
+ onClick: () => last ? onEnd("complete", step) : go(step + 1),
280
+ children: last ? labels.done : labels.next
281
+ })]
282
+ })]
283
+ })
284
+ ]
285
+ }) })]
286
+ })] });
287
+ }
288
+ /**
289
+ * One contextual hint beside a target: a new feature, a setup nudge. It is
290
+ * informational, so it does not take focus or block the page. Escape (unless
291
+ * the person is typing elsewhere) or the dismiss button closes it. If the target is
292
+ * not on screen, nothing renders.
293
+ */
294
+ function Spotlight({ target, open, onDismiss, title, children, side = "bottom", align = "center", dismissLabel = "Got it", action, highlight = true, className }) {
295
+ const key = typeof target === "string" ? target : "fn";
296
+ const [found, setFound] = React.useState(null);
297
+ const el = open && found?.key === key ? found.el : null;
298
+ const rect = useRect(el);
299
+ const titleId = React.useId();
300
+ const contentRef = React.useRef(null);
301
+ React.useEffect(() => {
302
+ if (!open) return;
303
+ const controller = new AbortController();
304
+ waitForTourTarget(target, 2e3, controller.signal).then((found) => {
305
+ if (!controller.signal.aborted) setFound({
306
+ key,
307
+ el: found
308
+ });
309
+ });
310
+ return () => controller.abort();
311
+ }, [open, key]);
312
+ if (!open || !el || !rect) return null;
313
+ return /* @__PURE__ */ jsxs(Fragment, { children: [highlight && /* @__PURE__ */ jsx(TourHighlight, {
314
+ rect,
315
+ dim: false
316
+ }), /* @__PURE__ */ jsxs(Popover.Root, {
317
+ open: true,
318
+ modal: false,
319
+ children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: { current: { getBoundingClientRect: () => rect } } }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
320
+ ref: contentRef,
321
+ "data-slot": "spotlight",
322
+ role: "note",
323
+ "aria-labelledby": titleId,
324
+ side,
325
+ align,
326
+ sideOffset: 10,
327
+ collisionPadding: 12,
328
+ className: cn(contentClasses, "w-72", className),
329
+ onOpenAutoFocus: (event) => event.preventDefault(),
330
+ onCloseAutoFocus: (event) => event.preventDefault(),
331
+ onInteractOutside: (event) => event.preventDefault(),
332
+ onEscapeKeyDown: (event) => {
333
+ if (typingElsewhere(contentRef.current)) event.preventDefault();
334
+ else onDismiss("escape");
335
+ },
336
+ children: [
337
+ /* @__PURE__ */ jsxs("span", {
338
+ className: "sr-only",
339
+ "aria-live": "polite",
340
+ children: ["Tip: ", title]
341
+ }),
342
+ /* @__PURE__ */ jsx("p", {
343
+ id: titleId,
344
+ className: "text-sm font-semibold text-foreground",
345
+ children: title
346
+ }),
347
+ /* @__PURE__ */ jsx("div", {
348
+ className: "mt-1.5 text-muted-foreground",
349
+ children
350
+ }),
351
+ /* @__PURE__ */ jsxs("div", {
352
+ className: "mt-3 flex justify-end gap-2",
353
+ children: [action, /* @__PURE__ */ jsx(Button, {
354
+ size: "sm",
355
+ variant: action ? "ghost" : "default",
356
+ onClick: () => onDismiss("dismiss"),
357
+ children: dismissLabel
358
+ })]
359
+ })
360
+ ]
361
+ }) })]
362
+ })] });
363
+ }
364
+ //#endregion
365
+ export { Spotlight, Tour, TourAnchor, findTourTarget, tourTarget, waitForTourTarget };
366
+
367
+ //# sourceMappingURL=tour.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.js","names":["_Fragment","PopoverPrimitive"],"sources":["../../src/components/tour.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Popover as PopoverPrimitive, Slot } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { surfaceClasses } from './popover';\n\n/**\n * A registered target id (see TourAnchor), or a function returning the\n * element. Ids are stable; never target generated class names or nth-child.\n */\nexport type TourTarget = string | (() => Element | null);\n\n/** Marks an element as a tour target. Merges onto its one child. */\nfunction TourAnchor({ id, children }: { id: string; children: React.ReactElement }) {\n return <Slot.Root data-tour-target={id}>{children}</Slot.Root>;\n}\n\n/** The same marker as TourAnchor, for elements you render yourself. */\nexport const tourTarget = (id: string) => ({ 'data-tour-target': id });\n\nconst isVisible = (el: Element | null): el is Element =>\n Boolean(el?.isConnected && el.getClientRects().length > 0);\n\nexport function findTourTarget(target: TourTarget | undefined): Element | null {\n if (!target) return null;\n const el =\n typeof target === 'function'\n ? target()\n : (Array.from(document.querySelectorAll('[data-tour-target]')).find(\n (node) => node.getAttribute('data-tour-target') === target,\n ) ?? null);\n return isVisible(el) ? el : null;\n}\n\n/**\n * Resolves when the target is mounted and visible, or with null after\n * `timeoutMs` or when `signal` aborts. Handles targets that render late\n * (lazy panels, async data) without polling forever.\n */\nexport function waitForTourTarget(\n target: TourTarget | undefined,\n timeoutMs: number,\n signal: AbortSignal,\n): Promise<Element | null> {\n return new Promise((resolve) => {\n const now = findTourTarget(target);\n if (now || !target || timeoutMs <= 0 || signal.aborted) return resolve(now);\n let done = false;\n const finish = (el: Element | null) => {\n if (done) return;\n done = true;\n observer.disconnect();\n clearInterval(poll);\n clearTimeout(timer);\n signal.removeEventListener('abort', abort);\n resolve(el);\n };\n const check = () => {\n const el = findTourTarget(target);\n if (el) finish(el);\n };\n const abort = () => finish(null);\n const observer = new MutationObserver(check);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true });\n // Visibility can change through CSS alone, which no mutation reports.\n const poll = setInterval(check, 150);\n const timer = setTimeout(() => finish(null), timeoutMs);\n signal.addEventListener('abort', abort);\n });\n}\n\n/**\n * Escape belongs to whatever the person is typing in (a field, CodeMirror's\n * contenteditable, xterm's hidden textarea), not to the tour.\n */\nconst typingElsewhere = (container: HTMLElement | null) => {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || container?.contains(active)) return false;\n return active.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName);\n};\n\nconst prefersReducedMotion = () =>\n typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n/** Follows an element's box across scroll and resize; null until measured. */\nfunction useRect(el: Element | null) {\n const [measured, setMeasured] = React.useState<{ el: Element; rect: DOMRect } | null>(null);\n React.useEffect(() => {\n if (!el) return;\n let frame = 0;\n const update = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() => setMeasured({ el, rect: el.getBoundingClientRect() }));\n };\n update();\n const ro = new ResizeObserver(update);\n ro.observe(el);\n window.addEventListener('scroll', update, true);\n window.addEventListener('resize', update);\n return () => {\n cancelAnimationFrame(frame);\n ro.disconnect();\n window.removeEventListener('scroll', update, true);\n window.removeEventListener('resize', update);\n };\n }, [el]);\n return measured && measured.el === el ? measured.rect : null;\n}\n\n/** A ring around the target, optionally dimming the rest of the page. Never blocks clicks. */\nfunction TourHighlight({ rect, dim }: { rect: DOMRect; dim: boolean }) {\n const pad = 4;\n return (\n <div\n aria-hidden\n data-slot=\"tour-highlight\"\n className={cn(\n 'pointer-events-none fixed z-40 rounded-md ring-2 ring-brand motion-safe:transition-[top,left,width,height] motion-safe:duration-200',\n dim && 'shadow-[0_0_0_9999px_rgb(10_8_14/0.45)]',\n )}\n style={{\n top: rect.top - pad,\n left: rect.left - pad,\n width: rect.width + pad * 2,\n height: rect.height + pad * 2,\n }}\n />\n );\n}\n\n/** A zero-size box a third of the way down the viewport, for steps without a target. */\nconst centerRect = () =>\n new DOMRect(window.innerWidth / 2, Math.max(80, window.innerHeight / 3), 0, 0);\n\nconst contentClasses = cn(\n surfaceClasses,\n 'w-80 max-w-[calc(100vw-2rem)] p-4 text-[13px] outline-none origin-(--radix-popover-content-transform-origin)',\n);\n\nexport interface TourStep {\n id: string;\n /** Omit for a centred step with no anchor. */\n target?: TourTarget;\n title: React.ReactNode;\n content: React.ReactNode;\n side?: 'top' | 'right' | 'bottom' | 'left';\n align?: 'start' | 'center' | 'end';\n /**\n * Reveal what the step points at (open a panel, switch a route) before it\n * shows. Aborted if the person leaves the step first; a rejection shows the\n * step centred rather than stranding the tour.\n */\n prepare?: (signal: AbortSignal) => void | Promise<void>;\n}\n\nexport type TourEndReason = 'complete' | 'skip' | 'close' | 'escape';\n\nexport interface TourLabels {\n next: string;\n back: string;\n skip: string;\n done: string;\n close: string;\n progress: (step: number, total: number) => string;\n}\n\nconst defaultLabels: TourLabels = {\n next: 'Next',\n back: 'Back',\n skip: 'Skip tour',\n done: 'Done',\n close: 'Close tour',\n progress: (step, total) => `${step} of ${total}`,\n};\n\nexport interface TourProps {\n steps: TourStep[];\n open: boolean;\n /** Controlled, zero-based. */\n step: number;\n onStepChange: (step: number) => void;\n /** The tour is over. Store dismissal or completion yourself, keyed by tour id and revision. */\n onEnd: (reason: TourEndReason, step: number) => void;\n /** `center` (default) shows a step whose target never appears in the middle; `skip` moves past it. */\n missingTarget?: 'center' | 'skip';\n /** How long to wait for a late target. Default 2000ms. */\n waitMs?: number;\n /** Dim the page outside the target. Default true. */\n dim?: boolean;\n labels?: Partial<TourLabels>;\n /** Where focus goes if the element that started the tour is gone. */\n fallbackFocus?: () => HTMLElement | null;\n}\n\n/**\n * A guided sequence of anchored steps. It is non-modal: the page stays\n * usable and outside clicks do not end it. Focus moves into each step,\n * Escape ends the tour (unless the person is typing in a field or editor\n * outside it), and focus returns to where it was when the tour started.\n * The app owns the copy, the eligibility rules and what it persists.\n */\nfunction Tour({\n steps,\n open,\n step,\n onStepChange,\n onEnd,\n missingTarget = 'center',\n waitMs = 2000,\n dim = true,\n labels: labelOverrides,\n fallbackFocus,\n}: TourProps) {\n const labels = { ...defaultLabels, ...labelOverrides };\n const current = steps[step];\n // The resolved step: which step it is for, and its target (null: centred).\n const stepKey = current ? `${step}:${current.id}` : '';\n const [resolved, setResolved] = React.useState<{ key: string; el: Element | null } | null>(null);\n const ready = open && resolved?.key === stepKey;\n const target = ready ? (resolved?.el ?? null) : null;\n const rect = useRect(target);\n const contentRef = React.useRef<HTMLDivElement>(null);\n // Radix mounts the content after the popper measures, so focus follows the node.\n const [contentEl, setContentEl] = React.useState<HTMLDivElement | null>(null);\n const returnFocus = React.useRef<HTMLElement | null>(null);\n const direction = React.useRef(1);\n const titleId = React.useId();\n const bodyId = React.useId();\n\n // Remember who started the tour; hand focus back when it ends.\n React.useLayoutEffect(() => {\n if (!open) return;\n const active = document.activeElement;\n returnFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;\n return () => {\n const back = returnFocus.current;\n const el = back && isVisible(back) ? back : (fallbackFocus?.() ?? null);\n el?.focus();\n };\n // fallbackFocus is read at close time; re-running on its identity would steal focus.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n React.useEffect(() => {\n if (!open || !current) return;\n const controller = new AbortController();\n const { signal } = controller;\n void (async () => {\n let prepared = true;\n try {\n await current.prepare?.(signal);\n } catch {\n prepared = false;\n }\n if (signal.aborted) return;\n const el = prepared ? await waitForTourTarget(current.target, waitMs, signal) : null;\n if (signal.aborted) return;\n if (!el && current.target && missingTarget === 'skip') {\n const next = step + direction.current;\n if (next >= 0 && next < steps.length) return onStepChange(next);\n }\n if (el)\n el.scrollIntoView?.({\n block: 'nearest',\n behavior: prefersReducedMotion() ? 'auto' : 'smooth',\n });\n setResolved({ key: stepKey, el });\n })();\n return () => controller.abort();\n // steps/onStepChange identity changes should not restart a step in progress.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, stepKey, waitMs, missingTarget]);\n\n // Shown once resolved and, for an anchored step, measured.\n const shown = ready && (!target || rect !== null);\n React.useEffect(() => {\n if (shown) contentEl?.focus();\n }, [shown, step, contentEl]);\n\n if (!open || !current) return null;\n const last = step === steps.length - 1;\n const go = (to: number) => {\n direction.current = to < step ? -1 : 1;\n onStepChange(to);\n };\n // Wait one frame for the target's box rather than flashing the step centred.\n if (!shown && ready) return null;\n const anchor = rect ?? centerRect();\n\n return (\n <>\n {ready && rect && <TourHighlight rect={rect} dim={dim} />}\n <PopoverPrimitive.Root open={shown} modal={false}>\n <PopoverPrimitive.Anchor\n virtualRef={{ current: { getBoundingClientRect: () => anchor } }}\n />\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={(el) => {\n contentRef.current = el;\n setContentEl(el);\n }}\n data-slot=\"tour\"\n data-step={current.id}\n tabIndex={-1}\n aria-labelledby={titleId}\n aria-describedby={bodyId}\n side={rect ? (current.side ?? 'bottom') : 'bottom'}\n align={rect ? (current.align ?? 'center') : 'center'}\n sideOffset={10}\n collisionPadding={12}\n className={contentClasses}\n onOpenAutoFocus={(event) => event.preventDefault()}\n onCloseAutoFocus={(event) => event.preventDefault()}\n onInteractOutside={(event) => event.preventDefault()}\n onEscapeKeyDown={(event) => {\n if (typingElsewhere(contentRef.current)) event.preventDefault();\n else onEnd('escape', step);\n }}\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-xs text-muted-foreground\">\n {labels.progress(step + 1, steps.length)}\n </p>\n <h2 id={titleId} className=\"mt-0.5 text-sm font-semibold text-foreground\">\n {current.title}\n </h2>\n </div>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={() => onEnd('close', step)}\n className=\"-me-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:shadow-[inset_0_0_0_1px_var(--ring)]\"\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n </div>\n <div id={bodyId} className=\"mt-2 text-muted-foreground\">\n {current.content}\n </div>\n <div className=\"mt-4 flex items-center gap-2\">\n {!last && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"-ms-2\"\n onClick={() => onEnd('skip', step)}\n >\n {labels.skip}\n </Button>\n )}\n <div className=\"ms-auto flex gap-2\">\n {step > 0 && (\n <Button variant=\"outline\" size=\"sm\" onClick={() => go(step - 1)}>\n {labels.back}\n </Button>\n )}\n <Button size=\"sm\" onClick={() => (last ? onEnd('complete', step) : go(step + 1))}>\n {last ? labels.done : labels.next}\n </Button>\n </div>\n </div>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </>\n );\n}\n\nexport interface SpotlightProps {\n target: TourTarget;\n open: boolean;\n onDismiss: (reason: 'dismiss' | 'escape') => void;\n title: React.ReactNode;\n children: React.ReactNode;\n side?: 'top' | 'right' | 'bottom' | 'left';\n align?: 'start' | 'center' | 'end';\n /** Default \"Got it\". */\n dismissLabel?: string;\n /** An extra button, e.g. \"Add a provider\". */\n action?: React.ReactNode;\n /** Ring the target. Default true. */\n highlight?: boolean;\n className?: string;\n}\n\n/**\n * One contextual hint beside a target: a new feature, a setup nudge. It is\n * informational, so it does not take focus or block the page. Escape (unless\n * the person is typing elsewhere) or the dismiss button closes it. If the target is\n * not on screen, nothing renders.\n */\nfunction Spotlight({\n target,\n open,\n onDismiss,\n title,\n children,\n side = 'bottom',\n align = 'center',\n dismissLabel = 'Got it',\n action,\n highlight = true,\n className,\n}: SpotlightProps) {\n const key = typeof target === 'string' ? target : 'fn';\n const [found, setFound] = React.useState<{ key: string; el: Element | null } | null>(null);\n const el = open && found?.key === key ? found.el : null;\n const rect = useRect(el);\n const titleId = React.useId();\n const contentRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const controller = new AbortController();\n void waitForTourTarget(target, 2000, controller.signal).then((found) => {\n if (!controller.signal.aborted) setFound({ key, el: found });\n });\n return () => controller.abort();\n // A new function identity for `target` should not re-run the lookup.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, key]);\n\n if (!open || !el || !rect) return null;\n return (\n <>\n {highlight && <TourHighlight rect={rect} dim={false} />}\n <PopoverPrimitive.Root open modal={false}>\n <PopoverPrimitive.Anchor virtualRef={{ current: { getBoundingClientRect: () => rect } }} />\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={contentRef}\n data-slot=\"spotlight\"\n role=\"note\"\n aria-labelledby={titleId}\n side={side}\n align={align}\n sideOffset={10}\n collisionPadding={12}\n className={cn(contentClasses, 'w-72', className)}\n onOpenAutoFocus={(event) => event.preventDefault()}\n onCloseAutoFocus={(event) => event.preventDefault()}\n onInteractOutside={(event) => event.preventDefault()}\n onEscapeKeyDown={(event) => {\n if (typingElsewhere(contentRef.current)) event.preventDefault();\n else onDismiss('escape');\n }}\n >\n <span className=\"sr-only\" aria-live=\"polite\">\n Tip: {title}\n </span>\n <p id={titleId} className=\"text-sm font-semibold text-foreground\">\n {title}\n </p>\n <div className=\"mt-1.5 text-muted-foreground\">{children}</div>\n <div className=\"mt-3 flex justify-end gap-2\">\n {action}\n <Button\n size=\"sm\"\n variant={action ? 'ghost' : 'default'}\n onClick={() => onDismiss('dismiss')}\n >\n {dismissLabel}\n </Button>\n </div>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </>\n );\n}\n\nexport { Spotlight, Tour, TourAnchor };\n"],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import * as React from 'react';
2
+ export interface TreeNode {
3
+ /** Stable identity. Keep it across renders (a path, a database id). */
4
+ id: string;
5
+ /** Plain text: the accessible name and the typeahead key. */
6
+ label: string;
7
+ icon?: React.ReactNode;
8
+ /** Undefined: a leaf, unless `hasChildren` says the children load later. */
9
+ children?: TreeNode[];
10
+ /** Expandable before its children are known; expanding calls onLoadChildren. */
11
+ hasChildren?: boolean;
12
+ /** Children are being fetched; shows a loading row while expanded. */
13
+ loading?: boolean;
14
+ disabled?: boolean;
15
+ /** Trailing detail, such as a git status letter or a count. */
16
+ meta?: React.ReactNode;
17
+ }
18
+ export interface TreeViewProps extends Omit<React.ComponentProps<'div'>, 'children' | 'onSelect' | 'aria-label'> {
19
+ nodes: TreeNode[];
20
+ /** Required: what the tree holds, e.g. "Files". */
21
+ 'aria-label': string;
22
+ expanded: string[];
23
+ onExpandedChange: (ids: string[]) => void;
24
+ selected: string[];
25
+ onSelectedChange: (ids: string[]) => void;
26
+ /** `multiple`: Shift extends a range, Ctrl/⌘ toggles, Space toggles. */
27
+ selectionMode?: 'single' | 'multiple';
28
+ /** Move selection with focus on arrow keys (a file explorer); default off. */
29
+ selectionFollowsFocus?: boolean;
30
+ /** Enter or double-click: open the file, run the item. */
31
+ onAction?: (id: string) => void;
32
+ /** Called once when a node with `hasChildren` and no children expands. */
33
+ onLoadChildren?: (id: string) => void;
34
+ /** Replace the label, e.g. with a rename input. Keys typed in an input never move focus. */
35
+ renderLabel?: (node: TreeNode) => React.ReactNode;
36
+ /** `compact` 24px rows for desktop explorers, `default` 28px. */
37
+ density?: 'compact' | 'default';
38
+ /** Shown when `nodes` is empty. */
39
+ empty?: React.ReactNode;
40
+ /** Right-click or the context-menu key on a row. */
41
+ onRowContextMenu?: (id: string, event: React.MouseEvent | React.KeyboardEvent) => void;
42
+ }
43
+ interface Row {
44
+ node: TreeNode;
45
+ level: number;
46
+ parentId: string | null;
47
+ setSize: number;
48
+ posInSet: number;
49
+ expandable: boolean;
50
+ expanded: boolean;
51
+ }
52
+ /** The rows a person can see, in order, with the ARIA position data for each. */
53
+ export declare function flattenTree(nodes: TreeNode[], expanded: ReadonlySet<string>): Row[];
54
+ /**
55
+ * A file-explorer style tree. Everything is controlled: the app owns which
56
+ * nodes are expanded and selected, and file operations are callbacks. Rows
57
+ * render flat (role="treeitem" with aria-level), one tab stop for the tree,
58
+ * arrows to move, Right/Left to open and close, Home/End, typeahead.
59
+ */
60
+ declare function TreeView({ nodes, expanded, onExpandedChange, selected, onSelectedChange, selectionMode, selectionFollowsFocus, onAction, onLoadChildren, renderLabel, density, empty, onRowContextMenu, className, ...props }: TreeViewProps): React.JSX.Element;
61
+ export { TreeView };