@glw907/cairn-cms 0.53.0 → 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 +24 -0
- package/dist/components/AdminLayout.svelte +52 -19
- package/dist/components/EditPage.svelte +372 -110
- package/dist/components/EditPage.svelte.d.ts +2 -1
- package/dist/components/EditorToolbar.svelte +26 -10
- package/dist/components/MarkdownEditor.svelte +108 -14
- 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 +138 -108
- 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/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/package.json +1 -1
- package/src/lib/components/AdminLayout.svelte +52 -19
- package/src/lib/components/EditPage.svelte +372 -110
- package/src/lib/components/EditorToolbar.svelte +26 -10
- package/src/lib/components/MarkdownEditor.svelte +108 -14
- 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 +24 -11
- 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/markdown-directives.ts +42 -0
- package/src/lib/components/topbar-context.ts +30 -0
|
@@ -53,12 +53,12 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
53
53
|
const structureButtons: ToolButton[] = [
|
|
54
54
|
{
|
|
55
55
|
kind: 'h2',
|
|
56
|
-
label: 'Heading',
|
|
56
|
+
label: 'Heading (Ctrl+Alt+2)',
|
|
57
57
|
paths: ['M4 12h8', 'M4 18V6', 'M12 18V6', 'M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1'],
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
kind: 'h3',
|
|
61
|
-
label: 'Smaller heading',
|
|
61
|
+
label: 'Smaller heading (Ctrl+Alt+3)',
|
|
62
62
|
paths: [
|
|
63
63
|
'M4 12h8',
|
|
64
64
|
'M4 18V6',
|
|
@@ -67,32 +67,45 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
67
67
|
'M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2',
|
|
68
68
|
],
|
|
69
69
|
},
|
|
70
|
-
{ 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'] },
|
|
71
71
|
{
|
|
72
72
|
kind: 'ol',
|
|
73
|
-
label: 'Numbered list',
|
|
73
|
+
label: 'Numbered list (Ctrl+Shift+7)',
|
|
74
74
|
paths: ['M10 12h11', 'M10 18h11', 'M10 6h11', 'M4 10h2', 'M4 6h1v4', 'M6 18H4c0-1 2-2 2-3s-1-1.5-2-1'],
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
kind: 'quote',
|
|
78
|
-
label: 'Quote',
|
|
78
|
+
label: 'Quote (Ctrl+Shift+9)',
|
|
79
79
|
paths: [
|
|
80
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',
|
|
81
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',
|
|
82
82
|
],
|
|
83
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
|
+
},
|
|
84
97
|
];
|
|
85
98
|
|
|
86
99
|
const ellipsisPaths = ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'];
|
|
87
100
|
// The check glyph marking an active pick, shared by the More menu's toggles and the device list.
|
|
88
101
|
const checkPaths = ['M20 6 9 17l-5-5'];
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 }[] = [
|
|
93
107
|
{ kind: 'codeblock', label: 'Code block' },
|
|
94
|
-
{ kind: '
|
|
95
|
-
{ kind: 'hr', label: 'Horizontal rule' },
|
|
108
|
+
{ kind: 'hr', label: 'Horizontal rule', divideBefore: true },
|
|
96
109
|
{ kind: 'task', label: 'Task list' },
|
|
97
110
|
];
|
|
98
111
|
|
|
@@ -254,6 +267,9 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
254
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)]"
|
|
255
268
|
>
|
|
256
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}
|
|
257
273
|
<li><button type="button" onclick={() => pickMore(item.kind)}>{item.label}</button></li>
|
|
258
274
|
{/each}
|
|
259
275
|
</ul>
|
|
@@ -77,6 +77,7 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
77
77
|
const autocompleteMod = await import('@codemirror/autocomplete');
|
|
78
78
|
const highlightMod = await import('./editor-highlight.js');
|
|
79
79
|
const modesMod = await import('./editor-modes.js');
|
|
80
|
+
const foldingMod = await import('./editor-folding.js');
|
|
80
81
|
|
|
81
82
|
if (!host) return;
|
|
82
83
|
|
|
@@ -85,9 +86,9 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
85
86
|
// tooltip above all) renders dark-on-dark instead of light-on-dark.
|
|
86
87
|
const isDark = host.closest('[data-theme]')?.getAttribute('data-theme')?.includes('dark') ?? false;
|
|
87
88
|
// The directive machinery treatment: rails, not bands. A row at depth N draws every rail
|
|
88
|
-
// 1..N as literal nested brackets: 2px accent bars
|
|
89
|
-
// of surface between them (
|
|
90
|
-
//
|
|
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
|
|
91
92
|
// layer first, so each bar sits over the spacer and deeper bar beneath it). The alphas step through the per-theme vars in
|
|
92
93
|
// cairn-admin.css; the fallbacks are the light values, so the editor still renders sensibly
|
|
93
94
|
// outside an admin theme wrapper. On a fence line the colon runs, brackets, and {attrs}
|
|
@@ -97,21 +98,25 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
97
98
|
const railFallbacks = ['72%', '82%', '92%'];
|
|
98
99
|
const railColor = (step: number | 'active', fallback: string) =>
|
|
99
100
|
`color-mix(in oklab, var(--color-accent) var(--cairn-directive-rail-${step}, ${fallback}), transparent)`;
|
|
100
|
-
// With `active`, the row's own (deepest) bar takes the full-strength -active mix
|
|
101
|
-
//
|
|
102
|
-
|
|
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 => {
|
|
103
108
|
const layers: string[] = [];
|
|
104
109
|
for (let d = 1; d <= depth; d++) {
|
|
105
|
-
const edge =
|
|
110
|
+
const edge = 8 * d - 6;
|
|
106
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;
|
|
107
113
|
const own = active && d === depth;
|
|
108
114
|
layers.push(
|
|
109
|
-
own
|
|
110
|
-
? `inset ${edge + 1}px 0 0 0 ${railColor('active', '100%')}`
|
|
111
|
-
: `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%')}`,
|
|
112
116
|
);
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
// A depth-1 opener drops its only bar, so the row paints no rail at all.
|
|
119
|
+
return layers.length ? layers.join(', ') : 'none';
|
|
115
120
|
};
|
|
116
121
|
const directiveInk = {
|
|
117
122
|
backgroundColor: 'color-mix(in oklab, var(--color-accent) 8%, transparent)',
|
|
@@ -127,6 +132,14 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
127
132
|
`${prefix}.cm-cairn-directive-fence.cm-cairn-depth-${depth}, ${prefix}.cm-cairn-directive-content.cm-cairn-depth-${depth}`;
|
|
128
133
|
railRules[row('')] = { boxShadow: rails(depth) };
|
|
129
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
|
+
};
|
|
130
143
|
}
|
|
131
144
|
const theme = EditorView.theme(
|
|
132
145
|
{
|
|
@@ -160,10 +173,19 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
160
173
|
outlineOffset: '-1px',
|
|
161
174
|
},
|
|
162
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)' },
|
|
163
182
|
// The gutter: directive rows pad left so the text clears the deepest rail stack (the
|
|
164
|
-
// depth-3 bar ends at
|
|
165
|
-
//
|
|
166
|
-
|
|
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
|
+
},
|
|
167
189
|
...railRules,
|
|
168
190
|
'.cm-cairn-directive-mark': { color: 'var(--color-muted)' },
|
|
169
191
|
'.cm-cairn-directive-label': { color: 'var(--color-accent)' },
|
|
@@ -177,6 +199,68 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
177
199
|
},
|
|
178
200
|
'.cm-cairn-directive-leaf': directiveInk,
|
|
179
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
|
+
},
|
|
180
264
|
// Focus mode's dim ink, on the lines editor-modes marks outside the caret's paragraph.
|
|
181
265
|
// Last on purpose: a dimmed line's spans (markers, tokens, directive labels) all drop to
|
|
182
266
|
// the dim tone, and spec order breaks the specificity ties with the label rules above.
|
|
@@ -190,6 +274,12 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
190
274
|
color: 'var(--cairn-focus-dim-ink, oklch(66% 0.01 75))',
|
|
191
275
|
backgroundColor: 'transparent',
|
|
192
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' },
|
|
193
283
|
// The rails dim with their text: the rail color-mix reads --cairn-directive-rail-N per
|
|
194
284
|
// element, so overriding the percentages on dimmed lines re-resolves every bar in place.
|
|
195
285
|
// Without this the directive block keeps full-strength bars and becomes the one
|
|
@@ -247,6 +337,10 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
247
337
|
EditorView.lineWrapping,
|
|
248
338
|
languageMod.syntaxHighlighting(highlightMod.cairnHighlightStyle()),
|
|
249
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(),
|
|
250
344
|
EditorView.contentAttributes.of({ spellcheck: 'true', autocorrect: 'on', autocapitalize: 'sentences' }),
|
|
251
345
|
theme,
|
|
252
346
|
surfaceCompartment.of(surface === 'prose' ? proseTheme : markupTheme),
|
|
@@ -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,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>
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
--cairn-directive-rail-2: 82%;
|
|
48
48
|
--cairn-directive-rail-3: 92%;
|
|
49
49
|
/* Cursor-aware emphasis: the caret container's own bar steps to a full-strength accent rail
|
|
50
|
-
(
|
|
51
|
-
accent (5.28:1, non-text floor 3:1), ink 46% (7.47:1). Do not
|
|
50
|
+
at the same 2px width (strength only) and an ink one notch past depth 3. Locked pairs on
|
|
51
|
+
base-100: rail = solid accent (5.28:1, non-text floor 3:1), ink 46% (7.47:1). Do not
|
|
52
|
+
lighten without re-checking. */
|
|
52
53
|
--cairn-directive-rail-active: 100%;
|
|
53
54
|
--cairn-directive-ink-active: oklch(46% 0.16 300);
|
|
54
55
|
/* The editor's inline-code chip, a quiet tint beside base-200. Locked pair: base-content ink
|
|
@@ -150,10 +151,10 @@
|
|
|
150
151
|
--cairn-directive-rail-2: 74%;
|
|
151
152
|
--cairn-directive-rail-3: 86%;
|
|
152
153
|
/* Cursor-aware emphasis on dark: the caret container's own bar steps to a solid accent rail
|
|
153
|
-
(
|
|
154
|
-
accent (5.86:1, non-text floor 3:1), ink 82% (8.84:1, priced on the
|
|
155
|
-
render; the 0.14 chroma sits outside sRGB at this lightness). Do not
|
|
156
|
-
re-checking. */
|
|
154
|
+
at the same 2px width (strength only) and an ink one notch past depth 3. Locked pairs on
|
|
155
|
+
base-100: rail = solid accent (5.86:1, non-text floor 3:1), ink 82% (8.84:1, priced on the
|
|
156
|
+
gamut-clipped sRGB render; the 0.14 chroma sits outside sRGB at this lightness). Do not
|
|
157
|
+
darken without re-checking. */
|
|
157
158
|
--cairn-directive-rail-active: 100%;
|
|
158
159
|
--cairn-directive-ink-active: oklch(82% 0.14 300);
|
|
159
160
|
/* The inline-code chip on dark sits one step above base-100 so it reads raised. Locked pair:
|
|
@@ -298,12 +299,24 @@
|
|
|
298
299
|
text-align: start;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
/* The
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
/* The same leveling for every bare admin button, not only menu items: a button styled with
|
|
303
|
+
utilities (the footer's posture segments and mode toggles, a list's sort-column headers, the
|
|
304
|
+
zen chip's exit) otherwise keeps the UA outset border and gray fill, since the admin omits
|
|
305
|
+
global Preflight. A .btn keeps its full DaisyUI chrome, and a utility (a bg-*, a border-l for
|
|
306
|
+
a segment divider) still wins from the utilities layer, so a control opts back into a border
|
|
307
|
+
or fill by naming it. */
|
|
308
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) button:not(.btn) {
|
|
309
|
+
border: 0 solid;
|
|
310
|
+
background-color: transparent;
|
|
311
|
+
font: inherit;
|
|
312
|
+
color: inherit;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* The one header band is the 4rem topbar, and a control the browser scrolls into view could land
|
|
316
|
+
hidden beneath it (WCAG 2.4.11 Focus Not Obscured). The scroll margin keeps any focus or
|
|
317
|
+
fragment scroll clear of the sticky band, with a little slack. */
|
|
305
318
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) :is(a, button, input, textarea, select, summary, [tabindex]) {
|
|
306
|
-
scroll-margin-top:
|
|
319
|
+
scroll-margin-top: 5.5rem;
|
|
307
320
|
}
|
|
308
321
|
}
|
|
309
322
|
|