@elytracms/studio 0.0.95 → 0.0.97

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 (104) hide show
  1. package/convex/cms.ts +272 -16
  2. package/convex/delivery-folded-drift.test.ts +104 -0
  3. package/convex/delivery.ts +24 -8
  4. package/convex/depth-codec-lookalike.test.ts +192 -0
  5. package/convex/fixture-accounts.test.ts +305 -0
  6. package/convex/fixtureAccounts.ts +298 -0
  7. package/convex/incarnation.test.ts +149 -0
  8. package/convex/ingest.test.ts +145 -3
  9. package/convex/ingest.ts +123 -17
  10. package/convex/receipt-races.test.ts +184 -0
  11. package/convex/records-zod-copy.test.ts +76 -0
  12. package/convex/schema.ts +20 -1
  13. package/convex/strict-delete.test.ts +129 -0
  14. package/convex/strict-reference-race.test.ts +139 -0
  15. package/dist/components/ai/ai-chat-panel.js +15 -2
  16. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  17. package/dist/components/ai/translate-document-action.d.ts +6 -0
  18. package/dist/components/ai/translate-document-action.js.map +1 -1
  19. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  20. package/dist/components/ai/translate-document-dialog.js +5 -5
  21. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  22. package/dist/components/assets/asset-detail.js +5 -2
  23. package/dist/components/assets/asset-detail.js.map +1 -1
  24. package/dist/components/assets/asset-locale.d.ts +7 -0
  25. package/dist/components/assets/asset-locale.js +16 -2
  26. package/dist/components/assets/asset-locale.js.map +1 -1
  27. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  28. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  29. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  30. package/dist/components/assets/asset-meta-label.d.ts +2 -1
  31. package/dist/components/assets/asset-meta-label.js +4 -2
  32. package/dist/components/assets/asset-meta-label.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +33 -13
  34. package/dist/components/assets/asset-metadata.js +46 -22
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.d.ts +6 -2
  37. package/dist/components/assets/asset-search.js +15 -5
  38. package/dist/components/assets/asset-search.js.map +1 -1
  39. package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
  40. package/dist/components/assets/assets-collection-view-source.js +7 -5
  41. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  42. package/dist/components/builder/canvas-rebase.d.ts +36 -12
  43. package/dist/components/builder/canvas-rebase.js +13 -3
  44. package/dist/components/builder/canvas-rebase.js.map +1 -1
  45. package/dist/components/builder/canvas-render-context.js +5 -4
  46. package/dist/components/builder/canvas-render-context.js.map +1 -1
  47. package/dist/components/builder/composition-field-editor.d.ts +12 -6
  48. package/dist/components/builder/composition-field-editor.js +6 -5
  49. package/dist/components/builder/composition-field-editor.js.map +1 -1
  50. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  51. package/dist/components/cms/asset-picker-core.js +2 -2
  52. package/dist/components/cms/asset-picker-core.js.map +1 -1
  53. package/dist/components/cms/asset-picker.js +3 -1
  54. package/dist/components/cms/asset-picker.js.map +1 -1
  55. package/dist/components/cms/canvas-commit.d.ts +6 -0
  56. package/dist/components/cms/canvas-commit.js +5 -1
  57. package/dist/components/cms/canvas-commit.js.map +1 -1
  58. package/dist/components/cms/cms-store.d.ts +24 -8
  59. package/dist/components/cms/cms-store.js +21 -14
  60. package/dist/components/cms/cms-store.js.map +1 -1
  61. package/dist/components/cms/collection-table.js +4 -11
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/detail-editor.js +2 -2
  64. package/dist/components/cms/detail-editor.js.map +1 -1
  65. package/dist/components/cms/draft-commit.d.ts +16 -3
  66. package/dist/components/cms/draft-commit.js +3 -2
  67. package/dist/components/cms/draft-commit.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +107 -10
  69. package/dist/components/cms/use-document-draft.js +157 -42
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/collection-view/cms-collection-view-source.js +6 -4
  72. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  73. package/dist/components/components/transfer-dialog.js +6 -4
  74. package/dist/components/components/transfer-dialog.js.map +1 -1
  75. package/dist/lib/ai/chat-state.d.ts +7 -0
  76. package/dist/lib/ai/chat-state.js.map +1 -1
  77. package/dist/lib/ai/tools.d.ts +8 -0
  78. package/dist/lib/ai/tools.js +4 -0
  79. package/dist/lib/ai/tools.js.map +1 -1
  80. package/dist/lib/cms-views/view-model.d.ts +15 -8
  81. package/dist/lib/cms-views/view-model.js +19 -5
  82. package/dist/lib/cms-views/view-model.js.map +1 -1
  83. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  84. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  85. package/dist/lib/collection-view/collection-view-source.d.ts +18 -7
  86. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  87. package/dist/lib/data/collection-content.js +5 -0
  88. package/dist/lib/data/collection-content.js.map +1 -1
  89. package/dist/lib/engine-version.d.ts +1 -1
  90. package/dist/lib/engine-version.js +1 -1
  91. package/dist/lib/engine-version.js.map +1 -1
  92. package/dist/lib/i18n/messages.de.js +15 -1
  93. package/dist/lib/i18n/messages.de.js.map +1 -1
  94. package/dist/lib/i18n/messages.en.d.ts +13 -0
  95. package/dist/lib/i18n/messages.en.js +16 -1
  96. package/dist/lib/i18n/messages.en.js.map +1 -1
  97. package/dist/lib/operations/cms-persistence.d.ts +119 -35
  98. package/dist/lib/operations/cms-persistence.js +441 -91
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +80 -8
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +2 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/package.json +4 -3
@@ -1 +1 @@
1
- {"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport { assetAcceptAttr, describeAccept } from '@elytracms/core/cms-core'\nimport { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n ExpandableSurface,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '../../lib/utils'\nimport { translate } from '../../lib/i18n'\nimport { AssetThumb } from '../assets/asset-thumb'\nimport { useAssetReadLocale } from '../assets/asset-locale'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport type { AssetsStore } from '../assets/assets-store'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport {\n ASSET_PICKER_STAGE_PRESET,\n commitPickerSelection,\n batchAttachment,\n commitPickerSelectionMany,\n splitByAccept,\n uploadBatchProgress,\n visiblePickerAssets,\n} from './asset-picker-core'\nimport type { UploadBatch } from './asset-picker-core'\n\n/** The picker's props — see {@link AssetPicker}. */\nexport interface AssetPickerProps {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** EC-460: commit a whole settled upload batch (a `many` field passes this). */\n onChangeMany?: (assetIds: string[]) => void\n /** Whether the consuming field holds many assets (enables batch commit). */\n multiple?: boolean\n /** The field's `accept` list (EC-588) — absent means any file. */\n accept?: readonly string[]\n}\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field.\n *\n * EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}\n * `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the\n * old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the\n * assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and\n * the upload-then-select path. It is a SELECTION browser, not a form — click = pick\n * (fires `onChange`, closes) plus the shipped clear/cancel affordances; the\n * forms-never-modal rule governs document forms and is untouched. Opened from the\n * expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer\n * stack — Escape closes the browser first, the rich-text stage stays.\n *\n * The component CONTRACT (`{ open, onOpenChange, value, accept, onChange }`) serves\n * every call site — asset fields, rich-text image insertion, editable cells. EC-460\n * adds an OPTIONAL multi-commit pair (`multiple` + `onChangeMany`): a `many` asset\n * field passes both so uploading N files in ONE picker interaction attaches ALL N\n * (the old flow committed the first upload to land and closed — the rest reached the\n * library but never the field, silent partial data loss). Grid clicks stay\n * single-pick-and-close in every mode.\n *\n * EC-588: `accept` is the field's STRUCTURED list (it was a file-input string that the\n * core re-split — two shapes for one fact). The grid filter, the file-input attribute\n * and the drop/upload split all derive from it, and an absent `accept` is unrestricted\n * everywhere (the Upload dialog used to fall back to `image/*`). A field-backed caller\n * passes `assetPickerBinding`'s output (`asset-picker-core`) rather than wiring props\n * by hand.\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: AssetPickerProps) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n </AssetsProvider>\n )\n}\n\n/** The open media-browser stage over a mounted assets store. */\nfunction AssetPickerStage(props: Omit<AssetPickerProps, 'open'>) {\n const store = useAssets()\n return (\n <ExpandableSurface\n title={translate('cms.asset.pickTitle')}\n closeLabel={translate('common.close')}\n variant=\"stage\"\n preset={ASSET_PICKER_STAGE_PRESET}\n onDismiss={() => props.onOpenChange(false)}\n headerLeading={<ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden />}\n >\n <AssetPickerBrowser store={store} {...props} />\n </ExpandableSurface>\n )\n}\n\n/** The slice of the assets store the browser reads and drives. */\nexport type AssetPickerStore = Pick<\n AssetsStore,\n 'status' | 'errorMessage' | 'assets' | 'uploads' | 'upload' | 'landedAssetId'\n>\n\n/**\n * The media browser itself — search, Upload, the grid, the drop zone — rendered inside\n * the stage. A named component of its own (EC-588) so its markup is reachable by a\n * node-env smoke render (the stage portal is `null` there): the file input's `accept`\n * attribute and the grid's filter are pinned against the field's `accept`.\n */\nexport function AssetPickerBrowser({\n store,\n value,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: Omit<AssetPickerProps, 'open'> & { store: AssetPickerStore }) {\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [dragging, setDragging] = useState(false)\n // Names of files a drop/upload refused against the field's `accept` (EC-588) — shown\n // as a visible notice and never uploaded. Cleared by the next upload attempt.\n const [rejected, setRejected] = useState<string[]>([])\n const fileInputRef = useRef<HTMLInputElement>(null)\n // The active upload BATCH (EC-460): exactly the upload ids this interaction started\n // (EC-588 A7), so the effect below attaches this batch's uploads and nothing else.\n const pendingRef = useRef<UploadBatch | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = visiblePickerAssets(store.assets, query, accept)\n\n // Auto-select what an in-picker upload added, then close. A `many` consumer\n // (multiple + onChangeMany) waits for the batch to SETTLE and commits every\n // added id — uploading 2 files attaches 2 (EC-460); a single consumer keeps\n // the first-to-land commit. Only uploads whose record satisfies `accept` commit.\n useEffect(() => {\n const batch = pendingRef.current\n if (!batch) return\n // EC-588 2i: the one batch-attach rule the field's own drop zone runs too.\n const many = multiple === true && onChangeMany !== undefined\n const { attach, disarm } = batchAttachment(uploadBatchProgress(batch, store, accept), many)\n if (disarm) pendingRef.current = null\n if (attach.length === 0) return\n if (many) commitPickerSelectionMany(attach, { onChangeMany, onOpenChange })\n else commitPickerSelection(attach[0]!, { onChange, onOpenChange })\n }, [store, accept, multiple, onChange, onChangeMany, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n // EC-588: the drop rule — the same split the field's drop zone uses. A drop\n // bypasses the dialog's soft filter; refused files are named, never uploaded.\n const { accepted, rejected: refused } = splitByAccept(files, accept)\n setRejected(refused)\n if (accepted.length === 0) return\n pendingRef.current = { uploadIds: store.upload(accepted) }\n }\n // The stage portals to document.body, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the stage would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <div\n className=\"relative flex min-h-0 flex-1 flex-col gap-3 p-4\"\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={assetAcceptAttr(accept)}\n className=\"hidden\"\n data-testid=\"asset-picker-file-input\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <AssetRejectedNotice rejected={rejected} accept={accept} />\n\n <div\n className=\"grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\"\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => commitPickerSelection(asset.id, { onChange, onOpenChange })}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => commitPickerSelection(null, { onChange, onOpenChange })}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </div>\n )\n}\n\n/**\n * The visible notice for files an upload path refused against the field's `accept`\n * (EC-301 → EC-588): named, never silently dropped. Shared by the field's drop zone and\n * the picker, which apply the same rule. Renders nothing when nothing was refused.\n */\nexport function AssetRejectedNotice({\n rejected,\n accept,\n}: {\n rejected: readonly string[]\n accept: readonly string[] | undefined\n}) {\n if (rejected.length === 0) return null\n return (\n <Row gap=\"2\" align=\"center\" data-testid=\"asset-rejected-notice\">\n <AlertTriangle className=\"size-4 shrink-0 text-warning\" />\n <Text size=\"xs\" tone=\"muted\">\n {rejected.length === 1\n ? translate('cms.asset.rejected.one', { name: rejected[0]!, types: describeAccept(accept) })\n : translate('cms.asset.rejected.other', {\n count: rejected.length,\n types: describeAccept(accept),\n })}\n </Text>\n </Row>\n )\n}\n\n/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The\n * internal asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n const alt = assetAlt(asset, useAssetReadLocale())\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n aria-pressed={selected}\n >\n <span className=\"relative block\">\n <AssetThumb record={asset} className=\"aspect-video w-full\" />\n {selected ? (\n <span className=\"absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow\">\n <Check className=\"size-3\" />\n </span>\n ) : null}\n </span>\n <Text size=\"xs\" className=\"min-w-0 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {/* EC-587 (ASSETS D20): the asset's alt through THE reader, as delivery serves it —\n EC-632: in the studio's locale. */}\n <Text size=\"xs\" tone={alt ? 'muted' : 'subtle'} className=\"truncate\">\n {alt || translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n"]}
1
+ {"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport { assetAcceptAttr, describeAccept } from '@elytracms/core/cms-core'\nimport { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n ExpandableSurface,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '../../lib/utils'\nimport { translate } from '../../lib/i18n'\nimport { AssetThumb } from '../assets/asset-thumb'\nimport { useAssetReadLocale } from '../assets/asset-locale'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport type { AssetsStore } from '../assets/assets-store'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport {\n ASSET_PICKER_STAGE_PRESET,\n commitPickerSelection,\n batchAttachment,\n commitPickerSelectionMany,\n splitByAccept,\n uploadBatchProgress,\n visiblePickerAssets,\n} from './asset-picker-core'\nimport type { UploadBatch } from './asset-picker-core'\n\n/** The picker's props — see {@link AssetPicker}. */\nexport interface AssetPickerProps {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** EC-460: commit a whole settled upload batch (a `many` field passes this). */\n onChangeMany?: (assetIds: string[]) => void\n /** Whether the consuming field holds many assets (enables batch commit). */\n multiple?: boolean\n /** The field's `accept` list (EC-588) — absent means any file. */\n accept?: readonly string[]\n}\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field.\n *\n * EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}\n * `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the\n * old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the\n * assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and\n * the upload-then-select path. It is a SELECTION browser, not a form — click = pick\n * (fires `onChange`, closes) plus the shipped clear/cancel affordances; the\n * forms-never-modal rule governs document forms and is untouched. Opened from the\n * expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer\n * stack — Escape closes the browser first, the rich-text stage stays.\n *\n * The component CONTRACT (`{ open, onOpenChange, value, accept, onChange }`) serves\n * every call site — asset fields, rich-text image insertion, editable cells. EC-460\n * adds an OPTIONAL multi-commit pair (`multiple` + `onChangeMany`): a `many` asset\n * field passes both so uploading N files in ONE picker interaction attaches ALL N\n * (the old flow committed the first upload to land and closed — the rest reached the\n * library but never the field, silent partial data loss). Grid clicks stay\n * single-pick-and-close in every mode.\n *\n * EC-588: `accept` is the field's STRUCTURED list (it was a file-input string that the\n * core re-split — two shapes for one fact). The grid filter, the file-input attribute\n * and the drop/upload split all derive from it, and an absent `accept` is unrestricted\n * everywhere (the Upload dialog used to fall back to `image/*`). A field-backed caller\n * passes `assetPickerBinding`'s output (`asset-picker-core`) rather than wiring props\n * by hand.\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: AssetPickerProps) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n </AssetsProvider>\n )\n}\n\n/** The open media-browser stage over a mounted assets store. */\nfunction AssetPickerStage(props: Omit<AssetPickerProps, 'open'>) {\n const store = useAssets()\n return (\n <ExpandableSurface\n title={translate('cms.asset.pickTitle')}\n closeLabel={translate('common.close')}\n variant=\"stage\"\n preset={ASSET_PICKER_STAGE_PRESET}\n onDismiss={() => props.onOpenChange(false)}\n headerLeading={<ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden />}\n >\n <AssetPickerBrowser store={store} {...props} />\n </ExpandableSurface>\n )\n}\n\n/** The slice of the assets store the browser reads and drives. */\nexport type AssetPickerStore = Pick<\n AssetsStore,\n 'status' | 'errorMessage' | 'assets' | 'uploads' | 'upload' | 'landedAssetId'\n>\n\n/**\n * The media browser itself — search, Upload, the grid, the drop zone — rendered inside\n * the stage. A named component of its own (EC-588) so its markup is reachable by a\n * node-env smoke render (the stage portal is `null` there): the file input's `accept`\n * attribute and the grid's filter are pinned against the field's `accept`.\n */\nexport function AssetPickerBrowser({\n store,\n value,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: Omit<AssetPickerProps, 'open'> & { store: AssetPickerStore }) {\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [dragging, setDragging] = useState(false)\n // Names of files a drop/upload refused against the field's `accept` (EC-588) — shown\n // as a visible notice and never uploaded. Cleared by the next upload attempt.\n const [rejected, setRejected] = useState<string[]>([])\n const fileInputRef = useRef<HTMLInputElement>(null)\n // The active upload BATCH (EC-460): exactly the upload ids this interaction started\n // (EC-588 A7), so the effect below attaches this batch's uploads and nothing else.\n const pendingRef = useRef<UploadBatch | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n // EC-659: the search reads the alts the picker shows — through the studio's asset lens.\n const searchLocale = useAssetReadLocale()\n const assets = visiblePickerAssets(store.assets, query, accept, searchLocale)\n\n // Auto-select what an in-picker upload added, then close. A `many` consumer\n // (multiple + onChangeMany) waits for the batch to SETTLE and commits every\n // added id — uploading 2 files attaches 2 (EC-460); a single consumer keeps\n // the first-to-land commit. Only uploads whose record satisfies `accept` commit.\n useEffect(() => {\n const batch = pendingRef.current\n if (!batch) return\n // EC-588 2i: the one batch-attach rule the field's own drop zone runs too.\n const many = multiple === true && onChangeMany !== undefined\n const { attach, disarm } = batchAttachment(uploadBatchProgress(batch, store, accept), many)\n if (disarm) pendingRef.current = null\n if (attach.length === 0) return\n if (many) commitPickerSelectionMany(attach, { onChangeMany, onOpenChange })\n else commitPickerSelection(attach[0]!, { onChange, onOpenChange })\n }, [store, accept, multiple, onChange, onChangeMany, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n // EC-588: the drop rule — the same split the field's drop zone uses. A drop\n // bypasses the dialog's soft filter; refused files are named, never uploaded.\n const { accepted, rejected: refused } = splitByAccept(files, accept)\n setRejected(refused)\n if (accepted.length === 0) return\n pendingRef.current = { uploadIds: store.upload(accepted) }\n }\n // The stage portals to document.body, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the stage would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <div\n className=\"relative flex min-h-0 flex-1 flex-col gap-3 p-4\"\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={assetAcceptAttr(accept)}\n className=\"hidden\"\n data-testid=\"asset-picker-file-input\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <AssetRejectedNotice rejected={rejected} accept={accept} />\n\n <div\n className=\"grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\"\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => commitPickerSelection(asset.id, { onChange, onOpenChange })}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => commitPickerSelection(null, { onChange, onOpenChange })}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </div>\n )\n}\n\n/**\n * The visible notice for files an upload path refused against the field's `accept`\n * (EC-301 → EC-588): named, never silently dropped. Shared by the field's drop zone and\n * the picker, which apply the same rule. Renders nothing when nothing was refused.\n */\nexport function AssetRejectedNotice({\n rejected,\n accept,\n}: {\n rejected: readonly string[]\n accept: readonly string[] | undefined\n}) {\n if (rejected.length === 0) return null\n return (\n <Row gap=\"2\" align=\"center\" data-testid=\"asset-rejected-notice\">\n <AlertTriangle className=\"size-4 shrink-0 text-warning\" />\n <Text size=\"xs\" tone=\"muted\">\n {rejected.length === 1\n ? translate('cms.asset.rejected.one', { name: rejected[0]!, types: describeAccept(accept) })\n : translate('cms.asset.rejected.other', {\n count: rejected.length,\n types: describeAccept(accept),\n })}\n </Text>\n </Row>\n )\n}\n\n/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The\n * internal asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n const alt = assetAlt(asset, useAssetReadLocale())\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n aria-pressed={selected}\n >\n <span className=\"relative block\">\n <AssetThumb record={asset} className=\"aspect-video w-full\" />\n {selected ? (\n <span className=\"absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow\">\n <Check className=\"size-3\" />\n </span>\n ) : null}\n </span>\n <Text size=\"xs\" className=\"min-w-0 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {/* EC-587 (ASSETS D20): the asset's alt through THE reader, as delivery serves it —\n EC-632: in the studio's locale. */}\n <Text size=\"xs\" tone={alt ? 'muted' : 'subtle'} className=\"truncate\">\n {alt || translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n"]}
@@ -46,11 +46,17 @@ export declare function isCanvasControlKey(key: string): boolean;
46
46
  * Anything else — a canvas action, a discrete control, an edit with no control focused —
47
47
  * commits now (`committed`), after any canvas position still uncommitted, so positions
48
48
  * reach the persister in the order the editor made them.
49
+ *
50
+ * EC-647: `revision` is the revision of the canvas value the write was authored on (its
51
+ * lift's) — the deferred position's base on its first keystroke, as `commitNow`'s caller
52
+ * pairs it with the patches it sends; never the draft's (which may be newer).
49
53
  */
50
54
  export declare function routeCanvasWrite(input: {
51
55
  patches: readonly DocumentPatch[];
52
56
  /** The focused canvas control, if focus is on one. */
53
57
  focused: CanvasControl | undefined;
54
58
  committer: DraftCommitter;
59
+ /** The revision the write was authored against (the canvas lift's). */
60
+ revision: number | undefined;
55
61
  commitNow: (patches: readonly DocumentPatch[]) => void;
56
62
  }): 'deferred' | 'committed';
@@ -20,11 +20,15 @@ function writesExactly(patch, path) {
20
20
  * Anything else — a canvas action, a discrete control, an edit with no control focused —
21
21
  * commits now (`committed`), after any canvas position still uncommitted, so positions
22
22
  * reach the persister in the order the editor made them.
23
+ *
24
+ * EC-647: `revision` is the revision of the canvas value the write was authored on (its
25
+ * lift's) — the deferred position's base on its first keystroke, as `commitNow`'s caller
26
+ * pairs it with the patches it sends; never the draft's (which may be newer).
23
27
  */
24
28
  export function routeCanvasWrite(input) {
25
29
  const { patches, focused, committer } = input;
26
30
  if (focused && focused.policy !== 'change' && patches.length === 1 && writesExactly(patches[0], focused.path)) {
27
- committer.edited(focused.key, focused.path);
31
+ committer.edited(focused.key, focused.path, { base: input.revision });
28
32
  return 'deferred';
29
33
  }
30
34
  if (committer.uncommitted().some((position) => isCanvasControlKey(position.field)))
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-commit.js","sourceRoot":"","sources":["../../../src/components/cms/canvas-commit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAqC1D,6GAA6G;AAC7G,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,QAA+B,EAAE,MAAoB;IACnG,MAAM,IAAI,GACR,MAAM,IAAI,QAAQ;QAChB,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QACxF,CAAC,CAAC,KAAK,CAAA;IACX,OAAO,EAAE,GAAG,EAAE,UAAU,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACjE,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;AAClC,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB,EAAE,IAAe;IAC1D,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,eAAe,CAAC,IAAI,CAAC,CAAA;AAC9G,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAMhC;IACC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IAC7C,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAE,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/G,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3C,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAAE,SAAS,CAAC,SAAS,EAAE,CAAA;IACzG,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IACxB,OAAO,WAAW,CAAA;AACpB,CAAC","sourcesContent":["import { formatValuePath } from '@elytracms/core/cms-core'\nimport type { DocumentPatch, ValuePath } from '@elytracms/core/cms-core'\nimport type { CommitPolicy } from './commit-policy'\nimport type { DraftCommitter } from './draft-commit'\n\n/**\n * EC-630 — the canvas inspector commits like every other editor: on its control kind's\n * quiescence policy (`commit-policy.ts`, EC-595 A2), never per keystroke (pure; the house\n * pattern).\n *\n * The canvas lifts every store change into the draft at once (live validation reads it).\n * A burst of typing in an inspector control used to be committed per keystroke too —\n * every canvas write went straight to the persister under the canvas's `change` policy:\n * 13 revisions for one typography-run edit, measured live (wave-0094), and one per\n * keystroke on any text prop. The canvas's own actions (insert, move, delete, undo, a\n * discrete control) ARE discrete commits; a control's typing is not. So the inspector\n * controls stand inside a commit boundary (focus / blur / Enter, the form's\n * `FieldCommitBoundary`) at a POSITION — a node's prop, or a typography run, which is\n * the field itself (prose is one position, EC-595 binding point 3) — with the policy\n * THE declaration gives that control kind (`commitPolicyOf`), and THE committer\n * (`draft-commit.ts`, shared with the form fields) holds the burst as that position's\n * uncommitted edit: committed on blur, Enter or the idle pause, with the base revision\n * of its first keystroke, overlaid on every rebase meanwhile, and in every forced flush.\n */\n\n/** Where a canvas inspector control writes: a node's prop, or a typography run (the field). */\nexport type CanvasControlPosition = { readonly node: string; readonly prop: string } | { readonly run: number }\n\n/** A focused canvas inspector control, as the committer knows it. */\nexport interface CanvasControl {\n /** The committer's key for it (never a field name — those carry no `:`). */\n readonly key: string\n /** The position it writes. */\n readonly path: ValuePath\n readonly policy: CommitPolicy\n}\n\n/** The control at `position` of the canvas field at `field` (the field's position path, locale included). */\nexport function canvasControl(field: ValuePath, position: CanvasControlPosition, policy: CommitPolicy): CanvasControl {\n const path: ValuePath =\n 'node' in position\n ? [...field, { kind: 'node', id: position.node }, { kind: 'prop', name: position.prop }]\n : field\n return { key: `canvas:${formatValuePath(path)}`, path, policy }\n}\n\n/** Whether a committer key names a canvas control. */\nexport function isCanvasControlKey(key: string): boolean {\n return key.startsWith('canvas:')\n}\n\nfunction writesExactly(patch: DocumentPatch, path: ValuePath): boolean {\n return (patch.op === 'set' || patch.op === 'unset') && formatValuePath(patch.path) === formatValuePath(path)\n}\n\n/**\n * Route one canvas write's patches. When they are exactly the focused control's value\n * write at its position and its policy waits for quiescence, the write is that control's\n * uncommitted position (`deferred` — the committer commits the latest value later).\n * Anything else — a canvas action, a discrete control, an edit with no control focused —\n * commits now (`committed`), after any canvas position still uncommitted, so positions\n * reach the persister in the order the editor made them.\n */\nexport function routeCanvasWrite(input: {\n patches: readonly DocumentPatch[]\n /** The focused canvas control, if focus is on one. */\n focused: CanvasControl | undefined\n committer: DraftCommitter\n commitNow: (patches: readonly DocumentPatch[]) => void\n}): 'deferred' | 'committed' {\n const { patches, focused, committer } = input\n if (focused && focused.policy !== 'change' && patches.length === 1 && writesExactly(patches[0]!, focused.path)) {\n committer.edited(focused.key, focused.path)\n return 'deferred'\n }\n if (committer.uncommitted().some((position) => isCanvasControlKey(position.field))) committer.commitAll()\n input.commitNow(patches)\n return 'committed'\n}\n"]}
1
+ {"version":3,"file":"canvas-commit.js","sourceRoot":"","sources":["../../../src/components/cms/canvas-commit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAqC1D,6GAA6G;AAC7G,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,QAA+B,EAAE,MAAoB;IACnG,MAAM,IAAI,GACR,MAAM,IAAI,QAAQ;QAChB,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QACxF,CAAC,CAAC,KAAK,CAAA;IACX,OAAO,EAAE,GAAG,EAAE,UAAU,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACjE,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;AAClC,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB,EAAE,IAAe;IAC1D,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,eAAe,CAAC,IAAI,CAAC,CAAA;AAC9G,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAQhC;IACC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IAC7C,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAE,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/G,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;QACrE,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAAE,SAAS,CAAC,SAAS,EAAE,CAAA;IACzG,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IACxB,OAAO,WAAW,CAAA;AACpB,CAAC","sourcesContent":["import { formatValuePath } from '@elytracms/core/cms-core'\nimport type { DocumentPatch, ValuePath } from '@elytracms/core/cms-core'\nimport type { CommitPolicy } from './commit-policy'\nimport type { DraftCommitter } from './draft-commit'\n\n/**\n * EC-630 — the canvas inspector commits like every other editor: on its control kind's\n * quiescence policy (`commit-policy.ts`, EC-595 A2), never per keystroke (pure; the house\n * pattern).\n *\n * The canvas lifts every store change into the draft at once (live validation reads it).\n * A burst of typing in an inspector control used to be committed per keystroke too —\n * every canvas write went straight to the persister under the canvas's `change` policy:\n * 13 revisions for one typography-run edit, measured live (wave-0094), and one per\n * keystroke on any text prop. The canvas's own actions (insert, move, delete, undo, a\n * discrete control) ARE discrete commits; a control's typing is not. So the inspector\n * controls stand inside a commit boundary (focus / blur / Enter, the form's\n * `FieldCommitBoundary`) at a POSITION — a node's prop, or a typography run, which is\n * the field itself (prose is one position, EC-595 binding point 3) — with the policy\n * THE declaration gives that control kind (`commitPolicyOf`), and THE committer\n * (`draft-commit.ts`, shared with the form fields) holds the burst as that position's\n * uncommitted edit: committed on blur, Enter or the idle pause, with the base revision\n * of its first keystroke, overlaid on every rebase meanwhile, and in every forced flush.\n */\n\n/** Where a canvas inspector control writes: a node's prop, or a typography run (the field). */\nexport type CanvasControlPosition = { readonly node: string; readonly prop: string } | { readonly run: number }\n\n/** A focused canvas inspector control, as the committer knows it. */\nexport interface CanvasControl {\n /** The committer's key for it (never a field name — those carry no `:`). */\n readonly key: string\n /** The position it writes. */\n readonly path: ValuePath\n readonly policy: CommitPolicy\n}\n\n/** The control at `position` of the canvas field at `field` (the field's position path, locale included). */\nexport function canvasControl(field: ValuePath, position: CanvasControlPosition, policy: CommitPolicy): CanvasControl {\n const path: ValuePath =\n 'node' in position\n ? [...field, { kind: 'node', id: position.node }, { kind: 'prop', name: position.prop }]\n : field\n return { key: `canvas:${formatValuePath(path)}`, path, policy }\n}\n\n/** Whether a committer key names a canvas control. */\nexport function isCanvasControlKey(key: string): boolean {\n return key.startsWith('canvas:')\n}\n\nfunction writesExactly(patch: DocumentPatch, path: ValuePath): boolean {\n return (patch.op === 'set' || patch.op === 'unset') && formatValuePath(patch.path) === formatValuePath(path)\n}\n\n/**\n * Route one canvas write's patches. When they are exactly the focused control's value\n * write at its position and its policy waits for quiescence, the write is that control's\n * uncommitted position (`deferred` — the committer commits the latest value later).\n * Anything else — a canvas action, a discrete control, an edit with no control focused —\n * commits now (`committed`), after any canvas position still uncommitted, so positions\n * reach the persister in the order the editor made them.\n *\n * EC-647: `revision` is the revision of the canvas value the write was authored on (its\n * lift's) — the deferred position's base on its first keystroke, as `commitNow`'s caller\n * pairs it with the patches it sends; never the draft's (which may be newer).\n */\nexport function routeCanvasWrite(input: {\n patches: readonly DocumentPatch[]\n /** The focused canvas control, if focus is on one. */\n focused: CanvasControl | undefined\n committer: DraftCommitter\n /** The revision the write was authored against (the canvas lift's). */\n revision: number | undefined\n commitNow: (patches: readonly DocumentPatch[]) => void\n}): 'deferred' | 'committed' {\n const { patches, focused, committer } = input\n if (focused && focused.policy !== 'change' && patches.length === 1 && writesExactly(patches[0]!, focused.path)) {\n committer.edited(focused.key, focused.path, { base: input.revision })\n return 'deferred'\n }\n if (committer.uncommitted().some((position) => isCanvasControlKey(position.field))) committer.commitAll()\n input.commitNow(patches)\n return 'committed'\n}\n"]}
@@ -57,14 +57,27 @@ export interface CmsStore {
57
57
  /**
58
58
  * EC-595 A2 — commit the patches a writer made to a document (the form's field
59
59
  * paths, the canvas's node patches, an AI proposal's changes, …): the session's view
60
- * updates at once and the patches persist through `documents.patch` against the
61
- * revision the view was at. A refused path becomes a per-field conflict
62
- * ({@link documentConflicts}); a failure, {@link persistError}. Resolves with the
63
- * commit's outcome once acknowledged (fixture sessions: applied locally, at once).
60
+ * updates at once and the patches persist through `documents.patch` against `bases` —
61
+ * REQUIRED, per patch: the revision of the view the writer authored it on (EC-647: a
62
+ * row's {@link revisionOfView}, the draft's revision, the read an AI proposal was built
63
+ * from — never the session's current one by default). A refused path becomes a
64
+ * per-field conflict ({@link documentConflicts}); a failure, {@link persistError}.
65
+ * Resolves with the commit's outcome once acknowledged (fixture sessions: applied
66
+ * locally, at once).
64
67
  */
65
- commitPatches(ref: DocumentRef, patches: readonly DocumentPatch[], bases?: readonly (number | undefined)[]): Promise<CommitOutcome | undefined>;
66
- /** EC-595 A2 — the revision the session's view of a document is at (the next patch's base). */
68
+ commitPatches(ref: DocumentRef, patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]): Promise<CommitOutcome | undefined>;
69
+ /** EC-595 A2 — the revision the session's view of a document is at now. */
67
70
  revisionOf(ref: DocumentRef): number | undefined;
71
+ /**
72
+ * EC-647 — the revision a document value this session SHOWED is at (a list row, the
73
+ * editor's `doc`): the value and its revision travel together, so a writer authoring
74
+ * from what it displayed names that revision. `undefined` for a value the session never
75
+ * showed (a fixture session, a copy) or a provisional one (a create in flight).
76
+ * EC-667: with `path`, the revision the content at that position was authored against
77
+ * (a view showing a pending write is a composite); without it, the EARLIEST of any
78
+ * position — the whole-document writer's token.
79
+ */
80
+ revisionOfView(view: CmsDocument, path?: ValuePath): number | undefined;
68
81
  /**
69
82
  * EC-595 A2 / EC-578 — create a document from a whole value (duplicate, import of a
70
83
  * new row, an AI create, a transfer, a reuse promote): insert-if-absent
@@ -105,8 +118,11 @@ export interface CmsStore {
105
118
  clearPresence(ref: DocumentRef): void;
106
119
  /** This tab's presence id. */
107
120
  presenceSessionId: string;
108
- /** Write a whole document at the session's revision (a document-level key — the alias). */
109
- replaceDocument(doc: CmsDocument): void;
121
+ /**
122
+ * Write a whole document at `revision` — the revision of the view its snapshot was
123
+ * authored on (EC-647; a document-level key — the alias).
124
+ */
125
+ replaceDocument(doc: CmsDocument, revision: number | undefined): void;
110
126
  deleteDocument(ref: DocumentRef): void;
111
127
  /**
112
128
  * Is this document currently published? Sourced from the version POINTER
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { applyDocumentPatches, createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults, titleFieldOf, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
3
+ import { applyDocumentPatches, createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults, titleFieldOf, touchedPaths, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
4
4
  import { createUuidIdFactory } from '@elytracms/core/persistence';
5
5
  import { translate } from '../../lib/i18n/index.js';
6
6
  import { configureStudioLocales, useStudioLocale } from '../../lib/locale/index.js';
@@ -10,7 +10,7 @@ import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
10
10
  import { assetLibraryOracle, gateValidateOptions } from './gate-validation.js';
11
11
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
12
12
  import { createRequiredContext } from '../../lib/required-context.js';
13
- import { createCmsPersister, getStudioActor, getStudioOperations } from '../../lib/operations/index.js';
13
+ import { createCmsPersister, getStudioActor, getStudioOperations, importWrite } from '../../lib/operations/index.js';
14
14
  import { publishInSession, publishWithRevision as publishRevision, } from '../../lib/operations/publish-with-revision.js';
15
15
  import { operationErrorMessage } from '../../lib/persona/labels.js';
16
16
  import { fieldWritePatch } from '../../lib/cms/field-patch.js';
@@ -329,8 +329,10 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
329
329
  return next;
330
330
  }),
331
331
  });
332
- // The base every hydrated document's patches are made against (EC-595 A2).
333
- for (const doc of initialDocuments ?? []) {
332
+ // The base every hydrated document's patches are made against (EC-595 A2) — tracked
333
+ // with the very objects the session shows (EC-647: a shown value carries its
334
+ // revision; the seed is cloned into state, so the originals would not be the rows).
335
+ for (const doc of documentsRef.current) {
334
336
  const revision = initialRevisions?.[documentKey(doc)];
335
337
  if (revision !== undefined)
336
338
  persisterRef.current.track({ document: doc, revision });
@@ -389,7 +391,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
389
391
  }
390
392
  return Promise.resolve(undefined);
391
393
  }
392
- return persister.commit(ref, patches, current, bases);
394
+ return persister.commit(ref, patches, bases, current);
393
395
  }, [stampUpdatedAt]);
394
396
  const insertDocument = useCallback((doc) => {
395
397
  stampUpdatedAt(doc); // EC-445: a fresh document's $updatedAt starts at creation
@@ -428,9 +430,11 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
428
430
  ? {
429
431
  // EC-595 A2: ONE field patch through the persister's queue — at the
430
432
  // revision the row was at — never a whole-document side channel.
433
+ // EC-647: the row the table SHOWED names that revision — EC-667: at the
434
+ // cell's position (the row may show a pending write elsewhere).
431
435
  dispatch: async (next) => {
432
436
  const patch = fieldWritePatch(input.collection, next, input.field.name, input.locale);
433
- const outcome = await persister.commit(ref, [patch], input.doc);
437
+ const outcome = await persister.commit(ref, [patch], [persister.revisionOfView(input.doc, touchedPaths(patch)[0])], input.doc);
434
438
  if (outcome.status === 'applied')
435
439
  return { ok: true, value: outcome.record };
436
440
  if (outcome.status === 'failed')
@@ -728,17 +732,15 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
728
732
  // entry for its ref.
729
733
  // EC-595 A2: a row new to the store is CREATED (insert-if-absent); a row the
730
734
  // session already holds is REPLACED at the revision it was loaded at — an import
731
- // never overwrites a change made since.
735
+ // never overwrites a change made since. EC-647: "loaded at" is the revision of
736
+ // the row the session SHOWED when the import was accepted (the one it judged).
732
737
  const persister = persisterRef.current;
733
738
  if (!persister)
734
739
  setDocuments((prev) => accepted.reduce((acc, doc) => upsertDoc(acc, doc), prev));
735
740
  for (const doc of accepted) {
736
741
  if (!persister)
737
742
  continue;
738
- if (persister.revisionOf(doc) !== undefined)
739
- persister.replace(doc);
740
- else
741
- persister.create(doc);
743
+ importWrite(persister, doc, documents.find((d) => matchesRef(d, doc)));
742
744
  }
743
745
  // EC-224: imports land as the working DRAFT. Publish-ness is the version
744
746
  // pointer (`publishedKeys`), set by publishing — never carried in the
@@ -866,7 +868,11 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
866
868
  });
867
869
  }
868
870
  const written = setFieldDoc(current);
869
- const outcome = await persister.commit(ref, [fieldWritePatch(collection, written, setField.field, activeLocale)], current);
871
+ // EC-647: authored from the row the session holds — its revision, never the
872
+ // tracked one (a colleague's change the user never saw is refused, visibly);
873
+ // EC-667: the revision of the field's position in that row.
874
+ const patch = fieldWritePatch(collection, written, setField.field, activeLocale);
875
+ const outcome = await persister.commit(ref, [patch], [persister.revisionOfView(current, touchedPaths(patch)[0])], current);
870
876
  if (outcome.status === 'applied')
871
877
  return { ok: true, value: outcome.record };
872
878
  if (outcome.status === 'failed')
@@ -985,10 +991,10 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
985
991
  .then((studio) => studio.clearPresence(projectId, ref, PRESENCE_SESSION_ID))
986
992
  .catch(() => { });
987
993
  }, [projectId]);
988
- const replaceDocument = useCallback((doc) => {
994
+ const replaceDocument = useCallback((doc, revision) => {
989
995
  const persister = persisterRef.current;
990
996
  if (persister)
991
- persister.replace(doc);
997
+ persister.replace(doc, revision);
992
998
  else
993
999
  setDocuments((prev) => replaceDoc(prev, doc));
994
1000
  }, []);
@@ -1003,6 +1009,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
1003
1009
  titleOf,
1004
1010
  commitPatches,
1005
1011
  revisionOf: (ref) => persisterRef.current?.revisionOf(ref),
1012
+ revisionOfView: (view, path) => persisterRef.current?.revisionOfView(view, path),
1006
1013
  insertDocument,
1007
1014
  newDocumentId: newId,
1008
1015
  commitFieldEdit,