@templatical/template-tools 0.38.0
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/LICENSE +109 -0
- package/README.md +48 -0
- package/dist/bin.d.ts +5 -0
- package/dist/bin.js +698 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +2 -0
- package/dist/live/index.d.ts +87 -0
- package/dist/live/index.js +279 -0
- package/dist/live/index.js.map +1 -0
- package/dist/src-unNYVMlC.js +1278 -0
- package/dist/src-unNYVMlC.js.map +1 -0
- package/live/index.html +978 -0
- package/package.json +107 -0
- package/schema.json +1504 -0
package/live/index.html
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Templatical — Live preview</title>
|
|
7
|
+
<link rel="icon" href="https://templatical.com/logo-small.svg" />
|
|
8
|
+
<!-- Geist, matching the playground (fonts.bunny.net). -->
|
|
9
|
+
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
|
|
10
|
+
<link
|
|
11
|
+
href="https://fonts.bunny.net/css?family=geist:400,500,600&display=swap"
|
|
12
|
+
rel="stylesheet"
|
|
13
|
+
/>
|
|
14
|
+
<!--
|
|
15
|
+
The real Templatical editor, loaded from the CDN. {{EDITOR_VERSION}} is
|
|
16
|
+
injected by src/live/index.ts (pinned to this package's schema version).
|
|
17
|
+
In shadow-DOM mode the editor adopts its own CSS; the stylesheet link is
|
|
18
|
+
kept for parity with the documented consumer setup.
|
|
19
|
+
-->
|
|
20
|
+
<link
|
|
21
|
+
rel="stylesheet"
|
|
22
|
+
href="https://cdn.jsdelivr.net/npm/@templatical/editor@{{EDITOR_VERSION}}/dist/cdn/editor.css"
|
|
23
|
+
/>
|
|
24
|
+
<!-- Set the stored theme before paint to avoid a flash. -->
|
|
25
|
+
<script>
|
|
26
|
+
(function () {
|
|
27
|
+
try {
|
|
28
|
+
var t = localStorage.getItem("tpl-live-theme");
|
|
29
|
+
if (t === "light" || t === "dark")
|
|
30
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
})();
|
|
33
|
+
</script>
|
|
34
|
+
<style>
|
|
35
|
+
/*
|
|
36
|
+
Design language borrowed from the playground + the SDK design system
|
|
37
|
+
(DESIGN.md): warm neutrals tinted toward hue 60, Signal Amber as the one
|
|
38
|
+
signal color, Geist, and the 120ms house spring on every transition.
|
|
39
|
+
*/
|
|
40
|
+
:root {
|
|
41
|
+
--spring: cubic-bezier(0.16, 1, 0.3, 1);
|
|
42
|
+
|
|
43
|
+
--paper: oklch(99.5% 0.002 60);
|
|
44
|
+
--n-50: oklch(97% 0.004 60);
|
|
45
|
+
--n-100: oklch(96% 0.006 60);
|
|
46
|
+
--n-200: oklch(92% 0.006 60);
|
|
47
|
+
--n-300: oklch(86% 0.01 60);
|
|
48
|
+
--n-400: oklch(68% 0.012 60);
|
|
49
|
+
--n-500: oklch(50% 0.015 60);
|
|
50
|
+
--n-700: oklch(30% 0.012 60);
|
|
51
|
+
--n-900: oklch(18% 0.01 60);
|
|
52
|
+
|
|
53
|
+
--primary: oklch(70% 0.16 55);
|
|
54
|
+
--primary-hover: oklch(63% 0.17 55);
|
|
55
|
+
--primary-tint: oklch(95% 0.04 55);
|
|
56
|
+
--ring: oklch(70% 0.16 55 / 0.32);
|
|
57
|
+
|
|
58
|
+
--success: oklch(62.8% 0.194 155.1);
|
|
59
|
+
--warning: oklch(76.9% 0.168 70.08);
|
|
60
|
+
--danger: oklch(63.7% 0.237 25.33);
|
|
61
|
+
|
|
62
|
+
--canvas: var(--n-100);
|
|
63
|
+
--bar: color-mix(in oklch, var(--paper) 82%, var(--n-100));
|
|
64
|
+
--surface: var(--paper);
|
|
65
|
+
--raised: var(--paper);
|
|
66
|
+
--border: var(--n-200);
|
|
67
|
+
--text: var(--n-900);
|
|
68
|
+
--muted: var(--n-500);
|
|
69
|
+
--hover: var(--n-100);
|
|
70
|
+
--shadow-modal: 0 24px 64px oklch(18% 0.01 60 / 0.18);
|
|
71
|
+
--scrim: oklch(24% 0.02 60 / 0.5);
|
|
72
|
+
color-scheme: light;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Dark tokens — applied on system-dark (unless explicitly overridden to
|
|
76
|
+
light) and whenever the toggle sets data-theme="dark". */
|
|
77
|
+
@media (prefers-color-scheme: dark) {
|
|
78
|
+
:root:not([data-theme="light"]) {
|
|
79
|
+
--canvas: oklch(16% 0.008 60);
|
|
80
|
+
--bar: oklch(22% 0.011 60);
|
|
81
|
+
--surface: oklch(24% 0.012 60);
|
|
82
|
+
--raised: oklch(27% 0.013 60);
|
|
83
|
+
--border: oklch(32% 0.012 60);
|
|
84
|
+
--text: oklch(96% 0.006 60);
|
|
85
|
+
--muted: oklch(70% 0.012 60);
|
|
86
|
+
--hover: oklch(30% 0.013 60);
|
|
87
|
+
--primary: oklch(78% 0.14 55);
|
|
88
|
+
--primary-hover: oklch(84% 0.12 55);
|
|
89
|
+
--primary-tint: oklch(40% 0.09 55);
|
|
90
|
+
--ring: oklch(78% 0.14 55 / 0.4);
|
|
91
|
+
--shadow-modal: 0 24px 64px oklch(0% 0 0 / 0.5);
|
|
92
|
+
--scrim: oklch(10% 0.01 60 / 0.6);
|
|
93
|
+
color-scheme: dark;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
:root[data-theme="dark"] {
|
|
97
|
+
--canvas: oklch(16% 0.008 60);
|
|
98
|
+
--bar: oklch(22% 0.011 60);
|
|
99
|
+
--surface: oklch(24% 0.012 60);
|
|
100
|
+
--raised: oklch(27% 0.013 60);
|
|
101
|
+
--border: oklch(32% 0.012 60);
|
|
102
|
+
--text: oklch(96% 0.006 60);
|
|
103
|
+
--muted: oklch(70% 0.012 60);
|
|
104
|
+
--hover: oklch(30% 0.013 60);
|
|
105
|
+
--primary: oklch(78% 0.14 55);
|
|
106
|
+
--primary-hover: oklch(84% 0.12 55);
|
|
107
|
+
--primary-tint: oklch(40% 0.09 55);
|
|
108
|
+
--ring: oklch(78% 0.14 55 / 0.4);
|
|
109
|
+
--shadow-modal: 0 24px 64px oklch(0% 0 0 / 0.5);
|
|
110
|
+
--scrim: oklch(10% 0.01 60 / 0.6);
|
|
111
|
+
color-scheme: dark;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
* {
|
|
115
|
+
box-sizing: border-box;
|
|
116
|
+
}
|
|
117
|
+
html,
|
|
118
|
+
body {
|
|
119
|
+
margin: 0;
|
|
120
|
+
height: 100%;
|
|
121
|
+
}
|
|
122
|
+
body {
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
background: var(--canvas);
|
|
126
|
+
color: var(--text);
|
|
127
|
+
font-family:
|
|
128
|
+
Geist,
|
|
129
|
+
ui-sans-serif,
|
|
130
|
+
system-ui,
|
|
131
|
+
-apple-system,
|
|
132
|
+
sans-serif;
|
|
133
|
+
-webkit-font-smoothing: antialiased;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ---- Top bar ---------------------------------------------------- */
|
|
137
|
+
.bar {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
gap: 12px;
|
|
142
|
+
height: 52px;
|
|
143
|
+
flex: none;
|
|
144
|
+
padding: 0 14px 0 16px;
|
|
145
|
+
background: var(--bar);
|
|
146
|
+
border-bottom: 1px solid var(--border);
|
|
147
|
+
overflow-x: auto;
|
|
148
|
+
scrollbar-width: none;
|
|
149
|
+
z-index: 10;
|
|
150
|
+
}
|
|
151
|
+
.bar::-webkit-scrollbar {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
.bar .group {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
flex: none;
|
|
159
|
+
}
|
|
160
|
+
.brand {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 9px;
|
|
164
|
+
font-size: 15px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
letter-spacing: -0.01em;
|
|
167
|
+
color: var(--text);
|
|
168
|
+
}
|
|
169
|
+
.brand img {
|
|
170
|
+
width: 22px;
|
|
171
|
+
height: 22px;
|
|
172
|
+
display: block;
|
|
173
|
+
}
|
|
174
|
+
.brand .sub {
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
color: var(--muted);
|
|
178
|
+
}
|
|
179
|
+
.brand .sep {
|
|
180
|
+
color: var(--n-300);
|
|
181
|
+
font-weight: 400;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.status {
|
|
185
|
+
display: inline-flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 7px;
|
|
188
|
+
padding: 4px 11px 4px 9px;
|
|
189
|
+
border: 1px solid var(--border);
|
|
190
|
+
border-radius: 999px;
|
|
191
|
+
background: var(--surface);
|
|
192
|
+
font-size: 12px;
|
|
193
|
+
font-weight: 500;
|
|
194
|
+
color: var(--muted);
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
}
|
|
197
|
+
.status .dot {
|
|
198
|
+
width: 7px;
|
|
199
|
+
height: 7px;
|
|
200
|
+
border-radius: 50%;
|
|
201
|
+
background: var(--n-400);
|
|
202
|
+
position: relative;
|
|
203
|
+
}
|
|
204
|
+
.status[data-state="live"] {
|
|
205
|
+
color: color-mix(in oklch, var(--success) 72%, var(--text));
|
|
206
|
+
}
|
|
207
|
+
.status[data-state="live"] .dot {
|
|
208
|
+
background: var(--success);
|
|
209
|
+
box-shadow: 0 0 0 3px
|
|
210
|
+
color-mix(in oklch, var(--success) 22%, transparent);
|
|
211
|
+
}
|
|
212
|
+
.status[data-state="live"] .dot::after {
|
|
213
|
+
content: "";
|
|
214
|
+
position: absolute;
|
|
215
|
+
inset: 0;
|
|
216
|
+
border-radius: 50%;
|
|
217
|
+
background: var(--success);
|
|
218
|
+
animation: pulse 2s var(--spring) infinite;
|
|
219
|
+
}
|
|
220
|
+
.status[data-state="reconnecting"] {
|
|
221
|
+
color: color-mix(in oklch, var(--warning) 66%, var(--text));
|
|
222
|
+
}
|
|
223
|
+
.status[data-state="reconnecting"] .dot {
|
|
224
|
+
background: var(--warning);
|
|
225
|
+
}
|
|
226
|
+
.status[data-state="error"] {
|
|
227
|
+
color: color-mix(in oklch, var(--danger) 72%, var(--text));
|
|
228
|
+
}
|
|
229
|
+
.status[data-state="error"] .dot {
|
|
230
|
+
background: var(--danger);
|
|
231
|
+
}
|
|
232
|
+
@keyframes pulse {
|
|
233
|
+
0% {
|
|
234
|
+
transform: scale(1);
|
|
235
|
+
opacity: 0.6;
|
|
236
|
+
}
|
|
237
|
+
70%,
|
|
238
|
+
100% {
|
|
239
|
+
transform: scale(2.6);
|
|
240
|
+
opacity: 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.divider {
|
|
245
|
+
width: 1px;
|
|
246
|
+
height: 20px;
|
|
247
|
+
background: var(--border);
|
|
248
|
+
flex: none;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ---- Buttons ---------------------------------------------------- */
|
|
252
|
+
.btn {
|
|
253
|
+
display: inline-flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 6px;
|
|
256
|
+
height: 32px;
|
|
257
|
+
padding: 0 10px;
|
|
258
|
+
border: 1px solid var(--border);
|
|
259
|
+
border-radius: 8px;
|
|
260
|
+
background: var(--surface);
|
|
261
|
+
color: var(--muted);
|
|
262
|
+
font: inherit;
|
|
263
|
+
font-size: 13px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
white-space: nowrap;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
transition:
|
|
268
|
+
color 120ms var(--spring),
|
|
269
|
+
background-color 120ms var(--spring),
|
|
270
|
+
border-color 120ms var(--spring),
|
|
271
|
+
box-shadow 120ms var(--spring),
|
|
272
|
+
transform 120ms var(--spring);
|
|
273
|
+
}
|
|
274
|
+
.btn:hover {
|
|
275
|
+
color: var(--text);
|
|
276
|
+
background: var(--hover);
|
|
277
|
+
}
|
|
278
|
+
.btn:active {
|
|
279
|
+
transform: translateY(0.5px);
|
|
280
|
+
}
|
|
281
|
+
.btn svg {
|
|
282
|
+
width: 14px;
|
|
283
|
+
height: 14px;
|
|
284
|
+
flex: none;
|
|
285
|
+
}
|
|
286
|
+
.btn.icon-only {
|
|
287
|
+
width: 32px;
|
|
288
|
+
padding: 0;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
}
|
|
291
|
+
.btn.primary {
|
|
292
|
+
background: var(--primary);
|
|
293
|
+
border-color: transparent;
|
|
294
|
+
color: var(--paper);
|
|
295
|
+
}
|
|
296
|
+
.btn.primary:hover {
|
|
297
|
+
background: var(--primary-hover);
|
|
298
|
+
color: var(--paper);
|
|
299
|
+
}
|
|
300
|
+
:focus-visible {
|
|
301
|
+
outline: none;
|
|
302
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ---- Editor + loading ------------------------------------------- */
|
|
306
|
+
.stage {
|
|
307
|
+
position: relative;
|
|
308
|
+
flex: 1;
|
|
309
|
+
min-height: 0;
|
|
310
|
+
}
|
|
311
|
+
#editor {
|
|
312
|
+
position: absolute;
|
|
313
|
+
inset: 0;
|
|
314
|
+
overflow: auto;
|
|
315
|
+
}
|
|
316
|
+
.loading {
|
|
317
|
+
position: absolute;
|
|
318
|
+
inset: 0;
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
gap: 18px;
|
|
324
|
+
background: var(--canvas);
|
|
325
|
+
z-index: 5;
|
|
326
|
+
transition: opacity 260ms var(--spring);
|
|
327
|
+
}
|
|
328
|
+
body.ready .loading {
|
|
329
|
+
opacity: 0;
|
|
330
|
+
pointer-events: none;
|
|
331
|
+
}
|
|
332
|
+
.loading img {
|
|
333
|
+
width: 40px;
|
|
334
|
+
height: 40px;
|
|
335
|
+
opacity: 0.9;
|
|
336
|
+
}
|
|
337
|
+
.loading .spinner {
|
|
338
|
+
width: 22px;
|
|
339
|
+
height: 22px;
|
|
340
|
+
border: 2px solid var(--border);
|
|
341
|
+
border-top-color: var(--primary);
|
|
342
|
+
border-radius: 50%;
|
|
343
|
+
animation: spin 0.7s linear infinite;
|
|
344
|
+
}
|
|
345
|
+
.loading .msg {
|
|
346
|
+
font-size: 13px;
|
|
347
|
+
color: var(--muted);
|
|
348
|
+
}
|
|
349
|
+
.loading.error .spinner {
|
|
350
|
+
display: none;
|
|
351
|
+
}
|
|
352
|
+
.loading.error .msg {
|
|
353
|
+
color: color-mix(in oklch, var(--danger) 70%, var(--text));
|
|
354
|
+
max-width: 460px;
|
|
355
|
+
text-align: center;
|
|
356
|
+
line-height: 1.5;
|
|
357
|
+
}
|
|
358
|
+
@keyframes spin {
|
|
359
|
+
to {
|
|
360
|
+
transform: rotate(360deg);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ---- Export dialog ---------------------------------------------- */
|
|
365
|
+
dialog {
|
|
366
|
+
width: min(840px, 92vw);
|
|
367
|
+
max-height: 86vh;
|
|
368
|
+
border: 1px solid var(--border);
|
|
369
|
+
border-radius: 14px;
|
|
370
|
+
padding: 0;
|
|
371
|
+
background: var(--raised);
|
|
372
|
+
color: var(--text);
|
|
373
|
+
box-shadow: var(--shadow-modal);
|
|
374
|
+
overflow: hidden;
|
|
375
|
+
}
|
|
376
|
+
dialog::backdrop {
|
|
377
|
+
background: var(--scrim);
|
|
378
|
+
backdrop-filter: blur(4px);
|
|
379
|
+
}
|
|
380
|
+
dialog[open] {
|
|
381
|
+
animation: dialog-in 200ms var(--spring);
|
|
382
|
+
}
|
|
383
|
+
dialog[open]::backdrop {
|
|
384
|
+
animation: backdrop-in 200ms var(--spring);
|
|
385
|
+
}
|
|
386
|
+
@keyframes dialog-in {
|
|
387
|
+
from {
|
|
388
|
+
opacity: 0;
|
|
389
|
+
transform: scale(0.97) translateY(6px);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
@keyframes backdrop-in {
|
|
393
|
+
from {
|
|
394
|
+
opacity: 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
.dlg-head {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 10px;
|
|
401
|
+
padding: 13px 14px 13px 18px;
|
|
402
|
+
border-bottom: 1px solid var(--border);
|
|
403
|
+
}
|
|
404
|
+
.dlg-head strong {
|
|
405
|
+
font-size: 14px;
|
|
406
|
+
font-weight: 600;
|
|
407
|
+
}
|
|
408
|
+
.dlg-head .dlg-spacer {
|
|
409
|
+
flex: 1;
|
|
410
|
+
}
|
|
411
|
+
.tabs {
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
gap: 4px;
|
|
414
|
+
}
|
|
415
|
+
.tab {
|
|
416
|
+
height: 28px;
|
|
417
|
+
padding: 0 12px;
|
|
418
|
+
border: none;
|
|
419
|
+
border-radius: 7px;
|
|
420
|
+
background: transparent;
|
|
421
|
+
color: var(--muted);
|
|
422
|
+
font: inherit;
|
|
423
|
+
font-size: 13px;
|
|
424
|
+
font-weight: 500;
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
transition:
|
|
427
|
+
color 120ms var(--spring),
|
|
428
|
+
background-color 120ms var(--spring);
|
|
429
|
+
}
|
|
430
|
+
.tab:hover {
|
|
431
|
+
color: var(--text);
|
|
432
|
+
}
|
|
433
|
+
.tab[aria-selected="true"] {
|
|
434
|
+
background: var(--hover);
|
|
435
|
+
color: var(--text);
|
|
436
|
+
font-weight: 600;
|
|
437
|
+
}
|
|
438
|
+
dialog textarea {
|
|
439
|
+
display: block;
|
|
440
|
+
width: 100%;
|
|
441
|
+
height: 56vh;
|
|
442
|
+
border: none;
|
|
443
|
+
padding: 16px 18px;
|
|
444
|
+
/* Must be a semantic token: the --n-* / --paper primitives are declared
|
|
445
|
+
only in the light :root, so pairing one with `color: var(--text)`
|
|
446
|
+
rendered the export near-white on near-white in dark mode. */
|
|
447
|
+
background: var(--canvas);
|
|
448
|
+
color: var(--text);
|
|
449
|
+
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
|
|
450
|
+
font-size: 12.5px;
|
|
451
|
+
line-height: 1.55;
|
|
452
|
+
resize: none;
|
|
453
|
+
outline: none;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* ---- Toast ------------------------------------------------------ */
|
|
457
|
+
.toast {
|
|
458
|
+
position: fixed;
|
|
459
|
+
bottom: 20px;
|
|
460
|
+
left: 50%;
|
|
461
|
+
transform: translate(-50%, 8px);
|
|
462
|
+
display: inline-flex;
|
|
463
|
+
align-items: center;
|
|
464
|
+
gap: 8px;
|
|
465
|
+
background: var(--n-900);
|
|
466
|
+
color: var(--paper);
|
|
467
|
+
padding: 9px 16px;
|
|
468
|
+
border-radius: 999px;
|
|
469
|
+
font-size: 13px;
|
|
470
|
+
font-weight: 500;
|
|
471
|
+
box-shadow: 0 8px 28px oklch(18% 0.01 60 / 0.28);
|
|
472
|
+
opacity: 0;
|
|
473
|
+
pointer-events: none;
|
|
474
|
+
transition:
|
|
475
|
+
opacity 200ms var(--spring),
|
|
476
|
+
transform 200ms var(--spring);
|
|
477
|
+
z-index: 50;
|
|
478
|
+
}
|
|
479
|
+
:root[data-theme="dark"] .toast,
|
|
480
|
+
.toast {
|
|
481
|
+
/* keep the toast readable in both themes */
|
|
482
|
+
}
|
|
483
|
+
.toast.show {
|
|
484
|
+
opacity: 1;
|
|
485
|
+
transform: translate(-50%, 0);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* ---- Note panel ------------------------------------------------- */
|
|
489
|
+
/* Alt-click a block to open this — see the capture script below for
|
|
490
|
+
why the target id comes from data-block-id rather than an editor
|
|
491
|
+
API. z-index is well above --z-modal (10000, the editor's own
|
|
492
|
+
popover/dialog layer) so a note stays reachable over any editor
|
|
493
|
+
dialog. */
|
|
494
|
+
#note-panel {
|
|
495
|
+
position: fixed;
|
|
496
|
+
right: 16px;
|
|
497
|
+
bottom: 16px;
|
|
498
|
+
z-index: 20000;
|
|
499
|
+
width: 280px;
|
|
500
|
+
padding: 12px;
|
|
501
|
+
border: 1px solid var(--border);
|
|
502
|
+
border-radius: 8px;
|
|
503
|
+
background: var(--surface);
|
|
504
|
+
color: var(--text);
|
|
505
|
+
box-shadow: 0 8px 24px oklch(18% 0.01 60 / 0.16);
|
|
506
|
+
font-size: 13px;
|
|
507
|
+
line-height: 1.4;
|
|
508
|
+
}
|
|
509
|
+
#note-panel .note-target {
|
|
510
|
+
color: var(--muted);
|
|
511
|
+
}
|
|
512
|
+
#note-panel textarea {
|
|
513
|
+
display: block;
|
|
514
|
+
width: 100%;
|
|
515
|
+
box-sizing: border-box;
|
|
516
|
+
margin: 8px 0;
|
|
517
|
+
padding: 6px 8px;
|
|
518
|
+
border: 1px solid var(--border);
|
|
519
|
+
border-radius: 6px;
|
|
520
|
+
background: var(--canvas);
|
|
521
|
+
color: var(--text);
|
|
522
|
+
font: inherit;
|
|
523
|
+
resize: vertical;
|
|
524
|
+
}
|
|
525
|
+
#note-panel #note-status {
|
|
526
|
+
margin: 8px 0 0;
|
|
527
|
+
min-height: 1.4em;
|
|
528
|
+
font-size: 12px;
|
|
529
|
+
color: var(--muted);
|
|
530
|
+
}
|
|
531
|
+
.note-actions {
|
|
532
|
+
display: flex;
|
|
533
|
+
gap: 8px;
|
|
534
|
+
justify-content: flex-end;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
@media (max-width: 720px) {
|
|
538
|
+
.btn .label,
|
|
539
|
+
.brand .sub,
|
|
540
|
+
.brand .sep {
|
|
541
|
+
display: none;
|
|
542
|
+
}
|
|
543
|
+
.btn {
|
|
544
|
+
width: 32px;
|
|
545
|
+
padding: 0;
|
|
546
|
+
justify-content: center;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
@media (prefers-reduced-motion: reduce) {
|
|
551
|
+
*,
|
|
552
|
+
*::before,
|
|
553
|
+
*::after {
|
|
554
|
+
animation-duration: 0.01ms !important;
|
|
555
|
+
animation-iteration-count: 1 !important;
|
|
556
|
+
transition-duration: 0.01ms !important;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
</style>
|
|
560
|
+
</head>
|
|
561
|
+
<body>
|
|
562
|
+
<header class="bar">
|
|
563
|
+
<div class="group">
|
|
564
|
+
<span class="brand">
|
|
565
|
+
<img
|
|
566
|
+
src="https://templatical.com/logo.svg"
|
|
567
|
+
alt=""
|
|
568
|
+
aria-hidden="true"
|
|
569
|
+
/>
|
|
570
|
+
Templatical <span class="sep">·</span>
|
|
571
|
+
<span class="sub">Live preview</span>
|
|
572
|
+
</span>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<div class="group">
|
|
576
|
+
<span
|
|
577
|
+
id="status"
|
|
578
|
+
class="status"
|
|
579
|
+
data-state="connecting"
|
|
580
|
+
title="Connecting to the live preview…"
|
|
581
|
+
>
|
|
582
|
+
<span class="dot"></span><span id="status-text">Connecting…</span>
|
|
583
|
+
</span>
|
|
584
|
+
<span class="divider"></span>
|
|
585
|
+
<button id="btn-export" class="btn" title="Export the template">
|
|
586
|
+
<svg
|
|
587
|
+
viewBox="0 0 24 24"
|
|
588
|
+
fill="none"
|
|
589
|
+
stroke="currentColor"
|
|
590
|
+
stroke-width="1.8"
|
|
591
|
+
stroke-linecap="round"
|
|
592
|
+
stroke-linejoin="round"
|
|
593
|
+
>
|
|
594
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
595
|
+
<polyline points="7 10 12 15 17 10" />
|
|
596
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
597
|
+
</svg>
|
|
598
|
+
<span class="label">Export</span>
|
|
599
|
+
</button>
|
|
600
|
+
<span class="divider"></span>
|
|
601
|
+
<button
|
|
602
|
+
id="btn-theme"
|
|
603
|
+
class="btn icon-only"
|
|
604
|
+
title="Toggle theme"
|
|
605
|
+
aria-label="Toggle theme"
|
|
606
|
+
></button>
|
|
607
|
+
</div>
|
|
608
|
+
</header>
|
|
609
|
+
|
|
610
|
+
<div class="stage">
|
|
611
|
+
<div id="editor"></div>
|
|
612
|
+
<div id="loading" class="loading">
|
|
613
|
+
<img src="https://templatical.com/logo.svg" alt="Templatical" />
|
|
614
|
+
<div class="spinner" aria-hidden="true"></div>
|
|
615
|
+
<div class="msg" id="loading-msg">Loading the editor…</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<div id="note-panel" hidden>
|
|
620
|
+
<div class="note-target">
|
|
621
|
+
Note on <strong id="note-target-label">the template</strong>
|
|
622
|
+
</div>
|
|
623
|
+
<textarea
|
|
624
|
+
id="note-text"
|
|
625
|
+
rows="3"
|
|
626
|
+
placeholder="What should change here?"
|
|
627
|
+
></textarea>
|
|
628
|
+
<div class="note-actions">
|
|
629
|
+
<button type="button" id="note-cancel" class="btn">Cancel</button>
|
|
630
|
+
<button type="button" id="note-save" class="btn primary">
|
|
631
|
+
Leave note
|
|
632
|
+
</button>
|
|
633
|
+
</div>
|
|
634
|
+
<p id="note-status" role="status"></p>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<dialog id="export">
|
|
638
|
+
<div class="dlg-head">
|
|
639
|
+
<strong>Export</strong>
|
|
640
|
+
<div class="tabs" role="tablist">
|
|
641
|
+
<button class="tab" data-format="json" aria-selected="true">
|
|
642
|
+
JSON
|
|
643
|
+
</button>
|
|
644
|
+
<button class="tab" data-format="mjml" aria-selected="false">
|
|
645
|
+
MJML
|
|
646
|
+
</button>
|
|
647
|
+
<button class="tab" data-format="html" aria-selected="false">
|
|
648
|
+
HTML
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
651
|
+
<span class="dlg-spacer"></span>
|
|
652
|
+
<button id="export-copy" class="btn">Copy</button>
|
|
653
|
+
<button id="export-close" class="btn">Close</button>
|
|
654
|
+
</div>
|
|
655
|
+
<textarea id="export-body" readonly spellcheck="false"></textarea>
|
|
656
|
+
</dialog>
|
|
657
|
+
|
|
658
|
+
<div id="toast" class="toast"></div>
|
|
659
|
+
|
|
660
|
+
<script type="module">
|
|
661
|
+
import { init } from "https://cdn.jsdelivr.net/npm/@templatical/editor@{{EDITOR_VERSION}}/dist/cdn/editor.js";
|
|
662
|
+
|
|
663
|
+
const $ = (id) => document.getElementById(id);
|
|
664
|
+
const statusEl = $("status");
|
|
665
|
+
const statusText = $("status-text");
|
|
666
|
+
const toastEl = $("toast");
|
|
667
|
+
const loadingEl = $("loading");
|
|
668
|
+
|
|
669
|
+
let editor = null;
|
|
670
|
+
// Guards the setContent() we trigger from an agent update so its onChange
|
|
671
|
+
// isn't mistaken for (and reported back as) a user hand-edit.
|
|
672
|
+
let applyingRemote = false;
|
|
673
|
+
let debounceTimer = null;
|
|
674
|
+
|
|
675
|
+
// ---- Theme (auto default, manual light/dark override) ----
|
|
676
|
+
const SUN =
|
|
677
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path 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"/></svg>';
|
|
678
|
+
const MOON =
|
|
679
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>';
|
|
680
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
681
|
+
const readTheme = () => {
|
|
682
|
+
try {
|
|
683
|
+
return localStorage.getItem("tpl-live-theme") || "auto";
|
|
684
|
+
} catch {
|
|
685
|
+
return "auto";
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const effective = (stored) =>
|
|
689
|
+
stored === "auto" ? (prefersDark.matches ? "dark" : "light") : stored;
|
|
690
|
+
function applyTheme(stored) {
|
|
691
|
+
if (stored === "auto")
|
|
692
|
+
document.documentElement.removeAttribute("data-theme");
|
|
693
|
+
else document.documentElement.setAttribute("data-theme", stored);
|
|
694
|
+
const eff = effective(stored);
|
|
695
|
+
const btn = $("btn-theme");
|
|
696
|
+
btn.innerHTML = eff === "dark" ? SUN : MOON;
|
|
697
|
+
btn.setAttribute(
|
|
698
|
+
"aria-label",
|
|
699
|
+
eff === "dark" ? "Switch to light theme" : "Switch to dark theme",
|
|
700
|
+
);
|
|
701
|
+
if (editor) {
|
|
702
|
+
try {
|
|
703
|
+
editor.setTheme(stored);
|
|
704
|
+
} catch {
|
|
705
|
+
/* older editor build without setTheme */
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
$("btn-theme").onclick = () => {
|
|
710
|
+
const next = effective(readTheme()) === "dark" ? "light" : "dark";
|
|
711
|
+
try {
|
|
712
|
+
localStorage.setItem("tpl-live-theme", next);
|
|
713
|
+
} catch {}
|
|
714
|
+
applyTheme(next);
|
|
715
|
+
};
|
|
716
|
+
prefersDark.addEventListener("change", () => {
|
|
717
|
+
if (readTheme() === "auto") applyTheme("auto");
|
|
718
|
+
});
|
|
719
|
+
applyTheme(readTheme());
|
|
720
|
+
|
|
721
|
+
// ---- Status + toast ----
|
|
722
|
+
// The status pill is a connection-state signal, not decoration: when it's
|
|
723
|
+
// "live" the SSE link is up, so agent updates land here and your in-browser
|
|
724
|
+
// edits are reported back for the divergence check. The hover title spells
|
|
725
|
+
// that out (a native tooltip — the bar's overflow would clip a styled one).
|
|
726
|
+
const STATUS_TITLE = {
|
|
727
|
+
connecting: "Connecting to the live preview…",
|
|
728
|
+
live: "Connected — your edits and the agent's changes sync here in real time.",
|
|
729
|
+
reconnecting:
|
|
730
|
+
"Reconnecting to the live server — changes won't sync until it's back.",
|
|
731
|
+
error:
|
|
732
|
+
"Disconnected — changes won't sync until the connection is restored.",
|
|
733
|
+
};
|
|
734
|
+
function setStatus(state, text) {
|
|
735
|
+
statusEl.dataset.state = state;
|
|
736
|
+
statusText.textContent = text;
|
|
737
|
+
statusEl.title = STATUS_TITLE[state] ?? "";
|
|
738
|
+
}
|
|
739
|
+
let toastTimer = null;
|
|
740
|
+
function toast(msg) {
|
|
741
|
+
toastEl.textContent = msg;
|
|
742
|
+
toastEl.classList.add("show");
|
|
743
|
+
clearTimeout(toastTimer);
|
|
744
|
+
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function copy(text) {
|
|
748
|
+
try {
|
|
749
|
+
await navigator.clipboard.writeText(text);
|
|
750
|
+
return true;
|
|
751
|
+
} catch {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ---- Sync with the bridge ----
|
|
757
|
+
// Report the editor's current content. `baseline: true` marks a post-apply
|
|
758
|
+
// snapshot (the normalized view of the agent's content), which resets the
|
|
759
|
+
// divergence tracker rather than counting as an edit.
|
|
760
|
+
async function postContent(baseline = false) {
|
|
761
|
+
if (!editor) return;
|
|
762
|
+
try {
|
|
763
|
+
await fetch("/content", {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: { "content-type": "application/json" },
|
|
766
|
+
body: JSON.stringify({ content: editor.getContent(), baseline }),
|
|
767
|
+
});
|
|
768
|
+
} catch {
|
|
769
|
+
/* bridge gone; ignore */
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function scheduleContentPost() {
|
|
773
|
+
clearTimeout(debounceTimer);
|
|
774
|
+
debounceTimer = setTimeout(() => postContent(false), 500);
|
|
775
|
+
}
|
|
776
|
+
async function applyRemote(content) {
|
|
777
|
+
if (!editor) return;
|
|
778
|
+
applyingRemote = true;
|
|
779
|
+
editor.setContent(content);
|
|
780
|
+
await new Promise((r) => setTimeout(r, 0)); // let Vue flush setContent
|
|
781
|
+
await postContent(true);
|
|
782
|
+
applyingRemote = false;
|
|
783
|
+
toast("Updated from agent");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ---- Export ----
|
|
787
|
+
// One "Export" button opens a modal with JSON / MJML / HTML tabs. JSON is
|
|
788
|
+
// instant; MJML calls editor.toMjml() (bundled in the CDN editor); HTML
|
|
789
|
+
// compiles that MJML in-browser via mjml-browser.
|
|
790
|
+
//
|
|
791
|
+
// mjml-browser loads from jsDelivr's /+esm (NOT unpkg) on purpose: it's a
|
|
792
|
+
// CommonJS package, and unpkg's ?module only rewrites import specifiers —
|
|
793
|
+
// it does not transpile CJS to ESM, so `import()` from unpkg yields an
|
|
794
|
+
// empty module. jsDelivr's /+esm does the CJS→ESM conversion and exposes a
|
|
795
|
+
// callable default.
|
|
796
|
+
//
|
|
797
|
+
// The editor bundle is on jsDelivr too, so the whole harness rides one
|
|
798
|
+
// CDN. It used to load from unpkg, which intermittently served the
|
|
799
|
+
// editor's LAZY chunks as text/plain with a failed CORS preflight — the
|
|
800
|
+
// entry resolved, the editor booted, and only the deferred chunk died. For
|
|
801
|
+
// the renderer chunk that surfaced as toMjml()'s "install @templatical/
|
|
802
|
+
// renderer" error (its catch can't tell a missing package from a failed
|
|
803
|
+
// fetch), even though the CDN build bundles the renderer.
|
|
804
|
+
const exportDialog = $("export");
|
|
805
|
+
const tabs = [...document.querySelectorAll("#export .tab")];
|
|
806
|
+
let mjml2html = null;
|
|
807
|
+
|
|
808
|
+
async function renderFormat(format) {
|
|
809
|
+
tabs.forEach((t) =>
|
|
810
|
+
t.setAttribute("aria-selected", String(t.dataset.format === format)),
|
|
811
|
+
);
|
|
812
|
+
const body = $("export-body");
|
|
813
|
+
if (!editor) return;
|
|
814
|
+
if (format === "json") {
|
|
815
|
+
body.value = JSON.stringify(editor.getContent(), null, 2);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
body.value = format === "html" ? "Compiling HTML…" : "Rendering MJML…";
|
|
819
|
+
try {
|
|
820
|
+
const mjml = await editor.toMjml();
|
|
821
|
+
if (format === "mjml") {
|
|
822
|
+
body.value = mjml;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!mjml2html) {
|
|
826
|
+
// The major here must track the mjml that @templatical/renderer's
|
|
827
|
+
// round-trip tests compile against, or this preview renders
|
|
828
|
+
// through a different compiler than the one the renderer's
|
|
829
|
+
// output is actually verified with. Guarded by
|
|
830
|
+
// tests/cdn-pin.test.ts — a version pinned inside a CDN URL in an
|
|
831
|
+
// HTML file is invisible to Renovate, so nothing else here flags
|
|
832
|
+
// a drift.
|
|
833
|
+
const mod =
|
|
834
|
+
await import("https://cdn.jsdelivr.net/npm/mjml-browser@5/+esm");
|
|
835
|
+
mjml2html = mod.default ?? mod.mjml2html ?? mod;
|
|
836
|
+
}
|
|
837
|
+
body.value = mjml2html(mjml).html;
|
|
838
|
+
} catch (err) {
|
|
839
|
+
body.value = String(err?.message ?? err);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
$("btn-export").onclick = () => {
|
|
844
|
+
exportDialog.showModal();
|
|
845
|
+
renderFormat("json");
|
|
846
|
+
};
|
|
847
|
+
tabs.forEach((t) => (t.onclick = () => renderFormat(t.dataset.format)));
|
|
848
|
+
$("export-close").onclick = () => exportDialog.close();
|
|
849
|
+
$("export-copy").onclick = async () => {
|
|
850
|
+
const ok = await copy($("export-body").value);
|
|
851
|
+
toast(ok ? "Copied to clipboard" : "Select the text to copy");
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// ---- Boot ----
|
|
855
|
+
async function boot() {
|
|
856
|
+
let content;
|
|
857
|
+
try {
|
|
858
|
+
const res = await fetch("/template");
|
|
859
|
+
if (res.status === 200) content = await res.json();
|
|
860
|
+
} catch {
|
|
861
|
+
/* fall through to an empty editor */
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
editor = await init({
|
|
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
|
+
|
|
879
|
+
document.body.classList.add("ready");
|
|
880
|
+
await postContent(true); // seed the baseline with the normalized view
|
|
881
|
+
setStatus("live", "Live");
|
|
882
|
+
|
|
883
|
+
const es = new EventSource("/events");
|
|
884
|
+
es.addEventListener("template", (e) => {
|
|
885
|
+
try {
|
|
886
|
+
applyRemote(JSON.parse(e.data));
|
|
887
|
+
} catch {
|
|
888
|
+
/* malformed frame; ignore */
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
es.onopen = () => setStatus("live", "Live");
|
|
892
|
+
es.onerror = () => setStatus("reconnecting", "Reconnecting…");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
boot().catch((err) => {
|
|
896
|
+
setStatus("error", "Editor failed to load");
|
|
897
|
+
loadingEl.classList.add("error");
|
|
898
|
+
$("loading-msg").textContent =
|
|
899
|
+
"The editor couldn't load from the CDN. Check your connection and reload.\n\n" +
|
|
900
|
+
String(err?.message ?? err);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// ---- Notes ----
|
|
904
|
+
// Notes are attached by clicking a block. The editor exposes no
|
|
905
|
+
// selection accessor, so the id comes from data-block-id on the
|
|
906
|
+
// click's composedPath - an attribute the editor's own e2e suite
|
|
907
|
+
// locks, and documented in-repo as a hook for consumers. composedPath
|
|
908
|
+
// (not event.target) is required because the editor mounts in shadow
|
|
909
|
+
// DOM and clicks retarget at the boundary.
|
|
910
|
+
const notePanel = $("note-panel");
|
|
911
|
+
const noteText = $("note-text");
|
|
912
|
+
const noteLabel = $("note-target-label");
|
|
913
|
+
const noteStatus = $("note-status");
|
|
914
|
+
let noteBlockId = null;
|
|
915
|
+
|
|
916
|
+
function blockIdFromEvent(event) {
|
|
917
|
+
for (const el of event.composedPath()) {
|
|
918
|
+
const id = el instanceof HTMLElement ? el.dataset?.blockId : undefined;
|
|
919
|
+
if (id) return id;
|
|
920
|
+
}
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Capture phase, not bubble: the editor's own BlockWrapper.handleClick
|
|
925
|
+
// calls stopPropagation() (so a click inside a section picks the
|
|
926
|
+
// section, not a stray column child) - a bubble listener on document
|
|
927
|
+
// never sees a click on a block. Capture runs root-to-target, before
|
|
928
|
+
// that stopPropagation is reached, so it is unaffected.
|
|
929
|
+
document.addEventListener(
|
|
930
|
+
"click",
|
|
931
|
+
(event) => {
|
|
932
|
+
if (notePanel.contains(event.target)) return;
|
|
933
|
+
// Alt-click opens the note panel; a plain click is the editor's own.
|
|
934
|
+
if (!event.altKey) return;
|
|
935
|
+
noteBlockId = blockIdFromEvent(event);
|
|
936
|
+
noteLabel.textContent = noteBlockId ? noteBlockId : "the template";
|
|
937
|
+
noteStatus.textContent = "";
|
|
938
|
+
noteText.value = "";
|
|
939
|
+
notePanel.hidden = false;
|
|
940
|
+
noteText.focus();
|
|
941
|
+
},
|
|
942
|
+
true,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
$("note-cancel").onclick = () => {
|
|
946
|
+
notePanel.hidden = true;
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
$("note-save").onclick = async () => {
|
|
950
|
+
const text = noteText.value.trim();
|
|
951
|
+
if (!text) {
|
|
952
|
+
noteStatus.textContent = "Write something first.";
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
const res = await fetch("/annotations", {
|
|
957
|
+
method: "POST",
|
|
958
|
+
headers: { "content-type": "application/json" },
|
|
959
|
+
body: JSON.stringify({ blockId: noteBlockId, text }),
|
|
960
|
+
});
|
|
961
|
+
if (res.ok) {
|
|
962
|
+
notePanel.hidden = true;
|
|
963
|
+
// Say plainly when it will be acted on: nothing can wake the
|
|
964
|
+
// agent from this page, so the note is picked up on its next
|
|
965
|
+
// turn. Reuses the existing toast() rather than a second
|
|
966
|
+
// notification affordance.
|
|
967
|
+
noteStatus.textContent = "";
|
|
968
|
+
toast("Note saved — your agent will see it on its next change.");
|
|
969
|
+
} else {
|
|
970
|
+
noteStatus.textContent = "Could not save that note.";
|
|
971
|
+
}
|
|
972
|
+
} catch {
|
|
973
|
+
noteStatus.textContent = "Could not reach the bridge.";
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
</script>
|
|
977
|
+
</body>
|
|
978
|
+
</html>
|