@namroodinc/liquid-editorial 0.1.1 → 0.1.4
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/AGENTS.md +11 -0
- package/README.md +8 -0
- package/UPGRADING.md +56 -0
- package/dist/chunks/{EntryEditor-cjDk4vdH.cjs → EntryEditor-C32lKKnV.cjs} +2 -2
- package/dist/chunks/{EntryEditor-cjDk4vdH.cjs.map → EntryEditor-C32lKKnV.cjs.map} +1 -1
- package/dist/chunks/EntryEditor-xVuiqtFo.js +181 -0
- package/dist/chunks/{EntryEditor-BF808OZR.js.map → EntryEditor-xVuiqtFo.js.map} +1 -1
- package/dist/chunks/RichTextEditor-CSpjsL1z.js +357 -0
- package/dist/chunks/RichTextEditor-CSpjsL1z.js.map +1 -0
- package/dist/chunks/RichTextEditor-DC_0oN1s.cjs +2 -0
- package/dist/chunks/RichTextEditor-DC_0oN1s.cjs.map +1 -0
- package/dist/components/AssetGrid/index.js.map +1 -1
- package/dist/components/EntryEditor/index.cjs +1 -1
- package/dist/components/EntryEditor/index.js +3 -3
- package/dist/components/EntryPicker/index.cjs +1 -1
- package/dist/components/EntryPicker/index.cjs.map +1 -1
- package/dist/components/EntryPicker/index.js +4 -4
- package/dist/components/EntryPicker/index.js.map +1 -1
- package/dist/components/Field/index.js.map +1 -1
- package/dist/components/ImageCropper/index.js.map +1 -1
- package/dist/components/Modal/index.cjs +1 -1
- package/dist/components/Modal/index.cjs.map +1 -1
- package/dist/components/Modal/index.js +2 -2
- package/dist/components/Modal/index.js.map +1 -1
- package/dist/components/Modal/style.css +1 -0
- package/dist/components/RichTextEditor/index.cjs +1 -1
- package/dist/components/RichTextEditor/index.d.cts.map +1 -1
- package/dist/components/RichTextEditor/index.d.ts.map +1 -1
- package/dist/components/RichTextEditor/index.js +3 -1
- package/dist/components/SortableList/index.cjs +1 -1
- package/dist/components/SortableList/index.cjs.map +1 -1
- package/dist/components/SortableList/index.d.cts +4 -3
- package/dist/components/SortableList/index.d.cts.map +1 -1
- package/dist/components/SortableList/index.d.ts +4 -3
- package/dist/components/SortableList/index.d.ts.map +1 -1
- package/dist/components/SortableList/index.js +55 -48
- package/dist/components/SortableList/index.js.map +1 -1
- package/dist/components/Table/index.cjs +1 -1
- package/dist/components/Table/index.cjs.map +1 -1
- package/dist/components/Table/index.d.cts +5 -1
- package/dist/components/Table/index.d.cts.map +1 -1
- package/dist/components/Table/index.d.ts +5 -1
- package/dist/components/Table/index.d.ts.map +1 -1
- package/dist/components/Table/index.js +74 -55
- package/dist/components/Table/index.js.map +1 -1
- package/dist/components/Table/style.css +4 -0
- package/dist/components/Time/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +13 -13
- package/package.json +3 -1
- package/dist/chunks/EntryEditor-BF808OZR.js +0 -181
- package/dist/chunks/RichTextEditor-DRt-kibg.js +0 -248
- package/dist/chunks/RichTextEditor-DRt-kibg.js.map +0 -1
- package/dist/chunks/RichTextEditor-tFkISOvq.cjs +0 -2
- package/dist/chunks/RichTextEditor-tFkISOvq.cjs.map +0 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Liquid Editorial agent guidance
|
|
2
|
+
|
|
3
|
+
## Contributing to this library
|
|
4
|
+
|
|
5
|
+
Add a decision document for every change going forward.
|
|
6
|
+
|
|
7
|
+
Every releasable change set must increment the package version and update the corresponding version entry in `UPGRADING.md` with changes, adoption steps, compatibility notes, and verification steps for agents in consuming projects. Start with `0.1.3` after `0.1.2`; subsequent change sets increment the patch version unless a minor or major release is explicitly requested. Related edits in one change set share a version. Keep the manifest and lockfile versions aligned and include this file and `UPGRADING.md` in the package.
|
|
8
|
+
|
|
9
|
+
## Using the installed package
|
|
10
|
+
|
|
11
|
+
For agents working in a consuming project: read `UPGRADING.md` beside this file in the installed package. Review every entry between the project's previous package version and the newly installed version, apply the relevant adoption steps in the consuming project, and run its verification checks. The contributor rules above apply to this library's source repository.
|
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ npm install @namroodinc/liquid-editorial
|
|
|
10
10
|
|
|
11
11
|
React and React DOM 18.2 through 19 are supported peer dependencies.
|
|
12
12
|
|
|
13
|
+
When updating the package, see [the versioned upgrade guide](UPGRADING.md) for adoption and compatibility notes. Agents can start with `AGENTS.md` in the installed package.
|
|
14
|
+
|
|
13
15
|
## Use
|
|
14
16
|
|
|
15
17
|
Import the complete component stylesheet once near your application entry point:
|
|
@@ -65,6 +67,12 @@ Use `defaultValue` for an uncontrolled editor, `errors` for app-specific message
|
|
|
65
67
|
|
|
66
68
|
`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.
|
|
67
69
|
|
|
70
|
+
Links open a URL modal from the toolbar or by clicking an existing body link. Save preserves selected text and formatting; with no selection, it inserts the URL as linked text. Existing links offer Remove link in the modal.
|
|
71
|
+
|
|
72
|
+
## Table states
|
|
73
|
+
|
|
74
|
+
Empty `TableBody` children automatically show “No data found.” across the table; override the text with `Table.emptyMessage`. Use `isEmpty` when custom row components determine whether there are results, or set it to `false` to suppress the message while the UI loads data. Loading indicators and skeleton rows are composed by the consuming UI. Standalone `TableEmpty` uses the same default message and supports a custom `title`. See the [upgrade guide example](UPGRADING.md#013).
|
|
75
|
+
|
|
68
76
|
## Themes and fonts
|
|
69
77
|
|
|
70
78
|
Set `data-theme` on the document root to `light`, `dark`, `cherry-glaze`, or `black-hawaii`. The default font tokens use system stacks and can be overridden by the consumer without downloading bundled font files:
|
package/UPGRADING.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Liquid Editorial upgrades
|
|
2
|
+
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Empty tables automatically show “No data found.” with an optional message override. Loading UI remains composed by the consuming application.
|
|
8
|
+
- Rich text links use a URL modal with Save and Cancel. Both the toolbar and a click on a body link open the modal; existing links also offer Remove link.
|
|
9
|
+
- The installed package includes `AGENTS.md` and this versioned upgrade guide for agents working in consuming projects.
|
|
10
|
+
|
|
11
|
+
### Adoption
|
|
12
|
+
|
|
13
|
+
Use the shared empty message while keeping loading indicators in your application:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<TableFrame>
|
|
17
|
+
<Table emptyMessage="No matching articles."
|
|
18
|
+
isEmpty={!isLoading && articles.length === 0} aria-busy={isLoading}>
|
|
19
|
+
<TableHead>
|
|
20
|
+
<TableRow>
|
|
21
|
+
<TableHeaderCell>Title</TableHeaderCell>
|
|
22
|
+
<TableHeaderCell align="end">Status</TableHeaderCell>
|
|
23
|
+
</TableRow>
|
|
24
|
+
</TableHead>
|
|
25
|
+
<TableBody>
|
|
26
|
+
{articles.map((article) => (
|
|
27
|
+
<TableRow key={article.id}>
|
|
28
|
+
<TableCell emphasis="primary">{article.title}</TableCell>
|
|
29
|
+
<TableCell align="end">{article.status}</TableCell>
|
|
30
|
+
</TableRow>
|
|
31
|
+
))}
|
|
32
|
+
</TableBody>
|
|
33
|
+
</Table>
|
|
34
|
+
</TableFrame>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The example suppresses the empty message while the application loads data. Compose loading indicators or skeleton rows in your UI as needed; the table renders the body children you provide. Native attributes such as `aria-busy` are forwarded unchanged. Use the library's `TableBody` to receive the automatic empty state.
|
|
38
|
+
|
|
39
|
+
An empty body now shows the default message automatically. Use `emptyMessage` to replace its text, or keep composing a custom row containing `TableEmpty` for descriptions and actions. When a custom row component renders the rows internally, pass `isEmpty={articles.length === 0}` because its rendered children cannot be inferred. `isEmpty={false}` explicitly allows a blank body. Standalone `TableEmpty` also defaults its optional `title` to “No data found.”
|
|
40
|
+
|
|
41
|
+
RTE consumers receive the modal automatically. Select text and choose Add link, or position the cursor in a link and choose Edit link. Clicking an existing body link opens Edit link for the full link, including formatted spans. Save changes only the URL and preserves the link text and formatting. With an empty cursor, Add link inserts the URL as linked text. Remove link keeps the text. Empty or TipTap-disallowed URLs show an error; relative URLs retain their existing support.
|
|
42
|
+
|
|
43
|
+
### Compatibility
|
|
44
|
+
|
|
45
|
+
Existing table props, native attributes, refs, populated rows, and application-defined loading rows remain supported. Empty bodies now display a message unless explicitly overridden; pass `isEmpty={false}` to retain a blank body, including while data is loading. Components that supply native `tbody` instead of `TableBody` keep managing their own body states. Existing `TableEmpty` descriptions and actions continue to work. No loading or column-count props are added to `Table`.
|
|
46
|
+
|
|
47
|
+
The RTE's HTML and JSON props and `onChange` payloads are unchanged. Opening, typing in, and cancelling the modal emit no content changes. The toolbar's previous immediate Remove link action is now Edit link, with removal available in the modal. Tests that depended on `window.prompt` or the previous toolbar label need updating. A disabled editor cannot open the dialog; replacing its document or disabling it closes an open dialog without applying its draft.
|
|
48
|
+
|
|
49
|
+
The existing aggregate and component CSS imports include the required table and modal styles; continue importing `@namroodinc/liquid-editorial/styles.css` for the complete stylesheet.
|
|
50
|
+
|
|
51
|
+
### Verify in the consuming project
|
|
52
|
+
|
|
53
|
+
- During initial loading and refetching, check your application's loading UI and confirm the empty message stays hidden until results are known.
|
|
54
|
+
- Check loaded rows, zero results, filtered zero results, custom empty messages, and custom row/header components.
|
|
55
|
+
- Add, edit, remove, and cancel links in both standalone RTEs and `EntryEditor` fields. Check text/formatting preservation, keyboard submission, focus, and that a containing form is not submitted by Save.
|
|
56
|
+
- Run the project's type checks, tests, and build against `0.1.3`.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("
|
|
2
|
-
//# sourceMappingURL=EntryEditor-
|
|
1
|
+
const e=require("../components/Field/index.cjs"),t=require("../components/TextField/index.cjs"),n=require("./RichTextEditor-DC_0oN1s.cjs"),r=require("../components/EntryPicker/index.cjs"),i=require("../components/NumberField/index.cjs"),a=require("../components/Switch/index.cjs");let o=require("react"),s=require("react/jsx-runtime");var c=new Set([`shortText`,`longText`,`richText`,`number`,`boolean`,`dateTime`,`asset`,`reference`]);function l(e){let t=new Set;for(let n of e.fields){if(!n.id?.trim()||t.has(n.id))throw Error(`Content model field IDs must be nonempty and unique.`);if(!c.has(n.type))throw Error(`Unsupported content field type: ${n.type}`);t.add(n.id)}}function u(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 d(e,t){return!!e&&typeof e==`object`&&t in e&&typeof e[t]==`string`&&e[t].trim().length>0}function f(e){return!!e.text?.trim()||e.type===`horizontalRule`||!!e.content?.some(f)}function p(e,t){return l(e),Object.fromEntries(e.fields.flatMap(e=>{let r=Object.hasOwn(t,e.id)?t[e.id]:void 0,i;if(r===void 0)e.required&&(i=`${e.label} is required.`);else switch(e.type){case`shortText`:case`longText`:typeof r==`string`?e.required&&!r.trim()&&(i=`${e.label} is required.`):i=`Enter text.`;break;case`richText`:i=n.n(r),!i&&e.required&&!f(r)&&(i=`${e.label} is required.`);break;case`number`:(typeof r!=`number`||!Number.isFinite(r))&&(i=`Enter a finite number.`);break;case`boolean`:typeof r!=`boolean`&&(i=`Choose a boolean value.`);break;case`dateTime`:u(r)||(i=`Enter a valid UTC date and time.`);break;case`asset`:case`reference`:{let t=e.type===`asset`?`assetId`:`entryId`;(e.multiple?!Array.isArray(r)||!r.every(e=>d(e,t)):!d(r,t))?i=e.multiple?`Choose a list of ${e.type===`asset`?`assets`:`entries`}.`:`Choose an ${e.type===`asset`?`asset`:`entry`}.`:e.required&&Array.isArray(r)&&r.length===0&&(i=`${e.label} is required.`);break}}return i?[[e.id,i]]:[]}))}function m(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 h({model:c,value:l,defaultValue:d={},onValueChange:f,pickerItems:h={},errors:g={},isDisabled:_=!1,showAllErrors:v=!1,className:y,...b}){let[x,S]=(0,o.useState)(d),[C,w]=(0,o.useState)(()=>new Set),T=l??x,E=p(c,T),D=(e,t)=>{if(_)return;let n={...T};t===void 0?delete n[e]:n[e]=t,l===void 0&&S(n),w(t=>new Set(t).add(e)),f?.(n)};return(0,s.jsx)(`div`,{className:[`ds-entry-editor`,y].filter(Boolean).join(` `),...b,children:c.fields.map(o=>{let c=Object.hasOwn(T,o.id)?T[o.id]:void 0,l=o.type===`asset`||o.type===`reference`,d=o.type===`dateTime`?`${o.label} (UTC)`:o.label,f=g[o.id]??(v||C.has(o.id)?E[o.id]:void 0);return(0,s.jsx)(e.Field,{label:d,hint:o.hint,error:f,isRequired:o.required,isGroup:l,children:({required:e,...l})=>{switch(o.type){case`shortText`:case`longText`:{let n=o.type===`shortText`?t.TextField:t.TextArea;return(0,s.jsx)(n,{...l,required:e,disabled:_,placeholder:o.placeholder,value:typeof c==`string`?c:``,onChange:e=>D(o.id,e.target.value)})}case`number`:return(0,s.jsx)(i.NumberField,{...l,required:e,isDisabled:_,placeholder:o.placeholder,step:`any`,hasSteppers:!1,value:typeof c==`number`&&Number.isFinite(c)?c:``,onValueChange:e=>{(e===``||Number.isFinite(e))&&D(o.id,e===``?void 0:e)}});case`boolean`:return(0,s.jsx)(a.Switch,{...l,isDisabled:_,isSelected:c===!0,onSelectedChange:e=>D(o.id,e)});case`dateTime`:return(0,s.jsx)(t.TextField,{...l,required:e,disabled:_,type:`datetime-local`,step:`0.001`,value:u(c)?.slice(0,-1)??``,onChange:e=>D(o.id,u(`${e.target.value}Z`))});case`richText`:return(0,s.jsx)(n.t,{...l,label:o.label,isRequired:e,isDisabled:_,placeholder:o.placeholder,format:`json`,value:c===void 0?n.r:c,onChange:e=>D(o.id,e)});case`asset`:case`reference`:return(0,s.jsx)(`div`,{...l,role:`group`,children:(0,s.jsx)(r.EntryPicker,{items:h[o.id]??[],value:m(o,c),label:o.type===`asset`?`Assets`:`Entries`,addLabel:`Choose ${o.label}`,mode:o.multiple?`multiple`:`single`,isDisabled:_,onValueChange:e=>{if(o.type===`asset`){let t=e.map(e=>({assetId:e}));D(o.id,o.multiple?t:t[0])}else{let t=e.map(e=>({entryId:e}));D(o.id,o.multiple?t:t[0])}}})})}}},o.id)})})}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return h}});
|
|
2
|
+
//# sourceMappingURL=EntryEditor-C32lKKnV.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntryEditor-cjDk4vdH.cjs","names":["documentError","useState","Field","TextField","TextArea","NumberField","Switch","RichTextEditor","emptyDocument","EntryPicker","require_RichTextEditor","require_components_EntryPicker_index","require_components_Field_index","require_components_NumberField_index","require_components_Switch_index","require_components_TextField_index","react","react_jsx_runtime","fieldTypes","assertModel","ids","field","model","parseUtcDateTime","value","match","canonical","date","isLink","key","hasRichText","node","validateEntryValues","values","error","item","selectedIds","link","id","EntryEditor","rest","internal","setInternal","defaultValue","changed","setChanged","current","validation","commit","isDisabled","next","previous","onValueChange","className","stored","isPicker","label","errors","showAllErrors","accessibility","Control","event","required","pickerItems","links"],"sources":["../../src/components/EntryEditor/model.ts","../../src/components/EntryEditor/index.tsx"],"sourcesContent":["import type { JSONContent } from \"@tiptap/react\";\nimport { documentError } from \"../RichTextEditor/document\";\nimport type { RichTextDocument } from \"../RichTextEditor\";\n\ninterface ContentFieldBase {\n id: string;\n label: string;\n hint?: string;\n placeholder?: string;\n required?: boolean;\n}\n\nexport type ContentField = ContentFieldBase & (\n | { type: \"shortText\" | \"longText\" | \"richText\" | \"number\" | \"boolean\" | \"dateTime\"; multiple?: never }\n | { type: \"asset\" | \"reference\"; multiple?: boolean }\n);\n\nexport interface ContentModel {\n fields: readonly ContentField[];\n}\n\nexport interface AssetValue { assetId: string }\nexport interface ReferenceValue { entryId: string }\nexport type EntryFieldValue = string | number | boolean | RichTextDocument | AssetValue | ReferenceValue | AssetValue[] | ReferenceValue[];\nexport type EntryValues = Record<string, EntryFieldValue | undefined>;\nexport type EntryErrors = Partial<Record<string, string>>;\n\nconst fieldTypes = new Set([\"shortText\", \"longText\", \"richText\", \"number\", \"boolean\", \"dateTime\", \"asset\", \"reference\"]);\n\nfunction assertModel(model: ContentModel) {\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(\"Content model field IDs must be nonempty and unique.\");\n if (!fieldTypes.has(field.type)) throw new Error(`Unsupported content field type: ${field.type}`);\n ids.add(field.id);\n }\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\n/** Validate data loaded from JSON as well as values emitted by EntryEditor. */\nexport function validateEntryValues(model: ContentModel, values: Record<string, unknown>): EntryErrors {\n assertModel(model);\n return Object.fromEntries(model.fields.flatMap((field): [string, string][] => {\n const value = Object.hasOwn(values, field.id) ? values[field.id] : undefined;\n let error: string | undefined;\n if (value === undefined) {\n if (field.required) error = `${field.label} is required.`;\n } else {\n switch (field.type) {\n case \"shortText\":\n case \"longText\":\n if (typeof value !== \"string\") error = \"Enter text.\";\n else if (field.required && !value.trim()) error = `${field.label} is required.`;\n break;\n case \"richText\":\n error = documentError(value);\n if (!error && field.required && !hasRichText(value as RichTextDocument)) error = `${field.label} is required.`;\n break;\n case \"number\":\n if (typeof value !== \"number\" || !Number.isFinite(value)) error = \"Enter a finite number.\";\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") error = \"Choose a boolean value.\";\n break;\n case \"dateTime\":\n if (!parseUtcDateTime(value)) error = \"Enter a valid UTC date and time.\";\n break;\n case \"asset\":\n case \"reference\": {\n const key = field.type === \"asset\" ? \"assetId\" : \"entryId\";\n if (field.multiple ? !Array.isArray(value) || !value.every((item) => isLink(item, key)) : !isLink(value, key)) {\n error = field.multiple\n ? `Choose a list of ${field.type === \"asset\" ? \"assets\" : \"entries\"}.`\n : `Choose an ${field.type === \"asset\" ? \"asset\" : \"entry\"}.`;\n } else if (field.required && Array.isArray(value) && value.length === 0) error = `${field.label} is required.`;\n break;\n }\n }\n }\n return error ? [[field.id, error]] : [];\n }));\n}\n","import { useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { EntryPicker, type EntryItem } from \"../EntryPicker\";\nimport { Field } from \"../Field\";\nimport { NumberField } from \"../NumberField\";\nimport { RichTextEditor, type RichTextDocument } from \"../RichTextEditor\";\nimport { emptyDocument } from \"../RichTextEditor/document\";\nimport { Switch } from \"../Switch\";\nimport { TextArea, TextField } from \"../TextField\";\nimport { parseUtcDateTime, validateEntryValues, type ContentField, type ContentModel, type EntryErrors, type EntryFieldValue, type EntryValues } from \"./model\";\nimport \"./style.css\";\n\nexport { validateEntryValues } from \"./model\";\nexport type { AssetValue, ContentField, ContentModel, EntryErrors, EntryFieldValue, EntryValues, ReferenceValue } from \"./model\";\n\nexport interface EntryEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n model: ContentModel;\n value?: EntryValues;\n defaultValue?: EntryValues;\n onValueChange?: (values: EntryValues) => void;\n /** Available assets or entries, keyed by content field ID. */\n pickerItems?: Record<string, EntryItem[]>;\n /** App-provided messages take precedence over built-in validation. */\n errors?: EntryErrors;\n isDisabled?: boolean;\n /** Reveal validation for untouched fields, for example after a submit attempt. */\n showAllErrors?: boolean;\n}\n\nfunction selectedIds(field: ContentField, value: EntryFieldValue | 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\n/** An ordered set of content inputs; submission and persistence belong to the app. */\nexport function EntryEditor({\n model,\n value,\n defaultValue = {},\n onValueChange,\n pickerItems = {},\n errors = {},\n isDisabled = false,\n showAllErrors = false,\n className,\n ...rest\n}: EntryEditorProps) {\n const [internal, setInternal] = useState<EntryValues>(defaultValue);\n const [changed, setChanged] = useState<Set<string>>(() => new Set());\n const current = value ?? internal;\n const validation = validateEntryValues(model, current);\n\n const commit = (id: string, next: EntryFieldValue | undefined) => {\n if (isDisabled) return;\n const values = { ...current };\n if (next === undefined) delete values[id];\n else values[id] = next;\n if (value === undefined) setInternal(values);\n setChanged((previous) => new Set(previous).add(id));\n onValueChange?.(values);\n };\n\n return (\n <div className={[\"ds-entry-editor\", className].filter(Boolean).join(\" \")} {...rest}>\n {model.fields.map((field) => {\n const stored = Object.hasOwn(current, field.id) ? current[field.id] : undefined;\n const isPicker = field.type === \"asset\" || field.type === \"reference\";\n const label = field.type === \"dateTime\" ? `${field.label} (UTC)` : field.label;\n const error = errors[field.id] ?? (showAllErrors || changed.has(field.id) ? validation[field.id] : undefined);\n return (\n <Field key={field.id} label={label} hint={field.hint} error={error} isRequired={field.required} isGroup={isPicker}>\n {({ required, ...accessibility }): ReactNode => {\n switch (field.type) {\n case \"shortText\":\n case \"longText\": {\n const Control = field.type === \"shortText\" ? TextField : TextArea;\n return <Control {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => commit(field.id, event.target.value)} />;\n }\n case \"number\":\n return <NumberField {...accessibility} required={required} isDisabled={isDisabled} placeholder={field.placeholder} step=\"any\"\n hasSteppers={false} value={typeof stored === \"number\" && Number.isFinite(stored) ? stored : \"\"}\n onValueChange={(next) => { if (next === \"\" || Number.isFinite(next)) commit(field.id, next === \"\" ? undefined : next); }} />;\n case \"boolean\":\n return <Switch {...accessibility} isDisabled={isDisabled} isSelected={stored === true}\n onSelectedChange={(next) => commit(field.id, next)} />;\n case \"dateTime\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} type=\"datetime-local\" step=\"0.001\"\n value={parseUtcDateTime(stored)?.slice(0, -1) ?? \"\"}\n onChange={(event) => commit(field.id, 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={(next) => commit(field.id, next)} />;\n case \"asset\":\n case \"reference\":\n return <div {...accessibility} role=\"group\">\n <EntryPicker items={pickerItems[field.id] ?? []} value={selectedIds(field, stored)}\n label={field.type === \"asset\" ? \"Assets\" : \"Entries\"} addLabel={`Choose ${field.label}`}\n mode={field.multiple ? \"multiple\" : \"single\"} isDisabled={isDisabled}\n onValueChange={(ids) => {\n if (field.type === \"asset\") {\n const links = ids.map((assetId) => ({ assetId }));\n commit(field.id, field.multiple ? links : links[0]);\n } else {\n const links = ids.map((entryId) => ({ entryId }));\n commit(field.id, field.multiple ? links : links[0]);\n }\n }} />\n </div>;\n }\n }}\n </Field>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EntryEditor-C32lKKnV.cjs","names":["documentError","useState","Field","TextField","TextArea","NumberField","Switch","RichTextEditor","emptyDocument","EntryPicker","require_components_Field_index","require_components_TextField_index","require_RichTextEditor","require_components_EntryPicker_index","require_components_NumberField_index","require_components_Switch_index","react","react_jsx_runtime","fieldTypes","assertModel","ids","field","model","parseUtcDateTime","value","match","canonical","date","isLink","key","hasRichText","node","validateEntryValues","values","error","item","selectedIds","link","id","EntryEditor","rest","internal","setInternal","defaultValue","changed","setChanged","current","validation","commit","isDisabled","next","previous","onValueChange","className","stored","isPicker","label","errors","showAllErrors","accessibility","Control","event","required","pickerItems","links"],"sources":["../../src/components/EntryEditor/model.ts","../../src/components/EntryEditor/index.tsx"],"sourcesContent":["import type { JSONContent } from \"@tiptap/react\";\nimport { documentError } from \"../RichTextEditor/document\";\nimport type { RichTextDocument } from \"../RichTextEditor\";\n\ninterface ContentFieldBase {\n id: string;\n label: string;\n hint?: string;\n placeholder?: string;\n required?: boolean;\n}\n\nexport type ContentField = ContentFieldBase & (\n | { type: \"shortText\" | \"longText\" | \"richText\" | \"number\" | \"boolean\" | \"dateTime\"; multiple?: never }\n | { type: \"asset\" | \"reference\"; multiple?: boolean }\n);\n\nexport interface ContentModel {\n fields: readonly ContentField[];\n}\n\nexport interface AssetValue { assetId: string }\nexport interface ReferenceValue { entryId: string }\nexport type EntryFieldValue = string | number | boolean | RichTextDocument | AssetValue | ReferenceValue | AssetValue[] | ReferenceValue[];\nexport type EntryValues = Record<string, EntryFieldValue | undefined>;\nexport type EntryErrors = Partial<Record<string, string>>;\n\nconst fieldTypes = new Set([\"shortText\", \"longText\", \"richText\", \"number\", \"boolean\", \"dateTime\", \"asset\", \"reference\"]);\n\nfunction assertModel(model: ContentModel) {\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(\"Content model field IDs must be nonempty and unique.\");\n if (!fieldTypes.has(field.type)) throw new Error(`Unsupported content field type: ${field.type}`);\n ids.add(field.id);\n }\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\n/** Validate data loaded from JSON as well as values emitted by EntryEditor. */\nexport function validateEntryValues(model: ContentModel, values: Record<string, unknown>): EntryErrors {\n assertModel(model);\n return Object.fromEntries(model.fields.flatMap((field): [string, string][] => {\n const value = Object.hasOwn(values, field.id) ? values[field.id] : undefined;\n let error: string | undefined;\n if (value === undefined) {\n if (field.required) error = `${field.label} is required.`;\n } else {\n switch (field.type) {\n case \"shortText\":\n case \"longText\":\n if (typeof value !== \"string\") error = \"Enter text.\";\n else if (field.required && !value.trim()) error = `${field.label} is required.`;\n break;\n case \"richText\":\n error = documentError(value);\n if (!error && field.required && !hasRichText(value as RichTextDocument)) error = `${field.label} is required.`;\n break;\n case \"number\":\n if (typeof value !== \"number\" || !Number.isFinite(value)) error = \"Enter a finite number.\";\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") error = \"Choose a boolean value.\";\n break;\n case \"dateTime\":\n if (!parseUtcDateTime(value)) error = \"Enter a valid UTC date and time.\";\n break;\n case \"asset\":\n case \"reference\": {\n const key = field.type === \"asset\" ? \"assetId\" : \"entryId\";\n if (field.multiple ? !Array.isArray(value) || !value.every((item) => isLink(item, key)) : !isLink(value, key)) {\n error = field.multiple\n ? `Choose a list of ${field.type === \"asset\" ? \"assets\" : \"entries\"}.`\n : `Choose an ${field.type === \"asset\" ? \"asset\" : \"entry\"}.`;\n } else if (field.required && Array.isArray(value) && value.length === 0) error = `${field.label} is required.`;\n break;\n }\n }\n }\n return error ? [[field.id, error]] : [];\n }));\n}\n","import { useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { EntryPicker, type EntryItem } from \"../EntryPicker\";\nimport { Field } from \"../Field\";\nimport { NumberField } from \"../NumberField\";\nimport { RichTextEditor, type RichTextDocument } from \"../RichTextEditor\";\nimport { emptyDocument } from \"../RichTextEditor/document\";\nimport { Switch } from \"../Switch\";\nimport { TextArea, TextField } from \"../TextField\";\nimport { parseUtcDateTime, validateEntryValues, type ContentField, type ContentModel, type EntryErrors, type EntryFieldValue, type EntryValues } from \"./model\";\nimport \"./style.css\";\n\nexport { validateEntryValues } from \"./model\";\nexport type { AssetValue, ContentField, ContentModel, EntryErrors, EntryFieldValue, EntryValues, ReferenceValue } from \"./model\";\n\nexport interface EntryEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n model: ContentModel;\n value?: EntryValues;\n defaultValue?: EntryValues;\n onValueChange?: (values: EntryValues) => void;\n /** Available assets or entries, keyed by content field ID. */\n pickerItems?: Record<string, EntryItem[]>;\n /** App-provided messages take precedence over built-in validation. */\n errors?: EntryErrors;\n isDisabled?: boolean;\n /** Reveal validation for untouched fields, for example after a submit attempt. */\n showAllErrors?: boolean;\n}\n\nfunction selectedIds(field: ContentField, value: EntryFieldValue | 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\n/** An ordered set of content inputs; submission and persistence belong to the app. */\nexport function EntryEditor({\n model,\n value,\n defaultValue = {},\n onValueChange,\n pickerItems = {},\n errors = {},\n isDisabled = false,\n showAllErrors = false,\n className,\n ...rest\n}: EntryEditorProps) {\n const [internal, setInternal] = useState<EntryValues>(defaultValue);\n const [changed, setChanged] = useState<Set<string>>(() => new Set());\n const current = value ?? internal;\n const validation = validateEntryValues(model, current);\n\n const commit = (id: string, next: EntryFieldValue | undefined) => {\n if (isDisabled) return;\n const values = { ...current };\n if (next === undefined) delete values[id];\n else values[id] = next;\n if (value === undefined) setInternal(values);\n setChanged((previous) => new Set(previous).add(id));\n onValueChange?.(values);\n };\n\n return (\n <div className={[\"ds-entry-editor\", className].filter(Boolean).join(\" \")} {...rest}>\n {model.fields.map((field) => {\n const stored = Object.hasOwn(current, field.id) ? current[field.id] : undefined;\n const isPicker = field.type === \"asset\" || field.type === \"reference\";\n const label = field.type === \"dateTime\" ? `${field.label} (UTC)` : field.label;\n const error = errors[field.id] ?? (showAllErrors || changed.has(field.id) ? validation[field.id] : undefined);\n return (\n <Field key={field.id} label={label} hint={field.hint} error={error} isRequired={field.required} isGroup={isPicker}>\n {({ required, ...accessibility }): ReactNode => {\n switch (field.type) {\n case \"shortText\":\n case \"longText\": {\n const Control = field.type === \"shortText\" ? TextField : TextArea;\n return <Control {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => commit(field.id, event.target.value)} />;\n }\n case \"number\":\n return <NumberField {...accessibility} required={required} isDisabled={isDisabled} placeholder={field.placeholder} step=\"any\"\n hasSteppers={false} value={typeof stored === \"number\" && Number.isFinite(stored) ? stored : \"\"}\n onValueChange={(next) => { if (next === \"\" || Number.isFinite(next)) commit(field.id, next === \"\" ? undefined : next); }} />;\n case \"boolean\":\n return <Switch {...accessibility} isDisabled={isDisabled} isSelected={stored === true}\n onSelectedChange={(next) => commit(field.id, next)} />;\n case \"dateTime\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} type=\"datetime-local\" step=\"0.001\"\n value={parseUtcDateTime(stored)?.slice(0, -1) ?? \"\"}\n onChange={(event) => commit(field.id, 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={(next) => commit(field.id, next)} />;\n case \"asset\":\n case \"reference\":\n return <div {...accessibility} role=\"group\">\n <EntryPicker items={pickerItems[field.id] ?? []} value={selectedIds(field, stored)}\n label={field.type === \"asset\" ? \"Assets\" : \"Entries\"} addLabel={`Choose ${field.label}`}\n mode={field.multiple ? \"multiple\" : \"single\"} isDisabled={isDisabled}\n onValueChange={(ids) => {\n if (field.type === \"asset\") {\n const links = ids.map((assetId) => ({ assetId }));\n commit(field.id, field.multiple ? links : links[0]);\n } else {\n const links = ids.map((entryId) => ({ entryId }));\n commit(field.id, field.multiple ? links : links[0]);\n }\n }} />\n </div>;\n }\n }}\n </Field>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
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-CSpjsL1z.js";
|
|
4
|
+
import { EntryPicker as o } from "../components/EntryPicker/index.js";
|
|
5
|
+
import { NumberField as s } from "../components/NumberField/index.js";
|
|
6
|
+
import { Switch as c } from "../components/Switch/index.js";
|
|
7
|
+
import { useState as l } from "react";
|
|
8
|
+
import { jsx as u } from "react/jsx-runtime";
|
|
9
|
+
//#region src/components/EntryEditor/model.ts
|
|
10
|
+
var d = /* @__PURE__ */ new Set([
|
|
11
|
+
"shortText",
|
|
12
|
+
"longText",
|
|
13
|
+
"richText",
|
|
14
|
+
"number",
|
|
15
|
+
"boolean",
|
|
16
|
+
"dateTime",
|
|
17
|
+
"asset",
|
|
18
|
+
"reference"
|
|
19
|
+
]);
|
|
20
|
+
function f(e) {
|
|
21
|
+
let t = /* @__PURE__ */ new Set();
|
|
22
|
+
for (let n of e.fields) {
|
|
23
|
+
if (!n.id?.trim() || t.has(n.id)) throw Error("Content model field IDs must be nonempty and unique.");
|
|
24
|
+
if (!d.has(n.type)) throw Error(`Unsupported content field type: ${n.type}`);
|
|
25
|
+
t.add(n.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function p(e) {
|
|
29
|
+
if (typeof e != "string") return;
|
|
30
|
+
let t = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(?:Z|\+00:00)$/.exec(e);
|
|
31
|
+
if (!t) return;
|
|
32
|
+
let n = `${t[1]}:${t[2] ?? "00"}.${(t[3] ?? "").padEnd(3, "0")}Z`, r = new Date(n);
|
|
33
|
+
if (Number.isFinite(r.getTime()) && r.toISOString() === n) return n;
|
|
34
|
+
}
|
|
35
|
+
function m(e, t) {
|
|
36
|
+
return !!e && typeof e == "object" && t in e && typeof e[t] == "string" && e[t].trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
function h(e) {
|
|
39
|
+
return !!e.text?.trim() || e.type === "horizontalRule" || !!e.content?.some(h);
|
|
40
|
+
}
|
|
41
|
+
function g(e, t) {
|
|
42
|
+
return f(e), Object.fromEntries(e.fields.flatMap((e) => {
|
|
43
|
+
let n = Object.hasOwn(t, e.id) ? t[e.id] : void 0, i;
|
|
44
|
+
if (n === void 0) e.required && (i = `${e.label} is required.`);
|
|
45
|
+
else switch (e.type) {
|
|
46
|
+
case "shortText":
|
|
47
|
+
case "longText":
|
|
48
|
+
typeof n == "string" ? e.required && !n.trim() && (i = `${e.label} is required.`) : i = "Enter text.";
|
|
49
|
+
break;
|
|
50
|
+
case "richText":
|
|
51
|
+
i = r(n), !i && e.required && !h(n) && (i = `${e.label} is required.`);
|
|
52
|
+
break;
|
|
53
|
+
case "number":
|
|
54
|
+
(typeof n != "number" || !Number.isFinite(n)) && (i = "Enter a finite number.");
|
|
55
|
+
break;
|
|
56
|
+
case "boolean":
|
|
57
|
+
typeof n != "boolean" && (i = "Choose a boolean value.");
|
|
58
|
+
break;
|
|
59
|
+
case "dateTime":
|
|
60
|
+
p(n) || (i = "Enter a valid UTC date and time.");
|
|
61
|
+
break;
|
|
62
|
+
case "asset":
|
|
63
|
+
case "reference": {
|
|
64
|
+
let t = e.type === "asset" ? "assetId" : "entryId";
|
|
65
|
+
(e.multiple ? !Array.isArray(n) || !n.every((e) => m(e, t)) : !m(n, t)) ? i = e.multiple ? `Choose a list of ${e.type === "asset" ? "assets" : "entries"}.` : `Choose an ${e.type === "asset" ? "asset" : "entry"}.` : e.required && Array.isArray(n) && n.length === 0 && (i = `${e.label} is required.`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return i ? [[e.id, i]] : [];
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/components/EntryEditor/index.tsx
|
|
74
|
+
function _(e, t) {
|
|
75
|
+
let n = e.type === "asset" ? "assetId" : "entryId";
|
|
76
|
+
return (Array.isArray(t) ? t : t === void 0 ? [] : [t]).flatMap((e) => {
|
|
77
|
+
if (!e || typeof e != "object" || !(n in e)) return [];
|
|
78
|
+
let t = e[n];
|
|
79
|
+
return typeof t == "string" ? [t] : [];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function v({ model: r, value: d, defaultValue: f = {}, onValueChange: m, pickerItems: h = {}, errors: v = {}, isDisabled: y = !1, showAllErrors: b = !1, className: x, ...S }) {
|
|
83
|
+
let [C, w] = l(f), [T, E] = l(() => /* @__PURE__ */ new Set()), D = d ?? C, O = g(r, D), k = (e, t) => {
|
|
84
|
+
if (y) return;
|
|
85
|
+
let n = { ...D };
|
|
86
|
+
t === void 0 ? delete n[e] : n[e] = t, d === void 0 && w(n), E((t) => new Set(t).add(e)), m?.(n);
|
|
87
|
+
};
|
|
88
|
+
return /* @__PURE__ */ u("div", {
|
|
89
|
+
className: ["ds-entry-editor", x].filter(Boolean).join(" "),
|
|
90
|
+
...S,
|
|
91
|
+
children: r.fields.map((r) => {
|
|
92
|
+
let l = Object.hasOwn(D, r.id) ? D[r.id] : void 0, d = r.type === "asset" || r.type === "reference", f = r.type === "dateTime" ? `${r.label} (UTC)` : r.label, m = v[r.id] ?? (b || T.has(r.id) ? O[r.id] : void 0);
|
|
93
|
+
return /* @__PURE__ */ u(e, {
|
|
94
|
+
label: f,
|
|
95
|
+
hint: r.hint,
|
|
96
|
+
error: m,
|
|
97
|
+
isRequired: r.required,
|
|
98
|
+
isGroup: d,
|
|
99
|
+
children: ({ required: e, ...d }) => {
|
|
100
|
+
switch (r.type) {
|
|
101
|
+
case "shortText":
|
|
102
|
+
case "longText": {
|
|
103
|
+
let i = r.type === "shortText" ? n : t;
|
|
104
|
+
return /* @__PURE__ */ u(i, {
|
|
105
|
+
...d,
|
|
106
|
+
required: e,
|
|
107
|
+
disabled: y,
|
|
108
|
+
placeholder: r.placeholder,
|
|
109
|
+
value: typeof l == "string" ? l : "",
|
|
110
|
+
onChange: (e) => k(r.id, e.target.value)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
case "number": return /* @__PURE__ */ u(s, {
|
|
114
|
+
...d,
|
|
115
|
+
required: e,
|
|
116
|
+
isDisabled: y,
|
|
117
|
+
placeholder: r.placeholder,
|
|
118
|
+
step: "any",
|
|
119
|
+
hasSteppers: !1,
|
|
120
|
+
value: typeof l == "number" && Number.isFinite(l) ? l : "",
|
|
121
|
+
onValueChange: (e) => {
|
|
122
|
+
(e === "" || Number.isFinite(e)) && k(r.id, e === "" ? void 0 : e);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
case "boolean": return /* @__PURE__ */ u(c, {
|
|
126
|
+
...d,
|
|
127
|
+
isDisabled: y,
|
|
128
|
+
isSelected: l === !0,
|
|
129
|
+
onSelectedChange: (e) => k(r.id, e)
|
|
130
|
+
});
|
|
131
|
+
case "dateTime": return /* @__PURE__ */ u(n, {
|
|
132
|
+
...d,
|
|
133
|
+
required: e,
|
|
134
|
+
disabled: y,
|
|
135
|
+
type: "datetime-local",
|
|
136
|
+
step: "0.001",
|
|
137
|
+
value: p(l)?.slice(0, -1) ?? "",
|
|
138
|
+
onChange: (e) => k(r.id, p(`${e.target.value}Z`))
|
|
139
|
+
});
|
|
140
|
+
case "richText": return /* @__PURE__ */ u(a, {
|
|
141
|
+
...d,
|
|
142
|
+
label: r.label,
|
|
143
|
+
isRequired: e,
|
|
144
|
+
isDisabled: y,
|
|
145
|
+
placeholder: r.placeholder,
|
|
146
|
+
format: "json",
|
|
147
|
+
value: l === void 0 ? i : l,
|
|
148
|
+
onChange: (e) => k(r.id, e)
|
|
149
|
+
});
|
|
150
|
+
case "asset":
|
|
151
|
+
case "reference": return /* @__PURE__ */ u("div", {
|
|
152
|
+
...d,
|
|
153
|
+
role: "group",
|
|
154
|
+
children: /* @__PURE__ */ u(o, {
|
|
155
|
+
items: h[r.id] ?? [],
|
|
156
|
+
value: _(r, l),
|
|
157
|
+
label: r.type === "asset" ? "Assets" : "Entries",
|
|
158
|
+
addLabel: `Choose ${r.label}`,
|
|
159
|
+
mode: r.multiple ? "multiple" : "single",
|
|
160
|
+
isDisabled: y,
|
|
161
|
+
onValueChange: (e) => {
|
|
162
|
+
if (r.type === "asset") {
|
|
163
|
+
let t = e.map((e) => ({ assetId: e }));
|
|
164
|
+
k(r.id, r.multiple ? t : t[0]);
|
|
165
|
+
} else {
|
|
166
|
+
let t = e.map((e) => ({ entryId: e }));
|
|
167
|
+
k(r.id, r.multiple ? t : t[0]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, r.id);
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { g as n, v as t };
|
|
180
|
+
|
|
181
|
+
//# sourceMappingURL=EntryEditor-xVuiqtFo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntryEditor-BF808OZR.js","names":["documentError","emptyDocument","RichTextEditor","fieldTypes","assertModel","ids","field","model","parseUtcDateTime","value","match","canonical","date","isLink","key","hasRichText","node","validateEntryValues","values","error","item","selectedIds","link","id","EntryEditor","rest","internal","setInternal","useState","defaultValue","changed","setChanged","current","validation","commit","isDisabled","next","previous","onValueChange","className","stored","isPicker","label","errors","showAllErrors","Field","accessibility","Control","TextField","TextArea","event","NumberField","Switch","required","EntryPicker","pickerItems","links"],"sources":["../../src/components/EntryEditor/model.ts","../../src/components/EntryEditor/index.tsx"],"sourcesContent":["import type { JSONContent } from \"@tiptap/react\";\nimport { documentError } from \"../RichTextEditor/document\";\nimport type { RichTextDocument } from \"../RichTextEditor\";\n\ninterface ContentFieldBase {\n id: string;\n label: string;\n hint?: string;\n placeholder?: string;\n required?: boolean;\n}\n\nexport type ContentField = ContentFieldBase & (\n | { type: \"shortText\" | \"longText\" | \"richText\" | \"number\" | \"boolean\" | \"dateTime\"; multiple?: never }\n | { type: \"asset\" | \"reference\"; multiple?: boolean }\n);\n\nexport interface ContentModel {\n fields: readonly ContentField[];\n}\n\nexport interface AssetValue { assetId: string }\nexport interface ReferenceValue { entryId: string }\nexport type EntryFieldValue = string | number | boolean | RichTextDocument | AssetValue | ReferenceValue | AssetValue[] | ReferenceValue[];\nexport type EntryValues = Record<string, EntryFieldValue | undefined>;\nexport type EntryErrors = Partial<Record<string, string>>;\n\nconst fieldTypes = new Set([\"shortText\", \"longText\", \"richText\", \"number\", \"boolean\", \"dateTime\", \"asset\", \"reference\"]);\n\nfunction assertModel(model: ContentModel) {\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(\"Content model field IDs must be nonempty and unique.\");\n if (!fieldTypes.has(field.type)) throw new Error(`Unsupported content field type: ${field.type}`);\n ids.add(field.id);\n }\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\n/** Validate data loaded from JSON as well as values emitted by EntryEditor. */\nexport function validateEntryValues(model: ContentModel, values: Record<string, unknown>): EntryErrors {\n assertModel(model);\n return Object.fromEntries(model.fields.flatMap((field): [string, string][] => {\n const value = Object.hasOwn(values, field.id) ? values[field.id] : undefined;\n let error: string | undefined;\n if (value === undefined) {\n if (field.required) error = `${field.label} is required.`;\n } else {\n switch (field.type) {\n case \"shortText\":\n case \"longText\":\n if (typeof value !== \"string\") error = \"Enter text.\";\n else if (field.required && !value.trim()) error = `${field.label} is required.`;\n break;\n case \"richText\":\n error = documentError(value);\n if (!error && field.required && !hasRichText(value as RichTextDocument)) error = `${field.label} is required.`;\n break;\n case \"number\":\n if (typeof value !== \"number\" || !Number.isFinite(value)) error = \"Enter a finite number.\";\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") error = \"Choose a boolean value.\";\n break;\n case \"dateTime\":\n if (!parseUtcDateTime(value)) error = \"Enter a valid UTC date and time.\";\n break;\n case \"asset\":\n case \"reference\": {\n const key = field.type === \"asset\" ? \"assetId\" : \"entryId\";\n if (field.multiple ? !Array.isArray(value) || !value.every((item) => isLink(item, key)) : !isLink(value, key)) {\n error = field.multiple\n ? `Choose a list of ${field.type === \"asset\" ? \"assets\" : \"entries\"}.`\n : `Choose an ${field.type === \"asset\" ? \"asset\" : \"entry\"}.`;\n } else if (field.required && Array.isArray(value) && value.length === 0) error = `${field.label} is required.`;\n break;\n }\n }\n }\n return error ? [[field.id, error]] : [];\n }));\n}\n","import { useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { EntryPicker, type EntryItem } from \"../EntryPicker\";\nimport { Field } from \"../Field\";\nimport { NumberField } from \"../NumberField\";\nimport { RichTextEditor, type RichTextDocument } from \"../RichTextEditor\";\nimport { emptyDocument } from \"../RichTextEditor/document\";\nimport { Switch } from \"../Switch\";\nimport { TextArea, TextField } from \"../TextField\";\nimport { parseUtcDateTime, validateEntryValues, type ContentField, type ContentModel, type EntryErrors, type EntryFieldValue, type EntryValues } from \"./model\";\nimport \"./style.css\";\n\nexport { validateEntryValues } from \"./model\";\nexport type { AssetValue, ContentField, ContentModel, EntryErrors, EntryFieldValue, EntryValues, ReferenceValue } from \"./model\";\n\nexport interface EntryEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n model: ContentModel;\n value?: EntryValues;\n defaultValue?: EntryValues;\n onValueChange?: (values: EntryValues) => void;\n /** Available assets or entries, keyed by content field ID. */\n pickerItems?: Record<string, EntryItem[]>;\n /** App-provided messages take precedence over built-in validation. */\n errors?: EntryErrors;\n isDisabled?: boolean;\n /** Reveal validation for untouched fields, for example after a submit attempt. */\n showAllErrors?: boolean;\n}\n\nfunction selectedIds(field: ContentField, value: EntryFieldValue | 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\n/** An ordered set of content inputs; submission and persistence belong to the app. */\nexport function EntryEditor({\n model,\n value,\n defaultValue = {},\n onValueChange,\n pickerItems = {},\n errors = {},\n isDisabled = false,\n showAllErrors = false,\n className,\n ...rest\n}: EntryEditorProps) {\n const [internal, setInternal] = useState<EntryValues>(defaultValue);\n const [changed, setChanged] = useState<Set<string>>(() => new Set());\n const current = value ?? internal;\n const validation = validateEntryValues(model, current);\n\n const commit = (id: string, next: EntryFieldValue | undefined) => {\n if (isDisabled) return;\n const values = { ...current };\n if (next === undefined) delete values[id];\n else values[id] = next;\n if (value === undefined) setInternal(values);\n setChanged((previous) => new Set(previous).add(id));\n onValueChange?.(values);\n };\n\n return (\n <div className={[\"ds-entry-editor\", className].filter(Boolean).join(\" \")} {...rest}>\n {model.fields.map((field) => {\n const stored = Object.hasOwn(current, field.id) ? current[field.id] : undefined;\n const isPicker = field.type === \"asset\" || field.type === \"reference\";\n const label = field.type === \"dateTime\" ? `${field.label} (UTC)` : field.label;\n const error = errors[field.id] ?? (showAllErrors || changed.has(field.id) ? validation[field.id] : undefined);\n return (\n <Field key={field.id} label={label} hint={field.hint} error={error} isRequired={field.required} isGroup={isPicker}>\n {({ required, ...accessibility }): ReactNode => {\n switch (field.type) {\n case \"shortText\":\n case \"longText\": {\n const Control = field.type === \"shortText\" ? TextField : TextArea;\n return <Control {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => commit(field.id, event.target.value)} />;\n }\n case \"number\":\n return <NumberField {...accessibility} required={required} isDisabled={isDisabled} placeholder={field.placeholder} step=\"any\"\n hasSteppers={false} value={typeof stored === \"number\" && Number.isFinite(stored) ? stored : \"\"}\n onValueChange={(next) => { if (next === \"\" || Number.isFinite(next)) commit(field.id, next === \"\" ? undefined : next); }} />;\n case \"boolean\":\n return <Switch {...accessibility} isDisabled={isDisabled} isSelected={stored === true}\n onSelectedChange={(next) => commit(field.id, next)} />;\n case \"dateTime\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} type=\"datetime-local\" step=\"0.001\"\n value={parseUtcDateTime(stored)?.slice(0, -1) ?? \"\"}\n onChange={(event) => commit(field.id, 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={(next) => commit(field.id, next)} />;\n case \"asset\":\n case \"reference\":\n return <div {...accessibility} role=\"group\">\n <EntryPicker items={pickerItems[field.id] ?? []} value={selectedIds(field, stored)}\n label={field.type === \"asset\" ? \"Assets\" : \"Entries\"} addLabel={`Choose ${field.label}`}\n mode={field.multiple ? \"multiple\" : \"single\"} isDisabled={isDisabled}\n onValueChange={(ids) => {\n if (field.type === \"asset\") {\n const links = ids.map((assetId) => ({ assetId }));\n commit(field.id, field.multiple ? links : links[0]);\n } else {\n const links = ids.map((entryId) => ({ entryId }));\n commit(field.id, field.multiple ? links : links[0]);\n }\n }} />\n </div>;\n }\n }}\n </Field>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EntryEditor-xVuiqtFo.js","names":["documentError","emptyDocument","RichTextEditor","fieldTypes","assertModel","ids","field","model","parseUtcDateTime","value","match","canonical","date","isLink","key","hasRichText","node","validateEntryValues","values","error","item","selectedIds","link","id","EntryEditor","rest","internal","setInternal","useState","defaultValue","changed","setChanged","current","validation","commit","isDisabled","next","previous","onValueChange","className","stored","isPicker","label","errors","showAllErrors","Field","accessibility","Control","TextField","TextArea","event","NumberField","Switch","required","EntryPicker","pickerItems","links"],"sources":["../../src/components/EntryEditor/model.ts","../../src/components/EntryEditor/index.tsx"],"sourcesContent":["import type { JSONContent } from \"@tiptap/react\";\nimport { documentError } from \"../RichTextEditor/document\";\nimport type { RichTextDocument } from \"../RichTextEditor\";\n\ninterface ContentFieldBase {\n id: string;\n label: string;\n hint?: string;\n placeholder?: string;\n required?: boolean;\n}\n\nexport type ContentField = ContentFieldBase & (\n | { type: \"shortText\" | \"longText\" | \"richText\" | \"number\" | \"boolean\" | \"dateTime\"; multiple?: never }\n | { type: \"asset\" | \"reference\"; multiple?: boolean }\n);\n\nexport interface ContentModel {\n fields: readonly ContentField[];\n}\n\nexport interface AssetValue { assetId: string }\nexport interface ReferenceValue { entryId: string }\nexport type EntryFieldValue = string | number | boolean | RichTextDocument | AssetValue | ReferenceValue | AssetValue[] | ReferenceValue[];\nexport type EntryValues = Record<string, EntryFieldValue | undefined>;\nexport type EntryErrors = Partial<Record<string, string>>;\n\nconst fieldTypes = new Set([\"shortText\", \"longText\", \"richText\", \"number\", \"boolean\", \"dateTime\", \"asset\", \"reference\"]);\n\nfunction assertModel(model: ContentModel) {\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(\"Content model field IDs must be nonempty and unique.\");\n if (!fieldTypes.has(field.type)) throw new Error(`Unsupported content field type: ${field.type}`);\n ids.add(field.id);\n }\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\n/** Validate data loaded from JSON as well as values emitted by EntryEditor. */\nexport function validateEntryValues(model: ContentModel, values: Record<string, unknown>): EntryErrors {\n assertModel(model);\n return Object.fromEntries(model.fields.flatMap((field): [string, string][] => {\n const value = Object.hasOwn(values, field.id) ? values[field.id] : undefined;\n let error: string | undefined;\n if (value === undefined) {\n if (field.required) error = `${field.label} is required.`;\n } else {\n switch (field.type) {\n case \"shortText\":\n case \"longText\":\n if (typeof value !== \"string\") error = \"Enter text.\";\n else if (field.required && !value.trim()) error = `${field.label} is required.`;\n break;\n case \"richText\":\n error = documentError(value);\n if (!error && field.required && !hasRichText(value as RichTextDocument)) error = `${field.label} is required.`;\n break;\n case \"number\":\n if (typeof value !== \"number\" || !Number.isFinite(value)) error = \"Enter a finite number.\";\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") error = \"Choose a boolean value.\";\n break;\n case \"dateTime\":\n if (!parseUtcDateTime(value)) error = \"Enter a valid UTC date and time.\";\n break;\n case \"asset\":\n case \"reference\": {\n const key = field.type === \"asset\" ? \"assetId\" : \"entryId\";\n if (field.multiple ? !Array.isArray(value) || !value.every((item) => isLink(item, key)) : !isLink(value, key)) {\n error = field.multiple\n ? `Choose a list of ${field.type === \"asset\" ? \"assets\" : \"entries\"}.`\n : `Choose an ${field.type === \"asset\" ? \"asset\" : \"entry\"}.`;\n } else if (field.required && Array.isArray(value) && value.length === 0) error = `${field.label} is required.`;\n break;\n }\n }\n }\n return error ? [[field.id, error]] : [];\n }));\n}\n","import { useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { EntryPicker, type EntryItem } from \"../EntryPicker\";\nimport { Field } from \"../Field\";\nimport { NumberField } from \"../NumberField\";\nimport { RichTextEditor, type RichTextDocument } from \"../RichTextEditor\";\nimport { emptyDocument } from \"../RichTextEditor/document\";\nimport { Switch } from \"../Switch\";\nimport { TextArea, TextField } from \"../TextField\";\nimport { parseUtcDateTime, validateEntryValues, type ContentField, type ContentModel, type EntryErrors, type EntryFieldValue, type EntryValues } from \"./model\";\nimport \"./style.css\";\n\nexport { validateEntryValues } from \"./model\";\nexport type { AssetValue, ContentField, ContentModel, EntryErrors, EntryFieldValue, EntryValues, ReferenceValue } from \"./model\";\n\nexport interface EntryEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n model: ContentModel;\n value?: EntryValues;\n defaultValue?: EntryValues;\n onValueChange?: (values: EntryValues) => void;\n /** Available assets or entries, keyed by content field ID. */\n pickerItems?: Record<string, EntryItem[]>;\n /** App-provided messages take precedence over built-in validation. */\n errors?: EntryErrors;\n isDisabled?: boolean;\n /** Reveal validation for untouched fields, for example after a submit attempt. */\n showAllErrors?: boolean;\n}\n\nfunction selectedIds(field: ContentField, value: EntryFieldValue | 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\n/** An ordered set of content inputs; submission and persistence belong to the app. */\nexport function EntryEditor({\n model,\n value,\n defaultValue = {},\n onValueChange,\n pickerItems = {},\n errors = {},\n isDisabled = false,\n showAllErrors = false,\n className,\n ...rest\n}: EntryEditorProps) {\n const [internal, setInternal] = useState<EntryValues>(defaultValue);\n const [changed, setChanged] = useState<Set<string>>(() => new Set());\n const current = value ?? internal;\n const validation = validateEntryValues(model, current);\n\n const commit = (id: string, next: EntryFieldValue | undefined) => {\n if (isDisabled) return;\n const values = { ...current };\n if (next === undefined) delete values[id];\n else values[id] = next;\n if (value === undefined) setInternal(values);\n setChanged((previous) => new Set(previous).add(id));\n onValueChange?.(values);\n };\n\n return (\n <div className={[\"ds-entry-editor\", className].filter(Boolean).join(\" \")} {...rest}>\n {model.fields.map((field) => {\n const stored = Object.hasOwn(current, field.id) ? current[field.id] : undefined;\n const isPicker = field.type === \"asset\" || field.type === \"reference\";\n const label = field.type === \"dateTime\" ? `${field.label} (UTC)` : field.label;\n const error = errors[field.id] ?? (showAllErrors || changed.has(field.id) ? validation[field.id] : undefined);\n return (\n <Field key={field.id} label={label} hint={field.hint} error={error} isRequired={field.required} isGroup={isPicker}>\n {({ required, ...accessibility }): ReactNode => {\n switch (field.type) {\n case \"shortText\":\n case \"longText\": {\n const Control = field.type === \"shortText\" ? TextField : TextArea;\n return <Control {...accessibility} required={required} disabled={isDisabled} placeholder={field.placeholder}\n value={typeof stored === \"string\" ? stored : \"\"} onChange={(event) => commit(field.id, event.target.value)} />;\n }\n case \"number\":\n return <NumberField {...accessibility} required={required} isDisabled={isDisabled} placeholder={field.placeholder} step=\"any\"\n hasSteppers={false} value={typeof stored === \"number\" && Number.isFinite(stored) ? stored : \"\"}\n onValueChange={(next) => { if (next === \"\" || Number.isFinite(next)) commit(field.id, next === \"\" ? undefined : next); }} />;\n case \"boolean\":\n return <Switch {...accessibility} isDisabled={isDisabled} isSelected={stored === true}\n onSelectedChange={(next) => commit(field.id, next)} />;\n case \"dateTime\":\n return <TextField {...accessibility} required={required} disabled={isDisabled} type=\"datetime-local\" step=\"0.001\"\n value={parseUtcDateTime(stored)?.slice(0, -1) ?? \"\"}\n onChange={(event) => commit(field.id, 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={(next) => commit(field.id, next)} />;\n case \"asset\":\n case \"reference\":\n return <div {...accessibility} role=\"group\">\n <EntryPicker items={pickerItems[field.id] ?? []} value={selectedIds(field, stored)}\n label={field.type === \"asset\" ? \"Assets\" : \"Entries\"} addLabel={`Choose ${field.label}`}\n mode={field.multiple ? \"multiple\" : \"single\"} isDisabled={isDisabled}\n onValueChange={(ids) => {\n if (field.type === \"asset\") {\n const links = ids.map((assetId) => ({ assetId }));\n commit(field.id, field.multiple ? links : links[0]);\n } else {\n const links = ids.map((entryId) => ({ entryId }));\n commit(field.id, field.multiple ? links : links[0]);\n }\n }} />\n </div>;\n }\n }}\n </Field>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
|