@glw907/cairn-cms 0.53.0 → 0.55.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/components/AdminLayout.svelte +52 -19
  3. package/dist/components/ConceptList.svelte +210 -73
  4. package/dist/components/ConceptList.svelte.d.ts +6 -4
  5. package/dist/components/EditPage.svelte +372 -110
  6. package/dist/components/EditPage.svelte.d.ts +2 -1
  7. package/dist/components/EditorToolbar.svelte +26 -10
  8. package/dist/components/MarkdownEditor.svelte +108 -14
  9. package/dist/components/MarkdownHelpDialog.svelte +5 -0
  10. package/dist/components/ShortcutsDialog.svelte +37 -0
  11. package/dist/components/ShortcutsDialog.svelte.d.ts +13 -0
  12. package/dist/components/ShortcutsGrid.svelte +18 -0
  13. package/dist/components/ShortcutsGrid.svelte.d.ts +23 -0
  14. package/dist/components/cairn-admin.css +184 -104
  15. package/dist/components/editor-folding.d.ts +7 -0
  16. package/dist/components/editor-folding.js +331 -0
  17. package/dist/components/editor-highlight.js +55 -6
  18. package/dist/components/editor-shortcuts.d.ts +16 -0
  19. package/dist/components/editor-shortcuts.js +36 -0
  20. package/dist/components/markdown-directives.d.ts +17 -0
  21. package/dist/components/markdown-directives.js +41 -0
  22. package/dist/components/topbar-context.d.ts +13 -0
  23. package/dist/components/topbar-context.js +17 -0
  24. package/dist/content/manifest.d.ts +1 -0
  25. package/dist/content/manifest.js +6 -0
  26. package/dist/delivery/content-index.js +1 -1
  27. package/dist/delivery/data.d.ts +1 -1
  28. package/dist/delivery/data.js +1 -1
  29. package/dist/sveltekit/content-routes.d.ts +3 -0
  30. package/dist/sveltekit/content-routes.js +10 -5
  31. package/package.json +1 -1
  32. package/src/lib/components/AdminLayout.svelte +52 -19
  33. package/src/lib/components/ConceptList.svelte +210 -73
  34. package/src/lib/components/EditPage.svelte +372 -110
  35. package/src/lib/components/EditorToolbar.svelte +26 -10
  36. package/src/lib/components/MarkdownEditor.svelte +108 -14
  37. package/src/lib/components/MarkdownHelpDialog.svelte +5 -0
  38. package/src/lib/components/ShortcutsDialog.svelte +37 -0
  39. package/src/lib/components/ShortcutsGrid.svelte +18 -0
  40. package/src/lib/components/cairn-admin.css +24 -11
  41. package/src/lib/components/editor-folding.ts +356 -0
  42. package/src/lib/components/editor-highlight.ts +54 -4
  43. package/src/lib/components/editor-shortcuts.ts +42 -0
  44. package/src/lib/components/markdown-directives.ts +42 -0
  45. package/src/lib/components/topbar-context.ts +30 -0
  46. package/src/lib/content/manifest.ts +7 -0
  47. package/src/lib/delivery/content-index.ts +1 -1
  48. package/src/lib/delivery/data.ts +1 -1
  49. package/src/lib/sveltekit/content-routes.ts +13 -5
  50. /package/dist/{delivery → content}/excerpt.d.ts +0 -0
  51. /package/dist/{delivery → content}/excerpt.js +0 -0
  52. /package/src/lib/{delivery → content}/excerpt.ts +0 -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
- const moreItems: { kind: FormatKind; label: string }[] = [
91
- { kind: 'strike', label: 'Strikethrough' },
92
- { kind: 'code', label: 'Inline code' },
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: 'table', label: 'Table' },
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 at x offsets 0-2, 6-8, and 12-14 with 4px
89
- // of surface between them (a gap of twice the bar weight, the floor for two parallel rules
90
- // to read as separate lines rather than one thick one), stacked as inset box shadows (top
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 and widens
101
- // 1px, so the caret's container reads at a glance; a bar-width change shifts no text.
102
- const rails = (depth: number, active = false): string => {
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 = 6 * d - 4;
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
- return layers.join(', ');
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 14px, the active one at 15px; 1.5rem keeps ~10px of air beyond
165
- // it). Static structure (caret-independent), so caret movement shifts no layout.
166
- '.cm-cairn-directive-fence, .cm-cairn-directive-content': { paddingLeft: '1.5rem' },
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
- (also 1px wider) and an ink one notch past depth 3. Locked pairs on base-100: rail = solid
51
- accent (5.28:1, non-text floor 3:1), ink 46% (7.47:1). Do not lighten without re-checking. */
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
- (also 1px wider) and an ink one notch past depth 3. Locked pairs on base-100: rail = solid
154
- accent (5.86:1, non-text floor 3:1), ink 82% (8.84:1, priced on the gamut-clipped sRGB
155
- render; the 0.14 chroma sits outside sRGB at this lightness). Do not darken without
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 admin topbar plus the edit page's sticky action header stack about 120px of veil over the
302
- top of the content column, and a control the browser scrolls into view could land hidden
303
- beneath them (WCAG 2.4.11 Focus Not Obscured). The scroll margin keeps any focus or fragment
304
- scroll clear of the whole sticky stack. */
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: 8.5rem;
319
+ scroll-margin-top: 5.5rem;
307
320
  }
308
321
  }
309
322