@pablo2410/shared-ui 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/{chunk-4QJMLQSH.js → chunk-7LI4Y6WT.js} +21 -2
- package/dist/chunk-7LI4Y6WT.js.map +1 -0
- package/dist/chunk-LUQAR6NB.js +1206 -0
- package/dist/chunk-LUQAR6NB.js.map +1 -0
- package/dist/{chunk-4ERWOH67.js → chunk-MGTOQY6O.js} +2 -2
- package/dist/{chunk-S4UQSTZN.js → chunk-UQUMDBCZ.js} +8 -2
- package/dist/chunk-UQUMDBCZ.js.map +1 -0
- package/dist/components/index.js +3 -3
- package/dist/hierarchy/index.js +1 -1
- package/dist/layout/index.js +4 -3
- package/dist/reporting/index.js +3 -3
- package/dist/services/index.d.ts +18 -2
- package/dist/services/index.js +2 -2
- package/dist/shell/index.js +5 -4
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/oplytics-shell.css +1 -1
- package/dist/tour/index.d.ts +197 -1
- package/dist/tour/index.js +55 -601
- package/dist/tour/index.js.map +1 -1
- package/package.json +15 -9
- package/dist/chunk-4QJMLQSH.js.map +0 -1
- package/dist/chunk-S4UQSTZN.js.map +0 -1
- /package/dist/{chunk-4ERWOH67.js.map → chunk-MGTOQY6O.js.map} +0 -0
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
portalUrl
|
|
3
|
+
} from "./chunk-7LI4Y6WT.js";
|
|
4
|
+
import {
|
|
5
|
+
cn
|
|
6
|
+
} from "./chunk-JT3XLKKD.js";
|
|
7
|
+
|
|
8
|
+
// src/tour/tourStore.ts
|
|
9
|
+
var MAX_STEP_ID_LENGTH = 120;
|
|
10
|
+
var MAX_TOUR_ID_LENGTH = 60;
|
|
11
|
+
var TOUR_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
12
|
+
var STEP_KINDS = ["intro", "service", "outro"];
|
|
13
|
+
var InvalidTourDefinitionError = class extends Error {
|
|
14
|
+
problems;
|
|
15
|
+
constructor(tourId, problems) {
|
|
16
|
+
super(`Invalid tour definition "${tourId}": ${problems.join("; ")}`);
|
|
17
|
+
this.name = "InvalidTourDefinitionError";
|
|
18
|
+
this.problems = problems;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function validateTourDefinition(def) {
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (typeof def.id !== "string" || def.id.length === 0) {
|
|
24
|
+
problems.push("id is required");
|
|
25
|
+
} else {
|
|
26
|
+
if (def.id.length > MAX_TOUR_ID_LENGTH) {
|
|
27
|
+
problems.push(`id is longer than ${MAX_TOUR_ID_LENGTH} characters`);
|
|
28
|
+
}
|
|
29
|
+
if (!TOUR_ID_PATTERN.test(def.id)) {
|
|
30
|
+
problems.push("id must be lowercase letters, digits and hyphens");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(def.steps) || def.steps.length === 0) {
|
|
34
|
+
problems.push("at least one step is required");
|
|
35
|
+
return problems;
|
|
36
|
+
}
|
|
37
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38
|
+
def.steps.forEach((step, i) => {
|
|
39
|
+
const at = `step ${i + 1}`;
|
|
40
|
+
if (!step.id) {
|
|
41
|
+
problems.push(`${at} has no id`);
|
|
42
|
+
} else {
|
|
43
|
+
if (step.id.length > MAX_STEP_ID_LENGTH) {
|
|
44
|
+
problems.push(
|
|
45
|
+
`${at} id is longer than ${MAX_STEP_ID_LENGTH} characters`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (seen.has(step.id)) problems.push(`duplicate step id "${step.id}"`);
|
|
49
|
+
seen.add(step.id);
|
|
50
|
+
}
|
|
51
|
+
if (!STEP_KINDS.includes(step.kind)) {
|
|
52
|
+
problems.push(`${at} has an unknown kind "${String(step.kind)}"`);
|
|
53
|
+
}
|
|
54
|
+
if (!step.label) problems.push(`${at} has no label`);
|
|
55
|
+
if (!step.fallbackText) problems.push(`${at} has no fallbackText`);
|
|
56
|
+
if (step.spotlight && !step.spotlight.selector) {
|
|
57
|
+
problems.push(`${at} has a spotlight with no selector`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return problems;
|
|
61
|
+
}
|
|
62
|
+
function createTourStore() {
|
|
63
|
+
const tours = /* @__PURE__ */ new Map();
|
|
64
|
+
let session = null;
|
|
65
|
+
let state = { active: null, session: null };
|
|
66
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
67
|
+
const recompute = () => {
|
|
68
|
+
const last = [...tours.values()].pop() ?? null;
|
|
69
|
+
if (state.active && last?.id !== state.active.id) session = null;
|
|
70
|
+
if (last !== state.active || session !== state.session) {
|
|
71
|
+
state = { active: last, session };
|
|
72
|
+
}
|
|
73
|
+
listeners.forEach((l) => l());
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
register(def) {
|
|
77
|
+
const problems = validateTourDefinition(def);
|
|
78
|
+
if (problems.length > 0) {
|
|
79
|
+
throw new InvalidTourDefinitionError(def?.id ?? "(no id)", problems);
|
|
80
|
+
}
|
|
81
|
+
tours.delete(def.id);
|
|
82
|
+
tours.set(def.id, def);
|
|
83
|
+
recompute();
|
|
84
|
+
return () => {
|
|
85
|
+
if (tours.get(def.id) === def) {
|
|
86
|
+
tours.delete(def.id);
|
|
87
|
+
recompute();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
open(next = { restart: true }) {
|
|
92
|
+
if (!state.active) return;
|
|
93
|
+
session = next;
|
|
94
|
+
recompute();
|
|
95
|
+
},
|
|
96
|
+
close() {
|
|
97
|
+
if (session === null) return;
|
|
98
|
+
session = null;
|
|
99
|
+
recompute();
|
|
100
|
+
},
|
|
101
|
+
getState: () => state,
|
|
102
|
+
subscribe(listener) {
|
|
103
|
+
listeners.add(listener);
|
|
104
|
+
return () => {
|
|
105
|
+
listeners.delete(listener);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
var defaultTourStore = createTourStore();
|
|
111
|
+
|
|
112
|
+
// src/tour/tourProgress.ts
|
|
113
|
+
var SERVICE_HUB_TOUR_ID = "service-hub";
|
|
114
|
+
var TourProgressError = class extends Error {
|
|
115
|
+
status;
|
|
116
|
+
constructor(message, status) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "TourProgressError";
|
|
119
|
+
this.status = status;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var UNSEEN = () => ({
|
|
123
|
+
status: "unseen",
|
|
124
|
+
lastStepId: null,
|
|
125
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
126
|
+
});
|
|
127
|
+
function unwrapTrpc(body) {
|
|
128
|
+
const data = body?.result?.data;
|
|
129
|
+
if (data === void 0 || data.json === void 0) {
|
|
130
|
+
throw new TourProgressError("Unexpected response from Portal");
|
|
131
|
+
}
|
|
132
|
+
return data.json;
|
|
133
|
+
}
|
|
134
|
+
var STATUSES = [
|
|
135
|
+
"unseen",
|
|
136
|
+
"in-progress",
|
|
137
|
+
"done",
|
|
138
|
+
"dismissed"
|
|
139
|
+
];
|
|
140
|
+
function createPortalTourProgressTransport(opts = {}) {
|
|
141
|
+
const doFetch = opts.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
142
|
+
const base = () => (opts.baseUrl ?? portalUrl("")).replace(/\/$/, "");
|
|
143
|
+
let perTourSupported;
|
|
144
|
+
const read = async (tourId) => {
|
|
145
|
+
const input = encodeURIComponent(JSON.stringify({ json: { tourId } }));
|
|
146
|
+
const res = await doFetch(
|
|
147
|
+
`${base()}/api/trpc/onboarding.userTourState?input=${input}`,
|
|
148
|
+
{ method: "GET", credentials: "include" }
|
|
149
|
+
);
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
throw new TourProgressError(
|
|
152
|
+
`Couldn't read tour progress (${res.status})`,
|
|
153
|
+
res.status
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const json = unwrapTrpc(await res.json());
|
|
157
|
+
perTourSupported = json.tourId === tourId;
|
|
158
|
+
if (tourId !== SERVICE_HUB_TOUR_ID && !perTourSupported) {
|
|
159
|
+
throw new TourProgressError(
|
|
160
|
+
"Portal does not store per-tour progress yet"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
status: STATUSES.includes(json.status) ? json.status : "unseen",
|
|
165
|
+
lastStepId: json.lastStepId ?? null,
|
|
166
|
+
updatedAt: json.updatedAt ?? UNSEEN().updatedAt
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
read,
|
|
171
|
+
async write(tourId, update) {
|
|
172
|
+
if (tourId !== SERVICE_HUB_TOUR_ID) {
|
|
173
|
+
if (perTourSupported === void 0) await read(tourId);
|
|
174
|
+
if (!perTourSupported) return;
|
|
175
|
+
}
|
|
176
|
+
const res = await doFetch(
|
|
177
|
+
`${base()}/api/trpc/onboarding.setUserTourState`,
|
|
178
|
+
{
|
|
179
|
+
method: "POST",
|
|
180
|
+
credentials: "include",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({ json: { tourId, ...update } })
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
throw new TourProgressError(
|
|
187
|
+
`Couldn't save tour progress (${res.status})`,
|
|
188
|
+
res.status
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function createTourProgressController(transport, tourId) {
|
|
195
|
+
let snapshot = {
|
|
196
|
+
phase: "loading",
|
|
197
|
+
progress: UNSEEN(),
|
|
198
|
+
error: null
|
|
199
|
+
};
|
|
200
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
201
|
+
let writeChain = Promise.resolve();
|
|
202
|
+
let recordedLocally = false;
|
|
203
|
+
const set = (next) => {
|
|
204
|
+
snapshot = next;
|
|
205
|
+
listeners.forEach((l) => l());
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
getSnapshot: () => snapshot,
|
|
209
|
+
subscribe(listener) {
|
|
210
|
+
listeners.add(listener);
|
|
211
|
+
return () => {
|
|
212
|
+
listeners.delete(listener);
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
async load() {
|
|
216
|
+
try {
|
|
217
|
+
const progress = await transport.read(tourId);
|
|
218
|
+
if (recordedLocally) return;
|
|
219
|
+
set({ phase: "ready", progress, error: null });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (recordedLocally) return;
|
|
222
|
+
set({
|
|
223
|
+
phase: "error",
|
|
224
|
+
progress: UNSEEN(),
|
|
225
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
record(update) {
|
|
230
|
+
recordedLocally = true;
|
|
231
|
+
set({
|
|
232
|
+
phase: snapshot.phase === "loading" ? "ready" : snapshot.phase,
|
|
233
|
+
progress: { ...update, updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
234
|
+
error: snapshot.error
|
|
235
|
+
});
|
|
236
|
+
writeChain = writeChain.then(() => transport.write(tourId, update)).catch((err) => {
|
|
237
|
+
set({
|
|
238
|
+
...snapshot,
|
|
239
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return writeChain;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function shouldAutoOpen(status) {
|
|
247
|
+
return status === "unseen" || status === "in-progress";
|
|
248
|
+
}
|
|
249
|
+
function decideAutoOpen(autoStart, snapshot) {
|
|
250
|
+
return autoStart === true && snapshot.phase === "ready" && shouldAutoOpen(snapshot.progress.status);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/tour/tourNarration.ts
|
|
254
|
+
var ONBOARDING_NARRATION_SERVICE = "onboarding";
|
|
255
|
+
var NARRATION_PROMPT = "Narrate this onboarding step for me in 2-3 sentences.";
|
|
256
|
+
function buildNarrationInput(step) {
|
|
257
|
+
return {
|
|
258
|
+
service: ONBOARDING_NARRATION_SERVICE,
|
|
259
|
+
insights: {
|
|
260
|
+
hook: step.fallbackText,
|
|
261
|
+
highlights: step.highlights,
|
|
262
|
+
features: step.features
|
|
263
|
+
},
|
|
264
|
+
messages: [{ role: "user", content: NARRATION_PROMPT }]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function createPortalNarrator(opts = {}) {
|
|
268
|
+
const doFetch = opts.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
269
|
+
return async (step) => {
|
|
270
|
+
const base = (opts.baseUrl ?? portalUrl("")).replace(/\/$/, "");
|
|
271
|
+
const res = await doFetch(`${base}/api/trpc/ai.askOpi`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
credentials: "include",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ json: buildNarrationInput(step) })
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) throw new Error(`Opi narration failed (${res.status})`);
|
|
278
|
+
const body = await res.json();
|
|
279
|
+
const content = body.result?.data?.json?.content;
|
|
280
|
+
if (typeof content !== "string" || content.trim() === "") {
|
|
281
|
+
throw new Error("Opi narration returned no text");
|
|
282
|
+
}
|
|
283
|
+
return content;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/tour/TourProvider.tsx
|
|
288
|
+
import {
|
|
289
|
+
createContext,
|
|
290
|
+
useContext,
|
|
291
|
+
useEffect,
|
|
292
|
+
useMemo,
|
|
293
|
+
useSyncExternalStore
|
|
294
|
+
} from "react";
|
|
295
|
+
import { jsx } from "react/jsx-runtime";
|
|
296
|
+
var defaultTransport;
|
|
297
|
+
var defaultNarrator;
|
|
298
|
+
var TourContext = createContext({});
|
|
299
|
+
function useTourContext() {
|
|
300
|
+
const ctx = useContext(TourContext);
|
|
301
|
+
return {
|
|
302
|
+
store: ctx.store ?? defaultTourStore,
|
|
303
|
+
// The default transport is shared app-wide: it remembers whether Portal
|
|
304
|
+
// supports per-tour storage after the first read.
|
|
305
|
+
transport: ctx.transport ?? (defaultTransport ??= createPortalTourProgressTransport()),
|
|
306
|
+
narrator: ctx.narrator ?? (defaultNarrator ??= createPortalNarrator())
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function TourProvider({
|
|
310
|
+
store,
|
|
311
|
+
transport,
|
|
312
|
+
narrator,
|
|
313
|
+
children
|
|
314
|
+
}) {
|
|
315
|
+
const value = useMemo(
|
|
316
|
+
() => ({ store, transport, narrator }),
|
|
317
|
+
[store, transport, narrator]
|
|
318
|
+
);
|
|
319
|
+
return /* @__PURE__ */ jsx(TourContext.Provider, { value, children });
|
|
320
|
+
}
|
|
321
|
+
function useRegisterTour(def) {
|
|
322
|
+
const { store } = useTourContext();
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (!def) return;
|
|
325
|
+
try {
|
|
326
|
+
return store.register(def);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (err instanceof InvalidTourDefinitionError) {
|
|
329
|
+
console.error(err.message);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
}, [store, def]);
|
|
335
|
+
}
|
|
336
|
+
function useTourState() {
|
|
337
|
+
const { store } = useTourContext();
|
|
338
|
+
return useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
339
|
+
}
|
|
340
|
+
function useActiveTour() {
|
|
341
|
+
return useTourState().active;
|
|
342
|
+
}
|
|
343
|
+
function useTourLauncher() {
|
|
344
|
+
const { store } = useTourContext();
|
|
345
|
+
const available = useTourState().active !== null;
|
|
346
|
+
return {
|
|
347
|
+
start: () => store.open({ restart: true }),
|
|
348
|
+
open: (session) => store.open(session),
|
|
349
|
+
close: () => store.close(),
|
|
350
|
+
available
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function useTourProgress(tourId) {
|
|
354
|
+
const { transport } = useTourContext();
|
|
355
|
+
const controller = useMemo(
|
|
356
|
+
() => createTourProgressController(transport, tourId),
|
|
357
|
+
[transport, tourId]
|
|
358
|
+
);
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
void controller.load();
|
|
361
|
+
}, [controller]);
|
|
362
|
+
const snapshot = useSyncExternalStore(
|
|
363
|
+
controller.subscribe,
|
|
364
|
+
controller.getSnapshot,
|
|
365
|
+
controller.getSnapshot
|
|
366
|
+
);
|
|
367
|
+
return { ...snapshot, record: controller.record };
|
|
368
|
+
}
|
|
369
|
+
function useTourNarrator() {
|
|
370
|
+
return useTourContext().narrator;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/tour/TakeTheTourButton.tsx
|
|
374
|
+
import { Sparkles } from "lucide-react";
|
|
375
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
376
|
+
function TakeTheTourButton({
|
|
377
|
+
label = "Take the tour",
|
|
378
|
+
className
|
|
379
|
+
}) {
|
|
380
|
+
const { available, start } = useTourLauncher();
|
|
381
|
+
if (!available) return null;
|
|
382
|
+
return /* @__PURE__ */ jsxs(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
type: "button",
|
|
386
|
+
onClick: start,
|
|
387
|
+
title: label,
|
|
388
|
+
"aria-label": label,
|
|
389
|
+
className: cn(
|
|
390
|
+
"flex items-center gap-1 px-2 py-1 rounded-md text-[10px]",
|
|
391
|
+
"text-[#8890A0] hover:text-[#E2E8F0] hover:bg-[#1E2738] transition-colors cursor-pointer",
|
|
392
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#8C34E9]",
|
|
393
|
+
className
|
|
394
|
+
),
|
|
395
|
+
children: [
|
|
396
|
+
/* @__PURE__ */ jsx2(Sparkles, { className: "h-3 w-3 text-[#8C34E9]" }),
|
|
397
|
+
/* @__PURE__ */ jsx2("span", { className: "shell-hidden shell-sm:inline", children: label })
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/tour/useSpotlight.ts
|
|
404
|
+
import { useEffect as useEffect2, useRef, useState } from "react";
|
|
405
|
+
var RESIZE_DEBOUNCE_MS = 100;
|
|
406
|
+
function useSpotlight(selector) {
|
|
407
|
+
const [rect, setRect] = useState(null);
|
|
408
|
+
const [found, setFound] = useState(false);
|
|
409
|
+
const scrolledForSelector = useRef(null);
|
|
410
|
+
useEffect2(() => {
|
|
411
|
+
if (!selector) {
|
|
412
|
+
setRect(null);
|
|
413
|
+
setFound(false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
let resizeTimer;
|
|
417
|
+
let cancelled = false;
|
|
418
|
+
const measure = () => {
|
|
419
|
+
if (cancelled) return;
|
|
420
|
+
const el = document.querySelector(
|
|
421
|
+
`[data-tour-target="${selector}"]`
|
|
422
|
+
);
|
|
423
|
+
if (!el) {
|
|
424
|
+
setRect(null);
|
|
425
|
+
setFound(false);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const domRect = el.getBoundingClientRect();
|
|
429
|
+
setRect({
|
|
430
|
+
top: domRect.top,
|
|
431
|
+
left: domRect.left,
|
|
432
|
+
width: domRect.width,
|
|
433
|
+
height: domRect.height
|
|
434
|
+
});
|
|
435
|
+
setFound(true);
|
|
436
|
+
if (scrolledForSelector.current !== selector) {
|
|
437
|
+
scrolledForSelector.current = selector;
|
|
438
|
+
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
measure();
|
|
442
|
+
const initialFrame = requestAnimationFrame(measure);
|
|
443
|
+
const onResize = () => {
|
|
444
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
445
|
+
resizeTimer = setTimeout(measure, RESIZE_DEBOUNCE_MS);
|
|
446
|
+
};
|
|
447
|
+
const onScroll = () => measure();
|
|
448
|
+
window.addEventListener("resize", onResize);
|
|
449
|
+
window.addEventListener("scroll", onScroll, { capture: true });
|
|
450
|
+
const observer = new MutationObserver(measure);
|
|
451
|
+
observer.observe(document.body, {
|
|
452
|
+
childList: true,
|
|
453
|
+
subtree: true,
|
|
454
|
+
attributes: true
|
|
455
|
+
});
|
|
456
|
+
return () => {
|
|
457
|
+
cancelled = true;
|
|
458
|
+
cancelAnimationFrame(initialFrame);
|
|
459
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
460
|
+
window.removeEventListener("resize", onResize);
|
|
461
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
462
|
+
observer?.disconnect();
|
|
463
|
+
};
|
|
464
|
+
}, [selector]);
|
|
465
|
+
return { rect, found };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/tour/spotlightMath.ts
|
|
469
|
+
function computeSpotlightFrame(target, viewport) {
|
|
470
|
+
const targetBottom = target.top + target.height;
|
|
471
|
+
const targetRight = target.left + target.width;
|
|
472
|
+
return {
|
|
473
|
+
// Full-width strip above the target.
|
|
474
|
+
top: {
|
|
475
|
+
top: 0,
|
|
476
|
+
left: 0,
|
|
477
|
+
width: viewport.width,
|
|
478
|
+
height: Math.max(0, target.top)
|
|
479
|
+
},
|
|
480
|
+
// Full-width strip below the target.
|
|
481
|
+
bottom: {
|
|
482
|
+
top: targetBottom,
|
|
483
|
+
left: 0,
|
|
484
|
+
width: viewport.width,
|
|
485
|
+
height: Math.max(0, viewport.height - targetBottom)
|
|
486
|
+
},
|
|
487
|
+
// Strip to the left of the target, spanning only the target's own row
|
|
488
|
+
// (the top/bottom strips already cover the rest of those rows).
|
|
489
|
+
left: {
|
|
490
|
+
top: target.top,
|
|
491
|
+
left: 0,
|
|
492
|
+
width: Math.max(0, target.left),
|
|
493
|
+
height: target.height
|
|
494
|
+
},
|
|
495
|
+
// Strip to the right of the target, same row-only span as `left`.
|
|
496
|
+
right: {
|
|
497
|
+
top: target.top,
|
|
498
|
+
left: targetRight,
|
|
499
|
+
width: Math.max(0, viewport.width - targetRight),
|
|
500
|
+
height: target.height
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
var FALLBACK_ORDER = {
|
|
505
|
+
bottom: ["bottom", "top", "right", "left"],
|
|
506
|
+
top: ["top", "bottom", "right", "left"],
|
|
507
|
+
right: ["right", "left", "bottom", "top"],
|
|
508
|
+
left: ["left", "right", "bottom", "top"]
|
|
509
|
+
};
|
|
510
|
+
var AUTO_ORDER = FALLBACK_ORDER.bottom;
|
|
511
|
+
var PANEL_GAP = 12;
|
|
512
|
+
function computePanelPosition(target, panelSize, viewport, placement = "auto") {
|
|
513
|
+
const order = placement === "auto" ? AUTO_ORDER : FALLBACK_ORDER[placement];
|
|
514
|
+
for (const candidate of order) {
|
|
515
|
+
const pos2 = placeAt(candidate, target, panelSize);
|
|
516
|
+
if (fits(pos2, panelSize, viewport)) {
|
|
517
|
+
return { ...pos2, placement: candidate };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const fallback = order[0];
|
|
521
|
+
const pos = placeAt(fallback, target, panelSize);
|
|
522
|
+
return {
|
|
523
|
+
top: clamp(pos.top, 0, Math.max(0, viewport.height - panelSize.height)),
|
|
524
|
+
left: clamp(pos.left, 0, Math.max(0, viewport.width - panelSize.width)),
|
|
525
|
+
placement: fallback
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function placeAt(side, target, panelSize) {
|
|
529
|
+
const targetCenterX = target.left + target.width / 2;
|
|
530
|
+
const targetCenterY = target.top + target.height / 2;
|
|
531
|
+
switch (side) {
|
|
532
|
+
case "bottom":
|
|
533
|
+
return {
|
|
534
|
+
top: target.top + target.height + PANEL_GAP,
|
|
535
|
+
left: targetCenterX - panelSize.width / 2
|
|
536
|
+
};
|
|
537
|
+
case "top":
|
|
538
|
+
return {
|
|
539
|
+
top: target.top - panelSize.height - PANEL_GAP,
|
|
540
|
+
left: targetCenterX - panelSize.width / 2
|
|
541
|
+
};
|
|
542
|
+
case "right":
|
|
543
|
+
return {
|
|
544
|
+
top: targetCenterY - panelSize.height / 2,
|
|
545
|
+
left: target.left + target.width + PANEL_GAP
|
|
546
|
+
};
|
|
547
|
+
case "left":
|
|
548
|
+
return {
|
|
549
|
+
top: targetCenterY - panelSize.height / 2,
|
|
550
|
+
left: target.left - panelSize.width - PANEL_GAP
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function fits(pos, panelSize, viewport) {
|
|
555
|
+
return pos.top >= 0 && pos.left >= 0 && pos.top + panelSize.height <= viewport.height && pos.left + panelSize.width <= viewport.width;
|
|
556
|
+
}
|
|
557
|
+
function clamp(value, min, max) {
|
|
558
|
+
return Math.min(Math.max(value, min), max);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/tour/TourShell.tsx
|
|
562
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
563
|
+
import {
|
|
564
|
+
X,
|
|
565
|
+
ChevronLeft,
|
|
566
|
+
ChevronRight,
|
|
567
|
+
ArrowRight,
|
|
568
|
+
ExternalLink
|
|
569
|
+
} from "lucide-react";
|
|
570
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
571
|
+
var defaultSkin = {
|
|
572
|
+
accent: "#8C34E9",
|
|
573
|
+
overlayBg: "rgba(10,14,26,0.72)",
|
|
574
|
+
panelBg: "#0e1624",
|
|
575
|
+
panelBorder: "1px solid #1e2738",
|
|
576
|
+
mutedText: "#596475",
|
|
577
|
+
bodyText: "#c0c6d0",
|
|
578
|
+
softText: "#8890a0"
|
|
579
|
+
};
|
|
580
|
+
var SPOTLIGHT_PANEL_SIZE = { width: 320, height: 280 };
|
|
581
|
+
function useHover() {
|
|
582
|
+
const [hovered, setHovered] = useState2(false);
|
|
583
|
+
return [
|
|
584
|
+
hovered,
|
|
585
|
+
{
|
|
586
|
+
onMouseEnter: () => setHovered(true),
|
|
587
|
+
onMouseLeave: () => setHovered(false)
|
|
588
|
+
}
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
function TourShell({
|
|
592
|
+
step,
|
|
593
|
+
index,
|
|
594
|
+
total,
|
|
595
|
+
isFirst,
|
|
596
|
+
isLast,
|
|
597
|
+
onNext,
|
|
598
|
+
onBack,
|
|
599
|
+
onDismiss,
|
|
600
|
+
narration,
|
|
601
|
+
narrationPending,
|
|
602
|
+
children,
|
|
603
|
+
skin = defaultSkin,
|
|
604
|
+
orb,
|
|
605
|
+
onSpotlightTargetActivated
|
|
606
|
+
}) {
|
|
607
|
+
const panelRef = useRef2(null);
|
|
608
|
+
const { rect, found } = useSpotlight(step.spotlight?.selector ?? null);
|
|
609
|
+
const isSpotlight = Boolean(step.spotlight) && found && rect !== null;
|
|
610
|
+
useEffect3(() => {
|
|
611
|
+
const onKeyDown = (e) => {
|
|
612
|
+
if (e.key === "Escape") onDismiss();
|
|
613
|
+
};
|
|
614
|
+
document.addEventListener("keydown", onKeyDown);
|
|
615
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
616
|
+
}, [onDismiss]);
|
|
617
|
+
useEffect3(() => {
|
|
618
|
+
panelRef.current?.focus();
|
|
619
|
+
}, [step.id]);
|
|
620
|
+
useEffect3(() => {
|
|
621
|
+
if (!isSpotlight || !onSpotlightTargetActivated || !step.spotlight) return;
|
|
622
|
+
const selector = step.spotlight.selector;
|
|
623
|
+
const onClickCapture = (e) => {
|
|
624
|
+
const target = e.target;
|
|
625
|
+
if (target?.closest(`[data-tour-target="${selector}"]`)) {
|
|
626
|
+
onSpotlightTargetActivated();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
document.addEventListener("click", onClickCapture, { capture: true });
|
|
630
|
+
return () => document.removeEventListener("click", onClickCapture, {
|
|
631
|
+
capture: true
|
|
632
|
+
});
|
|
633
|
+
}, [isSpotlight, onSpotlightTargetActivated, step.spotlight]);
|
|
634
|
+
if (isSpotlight && rect) {
|
|
635
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
636
|
+
const frame = computeSpotlightFrame(rect, viewport);
|
|
637
|
+
const panelPos = computePanelPosition(
|
|
638
|
+
rect,
|
|
639
|
+
SPOTLIGHT_PANEL_SIZE,
|
|
640
|
+
viewport,
|
|
641
|
+
step.spotlight?.placement ?? "auto"
|
|
642
|
+
);
|
|
643
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
644
|
+
["top", "bottom", "left", "right"].map((side) => {
|
|
645
|
+
const segment = frame[side];
|
|
646
|
+
if (segment.width <= 0 || segment.height <= 0) return null;
|
|
647
|
+
return /* @__PURE__ */ jsx3(
|
|
648
|
+
"div",
|
|
649
|
+
{
|
|
650
|
+
className: "fixed z-50",
|
|
651
|
+
style: {
|
|
652
|
+
top: segment.top,
|
|
653
|
+
left: segment.left,
|
|
654
|
+
width: segment.width,
|
|
655
|
+
height: segment.height,
|
|
656
|
+
background: skin.overlayBg
|
|
657
|
+
},
|
|
658
|
+
onClick: onDismiss
|
|
659
|
+
},
|
|
660
|
+
side
|
|
661
|
+
);
|
|
662
|
+
}),
|
|
663
|
+
/* @__PURE__ */ jsx3(
|
|
664
|
+
"div",
|
|
665
|
+
{
|
|
666
|
+
className: "fixed z-50 rounded-xl pointer-events-none transition-all",
|
|
667
|
+
style: {
|
|
668
|
+
top: rect.top - 4,
|
|
669
|
+
left: rect.left - 4,
|
|
670
|
+
width: rect.width + 8,
|
|
671
|
+
height: rect.height + 8,
|
|
672
|
+
boxShadow: `0 0 0 2px ${skin.accent}, 0 0 24px 4px ${skin.accent}66`
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
),
|
|
676
|
+
/* @__PURE__ */ jsxs2(
|
|
677
|
+
"div",
|
|
678
|
+
{
|
|
679
|
+
ref: panelRef,
|
|
680
|
+
tabIndex: -1,
|
|
681
|
+
role: "dialog",
|
|
682
|
+
"aria-modal": "true",
|
|
683
|
+
"aria-label": step.label,
|
|
684
|
+
className: "fixed z-50 rounded-xl overflow-hidden flex flex-col outline-none",
|
|
685
|
+
style: {
|
|
686
|
+
top: panelPos.top,
|
|
687
|
+
left: panelPos.left,
|
|
688
|
+
width: SPOTLIGHT_PANEL_SIZE.width,
|
|
689
|
+
maxHeight: SPOTLIGHT_PANEL_SIZE.height,
|
|
690
|
+
background: skin.panelBg,
|
|
691
|
+
border: skin.panelBorder
|
|
692
|
+
},
|
|
693
|
+
children: [
|
|
694
|
+
/* @__PURE__ */ jsx3(TourShellHeader, { step, orb, onDismiss }),
|
|
695
|
+
/* @__PURE__ */ jsx3(TourShellStepDots, { total, index, skin }),
|
|
696
|
+
/* @__PURE__ */ jsx3("div", { className: "overflow-y-auto", style: { padding: "12px 16px" }, children: /* @__PURE__ */ jsx3(
|
|
697
|
+
TourShellBody,
|
|
698
|
+
{
|
|
699
|
+
gap: 12,
|
|
700
|
+
step,
|
|
701
|
+
narration,
|
|
702
|
+
narrationPending,
|
|
703
|
+
skin,
|
|
704
|
+
children
|
|
705
|
+
}
|
|
706
|
+
) }),
|
|
707
|
+
/* @__PURE__ */ jsx3(
|
|
708
|
+
TourShellFooter,
|
|
709
|
+
{
|
|
710
|
+
isFirst,
|
|
711
|
+
isLast,
|
|
712
|
+
onNext,
|
|
713
|
+
onBack,
|
|
714
|
+
onDismiss,
|
|
715
|
+
skin
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
] });
|
|
722
|
+
}
|
|
723
|
+
return /* @__PURE__ */ jsx3(
|
|
724
|
+
"div",
|
|
725
|
+
{
|
|
726
|
+
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
727
|
+
style: { background: skin.overlayBg, padding: 16 },
|
|
728
|
+
onClick: (e) => {
|
|
729
|
+
if (e.target === e.currentTarget) onDismiss();
|
|
730
|
+
},
|
|
731
|
+
children: /* @__PURE__ */ jsxs2(
|
|
732
|
+
"div",
|
|
733
|
+
{
|
|
734
|
+
ref: panelRef,
|
|
735
|
+
tabIndex: -1,
|
|
736
|
+
role: "dialog",
|
|
737
|
+
"aria-modal": "true",
|
|
738
|
+
"aria-label": step.label,
|
|
739
|
+
className: "w-full max-w-md rounded-xl overflow-hidden flex flex-col max-h-[85vh] outline-none",
|
|
740
|
+
style: { background: skin.panelBg, border: skin.panelBorder },
|
|
741
|
+
children: [
|
|
742
|
+
/* @__PURE__ */ jsx3(TourShellHeader, { step, orb, onDismiss }),
|
|
743
|
+
/* @__PURE__ */ jsx3(TourShellStepDots, { total, index, skin }),
|
|
744
|
+
/* @__PURE__ */ jsx3("div", { className: "overflow-y-auto", style: { padding: "16px 20px" }, children: /* @__PURE__ */ jsx3(
|
|
745
|
+
TourShellBody,
|
|
746
|
+
{
|
|
747
|
+
gap: 16,
|
|
748
|
+
step,
|
|
749
|
+
narration,
|
|
750
|
+
narrationPending,
|
|
751
|
+
skin,
|
|
752
|
+
children
|
|
753
|
+
}
|
|
754
|
+
) }),
|
|
755
|
+
/* @__PURE__ */ jsx3(
|
|
756
|
+
TourShellFooter,
|
|
757
|
+
{
|
|
758
|
+
isFirst,
|
|
759
|
+
isLast,
|
|
760
|
+
onNext,
|
|
761
|
+
onBack,
|
|
762
|
+
onDismiss,
|
|
763
|
+
skin
|
|
764
|
+
}
|
|
765
|
+
)
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
)
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
function TourShellHeader({
|
|
773
|
+
step,
|
|
774
|
+
orb,
|
|
775
|
+
onDismiss
|
|
776
|
+
}) {
|
|
777
|
+
const [closeHovered, closeHover] = useHover();
|
|
778
|
+
return /* @__PURE__ */ jsxs2(
|
|
779
|
+
"div",
|
|
780
|
+
{
|
|
781
|
+
className: "flex items-center justify-between gap-3 shrink-0",
|
|
782
|
+
style: {
|
|
783
|
+
background: "linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)",
|
|
784
|
+
padding: "16px 20px"
|
|
785
|
+
},
|
|
786
|
+
children: [
|
|
787
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2.5 min-w-0", children: [
|
|
788
|
+
orb,
|
|
789
|
+
/* @__PURE__ */ jsx3("span", { className: "text-sm font-bold text-white truncate", children: step.label })
|
|
790
|
+
] }),
|
|
791
|
+
/* @__PURE__ */ jsx3(
|
|
792
|
+
"button",
|
|
793
|
+
{
|
|
794
|
+
onClick: onDismiss,
|
|
795
|
+
...closeHover,
|
|
796
|
+
className: "transition-colors shrink-0",
|
|
797
|
+
style: {
|
|
798
|
+
padding: 4,
|
|
799
|
+
borderRadius: 4,
|
|
800
|
+
background: closeHovered ? "rgba(255,255,255,0.1)" : "transparent"
|
|
801
|
+
},
|
|
802
|
+
title: "Skip tour",
|
|
803
|
+
children: /* @__PURE__ */ jsx3(X, { size: 16, className: "text-white/70" })
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
]
|
|
807
|
+
}
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
function TourShellStepDots({
|
|
811
|
+
total,
|
|
812
|
+
index,
|
|
813
|
+
skin
|
|
814
|
+
}) {
|
|
815
|
+
return /* @__PURE__ */ jsx3(
|
|
816
|
+
"div",
|
|
817
|
+
{
|
|
818
|
+
className: "flex items-center gap-1.5 shrink-0",
|
|
819
|
+
style: { borderBottom: skin.panelBorder, padding: "12px 20px" },
|
|
820
|
+
children: Array.from({ length: total }, (_, i) => /* @__PURE__ */ jsx3(
|
|
821
|
+
"span",
|
|
822
|
+
{
|
|
823
|
+
className: "h-1.5 rounded-full transition-all",
|
|
824
|
+
style: {
|
|
825
|
+
width: i === index ? 20 : 8,
|
|
826
|
+
background: i === index ? skin.accent : skin.mutedText,
|
|
827
|
+
opacity: i === index ? 1 : 0.4
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
i
|
|
831
|
+
))
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
function TourShellBody({
|
|
836
|
+
step,
|
|
837
|
+
narration,
|
|
838
|
+
narrationPending,
|
|
839
|
+
skin,
|
|
840
|
+
children,
|
|
841
|
+
gap
|
|
842
|
+
}) {
|
|
843
|
+
const features = step.features ?? [];
|
|
844
|
+
const highlights = step.highlights ?? [];
|
|
845
|
+
const hasList = features.length > 0 || highlights.length > 0;
|
|
846
|
+
const hasAction = Boolean(step.action);
|
|
847
|
+
const hasChildren = Boolean(children);
|
|
848
|
+
const gapIf = (hasNext) => hasNext ? gap : 0;
|
|
849
|
+
const itemGap = (i, count, px) => i < count - 1 ? px : 0;
|
|
850
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
851
|
+
/* @__PURE__ */ jsx3(
|
|
852
|
+
"p",
|
|
853
|
+
{
|
|
854
|
+
className: "text-[13px] leading-relaxed transition-opacity",
|
|
855
|
+
style: {
|
|
856
|
+
color: skin.bodyText,
|
|
857
|
+
opacity: narrationPending ? 0.7 : 1,
|
|
858
|
+
marginBottom: gapIf(hasList || hasAction || hasChildren)
|
|
859
|
+
},
|
|
860
|
+
children: narration
|
|
861
|
+
}
|
|
862
|
+
),
|
|
863
|
+
features.length > 0 ? /* @__PURE__ */ jsx3("ul", { style: { marginBottom: gapIf(hasAction || hasChildren) }, children: features.map((f, i) => /* @__PURE__ */ jsxs2(
|
|
864
|
+
"li",
|
|
865
|
+
{
|
|
866
|
+
className: "text-[12.5px] leading-relaxed",
|
|
867
|
+
style: { marginBottom: itemGap(i, features.length, 8) },
|
|
868
|
+
children: [
|
|
869
|
+
/* @__PURE__ */ jsx3("span", { className: "font-semibold", style: { color: skin.accent }, children: f.name }),
|
|
870
|
+
/* @__PURE__ */ jsxs2("span", { style: { color: skin.softText }, children: [
|
|
871
|
+
" \u2014 ",
|
|
872
|
+
f.detail
|
|
873
|
+
] })
|
|
874
|
+
]
|
|
875
|
+
},
|
|
876
|
+
i
|
|
877
|
+
)) }) : highlights.length > 0 && /* @__PURE__ */ jsx3("ul", { style: { marginBottom: gapIf(hasAction || hasChildren) }, children: highlights.map((h, i) => /* @__PURE__ */ jsxs2(
|
|
878
|
+
"li",
|
|
879
|
+
{
|
|
880
|
+
className: "text-[12.5px] leading-relaxed relative",
|
|
881
|
+
style: {
|
|
882
|
+
color: skin.softText,
|
|
883
|
+
paddingLeft: 12,
|
|
884
|
+
marginBottom: itemGap(i, highlights.length, 6)
|
|
885
|
+
},
|
|
886
|
+
children: [
|
|
887
|
+
/* @__PURE__ */ jsx3(
|
|
888
|
+
"span",
|
|
889
|
+
{
|
|
890
|
+
className: "absolute left-0",
|
|
891
|
+
style: { color: skin.mutedText },
|
|
892
|
+
children: "\u2022"
|
|
893
|
+
}
|
|
894
|
+
),
|
|
895
|
+
h
|
|
896
|
+
]
|
|
897
|
+
},
|
|
898
|
+
i
|
|
899
|
+
)) }),
|
|
900
|
+
step.action && /* @__PURE__ */ jsxs2(
|
|
901
|
+
"a",
|
|
902
|
+
{
|
|
903
|
+
href: step.action.href,
|
|
904
|
+
target: step.action.external ? "_blank" : void 0,
|
|
905
|
+
rel: step.action.external ? "noopener noreferrer" : void 0,
|
|
906
|
+
className: "inline-flex items-center gap-1.5 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90",
|
|
907
|
+
style: {
|
|
908
|
+
background: `${skin.accent}20`,
|
|
909
|
+
color: skin.accent,
|
|
910
|
+
padding: "8px 14px",
|
|
911
|
+
marginBottom: gapIf(hasChildren)
|
|
912
|
+
},
|
|
913
|
+
children: [
|
|
914
|
+
step.action.label,
|
|
915
|
+
step.action.external ? /* @__PURE__ */ jsx3(ExternalLink, { size: 13 }) : /* @__PURE__ */ jsx3(ArrowRight, { size: 13 })
|
|
916
|
+
]
|
|
917
|
+
}
|
|
918
|
+
),
|
|
919
|
+
children
|
|
920
|
+
] });
|
|
921
|
+
}
|
|
922
|
+
function TourShellFooter({
|
|
923
|
+
isFirst,
|
|
924
|
+
isLast,
|
|
925
|
+
onNext,
|
|
926
|
+
onBack,
|
|
927
|
+
onDismiss,
|
|
928
|
+
skin
|
|
929
|
+
}) {
|
|
930
|
+
const [nextHovered, nextHover] = useHover();
|
|
931
|
+
return /* @__PURE__ */ jsxs2(
|
|
932
|
+
"div",
|
|
933
|
+
{
|
|
934
|
+
className: "flex items-center justify-between gap-2 shrink-0",
|
|
935
|
+
style: { borderTop: skin.panelBorder, padding: "14px 20px" },
|
|
936
|
+
children: [
|
|
937
|
+
/* @__PURE__ */ jsx3(
|
|
938
|
+
"button",
|
|
939
|
+
{
|
|
940
|
+
onClick: onDismiss,
|
|
941
|
+
className: "transition-colors hover:underline",
|
|
942
|
+
style: { color: skin.mutedText, fontSize: 12, fontWeight: 500 },
|
|
943
|
+
children: "Skip tour"
|
|
944
|
+
}
|
|
945
|
+
),
|
|
946
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
947
|
+
!isFirst && /* @__PURE__ */ jsxs2(
|
|
948
|
+
"button",
|
|
949
|
+
{
|
|
950
|
+
onClick: onBack,
|
|
951
|
+
className: "flex items-center gap-1 transition-colors",
|
|
952
|
+
style: {
|
|
953
|
+
color: skin.bodyText,
|
|
954
|
+
border: skin.panelBorder,
|
|
955
|
+
padding: "6px 12px",
|
|
956
|
+
borderRadius: 6,
|
|
957
|
+
fontSize: 12.5,
|
|
958
|
+
fontWeight: 500
|
|
959
|
+
},
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsx3(ChevronLeft, { size: 13 }),
|
|
962
|
+
"Back"
|
|
963
|
+
]
|
|
964
|
+
}
|
|
965
|
+
),
|
|
966
|
+
/* @__PURE__ */ jsxs2(
|
|
967
|
+
"button",
|
|
968
|
+
{
|
|
969
|
+
onClick: onNext,
|
|
970
|
+
...nextHover,
|
|
971
|
+
className: "flex items-center gap-1 transition-opacity",
|
|
972
|
+
style: {
|
|
973
|
+
background: "linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)",
|
|
974
|
+
color: "#fff",
|
|
975
|
+
padding: "6px 14px",
|
|
976
|
+
borderRadius: 6,
|
|
977
|
+
fontSize: 12.5,
|
|
978
|
+
fontWeight: 700,
|
|
979
|
+
opacity: nextHovered ? 0.9 : 1
|
|
980
|
+
},
|
|
981
|
+
children: [
|
|
982
|
+
isLast ? "Done" : "Next",
|
|
983
|
+
!isLast && /* @__PURE__ */ jsx3(ChevronRight, { size: 13 })
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
)
|
|
987
|
+
] })
|
|
988
|
+
]
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/tour/useTour.ts
|
|
994
|
+
import { useCallback, useState as useState3 } from "react";
|
|
995
|
+
|
|
996
|
+
// src/tour/tourMachine.ts
|
|
997
|
+
function resolveInitialIndex(steps, initialStepId) {
|
|
998
|
+
if (!initialStepId) return 0;
|
|
999
|
+
const found = steps.findIndex((s) => s.id === initialStepId);
|
|
1000
|
+
return found === -1 ? 0 : found;
|
|
1001
|
+
}
|
|
1002
|
+
function nextIndex(steps, current) {
|
|
1003
|
+
if (current >= steps.length - 1) {
|
|
1004
|
+
return { index: current, completed: true };
|
|
1005
|
+
}
|
|
1006
|
+
return { index: current + 1, completed: false };
|
|
1007
|
+
}
|
|
1008
|
+
function backIndex(current) {
|
|
1009
|
+
return Math.max(0, current - 1);
|
|
1010
|
+
}
|
|
1011
|
+
function gotoIndex(steps, stepId, current) {
|
|
1012
|
+
const target = steps.findIndex((s) => s.id === stepId);
|
|
1013
|
+
return target === -1 ? current : target;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/tour/useTour.ts
|
|
1017
|
+
function useTour(steps, opts = {}) {
|
|
1018
|
+
if (steps.length === 0) {
|
|
1019
|
+
throw new Error("useTour requires at least one step");
|
|
1020
|
+
}
|
|
1021
|
+
const [index, setIndex] = useState3(
|
|
1022
|
+
() => resolveInitialIndex(steps, opts.initialStepId)
|
|
1023
|
+
);
|
|
1024
|
+
const next = useCallback(() => {
|
|
1025
|
+
const result = nextIndex(steps, index);
|
|
1026
|
+
if (result.completed) {
|
|
1027
|
+
opts.onComplete?.();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
setIndex(result.index);
|
|
1031
|
+
opts.onStepChange?.(steps[result.index], result.index);
|
|
1032
|
+
}, [index, steps]);
|
|
1033
|
+
const back = useCallback(() => {
|
|
1034
|
+
const target = backIndex(index);
|
|
1035
|
+
if (target === index) return;
|
|
1036
|
+
setIndex(target);
|
|
1037
|
+
opts.onStepChange?.(steps[target], target);
|
|
1038
|
+
}, [index, steps]);
|
|
1039
|
+
const goto = useCallback(
|
|
1040
|
+
(stepId) => {
|
|
1041
|
+
const target = gotoIndex(steps, stepId, index);
|
|
1042
|
+
if (target === index) return;
|
|
1043
|
+
setIndex(target);
|
|
1044
|
+
opts.onStepChange?.(steps[target], target);
|
|
1045
|
+
},
|
|
1046
|
+
[steps, index]
|
|
1047
|
+
);
|
|
1048
|
+
const dismiss = useCallback(() => {
|
|
1049
|
+
opts.onDismiss?.();
|
|
1050
|
+
}, []);
|
|
1051
|
+
return {
|
|
1052
|
+
step: steps[index],
|
|
1053
|
+
index,
|
|
1054
|
+
total: steps.length,
|
|
1055
|
+
isFirst: index === 0,
|
|
1056
|
+
isLast: index === steps.length - 1,
|
|
1057
|
+
next,
|
|
1058
|
+
back,
|
|
1059
|
+
goto,
|
|
1060
|
+
dismiss
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/tour/OpiTourHost.tsx
|
|
1065
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
|
|
1066
|
+
import { Sparkles as Sparkles2 } from "lucide-react";
|
|
1067
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1068
|
+
function OpiTourHost(props) {
|
|
1069
|
+
const { active } = useTourState();
|
|
1070
|
+
if (!active) return null;
|
|
1071
|
+
return /* @__PURE__ */ jsx4(ActiveTourHost, { tour: active, ...props }, active.id);
|
|
1072
|
+
}
|
|
1073
|
+
function ActiveTourHost({
|
|
1074
|
+
tour,
|
|
1075
|
+
orb,
|
|
1076
|
+
skin
|
|
1077
|
+
}) {
|
|
1078
|
+
const { session } = useTourState();
|
|
1079
|
+
const { open } = useTourLauncher();
|
|
1080
|
+
const { phase, progress, record } = useTourProgress(tour.id);
|
|
1081
|
+
const decided = useRef3(false);
|
|
1082
|
+
useEffect4(() => {
|
|
1083
|
+
if (phase === "loading" || decided.current) return;
|
|
1084
|
+
decided.current = true;
|
|
1085
|
+
if (decideAutoOpen(tour.autoStart, { phase, progress })) {
|
|
1086
|
+
open({ restart: false });
|
|
1087
|
+
}
|
|
1088
|
+
}, [phase, progress, tour.autoStart, open]);
|
|
1089
|
+
if (!session || phase === "loading") return null;
|
|
1090
|
+
return /* @__PURE__ */ jsx4(
|
|
1091
|
+
TourRunner,
|
|
1092
|
+
{
|
|
1093
|
+
steps: tour.steps,
|
|
1094
|
+
initialStepId: session.restart ? null : progress.lastStepId,
|
|
1095
|
+
onProgress: record,
|
|
1096
|
+
orb: orb ?? /* @__PURE__ */ jsx4(DefaultOrb, {}),
|
|
1097
|
+
skin
|
|
1098
|
+
}
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
function DefaultOrb() {
|
|
1102
|
+
return /* @__PURE__ */ jsx4(
|
|
1103
|
+
"span",
|
|
1104
|
+
{
|
|
1105
|
+
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
|
1106
|
+
style: {
|
|
1107
|
+
background: "linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)"
|
|
1108
|
+
},
|
|
1109
|
+
children: /* @__PURE__ */ jsx4(Sparkles2, { className: "h-4 w-4 text-white" })
|
|
1110
|
+
}
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
function TourRunner({
|
|
1114
|
+
steps,
|
|
1115
|
+
initialStepId,
|
|
1116
|
+
onProgress,
|
|
1117
|
+
orb,
|
|
1118
|
+
skin
|
|
1119
|
+
}) {
|
|
1120
|
+
const { close } = useTourLauncher();
|
|
1121
|
+
const narrate = useTourNarrator();
|
|
1122
|
+
const [narrationByStep, setNarrationByStep] = useState4({});
|
|
1123
|
+
const [pendingStepId, setPendingStepId] = useState4(null);
|
|
1124
|
+
const requested = useRef3(/* @__PURE__ */ new Set());
|
|
1125
|
+
const currentStepId = useRef3(
|
|
1126
|
+
steps.find((s) => s.id === initialStepId)?.id ?? steps[0].id
|
|
1127
|
+
);
|
|
1128
|
+
const requestNarration = (step) => {
|
|
1129
|
+
if (requested.current.has(step.id)) return;
|
|
1130
|
+
requested.current.add(step.id);
|
|
1131
|
+
setPendingStepId(step.id);
|
|
1132
|
+
narrate(step).then((text) => setNarrationByStep((prev) => ({ ...prev, [step.id]: text }))).catch(() => {
|
|
1133
|
+
}).finally(() => setPendingStepId((cur) => cur === step.id ? null : cur));
|
|
1134
|
+
};
|
|
1135
|
+
const tour = useTour(steps, {
|
|
1136
|
+
initialStepId,
|
|
1137
|
+
onStepChange: (step) => {
|
|
1138
|
+
currentStepId.current = step.id;
|
|
1139
|
+
void onProgress({ status: "in-progress", lastStepId: step.id });
|
|
1140
|
+
requestNarration(step);
|
|
1141
|
+
},
|
|
1142
|
+
onComplete: () => {
|
|
1143
|
+
void onProgress({ status: "done", lastStepId: null });
|
|
1144
|
+
close();
|
|
1145
|
+
},
|
|
1146
|
+
onDismiss: () => {
|
|
1147
|
+
void onProgress({
|
|
1148
|
+
status: "dismissed",
|
|
1149
|
+
lastStepId: currentStepId.current
|
|
1150
|
+
});
|
|
1151
|
+
close();
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
useEffect4(() => {
|
|
1155
|
+
requestNarration(tour.step);
|
|
1156
|
+
}, []);
|
|
1157
|
+
return /* @__PURE__ */ jsx4(
|
|
1158
|
+
TourShell,
|
|
1159
|
+
{
|
|
1160
|
+
step: tour.step,
|
|
1161
|
+
index: tour.index,
|
|
1162
|
+
total: tour.total,
|
|
1163
|
+
isFirst: tour.isFirst,
|
|
1164
|
+
isLast: tour.isLast,
|
|
1165
|
+
onNext: tour.next,
|
|
1166
|
+
onBack: tour.back,
|
|
1167
|
+
onDismiss: tour.dismiss,
|
|
1168
|
+
narration: narrationByStep[tour.step.id] ?? tour.step.fallbackText,
|
|
1169
|
+
narrationPending: pendingStepId === tour.step.id,
|
|
1170
|
+
orb,
|
|
1171
|
+
skin
|
|
1172
|
+
}
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export {
|
|
1177
|
+
InvalidTourDefinitionError,
|
|
1178
|
+
validateTourDefinition,
|
|
1179
|
+
createTourStore,
|
|
1180
|
+
defaultTourStore,
|
|
1181
|
+
SERVICE_HUB_TOUR_ID,
|
|
1182
|
+
TourProgressError,
|
|
1183
|
+
createPortalTourProgressTransport,
|
|
1184
|
+
createTourProgressController,
|
|
1185
|
+
shouldAutoOpen,
|
|
1186
|
+
decideAutoOpen,
|
|
1187
|
+
ONBOARDING_NARRATION_SERVICE,
|
|
1188
|
+
NARRATION_PROMPT,
|
|
1189
|
+
buildNarrationInput,
|
|
1190
|
+
createPortalNarrator,
|
|
1191
|
+
TourProvider,
|
|
1192
|
+
useRegisterTour,
|
|
1193
|
+
useTourState,
|
|
1194
|
+
useActiveTour,
|
|
1195
|
+
useTourLauncher,
|
|
1196
|
+
useTourProgress,
|
|
1197
|
+
useTourNarrator,
|
|
1198
|
+
TakeTheTourButton,
|
|
1199
|
+
useSpotlight,
|
|
1200
|
+
computeSpotlightFrame,
|
|
1201
|
+
computePanelPosition,
|
|
1202
|
+
TourShell,
|
|
1203
|
+
useTour,
|
|
1204
|
+
OpiTourHost
|
|
1205
|
+
};
|
|
1206
|
+
//# sourceMappingURL=chunk-LUQAR6NB.js.map
|