@shapio/schema 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare const createStableId: () => string;
17
17
  //#endregion
18
18
  //#region src/types/dataTypes.d.ts
19
19
  /** Every stored data type. A field's data type is separate from the editor that renders it (brief §2.5). */
20
- export declare const DATA_TYPES: readonly ["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"];
20
+ export declare const DATA_TYPES: readonly ["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"];
21
21
  export type DataType = (typeof DATA_TYPES)[number];
22
22
  export declare const isDataType: (value: unknown) => value is DataType;
23
23
  /**
@@ -48,6 +48,9 @@ export type JsonObject = {
48
48
  //#region src/types/settings.d.ts
49
49
  export declare const MEDIA_KINDS: readonly ["image", "video", "audio", "document", "other"];
50
50
  export declare const RELATION_CARDINALITIES: readonly ["one", "many"];
51
+ /** Languages a `code` field can hold. The language reaches the API (OpenAPI, GraphQL, generated types). */
52
+ export declare const CODE_LANGUAGES: readonly ["plain", "html", "css", "javascript", "json", "yaml", "markdown"];
53
+ export type CodeLanguage = (typeof CODE_LANGUAGES)[number];
51
54
  export declare const EnumValueSchema: Type.TObject<{
52
55
  value: Type.TString;
53
56
  label: Type.TString;
@@ -62,6 +65,13 @@ export declare const SETTINGS_SCHEMAS: {
62
65
  minLength: Type.TOptional<Type.TInteger>;
63
66
  maxLength: Type.TOptional<Type.TInteger>;
64
67
  }>;
68
+ code: Type.TObject<{
69
+ /** JSON only: a value that does not parse is refused. */
70
+ validate: Type.TOptional<Type.TBoolean>;
71
+ minLength: Type.TOptional<Type.TInteger>;
72
+ maxLength: Type.TOptional<Type.TInteger>;
73
+ language: Type.TOptional<Type.TEnum<["plain", "html", "css", "javascript", "json", "yaml", "markdown"]>>;
74
+ }>;
65
75
  richtext: Type.TObject<{
66
76
  formatVersion: Type.TOptional<Type.TInteger>;
67
77
  maxLength: Type.TOptional<Type.TInteger>;
@@ -146,7 +156,10 @@ type SettingsSchemas = typeof SETTINGS_SCHEMAS;
146
156
  /** Settings as authored (defaults may be missing). */
147
157
  export type DataTypeSettingsInput = { [T in DataType]: Static<SettingsSchemas[T]>; };
148
158
  /** Settings after normalization: the defaulted keys are always present. */
149
- export type DataTypeSettings = Omit<DataTypeSettingsInput, 'richtext' | 'enum' | 'media' | 'component'> & {
159
+ export type DataTypeSettings = Omit<DataTypeSettingsInput, 'code' | 'richtext' | 'enum' | 'media' | 'component'> & {
160
+ code: DataTypeSettingsInput['code'] & {
161
+ language: CodeLanguage;
162
+ };
150
163
  richtext: DataTypeSettingsInput['richtext'] & {
151
164
  formatVersion: number;
152
165
  };
@@ -168,6 +181,19 @@ export declare const DEFINITION_KINDS: readonly ["collection", "singleton", "com
168
181
  export type DefinitionKind = (typeof DEFINITION_KINDS)[number];
169
182
  export declare const SORT_DIRECTIONS: readonly ["asc", "desc"];
170
183
  export type SortDirection = (typeof SORT_DIRECTIONS)[number];
184
+ /**
185
+ * How an entry of a model opens: `document` (title typed into the page, blocks in a canvas, properties in
186
+ * the strip and the settings drawer) or `form` (every field inline at its `width`, sections from
187
+ * `display.groups`). Unset means `document`; normalizing drops an explicit `document`.
188
+ */
189
+ export declare const ENTRY_LAYOUTS: readonly ["document", "form"];
190
+ export type EntryLayout = (typeof ENTRY_LAYOUTS)[number];
191
+ /**
192
+ * A field's width in a form-layout entry, as a share of the row: rows fill in field order and wrap. Unset
193
+ * means `full`; normalizing drops an explicit `full`.
194
+ */
195
+ export declare const FIELD_WIDTHS: readonly ["full", "two-thirds", "half", "third"];
196
+ export type FieldWidth = (typeof FIELD_WIDTHS)[number];
171
197
  export declare const MAX_FIELDS_PER_DEFINITION = 500;
172
198
  export declare const MAX_LABEL_LENGTH = 200;
173
199
  export declare const MAX_DESCRIPTION_LENGTH = 2000;
@@ -181,7 +207,7 @@ export declare const FieldInputSchema: Type.TObject<{
181
207
  label: Type.TString;
182
208
  /** Help text shown under the control. */
183
209
  description: Type.TOptional<Type.TString>;
184
- type: Type.TEnum<["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
210
+ type: Type.TEnum<["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
185
211
  required: Type.TOptional<Type.TBoolean>;
186
212
  /** Per-locale value. Ignored (must be false) when the model is not localized. */
187
213
  localized: Type.TOptional<Type.TBoolean>;
@@ -200,8 +226,15 @@ export declare const FieldInputSchema: Type.TObject<{
200
226
  id: Type.TString;
201
227
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
202
228
  }>>;
229
+ /**
230
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
231
+ * validator refuses it on a component's field.
232
+ */
233
+ width: Type.TOptional<Type.TEnum<["full", "two-thirds", "half", "third"]>>;
203
234
  }>;
204
235
  export declare const ModelDisplayInputSchema: Type.TObject<{
236
+ /** How entries open: `document` (default) or `form`. See `ENTRY_LAYOUTS`. */
237
+ layout: Type.TOptional<Type.TEnum<["document", "form"]>>;
205
238
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
206
239
  titleFieldId: Type.TOptional<Type.TString>;
207
240
  /** Columns of the content list, in order. */
@@ -242,6 +275,8 @@ export declare const ModelDefinitionInputSchema: Type.TObject<{
242
275
  /** Entries have separate draft and published states. Default true. */
243
276
  draftAndPublish: Type.TOptional<Type.TBoolean>;
244
277
  display: Type.TOptional<Type.TObject<{
278
+ /** How entries open: `document` (default) or `form`. See `ENTRY_LAYOUTS`. */
279
+ layout: Type.TOptional<Type.TEnum<["document", "form"]>>;
245
280
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
246
281
  titleFieldId: Type.TOptional<Type.TString>;
247
282
  /** Columns of the content list, in order. */
@@ -273,7 +308,7 @@ export declare const ModelDefinitionInputSchema: Type.TObject<{
273
308
  label: Type.TString;
274
309
  /** Help text shown under the control. */
275
310
  description: Type.TOptional<Type.TString>;
276
- type: Type.TEnum<["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
311
+ type: Type.TEnum<["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
277
312
  required: Type.TOptional<Type.TBoolean>;
278
313
  /** Per-locale value. Ignored (must be false) when the model is not localized. */
279
314
  localized: Type.TOptional<Type.TBoolean>;
@@ -292,6 +327,11 @@ export declare const ModelDefinitionInputSchema: Type.TObject<{
292
327
  id: Type.TString;
293
328
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
294
329
  }>>;
330
+ /**
331
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
332
+ * validator refuses it on a component's field.
333
+ */
334
+ width: Type.TOptional<Type.TEnum<["full", "two-thirds", "half", "third"]>>;
295
335
  }>>;
296
336
  }>;
297
337
  export declare const ComponentDefinitionInputSchema: Type.TObject<{
@@ -316,7 +356,7 @@ export declare const ComponentDefinitionInputSchema: Type.TObject<{
316
356
  label: Type.TString;
317
357
  /** Help text shown under the control. */
318
358
  description: Type.TOptional<Type.TString>;
319
- type: Type.TEnum<["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
359
+ type: Type.TEnum<["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
320
360
  required: Type.TOptional<Type.TBoolean>;
321
361
  /** Per-locale value. Ignored (must be false) when the model is not localized. */
322
362
  localized: Type.TOptional<Type.TBoolean>;
@@ -335,6 +375,11 @@ export declare const ComponentDefinitionInputSchema: Type.TObject<{
335
375
  id: Type.TString;
336
376
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
337
377
  }>>;
378
+ /**
379
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
380
+ * validator refuses it on a component's field.
381
+ */
382
+ width: Type.TOptional<Type.TEnum<["full", "two-thirds", "half", "third"]>>;
338
383
  }>>;
339
384
  }>;
340
385
  export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
@@ -348,6 +393,8 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
348
393
  /** Entries have separate draft and published states. Default true. */
349
394
  draftAndPublish: Type.TOptional<Type.TBoolean>;
350
395
  display: Type.TOptional<Type.TObject<{
396
+ /** How entries open: `document` (default) or `form`. See `ENTRY_LAYOUTS`. */
397
+ layout: Type.TOptional<Type.TEnum<["document", "form"]>>;
351
398
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
352
399
  titleFieldId: Type.TOptional<Type.TString>;
353
400
  /** Columns of the content list, in order. */
@@ -379,7 +426,7 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
379
426
  label: Type.TString;
380
427
  /** Help text shown under the control. */
381
428
  description: Type.TOptional<Type.TString>;
382
- type: Type.TEnum<["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
429
+ type: Type.TEnum<["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
383
430
  required: Type.TOptional<Type.TBoolean>;
384
431
  /** Per-locale value. Ignored (must be false) when the model is not localized. */
385
432
  localized: Type.TOptional<Type.TBoolean>;
@@ -398,6 +445,11 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
398
445
  id: Type.TString;
399
446
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
400
447
  }>>;
448
+ /**
449
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
450
+ * validator refuses it on a component's field.
451
+ */
452
+ width: Type.TOptional<Type.TEnum<["full", "two-thirds", "half", "third"]>>;
401
453
  }>>;
402
454
  }>, Type.TObject<{
403
455
  kind: Type.TLiteral<"component">;
@@ -421,7 +473,7 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
421
473
  label: Type.TString;
422
474
  /** Help text shown under the control. */
423
475
  description: Type.TOptional<Type.TString>;
424
- type: Type.TEnum<["string", "text", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
476
+ type: Type.TEnum<["string", "text", "code", "richtext", "number", "integer", "decimal", "biginteger", "boolean", "date", "datetime", "time", "enum", "json", "slug", "email", "url", "uid", "media", "relation", "component", "dynamiczone"]>;
425
477
  required: Type.TOptional<Type.TBoolean>;
426
478
  /** Per-locale value. Ignored (must be false) when the model is not localized. */
427
479
  localized: Type.TOptional<Type.TBoolean>;
@@ -440,6 +492,11 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
440
492
  id: Type.TString;
441
493
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
442
494
  }>>;
495
+ /**
496
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
497
+ * validator refuses it on a component's field.
498
+ */
499
+ width: Type.TOptional<Type.TEnum<["full", "two-thirds", "half", "third"]>>;
443
500
  }>>;
444
501
  }>]>;
445
502
  export type FieldInput = Static<typeof FieldInputSchema>;
@@ -464,6 +521,8 @@ type FieldBase = {
464
521
  deprecated: boolean;
465
522
  defaultValue?: JsonValue;
466
523
  editor: EditorChoice;
524
+ /** Width in a form-layout entry; absent means `full`. */
525
+ width?: FieldWidth;
467
526
  };
468
527
  /** A normalized field. Narrow on `type` to get typed `settings`. */
469
528
  export type FieldDefinition<T extends DataType = DataType> = { [K in T]: FieldBase & {
@@ -476,6 +535,8 @@ export type FieldGroup = {
476
535
  fieldIds: string[];
477
536
  };
478
537
  export type ModelDisplay = {
538
+ /** Absent means `document`. */
539
+ layout?: EntryLayout;
479
540
  titleFieldId?: string;
480
541
  listFieldIds?: string[];
481
542
  defaultSort?: {
@@ -644,6 +705,34 @@ export type DocumentLayout = {
644
705
  export declare const effectiveLayout: (model: ModelDefinition) => DocumentLayout;
645
706
  /** The strip's properties: the configured ones, or the first `limit` that `hasValue` says are non-empty. */
646
707
  export declare const stripFieldsOf: (layout: DocumentLayout, hasValue: (field: FieldDefinition) => boolean, limit?: number) => FieldDefinition[];
708
+ /** How the model's entries open (`display.layout`): `document` unless set to `form`. */
709
+ export declare const entryLayoutOf: (model: ModelDefinition) => EntryLayout;
710
+ /**
711
+ * One section of a form-layout entry: a `display.groups` group (its `id` and `label`), or a run of
712
+ * consecutive ungrouped fields (`label` undefined). A run's `id` is `ungrouped-<ID of its first field>`:
713
+ * stable while that field stays first, and unique because a field starts one run at most. Should a group
714
+ * be given that very ID, the run's ID gets `~` prefixed until no group has it.
715
+ */
716
+ export type FieldSection = PropertyGroup;
717
+ export type FormLayout = {
718
+ /** The field that labels the entry (`effectiveTitleField`): the page heading. It is also in `sections`. */
719
+ title: FieldDefinition | undefined;
720
+ /** Every live field, in sections. */
721
+ sections: FieldSection[];
722
+ };
723
+ /**
724
+ * The entry form's layout (`display.layout: 'form'`): every live field, the title included, in sections.
725
+ * Field order decides everything: a group's section sits where its first field appears, with its fields in
726
+ * field order, and every run of consecutive ungrouped fields is an unlabelled section of its own at its
727
+ * place. So name, price, sku, description with a Pricing group (price, sku) reads [name], Pricing [price,
728
+ * sku], [description]. Unknown and deprecated ids are skipped. Pure: no values, no I/O.
729
+ */
730
+ export declare const effectiveFormLayout: (model: ModelDefinition) => FormLayout;
731
+ /**
732
+ * The entry's rich-text body, which "Summarize from body" reads: the document's rich-text canvas fields, or
733
+ * every live rich-text field of a form (a form has no canvas; its fields are all on the page).
734
+ */
735
+ export declare const richTextBodyOf: (model: ModelDefinition) => FieldDefinition[];
647
736
  //#endregion
648
737
  //#region src/display/titleField.d.ts
649
738
  /** Field types that may label entries: the only types `display.titleFieldId` accepts, and the fallback's. */
@@ -688,8 +777,9 @@ export type DefinitionInputWithIds = (WithIds<ModelDefinitionInput> & {
688
777
  /**
689
778
  * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
690
779
  * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
691
- * Empty optional strings and arrays are dropped. A collection without a plural API ID gets the suggested
692
- * one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
780
+ * Empty optional strings and arrays are dropped, and so are the defaults of keys added after definitions
781
+ * were first hashed (`width: 'full'`, `display.layout: 'document'`). A collection without a plural API ID
782
+ * gets the suggested one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
693
783
  */
694
784
  export declare const normalizeDefinition: (input: DefinitionInputWithIds) => SchemaDefinition;
695
785
  /** Normalizes a definition that is already normalized-shaped (e.g. read back from the database). */
@@ -1167,6 +1257,14 @@ export declare const richTextToPlainText: (document: RichTextDocument) => string
1167
1257
  /** Plain text → a document of paragraphs (blank lines split paragraphs; single newlines are hard breaks). */
1168
1258
  export declare const plainTextToRichText: (text: string) => RichTextDocument;
1169
1259
  //#endregion
1260
+ //#region src/richtext/plainText.d.ts
1261
+ /**
1262
+ * A stored rich text value (`{ format, version, doc }`) as one line of plain text: blocks separated by a
1263
+ * space, whitespace collapsed. For summaries (list cells, collapsed items, review rows), never for delivery.
1264
+ * Anything that is not a valid document (an unconverted value of a field that became rich text) reads as ''.
1265
+ */
1266
+ export declare const richTextPlainText: (value: unknown) => string;
1267
+ //#endregion
1170
1268
  //#region src/themes/tokens.d.ts
1171
1269
  /**
1172
1270
  * Admin themes (docs/plans/themes.md): a theme is a named, complete set of these tokens for a light variant, a
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { Value } from "typebox/value";
8
8
  const DATA_TYPES = [
9
9
  "string",
10
10
  "text",
11
+ "code",
11
12
  "richtext",
12
13
  "number",
13
14
  "integer",
@@ -74,6 +75,7 @@ const REFERENCE_DATA_TYPES = /* @__PURE__ */ new Set([
74
75
  const STORAGE_FAMILY = {
75
76
  string: "jsonString",
76
77
  text: "jsonString",
78
+ code: "jsonString",
77
79
  slug: "jsonString",
78
80
  email: "jsonString",
79
81
  url: "jsonString",
@@ -108,6 +110,22 @@ const closed$3 = { additionalProperties: false };
108
110
  const MODEL_KINDS = ["collection", "singleton"];
109
111
  const DEFINITION_KINDS = [...MODEL_KINDS, "component"];
110
112
  const SORT_DIRECTIONS = ["asc", "desc"];
113
+ /**
114
+ * How an entry of a model opens: `document` (title typed into the page, blocks in a canvas, properties in
115
+ * the strip and the settings drawer) or `form` (every field inline at its `width`, sections from
116
+ * `display.groups`). Unset means `document`; normalizing drops an explicit `document`.
117
+ */
118
+ const ENTRY_LAYOUTS = ["document", "form"];
119
+ /**
120
+ * A field's width in a form-layout entry, as a share of the row: rows fill in field order and wrap. Unset
121
+ * means `full`; normalizing drops an explicit `full`.
122
+ */
123
+ const FIELD_WIDTHS = [
124
+ "full",
125
+ "two-thirds",
126
+ "half",
127
+ "third"
128
+ ];
111
129
  const MAX_FIELDS_PER_DEFINITION = 500;
112
130
  const MAX_LABEL_LENGTH = 200;
113
131
  const MAX_DESCRIPTION_LENGTH = 2e3;
@@ -144,7 +162,12 @@ const FieldInputSchema = Type.Object({
144
162
  defaultValue: Type.Optional(Type.Unknown()),
145
163
  /** Type-specific settings, validated against SETTINGS_SCHEMAS[type]. */
146
164
  settings: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
147
- editor: Type.Optional(EditorChoiceInputSchema)
165
+ editor: Type.Optional(EditorChoiceInputSchema),
166
+ /**
167
+ * Width in a form-layout entry (`display.layout: 'form'`). Default `full`. Model fields only: the
168
+ * validator refuses it on a component's field.
169
+ */
170
+ width: Type.Optional(Type.Enum(FIELD_WIDTHS))
148
171
  }, closed$3);
149
172
  const FieldGroupSchema = Type.Object({
150
173
  id: Type.String({
@@ -155,6 +178,8 @@ const FieldGroupSchema = Type.Object({
155
178
  fieldIds: Type.Array(Type.String())
156
179
  }, closed$3);
157
180
  const ModelDisplayInputSchema = Type.Object({
181
+ /** How entries open: `document` (default) or `form`. See `ENTRY_LAYOUTS`. */
182
+ layout: Type.Optional(Type.Enum(ENTRY_LAYOUTS)),
158
183
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
159
184
  titleFieldId: Type.Optional(Type.String()),
160
185
  /** Columns of the content list, in order. */
@@ -255,6 +280,16 @@ const MEDIA_KINDS = [
255
280
  "other"
256
281
  ];
257
282
  const RELATION_CARDINALITIES = ["one", "many"];
283
+ /** Languages a `code` field can hold. The language reaches the API (OpenAPI, GraphQL, generated types). */
284
+ const CODE_LANGUAGES = [
285
+ "plain",
286
+ "html",
287
+ "css",
288
+ "javascript",
289
+ "json",
290
+ "yaml",
291
+ "markdown"
292
+ ];
258
293
  const EnumValueSchema = Type.Object({
259
294
  value: Type.String({
260
295
  minLength: 1,
@@ -274,6 +309,12 @@ const SETTINGS_SCHEMAS = {
274
309
  }))
275
310
  }, closed$2),
276
311
  text: Type.Object(lengthRules, closed$2),
312
+ code: Type.Object({
313
+ language: Type.Optional(Type.Enum(CODE_LANGUAGES)),
314
+ ...lengthRules,
315
+ /** JSON only: a value that does not parse is refused. */
316
+ validate: Type.Optional(Type.Boolean())
317
+ }, closed$2),
277
318
  richtext: Type.Object({
278
319
  formatVersion: Type.Optional(Type.Integer({ minimum: 1 })),
279
320
  maxLength: Type.Optional(Type.Integer({ minimum: 1 }))
@@ -482,6 +523,14 @@ const entries = [
482
523
  maximum: 80
483
524
  })) }, closed$1)
484
525
  },
526
+ {
527
+ id: "codeEditor",
528
+ dataTypes: ["code"],
529
+ optionsSchema: Type.Object({ rows: Type.Optional(Type.Integer({
530
+ minimum: 3,
531
+ maximum: 80
532
+ })) }, closed$1)
533
+ },
485
534
  {
486
535
  id: "mediaPicker",
487
536
  dataTypes: ["media"],
@@ -514,6 +563,7 @@ const EDITOR_CATALOGUE = new Map(entries.map((entry) => [entry.id, entry]));
514
563
  const DEFAULT_EDITORS = {
515
564
  string: "textInput",
516
565
  text: "textarea",
566
+ code: "codeEditor",
517
567
  richtext: "richText",
518
568
  number: "numberInput",
519
569
  integer: "numberInput",
@@ -666,6 +716,66 @@ const effectiveLayout = (model) => {
666
716
  };
667
717
  /** The strip's properties: the configured ones, or the first `limit` that `hasValue` says are non-empty. */
668
718
  const stripFieldsOf = (layout, hasValue, limit = 5) => layout.strip ?? layout.properties.filter(hasValue).slice(0, limit);
719
+ /** How the model's entries open (`display.layout`): `document` unless set to `form`. */
720
+ const entryLayoutOf = (model) => model.display.layout ?? "document";
721
+ const ungroupedSectionId = (first, groupIds) => {
722
+ let candidate = `ungrouped-${first.id}`;
723
+ while (groupIds.has(candidate)) candidate = `~${candidate}`;
724
+ return candidate;
725
+ };
726
+ /**
727
+ * The entry form's layout (`display.layout: 'form'`): every live field, the title included, in sections.
728
+ * Field order decides everything: a group's section sits where its first field appears, with its fields in
729
+ * field order, and every run of consecutive ungrouped fields is an unlabelled section of its own at its
730
+ * place. So name, price, sku, description with a Pricing group (price, sku) reads [name], Pricing [price,
731
+ * sku], [description]. Unknown and deprecated ids are skipped. Pure: no values, no I/O.
732
+ */
733
+ const effectiveFormLayout = (model) => {
734
+ const groups = model.display.groups ?? [];
735
+ const groupIds = new Set(groups.map((group) => group.id));
736
+ const groupOf = /* @__PURE__ */ new Map();
737
+ for (const group of groups) for (const fieldId of group.fieldIds) if (!groupOf.has(fieldId)) groupOf.set(fieldId, group);
738
+ const sections = [];
739
+ const groupSections = /* @__PURE__ */ new Map();
740
+ let run;
741
+ for (const field of model.fields) {
742
+ if (field.deprecated) continue;
743
+ const group = groupOf.get(field.id);
744
+ if (!group) {
745
+ if (!run) {
746
+ run = {
747
+ id: ungroupedSectionId(field, groupIds),
748
+ label: void 0,
749
+ fields: []
750
+ };
751
+ sections.push(run);
752
+ }
753
+ run.fields.push(field);
754
+ continue;
755
+ }
756
+ run = void 0;
757
+ let section = groupSections.get(group.id);
758
+ if (!section) {
759
+ section = {
760
+ id: group.id,
761
+ label: group.label,
762
+ fields: []
763
+ };
764
+ groupSections.set(group.id, section);
765
+ sections.push(section);
766
+ }
767
+ section.fields.push(field);
768
+ }
769
+ return {
770
+ title: effectiveTitleField(model),
771
+ sections
772
+ };
773
+ };
774
+ /**
775
+ * The entry's rich-text body, which "Summarize from body" reads: the document's rich-text canvas fields, or
776
+ * every live rich-text field of a form (a form has no canvas; its fields are all on the page).
777
+ */
778
+ const richTextBodyOf = (model) => entryLayoutOf(model) === "form" ? model.fields.filter((field) => field.type === "richtext" && !field.deprecated) : effectiveLayout(model).canvas.filter((field) => field.type === "richtext");
669
779
  //#endregion
670
780
  //#region src/validators/apiKey.ts
671
781
  /**
@@ -715,6 +825,7 @@ const suggestPlural = (apiKey) => {
715
825
  //#endregion
716
826
  //#region src/normalize.ts
717
827
  const SETTINGS_DEFAULTS = {
828
+ code: { language: "plain" },
718
829
  richtext: { formatVersion: 1 },
719
830
  enum: { multiple: false },
720
831
  media: { multiple: false },
@@ -768,17 +879,22 @@ const normalizeField = (field) => {
768
879
  editor: {
769
880
  id: field.editor?.id ?? DEFAULT_EDITORS[field.type],
770
881
  options: field.editor?.options ?? {}
771
- }
882
+ },
883
+ width: field.width === "full" ? void 0 : field.width
772
884
  };
773
885
  return withoutEmpty(normalized);
774
886
  };
775
- const normalizeModelDisplay = (display) => withoutEmpty({ ...display });
887
+ const normalizeModelDisplay = (display) => withoutEmpty({
888
+ ...display,
889
+ layout: display?.layout === "document" ? void 0 : display?.layout
890
+ });
776
891
  const normalizeComponentDisplay = (display) => withoutEmpty({ ...display });
777
892
  /**
778
893
  * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
779
894
  * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
780
- * Empty optional strings and arrays are dropped. A collection without a plural API ID gets the suggested
781
- * one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
895
+ * Empty optional strings and arrays are dropped, and so are the defaults of keys added after definitions
896
+ * were first hashed (`width: 'full'`, `display.layout: 'document'`). A collection without a plural API ID
897
+ * gets the suggested one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
782
898
  */
783
899
  const normalizeDefinition = (input) => {
784
900
  const fields = input.fields.map(normalizeField);
@@ -823,6 +939,7 @@ const checkDefaultValue = (field) => {
823
939
  switch (field.type) {
824
940
  case "string":
825
941
  case "text":
942
+ case "code":
826
943
  case "email":
827
944
  case "url": return isString ? null : "must be a string";
828
945
  case "slug": return matches(SLUG_PATTERN) ? null : "must be a lower-case slug";
@@ -1167,6 +1284,7 @@ const checkSettings = (field, path, definition) => {
1167
1284
  if (!source) issues.push(issue(`${path}/settings/sourceFieldId`, "UNKNOWN_FIELD_REFERENCE", "refers to no field of this definition"));
1168
1285
  else if (source.type !== "string" && source.type !== "text") issues.push(issue(`${path}/settings/sourceFieldId`, "INVALID_FIELD_REFERENCE", "must refer to a string or text field"));
1169
1286
  }
1287
+ if (field.type === "code" && field.settings.validate && field.settings.language !== "json") issues.push(issue(`${path}/settings/validate`, "INVALID_SETTINGS", "only a json code field can be validated"));
1170
1288
  if (field.type === "richtext" && field.settings.formatVersion !== 1) issues.push(issue(`${path}/settings/formatVersion`, "INVALID_SETTINGS", "unsupported rich-text format version"));
1171
1289
  if (field.type === "relation" && !isStableId(field.settings.target)) issues.push(issue(`${path}/settings/target`, "INVALID_ID", "must be the ID of a model"));
1172
1290
  if (field.type === "component" && !isStableId(field.settings.component)) issues.push(issue(`${path}/settings/component`, "INVALID_ID", "must be the ID of a component"));
@@ -1189,6 +1307,7 @@ const checkFlags = (field, path, isComponent) => {
1189
1307
  "filterable",
1190
1308
  "sortable"
1191
1309
  ]) if (field[flag]) reject(flag, `fields of a component cannot be ${flag}`);
1310
+ if (field.width !== void 0) reject("width", "fields of a component have no width");
1192
1311
  }
1193
1312
  if (field.deprecated && field.required) reject("required", "a deprecated field cannot be required");
1194
1313
  return issues;
@@ -1642,7 +1761,8 @@ const DEFINITION_METADATA = [
1642
1761
  const FIELD_METADATA = [
1643
1762
  "label",
1644
1763
  "description",
1645
- "defaultValue"
1764
+ "defaultValue",
1765
+ "width"
1646
1766
  ];
1647
1767
  const FIELD_FLAGS = [
1648
1768
  ["required", "field.required"],
@@ -1813,6 +1933,7 @@ const hasIndex = (field) => Boolean(field && (field.filterable || field.sortable
1813
1933
  const API_SCALAR = {
1814
1934
  string: "String",
1815
1935
  text: "String",
1936
+ code: "String",
1816
1937
  slug: "String",
1817
1938
  email: "String",
1818
1939
  url: "String",
@@ -1834,6 +1955,12 @@ const API_SCALAR = {
1834
1955
  dynamiczone: "DynamicZone"
1835
1956
  };
1836
1957
  const PLAIN_TEXT_TYPES = /* @__PURE__ */ new Set(["string", "text"]);
1958
+ /** Types any string value is valid for, so a same-family change to one without constraints only widens. */
1959
+ const TEXT_TARGETS = /* @__PURE__ */ new Set([
1960
+ "string",
1961
+ "text",
1962
+ "code"
1963
+ ]);
1837
1964
  const SCALAR_FAMILIES = /* @__PURE__ */ new Set([
1838
1965
  "jsonString",
1839
1966
  "jsonNumber",
@@ -1842,23 +1969,23 @@ const SCALAR_FAMILIES = /* @__PURE__ */ new Set([
1842
1969
  ]);
1843
1970
  const hasTextConstraints = (field) => {
1844
1971
  const settings = field?.settings ?? {};
1845
- return settings.minLength !== void 0 || settings.maxLength !== void 0 || settings.pattern !== void 0;
1972
+ return settings.minLength !== void 0 || settings.maxLength !== void 0 || settings.pattern !== void 0 || settings.validate === true;
1846
1973
  };
1847
1974
  /**
1848
1975
  * A type change. Within one storage family the stored JSON stays as it is: widening is live, anything else
1849
1976
  * re-validates existing values. Across families Shapio converts values with a job, for the pairs it knows.
1850
1977
  */
1851
1978
  const classifyTypeChange = (from, to, after) => {
1852
- const breaking = API_SCALAR[from] !== API_SCALAR[to];
1979
+ const breaking = API_SCALAR[from] !== API_SCALAR[to] || to === "code" && from !== "code";
1853
1980
  const fromFamily = STORAGE_FAMILY[from];
1854
1981
  const toFamily = STORAGE_FAMILY[to];
1855
- if (fromFamily === toFamily) return fromFamily === "jsonString" && PLAIN_TEXT_TYPES.has(to) && !hasTextConstraints(after) || from === "integer" && to === "number" || from === "biginteger" && to === "decimal" && after?.type === "decimal" && after.settings.precision === void 0 ? result("additive", { breaking }) : result("validation", {
1982
+ if (fromFamily === toFamily) return fromFamily === "jsonString" && TEXT_TARGETS.has(to) && !hasTextConstraints(after) || from === "integer" && to === "number" || from === "biginteger" && to === "decimal" && after?.type === "decimal" && after.settings.precision === void 0 ? result("additive", { breaking }) : result("validation", {
1856
1983
  breaking,
1857
1984
  prerequisites: ["validateValues"]
1858
1985
  });
1859
1986
  const numericPair = fromFamily === "jsonNumber" && toFamily === "numericString" || fromFamily === "numericString" && toFamily === "jsonNumber";
1860
1987
  const toText = SCALAR_FAMILIES.has(fromFamily) && PLAIN_TEXT_TYPES.has(to);
1861
- const textToRichtext = fromFamily === "jsonString" && to === "richtext";
1988
+ const textToRichtext = fromFamily === "jsonString" && from !== "code" && to === "richtext";
1862
1989
  const richtextToText = from === "richtext" && PLAIN_TEXT_TYPES.has(to);
1863
1990
  const supported = numericPair || toText || textToRichtext || richtextToText;
1864
1991
  return result("conversion", {
@@ -1905,7 +2032,9 @@ const classifySettingsChange = (change) => {
1905
2032
  case "components":
1906
2033
  case "allowedKinds":
1907
2034
  case "protocols": return validation(to !== void 0 && (from === void 0 || removesAny(from, to)));
1908
- case "sourceFieldId": return result("metadata");
2035
+ case "sourceFieldId":
2036
+ case "language": return result("metadata");
2037
+ case "validate": return validation(to === true);
1909
2038
  case "multiple":
1910
2039
  case "repeatable":
1911
2040
  case "cardinality": return result("conversion", {
@@ -2367,6 +2496,17 @@ const plainTextToRichText = (text) => {
2367
2496
  };
2368
2497
  };
2369
2498
  //#endregion
2499
+ //#region src/richtext/plainText.ts
2500
+ /**
2501
+ * A stored rich text value (`{ format, version, doc }`) as one line of plain text: blocks separated by a
2502
+ * space, whitespace collapsed. For summaries (list cells, collapsed items, review rows), never for delivery.
2503
+ * Anything that is not a valid document (an unconverted value of a field that became rich text) reads as ''.
2504
+ */
2505
+ const richTextPlainText = (value) => {
2506
+ const result = validateRichText(value);
2507
+ return result.ok ? richTextToPlainText(result.document).replace(/\s+/g, " ").trim() : "";
2508
+ };
2509
+ //#endregion
2370
2510
  //#region src/themes/tokens.ts
2371
2511
  /**
2372
2512
  * Admin themes (docs/plans/themes.md): a theme is a named, complete set of these tokens for a light variant, a
@@ -2633,6 +2773,6 @@ const SeoDefaultsSchema = Type.Object({
2633
2773
  twitterHandle: Type.Union([Type.String({ pattern: SEO_TWITTER_HANDLE_PATTERN.source }), Type.Null()])
2634
2774
  }, closed);
2635
2775
  //#endregion
2636
- export { API_KEY_PATTERN, BIGINTEGER_PATTERN, BUILT_IN_THEME_KEYS, CANVAS_ELIGIBLE, CHANGE_KINDS, COMPONENT_TYPE_SUFFIXES, CUSTOM_EDITOR_ID_PATTERN, ComponentDefinitionInputSchema, ComponentDisplayInputSchema, DATA_TYPES, DATETIME_PATTERN, DATE_PATTERN, DECIMAL_PATTERN, DEFAULT_EDITORS, DEFAULT_SEO_KEYS, DEFINITION_KINDS, DefinitionInputSchema, EDITOR_CATALOGUE, EMPTY_SEO_DEFAULTS, EditorChoiceInputSchema, EnumValueSchema, FieldInputSchema, INLINE_TITLE_TYPES, LOCALE_CODE_PATTERN, LOCK_FILE_FORMAT_VERSION, LOCK_FILE_PATH, LocaleInputSchema, LockEntrySchema, LockFileError, LockFileSchema, MARK_RULES, MAX_API_KEY_LENGTH, MAX_COMPONENT_DEPTH, MAX_DESCRIPTION_LENGTH, MAX_FALLBACK_CHAIN, MAX_FIELDS_PER_DEFINITION, MAX_LABEL_LENGTH, MAX_RICHTEXT_DEPTH, MEDIA_KINDS, MODEL_KINDS, MODEL_TYPE_SUFFIXES, MUTATION_PREFIXES, ModelDefinitionInputSchema, ModelDisplayInputSchema, NODE_RULES, PLURAL_FALLBACK_SUFFIX, REFERENCE_DATA_TYPES, RELATION_CARDINALITIES, RESERVED_DEFINITION_API_KEYS, RESERVED_FIELD_API_KEYS, RESERVED_QUERY_NAMES, RESERVED_TYPE_NAMES, RICHTEXT_FORMAT, RICHTEXT_FORMAT_VERSION, SAFE_LINK_PATTERN, SCALAR_DATA_TYPES, SCHEMA_KIND_DIRECTORIES, SEO_COMPONENT_API_KEY, SEO_COMPONENT_ID, SEO_DESCRIPTION_SOFT_MAX, SEO_EDITOR_ID, SEO_FIELD_IDS, SEO_TITLE_PLACEHOLDER, SEO_TITLE_SOFT_MAX, SEO_TITLE_TEMPLATE_PATTERN, SEO_TWITTER_HANDLE_PATTERN, SETTINGS_SCHEMAS, SITES_DIRECTORY, SLUG_PATTERN, SORT_DIRECTIONS, STABLE_ID_PATTERN, STORAGE_FAMILY, STRIP_DEFAULT_COUNT, SchemaValidationError, SeoDefaultsSchema, SeoLocaleDefaultsSchema, TEXT_TITLE_FIELD_TYPES, THEME_BRAND_TOKENS, THEME_COLOUR_PATTERN, THEME_DARK_UI_PAIRS, THEME_KEY_PATTERN, THEME_SEMANTIC_TOKENS, THEME_TEXT_PAIRS, THEME_TOKENS, THEME_UI_PAIRS, THEME_VARIANTS, TIME_PATTERN, TITLE_FIELD_TYPES, UID_PATTERN, UNIQUE_CAPABLE_DATA_TYPES, applyTitleTemplate, assignIds, canonicalJson, checkApiKeySyntax, checkDefaultValue, checkThemeContrast, classifyChange, classifyChanges, classifyLocaleChange, collectionQueryName, contrastRatio, createStableId, decideSync, diffDefinitions, effectiveLayout, effectiveTitleField, embeddedComponentIds, escapeHtml, findDependentModels, findReferencingDefinitions, foldApiKey, generatedNames, hashDefinition, isApiKeySyntaxValid, isCanvasEligible, isComponentDefinition, isCoverEligible, isCustomEditorId, isDataType, isDocumentPlaceable, isEmptyRichText, isModelDefinition, isSeoField, isStableId, isValidTitleTemplate, issue, listCompatibleEditors, lockCoversSite, lockEntriesForSite, mergePulledLock, normalizeDefinition, parseDefinition, parseLockFile, plainTextToRichText, renderRichTextHtml, renormalizeDefinition, resolveSeo, richTextMediaIds, richTextToPlainText, routeKeyOf, schemaFilePath, scopeDirectory, scopeOfSchemaFilePath, seoComponentDefinition, seoDefaultsForLocale, serializeDefinition, serializeLockFile, sha256, stripFieldsOf, suggestPlural, summarizeChanges, toTypeName, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
2776
+ export { API_KEY_PATTERN, BIGINTEGER_PATTERN, BUILT_IN_THEME_KEYS, CANVAS_ELIGIBLE, CHANGE_KINDS, CODE_LANGUAGES, COMPONENT_TYPE_SUFFIXES, CUSTOM_EDITOR_ID_PATTERN, ComponentDefinitionInputSchema, ComponentDisplayInputSchema, DATA_TYPES, DATETIME_PATTERN, DATE_PATTERN, DECIMAL_PATTERN, DEFAULT_EDITORS, DEFAULT_SEO_KEYS, DEFINITION_KINDS, DefinitionInputSchema, EDITOR_CATALOGUE, EMPTY_SEO_DEFAULTS, ENTRY_LAYOUTS, EditorChoiceInputSchema, EnumValueSchema, FIELD_WIDTHS, FieldInputSchema, INLINE_TITLE_TYPES, LOCALE_CODE_PATTERN, LOCK_FILE_FORMAT_VERSION, LOCK_FILE_PATH, LocaleInputSchema, LockEntrySchema, LockFileError, LockFileSchema, MARK_RULES, MAX_API_KEY_LENGTH, MAX_COMPONENT_DEPTH, MAX_DESCRIPTION_LENGTH, MAX_FALLBACK_CHAIN, MAX_FIELDS_PER_DEFINITION, MAX_LABEL_LENGTH, MAX_RICHTEXT_DEPTH, MEDIA_KINDS, MODEL_KINDS, MODEL_TYPE_SUFFIXES, MUTATION_PREFIXES, ModelDefinitionInputSchema, ModelDisplayInputSchema, NODE_RULES, PLURAL_FALLBACK_SUFFIX, REFERENCE_DATA_TYPES, RELATION_CARDINALITIES, RESERVED_DEFINITION_API_KEYS, RESERVED_FIELD_API_KEYS, RESERVED_QUERY_NAMES, RESERVED_TYPE_NAMES, RICHTEXT_FORMAT, RICHTEXT_FORMAT_VERSION, SAFE_LINK_PATTERN, SCALAR_DATA_TYPES, SCHEMA_KIND_DIRECTORIES, SEO_COMPONENT_API_KEY, SEO_COMPONENT_ID, SEO_DESCRIPTION_SOFT_MAX, SEO_EDITOR_ID, SEO_FIELD_IDS, SEO_TITLE_PLACEHOLDER, SEO_TITLE_SOFT_MAX, SEO_TITLE_TEMPLATE_PATTERN, SEO_TWITTER_HANDLE_PATTERN, SETTINGS_SCHEMAS, SITES_DIRECTORY, SLUG_PATTERN, SORT_DIRECTIONS, STABLE_ID_PATTERN, STORAGE_FAMILY, STRIP_DEFAULT_COUNT, SchemaValidationError, SeoDefaultsSchema, SeoLocaleDefaultsSchema, TEXT_TITLE_FIELD_TYPES, THEME_BRAND_TOKENS, THEME_COLOUR_PATTERN, THEME_DARK_UI_PAIRS, THEME_KEY_PATTERN, THEME_SEMANTIC_TOKENS, THEME_TEXT_PAIRS, THEME_TOKENS, THEME_UI_PAIRS, THEME_VARIANTS, TIME_PATTERN, TITLE_FIELD_TYPES, UID_PATTERN, UNIQUE_CAPABLE_DATA_TYPES, applyTitleTemplate, assignIds, canonicalJson, checkApiKeySyntax, checkDefaultValue, checkThemeContrast, classifyChange, classifyChanges, classifyLocaleChange, collectionQueryName, contrastRatio, createStableId, decideSync, diffDefinitions, effectiveFormLayout, effectiveLayout, effectiveTitleField, embeddedComponentIds, entryLayoutOf, escapeHtml, findDependentModels, findReferencingDefinitions, foldApiKey, generatedNames, hashDefinition, isApiKeySyntaxValid, isCanvasEligible, isComponentDefinition, isCoverEligible, isCustomEditorId, isDataType, isDocumentPlaceable, isEmptyRichText, isModelDefinition, isSeoField, isStableId, isValidTitleTemplate, issue, listCompatibleEditors, lockCoversSite, lockEntriesForSite, mergePulledLock, normalizeDefinition, parseDefinition, parseLockFile, plainTextToRichText, renderRichTextHtml, renormalizeDefinition, resolveSeo, richTextBodyOf, richTextMediaIds, richTextPlainText, richTextToPlainText, routeKeyOf, schemaFilePath, scopeDirectory, scopeOfSchemaFilePath, seoComponentDefinition, seoDefaultsForLocale, serializeDefinition, serializeLockFile, sha256, stripFieldsOf, suggestPlural, summarizeChanges, toTypeName, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
2637
2777
 
2638
2778
  //# sourceMappingURL=index.js.map