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/dist/tour-server.js +114 -5
- package/package.json +1 -1
- package/pill/dist/pill.js +926 -422
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
|
-
/*
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
z-index:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
.
|
|
217
|
-
display: flex;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.wt-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
border-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
border-radius: 8px;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
.insp-popup-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
.add-form
|
|
309
|
-
.add-form
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
border
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
.
|
|
322
|
-
.
|
|
323
|
-
.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 = '#
|
|
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 =
|
|
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 #
|
|
489
|
+
'border:2px solid #F4C43F',
|
|
432
490
|
'border-radius:4px',
|
|
433
|
-
'background:rgba(
|
|
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 =
|
|
503
|
-
btn.setAttribute('aria-
|
|
504
|
-
btn.
|
|
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', '
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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 =
|
|
1179
|
+
lbl.textContent = label;
|
|
799
1180
|
const val = document.createElement('span');
|
|
800
1181
|
val.className = 'insp-popup-val';
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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(
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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 = '
|
|
872
|
-
title.textContent = existing ? 'Edit stop' : 'Add to tour';
|
|
873
|
-
|
|
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
|
-
{
|
|
876
|
-
{
|
|
877
|
-
{
|
|
878
|
-
{
|
|
879
|
-
{
|
|
880
|
-
{
|
|
881
|
-
{
|
|
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
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const input = f.multi
|
|
890
|
-
|
|
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 (
|
|
895
|
-
input.
|
|
896
|
-
|
|
897
|
-
|
|
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.
|
|
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 = '
|
|
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', () =>
|
|
911
|
-
|
|
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
|
-
|
|
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:
|
|
918
|
-
target:
|
|
919
|
-
data:
|
|
920
|
-
code:
|
|
921
|
-
source:
|
|
922
|
-
docs:
|
|
923
|
-
prose:
|
|
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
|
-
|
|
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
|
-
|
|
928
|
-
this.
|
|
1351
|
+
}
|
|
1352
|
+
this.fillView(panel, el, stop);
|
|
1353
|
+
this.placeNear(panel, el);
|
|
929
1354
|
});
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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.
|
|
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
|
-
|
|
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() {
|