@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  4. package/dist/components/ComponentForm.svelte +44 -27
  5. package/dist/components/ComponentInsertDialog.svelte +5 -5
  6. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  7. package/dist/components/EditPage.svelte +29 -107
  8. package/dist/components/EditPage.svelte.d.ts +2 -7
  9. package/dist/components/EntryPicker.svelte +117 -0
  10. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  11. package/dist/components/FieldInput.svelte +218 -0
  12. package/dist/components/FieldInput.svelte.d.ts +51 -0
  13. package/dist/components/IconPicker.svelte +2 -2
  14. package/dist/components/IconPicker.svelte.d.ts +2 -0
  15. package/dist/components/LinkPicker.svelte +8 -75
  16. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  17. package/dist/components/MediaHeroField.svelte +8 -5
  18. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  19. package/dist/components/ObjectGroupField.svelte +54 -0
  20. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  21. package/dist/components/ReferenceField.svelte +94 -0
  22. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  23. package/dist/components/RepeatableField.svelte +221 -0
  24. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  25. package/dist/components/cairn-admin.css +4 -0
  26. package/dist/components/preview-doc.js +5 -1
  27. package/dist/components/tidy-validate.js +1 -1
  28. package/dist/content/adapter.js +18 -0
  29. package/dist/content/advisories.d.ts +2 -2
  30. package/dist/content/advisories.js +3 -5
  31. package/dist/content/compose.d.ts +7 -6
  32. package/dist/content/compose.js +26 -20
  33. package/dist/content/concepts.d.ts +21 -15
  34. package/dist/content/concepts.js +55 -32
  35. package/dist/content/field-rules.js +3 -4
  36. package/dist/content/fields.d.ts +49 -1
  37. package/dist/content/fields.js +11 -0
  38. package/dist/content/fieldset.d.ts +31 -10
  39. package/dist/content/fieldset.js +262 -109
  40. package/dist/content/frontmatter-region.d.ts +38 -0
  41. package/dist/content/frontmatter-region.js +75 -0
  42. package/dist/content/frontmatter.d.ts +35 -2
  43. package/dist/content/frontmatter.js +232 -11
  44. package/dist/content/manifest.d.ts +34 -0
  45. package/dist/content/manifest.js +80 -4
  46. package/dist/content/media-refs.d.ts +2 -2
  47. package/dist/content/media-rewrite.js +1 -69
  48. package/dist/content/reference-index.d.ts +56 -0
  49. package/dist/content/reference-index.js +95 -0
  50. package/dist/content/references.d.ts +40 -0
  51. package/dist/content/references.js +0 -0
  52. package/dist/content/standard-schema.d.ts +30 -0
  53. package/dist/content/standard-schema.js +4 -0
  54. package/dist/content/types.d.ts +127 -178
  55. package/dist/delivery/data.d.ts +2 -2
  56. package/dist/delivery/data.js +1 -1
  57. package/dist/delivery/public-routes.d.ts +2 -5
  58. package/dist/delivery/public-routes.js +15 -1
  59. package/dist/delivery/site-descriptors.d.ts +5 -1
  60. package/dist/delivery/site-descriptors.js +8 -3
  61. package/dist/delivery/site-indexes.d.ts +2 -2
  62. package/dist/delivery/site-resolver.d.ts +25 -0
  63. package/dist/delivery/site-resolver.js +49 -0
  64. package/dist/doctor/checks-local.js +6 -11
  65. package/dist/github/backend.d.ts +83 -0
  66. package/dist/github/backend.js +76 -0
  67. package/dist/github/credentials.d.ts +11 -5
  68. package/dist/github/credentials.js +3 -3
  69. package/dist/github/repo.d.ts +8 -19
  70. package/dist/github/repo.js +69 -80
  71. package/dist/github/types.d.ts +1 -1
  72. package/dist/github/types.js +4 -4
  73. package/dist/index.d.ts +16 -12
  74. package/dist/index.js +7 -8
  75. package/dist/islands/index.d.ts +12 -0
  76. package/dist/islands/index.js +83 -0
  77. package/dist/islands/types.d.ts +7 -0
  78. package/dist/islands/types.js +1 -0
  79. package/dist/media/rewrite-plan.d.ts +2 -3
  80. package/dist/media/rewrite-plan.js +2 -3
  81. package/dist/media/usage.d.ts +2 -2
  82. package/dist/media/usage.js +3 -5
  83. package/dist/nav/site-config.d.ts +0 -6
  84. package/dist/nav/site-config.js +6 -4
  85. package/dist/render/component-grammar.js +11 -11
  86. package/dist/render/component-reference.js +5 -3
  87. package/dist/render/component-validate.d.ts +4 -1
  88. package/dist/render/component-validate.js +10 -35
  89. package/dist/render/pipeline.d.ts +0 -6
  90. package/dist/render/pipeline.js +1 -1
  91. package/dist/render/registry.d.ts +34 -34
  92. package/dist/render/registry.js +26 -5
  93. package/dist/render/rehype-dispatch.d.ts +4 -4
  94. package/dist/render/rehype-dispatch.js +36 -11
  95. package/dist/render/remark-directives.js +4 -5
  96. package/dist/render/sanitize-schema.js +1 -1
  97. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  98. package/dist/sveltekit/cairn-admin.js +3 -4
  99. package/dist/sveltekit/content-routes.d.ts +10 -8
  100. package/dist/sveltekit/content-routes.js +269 -181
  101. package/dist/sveltekit/health.d.ts +7 -3
  102. package/dist/sveltekit/health.js +9 -3
  103. package/dist/sveltekit/index.d.ts +1 -1
  104. package/dist/sveltekit/nav-routes.d.ts +6 -5
  105. package/dist/sveltekit/nav-routes.js +22 -20
  106. package/dist/sveltekit/types.d.ts +2 -0
  107. package/dist/vite/index.d.ts +3 -3
  108. package/dist/vite/index.js +17 -8
  109. package/package.json +5 -1
  110. package/src/lib/ambient.ts +7 -0
  111. package/src/lib/components/CairnAdmin.svelte +2 -6
  112. package/src/lib/components/ComponentForm.svelte +48 -27
  113. package/src/lib/components/ComponentInsertDialog.svelte +9 -8
  114. package/src/lib/components/EditPage.svelte +43 -119
  115. package/src/lib/components/EntryPicker.svelte +154 -0
  116. package/src/lib/components/FieldInput.svelte +262 -0
  117. package/src/lib/components/IconPicker.svelte +4 -2
  118. package/src/lib/components/LinkPicker.svelte +10 -81
  119. package/src/lib/components/MediaHeroField.svelte +12 -5
  120. package/src/lib/components/ObjectGroupField.svelte +97 -0
  121. package/src/lib/components/ReferenceField.svelte +126 -0
  122. package/src/lib/components/RepeatableField.svelte +310 -0
  123. package/src/lib/components/preview-doc.ts +5 -1
  124. package/src/lib/components/tidy-validate.ts +1 -1
  125. package/src/lib/content/adapter.ts +21 -0
  126. package/src/lib/content/advisories.ts +4 -7
  127. package/src/lib/content/compose.ts +30 -23
  128. package/src/lib/content/concepts.ts +68 -40
  129. package/src/lib/content/field-rules.ts +3 -4
  130. package/src/lib/content/fields.ts +52 -1
  131. package/src/lib/content/fieldset.ts +291 -128
  132. package/src/lib/content/frontmatter-region.ts +90 -0
  133. package/src/lib/content/frontmatter.ts +231 -15
  134. package/src/lib/content/manifest.ts +101 -4
  135. package/src/lib/content/media-refs.ts +2 -2
  136. package/src/lib/content/media-rewrite.ts +7 -80
  137. package/src/lib/content/reference-index.ts +159 -0
  138. package/src/lib/content/references.ts +0 -0
  139. package/src/lib/content/standard-schema.ts +25 -0
  140. package/src/lib/content/types.ts +128 -195
  141. package/src/lib/delivery/data.ts +2 -2
  142. package/src/lib/delivery/public-routes.ts +17 -3
  143. package/src/lib/delivery/site-descriptors.ts +8 -3
  144. package/src/lib/delivery/site-indexes.ts +2 -2
  145. package/src/lib/delivery/site-resolver.ts +64 -0
  146. package/src/lib/doctor/checks-local.ts +6 -14
  147. package/src/lib/github/backend.ts +161 -0
  148. package/src/lib/github/credentials.ts +10 -7
  149. package/src/lib/github/repo.ts +79 -83
  150. package/src/lib/github/types.ts +5 -5
  151. package/src/lib/index.ts +38 -23
  152. package/src/lib/islands/index.ts +84 -0
  153. package/src/lib/islands/types.ts +11 -0
  154. package/src/lib/media/rewrite-plan.ts +4 -6
  155. package/src/lib/media/usage.ts +4 -7
  156. package/src/lib/nav/site-config.ts +8 -9
  157. package/src/lib/render/component-grammar.ts +10 -10
  158. package/src/lib/render/component-reference.ts +4 -3
  159. package/src/lib/render/component-validate.ts +10 -35
  160. package/src/lib/render/pipeline.ts +1 -7
  161. package/src/lib/render/registry.ts +58 -39
  162. package/src/lib/render/rehype-dispatch.ts +45 -10
  163. package/src/lib/render/remark-directives.ts +4 -5
  164. package/src/lib/render/sanitize-schema.ts +1 -1
  165. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  166. package/src/lib/sveltekit/content-routes.ts +330 -221
  167. package/src/lib/sveltekit/health.ts +13 -6
  168. package/src/lib/sveltekit/index.ts +2 -2
  169. package/src/lib/sveltekit/nav-routes.ts +33 -29
  170. package/src/lib/sveltekit/types.ts +5 -1
  171. package/src/lib/vite/index.ts +20 -11
  172. package/dist/content/schema.d.ts +0 -87
  173. package/dist/content/schema.js +0 -85
  174. package/dist/content/validate.d.ts +0 -17
  175. package/dist/content/validate.js +0 -93
  176. package/src/lib/content/schema.ts +0 -163
  177. package/src/lib/content/validate.ts +0 -90
@@ -1,24 +1,30 @@
1
1
  import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
2
- import { type SiteConfig } from '../nav/site-config.js';
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
- * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
5
- * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
6
- * entry here and one key under `content`.
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 CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
9
+ export declare function defineConcept<const C extends ConceptConfig>(concept: C): C;
9
10
  /**
10
- * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
11
- * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
12
- * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
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 normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
15
+ export declare function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void;
17
16
  /**
18
- * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
19
- * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
20
- * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
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 resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
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;
@@ -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) {
@@ -1,7 +1,6 @@
1
- // cairn-cms: the shared field constraint rules. Both the v1 `defineFields` validator and the v2
2
- // `fieldset` validator call these pure helpers, so the two validators cannot drift on the
3
- // constraint wording or the first-failing-rule-wins order. No I/O and no clock reads, so the
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 {
@@ -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
  };
@@ -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
- * The behavior table co-bundled with a fieldset, keyed by field name. It holds function-valued
6
- * behavior a descriptor cannot carry as plain data (a cross-field validator, an array itemLabel).
7
- * Scalars have no behavior, so the table is empty for now and reserved for later co-bundled functions.
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 type BehaviorTable = Record<string, never>;
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 Infer<R extends Record<string, FieldDescriptor>> = Prettify<{
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> ? Infer<R> : never;
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,