@adia-ai/web-components 0.6.37 → 0.6.39

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/components/accordion/accordion-item.a2ui.json +3 -0
  3. package/components/accordion/accordion-item.yaml +5 -0
  4. package/components/action-list/action-item.a2ui.json +5 -1
  5. package/components/action-list/action-item.yaml +7 -0
  6. package/components/card/card.a2ui.json +17 -1
  7. package/components/card/card.yaml +24 -1
  8. package/components/date-range-picker/date-range-picker.css +4 -4
  9. package/components/datetime-picker/datetime-picker.css +3 -3
  10. package/components/demo-toggle/demo-toggle.css +11 -11
  11. package/components/empty-state/empty-state.a2ui.json +9 -0
  12. package/components/empty-state/empty-state.yaml +15 -0
  13. package/components/feed/feed-item.a2ui.json +5 -0
  14. package/components/feed/feed-item.yaml +10 -0
  15. package/components/feed/feed.css +2 -2
  16. package/components/field/field.a2ui.json +6 -0
  17. package/components/field/field.css +18 -18
  18. package/components/field/field.yaml +10 -0
  19. package/components/heatmap/heatmap.css +1 -1
  20. package/components/index.js +3 -0
  21. package/components/inline-edit/inline-edit.a2ui.json +159 -0
  22. package/components/inline-edit/inline-edit.class.js +184 -0
  23. package/components/inline-edit/inline-edit.css +62 -0
  24. package/components/inline-edit/inline-edit.d.ts +52 -0
  25. package/components/inline-edit/inline-edit.js +12 -0
  26. package/components/inline-edit/inline-edit.yaml +125 -0
  27. package/components/inline-message/inline-message.css +1 -1
  28. package/components/list/list-item.a2ui.json +11 -1
  29. package/components/list/list-item.yaml +19 -0
  30. package/components/list/list.css +36 -6
  31. package/components/list-window/list-window.css +4 -4
  32. package/components/mark/mark.a2ui.json +109 -0
  33. package/components/mark/mark.class.js +22 -0
  34. package/components/mark/mark.css +39 -0
  35. package/components/mark/mark.d.ts +27 -0
  36. package/components/mark/mark.js +12 -0
  37. package/components/mark/mark.yaml +87 -0
  38. package/components/modal/modal.a2ui.json +9 -0
  39. package/components/modal/modal.css +8 -8
  40. package/components/modal/modal.yaml +14 -0
  41. package/components/nav-group/nav-group.a2ui.json +3 -0
  42. package/components/nav-group/nav-group.yaml +5 -0
  43. package/components/nav-item/nav-item.a2ui.json +3 -0
  44. package/components/nav-item/nav-item.yaml +5 -0
  45. package/components/option-card/option-card.css +9 -9
  46. package/components/segmented/segmented.class.js +10 -2
  47. package/components/select/select.a2ui.json +3 -0
  48. package/components/select/select.css +5 -5
  49. package/components/select/select.yaml +5 -0
  50. package/components/slider/slider.a2ui.json +6 -0
  51. package/components/slider/slider.yaml +10 -0
  52. package/components/stat/stat.css +18 -14
  53. package/components/stepper/stepper-item.a2ui.json +3 -0
  54. package/components/stepper/stepper-item.yaml +5 -0
  55. package/components/timeline/timeline-item.a2ui.json +8 -1
  56. package/components/timeline/timeline-item.yaml +12 -0
  57. package/components/timeline/timeline.css +19 -19
  58. package/components/tour/tour-step.a2ui.json +92 -0
  59. package/components/tour/tour-step.yaml +84 -0
  60. package/components/tour/tour.a2ui.json +172 -0
  61. package/components/tour/tour.class.js +309 -0
  62. package/components/tour/tour.css +135 -0
  63. package/components/tour/tour.d.ts +78 -0
  64. package/components/tour/tour.js +13 -0
  65. package/components/tour/tour.yaml +161 -0
  66. package/components/tree/tree-item.a2ui.json +5 -1
  67. package/components/tree/tree-item.yaml +7 -0
  68. package/components/tree/tree.a2ui.json +3 -0
  69. package/components/tree/tree.yaml +5 -0
  70. package/dist/web-components.min.css +1 -1
  71. package/dist/web-components.min.js +88 -74
  72. package/package.json +1 -1
  73. package/styles/components.css +3 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * `<tour-ui>` — spotlight-driven product tour / guided walkthrough.
3
+ *
4
+ * Orchestrates a sequence of `<tour-step>` children, each declaring a
5
+ * target element via CSS selector. When active, dims the page with a
6
+ * scrim, cuts a "hole" around the current step's target, and renders
7
+ * a popover next to it with step content + Skip / Previous / Next.
8
+ *
9
+ * Architecture:
10
+ * - Host is `display: contents` (no chrome).
11
+ * - On start, mounts two surfaces into `document.body`:
12
+ * 1. [data-tour-spotlight] — transparent rect with huge box-shadow
13
+ * scrim. Position via top/left, size via width/height (animated).
14
+ * 2. [data-tour-popover] — manual popover with step content + nav,
15
+ * anchored to the target via core/anchor.js (same mechanism as
16
+ * menu-ui / context-menu-ui / popover-ui).
17
+ * - Spotlight repositions on window resize/scroll while active.
18
+ *
19
+ * @see ../../core/anchor.js
20
+ * @see ../onboarding-checklist/ — peer onboarding primitive (user-paced).
21
+ */
22
+
23
+ import { UIElement } from '../../core/element.js';
24
+ import { anchorPopover } from '../../core/anchor.js';
25
+
26
+ export class UITour extends UIElement {
27
+ static properties = {
28
+ active: { type: Boolean, default: false, reflect: true },
29
+ step: { type: Number, default: 0, reflect: false },
30
+ autoStart: { type: Boolean, default: false, reflect: true, attribute: 'auto-start' },
31
+ storageKey: { type: String, default: '', reflect: true, attribute: 'storage-key' },
32
+ };
33
+
34
+ static template = () => null;
35
+
36
+ #spotlight = null;
37
+ #popover = null;
38
+ #anchorCleanup = null;
39
+ #onResize = null;
40
+ #onScroll = null;
41
+ #onKeydown = null;
42
+
43
+ connected() {
44
+ super.connected();
45
+ if (this.autoStart && !this.#alreadyDone()) {
46
+ // Defer one frame so consumer targets (which may also mount on the
47
+ // same tick) have a chance to land in the DOM first.
48
+ requestAnimationFrame(() => {
49
+ if (this.isConnected && this.autoStart) this.start();
50
+ });
51
+ }
52
+ }
53
+
54
+ disconnected() {
55
+ super.disconnected();
56
+ this.#teardown();
57
+ }
58
+
59
+ /* ── Public API ──────────────────────────────────────────────────── */
60
+
61
+ start() {
62
+ if (this.active) return;
63
+ if (!this.#steps.length) {
64
+ // eslint-disable-next-line no-console
65
+ console.warn('[tour-ui] start() called with no <tour-step> children — nothing to render.');
66
+ return;
67
+ }
68
+ this.step = 0;
69
+ this.active = true;
70
+ this.#mount();
71
+ this.#renderStep();
72
+ this.dispatchEvent(new CustomEvent('tour-start', {
73
+ bubbles: true,
74
+ detail: { step: 0 },
75
+ }));
76
+ }
77
+
78
+ next() {
79
+ if (!this.active) return;
80
+ const total = this.#steps.length;
81
+ if (this.step >= total - 1) { this.finish(); return; }
82
+ const from = this.step;
83
+ this.step = from + 1;
84
+ this.#renderStep();
85
+ this.dispatchEvent(new CustomEvent('tour-step-change', {
86
+ bubbles: true,
87
+ detail: { from, to: this.step, totalSteps: total },
88
+ }));
89
+ }
90
+
91
+ previous() {
92
+ if (!this.active) return;
93
+ if (this.step <= 0) return;
94
+ const from = this.step;
95
+ this.step = from - 1;
96
+ this.#renderStep();
97
+ this.dispatchEvent(new CustomEvent('tour-step-change', {
98
+ bubbles: true,
99
+ detail: { from, to: this.step, totalSteps: this.#steps.length },
100
+ }));
101
+ }
102
+
103
+ skip() {
104
+ if (!this.active) return;
105
+ const total = this.#steps.length;
106
+ const at = this.step;
107
+ this.#recordDone();
108
+ this.active = false;
109
+ this.#teardown();
110
+ this.dispatchEvent(new CustomEvent('tour-skip', {
111
+ bubbles: true,
112
+ detail: { step: at, totalSteps: total },
113
+ }));
114
+ }
115
+
116
+ finish() {
117
+ if (!this.active) return;
118
+ const total = this.#steps.length;
119
+ this.#recordDone();
120
+ this.active = false;
121
+ this.#teardown();
122
+ this.dispatchEvent(new CustomEvent('tour-finish', {
123
+ bubbles: true,
124
+ detail: { totalSteps: total },
125
+ }));
126
+ }
127
+
128
+ /* ── Internals ───────────────────────────────────────────────────── */
129
+
130
+ get #steps() {
131
+ return [...this.querySelectorAll(':scope > tour-step-ui')];
132
+ }
133
+
134
+ #alreadyDone() {
135
+ if (!this.storageKey) return false;
136
+ try { return localStorage.getItem(this.storageKey) === 'done'; }
137
+ catch { return false; }
138
+ }
139
+
140
+ #recordDone() {
141
+ if (!this.storageKey) return;
142
+ try { localStorage.setItem(this.storageKey, 'done'); }
143
+ catch { /* quota / privacy mode — silently ignore */ }
144
+ }
145
+
146
+ #mount() {
147
+ if (!this.#spotlight) {
148
+ const sp = document.createElement('div');
149
+ sp.setAttribute('data-tour-spotlight', '');
150
+ sp.setAttribute('aria-hidden', 'true');
151
+ document.body.appendChild(sp);
152
+ this.#spotlight = sp;
153
+ }
154
+ if (!this.#popover) {
155
+ const pop = document.createElement('div');
156
+ pop.setAttribute('data-tour-popover', '');
157
+ pop.setAttribute('popover', 'manual');
158
+ pop.setAttribute('role', 'dialog');
159
+ pop.setAttribute('aria-modal', 'false');
160
+ pop.tabIndex = -1;
161
+ pop.style.outline = 'none';
162
+ pop.addEventListener('click', this.#onPopoverClick);
163
+ document.body.appendChild(pop);
164
+ this.#popover = pop;
165
+ }
166
+
167
+ // Reposition spotlight on viewport changes while active. anchorPopover
168
+ // (called per-step in #renderStep) handles popover repositioning.
169
+ this.#onResize = () => this.#positionSpotlight();
170
+ this.#onScroll = () => this.#positionSpotlight();
171
+ window.addEventListener('resize', this.#onResize);
172
+ window.addEventListener('scroll', this.#onScroll, true /* capture */);
173
+
174
+ // Keyboard navigation
175
+ this.#onKeydown = (e) => {
176
+ if (!this.active) return;
177
+ if (e.key === 'Escape') { e.preventDefault(); this.skip(); }
178
+ else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); }
179
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); this.previous(); }
180
+ };
181
+ document.addEventListener('keydown', this.#onKeydown);
182
+ }
183
+
184
+ #teardown() {
185
+ this.#anchorCleanup?.();
186
+ this.#anchorCleanup = null;
187
+ if (this.#onResize) window.removeEventListener('resize', this.#onResize);
188
+ if (this.#onScroll) window.removeEventListener('scroll', this.#onScroll, true);
189
+ if (this.#onKeydown) document.removeEventListener('keydown', this.#onKeydown);
190
+ this.#onResize = this.#onScroll = this.#onKeydown = null;
191
+ try { this.#popover?.hidePopover?.(); } catch { /* noop */ }
192
+ this.#popover?.removeEventListener('click', this.#onPopoverClick);
193
+ this.#popover?.remove();
194
+ this.#popover = null;
195
+ this.#spotlight?.remove();
196
+ this.#spotlight = null;
197
+ }
198
+
199
+ #renderStep() {
200
+ const step = this.#steps[this.step];
201
+ if (!step) return;
202
+
203
+ const total = this.#steps.length;
204
+ const isLast = this.step === total - 1;
205
+ const isFirst = this.step === 0;
206
+ const title = step.getAttribute('title') || '';
207
+ const placement = step.getAttribute('placement') || 'bottom';
208
+ const targetSel = step.getAttribute('target') || '';
209
+ const target = targetSel ? document.querySelector(targetSel) : null;
210
+
211
+ // Spotlight position
212
+ this.#currentTarget = target;
213
+ this.#positionSpotlight();
214
+
215
+ // Popover content
216
+ const indicator = `${this.step + 1} of ${total}`;
217
+ const bodyHTML = step.innerHTML;
218
+ this.#popover.innerHTML = `
219
+ <div data-tour-header>
220
+ <span data-tour-indicator>${indicator}</span>
221
+ <span></span>
222
+ </div>
223
+ <h3 data-tour-title>${escapeHtml(title)}</h3>
224
+ <div data-tour-body>${bodyHTML}</div>
225
+ <div data-tour-footer>
226
+ <button-ui data-tour-skip variant="ghost" size="sm" text="Skip"></button-ui>
227
+ <div data-tour-nav>
228
+ ${isFirst ? '' : '<button-ui data-tour-previous variant="outline" size="sm" text="Back"></button-ui>'}
229
+ <button-ui data-tour-next variant="primary" size="sm" text="${isLast ? 'Done' : 'Next →'}"></button-ui>
230
+ </div>
231
+ </div>
232
+ `;
233
+
234
+ // Open the popover (idempotent) + position it.
235
+ try { this.#popover.showPopover(); } catch { /* popover API unavailable */ }
236
+ this.#anchorCleanup?.();
237
+ if (target) {
238
+ this.#anchorCleanup = anchorPopover(target, this.#popover, { placement, gap: 12 });
239
+ } else {
240
+ // No target — center the popover in the viewport
241
+ this.#popover.style.position = 'fixed';
242
+ this.#popover.style.top = '50%';
243
+ this.#popover.style.left = '50%';
244
+ this.#popover.style.transform = 'translate(-50%, -50%)';
245
+ }
246
+
247
+ // Move focus to the popover so keyboard nav works immediately
248
+ queueMicrotask(() => this.#popover?.focus?.());
249
+ }
250
+
251
+ #currentTarget = null;
252
+
253
+ #positionSpotlight() {
254
+ if (!this.#spotlight) return;
255
+ const target = this.#currentTarget;
256
+ if (!target) {
257
+ this.#spotlight.setAttribute('data-tour-no-target', '');
258
+ return;
259
+ }
260
+ this.#spotlight.removeAttribute('data-tour-no-target');
261
+ const rect = target.getBoundingClientRect();
262
+ // Add halo padding around the target
263
+ const cs = getComputedStyle(this);
264
+ const halo = parseFloat(cs.getPropertyValue('--tour-spotlight-padding')) || 8;
265
+ const top = Math.max(0, rect.top - halo);
266
+ const left = Math.max(0, rect.left - halo);
267
+ const w = rect.width + halo * 2;
268
+ const h = rect.height + halo * 2;
269
+ this.#spotlight.style.top = `${top}px`;
270
+ this.#spotlight.style.left = `${left}px`;
271
+ this.#spotlight.style.width = `${w}px`;
272
+ this.#spotlight.style.height = `${h}px`;
273
+ }
274
+
275
+ #onPopoverClick = (e) => {
276
+ const t = e.target;
277
+ if (!t || !t.closest) return;
278
+ if (t.closest('[data-tour-skip]')) { this.skip(); return; }
279
+ if (t.closest('[data-tour-previous]')) { this.previous(); return; }
280
+ if (t.closest('[data-tour-next]')) { this.next(); return; }
281
+ };
282
+ }
283
+
284
+ /**
285
+ * `<tour-step>` — data carrier for a single step inside `<tour-ui>`.
286
+ *
287
+ * Renders nothing on its own (`display: none` via tour.css). tour-ui
288
+ * reads target / title / placement / body content from it on activation.
289
+ */
290
+ export class UITourStep extends UIElement {
291
+ static properties = {
292
+ target: { type: String, default: '', reflect: true },
293
+ title: { type: String, default: '', reflect: true },
294
+ placement: { type: String, default: 'bottom', reflect: true },
295
+ };
296
+
297
+ static template = () => null;
298
+
299
+ // No connected/disconnected behavior needed — pure data carrier.
300
+ }
301
+
302
+ function escapeHtml(s) {
303
+ return String(s ?? '')
304
+ .replace(/&/g, '&amp;')
305
+ .replace(/</g, '&lt;')
306
+ .replace(/>/g, '&gt;')
307
+ .replace(/"/g, '&quot;')
308
+ .replace(/'/g, '&#39;');
309
+ }
@@ -0,0 +1,135 @@
1
+ @scope (tour-ui) {
2
+ :where(:scope) {
3
+ --tour-scrim-default: var(--a-scrim-dialog);
4
+ --tour-spotlight-padding-default: var(--a-space-2);
5
+ --tour-spotlight-radius-default: var(--a-radius-md);
6
+ }
7
+
8
+ :scope {
9
+ /* Behavioral wrapper — no chrome. Tour-step children are hidden;
10
+ the orchestrator clones their content into the popover surface. */
11
+ display: contents;
12
+ }
13
+
14
+ :scope > tour-step-ui {
15
+ display: none;
16
+ }
17
+ }
18
+
19
+ /* The spotlight + popover surfaces are NOT scoped to tour-ui — they're
20
+ appended to document.body (top-layer via Popover API), outside the
21
+ @scope (tour-ui) boundary. Mirror the menu-ui / context-menu-ui
22
+ pattern: chrome lives in the global scope keyed off a data-attribute. */
23
+
24
+ /* ── Spotlight ──
25
+ A position:fixed transparent rectangle with a huge box-shadow that
26
+ produces the dimmed scrim everywhere outside its bounds. Single
27
+ element, no clip-path / SVG mask. Animated via top/left/width/height
28
+ transitions when the step changes. */
29
+ [data-tour-spotlight] {
30
+ position: fixed;
31
+ inset: 0 auto auto 0; /* anchor at viewport top-left; top/left set imperatively */
32
+ width: 0;
33
+ height: 0;
34
+ pointer-events: none;
35
+ border-radius: var(--tour-spotlight-radius, var(--a-radius-md));
36
+ background: transparent;
37
+ box-shadow: 0 0 0 9999px var(--tour-scrim, var(--a-scrim-dialog));
38
+ z-index: 1000;
39
+ transition:
40
+ top var(--a-duration) var(--a-easing-out),
41
+ left var(--a-duration) var(--a-easing-out),
42
+ width var(--a-duration) var(--a-easing-out),
43
+ height var(--a-duration) var(--a-easing-out);
44
+ }
45
+
46
+ [data-tour-spotlight][data-tour-no-target] {
47
+ /* No target match — fall back to a non-cutout full-viewport scrim */
48
+ inset: 0;
49
+ width: 100vw;
50
+ height: 100vh;
51
+ box-shadow: none;
52
+ background: var(--tour-scrim, var(--a-scrim-dialog));
53
+ border-radius: 0;
54
+ }
55
+
56
+ @media (prefers-reduced-motion: reduce) {
57
+ [data-tour-spotlight] { transition: none; }
58
+ }
59
+
60
+ /* ── Step popover ── */
61
+ [data-tour-popover] {
62
+ margin: 0;
63
+ padding: var(--a-space-4);
64
+ border: 1px solid var(--tour-popover-border, var(--a-border-subtle));
65
+ border-radius: var(--tour-popover-radius, var(--a-radius-lg));
66
+ background: var(--tour-popover-bg, var(--a-bg-subtle));
67
+ box-shadow: var(--tour-popover-shadow, var(--a-shadow-lg));
68
+ min-width: var(--tour-popover-min-width, 18rem);
69
+ max-width: var(--tour-popover-max-width, 22rem);
70
+ font-family: inherit;
71
+ font-size: var(--a-ui-size);
72
+ color: var(--a-fg);
73
+ /* Above the spotlight (1000) so the user can read + click it */
74
+ z-index: 1001;
75
+ opacity: 1;
76
+ translate: 0 0;
77
+ transition:
78
+ opacity var(--a-duration-fast) var(--a-easing-out),
79
+ translate var(--a-duration-fast) var(--a-easing-out);
80
+ }
81
+
82
+ [data-tour-popover]:popover-open {
83
+ @starting-style {
84
+ opacity: 0;
85
+ translate: 0 -4px;
86
+ }
87
+ }
88
+
89
+ [data-tour-popover]:not(:popover-open) {
90
+ display: none !important;
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ [data-tour-popover] { transition: none; }
95
+ }
96
+
97
+ /* Layout of the popover internal content (set by the orchestrator). */
98
+ [data-tour-popover] [data-tour-header] {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ gap: var(--a-space-3);
103
+ margin-block-end: var(--a-space-2);
104
+ }
105
+
106
+ [data-tour-popover] [data-tour-indicator] {
107
+ font-size: var(--a-ui-sm);
108
+ color: var(--a-fg-muted);
109
+ font-variant-numeric: tabular-nums;
110
+ }
111
+
112
+ [data-tour-popover] [data-tour-title] {
113
+ margin: 0;
114
+ font-size: var(--a-text-lg);
115
+ font-weight: 600;
116
+ }
117
+
118
+ [data-tour-popover] [data-tour-body] {
119
+ margin: 0 0 var(--a-space-4);
120
+ font-size: var(--a-ui-size);
121
+ color: var(--a-fg);
122
+ line-height: 1.5;
123
+ }
124
+
125
+ [data-tour-popover] [data-tour-footer] {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ gap: var(--a-space-2);
130
+ }
131
+
132
+ [data-tour-popover] [data-tour-nav] {
133
+ display: flex;
134
+ gap: var(--a-space-2);
135
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `<tour-ui>` — Spotlight-driven product tour / guided walkthrough. Hosts a sequence
3
+ of `<tour-step>` children, each targeting an element via CSS selector.
4
+ When active, dims the page with a scrim, cuts a "hole" around the
5
+ current step's target, and renders a popover next to it with step
6
+ content + Skip / Previous / Next navigation.
7
+
8
+ Use for first-run onboarding tours, feature introductions after a
9
+ release, and inline walkthroughs the user opts into ("Take the tour").
10
+ Distinct from `<onboarding-checklist-ui>` (persistent todo-list of
11
+ setup steps the user works through at their own pace) and from
12
+ `<tooltip-ui>` (single hover hint with no orchestration).
13
+
14
+ Architecture: tour-ui is a behavioral wrapper (`display: contents`)
15
+ that reads target / title / content from `<tour-step>` children. On
16
+ start, mounts a spotlight + popover surface into `document.body`
17
+ (top-layer via Popover API). The spotlight is a transparent
18
+ position:fixed element with a huge box-shadow that produces the
19
+ dimmed scrim outside its bounds (single-element backdrop, no clip-path
20
+ or SVG mask needed). The popover is anchored to the target via the
21
+ same `core/anchor.js` pattern used by menu-ui / context-menu-ui /
22
+ popover-ui.
23
+
24
+ *
25
+ * @see https://ui-kit.exe.xyz/site/components/tour
26
+ *
27
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
28
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
29
+ * run `npm run build:components`, then `npm run codegen:dts` to
30
+ * regenerate; or hand-author this file fully if rich event types are
31
+ * needed beyond what the yaml `events:` block can express.
32
+ */
33
+
34
+ import { UIElement } from '../../core/element.js';
35
+
36
+ export type TourFinishEvent = CustomEvent<unknown>;
37
+ export type TourSkipEvent = CustomEvent<unknown>;
38
+ export type TourStartEvent = CustomEvent<unknown>;
39
+ export type TourStepChangeEvent = CustomEvent<unknown>;
40
+
41
+ export class UITour extends UIElement {
42
+ /** Whether the tour is currently running. Setting to `true` mounts
43
+ the spotlight + popover; setting to `false` tears down.
44
+ */
45
+ active: boolean;
46
+ /** Current step index (0-based). Setting this property advances the
47
+ tour to that step (updating spotlight + popover position).
48
+ */
49
+ step: number;
50
+
51
+ addEventListener<K extends keyof HTMLElementEventMap>(
52
+ type: K,
53
+ listener: (this: UITour, ev: HTMLElementEventMap[K]) => unknown,
54
+ options?: boolean | AddEventListenerOptions,
55
+ ): void;
56
+ addEventListener(type: 'tour-finish', listener: (ev: TourFinishEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
57
+ addEventListener(type: 'tour-skip', listener: (ev: TourSkipEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
58
+ addEventListener(type: 'tour-start', listener: (ev: TourStartEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
59
+ addEventListener(type: 'tour-step-change', listener: (ev: TourStepChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
60
+ }
61
+
62
+ export class UITourStep extends UIElement {
63
+ /** Step heading rendered in the popover. */
64
+ title: string;
65
+ /** Preferred popover placement relative to the target. Same vocabulary
66
+ as `core/anchor.js` (`top`, `bottom`, `left`, `right`,
67
+ `bottom-start`, `bottom-end`, etc.). Auto-flips on viewport overflow
68
+ via position-try-fallbacks.
69
+ */
70
+ placement: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end';
71
+ /** CSS selector for the element to spotlight when this step is
72
+ active. Resolved via `document.querySelector(target)` at step
73
+ activation time. If the selector matches nothing, the spotlight
74
+ falls back to viewport-center and the popover anchors to the
75
+ page (no halo cutout).
76
+ */
77
+ target: string;
78
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `<tour-ui>` + `<tour-step>` — auto-registers both tags on import.
3
+ *
4
+ * @see ../../USAGE.md#registration--auto-vs-explicit
5
+ */
6
+
7
+ import { defineIfFree } from '../../core/register.js';
8
+ import { UITour, UITourStep } from './tour.class.js';
9
+
10
+ defineIfFree('tour-ui', UITour);
11
+ defineIfFree('tour-step-ui', UITourStep);
12
+
13
+ export { UITour, UITourStep };
@@ -0,0 +1,161 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UITour
3
+ tag: tour-ui
4
+ status: stable
5
+ component: Tour
6
+ category: feedback
7
+ version: 1
8
+ description: |
9
+ Spotlight-driven product tour / guided walkthrough. Hosts a sequence
10
+ of `<tour-step>` children, each targeting an element via CSS selector.
11
+ When active, dims the page with a scrim, cuts a "hole" around the
12
+ current step's target, and renders a popover next to it with step
13
+ content + Skip / Previous / Next navigation.
14
+
15
+ Use for first-run onboarding tours, feature introductions after a
16
+ release, and inline walkthroughs the user opts into ("Take the tour").
17
+ Distinct from `<onboarding-checklist-ui>` (persistent todo-list of
18
+ setup steps the user works through at their own pace) and from
19
+ `<tooltip-ui>` (single hover hint with no orchestration).
20
+
21
+ Architecture: tour-ui is a behavioral wrapper (`display: contents`)
22
+ that reads target / title / content from `<tour-step>` children. On
23
+ start, mounts a spotlight + popover surface into `document.body`
24
+ (top-layer via Popover API). The spotlight is a transparent
25
+ position:fixed element with a huge box-shadow that produces the
26
+ dimmed scrim outside its bounds (single-element backdrop, no clip-path
27
+ or SVG mask needed). The popover is anchored to the target via the
28
+ same `core/anchor.js` pattern used by menu-ui / context-menu-ui /
29
+ popover-ui.
30
+ composes:
31
+ - button-ui
32
+ - tour-step-ui
33
+ props:
34
+ active:
35
+ description: |
36
+ Whether the tour is currently running. Setting to `true` mounts
37
+ the spotlight + popover; setting to `false` tears down.
38
+ type: boolean
39
+ default: false
40
+ reflect: true
41
+ step:
42
+ description: |
43
+ Current step index (0-based). Setting this property advances the
44
+ tour to that step (updating spotlight + popover position).
45
+ type: number
46
+ default: 0
47
+ reflect: false
48
+ auto-start:
49
+ description: |
50
+ Start the tour automatically when the element connects. Useful for
51
+ first-run flows gated by a storage flag on the consumer side.
52
+ type: boolean
53
+ default: false
54
+ reflect: true
55
+ storage-key:
56
+ description: |
57
+ Optional localStorage key. When set, the tour records its
58
+ completion state (`"done"`) to that key on finish/skip — and
59
+ refuses to auto-start on subsequent loads if the key is "done".
60
+ Useful for "show this tour once per user" flows.
61
+ type: string
62
+ default: ''
63
+ reflect: true
64
+ events:
65
+ tour-start:
66
+ description: 'Fired when the tour starts (after spotlight + popover mount). detail = { step }. Bubbles.'
67
+ tour-step-change:
68
+ description: 'Fired when the active step changes. detail = { from, to, totalSteps }. Bubbles.'
69
+ tour-skip:
70
+ description: 'Fired when the user skips the tour mid-way. detail = { step, totalSteps }. Bubbles.'
71
+ tour-finish:
72
+ description: 'Fired when the user completes the last step. detail = { totalSteps }. Bubbles.'
73
+ slots:
74
+ default:
75
+ description: '<tour-step> children — each declares target + title + body content.'
76
+ states:
77
+ - name: idle
78
+ description: Tour is dormant; no scrim / popover rendered.
79
+ - name: active
80
+ description: Tour is running; scrim dims the page, current target is spotlighted.
81
+ - name: complete
82
+ description: Last step reached (Done is shown instead of Next).
83
+ traits: []
84
+ tokens:
85
+ --tour-scrim:
86
+ description: Backdrop scrim color (everything outside the spotlight).
87
+ default: var(--a-scrim-dialog)
88
+ --tour-spotlight-padding:
89
+ description: Padding around the target bounding box (the spotlight's "halo").
90
+ default: var(--a-space-2)
91
+ --tour-spotlight-radius:
92
+ description: Border radius of the spotlight cutout.
93
+ default: var(--a-radius-md)
94
+ --tour-popover-bg:
95
+ description: Background of the step-content popover.
96
+ default: var(--a-bg-subtle)
97
+ --tour-popover-border:
98
+ description: Border color of the popover.
99
+ default: var(--a-border-subtle)
100
+ --tour-popover-shadow:
101
+ description: Shadow of the popover.
102
+ default: var(--a-shadow-lg)
103
+ --tour-popover-radius:
104
+ description: Border-radius of the popover.
105
+ default: var(--a-radius-lg)
106
+ --tour-popover-min-width:
107
+ description: Minimum width of the popover (so step text wraps reasonably).
108
+ default: 18rem
109
+ --tour-popover-max-width:
110
+ description: Maximum width of the popover.
111
+ default: 22rem
112
+ a2ui:
113
+ rules:
114
+ - rule: 'Use tour-ui for spotlight-driven product tours / walkthroughs — sequence of steps each targeting a CSS-selector-resolved element.'
115
+ reason: 'Primary use case — guided feature introduction.'
116
+ - rule: 'Children are <tour-step target="..." title="..."> with body content as the default slot. tour-ui reads them in DOM order.'
117
+ reason: 'Authoring contract.'
118
+ - rule: 'Set [auto-start] for first-run flows (paired with [storage-key] so it only shows once). Otherwise call .start() programmatically from a "Take the tour" button.'
119
+ reason: 'Activation contract.'
120
+ - rule: 'Distinct from <onboarding-checklist-ui> (persistent setup todo-list, user-paced) and <tooltip-ui> (single hover hint, no orchestration).'
121
+ reason: 'Sibling-component boundary.'
122
+ - rule: 'Listen to `tour-finish` for "user completed the tour" analytics; `tour-skip` for "user opted out". Both fire once per session.'
123
+ reason: 'Event-handling contract.'
124
+ anti_patterns:
125
+ - wrong: '<tour-ui><div>step 1 content</div><div>step 2 content</div></tour-ui>'
126
+ why: 'Bare divs have no target / title — tour-ui can''t position the spotlight.'
127
+ fix: '<tour-ui><tour-step target="#dashboard" title="Dashboard">Tour the dashboard…</tour-step>…</tour-ui>'
128
+ - wrong: '<tour-step target="dashboard">'
129
+ why: 'target is a CSS selector — bare "dashboard" matches a <dashboard> element, NOT an id; use the # prefix.'
130
+ fix: '<tour-step target="#dashboard">'
131
+ examples:
132
+ - name: basic-tour
133
+ description: A 3-step product tour anchored to selector-resolved elements.
134
+ a2ui: |
135
+ [
136
+ { "id": "tour", "component": "Tour", "auto-start": true, "children": ["s1", "s2", "s3"] },
137
+ { "id": "s1", "component": "TourStep", "target": "#dashboard", "title": "Your dashboard", "textContent": "Here's where you'll see your latest activity." },
138
+ { "id": "s2", "component": "TourStep", "target": "#filters", "title": "Filters", "textContent": "Narrow results by status, owner, or date." },
139
+ { "id": "s3", "component": "TourStep", "target": "#export", "title": "Export", "textContent": "Download a CSV of the current view." }
140
+ ]
141
+ keywords:
142
+ - tour
143
+ - product-tour
144
+ - walkthrough
145
+ - guided-tour
146
+ - spotlight
147
+ - coachmark
148
+ - feature-introduction
149
+ synonyms:
150
+ tour:
151
+ - product-tour
152
+ - guided-tour
153
+ - walkthrough
154
+ - feature-intro
155
+ spotlight:
156
+ - coachmark
157
+ - feature-highlight
158
+ related:
159
+ - tooltip
160
+ - popover
161
+ - onboarding-checklist