docugate 0.1.3 → 0.2.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.
@@ -0,0 +1,1546 @@
1
+ // @docugate/pill v0.1.0 — auto-generated, do not edit
2
+ (function(){
3
+ 'use strict';
4
+ // @docugate/pill — in-app tour pill
5
+ // No framework, no dependencies. Shadow DOM isolates styles.
6
+ // ---------------------------------------------------------------------------
7
+ // Route matching (mirrors matchRoute in tour.ts)
8
+ // ---------------------------------------------------------------------------
9
+ function matchRoute(route, pathname) {
10
+ const routeParts = route.split('/').filter(Boolean);
11
+ const pathParts = pathname.split('/').filter(Boolean);
12
+ if (routeParts.length !== pathParts.length)
13
+ return false;
14
+ for (let i = 0; i < routeParts.length; i++) {
15
+ if (routeParts[i].startsWith(':'))
16
+ continue;
17
+ if (routeParts[i] !== pathParts[i])
18
+ return false;
19
+ }
20
+ return true;
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Selector suggestion
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Suggest a [data-tour="..."] selector for an element.
27
+ * Priority: existing data-tour → id → role+text → tag+position.
28
+ */
29
+ function suggestSelector(el) {
30
+ const existing = el.getAttribute('data-tour');
31
+ if (existing) {
32
+ return { selector: `[data-tour="${existing}"]`, needsAttribute: false };
33
+ }
34
+ // Derive a slug from id, aria-label, or text content
35
+ const id = el.id;
36
+ if (id) {
37
+ const slug = id.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
38
+ return { selector: `[data-tour="${slug}"]`, needsAttribute: true };
39
+ }
40
+ const label = el.getAttribute('aria-label') || el.getAttribute('title');
41
+ if (label) {
42
+ const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
43
+ return { selector: `[data-tour="${slug}"]`, needsAttribute: true };
44
+ }
45
+ const text = (el.textContent ?? '').trim().slice(0, 30);
46
+ if (text) {
47
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
48
+ return { selector: `[data-tour="${slug}"]`, needsAttribute: true };
49
+ }
50
+ const tag = el.tagName.toLowerCase();
51
+ return { selector: `[data-tour="${tag}"]`, needsAttribute: true };
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Server URL: read from this script's own src attribute
55
+ // ---------------------------------------------------------------------------
56
+ function getServerBase() {
57
+ const scripts = document.querySelectorAll('script[src]');
58
+ for (const s of Array.from(scripts)) {
59
+ const src = s.src;
60
+ if (src && src.endsWith('/pill.js')) {
61
+ const url = new URL(src);
62
+ return `${url.protocol}//${url.host}`;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Fetch current tour from server or window.DOCUGATE_TOUR
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * The current page's tour, and whether the tour server answered at all. The
72
+ * two are different: a page with no tour yet still gets the pill, so its first
73
+ * stop can be added from the inspector. Only a silent server hides it.
74
+ */
75
+ async function fetchTour(base) {
76
+ const pathname = location.pathname;
77
+ if (window.DOCUGATE_TOUR) {
78
+ const match = window.DOCUGATE_TOUR.find((t) => matchRoute(t.route, pathname));
79
+ return { alive: true, tour: match ?? null };
80
+ }
81
+ try {
82
+ const res = await fetch(`${base}/tour?path=${encodeURIComponent(pathname)}`, { signal: AbortSignal.timeout(3000) });
83
+ if (res.status === 404)
84
+ return { alive: true, tour: null };
85
+ if (!res.ok)
86
+ return { alive: false, tour: null };
87
+ return { alive: true, tour: (await res.json()) };
88
+ }
89
+ catch {
90
+ return { alive: false, tour: null };
91
+ }
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // CSS (injected into shadow DOM)
95
+ // ---------------------------------------------------------------------------
96
+ const STYLES = `
97
+ :host { all: initial; }
98
+ *, *::before, *::after { box-sizing: border-box; }
99
+
100
+ /* One material for everything the pill draws: near-black glass, a hairline
101
+ border and a soft shadow, the way the Next.js dev indicator looks. */
102
+ .surface {
103
+ background: rgba(12, 12, 14, 0.92);
104
+ -webkit-backdrop-filter: blur(12px) saturate(140%);
105
+ backdrop-filter: blur(12px) saturate(140%);
106
+ border: 1px solid rgba(255, 255, 255, 0.1);
107
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 12px 32px -8px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.06);
108
+ color: #ededed;
109
+ font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
110
+ -webkit-font-smoothing: antialiased;
111
+ }
112
+
113
+ /* ── the badge ── */
114
+ .pill-btn {
115
+ position: fixed;
116
+ z-index: 2147483647;
117
+ display: flex;
118
+ align-items: center;
119
+ height: 36px;
120
+ padding: 0 9px;
121
+ border-radius: 999px;
122
+ cursor: pointer;
123
+ user-select: none;
124
+ touch-action: none;
125
+ transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.15s;
126
+ }
127
+ .pill-btn.away { opacity: 0; pointer-events: none; transform: scale(0.8); }
128
+ .pill-btn.dragging { cursor: grabbing; transition: none; }
129
+ .pill-btn:hover { background: rgba(24, 24, 28, 0.96); }
130
+ .pill-btn:active:not(.dragging) { transform: scale(0.96); }
131
+ .pill-btn:focus-visible { outline: 2px solid #F4C43F; outline-offset: 3px; }
132
+ .pill-glyph { width: 18px; height: 14px; flex-shrink: 0; display: block; }
133
+ .pill-label {
134
+ max-width: 0;
135
+ overflow: hidden;
136
+ white-space: nowrap;
137
+ opacity: 0;
138
+ margin-left: 0;
139
+ transition: max-width 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s, margin 0.28s;
140
+ }
141
+ .pill-btn:hover .pill-label, .pill-btn:focus-visible .pill-label, .pill-btn.open .pill-label {
142
+ max-width: 220px;
143
+ opacity: 1;
144
+ margin-left: 8px;
145
+ }
146
+ .pill-count {
147
+ position: absolute;
148
+ top: -4px;
149
+ right: -4px;
150
+ min-width: 17px;
151
+ height: 17px;
152
+ padding: 0 5px;
153
+ border-radius: 999px;
154
+ background: #F4C43F;
155
+ color: #10122F;
156
+ font: 700 10.5px/17px -apple-system, "Segoe UI", system-ui, sans-serif;
157
+ text-align: center;
158
+ box-shadow: 0 0 0 2px rgba(12, 12, 14, 0.92);
159
+ }
160
+ .pill-btn.inspecting { box-shadow: 0 0 0 2px #F4C43F, 0 12px 32px -8px rgba(0, 0, 0, 0.55); }
161
+
162
+ /* ── the panel ── */
163
+ .menu {
164
+ position: fixed;
165
+ z-index: 2147483647;
166
+ width: 288px;
167
+ border-radius: 12px;
168
+ overflow: hidden;
169
+ transform-origin: var(--origin, bottom right);
170
+ animation: pop 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
171
+ }
172
+ @keyframes pop { from { opacity: 0; transform: scale(0.96) translateY(var(--rise, 4px)); } }
173
+ .menu-head {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: space-between;
177
+ gap: 12px;
178
+ padding: 12px 14px 10px;
179
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
180
+ }
181
+ .menu-route { font: 12px ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace; color: #a1a1a1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
182
+ .menu-tag { flex-shrink: 0; font-size: 11px; color: #10122F; background: #F4C43F; border-radius: 999px; padding: 1px 8px; font-weight: 600; }
183
+ .menu-tag.empty { color: #a1a1a1; background: rgba(255, 255, 255, 0.08); }
184
+ .menu-list { padding: 6px; }
185
+ .menu-item {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: space-between;
189
+ gap: 12px;
190
+ width: 100%;
191
+ padding: 8px 10px;
192
+ border: 0;
193
+ border-radius: 7px;
194
+ background: none;
195
+ color: #ededed;
196
+ font: inherit;
197
+ text-align: left;
198
+ cursor: pointer;
199
+ }
200
+ .menu-item:hover, .menu-item:focus-visible { background: rgba(255, 255, 255, 0.07); outline: none; }
201
+ .menu-item:disabled { color: #6e6e6e; cursor: default; background: none; }
202
+ .menu-hint { color: #8f8f8f; font-size: 12px; }
203
+ kbd {
204
+ font: 11px ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
205
+ color: #a1a1a1;
206
+ border: 1px solid rgba(255, 255, 255, 0.14);
207
+ border-bottom-width: 2px;
208
+ border-radius: 5px;
209
+ padding: 0 5px;
210
+ }
211
+ .switch { position: relative; width: 28px; height: 16px; border-radius: 999px; background: rgba(255, 255, 255, 0.16); transition: background 0.15s; flex-shrink: 0; }
212
+ .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1); }
213
+ .menu-item[aria-checked="true"] .switch { background: #F4C43F; }
214
+ .menu-item { padding: 9px 10px; }
215
+ .menu-item[aria-checked="true"] .switch::after { transform: translateX(12px); background: #10122F; }
216
+ .menu-foot {
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: space-between;
220
+ gap: 8px;
221
+ padding: 9px 14px;
222
+ border-top: 1px solid rgba(255, 255, 255, 0.07);
223
+ font-size: 12px;
224
+ color: #8f8f8f;
225
+ }
226
+ .menu-foot .live { display: inline-flex; align-items: center; gap: 6px; }
227
+ .menu-foot .live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #45d483; box-shadow: 0 0 0 3px rgba(69, 212, 131, 0.15); }
228
+ .link-btn { border: 0; background: none; color: #a1a1a1; font: inherit; cursor: pointer; padding: 2px 4px; border-radius: 5px; }
229
+ .link-btn:hover, .link-btn:focus-visible { color: #ededed; background: rgba(255, 255, 255, 0.07); outline: none; }
230
+
231
+ /* ── walkthrough card and inspector popup ── */
232
+ .wt-card, .insp-popup {
233
+ position: fixed;
234
+ z-index: 2147483645;
235
+ border-radius: 12px;
236
+ width: 340px;
237
+ max-width: calc(100vw - 24px);
238
+ padding: 16px 16px 12px;
239
+ animation: pop 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
240
+ pointer-events: auto;
241
+ }
242
+ .wt-eyebrow { font-size: 11px; color: #8f8f8f; margin: 0 0 4px; letter-spacing: 0.02em; }
243
+ .wt-card-heading, .insp-popup-heading { font-size: 15px; font-weight: 600; color: #fff; margin: 0 0 6px; letter-spacing: -0.01em; }
244
+ .wt-prose, .insp-popup-prose { font-size: 13px; color: #c7c7c7; margin: 0 0 12px; line-height: 1.55; }
245
+ .wt-prose code, .insp-popup-prose code { font: 12px ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace; color: #f5f0e3; background: rgba(255, 255, 255, 0.08); border-radius: 4px; padding: 1px 5px; }
246
+ .wt-fields {
247
+ display: grid;
248
+ grid-template-columns: 58px 1fr;
249
+ gap: 6px 10px;
250
+ padding: 10px 12px;
251
+ margin: 0 0 12px;
252
+ border-radius: 8px;
253
+ background: rgba(255, 255, 255, 0.04);
254
+ border: 1px solid rgba(255, 255, 255, 0.06);
255
+ }
256
+ .wt-field-label, .insp-popup-label { font-size: 11px; color: #8f8f8f; padding-top: 1px; }
257
+ .wt-field-value, .insp-popup-val { font: 12px/1.45 ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace; color: #e4e4e4; overflow-wrap: break-word; }
258
+ .wt-field-value a, .insp-popup-val a { color: #F4C43F; text-decoration: none; }
259
+ .wt-field-value a:hover, .insp-popup-val a:hover { text-decoration: underline; }
260
+ .wt-nav, .insp-popup-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
261
+ .wt-dots { display: flex; gap: 4px; margin-right: auto; }
262
+ .wt-dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(255, 255, 255, 0.18); transition: background 0.2s, width 0.2s; }
263
+ .wt-dots i.on { width: 16px; border-radius: 999px; background: #F4C43F; }
264
+ .wt-btn {
265
+ height: 28px;
266
+ padding: 0 12px;
267
+ border-radius: 7px;
268
+ border: 1px solid rgba(255, 255, 255, 0.12);
269
+ background: rgba(255, 255, 255, 0.04);
270
+ color: #ededed;
271
+ font: 500 12.5px -apple-system, "Segoe UI", system-ui, sans-serif;
272
+ cursor: pointer;
273
+ transition: background 0.12s, border-color 0.12s;
274
+ }
275
+ .wt-btn:hover { background: rgba(255, 255, 255, 0.09); }
276
+ .wt-btn:focus-visible { outline: 2px solid #F4C43F; outline-offset: 2px; }
277
+ .wt-btn.primary { background: #F4C43F; border-color: #F4C43F; color: #10122F; font-weight: 600; }
278
+ .wt-btn.primary:hover { background: #ffd35c; border-color: #ffd35c; }
279
+
280
+ /* ── add / edit form ── */
281
+ .add-form {
282
+ position: fixed;
283
+ z-index: 2147483646;
284
+ top: 50%;
285
+ left: 50%;
286
+ transform: translate(-50%, -50%);
287
+ width: 380px;
288
+ max-width: calc(100vw - 24px);
289
+ max-height: calc(100vh - 48px);
290
+ overflow: auto;
291
+ border-radius: 14px;
292
+ padding: 18px 18px 14px;
293
+ }
294
+ .add-form-title { font-size: 15px; font-weight: 600; color: #fff; margin: 0 0 12px; }
295
+ .add-form label { display: block; font-size: 11.5px; color: #a1a1a1; margin: 10px 0 4px; }
296
+ .add-form label:first-of-type { margin-top: 0; }
297
+ .add-form input, .add-form textarea {
298
+ width: 100%;
299
+ background: rgba(0, 0, 0, 0.35);
300
+ border: 1px solid rgba(255, 255, 255, 0.12);
301
+ border-radius: 7px;
302
+ color: #ededed;
303
+ font: 12.5px/1.4 ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
304
+ padding: 7px 9px;
305
+ resize: vertical;
306
+ }
307
+ .add-form textarea { min-height: 64px; font-family: -apple-system, "Segoe UI", system-ui, sans-serif; }
308
+ .add-form input:focus, .add-form textarea:focus { outline: none; border-color: #F4C43F; box-shadow: 0 0 0 3px rgba(244, 196, 63, 0.18); }
309
+ .add-form-warn {
310
+ margin-top: 12px;
311
+ font-size: 12px;
312
+ line-height: 1.5;
313
+ color: #f1dc9a;
314
+ background: rgba(244, 196, 63, 0.08);
315
+ border: 1px solid rgba(244, 196, 63, 0.25);
316
+ border-radius: 8px;
317
+ padding: 8px 10px;
318
+ }
319
+ .add-form-warn code { font-family: ui-monospace, "Cascadia Mono", Menlo, monospace; color: #F4C43F; }
320
+ .insp-popup { width: 360px; }
321
+ .panel-section { margin: 4px 0 6px; font-size: 11px; color: #8f8f8f; letter-spacing: 0.02em; }
322
+ .panel-hint { margin: 0 0 12px; font-size: 12.5px; color: #a1a1a1; }
323
+ .panel-file { margin-right: auto; font: 11px ui-monospace, "Cascadia Mono", Menlo, monospace; color: #6e6e6e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
324
+ .panel-error { margin: 10px 0 0; font-size: 12px; color: #ff9b8a; }
325
+ .file-link { all: unset; cursor: pointer; font: inherit; color: #ededed; border-bottom: 1px dashed rgba(244, 196, 63, 0.55); overflow-wrap: anywhere; }
326
+ .file-link:hover, .file-link:focus-visible { color: #F4C43F; border-bottom-style: solid; }
327
+ .panel-form label { display: block; font-size: 11.5px; color: #a1a1a1; margin: 10px 0 4px; }
328
+ .panel-form label:first-of-type { margin-top: 0; }
329
+ .panel-form input, .panel-form textarea {
330
+ width: 100%;
331
+ background: rgba(0, 0, 0, 0.35);
332
+ border: 1px solid rgba(255, 255, 255, 0.12);
333
+ border-radius: 7px;
334
+ color: #ededed;
335
+ font: 12.5px/1.4 ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
336
+ padding: 7px 9px;
337
+ resize: vertical;
338
+ }
339
+ .panel-form textarea { min-height: 72px; font-family: -apple-system, "Segoe UI", system-ui, sans-serif; font-size: 13px; }
340
+ .panel-form input::placeholder, .panel-form textarea::placeholder { color: #555; font-style: italic; }
341
+ .panel-form input:focus, .panel-form textarea:focus { outline: none; border-color: #F4C43F; box-shadow: 0 0 0 3px rgba(244, 196, 63, 0.18); }
342
+ .panel-form .insp-popup-actions { margin-top: 14px; }
343
+ .wt-btn.ghost { border-color: transparent; background: none; color: #a1a1a1; }
344
+ .wt-btn.ghost:hover { color: #ededed; background: rgba(255, 255, 255, 0.07); }
345
+ .wt-btn.danger { border-color: transparent; background: none; color: #ff9b8a; }
346
+ .wt-btn.danger:hover { background: rgba(255, 110, 90, 0.12); }
347
+ .toast {
348
+ position: fixed;
349
+ z-index: 2147483647;
350
+ left: 50%;
351
+ bottom: 24px;
352
+ transform: translateX(-50%);
353
+ max-width: min(520px, calc(100vw - 32px));
354
+ padding: 9px 14px;
355
+ border-radius: 10px;
356
+ font-size: 12.5px;
357
+ animation: pop 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
358
+ }
359
+ .wt-section { padding: 6px; }
360
+ .wt-head { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 8px 8px 6px; border-radius: 8px; cursor: pointer; }
361
+ .wt-head:hover, .wt-head:focus-visible { background: rgba(255, 255, 255, 0.05); }
362
+ .wt-head .chev { width: 16px; height: 16px; color: #a1a1a1; transition: transform 0.2s; flex-shrink: 0; }
363
+ .wt-head[aria-expanded="false"] .chev { transform: rotate(180deg); }
364
+ .wt-title { font-weight: 600; color: #fff; margin-right: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
365
+ .ticks { display: flex; gap: 2px; }
366
+ .ticks i { width: 2px; height: 11px; border-radius: 1px; background: rgba(255, 255, 255, 0.16); }
367
+ .ticks i.on { background: #F4C43F; }
368
+ .wt-count { font-size: 12px; color: #a1a1a1; font-variant-numeric: tabular-nums; white-space: nowrap; }
369
+ .steps { margin: 4px 0 2px; padding: 6px; border-radius: 10px; background: rgba(255, 255, 255, 0.035); border: 1px solid rgba(255, 255, 255, 0.06); }
370
+ .step-row { display: flex; align-items: center; border-radius: 7px; }
371
+ .step-row:hover { background: rgba(255, 255, 255, 0.06); }
372
+ .step, .add-step { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; padding: 7px 8px; border-radius: 7px; cursor: pointer; color: #ededed; }
373
+ .step:focus-visible, .add-step:focus-visible, .step-edit:focus-visible { outline: 2px solid #F4C43F; outline-offset: -2px; }
374
+ .step:disabled { cursor: default; opacity: 0.45; }
375
+ .step .label, .add-step .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
376
+ .num { display: grid; place-items: center; width: 20px; height: 20px; flex-shrink: 0; border-radius: 6px; font-size: 11px; font-weight: 600; color: #a1a1a1; border: 1px solid rgba(255, 255, 255, 0.14); }
377
+ .num svg { width: 13px; height: 13px; }
378
+ .step-row.done .num { background: #F4C43F; border-color: #F4C43F; color: #10122F; }
379
+ .step-row.done .label { color: #8f8f8f; }
380
+ .step-row.current .num { background: #ededed; border-color: #ededed; color: #0c0c0e; }
381
+ .step-edit { all: unset; box-sizing: border-box; display: grid; place-items: center; width: 26px; height: 26px; margin-right: 4px; border-radius: 6px; color: #8f8f8f; cursor: pointer; opacity: 0; }
382
+ .step-edit svg { width: 15px; height: 15px; }
383
+ .step-row:hover .step-edit, .step-edit:focus-visible { opacity: 1; }
384
+ .step-edit:hover { color: #F4C43F; background: rgba(255, 255, 255, 0.07); }
385
+ .step-edit:disabled { display: none; }
386
+ .add-step { color: #a1a1a1; }
387
+ .add-step .num { border-style: dashed; font-size: 14px; font-weight: 400; }
388
+ .add-step:hover { background: rgba(255, 255, 255, 0.06); color: #ededed; }
389
+ .wt-empty { margin: 4px 8px 6px; font-size: 12.5px; line-height: 1.5; color: #a1a1a1; }
390
+ .item-text { display: flex; flex-direction: column; gap: 1px; }
391
+ .menu-list { border-top: 1px solid rgba(255, 255, 255, 0.07); }
392
+ .menu { width: 320px; }
393
+ .add-form-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 14px; }
394
+
395
+ @media (prefers-reduced-motion: reduce) {
396
+ *, *::before, *::after { transition: none !important; animation: none !important; }
397
+ }
398
+ `;
399
+ // ---------------------------------------------------------------------------
400
+ // Highlight ring drawn on a canvas over the page
401
+ // ---------------------------------------------------------------------------
402
+ function createRingCanvas() {
403
+ const c = document.createElement('canvas');
404
+ c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483641;';
405
+ c.width = window.innerWidth;
406
+ c.height = window.innerHeight;
407
+ return c;
408
+ }
409
+ function drawRing(canvas, rect) {
410
+ const ctx = canvas.getContext('2d');
411
+ const pad = 6;
412
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
413
+ // dim the rest of the page
414
+ ctx.fillStyle = 'rgba(0,0,0,0.55)';
415
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
416
+ // cut-out for target
417
+ const x = rect.left - pad;
418
+ const y = rect.top - pad;
419
+ const w = rect.width + pad * 2;
420
+ const h = rect.height + pad * 2;
421
+ const r = 6;
422
+ ctx.save();
423
+ ctx.globalCompositeOperation = 'destination-out';
424
+ ctx.beginPath();
425
+ ctx.moveTo(x + r, y);
426
+ ctx.lineTo(x + w - r, y);
427
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
428
+ ctx.lineTo(x + w, y + h - r);
429
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
430
+ ctx.lineTo(x + r, y + h);
431
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
432
+ ctx.lineTo(x, y + r);
433
+ ctx.quadraticCurveTo(x, y, x + r, y);
434
+ ctx.closePath();
435
+ ctx.fill();
436
+ ctx.restore();
437
+ // ring
438
+ ctx.strokeStyle = '#F4C43F';
439
+ ctx.lineWidth = 2;
440
+ ctx.beginPath();
441
+ ctx.moveTo(x + r, y);
442
+ ctx.lineTo(x + w - r, y);
443
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
444
+ ctx.lineTo(x + w, y + h - r);
445
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
446
+ ctx.lineTo(x + r, y + h);
447
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
448
+ ctx.lineTo(x, y + r);
449
+ ctx.quadraticCurveTo(x, y, x + r, y);
450
+ ctx.closePath();
451
+ ctx.stroke();
452
+ }
453
+ function clearCanvas(canvas) {
454
+ const ctx = canvas.getContext('2d');
455
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
456
+ }
457
+ // ---------------------------------------------------------------------------
458
+ // Card positioning: place card near target without clipping viewport
459
+ // ---------------------------------------------------------------------------
460
+ function positionCard(card, rect) {
461
+ const cw = card.offsetWidth || 340;
462
+ const ch = card.offsetHeight || 220;
463
+ const vw = window.innerWidth;
464
+ const vh = window.innerHeight;
465
+ const pad = 12;
466
+ let top = rect.bottom + 16;
467
+ let left = rect.left;
468
+ if (top + ch > vh - pad)
469
+ top = rect.top - ch - 16;
470
+ if (top < pad)
471
+ top = pad;
472
+ if (left + cw > vw - pad)
473
+ left = vw - cw - pad;
474
+ if (left < pad)
475
+ left = pad;
476
+ card.style.top = `${top}px`;
477
+ card.style.left = `${left}px`;
478
+ }
479
+ // ---------------------------------------------------------------------------
480
+ // Inspector: highlight element on hover
481
+ // ---------------------------------------------------------------------------
482
+ let inspectorHighlight = null;
483
+ function createHighlightEl() {
484
+ const el = document.createElement('div');
485
+ el.style.cssText = [
486
+ 'position:fixed',
487
+ 'pointer-events:none',
488
+ 'z-index:2147483642',
489
+ 'border:2px solid #F4C43F',
490
+ 'border-radius:4px',
491
+ 'background:rgba(244,196,63,0.10)',
492
+ 'transition:top 0.06s,left 0.06s,width 0.06s,height 0.06s',
493
+ 'display:none',
494
+ ].join(';');
495
+ return el;
496
+ }
497
+ function moveHighlight(el, rect) {
498
+ el.style.display = 'block';
499
+ el.style.top = `${rect.top - 2}px`;
500
+ el.style.left = `${rect.left - 2}px`;
501
+ el.style.width = `${rect.width + 4}px`;
502
+ el.style.height = `${rect.height + 4}px`;
503
+ }
504
+ // ---------------------------------------------------------------------------
505
+ // Text helpers for cards
506
+ // ---------------------------------------------------------------------------
507
+ /** The data-tour value inside a selector like [data-tour="invoice-total"], if any. */
508
+ function tourValue(selector) {
509
+ return selector.match(/data-tour=["']([^"']+)["']/)?.[1];
510
+ }
511
+ /** Lets a long path wrap after its slashes rather than in the middle of a name. */
512
+ function breakable(text) {
513
+ return text.replace(/\//g, '/\u200b');
514
+ }
515
+ /** Writes prose into `el`, turning `backticked` spans into inline code. Never HTML. */
516
+ function renderProse(el, prose) {
517
+ prose.split(/(`[^`]+`)/).forEach((part) => {
518
+ if (part.startsWith('`') && part.endsWith('`') && part.length > 2) {
519
+ const code = document.createElement('code');
520
+ code.textContent = part.slice(1, -1);
521
+ el.appendChild(code);
522
+ }
523
+ else if (part) {
524
+ el.appendChild(document.createTextNode(part));
525
+ }
526
+ });
527
+ }
528
+ const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
529
+ const CORNER_KEY = 'docugate-pill-corner';
530
+ const HIDDEN_KEY = 'docugate-pill-hidden';
531
+ const EDGE = 16;
532
+ /** The corner nearest to a point, for snapping the badge where it is dropped. */
533
+ function nearestCorner(x, y, width, height) {
534
+ const vertical = y < height / 2 ? 'top' : 'bottom';
535
+ const horizontal = x < width / 2 ? 'left' : 'right';
536
+ return `${vertical}-${horizontal}`;
537
+ }
538
+ function savedCorner() {
539
+ try {
540
+ const value = localStorage.getItem(CORNER_KEY);
541
+ return value && CORNERS.includes(value) ? value : 'bottom-right';
542
+ }
543
+ catch {
544
+ return 'bottom-right';
545
+ }
546
+ }
547
+ /** Pins an element to a corner, `offset` px further in from the edge. */
548
+ function pin(el, corner, offset = 0) {
549
+ const [v, h] = corner.split('-');
550
+ el.style.top = el.style.bottom = el.style.left = el.style.right = '';
551
+ el.style[v] = `${EDGE + offset}px`;
552
+ el.style[h] = `${EDGE}px`;
553
+ }
554
+ /** DocuGate's owl glyph, in cream and gold. */
555
+ const GLYPH = `<svg class="pill-glyph" viewBox="0 0 48 36" aria-hidden="true"><defs><mask id="dg-m" maskUnits="userSpaceOnUse" x="0" y="0" width="48" height="36"><rect width="48" height="36" fill="#fff"/><polygon points="24,19 30.5,25 24,33.5 17.5,25" fill="#000" stroke="#000" stroke-width="3.2" stroke-linejoin="round"/></mask></defs><g transform="translate(-0.9 -2.4)"><g mask="url(#dg-m)"><circle cx="12.5" cy="14" r="9.5" fill="none" stroke="#F5F0E3" stroke-width="4.2"/><circle cx="35.5" cy="14" r="9.5" fill="none" stroke="#F5F0E3" stroke-width="4.2"/><circle cx="14" cy="12.5" r="4.4" fill="#F5F0E3"/><circle cx="37" cy="12.5" r="4.4" fill="#F5F0E3"/></g><polygon points="24,19 24,33.5 17.5,25" fill="#F4C43F"/><polygon points="24,19 30.5,25 24,33.5" fill="#D99A1E"/></g></svg>`;
556
+ // ---------------------------------------------------------------------------
557
+ // Main Pill class
558
+ // ---------------------------------------------------------------------------
559
+ class DocugatePill {
560
+ constructor(base) {
561
+ this.tour = null;
562
+ this.alive = false;
563
+ this.menuOpen = false;
564
+ this.walkthroughActive = false;
565
+ this.inspectorActive = false;
566
+ this.wtIndex = 0;
567
+ this.wtStops = [];
568
+ this.canvas = null;
569
+ this.pillBtn = null;
570
+ this.menuEl = null;
571
+ this.wtCard = null;
572
+ this.inspPopup = null;
573
+ this.addForm = null;
574
+ this.inspEl = null;
575
+ this.inspectedPath = location.pathname;
576
+ this.listCollapsed = false;
577
+ this.highlightEl = null;
578
+ this.evtSource = null;
579
+ this.isStaticMode = false;
580
+ this.corner = savedCorner();
581
+ this.base = base;
582
+ this.isStaticMode = !!window.DOCUGATE_TOUR;
583
+ this.host = document.createElement('div');
584
+ this.host.setAttribute('data-docugate-pill', '');
585
+ this.shadow = this.host.attachShadow({ mode: 'open' });
586
+ const style = document.createElement('style');
587
+ style.textContent = STYLES;
588
+ this.shadow.appendChild(style);
589
+ this.boundHandleKey = this.handleKey.bind(this);
590
+ this.boundHandleMouseMove = this.handleInspectorMouseMove.bind(this);
591
+ this.boundHandleClick = this.handleInspectorClick.bind(this);
592
+ document.body.appendChild(this.host);
593
+ this.init();
594
+ this.patchHistory();
595
+ if (!this.isStaticMode) {
596
+ this.listenEvents();
597
+ }
598
+ }
599
+ async init() {
600
+ await this.load();
601
+ // Server didn't respond: draw nothing.
602
+ if (!this.alive)
603
+ return;
604
+ this.render();
605
+ }
606
+ async load() {
607
+ const { alive, tour } = await fetchTour(this.base);
608
+ this.alive = alive;
609
+ this.tour = tour;
610
+ }
611
+ render() {
612
+ this.removePill();
613
+ if (!this.alive)
614
+ return;
615
+ try {
616
+ if (sessionStorage.getItem(HIDDEN_KEY))
617
+ return;
618
+ }
619
+ catch {
620
+ // storage blocked: show the pill
621
+ }
622
+ const stops = this.tour?.stops.length ?? 0;
623
+ const btn = document.createElement('button');
624
+ btn.className = `pill-btn surface${this.inspectorActive ? ' inspecting' : ''}`;
625
+ btn.setAttribute('aria-haspopup', 'menu');
626
+ btn.setAttribute('aria-expanded', 'false');
627
+ btn.setAttribute('aria-label', `DocuGate tour: ${stops ? `${stops} stops on this page` : 'no tour on this page yet'}`);
628
+ btn.innerHTML =
629
+ GLYPH +
630
+ `<span class="pill-label">${stops ? `${stops} stop${stops === 1 ? '' : 's'} on this page` : 'No tour on this page yet'}</span>` +
631
+ (stops ? `<span class="pill-count" aria-hidden="true">${stops}</span>` : '');
632
+ pin(btn, this.corner);
633
+ this.makeDraggable(btn);
634
+ this.shadow.appendChild(btn);
635
+ this.pillBtn = btn;
636
+ }
637
+ /**
638
+ * Click opens the panel; a drag of a few pixels moves the badge instead and
639
+ * drops it in the nearest corner, which is remembered for next time.
640
+ */
641
+ makeDraggable(btn) {
642
+ let start = null;
643
+ let dragged = false;
644
+ btn.addEventListener('pointerdown', (e) => {
645
+ if (e.button !== 0)
646
+ return;
647
+ start = { x: e.clientX, y: e.clientY };
648
+ dragged = false;
649
+ btn.setPointerCapture(e.pointerId);
650
+ });
651
+ btn.addEventListener('pointermove', (e) => {
652
+ if (!start)
653
+ return;
654
+ const dx = e.clientX - start.x;
655
+ const dy = e.clientY - start.y;
656
+ if (!dragged && Math.hypot(dx, dy) < 5)
657
+ return;
658
+ if (!dragged) {
659
+ dragged = true;
660
+ this.closeMenu();
661
+ btn.classList.add('dragging');
662
+ }
663
+ btn.style.transform = `translate(${dx}px, ${dy}px)`;
664
+ });
665
+ btn.addEventListener('pointerup', (e) => {
666
+ if (!start)
667
+ return;
668
+ start = null;
669
+ if (!dragged)
670
+ return;
671
+ btn.classList.remove('dragging');
672
+ btn.style.transform = '';
673
+ this.corner = nearestCorner(e.clientX, e.clientY, window.innerWidth, window.innerHeight);
674
+ try {
675
+ localStorage.setItem(CORNER_KEY, this.corner);
676
+ }
677
+ catch {
678
+ // not remembered, still moved
679
+ }
680
+ pin(btn, this.corner);
681
+ });
682
+ btn.addEventListener('click', (e) => {
683
+ e.stopPropagation();
684
+ if (dragged) {
685
+ dragged = false;
686
+ return;
687
+ }
688
+ this.toggleMenu();
689
+ });
690
+ }
691
+ removePill() {
692
+ this.pillBtn?.remove();
693
+ this.pillBtn = null;
694
+ this.closeMenu();
695
+ }
696
+ // ── Menu ──────────────────────────────────────────────────────────────────
697
+ toggleMenu() {
698
+ if (this.menuOpen) {
699
+ this.closeMenu();
700
+ }
701
+ else {
702
+ this.openMenu();
703
+ }
704
+ }
705
+ openMenu() {
706
+ this.closeMenu();
707
+ const menu = document.createElement('div');
708
+ menu.className = 'menu surface';
709
+ menu.setAttribute('role', 'dialog');
710
+ menu.setAttribute('aria-label', 'DocuGate tour');
711
+ menu.append(this.walkthroughSection(), this.inspectorRow(), this.menuFooter());
712
+ // Open away from the corner the badge sits in.
713
+ const [v, h] = this.corner.split('-');
714
+ pin(menu, this.corner, 44);
715
+ menu.style.setProperty('--origin', `${v} ${h}`);
716
+ menu.style.setProperty('--rise', v === 'bottom' ? '6px' : '-6px');
717
+ this.shadow.appendChild(menu);
718
+ this.menuEl = menu;
719
+ this.menuOpen = true;
720
+ this.pillBtn?.classList.add('open');
721
+ this.pillBtn?.setAttribute('aria-expanded', 'true');
722
+ const close = (e) => {
723
+ const inside = e.composedPath().some((n) => n === menu || n === this.pillBtn);
724
+ if (e instanceof KeyboardEvent ? e.key === 'Escape' : !inside) {
725
+ this.closeMenu();
726
+ document.removeEventListener('click', close, true);
727
+ document.removeEventListener('keydown', close, true);
728
+ }
729
+ };
730
+ document.addEventListener('click', close, true);
731
+ document.addEventListener('keydown', close, true);
732
+ }
733
+ /**
734
+ * The walkthrough as a checklist: progress at the top, then every step with
735
+ * its state. Click a step to go there, the pencil to correct it, and the last
736
+ * row to add one. A page with no walkthrough offers its first step.
737
+ */
738
+ walkthroughSection() {
739
+ const stops = this.tour?.stops ?? [];
740
+ const seen = this.visited();
741
+ const done = stops.filter((s) => seen.has(s.target)).length;
742
+ const current = stops.find((s) => !seen.has(s.target));
743
+ const section = document.createElement('div');
744
+ section.className = 'wt-section';
745
+ const head = document.createElement('button');
746
+ head.className = 'wt-head';
747
+ head.setAttribute('aria-expanded', String(!this.listCollapsed));
748
+ const ticks = 20;
749
+ const lit = stops.length ? Math.round((done / stops.length) * ticks) : 0;
750
+ head.innerHTML =
751
+ `<svg class="chev" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 10l4-4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>` +
752
+ `<span class="wt-title"></span>` +
753
+ (stops.length
754
+ ? `<span class="ticks" aria-hidden="true">${Array.from({ length: ticks }, (_, i) => `<i class="${i < lit ? 'on' : ''}"></i>`).join('')}</span><span class="wt-count">${done}/${stops.length}</span>`
755
+ : `<span class="wt-count">No steps yet</span>`);
756
+ head.querySelector('.wt-title').textContent = this.tour?.title ?? 'Walkthrough';
757
+ head.addEventListener('click', () => {
758
+ this.listCollapsed = !this.listCollapsed;
759
+ this.openMenu();
760
+ });
761
+ section.appendChild(head);
762
+ if (this.listCollapsed)
763
+ return section;
764
+ const list = document.createElement('div');
765
+ list.className = 'steps';
766
+ stops.forEach((stop, i) => {
767
+ const state = seen.has(stop.target) ? 'done' : stop === current ? 'current' : 'next';
768
+ const onPage = document.querySelector(stop.target) !== null;
769
+ const row = document.createElement('div');
770
+ row.className = `step-row ${state}`;
771
+ const go = document.createElement('button');
772
+ go.className = 'step';
773
+ go.disabled = !onPage;
774
+ go.title = onPage ? `Go to step ${i + 1}` : 'Not on the page right now';
775
+ go.innerHTML =
776
+ `<span class="num">${state === 'done' ? '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 8.5l2.5 2.5L12 5.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' : i + 1}</span><span class="label"></span>`;
777
+ go.querySelector('.label').textContent = stop.heading;
778
+ go.addEventListener('click', () => {
779
+ this.closeMenu();
780
+ this.startWalkthrough(stop);
781
+ });
782
+ row.appendChild(go);
783
+ if (!this.isStaticMode) {
784
+ const edit = document.createElement('button');
785
+ edit.className = 'step-edit';
786
+ edit.setAttribute('aria-label', `Edit step ${i + 1}, ${stop.heading}`);
787
+ edit.innerHTML = `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>`;
788
+ edit.disabled = !onPage;
789
+ edit.addEventListener('click', () => {
790
+ const el = document.querySelector(stop.target);
791
+ this.closeMenu();
792
+ if (el)
793
+ this.showInspPopup(el, stop, true);
794
+ });
795
+ row.appendChild(edit);
796
+ }
797
+ list.appendChild(row);
798
+ });
799
+ if (!stops.length) {
800
+ const empty = document.createElement('p');
801
+ empty.className = 'wt-empty';
802
+ empty.textContent = this.isStaticMode
803
+ ? 'This page has no walkthrough.'
804
+ : 'This page has no walkthrough yet. Add the first step yourself, or let IBM Bob write it with docugate tour init.';
805
+ list.appendChild(empty);
806
+ }
807
+ if (!this.isStaticMode) {
808
+ const add = document.createElement('button');
809
+ add.className = 'add-step';
810
+ add.innerHTML = `<span class="num">+</span><span class="label"></span>`;
811
+ add.querySelector('.label').textContent = stops.length ? 'Add a step' : 'Add the first step';
812
+ add.addEventListener('click', () => {
813
+ this.closeMenu();
814
+ if (!this.inspectorActive)
815
+ this.toggleInspector();
816
+ this.toast('Click the element this step should explain.');
817
+ });
818
+ list.appendChild(add);
819
+ }
820
+ section.appendChild(list);
821
+ return section;
822
+ }
823
+ /** One switch: while it is on, clicking anything shows its info, or lets you add it. */
824
+ inspectorRow() {
825
+ const row = document.createElement('div');
826
+ row.className = 'menu-list';
827
+ const b = document.createElement('button');
828
+ b.className = 'menu-item';
829
+ b.setAttribute('role', 'switch');
830
+ b.setAttribute('aria-checked', String(this.inspectorActive));
831
+ b.innerHTML = `<span class="item-text"><span></span><span class="menu-hint"></span></span><span class="switch" aria-hidden="true"></span>`;
832
+ const [label, hint] = b.querySelectorAll('.item-text span');
833
+ label.textContent = 'Inspector';
834
+ hint.textContent = this.isStaticMode ? 'Click anything to see what it is' : 'Click anything to see it, fix it, or add it';
835
+ b.addEventListener('click', () => {
836
+ this.closeMenu();
837
+ this.toggleInspector();
838
+ });
839
+ row.appendChild(b);
840
+ return row;
841
+ }
842
+ menuFooter() {
843
+ const foot = document.createElement('div');
844
+ foot.className = 'menu-foot';
845
+ const status = document.createElement('span');
846
+ status.className = 'live';
847
+ status.textContent = this.isStaticMode ? 'Static demo' : this.base.replace(/^https?:\/\//, '');
848
+ const hide = document.createElement('button');
849
+ hide.className = 'link-btn';
850
+ hide.textContent = 'Hide';
851
+ hide.title = 'Hide until this tab is reloaded';
852
+ hide.addEventListener('click', () => {
853
+ try {
854
+ sessionStorage.setItem(HIDDEN_KEY, '1');
855
+ }
856
+ catch {
857
+ // hidden for now only
858
+ }
859
+ if (this.inspectorActive)
860
+ this.stopInspector();
861
+ this.removePill();
862
+ });
863
+ foot.append(status, hide);
864
+ return foot;
865
+ }
866
+ /** Steps seen on this screen in this tab, by target, so the checklist shows progress. */
867
+ visited() {
868
+ try {
869
+ return new Set(JSON.parse(sessionStorage.getItem(`docugate-seen:${this.tour?.route ?? location.pathname}`) ?? '[]'));
870
+ }
871
+ catch {
872
+ return new Set();
873
+ }
874
+ }
875
+ markVisited(target) {
876
+ const seen = this.visited();
877
+ seen.add(target);
878
+ try {
879
+ sessionStorage.setItem(`docugate-seen:${this.tour?.route ?? location.pathname}`, JSON.stringify([...seen]));
880
+ }
881
+ catch {
882
+ // progress is only shown for this session anyway
883
+ }
884
+ }
885
+ closeMenu() {
886
+ this.menuEl?.remove();
887
+ this.menuEl = null;
888
+ this.menuOpen = false;
889
+ this.pillBtn?.classList.remove('open');
890
+ this.pillBtn?.setAttribute('aria-expanded', 'false');
891
+ }
892
+ // ── Walkthrough ───────────────────────────────────────────────────────────
893
+ startWalkthrough(from) {
894
+ if (!this.tour)
895
+ return;
896
+ // Filter to stops whose target exists in the DOM
897
+ this.wtStops = this.tour.stops.filter((s) => document.querySelector(s.target) !== null);
898
+ if (!this.wtStops.length)
899
+ return;
900
+ const startAt = from ? Math.max(0, this.wtStops.indexOf(from)) : 0;
901
+ this.walkthroughActive = true;
902
+ // The card needs the corner the badge sits in; it steps aside until Done.
903
+ this.pillBtn?.classList.add('away');
904
+ this.wtIndex = startAt;
905
+ this.canvas = createRingCanvas();
906
+ document.body.appendChild(this.canvas);
907
+ document.addEventListener('keydown', this.boundHandleKey);
908
+ window.addEventListener('resize', () => this.refreshWtCard());
909
+ this.showWtStep();
910
+ }
911
+ showWtStep() {
912
+ const stop = this.wtStops[this.wtIndex];
913
+ const target = document.querySelector(stop.target);
914
+ if (!target) {
915
+ this.nextWtStep();
916
+ return;
917
+ }
918
+ this.markVisited(stop.target);
919
+ const rect = target.getBoundingClientRect();
920
+ if (this.canvas) {
921
+ this.canvas.width = window.innerWidth;
922
+ this.canvas.height = window.innerHeight;
923
+ drawRing(this.canvas, rect);
924
+ }
925
+ target.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
926
+ this.wtCard?.remove();
927
+ const card = document.createElement('div');
928
+ card.className = 'wt-card surface';
929
+ card.setAttribute('role', 'dialog');
930
+ card.setAttribute('aria-modal', 'true');
931
+ card.setAttribute('aria-label', stop.heading);
932
+ const eyebrow = document.createElement('p');
933
+ eyebrow.className = 'wt-eyebrow';
934
+ eyebrow.textContent = `${this.tour?.title ?? 'Tour'} · stop ${this.wtIndex + 1} of ${this.wtStops.length}`;
935
+ card.appendChild(eyebrow);
936
+ const heading = document.createElement('h2');
937
+ heading.className = 'wt-card-heading';
938
+ heading.textContent = stop.heading;
939
+ card.appendChild(heading);
940
+ if (stop.prose) {
941
+ const prose = document.createElement('p');
942
+ prose.className = 'wt-prose';
943
+ renderProse(prose, stop.prose);
944
+ card.appendChild(prose);
945
+ }
946
+ const grid = document.createElement('div');
947
+ grid.className = 'wt-fields';
948
+ const fields = [];
949
+ if (stop.data)
950
+ fields.push({ label: 'Data', value: stop.data });
951
+ fields.push({ label: 'Code', value: stop.code });
952
+ if (stop.source)
953
+ fields.push({ label: 'Source', value: stop.source });
954
+ if (stop.docs)
955
+ fields.push({ label: 'Docs', value: stop.docs, isLink: true });
956
+ for (const f of fields) {
957
+ const lbl = document.createElement('span');
958
+ lbl.className = 'wt-field-label';
959
+ lbl.textContent = f.label;
960
+ const val = document.createElement('span');
961
+ val.className = 'wt-field-value';
962
+ if (f.isLink && f.value.startsWith('http')) {
963
+ const a = document.createElement('a');
964
+ a.href = f.value;
965
+ a.target = '_blank';
966
+ a.rel = 'noopener noreferrer';
967
+ a.textContent = breakable(f.value);
968
+ val.appendChild(a);
969
+ }
970
+ else {
971
+ val.textContent = breakable(f.value);
972
+ }
973
+ grid.append(lbl, val);
974
+ }
975
+ card.appendChild(grid);
976
+ const nav = document.createElement('div');
977
+ nav.className = 'wt-nav';
978
+ const dots = document.createElement('span');
979
+ dots.className = 'wt-dots';
980
+ dots.setAttribute('aria-hidden', 'true');
981
+ dots.innerHTML = this.wtStops.map((_, i) => `<i class="${i === this.wtIndex ? 'on' : ''}"></i>`).join('');
982
+ nav.appendChild(dots);
983
+ if (!this.isStaticMode) {
984
+ const edit = document.createElement('button');
985
+ edit.className = 'wt-btn ghost';
986
+ edit.textContent = 'Edit';
987
+ edit.addEventListener('click', () => {
988
+ const el = document.querySelector(stop.target);
989
+ this.stopWalkthrough();
990
+ if (el)
991
+ this.showInspPopup(el, stop, true);
992
+ });
993
+ nav.appendChild(edit);
994
+ }
995
+ if (this.wtIndex > 0) {
996
+ const back = document.createElement('button');
997
+ back.className = 'wt-btn';
998
+ back.textContent = 'Back';
999
+ back.addEventListener('click', () => this.prevWtStep());
1000
+ nav.appendChild(back);
1001
+ }
1002
+ const isLast = this.wtIndex === this.wtStops.length - 1;
1003
+ const next = document.createElement('button');
1004
+ next.className = 'wt-btn primary';
1005
+ next.textContent = isLast ? 'Done' : 'Next';
1006
+ next.addEventListener('click', () => isLast ? this.stopWalkthrough() : this.nextWtStep());
1007
+ nav.appendChild(next);
1008
+ card.appendChild(nav);
1009
+ this.shadow.appendChild(card);
1010
+ this.wtCard = card;
1011
+ next.focus();
1012
+ // Position after appending (needs offsetHeight)
1013
+ requestAnimationFrame(() => positionCard(card, rect));
1014
+ }
1015
+ refreshWtCard() {
1016
+ if (!this.walkthroughActive)
1017
+ return;
1018
+ const stop = this.wtStops[this.wtIndex];
1019
+ const target = document.querySelector(stop.target);
1020
+ if (!target)
1021
+ return;
1022
+ const rect = target.getBoundingClientRect();
1023
+ if (this.canvas) {
1024
+ this.canvas.width = window.innerWidth;
1025
+ this.canvas.height = window.innerHeight;
1026
+ drawRing(this.canvas, rect);
1027
+ }
1028
+ if (this.wtCard)
1029
+ positionCard(this.wtCard, rect);
1030
+ }
1031
+ nextWtStep() {
1032
+ this.wtIndex++;
1033
+ if (this.wtIndex >= this.wtStops.length) {
1034
+ this.stopWalkthrough();
1035
+ return;
1036
+ }
1037
+ this.showWtStep();
1038
+ }
1039
+ prevWtStep() {
1040
+ if (this.wtIndex > 0) {
1041
+ this.wtIndex--;
1042
+ this.showWtStep();
1043
+ }
1044
+ }
1045
+ stopWalkthrough() {
1046
+ this.walkthroughActive = false;
1047
+ this.pillBtn?.classList.remove('away');
1048
+ this.wtCard?.remove();
1049
+ this.wtCard = null;
1050
+ if (this.canvas) {
1051
+ clearCanvas(this.canvas);
1052
+ this.canvas.remove();
1053
+ this.canvas = null;
1054
+ }
1055
+ document.removeEventListener('keydown', this.boundHandleKey);
1056
+ }
1057
+ // ── Inspector ─────────────────────────────────────────────────────────────
1058
+ toggleInspector() {
1059
+ if (this.inspectorActive) {
1060
+ this.stopInspector();
1061
+ }
1062
+ else {
1063
+ this.startInspector();
1064
+ }
1065
+ }
1066
+ startInspector() {
1067
+ this.inspectorActive = true;
1068
+ this.pillBtn?.classList.add('inspecting');
1069
+ this.highlightEl = createHighlightEl();
1070
+ document.body.appendChild(this.highlightEl);
1071
+ document.addEventListener('mousemove', this.boundHandleMouseMove, true);
1072
+ document.addEventListener('click', this.boundHandleClick, true);
1073
+ document.addEventListener('keydown', this.boundHandleKey);
1074
+ }
1075
+ stopInspector() {
1076
+ this.inspectorActive = false;
1077
+ this.pillBtn?.classList.remove('inspecting');
1078
+ document.removeEventListener('mousemove', this.boundHandleMouseMove, true);
1079
+ document.removeEventListener('click', this.boundHandleClick, true);
1080
+ document.removeEventListener('keydown', this.boundHandleKey);
1081
+ if (this.highlightEl) {
1082
+ this.highlightEl.remove();
1083
+ this.highlightEl = null;
1084
+ }
1085
+ inspectorHighlight = null;
1086
+ this.closeInspPopup();
1087
+ this.closeAddForm();
1088
+ }
1089
+ handleInspectorMouseMove(e) {
1090
+ if (!this.inspectorActive)
1091
+ return;
1092
+ const el = e.target;
1093
+ if (el && !this.host.contains(el)) {
1094
+ const rect = el.getBoundingClientRect();
1095
+ if (this.highlightEl)
1096
+ moveHighlight(this.highlightEl, rect);
1097
+ inspectorHighlight = el;
1098
+ }
1099
+ }
1100
+ handleInspectorClick(e) {
1101
+ if (!this.inspectorActive)
1102
+ return;
1103
+ const el = e.target;
1104
+ if (!el || this.host.contains(el))
1105
+ return;
1106
+ e.preventDefault();
1107
+ e.stopPropagation();
1108
+ const stop = this.findStopForElement(el);
1109
+ if (stop) {
1110
+ this.showInspPopup(el, stop);
1111
+ }
1112
+ else {
1113
+ this.showAddForm(el);
1114
+ }
1115
+ }
1116
+ findStopForElement(el) {
1117
+ if (!this.tour)
1118
+ return null;
1119
+ for (const stop of this.tour.stops) {
1120
+ try {
1121
+ if (el.matches(stop.target))
1122
+ return stop;
1123
+ }
1124
+ catch {
1125
+ // invalid selector — skip
1126
+ }
1127
+ }
1128
+ return null;
1129
+ }
1130
+ // ── Stop panel: read a stop, or correct it, next to its element ──────────
1131
+ /**
1132
+ * One panel for everything about a stop. It opens reading (what this is and
1133
+ * where its data comes from) and turns into a form in place when the person
1134
+ * edits, so fixing something the AI got wrong never leaves the element.
1135
+ * With no stop, it opens as the form for a new one.
1136
+ */
1137
+ showInspPopup(el, stop, editing = false) {
1138
+ this.closeInspPopup();
1139
+ const panel = document.createElement('div');
1140
+ panel.className = 'insp-popup surface';
1141
+ panel.setAttribute('role', 'dialog');
1142
+ panel.setAttribute('aria-label', stop ? stop.heading : 'Add to tour');
1143
+ this.shadow.appendChild(panel);
1144
+ this.inspPopup = panel;
1145
+ this.inspEl = el;
1146
+ if (stop && !editing)
1147
+ this.fillView(panel, el, stop);
1148
+ else
1149
+ this.fillForm(panel, el, stop);
1150
+ this.placeNear(panel, el);
1151
+ }
1152
+ fillView(panel, el, stop) {
1153
+ panel.replaceChildren();
1154
+ const stops = this.tour?.stops ?? [];
1155
+ const index = stops.findIndex((s) => s.target === stop.target);
1156
+ const eyebrow = document.createElement('p');
1157
+ eyebrow.className = 'wt-eyebrow';
1158
+ eyebrow.textContent = `${this.tour?.title ?? 'Tour'}${index >= 0 ? ` · stop ${index + 1} of ${stops.length}` : ''}`;
1159
+ const heading = document.createElement('h3');
1160
+ heading.className = 'insp-popup-heading';
1161
+ heading.textContent = stop.heading;
1162
+ panel.append(eyebrow, heading);
1163
+ if (stop.prose) {
1164
+ const prose = document.createElement('p');
1165
+ prose.className = 'insp-popup-prose';
1166
+ renderProse(prose, stop.prose);
1167
+ panel.appendChild(prose);
1168
+ }
1169
+ const section = document.createElement('p');
1170
+ section.className = 'panel-section';
1171
+ section.textContent = 'Where it comes from';
1172
+ panel.appendChild(section);
1173
+ const grid = document.createElement('div');
1174
+ grid.className = 'wt-fields';
1175
+ const find = tourValue(stop.target);
1176
+ const row = (label, content) => {
1177
+ const lbl = document.createElement('span');
1178
+ lbl.className = 'insp-popup-label';
1179
+ lbl.textContent = label;
1180
+ const val = document.createElement('span');
1181
+ val.className = 'insp-popup-val';
1182
+ val.appendChild(content);
1183
+ grid.append(lbl, val);
1184
+ };
1185
+ const fileLink = (path, findText) => {
1186
+ if (this.isStaticMode)
1187
+ return document.createTextNode(breakable(path));
1188
+ const b = document.createElement('button');
1189
+ b.className = 'file-link';
1190
+ b.title = 'Open in your editor';
1191
+ b.textContent = breakable(path);
1192
+ b.addEventListener('click', () => this.openInEditor(path, findText));
1193
+ return b;
1194
+ };
1195
+ if (stop.data)
1196
+ row('Data', document.createTextNode(stop.data));
1197
+ row('Code', fileLink(stop.code, find));
1198
+ if (stop.source)
1199
+ row('Source', fileLink(stop.source));
1200
+ if (stop.docs && /^https?:\/\//.test(stop.docs)) {
1201
+ const a = document.createElement('a');
1202
+ a.href = stop.docs;
1203
+ a.target = '_blank';
1204
+ a.rel = 'noopener noreferrer';
1205
+ a.textContent = breakable(stop.docs);
1206
+ row('Docs', a);
1207
+ }
1208
+ panel.appendChild(grid);
1209
+ const actions = document.createElement('div');
1210
+ actions.className = 'insp-popup-actions';
1211
+ const where = document.createElement('span');
1212
+ where.className = 'panel-file';
1213
+ where.textContent = this.tour?.file ?? '';
1214
+ actions.appendChild(where);
1215
+ if (!this.isStaticMode) {
1216
+ const edit = document.createElement('button');
1217
+ edit.className = 'wt-btn';
1218
+ edit.textContent = 'Edit';
1219
+ edit.addEventListener('click', () => {
1220
+ this.fillForm(panel, el, stop);
1221
+ this.placeNear(panel, el);
1222
+ });
1223
+ actions.appendChild(edit);
1224
+ }
1225
+ const close = document.createElement('button');
1226
+ close.className = 'wt-btn primary';
1227
+ close.textContent = 'Close';
1228
+ close.addEventListener('click', () => this.closeInspPopup());
1229
+ actions.appendChild(close);
1230
+ panel.appendChild(actions);
1231
+ close.focus();
1232
+ }
1233
+ fillForm(panel, el, existing) {
1234
+ panel.replaceChildren();
1235
+ panel.setAttribute('aria-label', existing ? `Edit ${existing.heading}` : 'Add to tour');
1236
+ const { selector, needsAttribute } = suggestSelector(el);
1237
+ const title = document.createElement('h3');
1238
+ title.className = 'insp-popup-heading';
1239
+ title.textContent = existing ? 'Edit this stop' : 'Add to tour';
1240
+ const hint = document.createElement('p');
1241
+ hint.className = 'panel-hint';
1242
+ hint.textContent = existing
1243
+ ? 'Correct anything that is wrong. Only this stop changes in the tour file.'
1244
+ : 'Say what this element is and where its data comes from.';
1245
+ panel.append(title, hint);
1246
+ const fields = [
1247
+ { key: 'heading', label: 'Name', value: existing?.heading ?? '', placeholder: 'e.g. Total' },
1248
+ { key: 'prose', label: 'What it is', value: existing?.prose ?? '', multi: true, placeholder: 'e.g. The amount due: the subtotal plus tax, computed by `totals()`.' },
1249
+ { key: 'data', label: 'Data', value: existing?.data ?? '', placeholder: 'e.g. GET /api/invoices/:id → total' },
1250
+ { key: 'code', label: 'Code file', value: existing?.code ?? '', placeholder: 'e.g. src/screens/Invoice.tsx' },
1251
+ { key: 'source', label: 'Source file', value: existing?.source ?? '', placeholder: 'Optional: where the value is computed' },
1252
+ { key: 'docs', label: 'Docs link', value: existing?.docs ?? '', placeholder: 'Optional' },
1253
+ { key: 'target', label: 'Selector', value: existing?.target ?? selector },
1254
+ ];
1255
+ const inputs = {};
1256
+ const form = document.createElement('form');
1257
+ form.className = 'panel-form';
1258
+ for (const f of fields) {
1259
+ const id = `dg-${f.key}`;
1260
+ const label = document.createElement('label');
1261
+ label.htmlFor = id;
1262
+ label.textContent = f.label;
1263
+ const input = f.multi ? document.createElement('textarea') : document.createElement('input');
1264
+ input.id = id;
1265
+ input.value = f.value;
1266
+ if (f.placeholder)
1267
+ input.placeholder = f.placeholder;
1268
+ if (f.key === 'target' || f.key === 'code')
1269
+ input.spellcheck = false;
1270
+ form.append(label, input);
1271
+ inputs[f.key] = input;
1272
+ }
1273
+ if (needsAttribute && !existing) {
1274
+ const warn = document.createElement('div');
1275
+ warn.className = 'add-form-warn';
1276
+ warn.append('Add ');
1277
+ const code = document.createElement('code');
1278
+ code.textContent = `data-tour="${tourValue(selector) ?? ''}"`;
1279
+ warn.append(code, ' to this element in your code, so the stop keeps finding it.');
1280
+ form.appendChild(warn);
1281
+ }
1282
+ const error = document.createElement('p');
1283
+ error.className = 'panel-error';
1284
+ error.hidden = true;
1285
+ form.appendChild(error);
1286
+ const actions = document.createElement('div');
1287
+ actions.className = 'insp-popup-actions';
1288
+ if (existing) {
1289
+ const remove = document.createElement('button');
1290
+ remove.type = 'button';
1291
+ remove.className = 'wt-btn danger';
1292
+ remove.textContent = 'Remove';
1293
+ let armed = false;
1294
+ remove.addEventListener('click', () => {
1295
+ if (!armed) {
1296
+ armed = true;
1297
+ remove.textContent = 'Remove this stop?';
1298
+ return;
1299
+ }
1300
+ this.removeStop(existing);
1301
+ });
1302
+ actions.appendChild(remove);
1303
+ }
1304
+ const spacer = document.createElement('span');
1305
+ spacer.className = 'panel-file';
1306
+ actions.appendChild(spacer);
1307
+ const cancel = document.createElement('button');
1308
+ cancel.type = 'button';
1309
+ cancel.className = 'wt-btn';
1310
+ cancel.textContent = 'Cancel';
1311
+ cancel.addEventListener('click', () => {
1312
+ if (existing) {
1313
+ this.fillView(panel, el, existing);
1314
+ this.placeNear(panel, el);
1315
+ }
1316
+ else {
1317
+ this.closeInspPopup();
1318
+ }
1319
+ });
1320
+ const save = document.createElement('button');
1321
+ save.type = 'submit';
1322
+ save.className = 'wt-btn primary';
1323
+ save.textContent = existing ? 'Save' : 'Add stop';
1324
+ actions.append(cancel, save);
1325
+ form.appendChild(actions);
1326
+ form.addEventListener('submit', async (e) => {
1327
+ e.preventDefault();
1328
+ const value = (k) => inputs[k].value.trim();
1329
+ const stop = {
1330
+ heading: value('heading'),
1331
+ target: value('target'),
1332
+ data: value('data') || undefined,
1333
+ code: value('code'),
1334
+ source: value('source') || undefined,
1335
+ docs: value('docs') || undefined,
1336
+ prose: value('prose'),
1337
+ };
1338
+ const missing = !stop.heading ? 'a name' : !stop.code ? 'the code file' : !stop.target ? 'a selector' : '';
1339
+ if (missing) {
1340
+ error.textContent = `Add ${missing} first.`;
1341
+ error.hidden = false;
1342
+ return;
1343
+ }
1344
+ save.disabled = true;
1345
+ const saved = await this.saveStop(stop, existing?.target);
1346
+ save.disabled = false;
1347
+ if (!saved) {
1348
+ error.textContent = 'Could not save. Is docugate tour serve still running?';
1349
+ error.hidden = false;
1350
+ return;
1351
+ }
1352
+ this.fillView(panel, el, stop);
1353
+ this.placeNear(panel, el);
1354
+ });
1355
+ panel.appendChild(form);
1356
+ inputs[existing ? 'prose' : 'heading'].focus();
1357
+ }
1358
+ /** Puts the panel beside its element, below it when there is room, else above. */
1359
+ placeNear(panel, el) {
1360
+ const rect = el.getBoundingClientRect();
1361
+ const w = panel.offsetWidth || 360;
1362
+ const h = panel.offsetHeight || 300;
1363
+ const pad = 12;
1364
+ let top = rect.bottom + 10;
1365
+ if (top + h > window.innerHeight - pad)
1366
+ top = Math.max(pad, rect.top - h - 10);
1367
+ const left = Math.max(pad, Math.min(rect.left, window.innerWidth - w - pad));
1368
+ panel.style.top = `${top}px`;
1369
+ panel.style.left = `${left}px`;
1370
+ }
1371
+ closeInspPopup() {
1372
+ this.inspPopup?.remove();
1373
+ this.inspPopup = null;
1374
+ this.inspEl = null;
1375
+ }
1376
+ showAddForm(el, existing) {
1377
+ this.showInspPopup(el, existing ?? null, true);
1378
+ }
1379
+ closeAddForm() {
1380
+ this.closeInspPopup();
1381
+ }
1382
+ async saveStop(stop, previousTarget) {
1383
+ // A page with no tour yet starts one at its own path.
1384
+ const route = this.tour?.route ?? location.pathname;
1385
+ try {
1386
+ const res = await fetch(`${this.base}/stop`, {
1387
+ method: 'PUT',
1388
+ headers: { 'Content-Type': 'application/json' },
1389
+ body: JSON.stringify({ route, stop, previousTarget }),
1390
+ });
1391
+ if (!res.ok)
1392
+ return false;
1393
+ this.patchLocalStop(stop, previousTarget);
1394
+ this.toast(`Saved to ${this.tour?.file ?? '.docugate/tour/'}`);
1395
+ return true;
1396
+ }
1397
+ catch {
1398
+ return false;
1399
+ }
1400
+ }
1401
+ async removeStop(stop) {
1402
+ try {
1403
+ const res = await fetch(`${this.base}/stop`, {
1404
+ method: 'DELETE',
1405
+ headers: { 'Content-Type': 'application/json' },
1406
+ body: JSON.stringify({ route: this.tour?.route ?? location.pathname, target: stop.target }),
1407
+ });
1408
+ if (!res.ok)
1409
+ throw new Error();
1410
+ if (this.tour)
1411
+ this.tour.stops = this.tour.stops.filter((s) => s.target !== stop.target);
1412
+ this.closeInspPopup();
1413
+ this.toast(`Removed "${stop.heading}"`);
1414
+ }
1415
+ catch {
1416
+ this.toast('Could not remove the stop. Is docugate tour serve still running?');
1417
+ }
1418
+ }
1419
+ /** Keeps the in-memory tour in step with a save, before the file watcher reports it. */
1420
+ patchLocalStop(stop, previousTarget) {
1421
+ if (!this.tour) {
1422
+ this.tour = { route: location.pathname, title: document.title || 'Tour', stops: [stop] };
1423
+ return;
1424
+ }
1425
+ const i = this.tour.stops.findIndex((s) => s.target === (previousTarget ?? stop.target));
1426
+ if (i >= 0)
1427
+ this.tour.stops[i] = stop;
1428
+ else
1429
+ this.tour.stops.push(stop);
1430
+ }
1431
+ async openInEditor(path, find) {
1432
+ try {
1433
+ const res = await fetch(`${this.base}/open`, {
1434
+ method: 'POST',
1435
+ headers: { 'Content-Type': 'application/json' },
1436
+ body: JSON.stringify({ path, find }),
1437
+ });
1438
+ if (!res.ok)
1439
+ throw new Error();
1440
+ this.toast(`Opened ${path.split('/').pop()}`);
1441
+ }
1442
+ catch {
1443
+ this.toast('Could not open your editor. Set DOCUGATE_EDITOR (for example: code) and restart docugate tour serve.');
1444
+ }
1445
+ }
1446
+ toast(message) {
1447
+ this.shadow.querySelector('.toast')?.remove();
1448
+ const t = document.createElement('div');
1449
+ t.className = 'toast surface';
1450
+ t.setAttribute('role', 'status');
1451
+ t.textContent = message;
1452
+ this.shadow.appendChild(t);
1453
+ setTimeout(() => t.remove(), 3200);
1454
+ }
1455
+ // ── Keyboard ──────────────────────────────────────────────────────────────
1456
+ handleKey(e) {
1457
+ if (e.key === 'Escape') {
1458
+ if (this.addForm) {
1459
+ this.closeAddForm();
1460
+ return;
1461
+ }
1462
+ if (this.inspPopup) {
1463
+ this.closeInspPopup();
1464
+ return;
1465
+ }
1466
+ if (this.walkthroughActive) {
1467
+ this.stopWalkthrough();
1468
+ return;
1469
+ }
1470
+ if (this.inspectorActive) {
1471
+ this.stopInspector();
1472
+ return;
1473
+ }
1474
+ }
1475
+ if (this.walkthroughActive) {
1476
+ if (e.key === 'ArrowRight')
1477
+ this.nextWtStep();
1478
+ if (e.key === 'ArrowLeft')
1479
+ this.prevWtStep();
1480
+ }
1481
+ }
1482
+ // ── SSE events ────────────────────────────────────────────────────────────
1483
+ listenEvents() {
1484
+ try {
1485
+ const es = new EventSource(`${this.base}/events`);
1486
+ es.addEventListener('message', (e) => {
1487
+ if (e.data === 'change')
1488
+ this.reload();
1489
+ });
1490
+ es.addEventListener('error', () => {
1491
+ es.close();
1492
+ });
1493
+ this.evtSource = es;
1494
+ }
1495
+ catch {
1496
+ // SSE not available
1497
+ }
1498
+ }
1499
+ async reload() {
1500
+ const path = location.pathname;
1501
+ await this.load();
1502
+ if (this.walkthroughActive)
1503
+ this.stopWalkthrough();
1504
+ this.render();
1505
+ // A new page ends the inspection; an edit on this page keeps it going.
1506
+ if (this.inspectorActive && path !== this.inspectedPath)
1507
+ this.stopInspector();
1508
+ this.inspectedPath = path;
1509
+ }
1510
+ // ── URL change detection ──────────────────────────────────────────────────
1511
+ patchHistory() {
1512
+ const onNavigate = () => { setTimeout(() => this.reload(), 50); };
1513
+ const origPush = history.pushState.bind(history);
1514
+ history.pushState = (...args) => {
1515
+ origPush(...args);
1516
+ onNavigate();
1517
+ };
1518
+ const origReplace = history.replaceState.bind(history);
1519
+ history.replaceState = (...args) => {
1520
+ origReplace(...args);
1521
+ onNavigate();
1522
+ };
1523
+ window.addEventListener('popstate', onNavigate);
1524
+ }
1525
+ }
1526
+ // ---------------------------------------------------------------------------
1527
+ // Bootstrap
1528
+ // ---------------------------------------------------------------------------
1529
+ function boot() {
1530
+ if (document.querySelector('[data-docugate-pill]'))
1531
+ return; // already mounted
1532
+ const base = window.DOCUGATE_TOUR ? '' : (getServerBase() ?? '');
1533
+ if (!window.DOCUGATE_TOUR && !base)
1534
+ return; // no server src and no static tour
1535
+ new DocugatePill(base);
1536
+ }
1537
+ if (typeof document !== 'undefined') {
1538
+ if (document.readyState === 'loading') {
1539
+ document.addEventListener('DOMContentLoaded', boot);
1540
+ }
1541
+ else {
1542
+ boot();
1543
+ }
1544
+ }
1545
+
1546
+ })();