@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,205 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronUp, GripVertical, MoreHorizontal, Plus, Trash2 } from 'lucide-react';
|
|
3
|
+
import { createContext, useContext, useRef, useState } from 'react';
|
|
4
|
+
import { isFieldHidden } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, Row, 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 { fieldLabel } from './cms-labels.js';
|
|
9
|
+
import { FieldControl } from './field-control.js';
|
|
10
|
+
import { depthRail, depthTint, isTabularObjectType, objectItemSummary, reorder } from './field-control-lib.js';
|
|
11
|
+
/**
|
|
12
|
+
* The object / repeater field controls (EC-253, legibility-rebuilt EC-305) —
|
|
13
|
+
* split from `field-control.tsx` (EC-330). {@link ObjectField} dispatches by
|
|
14
|
+
* shape (single card / editable table / card list); {@link ObjectItemForm} is
|
|
15
|
+
* the recursive subfield form the asset-field family reuses for per-use
|
|
16
|
+
* sub-fields and asset metadata. Cells and subfields render through
|
|
17
|
+
* `FieldControl`, so any subfield type — including a nested repeater — works
|
|
18
|
+
* for free.
|
|
19
|
+
*/
|
|
20
|
+
function asRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: {};
|
|
24
|
+
}
|
|
25
|
+
// EC-305: nested-array depth, propagated via context so each array level can encode
|
|
26
|
+
// its depth (rail colour + tint) without prop-drilling through FieldControl's many
|
|
27
|
+
// branches. The detail editor renders top-level fields with the default depth 0.
|
|
28
|
+
const ArrayDepthContext = createContext(0);
|
|
29
|
+
const useArrayDepth = () => useContext(ArrayDepthContext);
|
|
30
|
+
/**
|
|
31
|
+
* Shared array state for an object repeater (EC-253/EC-294/EC-305): the value-only
|
|
32
|
+
* items + a parallel stable-key list kept in lockstep through every structural op
|
|
33
|
+
* (the EC-253 C2 key↔value lock), add/remove/move, and native drag-reorder. Used by
|
|
34
|
+
* both the card list (B) and the table (C) so they behave identically.
|
|
35
|
+
*/
|
|
36
|
+
function useObjectArray(field, value, onChange) {
|
|
37
|
+
const items = Array.isArray(value) ? value : [];
|
|
38
|
+
const seq = useRef(0);
|
|
39
|
+
const keysRef = useRef([]);
|
|
40
|
+
if (keysRef.current.length !== items.length) {
|
|
41
|
+
keysRef.current = items.map((_, i) => keysRef.current[i] ?? seq.current++);
|
|
42
|
+
}
|
|
43
|
+
const keys = keysRef.current;
|
|
44
|
+
const commit = (nextItems, nextKeys) => {
|
|
45
|
+
keysRef.current = nextKeys;
|
|
46
|
+
onChange(nextItems);
|
|
47
|
+
};
|
|
48
|
+
const move = (from, to) => commit(reorder(items, from, to), reorder(keys, from, to));
|
|
49
|
+
const removeAt = (i) => commit(items.filter((_, j) => j !== i), keys.filter((_, j) => j !== i));
|
|
50
|
+
const addItem = () => {
|
|
51
|
+
const seed = {};
|
|
52
|
+
for (const sub of field.fields)
|
|
53
|
+
if (sub.default !== undefined)
|
|
54
|
+
seed[sub.name] = sub.default;
|
|
55
|
+
const key = seq.current++;
|
|
56
|
+
commit([...items, seed], [...keys, key]);
|
|
57
|
+
return key;
|
|
58
|
+
};
|
|
59
|
+
const setItem = (i, updated) => {
|
|
60
|
+
const nextItems = items.slice();
|
|
61
|
+
nextItems[i] = updated;
|
|
62
|
+
commit(nextItems, keys);
|
|
63
|
+
};
|
|
64
|
+
const [dragIndex, setDragIndex] = useState(null);
|
|
65
|
+
const [overIndex, setOverIndex] = useState(null);
|
|
66
|
+
const endDrag = () => {
|
|
67
|
+
setDragIndex(null);
|
|
68
|
+
setOverIndex(null);
|
|
69
|
+
};
|
|
70
|
+
const drop = (to) => {
|
|
71
|
+
if (dragIndex !== null && dragIndex !== to)
|
|
72
|
+
move(dragIndex, to);
|
|
73
|
+
endDrag();
|
|
74
|
+
};
|
|
75
|
+
return { items, keys, move, removeAt, addItem, setItem, dragIndex, overIndex, setDragIndex, setOverIndex, endDrag, drop };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* (E) Chrome reduction: the per-row move/remove controls collapse into one hover/
|
|
79
|
+
* focus overflow menu, so a row reads as content, not a six-button toolbar. Drag is
|
|
80
|
+
* the primary reorder; the arrows stay here for keyboard/a11y.
|
|
81
|
+
*/
|
|
82
|
+
function ItemOverflowMenu({ index, count, onMove, onRemove, }) {
|
|
83
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", "aria-label": translate('cms.field.itemActions'), children: _jsx(MoreHorizontal, { className: "size-3.5" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { disabled: index === 0, onClick: () => onMove(index, index - 1), children: [_jsx(ChevronUp, {}), " ", translate('cms.field.moveUp')] }), _jsxs(DropdownMenuItem, { disabled: index === count - 1, onClick: () => onMove(index, index + 1), children: [_jsx(ChevronDown, {}), " ", translate('cms.field.moveDown')] }), _jsxs(DropdownMenuItem, { onClick: onRemove, children: [_jsx(Trash2, {}), " ", translate('common.remove')] })] })] }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The nested subfield form for one object item (EC-253). Reuses {@link FieldControl}
|
|
87
|
+
* recursively, so any subfield type — including a nested object/repeater — works for
|
|
88
|
+
* free. Writing a subfield merges it back into the item and bubbles the new item up.
|
|
89
|
+
*/
|
|
90
|
+
export function ObjectItemForm({ fields, value, onChange, readOnly, idPrefix, }) {
|
|
91
|
+
return (_jsx(Stack, { gap: "3", className: "mt-2", children: fields.map((sub) => {
|
|
92
|
+
// EC-273: a conditionally-hidden subfield (its predicate keys off the item's
|
|
93
|
+
// own sibling values) is omitted — e.g. a discriminated link's `href` shows
|
|
94
|
+
// only when `linkType === 'href'`. Re-evaluated as the item is edited.
|
|
95
|
+
if (isFieldHidden(sub, { values: value }))
|
|
96
|
+
return null;
|
|
97
|
+
const subId = `${idPrefix}-${sub.name}`;
|
|
98
|
+
return (_jsx(Field, { htmlFor: subId, hint: localizedLabel(sub.form?.description) || undefined, label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(sub) }), sub.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null] }), children: _jsx(FieldControl, { id: subId, field: sub, value: value[sub.name], readOnly: readOnly, onChange: (next) => {
|
|
99
|
+
const updated = { ...value };
|
|
100
|
+
if (next === undefined)
|
|
101
|
+
delete updated[sub.name];
|
|
102
|
+
else
|
|
103
|
+
updated[sub.name] = next;
|
|
104
|
+
onChange(updated);
|
|
105
|
+
} }) }, sub.name));
|
|
106
|
+
}) }));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Object / repeater field control (EC-253), legibility-rebuilt (EC-305). A single
|
|
110
|
+
* object (`multiple: false`) renders a depth-encoded card; an array (`'many'`)
|
|
111
|
+
* dispatches by shape: a **flat** object type (all scalar subfields) → a compact
|
|
112
|
+
* editable TABLE (C); a richer/nested type → a card list with depth encoding (B),
|
|
113
|
+
* summary-first collapse (D), and reduced per-row chrome (E). Editing stays in the
|
|
114
|
+
* shared field form — no canvas escape (unlike `blocks`) — so props and document
|
|
115
|
+
* fields share this control. Depth flows via {@link ArrayDepthContext}.
|
|
116
|
+
*/
|
|
117
|
+
export function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
118
|
+
const depth = useArrayDepth();
|
|
119
|
+
if (field.multiple === false) {
|
|
120
|
+
return (_jsx(Card, { className: cn('border-l-2 p-3', depthRail(depth), depthTint(depth)), children: _jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsx(ObjectItemForm, { fields: field.fields, value: asRecord(value), readOnly: readOnly, idPrefix: id, onChange: onChange }) }) }));
|
|
121
|
+
}
|
|
122
|
+
const props = { field, value, onChange, readOnly, id, depth };
|
|
123
|
+
return isTabularObjectType(field) ? _jsx(ObjectTableField, { ...props }) : _jsx(ObjectCardList, { ...props });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* (B + D + E) The card list for a nested/rich object array: depth-encoded cards
|
|
127
|
+
* (coloured rail + tint), items collapsed by default to a scannable summary outline
|
|
128
|
+
* (D), accordion expansion (open one → collapse siblings), and the per-row controls
|
|
129
|
+
* collapsed into one overflow menu (E). Drag-to-reorder; arrows live in the menu for
|
|
130
|
+
* a11y. Nested arrays render one depth deeper via the context provider.
|
|
131
|
+
*/
|
|
132
|
+
function ObjectCardList({ field, value, onChange, readOnly, id, depth }) {
|
|
133
|
+
const arr = useObjectArray(field, value, onChange);
|
|
134
|
+
const { items, keys } = arr;
|
|
135
|
+
// (D) Default COLLAPSED: track which keys are EXPANDED (empty = all collapsed).
|
|
136
|
+
// (B) Accordion: expanding one collapses the siblings.
|
|
137
|
+
const [expandedKeys, setExpandedKeys] = useState(() => new Set());
|
|
138
|
+
const toggle = (key) => setExpandedKeys((prev) => {
|
|
139
|
+
if (prev.has(key)) {
|
|
140
|
+
const next = new Set(prev);
|
|
141
|
+
next.delete(key);
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
return new Set([key]);
|
|
145
|
+
});
|
|
146
|
+
const allExpanded = items.length > 0 && keys.every((k) => expandedKeys.has(k));
|
|
147
|
+
const onAdd = () => setExpandedKeys(new Set([arr.addItem()]));
|
|
148
|
+
return (_jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsxs(Stack, { gap: "2", children: [items.length > 1 && !readOnly ? (_jsx(Row, { justify: "end", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setExpandedKeys(allExpanded ? new Set() : new Set(keys)), children: translate(allExpanded ? 'cms.field.collapseAll' : 'cms.field.expandAll') }) })) : null, items.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.field.noItems') })) : (items.map((item, i) => {
|
|
149
|
+
// keysRef is reconciled to items.length, so keys[i] is always present;
|
|
150
|
+
// `?? i` only satisfies noUncheckedIndexedAccess (never taken at runtime).
|
|
151
|
+
const key = keys[i] ?? i;
|
|
152
|
+
const record = asRecord(item);
|
|
153
|
+
const isExpanded = expandedKeys.has(key);
|
|
154
|
+
return (_jsxs(Card, { onDragOver: (e) => {
|
|
155
|
+
if (arr.dragIndex === null)
|
|
156
|
+
return;
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
if (arr.overIndex !== i)
|
|
159
|
+
arr.setOverIndex(i);
|
|
160
|
+
}, onDrop: () => arr.drop(i), className: cn('border-l-2 p-0', depthRail(depth), depthTint(depth), arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
161
|
+
? 'border-l-primary ring-1 ring-primary/40'
|
|
162
|
+
: ''), children: [_jsxs(Row, { className: "select-none border-b border-border/60 bg-muted/20 px-2 py-1.5", children: [!readOnly ? (_jsx("span", { draggable: true, onDragStart: (e) => {
|
|
163
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
164
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
165
|
+
arr.setDragIndex(i);
|
|
166
|
+
}, onDragEnd: arr.endDrag, "aria-label": translate('cms.field.dragToReorder'), className: "cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) })) : null, _jsxs("button", { type: "button", onClick: () => toggle(key), "aria-expanded": isExpanded, className: "flex min-w-0 flex-1 items-center gap-1.5 text-left", children: [_jsx(ChevronDown, { className: cn('size-3.5 shrink-0 text-muted-foreground transition-transform', isExpanded ? '' : '-rotate-90'), "aria-hidden": true }), _jsx(Text, { size: "sm", className: "min-w-0 truncate font-medium", children: objectItemSummary(field.fields, record, i) })] }), !readOnly ? (_jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) })) : null] }), isExpanded ? (_jsx("div", { className: "px-3 pb-3", children: _jsx(ObjectItemForm, { fields: field.fields, value: record, readOnly: readOnly, idPrefix: `${id}-${i}`, onChange: (updated) => arr.setItem(i, updated) }) })) : null] }, key));
|
|
167
|
+
})), !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "size-3" }), " ", translate('cms.field.addItem')] })) : null] }) }));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* (C) A compact editable table for a FLAT object array — columns are the scalar
|
|
171
|
+
* subfields, one row per item. Kills the card-stack sprawl for the commonest case
|
|
172
|
+
* (option lists, opening hours, social links). Drag-reorder + an overflow menu per
|
|
173
|
+
* row; cells reuse {@link FieldControl} so each subfield keeps its native control.
|
|
174
|
+
*/
|
|
175
|
+
function ObjectTableField({ field, value, onChange, readOnly, id, depth }) {
|
|
176
|
+
const arr = useObjectArray(field, value, onChange);
|
|
177
|
+
const { items, keys } = arr;
|
|
178
|
+
const cols = field.fields;
|
|
179
|
+
const span = cols.length + (readOnly ? 0 : 2);
|
|
180
|
+
return (_jsxs("div", { className: cn('overflow-hidden rounded-md border border-border border-l-2', depthRail(depth), depthTint(depth)), children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border/60 bg-muted/30 text-left", children: [!readOnly ? _jsx("th", { className: "w-7" }) : null, cols.map((c) => (_jsxs("th", { className: "px-2 py-1.5 text-xs font-medium text-muted-foreground", children: [fieldLabel(c), c.validation?.required ? _jsx("span", { className: "text-destructive", children: " *" }) : null] }, c.name))), !readOnly ? _jsx("th", { className: "w-9" }) : null] }) }), _jsx("tbody", { children: items.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: span, className: "px-2 py-2 text-muted-foreground", children: _jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.field.noItems') }) }) })) : (items.map((item, i) => {
|
|
181
|
+
const key = keys[i] ?? i;
|
|
182
|
+
const record = asRecord(item);
|
|
183
|
+
return (_jsxs("tr", { onDragOver: (e) => {
|
|
184
|
+
if (arr.dragIndex === null)
|
|
185
|
+
return;
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
if (arr.overIndex !== i)
|
|
188
|
+
arr.setOverIndex(i);
|
|
189
|
+
}, onDrop: () => arr.drop(i), className: cn('border-b border-border/40 align-top last:border-0', arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
190
|
+
? 'bg-primary/5'
|
|
191
|
+
: ''), children: [!readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx("span", { draggable: true, onDragStart: (e) => {
|
|
192
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
193
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
194
|
+
arr.setDragIndex(i);
|
|
195
|
+
}, onDragEnd: arr.endDrag, "aria-label": translate('cms.field.dragToReorder'), className: "inline-flex cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) }) })) : null, cols.map((c) => (_jsx("td", { className: "px-2 py-1", children: _jsx(FieldControl, { id: `${id}-${i}-${c.name}`, field: c, value: record[c.name], readOnly: readOnly, onChange: (next) => {
|
|
196
|
+
const updated = { ...record };
|
|
197
|
+
if (next === undefined)
|
|
198
|
+
delete updated[c.name];
|
|
199
|
+
else
|
|
200
|
+
updated[c.name] = next;
|
|
201
|
+
arr.setItem(i, updated);
|
|
202
|
+
} }) }, c.name))), !readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) }) })) : null] }, key));
|
|
203
|
+
})) })] }), !readOnly ? (_jsx("div", { className: "border-t border-border/60 p-1.5", children: _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => arr.addItem(), children: [_jsx(Plus, { className: "size-3" }), " ", translate('common.add')] }) })) : null] }));
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=object-array-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-array-field.js","sourceRoot":"","sources":["../../../src/components/cms/object-array-field.tsx"],"names":[],"mappings":"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","sourcesContent":["import { ChevronDown, ChevronUp, GripVertical, MoreHorizontal, Plus, Trash2 } from 'lucide-react'\nimport { createContext, useContext, useRef, useState } from 'react'\nimport type { FieldDef, ObjectFieldDef } from '@elytracms/core/cms-core'\nimport { isFieldHidden } from '@elytracms/core/cms-core'\nimport {\n Button,\n Card,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Field,\n Row,\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 { fieldLabel } from './cms-labels'\nimport { FieldControl } from './field-control'\nimport { depthRail, depthTint, isTabularObjectType, objectItemSummary, reorder } from './field-control-lib'\n\n/**\n * The object / repeater field controls (EC-253, legibility-rebuilt EC-305) —\n * split from `field-control.tsx` (EC-330). {@link ObjectField} dispatches by\n * shape (single card / editable table / card list); {@link ObjectItemForm} is\n * the recursive subfield form the asset-field family reuses for per-use\n * sub-fields and asset metadata. Cells and subfields render through\n * `FieldControl`, so any subfield type — including a nested repeater — works\n * for free.\n */\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n}\n\n// EC-305: nested-array depth, propagated via context so each array level can encode\n// its depth (rail colour + tint) without prop-drilling through FieldControl's many\n// branches. The detail editor renders top-level fields with the default depth 0.\nconst ArrayDepthContext = createContext(0)\nconst useArrayDepth = () => useContext(ArrayDepthContext)\n\n/**\n * Shared array state for an object repeater (EC-253/EC-294/EC-305): the value-only\n * items + a parallel stable-key list kept in lockstep through every structural op\n * (the EC-253 C2 key↔value lock), add/remove/move, and native drag-reorder. Used by\n * both the card list (B) and the table (C) so they behave identically.\n */\nfunction useObjectArray(field: ObjectFieldDef, value: unknown, onChange: (value: unknown) => void) {\n const items = Array.isArray(value) ? (value as Record<string, unknown>[]) : []\n const seq = useRef(0)\n const keysRef = useRef<number[]>([])\n if (keysRef.current.length !== items.length) {\n keysRef.current = items.map((_, i) => keysRef.current[i] ?? seq.current++)\n }\n const keys = keysRef.current\n const commit = (nextItems: Record<string, unknown>[], nextKeys: number[]) => {\n keysRef.current = nextKeys\n onChange(nextItems)\n }\n const move = (from: number, to: number) => commit(reorder(items, from, to), reorder(keys, from, to))\n const removeAt = (i: number) =>\n commit(items.filter((_, j) => j !== i), keys.filter((_, j) => j !== i))\n const addItem = (): number => {\n const seed: Record<string, unknown> = {}\n for (const sub of field.fields) if (sub.default !== undefined) seed[sub.name] = sub.default\n const key = seq.current++\n commit([...items, seed], [...keys, key])\n return key\n }\n const setItem = (i: number, updated: Record<string, unknown>) => {\n const nextItems = items.slice()\n nextItems[i] = updated\n commit(nextItems, keys)\n }\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n const [overIndex, setOverIndex] = useState<number | null>(null)\n const endDrag = () => {\n setDragIndex(null)\n setOverIndex(null)\n }\n const drop = (to: number) => {\n if (dragIndex !== null && dragIndex !== to) move(dragIndex, to)\n endDrag()\n }\n return { items, keys, move, removeAt, addItem, setItem, dragIndex, overIndex, setDragIndex, setOverIndex, endDrag, drop }\n}\n\n/**\n * (E) Chrome reduction: the per-row move/remove controls collapse into one hover/\n * focus overflow menu, so a row reads as content, not a six-button toolbar. Drag is\n * the primary reorder; the arrows stay here for keyboard/a11y.\n */\nfunction ItemOverflowMenu({\n index,\n count,\n onMove,\n onRemove,\n}: {\n index: number\n count: number\n onMove: (from: number, to: number) => void\n onRemove: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" aria-label={translate('cms.field.itemActions')}>\n <MoreHorizontal className=\"size-3.5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem disabled={index === 0} onClick={() => onMove(index, index - 1)}>\n <ChevronUp /> {translate('cms.field.moveUp')}\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === count - 1} onClick={() => onMove(index, index + 1)}>\n <ChevronDown /> {translate('cms.field.moveDown')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onRemove}>\n <Trash2 /> {translate('common.remove')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The nested subfield form for one object item (EC-253). Reuses {@link FieldControl}\n * recursively, so any subfield type — including a nested object/repeater — works for\n * free. Writing a subfield merges it back into the item and bubbles the new item up.\n */\nexport function ObjectItemForm({\n fields,\n value,\n onChange,\n readOnly,\n idPrefix,\n}: {\n fields: readonly FieldDef[]\n value: Record<string, unknown>\n onChange: (value: Record<string, unknown>) => void\n readOnly: boolean\n idPrefix: string\n}) {\n return (\n <Stack gap=\"3\" className=\"mt-2\">\n {fields.map((sub) => {\n // EC-273: a conditionally-hidden subfield (its predicate keys off the item's\n // own sibling values) is omitted — e.g. a discriminated link's `href` shows\n // only when `linkType === 'href'`. Re-evaluated as the item is edited.\n if (isFieldHidden(sub, { values: value })) return null\n const subId = `${idPrefix}-${sub.name}`\n return (\n <Field\n key={sub.name}\n htmlFor={subId}\n hint={localizedLabel(sub.form?.description) || undefined}\n label={\n <Row gap=\"2\">\n <span>{fieldLabel(sub)}</span>\n {sub.validation?.required ? <span className=\"text-destructive\">*</span> : null}\n </Row>\n }\n >\n <FieldControl\n id={subId}\n field={sub}\n value={value[sub.name]}\n readOnly={readOnly}\n onChange={(next) => {\n const updated = { ...value }\n if (next === undefined) delete updated[sub.name]\n else updated[sub.name] = next\n onChange(updated)\n }}\n />\n </Field>\n )\n })}\n </Stack>\n )\n}\n\n/**\n * Object / repeater field control (EC-253), legibility-rebuilt (EC-305). A single\n * object (`multiple: false`) renders a depth-encoded card; an array (`'many'`)\n * dispatches by shape: a **flat** object type (all scalar subfields) → a compact\n * editable TABLE (C); a richer/nested type → a card list with depth encoding (B),\n * summary-first collapse (D), and reduced per-row chrome (E). Editing stays in the\n * shared field form — no canvas escape (unlike `blocks`) — so props and document\n * fields share this control. Depth flows via {@link ArrayDepthContext}.\n */\nexport function ObjectField({\n field,\n value,\n onChange,\n readOnly,\n id,\n}: {\n field: ObjectFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}) {\n const depth = useArrayDepth()\n if (field.multiple === false) {\n return (\n <Card className={cn('border-l-2 p-3', depthRail(depth), depthTint(depth))}>\n <ArrayDepthContext.Provider value={depth + 1}>\n <ObjectItemForm\n fields={field.fields}\n value={asRecord(value)}\n readOnly={readOnly}\n idPrefix={id}\n onChange={onChange}\n />\n </ArrayDepthContext.Provider>\n </Card>\n )\n }\n const props = { field, value, onChange, readOnly, id, depth }\n return isTabularObjectType(field) ? <ObjectTableField {...props} /> : <ObjectCardList {...props} />\n}\n\ninterface ObjectArrayProps {\n field: ObjectFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n depth: number\n}\n\n/**\n * (B + D + E) The card list for a nested/rich object array: depth-encoded cards\n * (coloured rail + tint), items collapsed by default to a scannable summary outline\n * (D), accordion expansion (open one → collapse siblings), and the per-row controls\n * collapsed into one overflow menu (E). Drag-to-reorder; arrows live in the menu for\n * a11y. Nested arrays render one depth deeper via the context provider.\n */\nfunction ObjectCardList({ field, value, onChange, readOnly, id, depth }: ObjectArrayProps) {\n const arr = useObjectArray(field, value, onChange)\n const { items, keys } = arr\n // (D) Default COLLAPSED: track which keys are EXPANDED (empty = all collapsed).\n // (B) Accordion: expanding one collapses the siblings.\n const [expandedKeys, setExpandedKeys] = useState<ReadonlySet<number>>(() => new Set())\n const toggle = (key: number) =>\n setExpandedKeys((prev) => {\n if (prev.has(key)) {\n const next = new Set(prev)\n next.delete(key)\n return next\n }\n return new Set([key])\n })\n const allExpanded = items.length > 0 && keys.every((k) => expandedKeys.has(k))\n const onAdd = () => setExpandedKeys(new Set([arr.addItem()]))\n return (\n <ArrayDepthContext.Provider value={depth + 1}>\n <Stack gap=\"2\">\n {items.length > 1 && !readOnly ? (\n <Row justify=\"end\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setExpandedKeys(allExpanded ? new Set() : new Set(keys))}\n >\n {translate(allExpanded ? 'cms.field.collapseAll' : 'cms.field.expandAll')}\n </Button>\n </Row>\n ) : null}\n {items.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.field.noItems')}\n </Text>\n ) : (\n items.map((item, i) => {\n // keysRef is reconciled to items.length, so keys[i] is always present;\n // `?? i` only satisfies noUncheckedIndexedAccess (never taken at runtime).\n const key = keys[i] ?? i\n const record = asRecord(item)\n const isExpanded = expandedKeys.has(key)\n return (\n <Card\n key={key}\n onDragOver={(e) => {\n if (arr.dragIndex === null) return\n e.preventDefault()\n if (arr.overIndex !== i) arr.setOverIndex(i)\n }}\n onDrop={() => arr.drop(i)}\n className={cn(\n 'border-l-2 p-0',\n depthRail(depth),\n depthTint(depth),\n arr.dragIndex === i ? 'opacity-50' : '',\n arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i\n ? 'border-l-primary ring-1 ring-primary/40'\n : '',\n )}\n >\n <Row className=\"select-none border-b border-border/60 bg-muted/20 px-2 py-1.5\">\n {!readOnly ? (\n <span\n draggable\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', String(i))\n arr.setDragIndex(i)\n }}\n onDragEnd={arr.endDrag}\n aria-label={translate('cms.field.dragToReorder')}\n className=\"cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing\"\n >\n <GripVertical className=\"size-3.5\" />\n </span>\n ) : null}\n <button\n type=\"button\"\n onClick={() => toggle(key)}\n aria-expanded={isExpanded}\n className=\"flex min-w-0 flex-1 items-center gap-1.5 text-left\"\n >\n <ChevronDown\n className={cn(\n 'size-3.5 shrink-0 text-muted-foreground transition-transform',\n isExpanded ? '' : '-rotate-90',\n )}\n aria-hidden\n />\n <Text size=\"sm\" className=\"min-w-0 truncate font-medium\">\n {objectItemSummary(field.fields, record, i)}\n </Text>\n </button>\n {!readOnly ? (\n <ItemOverflowMenu\n index={i}\n count={items.length}\n onMove={arr.move}\n onRemove={() => arr.removeAt(i)}\n />\n ) : null}\n </Row>\n {isExpanded ? (\n <div className=\"px-3 pb-3\">\n <ObjectItemForm\n fields={field.fields}\n value={record}\n readOnly={readOnly}\n idPrefix={`${id}-${i}`}\n onChange={(updated) => arr.setItem(i, updated)}\n />\n </div>\n ) : null}\n </Card>\n )\n })\n )}\n {!readOnly ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onAdd}>\n <Plus className=\"size-3\" /> {translate('cms.field.addItem')}\n </Button>\n ) : null}\n </Stack>\n </ArrayDepthContext.Provider>\n )\n}\n\n/**\n * (C) A compact editable table for a FLAT object array — columns are the scalar\n * subfields, one row per item. Kills the card-stack sprawl for the commonest case\n * (option lists, opening hours, social links). Drag-reorder + an overflow menu per\n * row; cells reuse {@link FieldControl} so each subfield keeps its native control.\n */\nfunction ObjectTableField({ field, value, onChange, readOnly, id, depth }: ObjectArrayProps) {\n const arr = useObjectArray(field, value, onChange)\n const { items, keys } = arr\n const cols = field.fields\n const span = cols.length + (readOnly ? 0 : 2)\n return (\n <div className={cn('overflow-hidden rounded-md border border-border border-l-2', depthRail(depth), depthTint(depth))}>\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border/60 bg-muted/30 text-left\">\n {!readOnly ? <th className=\"w-7\" /> : null}\n {cols.map((c) => (\n <th key={c.name} className=\"px-2 py-1.5 text-xs font-medium text-muted-foreground\">\n {fieldLabel(c)}\n {c.validation?.required ? <span className=\"text-destructive\"> *</span> : null}\n </th>\n ))}\n {!readOnly ? <th className=\"w-9\" /> : null}\n </tr>\n </thead>\n <tbody>\n {items.length === 0 ? (\n <tr>\n <td colSpan={span} className=\"px-2 py-2 text-muted-foreground\">\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.field.noItems')}\n </Text>\n </td>\n </tr>\n ) : (\n items.map((item, i) => {\n const key = keys[i] ?? i\n const record = asRecord(item)\n return (\n <tr\n key={key}\n onDragOver={(e) => {\n if (arr.dragIndex === null) return\n e.preventDefault()\n if (arr.overIndex !== i) arr.setOverIndex(i)\n }}\n onDrop={() => arr.drop(i)}\n className={cn(\n 'border-b border-border/40 align-top last:border-0',\n arr.dragIndex === i ? 'opacity-50' : '',\n arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i\n ? 'bg-primary/5'\n : '',\n )}\n >\n {!readOnly ? (\n <td className=\"px-1 py-1\">\n <span\n draggable\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', String(i))\n arr.setDragIndex(i)\n }}\n onDragEnd={arr.endDrag}\n aria-label={translate('cms.field.dragToReorder')}\n className=\"inline-flex cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing\"\n >\n <GripVertical className=\"size-3.5\" />\n </span>\n </td>\n ) : null}\n {cols.map((c) => (\n <td key={c.name} className=\"px-2 py-1\">\n <FieldControl\n id={`${id}-${i}-${c.name}`}\n field={c}\n value={record[c.name]}\n readOnly={readOnly}\n onChange={(next) => {\n const updated = { ...record }\n if (next === undefined) delete updated[c.name]\n else updated[c.name] = next\n arr.setItem(i, updated)\n }}\n />\n </td>\n ))}\n {!readOnly ? (\n <td className=\"px-1 py-1\">\n <ItemOverflowMenu\n index={i}\n count={items.length}\n onMove={arr.move}\n onRemove={() => arr.removeAt(i)}\n />\n </td>\n ) : null}\n </tr>\n )\n })\n )}\n </tbody>\n </table>\n {!readOnly ? (\n <div className=\"border-t border-border/60 p-1.5\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => arr.addItem()}>\n <Plus className=\"size-3\" /> {translate('common.add')}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a stored rich-text value as compact read-only prose. A legacy plain
|
|
3
|
+
* string stays visible as text; any other unparseable value degrades to the
|
|
4
|
+
* generic object note — visible, never silent, never a crash.
|
|
5
|
+
*/
|
|
6
|
+
export declare function RichTextPreview({ value, className }: {
|
|
7
|
+
value: unknown;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a rich-text value is EMPTY for review purposes: unset, an empty doc,
|
|
12
|
+
* or nothing but blank prose scaffolding. A doc carrying any atom (image, embed,
|
|
13
|
+
* custom node) or non-blank text is content — it must be shown, not skipped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isRichTextValueEmpty(value: unknown): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { parseRichTextValue, renderRichTextValue } from '@elytracms/core/rich-text';
|
|
3
|
+
import { Badge, Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
|
+
/**
|
|
7
|
+
* Compact read-only rich-text rendering for review surfaces (EC-379).
|
|
8
|
+
*
|
|
9
|
+
* The AI proposal diff (and any other change-review lane) must show the editor
|
|
10
|
+
* the ACTUAL prose they are approving — never a blind "Content changed." note.
|
|
11
|
+
* This renders a stored rich-text value through the engine's server-safe
|
|
12
|
+
* read-only renderer (`@elytracms/core/rich-text`) with modest, compact styles;
|
|
13
|
+
* component embeds render as an inspectable component-id chip (the diff reviews
|
|
14
|
+
* content, not layout), and unknown nodes keep the renderer's visible fallback.
|
|
15
|
+
*
|
|
16
|
+
* Self-contained by design: EC-382 (proposal-diff layout) restyles the diff
|
|
17
|
+
* chrome around it without touching this component's contract.
|
|
18
|
+
*/
|
|
19
|
+
/** Compact prose styles applied to the renderer's `div.rich-text` root. */
|
|
20
|
+
const RICH_TEXT_PREVIEW_STYLES = [
|
|
21
|
+
'text-sm leading-relaxed break-words space-y-1.5',
|
|
22
|
+
'[&_h1]:text-base [&_h1]:font-semibold',
|
|
23
|
+
'[&_h2]:text-sm [&_h2]:font-semibold',
|
|
24
|
+
'[&_:is(h3,h4,h5,h6)]:text-sm [&_:is(h3,h4,h5,h6)]:font-medium',
|
|
25
|
+
'[&_:is(ul,ol)]:pl-5 [&_:is(ul,ol)]:space-y-0.5 [&_ul]:list-disc [&_ol]:list-decimal',
|
|
26
|
+
'[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-2 [&_blockquote]:text-muted-foreground',
|
|
27
|
+
'[&_a]:underline [&_a]:underline-offset-2',
|
|
28
|
+
'[&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-muted [&_pre]:p-2 [&_code]:font-mono [&_code]:text-xs',
|
|
29
|
+
'[&_img]:max-w-full [&_img]:rounded',
|
|
30
|
+
'[&_hr]:border-border',
|
|
31
|
+
].join(' ');
|
|
32
|
+
/** The inspectable chip a `componentEmbed` renders as inside a review preview. */
|
|
33
|
+
function EmbedChip({ node }) {
|
|
34
|
+
return (_jsx(Badge, { variant: "outline", className: "font-mono text-[0.65rem]", children: node.componentId }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render a stored rich-text value as compact read-only prose. A legacy plain
|
|
38
|
+
* string stays visible as text; any other unparseable value degrades to the
|
|
39
|
+
* generic object note — visible, never silent, never a crash.
|
|
40
|
+
*/
|
|
41
|
+
export function RichTextPreview({ value, className }) {
|
|
42
|
+
const parsed = parseRichTextValue(value);
|
|
43
|
+
if (!parsed.ok) {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: value }));
|
|
46
|
+
}
|
|
47
|
+
return (_jsx(Text, { size: "sm", tone: "muted", children: translate('cms.version.objectValue') }));
|
|
48
|
+
}
|
|
49
|
+
return renderRichTextValue(parsed.value, {
|
|
50
|
+
className: cn(RICH_TEXT_PREVIEW_STYLES, className),
|
|
51
|
+
renderEmbed: (node) => _jsx(EmbedChip, { node: node }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Node types that carry no content of their own — an empty scaffold of these is "empty". */
|
|
55
|
+
const PROSE_SCAFFOLD_TYPES = new Set([
|
|
56
|
+
'paragraph',
|
|
57
|
+
'heading',
|
|
58
|
+
'text',
|
|
59
|
+
'hardBreak',
|
|
60
|
+
'bulletList',
|
|
61
|
+
'orderedList',
|
|
62
|
+
'listItem',
|
|
63
|
+
'blockquote',
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Whether a rich-text value is EMPTY for review purposes: unset, an empty doc,
|
|
67
|
+
* or nothing but blank prose scaffolding. A doc carrying any atom (image, embed,
|
|
68
|
+
* custom node) or non-blank text is content — it must be shown, not skipped.
|
|
69
|
+
*/
|
|
70
|
+
export function isRichTextValueEmpty(value) {
|
|
71
|
+
if (value === undefined || value === null || value === '')
|
|
72
|
+
return true;
|
|
73
|
+
const parsed = parseRichTextValue(value);
|
|
74
|
+
if (!parsed.ok)
|
|
75
|
+
return false;
|
|
76
|
+
const hasContent = (nodes) => (nodes ?? []).some((node) => (typeof node.text === 'string' && node.text.trim().length > 0) ||
|
|
77
|
+
!PROSE_SCAFFOLD_TYPES.has(node.type) ||
|
|
78
|
+
hasContent(node.content));
|
|
79
|
+
return !hasContent(parsed.value.doc.content);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=rich-text-preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-text-preview.js","sourceRoot":"","sources":["../../../src/components/cms/rich-text-preview.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAGnF,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAEhD;;;;;;;;;;;;GAYG;AAEH,2EAA2E;AAC3E,MAAM,wBAAwB,GAAG;IAC/B,iDAAiD;IACjD,uCAAuC;IACvC,qCAAqC;IACrC,+DAA+D;IAC/D,qFAAqF;IACrF,iHAAiH;IACjH,0CAA0C;IAC1C,0GAA0G;IAC1G,oCAAoC;IACpC,sBAAsB;CACvB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAEX,kFAAkF;AAClF,SAAS,SAAS,CAAC,EAAE,IAAI,EAA2B;IAClD,OAAO,CACL,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,EAAC,SAAS,EAAC,0BAA0B,YAC1D,IAAI,CAAC,WAAW,GACX,CACT,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,KAAK,EAAE,SAAS,EAA0C;IAC1F,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;IACxC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,iCAAiC,YACxD,KAAK,GACD,CACR,CAAA;QACH,CAAC;QACD,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,yBAAyB,CAAC,GAChC,CACR,CAAA;IACH,CAAC;IACD,OAAO,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE;QACvC,SAAS,EAAE,EAAE,CAAC,wBAAwB,EAAE,SAAS,CAAC;QAClD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAC,SAAS,IAAC,IAAI,EAAE,IAAI,GAAI;KACjD,CAAC,CAAA;AACJ,CAAC;AAED,6FAA6F;AAC7F,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACnC,WAAW;IACX,SAAS;IACT,MAAM;IACN,WAAW;IACX,YAAY;IACZ,aAAa;IACb,UAAU;IACV,YAAY;CACb,CAAC,CAAA;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACtE,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;IACxC,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,KAAK,CAAA;IAC5B,MAAM,UAAU,GAAG,CAAC,KAA0C,EAAW,EAAE,CACzE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAChB,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;QAC9D,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;QACpC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAC3B,CAAA;IACH,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AAC9C,CAAC","sourcesContent":["import { parseRichTextValue, renderRichTextValue } from '@elytracms/core/rich-text'\nimport type { RichTextNode } from '@elytracms/core/rich-text'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { Badge, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * Compact read-only rich-text rendering for review surfaces (EC-379).\n *\n * The AI proposal diff (and any other change-review lane) must show the editor\n * the ACTUAL prose they are approving — never a blind \"Content changed.\" note.\n * This renders a stored rich-text value through the engine's server-safe\n * read-only renderer (`@elytracms/core/rich-text`) with modest, compact styles;\n * component embeds render as an inspectable component-id chip (the diff reviews\n * content, not layout), and unknown nodes keep the renderer's visible fallback.\n *\n * Self-contained by design: EC-382 (proposal-diff layout) restyles the diff\n * chrome around it without touching this component's contract.\n */\n\n/** Compact prose styles applied to the renderer's `div.rich-text` root. */\nconst RICH_TEXT_PREVIEW_STYLES = [\n 'text-sm leading-relaxed break-words space-y-1.5',\n '[&_h1]:text-base [&_h1]:font-semibold',\n '[&_h2]:text-sm [&_h2]:font-semibold',\n '[&_:is(h3,h4,h5,h6)]:text-sm [&_:is(h3,h4,h5,h6)]:font-medium',\n '[&_:is(ul,ol)]:pl-5 [&_:is(ul,ol)]:space-y-0.5 [&_ul]:list-disc [&_ol]:list-decimal',\n '[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-2 [&_blockquote]:text-muted-foreground',\n '[&_a]:underline [&_a]:underline-offset-2',\n '[&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-muted [&_pre]:p-2 [&_code]:font-mono [&_code]:text-xs',\n '[&_img]:max-w-full [&_img]:rounded',\n '[&_hr]:border-border',\n].join(' ')\n\n/** The inspectable chip a `componentEmbed` renders as inside a review preview. */\nfunction EmbedChip({ node }: { node: ComponentNode }) {\n return (\n <Badge variant=\"outline\" className=\"font-mono text-[0.65rem]\">\n {node.componentId}\n </Badge>\n )\n}\n\n/**\n * Render a stored rich-text value as compact read-only prose. A legacy plain\n * string stays visible as text; any other unparseable value degrades to the\n * generic object note — visible, never silent, never a crash.\n */\nexport function RichTextPreview({ value, className }: { value: unknown; className?: string }) {\n const parsed = parseRichTextValue(value)\n if (!parsed.ok) {\n if (typeof value === 'string') {\n return (\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words\">\n {value}\n </Text>\n )\n }\n return (\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.version.objectValue')}\n </Text>\n )\n }\n return renderRichTextValue(parsed.value, {\n className: cn(RICH_TEXT_PREVIEW_STYLES, className),\n renderEmbed: (node) => <EmbedChip node={node} />,\n })\n}\n\n/** Node types that carry no content of their own — an empty scaffold of these is \"empty\". */\nconst PROSE_SCAFFOLD_TYPES = new Set([\n 'paragraph',\n 'heading',\n 'text',\n 'hardBreak',\n 'bulletList',\n 'orderedList',\n 'listItem',\n 'blockquote',\n])\n\n/**\n * Whether a rich-text value is EMPTY for review purposes: unset, an empty doc,\n * or nothing but blank prose scaffolding. A doc carrying any atom (image, embed,\n * custom node) or non-blank text is content — it must be shown, not skipped.\n */\nexport function isRichTextValueEmpty(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return true\n const parsed = parseRichTextValue(value)\n if (!parsed.ok) return false\n const hasContent = (nodes: readonly RichTextNode[] | undefined): boolean =>\n (nodes ?? []).some(\n (node) =>\n (typeof node.text === 'string' && node.text.trim().length > 0) ||\n !PROSE_SCAFFOLD_TYPES.has(node.type) ||\n hasContent(node.content),\n )\n return !hasContent(parsed.value.doc.content)\n}\n"]}
|
|
@@ -62,6 +62,32 @@ export declare function truncateTokens<T extends {
|
|
|
62
62
|
shown: T[];
|
|
63
63
|
isLong: boolean;
|
|
64
64
|
};
|
|
65
|
+
/** A change-anchored excerpt (EC-382): the visible token slice plus whether an
|
|
66
|
+
* ellipsis of hidden context should show before/after it. */
|
|
67
|
+
export interface AnchoredExcerpt<T> {
|
|
68
|
+
shown: T[];
|
|
69
|
+
isLong: boolean;
|
|
70
|
+
leadingEllipsis: boolean;
|
|
71
|
+
trailingEllipsis: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Change-anchored token truncation (EC-382, the EC-379 finding): like
|
|
75
|
+
* {@link truncateTokens}, but the visible window CENTERS on the first changed
|
|
76
|
+
* token instead of always starting at the head — so an edit that lands past the
|
|
77
|
+
* head threshold is never hidden behind "Show more". Presentation only: the
|
|
78
|
+
* caller keeps the full token stream and the semantic diff underneath is
|
|
79
|
+
* untouched; this just decides which contiguous run to render.
|
|
80
|
+
*
|
|
81
|
+
* The window keeps a little context (~`ANCHOR_LEADING_FRACTION` of the budget)
|
|
82
|
+
* before the first change, always includes the change token itself (even a
|
|
83
|
+
* change longer than the whole budget), then fills forward to the budget. A row
|
|
84
|
+
* with no changed token (defensive) degrades to plain head truncation. The
|
|
85
|
+
* `leadingEllipsis`/`trailingEllipsis` flags tell the renderer where hidden
|
|
86
|
+
* context exists so it can show a `…` marker.
|
|
87
|
+
*/
|
|
88
|
+
export declare function truncateTokensAroundChange<T extends {
|
|
89
|
+
text: string;
|
|
90
|
+
}>(tokens: readonly T[], expanded: boolean, isChanged: (token: T) => boolean, threshold?: number): AnchoredExcerpt<T>;
|
|
65
91
|
/**
|
|
66
92
|
* A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
|
|
67
93
|
* the stored value can be anything the field's editor writes: a plain/HTML STRING
|
|
@@ -133,6 +133,66 @@ export function truncateTokens(tokens, expanded, threshold = DIFF_TRUNCATE_THRES
|
|
|
133
133
|
}
|
|
134
134
|
return { shown, isLong };
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Fraction of the truncation budget spent on the CONTEXT before the first
|
|
138
|
+
* change — enough to orient (the sentence the edit sits in) without pushing the
|
|
139
|
+
* change itself out of the window. The remainder trails the change.
|
|
140
|
+
*/
|
|
141
|
+
const ANCHOR_LEADING_FRACTION = 0.3;
|
|
142
|
+
/**
|
|
143
|
+
* Change-anchored token truncation (EC-382, the EC-379 finding): like
|
|
144
|
+
* {@link truncateTokens}, but the visible window CENTERS on the first changed
|
|
145
|
+
* token instead of always starting at the head — so an edit that lands past the
|
|
146
|
+
* head threshold is never hidden behind "Show more". Presentation only: the
|
|
147
|
+
* caller keeps the full token stream and the semantic diff underneath is
|
|
148
|
+
* untouched; this just decides which contiguous run to render.
|
|
149
|
+
*
|
|
150
|
+
* The window keeps a little context (~`ANCHOR_LEADING_FRACTION` of the budget)
|
|
151
|
+
* before the first change, always includes the change token itself (even a
|
|
152
|
+
* change longer than the whole budget), then fills forward to the budget. A row
|
|
153
|
+
* with no changed token (defensive) degrades to plain head truncation. The
|
|
154
|
+
* `leadingEllipsis`/`trailingEllipsis` flags tell the renderer where hidden
|
|
155
|
+
* context exists so it can show a `…` marker.
|
|
156
|
+
*/
|
|
157
|
+
export function truncateTokensAroundChange(tokens, expanded, isChanged, threshold = DIFF_TRUNCATE_THRESHOLD) {
|
|
158
|
+
const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0);
|
|
159
|
+
const isLong = totalLength > threshold;
|
|
160
|
+
if (expanded || !isLong) {
|
|
161
|
+
return { shown: [...tokens], isLong, leadingEllipsis: false, trailingEllipsis: false };
|
|
162
|
+
}
|
|
163
|
+
const firstChange = tokens.findIndex(isChanged);
|
|
164
|
+
const anchor = firstChange === -1 ? 0 : firstChange;
|
|
165
|
+
// Walk back from the anchor, accumulating whole tokens up to the leading budget.
|
|
166
|
+
const leadBudget = Math.floor(threshold * ANCHOR_LEADING_FRACTION);
|
|
167
|
+
let start = anchor;
|
|
168
|
+
let lead = 0;
|
|
169
|
+
while (start > 0) {
|
|
170
|
+
const len = tokens[start - 1].text.length;
|
|
171
|
+
if (lead + len > leadBudget)
|
|
172
|
+
break;
|
|
173
|
+
lead += len;
|
|
174
|
+
start -= 1;
|
|
175
|
+
}
|
|
176
|
+
// Fill forward from `start`; the break never fires before the anchor, so the
|
|
177
|
+
// changed token is always shown even when it (with its lead) exceeds the budget.
|
|
178
|
+
const shown = [];
|
|
179
|
+
let used = 0;
|
|
180
|
+
let end = start;
|
|
181
|
+
while (end < tokens.length) {
|
|
182
|
+
const len = tokens[end].text.length;
|
|
183
|
+
if (end > anchor && used + len > threshold)
|
|
184
|
+
break;
|
|
185
|
+
shown.push(tokens[end]);
|
|
186
|
+
used += len;
|
|
187
|
+
end += 1;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
shown,
|
|
191
|
+
isLong,
|
|
192
|
+
leadingEllipsis: start > 0,
|
|
193
|
+
trailingEllipsis: end < tokens.length,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
136
196
|
/**
|
|
137
197
|
* A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
|
|
138
198
|
* the stored value can be anything the field's editor writes: a plain/HTML STRING
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-compare-text.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure text helpers for the EC-298 compare view — extracted so the fiddly bits\n * (word-level LCS, format-agnostic \"rich text\" extraction) are unit-tested apart\n * from the React surface.\n */\n\nexport interface WordToken {\n text: string\n changed: boolean\n}\n\n/** A token in the EC-317 unified (single-lane) prose diff: `same` reads plain,\n * `removed`/`added` are the struck/highlighted halves of a changed run. */\nexport interface MergedDiffToken {\n text: string\n kind: 'same' | 'removed' | 'added'\n}\n\nfunction tokenize(s: string): string[] {\n return s.split(/(\\s+)/).filter((t) => t.length > 0)\n}\n\n/**\n * Word-level LCS diff (prose, not code): tokens NOT on the longest common\n * subsequence are marked — removed on the before side, added on the after side.\n * Whitespace tokens participate, so spacing is preserved on render.\n */\nexport function wordDiff(before: string, after: string): { before: WordToken[]; after: WordToken[] } {\n const a = tokenize(before)\n const b = tokenize(after)\n // Bound the DP so a pathological pair can't stall the editor.\n if (a.length + b.length > 4000) {\n return {\n before: a.map((text) => ({ text, changed: true })),\n after: b.map((text) => ({ text, changed: true })),\n }\n }\n const m = a.length\n const n = b.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)\n }\n }\n const beforeTokens: WordToken[] = []\n const afterTokens: WordToken[] = []\n let i = 0\n let j = 0\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n beforeTokens.push({ text: a[i]!, changed: false })\n afterTokens.push({ text: b[j]!, changed: false })\n i++\n j++\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\n beforeTokens.push({ text: a[i]!, changed: true })\n i++\n } else {\n afterTokens.push({ text: b[j]!, changed: true })\n j++\n }\n }\n while (i < m) beforeTokens.push({ text: a[i++]!, changed: true })\n while (j < n) afterTokens.push({ text: b[j++]!, changed: true })\n return { before: beforeTokens, after: afterTokens }\n}\n\n/**\n * Merge a {@link wordDiff} pair into ONE reading lane (EC-317 unified view) —\n * a single stream of `same` / `removed` / `added` tokens instead of two\n * columns. Sound by construction: `wordDiff` pushes an `unchanged` token to\n * BOTH sides in the SAME loop iteration (so the Nth unchanged token in\n * `before` and the Nth unchanged token in `after` are always the same anchor),\n * and every stretch between two anchors is a contiguous `changed` run on\n * either side (possibly empty on one, or both — a pure add/remove — or both\n * nonempty — a replace). Walking both arrays in lockstep and draining each\n * side's changed run between anchors reconstructs that reading order exactly.\n */\nexport function mergeWordDiff(before: WordToken[], after: WordToken[]): MergedDiffToken[] {\n const merged: MergedDiffToken[] = []\n let i = 0\n let j = 0\n while (i < before.length && j < after.length) {\n if (!before[i]!.changed && !after[j]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'same' })\n i++\n j++\n continue\n }\n while (i < before.length && before[i]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'removed' })\n i++\n }\n while (j < after.length && after[j]!.changed) {\n merged.push({ text: after[j]!.text, kind: 'added' })\n j++\n }\n }\n // A trailing changed run on whichever side still has tokens once the other\n // is exhausted (wordDiff's own trailing while-loops produce exactly this).\n while (i < before.length) merged.push({ text: before[i++]!.text, kind: 'removed' })\n while (j < after.length) merged.push({ text: after[j++]!.text, kind: 'added' })\n return merged\n}\n\n/**\n * A sensible default length threshold (characters) past which the EC-317\n * compare view collapses a value behind a per-row \"Show more\" — long enough to\n * read a couple of sentences at a glance, short enough that a full rich-text\n * body doesn't dominate the change-list.\n */\nexport const DIFF_TRUNCATE_THRESHOLD = 400\n\n/**\n * Collapse a plain text value for display (EC-317) — presentation only, never\n * touching the semantic/identity diff underneath: the caller always has the\n * full string, this just decides how much of it to render.\n */\nexport function truncateText(\n text: string,\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: string; isLong: boolean } {\n const isLong = text.length > threshold\n if (expanded || !isLong) return { shown: text, isLong }\n return { shown: `${text.slice(0, threshold).trimEnd()}…`, isLong }\n}\n\n/**\n * The token-array counterpart of {@link truncateText} — used for word-diff /\n * merged-diff token streams so a collapsed row still ends on a whole token\n * (never mid-word) and the change markers (`changed`/`kind`) it slices off\n * are simply not rendered, not recomputed.\n */\nexport function truncateTokens<T extends { text: string }>(\n tokens: readonly T[],\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: T[]; isLong: boolean } {\n const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0)\n const isLong = totalLength > threshold\n if (expanded || !isLong) return { shown: [...tokens], isLong }\n const shown: T[] = []\n let used = 0\n for (const token of tokens) {\n if (used >= threshold) break\n shown.push(token)\n used += token.text.length\n }\n return { shown, isLong }\n}\n\n/**\n * A \"rich text\" value as readable text for the prose diff — FORMAT-AGNOSTIC, since\n * the stored value can be anything the field's editor writes: a plain/HTML STRING\n * (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show\n * whatever is entered rather than assuming a structure (EC-298).\n */\nexport function richTextToText(value: unknown): string {\n if (typeof value === 'string') return looksLikeHtml(value) ? htmlToText(value) : value\n if (value && typeof value === 'object') {\n const wrapper = value as { doc?: unknown; content?: unknown }\n const doc = (wrapper.doc ?? wrapper) as { content?: unknown }\n if (Array.isArray(doc.content)) return plainTextFromNodes(doc.content)\n }\n return ''\n}\n\nexport function looksLikeHtml(s: string): boolean {\n return /<[a-z!/][\\s\\S]*>/i.test(s)\n}\n\n/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|blockquote)\\s*>/gi, '\\n\\n')\n .replace(/<\\/(li|tr|ul|ol)\\s*>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */\nexport function plainTextFromNodes(nodes: unknown[]): string {\n const blockText = (node: unknown): string => {\n const parts: string[] = []\n const walk = (n: unknown) => {\n if (!n || typeof n !== 'object') return\n const rec = n as { text?: unknown; content?: unknown }\n if (typeof rec.text === 'string') parts.push(rec.text)\n if (Array.isArray(rec.content)) rec.content.forEach(walk)\n }\n walk(node)\n return parts.join('')\n }\n return nodes.map(blockText).filter(Boolean).join('\\n\\n')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"version-compare-text.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure text helpers for the EC-298 compare view — extracted so the fiddly bits\n * (word-level LCS, format-agnostic \"rich text\" extraction) are unit-tested apart\n * from the React surface.\n */\n\nexport interface WordToken {\n text: string\n changed: boolean\n}\n\n/** A token in the EC-317 unified (single-lane) prose diff: `same` reads plain,\n * `removed`/`added` are the struck/highlighted halves of a changed run. */\nexport interface MergedDiffToken {\n text: string\n kind: 'same' | 'removed' | 'added'\n}\n\nfunction tokenize(s: string): string[] {\n return s.split(/(\\s+)/).filter((t) => t.length > 0)\n}\n\n/**\n * Word-level LCS diff (prose, not code): tokens NOT on the longest common\n * subsequence are marked — removed on the before side, added on the after side.\n * Whitespace tokens participate, so spacing is preserved on render.\n */\nexport function wordDiff(before: string, after: string): { before: WordToken[]; after: WordToken[] } {\n const a = tokenize(before)\n const b = tokenize(after)\n // Bound the DP so a pathological pair can't stall the editor.\n if (a.length + b.length > 4000) {\n return {\n before: a.map((text) => ({ text, changed: true })),\n after: b.map((text) => ({ text, changed: true })),\n }\n }\n const m = a.length\n const n = b.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)\n }\n }\n const beforeTokens: WordToken[] = []\n const afterTokens: WordToken[] = []\n let i = 0\n let j = 0\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n beforeTokens.push({ text: a[i]!, changed: false })\n afterTokens.push({ text: b[j]!, changed: false })\n i++\n j++\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\n beforeTokens.push({ text: a[i]!, changed: true })\n i++\n } else {\n afterTokens.push({ text: b[j]!, changed: true })\n j++\n }\n }\n while (i < m) beforeTokens.push({ text: a[i++]!, changed: true })\n while (j < n) afterTokens.push({ text: b[j++]!, changed: true })\n return { before: beforeTokens, after: afterTokens }\n}\n\n/**\n * Merge a {@link wordDiff} pair into ONE reading lane (EC-317 unified view) —\n * a single stream of `same` / `removed` / `added` tokens instead of two\n * columns. Sound by construction: `wordDiff` pushes an `unchanged` token to\n * BOTH sides in the SAME loop iteration (so the Nth unchanged token in\n * `before` and the Nth unchanged token in `after` are always the same anchor),\n * and every stretch between two anchors is a contiguous `changed` run on\n * either side (possibly empty on one, or both — a pure add/remove — or both\n * nonempty — a replace). Walking both arrays in lockstep and draining each\n * side's changed run between anchors reconstructs that reading order exactly.\n */\nexport function mergeWordDiff(before: WordToken[], after: WordToken[]): MergedDiffToken[] {\n const merged: MergedDiffToken[] = []\n let i = 0\n let j = 0\n while (i < before.length && j < after.length) {\n if (!before[i]!.changed && !after[j]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'same' })\n i++\n j++\n continue\n }\n while (i < before.length && before[i]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'removed' })\n i++\n }\n while (j < after.length && after[j]!.changed) {\n merged.push({ text: after[j]!.text, kind: 'added' })\n j++\n }\n }\n // A trailing changed run on whichever side still has tokens once the other\n // is exhausted (wordDiff's own trailing while-loops produce exactly this).\n while (i < before.length) merged.push({ text: before[i++]!.text, kind: 'removed' })\n while (j < after.length) merged.push({ text: after[j++]!.text, kind: 'added' })\n return merged\n}\n\n/**\n * A sensible default length threshold (characters) past which the EC-317\n * compare view collapses a value behind a per-row \"Show more\" — long enough to\n * read a couple of sentences at a glance, short enough that a full rich-text\n * body doesn't dominate the change-list.\n */\nexport const DIFF_TRUNCATE_THRESHOLD = 400\n\n/**\n * Collapse a plain text value for display (EC-317) — presentation only, never\n * touching the semantic/identity diff underneath: the caller always has the\n * full string, this just decides how much of it to render.\n */\nexport function truncateText(\n text: string,\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: string; isLong: boolean } {\n const isLong = text.length > threshold\n if (expanded || !isLong) return { shown: text, isLong }\n return { shown: `${text.slice(0, threshold).trimEnd()}…`, isLong }\n}\n\n/**\n * The token-array counterpart of {@link truncateText} — used for word-diff /\n * merged-diff token streams so a collapsed row still ends on a whole token\n * (never mid-word) and the change markers (`changed`/`kind`) it slices off\n * are simply not rendered, not recomputed.\n */\nexport function truncateTokens<T extends { text: string }>(\n tokens: readonly T[],\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: T[]; isLong: boolean } {\n const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0)\n const isLong = totalLength > threshold\n if (expanded || !isLong) return { shown: [...tokens], isLong }\n const shown: T[] = []\n let used = 0\n for (const token of tokens) {\n if (used >= threshold) break\n shown.push(token)\n used += token.text.length\n }\n return { shown, isLong }\n}\n\n/** A change-anchored excerpt (EC-382): the visible token slice plus whether an\n * ellipsis of hidden context should show before/after it. */\nexport interface AnchoredExcerpt<T> {\n shown: T[]\n isLong: boolean\n leadingEllipsis: boolean\n trailingEllipsis: boolean\n}\n\n/**\n * Fraction of the truncation budget spent on the CONTEXT before the first\n * change — enough to orient (the sentence the edit sits in) without pushing the\n * change itself out of the window. The remainder trails the change.\n */\nconst ANCHOR_LEADING_FRACTION = 0.3\n\n/**\n * Change-anchored token truncation (EC-382, the EC-379 finding): like\n * {@link truncateTokens}, but the visible window CENTERS on the first changed\n * token instead of always starting at the head — so an edit that lands past the\n * head threshold is never hidden behind \"Show more\". Presentation only: the\n * caller keeps the full token stream and the semantic diff underneath is\n * untouched; this just decides which contiguous run to render.\n *\n * The window keeps a little context (~`ANCHOR_LEADING_FRACTION` of the budget)\n * before the first change, always includes the change token itself (even a\n * change longer than the whole budget), then fills forward to the budget. A row\n * with no changed token (defensive) degrades to plain head truncation. The\n * `leadingEllipsis`/`trailingEllipsis` flags tell the renderer where hidden\n * context exists so it can show a `…` marker.\n */\nexport function truncateTokensAroundChange<T extends { text: string }>(\n tokens: readonly T[],\n expanded: boolean,\n isChanged: (token: T) => boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): AnchoredExcerpt<T> {\n const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0)\n const isLong = totalLength > threshold\n if (expanded || !isLong) {\n return { shown: [...tokens], isLong, leadingEllipsis: false, trailingEllipsis: false }\n }\n const firstChange = tokens.findIndex(isChanged)\n const anchor = firstChange === -1 ? 0 : firstChange\n\n // Walk back from the anchor, accumulating whole tokens up to the leading budget.\n const leadBudget = Math.floor(threshold * ANCHOR_LEADING_FRACTION)\n let start = anchor\n let lead = 0\n while (start > 0) {\n const len = tokens[start - 1]!.text.length\n if (lead + len > leadBudget) break\n lead += len\n start -= 1\n }\n\n // Fill forward from `start`; the break never fires before the anchor, so the\n // changed token is always shown even when it (with its lead) exceeds the budget.\n const shown: T[] = []\n let used = 0\n let end = start\n while (end < tokens.length) {\n const len = tokens[end]!.text.length\n if (end > anchor && used + len > threshold) break\n shown.push(tokens[end]!)\n used += len\n end += 1\n }\n\n return {\n shown,\n isLong,\n leadingEllipsis: start > 0,\n trailingEllipsis: end < tokens.length,\n }\n}\n\n/**\n * A \"rich text\" value as readable text for the prose diff — FORMAT-AGNOSTIC, since\n * the stored value can be anything the field's editor writes: a plain/HTML STRING\n * (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show\n * whatever is entered rather than assuming a structure (EC-298).\n */\nexport function richTextToText(value: unknown): string {\n if (typeof value === 'string') return looksLikeHtml(value) ? htmlToText(value) : value\n if (value && typeof value === 'object') {\n const wrapper = value as { doc?: unknown; content?: unknown }\n const doc = (wrapper.doc ?? wrapper) as { content?: unknown }\n if (Array.isArray(doc.content)) return plainTextFromNodes(doc.content)\n }\n return ''\n}\n\nexport function looksLikeHtml(s: string): boolean {\n return /<[a-z!/][\\s\\S]*>/i.test(s)\n}\n\n/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|blockquote)\\s*>/gi, '\\n\\n')\n .replace(/<\\/(li|tr|ul|ol)\\s*>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */\nexport function plainTextFromNodes(nodes: unknown[]): string {\n const blockText = (node: unknown): string => {\n const parts: string[] = []\n const walk = (n: unknown) => {\n if (!n || typeof n !== 'object') return\n const rec = n as { text?: unknown; content?: unknown }\n if (typeof rec.text === 'string') parts.push(rec.text)\n if (Array.isArray(rec.content)) rec.content.forEach(walk)\n }\n walk(node)\n return parts.join('')\n }\n return nodes.map(blockText).filter(Boolean).join('\\n\\n')\n}\n"]}
|
|
@@ -20,6 +20,20 @@ import type { HydratedCms, HydratedProjectSession, OperationResult } from '@elyt
|
|
|
20
20
|
* BEFORE children render, so "Preview with" lists and section previews show
|
|
21
21
|
* live entries, not fixture titles.
|
|
22
22
|
*
|
|
23
|
+
* EC-361 — the playground gets the SAME side effect, without the render gate:
|
|
24
|
+
* the seeded in-memory adapter IS a persisted backend behind the operations
|
|
25
|
+
* seam (AD-17), so the content session hydrates from it through the identical
|
|
26
|
+
* `loadHydratedCms` loader connected mode uses — fire-and-forget after the
|
|
27
|
+
* synchronous first render (EC-138 stays intact), refreshed per CMS surface
|
|
28
|
+
* mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas
|
|
29
|
+
* listing/relation population therefore resolves from in-memory session
|
|
30
|
+
* documents with connected-mode semantics (published-ness = the version
|
|
31
|
+
* pointer; the Draft view resolves live rows) instead of the blanket-empty
|
|
32
|
+
* session the playground used to run with. The React `session`/`loadCms`
|
|
33
|
+
* VALUES stay `null` in playground — the CMS surfaces keep their fixture
|
|
34
|
+
* baseline (EC-320's static-baseline rule); only the preview content engine
|
|
35
|
+
* reads persisted state.
|
|
36
|
+
*
|
|
23
37
|
* EC-308/AD-17 note — this gate deliberately does NOT read through the
|
|
24
38
|
* `useOperationQuery` cache seam. It is boot/session orchestration, not a
|
|
25
39
|
* cacheable server read: (1) the synchronous playground first-render is the
|
|
@@ -55,6 +69,11 @@ export { useProjectSession };
|
|
|
55
69
|
* `configureContentSession` side effect of its own — `loadCms` (from
|
|
56
70
|
* `useProjectSession`) already calls it internally when it resolves — so no
|
|
57
71
|
* `onResolved`/`enabled` is passed here.
|
|
72
|
+
*
|
|
73
|
+
* EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),
|
|
74
|
+
* but `loadCms` is still fired per mount for its content-session side effect —
|
|
75
|
+
* the preview engine refreshes from the in-memory adapter on the same
|
|
76
|
+
* per-CMS-mount rhythm connected mode gets from the gated load.
|
|
58
77
|
*/
|
|
59
78
|
export declare function CmsSessionGate({ children, }: {
|
|
60
79
|
children: (cms: HydratedCms | null) => ReactNode;
|