@velastack/cms 0.4.0 → 0.5.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/README.md +107 -18
- package/dist/backend/index.d.ts +1 -1
- package/dist/backend/routes/seed.d.ts +6 -0
- package/dist/backend/routes/seed.js +96 -0
- package/dist/backend/routes/table.js +3 -0
- package/dist/backend/store/queries.d.ts +6 -1
- package/dist/backend/store/queries.js +89 -0
- package/dist/cms.js +13 -16
- package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
- package/dist/components/admin-bar/admin-bar.css +1 -1
- package/dist/components/admin-bar/field-input.svelte +220 -0
- package/dist/components/admin-bar/field-input.svelte.d.ts +29 -0
- package/dist/components/admin-bar/icons/pencil.svelte +19 -0
- package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
- package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
- package/dist/components/admin-bar/locales-panel.svelte +42 -1
- package/dist/components/admin-bar/page-config.d.ts +8 -1
- package/dist/components/admin-bar/page-config.js +15 -2
- package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
- package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
- package/dist/components/admin-bar/seo-panel.svelte +144 -189
- package/dist/components/admin-bar/site-options-panel.svelte +115 -0
- package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte +250 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
- package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/popover/index.js +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
- package/dist/components/admin-bar/ui/sheet/index.js +12 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/switch/index.js +2 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.js +7 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/cms/cms-boolean.svelte +9 -18
- package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
- package/dist/components/cms/cms-date-time-editable.svelte +3 -3
- package/dist/components/cms/cms-date-time.svelte +17 -17
- package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
- package/dist/components/cms/cms-image-editable.svelte +13 -4
- package/dist/components/cms/cms-image.svelte +37 -30
- package/dist/components/cms/cms-image.svelte.d.ts +11 -4
- package/dist/components/cms/cms-link.svelte +26 -19
- package/dist/components/cms/cms-link.svelte.d.ts +2 -0
- package/dist/components/cms/cms-markdown.svelte +15 -16
- package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
- package/dist/components/cms/cms-number-editable.svelte +4 -2
- package/dist/components/cms/cms-number.svelte +18 -17
- package/dist/components/cms/cms-number.svelte.d.ts +2 -1
- package/dist/components/cms/cms-repeater.svelte +15 -18
- package/dist/components/cms/cms-repeater.svelte.d.ts +1 -0
- package/dist/components/cms/cms-rich-text.svelte +17 -22
- package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
- package/dist/components/cms/cms-store.svelte.d.ts +14 -0
- package/dist/components/cms/cms-store.svelte.js +39 -2
- package/dist/components/cms/cms-text.svelte +23 -30
- package/dist/components/cms/cms-text.svelte.d.ts +3 -1
- package/dist/components/cms/scope.d.ts +25 -5
- package/dist/components/cms/structured-registry.d.ts +10 -0
- package/dist/components/cms/structured-registry.js +6 -0
- package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
- package/dist/components/cms/use-cms-field.svelte.js +83 -0
- package/dist/core/metadata.d.ts +44 -0
- package/dist/core/metadata.js +59 -0
- package/dist/core/structured.d.ts +108 -0
- package/dist/core/structured.js +184 -0
- package/dist/core/wire.d.ts +31 -1
- package/dist/editor.d.ts +22 -0
- package/dist/editor.js +21 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +13 -3
- package/dist/server/load-cms.js +1 -0
- package/package.json +12 -3
- package/plugin/README.md +22 -2
- package/plugin/dist/build.d.ts +74 -0
- package/plugin/dist/build.js +210 -0
- package/plugin/dist/build.js.map +1 -0
- package/plugin/dist/index.d.ts +10 -1
- package/plugin/dist/index.js +63 -4
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/manifest.d.ts +31 -0
- package/plugin/dist/manifest.js +146 -41
- package/plugin/dist/manifest.js.map +1 -1
- package/plugin/dist/parse-svelte.d.ts +15 -0
- package/plugin/dist/parse-svelte.js +26 -16
- package/plugin/dist/parse-svelte.js.map +1 -1
- package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
- package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
|
@@ -5,6 +5,16 @@ export type CmsScope = {
|
|
|
5
5
|
routeId: string;
|
|
6
6
|
params: Record<string, string>;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* One CMS component usage as recorded by the Vite plugin: which value it
|
|
10
|
+
* reads and which component type reads it.
|
|
11
|
+
*/
|
|
12
|
+
export type CmsManifestUsage = {
|
|
13
|
+
name: string;
|
|
14
|
+
/** Canonical exported component name (`CmsText`, `CmsHours`, …). */
|
|
15
|
+
component: string;
|
|
16
|
+
preset?: string;
|
|
17
|
+
};
|
|
8
18
|
export type CmsScopeEntry = {
|
|
9
19
|
scopeId: string;
|
|
10
20
|
kind: 'layout' | 'page';
|
|
@@ -15,23 +25,31 @@ export type CmsScopeEntry = {
|
|
|
15
25
|
* `metadata` branch shows up here too (e.g. `'metadata.title'`).
|
|
16
26
|
*/
|
|
17
27
|
fields: string[];
|
|
28
|
+
/** The usages behind `fields`, with component types. Absent on hand-built manifests. */
|
|
29
|
+
usages?: CmsManifestUsage[];
|
|
18
30
|
};
|
|
19
31
|
/**
|
|
20
|
-
* Schema for the project-wide site scope
|
|
21
|
-
* Top-level keys are sections rendered as
|
|
22
|
-
* each section's `fields` keys become path segments in the stored
|
|
23
|
-
* (`
|
|
32
|
+
* Schema for the project-wide site scope: the **Site Options** panel.
|
|
33
|
+
* Authored on `createCms({ site })`. Top-level keys are sections rendered as
|
|
34
|
+
* groups; each section's `fields` keys become path segments in the stored
|
|
35
|
+
* tree (`seo.schemaType`, `seo.shareImage`).
|
|
36
|
+
*
|
|
37
|
+
* Site options are for values that never render on the page — the schema.org
|
|
38
|
+
* type, the default share image, enabled locales. Anything a visitor sees
|
|
39
|
+
* belongs in the root layout scope, edited where it renders.
|
|
24
40
|
*
|
|
25
41
|
* `type` selects the input widget. Storage is unconstrained — the schema is
|
|
26
42
|
* a UI hint, not an enforced type for the underlying tree (to keep the data
|
|
27
43
|
* model uniform with pages/layouts and avoid destructive prunes when fields
|
|
28
44
|
* are added/removed).
|
|
29
45
|
*/
|
|
30
|
-
export type SiteFieldType = 'text' | 'markdown' | 'number' | 'datetime' | 'url' | 'color' | 'image';
|
|
46
|
+
export type SiteFieldType = 'text' | 'markdown' | 'number' | 'datetime' | 'url' | 'color' | 'image' | 'enum' | 'boolean';
|
|
31
47
|
export type SiteFieldSchema = {
|
|
32
48
|
type: SiteFieldType;
|
|
33
49
|
label: string;
|
|
34
50
|
placeholder?: string;
|
|
51
|
+
/** Options for `enum` fields. */
|
|
52
|
+
values?: readonly string[];
|
|
35
53
|
};
|
|
36
54
|
export type SiteSectionSchema = {
|
|
37
55
|
label: string;
|
|
@@ -104,6 +122,8 @@ export type CmsManifestScope = {
|
|
|
104
122
|
* `metadata` branch shows up here too (e.g. `'metadata.title'`).
|
|
105
123
|
*/
|
|
106
124
|
fields: string[];
|
|
125
|
+
/** The usages behind `fields`, with component types. Absent on hand-built manifests. */
|
|
126
|
+
usages?: CmsManifestUsage[];
|
|
107
127
|
};
|
|
108
128
|
export type CmsManifestRoute = {
|
|
109
129
|
scopes: CmsManifestScope[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which exported component reads which structured shape. Structured
|
|
3
|
+
* components register their schema at module load, so the Locales panel can
|
|
4
|
+
* count translatable strings for every usage the manifest recorded on the
|
|
5
|
+
* current route without importing the components themselves.
|
|
6
|
+
*/
|
|
7
|
+
import type { Structured } from '../../core/structured.js';
|
|
8
|
+
import type { Tree } from '../../core/path.js';
|
|
9
|
+
export declare const registerStructured: <T extends Tree>(schema: Structured<T>) => Structured<T>;
|
|
10
|
+
export declare const getStructured: (component: string) => Structured<Tree> | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one piece of plumbing every display component shares: find the scope,
|
|
3
|
+
* read the value, decide whether it is editable, write it back.
|
|
4
|
+
*
|
|
5
|
+
* Scope resolution (`scope` prop):
|
|
6
|
+
* - absent → the enclosing route scope from context, installed by the plugin
|
|
7
|
+
* into every `+layout.svelte` / `+page.svelte`.
|
|
8
|
+
* - `'root'` → the root layout scope `layout:/`, so a page can read and write
|
|
9
|
+
* a value that lives site-wide (hours, contact, navigation, branding). The
|
|
10
|
+
* payload already carries every enclosing layout tree, and the root layout
|
|
11
|
+
* encloses everything.
|
|
12
|
+
* - any other string → that scope id verbatim (`layout:/(public)`), for a
|
|
13
|
+
* value owned by an intermediate layout. Only param-less scopes can be
|
|
14
|
+
* addressed this way.
|
|
15
|
+
*
|
|
16
|
+
* Read rule (see `core/structured.ts`): `undefined` means nothing is stored
|
|
17
|
+
* and the component shows its fallback; `null` means the editor cleared the
|
|
18
|
+
* field, and the component shows it empty. Never conflate the two — that is
|
|
19
|
+
* the bug where a cleared field reverts to the template's demo copy.
|
|
20
|
+
*/
|
|
21
|
+
import { type CmsScope } from './scope.js';
|
|
22
|
+
import { type CmsScopeRef } from './cms-store.svelte.js';
|
|
23
|
+
export declare const ROOT_SCOPE_ID = "layout:/";
|
|
24
|
+
export type CmsFieldInput = {
|
|
25
|
+
name: string;
|
|
26
|
+
scope?: string;
|
|
27
|
+
/** Per-item override: disables editing and scope lookup. */
|
|
28
|
+
value?: unknown;
|
|
29
|
+
};
|
|
30
|
+
export declare const resolveScopeRef: (scope: string | undefined, context: CmsScope | undefined) => CmsScopeRef | null;
|
|
31
|
+
export type CmsField<T> = {
|
|
32
|
+
/** Resolved scope, or `null` outside any route scope. */
|
|
33
|
+
readonly ref: CmsScopeRef | null;
|
|
34
|
+
/** Raw stored value: `undefined` (nothing), `null` (cleared) or the value. */
|
|
35
|
+
readonly raw: unknown;
|
|
36
|
+
/** What to render, per the component's `read`. */
|
|
37
|
+
readonly current: T;
|
|
38
|
+
readonly editable: boolean;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/** Write the whole value into the draft. Pass `null` to clear. `locale`
|
|
41
|
+
* defaults to the locale being edited; structured editors pass the
|
|
42
|
+
* default locale for structure and another locale for its `$t` strings. */
|
|
43
|
+
set: (next: unknown, locale?: string) => void;
|
|
44
|
+
/** Write a value at a sub-path of this field, e.g. a `$t` string. */
|
|
45
|
+
setAt: (subPath: string, next: unknown, locale?: string) => void;
|
|
46
|
+
/** Read this field (or a sub-path of it) in a specific locale. */
|
|
47
|
+
getIn: (locale: string, subPath?: string) => unknown;
|
|
48
|
+
clear: () => void;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Call once at component init. `input` is an accessor so prop changes flow
|
|
52
|
+
* through; `read` turns the raw stored value into what the component renders.
|
|
53
|
+
*/
|
|
54
|
+
export declare const useCmsField: <T>(input: () => CmsFieldInput, read: (raw: unknown) => T) => CmsField<T>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one piece of plumbing every display component shares: find the scope,
|
|
3
|
+
* read the value, decide whether it is editable, write it back.
|
|
4
|
+
*
|
|
5
|
+
* Scope resolution (`scope` prop):
|
|
6
|
+
* - absent → the enclosing route scope from context, installed by the plugin
|
|
7
|
+
* into every `+layout.svelte` / `+page.svelte`.
|
|
8
|
+
* - `'root'` → the root layout scope `layout:/`, so a page can read and write
|
|
9
|
+
* a value that lives site-wide (hours, contact, navigation, branding). The
|
|
10
|
+
* payload already carries every enclosing layout tree, and the root layout
|
|
11
|
+
* encloses everything.
|
|
12
|
+
* - any other string → that scope id verbatim (`layout:/(public)`), for a
|
|
13
|
+
* value owned by an intermediate layout. Only param-less scopes can be
|
|
14
|
+
* addressed this way.
|
|
15
|
+
*
|
|
16
|
+
* Read rule (see `core/structured.ts`): `undefined` means nothing is stored
|
|
17
|
+
* and the component shows its fallback; `null` means the editor cleared the
|
|
18
|
+
* field, and the component shows it empty. Never conflate the two — that is
|
|
19
|
+
* the bug where a cleared field reverts to the template's demo copy.
|
|
20
|
+
*/
|
|
21
|
+
import { getCmsScope } from './scope.js';
|
|
22
|
+
import { cmsStore } from './cms-store.svelte.js';
|
|
23
|
+
export const ROOT_SCOPE_ID = 'layout:/';
|
|
24
|
+
export const resolveScopeRef = (scope, context) => {
|
|
25
|
+
if (scope === 'root')
|
|
26
|
+
return { scopeId: ROOT_SCOPE_ID, routeId: '/', params: {} };
|
|
27
|
+
if (scope) {
|
|
28
|
+
const colon = scope.indexOf(':');
|
|
29
|
+
const routeId = colon === -1 ? scope : scope.slice(colon + 1);
|
|
30
|
+
return { scopeId: colon === -1 ? `layout:${scope}` : scope, routeId, params: {} };
|
|
31
|
+
}
|
|
32
|
+
if (!context)
|
|
33
|
+
return null;
|
|
34
|
+
return { scopeId: context.scopeId, routeId: context.routeId, params: context.params };
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Call once at component init. `input` is an accessor so prop changes flow
|
|
38
|
+
* through; `read` turns the raw stored value into what the component renders.
|
|
39
|
+
*/
|
|
40
|
+
export const useCmsField = (input, read) => {
|
|
41
|
+
const context = getCmsScope();
|
|
42
|
+
const ref = $derived(resolveScopeRef(input().scope, context));
|
|
43
|
+
const raw = $derived.by(() => {
|
|
44
|
+
const { name, value } = input();
|
|
45
|
+
if (value !== undefined)
|
|
46
|
+
return value;
|
|
47
|
+
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
48
|
+
});
|
|
49
|
+
const current = $derived(read(raw));
|
|
50
|
+
const editable = $derived(cmsStore.isEditing && input().value === undefined && !!ref);
|
|
51
|
+
const set = (next, locale) => {
|
|
52
|
+
if (ref)
|
|
53
|
+
cmsStore.setValue(ref, input().name, next, locale);
|
|
54
|
+
};
|
|
55
|
+
const setAt = (subPath, next, locale) => {
|
|
56
|
+
if (ref)
|
|
57
|
+
cmsStore.setValue(ref, `${input().name}.${subPath}`, next, locale);
|
|
58
|
+
};
|
|
59
|
+
const getIn = (locale, subPath) => ref
|
|
60
|
+
? cmsStore.getValue(ref, subPath ? `${input().name}.${subPath}` : input().name, locale)
|
|
61
|
+
: undefined;
|
|
62
|
+
return {
|
|
63
|
+
get ref() {
|
|
64
|
+
return ref;
|
|
65
|
+
},
|
|
66
|
+
get raw() {
|
|
67
|
+
return raw;
|
|
68
|
+
},
|
|
69
|
+
get current() {
|
|
70
|
+
return current;
|
|
71
|
+
},
|
|
72
|
+
get editable() {
|
|
73
|
+
return editable;
|
|
74
|
+
},
|
|
75
|
+
get name() {
|
|
76
|
+
return input().name;
|
|
77
|
+
},
|
|
78
|
+
set,
|
|
79
|
+
setAt,
|
|
80
|
+
getIn,
|
|
81
|
+
clear: () => set(null)
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page metadata → `svelte-meta-tags` props.
|
|
3
|
+
*
|
|
4
|
+
* The page scope's `metadata` branch holds what the SEO panel edits (see
|
|
5
|
+
* `DEFAULT_METADATA_SCHEMA`). This maps it onto the object
|
|
6
|
+
* `definePageMetaTags` / `<MetaTags>` accept, so a template's root layout is
|
|
7
|
+
* one line: `deepMerge(baseMetaTags, toMetaTags(cms.metadata, { siteName }))`.
|
|
8
|
+
* Share fields fall back to the search fields, and the site name — read from
|
|
9
|
+
* the root layout's `branding.name` — supplies the title template and the
|
|
10
|
+
* Open Graph site name.
|
|
11
|
+
*/
|
|
12
|
+
export type MetaTagsInput = Record<string, unknown>;
|
|
13
|
+
export type MetaTagsOptions = {
|
|
14
|
+
/** The root layout's `branding.name`; drives `titleTemplate` and `openGraph.siteName`. */
|
|
15
|
+
siteName?: string;
|
|
16
|
+
/** Title template when `siteName` is set. `%s` is the page title. Default `'%s · <siteName>'`. */
|
|
17
|
+
titleTemplate?: string;
|
|
18
|
+
/** Absolute URL of the page, for `openGraph.url` when no canonical is set. */
|
|
19
|
+
url?: string;
|
|
20
|
+
};
|
|
21
|
+
export type MetaTagsOutput = {
|
|
22
|
+
title?: string;
|
|
23
|
+
titleTemplate?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
canonical?: string;
|
|
26
|
+
robots?: string;
|
|
27
|
+
openGraph?: {
|
|
28
|
+
title?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
siteName?: string;
|
|
32
|
+
images?: Array<{
|
|
33
|
+
url: string;
|
|
34
|
+
alt?: string;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
twitter?: {
|
|
38
|
+
cardType?: 'summary' | 'summary_large_image';
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
image?: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare const toMetaTags: (metadata: MetaTagsInput, options?: MetaTagsOptions) => MetaTagsOutput;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const str = (v) => (typeof v === 'string' && v !== '' ? v : undefined);
|
|
2
|
+
const imageUrl = (v) => {
|
|
3
|
+
if (typeof v === 'string')
|
|
4
|
+
return v ? { url: v } : undefined;
|
|
5
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
6
|
+
const r = v;
|
|
7
|
+
if (typeof r.url === 'string' && r.url) {
|
|
8
|
+
return typeof r.alt === 'string' && r.alt ? { url: r.url, alt: r.alt } : { url: r.url };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
13
|
+
export const toMetaTags = (metadata, options = {}) => {
|
|
14
|
+
const title = str(metadata.title);
|
|
15
|
+
const description = str(metadata.description);
|
|
16
|
+
const ogTitle = str(metadata.ogTitle) ?? title;
|
|
17
|
+
const ogDescription = str(metadata.ogDescription) ?? description;
|
|
18
|
+
const image = imageUrl(metadata.ogImage);
|
|
19
|
+
const canonical = str(metadata.canonical);
|
|
20
|
+
const card = str(metadata.twitterCard);
|
|
21
|
+
const out = {};
|
|
22
|
+
if (title)
|
|
23
|
+
out.title = title;
|
|
24
|
+
if (options.siteName)
|
|
25
|
+
out.titleTemplate = options.titleTemplate ?? `%s · ${options.siteName}`;
|
|
26
|
+
if (description)
|
|
27
|
+
out.description = description;
|
|
28
|
+
if (canonical)
|
|
29
|
+
out.canonical = canonical;
|
|
30
|
+
if (metadata.noindex === true)
|
|
31
|
+
out.robots = 'noindex, nofollow';
|
|
32
|
+
const og = {};
|
|
33
|
+
if (ogTitle)
|
|
34
|
+
og.title = ogTitle;
|
|
35
|
+
if (ogDescription)
|
|
36
|
+
og.description = ogDescription;
|
|
37
|
+
if (canonical ?? options.url)
|
|
38
|
+
og.url = canonical ?? options.url;
|
|
39
|
+
if (options.siteName)
|
|
40
|
+
og.siteName = options.siteName;
|
|
41
|
+
if (image)
|
|
42
|
+
og.images = [image];
|
|
43
|
+
if (Object.keys(og).length > 0)
|
|
44
|
+
out.openGraph = og;
|
|
45
|
+
const tw = {};
|
|
46
|
+
if (card === 'summary' || card === 'summary_large_image')
|
|
47
|
+
tw.cardType = card;
|
|
48
|
+
else if (image)
|
|
49
|
+
tw.cardType = 'summary_large_image';
|
|
50
|
+
if (ogTitle)
|
|
51
|
+
tw.title = ogTitle;
|
|
52
|
+
if (ogDescription)
|
|
53
|
+
tw.description = ogDescription;
|
|
54
|
+
if (image)
|
|
55
|
+
tw.image = image.url;
|
|
56
|
+
if (Object.keys(tw).length > 0)
|
|
57
|
+
out.twitter = tw;
|
|
58
|
+
return out;
|
|
59
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured values: the shapes behind the editor components (hours, team,
|
|
3
|
+
* testimonials, …) and the two rules every one of them follows.
|
|
4
|
+
*
|
|
5
|
+
* 1. **Versioned shapes, whole-object writes.** A structured value is a plain
|
|
6
|
+
* object with a `v` version number. Components never patch a leaf inside
|
|
7
|
+
* it; they read the whole value through the schema's `normalize`, change
|
|
8
|
+
* it, and write the whole object back. A field the editor clears is
|
|
9
|
+
* written as `null`, never dropped: the draft and release trees deep-merge
|
|
10
|
+
* over the published tree, and a missing key merges to "unchanged" while
|
|
11
|
+
* `null` merges to "cleared". That is what stops a cleared field from
|
|
12
|
+
* reverting to the template's demo copy.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Localisation is a string overlay, never a second structure.** The
|
|
15
|
+
* default locale stores the full value at `<name>`. Every other locale
|
|
16
|
+
* stores only strings, at `<name>.$t.<id>.<field>`, keyed by the stable
|
|
17
|
+
* `id` of the list item they translate (the reserved id `_` addresses the
|
|
18
|
+
* value's own root fields). Because objects deep-merge across locales,
|
|
19
|
+
* reading `es` yields the default structure plus the `es` overlay, and
|
|
20
|
+
* {@link applyTranslations} folds the strings in. Structure edits happen in
|
|
21
|
+
* the default locale only, so the item list can never fork between
|
|
22
|
+
* languages.
|
|
23
|
+
*/
|
|
24
|
+
import { type Tree } from './path.js';
|
|
25
|
+
/** Branch under a structured value that holds a locale's string overlay. */
|
|
26
|
+
export declare const TRANSLATIONS_KEY = "$t";
|
|
27
|
+
/** Overlay id that addresses the structured value's root fields. */
|
|
28
|
+
export declare const ROOT_ID = "_";
|
|
29
|
+
/** `{ [itemId]: { [field]: string } }` — what a non-default locale stores. */
|
|
30
|
+
export type TranslationOverlay = Record<string, Record<string, string>>;
|
|
31
|
+
export type StructuredItem = {
|
|
32
|
+
id: string;
|
|
33
|
+
} & Record<string, unknown>;
|
|
34
|
+
export type StructuredSchema<T extends Tree> = {
|
|
35
|
+
/** Exported component name, e.g. `'CmsHours'`. Recorded in the manifest. */
|
|
36
|
+
component: string;
|
|
37
|
+
/** Current shape version, written as `v` on every value. */
|
|
38
|
+
version: number;
|
|
39
|
+
/** Root-level fields holding translatable strings, e.g. `['note']`. */
|
|
40
|
+
translatable: readonly string[];
|
|
41
|
+
/** The item list, when the value has one, and the item fields that translate. */
|
|
42
|
+
items?: {
|
|
43
|
+
key: string;
|
|
44
|
+
translatable: readonly string[];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Turn anything the store may hold — an older version, a hand-seeded
|
|
48
|
+
* partial object, garbage — into the current shape. Must never throw.
|
|
49
|
+
*/
|
|
50
|
+
normalize: (raw: unknown) => T;
|
|
51
|
+
/** The cleared value: what an editor gets after "Clear". */
|
|
52
|
+
empty: () => T;
|
|
53
|
+
};
|
|
54
|
+
export type Structured<T extends Tree> = StructuredSchema<T> & {
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a stored value the way a display component reads it:
|
|
57
|
+
* `undefined` (nothing stored) → the fallback; `null` (cleared) → empty;
|
|
58
|
+
* anything else → normalized, with the locale overlay applied.
|
|
59
|
+
*/
|
|
60
|
+
read: (raw: unknown, fallback: T | undefined) => T;
|
|
61
|
+
};
|
|
62
|
+
export declare const isPlainObject: (v: unknown) => v is Tree;
|
|
63
|
+
export declare const defineStructured: <T extends Tree>(schema: StructuredSchema<T>) => Structured<T>;
|
|
64
|
+
/** Path of one translated string: `<name>.$t.<id>.<field>`. */
|
|
65
|
+
export declare const translationPath: (name: string, id: string, field: string) => string;
|
|
66
|
+
/** The `$t` branch of a stored value, when it is one. */
|
|
67
|
+
export declare const extractTranslations: (raw: unknown) => TranslationOverlay | undefined;
|
|
68
|
+
/** A copy of `raw` without its `$t` branch, so a whole-object write in the
|
|
69
|
+
* default locale never carries another locale's strings. */
|
|
70
|
+
export declare const stripTranslations: (raw: unknown) => unknown;
|
|
71
|
+
/**
|
|
72
|
+
* Fold a locale's string overlay into a normalized default-locale value.
|
|
73
|
+
* Root fields come from the `_` id; item fields from the item's own `id`.
|
|
74
|
+
* Only non-empty strings override, so an untranslated field shows the
|
|
75
|
+
* default-locale text rather than a blank.
|
|
76
|
+
*/
|
|
77
|
+
export declare const applyTranslations: <T extends Tree>(value: T, overlay: TranslationOverlay | undefined, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => T;
|
|
78
|
+
export type TranslatableField = {
|
|
79
|
+
/** Overlay id: an item id, or `_` for a root field. */
|
|
80
|
+
id: string;
|
|
81
|
+
field: string;
|
|
82
|
+
/** The default-locale string being translated. */
|
|
83
|
+
source: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Every translatable, non-empty string in a normalized default-locale value.
|
|
87
|
+
* Drives the popover's translation tab and the Locales panel's counts.
|
|
88
|
+
*/
|
|
89
|
+
export declare const translatableFields: <T extends Tree>(value: T, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => TranslatableField[];
|
|
90
|
+
/** How many translatable strings a locale's overlay still lacks. */
|
|
91
|
+
export declare const countTranslations: <T extends Tree>(value: T, overlay: TranslationOverlay | undefined, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => {
|
|
92
|
+
total: number;
|
|
93
|
+
missing: number;
|
|
94
|
+
};
|
|
95
|
+
export declare const asString: (v: unknown, fallback?: string) => string;
|
|
96
|
+
export declare const asNumber: (v: unknown, fallback?: number | null) => number | null;
|
|
97
|
+
export declare const asBoolean: (v: unknown, fallback?: boolean) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Normalize a list of items, guaranteeing every item carries a string `id`.
|
|
100
|
+
* An item without one (a hand-written seed, say) gets a deterministic
|
|
101
|
+
* `item-<index>` so reads stay stable until the editor next writes the list,
|
|
102
|
+
* at which point the ids are persisted.
|
|
103
|
+
*/
|
|
104
|
+
export declare const asItems: <I extends StructuredItem>(raw: unknown, normalizeItem: (item: Tree, index: number) => Omit<I, "id">) => I[];
|
|
105
|
+
/** A short random id for a newly added item. Stable once written. */
|
|
106
|
+
export declare const newItemId: () => string;
|
|
107
|
+
/** Read a structured value's overlay for one locale out of a whole scope tree. */
|
|
108
|
+
export declare const overlayAt: (tree: Tree | undefined, name: string) => TranslationOverlay | undefined;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured values: the shapes behind the editor components (hours, team,
|
|
3
|
+
* testimonials, …) and the two rules every one of them follows.
|
|
4
|
+
*
|
|
5
|
+
* 1. **Versioned shapes, whole-object writes.** A structured value is a plain
|
|
6
|
+
* object with a `v` version number. Components never patch a leaf inside
|
|
7
|
+
* it; they read the whole value through the schema's `normalize`, change
|
|
8
|
+
* it, and write the whole object back. A field the editor clears is
|
|
9
|
+
* written as `null`, never dropped: the draft and release trees deep-merge
|
|
10
|
+
* over the published tree, and a missing key merges to "unchanged" while
|
|
11
|
+
* `null` merges to "cleared". That is what stops a cleared field from
|
|
12
|
+
* reverting to the template's demo copy.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Localisation is a string overlay, never a second structure.** The
|
|
15
|
+
* default locale stores the full value at `<name>`. Every other locale
|
|
16
|
+
* stores only strings, at `<name>.$t.<id>.<field>`, keyed by the stable
|
|
17
|
+
* `id` of the list item they translate (the reserved id `_` addresses the
|
|
18
|
+
* value's own root fields). Because objects deep-merge across locales,
|
|
19
|
+
* reading `es` yields the default structure plus the `es` overlay, and
|
|
20
|
+
* {@link applyTranslations} folds the strings in. Structure edits happen in
|
|
21
|
+
* the default locale only, so the item list can never fork between
|
|
22
|
+
* languages.
|
|
23
|
+
*/
|
|
24
|
+
import { get } from './path.js';
|
|
25
|
+
/** Branch under a structured value that holds a locale's string overlay. */
|
|
26
|
+
export const TRANSLATIONS_KEY = '$t';
|
|
27
|
+
/** Overlay id that addresses the structured value's root fields. */
|
|
28
|
+
export const ROOT_ID = '_';
|
|
29
|
+
export const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
30
|
+
export const defineStructured = (schema) => ({
|
|
31
|
+
...schema,
|
|
32
|
+
read: (raw, fallback) => {
|
|
33
|
+
if (raw === undefined)
|
|
34
|
+
return fallback === undefined ? schema.empty() : fallback;
|
|
35
|
+
if (raw === null)
|
|
36
|
+
return schema.empty();
|
|
37
|
+
const overlay = extractTranslations(raw);
|
|
38
|
+
const value = schema.normalize(stripTranslations(raw));
|
|
39
|
+
return overlay ? applyTranslations(value, overlay, schema) : value;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
/** Path of one translated string: `<name>.$t.<id>.<field>`. */
|
|
43
|
+
export const translationPath = (name, id, field) => `${name}.${TRANSLATIONS_KEY}.${id}.${field}`;
|
|
44
|
+
/** The `$t` branch of a stored value, when it is one. */
|
|
45
|
+
export const extractTranslations = (raw) => {
|
|
46
|
+
if (!isPlainObject(raw))
|
|
47
|
+
return undefined;
|
|
48
|
+
const t = raw[TRANSLATIONS_KEY];
|
|
49
|
+
if (!isPlainObject(t))
|
|
50
|
+
return undefined;
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [id, fields] of Object.entries(t)) {
|
|
53
|
+
if (!isPlainObject(fields))
|
|
54
|
+
continue;
|
|
55
|
+
const strings = {};
|
|
56
|
+
for (const [field, v] of Object.entries(fields)) {
|
|
57
|
+
if (typeof v === 'string')
|
|
58
|
+
strings[field] = v;
|
|
59
|
+
}
|
|
60
|
+
out[id] = strings;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
/** A copy of `raw` without its `$t` branch, so a whole-object write in the
|
|
65
|
+
* default locale never carries another locale's strings. */
|
|
66
|
+
export const stripTranslations = (raw) => {
|
|
67
|
+
if (!isPlainObject(raw) || !(TRANSLATIONS_KEY in raw))
|
|
68
|
+
return raw;
|
|
69
|
+
const { [TRANSLATIONS_KEY]: _omit, ...rest } = raw;
|
|
70
|
+
return rest;
|
|
71
|
+
};
|
|
72
|
+
const overlayString = (overlay, id, field) => {
|
|
73
|
+
const v = overlay[id]?.[field];
|
|
74
|
+
return typeof v === 'string' && v !== '' ? v : undefined;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Fold a locale's string overlay into a normalized default-locale value.
|
|
78
|
+
* Root fields come from the `_` id; item fields from the item's own `id`.
|
|
79
|
+
* Only non-empty strings override, so an untranslated field shows the
|
|
80
|
+
* default-locale text rather than a blank.
|
|
81
|
+
*/
|
|
82
|
+
export const applyTranslations = (value, overlay, schema) => {
|
|
83
|
+
if (!overlay)
|
|
84
|
+
return value;
|
|
85
|
+
const out = { ...value };
|
|
86
|
+
for (const field of schema.translatable) {
|
|
87
|
+
const s = overlayString(overlay, ROOT_ID, field);
|
|
88
|
+
if (s !== undefined)
|
|
89
|
+
out[field] = s;
|
|
90
|
+
}
|
|
91
|
+
if (schema.items) {
|
|
92
|
+
const list = value[schema.items.key];
|
|
93
|
+
if (Array.isArray(list)) {
|
|
94
|
+
out[schema.items.key] = list.map((item) => {
|
|
95
|
+
if (!isPlainObject(item) || typeof item.id !== 'string')
|
|
96
|
+
return item;
|
|
97
|
+
const next = { ...item };
|
|
98
|
+
for (const field of schema.items.translatable) {
|
|
99
|
+
const s = overlayString(overlay, item.id, field);
|
|
100
|
+
if (s !== undefined)
|
|
101
|
+
next[field] = s;
|
|
102
|
+
}
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Every translatable, non-empty string in a normalized default-locale value.
|
|
111
|
+
* Drives the popover's translation tab and the Locales panel's counts.
|
|
112
|
+
*/
|
|
113
|
+
export const translatableFields = (value, schema) => {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const field of schema.translatable) {
|
|
116
|
+
const s = value[field];
|
|
117
|
+
if (typeof s === 'string' && s !== '')
|
|
118
|
+
out.push({ id: ROOT_ID, field, source: s });
|
|
119
|
+
}
|
|
120
|
+
if (schema.items) {
|
|
121
|
+
const list = value[schema.items.key];
|
|
122
|
+
if (Array.isArray(list)) {
|
|
123
|
+
for (const item of list) {
|
|
124
|
+
if (!isPlainObject(item) || typeof item.id !== 'string')
|
|
125
|
+
continue;
|
|
126
|
+
for (const field of schema.items.translatable) {
|
|
127
|
+
const s = item[field];
|
|
128
|
+
if (typeof s === 'string' && s !== '')
|
|
129
|
+
out.push({ id: item.id, field, source: s });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
};
|
|
136
|
+
/** How many translatable strings a locale's overlay still lacks. */
|
|
137
|
+
export const countTranslations = (value, overlay, schema) => {
|
|
138
|
+
const fields = translatableFields(value, schema);
|
|
139
|
+
let missing = 0;
|
|
140
|
+
for (const f of fields) {
|
|
141
|
+
if (!overlay || overlayString(overlay, f.id, f.field) === undefined)
|
|
142
|
+
missing += 1;
|
|
143
|
+
}
|
|
144
|
+
return { total: fields.length, missing };
|
|
145
|
+
};
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Normalizer building blocks
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
export const asString = (v, fallback = '') => typeof v === 'string' ? v : fallback;
|
|
150
|
+
export const asNumber = (v, fallback = null) => typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
151
|
+
export const asBoolean = (v, fallback = false) => typeof v === 'boolean' ? v : fallback;
|
|
152
|
+
/**
|
|
153
|
+
* Normalize a list of items, guaranteeing every item carries a string `id`.
|
|
154
|
+
* An item without one (a hand-written seed, say) gets a deterministic
|
|
155
|
+
* `item-<index>` so reads stay stable until the editor next writes the list,
|
|
156
|
+
* at which point the ids are persisted.
|
|
157
|
+
*/
|
|
158
|
+
export const asItems = (raw, normalizeItem) => {
|
|
159
|
+
if (!Array.isArray(raw))
|
|
160
|
+
return [];
|
|
161
|
+
const out = [];
|
|
162
|
+
raw.forEach((entry, index) => {
|
|
163
|
+
if (!isPlainObject(entry))
|
|
164
|
+
return;
|
|
165
|
+
const id = typeof entry.id === 'string' && entry.id !== '' ? entry.id : `item-${index}`;
|
|
166
|
+
out.push({ id, ...normalizeItem(entry, index) });
|
|
167
|
+
});
|
|
168
|
+
return out;
|
|
169
|
+
};
|
|
170
|
+
/** A short random id for a newly added item. Stable once written. */
|
|
171
|
+
export const newItemId = () => {
|
|
172
|
+
const bytes = new Uint8Array(6);
|
|
173
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues)
|
|
174
|
+
crypto.getRandomValues(bytes);
|
|
175
|
+
else
|
|
176
|
+
for (let i = 0; i < bytes.length; i++)
|
|
177
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
178
|
+
let out = '';
|
|
179
|
+
for (const b of bytes)
|
|
180
|
+
out += b.toString(16).padStart(2, '0');
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
183
|
+
/** Read a structured value's overlay for one locale out of a whole scope tree. */
|
|
184
|
+
export const overlayAt = (tree, name) => extractTranslations(get(tree, name));
|
package/dist/core/wire.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `published_at` stay snake_case because that is what the JSON carries.
|
|
12
12
|
*/
|
|
13
13
|
import type { Tree } from './path.js';
|
|
14
|
-
import type { PageDeleteOutcome } from './page-entry.js';
|
|
14
|
+
import type { PageDeleteOutcome, PageEntry } from './page-entry.js';
|
|
15
15
|
export type { PageDeleteOutcome };
|
|
16
16
|
/** One pending change in a release, without the bookkeeping the store adds. */
|
|
17
17
|
export type ReleaseItemCore = {
|
|
@@ -182,6 +182,36 @@ export type CmsDeployState = {
|
|
|
182
182
|
};
|
|
183
183
|
latest?: CmsDeployRun | null;
|
|
184
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* A project's published content in one object: `layouts` and `pages` keyed
|
|
187
|
+
* `[locale][routeId]`, plus the non-localised `site` tree. `POST /seed`
|
|
188
|
+
* accepts it and `GET /export` returns it, so a template's published
|
|
189
|
+
* `content/` can seed a fresh project and a project can be dumped back into
|
|
190
|
+
* the same shape. A page keyed by route id may be a bare tree (a static page,
|
|
191
|
+
* `params: {}`) or a full `PageEntry[]`.
|
|
192
|
+
*/
|
|
193
|
+
export type CmsSeed = {
|
|
194
|
+
layouts?: Record<string, Record<string, Tree>>;
|
|
195
|
+
pages?: Record<string, Record<string, Tree | PageEntry[]>>;
|
|
196
|
+
site?: Tree;
|
|
197
|
+
};
|
|
198
|
+
export type SeedRequest = CmsSeed & {
|
|
199
|
+
/** Overwrite a project that already has published rows. */
|
|
200
|
+
force?: boolean;
|
|
201
|
+
};
|
|
202
|
+
export type SeedSummary = {
|
|
203
|
+
locales: string[];
|
|
204
|
+
layouts: number;
|
|
205
|
+
pages: number;
|
|
206
|
+
site: boolean;
|
|
207
|
+
};
|
|
208
|
+
export type SeedResponse = {
|
|
209
|
+
ok: true;
|
|
210
|
+
seeded: SeedSummary;
|
|
211
|
+
} | {
|
|
212
|
+
ok: false;
|
|
213
|
+
reason: 'already-seeded';
|
|
214
|
+
};
|
|
185
215
|
export type DocsResponse = {
|
|
186
216
|
contents: Tree;
|
|
187
217
|
} | PageDeleteOutcome;
|