uncial-cms 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +618 -0
  2. package/dist/base64.d.ts +3 -0
  3. package/dist/base64.js +15 -0
  4. package/dist/cli/assert-clean-pages.d.ts +10 -0
  5. package/dist/cli/assert-clean-pages.js +153 -0
  6. package/dist/cli/bin.d.ts +2 -0
  7. package/dist/cli/bin.js +3 -0
  8. package/dist/cli/doctor.d.ts +23 -0
  9. package/dist/cli/doctor.js +217 -0
  10. package/dist/cli/run.d.ts +4 -0
  11. package/dist/cli/run.js +99 -0
  12. package/dist/constants.d.ts +6 -0
  13. package/dist/constants.js +6 -0
  14. package/dist/define-site.d.ts +37 -0
  15. package/dist/define-site.js +24 -0
  16. package/dist/deploy-status.d.ts +55 -0
  17. package/dist/deploy-status.js +118 -0
  18. package/dist/document.d.ts +6 -0
  19. package/dist/document.js +23 -0
  20. package/dist/editor-controller.d.ts +76 -0
  21. package/dist/editor-controller.js +172 -0
  22. package/dist/editor-session.d.ts +60 -0
  23. package/dist/editor-session.js +63 -0
  24. package/dist/errors.d.ts +8 -0
  25. package/dist/errors.js +14 -0
  26. package/dist/fit-image.d.ts +31 -0
  27. package/dist/fit-image.js +88 -0
  28. package/dist/github/adapter.d.ts +3 -0
  29. package/dist/github/adapter.js +135 -0
  30. package/dist/github/index.d.ts +3 -0
  31. package/dist/github/index.js +3 -0
  32. package/dist/github/pat.d.ts +7 -0
  33. package/dist/github/pat.js +35 -0
  34. package/dist/github/popup.d.ts +9 -0
  35. package/dist/github/popup.js +75 -0
  36. package/dist/index-actions.d.ts +74 -0
  37. package/dist/index-actions.js +147 -0
  38. package/dist/index-page.d.ts +19 -0
  39. package/dist/index-page.js +224 -0
  40. package/dist/index.d.ts +15 -0
  41. package/dist/index.js +21 -0
  42. package/dist/local/adapter.d.ts +2 -0
  43. package/dist/local/adapter.js +63 -0
  44. package/dist/local/constants.d.ts +1 -0
  45. package/dist/local/constants.js +1 -0
  46. package/dist/local/index.d.ts +4 -0
  47. package/dist/local/index.js +4 -0
  48. package/dist/local/session.d.ts +2 -0
  49. package/dist/local/session.js +12 -0
  50. package/dist/local/vite.d.ts +8 -0
  51. package/dist/local/vite.js +243 -0
  52. package/dist/mount.d.ts +43 -0
  53. package/dist/mount.js +151 -0
  54. package/dist/paths/index.d.ts +17 -0
  55. package/dist/paths/index.js +47 -0
  56. package/dist/sentinel.d.ts +6 -0
  57. package/dist/sentinel.js +6 -0
  58. package/dist/served-url.d.ts +16 -0
  59. package/dist/served-url.js +19 -0
  60. package/dist/session.d.ts +4 -0
  61. package/dist/session.js +30 -0
  62. package/dist/svelte/EditorPage.svelte +178 -0
  63. package/dist/svelte/EditorPage.svelte.d.ts +23 -0
  64. package/dist/svelte/index.d.ts +5 -0
  65. package/dist/svelte/index.js +5 -0
  66. package/dist/svelte/styles.d.ts +4 -0
  67. package/dist/sveltekit/index.d.ts +68 -0
  68. package/dist/sveltekit/index.js +98 -0
  69. package/dist/sveltekit/mapping.d.ts +1 -0
  70. package/dist/sveltekit/mapping.js +1 -0
  71. package/dist/types.d.ts +53 -0
  72. package/dist/types.js +1 -0
  73. package/dist/upload-context.d.ts +24 -0
  74. package/dist/upload-context.js +10 -0
  75. package/dist/vite/index.d.ts +9 -0
  76. package/dist/vite/index.js +49 -0
  77. package/package.json +110 -0
package/dist/mount.js ADDED
@@ -0,0 +1,151 @@
1
+ import 'uncial/web-components';
2
+ // The editor's own chrome: tokens, shell layout and controls. The host page's
3
+ // stylesheets are mirrored in below for prose parity, but they style the
4
+ // document, not the toolbar and panels around it — nothing else loads these.
5
+ import 'uncial/styles/chrome';
6
+ import {} from './editor-controller.js';
7
+ import { createEditorSession, defaultSessionProvider } from './editor-session.js';
8
+ import { UNCIAL_CMS_RUNTIME_SENTINEL } from './sentinel.js';
9
+ import { clearActiveForge } from './upload-context.js';
10
+ function mirrorPageStylesIntoEditor(editor, explicit) {
11
+ if (explicit) {
12
+ editor.stylesheet = explicit.join(' ');
13
+ return;
14
+ }
15
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'), (link) => link.href);
16
+ if (links.length > 0)
17
+ editor.stylesheet = links.join(' ');
18
+ // Dev servers inject <style> tags instead of links; clone those too.
19
+ for (const style of document.querySelectorAll('style')) {
20
+ editor.shadowRoot?.append(style.cloneNode(true));
21
+ }
22
+ }
23
+ export function mountEditorPage(target, opts) {
24
+ const { config, sourcePath } = opts;
25
+ const sessionProvider = opts.sessionProvider ?? defaultSessionProvider(config);
26
+ const blocks = opts.blocks;
27
+ const schema = opts.schema;
28
+ const root = document.createElement('div');
29
+ root.className = 'uncial-cms-editor-page';
30
+ root.dataset.uncialCmsRuntime = UNCIAL_CMS_RUNTIME_SENTINEL;
31
+ const chrome = document.createElement('div');
32
+ chrome.className = 'uncial-cms-chrome';
33
+ const manualSave = opts.autosaveMs === undefined;
34
+ const saveButton = document.createElement('button');
35
+ saveButton.type = 'button';
36
+ saveButton.textContent = 'Save';
37
+ saveButton.disabled = true;
38
+ const status = document.createElement('span');
39
+ status.className = 'uncial-cms-status';
40
+ status.setAttribute('role', 'status');
41
+ // Conflict recovery banner (ticket 05): blocking, offers download + reload,
42
+ // and must never lose the unsaved document.
43
+ const banner = document.createElement('div');
44
+ banner.className = 'uncial-cms-banner';
45
+ banner.setAttribute('role', 'alert');
46
+ banner.hidden = true;
47
+ const bannerText = document.createElement('p');
48
+ bannerText.className = 'uncial-cms-banner-message';
49
+ bannerText.textContent =
50
+ `This page changed on ${config.forge === 'github' ? config.branch : 'the local checkout'} since you loaded it. ` +
51
+ 'Your unsaved changes are safe — choose how to proceed.';
52
+ const downloadButton = document.createElement('button');
53
+ downloadButton.type = 'button';
54
+ downloadButton.textContent = 'Download my version';
55
+ const reloadButton = document.createElement('button');
56
+ reloadButton.type = 'button';
57
+ reloadButton.textContent = 'Reload latest';
58
+ const dismissButton = document.createElement('button');
59
+ dismissButton.type = 'button';
60
+ dismissButton.textContent = 'Dismiss';
61
+ const bannerActions = document.createElement('div');
62
+ bannerActions.className = 'uncial-cms-banner-actions';
63
+ bannerActions.append(downloadButton, reloadButton, dismissButton);
64
+ banner.append(bannerText, bannerActions);
65
+ const editor = document.createElement('uncial-editor');
66
+ mirrorPageStylesIntoEditor(editor, opts.editorStylesheets);
67
+ if (opts.attributesPanel !== undefined)
68
+ editor.attributesPanel = opts.attributesPanel;
69
+ if (opts.presentation !== undefined)
70
+ editor.presentation = opts.presentation;
71
+ editor.blocks = blocks;
72
+ editor.schema = schema;
73
+ // Forward the schema's declared meta fields so the editor renders (and edits)
74
+ // them in its "Edit document metadata" panel. Without this a consumer whose
75
+ // schema declares metaFields gets no metadata UI.
76
+ editor.metaFields = schema.metaFields;
77
+ if (manualSave)
78
+ chrome.append(saveButton);
79
+ chrome.append(status);
80
+ root.append(chrome, banner, editor);
81
+ target.append(root);
82
+ let destroyed = false;
83
+ const ui = {
84
+ status(view) {
85
+ status.replaceChildren(document.createTextNode(view.text));
86
+ status.dataset.tone = view.tone;
87
+ if (view.href) {
88
+ status.append(document.createTextNode(' '));
89
+ const link = document.createElement('a');
90
+ link.href = view.href;
91
+ link.target = '_blank';
92
+ link.rel = 'noopener';
93
+ link.textContent = 'View commit';
94
+ status.append(link);
95
+ }
96
+ },
97
+ setDocument(doc) {
98
+ editor.json = doc;
99
+ // Seed the metadata panel from the loaded document's meta. Without this
100
+ // the panel shows schema defaults, so committing metadata would clobber
101
+ // the document's existing meta (e.g. reset title to its default).
102
+ editor.meta = doc.meta ?? {};
103
+ },
104
+ saveEnabled(enabled) {
105
+ saveButton.disabled = !enabled;
106
+ },
107
+ conflictVisible(visible) {
108
+ banner.hidden = !visible;
109
+ }
110
+ };
111
+ const controller = createEditorSession({
112
+ config,
113
+ sourcePath,
114
+ pagePath: opts.pagePath,
115
+ blocks,
116
+ schema,
117
+ sessionProvider,
118
+ autosaveMs: opts.autosaveMs,
119
+ ui,
120
+ isDestroyed: () => destroyed
121
+ });
122
+ const onChange = (event) => {
123
+ controller.documentChanged(event.detail);
124
+ };
125
+ editor.addEventListener('uncial-change', onChange);
126
+ if (manualSave)
127
+ saveButton.addEventListener('click', () => void controller.save());
128
+ downloadButton.addEventListener('click', () => controller.downloadMyVersion());
129
+ reloadButton.addEventListener('click', () => void controller.reloadLatest());
130
+ dismissButton.addEventListener('click', () => controller.dismissConflict());
131
+ void controller.load().catch((error) => {
132
+ if (destroyed)
133
+ return;
134
+ ui.status({
135
+ tone: 'error',
136
+ text: error instanceof Error ? error.message : 'Failed to load the document.'
137
+ });
138
+ });
139
+ return {
140
+ destroy() {
141
+ destroyed = true;
142
+ controller.stop();
143
+ clearActiveForge();
144
+ editor.removeEventListener('uncial-change', onChange);
145
+ root.remove();
146
+ },
147
+ isDirty() {
148
+ return controller.isDirty();
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,17 @@
1
+ /** Site-relative URL path (base already stripped) → repo-root-relative JSON path. */
2
+ export declare function defaultMapPathToSource(sitePath: string, contentDir: string, locale?: string): string;
3
+ /** Repo-root-relative JSON path → site-relative URL path (no leading/trailing slash). */
4
+ export declare function defaultMapSourceToPath(source: string, contentDir: string, locale?: string): string;
5
+ export type PagePathValidation = {
6
+ ok: true;
7
+ path: string;
8
+ } | {
9
+ ok: false;
10
+ message: string;
11
+ };
12
+ /** Validate a user-typed page path; strips surrounding whitespace and slashes. */
13
+ export declare function validatePagePath(input: string): PagePathValidation;
14
+ /** `'about'` → `'#/about/'`; the site root (`''`) → `'#/'`. */
15
+ export declare function hashForPagePath(path: string): string;
16
+ /** `'#/about/'` → `'about'`; `'#/'` → `''` (site root); no hash → null (list view). */
17
+ export declare function pagePathFromHash(hash: string): string | null;
@@ -0,0 +1,47 @@
1
+ function normalizeSitePath(sitePath) {
2
+ return sitePath.replace(/^\/+/, '').replace(/\/+$/, '');
3
+ }
4
+ function contentDirForLocale(contentDir, locale) {
5
+ return locale === 'en' ? contentDir : `${contentDir}/${locale}`;
6
+ }
7
+ /** Site-relative URL path (base already stripped) → repo-root-relative JSON path. */
8
+ export function defaultMapPathToSource(sitePath, contentDir, locale = 'en') {
9
+ const path = normalizeSitePath(sitePath);
10
+ return `${contentDirForLocale(contentDir, locale)}/${path === '' ? 'index' : path}.json`;
11
+ }
12
+ /** Repo-root-relative JSON path → site-relative URL path (no leading/trailing slash). */
13
+ export function defaultMapSourceToPath(source, contentDir, locale = 'en') {
14
+ const prefix = `${contentDirForLocale(contentDir, locale)}/`;
15
+ if (!source.startsWith(prefix) || !source.endsWith('.json')) {
16
+ throw new Error(`Source "${source}" is not a JSON file under "${contentDir}".`);
17
+ }
18
+ const path = source.slice(prefix.length, -'.json'.length);
19
+ return path === 'index' ? '' : path;
20
+ }
21
+ const SEGMENT = /^[a-z0-9-]+$/;
22
+ /** Validate a user-typed page path; strips surrounding whitespace and slashes. */
23
+ export function validatePagePath(input) {
24
+ const path = input.trim().replace(/^\/+/, '').replace(/\/+$/, '');
25
+ if (path === '') {
26
+ return { ok: false, message: 'Enter a page path, e.g. team/new-page.' };
27
+ }
28
+ if (!path.split('/').every((segment) => SEGMENT.test(segment))) {
29
+ return {
30
+ ok: false,
31
+ message: 'Page paths must be lowercase segments of letters, digits, and hyphens separated by "/", e.g. team/new-page.'
32
+ };
33
+ }
34
+ return { ok: true, path };
35
+ }
36
+ /** `'about'` → `'#/about/'`; the site root (`''`) → `'#/'`. */
37
+ export function hashForPagePath(path) {
38
+ return path === '' ? '#/' : `#/${path}/`;
39
+ }
40
+ /** `'#/about/'` → `'about'`; `'#/'` → `''` (site root); no hash → null (list view). */
41
+ export function pagePathFromHash(hash) {
42
+ if (hash === '' || hash === '#')
43
+ return null;
44
+ if (!hash.startsWith('#/'))
45
+ return null;
46
+ return hash.slice(2).replace(/\/+$/, '');
47
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Embedded verbatim in every bundle that includes the editor runtime (see
3
+ * mount.ts). The clean-pages build assertion greps content-page HTML and its
4
+ * JS chunks for this string to prove production pages ship zero CMS code.
5
+ */
6
+ export declare const UNCIAL_CMS_RUNTIME_SENTINEL = "uncial-cms-runtime-sentinel-v1";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Embedded verbatim in every bundle that includes the editor runtime (see
3
+ * mount.ts). The clean-pages build assertion greps content-page HTML and its
4
+ * JS chunks for this string to prove production pages ship zero CMS code.
5
+ */
6
+ export const UNCIAL_CMS_RUNTIME_SENTINEL = 'uncial-cms-runtime-sentinel-v1';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Map a committed repo path to the URL the built site serves it from.
3
+ */
4
+ import type { Site } from './define-site.js';
5
+ /**
6
+ * Everything under the site's static directory is copied to the site root at
7
+ * build time, so a committed asset path becomes a served URL by dropping that
8
+ * prefix. `staticDir` is repo-root-relative, like `mediaDir`; a `mediaDir` that
9
+ * does not sit under it is not served by the copy, so the path is returned
10
+ * site-root-absolute unchanged.
11
+ *
12
+ * The result carries no base path: a stored `src` must stay correct at every
13
+ * `paths.base` the same content is built at, so the site prepends its base at
14
+ * render time.
15
+ */
16
+ export declare function servedUrl(site: Site, repoPath: string, staticDir?: string): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Everything under the site's static directory is copied to the site root at
3
+ * build time, so a committed asset path becomes a served URL by dropping that
4
+ * prefix. `staticDir` is repo-root-relative, like `mediaDir`; a `mediaDir` that
5
+ * does not sit under it is not served by the copy, so the path is returned
6
+ * site-root-absolute unchanged.
7
+ *
8
+ * The result carries no base path: a stored `src` must stay correct at every
9
+ * `paths.base` the same content is built at, so the site prepends its base at
10
+ * render time.
11
+ */
12
+ export function servedUrl(site, repoPath, staticDir = 'static') {
13
+ const prefix = `${staticDir.replace(/\/+$/, '')}/`;
14
+ const mediaDir = site.config.mediaDir;
15
+ if (mediaDir?.startsWith(prefix) && repoPath.startsWith(prefix)) {
16
+ return repoPath.slice(prefix.length - 1);
17
+ }
18
+ return `/${repoPath.replace(/^\/+/, '')}`;
19
+ }
@@ -0,0 +1,4 @@
1
+ import type { ForgeSession } from './types.js';
2
+ export declare function readCachedSession(repo: string): ForgeSession | null;
3
+ export declare function writeCachedSession(session: ForgeSession): void;
4
+ export declare function clearCachedSession(repo: string): void;
@@ -0,0 +1,30 @@
1
+ function storageKey(repo) {
2
+ return `uncial-cms:session:${repo}`;
3
+ }
4
+ function storage() {
5
+ return typeof sessionStorage === 'undefined' ? null : sessionStorage;
6
+ }
7
+ export function readCachedSession(repo) {
8
+ const raw = storage()?.getItem(storageKey(repo));
9
+ if (!raw)
10
+ return null;
11
+ let session;
12
+ try {
13
+ session = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ clearCachedSession(repo);
17
+ return null;
18
+ }
19
+ if (session.expiresAt !== null && session.expiresAt <= Date.now()) {
20
+ clearCachedSession(repo);
21
+ return null;
22
+ }
23
+ return session;
24
+ }
25
+ export function writeCachedSession(session) {
26
+ storage()?.setItem(storageKey(session.repo), JSON.stringify(session));
27
+ }
28
+ export function clearCachedSession(repo) {
29
+ storage()?.removeItem(storageKey(repo));
30
+ }
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The SvelteKit door onto an Editor variant: Uncial's `Editor` rendered in
4
+ * the host's own tree and cascade, with the whole editing surface
5
+ * `mountEditorPage` owns — status line, conflict banner, metadata seeding,
6
+ * Save or autosave — around it.
7
+ *
8
+ * `mountEditorPage` builds a custom element with a shadow root, which is the
9
+ * right shape for a host with no component model. It is the wrong shape for a
10
+ * SvelteKit site whose Editor variant exists to show an author the measure,
11
+ * face and ground a reader will see: no rule the site sets on `body` crosses
12
+ * that boundary, so the site has to restate every one of them.
13
+ */
14
+ import { onMount } from 'svelte';
15
+ import type { BlockRegistry, ContentDocument, ContentSchema } from 'uncial/core';
16
+ import type { Site } from '../define-site.js';
17
+ import type { EditorController, StatusView } from '../editor-controller.js';
18
+ import { UNCIAL_CMS_RUNTIME_SENTINEL } from '../sentinel.js';
19
+ import type { SessionProvider } from '../types.js';
20
+ import { clearActiveForge } from '../upload-context.js';
21
+
22
+ interface Props {
23
+ /** The site object from `defineSite`. */
24
+ site: Site;
25
+ /** Repo-root-relative JSON path, from the editor route's payload. */
26
+ sourcePath: string;
27
+ /** Site-relative page path, from the editor route's payload. */
28
+ pagePath: string;
29
+ blocks: BlockRegistry;
30
+ /** One schema for the site, or the schema this page path is written against. */
31
+ schema: ContentSchema | ((path: string) => ContentSchema);
32
+ /** Defaults to the provider the resolved forge implies. */
33
+ sessionProvider?: SessionProvider;
34
+ /** Forwarded to `Editor`; `'overlay'` keeps the document at the host's width. */
35
+ attributesPanel?: 'docked' | 'overlay' | 'off';
36
+ /** Forwarded to `Editor`; `'bare'` draws no surface of the editor's own. */
37
+ presentation?: 'card' | 'bare';
38
+ }
39
+
40
+ let {
41
+ site,
42
+ sourcePath,
43
+ pagePath,
44
+ blocks,
45
+ schema,
46
+ sessionProvider,
47
+ attributesPanel = 'overlay',
48
+ presentation = 'bare'
49
+ }: Props = $props();
50
+
51
+ const resolvedSchema = $derived(typeof schema === 'function' ? schema(pagePath) : schema);
52
+ // Autosave leaves nothing to press; a forge commit is never autosaved, so a
53
+ // Save button and autosave are exactly the two modes.
54
+ const manualSave = $derived(site.autosaveMs === undefined);
55
+ const branch = $derived(
56
+ site.config.forge === 'github' ? site.config.branch : 'the local checkout'
57
+ );
58
+
59
+ type EditorComponent = (typeof import('uncial/editor'))['Editor'];
60
+
61
+ let Editor = $state<EditorComponent | undefined>(undefined);
62
+ let doc = $state<ContentDocument | undefined>(undefined);
63
+ let meta = $state<Record<string, unknown>>({});
64
+ let status = $state<StatusView | undefined>(undefined);
65
+ let conflict = $state(false);
66
+ let saveEnabled = $state(false);
67
+ let controller: EditorController | undefined;
68
+ let root: HTMLDivElement;
69
+
70
+ onMount(() => {
71
+ // The editor stack hangs off dynamic imports behind a statically decidable
72
+ // condition, so a local-only production build has no reachable path to it
73
+ // and drops it rather than merely leaving it unrouted. Vite replaces both
74
+ // operands with literals at build time.
75
+ if (!import.meta.env.DEV && import.meta.env.UNCIAL_CMS_FORGE === 'none') return;
76
+
77
+ // Marked here rather than in the markup so that the gate above leaves the
78
+ // sentinel unreferenced in a local-only production build, and Rollup drops
79
+ // it with the rest of the editor stack.
80
+ root.dataset.uncialCmsRuntime = UNCIAL_CMS_RUNTIME_SENTINEL;
81
+
82
+ let cancelled = false;
83
+
84
+ void Promise.all([
85
+ import('uncial/editor'),
86
+ // The package's own session module, not the `uncial-cms/session`
87
+ // subpath: this file is inside the package.
88
+ import('../editor-session.js'),
89
+ // The editor's chrome — tokens, shell layout and controls. Loaded here
90
+ // so the host never has to know the component has a stylesheet.
91
+ import('uncial/styles/chrome')
92
+ ]).then(([editor, session]) => {
93
+ if (cancelled) return;
94
+ Editor = editor.Editor;
95
+ controller = session.createEditorSession({
96
+ config: site.config,
97
+ sourcePath,
98
+ pagePath,
99
+ blocks,
100
+ schema: resolvedSchema,
101
+ sessionProvider,
102
+ autosaveMs: site.autosaveMs,
103
+ isDestroyed: () => cancelled,
104
+ ui: {
105
+ status: (view) => (status = view),
106
+ setDocument: (next) => {
107
+ doc = next;
108
+ // Seed the metadata panel from the loaded document. Without
109
+ // this it shows schema defaults, and committing metadata
110
+ // would clobber the document's own.
111
+ meta = next.meta ?? {};
112
+ },
113
+ saveEnabled: (enabled) => (saveEnabled = enabled),
114
+ conflictVisible: (visible) => (conflict = visible)
115
+ }
116
+ });
117
+ void controller.load().catch((error: unknown) => {
118
+ if (cancelled) return;
119
+ status = {
120
+ tone: 'error',
121
+ text: error instanceof Error ? error.message : 'Failed to load the document.'
122
+ };
123
+ });
124
+ });
125
+
126
+ return () => {
127
+ cancelled = true;
128
+ controller?.stop();
129
+ clearActiveForge();
130
+ };
131
+ });
132
+ </script>
133
+
134
+ <div class="uncial-cms-editor-page" bind:this={root}>
135
+ <div class="uncial-cms-chrome">
136
+ {#if manualSave}
137
+ <button type="button" disabled={!saveEnabled} onclick={() => void controller?.save()}>
138
+ Save
139
+ </button>
140
+ {/if}
141
+ {#if status}
142
+ <p class="uncial-cms-status" role="status" data-tone={status.tone}>
143
+ {status.text}{#if status.href}&nbsp;<a href={status.href} target="_blank" rel="noopener"
144
+ >View commit</a
145
+ >{/if}
146
+ </p>
147
+ {/if}
148
+ </div>
149
+
150
+ {#if conflict}
151
+ <div class="uncial-cms-banner" role="alert">
152
+ <p class="uncial-cms-banner-message">
153
+ This page changed on {branch} since you loaded it. Your unsaved changes are safe — choose how
154
+ to proceed.
155
+ </p>
156
+ <div class="uncial-cms-banner-actions">
157
+ <button type="button" onclick={() => controller?.downloadMyVersion()}>
158
+ Download my version
159
+ </button>
160
+ <button type="button" onclick={() => void controller?.reloadLatest()}>Reload latest</button>
161
+ <button type="button" onclick={() => controller?.dismissConflict()}>Dismiss</button>
162
+ </div>
163
+ </div>
164
+ {/if}
165
+
166
+ {#if Editor && doc}
167
+ <Editor
168
+ {blocks}
169
+ schema={resolvedSchema}
170
+ metaFields={resolvedSchema.metaFields}
171
+ bind:json={doc}
172
+ bind:meta
173
+ {attributesPanel}
174
+ {presentation}
175
+ onChange={(next) => controller?.documentChanged(next as ContentDocument)}
176
+ />
177
+ {/if}
178
+ </div>
@@ -0,0 +1,23 @@
1
+ import type { BlockRegistry, ContentSchema } from 'uncial/core';
2
+ import type { Site } from '../define-site.js';
3
+ import type { SessionProvider } from '../types.js';
4
+ interface Props {
5
+ /** The site object from `defineSite`. */
6
+ site: Site;
7
+ /** Repo-root-relative JSON path, from the editor route's payload. */
8
+ sourcePath: string;
9
+ /** Site-relative page path, from the editor route's payload. */
10
+ pagePath: string;
11
+ blocks: BlockRegistry;
12
+ /** One schema for the site, or the schema this page path is written against. */
13
+ schema: ContentSchema | ((path: string) => ContentSchema);
14
+ /** Defaults to the provider the resolved forge implies. */
15
+ sessionProvider?: SessionProvider;
16
+ /** Forwarded to `Editor`; `'overlay'` keeps the document at the host's width. */
17
+ attributesPanel?: 'docked' | 'overlay' | 'off';
18
+ /** Forwarded to `Editor`; `'bare'` draws no surface of the editor's own. */
19
+ presentation?: 'card' | 'bare';
20
+ }
21
+ declare const EditorPage: import("svelte").Component<Props, {}, "">;
22
+ type EditorPage = ReturnType<typeof EditorPage>;
23
+ export default EditorPage;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `uncial-cms/svelte` — the SvelteKit door. `svelte` is a peer of this subpath
3
+ * only; the runtime root still imports no Svelte.
4
+ */
5
+ export { default as EditorPage } from './EditorPage.svelte';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `uncial-cms/svelte` — the SvelteKit door. `svelte` is a peer of this subpath
3
+ * only; the runtime root still imports no Svelte.
4
+ */
5
+ export { default as EditorPage } from './EditorPage.svelte';
@@ -0,0 +1,4 @@
1
+ // `uncial/styles/chrome` is a stylesheet, imported for its effect. Vite resolves
2
+ // it; TypeScript needs telling it exists. Ships with the package so a consumer
3
+ // type-checking this component resolves it too.
4
+ declare module 'uncial/styles/chrome';
@@ -0,0 +1,68 @@
1
+ import type { BlockRegistry, ContentDocument, ContentSchema } from 'uncial/core';
2
+ import type { Site } from '../define-site.js';
3
+ import type { UncialCmsSiteConfig } from '../types.js';
4
+ export { defaultMapPathToSource, defaultMapSourceToPath } from '../paths/index.js';
5
+ /** A content file as the factories see it: site-relative path, repo-root-relative source. */
6
+ export interface ContentEntry {
7
+ path: string;
8
+ source: string;
9
+ }
10
+ interface HandlerOptionsBase {
11
+ blocks: BlockRegistry;
12
+ /** One schema for the whole site, or the schema this page path is written against. */
13
+ schema: ContentSchema | ((path: string) => ContentSchema);
14
+ /** Site-relative URL path → repo-root-relative JSON path. */
15
+ mapPathToSource?: (path: string) => string;
16
+ /** Keep a non-page file — site settings, a manifest — out of the routes. */
17
+ exclude?: (entry: ContentEntry) => boolean;
18
+ }
19
+ /** The site object from `defineSite`, or the resolved config plus its build-time FS path. */
20
+ interface SiteSource {
21
+ site: Site;
22
+ config?: never;
23
+ localContentDir?: never;
24
+ }
25
+ interface ConfigSource {
26
+ site?: never;
27
+ config: UncialCmsSiteConfig;
28
+ /** FS path of the content dir at build time (differs from config.contentDir,
29
+ * which is repo-root-relative for the forge API). */
30
+ localContentDir: string;
31
+ }
32
+ export type ContentHandlerOptions = HandlerOptionsBase & (SiteSource | ConfigSource);
33
+ export type IndexHandlerOptions = HandlerOptionsBase & (SiteSource | (Omit<ConfigSource, 'localContentDir'> & {
34
+ localContentDir?: string;
35
+ }));
36
+ interface RouteEntry {
37
+ path: string;
38
+ }
39
+ export declare function createContentHandlers(opts: ContentHandlerOptions): {
40
+ entries: () => RouteEntry[];
41
+ load: (event: {
42
+ params: {
43
+ path: string;
44
+ };
45
+ }) => Promise<{
46
+ document: ContentDocument;
47
+ meta: Record<string, unknown>;
48
+ path: string;
49
+ }>;
50
+ };
51
+ export declare function createEditorHandlers(opts: ContentHandlerOptions & {
52
+ devOnly?: boolean;
53
+ }): {
54
+ entries: () => RouteEntry[];
55
+ load: (event: {
56
+ params: {
57
+ path: string;
58
+ };
59
+ }) => Promise<{
60
+ sourcePath: string;
61
+ pagePath: string;
62
+ }>;
63
+ };
64
+ export declare function createIndexHandlers(opts: IndexHandlerOptions): {
65
+ load: () => Promise<{
66
+ config: UncialCmsSiteConfig;
67
+ }>;
68
+ };