@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.
Files changed (196) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/auth/types.d.ts +7 -0
  4. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  5. package/dist/components/ComponentForm.svelte +44 -27
  6. package/dist/components/ComponentInsertDialog.svelte +22 -11
  7. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  8. package/dist/components/ConceptList.svelte +25 -4
  9. package/dist/components/EditPage.svelte +29 -107
  10. package/dist/components/EditPage.svelte.d.ts +2 -7
  11. package/dist/components/EntryPicker.svelte +117 -0
  12. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  13. package/dist/components/FieldInput.svelte +218 -0
  14. package/dist/components/FieldInput.svelte.d.ts +51 -0
  15. package/dist/components/IconPicker.svelte +2 -2
  16. package/dist/components/IconPicker.svelte.d.ts +2 -0
  17. package/dist/components/LinkPicker.svelte +8 -75
  18. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  19. package/dist/components/MediaHeroField.svelte +8 -5
  20. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  21. package/dist/components/ObjectGroupField.svelte +54 -0
  22. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  23. package/dist/components/ReferenceField.svelte +94 -0
  24. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  25. package/dist/components/RepeatableField.svelte +221 -0
  26. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  27. package/dist/components/cairn-admin.css +179 -2
  28. package/dist/components/preview-doc.js +5 -1
  29. package/dist/components/tidy-validate.js +1 -1
  30. package/dist/content/adapter.js +18 -0
  31. package/dist/content/advisories.d.ts +2 -2
  32. package/dist/content/advisories.js +3 -5
  33. package/dist/content/compose.d.ts +7 -6
  34. package/dist/content/compose.js +26 -20
  35. package/dist/content/concepts.d.ts +21 -15
  36. package/dist/content/concepts.js +55 -32
  37. package/dist/content/field-rules.d.ts +15 -0
  38. package/dist/content/field-rules.js +38 -0
  39. package/dist/content/fields.d.ts +169 -0
  40. package/dist/content/fields.js +41 -0
  41. package/dist/content/fieldset.d.ts +107 -0
  42. package/dist/content/fieldset.js +386 -0
  43. package/dist/content/frontmatter-region.d.ts +38 -0
  44. package/dist/content/frontmatter-region.js +75 -0
  45. package/dist/content/frontmatter.d.ts +35 -2
  46. package/dist/content/frontmatter.js +232 -11
  47. package/dist/content/manifest.d.ts +34 -0
  48. package/dist/content/manifest.js +80 -4
  49. package/dist/content/media-refs.d.ts +2 -2
  50. package/dist/content/media-rewrite.js +1 -69
  51. package/dist/content/reference-index.d.ts +56 -0
  52. package/dist/content/reference-index.js +95 -0
  53. package/dist/content/references.d.ts +40 -0
  54. package/dist/content/references.js +0 -0
  55. package/dist/content/standard-schema.d.ts +30 -0
  56. package/dist/content/standard-schema.js +4 -0
  57. package/dist/content/types.d.ts +127 -178
  58. package/dist/delivery/data.d.ts +2 -2
  59. package/dist/delivery/data.js +1 -1
  60. package/dist/delivery/public-routes.d.ts +10 -5
  61. package/dist/delivery/public-routes.js +25 -2
  62. package/dist/delivery/site-descriptors.d.ts +5 -1
  63. package/dist/delivery/site-descriptors.js +8 -3
  64. package/dist/delivery/site-indexes.d.ts +2 -2
  65. package/dist/delivery/site-resolver.d.ts +25 -0
  66. package/dist/delivery/site-resolver.js +49 -0
  67. package/dist/doctor/checks-local.js +6 -11
  68. package/dist/github/backend.d.ts +83 -0
  69. package/dist/github/backend.js +76 -0
  70. package/dist/github/credentials.d.ts +11 -5
  71. package/dist/github/credentials.js +3 -3
  72. package/dist/github/repo.d.ts +8 -19
  73. package/dist/github/repo.js +69 -80
  74. package/dist/github/types.d.ts +1 -1
  75. package/dist/github/types.js +4 -4
  76. package/dist/index.d.ts +18 -10
  77. package/dist/index.js +9 -5
  78. package/dist/islands/index.d.ts +12 -0
  79. package/dist/islands/index.js +83 -0
  80. package/dist/islands/types.d.ts +7 -0
  81. package/dist/islands/types.js +1 -0
  82. package/dist/log/events.d.ts +1 -1
  83. package/dist/media/index.d.ts +1 -1
  84. package/dist/media/index.js +1 -1
  85. package/dist/media/manifest.d.ts +11 -0
  86. package/dist/media/manifest.js +13 -0
  87. package/dist/media/rewrite-plan.d.ts +2 -3
  88. package/dist/media/rewrite-plan.js +2 -3
  89. package/dist/media/usage.d.ts +2 -2
  90. package/dist/media/usage.js +3 -5
  91. package/dist/nav/site-config.d.ts +0 -6
  92. package/dist/nav/site-config.js +6 -4
  93. package/dist/render/component-grammar.js +11 -11
  94. package/dist/render/component-reference.js +5 -3
  95. package/dist/render/component-validate.d.ts +4 -1
  96. package/dist/render/component-validate.js +10 -35
  97. package/dist/render/highlight.d.ts +9 -0
  98. package/dist/render/highlight.js +206 -0
  99. package/dist/render/pipeline.d.ts +0 -6
  100. package/dist/render/pipeline.js +13 -2
  101. package/dist/render/registry.d.ts +44 -36
  102. package/dist/render/registry.js +47 -6
  103. package/dist/render/rehype-dispatch.d.ts +6 -10
  104. package/dist/render/rehype-dispatch.js +38 -17
  105. package/dist/render/remark-directives.js +4 -5
  106. package/dist/render/sanitize-schema.d.ts +10 -0
  107. package/dist/render/sanitize-schema.js +30 -1
  108. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  109. package/dist/sveltekit/cairn-admin.js +3 -4
  110. package/dist/sveltekit/content-routes.d.ts +10 -8
  111. package/dist/sveltekit/content-routes.js +269 -181
  112. package/dist/sveltekit/guard.js +10 -0
  113. package/dist/sveltekit/health.d.ts +7 -3
  114. package/dist/sveltekit/health.js +9 -3
  115. package/dist/sveltekit/index.d.ts +1 -1
  116. package/dist/sveltekit/nav-routes.d.ts +6 -5
  117. package/dist/sveltekit/nav-routes.js +22 -20
  118. package/dist/sveltekit/types.d.ts +2 -0
  119. package/dist/vite/index.d.ts +3 -3
  120. package/dist/vite/index.js +17 -8
  121. package/package.json +17 -2
  122. package/src/lib/ambient.ts +7 -0
  123. package/src/lib/auth/types.ts +7 -0
  124. package/src/lib/components/CairnAdmin.svelte +2 -6
  125. package/src/lib/components/ComponentForm.svelte +48 -27
  126. package/src/lib/components/ComponentInsertDialog.svelte +26 -14
  127. package/src/lib/components/ConceptList.svelte +41 -4
  128. package/src/lib/components/EditPage.svelte +43 -119
  129. package/src/lib/components/EntryPicker.svelte +154 -0
  130. package/src/lib/components/FieldInput.svelte +262 -0
  131. package/src/lib/components/IconPicker.svelte +4 -2
  132. package/src/lib/components/LinkPicker.svelte +10 -81
  133. package/src/lib/components/MediaHeroField.svelte +12 -5
  134. package/src/lib/components/ObjectGroupField.svelte +97 -0
  135. package/src/lib/components/ReferenceField.svelte +126 -0
  136. package/src/lib/components/RepeatableField.svelte +310 -0
  137. package/src/lib/components/preview-doc.ts +5 -1
  138. package/src/lib/components/tidy-validate.ts +1 -1
  139. package/src/lib/content/adapter.ts +21 -0
  140. package/src/lib/content/advisories.ts +4 -7
  141. package/src/lib/content/compose.ts +30 -23
  142. package/src/lib/content/concepts.ts +68 -40
  143. package/src/lib/content/field-rules.ts +39 -0
  144. package/src/lib/content/fields.ts +178 -0
  145. package/src/lib/content/fieldset.ts +470 -0
  146. package/src/lib/content/frontmatter-region.ts +90 -0
  147. package/src/lib/content/frontmatter.ts +231 -15
  148. package/src/lib/content/manifest.ts +101 -4
  149. package/src/lib/content/media-refs.ts +2 -2
  150. package/src/lib/content/media-rewrite.ts +7 -80
  151. package/src/lib/content/reference-index.ts +159 -0
  152. package/src/lib/content/references.ts +0 -0
  153. package/src/lib/content/standard-schema.ts +25 -0
  154. package/src/lib/content/types.ts +128 -195
  155. package/src/lib/delivery/data.ts +2 -2
  156. package/src/lib/delivery/public-routes.ts +36 -4
  157. package/src/lib/delivery/site-descriptors.ts +8 -3
  158. package/src/lib/delivery/site-indexes.ts +2 -2
  159. package/src/lib/delivery/site-resolver.ts +64 -0
  160. package/src/lib/doctor/checks-local.ts +6 -14
  161. package/src/lib/github/backend.ts +161 -0
  162. package/src/lib/github/credentials.ts +10 -7
  163. package/src/lib/github/repo.ts +79 -83
  164. package/src/lib/github/types.ts +5 -5
  165. package/src/lib/index.ts +40 -18
  166. package/src/lib/islands/index.ts +84 -0
  167. package/src/lib/islands/types.ts +11 -0
  168. package/src/lib/log/events.ts +1 -0
  169. package/src/lib/media/index.ts +1 -0
  170. package/src/lib/media/manifest.ts +14 -0
  171. package/src/lib/media/rewrite-plan.ts +4 -6
  172. package/src/lib/media/usage.ts +4 -7
  173. package/src/lib/nav/site-config.ts +8 -9
  174. package/src/lib/render/component-grammar.ts +10 -10
  175. package/src/lib/render/component-reference.ts +4 -3
  176. package/src/lib/render/component-validate.ts +10 -35
  177. package/src/lib/render/highlight.ts +259 -0
  178. package/src/lib/render/pipeline.ts +13 -8
  179. package/src/lib/render/registry.ts +88 -42
  180. package/src/lib/render/rehype-dispatch.ts +47 -16
  181. package/src/lib/render/remark-directives.ts +4 -5
  182. package/src/lib/render/sanitize-schema.ts +32 -1
  183. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  184. package/src/lib/sveltekit/content-routes.ts +330 -221
  185. package/src/lib/sveltekit/guard.ts +15 -0
  186. package/src/lib/sveltekit/health.ts +13 -6
  187. package/src/lib/sveltekit/index.ts +2 -2
  188. package/src/lib/sveltekit/nav-routes.ts +33 -29
  189. package/src/lib/sveltekit/types.ts +5 -1
  190. package/src/lib/vite/index.ts +20 -11
  191. package/dist/content/schema.d.ts +0 -87
  192. package/dist/content/schema.js +0 -89
  193. package/dist/content/validate.d.ts +0 -17
  194. package/dist/content/validate.js +0 -93
  195. package/src/lib/content/schema.ts +0 -167
  196. 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
- // `content: { posts?, pages? }`; this turns each declared key into a uniform descriptor
3
- // (id, label, directory, concept-fixed routing, fields, validator) the admin reads. A
4
- // future Fragments concept attaches by adding one key under `content` and one routing
5
- // entry, with no reshape here.
6
- import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
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
- * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
11
- * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
12
- * entry here and one key under `content`.
13
- */
14
- export const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>> = {
15
- posts: { routable: true, dated: true, inFeeds: true },
16
- pages: { routable: true, dated: false, inFeeds: false },
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
- /** Routing for a concept with no table entry: a plain, non-feed, routable page. */
20
- const DEFAULT_ROUTING: RoutingRule = { routable: true, dated: false, inFeeds: false };
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). URL policy
71
- * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
72
- * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
73
- * otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
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(config.schema.fields.map((field) => field.name));
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
- const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
101
- const policy = urlPolicy[id] ?? {};
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: config.schema.fields,
140
+ fields: namedFields(fs),
141
+ schema: fs,
113
142
  summaryFields,
114
- validate: config.schema.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 and parsed site config. The admin runtime
122
- * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
123
- * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
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, urlPolicyFrom(siteConfig));
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
+ };