@glw907/cairn-cms 0.50.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 +61 -0
- package/dist/components/EditPage.svelte +125 -16
- package/dist/components/EditPage.svelte.d.ts +4 -1
- package/dist/components/EditorToolbar.svelte +135 -10
- package/dist/components/EditorToolbar.svelte.d.ts +19 -2
- package/dist/components/MarkdownEditor.svelte +112 -6
- package/dist/components/MarkdownEditor.svelte.d.ts +4 -0
- package/dist/components/cairn-admin.css +69 -9
- package/dist/components/editor-highlight.d.ts +2 -0
- package/dist/components/editor-highlight.js +79 -15
- 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 +51 -0
- package/dist/components/markdown-directives.js +130 -1
- package/dist/components/preview-doc.d.ts +27 -0
- package/dist/components/preview-doc.js +64 -0
- package/dist/content/compose.js +1 -0
- package/dist/content/types.d.ts +33 -0
- package/dist/diagnostics/conditions.js +24 -0
- package/dist/doctor/bin.js +30 -12
- package/dist/doctor/check-floors.d.ts +15 -0
- package/dist/doctor/check-floors.js +107 -0
- package/dist/doctor/check-probe.d.ts +3 -0
- package/dist/doctor/check-probe.js +123 -0
- package/dist/doctor/checks-github.js +1 -1
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +28 -2
- package/dist/doctor/cloudflare-api.js +2 -2
- package/dist/doctor/index.d.ts +28 -3
- package/dist/doctor/index.js +47 -6
- package/dist/doctor/types.d.ts +2 -0
- package/dist/doctor/wrangler-config.d.ts +4 -0
- package/dist/doctor/wrangler-config.js +11 -0
- package/dist/env.d.ts +2 -1
- package/dist/env.js +9 -4
- package/dist/index.d.ts +1 -1
- package/dist/sveltekit/content-routes.d.ts +5 -1
- package/dist/sveltekit/content-routes.js +25 -17
- package/dist/sveltekit/guard.d.ts +8 -2
- package/dist/sveltekit/guard.js +3 -1
- package/dist/sveltekit/nav-routes.js +3 -9
- package/dist/vite/index.d.ts +16 -0
- package/dist/vite/index.js +57 -13
- package/package.json +2 -2
- package/src/lib/components/EditPage.svelte +125 -16
- package/src/lib/components/EditorToolbar.svelte +135 -10
- package/src/lib/components/MarkdownEditor.svelte +112 -6
- package/src/lib/components/cairn-admin.css +95 -5
- package/src/lib/components/editor-highlight.ts +91 -14
- 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 +151 -1
- package/src/lib/components/preview-doc.ts +82 -0
- package/src/lib/content/compose.ts +1 -0
- package/src/lib/content/types.ts +32 -0
- package/src/lib/diagnostics/conditions.ts +24 -0
- package/src/lib/doctor/bin.ts +35 -10
- package/src/lib/doctor/check-floors.ts +124 -0
- package/src/lib/doctor/check-probe.ts +138 -0
- package/src/lib/doctor/checks-github.ts +3 -1
- package/src/lib/doctor/checks-local.ts +28 -2
- package/src/lib/doctor/cloudflare-api.ts +4 -2
- package/src/lib/doctor/index.ts +67 -6
- package/src/lib/doctor/types.ts +2 -0
- package/src/lib/doctor/wrangler-config.ts +11 -0
- package/src/lib/env.ts +9 -4
- package/src/lib/index.ts +2 -0
- package/src/lib/sveltekit/content-routes.ts +29 -17
- package/src/lib/sveltekit/guard.ts +4 -2
- package/src/lib/sveltekit/nav-routes.ts +3 -10
- package/src/lib/vite/index.ts +71 -17
|
@@ -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,22 +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
|
|
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
|
+
};
|
|
65
105
|
const directiveInk = {
|
|
66
106
|
backgroundColor: 'color-mix(in oklab, var(--color-accent) 8%, transparent)',
|
|
67
107
|
color: 'var(--color-accent)',
|
|
68
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
|
+
}
|
|
69
120
|
const theme = EditorView.theme(
|
|
70
121
|
{
|
|
71
|
-
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '
|
|
122
|
+
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '1rem' },
|
|
72
123
|
// The 50vh floor keeps a short entry reading as a writing surface, and because the
|
|
73
124
|
// contenteditable content area carries the height, a click in the empty space below the
|
|
74
|
-
// 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.
|
|
75
127
|
'.cm-content': {
|
|
76
|
-
|
|
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)",
|
|
77
131
|
padding: '0.875rem 1.25rem',
|
|
78
132
|
lineHeight: '1.8',
|
|
79
133
|
minHeight: '50vh',
|
|
134
|
+
maxWidth: '70ch',
|
|
135
|
+
margin: '0 auto',
|
|
80
136
|
},
|
|
81
137
|
'.cm-cursor': { borderLeftColor: 'var(--color-primary)' },
|
|
82
138
|
// A quiet always-on focus hairline. :focus-visible is no escape here: browsers treat a
|
|
@@ -89,21 +145,56 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
89
145
|
outlineOffset: '-1px',
|
|
90
146
|
},
|
|
91
147
|
'.cm-line': { padding: '0' },
|
|
92
|
-
|
|
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))',
|
|
161
|
+
},
|
|
93
162
|
'.cm-cairn-directive-leaf': directiveInk,
|
|
94
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
|
+
},
|
|
95
177
|
},
|
|
96
178
|
{ dark: isDark },
|
|
97
179
|
);
|
|
98
180
|
|
|
181
|
+
modes = modesMod;
|
|
182
|
+
focusCompartment = new stateMod.Compartment();
|
|
183
|
+
typewriterCompartment = new stateMod.Compartment();
|
|
184
|
+
|
|
99
185
|
view = new EditorView({
|
|
100
186
|
parent: host,
|
|
101
187
|
state: stateMod.EditorState.create({
|
|
102
188
|
doc: value,
|
|
103
189
|
extensions: [
|
|
190
|
+
focusCompartment.of(focusMode ? modesMod.focusMode() : []),
|
|
191
|
+
typewriterCompartment.of(typewriter ? modesMod.typewriterScroll() : []),
|
|
104
192
|
commandsMod.history(),
|
|
105
193
|
keymap.of([...autocompleteMod.completionKeymap, ...commandsMod.defaultKeymap, ...commandsMod.historyKeymap]),
|
|
106
|
-
|
|
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 }),
|
|
107
198
|
...(completionSources.length
|
|
108
199
|
? // interactionDelay 0: the popup opens only on an explicit `[[` trigger, so the default
|
|
109
200
|
// accidental-accept guard adds no value and would swallow an immediate Enter into a newline.
|
|
@@ -140,6 +231,21 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
140
231
|
view.dispatch({ changes: { from: 0, to: current.length, insert: incoming } });
|
|
141
232
|
});
|
|
142
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
|
+
|
|
143
249
|
function insertAtCursor(text: string) {
|
|
144
250
|
if (!view) {
|
|
145
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)))) {
|
|
@@ -176,6 +180,14 @@
|
|
|
176
180
|
outline-offset: -1px;
|
|
177
181
|
}
|
|
178
182
|
|
|
183
|
+
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .menu li > button:not(.btn) {
|
|
184
|
+
font: inherit;
|
|
185
|
+
color: inherit;
|
|
186
|
+
text-align: start;
|
|
187
|
+
background-color: #0000;
|
|
188
|
+
border: 0 solid;
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) :is(a, button, input, textarea, select, summary, [tabindex]) {
|
|
180
192
|
scroll-margin-top: 8.5rem;
|
|
181
193
|
}
|
|
@@ -3282,6 +3294,10 @@
|
|
|
3282
3294
|
}
|
|
3283
3295
|
}
|
|
3284
3296
|
|
|
3297
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .my-1 {
|
|
3298
|
+
margin-block: calc(var(--spacing) * 1);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3285
3301
|
@layer daisyui.l1.l2.l3 {
|
|
3286
3302
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .breadcrumbs {
|
|
3287
3303
|
max-width: 100%;
|
|
@@ -3400,15 +3416,6 @@
|
|
|
3400
3416
|
}
|
|
3401
3417
|
}
|
|
3402
3418
|
|
|
3403
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join-item:where(:not(:first-child, :disabled, [disabled], .btn-disabled)) {
|
|
3404
|
-
margin-block-start: 0;
|
|
3405
|
-
margin-inline-start: calc(var(--border, 1px) * -1);
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join-item:where(:is(:disabled, [disabled], .btn-disabled)) {
|
|
3409
|
-
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
3419
|
@layer daisyui.l1.l2.l3 {
|
|
3413
3420
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .modal-action {
|
|
3414
3421
|
justify-content: flex-end;
|
|
@@ -3503,6 +3510,10 @@
|
|
|
3503
3510
|
margin-bottom: calc(var(--spacing) * 7);
|
|
3504
3511
|
}
|
|
3505
3512
|
|
|
3513
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .-ml-px {
|
|
3514
|
+
margin-left: -1px;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3506
3517
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .ml-1 {
|
|
3507
3518
|
margin-left: calc(var(--spacing) * 1);
|
|
3508
3519
|
}
|
|
@@ -3919,6 +3930,10 @@
|
|
|
3919
3930
|
height: calc(var(--spacing) * 16);
|
|
3920
3931
|
}
|
|
3921
3932
|
|
|
3933
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .h-\[70vh\] {
|
|
3934
|
+
height: 70vh;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3922
3937
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-\[60vh\] {
|
|
3923
3938
|
max-height: 60vh;
|
|
3924
3939
|
}
|
|
@@ -4037,6 +4052,10 @@
|
|
|
4037
4052
|
max-width: 30%;
|
|
4038
4053
|
}
|
|
4039
4054
|
|
|
4055
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-full {
|
|
4056
|
+
max-width: 100%;
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4040
4059
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-md {
|
|
4041
4060
|
max-width: var(--container-md);
|
|
4042
4061
|
}
|
|
@@ -4337,6 +4356,16 @@
|
|
|
4337
4356
|
border-radius: var(--radius-xl);
|
|
4338
4357
|
}
|
|
4339
4358
|
|
|
4359
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-l-none {
|
|
4360
|
+
border-top-left-radius: 0;
|
|
4361
|
+
border-bottom-left-radius: 0;
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-r-none {
|
|
4365
|
+
border-top-right-radius: 0;
|
|
4366
|
+
border-bottom-right-radius: 0;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4340
4369
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border {
|
|
4341
4370
|
border-style: var(--tw-border-style);
|
|
4342
4371
|
border-width: 1px;
|
|
@@ -4996,6 +5025,12 @@
|
|
|
4996
5025
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
4997
5026
|
}
|
|
4998
5027
|
|
|
5028
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .transition-\[width\] {
|
|
5029
|
+
transition-property: width;
|
|
5030
|
+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
5031
|
+
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
5032
|
+
}
|
|
5033
|
+
|
|
4999
5034
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .transition-all {
|
|
5000
5035
|
transition-property: all;
|
|
5001
5036
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
@@ -5405,6 +5440,7 @@
|
|
|
5405
5440
|
color-scheme: light;
|
|
5406
5441
|
--font-body: "Figtree Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5407
5442
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5443
|
+
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5408
5444
|
font-family: var(--font-body);
|
|
5409
5445
|
-webkit-font-smoothing: antialiased;
|
|
5410
5446
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -5418,6 +5454,15 @@
|
|
|
5418
5454
|
--color-secondary-content: oklch(98% .004 75);
|
|
5419
5455
|
--color-accent: oklch(54% .16 300);
|
|
5420
5456
|
--color-accent-content: oklch(98% .012 300);
|
|
5457
|
+
--cairn-directive-ink-2: oklch(50% .16 300);
|
|
5458
|
+
--cairn-directive-ink-3: oklch(48% .16 300);
|
|
5459
|
+
--cairn-directive-rail-1: 72%;
|
|
5460
|
+
--cairn-directive-rail-2: 82%;
|
|
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);
|
|
5421
5466
|
--color-neutral: oklch(32% .012 75);
|
|
5422
5467
|
--color-neutral-content: oklch(96% .004 75);
|
|
5423
5468
|
--color-info: oklch(52% .12 240);
|
|
@@ -5446,6 +5491,7 @@
|
|
|
5446
5491
|
color-scheme: dark;
|
|
5447
5492
|
--font-body: "Figtree Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5448
5493
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5494
|
+
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5449
5495
|
font-family: var(--font-body);
|
|
5450
5496
|
-webkit-font-smoothing: antialiased;
|
|
5451
5497
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -5459,6 +5505,15 @@
|
|
|
5459
5505
|
--color-secondary-content: oklch(20% .008 75);
|
|
5460
5506
|
--color-accent: oklch(70% .14 300);
|
|
5461
5507
|
--color-accent-content: oklch(20% .04 300);
|
|
5508
|
+
--cairn-directive-ink-2: oklch(74% .14 300);
|
|
5509
|
+
--cairn-directive-ink-3: oklch(78% .14 300);
|
|
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);
|
|
5462
5517
|
--color-neutral: oklch(80% .01 75);
|
|
5463
5518
|
--color-neutral-content: oklch(22% .008 75);
|
|
5464
5519
|
--color-info: oklch(72% .12 240);
|
|
@@ -5487,6 +5542,11 @@
|
|
|
5487
5542
|
box-sizing: border-box;
|
|
5488
5543
|
}
|
|
5489
5544
|
|
|
5545
|
+
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .menu li > :is(button, a):focus-visible {
|
|
5546
|
+
outline: 2px solid var(--color-primary);
|
|
5547
|
+
outline-offset: -2px;
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5490
5550
|
@media (prefers-reduced-motion: reduce) {
|
|
5491
5551
|
[data-theme="cairn-admin"] *, [data-theme="cairn-admin-dark"] * {
|
|
5492
5552
|
scroll-behavior: auto !important;
|
|
@@ -1,9 +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;
|
|
9
|
+
scan: FenceScan;
|
|
8
10
|
update(update: ViewUpdate): void;
|
|
9
11
|
}, undefined>;
|
|
@@ -5,39 +5,97 @@ import { HighlightStyle } from '@codemirror/language';
|
|
|
5
5
|
import { tags } from '@lezer/highlight';
|
|
6
6
|
import { Decoration, ViewPlugin } from '@codemirror/view';
|
|
7
7
|
import { RangeSetBuilder } from '@codemirror/state';
|
|
8
|
-
import { directiveLineKind, findInlineDirectives } from './markdown-directives.js';
|
|
8
|
+
import { caretContainerRange, directiveLineKind, fenceScan, fenceTokens, findInlineDirectives, } from './markdown-directives.js';
|
|
9
9
|
/** Markdown token colors over the admin theme variables. */
|
|
10
10
|
export function cairnHighlightStyle() {
|
|
11
|
+
// Rule order is load-bearing. HighlightStyle emits its CSS in spec order, so on a span that
|
|
12
|
+
// carries several classes (a marker inherits its heading's or link's class on top of its own
|
|
13
|
+
// processingInstruction one) the later rule wins the tie. The url and processingInstruction
|
|
14
|
+
// rules sit last so the URL part of a link and every syntax marker stay muted under the
|
|
15
|
+
// heading, emphasis, and link inks.
|
|
11
16
|
return HighlightStyle.define([
|
|
12
|
-
{ tag: tags.
|
|
17
|
+
{ tag: tags.heading1, fontSize: '1.5em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
18
|
+
{ tag: tags.heading2, fontSize: '1.3em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
19
|
+
{ tag: tags.heading3, fontSize: '1.17em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
20
|
+
// h4 and deeper share the weight only; body size keeps the low levels from outranking h3.
|
|
21
|
+
{ tag: tags.heading, fontWeight: '700', color: 'var(--color-base-content)' },
|
|
13
22
|
{ tag: tags.strong, fontWeight: '700' },
|
|
14
23
|
{ tag: tags.emphasis, fontStyle: 'italic' },
|
|
15
24
|
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ tag: tags.quote, color: 'var(--color-
|
|
19
|
-
{ tag: tags.monospace, color: 'var(--color-accent)' },
|
|
20
|
-
{ tag: tags.processingInstruction, color: 'var(--color-muted)' },
|
|
25
|
+
// Quote TEXT is content, so it keeps the full ink; muted means machinery, and only the >
|
|
26
|
+
// marker (QuoteMark, under processingInstruction below) recedes to it.
|
|
27
|
+
{ tag: tags.quote, color: 'var(--color-base-content)', fontStyle: 'italic' },
|
|
21
28
|
{ tag: tags.list, color: 'var(--color-muted)' },
|
|
29
|
+
{ tag: tags.link, color: 'var(--color-accent)' },
|
|
30
|
+
{ tag: tags.url, color: 'var(--color-muted)' },
|
|
31
|
+
{
|
|
32
|
+
tag: tags.monospace,
|
|
33
|
+
color: 'var(--color-base-content)',
|
|
34
|
+
backgroundColor: 'var(--cairn-code-chip)',
|
|
35
|
+
borderRadius: '0.25rem',
|
|
36
|
+
padding: '0.05em 0.3em',
|
|
37
|
+
},
|
|
38
|
+
{ tag: tags.processingInstruction, color: 'var(--color-muted)', fontWeight: '400' },
|
|
22
39
|
]);
|
|
23
40
|
}
|
|
24
41
|
// The machinery lines explain themselves on hover, so an editor who has never seen ::: syntax
|
|
25
42
|
// learns what the line is without leaving the page.
|
|
26
43
|
const MACHINERY_HINT = 'Layout marker. Edit the text between these lines and leave this line as it is.';
|
|
27
|
-
|
|
44
|
+
// Nesting deeper than three steps shares the third visual step; the depth model itself is unbounded.
|
|
45
|
+
const DEPTH_STEPS = [1, 2, 3];
|
|
46
|
+
const fenceLines = DEPTH_STEPS.map((d) => Decoration.line({ class: `cm-cairn-directive-fence cm-cairn-depth-${d}`, attributes: { title: MACHINERY_HINT } }));
|
|
47
|
+
const contentLines = DEPTH_STEPS.map((d) => Decoration.line({ class: `cm-cairn-directive-content cm-cairn-depth-${d}` }));
|
|
28
48
|
const leafLine = Decoration.line({ class: 'cm-cairn-directive-leaf', attributes: { title: MACHINERY_HINT } });
|
|
29
49
|
const inlineMark = Decoration.mark({ class: 'cm-cairn-directive-inline' });
|
|
30
|
-
|
|
50
|
+
// Within a fence line, machinery (colons, brackets, braces) dims to the marker tone while the
|
|
51
|
+
// directive name and label keep a depth-stepped ink: meaning over machinery.
|
|
52
|
+
const fenceMark = Decoration.mark({ class: 'cm-cairn-directive-mark' });
|
|
53
|
+
const fenceLabels = DEPTH_STEPS.map((d) => Decoration.mark({ class: `cm-cairn-directive-label cm-cairn-depth-${d}` }));
|
|
54
|
+
// Cursor-aware emphasis: every row of the container the caret sits inside, fence and content
|
|
55
|
+
// alike, carries this class on top of its depth classes; the theme steps that block's rail and
|
|
56
|
+
// label ink up one notch while the other containers sit quieter.
|
|
57
|
+
const caretBlockLine = Decoration.line({ class: 'cm-cairn-caret-block' });
|
|
58
|
+
// Depth needs the whole document, since a visible line's containers can open above the viewport.
|
|
59
|
+
// One regex pass per line, linear in the document; at admin entry sizes (tens of kilobytes) that
|
|
60
|
+
// is well under a millisecond. The plugin caches the fence scan, so it reruns only when the
|
|
61
|
+
// document changes; a scroll or a caret move rebuilds the viewport decorations from the cached
|
|
62
|
+
// scan.
|
|
63
|
+
function docLines(view) {
|
|
64
|
+
const doc = view.state.doc;
|
|
65
|
+
const lines = [];
|
|
66
|
+
for (let n = 1; n <= doc.lines; n++)
|
|
67
|
+
lines.push(doc.line(n).text);
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
function buildDirectiveDecorations(view, scan) {
|
|
71
|
+
const { depths } = scan;
|
|
31
72
|
const builder = new RangeSetBuilder();
|
|
73
|
+
// The caret's container, one helper call over the cached scan per rebuild. Line decorations
|
|
74
|
+
// at the same position must enter the builder in add order, so the caret-block class goes in
|
|
75
|
+
// as its own line decoration just ahead of the row's depth decoration.
|
|
76
|
+
const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
|
|
77
|
+
const caret = caretContainerRange(scan, caretLine);
|
|
32
78
|
for (const { from, to } of view.visibleRanges) {
|
|
33
79
|
for (let pos = from; pos <= to;) {
|
|
34
80
|
const line = view.state.doc.lineAt(pos);
|
|
35
81
|
const kind = directiveLineKind(line.text);
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
const depth = Math.min(depths[line.number - 1] ?? 0, DEPTH_STEPS.length);
|
|
83
|
+
if (caret && line.number - 1 >= caret.fromLine && line.number - 1 <= caret.toLine) {
|
|
84
|
+
builder.add(line.from, line.from, caretBlockLine);
|
|
85
|
+
}
|
|
86
|
+
// A fence-shaped line at depth 0 is one the depth scan disowned (a documented example
|
|
87
|
+
// inside a code block, outside any container); it gets no machinery treatment.
|
|
88
|
+
if (kind === 'fence' && depth > 0) {
|
|
89
|
+
builder.add(line.from, line.from, fenceLines[depth - 1]);
|
|
90
|
+
for (const token of fenceTokens(line.text)) {
|
|
91
|
+
builder.add(line.from + token.from, line.from + token.to, token.kind === 'mark' ? fenceMark : fenceLabels[depth - 1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
38
94
|
else if (kind === 'leaf')
|
|
39
95
|
builder.add(line.from, line.from, leafLine);
|
|
40
|
-
else {
|
|
96
|
+
else if (kind === null) {
|
|
97
|
+
if (depth > 0)
|
|
98
|
+
builder.add(line.from, line.from, contentLines[depth - 1]);
|
|
41
99
|
for (const r of findInlineDirectives(line.text)) {
|
|
42
100
|
builder.add(line.from + r.from, line.from + r.to, inlineMark);
|
|
43
101
|
}
|
|
@@ -51,12 +109,18 @@ function buildDirectiveDecorations(view) {
|
|
|
51
109
|
export function cairnDirectivePlugin() {
|
|
52
110
|
return ViewPlugin.fromClass(class {
|
|
53
111
|
decorations;
|
|
112
|
+
scan;
|
|
54
113
|
constructor(view) {
|
|
55
|
-
this.
|
|
114
|
+
this.scan = fenceScan(docLines(view));
|
|
115
|
+
this.decorations = buildDirectiveDecorations(view, this.scan);
|
|
56
116
|
}
|
|
57
117
|
update(update) {
|
|
58
|
-
if (update.docChanged
|
|
59
|
-
this.
|
|
118
|
+
if (update.docChanged)
|
|
119
|
+
this.scan = fenceScan(docLines(update.view));
|
|
120
|
+
// A selection change rebuilds too, so the caret-block emphasis follows the cursor; the
|
|
121
|
+
// fence scan stays cached, keeping a caret move at viewport cost.
|
|
122
|
+
if (update.docChanged || update.viewportChanged || update.selectionSet)
|
|
123
|
+
this.decorations = buildDirectiveDecorations(update.view, this.scan);
|
|
60
124
|
}
|
|
61
125
|
}, { decorations: (v) => v.decorations });
|
|
62
126
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Extension } from '@codemirror/state';
|
|
2
|
+
/** An inclusive 0-based line range. */
|
|
3
|
+
export interface LineRange {
|
|
4
|
+
fromLine: number;
|
|
5
|
+
toLine: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The contiguous non-blank block around the caret line, the unit focus mode keeps at full ink.
|
|
9
|
+
* On a blank line the caret stands alone; the walk clamps at the document edges, and an
|
|
10
|
+
* out-of-range caret clamps into the document first.
|
|
11
|
+
*/
|
|
12
|
+
export declare function paragraphRange(lines: string[], caretLine: number): LineRange;
|
|
13
|
+
/**
|
|
14
|
+
* Focus mode: a line class (`cm-cairn-focus-dim`) on every line outside the caret's paragraph.
|
|
15
|
+
* The class only marks the lines; the dim ink itself lives in the editor theme so the per-theme
|
|
16
|
+
* `--cairn-focus-dim-ink` variable resolves it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function focusMode(): Extension;
|
|
19
|
+
/**
|
|
20
|
+
* Typewriter scroll: on every doc change, recenter the selection head vertically. An update
|
|
21
|
+
* listener may not dispatch while its update runs, so the recenter is queued as a microtask:
|
|
22
|
+
* it fires as soon as the update finishes, still ahead of the next paint, where an animation
|
|
23
|
+
* frame would trail the edit by a frame. The isConnected guard skips a view destroyed in the
|
|
24
|
+
* queue window.
|
|
25
|
+
*/
|
|
26
|
+
export declare function typewriterScroll(): Extension;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The iA writing modes: focus mode dims every line outside the caret's paragraph, and typewriter
|
|
2
|
+
// scroll holds the cursor line at vertical center while typing. Client-only, like editor-highlight:
|
|
3
|
+
// MarkdownEditor reaches this module through a dynamic import, so the static @codemirror imports
|
|
4
|
+
// here never enter a server bundle (guarded by the editor-boundary test).
|
|
5
|
+
import { Decoration, EditorView, ViewPlugin, } from '@codemirror/view';
|
|
6
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
7
|
+
const isBlank = (line) => !line || /^\s*$/.test(line);
|
|
8
|
+
/**
|
|
9
|
+
* The contiguous non-blank block around the caret line, the unit focus mode keeps at full ink.
|
|
10
|
+
* On a blank line the caret stands alone; the walk clamps at the document edges, and an
|
|
11
|
+
* out-of-range caret clamps into the document first.
|
|
12
|
+
*/
|
|
13
|
+
export function paragraphRange(lines, caretLine) {
|
|
14
|
+
const last = Math.max(lines.length - 1, 0);
|
|
15
|
+
const caret = Math.min(Math.max(caretLine, 0), last);
|
|
16
|
+
if (isBlank(lines[caret]))
|
|
17
|
+
return { fromLine: caret, toLine: caret };
|
|
18
|
+
let fromLine = caret;
|
|
19
|
+
while (fromLine > 0 && !isBlank(lines[fromLine - 1]))
|
|
20
|
+
fromLine--;
|
|
21
|
+
let toLine = caret;
|
|
22
|
+
while (toLine < last && !isBlank(lines[toLine + 1]))
|
|
23
|
+
toLine++;
|
|
24
|
+
return { fromLine, toLine };
|
|
25
|
+
}
|
|
26
|
+
const dimLine = Decoration.line({ class: 'cm-cairn-focus-dim' });
|
|
27
|
+
// The line cache mirrors editor-highlight's: one full-document read per doc change, so a caret
|
|
28
|
+
// move or scroll rebuilds the viewport decorations from the cached array.
|
|
29
|
+
function docLines(view) {
|
|
30
|
+
const doc = view.state.doc;
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (let n = 1; n <= doc.lines; n++)
|
|
33
|
+
lines.push(doc.line(n).text);
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
function buildFocusDecorations(view, lines) {
|
|
37
|
+
const builder = new RangeSetBuilder();
|
|
38
|
+
const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
|
|
39
|
+
const paragraph = paragraphRange(lines, caretLine);
|
|
40
|
+
for (const { from, to } of view.visibleRanges) {
|
|
41
|
+
for (let pos = from; pos <= to;) {
|
|
42
|
+
const line = view.state.doc.lineAt(pos);
|
|
43
|
+
const n = line.number - 1;
|
|
44
|
+
if (n < paragraph.fromLine || n > paragraph.toLine)
|
|
45
|
+
builder.add(line.from, line.from, dimLine);
|
|
46
|
+
pos = line.to + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return builder.finish();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Focus mode: a line class (`cm-cairn-focus-dim`) on every line outside the caret's paragraph.
|
|
53
|
+
* The class only marks the lines; the dim ink itself lives in the editor theme so the per-theme
|
|
54
|
+
* `--cairn-focus-dim-ink` variable resolves it.
|
|
55
|
+
*/
|
|
56
|
+
export function focusMode() {
|
|
57
|
+
return ViewPlugin.fromClass(class {
|
|
58
|
+
decorations;
|
|
59
|
+
lines;
|
|
60
|
+
constructor(view) {
|
|
61
|
+
this.lines = docLines(view);
|
|
62
|
+
this.decorations = buildFocusDecorations(view, this.lines);
|
|
63
|
+
}
|
|
64
|
+
update(update) {
|
|
65
|
+
if (update.docChanged)
|
|
66
|
+
this.lines = docLines(update.view);
|
|
67
|
+
if (update.docChanged || update.viewportChanged || update.selectionSet)
|
|
68
|
+
this.decorations = buildFocusDecorations(update.view, this.lines);
|
|
69
|
+
}
|
|
70
|
+
}, { decorations: (v) => v.decorations });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Typewriter scroll: on every doc change, recenter the selection head vertically. An update
|
|
74
|
+
* listener may not dispatch while its update runs, so the recenter is queued as a microtask:
|
|
75
|
+
* it fires as soon as the update finishes, still ahead of the next paint, where an animation
|
|
76
|
+
* frame would trail the edit by a frame. The isConnected guard skips a view destroyed in the
|
|
77
|
+
* queue window.
|
|
78
|
+
*/
|
|
79
|
+
export function typewriterScroll() {
|
|
80
|
+
return EditorView.updateListener.of((update) => {
|
|
81
|
+
if (!update.docChanged)
|
|
82
|
+
return;
|
|
83
|
+
const view = update.view;
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
if (!view.dom.isConnected)
|
|
86
|
+
return;
|
|
87
|
+
view.dispatch({
|
|
88
|
+
effects: EditorView.scrollIntoView(view.state.selection.main.head, { y: 'center' }),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|