docugate 0.2.0 → 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.
package/pill/dist/pill.js CHANGED
@@ -93,250 +93,308 @@ async function fetchTour(base) {
93
93
  // ---------------------------------------------------------------------------
94
94
  // CSS (injected into shadow DOM)
95
95
  // ---------------------------------------------------------------------------
96
- const STYLES = `
97
- :host { all: initial; }
98
- *, *::before, *::after { box-sizing: border-box; }
99
-
100
- /* ── pill button ── */
101
- .pill-btn {
102
- position: fixed;
103
- bottom: 20px;
104
- right: 20px;
105
- z-index: 2147483647;
106
- display: flex;
107
- align-items: center;
108
- gap: 6px;
109
- padding: 6px 12px;
110
- background: #1a1a1a;
111
- color: #e8e8e8;
112
- border: 1px solid #333;
113
- border-radius: 999px;
114
- font: 12px/1 -apple-system, "Segoe UI", system-ui, sans-serif;
115
- cursor: pointer;
116
- user-select: none;
117
- letter-spacing: 0.01em;
118
- transition: background 0.15s;
119
- }
120
- .pill-btn:hover { background: #2a2a2a; }
121
- .pill-btn:focus-visible { outline: 2px solid #4a90d9; outline-offset: 2px; }
122
- .pill-dot {
123
- width: 6px;
124
- height: 6px;
125
- background: #4a90d9;
126
- border-radius: 50%;
127
- flex-shrink: 0;
128
- }
129
-
130
- /* ── menu ── */
131
- .menu {
132
- position: fixed;
133
- bottom: 54px;
134
- right: 20px;
135
- z-index: 2147483647;
136
- background: #1a1a1a;
137
- border: 1px solid #333;
138
- border-radius: 8px;
139
- overflow: hidden;
140
- font: 13px/1 -apple-system, "Segoe UI", system-ui, sans-serif;
141
- min-width: 170px;
142
- }
143
- .menu-item {
144
- display: block;
145
- width: 100%;
146
- padding: 10px 14px;
147
- background: none;
148
- border: none;
149
- color: #e0e0e0;
150
- text-align: left;
151
- cursor: pointer;
152
- font: inherit;
153
- transition: background 0.1s;
154
- }
155
- .menu-item:hover { background: #2a2a2a; }
156
- .menu-item:focus-visible { outline: 2px solid #4a90d9; outline-offset: -2px; }
157
- .menu-item + .menu-item { border-top: 1px solid #2a2a2a; }
158
- .menu-item.active { color: #4a90d9; }
159
- .menu-item:disabled { color: #777; cursor: default; background: none; }
160
-
161
- /* ── overlay / backdrop ── */
162
- .overlay {
163
- position: fixed;
164
- inset: 0;
165
- z-index: 2147483640;
166
- pointer-events: none;
167
- }
168
-
169
- /* ── walkthrough card ── */
170
- .wt-card {
171
- position: fixed;
172
- z-index: 2147483645;
173
- background: #1a1a1a;
174
- border: 1px solid #333;
175
- border-radius: 10px;
176
- padding: 18px 20px 14px;
177
- width: 320px;
178
- max-width: calc(100vw - 32px);
179
- font: 13px/1.55 -apple-system, "Segoe UI", system-ui, sans-serif;
180
- color: #ddd;
181
- }
182
- .wt-card-heading {
183
- font-size: 14px;
184
- font-weight: 600;
185
- color: #fff;
186
- margin: 0 0 12px;
187
- }
188
- .wt-field {
189
- display: flex;
190
- flex-direction: column;
191
- gap: 2px;
192
- margin-bottom: 8px;
193
- }
194
- .wt-field-label {
195
- font-size: 10px;
196
- text-transform: uppercase;
197
- letter-spacing: 0.06em;
198
- color: #666;
199
- }
200
- .wt-field-value {
201
- font-size: 12px;
202
- color: #b0b0b0;
203
- word-break: break-all;
204
- }
205
- .wt-field-value a {
206
- color: #4a90d9;
207
- text-decoration: none;
208
- }
209
- .wt-field-value a:hover { text-decoration: underline; }
210
- .wt-prose {
211
- font-size: 13px;
212
- color: #bbb;
213
- margin: 10px 0 14px;
214
- line-height: 1.55;
215
- }
216
- .wt-nav {
217
- display: flex;
218
- justify-content: flex-end;
219
- gap: 8px;
220
- }
221
- .wt-btn {
222
- padding: 5px 14px;
223
- border-radius: 6px;
224
- border: 1px solid #444;
225
- background: #252525;
226
- color: #ddd;
227
- font: 12px -apple-system, "Segoe UI", system-ui, sans-serif;
228
- cursor: pointer;
229
- transition: background 0.1s;
230
- }
231
- .wt-btn:hover { background: #333; }
232
- .wt-btn:focus-visible { outline: 2px solid #4a90d9; outline-offset: 2px; }
233
- .wt-btn.primary {
234
- background: #1e4a8a;
235
- border-color: #2a5fa0;
236
- color: #fff;
237
- }
238
- .wt-btn.primary:hover { background: #25559a; }
239
- .wt-counter {
240
- font-size: 11px;
241
- color: #555;
242
- margin-right: auto;
243
- align-self: center;
244
- }
245
-
246
- /* ── inspector popup ── */
247
- .insp-popup {
248
- position: fixed;
249
- z-index: 2147483645;
250
- background: #1a1a1a;
251
- border: 1px solid #333;
252
- border-radius: 8px;
253
- padding: 12px 14px 10px;
254
- width: 280px;
255
- max-width: calc(100vw - 24px);
256
- font: 12px/1.5 -apple-system, "Segoe UI", system-ui, sans-serif;
257
- color: #ccc;
258
- pointer-events: auto;
259
- }
260
- .insp-popup-heading {
261
- font-size: 13px;
262
- font-weight: 600;
263
- color: #fff;
264
- margin: 0 0 8px;
265
- }
266
- .insp-popup-row {
267
- display: flex;
268
- flex-direction: column;
269
- gap: 2px;
270
- margin-bottom: 7px;
271
- }
272
- .insp-popup-label {
273
- font-size: 10px;
274
- text-transform: uppercase;
275
- letter-spacing: 0.06em;
276
- color: #555;
277
- }
278
- .insp-popup-val {
279
- font-size: 12px;
280
- color: #aaa;
281
- word-break: break-all;
282
- }
283
- .insp-popup-val a { color: #4a90d9; text-decoration: none; }
284
- .insp-popup-val a:hover { text-decoration: underline; }
285
- .insp-popup-prose { font-size: 12px; color: #bbb; margin: 6px 0 10px; }
286
- .insp-popup-actions {
287
- display: flex;
288
- gap: 6px;
289
- justify-content: flex-end;
290
- }
291
-
292
- /* ── add form ── */
293
- .add-form {
294
- position: fixed;
295
- z-index: 2147483646;
296
- background: #1a1a1a;
297
- border: 1px solid #333;
298
- border-radius: 10px;
299
- padding: 18px 20px 14px;
300
- width: 340px;
301
- max-width: calc(100vw - 32px);
302
- font: 13px/1.4 -apple-system, "Segoe UI", system-ui, sans-serif;
303
- color: #ddd;
304
- top: 50%;
305
- left: 50%;
306
- transform: translate(-50%, -50%);
307
- }
308
- .add-form-title { font-size: 14px; font-weight: 600; color: #fff; margin: 0 0 14px; }
309
- .add-form label { display: block; font-size: 11px; color: #666; margin-bottom: 4px; margin-top: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
310
- .add-form label:first-of-type { margin-top: 0; }
311
- .add-form input, .add-form textarea {
312
- width: 100%;
313
- background: #111;
314
- border: 1px solid #333;
315
- border-radius: 5px;
316
- color: #ddd;
317
- font: 12px/1.4 -apple-system, "Segoe UI", system-ui, sans-serif;
318
- padding: 6px 8px;
319
- resize: vertical;
320
- }
321
- .add-form input:focus, .add-form textarea:focus { outline: 2px solid #4a90d9; border-color: transparent; }
322
- .add-form textarea { min-height: 60px; }
323
- .add-form-warn {
324
- margin-top: 10px;
325
- font-size: 11px;
326
- color: #c09a40;
327
- background: #1e1a0a;
328
- border: 1px solid #3a2e0a;
329
- border-radius: 5px;
330
- padding: 7px 9px;
331
- line-height: 1.5;
332
- }
333
- .add-form-warn code { font-family: monospace; color: #e0b840; }
334
- .add-form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
335
-
336
- /* ── reduced motion ── */
337
- @media (prefers-reduced-motion: reduce) {
338
- *, *::before, *::after { transition: none !important; }
339
- }
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
+ }
340
398
  `;
341
399
  // ---------------------------------------------------------------------------
342
400
  // Highlight ring drawn on a canvas over the page
@@ -377,7 +435,7 @@ function drawRing(canvas, rect) {
377
435
  ctx.fill();
378
436
  ctx.restore();
379
437
  // ring
380
- ctx.strokeStyle = '#4a90d9';
438
+ ctx.strokeStyle = '#F4C43F';
381
439
  ctx.lineWidth = 2;
382
440
  ctx.beginPath();
383
441
  ctx.moveTo(x + r, y);
@@ -400,7 +458,7 @@ function clearCanvas(canvas) {
400
458
  // Card positioning: place card near target without clipping viewport
401
459
  // ---------------------------------------------------------------------------
402
460
  function positionCard(card, rect) {
403
- const cw = 320;
461
+ const cw = card.offsetWidth || 340;
404
462
  const ch = card.offsetHeight || 220;
405
463
  const vw = window.innerWidth;
406
464
  const vh = window.innerHeight;
@@ -428,9 +486,9 @@ function createHighlightEl() {
428
486
  'position:fixed',
429
487
  'pointer-events:none',
430
488
  'z-index:2147483642',
431
- 'border:2px solid #4a90d9',
489
+ 'border:2px solid #F4C43F',
432
490
  'border-radius:4px',
433
- 'background:rgba(74,144,217,0.08)',
491
+ 'background:rgba(244,196,63,0.10)',
434
492
  'transition:top 0.06s,left 0.06s,width 0.06s,height 0.06s',
435
493
  'display:none',
436
494
  ].join(';');
@@ -444,6 +502,58 @@ function moveHighlight(el, rect) {
444
502
  el.style.height = `${rect.height + 4}px`;
445
503
  }
446
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
+ // ---------------------------------------------------------------------------
447
557
  // Main Pill class
448
558
  // ---------------------------------------------------------------------------
449
559
  class DocugatePill {
@@ -461,9 +571,13 @@ class DocugatePill {
461
571
  this.wtCard = null;
462
572
  this.inspPopup = null;
463
573
  this.addForm = null;
574
+ this.inspEl = null;
575
+ this.inspectedPath = location.pathname;
576
+ this.listCollapsed = false;
464
577
  this.highlightEl = null;
465
578
  this.evtSource = null;
466
579
  this.isStaticMode = false;
580
+ this.corner = savedCorner();
467
581
  this.base = base;
468
582
  this.isStaticMode = !!window.DOCUGATE_TOUR;
469
583
  this.host = document.createElement('div');
@@ -498,16 +612,81 @@ class DocugatePill {
498
612
  this.removePill();
499
613
  if (!this.alive)
500
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;
501
623
  const btn = document.createElement('button');
502
- btn.className = 'pill-btn';
503
- btn.setAttribute('aria-label', 'Docugate tour');
504
- btn.innerHTML = `<span class="pill-dot"></span>Tour`;
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
+ });
505
682
  btn.addEventListener('click', (e) => {
506
683
  e.stopPropagation();
684
+ if (dragged) {
685
+ dragged = false;
686
+ return;
687
+ }
507
688
  this.toggleMenu();
508
689
  });
509
- this.shadow.appendChild(btn);
510
- this.pillBtn = btn;
511
690
  }
512
691
  removePill() {
513
692
  this.pillBtn?.remove();
@@ -526,48 +705,203 @@ class DocugatePill {
526
705
  openMenu() {
527
706
  this.closeMenu();
528
707
  const menu = document.createElement('div');
529
- menu.className = 'menu';
530
- menu.setAttribute('role', 'menu');
531
- const walkBtn = document.createElement('button');
532
- walkBtn.className = 'menu-item';
533
- const stops = this.tour?.stops.length ?? 0;
534
- walkBtn.textContent = stops ? 'Walkthrough' : 'No tour on this page yet';
535
- walkBtn.disabled = !stops;
536
- walkBtn.setAttribute('role', 'menuitem');
537
- walkBtn.addEventListener('click', () => { this.closeMenu(); this.startWalkthrough(); });
538
- const inspBtn = document.createElement('button');
539
- inspBtn.className = `menu-item${this.inspectorActive ? ' active' : ''}`;
540
- inspBtn.textContent = this.inspectorActive ? 'Inspector: on' : 'Inspector';
541
- inspBtn.setAttribute('role', 'menuitem');
542
- inspBtn.addEventListener('click', () => { this.closeMenu(); this.toggleInspector(); });
543
- menu.appendChild(walkBtn);
544
- menu.appendChild(inspBtn);
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');
545
717
  this.shadow.appendChild(menu);
546
718
  this.menuEl = menu;
547
719
  this.menuOpen = true;
720
+ this.pillBtn?.classList.add('open');
721
+ this.pillBtn?.setAttribute('aria-expanded', 'true');
548
722
  const close = (e) => {
549
- if (!menu.contains(e.target) && e.target !== this.pillBtn) {
723
+ const inside = e.composedPath().some((n) => n === menu || n === this.pillBtn);
724
+ if (e instanceof KeyboardEvent ? e.key === 'Escape' : !inside) {
550
725
  this.closeMenu();
551
726
  document.removeEventListener('click', close, true);
727
+ document.removeEventListener('keydown', close, true);
552
728
  }
553
729
  };
554
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
+ }
555
884
  }
556
885
  closeMenu() {
557
886
  this.menuEl?.remove();
558
887
  this.menuEl = null;
559
888
  this.menuOpen = false;
889
+ this.pillBtn?.classList.remove('open');
890
+ this.pillBtn?.setAttribute('aria-expanded', 'false');
560
891
  }
561
892
  // ── Walkthrough ───────────────────────────────────────────────────────────
562
- startWalkthrough() {
893
+ startWalkthrough(from) {
563
894
  if (!this.tour)
564
895
  return;
565
896
  // Filter to stops whose target exists in the DOM
566
897
  this.wtStops = this.tour.stops.filter((s) => document.querySelector(s.target) !== null);
567
898
  if (!this.wtStops.length)
568
899
  return;
900
+ const startAt = from ? Math.max(0, this.wtStops.indexOf(from)) : 0;
569
901
  this.walkthroughActive = true;
570
- this.wtIndex = 0;
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;
571
905
  this.canvas = createRingCanvas();
572
906
  document.body.appendChild(this.canvas);
573
907
  document.addEventListener('keydown', this.boundHandleKey);
@@ -581,6 +915,7 @@ class DocugatePill {
581
915
  this.nextWtStep();
582
916
  return;
583
917
  }
918
+ this.markVisited(stop.target);
584
919
  const rect = target.getBoundingClientRect();
585
920
  if (this.canvas) {
586
921
  this.canvas.width = window.innerWidth;
@@ -590,14 +925,26 @@ class DocugatePill {
590
925
  target.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
591
926
  this.wtCard?.remove();
592
927
  const card = document.createElement('div');
593
- card.className = 'wt-card';
928
+ card.className = 'wt-card surface';
594
929
  card.setAttribute('role', 'dialog');
595
930
  card.setAttribute('aria-modal', 'true');
596
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);
597
936
  const heading = document.createElement('h2');
598
937
  heading.className = 'wt-card-heading';
599
938
  heading.textContent = stop.heading;
600
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';
601
948
  const fields = [];
602
949
  if (stop.data)
603
950
  fields.push({ label: 'Data', value: stop.data });
@@ -607,8 +954,6 @@ class DocugatePill {
607
954
  if (stop.docs)
608
955
  fields.push({ label: 'Docs', value: stop.docs, isLink: true });
609
956
  for (const f of fields) {
610
- const row = document.createElement('div');
611
- row.className = 'wt-field';
612
957
  const lbl = document.createElement('span');
613
958
  lbl.className = 'wt-field-label';
614
959
  lbl.textContent = f.label;
@@ -619,28 +964,34 @@ class DocugatePill {
619
964
  a.href = f.value;
620
965
  a.target = '_blank';
621
966
  a.rel = 'noopener noreferrer';
622
- a.textContent = f.value;
967
+ a.textContent = breakable(f.value);
623
968
  val.appendChild(a);
624
969
  }
625
970
  else {
626
- val.textContent = f.value;
971
+ val.textContent = breakable(f.value);
627
972
  }
628
- row.appendChild(lbl);
629
- row.appendChild(val);
630
- card.appendChild(row);
631
- }
632
- if (stop.prose) {
633
- const prose = document.createElement('p');
634
- prose.className = 'wt-prose';
635
- prose.textContent = stop.prose;
636
- card.appendChild(prose);
973
+ grid.append(lbl, val);
637
974
  }
975
+ card.appendChild(grid);
638
976
  const nav = document.createElement('div');
639
977
  nav.className = 'wt-nav';
640
- const counter = document.createElement('span');
641
- counter.className = 'wt-counter';
642
- counter.textContent = `${this.wtIndex + 1} / ${this.wtStops.length}`;
643
- nav.appendChild(counter);
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
+ }
644
995
  if (this.wtIndex > 0) {
645
996
  const back = document.createElement('button');
646
997
  back.className = 'wt-btn';
@@ -657,6 +1008,7 @@ class DocugatePill {
657
1008
  card.appendChild(nav);
658
1009
  this.shadow.appendChild(card);
659
1010
  this.wtCard = card;
1011
+ next.focus();
660
1012
  // Position after appending (needs offsetHeight)
661
1013
  requestAnimationFrame(() => positionCard(card, rect));
662
1014
  }
@@ -692,6 +1044,7 @@ class DocugatePill {
692
1044
  }
693
1045
  stopWalkthrough() {
694
1046
  this.walkthroughActive = false;
1047
+ this.pillBtn?.classList.remove('away');
695
1048
  this.wtCard?.remove();
696
1049
  this.wtCard = null;
697
1050
  if (this.canvas) {
@@ -712,6 +1065,7 @@ class DocugatePill {
712
1065
  }
713
1066
  startInspector() {
714
1067
  this.inspectorActive = true;
1068
+ this.pillBtn?.classList.add('inspecting');
715
1069
  this.highlightEl = createHighlightEl();
716
1070
  document.body.appendChild(this.highlightEl);
717
1071
  document.addEventListener('mousemove', this.boundHandleMouseMove, true);
@@ -720,6 +1074,7 @@ class DocugatePill {
720
1074
  }
721
1075
  stopInspector() {
722
1076
  this.inspectorActive = false;
1077
+ this.pillBtn?.classList.remove('inspecting');
723
1078
  document.removeEventListener('mousemove', this.boundHandleMouseMove, true);
724
1079
  document.removeEventListener('click', this.boundHandleClick, true);
725
1080
  document.removeEventListener('keydown', this.boundHandleKey);
@@ -772,185 +1127,331 @@ class DocugatePill {
772
1127
  }
773
1128
  return null;
774
1129
  }
775
- showInspPopup(el, stop) {
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) {
776
1138
  this.closeInspPopup();
777
- const popup = document.createElement('div');
778
- popup.className = 'insp-popup';
779
- popup.setAttribute('role', 'dialog');
780
- popup.setAttribute('aria-label', stop.heading);
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}` : ''}`;
781
1159
  const heading = document.createElement('h3');
782
1160
  heading.className = 'insp-popup-heading';
783
1161
  heading.textContent = stop.heading;
784
- popup.appendChild(heading);
785
- const rows = [];
786
- if (stop.data)
787
- rows.push({ label: 'Data', value: stop.data });
788
- rows.push({ label: 'Code', value: stop.code });
789
- if (stop.source)
790
- rows.push({ label: 'Source', value: stop.source });
791
- if (stop.docs)
792
- rows.push({ label: 'Docs', value: stop.docs, isLink: true });
793
- for (const r of rows) {
794
- const row = document.createElement('div');
795
- row.className = 'insp-popup-row';
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) => {
796
1177
  const lbl = document.createElement('span');
797
1178
  lbl.className = 'insp-popup-label';
798
- lbl.textContent = r.label;
1179
+ lbl.textContent = label;
799
1180
  const val = document.createElement('span');
800
1181
  val.className = 'insp-popup-val';
801
- if (r.isLink && r.value.startsWith('http')) {
802
- const a = document.createElement('a');
803
- a.href = r.value;
804
- a.target = '_blank';
805
- a.rel = 'noopener noreferrer';
806
- a.textContent = r.value;
807
- val.appendChild(a);
808
- }
809
- else {
810
- val.textContent = r.value;
811
- }
812
- row.appendChild(lbl);
813
- row.appendChild(val);
814
- popup.appendChild(row);
815
- }
816
- if (stop.prose) {
817
- const prose = document.createElement('p');
818
- prose.className = 'insp-popup-prose';
819
- prose.textContent = stop.prose;
820
- popup.appendChild(prose);
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);
821
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);
822
1215
  if (!this.isStaticMode) {
823
- const actions = document.createElement('div');
824
- actions.className = 'insp-popup-actions';
825
- const editBtn = document.createElement('button');
826
- editBtn.className = 'wt-btn';
827
- editBtn.textContent = 'Edit';
828
- editBtn.addEventListener('click', () => {
829
- this.closeInspPopup();
830
- this.showAddForm(el, stop);
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);
831
1222
  });
832
- actions.appendChild(editBtn);
833
- const closeBtn = document.createElement('button');
834
- closeBtn.className = 'wt-btn primary';
835
- closeBtn.textContent = 'Close';
836
- closeBtn.addEventListener('click', () => this.closeInspPopup());
837
- actions.appendChild(closeBtn);
838
- popup.appendChild(actions);
1223
+ actions.appendChild(edit);
839
1224
  }
840
- else {
841
- const closeBtn = document.createElement('button');
842
- closeBtn.className = 'wt-btn primary';
843
- closeBtn.textContent = 'Close';
844
- closeBtn.addEventListener('click', () => this.closeInspPopup());
845
- const actions = document.createElement('div');
846
- actions.className = 'insp-popup-actions';
847
- actions.appendChild(closeBtn);
848
- popup.appendChild(actions);
849
- }
850
- // Position near element
851
- const rect = el.getBoundingClientRect();
852
- popup.style.top = `${Math.min(rect.bottom + 10, window.innerHeight - 200)}px`;
853
- popup.style.left = `${Math.min(rect.left, window.innerWidth - 296)}px`;
854
- this.shadow.appendChild(popup);
855
- this.inspPopup = popup;
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();
856
1232
  }
857
- closeInspPopup() {
858
- this.inspPopup?.remove();
859
- this.inspPopup = null;
860
- }
861
- // ── Add/Edit form ─────────────────────────────────────────────────────────
862
- showAddForm(el, existing) {
863
- this.closeAddForm();
1233
+ fillForm(panel, el, existing) {
1234
+ panel.replaceChildren();
1235
+ panel.setAttribute('aria-label', existing ? `Edit ${existing.heading}` : 'Add to tour');
864
1236
  const { selector, needsAttribute } = suggestSelector(el);
865
- const form = document.createElement('div');
866
- form.className = 'add-form';
867
- form.setAttribute('role', 'dialog');
868
- form.setAttribute('aria-modal', 'true');
869
- form.setAttribute('aria-label', existing ? 'Edit stop' : 'Add stop');
870
1237
  const title = document.createElement('h3');
871
- title.className = 'add-form-title';
872
- title.textContent = existing ? 'Edit stop' : 'Add to tour';
873
- form.appendChild(title);
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);
874
1246
  const fields = [
875
- { id: 'f-heading', label: 'Heading', value: existing?.heading ?? '' },
876
- { id: 'f-target', label: 'Target (selector)', value: existing?.target ?? selector },
877
- { id: 'f-data', label: 'Data (optional)', value: existing?.data ?? '' },
878
- { id: 'f-code', label: 'Code file', value: existing?.code ?? '' },
879
- { id: 'f-source', label: 'Source file (optional)', value: existing?.source ?? '' },
880
- { id: 'f-docs', label: 'Docs URL (optional)', value: existing?.docs ?? '' },
881
- { id: 'f-prose', label: 'Prose', value: existing?.prose ?? '', multi: true },
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 },
882
1254
  ];
883
1255
  const inputs = {};
1256
+ const form = document.createElement('form');
1257
+ form.className = 'panel-form';
884
1258
  for (const f of fields) {
885
- const lbl = document.createElement('label');
886
- lbl.setAttribute('for', f.id);
887
- lbl.textContent = f.label;
888
- form.appendChild(lbl);
889
- const input = f.multi
890
- ? document.createElement('textarea')
891
- : document.createElement('input');
892
- input.id = f.id;
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;
893
1265
  input.value = f.value;
894
- if (!f.multi)
895
- input.type = 'text';
896
- form.appendChild(input);
897
- inputs[f.id] = input;
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;
898
1272
  }
899
1273
  if (needsAttribute && !existing) {
900
1274
  const warn = document.createElement('div');
901
1275
  warn.className = 'add-form-warn';
902
- warn.innerHTML = `This element has no <code>data-tour</code> attribute yet. Add this attribute to identify it:<br><code>data-tour="${selector.replace(/^\[data-tour="/, '').replace(/"\]$/, '')}"</code>`;
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.');
903
1280
  form.appendChild(warn);
904
1281
  }
1282
+ const error = document.createElement('p');
1283
+ error.className = 'panel-error';
1284
+ error.hidden = true;
1285
+ form.appendChild(error);
905
1286
  const actions = document.createElement('div');
906
- actions.className = 'add-form-actions';
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);
907
1307
  const cancel = document.createElement('button');
1308
+ cancel.type = 'button';
908
1309
  cancel.className = 'wt-btn';
909
1310
  cancel.textContent = 'Cancel';
910
- cancel.addEventListener('click', () => this.closeAddForm());
911
- actions.appendChild(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
+ });
912
1320
  const save = document.createElement('button');
1321
+ save.type = 'submit';
913
1322
  save.className = 'wt-btn primary';
914
- save.textContent = 'Save';
915
- save.addEventListener('click', () => {
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();
916
1329
  const stop = {
917
- heading: inputs['f-heading'].value.trim(),
918
- target: inputs['f-target'].value.trim(),
919
- data: inputs['f-data'].value.trim() || undefined,
920
- code: inputs['f-code'].value.trim(),
921
- source: inputs['f-source'].value.trim() || undefined,
922
- docs: inputs['f-docs'].value.trim() || undefined,
923
- prose: inputs['f-prose'].value.trim(),
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'),
924
1337
  };
925
- if (!stop.heading || !stop.target || !stop.code)
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;
926
1350
  return;
927
- this.saveStop(stop);
928
- this.closeAddForm();
1351
+ }
1352
+ this.fillView(panel, el, stop);
1353
+ this.placeNear(panel, el);
929
1354
  });
930
- actions.appendChild(save);
931
- form.appendChild(actions);
932
- this.shadow.appendChild(form);
933
- this.addForm = form;
934
- inputs['f-heading'].focus();
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);
935
1378
  }
936
1379
  closeAddForm() {
937
- this.addForm?.remove();
938
- this.addForm = null;
1380
+ this.closeInspPopup();
939
1381
  }
940
- async saveStop(stop) {
1382
+ async saveStop(stop, previousTarget) {
941
1383
  // A page with no tour yet starts one at its own path.
942
1384
  const route = this.tour?.route ?? location.pathname;
943
1385
  try {
944
- await fetch(`${this.base}/stop`, {
1386
+ const res = await fetch(`${this.base}/stop`, {
945
1387
  method: 'PUT',
946
1388
  headers: { 'Content-Type': 'application/json' },
947
- body: JSON.stringify({ route, stop }),
1389
+ body: JSON.stringify({ route, stop, previousTarget }),
948
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;
949
1396
  }
950
1397
  catch {
951
- // best-effort
1398
+ return false;
952
1399
  }
953
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
+ }
954
1455
  // ── Keyboard ──────────────────────────────────────────────────────────────
955
1456
  handleKey(e) {
956
1457
  if (e.key === 'Escape') {
@@ -996,12 +1497,15 @@ class DocugatePill {
996
1497
  }
997
1498
  }
998
1499
  async reload() {
1500
+ const path = location.pathname;
999
1501
  await this.load();
1000
1502
  if (this.walkthroughActive)
1001
1503
  this.stopWalkthrough();
1002
- if (this.inspectorActive)
1003
- this.stopInspector();
1004
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;
1005
1509
  }
1006
1510
  // ── URL change detection ──────────────────────────────────────────────────
1007
1511
  patchHistory() {