@templatical/template-tools 0.42.0 → 0.42.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/bin.js +7 -4
- package/dist/bin.js.map +1 -1
- package/dist/live/index.d.ts +10 -2
- package/dist/live/index.js +90 -11
- package/dist/live/index.js.map +1 -1
- package/live/index.html +904 -119
- package/package.json +13 -13
package/live/index.html
CHANGED
|
@@ -92,6 +92,26 @@
|
|
|
92
92
|
--scrim: oklch(10% 0.01 60 / 0.6);
|
|
93
93
|
color-scheme: dark;
|
|
94
94
|
}
|
|
95
|
+
:root:not([data-theme="light"]) .btn.primary,
|
|
96
|
+
:root:not([data-theme="light"]) .btn.primary:hover {
|
|
97
|
+
background: var(--text);
|
|
98
|
+
color: var(--canvas);
|
|
99
|
+
}
|
|
100
|
+
:root:not([data-theme="light"]) .btn.primary:hover {
|
|
101
|
+
background: color-mix(in oklch, var(--text) 86%, var(--canvas));
|
|
102
|
+
}
|
|
103
|
+
:root:not([data-theme="light"]) #annotate-toggle[aria-pressed="true"],
|
|
104
|
+
:root:not([data-theme="light"])
|
|
105
|
+
#annotate-toggle[aria-pressed="true"]:hover {
|
|
106
|
+
color: var(--text);
|
|
107
|
+
}
|
|
108
|
+
:root:not([data-theme="light"]) #annotate-count,
|
|
109
|
+
:root:not([data-theme="light"])
|
|
110
|
+
#annotate-toggle[aria-pressed="true"]
|
|
111
|
+
#annotate-count {
|
|
112
|
+
background: var(--text);
|
|
113
|
+
color: var(--canvas);
|
|
114
|
+
}
|
|
95
115
|
}
|
|
96
116
|
:root[data-theme="dark"] {
|
|
97
117
|
--canvas: oklch(16% 0.008 60);
|
|
@@ -110,7 +130,25 @@
|
|
|
110
130
|
--scrim: oklch(10% 0.01 60 / 0.6);
|
|
111
131
|
color-scheme: dark;
|
|
112
132
|
}
|
|
113
|
-
|
|
133
|
+
:root[data-theme="dark"] .btn.primary,
|
|
134
|
+
:root[data-theme="dark"] .btn.primary:hover {
|
|
135
|
+
background: var(--text);
|
|
136
|
+
color: var(--canvas);
|
|
137
|
+
}
|
|
138
|
+
:root[data-theme="dark"] .btn.primary:hover {
|
|
139
|
+
background: color-mix(in oklch, var(--text) 86%, var(--canvas));
|
|
140
|
+
}
|
|
141
|
+
:root[data-theme="dark"] #annotate-toggle[aria-pressed="true"],
|
|
142
|
+
:root[data-theme="dark"] #annotate-toggle[aria-pressed="true"]:hover {
|
|
143
|
+
color: var(--text);
|
|
144
|
+
}
|
|
145
|
+
:root[data-theme="dark"] #annotate-count,
|
|
146
|
+
:root[data-theme="dark"]
|
|
147
|
+
#annotate-toggle[aria-pressed="true"]
|
|
148
|
+
#annotate-count {
|
|
149
|
+
background: var(--text);
|
|
150
|
+
color: var(--canvas);
|
|
151
|
+
}
|
|
114
152
|
* {
|
|
115
153
|
box-sizing: border-box;
|
|
116
154
|
}
|
|
@@ -139,17 +177,15 @@
|
|
|
139
177
|
align-items: center;
|
|
140
178
|
justify-content: space-between;
|
|
141
179
|
gap: 12px;
|
|
142
|
-
height:
|
|
180
|
+
height: 56px;
|
|
143
181
|
flex: none;
|
|
144
|
-
padding: 0
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
scrollbar-width: none;
|
|
149
|
-
z-index: 10;
|
|
182
|
+
padding: 0 16px;
|
|
183
|
+
position: relative;
|
|
184
|
+
background: var(--canvas);
|
|
185
|
+
z-index: 20;
|
|
150
186
|
}
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
#settings {
|
|
188
|
+
position: relative;
|
|
153
189
|
}
|
|
154
190
|
.bar .group {
|
|
155
191
|
display: flex;
|
|
@@ -182,6 +218,7 @@
|
|
|
182
218
|
}
|
|
183
219
|
|
|
184
220
|
.status {
|
|
221
|
+
position: relative;
|
|
185
222
|
display: inline-flex;
|
|
186
223
|
align-items: center;
|
|
187
224
|
gap: 7px;
|
|
@@ -194,6 +231,30 @@
|
|
|
194
231
|
color: var(--muted);
|
|
195
232
|
white-space: nowrap;
|
|
196
233
|
}
|
|
234
|
+
.status-tip {
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: calc(100% + 8px);
|
|
237
|
+
left: 0;
|
|
238
|
+
z-index: 2;
|
|
239
|
+
width: max-content;
|
|
240
|
+
max-width: 260px;
|
|
241
|
+
padding: 8px 10px;
|
|
242
|
+
border: 1px solid var(--border);
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
background: var(--surface);
|
|
245
|
+
color: var(--text);
|
|
246
|
+
font-size: 12px;
|
|
247
|
+
font-weight: 500;
|
|
248
|
+
line-height: 1.4;
|
|
249
|
+
white-space: normal;
|
|
250
|
+
box-shadow: 0 12px 32px oklch(18% 0.01 60 / 0.14);
|
|
251
|
+
opacity: 0;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
.status:hover .status-tip,
|
|
255
|
+
.status:focus-visible .status-tip {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
}
|
|
197
258
|
.status .dot {
|
|
198
259
|
width: 7px;
|
|
199
260
|
height: 7px;
|
|
@@ -289,24 +350,164 @@
|
|
|
289
350
|
justify-content: center;
|
|
290
351
|
}
|
|
291
352
|
.btn.primary {
|
|
292
|
-
background: var(--
|
|
353
|
+
background: var(--n-900);
|
|
293
354
|
border-color: transparent;
|
|
294
355
|
color: var(--paper);
|
|
295
356
|
}
|
|
296
357
|
.btn.primary:hover {
|
|
297
|
-
background: var(--
|
|
358
|
+
background: var(--n-700);
|
|
298
359
|
color: var(--paper);
|
|
299
360
|
}
|
|
361
|
+
/* Pressed Annotate is a tint, not a second amber fill. The editor
|
|
362
|
+
toolbar under this bar already uses amber for its own active state. */
|
|
363
|
+
#annotate-toggle[aria-pressed="true"] {
|
|
364
|
+
background: var(--primary-tint);
|
|
365
|
+
border-color: var(--primary);
|
|
366
|
+
color: var(--n-900);
|
|
367
|
+
}
|
|
368
|
+
#annotate-toggle[aria-pressed="true"]:hover {
|
|
369
|
+
background: var(--primary-tint);
|
|
370
|
+
border-color: var(--primary);
|
|
371
|
+
color: var(--n-900);
|
|
372
|
+
}
|
|
373
|
+
#annotate-count[hidden] {
|
|
374
|
+
display: none;
|
|
375
|
+
}
|
|
376
|
+
#annotate-count {
|
|
377
|
+
display: inline-flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
justify-content: center;
|
|
380
|
+
min-width: 16px;
|
|
381
|
+
height: 16px;
|
|
382
|
+
padding: 0 4px;
|
|
383
|
+
border-radius: 999px;
|
|
384
|
+
background: var(--n-900);
|
|
385
|
+
color: var(--paper);
|
|
386
|
+
font-size: 11px;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
line-height: 16px;
|
|
389
|
+
}
|
|
390
|
+
#annotate-toggle[aria-pressed="true"] #annotate-count {
|
|
391
|
+
background: var(--n-900);
|
|
392
|
+
color: var(--paper);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.settings-panel {
|
|
396
|
+
position: absolute;
|
|
397
|
+
top: calc(100% + 8px);
|
|
398
|
+
right: 0;
|
|
399
|
+
z-index: 1;
|
|
400
|
+
width: 240px;
|
|
401
|
+
padding: 12px;
|
|
402
|
+
border: 1px solid var(--border);
|
|
403
|
+
border-radius: 8px;
|
|
404
|
+
background: var(--surface);
|
|
405
|
+
color: var(--text);
|
|
406
|
+
box-shadow:
|
|
407
|
+
0 10px 15px -3px oklch(18% 0.01 60 / 0.12),
|
|
408
|
+
0 4px 6px -4px oklch(18% 0.01 60 / 0.08);
|
|
409
|
+
transform-origin: top right;
|
|
410
|
+
}
|
|
411
|
+
.settings-panel[hidden] {
|
|
412
|
+
display: none;
|
|
413
|
+
}
|
|
414
|
+
.settings-label {
|
|
415
|
+
display: block;
|
|
416
|
+
margin: 0 0 6px;
|
|
417
|
+
padding: 0;
|
|
418
|
+
font-size: 11px;
|
|
419
|
+
font-weight: 500;
|
|
420
|
+
letter-spacing: 0.04em;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
color: var(--muted);
|
|
423
|
+
}
|
|
424
|
+
.settings-label.spaced {
|
|
425
|
+
margin-top: 12px;
|
|
426
|
+
}
|
|
427
|
+
.theme-field {
|
|
428
|
+
margin: 0;
|
|
429
|
+
padding: 0;
|
|
430
|
+
border: 0;
|
|
431
|
+
}
|
|
432
|
+
.theme-options {
|
|
433
|
+
display: grid;
|
|
434
|
+
grid-template-columns: repeat(3, 1fr);
|
|
435
|
+
gap: 4px;
|
|
436
|
+
margin: 0;
|
|
437
|
+
padding: 4px;
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
background: var(--canvas);
|
|
440
|
+
}
|
|
441
|
+
.theme-options label {
|
|
442
|
+
position: relative;
|
|
443
|
+
display: flex;
|
|
444
|
+
align-items: center;
|
|
445
|
+
justify-content: center;
|
|
446
|
+
gap: 6px;
|
|
447
|
+
height: 32px;
|
|
448
|
+
border: 0;
|
|
449
|
+
border-radius: 4px;
|
|
450
|
+
background: transparent;
|
|
451
|
+
color: var(--muted);
|
|
452
|
+
font-size: 12px;
|
|
453
|
+
font-weight: 500;
|
|
454
|
+
cursor: pointer;
|
|
455
|
+
}
|
|
456
|
+
.theme-options label:hover {
|
|
457
|
+
color: var(--text);
|
|
458
|
+
}
|
|
459
|
+
.theme-options svg {
|
|
460
|
+
width: 14px;
|
|
461
|
+
height: 14px;
|
|
462
|
+
flex: none;
|
|
463
|
+
}
|
|
464
|
+
.theme-options input {
|
|
465
|
+
position: absolute;
|
|
466
|
+
opacity: 0;
|
|
467
|
+
pointer-events: none;
|
|
468
|
+
}
|
|
469
|
+
.theme-options label:has(input:checked) {
|
|
470
|
+
background: var(--surface);
|
|
471
|
+
color: var(--text);
|
|
472
|
+
box-shadow: 0 1px 2px oklch(18% 0.01 60 / 0.08);
|
|
473
|
+
}
|
|
474
|
+
#live-locale {
|
|
475
|
+
width: 100%;
|
|
476
|
+
height: 36px;
|
|
477
|
+
padding: 0 28px 0 10px;
|
|
478
|
+
border: 1px solid var(--border);
|
|
479
|
+
border-radius: 8px;
|
|
480
|
+
background-color: var(--surface);
|
|
481
|
+
color: var(--text);
|
|
482
|
+
font: inherit;
|
|
483
|
+
font-size: 13px;
|
|
484
|
+
appearance: none;
|
|
485
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2378706a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
486
|
+
background-repeat: no-repeat;
|
|
487
|
+
background-position: right 10px center;
|
|
488
|
+
}
|
|
489
|
+
#live-locale:focus-visible {
|
|
490
|
+
border-color: var(--primary);
|
|
491
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
492
|
+
}
|
|
300
493
|
:focus-visible {
|
|
301
494
|
outline: none;
|
|
302
495
|
box-shadow: 0 0 0 3px var(--ring);
|
|
303
496
|
}
|
|
304
497
|
|
|
305
498
|
/* ---- Editor + loading ------------------------------------------- */
|
|
499
|
+
/* The editor fills this card. The 15px well is the margin outside the border. */
|
|
306
500
|
.stage {
|
|
307
501
|
position: relative;
|
|
502
|
+
z-index: 0;
|
|
308
503
|
flex: 1;
|
|
309
504
|
min-height: 0;
|
|
505
|
+
margin: 0 15px 15px;
|
|
506
|
+
border: 1px solid var(--border);
|
|
507
|
+
border-radius: 8px;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
background: var(--surface);
|
|
510
|
+
box-shadow: 0 1px 2px oklch(18% 0.01 60 / 0.06);
|
|
310
511
|
}
|
|
311
512
|
#editor {
|
|
312
513
|
position: absolute;
|
|
@@ -321,7 +522,7 @@
|
|
|
321
522
|
align-items: center;
|
|
322
523
|
justify-content: center;
|
|
323
524
|
gap: 18px;
|
|
324
|
-
background: var(--
|
|
525
|
+
background: var(--surface);
|
|
325
526
|
z-index: 5;
|
|
326
527
|
transition: opacity 260ms var(--spring);
|
|
327
528
|
}
|
|
@@ -366,7 +567,7 @@
|
|
|
366
567
|
width: min(840px, 92vw);
|
|
367
568
|
max-height: 86vh;
|
|
368
569
|
border: 1px solid var(--border);
|
|
369
|
-
border-radius:
|
|
570
|
+
border-radius: 12px;
|
|
370
571
|
padding: 0;
|
|
371
572
|
background: var(--raised);
|
|
372
573
|
color: var(--text);
|
|
@@ -486,23 +687,22 @@
|
|
|
486
687
|
}
|
|
487
688
|
|
|
488
689
|
/* ---- Note panel ------------------------------------------------- */
|
|
489
|
-
/*
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
dialog. */
|
|
690
|
+
/* Opened from the Annotate toggle. The target id comes from
|
|
691
|
+
data-block-id on the click path. z-index is well above the editor's
|
|
692
|
+
popover layer (literal 10000) so a note stays reachable over any
|
|
693
|
+
editor dialog. */
|
|
494
694
|
#note-panel {
|
|
495
695
|
position: fixed;
|
|
496
696
|
right: 16px;
|
|
497
697
|
bottom: 16px;
|
|
498
698
|
z-index: 20000;
|
|
499
|
-
width:
|
|
500
|
-
padding:
|
|
699
|
+
width: 300px;
|
|
700
|
+
padding: 14px;
|
|
501
701
|
border: 1px solid var(--border);
|
|
502
702
|
border-radius: 8px;
|
|
503
703
|
background: var(--surface);
|
|
504
704
|
color: var(--text);
|
|
505
|
-
box-shadow: 0
|
|
705
|
+
box-shadow: 0 12px 32px oklch(18% 0.01 60 / 0.14);
|
|
506
706
|
font-size: 13px;
|
|
507
707
|
line-height: 1.4;
|
|
508
708
|
}
|
|
@@ -533,6 +733,54 @@
|
|
|
533
733
|
gap: 8px;
|
|
534
734
|
justify-content: flex-end;
|
|
535
735
|
}
|
|
736
|
+
#note-list {
|
|
737
|
+
list-style: none;
|
|
738
|
+
margin: 0 0 8px;
|
|
739
|
+
padding: 0;
|
|
740
|
+
max-height: 200px;
|
|
741
|
+
overflow-y: auto;
|
|
742
|
+
}
|
|
743
|
+
#note-list li {
|
|
744
|
+
display: grid;
|
|
745
|
+
grid-template-columns: auto 1fr auto;
|
|
746
|
+
gap: 6px;
|
|
747
|
+
align-items: start;
|
|
748
|
+
padding: 6px 0;
|
|
749
|
+
border-bottom: 1px solid var(--border);
|
|
750
|
+
}
|
|
751
|
+
#note-list button.note-row {
|
|
752
|
+
display: contents;
|
|
753
|
+
font: inherit;
|
|
754
|
+
color: inherit;
|
|
755
|
+
background: none;
|
|
756
|
+
border: 0;
|
|
757
|
+
text-align: left;
|
|
758
|
+
cursor: pointer;
|
|
759
|
+
}
|
|
760
|
+
#note-footer {
|
|
761
|
+
margin: 8px 0 0;
|
|
762
|
+
color: var(--muted);
|
|
763
|
+
font-size: 12px;
|
|
764
|
+
}
|
|
765
|
+
#note-badges {
|
|
766
|
+
position: fixed;
|
|
767
|
+
inset: 0;
|
|
768
|
+
z-index: 20;
|
|
769
|
+
pointer-events: none;
|
|
770
|
+
}
|
|
771
|
+
.note-badge {
|
|
772
|
+
position: fixed;
|
|
773
|
+
height: 18px;
|
|
774
|
+
min-width: 18px;
|
|
775
|
+
padding: 0 5px;
|
|
776
|
+
border: 0;
|
|
777
|
+
border-radius: 999px;
|
|
778
|
+
background: var(--primary);
|
|
779
|
+
color: var(--n-900);
|
|
780
|
+
font: 600 11px/18px Geist, ui-sans-serif, system-ui, sans-serif;
|
|
781
|
+
pointer-events: auto;
|
|
782
|
+
cursor: pointer;
|
|
783
|
+
}
|
|
536
784
|
|
|
537
785
|
@media (max-width: 720px) {
|
|
538
786
|
.btn .label,
|
|
@@ -577,12 +825,32 @@
|
|
|
577
825
|
id="status"
|
|
578
826
|
class="status"
|
|
579
827
|
data-state="connecting"
|
|
580
|
-
|
|
828
|
+
tabindex="0"
|
|
829
|
+
aria-describedby="status-tip"
|
|
581
830
|
>
|
|
582
831
|
<span class="dot"></span><span id="status-text">Connecting…</span>
|
|
832
|
+
<span id="status-tip" class="status-tip" role="tooltip">
|
|
833
|
+
Connecting to the live preview…
|
|
834
|
+
</span>
|
|
583
835
|
</span>
|
|
584
836
|
<span class="divider"></span>
|
|
585
|
-
<button
|
|
837
|
+
<button
|
|
838
|
+
id="annotate-toggle"
|
|
839
|
+
type="button"
|
|
840
|
+
class="btn"
|
|
841
|
+
aria-pressed="false"
|
|
842
|
+
title="Leave notes for your agent"
|
|
843
|
+
>
|
|
844
|
+
<span class="label">Annotate</span>
|
|
845
|
+
<span id="annotate-count" hidden></span>
|
|
846
|
+
</button>
|
|
847
|
+
<button
|
|
848
|
+
id="btn-export"
|
|
849
|
+
type="button"
|
|
850
|
+
class="btn icon-only"
|
|
851
|
+
title="Export the template"
|
|
852
|
+
aria-label="Export the template"
|
|
853
|
+
>
|
|
586
854
|
<svg
|
|
587
855
|
viewBox="0 0 24 24"
|
|
588
856
|
fill="none"
|
|
@@ -595,20 +863,108 @@
|
|
|
595
863
|
<polyline points="7 10 12 15 17 10" />
|
|
596
864
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
597
865
|
</svg>
|
|
598
|
-
<span class="label">Export</span>
|
|
599
866
|
</button>
|
|
600
867
|
<span class="divider"></span>
|
|
601
|
-
<
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
868
|
+
<div id="settings">
|
|
869
|
+
<button
|
|
870
|
+
id="btn-settings"
|
|
871
|
+
type="button"
|
|
872
|
+
class="btn icon-only"
|
|
873
|
+
title="Settings"
|
|
874
|
+
aria-label="Settings"
|
|
875
|
+
aria-haspopup="dialog"
|
|
876
|
+
aria-expanded="false"
|
|
877
|
+
aria-controls="settings-panel"
|
|
878
|
+
>
|
|
879
|
+
<svg
|
|
880
|
+
viewBox="0 0 24 24"
|
|
881
|
+
fill="none"
|
|
882
|
+
stroke="currentColor"
|
|
883
|
+
stroke-width="1.8"
|
|
884
|
+
stroke-linecap="round"
|
|
885
|
+
stroke-linejoin="round"
|
|
886
|
+
aria-hidden="true"
|
|
887
|
+
>
|
|
888
|
+
<path d="M20 7h-9" />
|
|
889
|
+
<path d="M14 17H5" />
|
|
890
|
+
<circle cx="17" cy="17" r="3" />
|
|
891
|
+
<circle cx="7" cy="7" r="3" />
|
|
892
|
+
</svg>
|
|
893
|
+
</button>
|
|
894
|
+
<div
|
|
895
|
+
id="settings-panel"
|
|
896
|
+
class="settings-panel"
|
|
897
|
+
role="dialog"
|
|
898
|
+
aria-label="Settings"
|
|
899
|
+
hidden
|
|
900
|
+
>
|
|
901
|
+
<fieldset class="theme-field">
|
|
902
|
+
<legend class="settings-label">Theme</legend>
|
|
903
|
+
<div class="theme-options">
|
|
904
|
+
<label>
|
|
905
|
+
<input type="radio" name="live-theme" value="auto" />
|
|
906
|
+
<svg
|
|
907
|
+
viewBox="0 0 24 24"
|
|
908
|
+
fill="none"
|
|
909
|
+
stroke="currentColor"
|
|
910
|
+
stroke-width="1.5"
|
|
911
|
+
stroke-linecap="round"
|
|
912
|
+
stroke-linejoin="round"
|
|
913
|
+
aria-hidden="true"
|
|
914
|
+
>
|
|
915
|
+
<rect width="20" height="14" x="2" y="3" rx="2" />
|
|
916
|
+
<path d="M8 21h8" />
|
|
917
|
+
<path d="M12 17v4" />
|
|
918
|
+
</svg>
|
|
919
|
+
System
|
|
920
|
+
</label>
|
|
921
|
+
<label>
|
|
922
|
+
<input type="radio" name="live-theme" value="light" />
|
|
923
|
+
<svg
|
|
924
|
+
viewBox="0 0 24 24"
|
|
925
|
+
fill="none"
|
|
926
|
+
stroke="currentColor"
|
|
927
|
+
stroke-width="1.5"
|
|
928
|
+
stroke-linecap="round"
|
|
929
|
+
stroke-linejoin="round"
|
|
930
|
+
aria-hidden="true"
|
|
931
|
+
>
|
|
932
|
+
<circle cx="12" cy="12" r="4" />
|
|
933
|
+
<path
|
|
934
|
+
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
|
935
|
+
/>
|
|
936
|
+
</svg>
|
|
937
|
+
Light
|
|
938
|
+
</label>
|
|
939
|
+
<label>
|
|
940
|
+
<input type="radio" name="live-theme" value="dark" />
|
|
941
|
+
<svg
|
|
942
|
+
viewBox="0 0 24 24"
|
|
943
|
+
fill="none"
|
|
944
|
+
stroke="currentColor"
|
|
945
|
+
stroke-width="1.5"
|
|
946
|
+
stroke-linecap="round"
|
|
947
|
+
stroke-linejoin="round"
|
|
948
|
+
aria-hidden="true"
|
|
949
|
+
>
|
|
950
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
951
|
+
</svg>
|
|
952
|
+
Dark
|
|
953
|
+
</label>
|
|
954
|
+
</div>
|
|
955
|
+
</fieldset>
|
|
956
|
+
<label class="settings-label spaced" for="live-locale">
|
|
957
|
+
Language
|
|
958
|
+
</label>
|
|
959
|
+
<select id="live-locale"></select>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
607
962
|
</div>
|
|
608
963
|
</header>
|
|
609
964
|
|
|
610
965
|
<div class="stage">
|
|
611
966
|
<div id="editor"></div>
|
|
967
|
+
<div id="note-badges"></div>
|
|
612
968
|
<div id="loading" class="loading">
|
|
613
969
|
<img src="https://templatical.com/logo.svg" alt="Templatical" />
|
|
614
970
|
<div class="spinner" aria-hidden="true"></div>
|
|
@@ -617,6 +973,7 @@
|
|
|
617
973
|
</div>
|
|
618
974
|
|
|
619
975
|
<div id="note-panel" hidden>
|
|
976
|
+
<ol id="note-list"></ol>
|
|
620
977
|
<div class="note-target">
|
|
621
978
|
Note on <strong id="note-target-label">the template</strong>
|
|
622
979
|
</div>
|
|
@@ -632,6 +989,7 @@
|
|
|
632
989
|
</button>
|
|
633
990
|
</div>
|
|
634
991
|
<p id="note-status" role="status"></p>
|
|
992
|
+
<p id="note-footer">Ask your agent to apply these.</p>
|
|
635
993
|
</div>
|
|
636
994
|
|
|
637
995
|
<dialog id="export">
|
|
@@ -658,7 +1016,10 @@
|
|
|
658
1016
|
<div id="toast" class="toast"></div>
|
|
659
1017
|
|
|
660
1018
|
<script type="module">
|
|
661
|
-
import {
|
|
1019
|
+
import {
|
|
1020
|
+
getSupportedLocales,
|
|
1021
|
+
init,
|
|
1022
|
+
} from "https://cdn.jsdelivr.net/npm/@templatical/editor@{{EDITOR_VERSION}}/dist/cdn/editor.js";
|
|
662
1023
|
|
|
663
1024
|
const $ = (id) => document.getElementById(id);
|
|
664
1025
|
const statusEl = $("status");
|
|
@@ -672,11 +1033,11 @@
|
|
|
672
1033
|
let applyingRemote = false;
|
|
673
1034
|
let debounceTimer = null;
|
|
674
1035
|
|
|
675
|
-
// ----
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1036
|
+
// ---- Settings: theme (auto / light / dark) and editor UI locale ----
|
|
1037
|
+
// Locale is read once at init. There is no setLocale, so a language
|
|
1038
|
+
// change remounts with the current template. The page's own labels stay
|
|
1039
|
+
// English. This does not write settings.locale — that is the email's
|
|
1040
|
+
// language, owned by Template Settings.
|
|
680
1041
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
681
1042
|
const readTheme = () => {
|
|
682
1043
|
try {
|
|
@@ -685,19 +1046,27 @@
|
|
|
685
1046
|
return "auto";
|
|
686
1047
|
}
|
|
687
1048
|
};
|
|
688
|
-
|
|
689
|
-
|
|
1049
|
+
function readLocale() {
|
|
1050
|
+
try {
|
|
1051
|
+
const stored = localStorage.getItem("tpl-live-locale");
|
|
1052
|
+
if (stored) return stored;
|
|
1053
|
+
} catch {
|
|
1054
|
+
/* private mode */
|
|
1055
|
+
}
|
|
1056
|
+
// The browser that opens this harness belongs to whoever is
|
|
1057
|
+
// hand-editing in it. Optional-chained: a runtime with no navigator
|
|
1058
|
+
// must not abort the mount. The editor falls back to English for a
|
|
1059
|
+
// tag it has no bundle for.
|
|
1060
|
+
return navigator?.language || undefined;
|
|
1061
|
+
}
|
|
690
1062
|
function applyTheme(stored) {
|
|
691
1063
|
if (stored === "auto")
|
|
692
1064
|
document.documentElement.removeAttribute("data-theme");
|
|
693
1065
|
else document.documentElement.setAttribute("data-theme", stored);
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
btn.innerHTML = eff === "dark" ? SUN : MOON;
|
|
697
|
-
btn.setAttribute(
|
|
698
|
-
"aria-label",
|
|
699
|
-
eff === "dark" ? "Switch to light theme" : "Switch to dark theme",
|
|
1066
|
+
const input = document.querySelector(
|
|
1067
|
+
`input[name="live-theme"][value="${stored}"]`,
|
|
700
1068
|
);
|
|
1069
|
+
if (input) input.checked = true;
|
|
701
1070
|
if (editor) {
|
|
702
1071
|
try {
|
|
703
1072
|
editor.setTheme(stored);
|
|
@@ -706,35 +1075,104 @@
|
|
|
706
1075
|
}
|
|
707
1076
|
}
|
|
708
1077
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1078
|
+
document.querySelectorAll('input[name="live-theme"]').forEach((input) => {
|
|
1079
|
+
input.addEventListener("change", () => {
|
|
1080
|
+
if (!input.checked) return;
|
|
1081
|
+
try {
|
|
1082
|
+
localStorage.setItem("tpl-live-theme", input.value);
|
|
1083
|
+
} catch {
|
|
1084
|
+
/* private mode */
|
|
1085
|
+
}
|
|
1086
|
+
applyTheme(input.value);
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
716
1089
|
prefersDark.addEventListener("change", () => {
|
|
717
1090
|
if (readTheme() === "auto") applyTheme("auto");
|
|
718
1091
|
});
|
|
719
1092
|
applyTheme(readTheme());
|
|
720
1093
|
|
|
1094
|
+
const settingsPanel = $("settings-panel");
|
|
1095
|
+
const settingsButton = $("btn-settings");
|
|
1096
|
+
function closeSettings() {
|
|
1097
|
+
settingsPanel.hidden = true;
|
|
1098
|
+
settingsButton.setAttribute("aria-expanded", "false");
|
|
1099
|
+
}
|
|
1100
|
+
function openSettings() {
|
|
1101
|
+
settingsPanel.hidden = false;
|
|
1102
|
+
settingsButton.setAttribute("aria-expanded", "true");
|
|
1103
|
+
}
|
|
1104
|
+
settingsButton.onclick = () => {
|
|
1105
|
+
if (settingsPanel.hidden) openSettings();
|
|
1106
|
+
else closeSettings();
|
|
1107
|
+
};
|
|
1108
|
+
document.addEventListener(
|
|
1109
|
+
"mousedown",
|
|
1110
|
+
(event) => {
|
|
1111
|
+
if (settingsPanel.hidden) return;
|
|
1112
|
+
if ($("settings").contains(event.target)) return;
|
|
1113
|
+
closeSettings();
|
|
1114
|
+
},
|
|
1115
|
+
true,
|
|
1116
|
+
);
|
|
1117
|
+
document.addEventListener(
|
|
1118
|
+
"keydown",
|
|
1119
|
+
(event) => {
|
|
1120
|
+
if (event.key !== "Escape" || settingsPanel.hidden) return;
|
|
1121
|
+
event.preventDefault();
|
|
1122
|
+
event.stopPropagation();
|
|
1123
|
+
closeSettings();
|
|
1124
|
+
settingsButton.focus();
|
|
1125
|
+
},
|
|
1126
|
+
true,
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
function localeLabel(code) {
|
|
1130
|
+
try {
|
|
1131
|
+
const name = new Intl.DisplayNames([code], { type: "language" }).of(
|
|
1132
|
+
code,
|
|
1133
|
+
);
|
|
1134
|
+
if (name && name.toLowerCase() !== code.toLowerCase()) {
|
|
1135
|
+
return name.charAt(0).toLocaleUpperCase(code) + name.slice(1);
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
/* a runtime without DisplayNames keeps the code */
|
|
1139
|
+
}
|
|
1140
|
+
return code;
|
|
1141
|
+
}
|
|
1142
|
+
const localeSelect = $("live-locale");
|
|
1143
|
+
const supportedLocales = getSupportedLocales()
|
|
1144
|
+
.slice()
|
|
1145
|
+
.sort((a, b) => localeLabel(a).localeCompare(localeLabel(b)));
|
|
1146
|
+
for (const code of supportedLocales) {
|
|
1147
|
+
const option = document.createElement("option");
|
|
1148
|
+
option.value = code;
|
|
1149
|
+
option.textContent = localeLabel(code);
|
|
1150
|
+
localeSelect.appendChild(option);
|
|
1151
|
+
}
|
|
1152
|
+
function localeChoice(tag) {
|
|
1153
|
+
if (tag && supportedLocales.includes(tag)) return tag;
|
|
1154
|
+
const base = tag ? tag.split("-")[0] : "";
|
|
1155
|
+
if (base && supportedLocales.includes(base)) return base;
|
|
1156
|
+
return supportedLocales.includes("en") ? "en" : supportedLocales[0];
|
|
1157
|
+
}
|
|
1158
|
+
localeSelect.value = localeChoice(readLocale());
|
|
1159
|
+
|
|
721
1160
|
// ---- Status + toast ----
|
|
722
|
-
// The status pill is
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
// that out (a native tooltip — the bar's overflow would clip a styled one).
|
|
1161
|
+
// The status pill is the connection to this preview server. "Live" means
|
|
1162
|
+
// the page can receive the agent's updates and report hand-edits back.
|
|
1163
|
+
// The green dot pulses only in that state.
|
|
726
1164
|
const STATUS_TITLE = {
|
|
727
1165
|
connecting: "Connecting to the live preview…",
|
|
728
|
-
live: "Connected
|
|
1166
|
+
live: "Connected. Edits here and the agent's changes both show up on this page.",
|
|
729
1167
|
reconnecting:
|
|
730
|
-
"Reconnecting to the live server
|
|
1168
|
+
"Reconnecting to the live server. Changes won't sync until it's back.",
|
|
731
1169
|
error:
|
|
732
|
-
"Disconnected
|
|
1170
|
+
"Disconnected. Changes won't sync until the connection is restored.",
|
|
733
1171
|
};
|
|
734
1172
|
function setStatus(state, text) {
|
|
735
1173
|
statusEl.dataset.state = state;
|
|
736
1174
|
statusText.textContent = text;
|
|
737
|
-
|
|
1175
|
+
$("status-tip").textContent = STATUS_TITLE[state] ?? "";
|
|
738
1176
|
}
|
|
739
1177
|
let toastTimer = null;
|
|
740
1178
|
function toast(msg) {
|
|
@@ -780,6 +1218,9 @@
|
|
|
780
1218
|
await new Promise((r) => setTimeout(r, 0)); // let Vue flush setContent
|
|
781
1219
|
await postContent(true);
|
|
782
1220
|
applyingRemote = false;
|
|
1221
|
+
// The reload may have consumed the note queue. The template frame does
|
|
1222
|
+
// not carry it, so re-read it or the badges stay until a manual refresh.
|
|
1223
|
+
await refreshNotes();
|
|
783
1224
|
toast("Updated from agent");
|
|
784
1225
|
}
|
|
785
1226
|
|
|
@@ -851,7 +1292,41 @@
|
|
|
851
1292
|
toast(ok ? "Copied to clipboard" : "Select the text to copy");
|
|
852
1293
|
};
|
|
853
1294
|
|
|
1295
|
+
localeSelect.onchange = async () => {
|
|
1296
|
+
const next = localeSelect.value;
|
|
1297
|
+
try {
|
|
1298
|
+
localStorage.setItem("tpl-live-locale", next);
|
|
1299
|
+
} catch {
|
|
1300
|
+
/* private mode */
|
|
1301
|
+
}
|
|
1302
|
+
const content = editor ? editor.getContent() : undefined;
|
|
1303
|
+
await mountEditor(content);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
854
1306
|
// ---- Boot ----
|
|
1307
|
+
async function mountEditor(content) {
|
|
1308
|
+
applyingRemote = true;
|
|
1309
|
+
try {
|
|
1310
|
+
if (editor) {
|
|
1311
|
+
editor.unmount();
|
|
1312
|
+
editor = null;
|
|
1313
|
+
}
|
|
1314
|
+
editor = await init({
|
|
1315
|
+
container: "#editor",
|
|
1316
|
+
locale: readLocale(),
|
|
1317
|
+
content, // undefined → editor starts from its default empty template
|
|
1318
|
+
uiTheme: readTheme(),
|
|
1319
|
+
onChange: () => {
|
|
1320
|
+
if (!applyingRemote) scheduleContentPost();
|
|
1321
|
+
},
|
|
1322
|
+
});
|
|
1323
|
+
await postContent(true); // seed the baseline with the normalized view
|
|
1324
|
+
await refreshNotes();
|
|
1325
|
+
} finally {
|
|
1326
|
+
applyingRemote = false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
855
1330
|
async function boot() {
|
|
856
1331
|
let content;
|
|
857
1332
|
try {
|
|
@@ -861,23 +1336,8 @@
|
|
|
861
1336
|
/* fall through to an empty editor */
|
|
862
1337
|
}
|
|
863
1338
|
|
|
864
|
-
|
|
865
|
-
container: "#editor",
|
|
866
|
-
// The browser that opens this harness belongs to whoever is
|
|
867
|
-
// hand-editing in it, so its language is the right one for the
|
|
868
|
-
// chrome. Optional-chained: a runtime that exposes no `navigator`
|
|
869
|
-
// must not abort the mount, and the editor falls back to English for
|
|
870
|
-
// any tag it has no bundle for.
|
|
871
|
-
locale: navigator?.language || undefined,
|
|
872
|
-
content, // undefined → editor starts from its default empty template
|
|
873
|
-
uiTheme: readTheme(),
|
|
874
|
-
onChange: () => {
|
|
875
|
-
if (!applyingRemote) scheduleContentPost();
|
|
876
|
-
},
|
|
877
|
-
});
|
|
878
|
-
|
|
1339
|
+
await mountEditor(content);
|
|
879
1340
|
document.body.classList.add("ready");
|
|
880
|
-
await postContent(true); // seed the baseline with the normalized view
|
|
881
1341
|
setStatus("live", "Live");
|
|
882
1342
|
|
|
883
1343
|
const es = new EventSource("/events");
|
|
@@ -901,49 +1361,360 @@
|
|
|
901
1361
|
});
|
|
902
1362
|
|
|
903
1363
|
// ---- Notes ----
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
// (not event.target) is required
|
|
909
|
-
// DOM and clicks retarget at the boundary.
|
|
1364
|
+
// Annotate mode takes canvas clicks. BlockWrapper.handleClick calls
|
|
1365
|
+
// stopPropagation() on bubble, so the listeners stay on the capture phase.
|
|
1366
|
+
// mousedown is stopped too: that is the event that focuses contenteditable.
|
|
1367
|
+
// The editor's header, sidebars, and properties panel are outside .tpl-canvas
|
|
1368
|
+
// and still receive clicks. composedPath (not event.target) is required
|
|
1369
|
+
// because the editor mounts in shadow DOM and clicks retarget at the boundary.
|
|
910
1370
|
const notePanel = $("note-panel");
|
|
911
1371
|
const noteText = $("note-text");
|
|
912
1372
|
const noteLabel = $("note-target-label");
|
|
913
1373
|
const noteStatus = $("note-status");
|
|
914
|
-
|
|
1374
|
+
const annotateToggle = $("annotate-toggle");
|
|
1375
|
+
let annotating = false;
|
|
1376
|
+
let panelDismissed = false;
|
|
1377
|
+
let draftTarget = null;
|
|
1378
|
+
let notes = [];
|
|
1379
|
+
let editingId = null;
|
|
1380
|
+
|
|
1381
|
+
function canvasRoot() {
|
|
1382
|
+
const host = document.querySelector("#editor");
|
|
1383
|
+
return host && host.shadowRoot;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function blockEl(id) {
|
|
1387
|
+
const root = canvasRoot();
|
|
1388
|
+
if (!root || !id) return null;
|
|
1389
|
+
return root.querySelector(
|
|
1390
|
+
".tpl-canvas [data-block-id='" + CSS.escape(id) + "']",
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function labelFromElement(el) {
|
|
1395
|
+
return (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
1396
|
+
}
|
|
915
1397
|
|
|
916
|
-
function
|
|
1398
|
+
function blockFromPath(event) {
|
|
1399
|
+
let blockId = null;
|
|
1400
|
+
let parentBlockId = null;
|
|
1401
|
+
let blockType = null;
|
|
1402
|
+
let label = "";
|
|
917
1403
|
for (const el of event.composedPath()) {
|
|
918
|
-
|
|
919
|
-
if (
|
|
1404
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
1405
|
+
if (!el.closest(".tpl-canvas")) continue;
|
|
1406
|
+
const id = el.dataset.blockId;
|
|
1407
|
+
if (!id) continue;
|
|
1408
|
+
if (!blockId) {
|
|
1409
|
+
blockId = id;
|
|
1410
|
+
blockType = el.dataset.blockType || null;
|
|
1411
|
+
label = labelFromElement(el);
|
|
1412
|
+
} else {
|
|
1413
|
+
parentBlockId = id;
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
920
1416
|
}
|
|
921
|
-
return null;
|
|
1417
|
+
if (!blockId) return null;
|
|
1418
|
+
return { blockId, parentBlockId, blockType, label };
|
|
922
1419
|
}
|
|
923
1420
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
(
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1421
|
+
function pathHitsCanvas(event) {
|
|
1422
|
+
return event.composedPath().some(
|
|
1423
|
+
(el) => el instanceof Element && el.closest(".tpl-canvas"),
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function syncChrome() {
|
|
1428
|
+
annotateToggle.setAttribute("aria-pressed", String(annotating));
|
|
1429
|
+
const count = $("annotate-count");
|
|
1430
|
+
count.hidden = notes.length === 0;
|
|
1431
|
+
count.textContent = String(notes.length);
|
|
1432
|
+
notePanel.hidden =
|
|
1433
|
+
panelDismissed || (!annotating && notes.length === 0);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function setAnnotating(next) {
|
|
1437
|
+
annotating = next;
|
|
1438
|
+
if (next) panelDismissed = false;
|
|
1439
|
+
if (!next) {
|
|
1440
|
+
editingId = null;
|
|
1441
|
+
draftTarget = null;
|
|
1442
|
+
noteText.value = "";
|
|
937
1443
|
noteStatus.textContent = "";
|
|
1444
|
+
$("note-save").textContent = "Leave note";
|
|
1445
|
+
}
|
|
1446
|
+
syncChrome();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
annotateToggle.onclick = () => setAnnotating(!annotating);
|
|
1450
|
+
|
|
1451
|
+
function showComposer(target, id) {
|
|
1452
|
+
panelDismissed = false;
|
|
1453
|
+
editingId = id;
|
|
1454
|
+
const note = id ? notes.find((item) => item.id === id) : null;
|
|
1455
|
+
if (note) {
|
|
1456
|
+
noteLabel.textContent = note.blockId
|
|
1457
|
+
? (note.blockType || "Block") + (note.label ? " · " + note.label : "")
|
|
1458
|
+
: "the template";
|
|
1459
|
+
noteText.value = note.text;
|
|
1460
|
+
} else if (target) {
|
|
1461
|
+
noteLabel.textContent =
|
|
1462
|
+
(target.blockType || "Block") + (target.label ? " · " + target.label : "");
|
|
938
1463
|
noteText.value = "";
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1464
|
+
} else {
|
|
1465
|
+
noteLabel.textContent = "the template";
|
|
1466
|
+
noteText.value = "";
|
|
1467
|
+
}
|
|
1468
|
+
$("note-save").textContent = id ? "Save" : "Leave note";
|
|
1469
|
+
noteStatus.textContent = "";
|
|
1470
|
+
notePanel.hidden = false;
|
|
1471
|
+
noteText.focus();
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function renderNotes() {
|
|
1475
|
+
const list = $("note-list");
|
|
1476
|
+
list.replaceChildren();
|
|
1477
|
+
notes.forEach((note, index) => {
|
|
1478
|
+
const li = document.createElement("li");
|
|
1479
|
+
const row = document.createElement("button");
|
|
1480
|
+
row.type = "button";
|
|
1481
|
+
row.className = "note-row";
|
|
1482
|
+
const num = document.createElement("span");
|
|
1483
|
+
num.textContent = String(index + 1);
|
|
1484
|
+
const body = document.createElement("span");
|
|
1485
|
+
const title = note.blockId
|
|
1486
|
+
? (note.blockType || "Block") + (note.label ? " · " + note.label : "")
|
|
1487
|
+
: "Whole template";
|
|
1488
|
+
const gone =
|
|
1489
|
+
note.blockId && !blockEl(note.blockId)
|
|
1490
|
+
? " This block is no longer on the canvas."
|
|
1491
|
+
: "";
|
|
1492
|
+
body.textContent = title + " — " + note.text + gone;
|
|
1493
|
+
row.append(num, body);
|
|
1494
|
+
row.onclick = () => showComposer(null, note.id);
|
|
1495
|
+
const remove = document.createElement("button");
|
|
1496
|
+
remove.type = "button";
|
|
1497
|
+
remove.className = "btn";
|
|
1498
|
+
remove.textContent = "Delete";
|
|
1499
|
+
remove.onclick = async () => {
|
|
1500
|
+
const res = await fetch(
|
|
1501
|
+
"/annotations/" + encodeURIComponent(note.id),
|
|
1502
|
+
{ method: "DELETE" },
|
|
1503
|
+
);
|
|
1504
|
+
if (res.status === 204) {
|
|
1505
|
+
if (editingId === note.id) editingId = null;
|
|
1506
|
+
await refreshNotes();
|
|
1507
|
+
} else {
|
|
1508
|
+
noteStatus.textContent = "Could not delete that note.";
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
li.append(row, remove);
|
|
1512
|
+
list.appendChild(li);
|
|
1513
|
+
});
|
|
1514
|
+
syncChrome();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function syncOutlines() {
|
|
1518
|
+
const host = document.querySelector("#editor");
|
|
1519
|
+
if (!host || !host.shadowRoot) return;
|
|
1520
|
+
let style = host.shadowRoot.querySelector("#note-outlines");
|
|
1521
|
+
if (!style) {
|
|
1522
|
+
style = document.createElement("style");
|
|
1523
|
+
style.id = "note-outlines";
|
|
1524
|
+
host.shadowRoot.appendChild(style);
|
|
1525
|
+
}
|
|
1526
|
+
const ids = [
|
|
1527
|
+
...new Set(notes.map((note) => note.blockId).filter((id) => id)),
|
|
1528
|
+
];
|
|
1529
|
+
style.textContent = ids
|
|
1530
|
+
.map(
|
|
1531
|
+
(id) =>
|
|
1532
|
+
`.tpl-canvas [data-block-id="${CSS.escape(id)}"] { outline: 2px solid var(--tpl-primary); outline-offset: 2px; }`,
|
|
1533
|
+
)
|
|
1534
|
+
.join("\n");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const NUDGE_GAP = 18;
|
|
1538
|
+
const NUDGE_DROP = 22;
|
|
1539
|
+
let badgeFrame = 0;
|
|
1540
|
+
let badgeScroller = null;
|
|
1541
|
+
let badgeObserver = null;
|
|
1542
|
+
|
|
1543
|
+
function schedulePlace() {
|
|
1544
|
+
if (notes.length === 0) return;
|
|
1545
|
+
if (badgeFrame) return;
|
|
1546
|
+
badgeFrame = requestAnimationFrame(() => {
|
|
1547
|
+
badgeFrame = 0;
|
|
1548
|
+
placeBadges();
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function bindBadgeTracking() {
|
|
1553
|
+
if (notes.length === 0) {
|
|
1554
|
+
if (badgeScroller) {
|
|
1555
|
+
badgeScroller.removeEventListener("scroll", schedulePlace);
|
|
1556
|
+
}
|
|
1557
|
+
badgeScroller = null;
|
|
1558
|
+
if (badgeObserver) badgeObserver.disconnect();
|
|
1559
|
+
badgeObserver = null;
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const root = canvasRoot();
|
|
1563
|
+
const scroller = root && root.querySelector(".tpl-body");
|
|
1564
|
+
if (scroller && scroller !== badgeScroller) {
|
|
1565
|
+
if (badgeScroller) {
|
|
1566
|
+
badgeScroller.removeEventListener("scroll", schedulePlace);
|
|
1567
|
+
}
|
|
1568
|
+
badgeScroller = scroller;
|
|
1569
|
+
badgeScroller.addEventListener("scroll", schedulePlace, {
|
|
1570
|
+
passive: true,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
const host = document.querySelector("#editor");
|
|
1574
|
+
if (host && !badgeObserver) {
|
|
1575
|
+
badgeObserver = new ResizeObserver(() => schedulePlace());
|
|
1576
|
+
badgeObserver.observe(host);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function placeBadges() {
|
|
1581
|
+
const layer = $("note-badges");
|
|
1582
|
+
layer.replaceChildren();
|
|
1583
|
+
if (notes.length === 0) return;
|
|
1584
|
+
const host = document.querySelector("#editor");
|
|
1585
|
+
if (!host) return;
|
|
1586
|
+
const hostBox = host.getBoundingClientRect();
|
|
1587
|
+
const root = canvasRoot();
|
|
1588
|
+
const canvas = root && root.querySelector(".tpl-canvas");
|
|
1589
|
+
const rtl = !!(canvas && canvas.getAttribute("dir") === "rtl");
|
|
1590
|
+
const groups = new Map();
|
|
1591
|
+
notes.forEach((note, index) => {
|
|
1592
|
+
if (!note.blockId) return;
|
|
1593
|
+
const existing = groups.get(note.blockId);
|
|
1594
|
+
if (existing) existing.extra += 1;
|
|
1595
|
+
else groups.set(note.blockId, { note, number: index + 1, extra: 0 });
|
|
1596
|
+
});
|
|
1597
|
+
// Nudge against anchors already placed, so a section noted before its
|
|
1598
|
+
// child is the anchor that wins the shared corner.
|
|
1599
|
+
const placed = [];
|
|
1600
|
+
for (const group of groups.values()) {
|
|
1601
|
+
const el = blockEl(group.note.blockId);
|
|
1602
|
+
if (!el) continue;
|
|
1603
|
+
const rect = el.getBoundingClientRect();
|
|
1604
|
+
const visible =
|
|
1605
|
+
rect.width > 0 &&
|
|
1606
|
+
rect.height > 0 &&
|
|
1607
|
+
rect.bottom > hostBox.top &&
|
|
1608
|
+
rect.top < hostBox.bottom &&
|
|
1609
|
+
rect.right > hostBox.left &&
|
|
1610
|
+
rect.left < hostBox.right;
|
|
1611
|
+
if (!visible) continue;
|
|
1612
|
+
let x = rtl ? rect.left : rect.right;
|
|
1613
|
+
let y = rect.top;
|
|
1614
|
+
for (const other of placed) {
|
|
1615
|
+
if (Math.hypot(other.x - x, other.y - y) < NUDGE_GAP) y += NUDGE_DROP;
|
|
1616
|
+
}
|
|
1617
|
+
placed.push({ x, y });
|
|
1618
|
+
const btn = document.createElement("button");
|
|
1619
|
+
btn.type = "button";
|
|
1620
|
+
btn.className = "note-badge";
|
|
1621
|
+
btn.textContent = group.extra
|
|
1622
|
+
? group.number + "+" + group.extra
|
|
1623
|
+
: String(group.number);
|
|
1624
|
+
btn.setAttribute(
|
|
1625
|
+
"aria-label",
|
|
1626
|
+
group.extra
|
|
1627
|
+
? group.extra +
|
|
1628
|
+
1 +
|
|
1629
|
+
" notes on " +
|
|
1630
|
+
(group.note.label || group.note.blockType || "block")
|
|
1631
|
+
: "Note " +
|
|
1632
|
+
group.number +
|
|
1633
|
+
" on " +
|
|
1634
|
+
(group.note.label || group.note.blockType || "block"),
|
|
1635
|
+
);
|
|
1636
|
+
btn.style.left = x + "px";
|
|
1637
|
+
btn.style.top = y + "px";
|
|
1638
|
+
btn.style.transform = rtl
|
|
1639
|
+
? "translate(calc(-100% - 2px), -100%)"
|
|
1640
|
+
: "translate(2px, -100%)";
|
|
1641
|
+
btn.addEventListener("click", (event) => {
|
|
1642
|
+
event.stopPropagation();
|
|
1643
|
+
showComposer(null, group.note.id);
|
|
1644
|
+
});
|
|
1645
|
+
layer.appendChild(btn);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async function refreshNotes() {
|
|
1650
|
+
const res = await fetch("/content");
|
|
1651
|
+
if (!res.ok) return;
|
|
1652
|
+
const body = await res.json();
|
|
1653
|
+
notes = Array.isArray(body.annotations) ? body.annotations : [];
|
|
1654
|
+
if (editingId && !notes.some((note) => note.id === editingId)) {
|
|
1655
|
+
editingId = null;
|
|
1656
|
+
draftTarget = null;
|
|
1657
|
+
noteText.value = "";
|
|
1658
|
+
noteStatus.textContent = "";
|
|
1659
|
+
noteLabel.textContent = "the template";
|
|
1660
|
+
$("note-save").textContent = "Leave note";
|
|
1661
|
+
}
|
|
1662
|
+
renderNotes();
|
|
1663
|
+
syncOutlines();
|
|
1664
|
+
bindBadgeTracking();
|
|
1665
|
+
placeBadges();
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function onAnnotatePointer(event) {
|
|
1669
|
+
if (!annotating) return;
|
|
1670
|
+
if (event.target instanceof Element && event.target.closest("#note-badges")) {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (notePanel.contains(event.target)) return;
|
|
1674
|
+
if (!pathHitsCanvas(event)) return;
|
|
1675
|
+
event.preventDefault();
|
|
1676
|
+
event.stopPropagation();
|
|
1677
|
+
if (event.type !== "click") return;
|
|
1678
|
+
draftTarget = blockFromPath(event);
|
|
1679
|
+
showComposer(draftTarget, null);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
document.addEventListener(
|
|
1683
|
+
"mousedown",
|
|
1684
|
+
onAnnotatePointer,
|
|
942
1685
|
true,
|
|
943
1686
|
);
|
|
944
1687
|
|
|
1688
|
+
document.addEventListener(
|
|
1689
|
+
"click",
|
|
1690
|
+
onAnnotatePointer,
|
|
1691
|
+
true,
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
document.addEventListener("keydown", (event) => {
|
|
1695
|
+
if (event.key !== "Escape" || !annotating) return;
|
|
1696
|
+
event.preventDefault();
|
|
1697
|
+
event.stopPropagation();
|
|
1698
|
+
setAnnotating(false);
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
function clearComposer() {
|
|
1702
|
+
editingId = null;
|
|
1703
|
+
draftTarget = null;
|
|
1704
|
+
noteText.value = "";
|
|
1705
|
+
noteStatus.textContent = "";
|
|
1706
|
+
noteLabel.textContent = "the template";
|
|
1707
|
+
$("note-save").textContent = "Leave note";
|
|
1708
|
+
}
|
|
1709
|
+
|
|
945
1710
|
$("note-cancel").onclick = () => {
|
|
946
|
-
|
|
1711
|
+
const pending = noteText.value.trim() || editingId;
|
|
1712
|
+
if (pending) {
|
|
1713
|
+
clearComposer();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
panelDismissed = true;
|
|
1717
|
+
setAnnotating(false);
|
|
947
1718
|
};
|
|
948
1719
|
|
|
949
1720
|
$("note-save").onclick = async () => {
|
|
@@ -953,26 +1724,40 @@
|
|
|
953
1724
|
return;
|
|
954
1725
|
}
|
|
955
1726
|
try {
|
|
956
|
-
const res =
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1727
|
+
const res = editingId
|
|
1728
|
+
? await fetch("/annotations/" + encodeURIComponent(editingId), {
|
|
1729
|
+
method: "PUT",
|
|
1730
|
+
headers: { "content-type": "application/json" },
|
|
1731
|
+
body: JSON.stringify({ text }),
|
|
1732
|
+
})
|
|
1733
|
+
: await fetch("/annotations", {
|
|
1734
|
+
method: "POST",
|
|
1735
|
+
headers: { "content-type": "application/json" },
|
|
1736
|
+
body: JSON.stringify({
|
|
1737
|
+
text,
|
|
1738
|
+
blockId: (draftTarget && draftTarget.blockId) || null,
|
|
1739
|
+
parentBlockId: (draftTarget && draftTarget.parentBlockId) || null,
|
|
1740
|
+
blockType: (draftTarget && draftTarget.blockType) || null,
|
|
1741
|
+
label: (draftTarget && draftTarget.label) || "",
|
|
1742
|
+
}),
|
|
1743
|
+
});
|
|
1744
|
+
if (!res.ok) {
|
|
1745
|
+
noteStatus.textContent =
|
|
1746
|
+
(await res.text()) || "Could not save that note.";
|
|
1747
|
+
return;
|
|
971
1748
|
}
|
|
1749
|
+
editingId = null;
|
|
1750
|
+
draftTarget = null;
|
|
1751
|
+
noteText.value = "";
|
|
1752
|
+
$("note-save").textContent = "Leave note";
|
|
1753
|
+
noteStatus.textContent = "";
|
|
1754
|
+
toast("Saved. Ask your agent to apply your notes.");
|
|
1755
|
+
await refreshNotes();
|
|
972
1756
|
} catch {
|
|
973
1757
|
noteStatus.textContent = "Could not reach the bridge.";
|
|
974
1758
|
}
|
|
975
1759
|
};
|
|
1760
|
+
|
|
976
1761
|
</script>
|
|
977
1762
|
</body>
|
|
978
1763
|
</html>
|