@elytracms/studio 0.0.57 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/ai-chat-panel.js +44 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.d.ts +1 -1
- package/dist/components/ai/chat-ui.js +11 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +30 -7
- package/dist/components/ai/proposal-card.js +81 -21
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/builder/blocks-model.js +12 -7
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +59 -0
- package/dist/components/builder/canvas-dnd.js +207 -0
- package/dist/components/builder/canvas-dnd.js.map +1 -0
- package/dist/components/builder/canvas-dom.d.ts +11 -0
- package/dist/components/builder/canvas-dom.js +17 -0
- package/dist/components/builder/canvas-dom.js.map +1 -0
- package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
- package/dist/components/builder/canvas-inline-editing.js +242 -0
- package/dist/components/builder/canvas-inline-editing.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +83 -0
- package/dist/components/builder/canvas-render-context.js +196 -0
- package/dist/components/builder/canvas-render-context.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +4 -1
- package/dist/components/builder/canvas.js +32 -567
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +14 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +5 -2
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +12 -0
- package/dist/components/builder/node-factory.js +18 -1
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +22 -0
- package/dist/components/cms/asset-field.js +300 -0
- package/dist/components/cms/asset-field.js.map +1 -0
- package/dist/components/cms/detail-editor.js +12 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +12 -1
- package/dist/components/cms/document-diff-view.js +119 -42
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +43 -0
- package/dist/components/cms/field-control-lib.js +98 -0
- package/dist/components/cms/field-control-lib.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +7 -35
- package/dist/components/cms/field-control.js +13 -556
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +29 -0
- package/dist/components/cms/object-array-field.js +205 -0
- package/dist/components/cms/object-array-field.js.map +1 -0
- package/dist/components/cms/rich-text-preview.d.ts +15 -0
- package/dist/components/cms/rich-text-preview.js +81 -0
- package/dist/components/cms/rich-text-preview.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +26 -0
- package/dist/components/cms/version-compare-text.js +60 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/project-session.d.ts +19 -0
- package/dist/components/project-session.js +29 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +6 -4
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/lib/ai/apply-content.js +52 -10
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +19 -3
- package/dist/lib/ai/chat-context.js +33 -4
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -3
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +60 -5
- package/dist/lib/ai/fake-provider.js +74 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +27 -0
- package/dist/lib/ai/rich-text-input.js +25 -0
- package/dist/lib/ai/rich-text-input.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +12 -1
- package/dist/lib/ai/shape.js +46 -20
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +88 -13
- package/dist/lib/ai/tools.js +253 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +20 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +68 -0
- package/dist/lib/ai/update-fields.js +296 -0
- package/dist/lib/ai/update-fields.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +53 -0
- package/dist/lib/ai-current-document.js +54 -0
- package/dist/lib/ai-current-document.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/data/collection-content.d.ts +12 -7
- package/dist/lib/data/collection-content.js +6 -4
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +10 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +4 -1
- package/dist/lib/operations/session-hydration.js +4 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/index.js +13 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +17 -22
- package/dist/lib/workspace/workspace-source.js +1 -49
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangle, ImagePlus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, readAssetId, } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Card, Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Field, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
+
import { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection.js';
|
|
9
|
+
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
10
|
+
import { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb.js';
|
|
11
|
+
import { newlyAddedAssetIds } from '../assets/focal-point.js';
|
|
12
|
+
import { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata.js';
|
|
13
|
+
import { fieldLabel } from './cms-labels.js';
|
|
14
|
+
import { useCollectionView } from '../collection-view/collection-view-context.js';
|
|
15
|
+
import { useFieldDensity } from './field-density.js';
|
|
16
|
+
import { AssetPicker } from './asset-picker.js';
|
|
17
|
+
import { FieldControl } from './field-control.js';
|
|
18
|
+
import { ObjectItemForm } from './object-array-field.js';
|
|
19
|
+
/**
|
|
20
|
+
* The asset-field family (EC-152 + EC-248 + EC-301) — split from
|
|
21
|
+
* `field-control.tsx` (EC-330): the {@link AssetField} control plus its
|
|
22
|
+
* preview/gallery/options sub-components. Per-use sub-fields and the options
|
|
23
|
+
* dialog's metadata form render through the shared object item form /
|
|
24
|
+
* `FieldControl`, so asset metadata stays schema-driven.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The asset field (EC-152 + EC-248). Beyond picking from the library it now shows
|
|
28
|
+
* a focal-point-aware preview, uploads directly from the field (reusing the EC-152
|
|
29
|
+
* upload path — no parallel uploader), and edits the selected image's alt + focal
|
|
30
|
+
* point inline (persisted on the asset document, where delivery reads them, EC-230).
|
|
31
|
+
* With no `<AssetsProvider>` (fixtures/playground) it degrades to the labelled-badge
|
|
32
|
+
* picker — no preview/upload — so it stays usable without a real library.
|
|
33
|
+
*/
|
|
34
|
+
export function AssetField({ field, value, onChange, readOnly, }) {
|
|
35
|
+
const source = useCollectionView();
|
|
36
|
+
const assets = useOptionalAssets();
|
|
37
|
+
const density = useFieldDensity();
|
|
38
|
+
const compact = density === 'compact';
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
const [editing, setEditing] = useState(false);
|
|
41
|
+
const [dragging, setDragging] = useState(false);
|
|
42
|
+
// Names of files a drop/pick rejected against the field's `accept` (EC-301) — shown
|
|
43
|
+
// as a transient inline issue, cleared on the next valid upload. Never a silent drop.
|
|
44
|
+
const [rejected, setRejected] = useState([]);
|
|
45
|
+
// Asset ids known when an upload-from-field started; the effect below diffs the
|
|
46
|
+
// library against it to learn the new id (the `upload()` API returns nothing).
|
|
47
|
+
const pendingRef = useRef(null);
|
|
48
|
+
const assetCollectionId = source.assetCollectionId ?? 'asset';
|
|
49
|
+
const many = field.multiple;
|
|
50
|
+
const ids = many ? assetIdsOf(value) : [];
|
|
51
|
+
const single = !many ? singleAssetIdOf(value) : null;
|
|
52
|
+
// EC-301 per-use sub-fields: with `field.fields` the value stores asset ENVELOPES
|
|
53
|
+
// carrying the sub-field siblings (`{ type:'asset', id, caption }`); without them it
|
|
54
|
+
// stays bare ids (self-typed on save). Picks/edits preserve a placement's sub-fields.
|
|
55
|
+
const subFields = field.fields ?? [];
|
|
56
|
+
const hasSub = subFields.length > 0;
|
|
57
|
+
// The raw `many` entries (envelopes or bare ids), index-aligned with `ids`, so each
|
|
58
|
+
// gallery item can read/write its own sub-fields.
|
|
59
|
+
const entries = many && Array.isArray(value) ? value : [];
|
|
60
|
+
const libraryIds = useMemo(() => (assets?.status === 'ready' ? assets.assets.map((a) => a.id) : []), [assets]);
|
|
61
|
+
// Auto-select an asset uploaded FROM this field once it lands in the library.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!pendingRef.current)
|
|
64
|
+
return;
|
|
65
|
+
const added = newlyAddedAssetIds(pendingRef.current, libraryIds);
|
|
66
|
+
if (added.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
pendingRef.current = null;
|
|
69
|
+
if (many) {
|
|
70
|
+
const current = Array.isArray(value) ? value : [];
|
|
71
|
+
onChange([...current, ...added.map((id) => (hasSub ? assetNode(id) : id))]);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
onChange(hasSub ? assetNode(added[0], assetSubfieldsOf(value)) : applyAssetSelection(value, added[0], false));
|
|
75
|
+
}
|
|
76
|
+
// Reacts to the library set growing (an upload landing), not to unrelated
|
|
77
|
+
// value edits — `value`/`onChange` are read from the closure on purpose.
|
|
78
|
+
}, [libraryIds, many, hasSub, onChange, value]);
|
|
79
|
+
function uploadFiles(files) {
|
|
80
|
+
if (!assets || files.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
// EC-301: a drag-drop bypasses the file dialog's `accept` filter — re-check each
|
|
83
|
+
// file against the field's `accept` and surface a visible issue for the rejects
|
|
84
|
+
// instead of silently uploading the wrong kind.
|
|
85
|
+
const ok = [];
|
|
86
|
+
const bad = [];
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
if (assetMatchesAccept({ contentType: file.type, filename: file.name }, field.accept))
|
|
89
|
+
ok.push(file);
|
|
90
|
+
else
|
|
91
|
+
bad.push(file.name);
|
|
92
|
+
}
|
|
93
|
+
setRejected(bad);
|
|
94
|
+
if (ok.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
// Remember the library set so the effect above can learn the new id and
|
|
97
|
+
// auto-select it once the upload lands (the upload API returns nothing).
|
|
98
|
+
pendingRef.current = new Set(libraryIds);
|
|
99
|
+
assets.upload(ok);
|
|
100
|
+
}
|
|
101
|
+
// Apply a picked asset id, preserving a placement's per-use sub-fields (EC-301).
|
|
102
|
+
// Without sub-fields it stays the plain id-list behaviour (self-typed on save).
|
|
103
|
+
function applyPicked(picked) {
|
|
104
|
+
if (!hasSub) {
|
|
105
|
+
onChange(applyAssetSelection(value, picked, field.multiple));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (many) {
|
|
109
|
+
if (picked === null || entries.some((e) => readAssetId(e) === picked))
|
|
110
|
+
return;
|
|
111
|
+
onChange([...entries, assetNode(picked)]);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
onChange(picked ? assetNode(picked, assetSubfieldsOf(value)) : undefined);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const canUpload = !readOnly && assets !== null;
|
|
118
|
+
// Drag-to-upload onto the field (EC-301): a depth counter keeps the overlay
|
|
119
|
+
// steady as the pointer crosses child element boundaries. `dragenter`/`dragleave`
|
|
120
|
+
// fire per descendant, so toggling on a bare `dragleave` blinks constantly; we
|
|
121
|
+
// only hide the overlay when the counter returns to zero (the pointer has truly
|
|
122
|
+
// left the field). The overlay is pointer-events-none, so it never fires its own
|
|
123
|
+
// enter/leave to confuse the count.
|
|
124
|
+
const dragDepth = useRef(0);
|
|
125
|
+
function onDragEnter(e) {
|
|
126
|
+
if (!canUpload)
|
|
127
|
+
return;
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
dragDepth.current += 1;
|
|
130
|
+
if (!dragging)
|
|
131
|
+
setDragging(true);
|
|
132
|
+
}
|
|
133
|
+
function onDragOver(e) {
|
|
134
|
+
if (!canUpload)
|
|
135
|
+
return;
|
|
136
|
+
e.preventDefault(); // required so a drop is allowed
|
|
137
|
+
}
|
|
138
|
+
function onDragLeave() {
|
|
139
|
+
if (!canUpload)
|
|
140
|
+
return;
|
|
141
|
+
dragDepth.current -= 1;
|
|
142
|
+
if (dragDepth.current <= 0) {
|
|
143
|
+
dragDepth.current = 0;
|
|
144
|
+
setDragging(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function onDrop(e) {
|
|
148
|
+
if (!canUpload)
|
|
149
|
+
return;
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
dragDepth.current = 0;
|
|
152
|
+
setDragging(false);
|
|
153
|
+
uploadFiles(Array.from(e.dataTransfer.files));
|
|
154
|
+
}
|
|
155
|
+
// While the picker is open the field is behind the modal, so any drop is happening
|
|
156
|
+
// INSIDE the dialog — detach the field's drag handlers entirely so a drop there can
|
|
157
|
+
// only be handled by the picker, never double-counted by the field too (EC-301). This
|
|
158
|
+
// is the belt to the picker's stopPropagation braces: the field cannot react at all.
|
|
159
|
+
const dragProps = canUpload && !open ? { onDragEnter, onDragOver, onDragLeave, onDrop } : {};
|
|
160
|
+
const selectButton = !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(ImagePlus, {}), " ", translate('common.select')] })) : null;
|
|
161
|
+
return (_jsxs(Stack, { gap: "2", className: cn('relative rounded-lg transition-shadow', dragging && 'ring-2 ring-primary ring-offset-2 ring-offset-background'), ...dragProps, children: [many && hasSub ? (_jsxs(Stack, { gap: "2", children: [entries.length > 0 ? (_jsx(Stack, { gap: "2", children: entries.map((entry, i) => {
|
|
162
|
+
const aid = readAssetId(entry);
|
|
163
|
+
if (!aid)
|
|
164
|
+
return null;
|
|
165
|
+
return (_jsx(AssetGalleryItem, { assetId: aid, collectionId: assetCollectionId, fields: subFields, value: assetSubfieldsOf(entry), readOnly: readOnly, idPrefix: `asset-${i}`, onChangeSubs: (next) => {
|
|
166
|
+
const nextEntries = entries.slice();
|
|
167
|
+
nextEntries[i] = assetNode(aid, next);
|
|
168
|
+
onChange(nextEntries);
|
|
169
|
+
}, onRemove: readOnly ? undefined : () => onChange(entries.filter((_, j) => j !== i)) }, `${aid}-${i}`));
|
|
170
|
+
}) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : many ? (_jsxs(Stack, { gap: "2", children: [ids.length > 0 ? (_jsx("div", { className: cn('grid gap-2', compact ? 'grid-cols-2' : 'grid-cols-3 sm:grid-cols-4'), children: ids.map((aid, i) => (_jsx(AssetChip, { assetId: aid, collectionId: assetCollectionId, onRemove: readOnly ? undefined : () => onChange(ids.filter((x) => x !== aid)) }, `${aid}-${i}`))) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : single && hasSub ? (_jsx(Card, { className: "border-l-2 border-l-border p-3", children: _jsxs(Stack, { gap: "3", children: [_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) }), _jsx("div", { className: "border-t border-border/60 pt-3", children: _jsx(ObjectItemForm, { fields: subFields, value: assetSubfieldsOf(value), readOnly: readOnly, idPrefix: "asset-sub", onChange: (next) => onChange(assetNode(single, next)) }) })] }) })) : single ? (_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) })) : (_jsxs(Stack, { gap: "2", children: [_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) }), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })), !many && single && assets ? (_jsx(AssetOptionsDialog, { assetId: single, readOnly: readOnly, open: editing, onOpenChange: setEditing })) : null, _jsx(AssetPicker, { value: single, open: open, onOpenChange: setOpen, accept: assetAcceptAttr(field.accept), onChange: applyPicked }), rejected.length > 0 ? (_jsxs(Row, { gap: "2", align: "center", children: [_jsx(AlertTriangle, { className: "size-4 shrink-0 text-warning" }), _jsx(Text, { size: "xs", tone: "muted", children: rejected.length === 1
|
|
171
|
+
? translate('cms.asset.rejected.one', {
|
|
172
|
+
name: rejected[0],
|
|
173
|
+
types: describeAccept(field.accept),
|
|
174
|
+
})
|
|
175
|
+
: translate('cms.asset.rejected.other', {
|
|
176
|
+
count: rejected.length,
|
|
177
|
+
types: describeAccept(field.accept),
|
|
178
|
+
}) })] })) : null, dragging ? (_jsx("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", children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Upload, { className: "size-4 text-primary" }), _jsx(Text, { size: "sm", className: "font-medium", children: translate('cms.asset.dropToUpload') })] }) })) : null] }));
|
|
179
|
+
}
|
|
180
|
+
/** Empty-state placeholder shown when an asset field has no selection (EC-301). */
|
|
181
|
+
function EmptyAssetState({ compact, canUpload, onSelect, }) {
|
|
182
|
+
return (_jsxs("button", { type: "button", onClick: onSelect, className: cn('grid w-full place-items-center gap-1 rounded-lg border border-dashed border-border bg-muted/30 text-center text-muted-foreground transition-colors hover:bg-muted/60', compact ? 'aspect-video' : 'aspect-[5/2]'), children: [_jsx(ImagePlus, { className: "size-6", "aria-hidden": true }), _jsx(Text, { size: "xs", tone: "muted", children: canUpload ? translate('cms.asset.selectOrDrop') : translate('cms.asset.selectAsset') })] }));
|
|
183
|
+
}
|
|
184
|
+
/** A large focal-aware preview of the selected asset (EC-301). */
|
|
185
|
+
function AssetPreview({ assetId }) {
|
|
186
|
+
const record = useAssetRecord(assetId);
|
|
187
|
+
return _jsx(AssetThumb, { record: record, className: "aspect-[4/3] w-full" });
|
|
188
|
+
}
|
|
189
|
+
/** Filename + dimensions/type for the selected asset — the id is never shown (EC-301). */
|
|
190
|
+
function AssetInfo({ assetId, fallbackTitle }) {
|
|
191
|
+
const record = useAssetRecord(assetId);
|
|
192
|
+
const name = record?.title ?? record?.filename ?? fallbackTitle ?? assetId;
|
|
193
|
+
const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null;
|
|
194
|
+
const kind = record?.contentType?.split('/')[1]?.toUpperCase();
|
|
195
|
+
const meta = [dims, kind].filter(Boolean).join(' · ');
|
|
196
|
+
return (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "sm", className: "truncate font-medium", title: name, children: name }), meta ? (_jsx(Text, { size: "xs", tone: "subtle", children: meta })) : null] }));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The selected image's alt text shown in the field (EC-301) — what an editor most
|
|
200
|
+
* wants at a glance; edited in the options dialog. Reads `meta.alt` with the legacy
|
|
201
|
+
* `alt` column as fallback, matching delivery.
|
|
202
|
+
*/
|
|
203
|
+
function AssetAltLine({ assetId }) {
|
|
204
|
+
const record = useAssetRecord(assetId);
|
|
205
|
+
const metaAlt = typeof record?.meta?.alt === 'string' ? record.meta.alt : undefined;
|
|
206
|
+
const alt = metaAlt ?? record?.alt;
|
|
207
|
+
return (_jsxs(Row, { gap: "1", align: "baseline", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "subtle", className: "shrink-0", children: translate('cms.asset.altLabel') }), _jsx(Text, { size: "xs", tone: alt ? 'muted' : 'subtle', className: cn('min-w-0 truncate', !alt && 'italic'), title: alt ?? undefined, children: alt || translate('cms.asset.notSet') })] }));
|
|
208
|
+
}
|
|
209
|
+
/** A single asset reference: focal-aware thumbnail + title, with an optional remove. */
|
|
210
|
+
function AssetChip({ assetId, collectionId, onRemove, }) {
|
|
211
|
+
const source = useCollectionView();
|
|
212
|
+
const record = useAssetRecord(assetId);
|
|
213
|
+
const title = source.titleOf({ collection: collectionId, id: assetId });
|
|
214
|
+
return (_jsxs("div", { className: "group relative overflow-hidden rounded-lg border border-border bg-muted/40", children: [_jsx(AssetThumb, { record: record, alt: title, className: "aspect-square w-full rounded-none border-0" }), _jsxs(Row, { gap: "1", align: "center", className: "px-2 py-1", children: [_jsx(Text, { size: "xs", tone: "subtle", className: "min-w-0 flex-1 truncate", title: title, children: title }), onRemove ? (_jsx("button", { type: "button", "aria-label": translate('common.remove'), onClick: onRemove, className: "text-muted-foreground hover:text-destructive", children: _jsx(X, { className: "size-3.5" }) })) : null] })] }));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The selected single asset's preview row (EC-301): large focal-aware preview + info
|
|
218
|
+
* column (filename/dimensions, Select/Edit/Remove, alt). Extracted so it renders either
|
|
219
|
+
* bare (a plain asset field) or inside the grouping card when the field carries per-use
|
|
220
|
+
* sub-fields — the asset + its fields then read as one unit.
|
|
221
|
+
*/
|
|
222
|
+
function AssetSinglePreview({ assetId, compact, readOnly, hasLibrary, fallbackTitle, selectButton, onEdit, onRemove, }) {
|
|
223
|
+
return (_jsxs("div", { className: compact ? 'flex flex-col gap-3' : 'flex gap-4', children: [_jsx("div", { className: compact ? 'w-full' : 'w-1/2 shrink-0', children: _jsx(AssetPreview, { assetId: assetId }) }), _jsxs(Stack, { gap: "2", className: cn('min-w-0', !compact && 'flex-1'), children: [_jsx(AssetInfo, { assetId: assetId, fallbackTitle: fallbackTitle }), !readOnly ? (_jsxs(Row, { gap: "2", wrap: true, align: "center", children: [selectButton, hasLibrary ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: onEdit, children: [_jsx(SlidersHorizontal, {}), " ", translate('common.edit')] })) : null, _jsxs(Button, { variant: "ghost", size: "sm", onClick: onRemove, children: [_jsx(Trash2, {}), " ", translate('common.remove')] })] })) : null, _jsx(AssetAltLine, { assetId: assetId })] })] }));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* One gallery placement when a `many` asset field declares per-use sub-fields (EC-301):
|
|
227
|
+
* a thumbnail + filename + remove header over the placement's own sub-field form,
|
|
228
|
+
* modelled on a repeater item card so a gallery's placements read as grouped units.
|
|
229
|
+
*/
|
|
230
|
+
function AssetGalleryItem({ assetId, collectionId, fields, value, onChangeSubs, onRemove, readOnly, idPrefix, }) {
|
|
231
|
+
const source = useCollectionView();
|
|
232
|
+
const record = useAssetRecord(assetId);
|
|
233
|
+
const title = source.titleOf({ collection: collectionId, id: assetId });
|
|
234
|
+
return (_jsxs(Card, { className: "border-l-2 border-l-border p-0", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-2 py-1.5", children: [_jsx(AssetThumb, { record: record, alt: title, className: "size-10 shrink-0" }), _jsx(Text, { size: "sm", className: "min-w-0 flex-1 truncate font-medium", title: title, children: title }), onRemove ? (_jsx(Button, { variant: "ghost", size: "sm", "aria-label": translate('common.remove'), onClick: onRemove, children: _jsx(Trash2, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "px-3 pb-3", children: _jsx(ObjectItemForm, { fields: fields, value: value, onChange: onChangeSubs, readOnly: readOnly, idPrefix: idPrefix }) })] }));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The image options dialog (EC-301): the focal point (engine-special visual
|
|
238
|
+
* control) + the asset's **schema-driven** metadata, rendered from the
|
|
239
|
+
* `kind:'asset'` collection's `fields` through the same generic field engine
|
|
240
|
+
* documents use — so a project declares asset metadata (alt, copyright, credit, …)
|
|
241
|
+
* in code, not hardcoded here. Edits persist on the asset, shared across every use.
|
|
242
|
+
* Metadata is buffered locally and committed once on close (one write); the focal
|
|
243
|
+
* point commits immediately (it drives a live visual). No-ops without a library.
|
|
244
|
+
*/
|
|
245
|
+
function AssetOptionsDialog({ assetId, readOnly, open, onOpenChange, }) {
|
|
246
|
+
const source = useCollectionView();
|
|
247
|
+
const assets = useOptionalAssets();
|
|
248
|
+
const record = useAssetRecord(assetId);
|
|
249
|
+
const metaFields = useMemo(() => {
|
|
250
|
+
const assetCollectionId = source.assetCollectionId ?? 'asset';
|
|
251
|
+
const collection = source.collections.find((c) => c.id === assetCollectionId);
|
|
252
|
+
return collection ? assetMetaFieldsOf(collection) : [];
|
|
253
|
+
}, [source]);
|
|
254
|
+
// Local edit buffers, seeded once per open from the record (so an external edit
|
|
255
|
+
// still wins between opens). Metadata is non-localized in v1 (the asset
|
|
256
|
+
// resolution path is locale-agnostic) — localized asset metadata is a follow-up.
|
|
257
|
+
const [meta, setMeta] = useState({});
|
|
258
|
+
const [focal, setFocal] = useState(undefined);
|
|
259
|
+
const dirty = useRef(false);
|
|
260
|
+
const seeded = useRef(false);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!open) {
|
|
263
|
+
seeded.current = false;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (seeded.current || !record)
|
|
267
|
+
return;
|
|
268
|
+
// Seed from the meta bag, but lift the legacy `alt` column in when meta lacks
|
|
269
|
+
// it (EC-301 transition): a not-yet-migrated asset still shows its alt, and
|
|
270
|
+
// saving writes it into meta — a lazy migration-on-edit, matching delivery's
|
|
271
|
+
// `meta.alt ?? record.alt` read fallback.
|
|
272
|
+
const seedMeta = { ...(record.alt !== undefined ? { alt: record.alt } : {}), ...(record.meta ?? {}) };
|
|
273
|
+
setMeta(seedMeta);
|
|
274
|
+
setFocal(record.focalPoint);
|
|
275
|
+
dirty.current = false;
|
|
276
|
+
seeded.current = true;
|
|
277
|
+
}, [open, record]);
|
|
278
|
+
function handleOpenChange(next) {
|
|
279
|
+
// Commit the buffered metadata once, on close (one write, not per keystroke).
|
|
280
|
+
if (!next && !readOnly && assets && dirty.current) {
|
|
281
|
+
void assets.updateMetadata(assetId, { meta: pruneMeta(meta) });
|
|
282
|
+
dirty.current = false;
|
|
283
|
+
}
|
|
284
|
+
onOpenChange(next);
|
|
285
|
+
}
|
|
286
|
+
function setField(name, value) {
|
|
287
|
+
dirty.current = true;
|
|
288
|
+
setMeta((m) => ({ ...m, [name]: value }));
|
|
289
|
+
}
|
|
290
|
+
// The focal point drives a live crop preview → commit immediately (optimistic
|
|
291
|
+
// marker), not deferred to close.
|
|
292
|
+
function commitFocal(point) {
|
|
293
|
+
if (readOnly || !assets)
|
|
294
|
+
return;
|
|
295
|
+
setFocal(point ?? undefined);
|
|
296
|
+
void assets.updateMetadata(assetId, { focalPoint: point });
|
|
297
|
+
}
|
|
298
|
+
return (_jsx(Dialog, { open: open, onOpenChange: handleOpenChange, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.asset.imageOptions.title') }), _jsx(DialogDescription, { children: translate('cms.asset.imageOptions.description') })] }), _jsxs(Stack, { gap: "3", children: [_jsxs(Stack, { gap: "2", children: [_jsxs(Row, { align: "center", children: [_jsx(Text, { size: "sm", className: "font-medium", children: translate('cms.asset.focalPoint') }), _jsx(Spacer, {}), focal && !readOnly ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => commitFocal(null), children: [_jsx(RotateCcw, {}), " ", translate('cms.asset.resetToCentre')] })) : null] }), _jsx(FocalPointPicker, { record: record, point: focal, onChange: commitFocal, onPreview: setFocal, disabled: readOnly }), _jsx(Text, { size: "xs", tone: "subtle", children: translate('cms.asset.focalPointHint') }), _jsx(FocalCropPreviews, { record: record, point: focal })] }), metaFields.map((f) => (_jsx(Field, { label: fieldLabel(f), hint: localizedLabel(f.form?.description) || undefined, children: _jsx(FieldControl, { id: `asset-meta-${f.name}`, field: f, value: meta[f.name], readOnly: readOnly, onChange: (v) => setField(f.name, v) }) }, f.name))), metaFields.length === 0 ? (_jsxs(Text, { size: "sm", tone: "subtle", children: ["No editable metadata fields are declared on the asset collection. Add fields (e.g.", _jsx(Code, { tone: "muted", children: " alt" }), ", ", _jsx(Code, { tone: "muted", children: "copyright" }), ") to it in code."] })) : null] })] }) }));
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=asset-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-field.js","sourceRoot":"","sources":["../../../src/components/cms/asset-field.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, ImagePlus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetFieldDef, FieldDef } from '@elytracms/core/cms-core'\nimport {\n assetAcceptAttr,\n assetMatchesAccept,\n assetNode,\n assetSubfieldsOf,\n describeAccept,\n readAssetId,\n} from '@elytracms/core/cms-core'\nimport {\n Button,\n Card,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Field,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata'\nimport { fieldLabel } from './cms-labels'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useFieldDensity } from './field-density'\nimport { AssetPicker } from './asset-picker'\nimport { FieldControl } from './field-control'\nimport { ObjectItemForm } from './object-array-field'\n\n/**\n * The asset-field family (EC-152 + EC-248 + EC-301) — split from\n * `field-control.tsx` (EC-330): the {@link AssetField} control plus its\n * preview/gallery/options sub-components. Per-use sub-fields and the options\n * dialog's metadata form render through the shared object item form /\n * `FieldControl`, so asset metadata stays schema-driven.\n */\n\n/**\n * The asset field (EC-152 + EC-248). Beyond picking from the library it now shows\n * a focal-point-aware preview, uploads directly from the field (reusing the EC-152\n * upload path — no parallel uploader), and edits the selected image's alt + focal\n * point inline (persisted on the asset document, where delivery reads them, EC-230).\n * With no `<AssetsProvider>` (fixtures/playground) it degrades to the labelled-badge\n * picker — no preview/upload — so it stays usable without a real library.\n */\nexport function AssetField({\n field,\n value,\n onChange,\n readOnly,\n}: {\n field: AssetFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const source = useCollectionView()\n const assets = useOptionalAssets()\n const density = useFieldDensity()\n const compact = density === 'compact'\n const [open, setOpen] = useState(false)\n const [editing, setEditing] = useState(false)\n const [dragging, setDragging] = useState(false)\n // Names of files a drop/pick rejected against the field's `accept` (EC-301) — shown\n // as a transient inline issue, cleared on the next valid upload. Never a silent drop.\n const [rejected, setRejected] = useState<string[]>([])\n // Asset ids known when an upload-from-field started; the effect below diffs the\n // library against it to learn the new id (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n const assetCollectionId = source.assetCollectionId ?? 'asset'\n const many = field.multiple\n const ids = many ? assetIdsOf(value) : []\n const single = !many ? singleAssetIdOf(value) : null\n // EC-301 per-use sub-fields: with `field.fields` the value stores asset ENVELOPES\n // carrying the sub-field siblings (`{ type:'asset', id, caption }`); without them it\n // stays bare ids (self-typed on save). Picks/edits preserve a placement's sub-fields.\n const subFields = field.fields ?? []\n const hasSub = subFields.length > 0\n // The raw `many` entries (envelopes or bare ids), index-aligned with `ids`, so each\n // gallery item can read/write its own sub-fields.\n const entries = many && Array.isArray(value) ? (value as unknown[]) : []\n\n const libraryIds = useMemo(\n () => (assets?.status === 'ready' ? assets.assets.map((a) => a.id) : []),\n [assets],\n )\n\n // Auto-select an asset uploaded FROM this field once it lands in the library.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n if (many) {\n const current = Array.isArray(value) ? (value as unknown[]) : []\n onChange([...current, ...added.map((id) => (hasSub ? assetNode(id) : id))])\n } else {\n onChange(hasSub ? assetNode(added[0]!, assetSubfieldsOf(value)) : applyAssetSelection(value, added[0]!, false))\n }\n // Reacts to the library set growing (an upload landing), not to unrelated\n // value edits — `value`/`onChange` are read from the closure on purpose.\n }, [libraryIds, many, hasSub, onChange, value])\n\n function uploadFiles(files: File[]) {\n if (!assets || files.length === 0) return\n // EC-301: a drag-drop bypasses the file dialog's `accept` filter — re-check each\n // file against the field's `accept` and surface a visible issue for the rejects\n // instead of silently uploading the wrong kind.\n const ok: File[] = []\n const bad: string[] = []\n for (const file of files) {\n if (assetMatchesAccept({ contentType: file.type, filename: file.name }, field.accept)) ok.push(file)\n else bad.push(file.name)\n }\n setRejected(bad)\n if (ok.length === 0) return\n // Remember the library set so the effect above can learn the new id and\n // auto-select it once the upload lands (the upload API returns nothing).\n pendingRef.current = new Set(libraryIds)\n assets.upload(ok)\n }\n\n // Apply a picked asset id, preserving a placement's per-use sub-fields (EC-301).\n // Without sub-fields it stays the plain id-list behaviour (self-typed on save).\n function applyPicked(picked: string | null) {\n if (!hasSub) {\n onChange(applyAssetSelection(value, picked, field.multiple))\n return\n }\n if (many) {\n if (picked === null || entries.some((e) => readAssetId(e) === picked)) return\n onChange([...entries, assetNode(picked)])\n } else {\n onChange(picked ? assetNode(picked, assetSubfieldsOf(value)) : undefined)\n }\n }\n\n const canUpload = !readOnly && assets !== null\n\n // Drag-to-upload onto the field (EC-301): a depth counter keeps the overlay\n // steady as the pointer crosses child element boundaries. `dragenter`/`dragleave`\n // fire per descendant, so toggling on a bare `dragleave` blinks constantly; we\n // only hide the overlay when the counter returns to zero (the pointer has truly\n // left the field). The overlay is pointer-events-none, so it never fires its own\n // enter/leave to confuse the count.\n const dragDepth = useRef(0)\n function onDragEnter(e: React.DragEvent) {\n if (!canUpload) return\n e.preventDefault()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n if (!canUpload) return\n e.preventDefault() // required so a drop is allowed\n }\n function onDragLeave() {\n if (!canUpload) return\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 if (!canUpload) return\n e.preventDefault()\n dragDepth.current = 0\n setDragging(false)\n uploadFiles(Array.from(e.dataTransfer.files))\n }\n // While the picker is open the field is behind the modal, so any drop is happening\n // INSIDE the dialog — detach the field's drag handlers entirely so a drop there can\n // only be handled by the picker, never double-counted by the field too (EC-301). This\n // is the belt to the picker's stopPropagation braces: the field cannot react at all.\n const dragProps = canUpload && !open ? { onDragEnter, onDragOver, onDragLeave, onDrop } : {}\n\n const selectButton = !readOnly ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(true)}>\n <ImagePlus /> {translate('common.select')}\n </Button>\n ) : null\n\n return (\n <Stack\n gap=\"2\"\n className={cn(\n 'relative rounded-lg transition-shadow',\n dragging && 'ring-2 ring-primary ring-offset-2 ring-offset-background',\n )}\n {...dragProps}\n >\n {many && hasSub ? (\n // Gallery with per-use sub-fields (EC-301): a card per placement — thumbnail +\n // its own sub-field form — reusing the object/repeater item rendering.\n <Stack gap=\"2\">\n {entries.length > 0 ? (\n <Stack gap=\"2\">\n {entries.map((entry, i) => {\n const aid = readAssetId(entry)\n if (!aid) return null\n return (\n <AssetGalleryItem\n key={`${aid}-${i}`}\n assetId={aid}\n collectionId={assetCollectionId}\n fields={subFields}\n value={assetSubfieldsOf(entry)}\n readOnly={readOnly}\n idPrefix={`asset-${i}`}\n onChangeSubs={(next) => {\n const nextEntries = entries.slice()\n nextEntries[i] = assetNode(aid, next)\n onChange(nextEntries)\n }}\n onRemove={readOnly ? undefined : () => onChange(entries.filter((_, j) => j !== i))}\n />\n )\n })}\n </Stack>\n ) : (\n <EmptyAssetState compact={compact} canUpload={canUpload} onSelect={() => setOpen(true)} />\n )}\n <Row gap=\"2\" wrap align=\"center\">\n {selectButton}\n </Row>\n </Stack>\n ) : many ? (\n // Gallery: a grid of focal-aware previews + the single Select entry.\n <Stack gap=\"2\">\n {ids.length > 0 ? (\n <div\n className={cn(\n 'grid gap-2',\n compact ? 'grid-cols-2' : 'grid-cols-3 sm:grid-cols-4',\n )}\n >\n {ids.map((aid, i) => (\n <AssetChip\n key={`${aid}-${i}`}\n assetId={aid}\n collectionId={assetCollectionId}\n onRemove={readOnly ? undefined : () => onChange(ids.filter((x) => x !== aid))}\n />\n ))}\n </div>\n ) : (\n <EmptyAssetState compact={compact} canUpload={canUpload} onSelect={() => setOpen(true)} />\n )}\n <Row gap=\"2\" wrap align=\"center\">\n {selectButton}\n </Row>\n </Stack>\n ) : single && hasSub ? (\n // With per-use sub-fields the asset + its fields read as ONE grouped unit\n // (like an object), so it's clear they belong together (EC-301 feedback).\n <Card className=\"border-l-2 border-l-border p-3\">\n <Stack gap=\"3\">\n <AssetSinglePreview\n assetId={single}\n compact={compact}\n readOnly={readOnly}\n hasLibrary={assets !== null}\n fallbackTitle={source.titleOf({ collection: assetCollectionId, id: single })}\n selectButton={selectButton}\n onEdit={() => setEditing(true)}\n onRemove={() => onChange(applyAssetSelection(value, null, field.multiple))}\n />\n <div className=\"border-t border-border/60 pt-3\">\n <ObjectItemForm\n fields={subFields}\n value={assetSubfieldsOf(value)}\n readOnly={readOnly}\n idPrefix=\"asset-sub\"\n onChange={(next) => onChange(assetNode(single, next))}\n />\n </div>\n </Stack>\n </Card>\n ) : single ? (\n <AssetSinglePreview\n assetId={single}\n compact={compact}\n readOnly={readOnly}\n hasLibrary={assets !== null}\n fallbackTitle={source.titleOf({ collection: assetCollectionId, id: single })}\n selectButton={selectButton}\n onEdit={() => setEditing(true)}\n onRemove={() => onChange(applyAssetSelection(value, null, field.multiple))}\n />\n ) : (\n <Stack gap=\"2\">\n <EmptyAssetState compact={compact} canUpload={canUpload} onSelect={() => setOpen(true)} />\n <Row gap=\"2\" wrap align=\"center\">\n {selectButton}\n </Row>\n </Stack>\n )}\n\n {/* Image options (focal point + metadata) for a single selected asset, in a\n dialog rather than an inline disclosure (EC-301). Shared across every use. */}\n {!many && single && assets ? (\n <AssetOptionsDialog\n assetId={single}\n readOnly={readOnly}\n open={editing}\n onOpenChange={setEditing}\n />\n ) : null}\n\n <AssetPicker\n value={single}\n open={open}\n onOpenChange={setOpen}\n accept={assetAcceptAttr(field.accept)}\n onChange={applyPicked}\n />\n\n {rejected.length > 0 ? (\n <Row gap=\"2\" align=\"center\">\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', {\n name: rejected[0]!,\n types: describeAccept(field.accept),\n })\n : translate('cms.asset.rejected.other', {\n count: rejected.length,\n types: describeAccept(field.accept),\n })}\n </Text>\n </Row>\n ) : null}\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-4 text-primary\" />\n <Text size=\"sm\" className=\"font-medium\">\n {translate('cms.asset.dropToUpload')}\n </Text>\n </Row>\n </div>\n ) : null}\n </Stack>\n )\n}\n\n/** Empty-state placeholder shown when an asset field has no selection (EC-301). */\nfunction EmptyAssetState({\n compact,\n canUpload,\n onSelect,\n}: {\n compact: boolean\n canUpload: boolean\n onSelect: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n className={cn(\n 'grid w-full place-items-center gap-1 rounded-lg border border-dashed border-border bg-muted/30 text-center text-muted-foreground transition-colors hover:bg-muted/60',\n compact ? 'aspect-video' : 'aspect-[5/2]',\n )}\n >\n <ImagePlus className=\"size-6\" aria-hidden />\n <Text size=\"xs\" tone=\"muted\">\n {canUpload ? translate('cms.asset.selectOrDrop') : translate('cms.asset.selectAsset')}\n </Text>\n </button>\n )\n}\n\n/** A large focal-aware preview of the selected asset (EC-301). */\nfunction AssetPreview({ assetId }: { assetId: string }) {\n const record = useAssetRecord(assetId)\n return <AssetThumb record={record} className=\"aspect-[4/3] w-full\" />\n}\n\n/** Filename + dimensions/type for the selected asset — the id is never shown (EC-301). */\nfunction AssetInfo({ assetId, fallbackTitle }: { assetId: string; fallbackTitle?: string }) {\n const record = useAssetRecord(assetId)\n const name = record?.title ?? record?.filename ?? fallbackTitle ?? assetId\n const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null\n const kind = record?.contentType?.split('/')[1]?.toUpperCase()\n const meta = [dims, kind].filter(Boolean).join(' · ')\n return (\n <Stack gap=\"1\">\n <Text size=\"sm\" className=\"truncate font-medium\" title={name}>\n {name}\n </Text>\n {meta ? (\n <Text size=\"xs\" tone=\"subtle\">\n {meta}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The selected image's alt text shown in the field (EC-301) — what an editor most\n * wants at a glance; edited in the options dialog. Reads `meta.alt` with the legacy\n * `alt` column as fallback, matching delivery.\n */\nfunction AssetAltLine({ assetId }: { assetId: string }) {\n const record = useAssetRecord(assetId)\n const metaAlt = typeof record?.meta?.alt === 'string' ? record.meta.alt : undefined\n const alt = metaAlt ?? record?.alt\n return (\n <Row gap=\"1\" align=\"baseline\" className=\"min-w-0\">\n <Text size=\"xs\" tone=\"subtle\" className=\"shrink-0\">\n {translate('cms.asset.altLabel')}\n </Text>\n <Text\n size=\"xs\"\n tone={alt ? 'muted' : 'subtle'}\n className={cn('min-w-0 truncate', !alt && 'italic')}\n title={alt ?? undefined}\n >\n {alt || translate('cms.asset.notSet')}\n </Text>\n </Row>\n )\n}\n\n/** A single asset reference: focal-aware thumbnail + title, with an optional remove. */\nfunction AssetChip({\n assetId,\n collectionId,\n onRemove,\n}: {\n assetId: string\n collectionId: string\n onRemove?: () => void\n}) {\n const source = useCollectionView()\n const record = useAssetRecord(assetId)\n const title = source.titleOf({ collection: collectionId, id: assetId })\n return (\n <div className=\"group relative overflow-hidden rounded-lg border border-border bg-muted/40\">\n <AssetThumb record={record} alt={title} className=\"aspect-square w-full rounded-none border-0\" />\n <Row gap=\"1\" align=\"center\" className=\"px-2 py-1\">\n <Text size=\"xs\" tone=\"subtle\" className=\"min-w-0 flex-1 truncate\" title={title}>\n {title}\n </Text>\n {onRemove ? (\n <button\n type=\"button\"\n aria-label={translate('common.remove')}\n onClick={onRemove}\n className=\"text-muted-foreground hover:text-destructive\"\n >\n <X className=\"size-3.5\" />\n </button>\n ) : null}\n </Row>\n </div>\n )\n}\n\n/**\n * The selected single asset's preview row (EC-301): large focal-aware preview + info\n * column (filename/dimensions, Select/Edit/Remove, alt). Extracted so it renders either\n * bare (a plain asset field) or inside the grouping card when the field carries per-use\n * sub-fields — the asset + its fields then read as one unit.\n */\nfunction AssetSinglePreview({\n assetId,\n compact,\n readOnly,\n hasLibrary,\n fallbackTitle,\n selectButton,\n onEdit,\n onRemove,\n}: {\n assetId: string\n compact: boolean\n readOnly: boolean\n hasLibrary: boolean\n fallbackTitle?: string\n selectButton: React.ReactNode\n onEdit: () => void\n onRemove: () => void\n}) {\n return (\n <div className={compact ? 'flex flex-col gap-3' : 'flex gap-4'}>\n <div className={compact ? 'w-full' : 'w-1/2 shrink-0'}>\n <AssetPreview assetId={assetId} />\n </div>\n <Stack gap=\"2\" className={cn('min-w-0', !compact && 'flex-1')}>\n <AssetInfo assetId={assetId} fallbackTitle={fallbackTitle} />\n {!readOnly ? (\n <Row gap=\"2\" wrap align=\"center\">\n {selectButton}\n {hasLibrary ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onEdit}>\n <SlidersHorizontal /> {translate('common.edit')}\n </Button>\n ) : null}\n <Button variant=\"ghost\" size=\"sm\" onClick={onRemove}>\n <Trash2 /> {translate('common.remove')}\n </Button>\n </Row>\n ) : null}\n <AssetAltLine assetId={assetId} />\n </Stack>\n </div>\n )\n}\n\n/**\n * One gallery placement when a `many` asset field declares per-use sub-fields (EC-301):\n * a thumbnail + filename + remove header over the placement's own sub-field form,\n * modelled on a repeater item card so a gallery's placements read as grouped units.\n */\nfunction AssetGalleryItem({\n assetId,\n collectionId,\n fields,\n value,\n onChangeSubs,\n onRemove,\n readOnly,\n idPrefix,\n}: {\n assetId: string\n collectionId: string\n fields: readonly FieldDef[]\n value: Record<string, unknown>\n onChangeSubs: (value: Record<string, unknown>) => void\n onRemove?: () => void\n readOnly: boolean\n idPrefix: string\n}) {\n const source = useCollectionView()\n const record = useAssetRecord(assetId)\n const title = source.titleOf({ collection: collectionId, id: assetId })\n return (\n <Card className=\"border-l-2 border-l-border p-0\">\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-2 py-1.5\">\n <AssetThumb record={record} alt={title} className=\"size-10 shrink-0\" />\n <Text size=\"sm\" className=\"min-w-0 flex-1 truncate font-medium\" title={title}>\n {title}\n </Text>\n {onRemove ? (\n <Button variant=\"ghost\" size=\"sm\" aria-label={translate('common.remove')} onClick={onRemove}>\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </Row>\n <div className=\"px-3 pb-3\">\n <ObjectItemForm\n fields={fields}\n value={value}\n onChange={onChangeSubs}\n readOnly={readOnly}\n idPrefix={idPrefix}\n />\n </div>\n </Card>\n )\n}\n\n/**\n * The image options dialog (EC-301): the focal point (engine-special visual\n * control) + the asset's **schema-driven** metadata, rendered from the\n * `kind:'asset'` collection's `fields` through the same generic field engine\n * documents use — so a project declares asset metadata (alt, copyright, credit, …)\n * in code, not hardcoded here. Edits persist on the asset, shared across every use.\n * Metadata is buffered locally and committed once on close (one write); the focal\n * point commits immediately (it drives a live visual). No-ops without a library.\n */\nfunction AssetOptionsDialog({\n assetId,\n readOnly,\n open,\n onOpenChange,\n}: {\n assetId: string\n readOnly: boolean\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const source = useCollectionView()\n const assets = useOptionalAssets()\n const record = useAssetRecord(assetId)\n const metaFields = useMemo(() => {\n const assetCollectionId = source.assetCollectionId ?? 'asset'\n const collection = source.collections.find((c) => c.id === assetCollectionId)\n return collection ? assetMetaFieldsOf(collection) : []\n }, [source])\n\n // Local edit buffers, seeded once per open from the record (so an external edit\n // still wins between opens). Metadata is non-localized in v1 (the asset\n // resolution path is locale-agnostic) — localized asset metadata is a follow-up.\n const [meta, setMeta] = useState<Record<string, unknown>>({})\n const [focal, setFocal] = useState<{ x: number; y: number } | undefined>(undefined)\n const dirty = useRef(false)\n const seeded = useRef(false)\n useEffect(() => {\n if (!open) {\n seeded.current = false\n return\n }\n if (seeded.current || !record) return\n // Seed from the meta bag, but lift the legacy `alt` column in when meta lacks\n // it (EC-301 transition): a not-yet-migrated asset still shows its alt, and\n // saving writes it into meta — a lazy migration-on-edit, matching delivery's\n // `meta.alt ?? record.alt` read fallback.\n const seedMeta = { ...(record.alt !== undefined ? { alt: record.alt } : {}), ...(record.meta ?? {}) }\n setMeta(seedMeta)\n setFocal(record.focalPoint)\n dirty.current = false\n seeded.current = true\n }, [open, record])\n\n function handleOpenChange(next: boolean) {\n // Commit the buffered metadata once, on close (one write, not per keystroke).\n if (!next && !readOnly && assets && dirty.current) {\n void assets.updateMetadata(assetId, { meta: pruneMeta(meta) })\n dirty.current = false\n }\n onOpenChange(next)\n }\n\n function setField(name: string, value: unknown) {\n dirty.current = true\n setMeta((m) => ({ ...m, [name]: value }))\n }\n\n // The focal point drives a live crop preview → commit immediately (optimistic\n // marker), not deferred to close.\n function commitFocal(point: { x: number; y: number } | null) {\n if (readOnly || !assets) return\n setFocal(point ?? undefined)\n void assets.updateMetadata(assetId, { focalPoint: point })\n }\n\n return (\n <Dialog open={open} onOpenChange={handleOpenChange}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.imageOptions.title')}</DialogTitle>\n <DialogDescription>\n {translate('cms.asset.imageOptions.description')}\n </DialogDescription>\n </DialogHeader>\n <Stack gap=\"3\">\n <Stack gap=\"2\">\n <Row align=\"center\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('cms.asset.focalPoint')}\n </Text>\n <Spacer />\n {focal && !readOnly ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => commitFocal(null)}>\n <RotateCcw /> {translate('cms.asset.resetToCentre')}\n </Button>\n ) : null}\n </Row>\n <FocalPointPicker\n record={record}\n point={focal}\n onChange={commitFocal}\n onPreview={setFocal}\n disabled={readOnly}\n />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('cms.asset.focalPointHint')}\n </Text>\n <FocalCropPreviews record={record} point={focal} />\n </Stack>\n {metaFields.map((f) => (\n <Field key={f.name} label={fieldLabel(f)} hint={localizedLabel(f.form?.description) || undefined}>\n <FieldControl\n id={`asset-meta-${f.name}`}\n field={f}\n value={meta[f.name]}\n readOnly={readOnly}\n onChange={(v) => setField(f.name, v)}\n />\n </Field>\n ))}\n {metaFields.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\">\n No editable metadata fields are declared on the asset collection. Add fields (e.g.\n <Code tone=\"muted\"> alt</Code>, <Code tone=\"muted\">copyright</Code>) to it in code.\n </Text>\n ) : null}\n </Stack>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -39,6 +39,7 @@ import { DocumentUrlPath } from './document-url-path.js';
|
|
|
39
39
|
import { EmbedInspectorProvider } from './rich-text/embed-inspector-context.js';
|
|
40
40
|
import { EmbedInspector } from './rich-text/embed-inspector.js';
|
|
41
41
|
import { useSetBreadcrumb } from '../breadcrumb-context.js';
|
|
42
|
+
import { useAiCurrentDocumentTap } from '@elytracms/studio/lib/ai-current-document';
|
|
42
43
|
import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
43
44
|
// EC-325 — the controller hooks EditorBody orchestrates (each independently
|
|
44
45
|
// unit-tested at its pure core); the shell threads shared draft/ref/replaceDraft
|
|
@@ -133,6 +134,17 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
|
|
|
133
134
|
setDraft(next);
|
|
134
135
|
setRestoreNonce((n) => n + 1);
|
|
135
136
|
}
|
|
137
|
+
// EC-378 — the current-document seam: publish WHICH document is open (and the
|
|
138
|
+
// draft-adoption channel) so the AI assistant resolves "this document" without
|
|
139
|
+
// asking, and an applied update replaces this buffer instead of being clobbered
|
|
140
|
+
// by the next autosave. A narrow read-only tap + the existing `replaceDraft`;
|
|
141
|
+
// nothing else of the editor is exposed.
|
|
142
|
+
useAiCurrentDocumentTap({
|
|
143
|
+
collection: collection.id,
|
|
144
|
+
id: doc.id,
|
|
145
|
+
title: primaryTitle(draft, collection, locale, source.localeConfig) || doc.id,
|
|
146
|
+
adoptDraft: replaceDraft,
|
|
147
|
+
});
|
|
136
148
|
// EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective AND the pinned
|
|
137
149
|
// published snapshot, both derived from the ONE shared version-history query
|
|
138
150
|
// (AD-17: the single operations-backed cache — no raw `source.versioning.load`
|