@glw907/cairn-cms 0.52.1 → 0.54.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/CHANGELOG.md +46 -0
- package/dist/components/AdminLayout.svelte +58 -23
- package/dist/components/EditPage.svelte +456 -124
- package/dist/components/EditPage.svelte.d.ts +4 -2
- package/dist/components/EditorToolbar.svelte +29 -53
- package/dist/components/EditorToolbar.svelte.d.ts +3 -11
- package/dist/components/MarkdownEditor.svelte +163 -24
- package/dist/components/MarkdownEditor.svelte.d.ts +3 -0
- package/dist/components/MarkdownHelpDialog.svelte +5 -0
- package/dist/components/ShortcutsDialog.svelte +37 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts +13 -0
- package/dist/components/ShortcutsGrid.svelte +18 -0
- package/dist/components/ShortcutsGrid.svelte.d.ts +23 -0
- package/dist/components/cairn-admin.css +199 -99
- package/dist/components/editor-folding.d.ts +7 -0
- package/dist/components/editor-folding.js +331 -0
- package/dist/components/editor-highlight.js +55 -6
- package/dist/components/editor-shortcuts.d.ts +16 -0
- package/dist/components/editor-shortcuts.js +36 -0
- package/dist/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/dist/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/dist/components/markdown-directives.d.ts +17 -0
- package/dist/components/markdown-directives.js +41 -0
- package/dist/components/topbar-context.d.ts +13 -0
- package/dist/components/topbar-context.js +17 -0
- package/dist/sveltekit/static-admin-page.js +2 -2
- package/package.json +1 -1
- package/src/lib/components/AdminLayout.svelte +58 -23
- package/src/lib/components/EditPage.svelte +456 -124
- package/src/lib/components/EditorToolbar.svelte +29 -53
- package/src/lib/components/MarkdownEditor.svelte +163 -24
- package/src/lib/components/MarkdownHelpDialog.svelte +5 -0
- package/src/lib/components/ShortcutsDialog.svelte +37 -0
- package/src/lib/components/ShortcutsGrid.svelte +18 -0
- package/src/lib/components/cairn-admin.css +51 -14
- package/src/lib/components/editor-folding.ts +356 -0
- package/src/lib/components/editor-highlight.ts +54 -4
- package/src/lib/components/editor-shortcuts.ts +42 -0
- package/src/lib/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/src/lib/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/src/lib/components/markdown-directives.ts +42 -0
- package/src/lib/components/topbar-context.ts +30 -0
- package/src/lib/sveltekit/static-admin-page.ts +2 -2
- package/dist/components/fonts/figtree.woff2 +0 -0
- package/src/lib/components/fonts/figtree.woff2 +0 -0
|
@@ -24,7 +24,8 @@ interface Props {
|
|
|
24
24
|
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
25
25
|
* markdown editor and a live, design-accurate preview. The whole surface is one form posting to the
|
|
26
26
|
* `?/save` action. The title field is hoisted above the editor card as the document title; the
|
|
27
|
-
* remaining fields group
|
|
27
|
+
* remaining fields group behind the Details slide-over (a fixed panel below the band, toggled from
|
|
28
|
+
* the band's Details trigger or Ctrl+.) under Details, Visibility (the draft boolean as the Hidden
|
|
28
29
|
* toggle), and Address (the slug with the Change URL trigger). The toolbar's Write/Preview tabs
|
|
29
30
|
* swap the editing surface for the rendered preview inside the same card; every visit lands on
|
|
30
31
|
* Write. Preview renders inside a sandboxed iframe that links the site's own stylesheets (the
|
|
@@ -33,7 +34,8 @@ interface Props {
|
|
|
33
34
|
* carries the breadcrumb, the status badges, the save-state indicator,
|
|
34
35
|
* and the lifecycle actions: Save, Publish (riding the same form via formaction while edits are
|
|
35
36
|
* pending), and an overflow menu for Discard and Delete. One feedback strip under the header carries the
|
|
36
|
-
* transient flashes, and the editor card's footer
|
|
37
|
+
* transient flashes, and the editor card's footer is the writing-environment strip: the word
|
|
38
|
+
* count, the Prose/Markup posture pair, the focus and typewriter toggles, and the Markdown help.
|
|
37
39
|
*/
|
|
38
40
|
declare const EditPage: import("svelte").Component<Props, {}, "">;
|
|
39
41
|
type EditPage = ReturnType<typeof EditPage>;
|
|
@@ -5,9 +5,9 @@ More overflow menu, then the host's Insert controls) and the Write/Preview segme
|
|
|
5
5
|
right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
6
6
|
Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
7
7
|
Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
8
|
-
widths, reported to the host through `onDevice`. The
|
|
9
|
-
|
|
10
|
-
house style (24x24 viewBox, `currentColor`, round caps).
|
|
8
|
+
widths, reported to the host through `onDevice`. The writing-mode toggles live in the host's card
|
|
9
|
+
footer (the bottom strip carries the writing environment; this strip acts on the text). The glyphs
|
|
10
|
+
are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`, round caps).
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import type { Snippet } from 'svelte';
|
|
@@ -26,14 +26,6 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
26
26
|
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
27
27
|
* while Preview shows. */
|
|
28
28
|
onDevice?: (id: PreviewDeviceId) => void;
|
|
29
|
-
/** Whether focus mode is on; the More menu's toggle reflects it. */
|
|
30
|
-
focusMode?: boolean;
|
|
31
|
-
/** Flip focus mode. When set, the toggle joins the More menu. */
|
|
32
|
-
onFocusMode?: (on: boolean) => void;
|
|
33
|
-
/** Whether typewriter scrolling is on; the More menu's toggle reflects it. */
|
|
34
|
-
typewriter?: boolean;
|
|
35
|
-
/** Flip typewriter scrolling. When set, the toggle joins the More menu. */
|
|
36
|
-
onTypewriter?: (on: boolean) => void;
|
|
37
29
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
38
30
|
insertControls?: Snippet;
|
|
39
31
|
}
|
|
@@ -44,10 +36,6 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
44
36
|
onMode,
|
|
45
37
|
device = 'desktop',
|
|
46
38
|
onDevice,
|
|
47
|
-
focusMode = false,
|
|
48
|
-
onFocusMode,
|
|
49
|
-
typewriter = false,
|
|
50
|
-
onTypewriter,
|
|
51
39
|
insertControls,
|
|
52
40
|
}: Props = $props();
|
|
53
41
|
|
|
@@ -65,12 +53,12 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
65
53
|
const structureButtons: ToolButton[] = [
|
|
66
54
|
{
|
|
67
55
|
kind: 'h2',
|
|
68
|
-
label: 'Heading',
|
|
56
|
+
label: 'Heading (Ctrl+Alt+2)',
|
|
69
57
|
paths: ['M4 12h8', 'M4 18V6', 'M12 18V6', 'M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1'],
|
|
70
58
|
},
|
|
71
59
|
{
|
|
72
60
|
kind: 'h3',
|
|
73
|
-
label: 'Smaller heading',
|
|
61
|
+
label: 'Smaller heading (Ctrl+Alt+3)',
|
|
74
62
|
paths: [
|
|
75
63
|
'M4 12h8',
|
|
76
64
|
'M4 18V6',
|
|
@@ -79,32 +67,45 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
79
67
|
'M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2',
|
|
80
68
|
],
|
|
81
69
|
},
|
|
82
|
-
{ kind: 'ul', label: 'Bulleted list', paths: ['M8 6h13', 'M8 12h13', 'M8 18h13', 'M3 6h.01', 'M3 12h.01', 'M3 18h.01'] },
|
|
70
|
+
{ kind: 'ul', label: 'Bulleted list (Ctrl+Shift+8)', paths: ['M8 6h13', 'M8 12h13', 'M8 18h13', 'M3 6h.01', 'M3 12h.01', 'M3 18h.01'] },
|
|
83
71
|
{
|
|
84
72
|
kind: 'ol',
|
|
85
|
-
label: 'Numbered list',
|
|
73
|
+
label: 'Numbered list (Ctrl+Shift+7)',
|
|
86
74
|
paths: ['M10 12h11', 'M10 18h11', 'M10 6h11', 'M4 10h2', 'M4 6h1v4', 'M6 18H4c0-1 2-2 2-3s-1-1.5-2-1'],
|
|
87
75
|
},
|
|
88
76
|
{
|
|
89
77
|
kind: 'quote',
|
|
90
|
-
label: 'Quote',
|
|
78
|
+
label: 'Quote (Ctrl+Shift+9)',
|
|
91
79
|
paths: [
|
|
92
80
|
'M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
93
81
|
'M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
94
82
|
],
|
|
95
83
|
},
|
|
84
|
+
// The everyday formats promoted out of the More menu onto the strip (after Quote, before
|
|
85
|
+
// More), per mockup screen 1. They keep the strip's glyph grammar.
|
|
86
|
+
{ kind: 'code', label: 'Inline code (Ctrl+E)', paths: ['m9 8-4 4 4 4', 'm15 8 4 4-4 4'] },
|
|
87
|
+
{
|
|
88
|
+
kind: 'strike',
|
|
89
|
+
label: 'Strikethrough',
|
|
90
|
+
paths: ['M14 12a4 4 0 0 1 0 8H8', 'M16 4H9.5a3.5 3.5 0 0 0-1.4 6.7', 'M4 12h16'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
kind: 'table',
|
|
94
|
+
label: 'Table',
|
|
95
|
+
paths: ['M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z', 'M3 10h18', 'M10 3v18'],
|
|
96
|
+
},
|
|
96
97
|
];
|
|
97
98
|
|
|
98
99
|
const ellipsisPaths = ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'];
|
|
99
100
|
// The check glyph marking an active pick, shared by the More menu's toggles and the device list.
|
|
100
101
|
const checkPaths = ['M20 6 9 17l-5-5'];
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// The trimmed overflow: the block formats that stay rare. A divider splits the code block from
|
|
104
|
+
// the rest (the spec keeps "code block and the rest" behind the ellipsis once inline code,
|
|
105
|
+
// strikethrough, and table promote into the strip).
|
|
106
|
+
const moreItems: { kind: FormatKind; label: string; divideBefore?: boolean }[] = [
|
|
105
107
|
{ kind: 'codeblock', label: 'Code block' },
|
|
106
|
-
{ kind: '
|
|
107
|
-
{ kind: 'hr', label: 'Horizontal rule' },
|
|
108
|
+
{ kind: 'hr', label: 'Horizontal rule', divideBefore: true },
|
|
108
109
|
{ kind: 'task', label: 'Task list' },
|
|
109
110
|
];
|
|
110
111
|
|
|
@@ -265,35 +266,10 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
265
266
|
ontoggle={(e) => (moreOpen = e.newState === 'open')}
|
|
266
267
|
class="dropdown menu menu-sm bg-base-100 rounded-box w-44 border border-[var(--cairn-card-border)] p-1 shadow-[var(--cairn-shadow)]"
|
|
267
268
|
>
|
|
268
|
-
<!-- The writing modes sit above the format items behind a hairline, persisted by the host.
|
|
269
|
-
The device list's idiom: plain buttons with aria-pressed carrying the on/off state (this
|
|
270
|
-
popover list is not an ARIA menu, so a menuitemcheckbox would sit in an invalid context);
|
|
271
|
-
the check glyph mirrors the state visually. A flip leaves the menu open so the new
|
|
272
|
-
pressed state is perceivable in place; only a format pick dismisses it. -->
|
|
273
|
-
{#if onFocusMode}
|
|
274
|
-
<li>
|
|
275
|
-
<button type="button" aria-pressed={focusMode} onclick={() => onFocusMode(!focusMode)}>
|
|
276
|
-
<span class="grow">Focus mode</span>
|
|
277
|
-
{#if focusMode}
|
|
278
|
-
{@render strokeIcon(checkPaths)}
|
|
279
|
-
{/if}
|
|
280
|
-
</button>
|
|
281
|
-
</li>
|
|
282
|
-
{/if}
|
|
283
|
-
{#if onTypewriter}
|
|
284
|
-
<li>
|
|
285
|
-
<button type="button" aria-pressed={typewriter} onclick={() => onTypewriter(!typewriter)}>
|
|
286
|
-
<span class="grow">Typewriter scrolling</span>
|
|
287
|
-
{#if typewriter}
|
|
288
|
-
{@render strokeIcon(checkPaths)}
|
|
289
|
-
{/if}
|
|
290
|
-
</button>
|
|
291
|
-
</li>
|
|
292
|
-
{/if}
|
|
293
|
-
{#if onFocusMode || onTypewriter}
|
|
294
|
-
<li class="my-1 border-t border-[var(--cairn-card-border)]" role="separator"></li>
|
|
295
|
-
{/if}
|
|
296
269
|
{#each moreItems as item (item.kind)}
|
|
270
|
+
{#if item.divideBefore}
|
|
271
|
+
<li class="menu-divider my-1 h-px bg-[var(--cairn-card-border)]" role="separator" aria-hidden="true"></li>
|
|
272
|
+
{/if}
|
|
297
273
|
<li><button type="button" onclick={() => pickMore(item.kind)}>{item.label}</button></li>
|
|
298
274
|
{/each}
|
|
299
275
|
</ul>
|
|
@@ -13,14 +13,6 @@ interface Props {
|
|
|
13
13
|
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
14
14
|
* while Preview shows. */
|
|
15
15
|
onDevice?: (id: PreviewDeviceId) => void;
|
|
16
|
-
/** Whether focus mode is on; the More menu's toggle reflects it. */
|
|
17
|
-
focusMode?: boolean;
|
|
18
|
-
/** Flip focus mode. When set, the toggle joins the More menu. */
|
|
19
|
-
onFocusMode?: (on: boolean) => void;
|
|
20
|
-
/** Whether typewriter scrolling is on; the More menu's toggle reflects it. */
|
|
21
|
-
typewriter?: boolean;
|
|
22
|
-
/** Flip typewriter scrolling. When set, the toggle joins the More menu. */
|
|
23
|
-
onTypewriter?: (on: boolean) => void;
|
|
24
16
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
25
17
|
insertControls?: Snippet;
|
|
26
18
|
}
|
|
@@ -30,9 +22,9 @@ interface Props {
|
|
|
30
22
|
* right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
31
23
|
* Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
32
24
|
* Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
33
|
-
* widths, reported to the host through `onDevice`. The
|
|
34
|
-
*
|
|
35
|
-
* house style (24x24 viewBox, `currentColor`, round caps).
|
|
25
|
+
* widths, reported to the host through `onDevice`. The writing-mode toggles live in the host's card
|
|
26
|
+
* footer (the bottom strip carries the writing environment; this strip acts on the text). The glyphs
|
|
27
|
+
* are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`, round caps).
|
|
36
28
|
*/
|
|
37
29
|
declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
|
|
38
30
|
type EditorToolbar = ReturnType<typeof EditorToolbar>;
|
|
@@ -31,6 +31,9 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
31
31
|
focusMode?: boolean;
|
|
32
32
|
/** Typewriter scroll: hold the cursor line at vertical center while typing. Off by default. */
|
|
33
33
|
typewriter?: boolean;
|
|
34
|
+
/** The surface posture. Prose is the writing instrument (72ch measure, larger type, looser
|
|
35
|
+
* leading); markup is the working surface (fills the card, denser). Prose by default. */
|
|
36
|
+
surface?: 'prose' | 'markup';
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
let {
|
|
@@ -43,6 +46,7 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
43
46
|
completionSources = [],
|
|
44
47
|
focusMode = false,
|
|
45
48
|
typewriter = false,
|
|
49
|
+
surface = 'prose',
|
|
46
50
|
}: Props = $props();
|
|
47
51
|
|
|
48
52
|
let host = $state<HTMLDivElement | null>(null);
|
|
@@ -57,6 +61,12 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
57
61
|
let modes: typeof import('./editor-modes.js') | null = null;
|
|
58
62
|
let focusCompartment: import('@codemirror/state').Compartment | null = null;
|
|
59
63
|
let typewriterCompartment: import('@codemirror/state').Compartment | null = null;
|
|
64
|
+
let surfaceCompartment: import('@codemirror/state').Compartment | null = null;
|
|
65
|
+
// The posture themes, swapped through the surface compartment. Each owns its type step and
|
|
66
|
+
// leading (the base theme deliberately sets neither on the content node, so the postures never
|
|
67
|
+
// contest it on adoption order). Built in onMount beside the base theme.
|
|
68
|
+
let proseTheme: import('@codemirror/state').Extension | null = null;
|
|
69
|
+
let markupTheme: import('@codemirror/state').Extension | null = null;
|
|
60
70
|
|
|
61
71
|
onMount(async () => {
|
|
62
72
|
const viewMod = await import('@codemirror/view');
|
|
@@ -67,6 +77,7 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
67
77
|
const autocompleteMod = await import('@codemirror/autocomplete');
|
|
68
78
|
const highlightMod = await import('./editor-highlight.js');
|
|
69
79
|
const modesMod = await import('./editor-modes.js');
|
|
80
|
+
const foldingMod = await import('./editor-folding.js');
|
|
70
81
|
|
|
71
82
|
if (!host) return;
|
|
72
83
|
|
|
@@ -75,9 +86,9 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
75
86
|
// tooltip above all) renders dark-on-dark instead of light-on-dark.
|
|
76
87
|
const isDark = host.closest('[data-theme]')?.getAttribute('data-theme')?.includes('dark') ?? false;
|
|
77
88
|
// The directive machinery treatment: rails, not bands. A row at depth N draws every rail
|
|
78
|
-
// 1..N as literal nested brackets: 2px accent bars
|
|
79
|
-
// of surface between them (
|
|
80
|
-
//
|
|
89
|
+
// 1..N as literal nested brackets: 2px accent bars on an 8px pitch (x offsets 0-2, 8-10,
|
|
90
|
+
// and 16-18) with 6px of surface between them (three times the bar weight, so nested bars
|
|
91
|
+
// separate cleanly instead of reading as one thick rule), stacked as inset box shadows (top
|
|
81
92
|
// layer first, so each bar sits over the spacer and deeper bar beneath it). The alphas step through the per-theme vars in
|
|
82
93
|
// cairn-admin.css; the fallbacks are the light values, so the editor still renders sensibly
|
|
83
94
|
// outside an admin theme wrapper. On a fence line the colon runs, brackets, and {attrs}
|
|
@@ -87,21 +98,25 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
87
98
|
const railFallbacks = ['72%', '82%', '92%'];
|
|
88
99
|
const railColor = (step: number | 'active', fallback: string) =>
|
|
89
100
|
`color-mix(in oklab, var(--color-accent) var(--cairn-directive-rail-${step}, ${fallback}), transparent)`;
|
|
90
|
-
// With `active`, the row's own (deepest) bar takes the full-strength -active mix
|
|
91
|
-
//
|
|
92
|
-
|
|
101
|
+
// With `active`, the row's own (deepest) bar takes the full-strength -active mix at the same
|
|
102
|
+
// 2px width. The emphasis is strength only: a rail column carrying both an active and a
|
|
103
|
+
// quiet segment (two sibling containers at one depth) keeps one weight top to bottom.
|
|
104
|
+
// With `dropInnermost`, the row's own deepest bar is omitted: a fold chevron replaces it on a
|
|
105
|
+
// paired opener row, so the bar would double the chevron's positional cue. The outer bars and
|
|
106
|
+
// their spacers stay, so the nesting still reads.
|
|
107
|
+
const rails = (depth: number, active = false, dropInnermost = false): string => {
|
|
93
108
|
const layers: string[] = [];
|
|
94
109
|
for (let d = 1; d <= depth; d++) {
|
|
95
|
-
const edge =
|
|
110
|
+
const edge = 8 * d - 6;
|
|
96
111
|
if (d > 1) layers.push(`inset ${edge - 2}px 0 0 0 var(--color-base-100, oklch(99% 0.004 75))`);
|
|
112
|
+
if (dropInnermost && d === depth) continue;
|
|
97
113
|
const own = active && d === depth;
|
|
98
114
|
layers.push(
|
|
99
|
-
own
|
|
100
|
-
? `inset ${edge + 1}px 0 0 0 ${railColor('active', '100%')}`
|
|
101
|
-
: `inset ${edge}px 0 0 0 ${railColor(d, railFallbacks[d - 1] ?? '92%')}`,
|
|
115
|
+
`inset ${edge}px 0 0 0 ${own ? railColor('active', '100%') : railColor(d, railFallbacks[d - 1] ?? '92%')}`,
|
|
102
116
|
);
|
|
103
117
|
}
|
|
104
|
-
|
|
118
|
+
// A depth-1 opener drops its only bar, so the row paints no rail at all.
|
|
119
|
+
return layers.length ? layers.join(', ') : 'none';
|
|
105
120
|
};
|
|
106
121
|
const directiveInk = {
|
|
107
122
|
backgroundColor: 'color-mix(in oklab, var(--color-accent) 8%, transparent)',
|
|
@@ -117,23 +132,35 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
117
132
|
`${prefix}.cm-cairn-directive-fence.cm-cairn-depth-${depth}, ${prefix}.cm-cairn-directive-content.cm-cairn-depth-${depth}`;
|
|
118
133
|
railRules[row('')] = { boxShadow: rails(depth) };
|
|
119
134
|
railRules[row('.cm-cairn-caret-block')] = { boxShadow: rails(depth, true) };
|
|
135
|
+
// A paired opener row drops its own innermost bar (the fold chevron stands in its place),
|
|
136
|
+
// both quiet and caret-active. The extra opener class outranks the base fence rule above.
|
|
137
|
+
railRules[`.cm-cairn-directive-fence.cm-cairn-directive-opener.cm-cairn-depth-${depth}`] = {
|
|
138
|
+
boxShadow: rails(depth, false, true),
|
|
139
|
+
};
|
|
140
|
+
railRules[`.cm-cairn-caret-block.cm-cairn-directive-opener.cm-cairn-depth-${depth}`] = {
|
|
141
|
+
boxShadow: rails(depth, true, true),
|
|
142
|
+
};
|
|
120
143
|
}
|
|
121
144
|
const theme = EditorView.theme(
|
|
122
145
|
{
|
|
123
146
|
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '1rem' },
|
|
124
|
-
// The
|
|
125
|
-
// contenteditable content area carries the height, a
|
|
126
|
-
// text still lands in the editor and focuses it.
|
|
127
|
-
//
|
|
147
|
+
// The 60vh floor keeps the surface reading as the page's center stage even when the
|
|
148
|
+
// entry is short, and because the contenteditable content area carries the height, a
|
|
149
|
+
// click in the empty space below the text still lands in the editor and focuses it.
|
|
150
|
+
// No inner measure cap: the surface fills the card the way a code editor fills its
|
|
151
|
+
// pane, and the card's own width (the host caps it near 89ch of this face) is the one
|
|
152
|
+
// constraint. The surface carries tables, attributed directives, and long URLs, so the
|
|
153
|
+
// ceiling leans toward the code-editor end of the ergonomic band rather than the
|
|
154
|
+
// long-form ideal; paragraphs wrap comfortably below it.
|
|
128
155
|
'.cm-content': {
|
|
129
156
|
// The theme roots set --font-editor to the self-hosted iA Writer Mono; the inline
|
|
130
157
|
// fallback keeps the surface monospace outside an admin theme wrapper.
|
|
131
158
|
fontFamily: "var(--font-editor, ui-monospace, monospace)",
|
|
132
|
-
padding
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
// Vertical padding holds at least one line-height of the body (1.8 x 1rem), with a
|
|
160
|
+
// touch more below than above (the optical center sits high); the sides then read as
|
|
161
|
+
// gutters rather than letterboxing.
|
|
162
|
+
padding: '2rem 1.25rem 2.5rem',
|
|
163
|
+
minHeight: '60vh',
|
|
137
164
|
},
|
|
138
165
|
'.cm-cursor': { borderLeftColor: 'var(--color-primary)' },
|
|
139
166
|
// A quiet always-on focus hairline. :focus-visible is no escape here: browsers treat a
|
|
@@ -146,10 +173,19 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
146
173
|
outlineOffset: '-1px',
|
|
147
174
|
},
|
|
148
175
|
'.cm-line': { padding: '0' },
|
|
176
|
+
// A quote or list line hangs its wrapped continuation under the content: padding-left
|
|
177
|
+
// holds the marker width (the --cairn-hang the decoration sets) and the line's own
|
|
178
|
+
// negative text-indent (set inline) pulls the first line back, so the marker sits in the
|
|
179
|
+
// indent. This rule sits before the gutter rule so a container content line, which
|
|
180
|
+
// carries both classes, takes the gutter-plus-hang rule below.
|
|
181
|
+
'.cm-cairn-hang': { paddingLeft: 'var(--cairn-hang, 0ch)' },
|
|
149
182
|
// The gutter: directive rows pad left so the text clears the deepest rail stack (the
|
|
150
|
-
// depth-3 bar ends at
|
|
151
|
-
//
|
|
152
|
-
|
|
183
|
+
// depth-3 bar ends at 18px; 1.75rem keeps 10px of air beyond it). Static structure
|
|
184
|
+
// (caret-independent), so caret movement shifts no layout. The --cairn-hang term composes
|
|
185
|
+
// a quote/list marker's hang on top of the gutter; it defaults to 0 on rows without one.
|
|
186
|
+
'.cm-cairn-directive-fence, .cm-cairn-directive-content': {
|
|
187
|
+
paddingLeft: 'calc(1.75rem + var(--cairn-hang, 0ch))',
|
|
188
|
+
},
|
|
153
189
|
...railRules,
|
|
154
190
|
'.cm-cairn-directive-mark': { color: 'var(--color-muted)' },
|
|
155
191
|
'.cm-cairn-directive-label': { color: 'var(--color-accent)' },
|
|
@@ -163,6 +199,68 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
163
199
|
},
|
|
164
200
|
'.cm-cairn-directive-leaf': directiveInk,
|
|
165
201
|
'.cm-cairn-directive-inline': directiveInk,
|
|
202
|
+
// Container folding. The fold band is the 28px gutter click target on an opener row; the
|
|
203
|
+
// line is the positioning context so the chevron sits over the container's own bar x. The
|
|
204
|
+
// band is laid over the gutter (a zero-width inline widget at line start, expanded by the
|
|
205
|
+
// absolute children), so only the gutter shows the pointer cursor, never the opener text.
|
|
206
|
+
'.cm-line:has(.cm-cairn-fold-band)': { position: 'relative' },
|
|
207
|
+
'.cm-cairn-fold-band': {
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
left: '0',
|
|
210
|
+
top: '0',
|
|
211
|
+
width: '28px',
|
|
212
|
+
height: '100%',
|
|
213
|
+
cursor: 'pointer',
|
|
214
|
+
zIndex: '1',
|
|
215
|
+
},
|
|
216
|
+
'.cm-cairn-fold-band svg': {
|
|
217
|
+
position: 'absolute',
|
|
218
|
+
top: '50%',
|
|
219
|
+
transform: 'translateY(-50%)',
|
|
220
|
+
width: '11px',
|
|
221
|
+
height: '11px',
|
|
222
|
+
// The chevron fades in on rail-band hover; folded and caret-inside states force it on.
|
|
223
|
+
opacity: '0',
|
|
224
|
+
transition: 'opacity 120ms ease',
|
|
225
|
+
color: 'var(--cairn-directive-ink-2, oklch(50% 0.16 300))',
|
|
226
|
+
},
|
|
227
|
+
'.cm-cairn-fold-band:hover svg, .cm-cairn-fold-folded svg, .cm-cairn-fold-active svg': {
|
|
228
|
+
opacity: '1',
|
|
229
|
+
},
|
|
230
|
+
// The chevron steps its ink with the container's depth, matching the label inks; the
|
|
231
|
+
// caret-inside state takes the strongest ink.
|
|
232
|
+
'.cm-cairn-fold-depth-1 svg': { color: 'var(--color-accent)' },
|
|
233
|
+
'.cm-cairn-fold-depth-3 svg': { color: 'var(--cairn-directive-ink-3, oklch(48% 0.16 300))' },
|
|
234
|
+
'.cm-cairn-fold-active svg': { color: 'var(--cairn-directive-ink-active, oklch(46% 0.16 300))' },
|
|
235
|
+
// The folded-row wash: a soft accent tint, square and full-row, returning as a STATE signal
|
|
236
|
+
// so folded spots read in a scan. The rails are inset box-shadows on the same line element
|
|
237
|
+
// and render above this background, so the rail column runs through the wash unbroken.
|
|
238
|
+
'.cm-cairn-folded-row': {
|
|
239
|
+
backgroundColor: 'color-mix(in oklab, var(--color-accent) 7%, transparent)',
|
|
240
|
+
},
|
|
241
|
+
// The fold pill: the placeholder widget and the screen-reader story, a real focusable
|
|
242
|
+
// button counting the hidden lines in accent ink. The 30% accent border lifts on hover.
|
|
243
|
+
'.cm-cairn-fold-pill': {
|
|
244
|
+
fontFamily: 'var(--font-body, ui-sans-serif, sans-serif)',
|
|
245
|
+
fontSize: '0.6875rem',
|
|
246
|
+
color: 'var(--color-accent)',
|
|
247
|
+
border: '1px solid color-mix(in oklab, var(--color-accent) 30%, transparent)',
|
|
248
|
+
borderRadius: '0.375rem',
|
|
249
|
+
padding: '1px 7px',
|
|
250
|
+
marginLeft: '10px',
|
|
251
|
+
verticalAlign: '1px',
|
|
252
|
+
backgroundColor: 'var(--color-base-100)',
|
|
253
|
+
cursor: 'pointer',
|
|
254
|
+
},
|
|
255
|
+
'.cm-cairn-fold-pill:hover': {
|
|
256
|
+
borderColor: 'color-mix(in oklab, var(--color-accent) 60%, transparent)',
|
|
257
|
+
},
|
|
258
|
+
// The one-time unfold flash: a low-alpha accent background on the revealed lines, removed
|
|
259
|
+
// after the animation. The transition runs as the field clears the class.
|
|
260
|
+
'.cm-cairn-fold-flash': {
|
|
261
|
+
backgroundColor: 'color-mix(in oklab, var(--color-accent) 12%, transparent)',
|
|
262
|
+
transition: 'background-color 400ms ease',
|
|
263
|
+
},
|
|
166
264
|
// Focus mode's dim ink, on the lines editor-modes marks outside the caret's paragraph.
|
|
167
265
|
// Last on purpose: a dimmed line's spans (markers, tokens, directive labels) all drop to
|
|
168
266
|
// the dim tone, and spec order breaks the specificity ties with the label rules above.
|
|
@@ -176,13 +274,47 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
176
274
|
color: 'var(--cairn-focus-dim-ink, oklch(66% 0.01 75))',
|
|
177
275
|
backgroundColor: 'transparent',
|
|
178
276
|
},
|
|
277
|
+
// The fold machinery dims with its row: a folded opener row under focus mode drops its
|
|
278
|
+
// chevron, pill, and wash to the dim tone like any other machinery line.
|
|
279
|
+
'.cm-cairn-focus-dim .cm-cairn-fold-band svg, .cm-cairn-focus-dim .cm-cairn-fold-pill': {
|
|
280
|
+
color: 'var(--cairn-focus-dim-ink, oklch(66% 0.01 75))',
|
|
281
|
+
},
|
|
282
|
+
'.cm-cairn-focus-dim.cm-cairn-folded-row': { backgroundColor: 'transparent' },
|
|
283
|
+
// The rails dim with their text: the rail color-mix reads --cairn-directive-rail-N per
|
|
284
|
+
// element, so overriding the percentages on dimmed lines re-resolves every bar in place.
|
|
285
|
+
// Without this the directive block keeps full-strength bars and becomes the one
|
|
286
|
+
// chromatic object in the dimmed field. The active step needs the override too: focus
|
|
287
|
+
// mode's lit unit is the caret PARAGRAPH while the caret-block class spans the whole
|
|
288
|
+
// container, so a container holding a blank line has dimmed rows that still carry the
|
|
289
|
+
// active rail.
|
|
290
|
+
'.cm-cairn-focus-dim': {
|
|
291
|
+
'--cairn-directive-rail-1': 'var(--cairn-focus-dim-rail-1, 24%)',
|
|
292
|
+
'--cairn-directive-rail-2': 'var(--cairn-focus-dim-rail-2, 28%)',
|
|
293
|
+
'--cairn-directive-rail-3': 'var(--cairn-focus-dim-rail-3, 32%)',
|
|
294
|
+
'--cairn-directive-rail-active': 'var(--cairn-focus-dim-rail-active, 36%)',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{ dark: isDark },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// The prose posture: the writing instrument. A 72ch measure centered in the card, one type
|
|
301
|
+
// step up, looser leading. Markup posture (the base theme) keeps the dense fill for tables,
|
|
302
|
+
// directives, and long URLs. Placed after the base theme in the extension list, so its keys
|
|
303
|
+
// win the spec-order ties.
|
|
304
|
+
proseTheme = EditorView.theme(
|
|
305
|
+
{
|
|
306
|
+
// Scoped to the content node (not the editor root) so the base theme's root font-size
|
|
307
|
+
// never contests it, and so the 72ch measure resolves against the prose type step.
|
|
308
|
+
'.cm-content': { fontSize: '1.0625rem', lineHeight: '1.9', maxWidth: '72ch', margin: '0 auto' },
|
|
179
309
|
},
|
|
180
310
|
{ dark: isDark },
|
|
181
311
|
);
|
|
312
|
+
markupTheme = EditorView.theme({ '.cm-content': { lineHeight: '1.8' } }, { dark: isDark });
|
|
182
313
|
|
|
183
314
|
modes = modesMod;
|
|
184
315
|
focusCompartment = new stateMod.Compartment();
|
|
185
316
|
typewriterCompartment = new stateMod.Compartment();
|
|
317
|
+
surfaceCompartment = new stateMod.Compartment();
|
|
186
318
|
|
|
187
319
|
view = new EditorView({
|
|
188
320
|
parent: host,
|
|
@@ -205,8 +337,13 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
205
337
|
EditorView.lineWrapping,
|
|
206
338
|
languageMod.syntaxHighlighting(highlightMod.cairnHighlightStyle()),
|
|
207
339
|
highlightMod.cairnDirectivePlugin(),
|
|
340
|
+
// Container folding: the fold system, the chevron and wash affordance, and the safety
|
|
341
|
+
// invariant. Placed after the directive plugin so its chevron widget on an opener row
|
|
342
|
+
// composes with the row's rail and gutter; its keymap is internal to the extension.
|
|
343
|
+
foldingMod.cairnFolding(),
|
|
208
344
|
EditorView.contentAttributes.of({ spellcheck: 'true', autocorrect: 'on', autocapitalize: 'sentences' }),
|
|
209
345
|
theme,
|
|
346
|
+
surfaceCompartment.of(surface === 'prose' ? proseTheme : markupTheme),
|
|
210
347
|
EditorView.updateListener.of((update) => {
|
|
211
348
|
if (update.docChanged) value = update.state.doc.toString();
|
|
212
349
|
}),
|
|
@@ -239,11 +376,13 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
239
376
|
$effect(() => {
|
|
240
377
|
const focus = focusMode;
|
|
241
378
|
const typing = typewriter;
|
|
242
|
-
|
|
379
|
+
const posture = surface;
|
|
380
|
+
if (!mounted || !view || !modes || !focusCompartment || !typewriterCompartment || !surfaceCompartment) return;
|
|
243
381
|
view.dispatch({
|
|
244
382
|
effects: [
|
|
245
383
|
focusCompartment.reconfigure(focus ? modes.focusMode() : []),
|
|
246
384
|
typewriterCompartment.reconfigure(typing ? modes.typewriterScroll() : []),
|
|
385
|
+
surfaceCompartment.reconfigure((posture === 'prose' ? proseTheme : markupTheme) ?? []),
|
|
247
386
|
],
|
|
248
387
|
});
|
|
249
388
|
});
|
|
@@ -19,6 +19,9 @@ interface Props {
|
|
|
19
19
|
focusMode?: boolean;
|
|
20
20
|
/** Typewriter scroll: hold the cursor line at vertical center while typing. Off by default. */
|
|
21
21
|
typewriter?: boolean;
|
|
22
|
+
/** The surface posture. Prose is the writing instrument (72ch measure, larger type, looser
|
|
23
|
+
* leading); markup is the working surface (fills the card, denser). Prose by default. */
|
|
24
|
+
surface?: 'prose' | 'markup';
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
@@ -6,6 +6,8 @@ Built on a native <dialog>, the DeleteDialog recipe; the host drives it through
|
|
|
6
6
|
open(), so the component renders no trigger of its own.
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">
|
|
9
|
+
import ShortcutsGrid from './ShortcutsGrid.svelte';
|
|
10
|
+
|
|
9
11
|
let dialog = $state<HTMLDialogElement | null>(null);
|
|
10
12
|
|
|
11
13
|
/** Open the cheat sheet. The trigger lives in the host (the edit page's editor footer). */
|
|
@@ -33,6 +35,7 @@ open(), so the component renders no trigger of its own.
|
|
|
33
35
|
<tbody>
|
|
34
36
|
<tr><td><code>## Heading</code></td><td>A heading</td></tr>
|
|
35
37
|
<tr><td><code>### Heading</code></td><td>A smaller heading</td></tr>
|
|
38
|
+
<tr><td><code>#### Heading</code></td><td>A fourth-level heading</td></tr>
|
|
36
39
|
<tr><td><code>**bold**</code></td><td>Bold text</td></tr>
|
|
37
40
|
<tr><td><code>*italic*</code></td><td>Italic text</td></tr>
|
|
38
41
|
<tr><td><code>~~text~~</code></td><td>Crossed-out text</td></tr>
|
|
@@ -47,6 +50,8 @@ open(), so the component renders no trigger of its own.
|
|
|
47
50
|
<tr><td><code>---</code></td><td>A horizontal rule</td></tr>
|
|
48
51
|
</tbody>
|
|
49
52
|
</table>
|
|
53
|
+
<h3 class="mt-4 mb-2 text-sm font-semibold">Keyboard shortcuts</h3>
|
|
54
|
+
<ShortcutsGrid />
|
|
50
55
|
<p class="mt-3 text-sm">
|
|
51
56
|
Lines starting with <code>:::</code> are layout blocks. Edit the text inside them and leave
|
|
52
57
|
the <code>:::</code> lines alone.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The keyboard shortcuts sheet, the third discoverability surface (the toolbar tooltips and the
|
|
4
|
+
Markdown help dialog are the other two). A two-column grid pairs each label with its chord, with a
|
|
5
|
+
closing line that the keys are always conveniences. Built on a native <dialog>, the
|
|
6
|
+
MarkdownHelpDialog recipe; the host (the edit page) drives it through the exported open() and opens
|
|
7
|
+
it on Ctrl+/, so the component renders no trigger of its own. Esc dismisses through the dialog's
|
|
8
|
+
native behavior.
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { shortcutsClosingLine } from './editor-shortcuts.js';
|
|
12
|
+
import ShortcutsGrid from './ShortcutsGrid.svelte';
|
|
13
|
+
|
|
14
|
+
let dialog = $state<HTMLDialogElement | null>(null);
|
|
15
|
+
|
|
16
|
+
/** Open the shortcuts sheet. The trigger lives in the host (the edit page's Ctrl+/ handler). */
|
|
17
|
+
export function open() {
|
|
18
|
+
dialog?.showModal();
|
|
19
|
+
}
|
|
20
|
+
function close() {
|
|
21
|
+
dialog?.close();
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<dialog class="modal" aria-labelledby="cairn-shortcuts-title" bind:this={dialog}>
|
|
26
|
+
<div class="modal-box">
|
|
27
|
+
<div class="mb-3 flex items-center justify-between">
|
|
28
|
+
<h2 id="cairn-shortcuts-title" class="text-base font-semibold">Keyboard shortcuts</h2>
|
|
29
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
30
|
+
</div>
|
|
31
|
+
<ShortcutsGrid />
|
|
32
|
+
<p class="mt-3 text-xs text-[var(--color-muted)]">{shortcutsClosingLine}</p>
|
|
33
|
+
</div>
|
|
34
|
+
<form method="dialog" class="modal-backdrop">
|
|
35
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
36
|
+
</form>
|
|
37
|
+
</dialog>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyboard shortcuts sheet, the third discoverability surface (the toolbar tooltips and the
|
|
3
|
+
* Markdown help dialog are the other two). A two-column grid pairs each label with its chord, with a
|
|
4
|
+
* closing line that the keys are always conveniences. Built on a native <dialog>, the
|
|
5
|
+
* MarkdownHelpDialog recipe; the host (the edit page) drives it through the exported open() and opens
|
|
6
|
+
* it on Ctrl+/, so the component renders no trigger of its own. Esc dismisses through the dialog's
|
|
7
|
+
* native behavior.
|
|
8
|
+
*/
|
|
9
|
+
declare const ShortcutsDialog: import("svelte").Component<Record<string, never>, {
|
|
10
|
+
open: () => void;
|
|
11
|
+
}, "">;
|
|
12
|
+
type ShortcutsDialog = ReturnType<typeof ShortcutsDialog>;
|
|
13
|
+
export default ShortcutsDialog;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The two-column shortcut grid, the shared body of both discoverability sheets (ShortcutsDialog and
|
|
4
|
+
the Markdown help dialog). Each row pairs a label with its chord, read from the single
|
|
5
|
+
editor-shortcuts source. The host wraps it with its own heading and any closing line.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { editorShortcuts } from './editor-shortcuts.js';
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class="grid grid-cols-1 gap-x-8 gap-y-1 text-sm sm:grid-cols-2">
|
|
12
|
+
{#each editorShortcuts as row (row.label)}
|
|
13
|
+
<div class="flex items-baseline justify-between gap-4">
|
|
14
|
+
<span>{row.label}</span>
|
|
15
|
+
<span class="font-mono text-[0.75rem] text-[var(--color-muted)]">{row.keys}</span>
|
|
16
|
+
</div>
|
|
17
|
+
{/each}
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The two-column shortcut grid, the shared body of both discoverability sheets (ShortcutsDialog and
|
|
16
|
+
* the Markdown help dialog). Each row pairs a label with its chord, read from the single
|
|
17
|
+
* editor-shortcuts source. The host wraps it with its own heading and any closing line.
|
|
18
|
+
*/
|
|
19
|
+
declare const ShortcutsGrid: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
20
|
+
[evt: string]: CustomEvent<any>;
|
|
21
|
+
}, {}, {}, string>;
|
|
22
|
+
type ShortcutsGrid = InstanceType<typeof ShortcutsGrid>;
|
|
23
|
+
export default ShortcutsGrid;
|