@glw907/cairn-cms 0.62.2 → 0.76.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 +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +179 -2
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +10 -5
- package/dist/delivery/public-routes.js +25 -2
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +18 -10
- package/dist/index.js +9 -5
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/guard.js +10 -0
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +36 -4
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- package/src/lib/content/validate.ts +0 -90
|
@@ -1,23 +1,41 @@
|
|
|
1
|
-
// cairn-cms: concept normalization (seam 1). The adapter declares concepts as
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
|
|
1
|
+
// cairn-cms: concept normalization (seam 1). The adapter declares concepts as an open `content`
|
|
2
|
+
// record; this turns each declared key into a uniform descriptor (id, label, directory, declared
|
|
3
|
+
// routing, fields, validator) the admin reads. A new concept attaches by adding one key under
|
|
4
|
+
// `content` and declaring its own routing and URL policy, with no reshape here.
|
|
5
|
+
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, NamedField, RoutingRule } from './types.js';
|
|
6
|
+
import type { Fieldset } from './fieldset.js';
|
|
8
7
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
/** Re-attach each fieldset record key to its descriptor as `name`, the normalized `NamedField[]`. */
|
|
9
|
+
function namedFields(schema: Fieldset): NamedField[] {
|
|
10
|
+
return Object.entries(schema.fields).map(([name, descriptor]) => ({ name, ...descriptor }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The named routing shorthands, each expanding to a concrete rule. */
|
|
14
|
+
const ROUTING_SHORTHANDS: Readonly<Record<'feed' | 'page' | 'embedded', RoutingRule>> = {
|
|
15
|
+
feed: { routable: true, dated: true, inFeeds: true },
|
|
16
|
+
page: { routable: true, dated: false, inFeeds: false },
|
|
17
|
+
embedded: { routable: false, dated: false, inFeeds: false },
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/** Expand a concept's routing shorthand to a concrete rule. The single resolution point: omitted is `page`. */
|
|
21
|
+
export function resolveRouting(routing: ConceptConfig['routing']): RoutingRule {
|
|
22
|
+
if (routing === undefined) return ROUTING_SHORTHANDS.page;
|
|
23
|
+
return typeof routing === 'string' ? ROUTING_SHORTHANDS[routing] : routing;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Declare a concept while preserving its fieldset type for typed reads, and validate its URL policy at
|
|
28
|
+
* declaration so a bad permalink or datePrefix fails at module load rather than at a defaulted render.
|
|
29
|
+
* Mirrors {@link defineAdapter}; the validation is the build-independent net for a concept with no entries.
|
|
30
|
+
*/
|
|
31
|
+
export function defineConcept<const C extends ConceptConfig>(concept: C): C {
|
|
32
|
+
validateUrlPolicy(
|
|
33
|
+
concept.label ?? concept.dir,
|
|
34
|
+
{ permalink: concept.permalink, datePrefix: concept.datePrefix },
|
|
35
|
+
resolveRouting(concept.routing).dated,
|
|
36
|
+
);
|
|
37
|
+
return concept;
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
/** Title-case a concept id for the default sidebar label, e.g. "posts" to "Posts". */
|
|
23
41
|
function defaultLabel(id: string): string {
|
|
@@ -41,7 +59,7 @@ const DATE_PREFIXES = new Set<string>(['year', 'month', 'day']);
|
|
|
41
59
|
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
42
60
|
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
43
61
|
*/
|
|
44
|
-
function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void {
|
|
62
|
+
export function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void {
|
|
45
63
|
if (policy.permalink !== undefined) {
|
|
46
64
|
const pattern = policy.permalink;
|
|
47
65
|
if (!pattern.startsWith('/')) {
|
|
@@ -67,38 +85,48 @@ function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean)
|
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
/**
|
|
70
|
-
* Normalize an adapter's declared concepts into uniform descriptors (seam 1).
|
|
71
|
-
* (
|
|
72
|
-
*
|
|
73
|
-
* otherwise; `datePrefix` defaults to `day`).
|
|
74
|
-
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
88
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each concept declares its
|
|
89
|
+
* own routing (a shorthand or an explicit rule, resolved by `resolveRouting`) and URL policy
|
|
90
|
+
* (`permalink`, `datePrefix`) on the config; both default when omitted (`/:slug` for Pages, `/<id>/:slug`
|
|
91
|
+
* otherwise; `datePrefix` defaults to `day`). A new concept attaches by adding one key under `content`.
|
|
75
92
|
*/
|
|
76
93
|
export function normalizeConcepts(
|
|
77
94
|
content: Record<string, ConceptConfig | undefined>,
|
|
78
|
-
urlPolicy: Record<string, ConceptUrlPolicy | undefined> = {},
|
|
79
|
-
routing: Readonly<Record<string, RoutingRule>> = CONCEPT_ROUTING,
|
|
80
95
|
): ConceptDescriptor[] {
|
|
81
96
|
const descriptors: ConceptDescriptor[] = [];
|
|
82
97
|
const declaredConcepts = new Set(
|
|
83
98
|
Object.keys(content).filter((key) => content[key] !== undefined),
|
|
84
99
|
);
|
|
85
|
-
for (const key of Object.keys(urlPolicy)) {
|
|
86
|
-
if (!declaredConcepts.has(key)) {
|
|
87
|
-
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
100
|
for (const [id, config] of Object.entries(content)) {
|
|
91
101
|
if (!config) continue;
|
|
102
|
+
const fs = config.fields;
|
|
92
103
|
const summaryFields = config.summaryFields ?? [];
|
|
93
|
-
const declared = new Set(
|
|
104
|
+
const declared = new Set(Object.keys(fs.fields));
|
|
94
105
|
const undeclared = summaryFields.find((key) => !declared.has(key));
|
|
95
106
|
if (undeclared !== undefined) {
|
|
96
107
|
throw new Error(
|
|
97
108
|
`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`,
|
|
98
109
|
);
|
|
99
110
|
}
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
// A reference (or array of reference) field names the concept it targets. Validate that concept at
|
|
112
|
+
// declaration, so a typo fails loudly here rather than at the build's verifyReferences gate (or, in
|
|
113
|
+
// the editor picker, as a silently empty target list). The check is the field descriptor's concept
|
|
114
|
+
// against the declared content keys.
|
|
115
|
+
for (const [name, descriptor] of Object.entries(fs.fields)) {
|
|
116
|
+
const targetConcept =
|
|
117
|
+
descriptor.type === 'reference'
|
|
118
|
+
? descriptor.concept
|
|
119
|
+
: descriptor.type === 'array' && descriptor.item.type === 'reference'
|
|
120
|
+
? descriptor.item.concept
|
|
121
|
+
: undefined;
|
|
122
|
+
if (targetConcept !== undefined && !declaredConcepts.has(targetConcept)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`cairn: concept "${id}" reference field "${name}" names concept "${targetConcept}", which is not declared under content`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const conceptRouting = resolveRouting(config.routing);
|
|
129
|
+
const policy: ConceptUrlPolicy = { permalink: config.permalink, datePrefix: config.datePrefix };
|
|
102
130
|
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
103
131
|
const label = config.label ?? defaultLabel(id);
|
|
104
132
|
descriptors.push({
|
|
@@ -109,24 +137,24 @@ export function normalizeConcepts(
|
|
|
109
137
|
routing: conceptRouting,
|
|
110
138
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
111
139
|
datePrefix: policy.datePrefix ?? 'day',
|
|
112
|
-
fields:
|
|
140
|
+
fields: namedFields(fs),
|
|
141
|
+
schema: fs,
|
|
113
142
|
summaryFields,
|
|
114
|
-
validate:
|
|
143
|
+
validate: fs.validate,
|
|
115
144
|
});
|
|
116
145
|
}
|
|
117
146
|
return descriptors;
|
|
118
147
|
}
|
|
119
148
|
|
|
120
149
|
/**
|
|
121
|
-
* Resolve a site's concept descriptors from its content map
|
|
122
|
-
*
|
|
123
|
-
*
|
|
150
|
+
* Resolve a site's concept descriptors from its content map. The admin runtime (composeRuntime) and the
|
|
151
|
+
* delivery layer (siteDescriptors) both call this, so the per-concept routing and URL policy are derived
|
|
152
|
+
* once from the concept declarations and the runtime and delivery permalinks cannot diverge.
|
|
124
153
|
*/
|
|
125
154
|
export function resolveConcepts(
|
|
126
155
|
content: Record<string, ConceptConfig | undefined>,
|
|
127
|
-
siteConfig: SiteConfig,
|
|
128
156
|
): ConceptDescriptor[] {
|
|
129
|
-
return normalizeConcepts(content
|
|
157
|
+
return normalizeConcepts(content);
|
|
130
158
|
}
|
|
131
159
|
|
|
132
160
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// cairn-cms: the shared field constraint rules the `fieldset` validator calls. They live apart from
|
|
2
|
+
// the validator as pure helpers, so the constraint wording and the first-failing-rule-wins order are
|
|
3
|
+
// stated once. No I/O and no clock reads, so the rules stay deterministic on Workers.
|
|
4
|
+
|
|
5
|
+
/** Compile a field pattern once, throwing a labeled error when the source is not a valid regex. */
|
|
6
|
+
export function compilePattern(source: string, label: string): RegExp {
|
|
7
|
+
try {
|
|
8
|
+
return new RegExp(source);
|
|
9
|
+
} catch (cause) {
|
|
10
|
+
throw new Error(`cairn: field "${label}" has an invalid pattern: ${source}`, { cause });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Return the first string-length violation message, or null when the value satisfies the bounds. */
|
|
15
|
+
export function stringLengthError(
|
|
16
|
+
value: string,
|
|
17
|
+
constraints: { min?: number; max?: number; length?: number },
|
|
18
|
+
label: string,
|
|
19
|
+
): string | null {
|
|
20
|
+
const { min, max, length } = constraints;
|
|
21
|
+
if (min != null && value.length < min) return `${label} must be at least ${min} characters`;
|
|
22
|
+
if (max != null && value.length > max) return `${label} must be at most ${max} characters`;
|
|
23
|
+
if (length != null && value.length !== length) return `${label} must be exactly ${length} characters`;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Return the format violation message when a compiled pattern rejects the value, else null. */
|
|
28
|
+
export function patternError(value: string, compiled: RegExp | undefined, label: string): string | null {
|
|
29
|
+
if (compiled && !compiled.test(value)) return `${label} is not in the expected format`;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Return the first date-bounds violation message, or null when the value is within the bounds. */
|
|
34
|
+
export function dateBoundsError(value: string, constraints: { min?: string; max?: string }, label: string): string | null {
|
|
35
|
+
const { min, max } = constraints;
|
|
36
|
+
if (min != null && value < min) return `${label} must be on or after ${min}`;
|
|
37
|
+
if (max != null && value > max) return `${label} must be on or before ${max}`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** The stored value of an image field; re-exported so this module owns the image shape too. */
|
|
2
|
+
export type { ImageValue } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Common to every field descriptor: the form label and the universal options. */
|
|
5
|
+
export interface FieldBase {
|
|
6
|
+
/** Form label. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** One author-facing sentence shown under the field. */
|
|
9
|
+
help?: string;
|
|
10
|
+
/** A required field fails validation when empty. */
|
|
11
|
+
required?: boolean;
|
|
12
|
+
/** Form-render-time initial value; a sentinel like "today" resolves at render (Task 9). */
|
|
13
|
+
default?: string | boolean;
|
|
14
|
+
}
|
|
15
|
+
/** A single-line text input. */
|
|
16
|
+
export interface TextField extends FieldBase {
|
|
17
|
+
type: 'text';
|
|
18
|
+
min?: number; max?: number; length?: number;
|
|
19
|
+
/** A regular-expression source string the value must match. */
|
|
20
|
+
pattern?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A multi-line text input. */
|
|
23
|
+
export interface TextareaField extends FieldBase {
|
|
24
|
+
type: 'textarea';
|
|
25
|
+
rows?: number; min?: number; max?: number; length?: number; pattern?: string;
|
|
26
|
+
}
|
|
27
|
+
/** A numeric input. */
|
|
28
|
+
export interface NumberField extends FieldBase {
|
|
29
|
+
type: 'number';
|
|
30
|
+
min?: number; max?: number;
|
|
31
|
+
/** Constrain the value to whole numbers. */
|
|
32
|
+
integer?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** A single-choice input over a closed option list. */
|
|
35
|
+
export interface SelectField extends FieldBase {
|
|
36
|
+
type: 'select';
|
|
37
|
+
/** The closed set of allowed values. */
|
|
38
|
+
options: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** A multiple-choice input. */
|
|
41
|
+
export interface MultiselectField extends FieldBase {
|
|
42
|
+
type: 'multiselect';
|
|
43
|
+
/** The allowed values; omitted leaves the set open. */
|
|
44
|
+
options?: readonly string[];
|
|
45
|
+
/** Allow the author to add values not in the list. */
|
|
46
|
+
creatable?: boolean;
|
|
47
|
+
/** Placeholder text for the open/creatable comma-separated input (freetags parity). */
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Mark the field as a site-wide taxonomy whose values pool across entries. */
|
|
50
|
+
taxonomy?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** A URL input whose format the validator enforces. */
|
|
53
|
+
export interface UrlField extends FieldBase {
|
|
54
|
+
type: 'url';
|
|
55
|
+
}
|
|
56
|
+
/** An email-address input whose format the validator enforces. */
|
|
57
|
+
export interface EmailField extends FieldBase {
|
|
58
|
+
type: 'email';
|
|
59
|
+
}
|
|
60
|
+
/** A calendar-date input. */
|
|
61
|
+
export interface DateField extends FieldBase {
|
|
62
|
+
type: 'date';
|
|
63
|
+
/** Earliest allowed date as YYYY-MM-DD. */
|
|
64
|
+
min?: string;
|
|
65
|
+
/** Latest allowed date as YYYY-MM-DD. */
|
|
66
|
+
max?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A date-and-time input. */
|
|
69
|
+
export interface DatetimeField extends FieldBase {
|
|
70
|
+
type: 'datetime';
|
|
71
|
+
/** Earliest allowed moment as an ISO string. */
|
|
72
|
+
min?: string;
|
|
73
|
+
/** Latest allowed moment as an ISO string. */
|
|
74
|
+
max?: string;
|
|
75
|
+
}
|
|
76
|
+
/** A checkbox; absent means false. */
|
|
77
|
+
export interface BooleanField extends FieldBase {
|
|
78
|
+
type: 'boolean';
|
|
79
|
+
}
|
|
80
|
+
/** A glyph chosen from the adapter's icon set; the stored value is the glyph's name. */
|
|
81
|
+
export interface IconField extends FieldBase {
|
|
82
|
+
type: 'icon';
|
|
83
|
+
}
|
|
84
|
+
/** A hero image whose stored value is the nested ImageValue object. */
|
|
85
|
+
export interface ImageField extends FieldBase {
|
|
86
|
+
type: 'image';
|
|
87
|
+
/** Whether this field feeds the social-card image. */
|
|
88
|
+
seo?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** A group of leaf fields, stored as a nested object. Holds only leaves (no nested container). */
|
|
91
|
+
export interface ObjectField extends Omit<FieldBase, 'label'> {
|
|
92
|
+
type: 'object';
|
|
93
|
+
/**
|
|
94
|
+
* Optional group label. An object inside an array is labeled by the array (and summarized per row by
|
|
95
|
+
* itemLabel), so it may omit this; a top-level object supplies it for the group legend.
|
|
96
|
+
*/
|
|
97
|
+
label?: string;
|
|
98
|
+
/** The leaf fields this group holds, keyed by frontmatter sub-key. */
|
|
99
|
+
fields: Record<string, FieldDescriptor>;
|
|
100
|
+
}
|
|
101
|
+
/** A single edge to one entry of a named concept, stored as that target's permanent id. */
|
|
102
|
+
export interface ReferenceField extends FieldBase {
|
|
103
|
+
type: 'reference';
|
|
104
|
+
/** The concept whose entries this field references. */
|
|
105
|
+
concept: string;
|
|
106
|
+
}
|
|
107
|
+
/** A repeatable field whose stored value is a list of its item's values. */
|
|
108
|
+
export interface ArrayField extends FieldBase {
|
|
109
|
+
type: 'array';
|
|
110
|
+
/** The descriptor each list element conforms to: a leaf, or a flat object of leaves. */
|
|
111
|
+
item: FieldDescriptor;
|
|
112
|
+
/** A label for one row, shown beside the add and remove controls. */
|
|
113
|
+
itemLabel?: string;
|
|
114
|
+
}
|
|
115
|
+
/** The plain-data descriptor union the form, validator, and inference all read. Grows per task. */
|
|
116
|
+
export type FieldDescriptor =
|
|
117
|
+
| TextField
|
|
118
|
+
| TextareaField
|
|
119
|
+
| NumberField
|
|
120
|
+
| SelectField
|
|
121
|
+
| MultiselectField
|
|
122
|
+
| UrlField
|
|
123
|
+
| EmailField
|
|
124
|
+
| DateField
|
|
125
|
+
| DatetimeField
|
|
126
|
+
| BooleanField
|
|
127
|
+
| IconField
|
|
128
|
+
| ImageField
|
|
129
|
+
| ObjectField
|
|
130
|
+
| ReferenceField
|
|
131
|
+
| ArrayField;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The constructor namespace a concept declares its fields with. Each constructor captures its
|
|
135
|
+
* argument with a `const` type parameter and intersects it onto the descriptor, so the call-site
|
|
136
|
+
* literals (`required: true`, a `select`/`multiselect` `options` union) survive into the descriptor
|
|
137
|
+
* type for `Infer` to read. The runtime value is unchanged: still `{ type, ...o }`.
|
|
138
|
+
*/
|
|
139
|
+
export const fields = {
|
|
140
|
+
/** A single-line text field. */
|
|
141
|
+
text: <const O extends Omit<TextField, 'type'>>(o: O): TextField & O => ({ type: 'text', ...o }),
|
|
142
|
+
/** A multi-line text field. */
|
|
143
|
+
textarea: <const O extends Omit<TextareaField, 'type'>>(o: O): TextareaField & O => ({ type: 'textarea', ...o }),
|
|
144
|
+
/** A numeric field. */
|
|
145
|
+
number: <const O extends Omit<NumberField, 'type'>>(o: O): NumberField & O => ({ type: 'number', ...o }),
|
|
146
|
+
/** A single-choice field over a closed option list, preserving the literal option union. */
|
|
147
|
+
select: <const O extends Omit<SelectField, 'type'>>(o: O): SelectField & O => ({ type: 'select', ...o }),
|
|
148
|
+
/** A multiple-choice field, preserving the literal option union when one is given. */
|
|
149
|
+
multiselect: <const O extends Omit<MultiselectField, 'type'>>(o: O): MultiselectField & O => ({ type: 'multiselect', ...o }),
|
|
150
|
+
/** A URL field. */
|
|
151
|
+
url: <const O extends Omit<UrlField, 'type'>>(o: O): UrlField & O => ({ type: 'url', ...o }),
|
|
152
|
+
/** An email-address field. */
|
|
153
|
+
email: <const O extends Omit<EmailField, 'type'>>(o: O): EmailField & O => ({ type: 'email', ...o }),
|
|
154
|
+
/** A calendar-date field. */
|
|
155
|
+
date: <const O extends Omit<DateField, 'type'>>(o: O): DateField & O => ({ type: 'date', ...o }),
|
|
156
|
+
/** A date-and-time field. */
|
|
157
|
+
datetime: <const O extends Omit<DatetimeField, 'type'>>(o: O): DatetimeField & O => ({ type: 'datetime', ...o }),
|
|
158
|
+
/** A boolean checkbox field. */
|
|
159
|
+
boolean: <const O extends Omit<BooleanField, 'type'>>(o: O): BooleanField & O => ({ type: 'boolean', ...o }),
|
|
160
|
+
/** An icon field whose value is a glyph name from the adapter's icon set. */
|
|
161
|
+
icon: <const O extends Omit<IconField, 'type'>>(o: O): IconField & O => ({ type: 'icon', ...o }),
|
|
162
|
+
/** An image field whose value is the nested ImageValue object. */
|
|
163
|
+
image: <const O extends Omit<ImageField, 'type'>>(o: O): ImageField & O => ({ type: 'image', ...o }),
|
|
164
|
+
/** A group of leaf fields, preserving each leaf's type for inference. Label is optional (the array labels a row group). */
|
|
165
|
+
object: <const F extends Record<string, FieldDescriptor>, const O extends Omit<ObjectField, 'type' | 'fields'>>(
|
|
166
|
+
o: { fields: F } & O,
|
|
167
|
+
): ObjectField & { fields: F } & O => ({ type: 'object', ...o }),
|
|
168
|
+
/** A single reference field storing one target entry's permanent id. */
|
|
169
|
+
reference: <const O extends Omit<ReferenceField, 'type'>>(o: O): ReferenceField & O => ({ type: 'reference', ...o }),
|
|
170
|
+
/**
|
|
171
|
+
* A repeatable field over one item descriptor, preserving the item type for inference. The item is
|
|
172
|
+
* a leaf, or a flat object of leaves; `fieldset` rejects deeper nesting at declaration.
|
|
173
|
+
*/
|
|
174
|
+
array: <const I extends FieldDescriptor, const O extends Omit<ArrayField, 'type' | 'item'>>(
|
|
175
|
+
item: I,
|
|
176
|
+
o?: O,
|
|
177
|
+
): ArrayField & { item: I } & O => ({ type: 'array', item, ...(o as O) }),
|
|
178
|
+
};
|