vowel 0.6.4 → 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 +1 -1
- package/bin.js +47 -27
- package/cli.js +15 -0
- package/config.js +93 -26
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +39 -8
- 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 +30 -16
- 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 +31 -5
- package/plugins/html/index.js +430 -458
- 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 +276 -334
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +229 -68
- 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 -18
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +49 -62
- 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 +105 -43
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +36 -4
- 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/plugins/markdown/robots.js +0 -18
- package/regex.js +0 -36
- package/stylesheets/brand-colors.css +0 -119
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// The editor's entry point, and the only thing a previewed page carries
|
|
3
|
+
// until it is pressed: everything Svedit needs is reached through
|
|
4
|
+
// onedit (see main.js's startEditing), not from here.
|
|
5
|
+
//
|
|
6
|
+
// Styled as the same pill as Toolbar.svelte's bottom bar, which takes
|
|
7
|
+
// this corner over once editing starts - so pressing Edit reads as the
|
|
8
|
+
// button becoming the toolbar rather than as one widget replacing
|
|
9
|
+
// another.
|
|
10
|
+
let { onedit, onsettings } = $props();
|
|
11
|
+
|
|
12
|
+
let editing = $state(false);
|
|
13
|
+
|
|
14
|
+
// Non-empty once a press found elements ingest could not read. The page
|
|
15
|
+
// can't be edited at all in that case (ingest fails whole-document),
|
|
16
|
+
// so the button stays put and says so instead of disappearing.
|
|
17
|
+
let unrecognised = $state([]);
|
|
18
|
+
|
|
19
|
+
function start() {
|
|
20
|
+
const failed = onedit();
|
|
21
|
+
|
|
22
|
+
if (failed.length) {
|
|
23
|
+
unrecognised = failed;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
editing = true;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#if !editing}
|
|
32
|
+
<div class="edit-launcher" data-vowel-client>
|
|
33
|
+
<button
|
|
34
|
+
class="edit settings"
|
|
35
|
+
onclick={onsettings}
|
|
36
|
+
title="Typography and theme settings"
|
|
37
|
+
>
|
|
38
|
+
Settings
|
|
39
|
+
</button>
|
|
40
|
+
<span class="divider" aria-hidden="true"></span>
|
|
41
|
+
<button
|
|
42
|
+
class="edit"
|
|
43
|
+
onclick={start}
|
|
44
|
+
disabled={unrecognised.length > 0}
|
|
45
|
+
title={unrecognised.length
|
|
46
|
+
? `This page can't be edited yet: ${unrecognised.join(', ')}`
|
|
47
|
+
: 'Edit this page'}
|
|
48
|
+
>
|
|
49
|
+
{unrecognised.length ? "Can't edit" : 'Edit'}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
/* Same surface as .editor-toolbar in Toolbar.svelte. Duplicated rather
|
|
56
|
+
than shared: scoped styles don't cross components, and a global
|
|
57
|
+
stylesheet injected into someone else's page is a collision waiting
|
|
58
|
+
to happen. */
|
|
59
|
+
.edit-launcher {
|
|
60
|
+
position: fixed;
|
|
61
|
+
bottom: max(var(--s-4, 16px), env(safe-area-inset-bottom, 0px));
|
|
62
|
+
right: var(--s-4, 16px);
|
|
63
|
+
z-index: 50;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
width: fit-content;
|
|
67
|
+
padding: 4px;
|
|
68
|
+
color: var(--foreground, #111);
|
|
69
|
+
background: var(--background, #fff);
|
|
70
|
+
border: 1px solid oklch(from var(--foreground, #111) l c h / 0.12);
|
|
71
|
+
border-radius: 9999px;
|
|
72
|
+
box-shadow:
|
|
73
|
+
0 1px 2px oklch(0% 0 0 / 0.12),
|
|
74
|
+
0 4px 16px oklch(0% 0 0 / 0.08);
|
|
75
|
+
pointer-events: auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Matches .toggle-editable, down to leaving font-family alone: the
|
|
79
|
+
toolbar's Save button inherits the UA button font too, and the two
|
|
80
|
+
sit in the same corner one after the other. */
|
|
81
|
+
.edit {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
height: 36px;
|
|
87
|
+
min-height: 36px;
|
|
88
|
+
flex: none;
|
|
89
|
+
padding: 0 1rem;
|
|
90
|
+
border: none;
|
|
91
|
+
border-radius: 9999px;
|
|
92
|
+
background: transparent;
|
|
93
|
+
color: var(--editing, #2563eb);
|
|
94
|
+
font-size: 0.875rem;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
pointer-events: auto;
|
|
99
|
+
transition:
|
|
100
|
+
background 150ms,
|
|
101
|
+
transform 150ms;
|
|
102
|
+
outline: 1px solid transparent;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (hover: hover) {
|
|
106
|
+
.edit:hover:not(:disabled) {
|
|
107
|
+
background: oklch(from var(--editing, #2563eb) l c h / 0.08);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.edit:active:not(:disabled) {
|
|
112
|
+
background: oklch(from var(--editing, #2563eb) l c h / 0.12);
|
|
113
|
+
transform: translateY(1px) scale(0.97);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.edit:focus-visible {
|
|
117
|
+
outline: none;
|
|
118
|
+
box-shadow: inset 0 0 0 1px var(--editing, #2563eb);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Separates the two actions inside the one pill, the same hairline
|
|
122
|
+
Toolbar.svelte uses between tool groups. */
|
|
123
|
+
.divider {
|
|
124
|
+
flex: none;
|
|
125
|
+
width: 1px;
|
|
126
|
+
height: 20px;
|
|
127
|
+
margin-inline: 2px;
|
|
128
|
+
background: oklch(from var(--foreground, #111) l c h / 0.15);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Secondary next to Edit: the same pill, in the text colour rather
|
|
132
|
+
than the accent, so Edit stays the one obvious action. */
|
|
133
|
+
.settings {
|
|
134
|
+
color: var(--foreground, #111);
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@media (hover: hover) {
|
|
139
|
+
.settings:hover:not(:disabled) {
|
|
140
|
+
background: oklch(from var(--foreground, #111) l c h / 0.06);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.settings:active:not(:disabled) {
|
|
145
|
+
background: oklch(from var(--foreground, #111) l c h / 0.09);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.edit:disabled {
|
|
149
|
+
color: oklch(from var(--foreground, #111) l c h / 0.4);
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { setContext } from "svelte"
|
|
3
|
+
import { Svedit, KeyMapper } from "svedit"
|
|
4
|
+
import serialize from "./serialize.js"
|
|
5
|
+
import Toolbar from "./Toolbar.svelte"
|
|
6
|
+
import { getSource } from "./source.js"
|
|
7
|
+
import { splitFrontmatter, writeSettings as writeFile } from "./settings-file.js"
|
|
8
|
+
|
|
9
|
+
// Both props are read off the rendered page before this mounts, by
|
|
10
|
+
// main.js's startEditing: the session from section#content, the title
|
|
11
|
+
// and frontmatter from the <main> around it. Ingesting has to happen
|
|
12
|
+
// while the server output is still there, and section#content has to be
|
|
13
|
+
// emptied before this component is mounted into it - which is why
|
|
14
|
+
// neither belongs here.
|
|
15
|
+
//
|
|
16
|
+
// The frontmatter round trips unchanged for now; carrying it is what
|
|
17
|
+
// lets a save reproduce the whole file rather than just the body.
|
|
18
|
+
let { session, frontmatter } = $props()
|
|
19
|
+
|
|
20
|
+
// The button that mounted this said Edit, so editing is already on and
|
|
21
|
+
// the toolbar's own toggle reads Save.
|
|
22
|
+
let editable = $state(true)
|
|
23
|
+
|
|
24
|
+
const key_mapper = new KeyMapper()
|
|
25
|
+
setContext("key_mapper", key_mapper)
|
|
26
|
+
|
|
27
|
+
// Frontmatter from the source, body from the editor. The source comes
|
|
28
|
+
// with the page (see source.js) and is refreshed by every rebuild the
|
|
29
|
+
// socket pushes, so it is read at save time rather than when the
|
|
30
|
+
// editor opened. Taking the frontmatter from the source rather than
|
|
31
|
+
// the rendered page is what lets a key that never renders survive a
|
|
32
|
+
// save. The title is the one key that isn't kept: it is written back
|
|
33
|
+
// as a `#` heading, never as `title:`, so a file whose title lived
|
|
34
|
+
// only in frontmatter gains a heading and loses the key.
|
|
35
|
+
async function save() {
|
|
36
|
+
const { path, markdown: text } = getSource()
|
|
37
|
+
if (!path || typeof text !== "string") {
|
|
38
|
+
console.error("[vowel] not saved: this page has no source to save to")
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const { title: ignoredTitle, ...properties } = splitFrontmatter(text).data
|
|
42
|
+
const markdown = serialize(session.doc, { title: frontmatter.title, properties })
|
|
43
|
+
await writeFile(path, markdown)
|
|
44
|
+
console.info("[vowel] saved %s", path)
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<svelte:window onkeydown={key_mapper.handle_keydown.bind(key_mapper)} />
|
|
49
|
+
|
|
50
|
+
<Svedit {session} bind:editable path={[session.doc.document_id]} />
|
|
51
|
+
<Toolbar {session} {save} bind:editable />
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// The settings drawer.
|
|
3
|
+
//
|
|
4
|
+
// Full height down the left edge, and deliberately **not** modal: no
|
|
5
|
+
// backdrop, the page behind stays scrollable and clickable. Live
|
|
6
|
+
// preview is the whole point - you drag a slider and watch the
|
|
7
|
+
// headings resize - so anything that blocks the page would defeat it.
|
|
8
|
+
//
|
|
9
|
+
// Every control is derived from data rather than hardcoded: the site
|
|
10
|
+
// fields from `siteFields` below, the typography controls from the
|
|
11
|
+
// same table the build emits type.css from (typography.js), so the
|
|
12
|
+
// variables the panel writes for preview are the ones the sheet reads.
|
|
13
|
+
import { rampSettings, typographyVariables } from '../../markdown/typography.js';
|
|
14
|
+
import { fallbackColors } from '../../markdown/brandColors.js';
|
|
15
|
+
import { readSettings, joinFrontmatter, themeObject, writeSettings } from './settings-file.js';
|
|
16
|
+
|
|
17
|
+
let { onclose } = $props();
|
|
18
|
+
|
|
19
|
+
let loading = $state(true);
|
|
20
|
+
let error = $state(null);
|
|
21
|
+
let saving = $state(false);
|
|
22
|
+
|
|
23
|
+
// The whole file, so a save rewrites only what changed and leaves
|
|
24
|
+
// every other key - and the body - alone.
|
|
25
|
+
let file = $state({ data: {}, body: '' });
|
|
26
|
+
|
|
27
|
+
// Held apart from `data` until a theme field is actually edited, so a
|
|
28
|
+
// site that wrote `theme: default` keeps that spelling unless it has a
|
|
29
|
+
// reason not to. Editing only the title must not silently rewrite the
|
|
30
|
+
// theme into its object form.
|
|
31
|
+
let editedTheme = $state(null);
|
|
32
|
+
|
|
33
|
+
const theme = $derived(editedTheme ?? themeObject(file.data.theme));
|
|
34
|
+
const colors = $derived([0, 1].map((index) => theme.colors?.[index] ?? fallbackColors[index]));
|
|
35
|
+
|
|
36
|
+
// Top-level frontmatter keys, in the order they read as a form. Only
|
|
37
|
+
// settings vowel actually consumes are here - `author` is in both demo
|
|
38
|
+
// settings.md files and is read by nothing, so it isn't offered.
|
|
39
|
+
const siteFields = [
|
|
40
|
+
{ key: 'name', label: 'Name', hint: 'The site name, shown in the header' },
|
|
41
|
+
{ key: 'breadcrumb', label: 'Breadcrumb', hint: 'Label for the site root in navigation' },
|
|
42
|
+
{ key: 'domain', label: 'Domain', hint: 'Needed for the sitemap and feed' },
|
|
43
|
+
{ key: 'icon', label: 'Icon', hint: 'An emoji, or a path to an image' },
|
|
44
|
+
{ key: 'logo', label: 'Logo', hint: 'Path to an SVG or image' },
|
|
45
|
+
{ key: 'wordmark', label: 'Wordmark', hint: 'Path to an SVG or image' }
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Setting a variable on the document is the whole of live preview:
|
|
49
|
+
// TypographyStyles.css computes every heading level from these, so
|
|
50
|
+
// the ramp recomputes as a slider moves. A key the author unsets goes
|
|
51
|
+
// back to the sheet's default by removing the override.
|
|
52
|
+
const preview = $derived(typographyVariables(theme));
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
const root = document.documentElement;
|
|
56
|
+
const set = new Set(preview.map(([name]) => name));
|
|
57
|
+
for (const { variable } of rampSettings) {
|
|
58
|
+
if (!set.has(variable)) root.style.removeProperty(variable);
|
|
59
|
+
}
|
|
60
|
+
for (const [name, value] of preview) root.style.setProperty(name, value);
|
|
61
|
+
// The colour scheme needs a rebuild to see: the ramps are generated
|
|
62
|
+
// by colorhorse on the server.
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
readSettings()
|
|
67
|
+
.then((result) => (file = result))
|
|
68
|
+
.catch((e) => (error = e.message))
|
|
69
|
+
.finally(() => (loading = false));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function setField(key, value) {
|
|
73
|
+
const data = { ...file.data };
|
|
74
|
+
if (value === '') delete data[key];
|
|
75
|
+
else data[key] = value;
|
|
76
|
+
file = { ...file, data };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setTheme(key, value) {
|
|
80
|
+
// Clearing a field removes the key rather than writing `undefined`,
|
|
81
|
+
// so the sheet's default takes over and settings.md stays clean.
|
|
82
|
+
const { [key]: _, ...rest } = theme;
|
|
83
|
+
editedTheme = value === undefined ? rest : { ...rest, [key]: value };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Both seeds are written together - colorhorse takes a pair. */
|
|
87
|
+
function setColor(index, value) {
|
|
88
|
+
setTheme('colors', colors.map((current, position) => (position === index ? value : current)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function save() {
|
|
92
|
+
saving = true;
|
|
93
|
+
error = null;
|
|
94
|
+
|
|
95
|
+
const data = editedTheme ? { ...file.data, theme: editedTheme } : file.data;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await writeSettings('settings.md', joinFrontmatter(data, file.body));
|
|
99
|
+
// No reload: the write lands in sourceFolder, the watcher
|
|
100
|
+
// rebuilds and votive's live-reload client patches the page - the
|
|
101
|
+
// same path a hand edit takes.
|
|
102
|
+
onclose();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
error = e.message;
|
|
105
|
+
} finally {
|
|
106
|
+
saving = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<aside class="settings-drawer" aria-label="Site settings" data-vowel-client>
|
|
112
|
+
<header>
|
|
113
|
+
<h2>Settings</h2>
|
|
114
|
+
<button class="close" onclick={onclose} title="Close settings">✕</button>
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
{#if loading}
|
|
118
|
+
<p class="note">Reading settings.md…</p>
|
|
119
|
+
{:else}
|
|
120
|
+
<section>
|
|
121
|
+
<h3>Site</h3>
|
|
122
|
+
{#each siteFields as field (field.key)}
|
|
123
|
+
<label class="field">
|
|
124
|
+
<span>{field.label}</span>
|
|
125
|
+
<input
|
|
126
|
+
type="text"
|
|
127
|
+
value={file.data[field.key] ?? ''}
|
|
128
|
+
placeholder={field.hint}
|
|
129
|
+
title={field.hint}
|
|
130
|
+
oninput={(event) => setField(field.key, event.currentTarget.value)}
|
|
131
|
+
/>
|
|
132
|
+
</label>
|
|
133
|
+
{/each}
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<section>
|
|
137
|
+
<h3>Colors</h3>
|
|
138
|
+
<p class="note">Two seeds; every shade on the site is generated from them.</p>
|
|
139
|
+
{#each ['One', 'Two'] as label, index (label)}
|
|
140
|
+
<label class="field color">
|
|
141
|
+
<span>{label}</span>
|
|
142
|
+
<input
|
|
143
|
+
type="color"
|
|
144
|
+
value={colors[index]}
|
|
145
|
+
oninput={(event) => setColor(index, event.currentTarget.value)}
|
|
146
|
+
/>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
class="hex"
|
|
150
|
+
value={colors[index]}
|
|
151
|
+
spellcheck="false"
|
|
152
|
+
oninput={(event) => setColor(index, event.currentTarget.value)}
|
|
153
|
+
/>
|
|
154
|
+
</label>
|
|
155
|
+
{/each}
|
|
156
|
+
</section>
|
|
157
|
+
|
|
158
|
+
<section>
|
|
159
|
+
<h3>Typography</h3>
|
|
160
|
+
<p class="note">Previews as you drag. Colours apply on save. A font family is set in your own stylesheet.</p>
|
|
161
|
+
|
|
162
|
+
{#each rampSettings as setting (setting.key)}
|
|
163
|
+
{@const current = Number(theme[setting.key] ?? setting.default)}
|
|
164
|
+
<label class="slider" title={setting.hint}>
|
|
165
|
+
<span>{setting.label}</span>
|
|
166
|
+
<input
|
|
167
|
+
type="range"
|
|
168
|
+
min={setting.min}
|
|
169
|
+
max={setting.max}
|
|
170
|
+
step={setting.step}
|
|
171
|
+
value={current}
|
|
172
|
+
oninput={(event) => setTheme(setting.key, Number(event.currentTarget.value))}
|
|
173
|
+
/>
|
|
174
|
+
<output>{current}{setting.unit}</output>
|
|
175
|
+
</label>
|
|
176
|
+
{/each}
|
|
177
|
+
</section>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
{#if error}
|
|
181
|
+
<p class="error">{error}</p>
|
|
182
|
+
{/if}
|
|
183
|
+
|
|
184
|
+
<footer>
|
|
185
|
+
<button class="save" onclick={save} disabled={saving || loading}>
|
|
186
|
+
{saving ? 'Saving…' : 'Save'}
|
|
187
|
+
</button>
|
|
188
|
+
</footer>
|
|
189
|
+
</aside>
|
|
190
|
+
|
|
191
|
+
<style>
|
|
192
|
+
/* Full height down the left edge. No backdrop and nothing fixed over
|
|
193
|
+
the page: the site stays usable while you edit, which is what makes
|
|
194
|
+
the live preview worth having. The page is not pushed aside either -
|
|
195
|
+
that would mean writing to the host document's own layout. */
|
|
196
|
+
.settings-drawer {
|
|
197
|
+
/* Physical, not logical: the drawer is injected into someone
|
|
198
|
+
else's document and would flip to the right edge on an RTL
|
|
199
|
+
page, which is not what "left" means here. */
|
|
200
|
+
position: fixed;
|
|
201
|
+
top: 0;
|
|
202
|
+
bottom: 0;
|
|
203
|
+
left: 0;
|
|
204
|
+
z-index: 70;
|
|
205
|
+
box-sizing: border-box;
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
width: min(340px, 100vw);
|
|
209
|
+
padding: 16px;
|
|
210
|
+
overflow-y: auto;
|
|
211
|
+
overscroll-behavior: contain;
|
|
212
|
+
color: var(--foreground, #111);
|
|
213
|
+
background: var(--background, #fff);
|
|
214
|
+
border-right: 1px solid oklch(from var(--foreground, #111) l c h / 0.12);
|
|
215
|
+
box-shadow: 0 0 24px oklch(0% 0 0 / 0.12);
|
|
216
|
+
font-size: 0.8125rem;
|
|
217
|
+
line-height: 1.4;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
header {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: space-between;
|
|
224
|
+
gap: 8px;
|
|
225
|
+
margin-bottom: 4px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
h2 {
|
|
229
|
+
margin: 0;
|
|
230
|
+
font-size: 1rem;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
letter-spacing: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
h3 {
|
|
236
|
+
margin: 0 0 8px;
|
|
237
|
+
font-size: 0.6875rem;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
letter-spacing: 0.08em;
|
|
240
|
+
text-transform: uppercase;
|
|
241
|
+
color: oklch(from var(--foreground, #111) l c h / 0.55);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
section {
|
|
245
|
+
padding-block: 14px;
|
|
246
|
+
border-top: 1px solid oklch(from var(--foreground, #111) l c h / 0.1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.close {
|
|
250
|
+
flex: none;
|
|
251
|
+
width: 28px;
|
|
252
|
+
height: 28px;
|
|
253
|
+
padding: 0;
|
|
254
|
+
border: none;
|
|
255
|
+
border-radius: 50%;
|
|
256
|
+
background: transparent;
|
|
257
|
+
color: inherit;
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@media (hover: hover) {
|
|
263
|
+
.close:hover {
|
|
264
|
+
background: oklch(from var(--foreground, #111) l c h / 0.06);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.note {
|
|
269
|
+
margin: 0 0 10px;
|
|
270
|
+
color: oklch(from var(--foreground, #111) l c h / 0.6);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.error {
|
|
274
|
+
margin: 10px 0 0;
|
|
275
|
+
color: oklch(0.55 0.2 25);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.field {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
justify-content: space-between;
|
|
282
|
+
gap: 8px;
|
|
283
|
+
margin-bottom: 8px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.field > span {
|
|
287
|
+
flex: none;
|
|
288
|
+
width: 5.5rem;
|
|
289
|
+
color: oklch(from var(--foreground, #111) l c h / 0.6);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.field.check {
|
|
293
|
+
justify-content: flex-start;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
input[type='text'],
|
|
297
|
+
select {
|
|
298
|
+
flex: 1;
|
|
299
|
+
min-width: 0;
|
|
300
|
+
box-sizing: border-box;
|
|
301
|
+
height: 30px;
|
|
302
|
+
padding: 0 6px;
|
|
303
|
+
color: inherit;
|
|
304
|
+
background: transparent;
|
|
305
|
+
border: 1px solid oklch(from var(--foreground, #111) l c h / 0.2);
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
font: inherit;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
input[type='text']:focus-visible,
|
|
311
|
+
select:focus-visible {
|
|
312
|
+
outline: none;
|
|
313
|
+
border-color: var(--editing, #2563eb);
|
|
314
|
+
box-shadow: inset 0 0 0 1px var(--editing, #2563eb);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* The swatch and its hex edit the same value; the swatch is for
|
|
318
|
+
choosing, the field is for pasting one you already have. */
|
|
319
|
+
.field.color input[type='color'] {
|
|
320
|
+
flex: none;
|
|
321
|
+
width: 34px;
|
|
322
|
+
height: 30px;
|
|
323
|
+
padding: 2px;
|
|
324
|
+
background: transparent;
|
|
325
|
+
border: 1px solid oklch(from var(--foreground, #111) l c h / 0.2);
|
|
326
|
+
border-radius: 8px;
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.hex {
|
|
331
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
332
|
+
text-transform: lowercase;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fieldset {
|
|
336
|
+
margin: 0 0 10px;
|
|
337
|
+
padding: 8px 10px;
|
|
338
|
+
border: 1px solid oklch(from var(--foreground, #111) l c h / 0.12);
|
|
339
|
+
border-radius: 10px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
legend {
|
|
343
|
+
padding: 0 4px;
|
|
344
|
+
font-weight: 600;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.slider {
|
|
348
|
+
display: grid;
|
|
349
|
+
grid-template-columns: 2.25rem 1fr 3.25rem;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 6px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.slider span {
|
|
355
|
+
color: oklch(from var(--foreground, #111) l c h / 0.6);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.slider input {
|
|
359
|
+
width: 100%;
|
|
360
|
+
min-width: 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
output {
|
|
364
|
+
font-variant-numeric: tabular-nums;
|
|
365
|
+
text-align: right;
|
|
366
|
+
color: oklch(from var(--foreground, #111) l c h / 0.6);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Sticks to the bottom of the drawer, above the scrolling content. */
|
|
370
|
+
footer {
|
|
371
|
+
position: sticky;
|
|
372
|
+
bottom: 0;
|
|
373
|
+
display: flex;
|
|
374
|
+
justify-content: flex-end;
|
|
375
|
+
margin-top: auto;
|
|
376
|
+
padding-top: 12px;
|
|
377
|
+
padding-bottom: 4px;
|
|
378
|
+
background: var(--background, #fff);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.save {
|
|
382
|
+
height: 32px;
|
|
383
|
+
padding: 0 1rem;
|
|
384
|
+
border: none;
|
|
385
|
+
border-radius: 9999px;
|
|
386
|
+
background: var(--editing, #2563eb);
|
|
387
|
+
color: var(--background, #fff);
|
|
388
|
+
font-size: 0.8125rem;
|
|
389
|
+
font-weight: 600;
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.save:disabled {
|
|
394
|
+
opacity: 0.5;
|
|
395
|
+
cursor: not-allowed;
|
|
396
|
+
}
|
|
397
|
+
</style>
|