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.
- package/README.md +23 -17
- package/bin.js +87 -30
- package/cli.js +15 -0
- package/config.js +102 -16
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +42 -7
- package/plugins/assets/index.js +65 -0
- package/plugins/atproto/index.js +132 -0
- package/plugins/atproto/password.js +71 -0
- package/plugins/atproto/publish.js +406 -0
- package/plugins/atproto/record.js +144 -0
- package/plugins/atproto/rkey.js +54 -0
- package/plugins/deploy-cloudflare/index.js +92 -0
- package/plugins/fonts/index.js +18 -21
- package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/index.js +21145 -0
- package/plugins/html/editor/EditButton.svelte +152 -0
- package/plugins/html/editor/Editor.svelte +51 -0
- package/plugins/html/editor/Overlays.svelte +6 -0
- package/plugins/html/editor/SettingsPanel.svelte +397 -0
- package/plugins/html/editor/Toolbar.svelte +644 -0
- package/plugins/html/editor/create-session.js +250 -0
- package/plugins/html/editor/directives.js +128 -0
- package/plugins/html/editor/frontmatter.js +97 -0
- package/plugins/html/editor/ingest.js +339 -0
- package/plugins/html/editor/main.js +94 -0
- package/plugins/html/editor/nodes/Alert.svelte +21 -0
- package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
- package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
- package/plugins/html/editor/nodes/Embed.svelte +19 -0
- package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
- package/plugins/html/editor/nodes/Heading.svelte +15 -0
- package/plugins/html/editor/nodes/Highlight.svelte +11 -0
- package/plugins/html/editor/nodes/Image.svelte +18 -0
- package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
- package/plugins/html/editor/nodes/Link.svelte +17 -0
- package/plugins/html/editor/nodes/List.svelte +15 -0
- package/plugins/html/editor/nodes/ListItem.svelte +9 -0
- package/plugins/html/editor/nodes/Page.svelte +21 -0
- package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
- package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
- package/plugins/html/editor/nodes/Strong.svelte +11 -0
- package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
- package/plugins/html/editor/nodes/index.js +18 -0
- package/plugins/html/editor/rollup.config.js +35 -0
- package/plugins/html/editor/serialize.js +157 -0
- package/plugins/html/editor/settings-file.js +83 -0
- package/plugins/html/editor/socket.js +202 -0
- package/plugins/html/editor/source.js +43 -0
- package/plugins/html/image.js +104 -0
- package/plugins/html/index.js +760 -0
- package/plugins/html/partials.js +299 -0
- package/plugins/html/socialLinks.js +67 -0
- package/plugins/html/writeRules.js +377 -0
- package/plugins/icons/index.js +2 -6
- package/plugins/images/index.js +78 -55
- package/plugins/markdown/brandColors.js +10 -0
- package/plugins/markdown/colorScheme.js +64 -0
- package/plugins/markdown/emoji.js +30 -0
- package/plugins/markdown/frontmatter.js +253 -0
- package/plugins/markdown/index.js +287 -1274
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +308 -0
- package/plugins/markdown/readRules.js +143 -0
- package/plugins/markdown/typography.js +109 -0
- package/plugins/markdown/visitor.js +143 -0
- package/plugins/publish/index.js +34 -0
- package/plugins/robots/agents.js +79 -0
- package/plugins/robots/index.js +30 -10
- package/plugins/robots/robots.js +48 -0
- package/plugins/styles/index.js +111 -26
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +78 -0
- package/plugins/urls/urlMetadata.js +246 -0
- package/plugins/vectors/index.js +5 -9
- package/plugins/xml/entryContent.js +103 -0
- package/plugins/xml/index.js +123 -50
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +269 -0
- package/stylesheets/TypographyStyles.css +386 -128
- package/systemPaths.js +48 -0
- package/utils.js +56 -5
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
- package/.github/ISSUE_TEMPLATE/config.yml +0 -7
- package/docs-source/.cache.json +0 -9
- package/docs-source/.obsidian/app.json +0 -3
- package/docs-source/.obsidian/appearance.json +0 -3
- package/docs-source/.obsidian/core-plugins-migration.json +0 -30
- package/docs-source/.obsidian/core-plugins.json +0 -20
- package/docs-source/.obsidian/workspace.json +0 -168
- package/docs-source/.stackblitzrc +0 -3
- package/docs-source/.votive.db +0 -0
- package/docs-source/about.md +0 -3
- package/docs-source/blog/home.md +0 -5
- package/docs-source/blog/url-ui.md +0 -21
- package/docs-source/blog.md +0 -5
- package/docs-source/docs/.votive.db +0 -0
- package/docs-source/docs/deploy.md +0 -67
- package/docs-source/docs/file-structure.md +0 -31
- package/docs-source/docs/folder-settings.md +0 -23
- package/docs-source/docs/home.md +0 -55
- package/docs-source/docs/images.md +0 -10
- package/docs-source/docs/items.md +0 -13
- package/docs-source/docs/pages.md +0 -141
- package/docs-source/docs/settings.md +0 -4
- package/docs-source/docs/styling.md +0 -34
- package/docs-source/docs/taxonomies.md +0 -37
- package/docs-source/features/cards.md +0 -7
- package/docs-source/features/editing.md +0 -7
- package/docs-source/features/emoji.md +0 -7
- package/docs-source/features/frontmatter.md +0 -7
- package/docs-source/features/lists.md +0 -7
- package/docs-source/features/navigation.md +0 -7
- package/docs-source/features/rich-previews.md +0 -7
- package/docs-source/features/robots.md +0 -7
- package/docs-source/features/rss.md +0 -7
- package/docs-source/features/sitemap.md +0 -7
- package/docs-source/features/speed.md +0 -7
- package/docs-source/features/static.md +0 -7
- package/docs-source/features/taxonomies.md +0 -7
- package/docs-source/home.md +0 -42
- package/docs-source/output/about.html +0 -1
- package/docs-source/output/blog/url-ui.html +0 -8
- package/docs-source/output/blog.html +0 -1
- package/docs-source/output/default.css +0 -1
- package/docs-source/output/docs/deploy.html +0 -34
- package/docs-source/output/docs/file-structure.html +0 -24
- package/docs-source/output/docs/folder-settings.html +0 -20
- package/docs-source/output/docs/images.html +0 -2
- package/docs-source/output/docs/items.html +0 -6
- package/docs-source/output/docs/pages.html +0 -101
- package/docs-source/output/docs/styling.html +0 -18
- package/docs-source/output/docs/taxonomies.html +0 -20
- package/docs-source/output/docs.html +0 -28
- package/docs-source/output/features/cards.html +0 -1
- package/docs-source/output/features/editing.html +0 -1
- package/docs-source/output/features/emoji.html +0 -1
- package/docs-source/output/features/frontmatter.html +0 -1
- package/docs-source/output/features/lists.html +0 -1
- package/docs-source/output/features/navigation.html +0 -1
- package/docs-source/output/features/rich-previews.html +0 -1
- package/docs-source/output/features/robots.html +0 -1
- package/docs-source/output/features/rss.html +0 -1
- package/docs-source/output/features/sitemap.html +0 -1
- package/docs-source/output/features/speed.html +0 -1
- package/docs-source/output/features/static.html +0 -1
- package/docs-source/output/features/taxonomies.html +0 -1
- package/docs-source/output/features.html +0 -1
- package/docs-source/output/feed.xml +0 -1
- package/docs-source/output/index.html +0 -21
- package/docs-source/output/reset.css +0 -1
- package/docs-source/output/roadmap.html +0 -87
- package/docs-source/output/robots.txt +0 -14
- package/docs-source/output/sitemap.xml +0 -16
- package/docs-source/output/typography.css +0 -1
- package/docs-source/roadmap.md +0 -98
- package/docs-source/settings.md +0 -13
- package/getMetadata.js +0 -41
- package/jsconfig.json +0 -10
- package/regex.js +0 -36
- 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}>↖</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
|
+
<>
|
|
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
|
+
>⌫</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
|
+
↶
|
|
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
|
+
↷
|
|
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>
|