@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.
- package/CHANGELOG.md +32 -0
- package/components/accordion/accordion-item.a2ui.json +3 -0
- package/components/accordion/accordion-item.yaml +5 -0
- package/components/action-list/action-item.a2ui.json +5 -1
- package/components/action-list/action-item.yaml +7 -0
- package/components/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- package/components/date-range-picker/date-range-picker.css +4 -4
- package/components/datetime-picker/datetime-picker.css +3 -3
- package/components/demo-toggle/demo-toggle.css +11 -11
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.yaml +15 -0
- package/components/feed/feed-item.a2ui.json +5 -0
- package/components/feed/feed-item.yaml +10 -0
- package/components/feed/feed.css +2 -2
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.css +18 -18
- package/components/field/field.yaml +10 -0
- package/components/heatmap/heatmap.css +1 -1
- package/components/index.js +3 -0
- package/components/inline-edit/inline-edit.a2ui.json +159 -0
- package/components/inline-edit/inline-edit.class.js +184 -0
- package/components/inline-edit/inline-edit.css +62 -0
- package/components/inline-edit/inline-edit.d.ts +52 -0
- package/components/inline-edit/inline-edit.js +12 -0
- package/components/inline-edit/inline-edit.yaml +125 -0
- package/components/inline-message/inline-message.css +1 -1
- package/components/list/list-item.a2ui.json +11 -1
- package/components/list/list-item.yaml +19 -0
- package/components/list/list.css +36 -6
- package/components/list-window/list-window.css +4 -4
- package/components/mark/mark.a2ui.json +109 -0
- package/components/mark/mark.class.js +22 -0
- package/components/mark/mark.css +39 -0
- package/components/mark/mark.d.ts +27 -0
- package/components/mark/mark.js +12 -0
- package/components/mark/mark.yaml +87 -0
- package/components/modal/modal.a2ui.json +9 -0
- package/components/modal/modal.css +8 -8
- package/components/modal/modal.yaml +14 -0
- package/components/nav-group/nav-group.a2ui.json +3 -0
- package/components/nav-group/nav-group.yaml +5 -0
- package/components/nav-item/nav-item.a2ui.json +3 -0
- package/components/nav-item/nav-item.yaml +5 -0
- package/components/option-card/option-card.css +9 -9
- package/components/segmented/segmented.class.js +10 -2
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.css +5 -5
- package/components/select/select.yaml +5 -0
- package/components/slider/slider.a2ui.json +6 -0
- package/components/slider/slider.yaml +10 -0
- package/components/stat/stat.css +18 -14
- package/components/stepper/stepper-item.a2ui.json +3 -0
- package/components/stepper/stepper-item.yaml +5 -0
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- package/components/timeline/timeline.css +19 -19
- package/components/tour/tour-step.a2ui.json +92 -0
- package/components/tour/tour-step.yaml +84 -0
- package/components/tour/tour.a2ui.json +172 -0
- package/components/tour/tour.class.js +309 -0
- package/components/tour/tour.css +135 -0
- package/components/tour/tour.d.ts +78 -0
- package/components/tour/tour.js +13 -0
- package/components/tour/tour.yaml +161 -0
- package/components/tree/tree-item.a2ui.json +5 -1
- package/components/tree/tree-item.yaml +7 -0
- package/components/tree/tree.a2ui.json +3 -0
- package/components/tree/tree.yaml +5 -0
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +88 -74
- package/package.json +1 -1
- 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, '&')
|
|
305
|
+
.replace(/</g, '<')
|
|
306
|
+
.replace(/>/g, '>')
|
|
307
|
+
.replace(/"/g, '"')
|
|
308
|
+
.replace(/'/g, ''');
|
|
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
|