@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,15 +1,28 @@
1
- import { urlPolicyFrom } from '../nav/site-config.js';
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
- * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
4
- * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
5
- * entry here and one key under `content`.
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 const CONCEPT_ROUTING = {
8
- posts: { routable: true, dated: true, inFeeds: true },
9
- pages: { routable: true, dated: false, inFeeds: false },
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). URL policy
54
- * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
55
- * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
56
- * otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
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, urlPolicy = {}, routing = CONCEPT_ROUTING) {
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(config.schema.fields.map((field) => field.name));
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
- const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
77
- const policy = urlPolicy[id] ?? {};
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: config.schema.fields,
110
+ fields: namedFields(fs),
111
+ schema: fs,
89
112
  summaryFields,
90
- validate: config.schema.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 and parsed site config. The admin runtime
97
- * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
98
- * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
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, siteConfig) {
101
- return normalizeConcepts(content, urlPolicyFrom(siteConfig));
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) {
@@ -0,0 +1,15 @@
1
+ /** Compile a field pattern once, throwing a labeled error when the source is not a valid regex. */
2
+ export declare function compilePattern(source: string, label: string): RegExp;
3
+ /** Return the first string-length violation message, or null when the value satisfies the bounds. */
4
+ export declare function stringLengthError(value: string, constraints: {
5
+ min?: number;
6
+ max?: number;
7
+ length?: number;
8
+ }, label: string): string | null;
9
+ /** Return the format violation message when a compiled pattern rejects the value, else null. */
10
+ export declare function patternError(value: string, compiled: RegExp | undefined, label: string): string | null;
11
+ /** Return the first date-bounds violation message, or null when the value is within the bounds. */
12
+ export declare function dateBoundsError(value: string, constraints: {
13
+ min?: string;
14
+ max?: string;
15
+ }, label: string): string | null;
@@ -0,0 +1,38 @@
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
+ /** Compile a field pattern once, throwing a labeled error when the source is not a valid regex. */
5
+ export function compilePattern(source, label) {
6
+ try {
7
+ return new RegExp(source);
8
+ }
9
+ catch (cause) {
10
+ throw new Error(`cairn: field "${label}" has an invalid pattern: ${source}`, { cause });
11
+ }
12
+ }
13
+ /** Return the first string-length violation message, or null when the value satisfies the bounds. */
14
+ export function stringLengthError(value, constraints, label) {
15
+ const { min, max, length } = constraints;
16
+ if (min != null && value.length < min)
17
+ return `${label} must be at least ${min} characters`;
18
+ if (max != null && value.length > max)
19
+ return `${label} must be at most ${max} characters`;
20
+ if (length != null && value.length !== length)
21
+ return `${label} must be exactly ${length} characters`;
22
+ return null;
23
+ }
24
+ /** Return the format violation message when a compiled pattern rejects the value, else null. */
25
+ export function patternError(value, compiled, label) {
26
+ if (compiled && !compiled.test(value))
27
+ return `${label} is not in the expected format`;
28
+ return null;
29
+ }
30
+ /** Return the first date-bounds violation message, or null when the value is within the bounds. */
31
+ export function dateBoundsError(value, constraints, label) {
32
+ const { min, max } = constraints;
33
+ if (min != null && value < min)
34
+ return `${label} must be on or after ${min}`;
35
+ if (max != null && value > max)
36
+ return `${label} must be on or before ${max}`;
37
+ return null;
38
+ }
@@ -0,0 +1,169 @@
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
+ /** Common to every field descriptor: the form label and the universal options. */
4
+ export interface FieldBase {
5
+ /** Form label. */
6
+ label: string;
7
+ /** One author-facing sentence shown under the field. */
8
+ help?: string;
9
+ /** A required field fails validation when empty. */
10
+ required?: boolean;
11
+ /** Form-render-time initial value; a sentinel like "today" resolves at render (Task 9). */
12
+ default?: string | boolean;
13
+ }
14
+ /** A single-line text input. */
15
+ export interface TextField extends FieldBase {
16
+ type: 'text';
17
+ min?: number;
18
+ max?: number;
19
+ length?: number;
20
+ /** A regular-expression source string the value must match. */
21
+ pattern?: string;
22
+ }
23
+ /** A multi-line text input. */
24
+ export interface TextareaField extends FieldBase {
25
+ type: 'textarea';
26
+ rows?: number;
27
+ min?: number;
28
+ max?: number;
29
+ length?: number;
30
+ pattern?: string;
31
+ }
32
+ /** A numeric input. */
33
+ export interface NumberField extends FieldBase {
34
+ type: 'number';
35
+ min?: number;
36
+ max?: number;
37
+ /** Constrain the value to whole numbers. */
38
+ integer?: boolean;
39
+ }
40
+ /** A single-choice input over a closed option list. */
41
+ export interface SelectField extends FieldBase {
42
+ type: 'select';
43
+ /** The closed set of allowed values. */
44
+ options: readonly string[];
45
+ }
46
+ /** A multiple-choice input. */
47
+ export interface MultiselectField extends FieldBase {
48
+ type: 'multiselect';
49
+ /** The allowed values; omitted leaves the set open. */
50
+ options?: readonly string[];
51
+ /** Allow the author to add values not in the list. */
52
+ creatable?: boolean;
53
+ /** Placeholder text for the open/creatable comma-separated input (freetags parity). */
54
+ placeholder?: string;
55
+ /** Mark the field as a site-wide taxonomy whose values pool across entries. */
56
+ taxonomy?: boolean;
57
+ }
58
+ /** A URL input whose format the validator enforces. */
59
+ export interface UrlField extends FieldBase {
60
+ type: 'url';
61
+ }
62
+ /** An email-address input whose format the validator enforces. */
63
+ export interface EmailField extends FieldBase {
64
+ type: 'email';
65
+ }
66
+ /** A calendar-date input. */
67
+ export interface DateField extends FieldBase {
68
+ type: 'date';
69
+ /** Earliest allowed date as YYYY-MM-DD. */
70
+ min?: string;
71
+ /** Latest allowed date as YYYY-MM-DD. */
72
+ max?: string;
73
+ }
74
+ /** A date-and-time input. */
75
+ export interface DatetimeField extends FieldBase {
76
+ type: 'datetime';
77
+ /** Earliest allowed moment as an ISO string. */
78
+ min?: string;
79
+ /** Latest allowed moment as an ISO string. */
80
+ max?: string;
81
+ }
82
+ /** A checkbox; absent means false. */
83
+ export interface BooleanField extends FieldBase {
84
+ type: 'boolean';
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
+ }
90
+ /** A hero image whose stored value is the nested ImageValue object. */
91
+ export interface ImageField extends FieldBase {
92
+ type: 'image';
93
+ /** Whether this field feeds the social-card image. */
94
+ seo?: boolean;
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
+ }
121
+ /** The plain-data descriptor union the form, validator, and inference all read. Grows per task. */
122
+ export type FieldDescriptor = TextField | TextareaField | NumberField | SelectField | MultiselectField | UrlField | EmailField | DateField | DatetimeField | BooleanField | IconField | ImageField | ObjectField | ReferenceField | ArrayField;
123
+ /**
124
+ * The constructor namespace a concept declares its fields with. Each constructor captures its
125
+ * argument with a `const` type parameter and intersects it onto the descriptor, so the call-site
126
+ * literals (`required: true`, a `select`/`multiselect` `options` union) survive into the descriptor
127
+ * type for `Infer` to read. The runtime value is unchanged: still `{ type, ...o }`.
128
+ */
129
+ export declare const fields: {
130
+ /** A single-line text field. */
131
+ text: <const O extends Omit<TextField, "type">>(o: O) => TextField & O;
132
+ /** A multi-line text field. */
133
+ textarea: <const O extends Omit<TextareaField, "type">>(o: O) => TextareaField & O;
134
+ /** A numeric field. */
135
+ number: <const O extends Omit<NumberField, "type">>(o: O) => NumberField & O;
136
+ /** A single-choice field over a closed option list, preserving the literal option union. */
137
+ select: <const O extends Omit<SelectField, "type">>(o: O) => SelectField & O;
138
+ /** A multiple-choice field, preserving the literal option union when one is given. */
139
+ multiselect: <const O extends Omit<MultiselectField, "type">>(o: O) => MultiselectField & O;
140
+ /** A URL field. */
141
+ url: <const O extends Omit<UrlField, "type">>(o: O) => UrlField & O;
142
+ /** An email-address field. */
143
+ email: <const O extends Omit<EmailField, "type">>(o: O) => EmailField & O;
144
+ /** A calendar-date field. */
145
+ date: <const O extends Omit<DateField, "type">>(o: O) => DateField & O;
146
+ /** A date-and-time field. */
147
+ datetime: <const O extends Omit<DatetimeField, "type">>(o: O) => DatetimeField & O;
148
+ /** A boolean checkbox field. */
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;
152
+ /** An image field whose value is the nested ImageValue object. */
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;
169
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The constructor namespace a concept declares its fields with. Each constructor captures its
3
+ * argument with a `const` type parameter and intersects it onto the descriptor, so the call-site
4
+ * literals (`required: true`, a `select`/`multiselect` `options` union) survive into the descriptor
5
+ * type for `Infer` to read. The runtime value is unchanged: still `{ type, ...o }`.
6
+ */
7
+ export const fields = {
8
+ /** A single-line text field. */
9
+ text: (o) => ({ type: 'text', ...o }),
10
+ /** A multi-line text field. */
11
+ textarea: (o) => ({ type: 'textarea', ...o }),
12
+ /** A numeric field. */
13
+ number: (o) => ({ type: 'number', ...o }),
14
+ /** A single-choice field over a closed option list, preserving the literal option union. */
15
+ select: (o) => ({ type: 'select', ...o }),
16
+ /** A multiple-choice field, preserving the literal option union when one is given. */
17
+ multiselect: (o) => ({ type: 'multiselect', ...o }),
18
+ /** A URL field. */
19
+ url: (o) => ({ type: 'url', ...o }),
20
+ /** An email-address field. */
21
+ email: (o) => ({ type: 'email', ...o }),
22
+ /** A calendar-date field. */
23
+ date: (o) => ({ type: 'date', ...o }),
24
+ /** A date-and-time field. */
25
+ datetime: (o) => ({ type: 'datetime', ...o }),
26
+ /** A boolean checkbox field. */
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 }),
30
+ /** An image field whose value is the nested ImageValue object. */
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 }),
41
+ };
@@ -0,0 +1,107 @@
1
+ import type { FieldDescriptor, ImageValue } from './fields.js';
2
+ import type { ValidationResult } from './types.js';
3
+ import type { StandardInput, StandardSchemaV1 } from './standard-schema.js';
4
+ /**
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
+ */
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>;
17
+ /**
18
+ * Options for `fieldset`. `refine` runs after the per-field coercion and constraints pass, for
19
+ * cross-field and body-dependent checks. It is validation-only: it returns field-keyed errors to
20
+ * merge, or nothing, and never transforms the data. Server-only, since it may carry closures.
21
+ */
22
+ export interface FieldsetOptions {
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;
26
+ }
27
+ /**
28
+ * A concept's fieldset: the plain-data descriptors, the co-bundled behavior table, the server-derived
29
+ * validator, and the Standard Schema conformance property.
30
+ */
31
+ export interface Fieldset<R extends Record<string, FieldDescriptor> = Record<string, FieldDescriptor>> {
32
+ /** The declared descriptors as plain serializable data, for the editor form. */
33
+ readonly fields: R;
34
+ /** Function-valued behavior keyed by field name; empty for a scalar-only fieldset. */
35
+ readonly behavior: BehaviorTable;
36
+ /** Validate raw frontmatter, returning field-keyed errors or the normalized data. */
37
+ validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
38
+ /** Standard Schema v1 conformance, for ecosystem interop. A thin adapter over `validate`. */
39
+ readonly '~standard': StandardSchemaV1<StandardInput, Record<string, unknown>>['~standard'];
40
+ }
41
+ /**
42
+ * Map one field descriptor to the TS type of its normalized value. number is number, boolean is
43
+ * boolean, image is the nested ImageValue object; a select with a literal option list is that
44
+ * option union, a multiselect with one is that union array (else string[]); everything else is a
45
+ * string.
46
+ */
47
+ type ValueOf<D extends FieldDescriptor> = D extends {
48
+ type: 'number';
49
+ } ? number : D extends {
50
+ type: 'boolean';
51
+ } ? boolean : D extends {
52
+ type: 'image';
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 {
60
+ type: 'select';
61
+ options: readonly (infer O extends string)[];
62
+ } ? O : D extends {
63
+ type: 'multiselect';
64
+ options: readonly (infer O extends string)[];
65
+ } ? O[] : D extends {
66
+ type: 'multiselect';
67
+ } ? string[] : string;
68
+ /** Flatten an intersection into a single readable object type. */
69
+ type Prettify<T> = {
70
+ [K in keyof T]: T[K];
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
+ };
76
+ /**
77
+ * The normalized frontmatter type inferred from a fieldset's descriptor record. A descriptor
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]`.
81
+ */
82
+ type InferRecord<RR extends Record<string, FieldDescriptor>, R = RemoveIndex<RR>> = Prettify<{
83
+ -readonly [K in keyof R as R[K] extends {
84
+ required: true;
85
+ } ? K : never]: ValueOf<R[K] extends FieldDescriptor ? R[K] : never>;
86
+ } & {
87
+ -readonly [K in keyof R as R[K] extends {
88
+ required: true;
89
+ } ? never : K]?: ValueOf<R[K] extends FieldDescriptor ? R[K] : never>;
90
+ }>;
91
+ /** Extract the inferred frontmatter type from a `Fieldset`. */
92
+ export type InferFieldset<S> = S extends Fieldset<infer R> ? InferRecord<R> : never;
93
+ /**
94
+ * Build a fieldset from a key-to-descriptor record. The returned schema carries the descriptors, a
95
+ * server-derived validator that coerces per type and returns field-keyed errors or normalized data,
96
+ * and the Standard Schema conformance property whose issues map each error to a single-segment path.
97
+ */
98
+ export declare function fieldset<const R extends Record<string, FieldDescriptor>>(record: R, options?: FieldsetOptions): Fieldset<R>;
99
+ /**
100
+ * Resolve each descriptor's `default` to a form-initial value, so a fresh entry opens prefilled. The
101
+ * `'today'` sentinel on a date field resolves through the passed `now` to its `YYYY-MM-DD` form; an
102
+ * empty-string or `false` default is omitted, so an untouched field commits no key (the
103
+ * minimal-frontmatter invariant). With no `now`, a `'today'` default is omitted rather than read off
104
+ * a real clock, since library code must stay deterministic and Workers-safe.
105
+ */
106
+ export declare function initialValues(fieldset: Fieldset, now?: Date): Record<string, unknown>;
107
+ export {};