@glw907/cairn-cms 0.51.0 → 0.52.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 +21 -0
- package/dist/components/EditPage.svelte +32 -1
- package/dist/components/EditorToolbar.svelte +60 -6
- package/dist/components/EditorToolbar.svelte.d.ts +10 -1
- package/dist/components/MarkdownEditor.svelte +112 -24
- package/dist/components/MarkdownEditor.svelte.d.ts +4 -0
- package/dist/components/cairn-admin.css +23 -11
- package/dist/components/editor-highlight.d.ts +2 -1
- package/dist/components/editor-highlight.js +60 -19
- package/dist/components/editor-modes.d.ts +26 -0
- package/dist/components/editor-modes.js +92 -0
- package/dist/components/fonts/iAWriterMono-OFL.txt +100 -0
- package/dist/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
- package/dist/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
- package/dist/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
- package/dist/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
- package/dist/components/markdown-directives.d.ts +48 -7
- package/dist/components/markdown-directives.js +89 -13
- package/package.json +1 -1
- package/src/lib/components/EditPage.svelte +32 -1
- package/src/lib/components/EditorToolbar.svelte +60 -6
- package/src/lib/components/MarkdownEditor.svelte +112 -24
- package/src/lib/components/cairn-admin.css +62 -31
- package/src/lib/components/editor-highlight.ts +72 -20
- package/src/lib/components/editor-modes.ts +106 -0
- package/src/lib/components/fonts/iAWriterMono-OFL.txt +100 -0
- package/src/lib/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
- package/src/lib/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
- package/src/lib/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
- package/src/lib/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
- package/src/lib/components/markdown-directives.ts +113 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.52.0
|
|
6
|
+
|
|
7
|
+
The editor became a quiet writing surface. The manuscript renders in self-hosted iA Writer Mono
|
|
8
|
+
(SIL OFL) at a centered 70-character measure, heading sizes step by level, every syntax marker
|
|
9
|
+
and URL recedes to the muted ink while the content keeps full strength, inline code sits on a
|
|
10
|
+
soft chip, and quote text reads in full ink with only the `>` dimmed. The editor also parses
|
|
11
|
+
GFM now, so the toolbar's strikethrough, tables, and task lists highlight as you type.
|
|
12
|
+
|
|
13
|
+
Directive machinery trades its row bands for bracket rails: a container draws a depth-stepped
|
|
14
|
+
rail from opener to closer, nested containers draw nested rails, the fence line's name and label
|
|
15
|
+
keep the accent while the colons and braces fade, and the block holding your caret reads one
|
|
16
|
+
step stronger. The treatment is AA-checked in both themes.
|
|
17
|
+
|
|
18
|
+
Two writing modes join the toolbar's overflow menu, each persisted per browser: focus mode fades
|
|
19
|
+
every paragraph but the caret's (a deliberate, documented sub-AA dim with chip backgrounds
|
|
20
|
+
flattened), and typewriter scrolling holds the caret line at vertical center.
|
|
21
|
+
|
|
22
|
+
Consumers may: pass the new optional `focusMode` and `typewriter` booleans when embedding
|
|
23
|
+
`MarkdownEditor` directly; sites on the stock `EditPage` get the toggles and persistence for
|
|
24
|
+
free. No action required; the release is additive.
|
|
25
|
+
|
|
5
26
|
## 0.51.0
|
|
6
27
|
|
|
7
28
|
The `svelte` peer dependency floor rises from `^5.0.0` to `^5.56.3`, turning the 0.40.0 advisory
|
|
@@ -178,6 +178,25 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
178
178
|
device = id;
|
|
179
179
|
localStorage.setItem(deviceStorageKey, id);
|
|
180
180
|
}
|
|
181
|
+
// The writing modes (focus, typewriter), per-browser preferences on the device pick's pattern:
|
|
182
|
+
// off by default, read in an effect so SSR never touches localStorage, written by the
|
|
183
|
+
// toolbar's toggles. The effect tracks nothing reactive, so it runs once.
|
|
184
|
+
const focusStorageKey = 'cairn-editor-focus-mode';
|
|
185
|
+
const typewriterStorageKey = 'cairn-editor-typewriter';
|
|
186
|
+
let focusMode = $state(false);
|
|
187
|
+
let typewriter = $state(false);
|
|
188
|
+
$effect(() => {
|
|
189
|
+
focusMode = localStorage.getItem(focusStorageKey) === 'true';
|
|
190
|
+
typewriter = localStorage.getItem(typewriterStorageKey) === 'true';
|
|
191
|
+
});
|
|
192
|
+
function setFocusMode(on: boolean) {
|
|
193
|
+
focusMode = on;
|
|
194
|
+
localStorage.setItem(focusStorageKey, String(on));
|
|
195
|
+
}
|
|
196
|
+
function setTypewriter(on: boolean) {
|
|
197
|
+
typewriter = on;
|
|
198
|
+
localStorage.setItem(typewriterStorageKey, String(on));
|
|
199
|
+
}
|
|
181
200
|
const activeDevice = $derived(previewDevice(device));
|
|
182
201
|
// The iframe document around the rendered html: the site's stylesheets from the adapter's
|
|
183
202
|
// preview knob, or a styleless document (behind the hint below) when the site sets none.
|
|
@@ -641,7 +660,17 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
641
660
|
role="group"
|
|
642
661
|
aria-label="Editor"
|
|
643
662
|
>
|
|
644
|
-
<EditorToolbar
|
|
663
|
+
<EditorToolbar
|
|
664
|
+
{format}
|
|
665
|
+
{mode}
|
|
666
|
+
onMode={setMode}
|
|
667
|
+
{device}
|
|
668
|
+
onDevice={setDevice}
|
|
669
|
+
{focusMode}
|
|
670
|
+
onFocusMode={setFocusMode}
|
|
671
|
+
{typewriter}
|
|
672
|
+
onTypewriter={setTypewriter}
|
|
673
|
+
>
|
|
645
674
|
{#snippet insertControls()}
|
|
646
675
|
<!-- Plain triggers only: the dialogs they open hold their own <form> elements, so the
|
|
647
676
|
dialogs themselves mount outside the edit form at the bottom of this component. -->
|
|
@@ -704,6 +733,8 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
704
733
|
registerGetSelection={(fn) => (getSelection = fn)}
|
|
705
734
|
registerFormat={(fn) => (format = fn)}
|
|
706
735
|
{completionSources}
|
|
736
|
+
{focusMode}
|
|
737
|
+
{typewriter}
|
|
707
738
|
/>
|
|
708
739
|
</div>
|
|
709
740
|
{#if mode === 'preview'}
|
|
@@ -5,7 +5,8 @@ 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
|
|
8
|
+
widths, reported to the host through `onDevice`. The More menu also carries the host's persisted
|
|
9
|
+
writing-mode toggles (focus mode, typewriter scrolling) as pressed-state buttons above the format picks. The glyphs are stroke SVG icons in the admin's
|
|
9
10
|
house style (24x24 viewBox, `currentColor`, round caps).
|
|
10
11
|
-->
|
|
11
12
|
<script lang="ts">
|
|
@@ -25,11 +26,30 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
25
26
|
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
26
27
|
* while Preview shows. */
|
|
27
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;
|
|
28
37
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
29
38
|
insertControls?: Snippet;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
let {
|
|
41
|
+
let {
|
|
42
|
+
format,
|
|
43
|
+
mode,
|
|
44
|
+
onMode,
|
|
45
|
+
device = 'desktop',
|
|
46
|
+
onDevice,
|
|
47
|
+
focusMode = false,
|
|
48
|
+
onFocusMode,
|
|
49
|
+
typewriter = false,
|
|
50
|
+
onTypewriter,
|
|
51
|
+
insertControls,
|
|
52
|
+
}: Props = $props();
|
|
33
53
|
|
|
34
54
|
// Each icon is a set of stroke `<path>` d-strings rendered into the shared 24x24 svg below, so the
|
|
35
55
|
// markup stays declarative (no per-icon raw html). Paths follow the house outline style.
|
|
@@ -76,6 +96,8 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
76
96
|
];
|
|
77
97
|
|
|
78
98
|
const ellipsisPaths = ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'];
|
|
99
|
+
// The check glyph marking an active pick, shared by the More menu's toggles and the device list.
|
|
100
|
+
const checkPaths = ['M20 6 9 17l-5-5'];
|
|
79
101
|
|
|
80
102
|
const moreItems: { kind: FormatKind; label: string }[] = [
|
|
81
103
|
{ kind: 'strike', label: 'Strikethrough' },
|
|
@@ -91,10 +113,14 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
91
113
|
let moreMenu = $state<HTMLUListElement | null>(null);
|
|
92
114
|
let moreOpen = $state(false);
|
|
93
115
|
|
|
116
|
+
// Picking dismisses the menu; hiding returns focus to the trigger, keeping the roving order.
|
|
117
|
+
function hideMenu(menu: HTMLUListElement | null) {
|
|
118
|
+
if (menu?.matches(':popover-open')) menu.hidePopover();
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
function pickMore(kind: FormatKind) {
|
|
95
122
|
format(kind);
|
|
96
|
-
|
|
97
|
-
if (moreMenu?.matches(':popover-open')) moreMenu.hidePopover();
|
|
123
|
+
hideMenu(moreMenu);
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
// The device menu's popover element and its open state, mirrored from the toggle event into
|
|
@@ -107,7 +133,7 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
107
133
|
|
|
108
134
|
function pickDevice(id: PreviewDeviceId) {
|
|
109
135
|
onDevice?.(id);
|
|
110
|
-
|
|
136
|
+
hideMenu(deviceMenu);
|
|
111
137
|
}
|
|
112
138
|
|
|
113
139
|
let toolbarEl = $state<HTMLDivElement | null>(null);
|
|
@@ -239,6 +265,34 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
239
265
|
ontoggle={(e) => (moreOpen = e.newState === 'open')}
|
|
240
266
|
class="dropdown menu menu-sm bg-base-100 rounded-box w-44 border border-[var(--cairn-card-border)] p-1 shadow-[var(--cairn-shadow)]"
|
|
241
267
|
>
|
|
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}
|
|
242
296
|
{#each moreItems as item (item.kind)}
|
|
243
297
|
<li><button type="button" onclick={() => pickMore(item.kind)}>{item.label}</button></li>
|
|
244
298
|
{/each}
|
|
@@ -299,7 +353,7 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
299
353
|
<button type="button" aria-pressed={device === d.id} onclick={() => pickDevice(d.id)}>
|
|
300
354
|
<span class="grow">{deviceLabel(d)}</span>
|
|
301
355
|
{#if device === d.id}
|
|
302
|
-
{@render strokeIcon(
|
|
356
|
+
{@render strokeIcon(checkPaths)}
|
|
303
357
|
{/if}
|
|
304
358
|
</button>
|
|
305
359
|
</li>
|
|
@@ -13,6 +13,14 @@ 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;
|
|
16
24
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
17
25
|
insertControls?: Snippet;
|
|
18
26
|
}
|
|
@@ -22,7 +30,8 @@ interface Props {
|
|
|
22
30
|
* right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
23
31
|
* Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
24
32
|
* Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
25
|
-
* widths, reported to the host through `onDevice`. The
|
|
33
|
+
* widths, reported to the host through `onDevice`. The More menu also carries the host's persisted
|
|
34
|
+
* writing-mode toggles (focus mode, typewriter scrolling) as pressed-state buttons above the format picks. The glyphs are stroke SVG icons in the admin's
|
|
26
35
|
* house style (24x24 viewBox, `currentColor`, round caps).
|
|
27
36
|
*/
|
|
28
37
|
declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
|
|
@@ -27,6 +27,10 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
27
27
|
/** Generic CodeMirror completion sources wired into the editor; the link autocomplete is one. The
|
|
28
28
|
* type is referenced inline so no static `@codemirror/*` import sits in this client-only file. */
|
|
29
29
|
completionSources?: import('@codemirror/autocomplete').CompletionSource[];
|
|
30
|
+
/** Focus mode: dim every line outside the caret's paragraph. Off by default. */
|
|
31
|
+
focusMode?: boolean;
|
|
32
|
+
/** Typewriter scroll: hold the cursor line at vertical center while typing. Off by default. */
|
|
33
|
+
typewriter?: boolean;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
let {
|
|
@@ -37,6 +41,8 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
37
41
|
registerGetSelection,
|
|
38
42
|
registerFormat,
|
|
39
43
|
completionSources = [],
|
|
44
|
+
focusMode = false,
|
|
45
|
+
typewriter = false,
|
|
40
46
|
}: Props = $props();
|
|
41
47
|
|
|
42
48
|
let host = $state<HTMLDivElement | null>(null);
|
|
@@ -45,6 +51,12 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
45
51
|
// browser. The type-only `import(...)` annotation is erased; the value import is dynamic in onMount,
|
|
46
52
|
// so the server bundle never pulls CodeMirror (guarded by the editor-boundary test).
|
|
47
53
|
let view: import('@codemirror/view').EditorView | null = null;
|
|
54
|
+
// The writing-mode extensions live in their own compartments so the toolbar toggles swap them
|
|
55
|
+
// in and out of the mounted editor without rebuilding it. Assigned in onMount with the rest of
|
|
56
|
+
// the dynamic editor modules.
|
|
57
|
+
let modes: typeof import('./editor-modes.js') | null = null;
|
|
58
|
+
let focusCompartment: import('@codemirror/state').Compartment | null = null;
|
|
59
|
+
let typewriterCompartment: import('@codemirror/state').Compartment | null = null;
|
|
48
60
|
|
|
49
61
|
onMount(async () => {
|
|
50
62
|
const viewMod = await import('@codemirror/view');
|
|
@@ -54,6 +66,7 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
54
66
|
const languageMod = await import('@codemirror/language');
|
|
55
67
|
const autocompleteMod = await import('@codemirror/autocomplete');
|
|
56
68
|
const highlightMod = await import('./editor-highlight.js');
|
|
69
|
+
const modesMod = await import('./editor-modes.js');
|
|
57
70
|
|
|
58
71
|
if (!host) return;
|
|
59
72
|
|
|
@@ -61,29 +74,65 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
61
74
|
// Mirror the admin theme into CodeMirror's own dark flag, so its base chrome (the autocomplete
|
|
62
75
|
// tooltip above all) renders dark-on-dark instead of light-on-dark.
|
|
63
76
|
const isDark = host.closest('[data-theme]')?.getAttribute('data-theme')?.includes('dark') ?? false;
|
|
64
|
-
// The directive machinery treatment
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
// 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 at x offsets 0-2, 4-6, and 8-10 with 2px
|
|
79
|
+
// of surface between them, stacked as inset box shadows (top layer first, so each bar sits
|
|
80
|
+
// over the spacer and deeper bar beneath it). The alphas step through the per-theme vars in
|
|
81
|
+
// cairn-admin.css; the fallbacks are the light values, so the editor still renders sensibly
|
|
82
|
+
// outside an admin theme wrapper. On a fence line the colon runs, brackets, and {attrs}
|
|
83
|
+
// braces dim to the marker tone while the name and label keep a depth-stepped ink. Leaf and
|
|
84
|
+
// inline directives keep a fixed 8% accent chip; the accent ink holds AA on it (4.75:1
|
|
85
|
+
// light, 5.20:1 dark).
|
|
86
|
+
const railFallbacks = ['72%', '82%', '92%'];
|
|
87
|
+
const railColor = (step: number | 'active', fallback: string) =>
|
|
88
|
+
`color-mix(in oklab, var(--color-accent) var(--cairn-directive-rail-${step}, ${fallback}), transparent)`;
|
|
89
|
+
// With `active`, the row's own (deepest) bar takes the full-strength -active mix and widens
|
|
90
|
+
// 1px, so the caret's container reads at a glance; a bar-width change shifts no text.
|
|
91
|
+
const rails = (depth: number, active = false): string => {
|
|
92
|
+
const layers: string[] = [];
|
|
93
|
+
for (let d = 1; d <= depth; d++) {
|
|
94
|
+
const edge = 4 * d - 2;
|
|
95
|
+
if (d > 1) layers.push(`inset ${edge - 2}px 0 0 0 var(--color-base-100, oklch(99% 0.004 75))`);
|
|
96
|
+
const own = active && d === depth;
|
|
97
|
+
layers.push(
|
|
98
|
+
own
|
|
99
|
+
? `inset ${edge + 1}px 0 0 0 ${railColor('active', '100%')}`
|
|
100
|
+
: `inset ${edge}px 0 0 0 ${railColor(d, railFallbacks[d - 1] ?? '92%')}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return layers.join(', ');
|
|
104
|
+
};
|
|
72
105
|
const directiveInk = {
|
|
73
|
-
backgroundColor:
|
|
106
|
+
backgroundColor: 'color-mix(in oklab, var(--color-accent) 8%, transparent)',
|
|
74
107
|
color: 'var(--color-accent)',
|
|
75
108
|
};
|
|
109
|
+
// The rail rules, one quiet and one caret-active pair per visual depth step (deeper nesting
|
|
110
|
+
// shares the third step). Fence and content rows at a depth share a rule, so a fence and its
|
|
111
|
+
// body rail identically. The caret-active selector adds the caret-block class, so it outranks
|
|
112
|
+
// its quiet twin on any contested row and the caret's container reads one step stronger.
|
|
113
|
+
const railRules: Record<string, { boxShadow: string }> = {};
|
|
114
|
+
for (const depth of [1, 2, 3]) {
|
|
115
|
+
const row = (prefix: string) =>
|
|
116
|
+
`${prefix}.cm-cairn-directive-fence.cm-cairn-depth-${depth}, ${prefix}.cm-cairn-directive-content.cm-cairn-depth-${depth}`;
|
|
117
|
+
railRules[row('')] = { boxShadow: rails(depth) };
|
|
118
|
+
railRules[row('.cm-cairn-caret-block')] = { boxShadow: rails(depth, true) };
|
|
119
|
+
}
|
|
76
120
|
const theme = EditorView.theme(
|
|
77
121
|
{
|
|
78
|
-
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '
|
|
122
|
+
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '1rem' },
|
|
79
123
|
// The 50vh floor keeps a short entry reading as a writing surface, and because the
|
|
80
124
|
// contenteditable content area carries the height, a click in the empty space below the
|
|
81
|
-
// text still lands in the editor and focuses it.
|
|
125
|
+
// text still lands in the editor and focuses it. The 70ch cap with auto margins holds
|
|
126
|
+
// the manuscript to a readable measure, centered in whatever width the card gives it.
|
|
82
127
|
'.cm-content': {
|
|
83
|
-
|
|
128
|
+
// The theme roots set --font-editor to the self-hosted iA Writer Mono; the inline
|
|
129
|
+
// fallback keeps the surface monospace outside an admin theme wrapper.
|
|
130
|
+
fontFamily: "var(--font-editor, ui-monospace, monospace)",
|
|
84
131
|
padding: '0.875rem 1.25rem',
|
|
85
132
|
lineHeight: '1.8',
|
|
86
133
|
minHeight: '50vh',
|
|
134
|
+
maxWidth: '70ch',
|
|
135
|
+
margin: '0 auto',
|
|
87
136
|
},
|
|
88
137
|
'.cm-cursor': { borderLeftColor: 'var(--color-primary)' },
|
|
89
138
|
// A quiet always-on focus hairline. :focus-visible is no escape here: browsers treat a
|
|
@@ -96,32 +145,56 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
96
145
|
outlineOffset: '-1px',
|
|
97
146
|
},
|
|
98
147
|
'.cm-line': { padding: '0' },
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
'.cm-cairn-directive-
|
|
105
|
-
|
|
106
|
-
|
|
148
|
+
// The gutter: directive rows pad left so the text clears the deepest rail stack. It is
|
|
149
|
+
// static structure (caret-independent), so caret movement shifts no layout.
|
|
150
|
+
'.cm-cairn-directive-fence, .cm-cairn-directive-content': { paddingLeft: '1.25rem' },
|
|
151
|
+
...railRules,
|
|
152
|
+
'.cm-cairn-directive-mark': { color: 'var(--color-muted)' },
|
|
153
|
+
'.cm-cairn-directive-label': { color: 'var(--color-accent)' },
|
|
154
|
+
'.cm-cairn-directive-label.cm-cairn-depth-2': { color: 'var(--cairn-directive-ink-2, oklch(50% 0.16 300))' },
|
|
155
|
+
'.cm-cairn-directive-label.cm-cairn-depth-3': { color: 'var(--cairn-directive-ink-3, oklch(48% 0.16 300))' },
|
|
156
|
+
// Cursor-aware emphasis for the label ink: the caret's container takes the strongest
|
|
157
|
+
// ink, through the -active variable in cairn-admin.css. This selector TIES the depth
|
|
158
|
+
// rules above at two classes, so its place after them breaks the tie in its favor.
|
|
159
|
+
'.cm-cairn-caret-block .cm-cairn-directive-label': {
|
|
160
|
+
color: 'var(--cairn-directive-ink-active, oklch(46% 0.16 300))',
|
|
107
161
|
},
|
|
108
|
-
'.cm-cairn-directive-content.cm-cairn-depth-1': { boxShadow: rail(1, '75%') },
|
|
109
|
-
'.cm-cairn-directive-content.cm-cairn-depth-2': { boxShadow: rail(2, '82%') },
|
|
110
|
-
'.cm-cairn-directive-content.cm-cairn-depth-3': { boxShadow: rail(3, '90%') },
|
|
111
162
|
'.cm-cairn-directive-leaf': directiveInk,
|
|
112
163
|
'.cm-cairn-directive-inline': directiveInk,
|
|
164
|
+
// Focus mode's dim ink, on the lines editor-modes marks outside the caret's paragraph.
|
|
165
|
+
// Last on purpose: a dimmed line's spans (markers, tokens, directive labels) all drop to
|
|
166
|
+
// the dim tone, and spec order breaks the specificity ties with the label rules above.
|
|
167
|
+
// The fallback is the light theme's value, like the rail fallbacks. Backgrounds flatten
|
|
168
|
+
// along with the ink: the dim tone on the code chip or an 8% accent chip measures under
|
|
169
|
+
// the design's 3:1 floor, so a dimmed line keeps no tinted chip behind its text. The
|
|
170
|
+
// span arm outranks the chip rules on specificity (the highlight style's generated
|
|
171
|
+
// class, the inline-directive mark); the line arm covers the leaf chip, where spec
|
|
172
|
+
// order breaks the tie.
|
|
173
|
+
'.cm-cairn-focus-dim, .cm-cairn-focus-dim span, .cm-cairn-focus-dim .cm-cairn-directive-label': {
|
|
174
|
+
color: 'var(--cairn-focus-dim-ink, oklch(66% 0.01 75))',
|
|
175
|
+
backgroundColor: 'transparent',
|
|
176
|
+
},
|
|
113
177
|
},
|
|
114
178
|
{ dark: isDark },
|
|
115
179
|
);
|
|
116
180
|
|
|
181
|
+
modes = modesMod;
|
|
182
|
+
focusCompartment = new stateMod.Compartment();
|
|
183
|
+
typewriterCompartment = new stateMod.Compartment();
|
|
184
|
+
|
|
117
185
|
view = new EditorView({
|
|
118
186
|
parent: host,
|
|
119
187
|
state: stateMod.EditorState.create({
|
|
120
188
|
doc: value,
|
|
121
189
|
extensions: [
|
|
190
|
+
focusCompartment.of(focusMode ? modesMod.focusMode() : []),
|
|
191
|
+
typewriterCompartment.of(typewriter ? modesMod.typewriterScroll() : []),
|
|
122
192
|
commandsMod.history(),
|
|
123
193
|
keymap.of([...autocompleteMod.completionKeymap, ...commandsMod.defaultKeymap, ...commandsMod.historyKeymap]),
|
|
124
|
-
|
|
194
|
+
// The GFM base (strikethrough, tables, task lists, autolink) over the commonmark
|
|
195
|
+
// default. markdown() also wires markdownKeymap (Enter continues a list, Backspace
|
|
196
|
+
// removes an empty marker) at high precedence through its addKeymap default.
|
|
197
|
+
markdownMod.markdown({ base: markdownMod.markdownLanguage }),
|
|
125
198
|
...(completionSources.length
|
|
126
199
|
? // interactionDelay 0: the popup opens only on an explicit `[[` trigger, so the default
|
|
127
200
|
// accidental-accept guard adds no value and would swallow an immediate Enter into a newline.
|
|
@@ -158,6 +231,21 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
158
231
|
view.dispatch({ changes: { from: 0, to: current.length, insert: incoming } });
|
|
159
232
|
});
|
|
160
233
|
|
|
234
|
+
// Reconfigure the writing-mode compartments when their props change. Reading `mounted` re-runs
|
|
235
|
+
// the effect once the editor exists, so a preference arriving between render and mount still
|
|
236
|
+
// applies; the reconfigure is idempotent, so the extra pass after mount costs nothing.
|
|
237
|
+
$effect(() => {
|
|
238
|
+
const focus = focusMode;
|
|
239
|
+
const typing = typewriter;
|
|
240
|
+
if (!mounted || !view || !modes || !focusCompartment || !typewriterCompartment) return;
|
|
241
|
+
view.dispatch({
|
|
242
|
+
effects: [
|
|
243
|
+
focusCompartment.reconfigure(focus ? modes.focusMode() : []),
|
|
244
|
+
typewriterCompartment.reconfigure(typing ? modes.typewriterScroll() : []),
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
161
249
|
function insertAtCursor(text: string) {
|
|
162
250
|
if (!view) {
|
|
163
251
|
value = value ? `${value}\n\n${text}` : text;
|
|
@@ -15,6 +15,10 @@ interface Props {
|
|
|
15
15
|
/** Generic CodeMirror completion sources wired into the editor; the link autocomplete is one. The
|
|
16
16
|
* type is referenced inline so no static `@codemirror/*` import sits in this client-only file. */
|
|
17
17
|
completionSources?: import('@codemirror/autocomplete').CompletionSource[];
|
|
18
|
+
/** Focus mode: dim every line outside the caret's paragraph. Off by default. */
|
|
19
|
+
focusMode?: boolean;
|
|
20
|
+
/** Typewriter scroll: hold the cursor line at vertical center while typing. Off by default. */
|
|
21
|
+
typewriter?: boolean;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
@font-face{font-family:'Figtree Variable';font-style:normal;font-display:swap;font-weight:300 900;src:url('./fonts/figtree.woff2') format('woff2')}
|
|
2
2
|
@font-face{font-family:'Bricolage Grotesque Variable';font-style:normal;font-display:swap;font-weight:400 800;src:url('./fonts/bricolage-grotesque.woff2') format('woff2')}
|
|
3
|
+
@font-face{font-family:'iA Writer Mono';font-style:normal;font-display:swap;font-weight:400;src:url('./fonts/ia-writer-mono-latin-400-normal.woff2') format('woff2')}
|
|
4
|
+
@font-face{font-family:'iA Writer Mono';font-style:normal;font-display:swap;font-weight:700;src:url('./fonts/ia-writer-mono-latin-700-normal.woff2') format('woff2')}
|
|
5
|
+
@font-face{font-family:'iA Writer Mono';font-style:italic;font-display:swap;font-weight:400;src:url('./fonts/ia-writer-mono-latin-400-italic.woff2') format('woff2')}
|
|
6
|
+
@font-face{font-family:'iA Writer Mono';font-style:italic;font-display:swap;font-weight:700;src:url('./fonts/ia-writer-mono-latin-700-italic.woff2') format('woff2')}
|
|
3
7
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
4
8
|
@layer properties {
|
|
5
9
|
@supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
|
|
@@ -3290,6 +3294,10 @@
|
|
|
3290
3294
|
}
|
|
3291
3295
|
}
|
|
3292
3296
|
|
|
3297
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .my-1 {
|
|
3298
|
+
margin-block: calc(var(--spacing) * 1);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3293
3301
|
@layer daisyui.l1.l2.l3 {
|
|
3294
3302
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .breadcrumbs {
|
|
3295
3303
|
max-width: 100%;
|
|
@@ -5432,6 +5440,7 @@
|
|
|
5432
5440
|
color-scheme: light;
|
|
5433
5441
|
--font-body: "Figtree Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5434
5442
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5443
|
+
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5435
5444
|
font-family: var(--font-body);
|
|
5436
5445
|
-webkit-font-smoothing: antialiased;
|
|
5437
5446
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -5445,14 +5454,15 @@
|
|
|
5445
5454
|
--color-secondary-content: oklch(98% .004 75);
|
|
5446
5455
|
--color-accent: oklch(54% .16 300);
|
|
5447
5456
|
--color-accent-content: oklch(98% .012 300);
|
|
5448
|
-
--cairn-directive-band-1: 8%;
|
|
5449
|
-
--cairn-directive-band-2: 14%;
|
|
5450
|
-
--cairn-directive-band-3: 20%;
|
|
5451
5457
|
--cairn-directive-ink-2: oklch(50% .16 300);
|
|
5452
5458
|
--cairn-directive-ink-3: oklch(48% .16 300);
|
|
5453
|
-
--cairn-directive-rail-1:
|
|
5459
|
+
--cairn-directive-rail-1: 72%;
|
|
5454
5460
|
--cairn-directive-rail-2: 82%;
|
|
5455
|
-
--cairn-directive-rail-3:
|
|
5461
|
+
--cairn-directive-rail-3: 92%;
|
|
5462
|
+
--cairn-directive-rail-active: 100%;
|
|
5463
|
+
--cairn-directive-ink-active: oklch(46% .16 300);
|
|
5464
|
+
--cairn-code-chip: oklch(94.5% .008 75);
|
|
5465
|
+
--cairn-focus-dim-ink: oklch(66% .01 75);
|
|
5456
5466
|
--color-neutral: oklch(32% .012 75);
|
|
5457
5467
|
--color-neutral-content: oklch(96% .004 75);
|
|
5458
5468
|
--color-info: oklch(52% .12 240);
|
|
@@ -5481,6 +5491,7 @@
|
|
|
5481
5491
|
color-scheme: dark;
|
|
5482
5492
|
--font-body: "Figtree Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5483
5493
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5494
|
+
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5484
5495
|
font-family: var(--font-body);
|
|
5485
5496
|
-webkit-font-smoothing: antialiased;
|
|
5486
5497
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -5494,14 +5505,15 @@
|
|
|
5494
5505
|
--color-secondary-content: oklch(20% .008 75);
|
|
5495
5506
|
--color-accent: oklch(70% .14 300);
|
|
5496
5507
|
--color-accent-content: oklch(20% .04 300);
|
|
5497
|
-
--cairn-directive-band-1: 10%;
|
|
5498
|
-
--cairn-directive-band-2: 16%;
|
|
5499
|
-
--cairn-directive-band-3: 22%;
|
|
5500
5508
|
--cairn-directive-ink-2: oklch(74% .14 300);
|
|
5501
5509
|
--cairn-directive-ink-3: oklch(78% .14 300);
|
|
5502
|
-
--cairn-directive-rail-1:
|
|
5503
|
-
--cairn-directive-rail-2:
|
|
5504
|
-
--cairn-directive-rail-3:
|
|
5510
|
+
--cairn-directive-rail-1: 62%;
|
|
5511
|
+
--cairn-directive-rail-2: 74%;
|
|
5512
|
+
--cairn-directive-rail-3: 86%;
|
|
5513
|
+
--cairn-directive-rail-active: 100%;
|
|
5514
|
+
--cairn-directive-ink-active: oklch(82% .14 300);
|
|
5515
|
+
--cairn-code-chip: oklch(29.5% .012 75);
|
|
5516
|
+
--cairn-focus-dim-ink: oklch(53% .01 75);
|
|
5505
5517
|
--color-neutral: oklch(80% .01 75);
|
|
5506
5518
|
--color-neutral-content: oklch(22% .008 75);
|
|
5507
5519
|
--color-info: oklch(72% .12 240);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { HighlightStyle } from '@codemirror/language';
|
|
2
2
|
import { ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view';
|
|
3
|
+
import { type FenceScan } from './markdown-directives.js';
|
|
3
4
|
/** Markdown token colors over the admin theme variables. */
|
|
4
5
|
export declare function cairnHighlightStyle(): HighlightStyle;
|
|
5
6
|
/** Line and mark decorations flagging remark-directive machinery. */
|
|
6
7
|
export declare function cairnDirectivePlugin(): ViewPlugin<{
|
|
7
8
|
decorations: DecorationSet;
|
|
8
|
-
|
|
9
|
+
scan: FenceScan;
|
|
9
10
|
update(update: ViewUpdate): void;
|
|
10
11
|
}, undefined>;
|