@elytracms/studio 0.0.86 → 0.0.88

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 (248) hide show
  1. package/convex/asset-revalidate.test.ts +186 -0
  2. package/convex/assets.ts +53 -19
  3. package/convex/cms.ts +22 -0
  4. package/convex/delivery.ts +33 -116
  5. package/convex/functions.ts +3 -3
  6. package/convex/rail-write-parity.test.ts +180 -0
  7. package/convex/redirects.ts +33 -11
  8. package/convex/revalidate.ts +7 -5
  9. package/convex/triggers.ts +53 -3
  10. package/dist/components/ai/ai-chat-panel.js +8 -1
  11. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  12. package/dist/components/app-views/default-modules.d.ts +20 -20
  13. package/dist/components/app-views/default-modules.js +27 -48
  14. package/dist/components/app-views/default-modules.js.map +1 -1
  15. package/dist/components/app-views/studio-config.d.ts +5 -6
  16. package/dist/components/app-views/studio-config.js.map +1 -1
  17. package/dist/components/assets/asset-metadata.d.ts +28 -19
  18. package/dist/components/assets/asset-metadata.js +24 -31
  19. package/dist/components/assets/asset-metadata.js.map +1 -1
  20. package/dist/components/assets/asset-selection.d.ts +24 -5
  21. package/dist/components/assets/asset-selection.js +39 -9
  22. package/dist/components/assets/asset-selection.js.map +1 -1
  23. package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
  24. package/dist/components/assets/assets-collection-view-source.js +25 -31
  25. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  26. package/dist/components/assets/assets-store.d.ts +11 -1
  27. package/dist/components/assets/assets-store.js +10 -0
  28. package/dist/components/assets/assets-store.js.map +1 -1
  29. package/dist/components/assets/focal-point.d.ts +0 -7
  30. package/dist/components/assets/focal-point.js +0 -10
  31. package/dist/components/assets/focal-point.js.map +1 -1
  32. package/dist/components/builder/blocks-panel.js +3 -3
  33. package/dist/components/builder/blocks-panel.js.map +1 -1
  34. package/dist/components/builder/builder-store.d.ts +25 -1
  35. package/dist/components/builder/builder-store.js +3 -2
  36. package/dist/components/builder/builder-store.js.map +1 -1
  37. package/dist/components/builder/canvas-chrome-css.js +0 -17
  38. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  39. package/dist/components/builder/canvas-chrome.d.ts +10 -12
  40. package/dist/components/builder/canvas-chrome.js +12 -13
  41. package/dist/components/builder/canvas-chrome.js.map +1 -1
  42. package/dist/components/builder/canvas-dnd.d.ts +8 -0
  43. package/dist/components/builder/canvas-dnd.js +5 -3
  44. package/dist/components/builder/canvas-dnd.js.map +1 -1
  45. package/dist/components/builder/canvas-render-context.d.ts +23 -24
  46. package/dist/components/builder/canvas-render-context.js +35 -60
  47. package/dist/components/builder/canvas-render-context.js.map +1 -1
  48. package/dist/components/builder/canvas.js +5 -11
  49. package/dist/components/builder/canvas.js.map +1 -1
  50. package/dist/components/builder/clipboard-actions.d.ts +14 -6
  51. package/dist/components/builder/clipboard-actions.js +29 -20
  52. package/dist/components/builder/clipboard-actions.js.map +1 -1
  53. package/dist/components/builder/composition-field-editor.d.ts +19 -26
  54. package/dist/components/builder/composition-field-editor.js +30 -52
  55. package/dist/components/builder/composition-field-editor.js.map +1 -1
  56. package/dist/components/builder/insert-context.d.ts +3 -1
  57. package/dist/components/builder/insert-context.js +15 -10
  58. package/dist/components/builder/insert-context.js.map +1 -1
  59. package/dist/components/builder/page-tree.d.ts +29 -0
  60. package/dist/components/builder/page-tree.js +56 -58
  61. package/dist/components/builder/page-tree.js.map +1 -1
  62. package/dist/components/builder/reuse.js +5 -2
  63. package/dist/components/builder/reuse.js.map +1 -1
  64. package/dist/components/cms/asset-field.d.ts +2 -1
  65. package/dist/components/cms/asset-field.js +39 -83
  66. package/dist/components/cms/asset-field.js.map +1 -1
  67. package/dist/components/cms/asset-picker-core.d.ts +77 -40
  68. package/dist/components/cms/asset-picker-core.js +73 -46
  69. package/dist/components/cms/asset-picker-core.js.map +1 -1
  70. package/dist/components/cms/asset-picker.d.ts +48 -19
  71. package/dist/components/cms/asset-picker.js +69 -30
  72. package/dist/components/cms/asset-picker.js.map +1 -1
  73. package/dist/components/cms/cell-edit.d.ts +3 -1
  74. package/dist/components/cms/cell-edit.js +2 -2
  75. package/dist/components/cms/cell-edit.js.map +1 -1
  76. package/dist/components/cms/cms-composition-validators.js +11 -2
  77. package/dist/components/cms/cms-composition-validators.js.map +1 -1
  78. package/dist/components/cms/cms-store.js +15 -23
  79. package/dist/components/cms/cms-store.js.map +1 -1
  80. package/dist/components/cms/editable-cell.js +3 -3
  81. package/dist/components/cms/editable-cell.js.map +1 -1
  82. package/dist/components/cms/gate-validation.d.ts +27 -2
  83. package/dist/components/cms/gate-validation.js +19 -1
  84. package/dist/components/cms/gate-validation.js.map +1 -1
  85. package/dist/components/cms/rich-text/editor.js +2 -2
  86. package/dist/components/cms/rich-text/editor.js.map +1 -1
  87. package/dist/components/cms/rich-text/insert.d.ts +7 -0
  88. package/dist/components/cms/rich-text/insert.js +7 -0
  89. package/dist/components/cms/rich-text/insert.js.map +1 -1
  90. package/dist/components/cms/use-document-versions.js +1 -0
  91. package/dist/components/cms/use-document-versions.js.map +1 -1
  92. package/dist/components/cms/versions-model.d.ts +0 -12
  93. package/dist/components/cms/versions-model.js +3 -15
  94. package/dist/components/cms/versions-model.js.map +1 -1
  95. package/dist/components/components/component-detail.js +1 -1
  96. package/dist/components/components/component-detail.js.map +1 -1
  97. package/dist/components/components/transfer-dialog.js +2 -4
  98. package/dist/components/components/transfer-dialog.js.map +1 -1
  99. package/dist/components/components/transfer.d.ts +13 -10
  100. package/dist/components/components/transfer.js +21 -19
  101. package/dist/components/components/transfer.js.map +1 -1
  102. package/dist/components/nav-issues.d.ts +23 -3
  103. package/dist/components/nav-issues.js +38 -2
  104. package/dist/components/nav-issues.js.map +1 -1
  105. package/dist/components/project-session.d.ts +9 -0
  106. package/dist/components/project-session.js +1 -1
  107. package/dist/components/project-session.js.map +1 -1
  108. package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
  109. package/dist/components/redirects/redirects-collection-view-source.js +20 -6
  110. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  111. package/dist/components/section-nav.js +26 -2
  112. package/dist/components/section-nav.js.map +1 -1
  113. package/dist/components/section-surface.js +3 -2
  114. package/dist/components/section-surface.js.map +1 -1
  115. package/dist/components/settings-form.js +1 -2
  116. package/dist/components/settings-form.js.map +1 -1
  117. package/dist/components/studio-context.js +1 -18
  118. package/dist/components/studio-context.js.map +1 -1
  119. package/dist/components/studio-project-screen.js +3 -5
  120. package/dist/components/studio-project-screen.js.map +1 -1
  121. package/dist/lib/ai/apply-content.js +73 -29
  122. package/dist/lib/ai/apply-content.js.map +1 -1
  123. package/dist/lib/ai/constraints.d.ts +35 -0
  124. package/dist/lib/ai/constraints.js +104 -4
  125. package/dist/lib/ai/constraints.js.map +1 -1
  126. package/dist/lib/ai/route.js +4 -1
  127. package/dist/lib/ai/route.js.map +1 -1
  128. package/dist/lib/ai/shape.d.ts +3 -2
  129. package/dist/lib/ai/shape.js +16 -24
  130. package/dist/lib/ai/shape.js.map +1 -1
  131. package/dist/lib/ai/tools.d.ts +10 -4
  132. package/dist/lib/ai/tools.js +12 -4
  133. package/dist/lib/ai/tools.js.map +1 -1
  134. package/dist/lib/ai/types.d.ts +8 -0
  135. package/dist/lib/ai/types.js.map +1 -1
  136. package/dist/lib/ai/update-fields.js +33 -12
  137. package/dist/lib/ai/update-fields.js.map +1 -1
  138. package/dist/lib/assets/asset-placements.d.ts +35 -0
  139. package/dist/lib/assets/asset-placements.js +74 -0
  140. package/dist/lib/assets/asset-placements.js.map +1 -0
  141. package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
  142. package/dist/lib/assets/backfill-thumbs.js +13 -25
  143. package/dist/lib/assets/backfill-thumbs.js.map +1 -1
  144. package/dist/lib/clipboard/paste-issues.d.ts +17 -15
  145. package/dist/lib/clipboard/paste-issues.js +8 -45
  146. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  147. package/dist/lib/clipboard/paste-target.d.ts +25 -1
  148. package/dist/lib/clipboard/paste-target.js +13 -6
  149. package/dist/lib/clipboard/paste-target.js.map +1 -1
  150. package/dist/lib/cms/published-documents.js +1 -0
  151. package/dist/lib/cms/published-documents.js.map +1 -1
  152. package/dist/lib/cms-views/view-model.d.ts +3 -3
  153. package/dist/lib/data/collection-content.d.ts +22 -1
  154. package/dist/lib/data/collection-content.js +15 -0
  155. package/dist/lib/data/collection-content.js.map +1 -1
  156. package/dist/lib/data/index.d.ts +0 -3
  157. package/dist/lib/data/index.js +4 -6
  158. package/dist/lib/data/index.js.map +1 -1
  159. package/dist/lib/dnd/drop-rules.d.ts +45 -2
  160. package/dist/lib/dnd/drop-rules.js +64 -9
  161. package/dist/lib/dnd/drop-rules.js.map +1 -1
  162. package/dist/lib/engine-version.d.ts +3 -3
  163. package/dist/lib/engine-version.js +3 -3
  164. package/dist/lib/engine-version.js.map +1 -1
  165. package/dist/lib/i18n/messages.de.js +9 -56
  166. package/dist/lib/i18n/messages.de.js.map +1 -1
  167. package/dist/lib/i18n/messages.en.d.ts +8 -53
  168. package/dist/lib/i18n/messages.en.js +14 -57
  169. package/dist/lib/i18n/messages.en.js.map +1 -1
  170. package/dist/lib/locale/fallback.js +8 -2
  171. package/dist/lib/locale/fallback.js.map +1 -1
  172. package/dist/lib/locale/index.d.ts +0 -1
  173. package/dist/lib/locale/index.js +0 -1
  174. package/dist/lib/locale/index.js.map +1 -1
  175. package/dist/lib/locale/locale-store.d.ts +10 -3
  176. package/dist/lib/locale/locale-store.js +12 -5
  177. package/dist/lib/locale/locale-store.js.map +1 -1
  178. package/dist/lib/operations/session-hydration.d.ts +24 -22
  179. package/dist/lib/operations/session-hydration.js +15 -53
  180. package/dist/lib/operations/session-hydration.js.map +1 -1
  181. package/dist/lib/persistence/convex-adapter.js +63 -75
  182. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  183. package/dist/lib/persona/labels.d.ts +0 -2
  184. package/dist/lib/persona/labels.js +1 -7
  185. package/dist/lib/persona/labels.js.map +1 -1
  186. package/dist/lib/registry/index.d.ts +4 -2
  187. package/dist/lib/registry/index.js +26 -21
  188. package/dist/lib/registry/index.js.map +1 -1
  189. package/dist/lib/registry/project-components.js +0 -3
  190. package/dist/lib/registry/project-components.js.map +1 -1
  191. package/dist/lib/routes/route-issues.js +4 -5
  192. package/dist/lib/routes/route-issues.js.map +1 -1
  193. package/dist/lib/theme.js +3 -2
  194. package/dist/lib/theme.js.map +1 -1
  195. package/dist/lib/webhooks/tags.js +11 -14
  196. package/dist/lib/webhooks/tags.js.map +1 -1
  197. package/dist/lib/workspace/config-issues.d.ts +6 -0
  198. package/dist/lib/workspace/config-issues.js +8 -4
  199. package/dist/lib/workspace/config-issues.js.map +1 -1
  200. package/dist/lib/workspace/index.d.ts +2 -2
  201. package/dist/lib/workspace/index.js +2 -2
  202. package/dist/lib/workspace/index.js.map +1 -1
  203. package/dist/previews/enumerate.d.ts +27 -9
  204. package/dist/previews/enumerate.js +27 -6
  205. package/dist/previews/enumerate.js.map +1 -1
  206. package/dist/previews/manifest-format.d.ts +9 -0
  207. package/dist/previews/manifest-format.js.map +1 -1
  208. package/dist/router/match.d.ts +0 -2
  209. package/dist/router/match.js +0 -2
  210. package/dist/router/match.js.map +1 -1
  211. package/dist/router/navigate-once.d.ts +19 -4
  212. package/dist/router/navigate-once.js +22 -5
  213. package/dist/router/navigate-once.js.map +1 -1
  214. package/dist/ui/badge.d.ts +1 -1
  215. package/package.json +3 -3
  216. package/src/styles/studio.css +5 -4
  217. package/src/styles/tokens.css +72 -55
  218. package/convex/content.ts +0 -291
  219. package/dist/components/collab/collab-context.d.ts +0 -27
  220. package/dist/components/collab/collab-context.js +0 -39
  221. package/dist/components/collab/collab-context.js.map +0 -1
  222. package/dist/components/collab/collab-seed.d.ts +0 -15
  223. package/dist/components/collab/collab-seed.js +0 -20
  224. package/dist/components/collab/collab-seed.js.map +0 -1
  225. package/dist/components/extensions/extensions-view.d.ts +0 -7
  226. package/dist/components/extensions/extensions-view.js +0 -127
  227. package/dist/components/extensions/extensions-view.js.map +0 -1
  228. package/dist/lib/bindings/section-bindings.d.ts +0 -24
  229. package/dist/lib/bindings/section-bindings.js +0 -9
  230. package/dist/lib/bindings/section-bindings.js.map +0 -1
  231. package/dist/lib/data/data-store.d.ts +0 -98
  232. package/dist/lib/data/data-store.js +0 -211
  233. package/dist/lib/data/data-store.js.map +0 -1
  234. package/dist/lib/data/providers.d.ts +0 -143
  235. package/dist/lib/data/providers.js +0 -255
  236. package/dist/lib/data/providers.js.map +0 -1
  237. package/dist/lib/data/sample-capture.d.ts +0 -30
  238. package/dist/lib/data/sample-capture.js +0 -70
  239. package/dist/lib/data/sample-capture.js.map +0 -1
  240. package/dist/lib/data/sources-store.d.ts +0 -42
  241. package/dist/lib/data/sources-store.js +0 -120
  242. package/dist/lib/data/sources-store.js.map +0 -1
  243. package/dist/lib/locale/binding-fallback.d.ts +0 -35
  244. package/dist/lib/locale/binding-fallback.js +0 -73
  245. package/dist/lib/locale/binding-fallback.js.map +0 -1
  246. package/dist/lib/starters/kit-session.d.ts +0 -5
  247. package/dist/lib/starters/kit-session.js +0 -17
  248. package/dist/lib/starters/kit-session.js.map +0 -1
@@ -5,7 +5,7 @@ import { Badge, Input, Row, Select, Text } from '@elytracms/studio/ui';
5
5
  import { cn } from '@elytracms/studio/lib/utils';
6
6
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
7
7
  import { editCreatesVariant, resolveDisplayedValue } from '@elytracms/studio/lib/locale';
8
- import { applyAssetSelection, singleAssetIdOf } from '../assets/asset-selection.js';
8
+ import { assetPickerBinding } from './asset-picker-core.js';
9
9
  import { CellValue, toRefArray } from './cell.js';
10
10
  import { DateInput } from './date-input.js';
11
11
  import { datePickerBounds } from './date-input-core.js';
@@ -173,10 +173,10 @@ function CellPicker({ field, value, onCommit, onCancel, }) {
173
173
  }, onChange: (refs) => onCommit(field.multiple ? refs : refs[0], null) }));
174
174
  }
175
175
  if (field.type === 'asset') {
176
- return (_jsx(AssetPicker, { value: field.multiple === false ? singleAssetIdOf(value) : null, open: true, onOpenChange: (open) => {
176
+ return (_jsx(AssetPicker, { open: true, onOpenChange: (open) => {
177
177
  if (!open)
178
178
  onCancel();
179
- }, onChange: (picked) => onCommit(applyAssetSelection(value, picked, field.multiple), null) }));
179
+ }, ...assetPickerBinding(field, value, (next) => onCommit(next, null)) }));
180
180
  }
181
181
  return null;
182
182
  }
@@ -1 +1 @@
1
- {"version":3,"file":"editable-cell.js","sourceRoot":"","sources":["../../../src/components/cms/editable-cell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useRef, useState } from 'react'\nimport type { FocusEvent, KeyboardEvent, ReactNode } from 'react'\nimport { isFieldLocalized } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef, Locale, SelectFieldDef } from '@elytracms/core/cms-core'\nimport { Badge, Input, Row, Select, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { editCreatesVariant, resolveDisplayedValue } from '@elytracms/studio/lib/locale'\nimport { applyAssetSelection, singleAssetIdOf } from '../assets/asset-selection'\nimport { CellValue, toRefArray } from './cell'\nimport { DateInput } from './date-input'\nimport { datePickerBounds } from './date-input-core'\nimport { cellEditKind, cellKey } from './cell-edit'\nimport { fieldLabel } from './cms-labels'\nimport type { AdvanceDirection } from './cell-edit'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { RelationPicker } from './relation-picker'\nimport { AssetPicker } from './asset-picker'\n\n/**\n * One editable table cell (EC-168, the Airtable-feel wedge of vision 3.2).\n *\n * Renders the field's `CellValue` until activated (click / Enter / F2), then\n * swaps to an inline editor (text, number, boolean, select, date) or opens the\n * existing relation/asset picker from the cell. Commits flow through the\n * table's cell-edit controller; rejections render as an error ring + message\n * on this cell — never a toast.\n */\nexport function EditableCell({\n collection,\n doc,\n field,\n editing,\n busy,\n errorMessage,\n onStartEdit,\n onCancel,\n onCommit,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n field: FieldDef\n editing: boolean\n busy: boolean\n errorMessage: string | null\n onStartEdit: () => void\n onCancel: () => void\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n}) {\n const source = useCollectionView()\n // Inline editing is a capability. EC-463: the gate used to sit only on the\n // Tab-advance list, so a rail WITHOUT it (redirects, users, the self-account\n // view) still rendered focusable, clickable, editor-opening cells whose commit\n // then hit \"no `inlineEditing.commitField` → close without dispatching\" — the\n // edit was accepted on screen and silently thrown away. A read-only rail now\n // really does yield no editable cells.\n const kind = source.capabilities.inlineEdit\n ? cellEditKind(field, source.isFieldReadOnly(field))\n : 'none'\n // The table's locale resolution (EC-176): the cell displays the AMBIENT\n // active locale (EC-018 fallback applied) and edits write that locale only.\n // Editing a fallback-shown value therefore creates the localized variant —\n // the fallback locale's value is never mutated.\n const locale = source.activeLocale\n const localized = isFieldLocalized(collection, field.name)\n const { value, fallback } = resolveDisplayedValue(\n collection,\n doc,\n field.name,\n locale,\n source.localeConfig,\n )\n const key = cellKey({ docId: doc.id, field: field.name })\n\n if (editing && kind === 'inline') {\n return (\n <CellEditorFrame\n locale={localized ? locale : null}\n createsLocalizedVariant={\n fallback !== null &&\n editCreatesVariant(collection, doc, field.name, locale, source.localeConfig)\n }\n cellKeyId={key}\n >\n <InlineCellEditor field={field} value={value} busy={busy} onCommit={onCommit} onCancel={onCancel} />\n </CellEditorFrame>\n )\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n // EC-463: Space too — it is half of the native activation contract the\n // `role=\"button\"` this cell now carries promises.\n if (event.key === 'Enter' || event.key === 'F2' || event.key === ' ') {\n event.preventDefault()\n onStartEdit()\n }\n }\n\n const editable = kind !== 'none'\n return (\n <div\n data-cell-key={key}\n data-testid={`cell-${key}`}\n tabIndex={editable ? 0 : undefined}\n // EC-463: an editable cell WAS an anonymous focusable `div` — the\n // accessibility tree listed it as a nameless \"generic, clickable\", so\n // neither a screen reader nor an agent editor could tell what activating it\n // would edit. It is an activation surface, so it says so and names itself.\n role={editable ? 'button' : undefined}\n aria-label={\n editable\n ? translate('cms.cell.editAria', {\n field: fieldLabel(field),\n title: source.titleOf({ collection: collection.id, id: doc.id }),\n })\n : undefined\n }\n // While a picker cell is editing, the dialog renders inside this div. React\n // bubbles portal events through the COMPONENT tree, so a click on the picker\n // (e.g. its ✕) would re-fire onStartEdit and reopen it — hence the `editing`\n // guard. ESC/click-outside dodge this (keydown / close-on-pointerdown).\n onClick={kind === 'none' || editing ? undefined : onStartEdit}\n onKeyDown={kind === 'none' || editing ? undefined : handleKeyDown}\n aria-invalid={errorMessage ? true : undefined}\n className={cn(\n '-mx-1.5 min-h-7 rounded-md px-1.5 py-0.5 outline-none',\n kind !== 'none' &&\n 'cursor-pointer transition-colors hover:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring/50',\n errorMessage && 'bg-destructive/5 ring-2 ring-inset ring-destructive/60',\n busy && 'opacity-60',\n )}\n >\n <CellValue collection={collection} doc={doc} field={field} />\n {errorMessage ? (\n <Text size=\"xs\" className=\"mt-0.5 text-destructive\" data-testid={`cell-error-${key}`}>\n {errorMessage}\n </Text>\n ) : null}\n {editing && kind === 'picker' ? (\n <CellPicker field={field} value={value} onCommit={onCommit} onCancel={onCancel} />\n ) : null}\n </div>\n )\n}\n\n/**\n * Editing chrome: marks the edited locale at the point of edit (EC-018), and\n * says so explicitly when committing will CREATE the active locale's variant\n * from a fallback-shown value (EC-176) — translation is a visible act.\n */\nfunction CellEditorFrame({\n locale,\n createsLocalizedVariant = false,\n cellKeyId,\n children,\n}: {\n locale: Locale | null\n /** True when the cell showed a fallback — committing creates the variant. */\n createsLocalizedVariant?: boolean\n cellKeyId: string\n children: ReactNode\n}) {\n return (\n <Row gap=\"1\" align=\"center\" data-cell-key={cellKeyId} className=\"-mx-1.5 min-w-[9rem]\">\n {locale ? (\n <Badge\n variant={createsLocalizedVariant ? 'info' : 'outline'}\n className=\"uppercase\"\n data-testid=\"cell-editing-locale\"\n title={\n createsLocalizedVariant\n ? translate('cms.cell.createsVariantTitle', { locale: locale.toUpperCase() })\n : undefined\n }\n >\n {locale}\n {createsLocalizedVariant ? (\n <span className=\"lowercase\"> · {translate('cms.cell.newVariant')}</span>\n ) : null}\n </Badge>\n ) : null}\n {children}\n </Row>\n )\n}\n\ntype CellDraft = string | boolean | string[]\n\n/**\n * The in-cell editor: draft → commit on Enter (advance down), Tab/Shift-Tab\n * (advance next/prev) or blur; Esc cancels without dispatching anything.\n */\nfunction InlineCellEditor({\n field,\n value,\n busy,\n onCommit,\n onCancel,\n}: {\n field: FieldDef\n value: unknown\n busy: boolean\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n onCancel: () => void\n}) {\n const [draft, setDraft] = useState<CellDraft>(() => initialDraft(field, value))\n const draftRef = useRef(draft)\n draftRef.current = draft\n // EC-463: the ref moves SYNCHRONOUSLY with the control, not at the next\n // render. A control that only emits its final value on the settling keystroke\n // (the date control's Enter) would otherwise be committed one render stale —\n // the same event that produces the value also triggers `commit()` below.\n function updateDraft(next: CellDraft) {\n draftRef.current = next\n setDraft(next)\n }\n // Each editor instance settles exactly once (Tab keydown + the blur it\n // causes must not commit twice).\n const settledRef = useRef(false)\n\n function commit(advance: AdvanceDirection | null) {\n if (settledRef.current) return\n settledRef.current = true\n onCommit(draftValue(field, draftRef.current), advance)\n }\n\n function cancel() {\n if (settledRef.current) return\n settledRef.current = true\n onCancel()\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (event.key === 'Enter') {\n event.preventDefault()\n commit('down')\n } else if (event.key === 'Tab') {\n event.preventDefault()\n commit(event.shiftKey ? 'prev' : 'next')\n } else if (event.key === 'Escape') {\n event.preventDefault()\n event.stopPropagation()\n cancel()\n }\n }\n\n function handleBlur(event: FocusEvent<HTMLDivElement>) {\n // Focus left the editor entirely (e.g. a click elsewhere) → commit in place.\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) commit(null)\n }\n\n return (\n <div\n onKeyDown={handleKeyDown}\n onBlur={handleBlur}\n onClick={(event) => event.stopPropagation()}\n className=\"min-w-0 flex-1\"\n data-testid=\"cell-editor\"\n aria-busy={busy || undefined}\n >\n <CellEditorControl field={field} draft={draft} onDraft={updateDraft} />\n </div>\n )\n}\n\n/** The actual input for the field type (compact, table-density variants). */\nfunction CellEditorControl({\n field,\n draft,\n onDraft,\n}: {\n field: FieldDef\n draft: CellDraft\n onDraft: (draft: CellDraft) => void\n}) {\n switch (field.type) {\n case 'boolean':\n return (\n <label className=\"flex h-7 items-center gap-2 text-sm\">\n <input\n autoFocus\n type=\"checkbox\"\n checked={draft === true}\n onChange={(event) => onDraft(event.target.checked)}\n className=\"size-4 accent-primary\"\n data-testid=\"cell-editor-boolean\"\n />\n <Text as=\"span\" size=\"xs\" tone=\"muted\">\n {draft === true ? translate('common.true') : translate('common.false')}\n </Text>\n </label>\n )\n case 'number':\n return (\n <Input\n autoFocus\n type=\"number\"\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 text-sm\"\n data-testid=\"cell-editor-number\"\n />\n )\n case 'date':\n // EC-463: the shared date control. The cell's own draft already commits on\n // Enter/Tab/blur, so what this adds here is the working insert/paste path\n // and the guard against a re-render clobbering half-typed segments.\n return (\n <DateInput\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(stored) => onDraft(stored ? stored.slice(0, 10) : '')}\n className=\"h-7 text-sm\"\n testId=\"cell-editor-date\"\n {...datePickerBounds(field.validation)}\n />\n )\n case 'select':\n if (field.multiple) {\n return <MultiSelectControl field={field} draft={draft} onDraft={onDraft} />\n }\n return (\n <Select\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 py-0 text-sm\"\n data-testid=\"cell-editor-select\"\n >\n <option value=\"\">—</option>\n {field.options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {localizedLabel(opt.label, opt.value)}\n </option>\n ))}\n </Select>\n )\n default:\n return (\n <Input\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 text-sm\"\n data-testid=\"cell-editor-text\"\n />\n )\n }\n}\n\nfunction MultiSelectControl({\n field,\n draft,\n onDraft,\n}: {\n field: SelectFieldDef\n draft: CellDraft\n onDraft: (draft: CellDraft) => void\n}) {\n const selected = Array.isArray(draft) ? draft : []\n return (\n <Row gap=\"2\" wrap data-testid=\"cell-editor-multiselect\">\n {field.options.map((opt, index) => {\n const on = selected.includes(opt.value)\n return (\n <label key={opt.value} className=\"flex items-center gap-1.5 text-xs\">\n <input\n autoFocus={index === 0}\n type=\"checkbox\"\n checked={on}\n onChange={() =>\n onDraft(on ? selected.filter((v) => v !== opt.value) : [...selected, opt.value])\n }\n className=\"size-3.5 accent-primary\"\n />\n {localizedLabel(opt.label, opt.value)}\n </label>\n )\n })}\n </Row>\n )\n}\n\n/** Relation/asset cells reuse the existing lightweight pickers, opened from the cell. */\nfunction CellPicker({\n field,\n value,\n onCommit,\n onCancel,\n}: {\n field: FieldDef\n value: unknown\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n onCancel: () => void\n}) {\n if (field.type === 'relation') {\n return (\n <RelationPicker\n targetCollection={field.target}\n multiple={field.multiple}\n value={toRefArray(value)}\n open\n onOpenChange={(open) => {\n if (!open) onCancel()\n }}\n onChange={(refs) => onCommit(field.multiple ? refs : refs[0], null)}\n />\n )\n }\n if (field.type === 'asset') {\n return (\n <AssetPicker\n value={field.multiple === false ? singleAssetIdOf(value) : null}\n open\n onOpenChange={(open) => {\n if (!open) onCancel()\n }}\n onChange={(picked) => onCommit(applyAssetSelection(value, picked, field.multiple), null)}\n />\n )\n }\n return null\n}\n\n// --- Draft <-> stored value mapping (mirrors `FieldControl`'s conventions) -----\n\nfunction initialDraft(field: FieldDef, value: unknown): CellDraft {\n switch (field.type) {\n case 'boolean':\n return value === true\n case 'number':\n return typeof value === 'number' ? String(value) : ''\n case 'date':\n return typeof value === 'string' ? value.slice(0, 10) : ''\n case 'select':\n if (field.multiple) {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []\n }\n return typeof value === 'string' ? value : ''\n default:\n return typeof value === 'string' ? value : ''\n }\n}\n\nfunction draftValue(field: FieldDef, draft: CellDraft): unknown {\n switch (field.type) {\n case 'boolean':\n return draft === true\n case 'number':\n return draft === '' ? undefined : Number(draft)\n case 'date':\n return draft === '' || typeof draft !== 'string' ? undefined : `${draft}T00:00:00.000Z`\n case 'select':\n if (field.multiple) return Array.isArray(draft) ? draft : []\n return draft === '' ? undefined : draft\n default:\n return draft === '' ? undefined : draft\n }\n}\n"]}
1
+ {"version":3,"file":"editable-cell.js","sourceRoot":"","sources":["../../../src/components/cms/editable-cell.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAExC,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,UAAU,EACV,GAAG,EACH,KAAK,EACL,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,QAAQ,GAWT;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,2EAA2E;IAC3E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,6EAA6E;IAC7E,uCAAuC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,UAAU;QACzC,CAAC,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QACpD,CAAC,CAAC,MAAM,CAAA;IACV,wEAAwE;IACxE,4EAA4E;IAC5E,2EAA2E;IAC3E,gDAAgD;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAA;IAClC,MAAM,SAAS,GAAG,gBAAgB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;IAC1D,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,qBAAqB,CAC/C,UAAU,EACV,GAAG,EACH,KAAK,CAAC,IAAI,EACV,MAAM,EACN,MAAM,CAAC,YAAY,CACpB,CAAA;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;IAEzD,IAAI,OAAO,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,CACL,KAAC,eAAe,IACd,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EACjC,uBAAuB,EACrB,QAAQ,KAAK,IAAI;gBACjB,kBAAkB,CAAC,UAAU,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,EAE9E,SAAS,EAAE,GAAG,YAEd,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAI,GACpF,CACnB,CAAA;IACH,CAAC;IAED,SAAS,aAAa,CAAC,KAAoC;QACzD,uEAAuE;QACvE,kDAAkD;QAClD,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;YACrE,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,WAAW,EAAE,CAAA;QACf,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,KAAK,MAAM,CAAA;IAChC,OAAO,CACL,gCACiB,GAAG,iBACL,QAAQ,GAAG,EAAE,EAC1B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;QAClC,kEAAkE;QAClE,sEAAsE;QACtE,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,gBAEnC,QAAQ;YACN,CAAC,CAAC,SAAS,CAAC,mBAAmB,EAAE;gBAC7B,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC;gBACxB,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;aACjE,CAAC;YACJ,CAAC,CAAC,SAAS;QAEf,4EAA4E;QAC5E,6EAA6E;QAC7E,6EAA6E;QAC7E,wEAAwE;QACxE,OAAO,EAAE,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAC7D,SAAS,EAAE,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,kBACnD,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAC7C,SAAS,EAAE,EAAE,CACX,uDAAuD,EACvD,IAAI,KAAK,MAAM;YACb,oGAAoG,EACtG,YAAY,IAAI,wDAAwD,EACxE,IAAI,IAAI,YAAY,CACrB,aAED,KAAC,SAAS,IAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAI,EAC5D,YAAY,CAAC,CAAC,CAAC,CACd,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,yBAAyB,iBAAc,cAAc,GAAG,EAAE,YACjF,YAAY,GACR,CACR,CAAC,CAAC,CAAC,IAAI,EACP,OAAO,IAAI,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAC9B,KAAC,UAAU,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAI,CACnF,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,EACvB,MAAM,EACN,uBAAuB,GAAG,KAAK,EAC/B,SAAS,EACT,QAAQ,GAOT;IACC,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,mBAAgB,SAAS,EAAE,SAAS,EAAC,sBAAsB,aACnF,MAAM,CAAC,CAAC,CAAC,CACR,MAAC,KAAK,IACJ,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EACrD,SAAS,EAAC,WAAW,iBACT,qBAAqB,EACjC,KAAK,EACH,uBAAuB;oBACrB,CAAC,CAAC,SAAS,CAAC,8BAA8B,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;oBAC7E,CAAC,CAAC,SAAS,aAGd,MAAM,EACN,uBAAuB,CAAC,CAAC,CAAC,CACzB,gBAAM,SAAS,EAAC,WAAW,yBAAK,SAAS,CAAC,qBAAqB,CAAC,IAAQ,CACzE,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,IACL,CACP,CAAA;AACH,CAAC;AAID;;;GAGG;AACH,SAAS,gBAAgB,CAAC,EACxB,KAAK,EACL,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,GAOT;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAY,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;IACxB,wEAAwE;IACxE,8EAA8E;IAC9E,6EAA6E;IAC7E,yEAAyE;IACzE,SAAS,WAAW,CAAC,IAAe;QAClC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC;IACD,uEAAuE;IACvE,iCAAiC;IACjC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAEhC,SAAS,MAAM,CAAC,OAAgC;QAC9C,IAAI,UAAU,CAAC,OAAO;YAAE,OAAM;QAC9B,UAAU,CAAC,OAAO,GAAG,IAAI,CAAA;QACzB,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;IACxD,CAAC;IAED,SAAS,MAAM;QACb,IAAI,UAAU,CAAC,OAAO;YAAE,OAAM;QAC9B,UAAU,CAAC,OAAO,GAAG,IAAI,CAAA;QACzB,QAAQ,EAAE,CAAA;IACZ,CAAC;IAED,SAAS,aAAa,CAAC,KAAoC;QACzD,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YAC1B,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,MAAM,CAAC,MAAM,CAAC,CAAA;QAChB,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QAC1C,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YAClC,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,KAAK,CAAC,eAAe,EAAE,CAAA;YACvB,MAAM,EAAE,CAAA;QACV,CAAC;IACH,CAAC;IAED,SAAS,UAAU,CAAC,KAAiC;QACnD,6EAA6E;QAC7E,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,aAA4B,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,CAAA;IACrF,CAAC;IAED,OAAO,CACL,cACE,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,EAC3C,SAAS,EAAC,gBAAgB,iBACd,aAAa,eACd,IAAI,IAAI,SAAS,YAE5B,KAAC,iBAAiB,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,GAAI,GACnE,CACP,CAAA;AACH,CAAC;AAED,6EAA6E;AAC7E,SAAS,iBAAiB,CAAC,EACzB,KAAK,EACL,KAAK,EACL,OAAO,GAKR;IACC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,CACL,iBAAO,SAAS,EAAC,qCAAqC,aACpD,gBACE,SAAS,QACT,IAAI,EAAC,UAAU,EACf,OAAO,EAAE,KAAK,KAAK,IAAI,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAClD,SAAS,EAAC,uBAAuB,iBACrB,qBAAqB,GACjC,EACF,KAAC,IAAI,IAAC,EAAE,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACnC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,GACjE,IACD,CACT,CAAA;QACH,KAAK,QAAQ;YACX,OAAO,CACL,KAAC,KAAK,IACJ,SAAS,QACT,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAChD,SAAS,EAAC,aAAa,iBACX,oBAAoB,GAChC,CACH,CAAA;QACH,KAAK,MAAM;YACT,2EAA2E;YAC3E,0EAA0E;YAC1E,oEAAoE;YACpE,OAAO,CACL,KAAC,SAAS,IACR,SAAS,QACT,KAAK,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAC7C,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAChE,SAAS,EAAC,aAAa,EACvB,MAAM,EAAC,kBAAkB,KACrB,gBAAgB,CAAC,KAAK,CAAC,UAAU,CAAC,GACtC,CACH,CAAA;QACH,KAAK,QAAQ;YACX,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACnB,OAAO,KAAC,kBAAkB,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAI,CAAA;YAC7E,CAAC;YACD,OAAO,CACL,MAAC,MAAM,IACL,SAAS,QACT,KAAK,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAChD,SAAS,EAAC,kBAAkB,iBAChB,oBAAoB,aAEhC,iBAAQ,KAAK,EAAC,EAAE,uBAAW,EAC1B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC1B,iBAAwB,KAAK,EAAE,GAAG,CAAC,KAAK,YACrC,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAD1B,GAAG,CAAC,KAAK,CAEb,CACV,CAAC,IACK,CACV,CAAA;QACH;YACE,OAAO,CACL,KAAC,KAAK,IACJ,SAAS,QACT,KAAK,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAChD,SAAS,EAAC,aAAa,iBACX,kBAAkB,GAC9B,CACH,CAAA;IACL,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,OAAO,GAKR;IACC,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;IAClD,OAAO,CACL,KAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,uBAAa,yBAAyB,YACpD,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACvC,OAAO,CACL,iBAAuB,SAAS,EAAC,mCAAmC,aAClE,gBACE,SAAS,EAAE,KAAK,KAAK,CAAC,EACtB,IAAI,EAAC,UAAU,EACf,OAAO,EAAE,EAAE,EACX,QAAQ,EAAE,GAAG,EAAE,CACb,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAElF,SAAS,EAAC,yBAAyB,GACnC,EACD,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,KAV3B,GAAG,CAAC,KAAK,CAWb,CACT,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC;AAED,yFAAyF;AACzF,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GAMT;IACC,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,OAAO,CACL,KAAC,cAAc,IACb,gBAAgB,EAAE,KAAK,CAAC,MAAM,EAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,EACxB,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,EACxB,IAAI,QACJ,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;gBACrB,IAAI,CAAC,IAAI;oBAAE,QAAQ,EAAE,CAAA;YACvB,CAAC,EACD,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GACnE,CACH,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,OAAO,CACL,KAAC,WAAW,IACV,IAAI,QACJ,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;gBACrB,IAAI,CAAC,IAAI;oBAAE,QAAQ,EAAE,CAAA;YACvB,CAAC,KAKG,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,GACpE,CACH,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,kFAAkF;AAElF,SAAS,YAAY,CAAC,KAAe,EAAE,KAAc;IACnD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,KAAK,KAAK,IAAI,CAAA;QACvB,KAAK,QAAQ;YACX,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACvD,KAAK,MAAM;YACT,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAC5D,KAAK,QAAQ;YACX,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACnB,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;YAC5F,CAAC;YACD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/C;YACE,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;IACjD,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAe,EAAE,KAAgB;IACnD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,KAAK,KAAK,IAAI,CAAA;QACvB,KAAK,QAAQ;YACX,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACjD,KAAK,MAAM;YACT,OAAO,KAAK,KAAK,EAAE,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,KAAK,gBAAgB,CAAA;QACzF,KAAK,QAAQ;YACX,IAAI,KAAK,CAAC,QAAQ;gBAAE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;YAC5D,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;QACzC;YACE,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAC3C,CAAC;AACH,CAAC","sourcesContent":["import { useRef, useState } from 'react'\nimport type { FocusEvent, KeyboardEvent, ReactNode } from 'react'\nimport { isFieldLocalized } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef, Locale, SelectFieldDef } from '@elytracms/core/cms-core'\nimport { Badge, Input, Row, Select, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { editCreatesVariant, resolveDisplayedValue } from '@elytracms/studio/lib/locale'\nimport { assetPickerBinding } from './asset-picker-core'\nimport { CellValue, toRefArray } from './cell'\nimport { DateInput } from './date-input'\nimport { datePickerBounds } from './date-input-core'\nimport { cellEditKind, cellKey } from './cell-edit'\nimport { fieldLabel } from './cms-labels'\nimport type { AdvanceDirection } from './cell-edit'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { RelationPicker } from './relation-picker'\nimport { AssetPicker } from './asset-picker'\n\n/**\n * One editable table cell (EC-168, the Airtable-feel wedge of vision 3.2).\n *\n * Renders the field's `CellValue` until activated (click / Enter / F2), then\n * swaps to an inline editor (text, number, boolean, select, date) or opens the\n * existing relation/asset picker from the cell. Commits flow through the\n * table's cell-edit controller; rejections render as an error ring + message\n * on this cell — never a toast.\n */\nexport function EditableCell({\n collection,\n doc,\n field,\n editing,\n busy,\n errorMessage,\n onStartEdit,\n onCancel,\n onCommit,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n field: FieldDef\n editing: boolean\n busy: boolean\n errorMessage: string | null\n onStartEdit: () => void\n onCancel: () => void\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n}) {\n const source = useCollectionView()\n // Inline editing is a capability. EC-463: the gate used to sit only on the\n // Tab-advance list, so a rail WITHOUT it (redirects, users, the self-account\n // view) still rendered focusable, clickable, editor-opening cells whose commit\n // then hit \"no `inlineEditing.commitField` → close without dispatching\" — the\n // edit was accepted on screen and silently thrown away. A read-only rail now\n // really does yield no editable cells.\n const kind = source.capabilities.inlineEdit\n ? cellEditKind(field, source.isFieldReadOnly(field))\n : 'none'\n // The table's locale resolution (EC-176): the cell displays the AMBIENT\n // active locale (EC-018 fallback applied) and edits write that locale only.\n // Editing a fallback-shown value therefore creates the localized variant —\n // the fallback locale's value is never mutated.\n const locale = source.activeLocale\n const localized = isFieldLocalized(collection, field.name)\n const { value, fallback } = resolveDisplayedValue(\n collection,\n doc,\n field.name,\n locale,\n source.localeConfig,\n )\n const key = cellKey({ docId: doc.id, field: field.name })\n\n if (editing && kind === 'inline') {\n return (\n <CellEditorFrame\n locale={localized ? locale : null}\n createsLocalizedVariant={\n fallback !== null &&\n editCreatesVariant(collection, doc, field.name, locale, source.localeConfig)\n }\n cellKeyId={key}\n >\n <InlineCellEditor field={field} value={value} busy={busy} onCommit={onCommit} onCancel={onCancel} />\n </CellEditorFrame>\n )\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n // EC-463: Space too — it is half of the native activation contract the\n // `role=\"button\"` this cell now carries promises.\n if (event.key === 'Enter' || event.key === 'F2' || event.key === ' ') {\n event.preventDefault()\n onStartEdit()\n }\n }\n\n const editable = kind !== 'none'\n return (\n <div\n data-cell-key={key}\n data-testid={`cell-${key}`}\n tabIndex={editable ? 0 : undefined}\n // EC-463: an editable cell WAS an anonymous focusable `div` — the\n // accessibility tree listed it as a nameless \"generic, clickable\", so\n // neither a screen reader nor an agent editor could tell what activating it\n // would edit. It is an activation surface, so it says so and names itself.\n role={editable ? 'button' : undefined}\n aria-label={\n editable\n ? translate('cms.cell.editAria', {\n field: fieldLabel(field),\n title: source.titleOf({ collection: collection.id, id: doc.id }),\n })\n : undefined\n }\n // While a picker cell is editing, the dialog renders inside this div. React\n // bubbles portal events through the COMPONENT tree, so a click on the picker\n // (e.g. its ✕) would re-fire onStartEdit and reopen it — hence the `editing`\n // guard. ESC/click-outside dodge this (keydown / close-on-pointerdown).\n onClick={kind === 'none' || editing ? undefined : onStartEdit}\n onKeyDown={kind === 'none' || editing ? undefined : handleKeyDown}\n aria-invalid={errorMessage ? true : undefined}\n className={cn(\n '-mx-1.5 min-h-7 rounded-md px-1.5 py-0.5 outline-none',\n kind !== 'none' &&\n 'cursor-pointer transition-colors hover:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring/50',\n errorMessage && 'bg-destructive/5 ring-2 ring-inset ring-destructive/60',\n busy && 'opacity-60',\n )}\n >\n <CellValue collection={collection} doc={doc} field={field} />\n {errorMessage ? (\n <Text size=\"xs\" className=\"mt-0.5 text-destructive\" data-testid={`cell-error-${key}`}>\n {errorMessage}\n </Text>\n ) : null}\n {editing && kind === 'picker' ? (\n <CellPicker field={field} value={value} onCommit={onCommit} onCancel={onCancel} />\n ) : null}\n </div>\n )\n}\n\n/**\n * Editing chrome: marks the edited locale at the point of edit (EC-018), and\n * says so explicitly when committing will CREATE the active locale's variant\n * from a fallback-shown value (EC-176) — translation is a visible act.\n */\nfunction CellEditorFrame({\n locale,\n createsLocalizedVariant = false,\n cellKeyId,\n children,\n}: {\n locale: Locale | null\n /** True when the cell showed a fallback — committing creates the variant. */\n createsLocalizedVariant?: boolean\n cellKeyId: string\n children: ReactNode\n}) {\n return (\n <Row gap=\"1\" align=\"center\" data-cell-key={cellKeyId} className=\"-mx-1.5 min-w-[9rem]\">\n {locale ? (\n <Badge\n variant={createsLocalizedVariant ? 'info' : 'outline'}\n className=\"uppercase\"\n data-testid=\"cell-editing-locale\"\n title={\n createsLocalizedVariant\n ? translate('cms.cell.createsVariantTitle', { locale: locale.toUpperCase() })\n : undefined\n }\n >\n {locale}\n {createsLocalizedVariant ? (\n <span className=\"lowercase\"> · {translate('cms.cell.newVariant')}</span>\n ) : null}\n </Badge>\n ) : null}\n {children}\n </Row>\n )\n}\n\ntype CellDraft = string | boolean | string[]\n\n/**\n * The in-cell editor: draft → commit on Enter (advance down), Tab/Shift-Tab\n * (advance next/prev) or blur; Esc cancels without dispatching anything.\n */\nfunction InlineCellEditor({\n field,\n value,\n busy,\n onCommit,\n onCancel,\n}: {\n field: FieldDef\n value: unknown\n busy: boolean\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n onCancel: () => void\n}) {\n const [draft, setDraft] = useState<CellDraft>(() => initialDraft(field, value))\n const draftRef = useRef(draft)\n draftRef.current = draft\n // EC-463: the ref moves SYNCHRONOUSLY with the control, not at the next\n // render. A control that only emits its final value on the settling keystroke\n // (the date control's Enter) would otherwise be committed one render stale —\n // the same event that produces the value also triggers `commit()` below.\n function updateDraft(next: CellDraft) {\n draftRef.current = next\n setDraft(next)\n }\n // Each editor instance settles exactly once (Tab keydown + the blur it\n // causes must not commit twice).\n const settledRef = useRef(false)\n\n function commit(advance: AdvanceDirection | null) {\n if (settledRef.current) return\n settledRef.current = true\n onCommit(draftValue(field, draftRef.current), advance)\n }\n\n function cancel() {\n if (settledRef.current) return\n settledRef.current = true\n onCancel()\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (event.key === 'Enter') {\n event.preventDefault()\n commit('down')\n } else if (event.key === 'Tab') {\n event.preventDefault()\n commit(event.shiftKey ? 'prev' : 'next')\n } else if (event.key === 'Escape') {\n event.preventDefault()\n event.stopPropagation()\n cancel()\n }\n }\n\n function handleBlur(event: FocusEvent<HTMLDivElement>) {\n // Focus left the editor entirely (e.g. a click elsewhere) → commit in place.\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) commit(null)\n }\n\n return (\n <div\n onKeyDown={handleKeyDown}\n onBlur={handleBlur}\n onClick={(event) => event.stopPropagation()}\n className=\"min-w-0 flex-1\"\n data-testid=\"cell-editor\"\n aria-busy={busy || undefined}\n >\n <CellEditorControl field={field} draft={draft} onDraft={updateDraft} />\n </div>\n )\n}\n\n/** The actual input for the field type (compact, table-density variants). */\nfunction CellEditorControl({\n field,\n draft,\n onDraft,\n}: {\n field: FieldDef\n draft: CellDraft\n onDraft: (draft: CellDraft) => void\n}) {\n switch (field.type) {\n case 'boolean':\n return (\n <label className=\"flex h-7 items-center gap-2 text-sm\">\n <input\n autoFocus\n type=\"checkbox\"\n checked={draft === true}\n onChange={(event) => onDraft(event.target.checked)}\n className=\"size-4 accent-primary\"\n data-testid=\"cell-editor-boolean\"\n />\n <Text as=\"span\" size=\"xs\" tone=\"muted\">\n {draft === true ? translate('common.true') : translate('common.false')}\n </Text>\n </label>\n )\n case 'number':\n return (\n <Input\n autoFocus\n type=\"number\"\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 text-sm\"\n data-testid=\"cell-editor-number\"\n />\n )\n case 'date':\n // EC-463: the shared date control. The cell's own draft already commits on\n // Enter/Tab/blur, so what this adds here is the working insert/paste path\n // and the guard against a re-render clobbering half-typed segments.\n return (\n <DateInput\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(stored) => onDraft(stored ? stored.slice(0, 10) : '')}\n className=\"h-7 text-sm\"\n testId=\"cell-editor-date\"\n {...datePickerBounds(field.validation)}\n />\n )\n case 'select':\n if (field.multiple) {\n return <MultiSelectControl field={field} draft={draft} onDraft={onDraft} />\n }\n return (\n <Select\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 py-0 text-sm\"\n data-testid=\"cell-editor-select\"\n >\n <option value=\"\">—</option>\n {field.options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {localizedLabel(opt.label, opt.value)}\n </option>\n ))}\n </Select>\n )\n default:\n return (\n <Input\n autoFocus\n value={typeof draft === 'string' ? draft : ''}\n onChange={(event) => onDraft(event.target.value)}\n className=\"h-7 text-sm\"\n data-testid=\"cell-editor-text\"\n />\n )\n }\n}\n\nfunction MultiSelectControl({\n field,\n draft,\n onDraft,\n}: {\n field: SelectFieldDef\n draft: CellDraft\n onDraft: (draft: CellDraft) => void\n}) {\n const selected = Array.isArray(draft) ? draft : []\n return (\n <Row gap=\"2\" wrap data-testid=\"cell-editor-multiselect\">\n {field.options.map((opt, index) => {\n const on = selected.includes(opt.value)\n return (\n <label key={opt.value} className=\"flex items-center gap-1.5 text-xs\">\n <input\n autoFocus={index === 0}\n type=\"checkbox\"\n checked={on}\n onChange={() =>\n onDraft(on ? selected.filter((v) => v !== opt.value) : [...selected, opt.value])\n }\n className=\"size-3.5 accent-primary\"\n />\n {localizedLabel(opt.label, opt.value)}\n </label>\n )\n })}\n </Row>\n )\n}\n\n/** Relation/asset cells reuse the existing lightweight pickers, opened from the cell. */\nfunction CellPicker({\n field,\n value,\n onCommit,\n onCancel,\n}: {\n field: FieldDef\n value: unknown\n onCommit: (value: unknown, advance: AdvanceDirection | null) => void\n onCancel: () => void\n}) {\n if (field.type === 'relation') {\n return (\n <RelationPicker\n targetCollection={field.target}\n multiple={field.multiple}\n value={toRefArray(value)}\n open\n onOpenChange={(open) => {\n if (!open) onCancel()\n }}\n onChange={(refs) => onCommit(field.multiple ? refs : refs[0], null)}\n />\n )\n }\n if (field.type === 'asset') {\n return (\n <AssetPicker\n open\n onOpenChange={(open) => {\n if (!open) onCancel()\n }}\n // EC-588: the SAME picker configuration the detail editor uses, derived from\n // the field — its `accept` (the grid offered PDFs to an image field before),\n // `multiple` with the batch commit (an N-file upload attached one, EC-570 #1),\n // and the EC-558 pick operation that keeps each placement's sub-fields.\n {...assetPickerBinding(field, value, (next) => onCommit(next, null))}\n />\n )\n }\n return null\n}\n\n// --- Draft <-> stored value mapping (mirrors `FieldControl`'s conventions) -----\n\nfunction initialDraft(field: FieldDef, value: unknown): CellDraft {\n switch (field.type) {\n case 'boolean':\n return value === true\n case 'number':\n return typeof value === 'number' ? String(value) : ''\n case 'date':\n return typeof value === 'string' ? value.slice(0, 10) : ''\n case 'select':\n if (field.multiple) {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []\n }\n return typeof value === 'string' ? value : ''\n default:\n return typeof value === 'string' ? value : ''\n }\n}\n\nfunction draftValue(field: FieldDef, draft: CellDraft): unknown {\n switch (field.type) {\n case 'boolean':\n return draft === true\n case 'number':\n return draft === '' ? undefined : Number(draft)\n case 'date':\n return draft === '' || typeof draft !== 'string' ? undefined : `${draft}T00:00:00.000Z`\n case 'select':\n if (field.multiple) return Array.isArray(draft) ? draft : []\n return draft === '' ? undefined : draft\n default:\n return draft === '' ? undefined : draft\n }\n}\n"]}
@@ -1,4 +1,7 @@
1
1
  import type { DocumentRef, DocumentStore, LocaleConfig, ValidateDocumentOptions } from '@elytracms/core/cms-core';
2
+ import type { AssetRecord } from '@elytracms/core/persistence';
3
+ /** The asset-library oracle the bag carries (EC-588): the record for an id, or `undefined`. */
4
+ export type FindAsset = NonNullable<ValidateDocumentOptions['findAsset']>;
2
5
  /**
3
6
  * THE option bag (EC-497, AD-497a).
4
7
  *
@@ -30,7 +33,29 @@ export interface GateValidationInput {
30
33
  localeConfig: LocaleConfig;
31
34
  /** Published-ness from the version POINTER (EC-224) — never a flat state flag. */
32
35
  isPublished: (ref: DocumentRef) => boolean;
33
- /** The real asset library oracle; absent where no library is mounted. */
34
- assetExists?: (id: string) => boolean;
36
+ /**
37
+ * The real asset library oracle (EC-588: returns the record, so the validator checks
38
+ * existence AND the field's `accept`); absent where no library is mounted or ready —
39
+ * see {@link assetLibraryOracle}.
40
+ */
41
+ findAsset?: FindAsset;
35
42
  }
36
43
  export declare function gateValidateOptions(input: GateValidationInput): ValidateDocumentOptions;
44
+ /** The slice of the assets store the oracle is derived from (`AssetsStore` satisfies it). */
45
+ export interface AssetLibraryState {
46
+ status: 'loading' | 'ready' | 'error';
47
+ assets: readonly AssetRecord[];
48
+ }
49
+ /**
50
+ * EC-588 A2 — THE asset-library oracle for the option bag, derived from the assets
51
+ * store: a record lookup over the loaded library.
52
+ *
53
+ * Absent — so the validator checks neither existence nor `accept` — when no library is
54
+ * mounted (a fixture-only session) AND while it is loading or errored. Until then there
55
+ * is no truthful answer to "which record is this?". This keeps today's outcome
56
+ * (the oracle was `() => true` there, so existence passed and `accept` was never
57
+ * checked) without inventing a fake record. Whether the publish gate should REFUSE
58
+ * while the library is unready is the "validation bound to the operation" question,
59
+ * left to EC-595.
60
+ */
61
+ export declare function assetLibraryOracle(library: AssetLibraryState | null): FindAsset | undefined;
@@ -13,7 +13,25 @@ export function gateValidateOptions(input) {
13
13
  validateComposition,
14
14
  validateRichTextEmbeds,
15
15
  isPublished: input.isPublished,
16
- ...(input.assetExists ? { assetExists: input.assetExists } : {}),
16
+ ...(input.findAsset ? { findAsset: input.findAsset } : {}),
17
17
  };
18
18
  }
19
+ /**
20
+ * EC-588 A2 — THE asset-library oracle for the option bag, derived from the assets
21
+ * store: a record lookup over the loaded library.
22
+ *
23
+ * Absent — so the validator checks neither existence nor `accept` — when no library is
24
+ * mounted (a fixture-only session) AND while it is loading or errored. Until then there
25
+ * is no truthful answer to "which record is this?". This keeps today's outcome
26
+ * (the oracle was `() => true` there, so existence passed and `accept` was never
27
+ * checked) without inventing a fake record. Whether the publish gate should REFUSE
28
+ * while the library is unready is the "validation bound to the operation" question,
29
+ * left to EC-595.
30
+ */
31
+ export function assetLibraryOracle(library) {
32
+ if (!library || library.status !== 'ready')
33
+ return undefined;
34
+ const byId = new Map(library.assets.map((asset) => [asset.id, asset]));
35
+ return (id) => byId.get(id);
36
+ }
19
37
  //# sourceMappingURL=gate-validation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate-validation.js","sourceRoot":"","sources":["../../../src/components/cms/gate-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AA2C1F,MAAM,UAAU,mBAAmB,CAAC,KAA0B;IAC5D,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,wEAAwE;QACxE,+EAA+E;QAC/E,4EAA4E;QAC5E,8EAA8E;QAC9E,6EAA6E;QAC7E,0EAA0E;QAC1E,0EAA0E;QAC1E,mBAAmB;QACnB,sBAAsB;QACtB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjE,CAAA;AACH,CAAC","sourcesContent":["import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators'\nimport type {\n DocumentRef,\n DocumentStore,\n LocaleConfig,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\n\n/**\n * THE option bag (EC-497, AD-497a).\n *\n * Before this module there were seven `validateDocument` call sites, each\n * hand-assembling its own subset of `ValidateDocumentOptions` — and the weakest of\n * the seven was the publish gate, the last thing between a draft and the live\n * site. One document therefore earned a different verdict depending on which\n * surface asked, which is a validation system that cannot be reasoned about.\n *\n * The contract, in one line: **surfaces differ in what they DO with the issues,\n * never in what they validated with.** Publish filters by severity, cell edit\n * filters by field + locale, the AI paths take `introducedErrors` (after − before)\n * — all three start from the identical list. There are deliberately NO named\n * \"weaker subsets\": a subset is how this defect happened, and the moment one\n * exists someone has to decide per call site which one applies.\n *\n * The one conditionality that remains is the engine's own and is documented on\n * `ValidateDocumentOptions`: `store`-dependent rules (relation/asset existence,\n * hierarchy cycles, `validation.unique`) fire exactly when a store is supplied.\n *\n * **Out of contract by declaration:** the assets and redirects rails. They\n * validate synthetic, non-`cmsDocuments` collections against their own registries\n * with no session store, and their writes go through `operations.assets.*` /\n * `operations.redirects.*` — a different entity, not a weaker check of this one.\n */\nexport interface GateValidationInput {\n /** The session document store, with the document under judgement swapped in. */\n store: DocumentStore\n localeConfig: LocaleConfig\n /** Published-ness from the version POINTER (EC-224) — never a flat state flag. */\n isPublished: (ref: DocumentRef) => boolean\n /** The real asset library oracle; absent where no library is mounted. */\n assetExists?: (id: string) => boolean\n}\n\nexport function gateValidateOptions(input: GateValidationInput): ValidateDocumentOptions {\n return {\n store: input.store,\n localeConfig: input.localeConfig,\n // EC-188/EC-189: the composition + rich-text-embed leashes, injected so\n // `cms-core` stays free of `project-graph` and `rich-text`. These are what the\n // operations layer could never supply on its own — it has no registry — and\n // the reason the canonical validator is injected downward rather than rebuilt\n // there. Since EC-547 both injected validators unwrap the converged v2 block\n // sequence (`{ version: 2, content }`) and validate its component nodes —\n // top-level and nested in typography — with the same field `allow` leash.\n validateComposition,\n validateRichTextEmbeds,\n isPublished: input.isPublished,\n ...(input.assetExists ? { assetExists: input.assetExists } : {}),\n }\n}\n"]}
1
+ {"version":3,"file":"gate-validation.js","sourceRoot":"","sources":["../../../src/components/cms/gate-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AAmD1F,MAAM,UAAU,mBAAmB,CAAC,KAA0B;IAC5D,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,wEAAwE;QACxE,+EAA+E;QAC/E,4EAA4E;QAC5E,8EAA8E;QAC9E,6EAA6E;QAC7E,0EAA0E;QAC1E,0EAA0E;QAC1E,mBAAmB;QACnB,sBAAsB;QACtB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3D,CAAA;AACH,CAAC;AAQD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAiC;IAClE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO;QAAE,OAAO,SAAS,CAAA;IAC5D,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IACtE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators'\nimport type {\n DocumentRef,\n DocumentStore,\n LocaleConfig,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\n\n/** The asset-library oracle the bag carries (EC-588): the record for an id, or `undefined`. */\nexport type FindAsset = NonNullable<ValidateDocumentOptions['findAsset']>\n\n/**\n * THE option bag (EC-497, AD-497a).\n *\n * Before this module there were seven `validateDocument` call sites, each\n * hand-assembling its own subset of `ValidateDocumentOptions` — and the weakest of\n * the seven was the publish gate, the last thing between a draft and the live\n * site. One document therefore earned a different verdict depending on which\n * surface asked, which is a validation system that cannot be reasoned about.\n *\n * The contract, in one line: **surfaces differ in what they DO with the issues,\n * never in what they validated with.** Publish filters by severity, cell edit\n * filters by field + locale, the AI paths take `introducedErrors` (after − before)\n * — all three start from the identical list. There are deliberately NO named\n * \"weaker subsets\": a subset is how this defect happened, and the moment one\n * exists someone has to decide per call site which one applies.\n *\n * The one conditionality that remains is the engine's own and is documented on\n * `ValidateDocumentOptions`: `store`-dependent rules (relation/asset existence,\n * hierarchy cycles, `validation.unique`) fire exactly when a store is supplied.\n *\n * **Out of contract by declaration:** the assets and redirects rails. They\n * validate synthetic, non-`cmsDocuments` collections against their own registries\n * with no session store, and their writes go through `operations.assets.*` /\n * `operations.redirects.*` — a different entity, not a weaker check of this one.\n */\nexport interface GateValidationInput {\n /** The session document store, with the document under judgement swapped in. */\n store: DocumentStore\n localeConfig: LocaleConfig\n /** Published-ness from the version POINTER (EC-224) — never a flat state flag. */\n isPublished: (ref: DocumentRef) => boolean\n /**\n * The real asset library oracle (EC-588: returns the record, so the validator checks\n * existence AND the field's `accept`); absent where no library is mounted or ready —\n * see {@link assetLibraryOracle}.\n */\n findAsset?: FindAsset\n}\n\nexport function gateValidateOptions(input: GateValidationInput): ValidateDocumentOptions {\n return {\n store: input.store,\n localeConfig: input.localeConfig,\n // EC-188/EC-189: the composition + rich-text-embed leashes, injected so\n // `cms-core` stays free of `project-graph` and `rich-text`. These are what the\n // operations layer could never supply on its own — it has no registry — and\n // the reason the canonical validator is injected downward rather than rebuilt\n // there. Since EC-547 both injected validators unwrap the converged v2 block\n // sequence (`{ version: 2, content }`) and validate its component nodes —\n // top-level and nested in typography — with the same field `allow` leash.\n validateComposition,\n validateRichTextEmbeds,\n isPublished: input.isPublished,\n ...(input.findAsset ? { findAsset: input.findAsset } : {}),\n }\n}\n\n/** The slice of the assets store the oracle is derived from (`AssetsStore` satisfies it). */\nexport interface AssetLibraryState {\n status: 'loading' | 'ready' | 'error'\n assets: readonly AssetRecord[]\n}\n\n/**\n * EC-588 A2 — THE asset-library oracle for the option bag, derived from the assets\n * store: a record lookup over the loaded library.\n *\n * Absent — so the validator checks neither existence nor `accept` — when no library is\n * mounted (a fixture-only session) AND while it is loading or errored. Until then there\n * is no truthful answer to \"which record is this?\". This keeps today's outcome\n * (the oracle was `() => true` there, so existence passed and `accept` was never\n * checked) without inventing a fake record. Whether the publish gate should REFUSE\n * while the library is unready is the \"validation bound to the operation\" question,\n * left to EC-595.\n */\nexport function assetLibraryOracle(library: AssetLibraryState | null): FindAsset | undefined {\n if (!library || library.status !== 'ready') return undefined\n const byId = new Map(library.assets.map((asset) => [asset.id, asset]))\n return (id) => byId.get(id)\n}\n"]}
@@ -10,7 +10,7 @@ import { useOptionalAssets } from '../../assets/assets-store.js';
10
10
  import { AssetPicker } from '../asset-picker.js';
11
11
  import { buildRichTextExtensions } from './extensions.js';
12
12
  import { blockSequenceValueFromDoc, isEffectivelyEmpty, isEquivalentToStored, normalizeEditorDoc, toEditorDoc, } from './value.js';
13
- import { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert.js';
13
+ import { INLINE_IMAGE_ACCEPT, insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert.js';
14
14
  import { EmbedPicker } from './embed-picker.js';
15
15
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
16
16
  import { RichTextToolbar } from './toolbar.js';
@@ -133,7 +133,7 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow, l
133
133
  // While expanded, the toolbar stays put above the scrolling prose column
134
134
  // (the scroll area is the EditorContent wrapper).
135
135
  const surface = (_jsxs(_Fragment, { children: [!readOnly ? (_jsx(RichTextToolbar, { editor: editor, targets: linkTargets, onOpenImage: openImage, onOpenEmbed: openEmbed, expanded: expanded, onToggleExpand: () => setExpanded((v) => !v) })) : null, _jsx(EditorContent, { editor: editor, className: cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS) })] }));
136
- return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandStage, { label: label, onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: "image/*", onChange: (assetId) => {
136
+ return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandStage, { label: label, onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: INLINE_IMAGE_ACCEPT, onChange: (assetId) => {
137
137
  setImageOpen(false);
138
138
  if (assetId)
139
139
  insertAssetImage(editor, assetId, assets);
@@ -1 +1 @@
1
- {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n blockSequenceValueFromDoc,\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored value (v2 `{ version: 2, content }`; staging: v1/legacy forms). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n /**\n * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: string\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : blockSequenceValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept=\"image/*\"\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n blockSequenceValueFromDoc,\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n toEditorDoc,\n} from './value'\nimport { INLINE_IMAGE_ACCEPT, insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored value (v2 `{ version: 2, content }`; staging: v1/legacy forms). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n /**\n * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: string\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : blockSequenceValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept={INLINE_IMAGE_ACCEPT}\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
@@ -7,6 +7,13 @@ import type { AssetsStore } from '../../assets/assets-store.js';
7
7
  * menu (EC-314). Centralised so both entry points produce byte-identical envelopes
8
8
  * (the shapes `reference-extraction` indexes).
9
9
  */
10
+ /**
11
+ * What the rich-text image insert accepts (EC-588): images, in the structured `accept`
12
+ * vocabulary every picker takes — stated once here rather than as a file-input literal
13
+ * at the call site. The picker derives its grid filter, its Upload dialog and its drop
14
+ * rule from it, like a field's own `accept`.
15
+ */
16
+ export declare const INLINE_IMAGE_ACCEPT: readonly string[];
10
17
  /** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */
11
18
  export declare function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void;
12
19
  /**
@@ -7,6 +7,13 @@ import { COMPONENT_EMBED_NODE_TYPE } from './component-embed.js';
7
7
  * menu (EC-314). Centralised so both entry points produce byte-identical envelopes
8
8
  * (the shapes `reference-extraction` indexes).
9
9
  */
10
+ /**
11
+ * What the rich-text image insert accepts (EC-588): images, in the structured `accept`
12
+ * vocabulary every picker takes — stated once here rather than as a file-input literal
13
+ * at the call site. The picker derives its grid filter, its Upload dialog and its drop
14
+ * rule from it, like a field's own `accept`.
15
+ */
16
+ export const INLINE_IMAGE_ACCEPT = ['image'];
10
17
  /** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */
11
18
  export function insertAssetImage(editor, assetId, assets) {
12
19
  const record = assets?.assets.find((a) => a.id === assetId);
@@ -1 +1 @@
1
- {"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { isCanvasEligible, isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n const record = assets?.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\n .run()\n}\n\n/**\n * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest\n * defaults. With `{ select: true }` the inserted node is then selected — so a host\n * embed inspector (0.0.41) opens straight onto its props for editing.\n */\nexport function insertComponentEmbed(\n editor: Editor,\n manifest: ComponentManifest,\n options?: { select?: boolean },\n): void {\n const embed = embedNodeFromManifest(manifest)\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })\n .run()\n if (options?.select) selectComponentEmbed(editor, embed.id)\n}\n\n/**\n * Select the just-inserted embed by its unique embedded-node id. Located by scan\n * (rather than fragile position arithmetic after `insertContent`, which may split a\n * paragraph or append a trailing block); the id is unique so the first match is it.\n */\nfunction selectComponentEmbed(editor: Editor, id: string): void {\n let pos = -1\n editor.state.doc.descendants((node, at) => {\n if (pos >= 0) return false\n if (node.type.name === COMPONENT_EMBED_NODE_TYPE) {\n const embedded = node.attrs.node as { id?: string } | null\n if (embedded?.id === id) {\n pos = at\n return false\n }\n }\n return true\n })\n if (pos >= 0) editor.commands.setNodeSelection(pos)\n}\n\n/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */\nexport function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return { id: newEmbedId(), componentId: manifest.id, props }\n}\n\n/**\n * The components embeddable in a rich-text field: the field's `allow` list (a\n * closed set — EC-544: an empty one offers nothing), or every non-primitive\n * component when absent — matching the `blocks` field and the\n * `validateRichTextEmbeds` leash. Eligibility is `isCanvasEligible`, the one\n * predicate the blocks panel and `elytra previews` read too (EC-514) — never a\n * local reading of the manifest flag.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const permitted = allow\n ? registered.filter((m) => allow.includes(m.id))\n : registered.filter((m) => !isPrimitive(m.id))\n return permitted.filter(isCanvasEligible)\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\n"]}
1
+ {"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { isCanvasEligible, isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/**\n * What the rich-text image insert accepts (EC-588): images, in the structured `accept`\n * vocabulary every picker takes — stated once here rather than as a file-input literal\n * at the call site. The picker derives its grid filter, its Upload dialog and its drop\n * rule from it, like a field's own `accept`.\n */\nexport const INLINE_IMAGE_ACCEPT: readonly string[] = ['image']\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n const record = assets?.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\n .run()\n}\n\n/**\n * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest\n * defaults. With `{ select: true }` the inserted node is then selected — so a host\n * embed inspector (0.0.41) opens straight onto its props for editing.\n */\nexport function insertComponentEmbed(\n editor: Editor,\n manifest: ComponentManifest,\n options?: { select?: boolean },\n): void {\n const embed = embedNodeFromManifest(manifest)\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })\n .run()\n if (options?.select) selectComponentEmbed(editor, embed.id)\n}\n\n/**\n * Select the just-inserted embed by its unique embedded-node id. Located by scan\n * (rather than fragile position arithmetic after `insertContent`, which may split a\n * paragraph or append a trailing block); the id is unique so the first match is it.\n */\nfunction selectComponentEmbed(editor: Editor, id: string): void {\n let pos = -1\n editor.state.doc.descendants((node, at) => {\n if (pos >= 0) return false\n if (node.type.name === COMPONENT_EMBED_NODE_TYPE) {\n const embedded = node.attrs.node as { id?: string } | null\n if (embedded?.id === id) {\n pos = at\n return false\n }\n }\n return true\n })\n if (pos >= 0) editor.commands.setNodeSelection(pos)\n}\n\n/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */\nexport function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return { id: newEmbedId(), componentId: manifest.id, props }\n}\n\n/**\n * The components embeddable in a rich-text field: the field's `allow` list (a\n * closed set — EC-544: an empty one offers nothing), or every non-primitive\n * component when absent — matching the `blocks` field and the\n * `validateRichTextEmbeds` leash. Eligibility is `isCanvasEligible`, the one\n * predicate the blocks panel and `elytra previews` read too (EC-514) — never a\n * local reading of the manifest flag.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const permitted = allow\n ? registered.filter((m) => allow.includes(m.id))\n : registered.filter((m) => !isPrimitive(m.id))\n return permitted.filter(isCanvasEligible)\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\n"]}
@@ -24,6 +24,7 @@ const VERSION_RELOAD_OPS = [
24
24
  'documents.pinVersion',
25
25
  'documents.restore',
26
26
  'documents.delete',
27
+ 'documents.clear',
27
28
  ];
28
29
  /** The one cache key for a document's version history — shared so the Versions
29
30
  * pane and the EC-298 compare view read through a single deduped query. */
@@ -1 +1 @@
1
- {"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,kBAAkB;CACV,CAAA;AAEV;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB,EAAE,OAA+B;IACxF,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;QAChC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Recorded-version freshness (EC-322): re-read on exactly the commands that\n * change a document's version history or its live pin — NOT the whole `documents`\n * domain. The draft the editor types (`documents.upsert`, fired per keystroke by\n * autosave) creates no version and moves no pin (versions are appended only on\n * publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so\n * listening on the whole domain refetched `listDocumentVersions` on every\n * keystroke while the Versions pane was open. The comparison the pane shows\n * (`liveMatchesDraft`) reads the live snapshot from THIS query and the draft from\n * local editor state, so a draft edit re-renders it without any refetch.\n *\n * Every command that DOES change the view is enumerated (a new version-affecting\n * op must be added here): `snapshot`/`restore` append a version; `publish` appends\n * one and moves the pin; `unpublish`/`pinVersion` move the pin; `delete` cascades\n * the history away.\n */\nconst VERSION_RELOAD_OPS = [\n 'documents.snapshot',\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.restore',\n 'documents.delete',\n] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n *\n * `enabled` (EC-316, default `true`) lets a caller that only needs this for a\n * gating check — e.g. the header's Discard-draft enablement, which isn't\n * otherwise on the version-browsing path — skip the fetch until it actually\n * applies (an unpublished/non-versioned document never needs its history).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef, options?: { enabled?: boolean }) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n ...(options?.enabled !== undefined ? { enabled: options.enabled } : {}),\n })\n return { versionsQuery, versionKey }\n}\n"]}
1
+ {"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB,EAAE,OAA+B;IACxF,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;QAChC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Recorded-version freshness (EC-322): re-read on exactly the commands that\n * change a document's version history or its live pin — NOT the whole `documents`\n * domain. The draft the editor types (`documents.upsert`, fired per keystroke by\n * autosave) creates no version and moves no pin (versions are appended only on\n * publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so\n * listening on the whole domain refetched `listDocumentVersions` on every\n * keystroke while the Versions pane was open. The comparison the pane shows\n * (`liveMatchesDraft`) reads the live snapshot from THIS query and the draft from\n * local editor state, so a draft edit re-renders it without any refetch.\n *\n * Every command that DOES change the view is enumerated (a new version-affecting\n * op must be added here): `snapshot`/`restore` append a version; `publish` appends\n * one and moves the pin; `unpublish`/`pinVersion` move the pin; `delete` cascades\n * the history away.\n */\nconst VERSION_RELOAD_OPS = [\n 'documents.snapshot',\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.restore',\n 'documents.delete',\n 'documents.clear',\n] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n *\n * `enabled` (EC-316, default `true`) lets a caller that only needs this for a\n * gating check — e.g. the header's Discard-draft enablement, which isn't\n * otherwise on the version-browsing path — skip the fetch until it actually\n * applies (an unpublished/non-versioned document never needs its history).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef, options?: { enabled?: boolean }) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n ...(options?.enabled !== undefined ? { enabled: options.enabled } : {}),\n })\n return { versionsQuery, versionKey }\n}\n"]}
@@ -1,17 +1,5 @@
1
1
  import type { CmsDocument } from '@elytracms/core/cms-core';
2
2
  import type { DocumentVersionsView } from './cms-store.js';
3
- /**
4
- * Does a recorded snapshot equal the working draft, by VALUE (EC-224)? Decides
5
- * whether the Versions pane shows "unpublished changes".
6
- *
7
- * The compare MUST be canonical (key-order-insensitive): a recorded snapshot is
8
- * round-tripped through `documentSchema` on save (which reorders object keys to
9
- * the schema's declaration order), while the live draft is assembled by the
10
- * editor's spread-mutations (keys in mutation order). A plain `JSON.stringify`
11
- * equality is order-sensitive, so it would report a just-published, byte-equal
12
- * document as still having changes — pinning the Publish affordance on forever.
13
- */
14
- export declare function snapshotMatchesDraft(snapshot: CmsDocument, draft: CmsDocument): boolean;
15
3
  /** Format a version's stored timestamp; fall back to the raw value if unparseable. */
16
4
  export declare function formatVersionTimestamp(value: string): string;
17
5
  /**
@@ -1,18 +1,6 @@
1
- import { stableStringify } from '@elytracms/core/content';
2
- /**
3
- * Does a recorded snapshot equal the working draft, by VALUE (EC-224)? Decides
4
- * whether the Versions pane shows "unpublished changes".
5
- *
6
- * The compare MUST be canonical (key-order-insensitive): a recorded snapshot is
7
- * round-tripped through `documentSchema` on save (which reorders object keys to
8
- * the schema's declaration order), while the live draft is assembled by the
9
- * editor's spread-mutations (keys in mutation order). A plain `JSON.stringify`
10
- * equality is order-sensitive, so it would report a just-published, byte-equal
11
- * document as still having changes — pinning the Publish affordance on forever.
12
- */
13
- export function snapshotMatchesDraft(snapshot, draft) {
14
- return stableStringify(snapshot) === stableStringify(draft);
15
- }
1
+ // EC-574: `snapshotMatchesDraft` (a raw-document `stableStringify` compare) left
2
+ // here; the unpublished-changes signal reads the semantic equality in
3
+ // `@elytracms/core/cms-core` (`documentsSemanticEqual`), one declaration.
16
4
  /** Format a version's stored timestamp; fall back to the raw value if unparseable. */
17
5
  export function formatVersionTimestamp(value) {
18
6
  const ms = Date.parse(value);
@@ -1 +1 @@
1
- {"version":3,"file":"versions-model.js","sourceRoot":"","sources":["../../../src/components/cms/versions-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { stableStringify } from '@elytracms/core/content'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Does a recorded snapshot equal the working draft, by VALUE (EC-224)? Decides\n * whether the Versions pane shows \"unpublished changes\".\n *\n * The compare MUST be canonical (key-order-insensitive): a recorded snapshot is\n * round-tripped through `documentSchema` on save (which reorders object keys to\n * the schema's declaration order), while the live draft is assembled by the\n * editor's spread-mutations (keys in mutation order). A plain `JSON.stringify`\n * equality is order-sensitive, so it would report a just-published, byte-equal\n * document as still having changes — pinning the Publish affordance on forever.\n */\nexport function snapshotMatchesDraft(snapshot: CmsDocument, draft: CmsDocument): boolean {\n return stableStringify(snapshot) === stableStringify(draft)\n}\n\n/** Format a version's stored timestamp; fall back to the raw value if unparseable. */\nexport function formatVersionTimestamp(value: string): string {\n const ms = Date.parse(value)\n if (Number.isNaN(ms)) return value\n return new Date(ms).toLocaleString()\n}\n\n/**\n * The auto-division ladder for {@link formatRelativeVersionTimestamp}: each step's\n * `amount` is how many of the CURRENT unit make up the NEXT one (60 seconds → a\n * minute, 60 minutes → an hour, …). The standard `Intl.RelativeTimeFormat`\n * unit-selection algorithm (MDN) walks it, dividing down until the magnitude fits\n * a unit — so \"just now\", \"5 minutes ago\", \"3 days ago\", \"2 weeks ago\" all pick\n * the largest sensible unit.\n */\nconst RELATIVE_DIVISIONS: readonly { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [\n { amount: 60, unit: 'second' },\n { amount: 60, unit: 'minute' },\n { amount: 24, unit: 'hour' },\n { amount: 7, unit: 'day' },\n { amount: 4.34524, unit: 'week' },\n { amount: 12, unit: 'month' },\n { amount: Number.POSITIVE_INFINITY, unit: 'year' },\n]\n\n/**\n * Format a version's stored timestamp RELATIVE to `nowMs`, localized to `locale`\n * (\"vor 3 Tagen\" / \"3 days ago\"), via `Intl.RelativeTimeFormat` (EC-446 — the\n * absolute value truncated to an unreadable stub in the redesigned pane; it now\n * rides the row's `title` while this readable relative label is the visible text).\n *\n * Pure: `(value, nowMs, locale) → string`, so it unit-tests without a clock or a\n * DOM. `numeric: 'auto'` yields the idiomatic \"now\"/\"jetzt\" for the sub-minute\n * case and \"yesterday\"/\"gestern\" where the locale has such words. Falls back to\n * the raw value when unparseable — mirroring {@link formatVersionTimestamp} for\n * the in-memory sequential clock, which is not a date.\n */\nexport function formatRelativeVersionTimestamp(value: string, nowMs: number, locale: string): string {\n const ms = Date.parse(value)\n if (Number.isNaN(ms)) return value\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n // Seconds from now; negative = past (the usual case for a recorded version).\n let duration = (ms - nowMs) / 1000\n for (const division of RELATIVE_DIVISIONS) {\n if (Math.abs(duration) < division.amount) {\n return rtf.format(Math.round(duration), division.unit)\n }\n duration /= division.amount\n }\n return rtf.format(Math.round(duration), 'year')\n}\n\n/**\n * The pinned PUBLISHED version's recorded snapshot (EC-224), if the view has\n * resolved and a version is currently pinned — `undefined` while loading (the\n * view itself is absent) or once resolved with nothing pinned. Shared by the\n * Discard-draft gating (`discard-draft.ts`'s `discardDraftStatus`) and\n * {@link derivePublishedSnapshot} below, so the two can never read a\n * different \"what's currently published\" answer off the same query.\n */\nexport function publishedVersionSnapshot(\n view: DocumentVersionsView | null | undefined,\n): CmsDocument | undefined {\n if (!view || view.publishedVersion === undefined) return undefined\n return view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n}\n\n/** The loaded state of a document's pinned published snapshot (EC-226). */\nexport type PublishedSnapshotState =\n | { status: 'loading' }\n | { status: 'ready'; doc: CmsDocument }\n | { status: 'missing' }\n\n/**\n * EC-325 — the Published-perspective snapshot, derived from the SAME\n * `useDocumentVersionsQuery` result the Discard-draft gating already reads,\n * replacing the bespoke raw-load effect the detail editor used to run on\n * every perspective switch — the only direct `versioning.load` call outside\n * the sanctioned cache hook (AD-17: one operations-backed cache).\n * `view === null | undefined` reads as `'loading'` (covers both \"the query\n * hasn't resolved yet\" and \"the query is disabled/absent\" — e.g. a source\n * whose `versions` capability is off); once resolved, a pinned version's\n * snapshot is `'ready'`, else `'missing'`.\n */\nexport function derivePublishedSnapshot(\n view: DocumentVersionsView | null | undefined,\n): PublishedSnapshotState {\n if (!view) return { status: 'loading' }\n const doc = publishedVersionSnapshot(view)\n return doc ? { status: 'ready', doc } : { status: 'missing' }\n}\n"]}
1
+ {"version":3,"file":"versions-model.js","sourceRoot":"","sources":["../../../src/components/cms/versions-model.ts"],"names":[],"mappings":"AAGA,iFAAiF;AACjF,sEAAsE;AACtE,0EAA0E;AAE1E,sFAAsF;AACtF,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IAClC,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,cAAc,EAAE,CAAA;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAqE;IAC3F,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;IAC5B,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE;IAC1B,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;IACjC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7B,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,EAAE,MAAM,EAAE;CACnD,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,8BAA8B,CAAC,KAAa,EAAE,KAAa,EAAE,MAAc;IACzF,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IAClC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;IACpE,6EAA6E;IAC7E,IAAI,QAAQ,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAAA;IAClC,KAAK,MAAM,QAAQ,IAAI,kBAAkB,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzC,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACxD,CAAC;QACD,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAA;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAA6C;IAE7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAClE,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAA;AACjF,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAA6C;IAE7C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IACvC,MAAM,GAAG,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAA;IAC1C,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;AAC/D,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { DocumentVersionsView } from './cms-store'\n\n// EC-574: `snapshotMatchesDraft` (a raw-document `stableStringify` compare) left\n// here; the unpublished-changes signal reads the semantic equality in\n// `@elytracms/core/cms-core` (`documentsSemanticEqual`), one declaration.\n\n/** Format a version's stored timestamp; fall back to the raw value if unparseable. */\nexport function formatVersionTimestamp(value: string): string {\n const ms = Date.parse(value)\n if (Number.isNaN(ms)) return value\n return new Date(ms).toLocaleString()\n}\n\n/**\n * The auto-division ladder for {@link formatRelativeVersionTimestamp}: each step's\n * `amount` is how many of the CURRENT unit make up the NEXT one (60 seconds → a\n * minute, 60 minutes → an hour, …). The standard `Intl.RelativeTimeFormat`\n * unit-selection algorithm (MDN) walks it, dividing down until the magnitude fits\n * a unit — so \"just now\", \"5 minutes ago\", \"3 days ago\", \"2 weeks ago\" all pick\n * the largest sensible unit.\n */\nconst RELATIVE_DIVISIONS: readonly { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [\n { amount: 60, unit: 'second' },\n { amount: 60, unit: 'minute' },\n { amount: 24, unit: 'hour' },\n { amount: 7, unit: 'day' },\n { amount: 4.34524, unit: 'week' },\n { amount: 12, unit: 'month' },\n { amount: Number.POSITIVE_INFINITY, unit: 'year' },\n]\n\n/**\n * Format a version's stored timestamp RELATIVE to `nowMs`, localized to `locale`\n * (\"vor 3 Tagen\" / \"3 days ago\"), via `Intl.RelativeTimeFormat` (EC-446 — the\n * absolute value truncated to an unreadable stub in the redesigned pane; it now\n * rides the row's `title` while this readable relative label is the visible text).\n *\n * Pure: `(value, nowMs, locale) → string`, so it unit-tests without a clock or a\n * DOM. `numeric: 'auto'` yields the idiomatic \"now\"/\"jetzt\" for the sub-minute\n * case and \"yesterday\"/\"gestern\" where the locale has such words. Falls back to\n * the raw value when unparseable — mirroring {@link formatVersionTimestamp} for\n * the in-memory sequential clock, which is not a date.\n */\nexport function formatRelativeVersionTimestamp(value: string, nowMs: number, locale: string): string {\n const ms = Date.parse(value)\n if (Number.isNaN(ms)) return value\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n // Seconds from now; negative = past (the usual case for a recorded version).\n let duration = (ms - nowMs) / 1000\n for (const division of RELATIVE_DIVISIONS) {\n if (Math.abs(duration) < division.amount) {\n return rtf.format(Math.round(duration), division.unit)\n }\n duration /= division.amount\n }\n return rtf.format(Math.round(duration), 'year')\n}\n\n/**\n * The pinned PUBLISHED version's recorded snapshot (EC-224), if the view has\n * resolved and a version is currently pinned — `undefined` while loading (the\n * view itself is absent) or once resolved with nothing pinned. Shared by the\n * Discard-draft gating (`discard-draft.ts`'s `discardDraftStatus`) and\n * {@link derivePublishedSnapshot} below, so the two can never read a\n * different \"what's currently published\" answer off the same query.\n */\nexport function publishedVersionSnapshot(\n view: DocumentVersionsView | null | undefined,\n): CmsDocument | undefined {\n if (!view || view.publishedVersion === undefined) return undefined\n return view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n}\n\n/** The loaded state of a document's pinned published snapshot (EC-226). */\nexport type PublishedSnapshotState =\n | { status: 'loading' }\n | { status: 'ready'; doc: CmsDocument }\n | { status: 'missing' }\n\n/**\n * EC-325 — the Published-perspective snapshot, derived from the SAME\n * `useDocumentVersionsQuery` result the Discard-draft gating already reads,\n * replacing the bespoke raw-load effect the detail editor used to run on\n * every perspective switch — the only direct `versioning.load` call outside\n * the sanctioned cache hook (AD-17: one operations-backed cache).\n * `view === null | undefined` reads as `'loading'` (covers both \"the query\n * hasn't resolved yet\" and \"the query is disabled/absent\" — e.g. a source\n * whose `versions` capability is off); once resolved, a pinned version's\n * snapshot is `'ready'`, else `'missing'`.\n */\nexport function derivePublishedSnapshot(\n view: DocumentVersionsView | null | undefined,\n): PublishedSnapshotState {\n if (!view) return { status: 'loading' }\n const doc = publishedVersionSnapshot(view)\n return doc ? { status: 'ready', doc } : { status: 'missing' }\n}\n"]}
@@ -56,6 +56,6 @@ function ComponentDetailBody({ projectId, studio, manifest, }) {
56
56
  render: () => _jsx(SlotsInfoPane, { manifest: manifest }),
57
57
  },
58
58
  ];
59
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.components() }, className: "inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden", children: [_jsx(ArrowLeft, { className: "size-3.5" }), " ", translate('components.detail.allComponents')] }), _jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0 flex-wrap", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: title }), _jsx(Badge, { variant: namespace === 'project' ? 'primary' : 'outline', children: namespace }), manifest.category ? _jsx(Badge, { variant: "outline", children: manifest.category }) : null] }), _jsx(Spacer, {}), _jsx(ComponentStatusBadge, { status: status }), _jsxs(Button, { size: "sm", onClick: () => setTransferOpen(true), "data-testid": "use-in-document", children: [_jsx(FilePlus2, {}), " ", translate('components.transfer.action')] })] }), _jsx(Row, { gap: "2", align: "center", className: "mt-1", children: _jsx(Code, { tone: "muted", children: manifest.id }) }), manifest.description ? (_jsx(Text, { tone: "muted", size: "sm", className: "mt-1 max-w-prose", children: manifest.description })) : null, status !== 'valid' ? (_jsx(Text, { size: "sm", className: cn('mt-1', status === 'invalid' ? 'text-destructive' : 'text-warning'), children: statusDescription(status) })) : null, _jsxs(Row, { gap: "1", align: "center", className: "mt-3", children: [_jsxs(TabButton, { active: tab === 'preview', onClick: () => setTab('preview'), children: [_jsx(Play, { className: "size-3" }), translate('components.detail.tab.preview')] }), _jsxs(TabButton, { active: tab === 'manifest', onClick: () => setTab('manifest'), children: [_jsx(ListTree, { className: "size-3" }), translate('components.detail.tab.manifest')] }), _jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), _jsxs(TabButton, { active: tab === 'references', onClick: () => setTab('references'), children: [_jsx(Link2, { className: "size-3" }), translate('references.title')] })] })] }), tab === 'preview' ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(ComponentPreview, { manifest: manifest, studio: studio, state: preview }) }), _jsx(InspectorSidebar, { panes: panes, className: "w-[320px] shrink-0 border-l border-border" })] })) : tab === 'manifest' ? (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-4xl", children: _jsxs(Stack, { gap: "6", children: [_jsx(FieldInspector, { manifest: manifest }), manifest.designTokens && manifest.designTokens.length > 0 ? (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, children: translate('components.detail.designTokens') }), _jsx(Row, { gap: "1", wrap: true, children: manifest.designTokens.map((token) => (_jsx(Code, { tone: "muted", children: token }, token))) })] })) : null] }) }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, target: { type: 'component', id: manifest.id } }) }) })), _jsx(TransferToDocumentDialog, { projectId: projectId, manifest: manifest, node: preview.node, open: transferOpen, onOpenChange: setTransferOpen })] }));
59
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.components() }, className: "inline-flex items-center gap-1 px-4 pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground md:hidden", children: [_jsx(ArrowLeft, { className: "size-3.5" }), " ", translate('components.detail.allComponents')] }), _jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0 flex-wrap", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: title }), _jsx(Badge, { variant: namespace === 'project' ? 'primary' : 'outline', children: namespace }), manifest.category ? _jsx(Badge, { variant: "outline", children: manifest.category }) : null] }), _jsx(Spacer, {}), _jsx(ComponentStatusBadge, { status: status }), _jsxs(Button, { size: "sm", onClick: () => setTransferOpen(true), "data-testid": "use-in-document", children: [_jsx(FilePlus2, {}), " ", translate('components.transfer.action')] })] }), _jsx(Row, { gap: "2", align: "center", className: "mt-1", children: _jsx(Code, { tone: "muted", children: manifest.id }) }), manifest.description ? (_jsx(Text, { tone: "muted", size: "sm", className: "mt-1 max-w-prose", children: manifest.description })) : null, status !== 'valid' ? (_jsx(Text, { size: "sm", className: cn('mt-1', status === 'invalid' ? 'text-destructive' : 'text-warning'), children: statusDescription(status) })) : null, _jsxs(Row, { gap: "1", align: "center", className: "mt-3", children: [_jsxs(TabButton, { active: tab === 'preview', onClick: () => setTab('preview'), children: [_jsx(Play, { className: "size-3" }), translate('components.detail.tab.preview')] }), _jsxs(TabButton, { active: tab === 'manifest', onClick: () => setTab('manifest'), children: [_jsx(ListTree, { className: "size-3" }), translate('components.detail.tab.manifest')] }), _jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), _jsxs(TabButton, { active: tab === 'references', onClick: () => setTab('references'), children: [_jsx(Link2, { className: "size-3" }), translate('references.title')] })] })] }), tab === 'preview' ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(ComponentPreview, { manifest: manifest, studio: studio, state: preview }) }), _jsx(InspectorSidebar, { panes: panes, className: "w-[320px] shrink-0 border-l border-border" })] })) : tab === 'manifest' ? (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-4xl", children: _jsx(FieldInspector, { manifest: manifest }) }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, target: { type: 'component', id: manifest.id } }) }) })), _jsx(TransferToDocumentDialog, { projectId: projectId, manifest: manifest, node: preview.node, open: transferOpen, onOpenChange: setTransferOpen })] }));
60
60
  }
61
61
  //# sourceMappingURL=component-detail.js.map