@namroodinc/liquid-editorial 0.1.10 → 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.
Files changed (88) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +32 -16
  3. package/UPGRADING.md +68 -0
  4. package/dist/chunks/FormEditor-8bz3pOdP.cjs +2 -0
  5. package/dist/chunks/FormEditor-8bz3pOdP.cjs.map +1 -0
  6. package/dist/chunks/FormEditor-DuW8x78Y.js +330 -0
  7. package/dist/chunks/FormEditor-DuW8x78Y.js.map +1 -0
  8. package/dist/components/AppShell/index.cjs +1 -1
  9. package/dist/components/AppShell/index.cjs.map +1 -1
  10. package/dist/components/AppShell/index.d.cts.map +1 -1
  11. package/dist/components/AppShell/index.d.ts.map +1 -1
  12. package/dist/components/AppShell/index.js +1 -0
  13. package/dist/components/AppShell/index.js.map +1 -1
  14. package/dist/components/Badge/style.css +24 -21
  15. package/dist/components/Drawer/index.cjs +1 -1
  16. package/dist/components/Drawer/index.cjs.map +1 -1
  17. package/dist/components/Drawer/index.d.cts +3 -2
  18. package/dist/components/Drawer/index.d.cts.map +1 -1
  19. package/dist/components/Drawer/index.d.ts +3 -2
  20. package/dist/components/Drawer/index.d.ts.map +1 -1
  21. package/dist/components/Drawer/index.js +11 -10
  22. package/dist/components/Drawer/index.js.map +1 -1
  23. package/dist/components/Drawer/style.css +37 -79
  24. package/dist/components/EntryPicker/index.cjs +1 -1
  25. package/dist/components/EntryPicker/index.cjs.map +1 -1
  26. package/dist/components/EntryPicker/index.d.cts +3 -1
  27. package/dist/components/EntryPicker/index.d.cts.map +1 -1
  28. package/dist/components/EntryPicker/index.d.ts +3 -1
  29. package/dist/components/EntryPicker/index.d.ts.map +1 -1
  30. package/dist/components/EntryPicker/index.js +45 -43
  31. package/dist/components/EntryPicker/index.js.map +1 -1
  32. package/dist/components/FormEditor/control.d.cts +15 -0
  33. package/dist/components/FormEditor/control.d.cts.map +1 -0
  34. package/dist/components/FormEditor/control.d.ts +15 -0
  35. package/dist/components/FormEditor/control.d.ts.map +1 -0
  36. package/dist/components/FormEditor/index.cjs +1 -0
  37. package/dist/components/FormEditor/index.d.cts +28 -0
  38. package/dist/components/FormEditor/index.d.cts.map +1 -0
  39. package/dist/components/FormEditor/index.d.ts +28 -0
  40. package/dist/components/FormEditor/index.d.ts.map +1 -0
  41. package/dist/components/FormEditor/index.js +2 -0
  42. package/dist/components/FormEditor/model.d.cts +86 -0
  43. package/dist/components/FormEditor/model.d.cts.map +1 -0
  44. package/dist/components/FormEditor/model.d.ts +86 -0
  45. package/dist/components/FormEditor/model.d.ts.map +1 -0
  46. package/dist/components/FormEditor/style.css +25 -0
  47. package/dist/components/NumberField/index.cjs +1 -1
  48. package/dist/components/NumberField/index.cjs.map +1 -1
  49. package/dist/components/NumberField/index.d.cts +3 -0
  50. package/dist/components/NumberField/index.d.cts.map +1 -1
  51. package/dist/components/NumberField/index.d.ts +3 -0
  52. package/dist/components/NumberField/index.d.ts.map +1 -1
  53. package/dist/components/NumberField/index.js +20 -20
  54. package/dist/components/NumberField/index.js.map +1 -1
  55. package/dist/components/RichTextEditor/index.cjs +1 -1
  56. package/dist/components/RichTextEditor/index.js +0 -1
  57. package/dist/components/SelectionCard/index.cjs +1 -1
  58. package/dist/components/SelectionCard/index.cjs.map +1 -1
  59. package/dist/components/SelectionCard/index.d.cts +4 -1
  60. package/dist/components/SelectionCard/index.d.cts.map +1 -1
  61. package/dist/components/SelectionCard/index.d.ts +4 -1
  62. package/dist/components/SelectionCard/index.d.ts.map +1 -1
  63. package/dist/components/SelectionCard/index.js +3 -3
  64. package/dist/components/SelectionCard/index.js.map +1 -1
  65. package/dist/index.cjs +1 -1
  66. package/dist/index.d.cts +2 -2
  67. package/dist/index.d.cts.map +1 -1
  68. package/dist/index.d.ts +2 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +20 -20
  71. package/dist/styles.css +1 -1
  72. package/dist/tokens.css +15 -5
  73. package/package.json +1 -1
  74. package/dist/chunks/EntryEditor-CcyYtH74.js +0 -191
  75. package/dist/chunks/EntryEditor-CcyYtH74.js.map +0 -1
  76. package/dist/chunks/EntryEditor-DIpc1iPF.cjs +0 -2
  77. package/dist/chunks/EntryEditor-DIpc1iPF.cjs.map +0 -1
  78. package/dist/components/EntryEditor/index.cjs +0 -1
  79. package/dist/components/EntryEditor/index.d.cts +0 -21
  80. package/dist/components/EntryEditor/index.d.cts.map +0 -1
  81. package/dist/components/EntryEditor/index.d.ts +0 -21
  82. package/dist/components/EntryEditor/index.d.ts.map +0 -1
  83. package/dist/components/EntryEditor/index.js +0 -6
  84. package/dist/components/EntryEditor/model.d.cts +0 -33
  85. package/dist/components/EntryEditor/model.d.cts.map +0 -1
  86. package/dist/components/EntryEditor/model.d.ts +0 -33
  87. package/dist/components/EntryEditor/model.d.ts.map +0 -1
  88. package/dist/components/EntryEditor/style.css +0 -8
package/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ End every completed change handoff with a suggested Git commit subject and descr
8
8
 
9
9
  Render button controls through the shared `Button` component, including controls inside other components and examples. Native button markup belongs only in `src/components/Button` (isolated test fixtures may use native controls). Use Button's standard variants, sizes, icons, focus, and disabled styling instead of copying them into a component stylesheet. Specialized widgets may retain their semantic layout, shared Field skin, or switch track/knob geometry.
10
10
 
11
- Every releasable change set must update the `Unreleased` section in `UPGRADING.md` with changes, adoption steps, compatibility notes, and verification steps for agents in consuming projects. Related edits share that section. Leave version increments to `.github/workflows/release.yml`: every push to `main` automatically bumps the patch version, versions the upgrade notes, validates, commits the release files, and publishes to npm. Keep the manifest and lockfile versions aligned and include this file and `UPGRADING.md` in the package.
11
+ Every releasable change set must update the `Unreleased` section in `UPGRADING.md` with changes, adoption steps, compatibility notes, and verification steps for agents in consuming projects. A change set that removes or renames public API also lists it under a `### Breaking changes` heading there. Related edits share that section. Leave version increments to `.github/workflows/release.yml`: every push to `main` automatically bumps the version (patch, or minor below 1.0 and major from 1.0 when `Unreleased` has `### Breaking changes`), versions the upgrade notes, validates, commits the release files, and publishes to npm. Keep the manifest and lockfile versions aligned and include this file and `UPGRADING.md` in the package.
12
12
 
13
13
  ## Using the installed package
14
14
 
package/README.md CHANGED
@@ -49,20 +49,26 @@ Pending Buttons replace custom glyphs with the shared spinner. Button-backed sel
49
49
 
50
50
  If an application only needs the design tokens, import `@namroodinc/liquid-editorial/tokens.css`. A small global normalization is available separately at `@namroodinc/liquid-editorial/reset.css`; it is never included automatically.
51
51
 
52
- ## Content models
52
+ ## Badges
53
53
 
54
- `EntryEditor` renders editable fields in the order defined by a JSON model:
54
+ `Badge` is a non-interactive editorial label. Use `shape="status"` with one of the six publication tones, `shape="tag"` (the default) for metadata, and `shape="count"` for totals. Status labels and tags display uppercase with 4px corners; status labels add 6px square markers. Counts keep 12px untracked, tabular numerals; zero renders nothing and values above 99 display `99+`. The original child text remains unchanged in the markup.
55
+
56
+ Use `appearance="outline"` (the default) on board and selection cards, `appearance="soft"` in dense tables and toolbar counts, and `appearance="solid"` for the one state a row or tile emphasizes, such as a Cover asset badge. Soft badges have no visible stroke. Tags inside sentences also display uppercase, including path labels such as `/news`.
57
+
58
+ ## Forms and content models
59
+
60
+ `FormEditor` renders standard fields in schema order. Applications own state, choice loading, permissions, submission, and persistence:
55
61
 
56
62
  ```tsx
57
63
  import { useState } from "react";
58
64
  import {
59
- EntryEditor,
60
- validateEntryValues,
61
- type ContentModel,
62
- type EntryValues,
65
+ FormEditor,
66
+ validateFormValues,
67
+ type FormSchema,
68
+ type FormValues,
63
69
  } from "@namroodinc/liquid-editorial";
64
70
 
65
- const model: ContentModel = {
71
+ const schema: FormSchema = {
66
72
  fields: [
67
73
  { id: "title", type: "shortText", label: "Title", required: true },
68
74
  { id: "gallery", type: "asset", label: "Gallery", multiple: true },
@@ -70,10 +76,10 @@ const model: ContentModel = {
70
76
  };
71
77
 
72
78
  export function ContentEditor() {
73
- const [value, setValue] = useState<EntryValues>({});
79
+ const [value, setValue] = useState<FormValues>({});
74
80
  return (
75
- <EntryEditor
76
- model={model}
81
+ <FormEditor
82
+ schema={schema}
77
83
  value={value}
78
84
  onValueChange={setValue}
79
85
  pickerItems={{ gallery: [{ id: "image-1", title: "Cover.jpg" }] }}
@@ -81,12 +87,18 @@ export function ContentEditor() {
81
87
  );
82
88
  }
83
89
 
84
- // Before saving: validateEntryValues(model, value) returns errors keyed by field ID.
90
+ // Before saving: validateFormValues(schema, value) returns errors keyed by field ID.
85
91
  ```
86
92
 
87
- Supported types are `shortText`, `longText`, `richText`, `number`, `boolean`, `dateTime`, `asset`, and `reference`. Rich text stores a TipTap JSON document rooted at `doc`, using the editor's existing StarterKit and Link schema. Dates are edited in UTC and emitted as ISO-8601 strings ending in `Z`. Assets store `{ assetId }`; references store `{ entryId }`. Set `multiple: true` for an ordered array of these objects.
93
+ Supported types are `shortText`, `longText`, `richText`, `number`, `boolean`, `dateTime`, `asset`, `reference`, and string-valued `select`. Rich text stores a TipTap JSON document rooted at `doc`, using the editor's existing StarterKit and Link schema. Dates are edited in UTC and emitted as ISO-8601 strings ending in `Z`. Assets store `{ assetId }`; references store `{ entryId }`. Set `multiple: true` for an ordered array of these objects.
94
+
95
+ Use `defaultValue` for an uncontrolled editor, `errors` for app-specific messages, `showAllErrors` after a submit attempt, and `isDisabled` to disable editing. Required and format errors appear after a field changes; an out-of-range number shows its message straight away, because the control already reads invalid. `false` and `0` are populated values. Missing keys render empty controls without emitting changes. Clearing a number, date, select, or single selection removes its key; empty text, rich-text documents, and lists keep their typed values. Unrelated entry keys are preserved. Validation runs per field and is reused until that field or its value changes, so define schemas outside render or memoize them.
88
96
 
89
- Use `defaultValue` for an uncontrolled editor, `errors` for app-specific messages, `showAllErrors` after a submit attempt, and `isDisabled` to disable editing. Required errors appear after a field changes; `false` and `0` are populated values. Missing keys render empty controls without emitting changes. Clearing a number, date, or single selection removes its key; empty text, rich-text documents, and lists keep their typed values. Unrelated entry keys are preserved.
97
+ General forms can use select fields with `options: [{ value, label }]`, per-field `isDisabled`, and React-node hints. Optional selects lead with an empty choice, labelled with the field's `placeholder` or "None", that clears the value. Pass `options: []` while choices load; a stored value stays valid until they arrive. Text fields support `maxLength` and `readOnly`; short text also supports `inputType: "email"`, long text supports `rows`, and numeric fields support `min`, `max`, `step`, and `hasSteppers`. Steppers are named after their field, such as "Increase Width". `validateFormValues` enforces `maxLength`, numeric limits, and `picker.max`. `autoFocus` applies to text, number, select, boolean, and date fields.
98
+
99
+ `columns` sets the most columns and defaults to one. The form drops a column whenever a field would be narrower than `minColumnWidth` (default `16rem`), measured against the form's own width, so it steps down inside drawers and modals. Use `columns="auto"` to fit as many columns as the width allows. FormEditor is not a size container, so it keeps its content width in flex rows and popovers.
100
+
101
+ Changes emit `onValueChange(nextValues, { fieldId })`. Use the changed field ID when adapting to APIs that accept patches, generated IDs, or dependent values. Runtime form schemas can hold JSX and live choices, so keep them separate from persisted content definitions. Picker field options such as `picker: { itemLabel: "image", itemsLabel: "images" }` provide contextual search and selection copy without putting data access in the library. Omitted or `undefined` options keep the asset or entry wording.
90
102
 
91
103
  `RichTextEditor` also supports `format="json"` with `value`/`defaultValue` and an `onChange` callback receiving the document. HTML remains its default format. Unsupported JSON content produces an error and cannot be edited until corrected.
92
104
 
@@ -126,7 +138,11 @@ The optional `actions` render function receives `"topnav"` or `"drawer"`. Return
126
138
 
127
139
  The drawer traps focus and locks body scroll. Escape, Close, the scrim, and navigation links dismiss it and return focus to the hamburger. A popup inside it, such as a Menu in the actions footer, closes first on Escape. Modifier-key clicks that open a link elsewhere leave the drawer open. Widening the shell to desktop closes it and moves focus to the active desktop link when focus was inside the drawer. Reduced motion skips the transition.
128
140
 
129
- For other navigation compositions, `Drawer` supports `variant="navigation"`, a React-node `title`, `closeLabel`, and `portalContainer`. By default the drawer portals to `document.body` and covers the viewport; pass a positioned element to keep it inside that element. Supply `aria-label` for a dialog name independent of its visual brand. This variant retains an inert, accessibility-hidden panel during its closing transition; the default Drawer keeps its existing appearance and immediate unmounting.
141
+ All `Drawer` variants have square corners and slide fully in and out through their anchored edge, with a fading scrim. Use `side="end"` (the default) for the right edge in LTR, `side="start"` for the left, and `side="bottom"` for a bottom sheet. Start and end mirror in RTL, including when passing `dir="rtl"` directly to Drawer. Motion uses `--duration-panel` and `--ease-panel`; reduced motion skips the transition. Closing immediately makes the exiting panel inert and accessibility-hidden, restores focus and body scroll, and removes the panel when the transition finishes. Tests checking actual DOM removal should await it rather than expect immediate unmounting. Keep Drawer mounted while changing `isOpen` so it can animate its exit.
142
+
143
+ Side drawers have one outer border facing the page: the left border for a panel entering from the right, or the right border for one entering from the left. They have no outer top or bottom border. This applies to both variants and mirrors in RTL. Internal header/footer separators remain.
144
+
145
+ AppShell uses the shared `Drawer` with `variant="navigation"` and `side="end"`. For other navigation compositions, use the same variant for a compact width, header and footer. Drawer supports a React-node `title` and `closeLabel`; supply `aria-label` for a dialog name independent of its visual brand. By default the drawer portals to `document.body` and covers the viewport; for a navigation drawer, pass a positioned element as `portalContainer` to keep it inside that element. Default drawers retain their standard widths, shadow, and header/footer spacing. Both variants use the same slide, exit handling, and focus protection.
130
146
 
131
147
  ## Image cropping
132
148
 
@@ -246,7 +262,7 @@ Storybook is the component catalogue and documentation surface. Stories and test
246
262
 
247
263
  ## Publishing
248
264
 
249
- Merge a PR into `main` to publish a release. Every push to `main` runs [the release workflow](.github/workflows/release.yml), which installs dependencies, bumps the patch version, moves the `Unreleased` upgrade notes under that version, and runs `npm run validate`. It then commits the version files and guide back to `main` and publishes `@namroodinc/liquid-editorial` to npm under `latest`. All commands run in GitHub Actions; contributors do not run local release commands or create GitHub Releases or tags. Other branches and pull requests do not publish.
265
+ Merge a PR into `main` to publish a release. Every push to `main` runs [the release workflow](.github/workflows/release.yml), which installs dependencies, bumps the version, moves the `Unreleased` upgrade notes under that version, and runs `npm run validate`. It then commits the version files and guide back to `main` and publishes `@namroodinc/liquid-editorial` to npm under `latest`. All commands run in GitHub Actions; contributors do not run local release commands or create GitHub Releases or tags. Other branches and pull requests do not publish.
250
266
 
251
267
  ### One-time npm setup
252
268
 
@@ -268,7 +284,7 @@ npm requires two-factor verification for this setup. The workflow subsequently a
268
284
 
269
285
  ### Release a change
270
286
 
271
- Add the change's adoption, compatibility, and verification notes under `Unreleased` in `UPGRADING.md`, then merge the PR to `main`. The workflow handles the version bump; for example, `0.1.4` becomes `0.1.5`. Pull the generated version commit before continuing local work.
287
+ Add the change's adoption, compatibility, and verification notes under `Unreleased` in `UPGRADING.md`, then merge the PR to `main`. The workflow handles the version bump; for example, `0.1.4` becomes `0.1.5`. When a change removes or renames public API, add a `### Breaking changes` section under `Unreleased` that lists it. The workflow then bumps the minor version below 1.0 (`0.1.11` becomes `0.2.0`) and the major version from 1.0, so caret ranges do not pick it up. Pull the generated version commit before continuing local work.
272
288
 
273
289
  Follow **Publish to npm** in GitHub Actions to check the result. Runs are serialized, and the bot's version commit uses `GITHUB_TOKEN`, which does not trigger another workflow run.
274
290
 
package/UPGRADING.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0
6
+
7
+ ### Changes
8
+
9
+ - Replaced EntryEditor with FormEditor for content and general forms. Added select fields, per-field disabled state, read-only text, autofocus, email inputs, text limits, textarea rows, numeric limits/steps/steppers, React-node hints, responsive columns, and contextual asset/image picker wording.
10
+ - FormEditor reports each edit as `onValueChange(values, { fieldId })`. Asset/reference value shapes and existing scalar/rich-text/date behavior are unchanged. Persistence, choice loading, permissions, and dependent-field rules remain in the consuming application.
11
+ - Optional selects lead with an empty choice (the field's `placeholder`, or "None") that removes the key. Required selects set `aria-required`. Number steppers are named after their field, such as "Increase Width", and out-of-range numbers show their message on load.
12
+ - `validateFormValues` enforces text `maxLength` and multiple-picker `picker.max`. A non-string select value reports "Choose an available option.", and a select with an empty `options` array keeps its stored value valid while choices load. A select field without an `options` array throws a schema error.
13
+ - NumberField accepts `decreaseLabel` and `increaseLabel` for its stepper names. The defaults remain "Decrease" and "Increase".
14
+
15
+ ### Breaking changes
16
+
17
+ - Removed `EntryEditor`, `EntryEditorProps`, `ContentModel`, `ContentField`, `EntryFieldValue`, `EntryValues`, `EntryErrors`, and `validateEntryValues` from the package root, and the `components/EntryEditor` subpath. There are no aliases.
18
+ - The editor's `model` prop is now `schema`, and its CSS hook is `.ds-form-editor` instead of `.ds-entry-editor`.
19
+ - This release is published as 0.2.0, so `^0.1.x` ranges do not install it automatically.
20
+
21
+ ### Adoption
22
+
23
+ Import `FormEditor`, `FormEditorProps`, `FormSchema`, `FormField`, `FormFieldValue`, `FormValues`, `FormErrors`, `FormChange`, and `validateFormValues` from the package root or `components/FormEditor`. Replace EntryEditor, ContentModel, ContentField, EntryFieldValue, EntryValues, EntryErrors, and validateEntryValues accordingly. Change the component's `model` prop to `schema` and CSS hooks from `.ds-entry-editor` to `.ds-form-editor`. `columns` sets the most columns, defaulting to one; the form drops a column whenever a field would be narrower than `minColumnWidth` (default `16rem`). Use `columns="auto"` to fit as many as the width allows. Existing one-argument callbacks may continue to ignore change metadata; use `fieldId` when only edited fields should be sent to an API.
24
+
25
+ For selects, use `{ type: "select", options: [{ value, label }] }` and store strings. Pass `options: []` while choices load, and a placeholder such as "Loading roles…". Field definitions accept `isDisabled`; short/long text support `readOnly` and `maxLength`; short text supports `inputType: "email"`; long text supports `rows`; numbers support `min`, `max`, `step`, and `hasSteppers`. `autoFocus` applies to text, number, select, boolean, and date fields. Pass asset/reference choices through `pickerItems` and picker copy through the field's `picker` options (`itemLabel`, `itemsLabel`, `label`, `addLabel`, `emptyLabel`); omitted or `undefined` options keep the asset or entry wording. Rich hints and loading messages are React values, so keep runtime form schemas separate from persisted content-model DTOs. Schema definitions do not fetch data. Define schemas outside render, or memoize them, so per-field validation is reused between keystrokes.
26
+
27
+ ### Compatibility
28
+
29
+ AssetValue and ReferenceValue remain available. Select fields are UI configuration; consuming CMS APIs do not automatically gain new content field types. FormEditor is not a Grid: it carries no `ds-grid*` classes or container query and sizes its columns against its own width, so it also keeps its content width in flex rows and popovers. `autoFocus` is a type error on rich-text, asset, and reference fields. Loaded text over `maxLength` or lists over `picker.max` now fail `validateFormValues`. No dependencies are added. The release workflow owns the version increment; a `### Breaking changes` section makes it a minor bump below 1.0.
30
+
31
+ ### Verification
32
+
33
+ Run consuming-project typechecks, tests, and builds against the packed build. Check every migrated form's save rules, read-only access, explicit clears, generated IDs, dependent fields, and nested pickers. Confirm touched-field patches use change metadata and do not overwrite concurrent changes. Check labels/hints/errors, select keyboard controls and clearing, single/multiple asset selection, numeric bounds and stepper names, rich text and UTC dates, columns in drawers and narrow containers, and disabled/read-only controls. Re-run pre-save validation against stored data for text and picker limits. Update tests that query FormEditor steppers as "Increase" or "Decrease". The FormEditor stories demonstrate entry and general forms. Run library validation and package checks for React 18 and 19 before publishing.
34
+
35
+ ## 0.1.11
36
+
37
+ ### Changes
38
+
39
+ - `SelectionCardGroup` now accepts an optional `gap` prop, passed to its shared Grid to control spacing between cards in both axes. The default is now `md`, increased from the previous fixed `sm` spacing.
40
+ - `Badge` now follows the editorial-label handoff: 11px bold uppercase status labels and tags, 4px corners, 6px square status markers, and 24px default height across appearances. Soft badges lose their visible border. Counts retain 12px untracked tabular numerals with a 22px minimum width and 6px inline padding. Draft soft fill now uses a defined surface token; archived text uses a stronger muted-text mix.
41
+ - All `Drawer` variants now have square corners and share full-distance slide-in/out transitions and a fading scrim. `start`/`end` mirror in RTL; `bottom` remains supported and slides vertically. Side drawers have only the vertical outer border facing the page, with no outer top or bottom border. Default drawers now retain an inert, accessibility-hidden panel during exit, as navigation drawers already did. AppShell continues to compose the shared Drawer and explicitly uses `side="end"`.
42
+
43
+ ### Adoption
44
+
45
+ Pass a Grid spacing step, for example `<SelectionCardGroup label="Distribution channels" gap="lg">…</SelectionCardGroup>`. Supported values are `none`, `xs`, `sm`, `md`, `lg`, `xl`, and `2xl`. Omit `gap` to use `md`, or pass `gap="sm"` to retain the previous spacing.
46
+
47
+ Badge props and imports need no changes; retain the aggregate stylesheet import. Prefer `outline` for board and selection cards, `soft` for table states and toolbar counts, and `solid` for a single emphasized state such as Cover. Review custom CSS and layouts that assume pill corners, round markers, mixed-case tags, or the old spacing. Inline tags such as `/news` now display uppercase. The new scale tokens are `--badge-radius`, `--badge-font-size`, `--badge-line-height`, `--badge-tracking`, `--badge-pad-block`, `--badge-gap`, `--badge-marker-size`, `--badge-count-min`, and `--badge-count-pad-inline`; use scoped overrides when needed.
48
+
49
+ Drawer props and imports need no changes. Keep Drawer mounted and control visibility with `isOpen`; conditional unmounting skips its exit. Use `side="start"` or `side="end"` for horizontal drawers and `side="bottom"` for sheets. Remove local corner-radius, short-entrance-animation, and page-scroll workarounds superseded by the shared behavior. Tests that check removal should await the panel or scrim leaving the DOM; role queries stop finding it immediately on close. Continue using `variant="navigation"` for compact navigation content. Customize motion with the existing `--duration-panel` and `--ease-panel` tokens.
50
+
51
+ Remove local overrides that only hide a side drawer's outer top/bottom borders; the shared stylesheet now handles that. Review custom frame styles that assume a border around three edges.
52
+
53
+ ### Compatibility
54
+
55
+ The prop uses the existing `GapSize` type and Grid styles. Groups without an explicit gap now have more space between cards; pass `gap="sm"` where the previous density is required. Legend spacing and column behavior are unchanged. No dependency or version changes are required; the release workflow owns version increments.
56
+
57
+ Badge's shapes, appearances, six tones, forwarded ref, data attributes, and `.ds-badge__dot` hook remain supported. Uppercase is a CSS treatment; child text is unchanged. Zero counts remain hidden and values above 99 still display `99+`. Default badge height is 24px at a 16px root font size and text scales with the root font. Shared `--status-draft-soft` and `--status-archived-text` corrections also affect custom status tabs or other consumers of those aliases. Theme palettes and dependencies are unchanged.
58
+
59
+ Drawer's public types, sizes, default `side="end"`, and variants remain supported. Standard widths, border weights, shadows, and header/footer spacing are retained; navigation keeps its compact geometry and shell containment. The intentional behavior change is delayed DOM removal for default drawers. Focus restoration and body-scroll restoration still happen when `isOpen` becomes false, and closed panels cannot receive interaction while exiting. Default initial focus stays on the first control; navigation still focuses Close. Both now use `preventScroll` for initial and restored focus. A native `dir` prop also reaches the scrim, keeping logical-edge alignment consistent with the panel. Reduced motion finishes exit without a visible transition. No dependency or version changes are needed.
60
+
61
+ The side-drawer border adjustment changes only the outer frame for `start`/`end`: the page-facing border retains `--frame-width` and `--frame-color`. Header/footer separators and bottom-sheet framing are retained. Logical border properties keep the remaining vertical border correct in RTL.
62
+
63
+ ### Verification
64
+
65
+ Run the consuming project's typecheck, tests, and build. Compare a group without `gap` to one with `gap="md"`, confirm `gap="sm"` restores the previous density, and check custom spacing between columns and rows, including when the group collapses to one column. The library's `CustomGap` SelectionCard story offers a selector for every supported gap.
66
+
67
+ Review Badge's `VariantsAndStates` story in light, dark, cherry-glaze, and black-hawaii: all three appearances should share a height, status markers should be square, draft soft should have a fill, and archived text should use the stronger ink. Check count values 0, 3, 42, and 128 (hidden, 3, 42, and 99+). Inspect table status columns and toolbar counts, board and selection cards (including inline tags), Cover asset badges, EntryPicker and SideNav counts, and any custom status tabs that consume the corrected aliases. Check narrow layouts and custom token overrides, and run the consuming project's normal checks against the packed build.
68
+
69
+ Exercise the Drawer `Start`, `End`, and `Bottom` stories and AppShell navigation in every theme, LTR/RTL, narrow/wide viewports, and reduced motion. Confirm square corners and a full slide through the anchored edge on open and close, with stationary page/shell backgrounds and no horizontal scrolling. Check long content scrolls inside the panel while header/footer stay usable. Test Close, scrim and Escape dismissal, nested popup Escape handling, Tab/Shift+Tab, first-control versus navigation Close focus, focus/body-scroll restoration, rapid reopening, transition completion, and zero-duration exit. Verify AppShell still contains the shared navigation drawer and closes correctly at its desktop breakpoint. Run the consuming project's typecheck, tests, and build; update any tests or code that assume immediate DOM removal.
70
+
71
+ For both Drawer variants, check that `start`/`end` panels have zero outer top/bottom borders and exactly one vertical frame border facing the page, in LTR and RTL. Confirm internal header/footer separators and the Bottom story's existing frame still render correctly.
72
+
5
73
  ## 0.1.10
6
74
 
7
75
  ### Changes
@@ -0,0 +1,2 @@
1
+ const e=require("../components/Field/index.cjs"),t=require("../components/TextField/index.cjs"),n=require("./RichTextEditor-BxtlbjXj.cjs"),r=require("../components/EntryPicker/index.cjs"),i=require("../components/NumberField/index.cjs"),a=require("../components/Select/index.cjs"),o=require("../components/Switch/index.cjs");let s=require("react"),c=require("react/jsx-runtime");var l=new Set([`shortText`,`longText`,`richText`,`number`,`boolean`,`dateTime`,`asset`,`reference`,`select`]);function u(e){let t=new Set;for(let n of e.fields){if(!n.id?.trim()||t.has(n.id))throw Error(`Form field IDs must be nonempty and unique.`);if(!l.has(n.type))throw Error(`Unsupported form field type: ${n.type}`);if(n.type===`select`&&!Array.isArray(n.options))throw Error(`Select field ${n.id} needs an options array.`);t.add(n.id)}}var d={asset:{key:`assetId`,label:`Assets`,itemLabel:`asset`,itemsLabel:`assets`},reference:{key:`entryId`,label:`Entries`,itemLabel:`entry`,itemsLabel:`entries`}};function f(e){let t=d[e.type],{label:n=t.label,addLabel:r=`Choose ${e.label}`,itemLabel:i=t.itemLabel,itemsLabel:a=t.itemsLabel,...o}=e.picker??{};return{...o,label:n,addLabel:r,itemLabel:i,itemsLabel:a}}function p(e){if(typeof e!=`string`)return;let t=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(?:Z|\+00:00)$/.exec(e);if(!t)return;let n=`${t[1]}:${t[2]??`00`}.${(t[3]??``).padEnd(3,`0`)}Z`,r=new Date(n);if(Number.isFinite(r.getTime())&&r.toISOString()===n)return n}function m(e,t){return!!e&&typeof e==`object`&&t in e&&typeof e[t]==`string`&&e[t].trim().length>0}function h(e){return!!e.text?.trim()||e.type===`horizontalRule`||!!e.content?.some(h)}function g(e){return`${e.label} is required.`}function _(e,t){if(typeof t!=`string`)return`Enter text.`;if(e.required&&!t.trim())return g(e);if((e.type===`shortText`||e.type===`longText`)&&e.maxLength!==void 0&&t.length>e.maxLength)return`Enter no more than ${e.maxLength} characters.`}function v(e,t){let r=n.n(t);if(r)return r;if(e.required&&!h(t))return g(e)}function y(e,t){if(typeof t!=`string`)return`Choose an available option.`;if(t===``)return e.required?g(e):void 0;if(e.type===`select`&&e.options.length>0&&!e.options.some(e=>e.value===t))return`Choose an available option.`}function b(e,t){if(e.type!==`asset`&&e.type!==`reference`)return;let{key:n,itemLabel:r,itemsLabel:i}=d[e.type];if(!e.multiple)return m(t,n)?void 0:`Choose an ${r}.`;if(!Array.isArray(t)||!t.every(e=>m(e,n)))return`Choose a list of ${i}.`;if(e.required&&t.length===0)return g(e);let a=e.picker?.max;if(a!==void 0&&t.length>a)return`Choose no more than ${a} ${f(e).itemsLabel}.`}var x={shortText:_,longText:_,richText:v,number:(e,t)=>{if(typeof t!=`number`||!Number.isFinite(t))return`Enter a finite number.`;if(e.type===`number`){if(e.min!==void 0&&t<e.min)return`Enter a number of at least ${e.min}.`;if(e.max!==void 0&&t>e.max)return`Enter a number no greater than ${e.max}.`}},boolean:(e,t)=>typeof t==`boolean`?void 0:`Choose a boolean value.`,dateTime:(e,t)=>p(t)?void 0:`Enter a valid UTC date and time.`,select:y,asset:b,reference:b};function S(e,t){return t===void 0?e.required?g(e):void 0:x[e.type](e,t)}function C(e,t){return u(e),Object.fromEntries(e.fields.flatMap(e=>{let n=S(e,Object.hasOwn(t,e.id)?t[e.id]:void 0);return n?[[e.id,n]]:[]}))}function w(e,t){let n=e.type===`asset`?`assetId`:`entryId`;return(Array.isArray(t)?t:t===void 0?[]:[t]).flatMap(e=>{if(!e||typeof e!=`object`||!(n in e))return[];let t=e[n];return typeof t==`string`?[t]:[]})}function T(e,t){if(e.type===`asset`){let n=t.map(e=>({assetId:e}));return e.multiple?n:n[0]}let n=t.map(e=>({entryId:e}));return e.multiple?n:n[0]}function E({field:e,stored:t,isDisabled:n,required:r,accessibility:a,onChange:o}){return(0,c.jsx)(i.NumberField,{...a,required:r,isDisabled:n,placeholder:e.placeholder,min:e.min,max:e.max,step:e.step??`any`,hasSteppers:e.hasSteppers??!1,autoFocus:e.autoFocus,decreaseLabel:`Decrease ${e.label}`,increaseLabel:`Increase ${e.label}`,value:typeof t==`number`&&Number.isFinite(t)?t:``,onValueChange:e=>{(e===``||Number.isFinite(e))&&o(e===``?void 0:e)}})}function D({field:e,stored:t,isDisabled:n,required:r,accessibility:i,onChange:o}){let s=!r&&!e.options.some(e=>e.value===``);return(0,c.jsx)(a.Select,{...i,"aria-required":r||void 0,isDisabled:n,isInvalid:i[`aria-invalid`],options:s?[{value:``,label:e.placeholder??`None`},...e.options]:e.options,placeholder:e.placeholder,autoFocus:e.autoFocus,value:typeof t==`string`?t:``,onValueChange:e=>o(e===``?void 0:e)})}function O({field:e,stored:t,items:n,isDisabled:i,accessibility:a,onChange:o}){return(0,c.jsx)(`div`,{...a,role:`group`,children:(0,c.jsx)(r.EntryPicker,{items:n,value:w(e,t),...f(e),mode:e.multiple?`multiple`:`single`,isDisabled:i,onValueChange:t=>o(T(e,t))})})}function k({field:e,stored:r,items:i,isDisabled:a,accessibility:{required:s,...l},onChange:u}){let d={stored:r,isDisabled:a,required:s,accessibility:l,onChange:u};switch(e.type){case`shortText`:return(0,c.jsx)(t.TextField,{...l,required:s,disabled:a,placeholder:e.placeholder,type:e.inputType,maxLength:e.maxLength,readOnly:e.readOnly,autoFocus:e.autoFocus,value:typeof r==`string`?r:``,onChange:e=>u(e.target.value)});case`longText`:return(0,c.jsx)(t.TextArea,{...l,required:s,disabled:a,placeholder:e.placeholder,rows:e.rows,maxLength:e.maxLength,readOnly:e.readOnly,autoFocus:e.autoFocus,value:typeof r==`string`?r:``,onChange:e=>u(e.target.value)});case`number`:return(0,c.jsx)(E,{field:e,...d});case`boolean`:return(0,c.jsx)(o.Switch,{...l,isDisabled:a,autoFocus:e.autoFocus,isSelected:r===!0,onSelectedChange:u});case`dateTime`:return(0,c.jsx)(t.TextField,{...l,required:s,disabled:a,type:`datetime-local`,step:`0.001`,autoFocus:e.autoFocus,value:p(r)?.slice(0,-1)??``,onChange:e=>u(p(`${e.target.value}Z`))});case`richText`:return(0,c.jsx)(n.t,{...l,label:e.label,isRequired:s,isDisabled:a,placeholder:e.placeholder,format:`json`,value:r===void 0?n.r:r,onChange:u});case`select`:return(0,c.jsx)(D,{field:e,...d});case`asset`:case`reference`:return(0,c.jsx)(O,{field:e,items:i,...d})}}function A({field:t,stored:n,items:r,isDisabled:i,error:a,isErrorVisible:o,onChange:l}){let u=(0,s.useMemo)(()=>a===void 0&&o?S(t,n):void 0,[a,o,t,n]),d=t.type===`dateTime`?`${t.label} (UTC)`:t.label;return(0,c.jsx)(e.Field,{label:d,hint:t.hint,error:a??u,isRequired:t.required,isGroup:t.type===`asset`||t.type===`reference`,children:e=>(0,c.jsx)(k,{field:t,stored:n,items:r,isDisabled:i,accessibility:e,onChange:l})})}function j({schema:e,value:t,defaultValue:n={},onValueChange:r,pickerItems:i={},errors:a={},isDisabled:o=!1,showAllErrors:l=!1,columns:d=1,minColumnWidth:f=`16rem`,className:p,style:m,...h}){let[g,_]=(0,s.useState)(n),[v,y]=(0,s.useState)(()=>new Set),b=t??g;(0,s.useMemo)(()=>u(e),[e]);let x=(e,n)=>{let i={...b};n===void 0?delete i[e]:i[e]=n,t===void 0&&_(i),y(t=>new Set(t).add(e)),r?.(i,{fieldId:e})},S=d===`auto`?{"--form-editor-min-column":f}:{"--form-editor-columns":d,"--form-editor-min-column":f};return(0,c.jsx)(`div`,{className:[`ds-form-editor`,d===`auto`&&`ds-form-editor--auto`,p].filter(Boolean).join(` `),style:{...S,...m},...h,children:e.fields.map(e=>{let t=Object.hasOwn(b,e.id)?b[e.id]:void 0,n=o||!!e.isDisabled,r=n||`readOnly`in e&&!!e.readOnly,s=l||v.has(e.id)||e.type===`number`&&typeof t==`number`;return(0,c.jsx)(A,{field:e,stored:t,items:i[e.id]??[],isDisabled:n,error:a[e.id],isErrorVisible:s,onChange:t=>{r||x(e.id,t)}},e.id)})})}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return j}});
2
+ //# sourceMappingURL=FormEditor-8bz3pOdP.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormEditor-8bz3pOdP.cjs","names":["documentError","NumberField","Select","EntryPicker","TextField","TextArea","Switch","RichTextEditor","emptyDocument","useMemo","Field","useState","require_components_Field_index","require_components_TextField_index","require_RichTextEditor","require_components_EntryPicker_index","require_components_NumberField_index","require_components_Select_index","require_components_Switch_index","react","react_jsx_runtime","fieldTypes","assertModel","ids","field","model","linkCopy","pickerCopy","defaults","rest","parseUtcDateTime","value","match","canonical","date","isLink","key","hasRichText","node","requiredError","textError","richTextError","error","choiceError","option","linkError","itemLabel","item","itemsLabel","max","validators","fieldError","validateFormValues","values","selectedIds","link","id","pickerValue","links","NumberControl","accessibility","stored","next","onChange","SelectControl","isClearable","required","PickerControl","FormControl","shared","isDisabled","event","FormEditorField","validation","isErrorVisible","label","FormEditor","internal","setInternal","defaultValue","changed","setChanged","current","schema","commit","fieldId","previous","onValueChange","layout","columns","minColumnWidth","className","style","fieldDisabled","isLocked","showAllErrors","pickerItems","errors"],"sources":["../../src/components/FormEditor/model.ts","../../src/components/FormEditor/control.tsx","../../src/components/FormEditor/index.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { SelectOption } from \"../Select\";\nimport type { EntryPickerProps } from \"../EntryPicker\";\nimport type { JSONContent } from \"@tiptap/react\";\nimport { documentError } from \"../RichTextEditor/document\";\nimport type { RichTextDocument } from \"../RichTextEditor\";\n\ninterface FormFieldBase {\n id: string;\n label: string;\n hint?: ReactNode;\n placeholder?: string;\n required?: boolean;\n isDisabled?: boolean;\n}\n\nexport type FormField = FormFieldBase & (\n | { type: \"shortText\"; inputType?: \"text\" | \"email\"; maxLength?: number; readOnly?: boolean; autoFocus?: boolean; multiple?: never }\n | { type: \"longText\"; rows?: number; maxLength?: number; readOnly?: boolean; autoFocus?: boolean; multiple?: never }\n | { type: \"boolean\" | \"dateTime\"; autoFocus?: boolean; multiple?: never }\n | { type: \"richText\"; autoFocus?: never; multiple?: never }\n | { type: \"number\"; min?: number; max?: number; step?: number | \"any\"; hasSteppers?: boolean; autoFocus?: boolean; multiple?: never }\n | { type: \"select\"; options: SelectOption[]; autoFocus?: boolean; multiple?: never }\n | { type: \"asset\" | \"reference\"; multiple?: boolean; autoFocus?: never; picker?: Pick<EntryPickerProps, \"label\" | \"addLabel\" | \"emptyLabel\" | \"itemLabel\" | \"itemsLabel\" | \"max\" | \"isReorderable\"> }\n);\n\nexport type FieldOf<T extends FormField[\"type\"]> = Extract<FormField, { type: T }>;\n\nexport interface FormSchema {\n fields: readonly FormField[];\n}\n\nexport interface AssetValue { assetId: string }\nexport interface ReferenceValue { entryId: string }\nexport type FormFieldValue = string | number | boolean | RichTextDocument | AssetValue | ReferenceValue | AssetValue[] | ReferenceValue[];\nexport type FormValues = Record<string, FormFieldValue | undefined>;\nexport type FormErrors = Partial<Record<string, string>>;\n\nconst fieldTypes = new Set([\"shortText\", \"longText\", \"richText\", \"number\", \"boolean\", \"dateTime\", \"asset\", \"reference\", \"select\"]);\n\nexport function assertModel(model: FormSchema) {\n const ids = new Set<string>();\n for (const field of model.fields) {\n if (!field.id?.trim() || ids.has(field.id)) throw new Error(\"Form field IDs must be nonempty and unique.\");\n if (!fieldTypes.has(field.type)) throw new Error(`Unsupported form field type: ${field.type}`);\n if (field.type === \"select\" && !Array.isArray(field.options)) throw new Error(`Select field ${field.id} needs an options array.`);\n ids.add(field.id);\n }\n}\n\nconst linkCopy = {\n asset: { key: \"assetId\", label: \"Assets\", itemLabel: \"asset\", itemsLabel: \"assets\" },\n reference: { key: \"entryId\", label: \"Entries\", itemLabel: \"entry\", itemsLabel: \"entries\" },\n} as const;\n\n/** Picker copy, with asset or entry wording for any option the field leaves out or sets to undefined. */\nexport function pickerCopy(field: FieldOf<\"asset\" | \"reference\">) {\n const defaults = linkCopy[field.type];\n const {\n label = defaults.label,\n addLabel = `Choose ${field.label}`,\n itemLabel = defaults.itemLabel,\n itemsLabel = defaults.itemsLabel,\n ...rest\n } = field.picker ?? {};\n return { ...rest, label, addLabel, itemLabel, itemsLabel };\n}\n\n/** Accept UTC ISO strings without rolling invalid dates into the next month. */\nexport function parseUtcDateTime(value: unknown): string | undefined {\n if (typeof value !== \"string\") return;\n const match = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2})(?::(\\d{2})(?:\\.(\\d{1,3}))?)?(?:Z|\\+00:00)$/.exec(value);\n if (!match) return;\n const canonical = `${match[1]}:${match[2] ?? \"00\"}.${(match[3] ?? \"\").padEnd(3, \"0\")}Z`;\n const date = new Date(canonical);\n if (Number.isFinite(date.getTime()) && date.toISOString() === canonical) return canonical;\n}\n\nfunction isLink(value: unknown, key: \"assetId\" | \"entryId\"): boolean {\n return !!value && typeof value === \"object\" && key in value &&\n typeof (value as Record<string, unknown>)[key] === \"string\" &&\n ((value as Record<string, string>)[key]).trim().length > 0;\n}\n\nfunction hasRichText(node: JSONContent): boolean {\n return !!node.text?.trim() || node.type === \"horizontalRule\" || !!node.content?.some(hasRichText);\n}\n\nfunction requiredError(field: FormField): string {\n return `${field.label} is required.`;\n}\n\nfunction textError(field: FormField, value: unknown): string | undefined {\n if (typeof value !== \"string\") return \"Enter text.\";\n if (field.required && !value.trim()) return requiredError(field);\n if ((field.type === \"shortText\" || field.type === \"longText\") && field.maxLength !== undefined && value.length > field.maxLength) {\n return `Enter no more than ${field.maxLength} characters.`;\n }\n}\n\nfunction richTextError(field: FormField, value: unknown): string | undefined {\n const error = documentError(value);\n if (error) return error;\n if (field.required && !hasRichText(value as RichTextDocument)) return requiredError(field);\n}\n\nfunction choiceError(field: FormField, value: unknown): string | undefined {\n if (typeof value !== \"string\") return \"Choose an available option.\";\n if (value === \"\") return field.required ? requiredError(field) : undefined;\n // An empty list means the choices are still loading, so a stored value stays valid until they arrive.\n if (field.type === \"select\" && field.options.length > 0 && !field.options.some((option) => option.value === value)) {\n return \"Choose an available option.\";\n }\n}\n\nfunction linkError(field: FormField, value: unknown): string | undefined {\n if (field.type !== \"asset\" && field.type !== \"reference\") return;\n const { key, itemLabel, itemsLabel } = linkCopy[field.type];\n if (!field.multiple) return isLink(value, key) ? undefined : `Choose an ${itemLabel}.`;\n if (!Array.isArray(value) || !value.every((item) => isLink(item, key))) return `Choose a list of ${itemsLabel}.`;\n if (field.required && value.length === 0) return requiredError(field);\n const max = field.picker?.max;\n if (max !== undefined && value.length > max) return `Choose no more than ${max} ${pickerCopy(field).itemsLabel}.`;\n}\n\nconst validators: Record<FormField[\"type\"], (field: FormField, value: unknown) => string | undefined> = {\n shortText: textError,\n longText: textError,\n richText: richTextError,\n number: (field, value) => {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return \"Enter a finite number.\";\n if (field.type !== \"number\") return;\n if (field.min !== undefined && value < field.min) return `Enter a number of at least ${field.min}.`;\n if (field.max !== undefined && value > field.max) return `Enter a number no greater than ${field.max}.`;\n },\n boolean: (_field, value) => typeof value === \"boolean\" ? undefined : \"Choose a boolean value.\",\n dateTime: (_field, value) => parseUtcDateTime(value) ? undefined : \"Enter a valid UTC date and time.\",\n select: choiceError,\n asset: linkError,\n reference: linkError,\n};\n\nexport function fieldError(field: FormField, value: unknown): string | undefined {\n if (value === undefined) return field.required ? requiredError(field) : undefined;\n return validators[field.type](field, value);\n}\n\n/** Validate data loaded from JSON as well as values emitted by FormEditor. */\nexport function validateFormValues(model: FormSchema, values: Record<string, unknown>): FormErrors {\n assertModel(model);\n return Object.fromEntries(model.fields.flatMap((field): [string, string][] => {\n const error = fieldError(field, Object.hasOwn(values, field.id) ? values[field.id] : undefined);\n return error ? [[field.id, error]] : [];\n }));\n}\n","import { EntryPicker, type EntryItem } from \"../EntryPicker\";\nimport type { FieldProps } from \"../Field\";\nimport { NumberField } from \"../NumberField\";\nimport { RichTextEditor, type RichTextDocument } from \"../RichTextEditor\";\nimport { emptyDocument } from \"../RichTextEditor/document\";\nimport { Select } from \"../Select\";\nimport { Switch } from \"../Switch\";\nimport { TextArea, TextField } from \"../TextField\";\nimport { parseUtcDateTime, pickerCopy, type FieldOf, type FormField, type FormFieldValue } from \"./model\";\n\nfunction selectedIds(field: FormField, value: FormFieldValue | undefined): string[] {\n const key = field.type === \"asset\" ? \"assetId\" : \"entryId\";\n const links = Array.isArray(value) ? value : value === undefined ? [] : [value];\n return links.flatMap((link) => {\n if (!link || typeof link !== \"object\" || !(key in link)) return [];\n const id = (link as unknown as Record<string, unknown>)[key];\n return typeof id === \"string\" ? [id] : [];\n });\n}\n\nfunction pickerValue(field: FormField, ids: string[]): FormFieldValue | undefined {\n if (field.type === \"asset\") {\n const links = ids.map((assetId) => ({ assetId }));\n return field.multiple ? links : links[0];\n }\n const links = ids.map((entryId) => ({ entryId }));\n return field.multiple ? links : links[0];\n}\n\ntype Accessibility = Parameters<FieldProps[\"children\"]>[0];\n\ninterface ControlProps<F extends FormField> {\n field: F;\n stored: FormFieldValue | undefined;\n isDisabled: boolean;\n required: boolean | undefined;\n accessibility: Omit<Accessibility, \"required\">;\n onChange: (value: FormFieldValue | undefined) => void;\n}\n\nfunction NumberControl({ field, stored, isDisabled, required, accessibility, onChange }: ControlProps<FieldOf<\"number\">>) {\n return <NumberField {...accessibility} required={required} isDisabled={isDisabled} placeholder={field.placeholder}\n min={field.min} max={field.max} step={field.step ?? \"any\"} hasSteppers={field.hasSteppers ?? false} autoFocus={field.autoFocus}\n decreaseLabel={`Decrease ${field.label}`} increaseLabel={`Increase ${field.label}`}\n value={typeof stored === \"number\" && Number.isFinite(stored) ? stored : \"\"}\n onValueChange={(next) => { if (next === \"\" || Number.isFinite(next)) onChange(next === \"\" ? undefined : next); }} />;\n}\n\nfunction SelectControl({ field, stored, isDisabled, required, accessibility, onChange }: ControlProps<FieldOf<\"select\">>) {\n // Optional selects lead with an empty choice; picking it removes the key.\n const isClearable = !required && !field.options.some((option) => option.value === \"\");\n return <Select {...accessibility} aria-required={required || undefined} isDisabled={isDisabled} isInvalid={accessibility[\"aria-invalid\"]}\n options={isClearable ? [{ value: \"\", label: field.placeholder ?? \"None\" }, ...field.options] : field.options}\n placeholder={field.placeholder} autoFocus={field.autoFocus}\n value={typeof stored === \"string\" ? stored : \"\"} onValueChange={(next) => onChange(next === \"\" ? undefined : next)} />;\n}\n\nfunction PickerControl({ field, stored, items, isDisabled, accessibility, onChange }: ControlProps<FieldOf<\"asset\" | \"reference\">> & { items: EntryItem[] }) {\n return <div {...accessibility} role=\"group\">\n <EntryPicker items={items} value={selectedIds(field, stored)}\n {...pickerCopy(field)} mode={field.multiple ? \"multiple\" : \"single\"} isDisabled={isDisabled}\n onValueChange={(ids) => onChange(pickerValue(field, ids))} />\n </div>;\n}\n\ninterface FormControlProps {\n field: FormField;\n stored: FormFieldValue | undefined;\n items: EntryItem[];\n isDisabled: boolean;\n accessibility: Accessibility;\n onChange: (value: FormFieldValue | undefined) => void;\n}\n\nexport function FormControl({ field, stored, items, isDisabled, accessibility: { required, ...accessibility }, onChange }: FormControlProps) {\n const shared = { stored, isDisabled, required, accessibility, onChange };\n switch (field.type) {\n case \"shortText\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n type={field.inputType} maxLength={field.maxLength} readOnly={field.readOnly} autoFocus={field.autoFocus}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => onChange(event.target.value)} />;\n case \"longText\":\n return <TextArea {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n rows={field.rows} maxLength={field.maxLength} readOnly={field.readOnly} autoFocus={field.autoFocus}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => onChange(event.target.value)} />;\n case \"number\":\n return <NumberControl field={field} {...shared} />;\n case \"boolean\":\n return <Switch {...accessibility} isDisabled={isDisabled} autoFocus={field.autoFocus} isSelected={stored === true} onSelectedChange={onChange} />;\n case \"dateTime\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} type=\"datetime-local\" step=\"0.001\" autoFocus={field.autoFocus}\n value={parseUtcDateTime(stored)?.slice(0, -1) ?? \"\"}\n onChange={(event) => onChange(parseUtcDateTime(`${event.target.value}Z`))} />;\n case \"richText\":\n return <RichTextEditor {...accessibility} label={field.label} isRequired={required} isDisabled={isDisabled}\n placeholder={field.placeholder} format=\"json\" value={stored === undefined ? emptyDocument : stored as RichTextDocument}\n onChange={onChange} />;\n case \"select\":\n return <SelectControl field={field} {...shared} />;\n case \"asset\":\n case \"reference\":\n return <PickerControl field={field} items={items} {...shared} />;\n }\n}\n","import { useMemo, useState, type CSSProperties, type HTMLAttributes } from \"react\";\nimport type { EntryItem } from \"../EntryPicker\";\nimport { Field } from \"../Field\";\nimport type { GridColumns } from \"../Grid\";\nimport { FormControl } from \"./control\";\nimport { assertModel, fieldError, type FormField, type FormSchema, type FormErrors, type FormFieldValue, type FormValues } from \"./model\";\nimport \"./style.css\";\n\nexport { validateFormValues } from \"./model\";\nexport type { AssetValue, FormField, FormSchema, FormErrors, FormFieldValue, FormValues, ReferenceValue } from \"./model\";\n\nexport interface FormChange { fieldId: string }\n\nexport interface FormEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n schema: FormSchema;\n value?: FormValues;\n defaultValue?: FormValues;\n onValueChange?: (values: FormValues, change: FormChange) => void;\n /** Available assets or entries, keyed by field ID. Loading belongs to the app. */\n pickerItems?: Record<string, EntryItem[]>;\n /** App-provided messages take precedence over built-in validation. */\n errors?: FormErrors;\n isDisabled?: boolean;\n showAllErrors?: boolean;\n /** Columns at the widest. The form drops one whenever a field would be narrower than `minColumnWidth`. */\n columns?: GridColumns;\n /** Narrowest a field may get before the form drops a column. */\n minColumnWidth?: string;\n}\n\ninterface FormEditorFieldProps {\n field: FormField;\n stored: FormFieldValue | undefined;\n items: EntryItem[];\n isDisabled: boolean;\n error: string | undefined;\n isErrorVisible: boolean;\n onChange: (value: FormFieldValue | undefined) => void;\n}\n\n/** Validates only when its message can show, and only when its own field or value changes. */\nfunction FormEditorField({ field, stored, items, isDisabled, error, isErrorVisible, onChange }: FormEditorFieldProps) {\n const validation = useMemo(() => error === undefined && isErrorVisible ? fieldError(field, stored) : undefined,\n [error, isErrorVisible, field, stored]);\n const label = field.type === \"dateTime\" ? `${field.label} (UTC)` : field.label;\n return (\n <Field label={label} hint={field.hint} error={error ?? validation} isRequired={field.required}\n isGroup={field.type === \"asset\" || field.type === \"reference\"}>\n {(accessibility) => <FormControl field={field} stored={stored} items={items}\n isDisabled={isDisabled} accessibility={accessibility} onChange={onChange} />}\n </Field>\n );\n}\n\n/** Ordered form controls. Loading, submission, and persistence belong to the app. */\nexport function FormEditor({ schema, value, defaultValue = {}, onValueChange, pickerItems = {}, errors = {},\n isDisabled = false, showAllErrors = false, columns = 1, minColumnWidth = \"16rem\", className, style, ...rest }: FormEditorProps) {\n const [internal, setInternal] = useState<FormValues>(defaultValue);\n const [changed, setChanged] = useState<Set<string>>(() => new Set());\n const current = value ?? internal;\n useMemo(() => assertModel(schema), [schema]);\n\n const commit = (fieldId: string, next: FormFieldValue | undefined) => {\n const values = { ...current };\n if (next === undefined) delete values[fieldId];\n else values[fieldId] = next;\n if (value === undefined) setInternal(values);\n setChanged((previous) => new Set(previous).add(fieldId));\n onValueChange?.(values, { fieldId });\n };\n\n const layout = columns === \"auto\"\n ? { \"--form-editor-min-column\": minColumnWidth }\n : { \"--form-editor-columns\": columns, \"--form-editor-min-column\": minColumnWidth };\n\n return (\n <div className={[\"ds-form-editor\", columns === \"auto\" && \"ds-form-editor--auto\", className].filter(Boolean).join(\" \")}\n style={{ ...layout, ...style } as CSSProperties} {...rest}>\n {schema.fields.map((field) => {\n const stored = Object.hasOwn(current, field.id) ? current[field.id] : undefined;\n const fieldDisabled = isDisabled || Boolean(field.isDisabled);\n const isLocked = fieldDisabled || (\"readOnly\" in field && Boolean(field.readOnly));\n // NumberField marks an out-of-range number invalid itself, so its message shows straight away.\n const isErrorVisible = showAllErrors || changed.has(field.id) || (field.type === \"number\" && typeof stored === \"number\");\n return <FormEditorField key={field.id} field={field} stored={stored} items={pickerItems[field.id] ?? []}\n isDisabled={fieldDisabled} error={errors[field.id]} isErrorVisible={isErrorVisible}\n onChange={(next) => { if (!isLocked) commit(field.id, next); }} />;\n })}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,330 @@
1
+ import { Field as e } from "../components/Field/index.js";
2
+ import { TextArea as t, TextField as n } from "../components/TextField/index.js";
3
+ import { n as r, r as i, t as a } from "./RichTextEditor-Dnx4i7bQ.js";
4
+ import { EntryPicker as o } from "../components/EntryPicker/index.js";
5
+ import { NumberField as s } from "../components/NumberField/index.js";
6
+ import { Select as c } from "../components/Select/index.js";
7
+ import { Switch as l } from "../components/Switch/index.js";
8
+ import { useMemo as u, useState as d } from "react";
9
+ import { jsx as f } from "react/jsx-runtime";
10
+ //#region src/components/FormEditor/model.ts
11
+ var p = /* @__PURE__ */ new Set([
12
+ "shortText",
13
+ "longText",
14
+ "richText",
15
+ "number",
16
+ "boolean",
17
+ "dateTime",
18
+ "asset",
19
+ "reference",
20
+ "select"
21
+ ]);
22
+ function m(e) {
23
+ let t = /* @__PURE__ */ new Set();
24
+ for (let n of e.fields) {
25
+ if (!n.id?.trim() || t.has(n.id)) throw Error("Form field IDs must be nonempty and unique.");
26
+ if (!p.has(n.type)) throw Error(`Unsupported form field type: ${n.type}`);
27
+ if (n.type === "select" && !Array.isArray(n.options)) throw Error(`Select field ${n.id} needs an options array.`);
28
+ t.add(n.id);
29
+ }
30
+ }
31
+ var h = {
32
+ asset: {
33
+ key: "assetId",
34
+ label: "Assets",
35
+ itemLabel: "asset",
36
+ itemsLabel: "assets"
37
+ },
38
+ reference: {
39
+ key: "entryId",
40
+ label: "Entries",
41
+ itemLabel: "entry",
42
+ itemsLabel: "entries"
43
+ }
44
+ };
45
+ function g(e) {
46
+ let t = h[e.type], { label: n = t.label, addLabel: r = `Choose ${e.label}`, itemLabel: i = t.itemLabel, itemsLabel: a = t.itemsLabel, ...o } = e.picker ?? {};
47
+ return {
48
+ ...o,
49
+ label: n,
50
+ addLabel: r,
51
+ itemLabel: i,
52
+ itemsLabel: a
53
+ };
54
+ }
55
+ function _(e) {
56
+ if (typeof e != "string") return;
57
+ let t = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(?:Z|\+00:00)$/.exec(e);
58
+ if (!t) return;
59
+ let n = `${t[1]}:${t[2] ?? "00"}.${(t[3] ?? "").padEnd(3, "0")}Z`, r = new Date(n);
60
+ if (Number.isFinite(r.getTime()) && r.toISOString() === n) return n;
61
+ }
62
+ function v(e, t) {
63
+ return !!e && typeof e == "object" && t in e && typeof e[t] == "string" && e[t].trim().length > 0;
64
+ }
65
+ function y(e) {
66
+ return !!e.text?.trim() || e.type === "horizontalRule" || !!e.content?.some(y);
67
+ }
68
+ function b(e) {
69
+ return `${e.label} is required.`;
70
+ }
71
+ function x(e, t) {
72
+ if (typeof t != "string") return "Enter text.";
73
+ if (e.required && !t.trim()) return b(e);
74
+ if ((e.type === "shortText" || e.type === "longText") && e.maxLength !== void 0 && t.length > e.maxLength) return `Enter no more than ${e.maxLength} characters.`;
75
+ }
76
+ function S(e, t) {
77
+ let n = r(t);
78
+ if (n) return n;
79
+ if (e.required && !y(t)) return b(e);
80
+ }
81
+ function C(e, t) {
82
+ if (typeof t != "string") return "Choose an available option.";
83
+ if (t === "") return e.required ? b(e) : void 0;
84
+ if (e.type === "select" && e.options.length > 0 && !e.options.some((e) => e.value === t)) return "Choose an available option.";
85
+ }
86
+ function w(e, t) {
87
+ if (e.type !== "asset" && e.type !== "reference") return;
88
+ let { key: n, itemLabel: r, itemsLabel: i } = h[e.type];
89
+ if (!e.multiple) return v(t, n) ? void 0 : `Choose an ${r}.`;
90
+ if (!Array.isArray(t) || !t.every((e) => v(e, n))) return `Choose a list of ${i}.`;
91
+ if (e.required && t.length === 0) return b(e);
92
+ let a = e.picker?.max;
93
+ if (a !== void 0 && t.length > a) return `Choose no more than ${a} ${g(e).itemsLabel}.`;
94
+ }
95
+ var T = {
96
+ shortText: x,
97
+ longText: x,
98
+ richText: S,
99
+ number: (e, t) => {
100
+ if (typeof t != "number" || !Number.isFinite(t)) return "Enter a finite number.";
101
+ if (e.type === "number") {
102
+ if (e.min !== void 0 && t < e.min) return `Enter a number of at least ${e.min}.`;
103
+ if (e.max !== void 0 && t > e.max) return `Enter a number no greater than ${e.max}.`;
104
+ }
105
+ },
106
+ boolean: (e, t) => typeof t == "boolean" ? void 0 : "Choose a boolean value.",
107
+ dateTime: (e, t) => _(t) ? void 0 : "Enter a valid UTC date and time.",
108
+ select: C,
109
+ asset: w,
110
+ reference: w
111
+ };
112
+ function E(e, t) {
113
+ return t === void 0 ? e.required ? b(e) : void 0 : T[e.type](e, t);
114
+ }
115
+ function D(e, t) {
116
+ return m(e), Object.fromEntries(e.fields.flatMap((e) => {
117
+ let n = E(e, Object.hasOwn(t, e.id) ? t[e.id] : void 0);
118
+ return n ? [[e.id, n]] : [];
119
+ }));
120
+ }
121
+ //#endregion
122
+ //#region src/components/FormEditor/control.tsx
123
+ function O(e, t) {
124
+ let n = e.type === "asset" ? "assetId" : "entryId";
125
+ return (Array.isArray(t) ? t : t === void 0 ? [] : [t]).flatMap((e) => {
126
+ if (!e || typeof e != "object" || !(n in e)) return [];
127
+ let t = e[n];
128
+ return typeof t == "string" ? [t] : [];
129
+ });
130
+ }
131
+ function k(e, t) {
132
+ if (e.type === "asset") {
133
+ let n = t.map((e) => ({ assetId: e }));
134
+ return e.multiple ? n : n[0];
135
+ }
136
+ let n = t.map((e) => ({ entryId: e }));
137
+ return e.multiple ? n : n[0];
138
+ }
139
+ function A({ field: e, stored: t, isDisabled: n, required: r, accessibility: i, onChange: a }) {
140
+ return /* @__PURE__ */ f(s, {
141
+ ...i,
142
+ required: r,
143
+ isDisabled: n,
144
+ placeholder: e.placeholder,
145
+ min: e.min,
146
+ max: e.max,
147
+ step: e.step ?? "any",
148
+ hasSteppers: e.hasSteppers ?? !1,
149
+ autoFocus: e.autoFocus,
150
+ decreaseLabel: `Decrease ${e.label}`,
151
+ increaseLabel: `Increase ${e.label}`,
152
+ value: typeof t == "number" && Number.isFinite(t) ? t : "",
153
+ onValueChange: (e) => {
154
+ (e === "" || Number.isFinite(e)) && a(e === "" ? void 0 : e);
155
+ }
156
+ });
157
+ }
158
+ function j({ field: e, stored: t, isDisabled: n, required: r, accessibility: i, onChange: a }) {
159
+ let o = !r && !e.options.some((e) => e.value === "");
160
+ return /* @__PURE__ */ f(c, {
161
+ ...i,
162
+ "aria-required": r || void 0,
163
+ isDisabled: n,
164
+ isInvalid: i["aria-invalid"],
165
+ options: o ? [{
166
+ value: "",
167
+ label: e.placeholder ?? "None"
168
+ }, ...e.options] : e.options,
169
+ placeholder: e.placeholder,
170
+ autoFocus: e.autoFocus,
171
+ value: typeof t == "string" ? t : "",
172
+ onValueChange: (e) => a(e === "" ? void 0 : e)
173
+ });
174
+ }
175
+ function M({ field: e, stored: t, items: n, isDisabled: r, accessibility: i, onChange: a }) {
176
+ return /* @__PURE__ */ f("div", {
177
+ ...i,
178
+ role: "group",
179
+ children: /* @__PURE__ */ f(o, {
180
+ items: n,
181
+ value: O(e, t),
182
+ ...g(e),
183
+ mode: e.multiple ? "multiple" : "single",
184
+ isDisabled: r,
185
+ onValueChange: (t) => a(k(e, t))
186
+ })
187
+ });
188
+ }
189
+ function N({ field: e, stored: r, items: o, isDisabled: s, accessibility: { required: c, ...u }, onChange: d }) {
190
+ let p = {
191
+ stored: r,
192
+ isDisabled: s,
193
+ required: c,
194
+ accessibility: u,
195
+ onChange: d
196
+ };
197
+ switch (e.type) {
198
+ case "shortText": return /* @__PURE__ */ f(n, {
199
+ ...u,
200
+ required: c,
201
+ disabled: s,
202
+ placeholder: e.placeholder,
203
+ type: e.inputType,
204
+ maxLength: e.maxLength,
205
+ readOnly: e.readOnly,
206
+ autoFocus: e.autoFocus,
207
+ value: typeof r == "string" ? r : "",
208
+ onChange: (e) => d(e.target.value)
209
+ });
210
+ case "longText": return /* @__PURE__ */ f(t, {
211
+ ...u,
212
+ required: c,
213
+ disabled: s,
214
+ placeholder: e.placeholder,
215
+ rows: e.rows,
216
+ maxLength: e.maxLength,
217
+ readOnly: e.readOnly,
218
+ autoFocus: e.autoFocus,
219
+ value: typeof r == "string" ? r : "",
220
+ onChange: (e) => d(e.target.value)
221
+ });
222
+ case "number": return /* @__PURE__ */ f(A, {
223
+ field: e,
224
+ ...p
225
+ });
226
+ case "boolean": return /* @__PURE__ */ f(l, {
227
+ ...u,
228
+ isDisabled: s,
229
+ autoFocus: e.autoFocus,
230
+ isSelected: r === !0,
231
+ onSelectedChange: d
232
+ });
233
+ case "dateTime": return /* @__PURE__ */ f(n, {
234
+ ...u,
235
+ required: c,
236
+ disabled: s,
237
+ type: "datetime-local",
238
+ step: "0.001",
239
+ autoFocus: e.autoFocus,
240
+ value: _(r)?.slice(0, -1) ?? "",
241
+ onChange: (e) => d(_(`${e.target.value}Z`))
242
+ });
243
+ case "richText": return /* @__PURE__ */ f(a, {
244
+ ...u,
245
+ label: e.label,
246
+ isRequired: c,
247
+ isDisabled: s,
248
+ placeholder: e.placeholder,
249
+ format: "json",
250
+ value: r === void 0 ? i : r,
251
+ onChange: d
252
+ });
253
+ case "select": return /* @__PURE__ */ f(j, {
254
+ field: e,
255
+ ...p
256
+ });
257
+ case "asset":
258
+ case "reference": return /* @__PURE__ */ f(M, {
259
+ field: e,
260
+ items: o,
261
+ ...p
262
+ });
263
+ }
264
+ }
265
+ //#endregion
266
+ //#region src/components/FormEditor/index.tsx
267
+ function P({ field: t, stored: n, items: r, isDisabled: i, error: a, isErrorVisible: o, onChange: s }) {
268
+ let c = u(() => a === void 0 && o ? E(t, n) : void 0, [
269
+ a,
270
+ o,
271
+ t,
272
+ n
273
+ ]), l = t.type === "dateTime" ? `${t.label} (UTC)` : t.label;
274
+ return /* @__PURE__ */ f(e, {
275
+ label: l,
276
+ hint: t.hint,
277
+ error: a ?? c,
278
+ isRequired: t.required,
279
+ isGroup: t.type === "asset" || t.type === "reference",
280
+ children: (e) => /* @__PURE__ */ f(N, {
281
+ field: t,
282
+ stored: n,
283
+ items: r,
284
+ isDisabled: i,
285
+ accessibility: e,
286
+ onChange: s
287
+ })
288
+ });
289
+ }
290
+ function F({ schema: e, value: t, defaultValue: n = {}, onValueChange: r, pickerItems: i = {}, errors: a = {}, isDisabled: o = !1, showAllErrors: s = !1, columns: c = 1, minColumnWidth: l = "16rem", className: p, style: h, ...g }) {
291
+ let [_, v] = d(n), [y, b] = d(() => /* @__PURE__ */ new Set()), x = t ?? _;
292
+ u(() => m(e), [e]);
293
+ let S = (e, n) => {
294
+ let i = { ...x };
295
+ n === void 0 ? delete i[e] : i[e] = n, t === void 0 && v(i), b((t) => new Set(t).add(e)), r?.(i, { fieldId: e });
296
+ }, C = c === "auto" ? { "--form-editor-min-column": l } : {
297
+ "--form-editor-columns": c,
298
+ "--form-editor-min-column": l
299
+ };
300
+ return /* @__PURE__ */ f("div", {
301
+ className: [
302
+ "ds-form-editor",
303
+ c === "auto" && "ds-form-editor--auto",
304
+ p
305
+ ].filter(Boolean).join(" "),
306
+ style: {
307
+ ...C,
308
+ ...h
309
+ },
310
+ ...g,
311
+ children: e.fields.map((e) => {
312
+ let t = Object.hasOwn(x, e.id) ? x[e.id] : void 0, n = o || !!e.isDisabled, r = n || "readOnly" in e && !!e.readOnly, c = s || y.has(e.id) || e.type === "number" && typeof t == "number";
313
+ return /* @__PURE__ */ f(P, {
314
+ field: e,
315
+ stored: t,
316
+ items: i[e.id] ?? [],
317
+ isDisabled: n,
318
+ error: a[e.id],
319
+ isErrorVisible: c,
320
+ onChange: (t) => {
321
+ r || S(e.id, t);
322
+ }
323
+ }, e.id);
324
+ })
325
+ });
326
+ }
327
+ //#endregion
328
+ export { D as n, F as t };
329
+
330
+ //# sourceMappingURL=FormEditor-DuW8x78Y.js.map