@glw907/cairn-cms 0.52.1 → 0.53.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 +22 -0
- package/dist/components/AdminLayout.svelte +6 -4
- package/dist/components/EditPage.svelte +120 -50
- package/dist/components/EditPage.svelte.d.ts +2 -1
- package/dist/components/EditorToolbar.svelte +3 -43
- package/dist/components/EditorToolbar.svelte.d.ts +3 -11
- package/dist/components/MarkdownEditor.svelte +55 -10
- package/dist/components/MarkdownEditor.svelte.d.ts +3 -0
- package/dist/components/cairn-admin.css +79 -9
- package/dist/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/dist/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/dist/sveltekit/static-admin-page.js +2 -2
- package/package.json +1 -1
- package/src/lib/components/AdminLayout.svelte +6 -4
- package/src/lib/components/EditPage.svelte +120 -50
- package/src/lib/components/EditorToolbar.svelte +3 -43
- package/src/lib/components/MarkdownEditor.svelte +55 -10
- package/src/lib/components/cairn-admin.css +27 -3
- package/src/lib/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/src/lib/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/src/lib/sveltekit/static-admin-page.ts +2 -2
- package/dist/components/fonts/figtree.woff2 +0 -0
- package/src/lib/components/fonts/figtree.woff2 +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@font-face{font-family:'
|
|
1
|
+
@font-face{font-family:'IBM Plex Sans Variable';font-style:normal;font-display:swap;font-weight:100 700;src:url('./fonts/ibm-plex-sans.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
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
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')}
|
|
@@ -180,6 +180,10 @@
|
|
|
180
180
|
outline-offset: -1px;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .cairn-doc-title-dim:not(:focus) {
|
|
184
|
+
color: var(--cairn-focus-dim-ink);
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .menu li > button:not(.btn) {
|
|
184
188
|
font: inherit;
|
|
185
189
|
color: inherit;
|
|
@@ -3278,6 +3282,10 @@
|
|
|
3278
3282
|
margin-inline: calc(var(--spacing) * -4);
|
|
3279
3283
|
}
|
|
3280
3284
|
|
|
3285
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mx-1 {
|
|
3286
|
+
margin-inline: calc(var(--spacing) * 1);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3281
3289
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mx-auto {
|
|
3282
3290
|
margin-inline: auto;
|
|
3283
3291
|
}
|
|
@@ -3294,10 +3302,6 @@
|
|
|
3294
3302
|
}
|
|
3295
3303
|
}
|
|
3296
3304
|
|
|
3297
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .my-1 {
|
|
3298
|
-
margin-block: calc(var(--spacing) * 1);
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
3305
|
@layer daisyui.l1.l2.l3 {
|
|
3302
3306
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .breadcrumbs {
|
|
3303
3307
|
max-width: 100%;
|
|
@@ -3938,6 +3942,10 @@
|
|
|
3938
3942
|
max-height: 60vh;
|
|
3939
3943
|
}
|
|
3940
3944
|
|
|
3945
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .min-h-16 {
|
|
3946
|
+
min-height: calc(var(--spacing) * 16);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3941
3949
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .min-h-64 {
|
|
3942
3950
|
min-height: calc(var(--spacing) * 64);
|
|
3943
3951
|
}
|
|
@@ -4052,8 +4060,16 @@
|
|
|
4052
4060
|
max-width: 30%;
|
|
4053
4061
|
}
|
|
4054
4062
|
|
|
4055
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[
|
|
4056
|
-
max-width:
|
|
4063
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[49rem\] {
|
|
4064
|
+
max-width: 49rem;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[56rem\] {
|
|
4068
|
+
max-width: 56rem;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[72ch\] {
|
|
4072
|
+
max-width: 72ch;
|
|
4057
4073
|
}
|
|
4058
4074
|
|
|
4059
4075
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-full {
|
|
@@ -4250,6 +4266,10 @@
|
|
|
4250
4266
|
gap: calc(var(--spacing) * 6);
|
|
4251
4267
|
}
|
|
4252
4268
|
|
|
4269
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .gap-10 {
|
|
4270
|
+
gap: calc(var(--spacing) * 10);
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4253
4273
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) :where(.space-y-1 > :not(:last-child)) {
|
|
4254
4274
|
--tw-space-y-reverse: 0;
|
|
4255
4275
|
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
|
|
@@ -4630,6 +4650,10 @@
|
|
|
4630
4650
|
padding-inline: calc(var(--spacing) * 6);
|
|
4631
4651
|
}
|
|
4632
4652
|
|
|
4653
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-0 {
|
|
4654
|
+
padding-block: calc(var(--spacing) * 0);
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4633
4657
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-1 {
|
|
4634
4658
|
padding-block: calc(var(--spacing) * 1);
|
|
4635
4659
|
}
|
|
@@ -4706,6 +4730,10 @@
|
|
|
4706
4730
|
font-family: var(--font-display);
|
|
4707
4731
|
}
|
|
4708
4732
|
|
|
4733
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .font-\[family-name\:var\(--font-editor\,ui-monospace\,monospace\)\] {
|
|
4734
|
+
font-family: var(--font-editor, ui-monospace,monospace);
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4709
4737
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .font-mono {
|
|
4710
4738
|
font-family: var(--font-mono);
|
|
4711
4739
|
}
|
|
@@ -4764,6 +4792,16 @@
|
|
|
4764
4792
|
font-size: .8125rem;
|
|
4765
4793
|
}
|
|
4766
4794
|
|
|
4795
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[1\.0625rem\] {
|
|
4796
|
+
font-size: 1.0625rem;
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
@layer daisyui.l1.l2 {
|
|
4800
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .textarea-sm {
|
|
4801
|
+
font-size: max(var(--font-size, .75rem), .75rem);
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4767
4805
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .leading-relaxed {
|
|
4768
4806
|
--tw-leading: var(--leading-relaxed);
|
|
4769
4807
|
line-height: var(--leading-relaxed);
|
|
@@ -5383,6 +5421,12 @@
|
|
|
5383
5421
|
}
|
|
5384
5422
|
}
|
|
5385
5423
|
|
|
5424
|
+
@media (width >= 64rem) {
|
|
5425
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:grid-cols-\[1fr_17rem\] {
|
|
5426
|
+
grid-template-columns: 1fr 17rem;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5386
5430
|
@media (width >= 64rem) {
|
|
5387
5431
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:grid-cols-\[1fr_20rem\] {
|
|
5388
5432
|
grid-template-columns: 1fr 20rem;
|
|
@@ -5401,6 +5445,12 @@
|
|
|
5401
5445
|
}
|
|
5402
5446
|
}
|
|
5403
5447
|
|
|
5448
|
+
@media (width >= 64rem) {
|
|
5449
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:gap-10 {
|
|
5450
|
+
gap: calc(var(--spacing) * 10);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5404
5454
|
@media (width >= 64rem) {
|
|
5405
5455
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:border-r {
|
|
5406
5456
|
border-right-style: var(--tw-border-style);
|
|
@@ -5426,6 +5476,18 @@
|
|
|
5426
5476
|
}
|
|
5427
5477
|
}
|
|
5428
5478
|
|
|
5479
|
+
@media (width >= 64rem) {
|
|
5480
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:px-10 {
|
|
5481
|
+
padding-inline: calc(var(--spacing) * 10);
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
@media (width >= 64rem) {
|
|
5486
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .lg\:py-8 {
|
|
5487
|
+
padding-block: calc(var(--spacing) * 8);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5429
5491
|
@starting-style {
|
|
5430
5492
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .starting\:-translate-y-2 {
|
|
5431
5493
|
--tw-translate-y: calc(var(--spacing) * -2);
|
|
@@ -5442,7 +5504,7 @@
|
|
|
5442
5504
|
|
|
5443
5505
|
[data-theme="cairn-admin"] {
|
|
5444
5506
|
color-scheme: light;
|
|
5445
|
-
--font-body: "
|
|
5507
|
+
--font-body: "IBM Plex Sans Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5446
5508
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5447
5509
|
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5448
5510
|
font-family: var(--font-body);
|
|
@@ -5467,6 +5529,10 @@
|
|
|
5467
5529
|
--cairn-directive-ink-active: oklch(46% .16 300);
|
|
5468
5530
|
--cairn-code-chip: oklch(94.5% .008 75);
|
|
5469
5531
|
--cairn-focus-dim-ink: oklch(66% .01 75);
|
|
5532
|
+
--cairn-focus-dim-rail-1: 24%;
|
|
5533
|
+
--cairn-focus-dim-rail-2: 28%;
|
|
5534
|
+
--cairn-focus-dim-rail-3: 32%;
|
|
5535
|
+
--cairn-focus-dim-rail-active: 36%;
|
|
5470
5536
|
--color-neutral: oklch(32% .012 75);
|
|
5471
5537
|
--color-neutral-content: oklch(96% .004 75);
|
|
5472
5538
|
--color-info: oklch(52% .12 240);
|
|
@@ -5493,7 +5559,7 @@
|
|
|
5493
5559
|
|
|
5494
5560
|
[data-theme="cairn-admin-dark"] {
|
|
5495
5561
|
color-scheme: dark;
|
|
5496
|
-
--font-body: "
|
|
5562
|
+
--font-body: "IBM Plex Sans Variable", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5497
5563
|
--font-display: "Bricolage Grotesque Variable", var(--font-body);
|
|
5498
5564
|
--font-editor: "iA Writer Mono", ui-monospace, monospace;
|
|
5499
5565
|
font-family: var(--font-body);
|
|
@@ -5518,6 +5584,10 @@
|
|
|
5518
5584
|
--cairn-directive-ink-active: oklch(82% .14 300);
|
|
5519
5585
|
--cairn-code-chip: oklch(29.5% .012 75);
|
|
5520
5586
|
--cairn-focus-dim-ink: oklch(53% .01 75);
|
|
5587
|
+
--cairn-focus-dim-rail-1: 21%;
|
|
5588
|
+
--cairn-focus-dim-rail-2: 25%;
|
|
5589
|
+
--cairn-focus-dim-rail-3: 29%;
|
|
5590
|
+
--cairn-focus-dim-rail-active: 33%;
|
|
5521
5591
|
--color-neutral: oklch(80% .01 75);
|
|
5522
5592
|
--color-neutral-content: oklch(22% .008 75);
|
|
5523
5593
|
--color-info: oklch(72% .12 240);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
|
|
2
2
|
|
|
3
3
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
4
|
This license is copied below, and is also available with a FAQ at:
|
|
@@ -18,7 +18,7 @@ with others.
|
|
|
18
18
|
|
|
19
19
|
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
20
|
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
-
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
22
|
redistributed and/or sold with any software provided that any reserved
|
|
23
23
|
names are not used by derivative works. The fonts and derivatives,
|
|
24
24
|
however, cannot be released under any other type of license. The
|
|
Binary file
|
|
@@ -29,7 +29,7 @@ const SHARED_STYLE = `:root {
|
|
|
29
29
|
--shadow: 0 1px 2px oklch(28% 0.02 75 / 0.05), 0 18px 40px -12px oklch(28% 0.02 75 / 0.16);
|
|
30
30
|
--radius-box: 1rem;
|
|
31
31
|
--radius-field: 0.625rem;
|
|
32
|
-
--font: '
|
|
32
|
+
--font: 'IBM Plex Sans Variable', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
33
33
|
}
|
|
34
34
|
@media (prefers-color-scheme: dark) {
|
|
35
35
|
:root {
|
|
@@ -104,7 +104,7 @@ main {
|
|
|
104
104
|
h1 {
|
|
105
105
|
margin: 0 0 0.75rem;
|
|
106
106
|
font-size: 1.6rem;
|
|
107
|
-
font-weight:
|
|
107
|
+
font-weight: 700;
|
|
108
108
|
letter-spacing: -0.02em;
|
|
109
109
|
line-height: 1.15;
|
|
110
110
|
}
|
package/package.json
CHANGED
|
@@ -244,8 +244,10 @@ identical on every host regardless of the site's own theme.
|
|
|
244
244
|
|
|
245
245
|
<div class="drawer-content flex flex-col">
|
|
246
246
|
<!-- The topbar is a flat, opaque continuation of the sidebar's brand band: same surface and the
|
|
247
|
-
same hairline, no shadow, so the two form one clean header strip across the sidebar seam.
|
|
248
|
-
|
|
247
|
+
same hairline, no shadow, so the two form one clean header strip across the sidebar seam.
|
|
248
|
+
The height is pinned to the brand band's h-16 (a content-driven navbar drifts with font
|
|
249
|
+
metrics, and the two border-bottoms stop meeting at the seam). -->
|
|
250
|
+
<div class="navbar bg-base-100 border-b border-[var(--cairn-card-border)] sticky top-0 z-30 h-16 min-h-16 gap-2 px-4 py-0 lg:px-8">
|
|
249
251
|
<div class="flex-none lg:hidden">
|
|
250
252
|
<label for="cairn-drawer" aria-label="Open menu" class="btn btn-square btn-ghost">
|
|
251
253
|
<MenuIcon class="h-5 w-5" />
|
|
@@ -293,7 +295,7 @@ identical on every host regardless of the site's own theme.
|
|
|
293
295
|
</div>
|
|
294
296
|
</div>
|
|
295
297
|
|
|
296
|
-
<main class="flex-1 p-4 lg:
|
|
298
|
+
<main class="flex-1 p-4 lg:px-10 lg:py-8">
|
|
297
299
|
{@render children()}
|
|
298
300
|
</main>
|
|
299
301
|
|
|
@@ -380,7 +382,7 @@ identical on every host regardless of the site's own theme.
|
|
|
380
382
|
|
|
381
383
|
<div class="drawer-side">
|
|
382
384
|
<label for="cairn-drawer" aria-label="Close menu" class="drawer-overlay"></label>
|
|
383
|
-
<nav class="bg-base-100 flex min-h-full w-
|
|
385
|
+
<nav class="bg-base-100 flex min-h-full w-56 flex-col border-r border-[var(--cairn-card-border)]" aria-label="Site content">
|
|
384
386
|
<!-- Brand band, the same height as the topbar. The mark sits in a filled "app-icon" tile, which
|
|
385
387
|
anchors the corner as a deliberate brand object rather than a washed box. The logo and
|
|
386
388
|
wordmark link to the admin home. -->
|
|
@@ -12,12 +12,16 @@ sizes to a persisted device width picked from the toolbar's capsule. A sticky gl
|
|
|
12
12
|
carries the breadcrumb, the status badges, the save-state indicator,
|
|
13
13
|
and the lifecycle actions: Save, Publish (riding the same form via formaction while edits are
|
|
14
14
|
pending), and an overflow menu for Discard and Delete. One feedback strip under the header carries the
|
|
15
|
-
transient flashes, and the editor card's footer
|
|
15
|
+
transient flashes, and the editor card's footer is the writing-environment strip: the word
|
|
16
|
+
count, the Prose/Markup posture pair, the focus and typewriter toggles, and the Markdown help.
|
|
16
17
|
-->
|
|
17
18
|
<script lang="ts">
|
|
18
19
|
import { flushSync, untrack } from 'svelte';
|
|
19
20
|
import { beforeNavigate } from '$app/navigation';
|
|
20
21
|
import { page } from '$app/state';
|
|
22
|
+
import BlocksIcon from '@lucide/svelte/icons/blocks';
|
|
23
|
+
import LinkIcon from '@lucide/svelte/icons/link';
|
|
24
|
+
import FileSymlinkIcon from '@lucide/svelte/icons/file-symlink';
|
|
21
25
|
import CsrfField from './CsrfField.svelte';
|
|
22
26
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
|
23
27
|
import EditorToolbar from './EditorToolbar.svelte';
|
|
@@ -178,16 +182,21 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
178
182
|
device = id;
|
|
179
183
|
localStorage.setItem(deviceStorageKey, id);
|
|
180
184
|
}
|
|
181
|
-
// The writing modes (focus, typewriter), per-browser preferences on
|
|
182
|
-
//
|
|
183
|
-
//
|
|
185
|
+
// The writing modes (focus, typewriter) and the surface posture, per-browser preferences on
|
|
186
|
+
// the device pick's pattern: read in an effect so SSR never touches localStorage, written by
|
|
187
|
+
// the card footer's toggles. The effect tracks nothing reactive, so it runs once.
|
|
184
188
|
const focusStorageKey = 'cairn-editor-focus-mode';
|
|
185
189
|
const typewriterStorageKey = 'cairn-editor-typewriter';
|
|
190
|
+
const surfaceStorageKey = 'cairn-editor-surface';
|
|
186
191
|
let focusMode = $state(false);
|
|
187
192
|
let typewriter = $state(false);
|
|
193
|
+
// The surface posture: prose (the writing instrument) by default; markup is the dense
|
|
194
|
+
// working surface.
|
|
195
|
+
let surface = $state<'prose' | 'markup'>('prose');
|
|
188
196
|
$effect(() => {
|
|
189
197
|
focusMode = localStorage.getItem(focusStorageKey) === 'true';
|
|
190
198
|
typewriter = localStorage.getItem(typewriterStorageKey) === 'true';
|
|
199
|
+
if (localStorage.getItem(surfaceStorageKey) === 'markup') surface = 'markup';
|
|
191
200
|
});
|
|
192
201
|
function setFocusMode(on: boolean) {
|
|
193
202
|
focusMode = on;
|
|
@@ -197,6 +206,15 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
197
206
|
typewriter = on;
|
|
198
207
|
localStorage.setItem(typewriterStorageKey, String(on));
|
|
199
208
|
}
|
|
209
|
+
function setSurface(posture: 'prose' | 'markup') {
|
|
210
|
+
surface = posture;
|
|
211
|
+
localStorage.setItem(surfaceStorageKey, posture);
|
|
212
|
+
}
|
|
213
|
+
// One source for the footer toggles' pressed/idle styling. The class names must stay verbatim
|
|
214
|
+
// string literals: the admin CSS build's @source scan reads this file as raw text.
|
|
215
|
+
function footerToggleClass(pressed: boolean): string {
|
|
216
|
+
return `btn btn-ghost btn-xs font-normal ${pressed ? 'bg-primary/10 text-primary' : 'text-[var(--color-muted)]'}`;
|
|
217
|
+
}
|
|
200
218
|
const activeDevice = $derived(previewDevice(device));
|
|
201
219
|
// The iframe document around the rendered html: the site's stylesheets from the adapter's
|
|
202
220
|
// preview knob, or a styleless document (behind the hint below) when the site sets none.
|
|
@@ -633,29 +651,37 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
633
651
|
onsubmit={onEditSubmit}
|
|
634
652
|
oninput={onFormInput}
|
|
635
653
|
oninvalidcapture={onFormInvalid}
|
|
636
|
-
class={mode === 'preview' ? '' : 'lg:grid lg:grid-cols-[
|
|
654
|
+
class={mode === 'preview' ? '' : 'lg:grid lg:grid-cols-[1fr_17rem] lg:gap-10'}
|
|
637
655
|
>
|
|
638
656
|
<CsrfField />
|
|
639
657
|
{#if data.isNew}<input type="hidden" name="new" value="1" />{/if}
|
|
640
658
|
|
|
641
659
|
<!-- In Write mode the card hugs the manuscript: the column caps near the 70ch measure and
|
|
642
660
|
centers, so the card frame never spans emptiness on a wide window. Preview keeps the full
|
|
643
|
-
column for its device frames.
|
|
644
|
-
|
|
645
|
-
the
|
|
646
|
-
|
|
661
|
+
column for its device frames. The cap follows the surface posture: prose hugs its 72ch
|
|
662
|
+
measure (49rem covers it at the prose type step), markup puts the ceiling near 89ch of
|
|
663
|
+
the base face for tables, attributed directives, and long URLs. The toggle lives in the
|
|
664
|
+
card footer with the other writing preferences. -->
|
|
665
|
+
<div class={mode === 'preview' ? 'lg:order-1' : `lg:order-1 mx-auto w-full ${surface === 'prose' ? 'max-w-[49rem]' : 'max-w-[56rem]'}`}>
|
|
647
666
|
{#if titleField}
|
|
648
667
|
<!-- The hoisted document title: large, borderless, in the display face, so the manuscript
|
|
649
668
|
reads as the protagonist. It submits as name="title", the same field as before. The
|
|
650
|
-
admin sheet gives it the editor's quiet focus hairline (see .cairn-doc-title there).
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
669
|
+
admin sheet gives it the editor's quiet focus hairline (see .cairn-doc-title there).
|
|
670
|
+
In markup posture the surface fills the card, so shared inline padding is the whole
|
|
671
|
+
alignment; in prose posture the manuscript centers on its measure, so the wrapper
|
|
672
|
+
mirrors that geometry (the editor face at the prose size, the measure, auto margins).
|
|
673
|
+
Under focus mode the title eases back with the rest of the context unless it holds
|
|
674
|
+
focus itself. -->
|
|
675
|
+
<div class={surface === 'prose' ? 'mb-4 mx-auto w-full max-w-[72ch] px-5 text-[1.0625rem] font-[family-name:var(--font-editor,ui-monospace,monospace)]' : 'mb-4 w-full px-5'}>
|
|
676
|
+
<input
|
|
677
|
+
class="cairn-doc-title w-full border-0 bg-transparent text-3xl font-bold tracking-tight font-[family-name:var(--font-display)] placeholder:text-[var(--color-muted)] {focusMode ? 'cairn-doc-title-dim' : ''}"
|
|
678
|
+
name="title"
|
|
679
|
+
value={str(data.frontmatter.title)}
|
|
680
|
+
placeholder={titleField.label}
|
|
681
|
+
aria-label={titleField.label}
|
|
682
|
+
required={titleField.required}
|
|
683
|
+
/>
|
|
684
|
+
</div>
|
|
659
685
|
{/if}
|
|
660
686
|
<!-- The editor card: the toolbar strip and the editing surface share one frame, so the editor
|
|
661
687
|
reads as a single object. The card carries the formatting shortcuts for everything in it. -->
|
|
@@ -665,52 +691,46 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
665
691
|
role="group"
|
|
666
692
|
aria-label="Editor"
|
|
667
693
|
>
|
|
668
|
-
<EditorToolbar
|
|
669
|
-
{format}
|
|
670
|
-
{mode}
|
|
671
|
-
onMode={setMode}
|
|
672
|
-
{device}
|
|
673
|
-
onDevice={setDevice}
|
|
674
|
-
{focusMode}
|
|
675
|
-
onFocusMode={setFocusMode}
|
|
676
|
-
{typewriter}
|
|
677
|
-
onTypewriter={setTypewriter}
|
|
678
|
-
>
|
|
694
|
+
<EditorToolbar {format} {mode} onMode={setMode} {device} onDevice={setDevice}>
|
|
679
695
|
{#snippet insertControls()}
|
|
680
696
|
<!-- Plain triggers only: the dialogs they open hold their own <form> elements, so the
|
|
681
|
-
dialogs themselves mount outside the edit form at the bottom of this component.
|
|
697
|
+
dialogs themselves mount outside the edit form at the bottom of this component.
|
|
698
|
+
Icon buttons like the format strip beside them: the labels live in aria-label and
|
|
699
|
+
the title tooltip, so the Insert group reads as part of one instrument strip. -->
|
|
682
700
|
{#if hasComponents}
|
|
683
701
|
<button
|
|
684
702
|
type="button"
|
|
685
|
-
class="btn btn-sm btn-ghost"
|
|
703
|
+
class="btn btn-sm btn-ghost btn-square"
|
|
686
704
|
aria-haspopup="dialog"
|
|
687
705
|
aria-label="Insert block"
|
|
706
|
+
title="Insert block"
|
|
688
707
|
disabled={insertDisabled}
|
|
689
708
|
onclick={() => insertDialog?.open()}
|
|
690
709
|
>
|
|
691
|
-
|
|
710
|
+
<BlocksIcon class="h-4 w-4" aria-hidden="true" />
|
|
692
711
|
</button>
|
|
693
712
|
{/if}
|
|
694
713
|
<button
|
|
695
714
|
type="button"
|
|
696
|
-
class="btn btn-sm btn-ghost"
|
|
715
|
+
class="btn btn-sm btn-ghost btn-square"
|
|
697
716
|
aria-haspopup="dialog"
|
|
698
717
|
aria-label="Web link (Ctrl+K)"
|
|
699
718
|
title="Web link (Ctrl+K)"
|
|
700
719
|
disabled={insertDisabled}
|
|
701
720
|
onclick={() => webLinkDialog?.open()}
|
|
702
721
|
>
|
|
703
|
-
|
|
722
|
+
<LinkIcon class="h-4 w-4" aria-hidden="true" />
|
|
704
723
|
</button>
|
|
705
724
|
<button
|
|
706
725
|
type="button"
|
|
707
|
-
class="btn btn-sm btn-ghost"
|
|
726
|
+
class="btn btn-sm btn-ghost btn-square"
|
|
708
727
|
aria-haspopup="dialog"
|
|
709
728
|
aria-label="Link to page"
|
|
729
|
+
title="Link to page"
|
|
710
730
|
disabled={insertDisabled}
|
|
711
731
|
onclick={() => linkPicker?.open()}
|
|
712
732
|
>
|
|
713
|
-
|
|
733
|
+
<FileSymlinkIcon class="h-4 w-4" aria-hidden="true" />
|
|
714
734
|
</button>
|
|
715
735
|
<button
|
|
716
736
|
type="button"
|
|
@@ -733,6 +753,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
733
753
|
<MarkdownEditor
|
|
734
754
|
bind:value={body}
|
|
735
755
|
name="body"
|
|
756
|
+
{surface}
|
|
736
757
|
registerInsert={(fn) => (insert = fn)}
|
|
737
758
|
registerInsertLink={(fn) => (insertLink = fn)}
|
|
738
759
|
registerGetSelection={(fn) => (getSelection = fn)}
|
|
@@ -793,17 +814,64 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
793
814
|
</div>
|
|
794
815
|
{/if}
|
|
795
816
|
<!-- The card footer, part of the same instrument frame. It stays up in Preview too, so the
|
|
796
|
-
frame never jumps between tabs and the count keeps reading while proofing.
|
|
817
|
+
frame never jumps between tabs and the count keeps reading while proofing. The strip
|
|
818
|
+
carries the writing environment (the count, the persisted writing modes, help) while
|
|
819
|
+
the top toolbar acts on the text; the toggles live here visible rather than buried in
|
|
820
|
+
an overflow menu. -->
|
|
797
821
|
<div class="flex items-center justify-between border-t border-[var(--cairn-card-border)] px-3 py-1 text-xs text-[var(--color-muted)]">
|
|
798
822
|
<span>{wordLabel}</span>
|
|
799
|
-
<
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
aria-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
823
|
+
<div class="flex items-center gap-1">
|
|
824
|
+
<!-- The pressed check is the non-color state cue (WCAG 1.4.1): pressed and idle
|
|
825
|
+
toggles share weight and size, so hue alone must not carry the state. -->
|
|
826
|
+
<div role="group" aria-label="Editing surface" class="flex items-center gap-1">
|
|
827
|
+
<button
|
|
828
|
+
type="button"
|
|
829
|
+
class={footerToggleClass(surface === 'prose')}
|
|
830
|
+
aria-pressed={surface === 'prose'}
|
|
831
|
+
onclick={() => setSurface('prose')}
|
|
832
|
+
>
|
|
833
|
+
{#if surface === 'prose'}<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{/if}
|
|
834
|
+
Prose
|
|
835
|
+
</button>
|
|
836
|
+
<button
|
|
837
|
+
type="button"
|
|
838
|
+
class={footerToggleClass(surface === 'markup')}
|
|
839
|
+
aria-pressed={surface === 'markup'}
|
|
840
|
+
onclick={() => setSurface('markup')}
|
|
841
|
+
>
|
|
842
|
+
{#if surface === 'markup'}<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{/if}
|
|
843
|
+
Markup
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
846
|
+
<div class="mx-1 h-4 w-px bg-[var(--cairn-card-border)]" aria-hidden="true"></div>
|
|
847
|
+
<button
|
|
848
|
+
type="button"
|
|
849
|
+
class={footerToggleClass(focusMode)}
|
|
850
|
+
aria-pressed={focusMode}
|
|
851
|
+
onclick={() => setFocusMode(!focusMode)}
|
|
852
|
+
>
|
|
853
|
+
{#if focusMode}<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{/if}
|
|
854
|
+
Focus mode
|
|
855
|
+
</button>
|
|
856
|
+
<button
|
|
857
|
+
type="button"
|
|
858
|
+
class={footerToggleClass(typewriter)}
|
|
859
|
+
aria-pressed={typewriter}
|
|
860
|
+
onclick={() => setTypewriter(!typewriter)}
|
|
861
|
+
>
|
|
862
|
+
{#if typewriter}<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{/if}
|
|
863
|
+
Typewriter
|
|
864
|
+
</button>
|
|
865
|
+
<div class="mx-1 h-4 w-px bg-[var(--cairn-card-border)]" aria-hidden="true"></div>
|
|
866
|
+
<button
|
|
867
|
+
type="button"
|
|
868
|
+
class="btn btn-ghost btn-xs font-normal text-[var(--color-muted)]"
|
|
869
|
+
aria-haspopup="dialog"
|
|
870
|
+
onclick={() => helpDialog?.open()}
|
|
871
|
+
>
|
|
872
|
+
Markdown help
|
|
873
|
+
</button>
|
|
874
|
+
</div>
|
|
807
875
|
</div>
|
|
808
876
|
</div>
|
|
809
877
|
</div>
|
|
@@ -813,7 +881,9 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
813
881
|
<aside class="lg:order-2 mt-4 lg:mt-0" class:hidden={mode === 'preview'}>
|
|
814
882
|
<!-- One sidebar card, three labeled groups. Each group is its own fieldset so its eyebrow is
|
|
815
883
|
a real legend that screen readers announce with the fields it holds. -->
|
|
816
|
-
|
|
884
|
+
<!-- Quieter than the editor card on purpose (hairline, no shadow): the editor is the one
|
|
885
|
+
floating object on the page, and the details read as margin furniture beside it. -->
|
|
886
|
+
<div class="rounded-box border border-[var(--cairn-card-border)] bg-base-100 flex flex-col gap-6 p-4">
|
|
817
887
|
{#if detailFields.length}
|
|
818
888
|
<fieldset class="m-0 flex min-w-0 flex-col gap-3 border-0 p-0">
|
|
819
889
|
<legend class={eyebrowClass}>Details</legend>
|
|
@@ -822,12 +892,12 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
822
892
|
{@const f = field as TextareaField}
|
|
823
893
|
<label class="flex flex-col gap-1">
|
|
824
894
|
<span class="text-sm font-medium">{f.label}</span>
|
|
825
|
-
<textarea class="textarea" name={f.name} aria-label={f.label} rows={f.rows ?? 3}>{str(data.frontmatter[f.name])}</textarea>
|
|
895
|
+
<textarea class="textarea textarea-sm" name={f.name} aria-label={f.label} rows={f.rows ?? 3}>{str(data.frontmatter[f.name])}</textarea>
|
|
826
896
|
</label>
|
|
827
897
|
{:else if field.type === 'date'}
|
|
828
898
|
<label class="flex flex-col gap-1">
|
|
829
899
|
<span class="text-sm font-medium">{field.label}</span>
|
|
830
|
-
<input class="input" type="date" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} />
|
|
900
|
+
<input class="input input-sm" type="date" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} />
|
|
831
901
|
</label>
|
|
832
902
|
{:else if field.type === 'boolean'}
|
|
833
903
|
<label class="label cursor-pointer justify-start gap-2">
|
|
@@ -860,7 +930,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
860
930
|
<label class="flex flex-col gap-1">
|
|
861
931
|
<span class="text-sm font-medium">{f.label}</span>
|
|
862
932
|
<input
|
|
863
|
-
class="input"
|
|
933
|
+
class="input input-sm"
|
|
864
934
|
name={f.name}
|
|
865
935
|
aria-label={f.label}
|
|
866
936
|
placeholder={f.placeholder}
|
|
@@ -870,7 +940,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
870
940
|
{:else}
|
|
871
941
|
<label class="flex flex-col gap-1">
|
|
872
942
|
<span class="text-sm font-medium">{field.label}</span>
|
|
873
|
-
<input class="input" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} required={field.required} />
|
|
943
|
+
<input class="input input-sm" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} required={field.required} />
|
|
874
944
|
</label>
|
|
875
945
|
{/if}
|
|
876
946
|
{/each}
|
|
@@ -5,9 +5,9 @@ More overflow menu, then the host's Insert controls) and the Write/Preview segme
|
|
|
5
5
|
right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
6
6
|
Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
7
7
|
Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
8
|
-
widths, reported to the host through `onDevice`. The
|
|
9
|
-
|
|
10
|
-
house style (24x24 viewBox, `currentColor`, round caps).
|
|
8
|
+
widths, reported to the host through `onDevice`. The writing-mode toggles live in the host's card
|
|
9
|
+
footer (the bottom strip carries the writing environment; this strip acts on the text). The glyphs
|
|
10
|
+
are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`, round caps).
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import type { Snippet } from 'svelte';
|
|
@@ -26,14 +26,6 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
26
26
|
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
27
27
|
* while Preview shows. */
|
|
28
28
|
onDevice?: (id: PreviewDeviceId) => void;
|
|
29
|
-
/** Whether focus mode is on; the More menu's toggle reflects it. */
|
|
30
|
-
focusMode?: boolean;
|
|
31
|
-
/** Flip focus mode. When set, the toggle joins the More menu. */
|
|
32
|
-
onFocusMode?: (on: boolean) => void;
|
|
33
|
-
/** Whether typewriter scrolling is on; the More menu's toggle reflects it. */
|
|
34
|
-
typewriter?: boolean;
|
|
35
|
-
/** Flip typewriter scrolling. When set, the toggle joins the More menu. */
|
|
36
|
-
onTypewriter?: (on: boolean) => void;
|
|
37
29
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
38
30
|
insertControls?: Snippet;
|
|
39
31
|
}
|
|
@@ -44,10 +36,6 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
44
36
|
onMode,
|
|
45
37
|
device = 'desktop',
|
|
46
38
|
onDevice,
|
|
47
|
-
focusMode = false,
|
|
48
|
-
onFocusMode,
|
|
49
|
-
typewriter = false,
|
|
50
|
-
onTypewriter,
|
|
51
39
|
insertControls,
|
|
52
40
|
}: Props = $props();
|
|
53
41
|
|
|
@@ -265,34 +253,6 @@ house style (24x24 viewBox, `currentColor`, round caps).
|
|
|
265
253
|
ontoggle={(e) => (moreOpen = e.newState === 'open')}
|
|
266
254
|
class="dropdown menu menu-sm bg-base-100 rounded-box w-44 border border-[var(--cairn-card-border)] p-1 shadow-[var(--cairn-shadow)]"
|
|
267
255
|
>
|
|
268
|
-
<!-- The writing modes sit above the format items behind a hairline, persisted by the host.
|
|
269
|
-
The device list's idiom: plain buttons with aria-pressed carrying the on/off state (this
|
|
270
|
-
popover list is not an ARIA menu, so a menuitemcheckbox would sit in an invalid context);
|
|
271
|
-
the check glyph mirrors the state visually. A flip leaves the menu open so the new
|
|
272
|
-
pressed state is perceivable in place; only a format pick dismisses it. -->
|
|
273
|
-
{#if onFocusMode}
|
|
274
|
-
<li>
|
|
275
|
-
<button type="button" aria-pressed={focusMode} onclick={() => onFocusMode(!focusMode)}>
|
|
276
|
-
<span class="grow">Focus mode</span>
|
|
277
|
-
{#if focusMode}
|
|
278
|
-
{@render strokeIcon(checkPaths)}
|
|
279
|
-
{/if}
|
|
280
|
-
</button>
|
|
281
|
-
</li>
|
|
282
|
-
{/if}
|
|
283
|
-
{#if onTypewriter}
|
|
284
|
-
<li>
|
|
285
|
-
<button type="button" aria-pressed={typewriter} onclick={() => onTypewriter(!typewriter)}>
|
|
286
|
-
<span class="grow">Typewriter scrolling</span>
|
|
287
|
-
{#if typewriter}
|
|
288
|
-
{@render strokeIcon(checkPaths)}
|
|
289
|
-
{/if}
|
|
290
|
-
</button>
|
|
291
|
-
</li>
|
|
292
|
-
{/if}
|
|
293
|
-
{#if onFocusMode || onTypewriter}
|
|
294
|
-
<li class="my-1 border-t border-[var(--cairn-card-border)]" role="separator"></li>
|
|
295
|
-
{/if}
|
|
296
256
|
{#each moreItems as item (item.kind)}
|
|
297
257
|
<li><button type="button" onclick={() => pickMore(item.kind)}>{item.label}</button></li>
|
|
298
258
|
{/each}
|