@glw907/cairn-cms 0.41.0 → 0.51.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 +82 -0
- package/README.md +2 -2
- package/dist/ambient.d.ts +9 -0
- package/dist/ambient.js +1 -0
- package/dist/components/AdminLayout.svelte +6 -8
- package/dist/components/CairnAdmin.svelte +67 -0
- package/dist/components/CairnAdmin.svelte.d.ts +35 -0
- package/dist/components/ConceptList.svelte +4 -5
- package/dist/components/ConceptList.svelte.d.ts +4 -8
- package/dist/components/ConfirmPage.svelte +1 -1
- package/dist/components/EditPage.svelte +107 -25
- package/dist/components/EditPage.svelte.d.ts +8 -10
- package/dist/components/EditorToolbar.svelte +79 -8
- package/dist/components/EditorToolbar.svelte.d.ts +10 -2
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/LoginPage.svelte.d.ts +1 -1
- package/dist/components/ManageEditors.svelte +4 -3
- package/dist/components/ManageEditors.svelte.d.ts +2 -1
- package/dist/components/MarkdownEditor.svelte +20 -2
- package/dist/components/cairn-admin.css +57 -9
- package/dist/components/editor-highlight.d.ts +1 -0
- package/dist/components/editor-highlight.js +31 -8
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +10 -0
- package/dist/components/markdown-directives.js +54 -1
- package/dist/components/markdown-format.d.ts +0 -8
- package/dist/components/markdown-format.js +0 -28
- 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/links.d.ts +8 -0
- package/dist/content/links.js +28 -0
- package/dist/content/types.d.ts +35 -2
- package/dist/delivery/data.d.ts +3 -5
- package/dist/delivery/data.js +2 -3
- package/dist/delivery/feeds.js +1 -7
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/manifest.d.ts +0 -5
- package/dist/delivery/manifest.js +5 -16
- package/dist/{sveltekit → delivery}/public-routes.d.ts +4 -4
- package/dist/{sveltekit → delivery}/public-routes.js +7 -7
- package/dist/delivery/site-indexes.d.ts +3 -3
- package/dist/delivery/site-indexes.js +3 -3
- package/dist/delivery/{site-index.d.ts → site-resolver.d.ts} +7 -3
- package/dist/delivery/{site-index.js → site-resolver.js} +13 -3
- package/dist/delivery/sitemap.js +1 -3
- package/dist/delivery/xml.d.ts +2 -0
- package/dist/delivery/xml.js +11 -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/email.js +4 -11
- package/dist/env.d.ts +3 -2
- package/dist/env.js +12 -6
- package/dist/escape.d.ts +2 -0
- package/dist/escape.js +11 -0
- package/dist/github/credentials.d.ts +2 -1
- package/dist/github/credentials.js +10 -2
- package/dist/github/types.d.ts +2 -0
- package/dist/github/types.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +2 -0
- package/dist/nav/site-config.js +2 -0
- package/dist/sveltekit/admin-dispatch.d.ts +28 -0
- package/dist/sveltekit/admin-dispatch.js +62 -0
- package/dist/sveltekit/cairn-admin.d.ts +94 -0
- package/dist/sveltekit/cairn-admin.js +126 -0
- package/dist/sveltekit/condition-response.d.ts +1 -0
- package/dist/sveltekit/condition-response.js +25 -0
- package/dist/sveltekit/content-routes.d.ts +39 -15
- package/dist/sveltekit/content-routes.js +84 -50
- package/dist/sveltekit/guard.d.ts +8 -2
- package/dist/sveltekit/guard.js +18 -4
- package/dist/sveltekit/https-required-page.js +2 -1
- package/dist/sveltekit/index.d.ts +3 -1
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/nav-routes.d.ts +3 -1
- package/dist/sveltekit/nav-routes.js +22 -19
- package/dist/sveltekit/static-admin-page.d.ts +0 -2
- package/dist/sveltekit/static-admin-page.js +1 -8
- package/dist/sveltekit/types.d.ts +18 -11
- package/dist/vite/index.d.ts +16 -0
- package/dist/vite/index.js +57 -13
- package/package.json +6 -2
- package/src/lib/ambient.ts +19 -0
- package/src/lib/components/AdminLayout.svelte +6 -8
- package/src/lib/components/CairnAdmin.svelte +67 -0
- package/src/lib/components/ConceptList.svelte +4 -5
- package/src/lib/components/ConfirmPage.svelte +1 -1
- package/src/lib/components/EditPage.svelte +107 -25
- package/src/lib/components/EditorToolbar.svelte +79 -8
- package/src/lib/components/LoginPage.svelte +2 -2
- package/src/lib/components/ManageEditors.svelte +4 -3
- package/src/lib/components/MarkdownEditor.svelte +20 -2
- package/src/lib/components/cairn-admin.css +59 -0
- package/src/lib/components/editor-highlight.ts +32 -7
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +51 -1
- package/src/lib/components/markdown-format.ts +0 -27
- package/src/lib/components/preview-doc.ts +82 -0
- package/src/lib/content/compose.ts +1 -0
- package/src/lib/content/links.ts +28 -0
- package/src/lib/content/types.ts +34 -2
- package/src/lib/delivery/data.ts +3 -5
- package/src/lib/delivery/feeds.ts +1 -8
- package/src/lib/delivery/index.ts +2 -2
- package/src/lib/delivery/manifest.ts +5 -18
- package/src/lib/{sveltekit → delivery}/public-routes.ts +11 -11
- package/src/lib/delivery/site-indexes.ts +6 -6
- package/src/lib/delivery/{site-index.ts → site-resolver.ts} +20 -8
- package/src/lib/delivery/sitemap.ts +1 -4
- package/src/lib/delivery/xml.ts +12 -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/email.ts +4 -11
- package/src/lib/env.ts +12 -6
- package/src/lib/escape.ts +12 -0
- package/src/lib/github/credentials.ts +6 -2
- package/src/lib/github/types.ts +5 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/nav/site-config.ts +3 -0
- package/src/lib/sveltekit/admin-dispatch.ts +75 -0
- package/src/lib/sveltekit/cairn-admin.ts +177 -0
- package/src/lib/sveltekit/condition-response.ts +27 -1
- package/src/lib/sveltekit/content-routes.ts +131 -62
- package/src/lib/sveltekit/guard.ts +20 -5
- package/src/lib/sveltekit/https-required-page.ts +2 -1
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/nav-routes.ts +24 -21
- package/src/lib/sveltekit/static-admin-page.ts +1 -9
- package/src/lib/sveltekit/types.ts +16 -7
- package/src/lib/vite/index.ts +71 -17
- package/dist/delivery/paginate.d.ts +0 -12
- package/dist/delivery/paginate.js +0 -20
- package/dist/render/index.d.ts +0 -5
- package/dist/render/index.js +0 -8
- package/src/lib/delivery/paginate.ts +0 -32
- package/src/lib/render/index.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.51.0
|
|
6
|
+
|
|
7
|
+
The `svelte` peer dependency floor rises from `^5.0.0` to `^5.56.3`, turning the 0.40.0 advisory
|
|
8
|
+
into an enforced range: consumer sites compile the shipped `.svelte` sources, and svelte `5.56.1`
|
|
9
|
+
miscompiles parenthesized boolean groupings. `cairn-doctor` gains a `config.dependency-floors`
|
|
10
|
+
check in its default set, which compares the lockfile's resolved `svelte` and `@sveltejs/kit`
|
|
11
|
+
versions against the peer ranges the installed engine declares.
|
|
12
|
+
|
|
13
|
+
Consumers must: raise the `svelte` devDependency range to at least `^5.56.3` (and `@sveltejs/kit`
|
|
14
|
+
to `^2.12` where it sits lower) and reinstall so the lockfile re-resolves. A site pinning svelte
|
|
15
|
+
below the floor now draws an npm peer warning or resolution failure on install, and the doctor
|
|
16
|
+
reports the below-floor version as a blocker.
|
|
17
|
+
|
|
18
|
+
The edit page's preview now renders inside a sandboxed iframe whose document links the site's own
|
|
19
|
+
stylesheets, so an entry proofs in the site's real styling without that CSS ever touching the
|
|
20
|
+
admin document. The adapter gains an optional `preview` member naming the compiled CSS URLs (a
|
|
21
|
+
Vite `?url` import resolves the hashed asset) plus `bodyClass` and `containerClass` for the site's
|
|
22
|
+
body classes and content wrapper, and a `byConcept` map overrides either class per concept for a
|
|
23
|
+
site whose posts and pages wrap content differently. While Preview shows, the sidebar steps aside
|
|
24
|
+
so the document takes the full width, and a width menu on the Preview tab sizes the frame to
|
|
25
|
+
Desktop, Tablet, Phone, or Small phone, persisted per browser.
|
|
26
|
+
|
|
27
|
+
Consumers should: wire `preview` in the adapter, referencing the sheet only through `?url` and
|
|
28
|
+
linking the same URL from the site layout, the way
|
|
29
|
+
[the adapter guide](docs/guides/define-an-adapter-and-schema.md) shows. Without the knob the
|
|
30
|
+
preview renders unstyled markup behind a one-line hint.
|
|
31
|
+
|
|
32
|
+
The editor's directive highlighting now recognizes labeled and attributed `:::` openers and fences
|
|
33
|
+
of four or more colons, where before only bare closers matched, and nested containers step their
|
|
34
|
+
band and rail tint by depth. No consumer action.
|
|
35
|
+
|
|
36
|
+
`cairn-doctor` derives its missing inputs from the repo it runs in: the backend owner and repo
|
|
37
|
+
plus the sender address come from evaluating the site's config module through the manifest bin's
|
|
38
|
+
Vite machinery, and the Cloudflare account id comes from the wrangler config, with flags and
|
|
39
|
+
environment variables taking precedence. A new `--probe <url>` flag runs a zero-side-effect live
|
|
40
|
+
check against the deployed admin's sign-in surface: the login envelope, the CSRF cookie and field,
|
|
41
|
+
and the uniform non-leak answer to a stranger's sign-in request. Consumers may: drop the `--from`
|
|
42
|
+
and `--repo` flags from doctor invocations and run `npx cairn-doctor --probe <url>` after a
|
|
43
|
+
deploy.
|
|
44
|
+
|
|
45
|
+
## 0.50.0
|
|
46
|
+
|
|
47
|
+
The admin now mounts as one catch-all route. A new `createCairnAdmin(runtime, deps)` facade
|
|
48
|
+
serves every admin view through a single `load` and a single `actions` record, the new
|
|
49
|
+
`CairnAdmin` component switches the views on the discriminated `AdminData`, and `parseAdminPath`
|
|
50
|
+
is the one path authority behind both. A site's whole `/admin` surface is now three files (the
|
|
51
|
+
`$lib/cairn.server.ts` composer plus the `/admin/[...path]` route pair) instead of a per-route
|
|
52
|
+
tree of shims whose action names coupled to engine components by bare string. The admin URLs are
|
|
53
|
+
unchanged. The per-surface factories (`createContentRoutes` and friends) stay public as the
|
|
54
|
+
advanced seam.
|
|
55
|
+
|
|
56
|
+
Consumers must: delete the admin route tree and replace it with the two-file mount plus the
|
|
57
|
+
composer; the exact files are in
|
|
58
|
+
[the canonical admin mount](docs/reference/admin-routes.md) and the migration is the `0.50.0`
|
|
59
|
+
section of [the upgrade guide](docs/guides/upgrade-cairn.md). The engine's auth and shell forms
|
|
60
|
+
now post named actions (`?/request`, `?/confirm`, `?/logout`, `?/publishAll`), so a site that
|
|
61
|
+
mounts `LoginPage`, `ConfirmPage`, or `AdminLayout` directly must register those names, and the
|
|
62
|
+
`/admin/auth/logout` server route leaves the contract.
|
|
63
|
+
|
|
64
|
+
Consumers must: rename `createSiteIndex` to `createSiteResolver` and `SiteIndex` to
|
|
65
|
+
`SiteResolver` where imported from `/delivery/data`; the `paginate` helper is deleted.
|
|
66
|
+
|
|
67
|
+
Consumers must: read `form.error` where they read `form.renameError`. Every action failure now
|
|
68
|
+
carries `error: string` as its one-line summary; the structured extras (`brokenLinks`,
|
|
69
|
+
`inboundLinks`) keep their keys beside it.
|
|
70
|
+
|
|
71
|
+
Consumers must: replace the hand-written `App.Locals` block in `src/app.d.ts` with
|
|
72
|
+
`import '@glw907/cairn-cms/ambient';`, the new type-only subpath that ships the
|
|
73
|
+
`App.Locals.editor` augmentation.
|
|
74
|
+
|
|
75
|
+
The diagnostics registry reaches its remaining runtime sites. A missing `AUTH_DB` on a gated
|
|
76
|
+
admin request renders a branded condition page instead of a silent login redirect, and a missing
|
|
77
|
+
email binding, missing GitHub App credentials, and an invalid site config now carry their
|
|
78
|
+
registered condition ids through the error chain and the logs.
|
|
79
|
+
`deps.mintToken` widens to accept a plain string return. The concept list reads published rows
|
|
80
|
+
from the committed manifest in one call, falling back to the per-file crawl only on a repo with
|
|
81
|
+
no manifest yet. Internal layering rides along (one home each for the link rewriter, the escape
|
|
82
|
+
helpers, and the conflict check) with no consumer surface change.
|
|
83
|
+
|
|
84
|
+
This release publishes together with `0.41.0`, so a site crossing from `0.40.0` takes both
|
|
85
|
+
windows in one upgrade.
|
|
86
|
+
|
|
5
87
|
## 0.41.0
|
|
6
88
|
|
|
7
89
|
`cairn-doctor` ships as a second bin: a setup preflight that runs nine checks over the local config
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ doesn't, cairn is the wrong tool and will not try to meet you halfway.
|
|
|
45
45
|
tutorial, takes you from an empty directory to a deployed site with a working `/admin`.
|
|
46
46
|
2. **[`examples/showcase`](./examples/showcase)** is a complete consumer site wired to the
|
|
47
47
|
engine, and the worked reference for every shape in the docs. When a guide says "mount the
|
|
48
|
-
|
|
48
|
+
admin," the showcase shows the mounted result.
|
|
49
49
|
3. **[The docs](./docs/README.md)** are organized in four arms: the tutorial, task guides,
|
|
50
50
|
one reference page per export, and explanation pages for the architecture and design
|
|
51
51
|
rules.
|
|
@@ -66,7 +66,7 @@ npm install @glw907/cairn-cms
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Peer dependencies: `svelte@^5` and `@sveltejs/kit@^2.12`. A consumer site implements a
|
|
69
|
-
`CairnAdapter` and mounts
|
|
69
|
+
`CairnAdapter` and mounts the whole `/admin` with one catch-all route over the package subpaths:
|
|
70
70
|
|
|
71
71
|
- `@glw907/cairn-cms`: the core engine and adapter contract.
|
|
72
72
|
- `@glw907/cairn-cms/sveltekit`: the server load and action logic.
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -279,7 +279,7 @@ identical on every host regardless of the site's own theme.
|
|
|
279
279
|
<kbd class="ml-auto hidden rounded border border-[var(--cairn-card-border)] px-1.5 text-[0.6875rem] font-medium sm:inline">⌘K</kbd>
|
|
280
280
|
</button>
|
|
281
281
|
</div>
|
|
282
|
-
{#if pendingCount > 0
|
|
282
|
+
{#if pendingCount > 0}
|
|
283
283
|
<div class="flex-none">
|
|
284
284
|
<button type="button" class="btn btn-primary btn-sm" aria-haspopup="dialog" onclick={() => publishAllDialog?.showModal()}>
|
|
285
285
|
Publish site ({pendingCount})
|
|
@@ -349,9 +349,7 @@ identical on every host regardless of the site's own theme.
|
|
|
349
349
|
<form method="dialog" class="modal-backdrop"><button tabindex="-1" aria-label="Close">close</button></form>
|
|
350
350
|
</dialog>
|
|
351
351
|
|
|
352
|
-
|
|
353
|
-
pending ref) must hide the dialog along with its trigger. -->
|
|
354
|
-
{#if pendingCount > 0 && data.concepts.length > 0}
|
|
352
|
+
{#if pendingCount > 0}
|
|
355
353
|
<dialog bind:this={publishAllDialog} class="modal" aria-labelledby="cairn-publish-all-title">
|
|
356
354
|
<div class="modal-box">
|
|
357
355
|
<div class="mb-3 flex items-center justify-between">
|
|
@@ -367,9 +365,9 @@ identical on every host regardless of the site's own theme.
|
|
|
367
365
|
{/each}
|
|
368
366
|
</ul>
|
|
369
367
|
{/each}
|
|
370
|
-
<!-- The publishAll action is
|
|
371
|
-
|
|
372
|
-
<form method="POST" action=
|
|
368
|
+
<!-- The publishAll named action is valid on every authed admin view, so the confirm
|
|
369
|
+
posts to the current page and the topbar works from anywhere. -->
|
|
370
|
+
<form method="POST" action="?/publishAll" class="mt-4 flex justify-end gap-2">
|
|
373
371
|
<CsrfField token={data.csrf} />
|
|
374
372
|
<button type="button" class="btn btn-sm" onclick={() => publishAllDialog?.close()}>Cancel</button>
|
|
375
373
|
<button type="submit" class="btn btn-sm btn-primary">Publish site</button>
|
|
@@ -455,7 +453,7 @@ identical on every host regardless of the site's own theme.
|
|
|
455
453
|
<div class="text-xs capitalize text-[var(--color-subtle)]">{data.user.role}</div>
|
|
456
454
|
</div>
|
|
457
455
|
</div>
|
|
458
|
-
<form method="POST" action="
|
|
456
|
+
<form method="POST" action="?/logout" class="mt-4">
|
|
459
457
|
<CsrfField token={data.csrf} />
|
|
460
458
|
<button type="submit" class="btn btn-ghost btn-sm btn-block justify-start">
|
|
461
459
|
<LogOutIcon class="h-4 w-4" /> Sign out
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The single-mount admin page. A site's catch-all `/admin/[...path]` route renders this one
|
|
4
|
+
component for every admin view, feeding it the discriminated `AdminData` from `createCairnAdmin`'s
|
|
5
|
+
load. It is a pure switcher on `data.view`: the public auth pages mount bare, and the authed views
|
|
6
|
+
mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import AdminLayout from './AdminLayout.svelte';
|
|
10
|
+
import LoginPage from './LoginPage.svelte';
|
|
11
|
+
import ConfirmPage from './ConfirmPage.svelte';
|
|
12
|
+
import ConceptList from './ConceptList.svelte';
|
|
13
|
+
import EditPage from './EditPage.svelte';
|
|
14
|
+
import ManageEditors from './ManageEditors.svelte';
|
|
15
|
+
import NavTree from './NavTree.svelte';
|
|
16
|
+
import type { AdminData } from '../sveltekit/cairn-admin.js';
|
|
17
|
+
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
18
|
+
import type { ComponentRegistry } from '../render/registry.js';
|
|
19
|
+
import type { IconSet } from '../render/glyph.js';
|
|
20
|
+
import type { LinkResolve } from '../content/links.js';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** The discriminated view data from `createCairnAdmin`'s load. */
|
|
24
|
+
data: AdminData;
|
|
25
|
+
/** The last action's result, forwarded to whichever view rendered: the shared content-action
|
|
26
|
+
* failure family (every failure carries `error`), merged with the auth and editors results,
|
|
27
|
+
* so the route's one `form` export covers every view. */
|
|
28
|
+
form?:
|
|
29
|
+
| (ContentFormFailure & {
|
|
30
|
+
sent?: boolean;
|
|
31
|
+
status?: 'sent' | 'send_error' | 'throttled';
|
|
32
|
+
ok?: boolean;
|
|
33
|
+
})
|
|
34
|
+
| null;
|
|
35
|
+
/** The site's design-accurate render pipeline, for the edit view's preview pane. */
|
|
36
|
+
render?: (md: string, opts?: { stagger?: boolean; resolve?: LinkResolve }) => string | Promise<string>;
|
|
37
|
+
/** The site's component registry, for the edit view's insert palette. */
|
|
38
|
+
registry?: ComponentRegistry;
|
|
39
|
+
/** The site's icon set, for the edit view's guided form fields. */
|
|
40
|
+
icons?: IconSet;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let { data, form = null, render, registry, icons }: Props = $props();
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#if data.view === 'login'}
|
|
47
|
+
<LoginPage data={data.page} {form} />
|
|
48
|
+
{:else if data.view === 'confirm'}
|
|
49
|
+
<ConfirmPage data={data.page} />
|
|
50
|
+
{:else}
|
|
51
|
+
<AdminLayout data={data.layout}>
|
|
52
|
+
{#if data.view === 'list'}
|
|
53
|
+
<!-- The single mount reuses this component across /admin/posts -> /admin/pages, so the
|
|
54
|
+
concept id keys the list: crossing concepts remounts it and drops the old query,
|
|
55
|
+
sort, page, and dialog state. -->
|
|
56
|
+
{#key data.page.conceptId}
|
|
57
|
+
<ConceptList data={data.page} {form} />
|
|
58
|
+
{/key}
|
|
59
|
+
{:else if data.view === 'edit'}
|
|
60
|
+
<EditPage data={{ ...data.page, siteName: data.layout.siteName }} {render} {registry} {icons} {form} />
|
|
61
|
+
{:else if data.view === 'editors'}
|
|
62
|
+
<ManageEditors data={data.page} {form} />
|
|
63
|
+
{:else if data.view === 'nav'}
|
|
64
|
+
<NavTree data={data.page} />
|
|
65
|
+
{/if}
|
|
66
|
+
</AdminLayout>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AdminData } from '../sveltekit/cairn-admin.js';
|
|
2
|
+
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
3
|
+
import type { ComponentRegistry } from '../render/registry.js';
|
|
4
|
+
import type { IconSet } from '../render/glyph.js';
|
|
5
|
+
import type { LinkResolve } from '../content/links.js';
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The discriminated view data from `createCairnAdmin`'s load. */
|
|
8
|
+
data: AdminData;
|
|
9
|
+
/** The last action's result, forwarded to whichever view rendered: the shared content-action
|
|
10
|
+
* failure family (every failure carries `error`), merged with the auth and editors results,
|
|
11
|
+
* so the route's one `form` export covers every view. */
|
|
12
|
+
form?: (ContentFormFailure & {
|
|
13
|
+
sent?: boolean;
|
|
14
|
+
status?: 'sent' | 'send_error' | 'throttled';
|
|
15
|
+
ok?: boolean;
|
|
16
|
+
}) | null;
|
|
17
|
+
/** The site's design-accurate render pipeline, for the edit view's preview pane. */
|
|
18
|
+
render?: (md: string, opts?: {
|
|
19
|
+
stagger?: boolean;
|
|
20
|
+
resolve?: LinkResolve;
|
|
21
|
+
}) => string | Promise<string>;
|
|
22
|
+
/** The site's component registry, for the edit view's insert palette. */
|
|
23
|
+
registry?: ComponentRegistry;
|
|
24
|
+
/** The site's icon set, for the edit view's guided form fields. */
|
|
25
|
+
icons?: IconSet;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The single-mount admin page. A site's catch-all `/admin/[...path]` route renders this one
|
|
29
|
+
* component for every admin view, feeding it the discriminated `AdminData` from `createCairnAdmin`'s
|
|
30
|
+
* load. It is a pure switcher on `data.view`: the public auth pages mount bare, and the authed views
|
|
31
|
+
* mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
32
|
+
*/
|
|
33
|
+
declare const CairnAdmin: import("svelte").Component<Props, {}, "">;
|
|
34
|
+
type CairnAdmin = ReturnType<typeof CairnAdmin>;
|
|
35
|
+
export default CairnAdmin;
|
|
@@ -7,8 +7,7 @@ content sizes. The header New button opens a dialog holding the create form.
|
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
import { slugify } from '../content/ids.js';
|
|
10
|
-
import type { EntrySummary, ListData } from '../sveltekit/content-routes.js';
|
|
11
|
-
import type { InboundLink } from '../content/manifest.js';
|
|
10
|
+
import type { DeleteRefusal, EntrySummary, ListData } from '../sveltekit/content-routes.js';
|
|
12
11
|
import CsrfField from './CsrfField.svelte';
|
|
13
12
|
import DeleteDialog from './DeleteDialog.svelte';
|
|
14
13
|
import CairnLogo from './CairnLogo.svelte';
|
|
@@ -17,10 +16,10 @@ content sizes. The header New button opens a dialog holding the create form.
|
|
|
17
16
|
interface Props {
|
|
18
17
|
/** The list load's data: the concept, its entries, and any inline or form errors. */
|
|
19
18
|
data: ListData;
|
|
20
|
-
/** The `?/delete` action result. A blocked delete returns the
|
|
21
|
-
*
|
|
19
|
+
/** The `?/delete` action result. A blocked delete returns the `DeleteRefusal` payload (the
|
|
20
|
+
* shared `error` summary, the refused entry id, and its inbound linkers), so the list names
|
|
22
21
|
* the blockers and refuses (block-until-clean). */
|
|
23
|
-
form?:
|
|
22
|
+
form?: Partial<DeleteRefusal> | null;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
let { data, form = null }: Props = $props();
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import type { ListData } from '../sveltekit/content-routes.js';
|
|
2
|
-
import type { InboundLink } from '../content/manifest.js';
|
|
1
|
+
import type { DeleteRefusal, ListData } from '../sveltekit/content-routes.js';
|
|
3
2
|
interface Props {
|
|
4
3
|
/** The list load's data: the concept, its entries, and any inline or form errors. */
|
|
5
4
|
data: ListData;
|
|
6
|
-
/** The `?/delete` action result. A blocked delete returns the
|
|
7
|
-
*
|
|
5
|
+
/** The `?/delete` action result. A blocked delete returns the `DeleteRefusal` payload (the
|
|
6
|
+
* shared `error` summary, the refused entry id, and its inbound linkers), so the list names
|
|
8
7
|
* the blockers and refuses (block-until-clean). */
|
|
9
|
-
form?:
|
|
10
|
-
id?: string;
|
|
11
|
-
inboundLinks?: InboundLink[];
|
|
12
|
-
} | null;
|
|
8
|
+
form?: Partial<DeleteRefusal> | null;
|
|
13
9
|
}
|
|
14
10
|
/**
|
|
15
11
|
* One concept's list view as a DaisyUI data-table: a search filter, a result count, sortable Title and
|
|
@@ -40,7 +40,7 @@ in a hidden field and consumes nothing; only the explicit POST verifies (spec §
|
|
|
40
40
|
{:else}
|
|
41
41
|
<h1 class="text-lg font-semibold">Almost there</h1>
|
|
42
42
|
<p class="mt-1 mb-5 text-sm text-[var(--color-muted)]">Confirm to finish signing in to {data.siteName}.</p>
|
|
43
|
-
<form method="POST">
|
|
43
|
+
<form method="POST" action="?/confirm">
|
|
44
44
|
<input type="hidden" name="token" value={data.token} />
|
|
45
45
|
<CsrfField token={data.csrf} />
|
|
46
46
|
<button type="submit" class="btn btn-primary btn-block">Confirm sign-in</button>
|
|
@@ -6,13 +6,16 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
6
6
|
remaining fields group in the sidebar under Details, Visibility (the draft boolean as the Hidden
|
|
7
7
|
toggle), and Address (the slug with the Change URL trigger). The toolbar's Write/Preview tabs
|
|
8
8
|
swap the editing surface for the rendered preview inside the same card; every visit lands on
|
|
9
|
-
Write.
|
|
9
|
+
Write. Preview renders inside a sandboxed iframe that links the site's own stylesheets (the
|
|
10
|
+
adapter's `preview` knob), takes the full content width (the sidebar hides until Write), and
|
|
11
|
+
sizes to a persisted device width picked from the toolbar's capsule. A sticky glass header
|
|
12
|
+
carries the breadcrumb, the status badges, the save-state indicator,
|
|
10
13
|
and the lifecycle actions: Save, Publish (riding the same form via formaction while edits are
|
|
11
14
|
pending), and an overflow menu for Discard and Delete. One feedback strip under the header carries the
|
|
12
15
|
transient flashes, and the editor card's footer holds the word count and the Markdown help.
|
|
13
16
|
-->
|
|
14
17
|
<script lang="ts">
|
|
15
|
-
import { untrack } from 'svelte';
|
|
18
|
+
import { flushSync, untrack } from 'svelte';
|
|
16
19
|
import { beforeNavigate } from '$app/navigation';
|
|
17
20
|
import { page } from '$app/state';
|
|
18
21
|
import CsrfField from './CsrfField.svelte';
|
|
@@ -26,10 +29,11 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
26
29
|
import MarkdownHelpDialog from './MarkdownHelpDialog.svelte';
|
|
27
30
|
import { cairnLinkCompletionSource } from './link-completion.js';
|
|
28
31
|
import { unwrapCairnLink, type FormatKind } from './markdown-format.js';
|
|
32
|
+
import { buildPreviewDoc, deviceLabel, previewDevice, previewDevices, type PreviewDeviceId } from './preview-doc.js';
|
|
29
33
|
import { directiveLineKind, findInlineDirectives } from './markdown-directives.js';
|
|
30
34
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
31
35
|
import type { IconSet } from '../render/glyph.js';
|
|
32
|
-
import type { EditData } from '../sveltekit/content-routes.js';
|
|
36
|
+
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
33
37
|
import type { TextareaField, TagsField, FreeTagsField } from '../content/types.js';
|
|
34
38
|
import type { LinkResolve } from '../content/links.js';
|
|
35
39
|
import { manifestLinkResolver } from '../content/manifest.js';
|
|
@@ -43,9 +47,9 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
43
47
|
render?: (md: string, opts?: { stagger?: boolean; resolve?: LinkResolve }) => string | Promise<string>;
|
|
44
48
|
/** The site's icon set, for the guided form's icon fields. */
|
|
45
49
|
icons?: IconSet;
|
|
46
|
-
/** The
|
|
47
|
-
*
|
|
48
|
-
form?:
|
|
50
|
+
/** The last content action's failure: the save guard's broken links, the delete guard's
|
|
51
|
+
* inbound linkers, or a rename refusal, each carrying the shared `error` summary. */
|
|
52
|
+
form?: ContentFormFailure | null;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
let { data, registry, render, icons, form }: Props = $props();
|
|
@@ -97,6 +101,16 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
97
101
|
// The edit form element, for the Ctrl/Cmd+S shortcut's requestSubmit.
|
|
98
102
|
let editForm = $state<HTMLFormElement | null>(null);
|
|
99
103
|
|
|
104
|
+
// A required sidebar field hidden by Preview cannot take the browser's validation report: an
|
|
105
|
+
// invisible control is unfocusable, so the browser cancels the save silently with no message.
|
|
106
|
+
// This capture-phase invalid listener flips back to Write first, and flushSync forces the pane
|
|
107
|
+
// swap inside the event, so the report that follows the invalid events lands on a visible
|
|
108
|
+
// control and the author sees what blocked the save.
|
|
109
|
+
function onFormInvalid() {
|
|
110
|
+
if (mode === 'write') return;
|
|
111
|
+
flushSync(() => (mode = 'write'));
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
// The SvelteKit half of the leave guard. Registered at component init (beforeNavigate wraps
|
|
101
115
|
// onMount, so it must run synchronously here) and auto-unregistered on destroy. A submit's own
|
|
102
116
|
// navigation passes through because busy flips before it starts, and a non-edit POST's because
|
|
@@ -150,6 +164,24 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
150
164
|
let previewHtml = $state('');
|
|
151
165
|
// True after a render call threw, so the preview pane can say so instead of going blank.
|
|
152
166
|
let previewFailed = $state(false);
|
|
167
|
+
// The preview frame's device width, a per-browser preference under its own key (the legacy
|
|
168
|
+
// 'cairn-admin:preview' key from the removed split-pane preview stays untouched). Desktop is
|
|
169
|
+
// the default; the storage read sits in an effect so it never runs during SSR, and it tracks
|
|
170
|
+
// nothing reactive, so it runs once.
|
|
171
|
+
const deviceStorageKey = 'cairn-editor-preview-device';
|
|
172
|
+
let device = $state<PreviewDeviceId>('desktop');
|
|
173
|
+
$effect(() => {
|
|
174
|
+
const stored = localStorage.getItem(deviceStorageKey);
|
|
175
|
+
if (previewDevices.some((d) => d.id === stored)) device = stored as PreviewDeviceId;
|
|
176
|
+
});
|
|
177
|
+
function setDevice(id: PreviewDeviceId) {
|
|
178
|
+
device = id;
|
|
179
|
+
localStorage.setItem(deviceStorageKey, id);
|
|
180
|
+
}
|
|
181
|
+
const activeDevice = $derived(previewDevice(device));
|
|
182
|
+
// The iframe document around the rendered html: the site's stylesheets from the adapter's
|
|
183
|
+
// preview knob, or a styleless document (behind the hint below) when the site sets none.
|
|
184
|
+
const previewDoc = $derived(buildPreviewDoc(previewHtml, data.preview));
|
|
153
185
|
let insert = $state.raw<(text: string) => void>(() => {});
|
|
154
186
|
let insertLink = $state.raw<(href: string, title: string) => void>(() => {});
|
|
155
187
|
// The editor's current selection, registered by MarkdownEditor on mount; the web link dialog
|
|
@@ -220,8 +252,12 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
220
252
|
// not refused. When set, a delete was blocked by a link that appeared since the page loaded.
|
|
221
253
|
const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
|
|
222
254
|
|
|
223
|
-
//
|
|
224
|
-
|
|
255
|
+
// The shared failure summary, rendered only when no richer banner claims the failure: the save
|
|
256
|
+
// and delete guards get their own banners from brokenLinks and inboundLinks below, so this
|
|
257
|
+
// surfaces the rest (a rename refusal, today).
|
|
258
|
+
const formError = $derived(
|
|
259
|
+
form?.error && !form.brokenLinks?.length && !form.inboundLinks?.length ? form.error : '',
|
|
260
|
+
);
|
|
225
261
|
|
|
226
262
|
// The entry this surface is editing. SvelteKit reuses the page component across a same-route
|
|
227
263
|
// navigation (the delete-refused and broken-link banners link entry to entry), so the per-entry
|
|
@@ -279,7 +315,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
279
315
|
);
|
|
280
316
|
const assertiveMessage = $derived.by(() => {
|
|
281
317
|
if (data.error) return data.error;
|
|
282
|
-
if (
|
|
318
|
+
if (formError) return formError;
|
|
283
319
|
if (deleteRefusedLinks.length) {
|
|
284
320
|
const count = deleteRefusedLinks.length;
|
|
285
321
|
return `This ${data.label.toLowerCase()} could not be deleted. ${count} ${count === 1 ? 'page links' : 'pages link'} to it.`;
|
|
@@ -384,7 +420,13 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
384
420
|
}
|
|
385
421
|
}
|
|
386
422
|
}, 150);
|
|
387
|
-
return () =>
|
|
423
|
+
return () => {
|
|
424
|
+
clearTimeout(handle);
|
|
425
|
+
// Every re-run and the final teardown invalidate the in-flight render. The entry-key reset
|
|
426
|
+
// above cannot reach this counter, so without the bump a slow render for entry A could
|
|
427
|
+
// resolve after a same-route hop and write A's html into entry B's pane.
|
|
428
|
+
previewRun++;
|
|
429
|
+
};
|
|
388
430
|
});
|
|
389
431
|
|
|
390
432
|
// Coerce a frontmatter value to a string for text/date/textarea inputs.
|
|
@@ -529,8 +571,8 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
529
571
|
{#if data.error}
|
|
530
572
|
<div class="alert alert-error mb-4 text-sm">{data.error}</div>
|
|
531
573
|
{/if}
|
|
532
|
-
{#if
|
|
533
|
-
<div class="alert alert-error mb-4 text-sm">{
|
|
574
|
+
{#if formError}
|
|
575
|
+
<div class="alert alert-error mb-4 text-sm">{formError}</div>
|
|
534
576
|
{/if}
|
|
535
577
|
{#if deleteRefusedLinks.length}
|
|
536
578
|
<div class="alert alert-error mb-4 flex-col items-start text-sm">
|
|
@@ -571,7 +613,8 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
571
613
|
bind:this={editForm}
|
|
572
614
|
onsubmit={onEditSubmit}
|
|
573
615
|
oninput={onFormInput}
|
|
574
|
-
|
|
616
|
+
oninvalidcapture={onFormInvalid}
|
|
617
|
+
class={mode === 'preview' ? '' : 'lg:grid lg:grid-cols-[1fr_20rem] lg:gap-6'}
|
|
575
618
|
>
|
|
576
619
|
<CsrfField />
|
|
577
620
|
{#if data.isNew}<input type="hidden" name="new" value="1" />{/if}
|
|
@@ -598,7 +641,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
598
641
|
role="group"
|
|
599
642
|
aria-label="Editor"
|
|
600
643
|
>
|
|
601
|
-
<EditorToolbar {format} {mode} onMode={setMode}>
|
|
644
|
+
<EditorToolbar {format} {mode} onMode={setMode} {device} onDevice={setDevice}>
|
|
602
645
|
{#snippet insertControls()}
|
|
603
646
|
<!-- Plain triggers only: the dialogs they open hold their own <form> elements, so the
|
|
604
647
|
dialogs themselves mount outside the edit form at the bottom of this component. -->
|
|
@@ -664,16 +707,53 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
664
707
|
/>
|
|
665
708
|
</div>
|
|
666
709
|
{#if mode === 'preview'}
|
|
667
|
-
<!--
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
710
|
+
<!-- The preview ground: recessed under the floating frame card so the page reads as a
|
|
711
|
+
sheet on the desk. tabindex 0 only while a message shows in place of the iframe;
|
|
712
|
+
with the iframe up the frame itself is the pane's focusable content (the tabpanel
|
|
713
|
+
pattern's completeness requirement). -->
|
|
714
|
+
<div
|
|
715
|
+
id="cairn-pane-preview"
|
|
716
|
+
role="tabpanel"
|
|
717
|
+
aria-labelledby="cairn-tab-preview"
|
|
718
|
+
tabindex={previewHtml && !previewFailed ? undefined : 0}
|
|
719
|
+
class="bg-base-200 px-4 py-6 lg:px-8"
|
|
720
|
+
>
|
|
721
|
+
<!-- The frame column: centered, sized by the picked device (capped at the pane), with
|
|
722
|
+
the width eased; the admin sheet's prefers-reduced-motion rule squashes the move. -->
|
|
723
|
+
<div
|
|
724
|
+
class="cairn-preview-frame mx-auto max-w-full transition-[width] duration-300"
|
|
725
|
+
style:width={activeDevice.width === null ? '100%' : `${activeDevice.width}px`}
|
|
726
|
+
>
|
|
727
|
+
{#if activeDevice.width !== null}
|
|
728
|
+
<p class="mb-2 text-right text-[0.6875rem] font-semibold uppercase tracking-[0.08em] text-[var(--color-muted)]">
|
|
729
|
+
{deviceLabel(activeDevice)}
|
|
730
|
+
</p>
|
|
731
|
+
{/if}
|
|
732
|
+
{#if !data.preview}
|
|
733
|
+
<p class="mb-2 text-xs text-[var(--color-muted)]">
|
|
734
|
+
Preview shows unstyled markup until the adapter's preview option names the site's stylesheets.
|
|
735
|
+
</p>
|
|
736
|
+
{/if}
|
|
737
|
+
<div class="rounded-box border border-[var(--cairn-card-border)] bg-base-100 overflow-hidden shadow-[var(--cairn-shadow)]">
|
|
738
|
+
{#if previewFailed}
|
|
739
|
+
<p class="p-4 text-sm text-[var(--color-muted)]">The preview could not render this content.</p>
|
|
740
|
+
{:else if !previewHtml}
|
|
741
|
+
<p class="p-4 text-sm text-[var(--color-muted)]">Nothing to preview yet.</p>
|
|
742
|
+
{:else}
|
|
743
|
+
<!-- The site's render pipeline already sanitized the html (the floor strips
|
|
744
|
+
scripts and handlers); the empty sandbox is belt and braces on top. The
|
|
745
|
+
frame document's base tag targets every link at a new tab, which the
|
|
746
|
+
sandbox (no allow-popups) blocks, so a proofing click never navigates the
|
|
747
|
+
admin or the frame itself. tabindex 0 keeps the scrollable preview
|
|
748
|
+
keyboard-reachable (an iframe is not a sequential tab stop by itself); on
|
|
749
|
+
a link-heavy page that one inert Tab stop is a deliberate tradeoff. The
|
|
750
|
+
a11y rule reads any tabindex on a non-interactive element as a smell, but
|
|
751
|
+
a scrollable region is the recognized exception. -->
|
|
752
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
753
|
+
<iframe sandbox="" tabindex="0" title="Page preview" srcdoc={previewDoc} class="block h-[70vh] w-full"></iframe>
|
|
754
|
+
{/if}
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
677
757
|
</div>
|
|
678
758
|
{/if}
|
|
679
759
|
<!-- The card footer, part of the same instrument frame. It stays up in Preview too, so the
|
|
@@ -692,7 +772,9 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
692
772
|
</div>
|
|
693
773
|
</div>
|
|
694
774
|
|
|
695
|
-
|
|
775
|
+
<!-- Preview takes the full surface: the sidebar hides (never unmounts, so the uncontrolled
|
|
776
|
+
field edits survive the round trip) and the editor column above spans the whole width. -->
|
|
777
|
+
<aside class="lg:order-2 mt-4 lg:mt-0" class:hidden={mode === 'preview'}>
|
|
696
778
|
<!-- One sidebar card, three labeled groups. Each group is its own fieldset so its eyebrow is
|
|
697
779
|
a real legend that screen readers announce with the fields it holds. -->
|
|
698
780
|
<div class="rounded-box border border-[var(--cairn-card-border)] bg-base-100 flex flex-col gap-5 p-4 shadow-[var(--cairn-shadow)]">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
2
|
import type { IconSet } from '../render/glyph.js';
|
|
3
|
-
import type { EditData } from '../sveltekit/content-routes.js';
|
|
3
|
+
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
4
4
|
import type { LinkResolve } from '../content/links.js';
|
|
5
5
|
interface Props {
|
|
6
6
|
/** The edit load's data, plus the site name for the heading. */
|
|
@@ -16,14 +16,9 @@ interface Props {
|
|
|
16
16
|
}) => string | Promise<string>;
|
|
17
17
|
/** The site's icon set, for the guided form's icon fields. */
|
|
18
18
|
icons?: IconSet;
|
|
19
|
-
/** The
|
|
20
|
-
*
|
|
21
|
-
form?:
|
|
22
|
-
brokenLinks?: string[];
|
|
23
|
-
body?: string;
|
|
24
|
-
inboundLinks?: import('../content/manifest.js').InboundLink[];
|
|
25
|
-
renameError?: string;
|
|
26
|
-
} | null;
|
|
19
|
+
/** The last content action's failure: the save guard's broken links, the delete guard's
|
|
20
|
+
* inbound linkers, or a rename refusal, each carrying the shared `error` summary. */
|
|
21
|
+
form?: ContentFormFailure | null;
|
|
27
22
|
}
|
|
28
23
|
/**
|
|
29
24
|
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
@@ -32,7 +27,10 @@ interface Props {
|
|
|
32
27
|
* remaining fields group in the sidebar under Details, Visibility (the draft boolean as the Hidden
|
|
33
28
|
* toggle), and Address (the slug with the Change URL trigger). The toolbar's Write/Preview tabs
|
|
34
29
|
* swap the editing surface for the rendered preview inside the same card; every visit lands on
|
|
35
|
-
* Write.
|
|
30
|
+
* Write. Preview renders inside a sandboxed iframe that links the site's own stylesheets (the
|
|
31
|
+
* adapter's `preview` knob), takes the full content width (the sidebar hides until Write), and
|
|
32
|
+
* sizes to a persisted device width picked from the toolbar's capsule. A sticky glass header
|
|
33
|
+
* carries the breadcrumb, the status badges, the save-state indicator,
|
|
36
34
|
* and the lifecycle actions: Save, Publish (riding the same form via formaction while edits are
|
|
37
35
|
* pending), and an overflow menu for Discard and Delete. One feedback strip under the header carries the
|
|
38
36
|
* transient flashes, and the editor card's footer holds the word count and the Markdown help.
|