vowel 0.6.3 → 0.7.0-beta.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.
Files changed (165) hide show
  1. package/README.md +23 -17
  2. package/bin.js +87 -30
  3. package/cli.js +15 -0
  4. package/config.js +102 -16
  5. package/extractDate.js +70 -6
  6. package/index.js +14 -5
  7. package/package.json +42 -7
  8. package/plugins/assets/index.js +65 -0
  9. package/plugins/atproto/index.js +132 -0
  10. package/plugins/atproto/password.js +71 -0
  11. package/plugins/atproto/publish.js +406 -0
  12. package/plugins/atproto/record.js +144 -0
  13. package/plugins/atproto/rkey.js +54 -0
  14. package/plugins/deploy-cloudflare/index.js +92 -0
  15. package/plugins/fonts/index.js +18 -21
  16. package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
  17. package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
  18. package/plugins/html/bundle/index.js +21145 -0
  19. package/plugins/html/editor/EditButton.svelte +152 -0
  20. package/plugins/html/editor/Editor.svelte +51 -0
  21. package/plugins/html/editor/Overlays.svelte +6 -0
  22. package/plugins/html/editor/SettingsPanel.svelte +397 -0
  23. package/plugins/html/editor/Toolbar.svelte +644 -0
  24. package/plugins/html/editor/create-session.js +250 -0
  25. package/plugins/html/editor/directives.js +128 -0
  26. package/plugins/html/editor/frontmatter.js +97 -0
  27. package/plugins/html/editor/ingest.js +339 -0
  28. package/plugins/html/editor/main.js +94 -0
  29. package/plugins/html/editor/nodes/Alert.svelte +21 -0
  30. package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
  31. package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
  32. package/plugins/html/editor/nodes/Embed.svelte +19 -0
  33. package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
  34. package/plugins/html/editor/nodes/Heading.svelte +15 -0
  35. package/plugins/html/editor/nodes/Highlight.svelte +11 -0
  36. package/plugins/html/editor/nodes/Image.svelte +18 -0
  37. package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
  38. package/plugins/html/editor/nodes/Link.svelte +17 -0
  39. package/plugins/html/editor/nodes/List.svelte +15 -0
  40. package/plugins/html/editor/nodes/ListItem.svelte +9 -0
  41. package/plugins/html/editor/nodes/Page.svelte +21 -0
  42. package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
  43. package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
  44. package/plugins/html/editor/nodes/Strong.svelte +11 -0
  45. package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
  46. package/plugins/html/editor/nodes/index.js +18 -0
  47. package/plugins/html/editor/rollup.config.js +35 -0
  48. package/plugins/html/editor/serialize.js +157 -0
  49. package/plugins/html/editor/settings-file.js +83 -0
  50. package/plugins/html/editor/socket.js +202 -0
  51. package/plugins/html/editor/source.js +43 -0
  52. package/plugins/html/image.js +104 -0
  53. package/plugins/html/index.js +760 -0
  54. package/plugins/html/partials.js +299 -0
  55. package/plugins/html/socialLinks.js +67 -0
  56. package/plugins/html/writeRules.js +377 -0
  57. package/plugins/icons/index.js +2 -6
  58. package/plugins/images/index.js +78 -55
  59. package/plugins/markdown/brandColors.js +10 -0
  60. package/plugins/markdown/colorScheme.js +64 -0
  61. package/plugins/markdown/emoji.js +30 -0
  62. package/plugins/markdown/frontmatter.js +253 -0
  63. package/plugins/markdown/index.js +287 -1274
  64. package/plugins/markdown/links.js +97 -0
  65. package/plugins/markdown/metadata.js +308 -0
  66. package/plugins/markdown/readRules.js +143 -0
  67. package/plugins/markdown/typography.js +109 -0
  68. package/plugins/markdown/visitor.js +143 -0
  69. package/plugins/publish/index.js +34 -0
  70. package/plugins/robots/agents.js +79 -0
  71. package/plugins/robots/index.js +30 -10
  72. package/plugins/robots/robots.js +48 -0
  73. package/plugins/styles/index.js +111 -26
  74. package/plugins/styles/theme.js +116 -0
  75. package/plugins/urls/index.js +78 -0
  76. package/plugins/urls/urlMetadata.js +246 -0
  77. package/plugins/vectors/index.js +5 -9
  78. package/plugins/xml/entryContent.js +103 -0
  79. package/plugins/xml/index.js +123 -50
  80. package/secretPaths.js +170 -0
  81. package/stylesheets/DefaultStyles.css +367 -437
  82. package/stylesheets/ResetStyles.css +2 -38
  83. package/stylesheets/SyntaxHighlightingStyles.css +269 -0
  84. package/stylesheets/TypographyStyles.css +386 -128
  85. package/systemPaths.js +48 -0
  86. package/utils.js +56 -5
  87. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
  88. package/.github/ISSUE_TEMPLATE/config.yml +0 -7
  89. package/docs-source/.cache.json +0 -9
  90. package/docs-source/.obsidian/app.json +0 -3
  91. package/docs-source/.obsidian/appearance.json +0 -3
  92. package/docs-source/.obsidian/core-plugins-migration.json +0 -30
  93. package/docs-source/.obsidian/core-plugins.json +0 -20
  94. package/docs-source/.obsidian/workspace.json +0 -168
  95. package/docs-source/.stackblitzrc +0 -3
  96. package/docs-source/.votive.db +0 -0
  97. package/docs-source/about.md +0 -3
  98. package/docs-source/blog/home.md +0 -5
  99. package/docs-source/blog/url-ui.md +0 -21
  100. package/docs-source/blog.md +0 -5
  101. package/docs-source/docs/.votive.db +0 -0
  102. package/docs-source/docs/deploy.md +0 -67
  103. package/docs-source/docs/file-structure.md +0 -31
  104. package/docs-source/docs/folder-settings.md +0 -23
  105. package/docs-source/docs/home.md +0 -55
  106. package/docs-source/docs/images.md +0 -10
  107. package/docs-source/docs/items.md +0 -13
  108. package/docs-source/docs/pages.md +0 -141
  109. package/docs-source/docs/settings.md +0 -4
  110. package/docs-source/docs/styling.md +0 -34
  111. package/docs-source/docs/taxonomies.md +0 -37
  112. package/docs-source/features/cards.md +0 -7
  113. package/docs-source/features/editing.md +0 -7
  114. package/docs-source/features/emoji.md +0 -7
  115. package/docs-source/features/frontmatter.md +0 -7
  116. package/docs-source/features/lists.md +0 -7
  117. package/docs-source/features/navigation.md +0 -7
  118. package/docs-source/features/rich-previews.md +0 -7
  119. package/docs-source/features/robots.md +0 -7
  120. package/docs-source/features/rss.md +0 -7
  121. package/docs-source/features/sitemap.md +0 -7
  122. package/docs-source/features/speed.md +0 -7
  123. package/docs-source/features/static.md +0 -7
  124. package/docs-source/features/taxonomies.md +0 -7
  125. package/docs-source/home.md +0 -42
  126. package/docs-source/output/about.html +0 -1
  127. package/docs-source/output/blog/url-ui.html +0 -8
  128. package/docs-source/output/blog.html +0 -1
  129. package/docs-source/output/default.css +0 -1
  130. package/docs-source/output/docs/deploy.html +0 -34
  131. package/docs-source/output/docs/file-structure.html +0 -24
  132. package/docs-source/output/docs/folder-settings.html +0 -20
  133. package/docs-source/output/docs/images.html +0 -2
  134. package/docs-source/output/docs/items.html +0 -6
  135. package/docs-source/output/docs/pages.html +0 -101
  136. package/docs-source/output/docs/styling.html +0 -18
  137. package/docs-source/output/docs/taxonomies.html +0 -20
  138. package/docs-source/output/docs.html +0 -28
  139. package/docs-source/output/features/cards.html +0 -1
  140. package/docs-source/output/features/editing.html +0 -1
  141. package/docs-source/output/features/emoji.html +0 -1
  142. package/docs-source/output/features/frontmatter.html +0 -1
  143. package/docs-source/output/features/lists.html +0 -1
  144. package/docs-source/output/features/navigation.html +0 -1
  145. package/docs-source/output/features/rich-previews.html +0 -1
  146. package/docs-source/output/features/robots.html +0 -1
  147. package/docs-source/output/features/rss.html +0 -1
  148. package/docs-source/output/features/sitemap.html +0 -1
  149. package/docs-source/output/features/speed.html +0 -1
  150. package/docs-source/output/features/static.html +0 -1
  151. package/docs-source/output/features/taxonomies.html +0 -1
  152. package/docs-source/output/features.html +0 -1
  153. package/docs-source/output/feed.xml +0 -1
  154. package/docs-source/output/index.html +0 -21
  155. package/docs-source/output/reset.css +0 -1
  156. package/docs-source/output/roadmap.html +0 -87
  157. package/docs-source/output/robots.txt +0 -14
  158. package/docs-source/output/sitemap.xml +0 -16
  159. package/docs-source/output/typography.css +0 -1
  160. package/docs-source/roadmap.md +0 -98
  161. package/docs-source/settings.md +0 -13
  162. package/getMetadata.js +0 -41
  163. package/jsconfig.json +0 -10
  164. package/regex.js +0 -36
  165. package/stylesheets/brand-colors.css +0 -119
@@ -0,0 +1,644 @@
1
+ <script>
2
+ import { untrack } from 'svelte';
3
+ import { serialize_path } from 'svedit';
4
+
5
+ // Trimmed down from Svedit's demo Toolbar: no Icon/NodeNavigator components
6
+ // (this project doesn't have them), and only the commands this project's
7
+ // create-session.js actually registers (bold, highlight, undo/redo,
8
+ // select-parent, delete). Buttons use plain-text glyphs instead of icons.
9
+ let {
10
+ session,
11
+ save,
12
+ editable = $bindable(false)
13
+ } = $props();
14
+
15
+ function toggle_editable() {
16
+ if (editable) {
17
+ save();
18
+ session.selection = null;
19
+ }
20
+ editable = !editable;
21
+ }
22
+
23
+ let selection_type = $derived(session.selection?.type ?? null);
24
+
25
+ // Check if we have a collapsed node selection (node caret)
26
+ let is_node_caret = $derived(
27
+ session.selection?.type === 'node' &&
28
+ session.selection.anchor_offset === session.selection.focus_offset
29
+ );
30
+
31
+ // Get default node_type for current node_array
32
+ let default_node_type = $derived.by(() => {
33
+ if (!is_node_caret) return null;
34
+
35
+ const node_array_path = session.selection.path;
36
+ const node_array_node = session.get(node_array_path.slice(0, -1)); // Get the parent node
37
+ const node_array_property = node_array_path.at(-1); // Get the property name
38
+
39
+ const node_schema = session.schema[node_array_node?.type];
40
+ if (!node_schema) return null;
41
+
42
+ const property_definition = node_schema.properties[node_array_property];
43
+ if (property_definition?.type !== 'node_array') return null;
44
+
45
+ return (
46
+ property_definition.default_node_type ||
47
+ (property_definition.node_types?.length === 1 ? property_definition.node_types[0] : null)
48
+ );
49
+ });
50
+
51
+ let can_insert_default = $derived(
52
+ Boolean(is_node_caret && default_node_type && session.config.inserters?.[default_node_type])
53
+ );
54
+
55
+ let can_delete = $derived(selection_type === 'node' || selection_type === 'property');
56
+
57
+ function insert_default_node(event) {
58
+ event.preventDefault();
59
+ const inserter = default_node_type ? session.config.inserters?.[default_node_type] : null;
60
+ if (!is_node_caret || !inserter) return;
61
+
62
+ const tr = session.tr;
63
+ inserter(tr);
64
+ session.apply(tr);
65
+ }
66
+
67
+ function delete_node_selection(event) {
68
+ event.preventDefault();
69
+ if (!can_delete) return;
70
+ session.apply(session.tr.delete_selection('backward'));
71
+ }
72
+
73
+ function select_parent(event) {
74
+ event.preventDefault();
75
+ if (session.commands.select_parent?.disabled) return;
76
+ session.commands.select_parent?.execute();
77
+ }
78
+
79
+ // While a pointer drags a selection the floating toolbar stays hidden,
80
+ // otherwise it flickers along the growing selection. It appears
81
+ // instantly on pointer up.
82
+ let is_dragging = $state(false);
83
+
84
+ function handle_window_pointerdown(event) {
85
+ // Presses on the toolbars themselves must not hide them
86
+ if (event.target instanceof Element && event.target.closest('.editor-toolbar')) return;
87
+ is_dragging = true;
88
+ }
89
+
90
+ function handle_window_pointerup() {
91
+ is_dragging = false;
92
+ }
93
+
94
+ // Anchor for the floating toolbar. Every selectable element exposes
95
+ // anchor-name: --{serialized_path}, so the toolbar attaches to the element
96
+ // owning the current selection with pure CSS anchor positioning.
97
+ // last_node_anchor (multi-node only) is the last selected node's path,
98
+ // exposed as --last-selected-node-anchor for a position-try fallback.
99
+ let floating_anchor = $derived.by(() => {
100
+ if (!editable) return null;
101
+ const sel = session.selection;
102
+ if (!sel) return null;
103
+
104
+ if (sel.type === 'text') {
105
+ // No toolbar at a collapsed text caret: it would hover over the
106
+ // line above while the user is typing.
107
+ if (sel.anchor_offset === sel.focus_offset) return null;
108
+ return { name: serialize_path(sel.path) };
109
+ }
110
+
111
+ if (sel.type === 'property') {
112
+ return { name: serialize_path(sel.path) };
113
+ }
114
+
115
+ if (sel.type === 'node') {
116
+ const start = Math.min(sel.anchor_offset, sel.focus_offset);
117
+ const end = Math.max(sel.anchor_offset, sel.focus_offset);
118
+ if (start !== end) {
119
+ return {
120
+ name: serialize_path([...sel.path, start]),
121
+ ...(end - start > 1 ? { last_node_anchor: serialize_path([...sel.path, end - 1]) } : {})
122
+ };
123
+ }
124
+ // Node caret: nothing to offer here yet (this project has no
125
+ // gap-insert tool configured), the bottom toolbar's insert
126
+ // button covers it instead.
127
+ return null;
128
+ }
129
+
130
+ return null;
131
+ });
132
+
133
+ // Distance the visual viewport bottom sits above the layout viewport
134
+ // bottom. Chromium and Firefox resize the layout viewport when the
135
+ // virtual keyboard opens (interactive-widget=resizes-content), so this
136
+ // stays 0 there. iOS Safari only shrinks and pans the visual viewport,
137
+ // so the bottom toolbar must be translated up by this inset to stay
138
+ // visible above the keyboard, also while scrolling.
139
+ let keyboard_inset = $state(0);
140
+
141
+ $effect(() => {
142
+ const visual_viewport = window.visualViewport;
143
+ if (!visual_viewport) return;
144
+
145
+ let current = untrack(() => keyboard_inset);
146
+ let target = current;
147
+ let raf = 0;
148
+
149
+ function measure() {
150
+ target = Math.max(
151
+ 0,
152
+ Math.round(window.innerHeight - visual_viewport.height - visual_viewport.offsetTop)
153
+ );
154
+ if (!raf) raf = requestAnimationFrame(follow);
155
+ }
156
+
157
+ // Exponential follower: large distances (keyboard opening) are covered
158
+ // within a few frames while the small stale-position corrections that
159
+ // Safari reports during touch pans get smoothed instead of rendering
160
+ // as jitter. Speed adapts to the remaining distance by construction.
161
+ function follow() {
162
+ raf = 0;
163
+ const delta = target - current;
164
+ if (Math.abs(delta) <= 1) {
165
+ current = target;
166
+ } else {
167
+ current += delta * 0.35;
168
+ raf = requestAnimationFrame(follow);
169
+ }
170
+ keyboard_inset = current;
171
+ }
172
+
173
+ measure();
174
+ visual_viewport.addEventListener('resize', measure);
175
+ visual_viewport.addEventListener('scroll', measure);
176
+ return () => {
177
+ cancelAnimationFrame(raf);
178
+ visual_viewport.removeEventListener('resize', measure);
179
+ visual_viewport.removeEventListener('scroll', measure);
180
+ };
181
+ });
182
+ </script>
183
+
184
+ {#snippet divider()}
185
+ <span class="divider" aria-hidden="true"></span>
186
+ {/snippet}
187
+
188
+ <!-- Way back up the node hierarchy, like Notion's << button in its text
189
+ formatting toolbar. Repeated presses ascend text -> node -> parent node
190
+ etc. Hidden entirely at the top level where there is no parent, and
191
+ pinned to the left edge of a scrollable bar so it stays reachable. -->
192
+ {#snippet select_parent_button()}
193
+ {#if session.commands.select_parent && !session.commands.select_parent.disabled}
194
+ <span class="select-parent-group">
195
+ <button title="Select parent (Esc)" onmousedown={select_parent}>&#8598;</button>
196
+ {@render divider()}
197
+ </span>
198
+ {/if}
199
+ {/snippet}
200
+
201
+ {#snippet mark_buttons()}
202
+ {#if session.commands.toggle_strong}
203
+ <button
204
+ title="Bold"
205
+ class="bold"
206
+ onmousedown={(event) => {
207
+ event.preventDefault();
208
+ session.commands.toggle_strong.execute();
209
+ }}
210
+ disabled={session.commands.toggle_strong.disabled}
211
+ class:active={session.commands.toggle_strong.active}
212
+ >
213
+ B
214
+ </button>
215
+ {/if}
216
+ {#if session.commands.toggle_emphasis}
217
+ <button
218
+ title="Italic"
219
+ class="italic"
220
+ onmousedown={(event) => {
221
+ event.preventDefault();
222
+ session.commands.toggle_emphasis.execute();
223
+ }}
224
+ disabled={session.commands.toggle_emphasis.disabled}
225
+ class:active={session.commands.toggle_emphasis.active}
226
+ >
227
+ I
228
+ </button>
229
+ {/if}
230
+ {#if session.commands.toggle_inline_code}
231
+ <button
232
+ title="Code"
233
+ class="code"
234
+ onmousedown={(event) => {
235
+ event.preventDefault();
236
+ session.commands.toggle_inline_code.execute();
237
+ }}
238
+ disabled={session.commands.toggle_inline_code.disabled}
239
+ class:active={session.commands.toggle_inline_code.active}
240
+ >
241
+ &lt;&gt;
242
+ </button>
243
+ {/if}
244
+ {#if session.commands.toggle_highlight}
245
+ <button
246
+ title="Highlight"
247
+ class="highlight"
248
+ onmousedown={(event) => {
249
+ event.preventDefault();
250
+ session.commands.toggle_highlight.execute();
251
+ }}
252
+ disabled={session.commands.toggle_highlight.disabled}
253
+ class:active={session.commands.toggle_highlight.active}
254
+ >
255
+ H
256
+ </button>
257
+ {/if}
258
+ {#if session.commands.toggle_strikethrough}
259
+ <button
260
+ title="Strikethrough"
261
+ class="strikethrough"
262
+ onmousedown={(event) => {
263
+ event.preventDefault();
264
+ session.commands.toggle_strikethrough.execute();
265
+ }}
266
+ disabled={session.commands.toggle_strikethrough.disabled}
267
+ class:active={session.commands.toggle_strikethrough.active}
268
+ >
269
+ S
270
+ </button>
271
+ {/if}
272
+ {/snippet}
273
+
274
+ {#snippet insert_button()}
275
+ <button title="Insert (↵)" onmousedown={insert_default_node} disabled={!can_insert_default}>
276
+ <svg
277
+ class="toolbar-icon"
278
+ xmlns="http://www.w3.org/2000/svg"
279
+ viewBox="0 0 15 15"
280
+ fill="none"
281
+ aria-hidden="true"
282
+ >
283
+ <path d="M7.5 3V12M3 7.5H12" stroke="currentColor" stroke-linecap="square" />
284
+ </svg>
285
+ </button>
286
+ {/snippet}
287
+
288
+ {#snippet delete_button()}
289
+ <button title="Delete backwards (⌫)" onmousedown={delete_node_selection} disabled={!can_delete}
290
+ >&#9003;</button
291
+ >
292
+ {/snippet}
293
+
294
+ <!-- Rendered unconditionally: commands register only after Svedit has
295
+ initialized, and the persistent bottom bar must not reflow on first
296
+ paint when undo/redo appear. Until then the buttons are disabled. -->
297
+ {#snippet history_buttons()}
298
+ <button
299
+ title="Undo"
300
+ onmousedown={(event) => {
301
+ event.preventDefault();
302
+ session.commands.undo?.execute();
303
+ }}
304
+ disabled={session.commands.undo?.disabled ?? true}
305
+ >
306
+ &#8630;
307
+ </button>
308
+ <button
309
+ title="Redo"
310
+ onmousedown={(event) => {
311
+ event.preventDefault();
312
+ session.commands.redo?.execute();
313
+ }}
314
+ disabled={session.commands.redo?.disabled ?? true}
315
+ >
316
+ &#8631;
317
+ </button>
318
+ {/snippet}
319
+
320
+ <svelte:window
321
+ onpointerdown={handle_window_pointerdown}
322
+ onpointerup={handle_window_pointerup}
323
+ onpointercancel={handle_window_pointerup}
324
+ />
325
+
326
+ {#if floating_anchor && !is_dragging}
327
+ <!-- Re-mount the toolbar whenever the anchor changes: swapping
328
+ position-anchor on a persistent fixed element can leave it at the
329
+ stale position of the previous anchor. -->
330
+ {#key floating_anchor.name}
331
+ <div
332
+ class="editor-toolbar floating-toolbar"
333
+ class:has-last-node-anchor={Boolean(floating_anchor.last_node_anchor)}
334
+ style="position-anchor: --{floating_anchor.name};{floating_anchor.last_node_anchor
335
+ ? ` --last-selected-node-anchor: --${floating_anchor.last_node_anchor};`
336
+ : ''}"
337
+ >
338
+ <div class="toolbar-scroller">
339
+ <!-- Only the tools that belong to the current selection context: text
340
+ selections get text tools, node/property selections just get
341
+ select-parent + delete (no per-type navigator in this project yet). -->
342
+ {#if selection_type === 'text'}
343
+ {@render select_parent_button()}
344
+ {@render mark_buttons()}
345
+ {:else if selection_type === 'node' || selection_type === 'property'}
346
+ {@render select_parent_button()}
347
+ {@render delete_button()}
348
+ {/if}
349
+ </div>
350
+ </div>
351
+ {/key}
352
+ {/if}
353
+
354
+ <div class="editor-toolbar bottom-toolbar" style:--keyboard-inset="{keyboard_inset}px">
355
+ <div class="toolbar-scroller">
356
+ {#if editable}
357
+ <!-- Same contextual composition as the floating toolbar: text tools for
358
+ text selections, just the insert button at node gaps, select-parent
359
+ + delete for node/property selections. -->
360
+ <div class="contextual-tools">
361
+ {#if selection_type === 'text'}
362
+ {@render select_parent_button()}
363
+ {@render mark_buttons()}
364
+ {@render divider()}
365
+ {:else if is_node_caret}
366
+ {#if can_insert_default}
367
+ {@render insert_button()}
368
+ {@render divider()}
369
+ {/if}
370
+ {:else if selection_type === 'node' || selection_type === 'property'}
371
+ {@render select_parent_button()}
372
+ {@render divider()}
373
+ {/if}
374
+ </div>
375
+ {@render history_buttons()}
376
+ {#if can_delete}
377
+ <!-- Destructive action last, behind undo/redo: harder to hit by
378
+ accident while scrolling the bar, and undo sits right next to
379
+ it when it does happen. -->
380
+ <div class="contextual-tools">
381
+ {@render divider()}
382
+ {@render delete_button()}
383
+ </div>
384
+ {/if}
385
+ {/if}
386
+ <!-- Save pinned to the right edge of the scroller so it never needs
387
+ scrolling. -->
388
+ <div class="save-group" class:has-leading-tools={editable}>
389
+ {#if editable}
390
+ {@render divider()}
391
+ {/if}
392
+ <button class="toggle-editable" onclick={toggle_editable}>
393
+ {editable ? 'Save' : 'Edit'}
394
+ </button>
395
+ </div>
396
+ </div>
397
+ </div>
398
+
399
+ <style>
400
+ /* Both toolbars share one unified pill container: a single surface,
401
+ border and shadow instead of per-button bubbles. The pill itself does
402
+ not scroll — scrolling lives in the unpadded inner scroller below, so
403
+ pinned (sticky) tools sit at the exact scrollport edge and scrolled
404
+ content can never leak into the pill's padding or rounded corners. */
405
+ .editor-toolbar {
406
+ display: flex;
407
+ align-items: center;
408
+ width: fit-content;
409
+ padding: 4px;
410
+ color: var(--foreground, #111);
411
+ background: var(--background, #fff);
412
+ border: 1px solid oklch(from var(--foreground, #111) l c h / 0.12);
413
+ border-radius: 9999px;
414
+ box-shadow:
415
+ 0 1px 2px oklch(0% 0 0 / 0.12),
416
+ 0 4px 16px oklch(0% 0 0 / 0.08);
417
+ z-index: 50;
418
+ pointer-events: auto;
419
+ max-width: calc(100vw - 2 * var(--s-4, 16px));
420
+ }
421
+
422
+ .toolbar-scroller {
423
+ display: flex;
424
+ flex-direction: row;
425
+ align-items: center;
426
+ /* No gap between buttons: adjacent hitboxes tile the toolbar without
427
+ dead zones, the visual spacing comes from the icon padding inside
428
+ each 36px button. */
429
+ gap: 0;
430
+ min-width: 0;
431
+ overflow-x: auto;
432
+ scrollbar-width: none;
433
+ border-radius: 9999px;
434
+ }
435
+
436
+ .bottom-toolbar {
437
+ position: fixed;
438
+ bottom: max(var(--s-4, 16px), env(safe-area-inset-bottom, 0px));
439
+ right: var(--s-4, 16px);
440
+ }
441
+
442
+ @position-try --stay-in-viewport {
443
+ position-area: none;
444
+ position-anchor: none;
445
+ top: calc(var(--top-toolbar-safe-area, 0px) + var(--s-2, 8px));
446
+ right: auto;
447
+ bottom: auto;
448
+ left: auto;
449
+ }
450
+
451
+ /* Multi-node selection: when above the first node overflows, sit below
452
+ the last selected node instead (--last-selected-node-anchor). */
453
+ @position-try --below-last-node {
454
+ position-anchor: var(--last-selected-node-anchor);
455
+ bottom: auto;
456
+ top: anchor(bottom);
457
+ margin-bottom: 0;
458
+ margin-top: var(--s-2, 8px);
459
+ }
460
+
461
+ .floating-toolbar {
462
+ position: fixed;
463
+ bottom: anchor(top);
464
+ justify-self: anchor-center;
465
+ margin-bottom: var(--s-2, 8px);
466
+ position-visibility: always;
467
+ position-try-fallbacks: --stay-in-viewport, flip-block;
468
+ z-index: 60;
469
+ }
470
+
471
+ .floating-toolbar.has-last-node-anchor {
472
+ position-try-fallbacks: --below-last-node, --stay-in-viewport, flip-block;
473
+ }
474
+
475
+ /* The floating toolbar targets precise mouse interactions. On touch
476
+ devices all tools live in the single bottom toolbar instead, so the
477
+ virtual keyboard handling only has one element to care about. */
478
+ @media (hover: none), (pointer: coarse) {
479
+ .floating-toolbar {
480
+ display: none;
481
+ }
482
+
483
+ .bottom-toolbar {
484
+ right: auto;
485
+ left: 50%;
486
+ transform: translateX(-50%) translateY(calc(-1 * var(--keyboard-inset, 0px)));
487
+ }
488
+ }
489
+
490
+ .contextual-tools {
491
+ display: contents;
492
+ }
493
+
494
+ @media (hover: hover) and (pointer: fine) {
495
+ .bottom-toolbar .contextual-tools {
496
+ display: none;
497
+ }
498
+ }
499
+
500
+ .editor-toolbar .save-group {
501
+ position: sticky;
502
+ right: 0;
503
+ z-index: 1;
504
+ display: flex;
505
+ align-items: center;
506
+ flex: none;
507
+ background: var(--background, #fff);
508
+ }
509
+
510
+ .editor-toolbar .save-group.has-leading-tools {
511
+ margin-inline-start: 4px;
512
+ }
513
+
514
+ .editor-toolbar .save-group .divider {
515
+ margin-inline-start: 0;
516
+ }
517
+
518
+ .editor-toolbar .divider {
519
+ flex: none;
520
+ width: 1px;
521
+ height: 20px;
522
+ margin-inline: 4px;
523
+ background: oklch(from var(--foreground, #111) l c h / 0.15);
524
+ }
525
+
526
+ .editor-toolbar button:not(.toggle-editable) {
527
+ display: flex;
528
+ align-items: center;
529
+ justify-content: center;
530
+ box-sizing: border-box;
531
+ width: 36px;
532
+ height: 36px;
533
+ min-width: 36px;
534
+ min-height: 36px;
535
+ aspect-ratio: 1 / 1;
536
+ padding: 0;
537
+ flex: 0 0 36px;
538
+ border: none;
539
+ border-radius: 50%;
540
+ background: transparent;
541
+ color: var(--foreground, #111);
542
+ font-size: 15px;
543
+ font-weight: 600;
544
+ text-wrap: nowrap;
545
+ cursor: pointer;
546
+ pointer-events: auto;
547
+ transition:
548
+ background 150ms,
549
+ transform 150ms;
550
+ outline: 1px solid transparent;
551
+ position: relative;
552
+ }
553
+
554
+ @media (hover: hover) {
555
+ .editor-toolbar button:not(.toggle-editable):hover:not(:disabled) {
556
+ background: oklch(from var(--foreground, #111) l c h / 0.06);
557
+ }
558
+ }
559
+
560
+ .editor-toolbar button:not(.toggle-editable):active:not(:disabled) {
561
+ background: oklch(from var(--foreground, #111) l c h / 0.09);
562
+ transform: translateY(1px) scale(0.95);
563
+ }
564
+
565
+ .editor-toolbar button:not(.toggle-editable):focus-visible {
566
+ outline: none;
567
+ box-shadow: inset 0 0 0 1px var(--editing, #2563eb);
568
+ }
569
+
570
+ .editor-toolbar button:not(.toggle-editable):disabled {
571
+ background: transparent;
572
+ cursor: not-allowed;
573
+ color: oklch(from var(--foreground, #111) l c h / 0.3);
574
+ }
575
+
576
+ .editor-toolbar button:not(.toggle-editable).active {
577
+ color: var(--editing, #2563eb);
578
+ background: var(--editing-muted, oklch(0.6 0.15 250 / 0.12));
579
+ }
580
+
581
+ /* Select-parent pinned to the left edge of the scroller, mirroring the
582
+ keyboard tools on the right. */
583
+ .editor-toolbar .select-parent-group {
584
+ position: sticky;
585
+ left: 0;
586
+ z-index: 1;
587
+ display: flex;
588
+ align-items: center;
589
+ flex: none;
590
+ background: var(--background, #fff);
591
+ margin-inline-end: 4px;
592
+ }
593
+
594
+ .editor-toolbar .select-parent-group .divider {
595
+ margin-inline-end: 0;
596
+ }
597
+
598
+ .toggle-editable {
599
+ display: inline-flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ box-sizing: border-box;
603
+ height: 36px;
604
+ min-height: 36px;
605
+ flex: none;
606
+ padding: 0 1rem;
607
+ border: none;
608
+ border-radius: 9999px;
609
+ background: transparent;
610
+ color: var(--editing, #2563eb);
611
+ font-size: 0.875rem;
612
+ font-weight: 600;
613
+ line-height: 1;
614
+ text-decoration: none;
615
+ cursor: pointer;
616
+ pointer-events: auto;
617
+ transition:
618
+ background 150ms,
619
+ transform 150ms;
620
+ outline: 1px solid transparent;
621
+ }
622
+
623
+ @media (hover: hover) {
624
+ .toggle-editable:hover {
625
+ background: oklch(from var(--editing, #2563eb) l c h / 0.08);
626
+ }
627
+ }
628
+
629
+ .toggle-editable:active {
630
+ background: oklch(from var(--editing, #2563eb) l c h / 0.12);
631
+ transform: translateY(1px) scale(0.97);
632
+ }
633
+
634
+ .toggle-editable:focus-visible {
635
+ outline: none;
636
+ box-shadow: inset 0 0 0 1px var(--editing, #2563eb);
637
+ }
638
+
639
+ .editor-toolbar .toolbar-icon {
640
+ width: var(--icon-size, 18px);
641
+ height: var(--icon-size, 18px);
642
+ color: currentColor;
643
+ }
644
+ </style>