@glw907/cairn-cms 0.68.0 → 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 +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- 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 +4 -0
- 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.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- 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 +2 -5
- package/dist/delivery/public-routes.js +15 -1
- 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 +16 -12
- package/dist/index.js +7 -8
- 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/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/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -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/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 +5 -1
- package/src/lib/ambient.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 +9 -8
- 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 +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- 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 +17 -3
- 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 +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -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/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- 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 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
2
|
-
|
|
2
|
+
/** Expand a concept's routing shorthand to a concrete rule. The single resolution point: omitted is `page`. */
|
|
3
|
+
export declare function resolveRouting(routing: ConceptConfig['routing']): RoutingRule;
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Declare a concept while preserving its fieldset type for typed reads, and validate its URL policy at
|
|
6
|
+
* declaration so a bad permalink or datePrefix fails at module load rather than at a defaulted render.
|
|
7
|
+
* Mirrors {@link defineAdapter}; the validation is the build-independent net for a concept with no entries.
|
|
7
8
|
*/
|
|
8
|
-
export declare const
|
|
9
|
+
export declare function defineConcept<const C extends ConceptConfig>(concept: C): C;
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
|
|
14
|
-
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
11
|
+
* Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
|
|
12
|
+
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
13
|
+
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
15
14
|
*/
|
|
16
|
-
export declare function
|
|
15
|
+
export declare function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void;
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each concept declares its
|
|
18
|
+
* own routing (a shorthand or an explicit rule, resolved by `resolveRouting`) and URL policy
|
|
19
|
+
* (`permalink`, `datePrefix`) on the config; both default when omitted (`/:slug` for Pages, `/<id>/:slug`
|
|
20
|
+
* otherwise; `datePrefix` defaults to `day`). A new concept attaches by adding one key under `content`.
|
|
21
21
|
*/
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>): ConceptDescriptor[];
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a site's concept descriptors from its content map. The admin runtime (composeRuntime) and the
|
|
25
|
+
* delivery layer (siteDescriptors) both call this, so the per-concept routing and URL policy are derived
|
|
26
|
+
* once from the concept declarations and the runtime and delivery permalinks cannot diverge.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>): ConceptDescriptor[];
|
|
23
29
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
24
30
|
export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
|
package/dist/content/concepts.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/** Re-attach each fieldset record key to its descriptor as `name`, the normalized `NamedField[]`. */
|
|
2
|
+
function namedFields(schema) {
|
|
3
|
+
return Object.entries(schema.fields).map(([name, descriptor]) => ({ name, ...descriptor }));
|
|
4
|
+
}
|
|
5
|
+
/** The named routing shorthands, each expanding to a concrete rule. */
|
|
6
|
+
const ROUTING_SHORTHANDS = {
|
|
7
|
+
feed: { routable: true, dated: true, inFeeds: true },
|
|
8
|
+
page: { routable: true, dated: false, inFeeds: false },
|
|
9
|
+
embedded: { routable: false, dated: false, inFeeds: false },
|
|
10
|
+
};
|
|
11
|
+
/** Expand a concept's routing shorthand to a concrete rule. The single resolution point: omitted is `page`. */
|
|
12
|
+
export function resolveRouting(routing) {
|
|
13
|
+
if (routing === undefined)
|
|
14
|
+
return ROUTING_SHORTHANDS.page;
|
|
15
|
+
return typeof routing === 'string' ? ROUTING_SHORTHANDS[routing] : routing;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
18
|
+
* Declare a concept while preserving its fieldset type for typed reads, and validate its URL policy at
|
|
19
|
+
* declaration so a bad permalink or datePrefix fails at module load rather than at a defaulted render.
|
|
20
|
+
* Mirrors {@link defineAdapter}; the validation is the build-independent net for a concept with no entries.
|
|
6
21
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
/** Routing for a concept with no table entry: a plain, non-feed, routable page. */
|
|
12
|
-
const DEFAULT_ROUTING = { routable: true, dated: false, inFeeds: false };
|
|
22
|
+
export function defineConcept(concept) {
|
|
23
|
+
validateUrlPolicy(concept.label ?? concept.dir, { permalink: concept.permalink, datePrefix: concept.datePrefix }, resolveRouting(concept.routing).dated);
|
|
24
|
+
return concept;
|
|
25
|
+
}
|
|
13
26
|
/** Title-case a concept id for the default sidebar label, e.g. "posts" to "Posts". */
|
|
14
27
|
function defaultLabel(id) {
|
|
15
28
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
@@ -29,7 +42,7 @@ const DATE_PREFIXES = new Set(['year', 'month', 'day']);
|
|
|
29
42
|
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
30
43
|
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
31
44
|
*/
|
|
32
|
-
function validateUrlPolicy(id, policy, dated) {
|
|
45
|
+
export function validateUrlPolicy(id, policy, dated) {
|
|
33
46
|
if (policy.permalink !== undefined) {
|
|
34
47
|
const pattern = policy.permalink;
|
|
35
48
|
if (!pattern.startsWith('/')) {
|
|
@@ -50,31 +63,40 @@ function validateUrlPolicy(id, policy, dated) {
|
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
/**
|
|
53
|
-
* Normalize an adapter's declared concepts into uniform descriptors (seam 1).
|
|
54
|
-
* (
|
|
55
|
-
*
|
|
56
|
-
* otherwise; `datePrefix` defaults to `day`).
|
|
57
|
-
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
66
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each concept declares its
|
|
67
|
+
* own routing (a shorthand or an explicit rule, resolved by `resolveRouting`) and URL policy
|
|
68
|
+
* (`permalink`, `datePrefix`) on the config; both default when omitted (`/:slug` for Pages, `/<id>/:slug`
|
|
69
|
+
* otherwise; `datePrefix` defaults to `day`). A new concept attaches by adding one key under `content`.
|
|
58
70
|
*/
|
|
59
|
-
export function normalizeConcepts(content
|
|
71
|
+
export function normalizeConcepts(content) {
|
|
60
72
|
const descriptors = [];
|
|
61
73
|
const declaredConcepts = new Set(Object.keys(content).filter((key) => content[key] !== undefined));
|
|
62
|
-
for (const key of Object.keys(urlPolicy)) {
|
|
63
|
-
if (!declaredConcepts.has(key)) {
|
|
64
|
-
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
74
|
for (const [id, config] of Object.entries(content)) {
|
|
68
75
|
if (!config)
|
|
69
76
|
continue;
|
|
77
|
+
const fs = config.fields;
|
|
70
78
|
const summaryFields = config.summaryFields ?? [];
|
|
71
|
-
const declared = new Set(
|
|
79
|
+
const declared = new Set(Object.keys(fs.fields));
|
|
72
80
|
const undeclared = summaryFields.find((key) => !declared.has(key));
|
|
73
81
|
if (undeclared !== undefined) {
|
|
74
82
|
throw new Error(`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`);
|
|
75
83
|
}
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
// A reference (or array of reference) field names the concept it targets. Validate that concept at
|
|
85
|
+
// declaration, so a typo fails loudly here rather than at the build's verifyReferences gate (or, in
|
|
86
|
+
// the editor picker, as a silently empty target list). The check is the field descriptor's concept
|
|
87
|
+
// against the declared content keys.
|
|
88
|
+
for (const [name, descriptor] of Object.entries(fs.fields)) {
|
|
89
|
+
const targetConcept = descriptor.type === 'reference'
|
|
90
|
+
? descriptor.concept
|
|
91
|
+
: descriptor.type === 'array' && descriptor.item.type === 'reference'
|
|
92
|
+
? descriptor.item.concept
|
|
93
|
+
: undefined;
|
|
94
|
+
if (targetConcept !== undefined && !declaredConcepts.has(targetConcept)) {
|
|
95
|
+
throw new Error(`cairn: concept "${id}" reference field "${name}" names concept "${targetConcept}", which is not declared under content`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const conceptRouting = resolveRouting(config.routing);
|
|
99
|
+
const policy = { permalink: config.permalink, datePrefix: config.datePrefix };
|
|
78
100
|
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
79
101
|
const label = config.label ?? defaultLabel(id);
|
|
80
102
|
descriptors.push({
|
|
@@ -85,20 +107,21 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
|
|
|
85
107
|
routing: conceptRouting,
|
|
86
108
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
87
109
|
datePrefix: policy.datePrefix ?? 'day',
|
|
88
|
-
fields:
|
|
110
|
+
fields: namedFields(fs),
|
|
111
|
+
schema: fs,
|
|
89
112
|
summaryFields,
|
|
90
|
-
validate:
|
|
113
|
+
validate: fs.validate,
|
|
91
114
|
});
|
|
92
115
|
}
|
|
93
116
|
return descriptors;
|
|
94
117
|
}
|
|
95
118
|
/**
|
|
96
|
-
* Resolve a site's concept descriptors from its content map
|
|
97
|
-
*
|
|
98
|
-
*
|
|
119
|
+
* Resolve a site's concept descriptors from its content map. The admin runtime (composeRuntime) and the
|
|
120
|
+
* delivery layer (siteDescriptors) both call this, so the per-concept routing and URL policy are derived
|
|
121
|
+
* once from the concept declarations and the runtime and delivery permalinks cannot diverge.
|
|
99
122
|
*/
|
|
100
|
-
export function resolveConcepts(content
|
|
101
|
-
return normalizeConcepts(content
|
|
123
|
+
export function resolveConcepts(content) {
|
|
124
|
+
return normalizeConcepts(content);
|
|
102
125
|
}
|
|
103
126
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
104
127
|
export function findConcept(concepts, id) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// cairn-cms: the shared field constraint rules
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// rules stay deterministic on Workers.
|
|
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.
|
|
5
4
|
/** Compile a field pattern once, throwing a labeled error when the source is not a valid regex. */
|
|
6
5
|
export function compilePattern(source, label) {
|
|
7
6
|
try {
|
package/dist/content/fields.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface MultiselectField extends FieldBase {
|
|
|
50
50
|
options?: readonly string[];
|
|
51
51
|
/** Allow the author to add values not in the list. */
|
|
52
52
|
creatable?: boolean;
|
|
53
|
+
/** Placeholder text for the open/creatable comma-separated input (freetags parity). */
|
|
54
|
+
placeholder?: string;
|
|
53
55
|
/** Mark the field as a site-wide taxonomy whose values pool across entries. */
|
|
54
56
|
taxonomy?: boolean;
|
|
55
57
|
}
|
|
@@ -81,14 +83,43 @@ export interface DatetimeField extends FieldBase {
|
|
|
81
83
|
export interface BooleanField extends FieldBase {
|
|
82
84
|
type: 'boolean';
|
|
83
85
|
}
|
|
86
|
+
/** A glyph chosen from the adapter's icon set; the stored value is the glyph's name. */
|
|
87
|
+
export interface IconField extends FieldBase {
|
|
88
|
+
type: 'icon';
|
|
89
|
+
}
|
|
84
90
|
/** A hero image whose stored value is the nested ImageValue object. */
|
|
85
91
|
export interface ImageField extends FieldBase {
|
|
86
92
|
type: 'image';
|
|
87
93
|
/** Whether this field feeds the social-card image. */
|
|
88
94
|
seo?: boolean;
|
|
89
95
|
}
|
|
96
|
+
/** A group of leaf fields, stored as a nested object. Holds only leaves (no nested container). */
|
|
97
|
+
export interface ObjectField extends Omit<FieldBase, 'label'> {
|
|
98
|
+
type: 'object';
|
|
99
|
+
/**
|
|
100
|
+
* Optional group label. An object inside an array is labeled by the array (and summarized per row by
|
|
101
|
+
* itemLabel), so it may omit this; a top-level object supplies it for the group legend.
|
|
102
|
+
*/
|
|
103
|
+
label?: string;
|
|
104
|
+
/** The leaf fields this group holds, keyed by frontmatter sub-key. */
|
|
105
|
+
fields: Record<string, FieldDescriptor>;
|
|
106
|
+
}
|
|
107
|
+
/** A single edge to one entry of a named concept, stored as that target's permanent id. */
|
|
108
|
+
export interface ReferenceField extends FieldBase {
|
|
109
|
+
type: 'reference';
|
|
110
|
+
/** The concept whose entries this field references. */
|
|
111
|
+
concept: string;
|
|
112
|
+
}
|
|
113
|
+
/** A repeatable field whose stored value is a list of its item's values. */
|
|
114
|
+
export interface ArrayField extends FieldBase {
|
|
115
|
+
type: 'array';
|
|
116
|
+
/** The descriptor each list element conforms to: a leaf, or a flat object of leaves. */
|
|
117
|
+
item: FieldDescriptor;
|
|
118
|
+
/** A label for one row, shown beside the add and remove controls. */
|
|
119
|
+
itemLabel?: string;
|
|
120
|
+
}
|
|
90
121
|
/** The plain-data descriptor union the form, validator, and inference all read. Grows per task. */
|
|
91
|
-
export type FieldDescriptor = TextField | TextareaField | NumberField | SelectField | MultiselectField | UrlField | EmailField | DateField | DatetimeField | BooleanField | ImageField;
|
|
122
|
+
export type FieldDescriptor = TextField | TextareaField | NumberField | SelectField | MultiselectField | UrlField | EmailField | DateField | DatetimeField | BooleanField | IconField | ImageField | ObjectField | ReferenceField | ArrayField;
|
|
92
123
|
/**
|
|
93
124
|
* The constructor namespace a concept declares its fields with. Each constructor captures its
|
|
94
125
|
* argument with a `const` type parameter and intersects it onto the descriptor, so the call-site
|
|
@@ -116,6 +147,23 @@ export declare const fields: {
|
|
|
116
147
|
datetime: <const O extends Omit<DatetimeField, "type">>(o: O) => DatetimeField & O;
|
|
117
148
|
/** A boolean checkbox field. */
|
|
118
149
|
boolean: <const O extends Omit<BooleanField, "type">>(o: O) => BooleanField & O;
|
|
150
|
+
/** An icon field whose value is a glyph name from the adapter's icon set. */
|
|
151
|
+
icon: <const O extends Omit<IconField, "type">>(o: O) => IconField & O;
|
|
119
152
|
/** An image field whose value is the nested ImageValue object. */
|
|
120
153
|
image: <const O extends Omit<ImageField, "type">>(o: O) => ImageField & O;
|
|
154
|
+
/** A group of leaf fields, preserving each leaf's type for inference. Label is optional (the array labels a row group). */
|
|
155
|
+
object: <const F extends Record<string, FieldDescriptor>, const O extends Omit<ObjectField, "type" | "fields">>(o: {
|
|
156
|
+
fields: F;
|
|
157
|
+
} & O) => ObjectField & {
|
|
158
|
+
fields: F;
|
|
159
|
+
} & O;
|
|
160
|
+
/** A single reference field storing one target entry's permanent id. */
|
|
161
|
+
reference: <const O extends Omit<ReferenceField, "type">>(o: O) => ReferenceField & O;
|
|
162
|
+
/**
|
|
163
|
+
* A repeatable field over one item descriptor, preserving the item type for inference. The item is
|
|
164
|
+
* a leaf, or a flat object of leaves; `fieldset` rejects deeper nesting at declaration.
|
|
165
|
+
*/
|
|
166
|
+
array: <const I extends FieldDescriptor, const O extends Omit<ArrayField, "type" | "item">>(item: I, o?: O) => ArrayField & {
|
|
167
|
+
item: I;
|
|
168
|
+
} & O;
|
|
121
169
|
};
|
package/dist/content/fields.js
CHANGED
|
@@ -25,6 +25,17 @@ export const fields = {
|
|
|
25
25
|
datetime: (o) => ({ type: 'datetime', ...o }),
|
|
26
26
|
/** A boolean checkbox field. */
|
|
27
27
|
boolean: (o) => ({ type: 'boolean', ...o }),
|
|
28
|
+
/** An icon field whose value is a glyph name from the adapter's icon set. */
|
|
29
|
+
icon: (o) => ({ type: 'icon', ...o }),
|
|
28
30
|
/** An image field whose value is the nested ImageValue object. */
|
|
29
31
|
image: (o) => ({ type: 'image', ...o }),
|
|
32
|
+
/** A group of leaf fields, preserving each leaf's type for inference. Label is optional (the array labels a row group). */
|
|
33
|
+
object: (o) => ({ type: 'object', ...o }),
|
|
34
|
+
/** A single reference field storing one target entry's permanent id. */
|
|
35
|
+
reference: (o) => ({ type: 'reference', ...o }),
|
|
36
|
+
/**
|
|
37
|
+
* A repeatable field over one item descriptor, preserving the item type for inference. The item is
|
|
38
|
+
* a leaf, or a flat object of leaves; `fieldset` rejects deeper nesting at declaration.
|
|
39
|
+
*/
|
|
40
|
+
array: (item, o) => ({ type: 'array', item, ...o }),
|
|
30
41
|
};
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { FieldDescriptor, ImageValue } from './fields.js';
|
|
2
2
|
import type { ValidationResult } from './types.js';
|
|
3
|
-
import type { StandardInput, StandardSchemaV1 } from './schema.js';
|
|
3
|
+
import type { StandardInput, StandardSchemaV1 } from './standard-schema.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Function-valued behavior a field descriptor cannot carry as plain data, keyed by field name. A
|
|
6
|
+
* `validate` runs cross-field after per-field coercion; an `itemLabel` derives an array row's label.
|
|
7
|
+
* Resident in the app bundle, never in the `load` payload.
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export interface FieldBehavior {
|
|
10
|
+
/** A cross-field validator: returns an error string, or null when valid. `siblings` is the raw input record. */
|
|
11
|
+
validate?(value: unknown, siblings: Record<string, unknown>): string | null;
|
|
12
|
+
/** Derive an array row's label from its item value and zero-based index. */
|
|
13
|
+
itemLabel?(item: Record<string, unknown>, index: number): string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The behavior table co-bundled with a fieldset, keyed by field name. Empty for a behavior-free fieldset. */
|
|
16
|
+
export type BehaviorTable = Record<string, FieldBehavior>;
|
|
10
17
|
/**
|
|
11
18
|
* Options for `fieldset`. `refine` runs after the per-field coercion and constraints pass, for
|
|
12
19
|
* cross-field and body-dependent checks. It is validation-only: it returns field-keyed errors to
|
|
@@ -14,6 +21,8 @@ export type BehaviorTable = Record<string, never>;
|
|
|
14
21
|
*/
|
|
15
22
|
export interface FieldsetOptions {
|
|
16
23
|
refine?: (data: Record<string, unknown>, body: string) => Record<string, string> | undefined;
|
|
24
|
+
/** Function-valued per-field behavior, keyed by field name. Each key must name a declared field. */
|
|
25
|
+
behavior?: BehaviorTable;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* A concept's fieldset: the plain-data descriptors, the co-bundled behavior table, the server-derived
|
|
@@ -42,6 +51,12 @@ type ValueOf<D extends FieldDescriptor> = D extends {
|
|
|
42
51
|
} ? boolean : D extends {
|
|
43
52
|
type: 'image';
|
|
44
53
|
} ? ImageValue : D extends {
|
|
54
|
+
type: 'object';
|
|
55
|
+
fields: infer F extends Record<string, FieldDescriptor>;
|
|
56
|
+
} ? InferRecord<F> : D extends {
|
|
57
|
+
type: 'array';
|
|
58
|
+
item: infer I extends FieldDescriptor;
|
|
59
|
+
} ? ValueOf<I>[] : D extends {
|
|
45
60
|
type: 'select';
|
|
46
61
|
options: readonly (infer O extends string)[];
|
|
47
62
|
} ? O : D extends {
|
|
@@ -54,21 +69,27 @@ type ValueOf<D extends FieldDescriptor> = D extends {
|
|
|
54
69
|
type Prettify<T> = {
|
|
55
70
|
[K in keyof T]: T[K];
|
|
56
71
|
} & {};
|
|
72
|
+
/** Drop an index signature so a captured literal record infers its own keys only, not `[x: string]`. */
|
|
73
|
+
type RemoveIndex<T> = {
|
|
74
|
+
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
|
|
75
|
+
};
|
|
57
76
|
/**
|
|
58
77
|
* The normalized frontmatter type inferred from a fieldset's descriptor record. A descriptor
|
|
59
|
-
* declared `required: true` is a required key; every other descriptor is optional.
|
|
78
|
+
* declared `required: true` is a required key; every other descriptor is optional. The captured
|
|
79
|
+
* literal record carries an index signature (the constructor's `Record<string, FieldDescriptor>`
|
|
80
|
+
* intersected with the literal), so strip it first or every nested key would also infer `[x: string]`.
|
|
60
81
|
*/
|
|
61
|
-
type
|
|
82
|
+
type InferRecord<RR extends Record<string, FieldDescriptor>, R = RemoveIndex<RR>> = Prettify<{
|
|
62
83
|
-readonly [K in keyof R as R[K] extends {
|
|
63
84
|
required: true;
|
|
64
|
-
} ? K : never]: ValueOf<R[K]>;
|
|
85
|
+
} ? K : never]: ValueOf<R[K] extends FieldDescriptor ? R[K] : never>;
|
|
65
86
|
} & {
|
|
66
87
|
-readonly [K in keyof R as R[K] extends {
|
|
67
88
|
required: true;
|
|
68
|
-
} ? never : K]?: ValueOf<R[K]>;
|
|
89
|
+
} ? never : K]?: ValueOf<R[K] extends FieldDescriptor ? R[K] : never>;
|
|
69
90
|
}>;
|
|
70
91
|
/** Extract the inferred frontmatter type from a `Fieldset`. */
|
|
71
|
-
export type InferFieldset<S> = S extends Fieldset<infer R> ?
|
|
92
|
+
export type InferFieldset<S> = S extends Fieldset<infer R> ? InferRecord<R> : never;
|
|
72
93
|
/**
|
|
73
94
|
* Build a fieldset from a key-to-descriptor record. The returned schema carries the descriptors, a
|
|
74
95
|
* server-derived validator that coerces per type and returns field-keyed errors or normalized data,
|