@glw907/cairn-cms 0.41.0 → 0.50.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 +42 -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 +13 -9
- package/dist/components/EditPage.svelte.d.ts +4 -9
- 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/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-format.d.ts +0 -8
- package/dist/components/markdown-format.js +0 -28
- package/dist/content/links.d.ts +8 -0
- package/dist/content/links.js +28 -0
- package/dist/content/types.d.ts +2 -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/email.js +4 -11
- package/dist/env.d.ts +1 -1
- package/dist/env.js +3 -2
- 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/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 +34 -14
- package/dist/sveltekit/content-routes.js +59 -33
- package/dist/sveltekit/guard.js +15 -3
- 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 +19 -10
- 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/package.json +5 -1
- 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 +13 -9
- package/src/lib/components/LoginPage.svelte +2 -2
- package/src/lib/components/ManageEditors.svelte +4 -3
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-format.ts +0 -27
- package/src/lib/content/links.ts +28 -0
- package/src/lib/content/types.ts +2 -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/email.ts +4 -11
- package/src/lib/env.ts +3 -2
- 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/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 +102 -45
- package/src/lib/sveltekit/guard.ts +16 -3
- 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 +21 -11
- package/src/lib/sveltekit/static-admin-page.ts +1 -9
- package/src/lib/sveltekit/types.ts +16 -7
- 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,48 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.50.0
|
|
6
|
+
|
|
7
|
+
The admin now mounts as one catch-all route. A new `createCairnAdmin(runtime, deps)` facade
|
|
8
|
+
serves every admin view through a single `load` and a single `actions` record, the new
|
|
9
|
+
`CairnAdmin` component switches the views on the discriminated `AdminData`, and `parseAdminPath`
|
|
10
|
+
is the one path authority behind both. A site's whole `/admin` surface is now three files (the
|
|
11
|
+
`$lib/cairn.server.ts` composer plus the `/admin/[...path]` route pair) instead of a per-route
|
|
12
|
+
tree of shims whose action names coupled to engine components by bare string. The admin URLs are
|
|
13
|
+
unchanged. The per-surface factories (`createContentRoutes` and friends) stay public as the
|
|
14
|
+
advanced seam.
|
|
15
|
+
|
|
16
|
+
Consumers must: delete the admin route tree and replace it with the two-file mount plus the
|
|
17
|
+
composer; the exact files are in
|
|
18
|
+
[the canonical admin mount](docs/reference/admin-routes.md) and the migration is the `0.50.0`
|
|
19
|
+
section of [the upgrade guide](docs/guides/upgrade-cairn.md). The engine's auth and shell forms
|
|
20
|
+
now post named actions (`?/request`, `?/confirm`, `?/logout`, `?/publishAll`), so a site that
|
|
21
|
+
mounts `LoginPage`, `ConfirmPage`, or `AdminLayout` directly must register those names, and the
|
|
22
|
+
`/admin/auth/logout` server route leaves the contract.
|
|
23
|
+
|
|
24
|
+
Consumers must: rename `createSiteIndex` to `createSiteResolver` and `SiteIndex` to
|
|
25
|
+
`SiteResolver` where imported from `/delivery/data`; the `paginate` helper is deleted.
|
|
26
|
+
|
|
27
|
+
Consumers must: read `form.error` where they read `form.renameError`. Every action failure now
|
|
28
|
+
carries `error: string` as its one-line summary; the structured extras (`brokenLinks`,
|
|
29
|
+
`inboundLinks`) keep their keys beside it.
|
|
30
|
+
|
|
31
|
+
Consumers must: replace the hand-written `App.Locals` block in `src/app.d.ts` with
|
|
32
|
+
`import '@glw907/cairn-cms/ambient';`, the new type-only subpath that ships the
|
|
33
|
+
`App.Locals.editor` augmentation.
|
|
34
|
+
|
|
35
|
+
The diagnostics registry reaches its remaining runtime sites. A missing `AUTH_DB` on a gated
|
|
36
|
+
admin request renders a branded condition page instead of a silent login redirect, and a missing
|
|
37
|
+
email binding, missing GitHub App credentials, and an invalid site config now carry their
|
|
38
|
+
registered condition ids through the error chain and the logs.
|
|
39
|
+
`deps.mintToken` widens to accept a plain string return. The concept list reads published rows
|
|
40
|
+
from the committed manifest in one call, falling back to the per-file crawl only on a repo with
|
|
41
|
+
no manifest yet. Internal layering rides along (one home each for the link rewriter, the escape
|
|
42
|
+
helpers, and the conflict check) with no consumer surface change.
|
|
43
|
+
|
|
44
|
+
This release publishes together with `0.41.0`, so a site crossing from `0.40.0` takes both
|
|
45
|
+
windows in one upgrade.
|
|
46
|
+
|
|
5
47
|
## 0.41.0
|
|
6
48
|
|
|
7
49
|
`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>
|
|
@@ -29,7 +29,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
29
29
|
import { directiveLineKind, findInlineDirectives } from './markdown-directives.js';
|
|
30
30
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
31
31
|
import type { IconSet } from '../render/glyph.js';
|
|
32
|
-
import type { EditData } from '../sveltekit/content-routes.js';
|
|
32
|
+
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
33
33
|
import type { TextareaField, TagsField, FreeTagsField } from '../content/types.js';
|
|
34
34
|
import type { LinkResolve } from '../content/links.js';
|
|
35
35
|
import { manifestLinkResolver } from '../content/manifest.js';
|
|
@@ -43,9 +43,9 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
43
43
|
render?: (md: string, opts?: { stagger?: boolean; resolve?: LinkResolve }) => string | Promise<string>;
|
|
44
44
|
/** The site's icon set, for the guided form's icon fields. */
|
|
45
45
|
icons?: IconSet;
|
|
46
|
-
/** The
|
|
47
|
-
*
|
|
48
|
-
form?:
|
|
46
|
+
/** The last content action's failure: the save guard's broken links, the delete guard's
|
|
47
|
+
* inbound linkers, or a rename refusal, each carrying the shared `error` summary. */
|
|
48
|
+
form?: ContentFormFailure | null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
let { data, registry, render, icons, form }: Props = $props();
|
|
@@ -220,8 +220,12 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
220
220
|
// not refused. When set, a delete was blocked by a link that appeared since the page loaded.
|
|
221
221
|
const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
|
|
222
222
|
|
|
223
|
-
//
|
|
224
|
-
|
|
223
|
+
// The shared failure summary, rendered only when no richer banner claims the failure: the save
|
|
224
|
+
// and delete guards get their own banners from brokenLinks and inboundLinks below, so this
|
|
225
|
+
// surfaces the rest (a rename refusal, today).
|
|
226
|
+
const formError = $derived(
|
|
227
|
+
form?.error && !form.brokenLinks?.length && !form.inboundLinks?.length ? form.error : '',
|
|
228
|
+
);
|
|
225
229
|
|
|
226
230
|
// The entry this surface is editing. SvelteKit reuses the page component across a same-route
|
|
227
231
|
// navigation (the delete-refused and broken-link banners link entry to entry), so the per-entry
|
|
@@ -279,7 +283,7 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
279
283
|
);
|
|
280
284
|
const assertiveMessage = $derived.by(() => {
|
|
281
285
|
if (data.error) return data.error;
|
|
282
|
-
if (
|
|
286
|
+
if (formError) return formError;
|
|
283
287
|
if (deleteRefusedLinks.length) {
|
|
284
288
|
const count = deleteRefusedLinks.length;
|
|
285
289
|
return `This ${data.label.toLowerCase()} could not be deleted. ${count} ${count === 1 ? 'page links' : 'pages link'} to it.`;
|
|
@@ -529,8 +533,8 @@ transient flashes, and the editor card's footer holds the word count and the Mar
|
|
|
529
533
|
{#if data.error}
|
|
530
534
|
<div class="alert alert-error mb-4 text-sm">{data.error}</div>
|
|
531
535
|
{/if}
|
|
532
|
-
{#if
|
|
533
|
-
<div class="alert alert-error mb-4 text-sm">{
|
|
536
|
+
{#if formError}
|
|
537
|
+
<div class="alert alert-error mb-4 text-sm">{formError}</div>
|
|
534
538
|
{/if}
|
|
535
539
|
{#if deleteRefusedLinks.length}
|
|
536
540
|
<div class="alert alert-error mb-4 flex-col items-start text-sm">
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The magic-link sign-in page. A plain form POST to the
|
|
3
|
+
The magic-link sign-in page. A plain form POST to the named `?/request` action (the engine's
|
|
4
4
|
`requestAction`); no client SDK. The success message is identical whether or not the email is on
|
|
5
5
|
the allowlist, so the page never leaks membership (spec §7.1).
|
|
6
6
|
-->
|
|
@@ -101,7 +101,7 @@ the allowlist, so the page never leaks membership (spec §7.1).
|
|
|
101
101
|
{#if data.error && !form?.status}
|
|
102
102
|
<div role="alert" class="alert alert-error mb-3 text-sm">That link expired. Request a new one below.</div>
|
|
103
103
|
{/if}
|
|
104
|
-
<form method="POST" class="flex flex-col gap-3">
|
|
104
|
+
<form method="POST" action="?/request" class="flex flex-col gap-3">
|
|
105
105
|
<CsrfField token={data.csrf} />
|
|
106
106
|
<label class="flex flex-col gap-1">
|
|
107
107
|
<span class="text-sm font-medium">Email</span>
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
} | null;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The magic-link sign-in page. A plain form POST to the
|
|
17
|
+
* The magic-link sign-in page. A plain form POST to the named `?/request` action (the engine's
|
|
18
18
|
* `requestAction`); no client SDK. The success message is identical whether or not the email is on
|
|
19
19
|
* the allowlist, so the page never leaks membership (spec §7.1).
|
|
20
20
|
*/
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
The owner-gated editor management surface: a table of editors with role-flip and remove actions,
|
|
4
4
|
and an add-editor form. The acting owner's own row disables its destructive controls; the
|
|
5
5
|
last-owner anti-lockout rule itself is enforced server-side (editors-routes). Actions post to the
|
|
6
|
-
named `?/setRole`, `?/
|
|
6
|
+
named `?/setRole`, `?/removeEditor`, and `?/addEditor` actions, the names the single-mount
|
|
7
|
+
dispatcher defines.
|
|
7
8
|
-->
|
|
8
9
|
<script lang="ts">
|
|
9
10
|
import CsrfField from './CsrfField.svelte';
|
|
@@ -53,7 +54,7 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
|
|
|
53
54
|
{editor.role === 'owner' ? 'Make editor' : 'Make owner'}
|
|
54
55
|
</button>
|
|
55
56
|
</form>
|
|
56
|
-
<form method="POST" action="?/
|
|
57
|
+
<form method="POST" action="?/removeEditor">
|
|
57
58
|
<CsrfField />
|
|
58
59
|
<input type="hidden" name="email" value={editor.email} />
|
|
59
60
|
<button type="submit" class="btn btn-ghost btn-xs text-error" disabled={isSelf} aria-label={`Remove ${editor.displayName}`}>
|
|
@@ -67,7 +68,7 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
|
|
|
67
68
|
</table>
|
|
68
69
|
</div>
|
|
69
70
|
|
|
70
|
-
<form method="POST" action="?/
|
|
71
|
+
<form method="POST" action="?/addEditor" class="rounded-box border border-[var(--cairn-card-border)] bg-base-100 grid gap-3 p-4 shadow-[var(--cairn-shadow)] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
|
|
71
72
|
<CsrfField />
|
|
72
73
|
<label class="flex flex-col gap-1">
|
|
73
74
|
<span class="text-sm font-medium">Name</span>
|
|
@@ -15,7 +15,8 @@ interface Props {
|
|
|
15
15
|
* The owner-gated editor management surface: a table of editors with role-flip and remove actions,
|
|
16
16
|
* and an add-editor form. The acting owner's own row disables its destructive controls; the
|
|
17
17
|
* last-owner anti-lockout rule itself is enforced server-side (editors-routes). Actions post to the
|
|
18
|
-
* named `?/setRole`, `?/
|
|
18
|
+
* named `?/setRole`, `?/removeEditor`, and `?/addEditor` actions, the names the single-mount
|
|
19
|
+
* dispatcher defines.
|
|
19
20
|
*/
|
|
20
21
|
declare const ManageEditors: import("svelte").Component<Props, {}, "">;
|
|
21
22
|
type ManageEditors = ReturnType<typeof ManageEditors>;
|
package/dist/components/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Admin Svelte components (Plan 05). The Warm Stone theme ships as a CSS side effect imported
|
|
2
2
|
// by the components that set `data-theme="cairn-admin"`.
|
|
3
|
+
export { default as CairnAdmin } from './CairnAdmin.svelte';
|
|
3
4
|
export { default as AdminLayout } from './AdminLayout.svelte';
|
|
4
5
|
export { default as LoginPage } from './LoginPage.svelte';
|
|
5
6
|
export { default as ConfirmPage } from './ConfirmPage.svelte';
|
|
@@ -22,11 +22,3 @@ export declare function insertInlineLink(doc: string, from: number, to: number,
|
|
|
22
22
|
* is left in place.
|
|
23
23
|
*/
|
|
24
24
|
export declare function unwrapCairnLink(doc: string, href: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
27
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
28
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
29
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
30
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
31
|
-
*/
|
|
32
|
-
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
|
@@ -157,31 +157,3 @@ export function unwrapCairnLink(doc, href) {
|
|
|
157
157
|
}
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
162
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
163
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
164
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
165
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
166
|
-
*/
|
|
167
|
-
export function rewriteCairnLink(doc, oldHref, newHref) {
|
|
168
|
-
const tree = unified().use(remarkParse).use(remarkGfm).parse(doc);
|
|
169
|
-
const spans = [];
|
|
170
|
-
visit(tree, 'link', (node) => {
|
|
171
|
-
if (node.url !== oldHref)
|
|
172
|
-
return;
|
|
173
|
-
const start = node.position?.start?.offset;
|
|
174
|
-
const end = node.position?.end?.offset;
|
|
175
|
-
if (start == null || end == null)
|
|
176
|
-
return;
|
|
177
|
-
spans.push({ start, end });
|
|
178
|
-
});
|
|
179
|
-
spans.sort((a, b) => b.start - a.start);
|
|
180
|
-
let out = doc;
|
|
181
|
-
for (const span of spans) {
|
|
182
|
-
const src = out.slice(span.start, span.end);
|
|
183
|
-
const rewritten = src.replace(`](${oldHref}`, `](${newHref}`);
|
|
184
|
-
out = out.slice(0, span.start) + rewritten + out.slice(span.end);
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
package/dist/content/links.d.ts
CHANGED
|
@@ -18,3 +18,11 @@ export declare function escapeLinkText(text: string): string;
|
|
|
18
18
|
/** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
19
19
|
* Parses the body as mdast, so a token inside a code span or fence is never matched. */
|
|
20
20
|
export declare function extractCairnLinks(body: string): CairnRef[];
|
|
21
|
+
/**
|
|
22
|
+
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
23
|
+
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
24
|
+
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
25
|
+
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
26
|
+
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
package/dist/content/links.js
CHANGED
|
@@ -50,3 +50,31 @@ export function extractCairnLinks(body) {
|
|
|
50
50
|
});
|
|
51
51
|
return refs;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
55
|
+
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
56
|
+
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
57
|
+
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
58
|
+
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
59
|
+
*/
|
|
60
|
+
export function rewriteCairnLink(doc, oldHref, newHref) {
|
|
61
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(doc);
|
|
62
|
+
const spans = [];
|
|
63
|
+
visit(tree, 'link', (node) => {
|
|
64
|
+
if (node.url !== oldHref)
|
|
65
|
+
return;
|
|
66
|
+
const start = node.position?.start?.offset;
|
|
67
|
+
const end = node.position?.end?.offset;
|
|
68
|
+
if (start == null || end == null)
|
|
69
|
+
return;
|
|
70
|
+
spans.push({ start, end });
|
|
71
|
+
});
|
|
72
|
+
spans.sort((a, b) => b.start - a.start);
|
|
73
|
+
let out = doc;
|
|
74
|
+
for (const span of spans) {
|
|
75
|
+
const src = out.slice(span.start, span.end);
|
|
76
|
+
const rewritten = src.replace(`](${oldHref}`, `](${newHref}`);
|
|
77
|
+
out = out.slice(0, span.start) + rewritten + out.slice(span.end);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/dist/content/types.d.ts
CHANGED
|
@@ -154,8 +154,8 @@ export interface CairnAdapter {
|
|
|
154
154
|
backend: BackendConfig;
|
|
155
155
|
sender: SenderConfig;
|
|
156
156
|
/** The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
157
|
-
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-
|
|
158
|
-
* preview a manifest one. */
|
|
157
|
+
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-resolver-backed
|
|
158
|
+
* one, the preview a manifest one. */
|
|
159
159
|
render(md: string, opts?: {
|
|
160
160
|
stagger?: boolean;
|
|
161
161
|
resolve?: LinkResolve;
|
package/dist/delivery/data.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
2
2
|
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
4
|
+
export type { SiteResolver, ConceptIndex } from './site-resolver.js';
|
|
5
5
|
export { createSiteIndexes } from './site-indexes.js';
|
|
6
6
|
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
|
@@ -15,9 +15,7 @@ export { buildSeoMeta } from './seo.js';
|
|
|
15
15
|
export type { SeoInput, SeoMeta } from './seo.js';
|
|
16
16
|
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
17
17
|
export type { SeoFields } from './seo-fields.js';
|
|
18
|
-
export { paginate } from './paginate.js';
|
|
19
|
-
export type { Page } from './paginate.js';
|
|
20
18
|
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
21
19
|
export { jsonLdScript } from './json-ld.js';
|
|
22
20
|
export { permalink } from '../content/permalink.js';
|
|
23
|
-
export { buildSiteManifest
|
|
21
|
+
export { buildSiteManifest } from './manifest.js';
|
package/dist/delivery/data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
|
|
3
3
|
// the graph. The full ./delivery barrel re-exports this and adds the route loaders.
|
|
4
4
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
5
|
-
export {
|
|
5
|
+
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
6
6
|
export { createSiteIndexes } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
|
8
8
|
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
@@ -11,8 +11,7 @@ export { buildSitemap } from './sitemap.js';
|
|
|
11
11
|
export { buildRobots } from './robots.js';
|
|
12
12
|
export { buildSeoMeta } from './seo.js';
|
|
13
13
|
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
14
|
-
export { paginate } from './paginate.js';
|
|
15
14
|
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
16
15
|
export { jsonLdScript } from './json-ld.js';
|
|
17
16
|
export { permalink } from '../content/permalink.js';
|
|
18
|
-
export { buildSiteManifest
|
|
17
|
+
export { buildSiteManifest } from './manifest.js';
|
package/dist/delivery/feeds.js
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
// channel and a list of items, so they unit-test without a render or a network. The caller
|
|
3
3
|
// (a template +server.ts shim) assembles items from the content index and passes absolute
|
|
4
4
|
// URLs built from PUBLIC_ORIGIN.
|
|
5
|
-
|
|
6
|
-
return value
|
|
7
|
-
.replace(/&/g, '&')
|
|
8
|
-
.replace(/</g, '<')
|
|
9
|
-
.replace(/>/g, '>')
|
|
10
|
-
.replace(/"/g, '"');
|
|
11
|
-
}
|
|
5
|
+
import { escapeXml } from './xml.js';
|
|
12
6
|
/** Make a string safe inside a CDATA section by splitting any `]]>` across two sections. */
|
|
13
7
|
function cdataSafe(value) {
|
|
14
8
|
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './data.js';
|
|
2
|
-
export { createPublicRoutes } from '
|
|
3
|
-
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '
|
|
2
|
+
export { createPublicRoutes } from './public-routes.js';
|
|
3
|
+
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from './public-routes.js';
|
package/dist/delivery/index.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// lives at ./delivery/head. Importing this pulls @sveltejs/kit through the route loaders, so a
|
|
4
4
|
// plain-Node tool imports from ./delivery/data instead.
|
|
5
5
|
export * from './data.js';
|
|
6
|
-
export { createPublicRoutes } from '
|
|
6
|
+
export { createPublicRoutes } from './public-routes.js';
|