@glw907/cairn-cms 0.56.1 → 0.57.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 +148 -0
- package/README.md +10 -4
- package/dist/components/AdminLayout.svelte +3 -0
- package/dist/components/CairnAdmin.svelte +8 -1
- package/dist/components/CairnAdmin.svelte.d.ts +2 -0
- package/dist/components/CairnMediaLibrary.svelte +929 -0
- package/dist/components/CairnMediaLibrary.svelte.d.ts +37 -0
- package/dist/components/ComponentForm.svelte +175 -46
- package/dist/components/ComponentForm.svelte.d.ts +22 -8
- package/dist/components/ComponentInsertDialog.svelte +379 -26
- package/dist/components/ComponentInsertDialog.svelte.d.ts +31 -2
- package/dist/components/EditPage.svelte +477 -15
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/MarkdownEditor.svelte +358 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +51 -1
- package/dist/components/MediaCaptureCard.svelte +135 -0
- package/dist/components/MediaCaptureCard.svelte.d.ts +40 -0
- package/dist/components/MediaFigureControl.svelte +247 -0
- package/dist/components/MediaFigureControl.svelte.d.ts +40 -0
- package/dist/components/MediaHeroField.svelte +569 -0
- package/dist/components/MediaHeroField.svelte.d.ts +67 -0
- package/dist/components/MediaInsertPopover.svelte +449 -0
- package/dist/components/MediaInsertPopover.svelte.d.ts +58 -0
- package/dist/components/MediaPicker.svelte +257 -0
- package/dist/components/MediaPicker.svelte.d.ts +41 -0
- package/dist/components/admin-icons.d.ts +12 -0
- package/dist/components/admin-icons.js +12 -0
- package/dist/components/cairn-admin.css +1045 -28
- package/dist/components/client-ingest.d.ts +142 -0
- package/dist/components/client-ingest.js +297 -0
- package/dist/components/editor-media.d.ts +11 -0
- package/dist/components/editor-media.js +206 -0
- package/dist/components/editor-placeholder.d.ts +26 -0
- package/dist/components/editor-placeholder.js +166 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +19 -0
- package/dist/components/markdown-directives.js +52 -0
- package/dist/components/markdown-format.d.ts +89 -0
- package/dist/components/markdown-format.js +255 -0
- package/dist/components/media-upload-outcome.d.ts +52 -0
- package/dist/components/media-upload-outcome.js +48 -0
- package/dist/content/compose.js +3 -0
- package/dist/content/frontmatter.js +17 -0
- package/dist/content/manifest.d.ts +4 -0
- package/dist/content/manifest.js +41 -1
- package/dist/content/media-refs.d.ts +7 -0
- package/dist/content/media-refs.js +52 -0
- package/dist/content/schema.d.ts +5 -2
- package/dist/content/schema.js +17 -0
- package/dist/content/types.d.ts +62 -11
- package/dist/content/validate.js +27 -0
- package/dist/delivery/public-routes.d.ts +16 -0
- package/dist/delivery/public-routes.js +46 -3
- package/dist/delivery/seo-fields.js +7 -1
- package/dist/delivery/seo.d.ts +2 -0
- package/dist/delivery/seo.js +3 -0
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +21 -0
- package/dist/doctor/index.d.ts +3 -1
- package/dist/doctor/index.js +11 -2
- package/dist/doctor/types.d.ts +3 -0
- package/dist/doctor/wrangler-config.d.ts +3 -0
- package/dist/doctor/wrangler-config.js +20 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.js +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/media/config.d.ts +24 -0
- package/dist/media/config.js +69 -0
- package/dist/media/delivery-bucket.d.ts +34 -0
- package/dist/media/delivery-bucket.js +10 -0
- package/dist/media/index.d.ts +6 -0
- package/dist/media/index.js +13 -0
- package/dist/media/library-entry.d.ts +30 -0
- package/dist/media/library-entry.js +17 -0
- package/dist/media/manifest.d.ts +44 -0
- package/dist/media/manifest.js +105 -0
- package/dist/media/naming.d.ts +18 -0
- package/dist/media/naming.js +112 -0
- package/dist/media/reconcile.d.ts +36 -0
- package/dist/media/reconcile.js +45 -0
- package/dist/media/reference.d.ts +12 -0
- package/dist/media/reference.js +33 -0
- package/dist/media/sniff.d.ts +18 -0
- package/dist/media/sniff.js +106 -0
- package/dist/media/store.d.ts +25 -0
- package/dist/media/store.js +16 -0
- package/dist/media/transform-url.d.ts +26 -0
- package/dist/media/transform-url.js +38 -0
- package/dist/media/usage.d.ts +48 -0
- package/dist/media/usage.js +90 -0
- package/dist/render/component-grammar.d.ts +20 -0
- package/dist/render/component-grammar.js +47 -3
- package/dist/render/component-validate.js +22 -0
- package/dist/render/pipeline.d.ts +2 -0
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +28 -0
- package/dist/render/registry.js +15 -0
- package/dist/render/remark-figure.d.ts +4 -0
- package/dist/render/remark-figure.js +103 -0
- package/dist/render/resolve-media.d.ts +34 -0
- package/dist/render/resolve-media.js +78 -0
- package/dist/render/sanitize-schema.d.ts +4 -2
- package/dist/render/sanitize-schema.js +5 -3
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +5 -0
- package/dist/sveltekit/cairn-admin.d.ts +8 -1
- package/dist/sveltekit/cairn-admin.js +10 -2
- package/dist/sveltekit/content-routes.d.ts +68 -2
- package/dist/sveltekit/content-routes.js +461 -10
- package/dist/sveltekit/csrf.d.ts +16 -0
- package/dist/sveltekit/csrf.js +18 -0
- package/dist/sveltekit/guard.js +10 -3
- package/dist/sveltekit/index.d.ts +2 -1
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/media-route.d.ts +12 -0
- package/dist/sveltekit/media-route.js +137 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.js +7 -2
- package/package.json +8 -1
- package/src/lib/components/AdminLayout.svelte +3 -0
- package/src/lib/components/CairnAdmin.svelte +8 -1
- package/src/lib/components/CairnMediaLibrary.svelte +929 -0
- package/src/lib/components/ComponentForm.svelte +175 -46
- package/src/lib/components/ComponentInsertDialog.svelte +379 -26
- package/src/lib/components/EditPage.svelte +477 -15
- package/src/lib/components/MarkdownEditor.svelte +358 -1
- package/src/lib/components/MediaCaptureCard.svelte +135 -0
- package/src/lib/components/MediaFigureControl.svelte +247 -0
- package/src/lib/components/MediaHeroField.svelte +569 -0
- package/src/lib/components/MediaInsertPopover.svelte +449 -0
- package/src/lib/components/MediaPicker.svelte +257 -0
- package/src/lib/components/admin-icons.ts +12 -0
- package/src/lib/components/cairn-admin.css +37 -0
- package/src/lib/components/client-ingest.ts +380 -0
- package/src/lib/components/editor-media.ts +248 -0
- package/src/lib/components/editor-placeholder.ts +213 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +57 -0
- package/src/lib/components/markdown-format.ts +307 -1
- package/src/lib/components/media-upload-outcome.ts +83 -0
- package/src/lib/content/compose.ts +3 -0
- package/src/lib/content/frontmatter.ts +16 -1
- package/src/lib/content/manifest.ts +44 -1
- package/src/lib/content/media-refs.ts +58 -0
- package/src/lib/content/schema.ts +31 -7
- package/src/lib/content/types.ts +78 -13
- package/src/lib/content/validate.ts +26 -1
- package/src/lib/delivery/public-routes.ts +52 -3
- package/src/lib/delivery/seo-fields.ts +6 -1
- package/src/lib/delivery/seo.ts +5 -0
- package/src/lib/doctor/checks-local.ts +22 -0
- package/src/lib/doctor/index.ts +21 -3
- package/src/lib/doctor/types.ts +3 -0
- package/src/lib/doctor/wrangler-config.ts +23 -0
- package/src/lib/env.ts +28 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/log/events.ts +8 -1
- package/src/lib/media/config.ts +103 -0
- package/src/lib/media/delivery-bucket.ts +41 -0
- package/src/lib/media/index.ts +22 -0
- package/src/lib/media/library-entry.ts +58 -0
- package/src/lib/media/manifest.ts +122 -0
- package/src/lib/media/naming.ts +130 -0
- package/src/lib/media/reconcile.ts +79 -0
- package/src/lib/media/reference.ts +40 -0
- package/src/lib/media/sniff.ts +114 -0
- package/src/lib/media/store.ts +57 -0
- package/src/lib/media/transform-url.ts +58 -0
- package/src/lib/media/usage.ts +152 -0
- package/src/lib/render/component-grammar.ts +59 -3
- package/src/lib/render/component-validate.ts +22 -1
- package/src/lib/render/pipeline.ts +17 -3
- package/src/lib/render/registry.ts +38 -0
- package/src/lib/render/remark-figure.ts +132 -0
- package/src/lib/render/resolve-media.ts +96 -0
- package/src/lib/render/sanitize-schema.ts +5 -3
- package/src/lib/sveltekit/admin-dispatch.ts +6 -1
- package/src/lib/sveltekit/cairn-admin.ts +13 -3
- package/src/lib/sveltekit/content-routes.ts +573 -12
- package/src/lib/sveltekit/csrf.ts +18 -0
- package/src/lib/sveltekit/guard.ts +12 -3
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/media-route.ts +158 -0
- package/src/lib/vite/index.ts +9 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import type { ResolvedAssetConfig } from '../media/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the media delivery `RequestHandler` for a site's resolved media config.
|
|
5
|
+
*
|
|
6
|
+
* The handler validates the hash and extension before any R2 call, derives the object key from the
|
|
7
|
+
* validated values only (never trusting the URL's fan-out), guards the Cloudflare Images self-loop,
|
|
8
|
+
* and sets the security headers on every served response.
|
|
9
|
+
*
|
|
10
|
+
* @param resolved the adapter's resolved media config; when media is off the handler always 404s.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createMediaRoute(resolved: ResolvedAssetConfig): RequestHandler;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { requireBucket } from '../env.js';
|
|
2
|
+
import { CairnError } from '../diagnostics/index.js';
|
|
3
|
+
import { r2Key } from '../media/naming.js';
|
|
4
|
+
import { log } from '../log/index.js';
|
|
5
|
+
/** A 16-character lowercase hex content-hash prefix, validated before any R2 lookup. */
|
|
6
|
+
const HASH_RE = /^[0-9a-f]{16}$/;
|
|
7
|
+
/** The closed delivery extension allow-list. A filename ext outside this set is a 404 with no R2
|
|
8
|
+
* read, so the route can never serve a type it cannot vouch for. */
|
|
9
|
+
const DELIVERY_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']);
|
|
10
|
+
/** The load-bearing XSS control: set on every non-404 response, so a served object can never run as
|
|
11
|
+
* active content. `Content-Type` comes from the stored, server-validated metadata via
|
|
12
|
+
* `writeHttpMetadata`; these override or add to it. */
|
|
13
|
+
function applySecurityHeaders(headers, etag) {
|
|
14
|
+
headers.set('X-Content-Type-Options', 'nosniff');
|
|
15
|
+
headers.set('Content-Disposition', 'inline');
|
|
16
|
+
headers.set('Content-Security-Policy', "default-src 'none'; sandbox");
|
|
17
|
+
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
|
|
18
|
+
headers.set('ETag', etag);
|
|
19
|
+
// An object stored outside the upload pipeline (a manual put, a future import) may carry no
|
|
20
|
+
// content type, so writeHttpMetadata would set none. Pair `nosniff` with an explicit safe
|
|
21
|
+
// default rather than serving a typeless response.
|
|
22
|
+
if (!headers.has('Content-Type'))
|
|
23
|
+
headers.set('Content-Type', 'application/octet-stream');
|
|
24
|
+
}
|
|
25
|
+
/** True when the returned object carries a body (a full or ranged read), narrowing it to the body
|
|
26
|
+
* variant. R2 returns a body-less object on an `If-None-Match` hit. */
|
|
27
|
+
function hasBody(obj) {
|
|
28
|
+
return 'body' in obj && obj.body != null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the media delivery `RequestHandler` for a site's resolved media config.
|
|
32
|
+
*
|
|
33
|
+
* The handler validates the hash and extension before any R2 call, derives the object key from the
|
|
34
|
+
* validated values only (never trusting the URL's fan-out), guards the Cloudflare Images self-loop,
|
|
35
|
+
* and sets the security headers on every served response.
|
|
36
|
+
*
|
|
37
|
+
* @param resolved the adapter's resolved media config; when media is off the handler always 404s.
|
|
38
|
+
*/
|
|
39
|
+
export function createMediaRoute(resolved) {
|
|
40
|
+
return async (event) => {
|
|
41
|
+
// Media off: the route is mounted but serves nothing.
|
|
42
|
+
if (!resolved.enabled)
|
|
43
|
+
return new Response(null, { status: 404 });
|
|
44
|
+
// The catch-all param is conventionally `path` (route `/media/[...path]`). Decode each segment
|
|
45
|
+
// on its own and reject a traversal or an embedded slash, so no undecoded or `..` path reaches
|
|
46
|
+
// R2. params.path is `string | undefined` (kit's fallback ambient types), so guard the absence.
|
|
47
|
+
const raw = event.params.path;
|
|
48
|
+
if (typeof raw !== 'string' || raw === '')
|
|
49
|
+
return new Response(null, { status: 404 });
|
|
50
|
+
const segments = [];
|
|
51
|
+
for (const part of raw.split('/')) {
|
|
52
|
+
let decoded;
|
|
53
|
+
try {
|
|
54
|
+
decoded = decodeURIComponent(part);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return new Response(null, { status: 404 });
|
|
58
|
+
}
|
|
59
|
+
if (decoded === '' || decoded.includes('/') || decoded === '..') {
|
|
60
|
+
return new Response(null, { status: 404 });
|
|
61
|
+
}
|
|
62
|
+
segments.push(decoded);
|
|
63
|
+
}
|
|
64
|
+
// The filename is the last segment; its dot fields end with `<hash>.<ext>`. The slug, if any, is
|
|
65
|
+
// attacker-controlled and ignored. parseMediaToken does not run here.
|
|
66
|
+
const filename = segments[segments.length - 1];
|
|
67
|
+
const fields = filename.split('.');
|
|
68
|
+
if (fields.length < 2)
|
|
69
|
+
return new Response(null, { status: 404 });
|
|
70
|
+
const ext = fields[fields.length - 1].toLowerCase();
|
|
71
|
+
const hash = fields[fields.length - 2];
|
|
72
|
+
// Validate before any R2 call: a bad hash or ext is a 404 with no read.
|
|
73
|
+
if (!HASH_RE.test(hash) || !DELIVERY_EXTS.has(ext)) {
|
|
74
|
+
return new Response(null, { status: 404 });
|
|
75
|
+
}
|
|
76
|
+
// Derive the key from the validated values only; r2Key recomputes the fan-out from the hash.
|
|
77
|
+
const key = r2Key(hash, ext);
|
|
78
|
+
// Resolve the bucket. A missing binding is a drained 503 with a log, never a thrown 500.
|
|
79
|
+
let bucket;
|
|
80
|
+
try {
|
|
81
|
+
// `event.platform` is `App.Platform`, which the engine does not declare (a site does, with an
|
|
82
|
+
// `env`), so read it through a structural cast rather than naming the site's ambient type.
|
|
83
|
+
const platform = event.platform;
|
|
84
|
+
bucket = requireBucket(platform?.env ?? {}, resolved.bucketBinding);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err instanceof CairnError && err.conditionId === 'config.bindings-missing') {
|
|
88
|
+
log.warn('media.delivery_failed', {
|
|
89
|
+
reason: 'binding-missing',
|
|
90
|
+
binding: resolved.bucketBinding,
|
|
91
|
+
});
|
|
92
|
+
return new Response(null, { status: 503 });
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
// Self-loop guard: the Cloudflare Images origin subrequest carries `Via: image-resizing`. Serve
|
|
97
|
+
// it a clean full-body 200 with no conditional or range handling, so a transform cannot loop.
|
|
98
|
+
const via = event.request.headers.get('Via') ?? '';
|
|
99
|
+
const isImageResizing = via.includes('image-resizing');
|
|
100
|
+
// Only forward `range` when the request actually carried a `Range` header. R2 populates the
|
|
101
|
+
// returned object's `.range` whenever a `range` option is passed (even a header-less one), so
|
|
102
|
+
// passing it unconditionally would turn every full GET into a 206.
|
|
103
|
+
const hasRangeRequest = !isImageResizing && event.request.headers.has('Range');
|
|
104
|
+
const getOpts = isImageResizing
|
|
105
|
+
? undefined
|
|
106
|
+
: {
|
|
107
|
+
onlyIf: event.request.headers,
|
|
108
|
+
...(hasRangeRequest ? { range: event.request.headers } : {}),
|
|
109
|
+
};
|
|
110
|
+
const obj = await bucket.get(key, getOpts);
|
|
111
|
+
if (obj === null)
|
|
112
|
+
return new Response(null, { status: 404 });
|
|
113
|
+
// A body-less object is R2's `If-None-Match` hit: 304, no body. Skipped for the self-loop path,
|
|
114
|
+
// which always requested the full body.
|
|
115
|
+
if (!isImageResizing && !hasBody(obj)) {
|
|
116
|
+
const headers = new Headers();
|
|
117
|
+
obj.writeHttpMetadata(headers);
|
|
118
|
+
applySecurityHeaders(headers, obj.httpEtag);
|
|
119
|
+
return new Response(null, { status: 304, headers });
|
|
120
|
+
}
|
|
121
|
+
const headers = new Headers();
|
|
122
|
+
obj.writeHttpMetadata(headers);
|
|
123
|
+
applySecurityHeaders(headers, obj.httpEtag);
|
|
124
|
+
// A ranged read carries `obj.range`: respond 206 with a Content-Range. R2 fills the served
|
|
125
|
+
// window; derive the bounds defensively against the full size.
|
|
126
|
+
if (hasRangeRequest && obj.range) {
|
|
127
|
+
const start = obj.range.offset ?? 0;
|
|
128
|
+
const length = obj.range.length ?? obj.size - start;
|
|
129
|
+
const end = start + length - 1;
|
|
130
|
+
headers.set('Content-Range', `bytes ${start}-${end}/${obj.size}`);
|
|
131
|
+
const body = hasBody(obj) ? obj.body : null;
|
|
132
|
+
return new Response(body, { status: 206, headers });
|
|
133
|
+
}
|
|
134
|
+
const body = hasBody(obj) ? obj.body : null;
|
|
135
|
+
return new Response(body, { status: 200, headers });
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface AdapterFacts {
|
|
|
40
40
|
repo?: string;
|
|
41
41
|
/** `cairn.sender.from`. */
|
|
42
42
|
from?: string;
|
|
43
|
+
/** `cairn.assets.bucketBinding`, the media R2 binding name; undefined when the adapter declares no
|
|
44
|
+
* assets. The doctor's conditional media-bucket check reads it. */
|
|
45
|
+
mediaBucketBinding?: string;
|
|
43
46
|
}
|
|
44
47
|
/** Read `{ owner, repo, from }` off the consumer's adapter by evaluating a tiny virtual module
|
|
45
48
|
* through the consumer's own Vite resolution, the same machinery the cairn-manifest bin uses.
|
package/dist/vite/index.js
CHANGED
|
@@ -181,17 +181,20 @@ function cairnVirtualOnly(source) {
|
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
/** Build the virtual module that reads only the adapter facts the doctor derives. It imports the
|
|
184
|
-
* configured config module and exports the string-typed `owner`, `repo`,
|
|
185
|
-
* nothing else of the adapter (least of all a secret) crosses the
|
|
184
|
+
* configured config module and exports the string-typed `owner`, `repo`, `from`, and the media
|
|
185
|
+
* `bucketBinding` as JSON, so nothing else of the adapter (least of all a secret) crosses the
|
|
186
|
+
* boundary. */
|
|
186
187
|
function adapterFactsSource(opts) {
|
|
187
188
|
return `
|
|
188
189
|
import { cairn } from ${JSON.stringify(opts.configModule)};
|
|
189
190
|
const backend = cairn?.backend ?? {};
|
|
190
191
|
const sender = cairn?.sender ?? {};
|
|
192
|
+
const assets = cairn?.assets ?? {};
|
|
191
193
|
const facts = {};
|
|
192
194
|
if (typeof backend.owner === 'string') facts.owner = backend.owner;
|
|
193
195
|
if (typeof backend.repo === 'string') facts.repo = backend.repo;
|
|
194
196
|
if (typeof sender.from === 'string') facts.from = sender.from;
|
|
197
|
+
if (typeof assets.bucketBinding === 'string') facts.mediaBucketBinding = assets.bucketBinding;
|
|
195
198
|
export const result = JSON.stringify(facts);
|
|
196
199
|
`;
|
|
197
200
|
}
|
|
@@ -218,6 +221,8 @@ export async function readAdapterFacts(cwd = process.cwd()) {
|
|
|
218
221
|
facts.repo = parsed.repo;
|
|
219
222
|
if (typeof parsed.from === 'string')
|
|
220
223
|
facts.from = parsed.from;
|
|
224
|
+
if (typeof parsed.mediaBucketBinding === 'string')
|
|
225
|
+
facts.mediaBucketBinding = parsed.mediaBucketBinding;
|
|
221
226
|
return facts;
|
|
222
227
|
}
|
|
223
228
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"check:reference:signatures": "npm run package && node scripts/check-reference-signatures.mjs",
|
|
33
33
|
"check:readiness": "npm run package && node scripts/check-readiness.mjs",
|
|
34
34
|
"check:docs": "node scripts/docs-links.mjs",
|
|
35
|
+
"check:version": "node scripts/check-version.mjs",
|
|
35
36
|
"check:prose": "node scripts/check-admin-prose.mjs",
|
|
36
37
|
"prepare": "npm run package",
|
|
37
38
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
@@ -77,6 +78,11 @@
|
|
|
77
78
|
"svelte": "./dist/delivery/data.js",
|
|
78
79
|
"default": "./dist/delivery/data.js"
|
|
79
80
|
},
|
|
81
|
+
"./media": {
|
|
82
|
+
"types": "./dist/media/index.d.ts",
|
|
83
|
+
"svelte": "./dist/media/index.js",
|
|
84
|
+
"default": "./dist/media/index.js"
|
|
85
|
+
},
|
|
80
86
|
"./vite": {
|
|
81
87
|
"types": "./dist/vite/index.d.ts",
|
|
82
88
|
"default": "./dist/vite/index.js"
|
|
@@ -116,6 +122,7 @@
|
|
|
116
122
|
"gray-matter": "^4",
|
|
117
123
|
"hast-util-sanitize": "^5.0.2",
|
|
118
124
|
"hastscript": "^9.0.1",
|
|
125
|
+
"heic-to": "^1.5.2",
|
|
119
126
|
"mdast-util-directive": "^3.1.0",
|
|
120
127
|
"rehype-raw": "^7.0.0",
|
|
121
128
|
"rehype-sanitize": "^6.0.0",
|
|
@@ -20,6 +20,7 @@ identical on every host regardless of the site's own theme.
|
|
|
20
20
|
import SignpostIcon from '@lucide/svelte/icons/signpost';
|
|
21
21
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
|
22
22
|
import UsersIcon from '@lucide/svelte/icons/users';
|
|
23
|
+
import ImageIcon from '@lucide/svelte/icons/image';
|
|
23
24
|
import BlocksIcon from '@lucide/svelte/icons/blocks';
|
|
24
25
|
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
|
25
26
|
import './cairn-admin.css';
|
|
@@ -56,6 +57,8 @@ identical on every host regardless of the site's own theme.
|
|
|
56
57
|
// the owner-only Editors.
|
|
57
58
|
const coreItems: NavItem[] = $derived([
|
|
58
59
|
...data.concepts.map((c) => ({ label: c.label, icon: FileTextIcon, href: `/admin/${c.id}` })),
|
|
60
|
+
// Media is a content peer, immediately after the concepts.
|
|
61
|
+
{ label: 'Media', icon: ImageIcon, href: '/admin/media' },
|
|
59
62
|
...(data.navLabel ? [{ label: data.navLabel, icon: SignpostIcon, href: '/admin/nav' }] : []),
|
|
60
63
|
{ label: 'Settings', icon: SettingsIcon, href: '/admin/settings' },
|
|
61
64
|
...(data.canManageEditors ? [{ label: 'Editors', icon: UsersIcon, href: '/admin/editors' }] : []),
|
|
@@ -13,11 +13,13 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
13
13
|
import EditPage from './EditPage.svelte';
|
|
14
14
|
import ManageEditors from './ManageEditors.svelte';
|
|
15
15
|
import NavTree from './NavTree.svelte';
|
|
16
|
+
import CairnMediaLibrary from './CairnMediaLibrary.svelte';
|
|
16
17
|
import type { AdminData } from '../sveltekit/cairn-admin.js';
|
|
17
18
|
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
18
19
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
19
20
|
import type { IconSet } from '../render/glyph.js';
|
|
20
21
|
import type { LinkResolve } from '../content/links.js';
|
|
22
|
+
import type { MediaResolve } from '../render/resolve-media.js';
|
|
21
23
|
|
|
22
24
|
interface Props {
|
|
23
25
|
/** The discriminated view data from `createCairnAdmin`'s load. */
|
|
@@ -33,7 +35,10 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
33
35
|
})
|
|
34
36
|
| null;
|
|
35
37
|
/** The site's design-accurate render pipeline, for the edit view's preview pane. */
|
|
36
|
-
render?: (
|
|
38
|
+
render?: (
|
|
39
|
+
md: string,
|
|
40
|
+
opts?: { stagger?: boolean; resolve?: LinkResolve; resolveMedia?: MediaResolve },
|
|
41
|
+
) => string | Promise<string>;
|
|
37
42
|
/** The site's component registry, for the edit view's insert palette. */
|
|
38
43
|
registry?: ComponentRegistry;
|
|
39
44
|
/** The site's icon set, for the edit view's guided form fields. */
|
|
@@ -62,6 +67,8 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
62
67
|
<ManageEditors data={data.page} {form} />
|
|
63
68
|
{:else if data.view === 'nav'}
|
|
64
69
|
<NavTree data={data.page} />
|
|
70
|
+
{:else if data.view === 'media'}
|
|
71
|
+
<CairnMediaLibrary data={data.page} {form} />
|
|
65
72
|
{/if}
|
|
66
73
|
</AdminLayout>
|
|
67
74
|
{/if}
|