@agntcms/next 0.2.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.
@@ -0,0 +1,71 @@
1
+ import { a as FormSchema, b as FormDefinition } from './form-BqY0H1V5.js';
2
+
3
+ /**
4
+ * Input shape for `defineForm`. Split out so the factory can use a single
5
+ * generic parameter for both input and output, the same pattern as
6
+ * `defineSection`.
7
+ */
8
+ interface DefineFormInput<S extends FormSchema> {
9
+ readonly name: string;
10
+ readonly schema: S;
11
+ /**
12
+ * Optional honeypot field name (out-of-schema). The route handler treats
13
+ * a non-empty value at this key in the submit payload as a bot
14
+ * indicator and silently drops the submission. The handler MUST NOT
15
+ * leak suppression to the client (returns 200 ok with `stored: false`).
16
+ *
17
+ * MUST NOT collide with any field name in `schema`.
18
+ */
19
+ readonly honeypot?: string;
20
+ }
21
+ /** Thrown when `defineForm` rejects a form schema that violates the v1 restrictions. */
22
+ declare class InvalidFormFieldError extends Error {
23
+ readonly formName: string;
24
+ readonly fieldName: string;
25
+ readonly fieldKind: string;
26
+ constructor(formName: string, fieldName: string, fieldKind: string);
27
+ }
28
+ /**
29
+ * Thrown when a `honeypot` value is unusable. Discriminated by `reason`:
30
+ *
31
+ * - `'collision'` — the honeypot name matches a field already declared
32
+ * in the schema. This would let bot traffic occupy a real field key.
33
+ * - `'empty'` — the honeypot name is the empty string. An empty key is
34
+ * never a valid identifier in the submit payload, so the honeypot
35
+ * check would silently always pass; this is treated as an authoring
36
+ * mistake.
37
+ *
38
+ * Single class with a discriminator (rather than a second error class)
39
+ * keeps the public error surface small while still letting callers
40
+ * distinguish the two failure modes through `err.reason`.
41
+ */
42
+ declare class HoneypotCollisionError extends Error {
43
+ readonly formName: string;
44
+ readonly fieldName: string;
45
+ readonly reason: 'collision' | 'empty';
46
+ constructor(formName: string, fieldName: string, reason?: 'collision' | 'empty');
47
+ }
48
+ /** Thrown when `name` is empty or fails the conservative URL-segment check. */
49
+ declare class InvalidFormNameError extends Error {
50
+ constructor(name: string);
51
+ }
52
+ /**
53
+ * Type-safe factory for a `FormDefinition`.
54
+ *
55
+ * Compile-time guarantee: `S extends FormSchema` constrains each field
56
+ * to `FormFieldDescriptor` (the closed v1 subset). Runtime guarantee:
57
+ * the factory iterates `schema` and throws `InvalidFormFieldError` if any
58
+ * descriptor's `kind` is in `FORM_FORBIDDEN_KINDS`. Both layers exist on
59
+ * purpose — see file header.
60
+ *
61
+ * Allowed field kinds (see ARCHITECTURE.md §6.5):
62
+ * text, richText, number, boolean, select, link
63
+ *
64
+ * Forbidden in v1 (throws):
65
+ * image — file upload deferred (§6.5, §12)
66
+ * reference — semantic mismatch for user input
67
+ * list — recursive payload validation, YAGNI for v1
68
+ */
69
+ declare function defineForm<S extends FormSchema>(input: DefineFormInput<S>): FormDefinition<S>;
70
+
71
+ export { type DefineFormInput as D, HoneypotCollisionError as H, InvalidFormFieldError as I, InvalidFormNameError as a, defineForm as d };
@@ -0,0 +1,71 @@
1
+ import { a as FormSchema, b as FormDefinition } from './form-BqY0H1V5.cjs';
2
+
3
+ /**
4
+ * Input shape for `defineForm`. Split out so the factory can use a single
5
+ * generic parameter for both input and output, the same pattern as
6
+ * `defineSection`.
7
+ */
8
+ interface DefineFormInput<S extends FormSchema> {
9
+ readonly name: string;
10
+ readonly schema: S;
11
+ /**
12
+ * Optional honeypot field name (out-of-schema). The route handler treats
13
+ * a non-empty value at this key in the submit payload as a bot
14
+ * indicator and silently drops the submission. The handler MUST NOT
15
+ * leak suppression to the client (returns 200 ok with `stored: false`).
16
+ *
17
+ * MUST NOT collide with any field name in `schema`.
18
+ */
19
+ readonly honeypot?: string;
20
+ }
21
+ /** Thrown when `defineForm` rejects a form schema that violates the v1 restrictions. */
22
+ declare class InvalidFormFieldError extends Error {
23
+ readonly formName: string;
24
+ readonly fieldName: string;
25
+ readonly fieldKind: string;
26
+ constructor(formName: string, fieldName: string, fieldKind: string);
27
+ }
28
+ /**
29
+ * Thrown when a `honeypot` value is unusable. Discriminated by `reason`:
30
+ *
31
+ * - `'collision'` — the honeypot name matches a field already declared
32
+ * in the schema. This would let bot traffic occupy a real field key.
33
+ * - `'empty'` — the honeypot name is the empty string. An empty key is
34
+ * never a valid identifier in the submit payload, so the honeypot
35
+ * check would silently always pass; this is treated as an authoring
36
+ * mistake.
37
+ *
38
+ * Single class with a discriminator (rather than a second error class)
39
+ * keeps the public error surface small while still letting callers
40
+ * distinguish the two failure modes through `err.reason`.
41
+ */
42
+ declare class HoneypotCollisionError extends Error {
43
+ readonly formName: string;
44
+ readonly fieldName: string;
45
+ readonly reason: 'collision' | 'empty';
46
+ constructor(formName: string, fieldName: string, reason?: 'collision' | 'empty');
47
+ }
48
+ /** Thrown when `name` is empty or fails the conservative URL-segment check. */
49
+ declare class InvalidFormNameError extends Error {
50
+ constructor(name: string);
51
+ }
52
+ /**
53
+ * Type-safe factory for a `FormDefinition`.
54
+ *
55
+ * Compile-time guarantee: `S extends FormSchema` constrains each field
56
+ * to `FormFieldDescriptor` (the closed v1 subset). Runtime guarantee:
57
+ * the factory iterates `schema` and throws `InvalidFormFieldError` if any
58
+ * descriptor's `kind` is in `FORM_FORBIDDEN_KINDS`. Both layers exist on
59
+ * purpose — see file header.
60
+ *
61
+ * Allowed field kinds (see ARCHITECTURE.md §6.5):
62
+ * text, richText, number, boolean, select, link
63
+ *
64
+ * Forbidden in v1 (throws):
65
+ * image — file upload deferred (§6.5, §12)
66
+ * reference — semantic mismatch for user input
67
+ * list — recursive payload validation, YAGNI for v1
68
+ */
69
+ declare function defineForm<S extends FormSchema>(input: DefineFormInput<S>): FormDefinition<S>;
70
+
71
+ export { type DefineFormInput as D, HoneypotCollisionError as H, InvalidFormFieldError as I, InvalidFormNameError as a, defineForm as d };
@@ -0,0 +1,243 @@
1
+ import { S as SectionSchema, c as FieldDescriptor, T as TextField, R as RichTextField, I as ImageField, d as ImageValue, V as VideoField, e as VideoValue, f as ReferenceField, g as ReferenceValue, L as LinkField, h as LinkValue, B as ButtonField, i as ButtonValue, N as NumberField, j as BooleanField, k as SelectField, l as ListField, m as FormOverridesFieldDescriptor, n as FormFieldOverrides } from './form-BqY0H1V5.cjs';
2
+
3
+ declare const __slot: unique symbol;
4
+ /**
5
+ * Opaque editable slot. The runtime hands `<EditableText>` /
6
+ * `<EditableImage>` / etc. exactly this shape; the brand `[__slot]: K` is
7
+ * phantom (never set at runtime) and exists only to stop the slot from
8
+ * being assigned to `React.ReactNode` at JSX render sites. The slot kind
9
+ * `K` lets the matching editable component refuse a mismatched widget at
10
+ * compile time (`<EditableText field={imageSlot}>` is a TS error).
11
+ *
12
+ * The `value` is `V | PreviewFieldLike<V>` because the runtime wraps with
13
+ * `PreviewField` only in preview mode; published mode hands the bare
14
+ * value. The `read()` helper unwraps both branches.
15
+ */
16
+ interface EditableSlot<K extends string, V> {
17
+ readonly [__slot]: K;
18
+ readonly value: V | PreviewFieldLike<V>;
19
+ }
20
+ /**
21
+ * Local structural replica of `PreviewFieldLike<T>` from
22
+ * `react/editable/isPreviewField.ts`. Duplicated here (not imported) for
23
+ * the same reason that file exists: keeping `sections/` free of any
24
+ * dependency on `react/`. Both shapes must stay structurally identical —
25
+ * if one moves, both move.
26
+ */
27
+ interface PreviewFieldLike<T> {
28
+ readonly __agntcmsPreview: true;
29
+ readonly value: T;
30
+ readonly origin: PreviewFieldOriginLike;
31
+ }
32
+ interface PreviewFieldOriginLike {
33
+ readonly pageSlug: string;
34
+ readonly sectionId: string;
35
+ readonly fieldPath: string;
36
+ readonly source: 'draft' | 'published';
37
+ readonly revision: string;
38
+ readonly kind?: 'page' | 'global';
39
+ readonly globalName?: string;
40
+ }
41
+ /**
42
+ * Recursive item shape for `ListField`. Every field on a list item becomes
43
+ * its own slot, so a section author who writes `{item.text}` raw inside
44
+ * `<EditableList renderItem={…}>` gets the SAME compile error as a raw
45
+ * `{title}` at the section level. This closes the structural blind spot
46
+ * a heuristic gate could not (see EDITABILITY_DESIGN.md, decision #2).
47
+ *
48
+ * `_id` is preserved as a bare string — section components routinely use
49
+ * it as a React key, and there is no inline-editing surface for it.
50
+ */
51
+ type SlotItem<S extends SectionSchema> = {
52
+ readonly _id: string;
53
+ } & {
54
+ readonly [K in keyof S]: FieldDataType<S[K]>;
55
+ };
56
+
57
+ /**
58
+ * Maps a single `FieldDescriptor` to the type a section component receives
59
+ * on its `props` for that field.
60
+ *
61
+ * Built-in field → component-prop type:
62
+ * text → EditableSlot<'text', string>
63
+ * richText → EditableSlot<'richText', string>
64
+ * image → EditableSlot<'image', ImageValue>
65
+ * video → EditableSlot<'video', VideoValue>
66
+ * link → EditableSlot<'link', LinkValue>
67
+ * button → EditableSlot<'button', ButtonValue>
68
+ * number → EditableSlot<'number', number>
69
+ * boolean → EditableSlot<'boolean', boolean>
70
+ * select → EditableSlot<'select', string>
71
+ * list → EditableSlot<'list', ReadonlyArray<SlotItem<S>>>
72
+ * reference → ReferenceValue (raw — see decision #3)
73
+ * formOverrides → FormFieldOverrides (raw — see decision #4)
74
+ *
75
+ * EDITABILITY_DESIGN.md gates this design. The slot wrapping is the
76
+ * load-bearing change: a slot is not assignable to `React.ReactNode`, so
77
+ * `<h1>{title}</h1>` produces a TS error and forces the author into
78
+ * `<EditableText field={title}>`. `ListField` items recurse via
79
+ * `SlotItem<S>`, so raw rendering inside `renderItem` is also a TS error.
80
+ *
81
+ * `ReferenceField` stays raw because references are not inline-editable
82
+ * in v1 (decision #3): a slot would force a no-op `<EditableReference>`
83
+ * wrapper without UX value. `FormOverridesField` stays raw because
84
+ * forms render via a built-in `<Form>` component that reads the schema
85
+ * itself; there is no author-defined component receiving raw form-field
86
+ * values, and therefore no JSX surface where a raw render could leak
87
+ * (decision #4).
88
+ *
89
+ * `FieldDataType` and `FieldValueFor` (`domain/schema.ts`) NO LONGER
90
+ * agree element-by-element: `FieldValueFor` is the bare runtime payload
91
+ * shape (what storage carries, what `getContent` returns), and stays as
92
+ * it is; `FieldDataType` is the section-author-facing mapping that
93
+ * drives `DataOf<S>` and now wraps editable kinds in slots. The runtime
94
+ * wrapping happens at the SectionRenderer boundary (sub-task 2 wires
95
+ * `wrapAsSlot`) and is INTENTIONALLY NOT mirrored in `getContent` — see
96
+ * the comment in `runtime/getContent.ts` for why.
97
+ *
98
+ * The mapping is expressed per-descriptor so a future field type is one
99
+ * line of change instead of a redesign. That is not the same as
100
+ * "user-extensible field types" — invariant 5 (CLAUDE.md) forbids a
101
+ * plugin system; this is just the internal seam.
102
+ */
103
+ type FieldDataType<F extends FieldDescriptor> = F extends TextField ? EditableSlot<'text', string> : F extends RichTextField ? EditableSlot<'richText', string> : F extends ImageField ? EditableSlot<'image', ImageValue> : F extends VideoField ? EditableSlot<'video', VideoValue> : F extends ReferenceField ? ReferenceValue : F extends LinkField ? EditableSlot<'link', LinkValue> : F extends ButtonField ? EditableSlot<'button', ButtonValue> : F extends NumberField ? EditableSlot<'number', number> : F extends BooleanField ? EditableSlot<'boolean', boolean> : F extends SelectField ? EditableSlot<'select', string> : F extends ListField<infer S> ? EditableSlot<'list', ReadonlyArray<SlotItem<S>>> : F extends FormOverridesFieldDescriptor ? FormFieldOverrides : never;
104
+ /**
105
+ * Derives the SECTION-COMPONENT prop shape from its schema.
106
+ *
107
+ * Per `FieldDataType`, every editable field becomes an `EditableSlot<K,V>`
108
+ * (so `<h1>{title}</h1>` is a TS error); `ReferenceValue` and
109
+ * `FormFieldOverrides` stay raw. Example:
110
+ *
111
+ * type HeroSchema = { title: TextField; image: ImageField; ref: ReferenceField }
112
+ * DataOf<HeroSchema> ≡ {
113
+ * readonly title: EditableSlot<'text', string>
114
+ * readonly image: EditableSlot<'image', ImageValue>
115
+ * readonly ref: ReferenceValue
116
+ * }
117
+ *
118
+ * NOTE: this is the COMPONENT-SIDE shape. The bare runtime payload (what
119
+ * `getContent` returns and what storage carries) is described by
120
+ * `FieldValueFor<F>` in `domain/schema.ts` — that mapping has not changed
121
+ * and must not change (see the slot-non-wrapping comment in
122
+ * `runtime/getContent.ts`).
123
+ */
124
+ type DataOf<S extends SectionSchema> = {
125
+ readonly [K in keyof S]: FieldDataType<S[K]>;
126
+ };
127
+ /**
128
+ * The contract a section's React component must satisfy: a function taking
129
+ * exactly the derived props for its schema. The return type is intentionally
130
+ * `unknown` — `sections/` may not name React types (see file header). The
131
+ * downstream `react/` module is where the stricter `ReactElement` typing
132
+ * lives; here we only care that the props line up with the schema.
133
+ */
134
+ type SectionComponent<S extends SectionSchema> = (props: DataOf<S>) => unknown;
135
+ /**
136
+ * Registration record produced by `defineSection`. Consumers (the registry
137
+ * built by `defineConfig` in T-019, `SectionRenderer` in T-016) look up
138
+ * definitions by `name` and use `component` to render.
139
+ *
140
+ * Both generic parameters are required (no defaults). A heterogeneous
141
+ * registry — the list the runtime actually stores — is typed as
142
+ * `readonly AnySectionDefinition[]`, NOT `readonly SectionDefinition[]`.
143
+ *
144
+ * Why this split exists
145
+ * ---------------------
146
+ * `SectionComponent<S>` is a function whose parameter is contravariant. If
147
+ * we widened `SectionDefinition` to its default parameters and asked a
148
+ * concrete `SectionDefinition<{title: TextField}, (p: {title: string}) => unknown>`
149
+ * to be assignable to it, the function-parameter contravariance would
150
+ * refuse: the "widened" parameter type `DataOf<SectionSchema>` is stricter
151
+ * (more required keys, from TypeScript's perspective) than any concrete
152
+ * `DataOf<S>`. The erasure-safe form lives in `AnySectionDefinition`, which
153
+ * stores the component with `unknown` props. Every precise definition is
154
+ * assignable to `AnySectionDefinition`.
155
+ */
156
+ interface SectionDefinition<S extends SectionSchema, C extends SectionComponent<S>> {
157
+ /** Stable type discriminator used to tag `Section.type` and in lookups. */
158
+ readonly name: string;
159
+ /** Optional grouping label for the section picker modal. */
160
+ readonly category?: string;
161
+ /** Field-descriptor schema. */
162
+ readonly schema: S;
163
+ /** React component reference. See file header on why not a thunk. */
164
+ readonly component: C;
165
+ /** Pre-computed placeholder values for each field, used when inserting a
166
+ * new section in preview mode. Computed from field descriptors' `default`
167
+ * property (when present) or from the built-in per-kind fallback.
168
+ *
169
+ * Typed as `Record<string, unknown>` so the registry type stays stable
170
+ * even if a future built-in field type introduces a non-string runtime
171
+ * value. In v1 every value here is a `string`. The registry itself
172
+ * never interprets these — consumers that read a specific field narrow
173
+ * at the call site. */
174
+ readonly defaults: Record<string, unknown>;
175
+ /** Optional list of layout names supported by this section. When present
176
+ * with length >= 2, the preview-mode section picker renders a "Layout"
177
+ * tab so authors can switch the layout without editing JSON. The section
178
+ * component reads the active layout from its `layout` prop; the picker
179
+ * writes that value via the existing per-field save round-trip.
180
+ *
181
+ * Layouts are a SECTION-level concern, not a field descriptor, because
182
+ * they gate structure inside a single component rather than introducing
183
+ * a new field type. See ARCHITECTURE.md §4. */
184
+ readonly layouts?: readonly string[];
185
+ }
186
+ /**
187
+ * The erased/heterogeneous form used by the registry and by
188
+ * `defineConfig({ sections: [...] })`. Precise definitions produced by
189
+ * `defineSection` are assignable to this type because:
190
+ * - `schema` widens to `SectionSchema` (covariant read),
191
+ * - `component` widens to a function whose parameter type is `never`.
192
+ * Under parameter contravariance, any concrete `(props: X) => unknown`
193
+ * is assignable to `(props: never) => unknown` (for any `X`, `never` is
194
+ * a subtype of `X`). Return widens to `unknown`.
195
+ *
196
+ * Callers that only need `name` / `schema` metadata treat a list of
197
+ * definitions as `readonly AnySectionDefinition[]`. Rendering code — which
198
+ * does need to INVOKE the component — recovers strong typing at the call
199
+ * site by looking up by name and casting to its section-specific
200
+ * `SectionComponent<S>` before calling. The registry itself never invokes
201
+ * components.
202
+ */
203
+ interface AnySectionDefinition {
204
+ readonly name: string;
205
+ /** Optional grouping label for the section picker modal. */
206
+ readonly category?: string;
207
+ readonly schema: SectionSchema;
208
+ readonly component: (props: never) => unknown;
209
+ /** Pre-computed placeholder values — see `SectionDefinition.defaults`. */
210
+ readonly defaults: Record<string, unknown>;
211
+ /** Optional layout names — see `SectionDefinition.layouts`. */
212
+ readonly layouts?: readonly string[];
213
+ }
214
+ /**
215
+ * Input shape for `defineSection`. Split out so the factory can use a single
216
+ * generic parameter set for both input and output.
217
+ */
218
+ interface DefineSectionInput<S extends SectionSchema, C extends SectionComponent<S>> {
219
+ readonly name: string;
220
+ /** Optional grouping label for the section picker modal. */
221
+ readonly category?: string;
222
+ readonly schema: S;
223
+ readonly component: C;
224
+ /** Optional layout names — see `SectionDefinition.layouts`. */
225
+ readonly layouts?: readonly string[];
226
+ }
227
+ /**
228
+ * Type-safe factory for a `SectionDefinition`.
229
+ *
230
+ * The compile-time win is the constraint `C extends SectionComponent<S>`: if
231
+ * the component's props do not match the derived `DataOf<S>`, TypeScript
232
+ * rejects the call. The factory itself is purely about binding — it does no
233
+ * validation at runtime, because in v1 there is nothing to validate (the
234
+ * schema is plain descriptor metadata, not a parser).
235
+ *
236
+ * Object and schema are not deep-frozen: doing so would add a runtime cost on
237
+ * every module load without preventing anything the type system does not
238
+ * already prevent in strict mode. `as const` at the author's call site is
239
+ * enough for literal inference.
240
+ */
241
+ declare function defineSection<S extends SectionSchema, C extends SectionComponent<S>>(input: DefineSectionInput<S, C>): SectionDefinition<S, C>;
242
+
243
+ export { type AnySectionDefinition as A, type DataOf as D, type EditableSlot as E, type FieldDataType as F, type SectionComponent as S, type DefineSectionInput as a, type SectionDefinition as b, type SlotItem as c, defineSection as d };
@@ -0,0 +1,243 @@
1
+ import { S as SectionSchema, c as FieldDescriptor, T as TextField, R as RichTextField, I as ImageField, d as ImageValue, V as VideoField, e as VideoValue, f as ReferenceField, g as ReferenceValue, L as LinkField, h as LinkValue, B as ButtonField, i as ButtonValue, N as NumberField, j as BooleanField, k as SelectField, l as ListField, m as FormOverridesFieldDescriptor, n as FormFieldOverrides } from './form-BqY0H1V5.js';
2
+
3
+ declare const __slot: unique symbol;
4
+ /**
5
+ * Opaque editable slot. The runtime hands `<EditableText>` /
6
+ * `<EditableImage>` / etc. exactly this shape; the brand `[__slot]: K` is
7
+ * phantom (never set at runtime) and exists only to stop the slot from
8
+ * being assigned to `React.ReactNode` at JSX render sites. The slot kind
9
+ * `K` lets the matching editable component refuse a mismatched widget at
10
+ * compile time (`<EditableText field={imageSlot}>` is a TS error).
11
+ *
12
+ * The `value` is `V | PreviewFieldLike<V>` because the runtime wraps with
13
+ * `PreviewField` only in preview mode; published mode hands the bare
14
+ * value. The `read()` helper unwraps both branches.
15
+ */
16
+ interface EditableSlot<K extends string, V> {
17
+ readonly [__slot]: K;
18
+ readonly value: V | PreviewFieldLike<V>;
19
+ }
20
+ /**
21
+ * Local structural replica of `PreviewFieldLike<T>` from
22
+ * `react/editable/isPreviewField.ts`. Duplicated here (not imported) for
23
+ * the same reason that file exists: keeping `sections/` free of any
24
+ * dependency on `react/`. Both shapes must stay structurally identical —
25
+ * if one moves, both move.
26
+ */
27
+ interface PreviewFieldLike<T> {
28
+ readonly __agntcmsPreview: true;
29
+ readonly value: T;
30
+ readonly origin: PreviewFieldOriginLike;
31
+ }
32
+ interface PreviewFieldOriginLike {
33
+ readonly pageSlug: string;
34
+ readonly sectionId: string;
35
+ readonly fieldPath: string;
36
+ readonly source: 'draft' | 'published';
37
+ readonly revision: string;
38
+ readonly kind?: 'page' | 'global';
39
+ readonly globalName?: string;
40
+ }
41
+ /**
42
+ * Recursive item shape for `ListField`. Every field on a list item becomes
43
+ * its own slot, so a section author who writes `{item.text}` raw inside
44
+ * `<EditableList renderItem={…}>` gets the SAME compile error as a raw
45
+ * `{title}` at the section level. This closes the structural blind spot
46
+ * a heuristic gate could not (see EDITABILITY_DESIGN.md, decision #2).
47
+ *
48
+ * `_id` is preserved as a bare string — section components routinely use
49
+ * it as a React key, and there is no inline-editing surface for it.
50
+ */
51
+ type SlotItem<S extends SectionSchema> = {
52
+ readonly _id: string;
53
+ } & {
54
+ readonly [K in keyof S]: FieldDataType<S[K]>;
55
+ };
56
+
57
+ /**
58
+ * Maps a single `FieldDescriptor` to the type a section component receives
59
+ * on its `props` for that field.
60
+ *
61
+ * Built-in field → component-prop type:
62
+ * text → EditableSlot<'text', string>
63
+ * richText → EditableSlot<'richText', string>
64
+ * image → EditableSlot<'image', ImageValue>
65
+ * video → EditableSlot<'video', VideoValue>
66
+ * link → EditableSlot<'link', LinkValue>
67
+ * button → EditableSlot<'button', ButtonValue>
68
+ * number → EditableSlot<'number', number>
69
+ * boolean → EditableSlot<'boolean', boolean>
70
+ * select → EditableSlot<'select', string>
71
+ * list → EditableSlot<'list', ReadonlyArray<SlotItem<S>>>
72
+ * reference → ReferenceValue (raw — see decision #3)
73
+ * formOverrides → FormFieldOverrides (raw — see decision #4)
74
+ *
75
+ * EDITABILITY_DESIGN.md gates this design. The slot wrapping is the
76
+ * load-bearing change: a slot is not assignable to `React.ReactNode`, so
77
+ * `<h1>{title}</h1>` produces a TS error and forces the author into
78
+ * `<EditableText field={title}>`. `ListField` items recurse via
79
+ * `SlotItem<S>`, so raw rendering inside `renderItem` is also a TS error.
80
+ *
81
+ * `ReferenceField` stays raw because references are not inline-editable
82
+ * in v1 (decision #3): a slot would force a no-op `<EditableReference>`
83
+ * wrapper without UX value. `FormOverridesField` stays raw because
84
+ * forms render via a built-in `<Form>` component that reads the schema
85
+ * itself; there is no author-defined component receiving raw form-field
86
+ * values, and therefore no JSX surface where a raw render could leak
87
+ * (decision #4).
88
+ *
89
+ * `FieldDataType` and `FieldValueFor` (`domain/schema.ts`) NO LONGER
90
+ * agree element-by-element: `FieldValueFor` is the bare runtime payload
91
+ * shape (what storage carries, what `getContent` returns), and stays as
92
+ * it is; `FieldDataType` is the section-author-facing mapping that
93
+ * drives `DataOf<S>` and now wraps editable kinds in slots. The runtime
94
+ * wrapping happens at the SectionRenderer boundary (sub-task 2 wires
95
+ * `wrapAsSlot`) and is INTENTIONALLY NOT mirrored in `getContent` — see
96
+ * the comment in `runtime/getContent.ts` for why.
97
+ *
98
+ * The mapping is expressed per-descriptor so a future field type is one
99
+ * line of change instead of a redesign. That is not the same as
100
+ * "user-extensible field types" — invariant 5 (CLAUDE.md) forbids a
101
+ * plugin system; this is just the internal seam.
102
+ */
103
+ type FieldDataType<F extends FieldDescriptor> = F extends TextField ? EditableSlot<'text', string> : F extends RichTextField ? EditableSlot<'richText', string> : F extends ImageField ? EditableSlot<'image', ImageValue> : F extends VideoField ? EditableSlot<'video', VideoValue> : F extends ReferenceField ? ReferenceValue : F extends LinkField ? EditableSlot<'link', LinkValue> : F extends ButtonField ? EditableSlot<'button', ButtonValue> : F extends NumberField ? EditableSlot<'number', number> : F extends BooleanField ? EditableSlot<'boolean', boolean> : F extends SelectField ? EditableSlot<'select', string> : F extends ListField<infer S> ? EditableSlot<'list', ReadonlyArray<SlotItem<S>>> : F extends FormOverridesFieldDescriptor ? FormFieldOverrides : never;
104
+ /**
105
+ * Derives the SECTION-COMPONENT prop shape from its schema.
106
+ *
107
+ * Per `FieldDataType`, every editable field becomes an `EditableSlot<K,V>`
108
+ * (so `<h1>{title}</h1>` is a TS error); `ReferenceValue` and
109
+ * `FormFieldOverrides` stay raw. Example:
110
+ *
111
+ * type HeroSchema = { title: TextField; image: ImageField; ref: ReferenceField }
112
+ * DataOf<HeroSchema> ≡ {
113
+ * readonly title: EditableSlot<'text', string>
114
+ * readonly image: EditableSlot<'image', ImageValue>
115
+ * readonly ref: ReferenceValue
116
+ * }
117
+ *
118
+ * NOTE: this is the COMPONENT-SIDE shape. The bare runtime payload (what
119
+ * `getContent` returns and what storage carries) is described by
120
+ * `FieldValueFor<F>` in `domain/schema.ts` — that mapping has not changed
121
+ * and must not change (see the slot-non-wrapping comment in
122
+ * `runtime/getContent.ts`).
123
+ */
124
+ type DataOf<S extends SectionSchema> = {
125
+ readonly [K in keyof S]: FieldDataType<S[K]>;
126
+ };
127
+ /**
128
+ * The contract a section's React component must satisfy: a function taking
129
+ * exactly the derived props for its schema. The return type is intentionally
130
+ * `unknown` — `sections/` may not name React types (see file header). The
131
+ * downstream `react/` module is where the stricter `ReactElement` typing
132
+ * lives; here we only care that the props line up with the schema.
133
+ */
134
+ type SectionComponent<S extends SectionSchema> = (props: DataOf<S>) => unknown;
135
+ /**
136
+ * Registration record produced by `defineSection`. Consumers (the registry
137
+ * built by `defineConfig` in T-019, `SectionRenderer` in T-016) look up
138
+ * definitions by `name` and use `component` to render.
139
+ *
140
+ * Both generic parameters are required (no defaults). A heterogeneous
141
+ * registry — the list the runtime actually stores — is typed as
142
+ * `readonly AnySectionDefinition[]`, NOT `readonly SectionDefinition[]`.
143
+ *
144
+ * Why this split exists
145
+ * ---------------------
146
+ * `SectionComponent<S>` is a function whose parameter is contravariant. If
147
+ * we widened `SectionDefinition` to its default parameters and asked a
148
+ * concrete `SectionDefinition<{title: TextField}, (p: {title: string}) => unknown>`
149
+ * to be assignable to it, the function-parameter contravariance would
150
+ * refuse: the "widened" parameter type `DataOf<SectionSchema>` is stricter
151
+ * (more required keys, from TypeScript's perspective) than any concrete
152
+ * `DataOf<S>`. The erasure-safe form lives in `AnySectionDefinition`, which
153
+ * stores the component with `unknown` props. Every precise definition is
154
+ * assignable to `AnySectionDefinition`.
155
+ */
156
+ interface SectionDefinition<S extends SectionSchema, C extends SectionComponent<S>> {
157
+ /** Stable type discriminator used to tag `Section.type` and in lookups. */
158
+ readonly name: string;
159
+ /** Optional grouping label for the section picker modal. */
160
+ readonly category?: string;
161
+ /** Field-descriptor schema. */
162
+ readonly schema: S;
163
+ /** React component reference. See file header on why not a thunk. */
164
+ readonly component: C;
165
+ /** Pre-computed placeholder values for each field, used when inserting a
166
+ * new section in preview mode. Computed from field descriptors' `default`
167
+ * property (when present) or from the built-in per-kind fallback.
168
+ *
169
+ * Typed as `Record<string, unknown>` so the registry type stays stable
170
+ * even if a future built-in field type introduces a non-string runtime
171
+ * value. In v1 every value here is a `string`. The registry itself
172
+ * never interprets these — consumers that read a specific field narrow
173
+ * at the call site. */
174
+ readonly defaults: Record<string, unknown>;
175
+ /** Optional list of layout names supported by this section. When present
176
+ * with length >= 2, the preview-mode section picker renders a "Layout"
177
+ * tab so authors can switch the layout without editing JSON. The section
178
+ * component reads the active layout from its `layout` prop; the picker
179
+ * writes that value via the existing per-field save round-trip.
180
+ *
181
+ * Layouts are a SECTION-level concern, not a field descriptor, because
182
+ * they gate structure inside a single component rather than introducing
183
+ * a new field type. See ARCHITECTURE.md §4. */
184
+ readonly layouts?: readonly string[];
185
+ }
186
+ /**
187
+ * The erased/heterogeneous form used by the registry and by
188
+ * `defineConfig({ sections: [...] })`. Precise definitions produced by
189
+ * `defineSection` are assignable to this type because:
190
+ * - `schema` widens to `SectionSchema` (covariant read),
191
+ * - `component` widens to a function whose parameter type is `never`.
192
+ * Under parameter contravariance, any concrete `(props: X) => unknown`
193
+ * is assignable to `(props: never) => unknown` (for any `X`, `never` is
194
+ * a subtype of `X`). Return widens to `unknown`.
195
+ *
196
+ * Callers that only need `name` / `schema` metadata treat a list of
197
+ * definitions as `readonly AnySectionDefinition[]`. Rendering code — which
198
+ * does need to INVOKE the component — recovers strong typing at the call
199
+ * site by looking up by name and casting to its section-specific
200
+ * `SectionComponent<S>` before calling. The registry itself never invokes
201
+ * components.
202
+ */
203
+ interface AnySectionDefinition {
204
+ readonly name: string;
205
+ /** Optional grouping label for the section picker modal. */
206
+ readonly category?: string;
207
+ readonly schema: SectionSchema;
208
+ readonly component: (props: never) => unknown;
209
+ /** Pre-computed placeholder values — see `SectionDefinition.defaults`. */
210
+ readonly defaults: Record<string, unknown>;
211
+ /** Optional layout names — see `SectionDefinition.layouts`. */
212
+ readonly layouts?: readonly string[];
213
+ }
214
+ /**
215
+ * Input shape for `defineSection`. Split out so the factory can use a single
216
+ * generic parameter set for both input and output.
217
+ */
218
+ interface DefineSectionInput<S extends SectionSchema, C extends SectionComponent<S>> {
219
+ readonly name: string;
220
+ /** Optional grouping label for the section picker modal. */
221
+ readonly category?: string;
222
+ readonly schema: S;
223
+ readonly component: C;
224
+ /** Optional layout names — see `SectionDefinition.layouts`. */
225
+ readonly layouts?: readonly string[];
226
+ }
227
+ /**
228
+ * Type-safe factory for a `SectionDefinition`.
229
+ *
230
+ * The compile-time win is the constraint `C extends SectionComponent<S>`: if
231
+ * the component's props do not match the derived `DataOf<S>`, TypeScript
232
+ * rejects the call. The factory itself is purely about binding — it does no
233
+ * validation at runtime, because in v1 there is nothing to validate (the
234
+ * schema is plain descriptor metadata, not a parser).
235
+ *
236
+ * Object and schema are not deep-frozen: doing so would add a runtime cost on
237
+ * every module load without preventing anything the type system does not
238
+ * already prevent in strict mode. `as const` at the author's call site is
239
+ * enough for literal inference.
240
+ */
241
+ declare function defineSection<S extends SectionSchema, C extends SectionComponent<S>>(input: DefineSectionInput<S, C>): SectionDefinition<S, C>;
242
+
243
+ export { type AnySectionDefinition as A, type DataOf as D, type EditableSlot as E, type FieldDataType as F, type SectionComponent as S, type DefineSectionInput as a, type SectionDefinition as b, type SlotItem as c, defineSection as d };