@elytracms/studio 0.0.37 → 0.0.39

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 (198) hide show
  1. package/convex/cms.ts +8 -2
  2. package/dist/components/app-views/default-modules.d.ts +60 -5
  3. package/dist/components/app-views/default-modules.js +53 -15
  4. package/dist/components/app-views/default-modules.js.map +1 -1
  5. package/dist/components/app-views/module.d.ts +2 -0
  6. package/dist/components/app-views/module.js +8 -0
  7. package/dist/components/app-views/module.js.map +1 -1
  8. package/dist/components/assets/asset-detail.d.ts +24 -0
  9. package/dist/components/assets/asset-detail.js +163 -0
  10. package/dist/components/assets/asset-detail.js.map +1 -0
  11. package/dist/components/assets/asset-grid.d.ts +28 -0
  12. package/dist/components/assets/asset-grid.js +68 -0
  13. package/dist/components/assets/asset-grid.js.map +1 -0
  14. package/dist/components/assets/asset-metadata.d.ts +19 -0
  15. package/dist/components/assets/asset-metadata.js +44 -0
  16. package/dist/components/assets/asset-metadata.js.map +1 -1
  17. package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
  18. package/dist/components/assets/assets-collection-view-source.js +186 -0
  19. package/dist/components/assets/assets-collection-view-source.js.map +1 -0
  20. package/dist/components/assets/assets-module.d.ts +18 -0
  21. package/dist/components/assets/assets-module.js +94 -0
  22. package/dist/components/assets/assets-module.js.map +1 -0
  23. package/dist/components/assets/assets-store.js +2 -7
  24. package/dist/components/assets/assets-store.js.map +1 -1
  25. package/dist/components/assets/assets-view-preference.d.ts +15 -0
  26. package/dist/components/assets/assets-view-preference.js +22 -0
  27. package/dist/components/assets/assets-view-preference.js.map +1 -0
  28. package/dist/components/builder/builder-store.d.ts +12 -0
  29. package/dist/components/builder/builder-store.js +18 -16
  30. package/dist/components/builder/builder-store.js.map +1 -1
  31. package/dist/components/builder/clipboard-actions.js +5 -14
  32. package/dist/components/builder/clipboard-actions.js.map +1 -1
  33. package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
  34. package/dist/components/builder/composition-field-editor-lazy.js +29 -0
  35. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
  36. package/dist/components/builder/composition-field-editor.js +1 -4
  37. package/dist/components/builder/composition-field-editor.js.map +1 -1
  38. package/dist/components/builder/insert-context.d.ts +8 -0
  39. package/dist/components/builder/insert-context.js +1 -3
  40. package/dist/components/builder/insert-context.js.map +1 -1
  41. package/dist/components/builder/page-tree.js +1 -3
  42. package/dist/components/builder/page-tree.js.map +1 -1
  43. package/dist/components/builder/property-inspector.js +7 -134
  44. package/dist/components/builder/property-inspector.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +15 -4
  46. package/dist/components/cms/cms-store.js +42 -24
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.d.ts +8 -1
  49. package/dist/components/cms/collection-table.js +62 -10
  50. package/dist/components/cms/collection-table.js.map +1 -1
  51. package/dist/components/cms/column-width.d.ts +25 -0
  52. package/dist/components/cms/column-width.js +47 -0
  53. package/dist/components/cms/column-width.js.map +1 -0
  54. package/dist/components/cms/detail-editor.js +101 -68
  55. package/dist/components/cms/detail-editor.js.map +1 -1
  56. package/dist/components/cms/discard-draft.d.ts +32 -0
  57. package/dist/components/cms/discard-draft.js +13 -0
  58. package/dist/components/cms/discard-draft.js.map +1 -0
  59. package/dist/components/cms/document-staging.d.ts +7 -0
  60. package/dist/components/cms/document-staging.js +44 -0
  61. package/dist/components/cms/document-staging.js.map +1 -0
  62. package/dist/components/cms/field-connection.d.ts +22 -0
  63. package/dist/components/cms/field-connection.js +37 -0
  64. package/dist/components/cms/field-connection.js.map +1 -0
  65. package/dist/components/cms/field-control.js +158 -34
  66. package/dist/components/cms/field-control.js.map +1 -1
  67. package/dist/components/cms/prop-fields.d.ts +43 -0
  68. package/dist/components/cms/prop-fields.js +130 -0
  69. package/dist/components/cms/prop-fields.js.map +1 -0
  70. package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
  71. package/dist/components/cms/rich-text/component-embed.js +29 -7
  72. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  73. package/dist/components/cms/rich-text/editor.d.ts +3 -0
  74. package/dist/components/cms/rich-text/editor.js +21 -3
  75. package/dist/components/cms/rich-text/editor.js.map +1 -1
  76. package/dist/components/cms/rich-text/embed-picker.d.ts +17 -0
  77. package/dist/components/cms/rich-text/embed-picker.js +34 -0
  78. package/dist/components/cms/rich-text/embed-picker.js.map +1 -0
  79. package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
  80. package/dist/components/cms/rich-text/embed-props.js +106 -0
  81. package/dist/components/cms/rich-text/embed-props.js.map +1 -0
  82. package/dist/components/cms/rich-text/extensions.d.ts +7 -1
  83. package/dist/components/cms/rich-text/extensions.js +6 -1
  84. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  85. package/dist/components/cms/rich-text/index.d.ts +6 -1
  86. package/dist/components/cms/rich-text/index.js +6 -1
  87. package/dist/components/cms/rich-text/index.js.map +1 -1
  88. package/dist/components/cms/rich-text/insert.d.ts +21 -0
  89. package/dist/components/cms/rich-text/insert.js +55 -0
  90. package/dist/components/cms/rich-text/insert.js.map +1 -0
  91. package/dist/components/cms/rich-text/lazy.d.ts +19 -0
  92. package/dist/components/cms/rich-text/lazy.js +26 -0
  93. package/dist/components/cms/rich-text/lazy.js.map +1 -0
  94. package/dist/components/cms/rich-text/slash-command.d.ts +27 -0
  95. package/dist/components/cms/rich-text/slash-command.js +158 -0
  96. package/dist/components/cms/rich-text/slash-command.js.map +1 -0
  97. package/dist/components/cms/rich-text/toolbar.d.ts +5 -6
  98. package/dist/components/cms/rich-text/toolbar.js +3 -5
  99. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  100. package/dist/components/cms/row-actions.d.ts +28 -0
  101. package/dist/components/cms/row-actions.js +20 -0
  102. package/dist/components/cms/row-actions.js.map +1 -0
  103. package/dist/components/cms/use-document-versions.d.ts +8 -1
  104. package/dist/components/cms/use-document-versions.js +31 -4
  105. package/dist/components/cms/use-document-versions.js.map +1 -1
  106. package/dist/components/cms/version-compare-lazy.d.ts +1 -0
  107. package/dist/components/cms/version-compare-lazy.js +23 -0
  108. package/dist/components/cms/version-compare-lazy.js.map +1 -0
  109. package/dist/components/cms/version-compare-text.d.ts +46 -0
  110. package/dist/components/cms/version-compare-text.js +78 -0
  111. package/dist/components/cms/version-compare-text.js.map +1 -1
  112. package/dist/components/cms/version-compare.d.ts +9 -5
  113. package/dist/components/cms/version-compare.js +184 -59
  114. package/dist/components/cms/version-compare.js.map +1 -1
  115. package/dist/components/cms/versions-pane.js +10 -4
  116. package/dist/components/cms/versions-pane.js.map +1 -1
  117. package/dist/components/components/component-detail.d.ts +5 -3
  118. package/dist/components/components/component-detail.js +41 -8
  119. package/dist/components/components/component-detail.js.map +1 -1
  120. package/dist/components/components/component-preview.d.ts +61 -0
  121. package/dist/components/components/component-preview.js +103 -0
  122. package/dist/components/components/component-preview.js.map +1 -0
  123. package/dist/components/components/components-index.d.ts +5 -3
  124. package/dist/components/components/components-index.js +19 -24
  125. package/dist/components/components/components-index.js.map +1 -1
  126. package/dist/components/components/registry-browser.d.ts +7 -0
  127. package/dist/components/components/registry-browser.js +20 -36
  128. package/dist/components/components/registry-browser.js.map +1 -1
  129. package/dist/components/components/taxonomy.d.ts +51 -0
  130. package/dist/components/components/taxonomy.js +73 -0
  131. package/dist/components/components/taxonomy.js.map +1 -0
  132. package/dist/components/components/transfer-dialog.d.ts +20 -0
  133. package/dist/components/components/transfer-dialog.js +64 -0
  134. package/dist/components/components/transfer-dialog.js.map +1 -0
  135. package/dist/components/components/transfer.d.ts +43 -0
  136. package/dist/components/components/transfer.js +31 -0
  137. package/dist/components/components/transfer.js.map +1 -0
  138. package/dist/components/project-list.js +3 -4
  139. package/dist/components/project-list.js.map +1 -1
  140. package/dist/components/project-switcher.js +3 -2
  141. package/dist/components/project-switcher.js.map +1 -1
  142. package/dist/components/rail-store-status.d.ts +21 -0
  143. package/dist/components/rail-store-status.js +21 -0
  144. package/dist/components/rail-store-status.js.map +1 -0
  145. package/dist/components/redirects/redirects-module.js +4 -8
  146. package/dist/components/redirects/redirects-module.js.map +1 -1
  147. package/dist/components/redirects/redirects-store.js +2 -7
  148. package/dist/components/redirects/redirects-store.js.map +1 -1
  149. package/dist/components/sign-in-needed-badge.d.ts +6 -0
  150. package/dist/components/sign-in-needed-badge.js +13 -0
  151. package/dist/components/sign-in-needed-badge.js.map +1 -0
  152. package/dist/components/studio-project-screen.js +1 -4
  153. package/dist/components/studio-project-screen.js.map +1 -1
  154. package/dist/components/studio-shell.js +1 -4
  155. package/dist/components/studio-shell.js.map +1 -1
  156. package/dist/components/users/users-module.js +5 -7
  157. package/dist/components/users/users-module.js.map +1 -1
  158. package/dist/components/users/users-store.js +2 -7
  159. package/dist/components/users/users-store.js.map +1 -1
  160. package/dist/lib/auth/index.js +1 -1
  161. package/dist/lib/auth/index.js.map +1 -1
  162. package/dist/lib/clipboard/paste-target.js +1 -3
  163. package/dist/lib/clipboard/paste-target.js.map +1 -1
  164. package/dist/lib/cms-views/view-model.d.ts +0 -2
  165. package/dist/lib/cms-views/view-model.js +0 -7
  166. package/dist/lib/cms-views/view-model.js.map +1 -1
  167. package/dist/lib/diff-view-preference.d.ts +7 -0
  168. package/dist/lib/diff-view-preference.js +21 -0
  169. package/dist/lib/diff-view-preference.js.map +1 -0
  170. package/dist/lib/i18n/messages.en.js +81 -30
  171. package/dist/lib/i18n/messages.en.js.map +1 -1
  172. package/dist/lib/local-preference.d.ts +11 -0
  173. package/dist/lib/local-preference.js +26 -0
  174. package/dist/lib/local-preference.js.map +1 -0
  175. package/dist/lib/locale/fallback.d.ts +45 -0
  176. package/dist/lib/locale/fallback.js +85 -0
  177. package/dist/lib/locale/fallback.js.map +1 -1
  178. package/dist/lib/persona/labels.d.ts +6 -0
  179. package/dist/lib/persona/labels.js +8 -0
  180. package/dist/lib/persona/labels.js.map +1 -1
  181. package/dist/lib/query/index.d.ts +1 -1
  182. package/dist/lib/query/index.js +1 -1
  183. package/dist/lib/query/index.js.map +1 -1
  184. package/dist/lib/query/operations-query.d.ts +14 -2
  185. package/dist/lib/query/operations-query.js +11 -6
  186. package/dist/lib/query/operations-query.js.map +1 -1
  187. package/dist/lib/rail.d.ts +0 -6
  188. package/dist/lib/rail.js +6 -25
  189. package/dist/lib/rail.js.map +1 -1
  190. package/dist/lib/theme.d.ts +0 -6
  191. package/dist/lib/theme.js +10 -13
  192. package/dist/lib/theme.js.map +1 -1
  193. package/dist/router/match.d.ts +1 -0
  194. package/dist/router/match.js +4 -0
  195. package/dist/router/match.js.map +1 -1
  196. package/dist/ui/button.d.ts +1 -1
  197. package/package.json +22 -24
  198. package/src/styles/studio.css +17 -0
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { AlertTriangle, Link2, RotateCcw, Trash2 } from 'lucide-react';
4
+ import { Link, STUDIO_PROJECT_ROUTE, useNavigate } from '@elytracms/studio/router';
5
+ import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
6
+ import { buildAssetReferenceImpact, loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references';
7
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Heading, Panel, PanelBody, PanelHeader, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
8
+ import { FieldControl } from '../cms/field-control';
9
+ import { fieldLabel } from '../cms/cms-store';
10
+ import { ReferenceImpactRows } from '../cms/reference-confirm';
11
+ import { ReferencesPanel } from '../references/references-panel';
12
+ import { useSetBreadcrumb } from '../breadcrumb-context';
13
+ import { useCollectionView } from '../collection-view/collection-view-context';
14
+ import { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb';
15
+ import { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata';
16
+ import { useAssets } from './assets-store';
17
+ /**
18
+ * The assets rail's DETAIL surface (EC-319). Pragmatic call: assets don't fit
19
+ * the portable `PortableDetailEditor` shell cleanly — there's no dirty/Save/
20
+ * Publish document model here (an asset record isn't a `cmsDocument`), and the
21
+ * focal-point picker is a bespoke drag interaction over the image, not a
22
+ * scalar field value a generic form row can host without contortions. So this
23
+ * is a bespoke surface (mirrors the CMS document editor's shape: a header +
24
+ * panels), reached via the SAME rail-owned `detailPath`/`listPath` seam every
25
+ * other rail uses (AD-19) — only the body differs.
26
+ *
27
+ * Unifies what were two divergent asset-edit UIs before this task: the plain
28
+ * alt/title dialog on the old asset library card, and the richer schema-driven
29
+ * + focal-point "image options" dialog opened from an `asset` field elsewhere
30
+ * (`field-control.tsx`). This page reuses the LATTER (richer) content, so
31
+ * editing an asset here is never a downgrade from editing it via a field.
32
+ *
33
+ * Deletion keeps the EC-202 reference-index safe-delete path (`assets-store.tsx`
34
+ * → `deleteAsset`, keyed on `{ type: 'asset' }` — never the generic document
35
+ * reference-confirm the universal table's row/bulk delete would wrongly apply).
36
+ */
37
+ export function AssetDetail({ projectId, assetId }) {
38
+ const source = useCollectionView();
39
+ const store = useAssets();
40
+ const record = useAssetRecord(assetId);
41
+ const collection = source.schema;
42
+ const collectionLabel = localizedLabel(collection.form?.label, collection.id);
43
+ useSetBreadcrumb(useMemo(() => {
44
+ if (source.listPath === undefined)
45
+ return [{ label: collectionLabel }];
46
+ const title = record?.title ?? record?.filename ?? assetId;
47
+ return [
48
+ { label: collectionLabel, to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } },
49
+ { label: title },
50
+ ];
51
+ }, [collectionLabel, source.listPath, record, projectId, assetId]));
52
+ if (record === undefined) {
53
+ if (store.status === 'loading') {
54
+ return (_jsx(Stack, { className: "h-full place-items-center p-8", align: "center", justify: "center", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('assets.loading') }) }));
55
+ }
56
+ return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "text-center", children: [_jsx(Heading, { level: 2, size: "h2", children: translate('cms.editor.notFoundTitle') }), source.listPath !== undefined ? (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath }, className: "text-sm text-primary hover:underline", children: translate('cms.editor.backToList', { label: collectionLabel }) })) : null] }) }));
57
+ }
58
+ // Remount per asset (the surface's own key, set by the caller) resets every
59
+ // local buffer below cleanly when navigating from one asset's detail to
60
+ // another — no stale-state carryover to guard against by hand.
61
+ return _jsx(AssetDetailBody, { projectId: projectId, record: record, collection: source.schema });
62
+ }
63
+ function AssetDetailBody({ projectId, record, collection, }) {
64
+ const store = useAssets();
65
+ const metaFields = useMemo(() => assetMetaFieldsOf(collection), [collection]);
66
+ // Metadata buffers locally and commits explicitly (Save) — an asset record has
67
+ // no dirty/Save document model of its own to piggyback on. The focal point
68
+ // commits immediately (it drives a live visual), matching the field's options
69
+ // dialog (`field-control.tsx`).
70
+ const [meta, setMeta] = useState(() => ({
71
+ ...(record.alt !== undefined ? { alt: record.alt } : {}),
72
+ ...(record.meta ?? {}),
73
+ }));
74
+ const [focal, setFocal] = useState(record.focalPoint);
75
+ const [dirty, setDirty] = useState(false);
76
+ const [saving, setSaving] = useState(false);
77
+ const [saveError, setSaveError] = useState(null);
78
+ function setField(name, value) {
79
+ setDirty(true);
80
+ setMeta((m) => ({ ...m, [name]: value }));
81
+ }
82
+ async function save() {
83
+ setSaving(true);
84
+ setSaveError(null);
85
+ const result = await store.updateMetadata(record.id, { meta: pruneMeta(meta) });
86
+ setSaving(false);
87
+ if (result.ok)
88
+ setDirty(false);
89
+ else
90
+ setSaveError(result.message);
91
+ }
92
+ function commitFocal(point) {
93
+ setFocal(point ?? undefined);
94
+ void store.updateMetadata(record.id, { focalPoint: point });
95
+ }
96
+ return (_jsxs(Stack, { gap: "4", className: "mx-auto max-w-3xl p-4 pb-10 sm:p-6", children: [_jsx(AssetDetailHeader, { projectId: projectId, record: record }), _jsxs(Panel, { children: [_jsx(PanelHeader, { children: _jsxs(Row, { align: "center", className: "w-full", children: [_jsx(Text, { size: "xs", weight: "semibold", tone: "muted", children: translate('cms.asset.focalPoint') }), _jsx(Spacer, {}), focal ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => commitFocal(null), children: [_jsx(RotateCcw, {}), " ", translate('cms.asset.resetToCentre')] })) : null] }) }), _jsx(PanelBody, { children: _jsxs(Stack, { gap: "2", children: [_jsx(FocalPointPicker, { record: record, point: focal, onChange: commitFocal, onPreview: setFocal }), _jsx(Text, { size: "xs", tone: "subtle", children: translate('cms.asset.focalPointHint') }), _jsx(FocalCropPreviews, { record: record, point: focal })] }) })] }), _jsxs(Panel, { "data-testid": "asset-detail-meta", children: [_jsx(PanelHeader, { children: _jsx(Text, { size: "xs", weight: "semibold", tone: "muted", children: translate('assets.detail.metadataHeading') }) }), _jsx(PanelBody, { children: _jsxs(Stack, { gap: "3", children: [metaFields.map((f) => (_jsx(Field, { label: fieldLabel(f), hint: localizedLabel(f.form?.description) || undefined, children: _jsx(FieldControl, { id: `asset-detail-${f.name}`, field: f, value: meta[f.name], readOnly: false, onChange: (v) => setField(f.name, v) }) }, f.name))), metaFields.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.asset.noMetaFields', { code1: 'alt', code2: 'copyright' }) })) : null, saveError ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-detail-save-error", children: saveError })) : null, metaFields.length > 0 ? (_jsxs(Row, { gap: "2", children: [_jsx(Spacer, {}), _jsx(Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, "data-testid": "asset-detail-save", children: saving ? translate('common.saving') : translate('common.save') })] })) : null] }) })] })] }));
97
+ }
98
+ /** Filename/title heading + system info (content type, dimensions, size) + actions. */
99
+ function AssetDetailHeader({ projectId, record }) {
100
+ const dims = record.width && record.height ? `${record.width}×${record.height}` : null;
101
+ const info = [record.contentType, dims, formatBytes(record.byteSize)].filter(Boolean).join(' · ');
102
+ return (_jsxs(Row, { gap: "3", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Heading, { level: 2, size: "h2", className: "truncate", title: record.title ?? record.filename, children: record.title ?? record.filename }), record.title ? (_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: record.filename })) : null, _jsx(Text, { size: "xs", tone: "subtle", children: info })] }), _jsxs(Row, { gap: "2", children: [_jsx(AssetReferencesButton, { projectId: projectId, record: record }), _jsx(AssetDeleteButton, { projectId: projectId, record: record })] })] }));
103
+ }
104
+ /** "Where is this asset used?" (EC-172): the shared references panel, EC-155-backed. */
105
+ function AssetReferencesButton({ projectId, record }) {
106
+ const [open, setOpen] = useState(false);
107
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), "data-testid": "asset-detail-references", children: [_jsx(Link2, {}), " ", translate('assets.references.buttonLabel')] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('assets.references.title', { name: record.title ?? record.filename }) }), _jsx(DialogDescription, { children: translate('assets.references.description') })] }), open ? (_jsx(ReferencesPanel, { projectId: projectId, target: { type: 'asset', id: record.id } })) : null] }) })] }));
108
+ }
109
+ /**
110
+ * Reference-respecting delete (EC-151, EC-202): enumerates the exact inbound
111
+ * references — index-accurate, with deep links to the field/node — before
112
+ * removing. Deleting a referenced asset is weak-by-default (allowed with an
113
+ * explicit "delete anyway"); every reference dangles into a visible validation
114
+ * issue rather than silently breaking. On success, returns to the list.
115
+ */
116
+ function AssetDeleteButton({ projectId, record }) {
117
+ const source = useCollectionView();
118
+ const store = useAssets();
119
+ const navigate = useNavigate();
120
+ const [open, setOpen] = useState(false);
121
+ const [usages, setUsages] = useState(null);
122
+ const [error, setError] = useState(null);
123
+ const [busy, setBusy] = useState(false);
124
+ const openDialog = async () => {
125
+ setOpen(true);
126
+ setError(null);
127
+ setUsages(null);
128
+ try {
129
+ setUsages(await loadInboundAssetReferenceViews(projectId, record.id));
130
+ }
131
+ catch (cause) {
132
+ setError(cause instanceof Error ? cause.message : String(cause));
133
+ }
134
+ };
135
+ const confirm = async () => {
136
+ setBusy(true);
137
+ setError(null);
138
+ // The dialog has already enumerated usages (or shown there are none) and the
139
+ // user is confirming either way — confirm unconditionally, skipping the
140
+ // redundant inbound re-check (EC-301 perf). Weak-by-default: a still-
141
+ // referenced asset deletes anyway, dangling into a visible issue.
142
+ const result = await store.remove(record.id, { confirmInUse: true });
143
+ setBusy(false);
144
+ if (result.outcome === 'deleted' || result.outcome === 'not-found') {
145
+ setOpen(false);
146
+ if (source.listPath !== undefined) {
147
+ void navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } });
148
+ }
149
+ }
150
+ else if (result.outcome === 'blocked') {
151
+ setError(translate(result.usageCount === 1 ? 'assets.delete.raceError.one' : 'assets.delete.raceError.other', { count: result.usageCount }));
152
+ }
153
+ else {
154
+ setError(result.message);
155
+ }
156
+ };
157
+ const impact = usages ? buildAssetReferenceImpact(usages) : null;
158
+ const usageCount = impact?.broken.length ?? 0;
159
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => void openDialog(), "data-testid": "asset-detail-delete", children: [_jsx(Trash2, {}), " ", translate('common.delete')] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-h-[80vh] max-w-md overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('assets.delete.title', { filename: record.filename }) }), _jsx(DialogDescription, { children: translate('assets.delete.description') })] }), usages === null && error === null ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('assets.delete.checking') })) : usageCount > 0 ? (_jsxs(Stack, { gap: "2", "data-testid": "asset-usage-warning", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-warning" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate(usageCount === 1 ? 'assets.delete.inUse.one' : 'assets.delete.inUse.other', {
160
+ count: usageCount,
161
+ }) })] }), _jsx(ReferenceImpactRows, { projectId: projectId, broken: impact.broken, onNavigate: () => setOpen(false) }), _jsx(Text, { size: "xs", tone: "muted", children: translate('assets.delete.brokenWarning') })] })) : (_jsx(Text, { size: "sm", tone: "muted", children: translate('assets.delete.noReferences') })), error ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-delete-error", children: error })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), disabled: busy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => void confirm(), disabled: busy || usages === null, "data-testid": "asset-delete-confirm", children: usageCount > 0 ? translate('assets.delete.confirmAnyway') : translate('common.delete') })] })] }) })] }));
162
+ }
163
+ //# sourceMappingURL=asset-detail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-detail.js","sourceRoot":"","sources":["../../../src/components/assets/asset-detail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { AlertTriangle, Link2, RotateCcw, Trash2 } from 'lucide-react'\nimport type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Link, STUDIO_PROJECT_ROUTE, useNavigate } from '@elytracms/studio/router'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { buildAssetReferenceImpact, loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references'\nimport type { ReferenceEntryView } from '@elytracms/studio/lib/references'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Heading,\n Panel,\n PanelBody,\n PanelHeader,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { FieldControl } from '../cms/field-control'\nimport { fieldLabel } from '../cms/cms-store'\nimport { ReferenceImpactRows } from '../cms/reference-confirm'\nimport { ReferencesPanel } from '../references/references-panel'\nimport { useSetBreadcrumb } from '../breadcrumb-context'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb'\nimport type { FocalPoint } from './focal-point'\nimport { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata'\nimport { useAssets } from './assets-store'\n\n/**\n * The assets rail's DETAIL surface (EC-319). Pragmatic call: assets don't fit\n * the portable `PortableDetailEditor` shell cleanly — there's no dirty/Save/\n * Publish document model here (an asset record isn't a `cmsDocument`), and the\n * focal-point picker is a bespoke drag interaction over the image, not a\n * scalar field value a generic form row can host without contortions. So this\n * is a bespoke surface (mirrors the CMS document editor's shape: a header +\n * panels), reached via the SAME rail-owned `detailPath`/`listPath` seam every\n * other rail uses (AD-19) — only the body differs.\n *\n * Unifies what were two divergent asset-edit UIs before this task: the plain\n * alt/title dialog on the old asset library card, and the richer schema-driven\n * + focal-point \"image options\" dialog opened from an `asset` field elsewhere\n * (`field-control.tsx`). This page reuses the LATTER (richer) content, so\n * editing an asset here is never a downgrade from editing it via a field.\n *\n * Deletion keeps the EC-202 reference-index safe-delete path (`assets-store.tsx`\n * → `deleteAsset`, keyed on `{ type: 'asset' }` — never the generic document\n * reference-confirm the universal table's row/bulk delete would wrongly apply).\n */\nexport function AssetDetail({ projectId, assetId }: { projectId: string; assetId: string }) {\n const source = useCollectionView()\n const store = useAssets()\n const record = useAssetRecord(assetId)\n const collection = source.schema\n\n const collectionLabel = localizedLabel(collection.form?.label, collection.id)\n useSetBreadcrumb(\n useMemo(() => {\n if (source.listPath === undefined) return [{ label: collectionLabel }]\n const title = record?.title ?? record?.filename ?? assetId\n return [\n { label: collectionLabel, to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } },\n { label: title },\n ]\n }, [collectionLabel, source.listPath, record, projectId, assetId]),\n )\n\n if (record === undefined) {\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.loading')}\n </Text>\n </Stack>\n )\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"text-center\">\n <Heading level={2} size=\"h2\">\n {translate('cms.editor.notFoundTitle')}\n </Heading>\n {source.listPath !== undefined ? (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: source.listPath }}\n className=\"text-sm text-primary hover:underline\"\n >\n {translate('cms.editor.backToList', { label: collectionLabel })}\n </Link>\n ) : null}\n </Stack>\n </div>\n )\n }\n\n // Remount per asset (the surface's own key, set by the caller) resets every\n // local buffer below cleanly when navigating from one asset's detail to\n // another — no stale-state carryover to guard against by hand.\n return <AssetDetailBody projectId={projectId} record={record} collection={source.schema} />\n}\n\nfunction AssetDetailBody({\n projectId,\n record,\n collection,\n}: {\n projectId: string\n record: AssetRecord\n collection: CollectionDef\n}) {\n const store = useAssets()\n const metaFields = useMemo(() => assetMetaFieldsOf(collection), [collection])\n\n // Metadata buffers locally and commits explicitly (Save) — an asset record has\n // no dirty/Save document model of its own to piggyback on. The focal point\n // commits immediately (it drives a live visual), matching the field's options\n // dialog (`field-control.tsx`).\n const [meta, setMeta] = useState<Record<string, unknown>>(() => ({\n ...(record.alt !== undefined ? { alt: record.alt } : {}),\n ...(record.meta ?? {}),\n }))\n const [focal, setFocal] = useState<FocalPoint | undefined>(record.focalPoint)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n\n function setField(name: string, value: unknown) {\n setDirty(true)\n setMeta((m) => ({ ...m, [name]: value }))\n }\n\n async function save() {\n setSaving(true)\n setSaveError(null)\n const result = await store.updateMetadata(record.id, { meta: pruneMeta(meta) })\n setSaving(false)\n if (result.ok) setDirty(false)\n else setSaveError(result.message)\n }\n\n function commitFocal(point: FocalPoint | null) {\n setFocal(point ?? undefined)\n void store.updateMetadata(record.id, { focalPoint: point })\n }\n\n return (\n <Stack gap=\"4\" className=\"mx-auto max-w-3xl p-4 pb-10 sm:p-6\">\n <AssetDetailHeader projectId={projectId} record={record} />\n\n <Panel>\n <PanelHeader>\n <Row align=\"center\" className=\"w-full\">\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\">\n {translate('cms.asset.focalPoint')}\n </Text>\n <Spacer />\n {focal ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => commitFocal(null)}>\n <RotateCcw /> {translate('cms.asset.resetToCentre')}\n </Button>\n ) : null}\n </Row>\n </PanelHeader>\n <PanelBody>\n <Stack gap=\"2\">\n <FocalPointPicker record={record} point={focal} onChange={commitFocal} onPreview={setFocal} />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('cms.asset.focalPointHint')}\n </Text>\n <FocalCropPreviews record={record} point={focal} />\n </Stack>\n </PanelBody>\n </Panel>\n\n <Panel data-testid=\"asset-detail-meta\">\n <PanelHeader>\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\">\n {translate('assets.detail.metadataHeading')}\n </Text>\n </PanelHeader>\n <PanelBody>\n <Stack gap=\"3\">\n {metaFields.map((f: FieldDef) => (\n <Field\n key={f.name}\n label={fieldLabel(f)}\n hint={localizedLabel(f.form?.description) || undefined}\n >\n <FieldControl\n id={`asset-detail-${f.name}`}\n field={f}\n value={meta[f.name]}\n readOnly={false}\n onChange={(v) => setField(f.name, v)}\n />\n </Field>\n ))}\n {metaFields.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.asset.noMetaFields', { code1: 'alt', code2: 'copyright' })}\n </Text>\n ) : null}\n {saveError ? (\n <Text size=\"sm\" className=\"text-destructive\" data-testid=\"asset-detail-save-error\">\n {saveError}\n </Text>\n ) : null}\n {metaFields.length > 0 ? (\n <Row gap=\"2\">\n <Spacer />\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!dirty || saving}\n data-testid=\"asset-detail-save\"\n >\n {saving ? translate('common.saving') : translate('common.save')}\n </Button>\n </Row>\n ) : null}\n </Stack>\n </PanelBody>\n </Panel>\n </Stack>\n )\n}\n\n/** Filename/title heading + system info (content type, dimensions, size) + actions. */\nfunction AssetDetailHeader({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const dims = record.width && record.height ? `${record.width}×${record.height}` : null\n const info = [record.contentType, dims, formatBytes(record.byteSize)].filter(Boolean).join(' · ')\n return (\n <Row gap=\"3\" align=\"start\" wrap>\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Heading level={2} size=\"h2\" className=\"truncate\" title={record.title ?? record.filename}>\n {record.title ?? record.filename}\n </Heading>\n {record.title ? (\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {record.filename}\n </Text>\n ) : null}\n <Text size=\"xs\" tone=\"subtle\">\n {info}\n </Text>\n </Stack>\n <Row gap=\"2\">\n <AssetReferencesButton projectId={projectId} record={record} />\n <AssetDeleteButton projectId={projectId} record={record} />\n </Row>\n </Row>\n )\n}\n\n/** \"Where is this asset used?\" (EC-172): the shared references panel, EC-155-backed. */\nfunction AssetReferencesButton({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const [open, setOpen] = useState(false)\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOpen(true)}\n data-testid=\"asset-detail-references\"\n >\n <Link2 /> {translate('assets.references.buttonLabel')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>\n {translate('assets.references.title', { name: record.title ?? record.filename })}\n </DialogTitle>\n <DialogDescription>{translate('assets.references.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <ReferencesPanel projectId={projectId} target={{ type: 'asset', id: record.id }} />\n ) : null}\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\n/**\n * Reference-respecting delete (EC-151, EC-202): enumerates the exact inbound\n * references — index-accurate, with deep links to the field/node — before\n * removing. Deleting a referenced asset is weak-by-default (allowed with an\n * explicit \"delete anyway\"); every reference dangles into a visible validation\n * issue rather than silently breaking. On success, returns to the list.\n */\nfunction AssetDeleteButton({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const source = useCollectionView()\n const store = useAssets()\n const navigate = useNavigate()\n const [open, setOpen] = useState(false)\n const [usages, setUsages] = useState<ReferenceEntryView[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n\n const openDialog = async () => {\n setOpen(true)\n setError(null)\n setUsages(null)\n try {\n setUsages(await loadInboundAssetReferenceViews(projectId, record.id))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const confirm = async () => {\n setBusy(true)\n setError(null)\n // The dialog has already enumerated usages (or shown there are none) and the\n // user is confirming either way — confirm unconditionally, skipping the\n // redundant inbound re-check (EC-301 perf). Weak-by-default: a still-\n // referenced asset deletes anyway, dangling into a visible issue.\n const result = await store.remove(record.id, { confirmInUse: true })\n setBusy(false)\n if (result.outcome === 'deleted' || result.outcome === 'not-found') {\n setOpen(false)\n if (source.listPath !== undefined) {\n void navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } })\n }\n } else if (result.outcome === 'blocked') {\n setError(\n translate(\n result.usageCount === 1 ? 'assets.delete.raceError.one' : 'assets.delete.raceError.other',\n { count: result.usageCount },\n ),\n )\n } else {\n setError(result.message)\n }\n }\n\n const impact = usages ? buildAssetReferenceImpact(usages) : null\n const usageCount = impact?.broken.length ?? 0\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void openDialog()}\n data-testid=\"asset-detail-delete\"\n >\n <Trash2 /> {translate('common.delete')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-h-[80vh] max-w-md overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>{translate('assets.delete.title', { filename: record.filename })}</DialogTitle>\n <DialogDescription>{translate('assets.delete.description')}</DialogDescription>\n </DialogHeader>\n {usages === null && error === null ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.delete.checking')}\n </Text>\n ) : usageCount > 0 ? (\n <Stack gap=\"2\" data-testid=\"asset-usage-warning\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate(usageCount === 1 ? 'assets.delete.inUse.one' : 'assets.delete.inUse.other', {\n count: usageCount,\n })}\n </Text>\n </Row>\n <ReferenceImpactRows\n projectId={projectId}\n broken={impact!.broken}\n onNavigate={() => setOpen(false)}\n />\n <Text size=\"xs\" tone=\"muted\">\n {translate('assets.delete.brokenWarning')}\n </Text>\n </Stack>\n ) : (\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.delete.noReferences')}\n </Text>\n )}\n {error ? (\n <Text size=\"sm\" className=\"text-destructive\" data-testid=\"asset-delete-error\">\n {error}\n </Text>\n ) : null}\n <DialogFooter>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setOpen(false)} disabled={busy}>\n {translate('common.cancel')}\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={() => void confirm()}\n disabled={busy || usages === null}\n data-testid=\"asset-delete-confirm\"\n >\n {usageCount > 0 ? translate('assets.delete.confirmAnyway') : translate('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { CmsDocument } from '@elytracms/core/cms-core';
3
+ import type { AssetRecord } from '@elytracms/core/persistence';
4
+ /**
5
+ * The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to
6
+ * `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the
7
+ * same search/filter state, `applyViewState`, and count — genuine parity with
8
+ * list mode, not a separate ad-hoc query). A media library reads naturally as
9
+ * thumbnails; the universal TABLE (list mode) is still one toggle away for
10
+ * columns/sort.
11
+ */
12
+ export declare function AssetGridView({ projectId, toolbarExtra, }: {
13
+ projectId: string;
14
+ toolbarExtra?: ReactNode;
15
+ }): import("react").JSX.Element;
16
+ /**
17
+ * One thumbnail card: image preview (focal-point aware, EC-248) + filename/title
18
+ * + key record metadata (dimensions, content type, size). Links to the asset's
19
+ * detail surface (`asset-detail.tsx`) — the same place list mode's row/"…" menu
20
+ * opens — so edit/delete/references live in exactly one surface, not duplicated
21
+ * per view mode. Pure presentational: the record is resolved once by the parent
22
+ * grid, not looked up per card.
23
+ */
24
+ export declare function AssetGridCard({ doc, record, projectId, }: {
25
+ doc: CmsDocument;
26
+ record: AssetRecord | undefined;
27
+ projectId: string;
28
+ }): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Image as ImageIcon } from 'lucide-react';
4
+ import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router';
5
+ import { applyViewState } from '@elytracms/studio/lib/cms-views';
6
+ import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
7
+ import { Code, Grid, Panel, Row, Separator, Spacer, Stack, Text } from '@elytracms/studio/ui';
8
+ import { FilterControl } from '../cms/filter-builder';
9
+ import { ViewTabs } from '../cms/view-bar';
10
+ import { useSetBreadcrumb } from '../breadcrumb-context';
11
+ import { useCollectionView } from '../collection-view/collection-view-context';
12
+ import { AssetThumb } from './asset-thumb';
13
+ import { formatBytes } from './asset-metadata';
14
+ import { useAssets } from './assets-store';
15
+ /**
16
+ * The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to
17
+ * `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the
18
+ * same search/filter state, `applyViewState`, and count — genuine parity with
19
+ * list mode, not a separate ad-hoc query). A media library reads naturally as
20
+ * thumbnails; the universal TABLE (list mode) is still one toggle away for
21
+ * columns/sort.
22
+ */
23
+ export function AssetGridView({ projectId, toolbarExtra, }) {
24
+ const source = useCollectionView();
25
+ const collection = source.schema;
26
+ const collectionId = source.collectionId;
27
+ useSetBreadcrumb([{ label: localizedLabel(collection.form?.label, collectionId) }]);
28
+ const viewState = source.viewState();
29
+ const docs = source.documents();
30
+ const { docs: visibleDocs } = useMemo(() => applyViewState(collection, docs, source.localeConfig, viewState), [collection, docs, source.localeConfig, viewState]);
31
+ // Resolved once per render (O(n), not per-card `.find()`) — the card stays a
32
+ // pure presentational component (record passed in, no store lookup of its own).
33
+ const store = useAssets();
34
+ const recordsById = useMemo(() => new Map(store.assets.map((record) => [record.id, record])), [store.assets]);
35
+ const pluralLabel = localizedLabel(collection.form?.label, `${collectionId}s`);
36
+ const singularLabel = localizedLabel(collection.form?.labelSingular, collectionId);
37
+ const countLabel = visibleDocs.length === docs.length
38
+ ? translate('cms.list.count', {
39
+ count: docs.length,
40
+ noun: docs.length === 1 ? singularLabel : pluralLabel,
41
+ })
42
+ : translate('cms.list.countFiltered', {
43
+ visible: visibleDocs.length,
44
+ total: docs.length,
45
+ noun: pluralLabel,
46
+ });
47
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: _jsxs(Row, { gap: "2", wrap: true, children: [_jsx(ViewTabs, {}), _jsx(FilterControl, { collection: collection }), _jsx(Separator, { orientation: "vertical", className: "h-5 self-center" }), _jsx(Text, { as: "span", size: "xs", tone: "muted", className: "whitespace-nowrap tabular-nums", "data-testid": "asset-grid-count", children: countLabel }), _jsx(Spacer, {}), toolbarExtra] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: docs.length === 0 ? (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsx(Text, { tone: "muted", children: translate('cms.list.emptyNone', { noun: pluralLabel.toLowerCase() }) }) })) : visibleDocs.length === 0 ? (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsx(Text, { tone: "muted", "data-testid": "empty-filtered", children: translate('cms.list.emptyFiltered') }) })) : (_jsx(Grid, { minItemWidth: "220px", gap: "3", "data-testid": "asset-grid", children: visibleDocs.map((doc) => (_jsx(AssetGridCard, { doc: doc, record: recordsById.get(doc.id), projectId: projectId }, doc.id))) })) })] }));
48
+ }
49
+ /**
50
+ * One thumbnail card: image preview (focal-point aware, EC-248) + filename/title
51
+ * + key record metadata (dimensions, content type, size). Links to the asset's
52
+ * detail surface (`asset-detail.tsx`) — the same place list mode's row/"…" menu
53
+ * opens — so edit/delete/references live in exactly one surface, not duplicated
54
+ * per view mode. Pure presentational: the record is resolved once by the parent
55
+ * grid, not looked up per card.
56
+ */
57
+ export function AssetGridCard({ doc, record, projectId, }) {
58
+ const source = useCollectionView();
59
+ const title = record?.title ?? record?.filename ?? doc.id;
60
+ const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null;
61
+ const kind = record?.contentType?.split('/')[1]?.toUpperCase();
62
+ const splat = source.detailPath?.(doc.id);
63
+ const card = (_jsxs(Panel, { className: "overflow-hidden", "data-testid": "asset-grid-card", children: [_jsx(AssetThumb, { record: record, alt: title, className: "aspect-video w-full rounded-none border-0" }), _jsxs(Stack, { gap: "1", className: "p-3", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate", title: record?.filename ?? title, children: title }), record === undefined ? (_jsxs(Row, { gap: "1", align: "center", children: [_jsx(ImageIcon, { className: "size-3.5 text-muted-foreground" }), _jsx(Text, { size: "xs", tone: "subtle", children: translate('assets.loading') })] })) : (_jsxs(Row, { gap: "1", wrap: true, children: [kind ? _jsx(Code, { tone: "muted", children: kind }) : null, dims ? _jsx(Code, { tone: "muted", children: dims }) : null, _jsx(Code, { tone: "muted", children: formatBytes(record.byteSize) })] }))] })] }));
64
+ if (splat === undefined)
65
+ return card;
66
+ return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: splat }, className: "block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40", children: card }));
67
+ }
68
+ //# sourceMappingURL=asset-grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-grid.js","sourceRoot":"","sources":["../../../src/components/assets/asset-grid.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AAGjD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,SAAS,EACT,YAAY,GAIb;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;IAChC,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAA;IACxC,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,CAAA;IAEnF,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,EAAE,CAAA;IAC/B,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO,CACnC,GAAG,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,EACtE,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CACnD,CAAA;IAED,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,KAAK,GAAG,SAAS,EAAE,CAAA;IACzB,MAAM,WAAW,GAAG,OAAO,CACzB,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAChE,CAAC,KAAK,CAAC,MAAM,CAAC,CACf,CAAA;IAED,MAAM,WAAW,GAAG,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,YAAY,GAAG,CAAC,CAAA;IAC9E,MAAM,aAAa,GAAG,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,YAAY,CAAC,CAAA;IAClF,MAAM,UAAU,GACd,WAAW,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;QAChC,CAAC,CAAC,SAAS,CAAC,gBAAgB,EAAE;YAC1B,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,IAAI,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW;SACtD,CAAC;QACJ,CAAC,CAAC,SAAS,CAAC,wBAAwB,EAAE;YAClC,OAAO,EAAE,WAAW,CAAC,MAAM;YAC3B,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,IAAI,EAAE,WAAW;SAClB,CAAC,CAAA;IAER,OAAO,CACL,eAAK,SAAS,EAAC,8BAA8B,aAC3C,cAAK,SAAS,EAAC,mDAAmD,YAChE,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,mBACf,KAAC,QAAQ,KAAG,EACZ,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,EACzC,KAAC,SAAS,IAAC,WAAW,EAAC,UAAU,EAAC,SAAS,EAAC,iBAAiB,GAAG,EAChE,KAAC,IAAI,IACH,EAAE,EAAC,MAAM,EACT,IAAI,EAAC,IAAI,EACT,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,gCAAgC,iBAC9B,kBAAkB,YAE7B,UAAU,GACN,EACP,KAAC,MAAM,KAAG,EACT,YAAY,IACT,GACF,EAEN,cAAK,SAAS,EAAC,yCAAyC,YACrD,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACnB,cAAK,SAAS,EAAC,oCAAoC,YACjD,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,SAAS,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,WAAW,EAAE,EAAE,CAAC,GAAQ,GAC5F,CACP,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAC7B,cAAK,SAAS,EAAC,oCAAoC,YACjD,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,iBAAa,gBAAgB,YAC5C,SAAS,CAAC,wBAAwB,CAAC,GAC/B,GACH,CACP,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,YAAY,EAAC,OAAO,EAAC,GAAG,EAAC,GAAG,iBAAa,YAAY,YACxD,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACxB,KAAC,aAAa,IAEZ,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAC/B,SAAS,EAAE,SAAS,IAHf,GAAG,CAAC,EAAE,CAIX,CACH,CAAC,GACG,CACR,GACG,IACF,CACP,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,GAAG,EACH,MAAM,EACN,SAAS,GAKV;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,GAAG,CAAC,EAAE,CAAA;IACzD,MAAM,IAAI,GAAG,MAAM,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACxF,MAAM,IAAI,GAAG,MAAM,EAAE,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAA;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAEzC,MAAM,IAAI,GAAG,CACX,MAAC,KAAK,IAAC,SAAS,EAAC,iBAAiB,iBAAa,iBAAiB,aAC9D,KAAC,UAAU,IAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAC,2CAA2C,GAAG,EAChG,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,aAC5B,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,EAAC,SAAS,EAAC,UAAU,EAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,IAAI,KAAK,YAClF,KAAK,GACD,EACN,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CACtB,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,SAAS,IAAC,SAAS,EAAC,gCAAgC,GAAG,EACxD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,YAC1B,SAAS,CAAC,gBAAgB,CAAC,GACvB,IACH,CACP,CAAC,CAAC,CAAC,CACF,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,mBACd,IAAI,CAAC,CAAC,CAAC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,IAAI,GAAQ,CAAC,CAAC,CAAC,IAAI,EAC9C,IAAI,CAAC,CAAC,CAAC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,IAAI,GAAQ,CAAC,CAAC,CAAC,IAAI,EAC/C,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAQ,IACpD,CACP,IACK,IACF,CACT,CAAA;IAED,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,CACL,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EACpC,SAAS,EAAC,6FAA6F,YAEtG,IAAI,GACA,CACR,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport { useMemo } from 'react'\nimport { Image as ImageIcon } from 'lucide-react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport { applyViewState } from '@elytracms/studio/lib/cms-views'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Code, Grid, Panel, Row, Separator, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { FilterControl } from '../cms/filter-builder'\nimport { ViewTabs } from '../cms/view-bar'\nimport { useSetBreadcrumb } from '../breadcrumb-context'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { AssetThumb } from './asset-thumb'\nimport { formatBytes } from './asset-metadata'\nimport { useAssets } from './assets-store'\n\n/**\n * The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to\n * `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the\n * same search/filter state, `applyViewState`, and count — genuine parity with\n * list mode, not a separate ad-hoc query). A media library reads naturally as\n * thumbnails; the universal TABLE (list mode) is still one toggle away for\n * columns/sort.\n */\nexport function AssetGridView({\n projectId,\n toolbarExtra,\n}: {\n projectId: string\n toolbarExtra?: ReactNode\n}) {\n const source = useCollectionView()\n const collection = source.schema\n const collectionId = source.collectionId\n useSetBreadcrumb([{ label: localizedLabel(collection.form?.label, collectionId) }])\n\n const viewState = source.viewState()\n const docs = source.documents()\n const { docs: visibleDocs } = useMemo(\n () => applyViewState(collection, docs, source.localeConfig, viewState),\n [collection, docs, source.localeConfig, viewState],\n )\n\n // Resolved once per render (O(n), not per-card `.find()`) — the card stays a\n // pure presentational component (record passed in, no store lookup of its own).\n const store = useAssets()\n const recordsById = useMemo(\n () => new Map(store.assets.map((record) => [record.id, record])),\n [store.assets],\n )\n\n const pluralLabel = localizedLabel(collection.form?.label, `${collectionId}s`)\n const singularLabel = localizedLabel(collection.form?.labelSingular, collectionId)\n const countLabel =\n visibleDocs.length === docs.length\n ? translate('cms.list.count', {\n count: docs.length,\n noun: docs.length === 1 ? singularLabel : pluralLabel,\n })\n : translate('cms.list.countFiltered', {\n visible: visibleDocs.length,\n total: docs.length,\n noun: pluralLabel,\n })\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border px-4 py-3 sm:px-6\">\n <Row gap=\"2\" wrap>\n <ViewTabs />\n <FilterControl collection={collection} />\n <Separator orientation=\"vertical\" className=\"h-5 self-center\" />\n <Text\n as=\"span\"\n size=\"xs\"\n tone=\"muted\"\n className=\"whitespace-nowrap tabular-nums\"\n data-testid=\"asset-grid-count\"\n >\n {countLabel}\n </Text>\n <Spacer />\n {toolbarExtra}\n </Row>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n {docs.length === 0 ? (\n <div className=\"grid h-full place-items-center p-8\">\n <Text tone=\"muted\">{translate('cms.list.emptyNone', { noun: pluralLabel.toLowerCase() })}</Text>\n </div>\n ) : visibleDocs.length === 0 ? (\n <div className=\"grid h-full place-items-center p-8\">\n <Text tone=\"muted\" data-testid=\"empty-filtered\">\n {translate('cms.list.emptyFiltered')}\n </Text>\n </div>\n ) : (\n <Grid minItemWidth=\"220px\" gap=\"3\" data-testid=\"asset-grid\">\n {visibleDocs.map((doc) => (\n <AssetGridCard\n key={doc.id}\n doc={doc}\n record={recordsById.get(doc.id)}\n projectId={projectId}\n />\n ))}\n </Grid>\n )}\n </div>\n </div>\n )\n}\n\n/**\n * One thumbnail card: image preview (focal-point aware, EC-248) + filename/title\n * + key record metadata (dimensions, content type, size). Links to the asset's\n * detail surface (`asset-detail.tsx`) — the same place list mode's row/\"…\" menu\n * opens — so edit/delete/references live in exactly one surface, not duplicated\n * per view mode. Pure presentational: the record is resolved once by the parent\n * grid, not looked up per card.\n */\nexport function AssetGridCard({\n doc,\n record,\n projectId,\n}: {\n doc: CmsDocument\n record: AssetRecord | undefined\n projectId: string\n}) {\n const source = useCollectionView()\n const title = record?.title ?? record?.filename ?? doc.id\n const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null\n const kind = record?.contentType?.split('/')[1]?.toUpperCase()\n const splat = source.detailPath?.(doc.id)\n\n const card = (\n <Panel className=\"overflow-hidden\" data-testid=\"asset-grid-card\">\n <AssetThumb record={record} alt={title} className=\"aspect-video w-full rounded-none border-0\" />\n <Stack gap=\"1\" className=\"p-3\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\" title={record?.filename ?? title}>\n {title}\n </Text>\n {record === undefined ? (\n <Row gap=\"1\" align=\"center\">\n <ImageIcon className=\"size-3.5 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.loading')}\n </Text>\n </Row>\n ) : (\n <Row gap=\"1\" wrap>\n {kind ? <Code tone=\"muted\">{kind}</Code> : null}\n {dims ? <Code tone=\"muted\">{dims}</Code> : null}\n <Code tone=\"muted\">{formatBytes(record.byteSize)}</Code>\n </Row>\n )}\n </Stack>\n </Panel>\n )\n\n if (splat === undefined) return card\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: splat }}\n className=\"block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n >\n {card}\n </Link>\n )\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core';
1
2
  import type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/core/persistence';
2
3
  /**
3
4
  * Asset metadata editing (EC-152). A patch is applied through the
@@ -6,6 +7,24 @@ import type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/cor
6
7
  * (validate → authorize → mutate → event). Pure orchestration; injectable
7
8
  * for tests.
8
9
  */
10
+ /**
11
+ * System columns the upload pipeline owns — never editable metadata, so they're
12
+ * excluded from the asset collection's schema-driven forms: the image field's
13
+ * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail
14
+ * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither
15
+ * drifts from the other. Everything else a project declares on the `kind:'asset'`
16
+ * collection round-trips through the record's open `meta` bag.
17
+ */
18
+ export declare const SYSTEM_ASSET_FIELDS: ReadonlySet<string>;
19
+ /**
20
+ * The asset collection's EDITABLE metadata fields — declared fields minus the
21
+ * system columns above and anything explicitly `readOnly` (EC-301/EC-319).
22
+ */
23
+ export declare function assetMetaFieldsOf(collection: CollectionDef): FieldDef[];
24
+ /** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */
25
+ export declare function pruneMeta(meta: Record<string, unknown>): Record<string, unknown>;
26
+ /** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */
27
+ export declare function formatBytes(byteSize: number): string;
9
28
  export interface AssetMetadataPatch {
10
29
  /**
11
30
  * New alt text. A present key overwrites; a trimmed-empty string clears
@@ -1,3 +1,47 @@
1
+ /**
2
+ * Asset metadata editing (EC-152). A patch is applied through the
3
+ * {@link AssetRepository} interface — in the studio that is the EC-141
4
+ * operations facade, so every edit runs the `assets.upsert` command
5
+ * (validate → authorize → mutate → event). Pure orchestration; injectable
6
+ * for tests.
7
+ */
8
+ /**
9
+ * System columns the upload pipeline owns — never editable metadata, so they're
10
+ * excluded from the asset collection's schema-driven forms: the image field's
11
+ * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail
12
+ * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither
13
+ * drifts from the other. Everything else a project declares on the `kind:'asset'`
14
+ * collection round-trips through the record's open `meta` bag.
15
+ */
16
+ export const SYSTEM_ASSET_FIELDS = new Set([
17
+ 'filename',
18
+ 'url',
19
+ 'contentType',
20
+ 'byteSize',
21
+ 'width',
22
+ 'height',
23
+ 'storageKey',
24
+ 'title',
25
+ ]);
26
+ /**
27
+ * The asset collection's EDITABLE metadata fields — declared fields minus the
28
+ * system columns above and anything explicitly `readOnly` (EC-301/EC-319).
29
+ */
30
+ export function assetMetaFieldsOf(collection) {
31
+ return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly);
32
+ }
33
+ /** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */
34
+ export function pruneMeta(meta) {
35
+ return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
36
+ }
37
+ /** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */
38
+ export function formatBytes(byteSize) {
39
+ if (byteSize < 1024)
40
+ return `${byteSize} B`;
41
+ if (byteSize < 1024 * 1024)
42
+ return `${(byteSize / 1024).toFixed(1)} KB`;
43
+ return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`;
44
+ }
1
45
  /** Merge one patch field: present+empty clears, present+value sets, absent keeps. */
2
46
  function mergeField(patchValue, patchHasKey, currentValue) {
3
47
  if (!patchHasKey)
@@ -1 +1 @@
1
- {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\nexport interface AssetMetadataPatch {\n /**\n * New alt text. A present key overwrites; a trimmed-empty string clears\n * the field. An absent key leaves the current value untouched.\n */\n alt?: string\n /** New display title; same present/empty/absent semantics as `alt`. */\n title?: string\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). A present `{x,y}` sets it, `null` clears it, an\n * absent key leaves the current value untouched.\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * Schema-driven asset metadata bag (EC-301): alt, copyright, credit, … keyed by\n * the asset collection's `fields`. A present object **replaces** the whole bag\n * (the image options dialog holds the complete edited meta); an absent key\n * leaves the current bag untouched.\n */\n meta?: Record<string, unknown>\n}\n\n/** Merge one patch field: present+empty clears, present+value sets, absent keeps. */\nfunction mergeField(\n patchValue: string | undefined,\n patchHasKey: boolean,\n currentValue: string | undefined,\n): string | undefined {\n if (!patchHasKey) return currentValue\n const trimmed = patchValue?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The full upsert input that re-states `current` with the patch applied. */\nexport function assetMetadataUpsertInput(\n current: AssetRecord,\n patch: AssetMetadataPatch,\n): NewAssetInput {\n const alt = mergeField(patch.alt, 'alt' in patch, current.alt)\n const title = mergeField(patch.title, 'title' in patch, current.title)\n // present `{x,y}` sets, present `null` clears, absent keeps the current point.\n const focalPoint = 'focalPoint' in patch ? (patch.focalPoint ?? undefined) : current.focalPoint\n // present object replaces the whole bag; absent keeps the current one (EC-301).\n const meta = 'meta' in patch ? patch.meta : current.meta\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(alt !== undefined ? { alt } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(focalPoint !== undefined ? { focalPoint } : {}),\n ...(meta !== undefined ? { meta } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Load the current record, apply the patch, and persist it via the\n * repository (the operations facade in the studio). Failures come back as a\n * structured result — never a throw the UI has to guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const current = await deps.assets.getAsset(projectId, assetId)\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataUpsertInput(current, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
1
+ {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport interface AssetMetadataPatch {\n /**\n * New alt text. A present key overwrites; a trimmed-empty string clears\n * the field. An absent key leaves the current value untouched.\n */\n alt?: string\n /** New display title; same present/empty/absent semantics as `alt`. */\n title?: string\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). A present `{x,y}` sets it, `null` clears it, an\n * absent key leaves the current value untouched.\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * Schema-driven asset metadata bag (EC-301): alt, copyright, credit, … keyed by\n * the asset collection's `fields`. A present object **replaces** the whole bag\n * (the image options dialog holds the complete edited meta); an absent key\n * leaves the current bag untouched.\n */\n meta?: Record<string, unknown>\n}\n\n/** Merge one patch field: present+empty clears, present+value sets, absent keeps. */\nfunction mergeField(\n patchValue: string | undefined,\n patchHasKey: boolean,\n currentValue: string | undefined,\n): string | undefined {\n if (!patchHasKey) return currentValue\n const trimmed = patchValue?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The full upsert input that re-states `current` with the patch applied. */\nexport function assetMetadataUpsertInput(\n current: AssetRecord,\n patch: AssetMetadataPatch,\n): NewAssetInput {\n const alt = mergeField(patch.alt, 'alt' in patch, current.alt)\n const title = mergeField(patch.title, 'title' in patch, current.title)\n // present `{x,y}` sets, present `null` clears, absent keeps the current point.\n const focalPoint = 'focalPoint' in patch ? (patch.focalPoint ?? undefined) : current.focalPoint\n // present object replaces the whole bag; absent keeps the current one (EC-301).\n const meta = 'meta' in patch ? patch.meta : current.meta\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(alt !== undefined ? { alt } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(focalPoint !== undefined ? { focalPoint } : {}),\n ...(meta !== undefined ? { meta } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Load the current record, apply the patch, and persist it via the\n * repository (the operations facade in the studio). Failures come back as a\n * structured result — never a throw the UI has to guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const current = await deps.assets.getAsset(projectId, assetId)\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataUpsertInput(current, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
+ import type { AssetRecord } from '@elytracms/core/persistence';
3
+ import type { CollectionViewCapabilities, CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ /**
5
+ * The capability profile of the assets rail (EC-319). Hoisted to a module-level
6
+ * constant (no prop/state dependency) so it's both the value the hook returns
7
+ * AND independently unit-testable.
8
+ *
9
+ * `create` is off — a new asset comes from an upload, never a blank draft.
10
+ * `remove` is off DELIBERATELY, even though the store can safely delete one
11
+ * (EC-202): the universal table's generic row/bulk delete runs through
12
+ * `ReferenceConfirmDialog`, which enumerates inbound references as
13
+ * `{ type: 'document' }` targets — the wrong index shape for an asset (indexed
14
+ * as `{ type: 'asset' }`, see `loadInboundAssetReferenceViews`). Wiring
15
+ * `remove: true` here would silently show "not referenced" for an asset that
16
+ * IS referenced. Deletion instead stays on the asset detail surface
17
+ * (`asset-detail.tsx`), which keeps calling the asset-specific reference check.
18
+ * Nothing else applies to assets: no publish/versions/locales/inline-edit/bulk/
19
+ * live-URL/hierarchy/import-export.
20
+ */
21
+ export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
22
+ /**
23
+ * Project a stored asset record into the lens's `values` shape (EC-319), reading
24
+ * only the schema's DECLARED fields — the same "schema is a lens" projection the
25
+ * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)
26
+ * reads straight off the record; everything else — the built-in `alt`, a
27
+ * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips
28
+ * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO
29
+ * has a legacy record column: the meta value wins, falling back to the column
30
+ * (matches delivery's `meta.alt ?? record.alt` read and the image field's
31
+ * options dialog).
32
+ */
33
+ export declare function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument;
34
+ /**
35
+ * The inverse of {@link assetToDocument}: the schema-driven meta bag a save
36
+ * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),
37
+ * keyed exactly as the record stores it. System columns are upload-managed and
38
+ * never written here.
39
+ */
40
+ export declare function documentToAssetMetaPatch(collection: CollectionDef, doc: CmsDocument): Record<string, unknown>;
41
+ /**
42
+ * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the
43
+ * portable list surface over the existing asset library store instead of a
44
+ * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection
45
+ * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`
46
+ * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),
47
+ * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields
48
+ * (and any EC-311 override) drive the list's columns/filters exactly as they
49
+ * drive the image field's options dialog.
50
+ *
51
+ * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already
52
+ * ambient at the project level (EC-195), so callers rarely mount their own.
53
+ */
54
+ export declare function useAssetsCollectionViewSource(projectId: string, schema: CollectionDef): CollectionViewSource;