@shapio/schema 0.5.4 → 0.5.6

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
@@ -181,6 +181,19 @@ export declare const DEFINITION_KINDS: readonly ["collection", "singleton", "com
181
181
  export type DefinitionKind = (typeof DEFINITION_KINDS)[number];
182
182
  export declare const SORT_DIRECTIONS: readonly ["asc", "desc"];
183
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];
184
197
  export declare const MAX_FIELDS_PER_DEFINITION = 500;
185
198
  export declare const MAX_LABEL_LENGTH = 200;
186
199
  export declare const MAX_DESCRIPTION_LENGTH = 2000;
@@ -213,8 +226,15 @@ export declare const FieldInputSchema: Type.TObject<{
213
226
  id: Type.TString;
214
227
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
215
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"]>>;
216
234
  }>;
217
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"]>>;
218
238
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
219
239
  titleFieldId: Type.TOptional<Type.TString>;
220
240
  /** Columns of the content list, in order. */
@@ -255,6 +275,8 @@ export declare const ModelDefinitionInputSchema: Type.TObject<{
255
275
  /** Entries have separate draft and published states. Default true. */
256
276
  draftAndPublish: Type.TOptional<Type.TBoolean>;
257
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"]>>;
258
280
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
259
281
  titleFieldId: Type.TOptional<Type.TString>;
260
282
  /** Columns of the content list, in order. */
@@ -305,6 +327,11 @@ export declare const ModelDefinitionInputSchema: Type.TObject<{
305
327
  id: Type.TString;
306
328
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
307
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"]>>;
308
335
  }>>;
309
336
  }>;
310
337
  export declare const ComponentDefinitionInputSchema: Type.TObject<{
@@ -348,6 +375,11 @@ export declare const ComponentDefinitionInputSchema: Type.TObject<{
348
375
  id: Type.TString;
349
376
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
350
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"]>>;
351
383
  }>>;
352
384
  }>;
353
385
  export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
@@ -361,6 +393,8 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
361
393
  /** Entries have separate draft and published states. Default true. */
362
394
  draftAndPublish: Type.TOptional<Type.TBoolean>;
363
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"]>>;
364
398
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
365
399
  titleFieldId: Type.TOptional<Type.TString>;
366
400
  /** Columns of the content list, in order. */
@@ -411,6 +445,11 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
411
445
  id: Type.TString;
412
446
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
413
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"]>>;
414
453
  }>>;
415
454
  }>, Type.TObject<{
416
455
  kind: Type.TLiteral<"component">;
@@ -453,6 +492,11 @@ export declare const DefinitionInputSchema: Type.TUnion<[Type.TObject<{
453
492
  id: Type.TString;
454
493
  options: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
455
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"]>>;
456
500
  }>>;
457
501
  }>]>;
458
502
  export type FieldInput = Static<typeof FieldInputSchema>;
@@ -477,6 +521,8 @@ type FieldBase = {
477
521
  deprecated: boolean;
478
522
  defaultValue?: JsonValue;
479
523
  editor: EditorChoice;
524
+ /** Width in a form-layout entry; absent means `full`. */
525
+ width?: FieldWidth;
480
526
  };
481
527
  /** A normalized field. Narrow on `type` to get typed `settings`. */
482
528
  export type FieldDefinition<T extends DataType = DataType> = { [K in T]: FieldBase & {
@@ -489,6 +535,8 @@ export type FieldGroup = {
489
535
  fieldIds: string[];
490
536
  };
491
537
  export type ModelDisplay = {
538
+ /** Absent means `document`. */
539
+ layout?: EntryLayout;
492
540
  titleFieldId?: string;
493
541
  listFieldIds?: string[];
494
542
  defaultSort?: {
@@ -657,6 +705,34 @@ export type DocumentLayout = {
657
705
  export declare const effectiveLayout: (model: ModelDefinition) => DocumentLayout;
658
706
  /** The strip's properties: the configured ones, or the first `limit` that `hasValue` says are non-empty. */
659
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[];
660
736
  //#endregion
661
737
  //#region src/display/titleField.d.ts
662
738
  /** Field types that may label entries: the only types `display.titleFieldId` accepts, and the fallback's. */
@@ -701,8 +777,9 @@ export type DefinitionInputWithIds = (WithIds<ModelDefinitionInput> & {
701
777
  /**
702
778
  * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
703
779
  * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
704
- * Empty optional strings and arrays are dropped. A collection without a plural API ID gets the suggested
705
- * 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.
706
783
  */
707
784
  export declare const normalizeDefinition: (input: DefinitionInputWithIds) => SchemaDefinition;
708
785
  /** Normalizes a definition that is already normalized-shaped (e.g. read back from the database). */
@@ -905,6 +982,7 @@ export declare const diffDefinitions: (before: SchemaDefinition | null, after: S
905
982
  * | (plus) type change of a unique field | as its type change | + checkUnique |
906
983
  * | (plus) tightened validation rules | validation | validateValues |
907
984
  * | (plus) localized true→false | conversion (destructive) | convert |
985
+ * | (plus) type change Shapio can't convert | conversion (destructive, drops) | convert (+ validate/required) |
908
986
  */
909
987
  export type ChangeCategory = 'metadata' | 'editorSwap' | 'additive' | 'requiredField' | 'validation' | 'conversion' | 'contract' | 'removal' | 'constraint' | 'index';
910
988
  /** Work that must succeed before activation. Content-touching kinds run through the content ports. */
@@ -917,8 +995,10 @@ export type ClassifiedChange = SchemaChange & {
917
995
  breaking: boolean;
918
996
  /** Loses stored information that a schema rollback cannot restore. */
919
997
  destructive: boolean;
920
- /** False for conversions Shapio cannot perform; the planner refuses them. */
998
+ /** False for changes Shapio cannot perform; the planner refuses them. None today; kept for future refusals. */
921
999
  supported: boolean;
1000
+ /** A type change Shapio cannot convert: every stored value of the field is dropped (destructive). */
1001
+ dropsValues: boolean;
922
1002
  prerequisites: PrerequisiteKind[];
923
1003
  cleanup: CleanupKind[];
924
1004
  };
@@ -926,6 +1006,11 @@ export type ClassifyContext = {
926
1006
  before: SchemaDefinition | null;
927
1007
  after: SchemaDefinition | null;
928
1008
  };
1009
+ /**
1010
+ * A type change Shapio cannot convert: the field keeps its stable ID and API ID, and every stored value is
1011
+ * dropped. The planner (`classifyChange`) and the content conversion share this one rule.
1012
+ */
1013
+ export declare const typeChangeDropsValues: (from: DataType, to: DataType) => boolean;
929
1014
  /** Classifies one change in the context of the definitions it was diffed from. */
930
1015
  export declare const classifyChange: (change: SchemaChange, context: ClassifyContext) => ClassifiedChange;
931
1016
  export declare const classifyChanges: (changes: readonly SchemaChange[], context: ClassifyContext) => ClassifiedChange[];
@@ -1180,6 +1265,14 @@ export declare const richTextToPlainText: (document: RichTextDocument) => string
1180
1265
  /** Plain text → a document of paragraphs (blank lines split paragraphs; single newlines are hard breaks). */
1181
1266
  export declare const plainTextToRichText: (text: string) => RichTextDocument;
1182
1267
  //#endregion
1268
+ //#region src/richtext/plainText.d.ts
1269
+ /**
1270
+ * A stored rich text value (`{ format, version, doc }`) as one line of plain text: blocks separated by a
1271
+ * space, whitespace collapsed. For summaries (list cells, collapsed items, review rows), never for delivery.
1272
+ * Anything that is not a valid document (an unconverted value of a field that became rich text) reads as ''.
1273
+ */
1274
+ export declare const richTextPlainText: (value: unknown) => string;
1275
+ //#endregion
1183
1276
  //#region src/themes/tokens.d.ts
1184
1277
  /**
1185
1278
  * 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
@@ -110,6 +110,22 @@ const closed$3 = { additionalProperties: false };
110
110
  const MODEL_KINDS = ["collection", "singleton"];
111
111
  const DEFINITION_KINDS = [...MODEL_KINDS, "component"];
112
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
+ ];
113
129
  const MAX_FIELDS_PER_DEFINITION = 500;
114
130
  const MAX_LABEL_LENGTH = 200;
115
131
  const MAX_DESCRIPTION_LENGTH = 2e3;
@@ -146,7 +162,12 @@ const FieldInputSchema = Type.Object({
146
162
  defaultValue: Type.Optional(Type.Unknown()),
147
163
  /** Type-specific settings, validated against SETTINGS_SCHEMAS[type]. */
148
164
  settings: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
149
- 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))
150
171
  }, closed$3);
151
172
  const FieldGroupSchema = Type.Object({
152
173
  id: Type.String({
@@ -157,6 +178,8 @@ const FieldGroupSchema = Type.Object({
157
178
  fieldIds: Type.Array(Type.String())
158
179
  }, closed$3);
159
180
  const ModelDisplayInputSchema = Type.Object({
181
+ /** How entries open: `document` (default) or `form`. See `ENTRY_LAYOUTS`. */
182
+ layout: Type.Optional(Type.Enum(ENTRY_LAYOUTS)),
160
183
  /** Field whose value labels an entry in lists, pickers and relation summaries. */
161
184
  titleFieldId: Type.Optional(Type.String()),
162
185
  /** Columns of the content list, in order. */
@@ -693,6 +716,66 @@ const effectiveLayout = (model) => {
693
716
  };
694
717
  /** The strip's properties: the configured ones, or the first `limit` that `hasValue` says are non-empty. */
695
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");
696
779
  //#endregion
697
780
  //#region src/validators/apiKey.ts
698
781
  /**
@@ -796,17 +879,22 @@ const normalizeField = (field) => {
796
879
  editor: {
797
880
  id: field.editor?.id ?? DEFAULT_EDITORS[field.type],
798
881
  options: field.editor?.options ?? {}
799
- }
882
+ },
883
+ width: field.width === "full" ? void 0 : field.width
800
884
  };
801
885
  return withoutEmpty(normalized);
802
886
  };
803
- const normalizeModelDisplay = (display) => withoutEmpty({ ...display });
887
+ const normalizeModelDisplay = (display) => withoutEmpty({
888
+ ...display,
889
+ layout: display?.layout === "document" ? void 0 : display?.layout
890
+ });
804
891
  const normalizeComponentDisplay = (display) => withoutEmpty({ ...display });
805
892
  /**
806
893
  * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
807
894
  * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
808
- * Empty optional strings and arrays are dropped. A collection without a plural API ID gets the suggested
809
- * 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.
810
898
  */
811
899
  const normalizeDefinition = (input) => {
812
900
  const fields = input.fields.map(normalizeField);
@@ -1219,6 +1307,7 @@ const checkFlags = (field, path, isComponent) => {
1219
1307
  "filterable",
1220
1308
  "sortable"
1221
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");
1222
1311
  }
1223
1312
  if (field.deprecated && field.required) reject("required", "a deprecated field cannot be required");
1224
1313
  return issues;
@@ -1672,7 +1761,8 @@ const DEFINITION_METADATA = [
1672
1761
  const FIELD_METADATA = [
1673
1762
  "label",
1674
1763
  "description",
1675
- "defaultValue"
1764
+ "defaultValue",
1765
+ "width"
1676
1766
  ];
1677
1767
  const FIELD_FLAGS = [
1678
1768
  ["required", "field.required"],
@@ -1833,6 +1923,7 @@ const result = (category, extra = {}) => ({
1833
1923
  breaking: false,
1834
1924
  destructive: false,
1835
1925
  supported: true,
1926
+ dropsValues: false,
1836
1927
  prerequisites: [],
1837
1928
  cleanup: [],
1838
1929
  ...extra
@@ -1882,27 +1973,54 @@ const hasTextConstraints = (field) => {
1882
1973
  return settings.minLength !== void 0 || settings.maxLength !== void 0 || settings.pattern !== void 0 || settings.validate === true;
1883
1974
  };
1884
1975
  /**
1976
+ * Whether Shapio converts a cross-family type change value by value (the pairs it knows); within one storage
1977
+ * family the stored JSON stays as it is.
1978
+ */
1979
+ const convertsAcrossFamilies = (from, to) => {
1980
+ const fromFamily = STORAGE_FAMILY[from];
1981
+ const toFamily = STORAGE_FAMILY[to];
1982
+ const numericPair = fromFamily === "jsonNumber" && toFamily === "numericString" || fromFamily === "numericString" && toFamily === "jsonNumber";
1983
+ const toText = SCALAR_FAMILIES.has(fromFamily) && PLAIN_TEXT_TYPES.has(to);
1984
+ const textToRichtext = fromFamily === "jsonString" && from !== "code" && to === "richtext";
1985
+ const richtextToText = from === "richtext" && PLAIN_TEXT_TYPES.has(to);
1986
+ return numericPair || toText || textToRichtext || richtextToText;
1987
+ };
1988
+ /**
1989
+ * A type change Shapio cannot convert: the field keeps its stable ID and API ID, and every stored value is
1990
+ * dropped. The planner (`classifyChange`) and the content conversion share this one rule.
1991
+ */
1992
+ const typeChangeDropsValues = (from, to) => STORAGE_FAMILY[from] !== STORAGE_FAMILY[to] && !convertsAcrossFamilies(from, to);
1993
+ /** A required field whose values are dropped is backfilled from its default, else checked (no silent nulls). */
1994
+ const requiredStep = (field) => {
1995
+ if (!field?.required) return [];
1996
+ return [field.defaultValue !== void 0 ? "backfill" : "validateRequired"];
1997
+ };
1998
+ /**
1885
1999
  * A type change. Within one storage family the stored JSON stays as it is: widening is live, anything else
1886
- * re-validates existing values. Across families Shapio converts values with a job, for the pairs it knows.
2000
+ * re-validates existing values. Across families Shapio converts values with a job, for the pairs it knows;
2001
+ * any other pair drops the values (destructive, acknowledged in the review).
1887
2002
  */
1888
2003
  const classifyTypeChange = (from, to, after) => {
1889
2004
  const breaking = API_SCALAR[from] !== API_SCALAR[to] || to === "code" && from !== "code";
1890
2005
  const fromFamily = STORAGE_FAMILY[from];
1891
- const toFamily = STORAGE_FAMILY[to];
1892
- 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", {
2006
+ if (fromFamily === STORAGE_FAMILY[to]) 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", {
1893
2007
  breaking,
1894
2008
  prerequisites: ["validateValues"]
1895
2009
  });
1896
- const numericPair = fromFamily === "jsonNumber" && toFamily === "numericString" || fromFamily === "numericString" && toFamily === "jsonNumber";
1897
- const toText = SCALAR_FAMILIES.has(fromFamily) && PLAIN_TEXT_TYPES.has(to);
1898
- const textToRichtext = fromFamily === "jsonString" && from !== "code" && to === "richtext";
1899
- const richtextToText = from === "richtext" && PLAIN_TEXT_TYPES.has(to);
1900
- const supported = numericPair || toText || textToRichtext || richtextToText;
2010
+ if (typeChangeDropsValues(from, to)) return result("conversion", {
2011
+ breaking,
2012
+ destructive: true,
2013
+ dropsValues: true,
2014
+ prerequisites: [
2015
+ "convert",
2016
+ "validateValues",
2017
+ ...requiredStep(after)
2018
+ ]
2019
+ });
1901
2020
  return result("conversion", {
1902
2021
  breaking,
1903
- supported,
1904
- destructive: richtextToText,
1905
- prerequisites: supported ? ["convert", "validateValues"] : []
2022
+ destructive: from === "richtext" && PLAIN_TEXT_TYPES.has(to),
2023
+ prerequisites: ["convert", "validateValues"]
1906
2024
  });
1907
2025
  };
1908
2026
  const compareValues = (a, b) => {
@@ -2406,6 +2524,17 @@ const plainTextToRichText = (text) => {
2406
2524
  };
2407
2525
  };
2408
2526
  //#endregion
2527
+ //#region src/richtext/plainText.ts
2528
+ /**
2529
+ * A stored rich text value (`{ format, version, doc }`) as one line of plain text: blocks separated by a
2530
+ * space, whitespace collapsed. For summaries (list cells, collapsed items, review rows), never for delivery.
2531
+ * Anything that is not a valid document (an unconverted value of a field that became rich text) reads as ''.
2532
+ */
2533
+ const richTextPlainText = (value) => {
2534
+ const result = validateRichText(value);
2535
+ return result.ok ? richTextToPlainText(result.document).replace(/\s+/g, " ").trim() : "";
2536
+ };
2537
+ //#endregion
2409
2538
  //#region src/themes/tokens.ts
2410
2539
  /**
2411
2540
  * Admin themes (docs/plans/themes.md): a theme is a named, complete set of these tokens for a light variant, a
@@ -2672,6 +2801,6 @@ const SeoDefaultsSchema = Type.Object({
2672
2801
  twitterHandle: Type.Union([Type.String({ pattern: SEO_TWITTER_HANDLE_PATTERN.source }), Type.Null()])
2673
2802
  }, closed);
2674
2803
  //#endregion
2675
- 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, 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 };
2804
+ 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, typeChangeDropsValues, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
2676
2805
 
2677
2806
  //# sourceMappingURL=index.js.map