@elytracms/studio 0.0.38 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/convex/cms.ts +8 -2
  2. package/dist/components/app-views/default-modules.d.ts +60 -5
  3. package/dist/components/app-views/default-modules.js +53 -15
  4. package/dist/components/app-views/default-modules.js.map +1 -1
  5. package/dist/components/app-views/module.d.ts +2 -0
  6. package/dist/components/app-views/module.js +8 -0
  7. package/dist/components/app-views/module.js.map +1 -1
  8. package/dist/components/assets/asset-detail.d.ts +24 -0
  9. package/dist/components/assets/asset-detail.js +163 -0
  10. package/dist/components/assets/asset-detail.js.map +1 -0
  11. package/dist/components/assets/asset-grid.d.ts +28 -0
  12. package/dist/components/assets/asset-grid.js +68 -0
  13. package/dist/components/assets/asset-grid.js.map +1 -0
  14. package/dist/components/assets/asset-metadata.d.ts +19 -0
  15. package/dist/components/assets/asset-metadata.js +44 -0
  16. package/dist/components/assets/asset-metadata.js.map +1 -1
  17. package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
  18. package/dist/components/assets/assets-collection-view-source.js +186 -0
  19. package/dist/components/assets/assets-collection-view-source.js.map +1 -0
  20. package/dist/components/assets/assets-module.d.ts +18 -0
  21. package/dist/components/assets/assets-module.js +94 -0
  22. package/dist/components/assets/assets-module.js.map +1 -0
  23. package/dist/components/assets/assets-store.js +2 -7
  24. package/dist/components/assets/assets-store.js.map +1 -1
  25. package/dist/components/assets/assets-view-preference.d.ts +15 -0
  26. package/dist/components/assets/assets-view-preference.js +22 -0
  27. package/dist/components/assets/assets-view-preference.js.map +1 -0
  28. package/dist/components/builder/builder-store.d.ts +12 -0
  29. package/dist/components/builder/builder-store.js +18 -16
  30. package/dist/components/builder/builder-store.js.map +1 -1
  31. package/dist/components/builder/clipboard-actions.js +5 -14
  32. package/dist/components/builder/clipboard-actions.js.map +1 -1
  33. package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
  34. package/dist/components/builder/composition-field-editor-lazy.js +29 -0
  35. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
  36. package/dist/components/builder/composition-field-editor.js +1 -4
  37. package/dist/components/builder/composition-field-editor.js.map +1 -1
  38. package/dist/components/builder/insert-context.d.ts +8 -0
  39. package/dist/components/builder/insert-context.js +1 -3
  40. package/dist/components/builder/insert-context.js.map +1 -1
  41. package/dist/components/builder/page-tree.js +1 -3
  42. package/dist/components/builder/page-tree.js.map +1 -1
  43. package/dist/components/builder/property-inspector.js +7 -134
  44. package/dist/components/builder/property-inspector.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +15 -4
  46. package/dist/components/cms/cms-store.js +42 -24
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.d.ts +8 -1
  49. package/dist/components/cms/collection-table.js +75 -11
  50. package/dist/components/cms/collection-table.js.map +1 -1
  51. package/dist/components/cms/column-width.d.ts +25 -0
  52. package/dist/components/cms/column-width.js +47 -0
  53. package/dist/components/cms/column-width.js.map +1 -0
  54. package/dist/components/cms/detail-editor.js +143 -79
  55. package/dist/components/cms/detail-editor.js.map +1 -1
  56. package/dist/components/cms/details-pane.d.ts +21 -8
  57. package/dist/components/cms/details-pane.js +31 -11
  58. package/dist/components/cms/details-pane.js.map +1 -1
  59. package/dist/components/cms/discard-draft.d.ts +32 -0
  60. package/dist/components/cms/discard-draft.js +13 -0
  61. package/dist/components/cms/discard-draft.js.map +1 -0
  62. package/dist/components/cms/document-staging.d.ts +7 -0
  63. package/dist/components/cms/document-staging.js +44 -0
  64. package/dist/components/cms/document-staging.js.map +1 -0
  65. package/dist/components/cms/field-connection.d.ts +22 -0
  66. package/dist/components/cms/field-connection.js +37 -0
  67. package/dist/components/cms/field-connection.js.map +1 -0
  68. package/dist/components/cms/field-control.js +167 -34
  69. package/dist/components/cms/field-control.js.map +1 -1
  70. package/dist/components/cms/prop-fields.d.ts +43 -0
  71. package/dist/components/cms/prop-fields.js +154 -0
  72. package/dist/components/cms/prop-fields.js.map +1 -0
  73. package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
  74. package/dist/components/cms/rich-text/component-embed.js +29 -7
  75. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  76. package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
  77. package/dist/components/cms/rich-text/embed-props.js +106 -0
  78. package/dist/components/cms/rich-text/embed-props.js.map +1 -0
  79. package/dist/components/cms/rich-text/index.d.ts +6 -1
  80. package/dist/components/cms/rich-text/index.js +6 -1
  81. package/dist/components/cms/rich-text/index.js.map +1 -1
  82. package/dist/components/cms/rich-text/lazy.d.ts +19 -0
  83. package/dist/components/cms/rich-text/lazy.js +26 -0
  84. package/dist/components/cms/rich-text/lazy.js.map +1 -0
  85. package/dist/components/cms/rich-text/link-control.js +3 -1
  86. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  87. package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
  88. package/dist/components/cms/rich-text/toolbar-state.js +66 -0
  89. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
  90. package/dist/components/cms/rich-text/toolbar.js +7 -15
  91. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  92. package/dist/components/cms/row-actions.d.ts +28 -0
  93. package/dist/components/cms/row-actions.js +20 -0
  94. package/dist/components/cms/row-actions.js.map +1 -0
  95. package/dist/components/cms/use-document-versions.d.ts +8 -1
  96. package/dist/components/cms/use-document-versions.js +31 -4
  97. package/dist/components/cms/use-document-versions.js.map +1 -1
  98. package/dist/components/cms/version-compare-lazy.d.ts +1 -0
  99. package/dist/components/cms/version-compare-lazy.js +23 -0
  100. package/dist/components/cms/version-compare-lazy.js.map +1 -0
  101. package/dist/components/cms/version-compare-text.d.ts +46 -0
  102. package/dist/components/cms/version-compare-text.js +78 -0
  103. package/dist/components/cms/version-compare-text.js.map +1 -1
  104. package/dist/components/cms/version-compare.d.ts +9 -5
  105. package/dist/components/cms/version-compare.js +217 -64
  106. package/dist/components/cms/version-compare.js.map +1 -1
  107. package/dist/components/cms/versions-pane.js +10 -4
  108. package/dist/components/cms/versions-pane.js.map +1 -1
  109. package/dist/components/components/component-detail.d.ts +5 -3
  110. package/dist/components/components/component-detail.js +41 -8
  111. package/dist/components/components/component-detail.js.map +1 -1
  112. package/dist/components/components/component-preview.d.ts +97 -0
  113. package/dist/components/components/component-preview.js +202 -0
  114. package/dist/components/components/component-preview.js.map +1 -0
  115. package/dist/components/components/components-index.d.ts +5 -3
  116. package/dist/components/components/components-index.js +19 -24
  117. package/dist/components/components/components-index.js.map +1 -1
  118. package/dist/components/components/registry-browser.d.ts +7 -0
  119. package/dist/components/components/registry-browser.js +20 -36
  120. package/dist/components/components/registry-browser.js.map +1 -1
  121. package/dist/components/components/taxonomy.d.ts +51 -0
  122. package/dist/components/components/taxonomy.js +73 -0
  123. package/dist/components/components/taxonomy.js.map +1 -0
  124. package/dist/components/components/transfer-dialog.d.ts +20 -0
  125. package/dist/components/components/transfer-dialog.js +64 -0
  126. package/dist/components/components/transfer-dialog.js.map +1 -0
  127. package/dist/components/components/transfer.d.ts +43 -0
  128. package/dist/components/components/transfer.js +31 -0
  129. package/dist/components/components/transfer.js.map +1 -0
  130. package/dist/components/project-list.js +3 -4
  131. package/dist/components/project-list.js.map +1 -1
  132. package/dist/components/project-switcher.js +3 -2
  133. package/dist/components/project-switcher.js.map +1 -1
  134. package/dist/components/rail-store-status.d.ts +21 -0
  135. package/dist/components/rail-store-status.js +21 -0
  136. package/dist/components/rail-store-status.js.map +1 -0
  137. package/dist/components/redirects/redirects-module.js +4 -8
  138. package/dist/components/redirects/redirects-module.js.map +1 -1
  139. package/dist/components/redirects/redirects-store.js +2 -7
  140. package/dist/components/redirects/redirects-store.js.map +1 -1
  141. package/dist/components/sign-in-needed-badge.d.ts +6 -0
  142. package/dist/components/sign-in-needed-badge.js +13 -0
  143. package/dist/components/sign-in-needed-badge.js.map +1 -0
  144. package/dist/components/studio-project-screen.js +1 -4
  145. package/dist/components/studio-project-screen.js.map +1 -1
  146. package/dist/components/studio-shell.js +1 -4
  147. package/dist/components/studio-shell.js.map +1 -1
  148. package/dist/components/users/users-module.js +5 -7
  149. package/dist/components/users/users-module.js.map +1 -1
  150. package/dist/components/users/users-store.js +2 -7
  151. package/dist/components/users/users-store.js.map +1 -1
  152. package/dist/lib/auth/index.js +1 -1
  153. package/dist/lib/auth/index.js.map +1 -1
  154. package/dist/lib/clipboard/paste-target.js +1 -3
  155. package/dist/lib/clipboard/paste-target.js.map +1 -1
  156. package/dist/lib/cms-views/view-model.d.ts +0 -2
  157. package/dist/lib/cms-views/view-model.js +0 -7
  158. package/dist/lib/cms-views/view-model.js.map +1 -1
  159. package/dist/lib/diff-view-preference.d.ts +7 -0
  160. package/dist/lib/diff-view-preference.js +21 -0
  161. package/dist/lib/diff-view-preference.js.map +1 -0
  162. package/dist/lib/i18n/messages.en.js +94 -33
  163. package/dist/lib/i18n/messages.en.js.map +1 -1
  164. package/dist/lib/local-preference.d.ts +11 -0
  165. package/dist/lib/local-preference.js +26 -0
  166. package/dist/lib/local-preference.js.map +1 -0
  167. package/dist/lib/locale/fallback.d.ts +45 -0
  168. package/dist/lib/locale/fallback.js +85 -0
  169. package/dist/lib/locale/fallback.js.map +1 -1
  170. package/dist/lib/persona/labels.d.ts +6 -0
  171. package/dist/lib/persona/labels.js +8 -0
  172. package/dist/lib/persona/labels.js.map +1 -1
  173. package/dist/lib/query/index.d.ts +1 -1
  174. package/dist/lib/query/index.js +1 -1
  175. package/dist/lib/query/index.js.map +1 -1
  176. package/dist/lib/query/operations-query.d.ts +14 -2
  177. package/dist/lib/query/operations-query.js +11 -6
  178. package/dist/lib/query/operations-query.js.map +1 -1
  179. package/dist/lib/rail.d.ts +0 -6
  180. package/dist/lib/rail.js +6 -25
  181. package/dist/lib/rail.js.map +1 -1
  182. package/dist/lib/theme.d.ts +0 -6
  183. package/dist/lib/theme.js +10 -13
  184. package/dist/lib/theme.js.map +1 -1
  185. package/dist/router/match.d.ts +1 -0
  186. package/dist/router/match.js +4 -0
  187. package/dist/router/match.js.map +1 -1
  188. package/dist/ui/button.d.ts +1 -1
  189. package/package.json +22 -25
@@ -1,15 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, MoreHorizontal, Pencil, Plus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
2
+ import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, MoreHorizontal, Pencil, Plus, RotateCcw, RotateCw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
3
3
  import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
5
- import { BUILTIN_FIELD_CONTROLS, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, isFieldHidden, parentRelationField, readAssetId, } from '@elytracms/core/cms-core';
5
+ import { BUILTIN_FIELD_CONTROLS, applySlugify, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, findSlugCollision, isFieldHidden, parentRelationField, readAssetId, resolveSlugPath, slugSourceValue, } from '@elytracms/core/cms-core';
6
+ import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
6
7
  import { Button, Card, Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, Input, Row, RowItem, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
7
8
  import { cn } from '@elytracms/studio/lib/utils';
8
9
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
10
+ import { activeLocaleValues } from '@elytracms/studio/lib/locale';
9
11
  import { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection';
10
12
  import { useOptionalAssets } from '../assets/assets-store';
11
13
  import { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb';
12
14
  import { newlyAddedAssetIds } from '../assets/focal-point';
15
+ import { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata';
13
16
  import { fieldLabel } from './cms-store';
14
17
  import { useCollectionView } from '../collection-view/collection-view-context';
15
18
  import { useFieldDensity } from './field-density';
@@ -50,7 +53,26 @@ export function FieldControl(props) {
50
53
  }
51
54
  return builtIn;
52
55
  }
56
+ /** The conventional slug field name (mirrors `url-for-document`'s `SLUG_FIELD`). */
57
+ const SLUG_FIELD_NAME = 'slug';
58
+ /** The built-in control name that opts any `text` field into the slug control (EC-315). */
59
+ const SLUG_CONTROL = 'slug';
60
+ /**
61
+ * EC-315: a `text` field is rendered by the smart slug control when it opts in via
62
+ * `form.control: 'slug'`, OR (the default auto-upgrade) when it is simply named
63
+ * `slug` — the long-standing convention `url-for-document`/hierarchy already key off.
64
+ */
65
+ function isSlugControl(field) {
66
+ return field.type === 'text' && (field.form?.control === SLUG_CONTROL || field.name === SLUG_FIELD_NAME);
67
+ }
53
68
  function BuiltInFieldControl({ field, value, onChange, readOnly, id }) {
69
+ // EC-315: the smart slug control (composed path, auto-slug, per-locale segments)
70
+ // subsumes the old inline `slug` input + PathPreview. It degrades to a plain input
71
+ // when the collection isn't routable (name-based auto-upgrade only) or no document
72
+ // editor context is present (e.g. a table-cell picker).
73
+ if (isSlugControl(field)) {
74
+ return _jsx(SlugField, { field: field, value: value, onChange: onChange, readOnly: readOnly, id: id });
75
+ }
54
76
  switch (field.type) {
55
77
  case 'boolean':
56
78
  return (_jsxs("label", { className: "flex h-8 items-center gap-2 text-sm", children: [_jsx("input", { id: id, type: "checkbox", checked: value === true, disabled: readOnly, onChange: (e) => onChange(e.target.checked), className: "size-4 accent-primary disabled:opacity-50" }), _jsx(Text, { size: "sm", tone: "muted", children: translate(value === true ? 'common.true' : 'common.false') })] }));
@@ -99,6 +121,144 @@ export function SegmentedOptions({ field, value, onChange, readOnly, id, }) {
99
121
  return (_jsx(Button, { type: "button", variant: active ? 'default' : 'outline', size: "sm", role: "radio", "aria-checked": active, title: localizedLabel(opt.description) || undefined, disabled: readOnly, onClick: () => onChange(opt.value), children: localizedLabel(opt.label, opt.value) }, opt.value));
100
122
  }) }));
101
123
  }
124
+ /**
125
+ * EC-315 — the smart slug / URL-segment control. A `text` field named `slug` (or any
126
+ * `text` field with `form.control: 'slug'`) renders here instead of a bare input:
127
+ *
128
+ * - **Composed path**: the route-derived URL, decomposed by {@link resolveSlugPath}
129
+ * into read-only parent/context segments + the editable slug segment inline + the
130
+ * locale prefix (non-default locales). Reuses the same `urlForDocument` the live
131
+ * site uses, so the preview matches what visitors get. Draft-derived (updates as
132
+ * you type); the live "View live" button in the header stays the published open.
133
+ * - **Auto-slug**: while the slug is empty or was last produced by auto-derivation,
134
+ * it live-derives from the source field (default: the collection's `titleField`).
135
+ * A manual edit stops the auto-fill; the ↻ button always re-derives.
136
+ * - **Per-locale**: reads/writes through the shared `value`/`onChange` seam, so a
137
+ * `localized` slug edits the active locale's segment with no bespoke locale code.
138
+ * - **Uniqueness**: an inline warning when the candidate collides with a sibling in
139
+ * the same scope (same parent when hierarchical, same locale). A custom
140
+ * `slug.unique` hook replaces the default sibling scan.
141
+ *
142
+ * Degrades to a plain input when there is no document editor context (a table-cell
143
+ * picker) or when a name-based auto-upgrade lands on a non-routable collection.
144
+ */
145
+ function SlugField({ field, value, onChange, readOnly, id, }) {
146
+ const source = useCollectionView();
147
+ const editor = useOptionalDocumentEditor();
148
+ const slugConfig = field.slug;
149
+ const explicit = field.form?.control === SLUG_CONTROL;
150
+ const current = typeof value === 'string' ? value : '';
151
+ const locale = source.activeLocale;
152
+ const localeConfig = source.localeConfig;
153
+ // 0.0.39 regression fix: resolve the deployment routes on EVERY render (a cheap
154
+ // synchronous accessor), never freeze them at first paint. The earlier
155
+ // `useMemo(() => resolveDeploymentRoutes(), [])` captured the EMPTY route set from
156
+ // before the workspace/config finished loading — so the composed path never
157
+ // appeared once routes arrived, and a stale set could paint a misleading prefix.
158
+ const routes = resolveDeploymentRoutes();
159
+ const routable = routes.some((r) => r.document?.collection === source.collectionId);
160
+ const draft = editor?.draft;
161
+ // The active-locale value bag the auto-slug source derives from (title, etc.).
162
+ const values = useMemo(() => (draft ? activeLocaleValues(draft, locale) : {}), [draft, locale]);
163
+ const derived = applySlugify(slugConfig, slugSourceValue(slugConfig, source.schema, values));
164
+ // Track whether the slug was manually set — auto-derive stays active while the
165
+ // slug is empty or was last produced by auto-derivation (never overwrites a manual
166
+ // slug). Seeded true when the loaded document already carries a slug.
167
+ const [manuallyEdited, setManuallyEdited] = useState(() => current.trim() !== '');
168
+ const onChangeRef = useRef(onChange);
169
+ onChangeRef.current = onChange;
170
+ useEffect(() => {
171
+ if (readOnly || manuallyEdited)
172
+ return;
173
+ if (derived && derived !== current)
174
+ onChangeRef.current(derived);
175
+ }, [derived, manuallyEdited, readOnly, current]);
176
+ // The composed path parts (routable collections with a document editor only).
177
+ const siblings = draft ? source.documents() : [];
178
+ // Draft-derived (routes + locale only, no `frontendUrl`) — the composed path is a
179
+ // read-only preview that updates as you type, mirroring the detail editor's draft
180
+ // URL. The header's published-gated "View live" stays the sole open-live action,
181
+ // so the draft/live distinction is preserved (an unpublished slug never links out).
182
+ const parts = draft && routable
183
+ ? resolveSlugPath({ routes, localeConfig }, source.schema, draft, { siblings, locale })
184
+ : undefined;
185
+ // Default (sync) uniqueness — the first colliding sibling's title, or null.
186
+ const defaultCollisionTitle = useMemo(() => {
187
+ if (slugConfig?.unique || !draft)
188
+ return null;
189
+ const trimmed = current.trim();
190
+ if (trimmed === '')
191
+ return null;
192
+ const hit = findSlugCollision(trimmed, draft, source.documents(), {
193
+ collection: source.schema,
194
+ locale,
195
+ localeConfig,
196
+ });
197
+ return hit ? source.titleOf({ collection: source.collectionId, id: hit.id }) : null;
198
+ }, [current, draft, slugConfig, locale, localeConfig, source]);
199
+ // Custom (possibly async) uniqueness hook — race-guarded.
200
+ const [asyncTaken, setAsyncTaken] = useState(false);
201
+ useEffect(() => {
202
+ const unique = slugConfig?.unique;
203
+ if (!unique || !draft) {
204
+ setAsyncTaken(false);
205
+ return;
206
+ }
207
+ const trimmed = current.trim();
208
+ if (trimmed === '') {
209
+ setAsyncTaken(false);
210
+ return;
211
+ }
212
+ let cancelled = false;
213
+ Promise.resolve(unique(trimmed, { document: draft, locale, siblings: source.documents() }))
214
+ .then((ok) => {
215
+ if (!cancelled)
216
+ setAsyncTaken(ok === false);
217
+ })
218
+ .catch(() => {
219
+ if (!cancelled)
220
+ setAsyncTaken(false);
221
+ });
222
+ return () => {
223
+ cancelled = true;
224
+ };
225
+ }, [current, slugConfig, draft, locale, source]);
226
+ const collisionMessage = slugConfig?.unique
227
+ ? asyncTaken
228
+ ? translate('cms.field.slug.takenGeneric')
229
+ : undefined
230
+ : defaultCollisionTitle
231
+ ? translate('cms.field.slug.taken', { title: defaultCollisionTitle })
232
+ : undefined;
233
+ function handleInput(next) {
234
+ onChange(next || undefined);
235
+ // Clearing re-enables auto-derive; any non-empty entry is a manual override.
236
+ setManuallyEdited(next.trim() !== '');
237
+ }
238
+ function regenerate() {
239
+ setManuallyEdited(false);
240
+ const next = applySlugify(slugConfig, slugSourceValue(slugConfig, source.schema, values));
241
+ onChange(next || undefined);
242
+ }
243
+ // No document editor (a picker), or a name-based auto-upgrade on a non-routable
244
+ // collection: keep the historical plain input, no slug chrome (no regression).
245
+ if (!editor || (!explicit && !routable)) {
246
+ return (_jsx(Input, { id: id, disabled: readOnly, value: current, onChange: (e) => onChange(e.target.value || undefined) }));
247
+ }
248
+ const input = (_jsx(Input, { id: id, disabled: readOnly, value: current, placeholder: derived || undefined, onChange: (e) => handleInput(e.target.value), className: cn(parts ? 'h-7 min-w-[10ch] flex-1 bg-background font-mono text-xs' : 'flex-1') }));
249
+ const regenerateButton = !readOnly ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: regenerate, "aria-label": translate('cms.field.slug.regenerate'), title: translate('cms.field.slug.regenerate'), children: _jsx(RotateCw, {}) })) : null;
250
+ // The compact, ellipsized read-only prefix (0.0.39 "hybrid split"): only the LAST
251
+ // context segment (`…/produkte/`), so a deep path can't overflow the narrow Details
252
+ // sidebar. The full composed path is the tooltip AND the document-level URL row in
253
+ // the Details pane (which owns the composed URL — a first-class document property).
254
+ const prefixSegments = parts
255
+ ? [...(parts.localePrefix ? [parts.localePrefix] : []), ...parts.parentSegments]
256
+ : [];
257
+ const lastSegment = prefixSegments[prefixSegments.length - 1];
258
+ const compactPrefix = lastSegment ? `${prefixSegments.length > 1 ? '…/' : '/'}${lastSegment}/` : '/';
259
+ const prefixTitle = parts?.path ?? (prefixSegments.length > 0 ? `/${prefixSegments.join('/')}/` : '/');
260
+ return (_jsxs(Stack, { gap: "2", children: [parts ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-composed-path", children: [_jsx("span", { className: "shrink-0 rounded bg-muted/40 px-1.5 py-1 font-mono text-xs text-muted-foreground ring-1 ring-inset ring-border", title: prefixTitle, "data-testid": "slug-prefix", children: compactPrefix }), input, regenerateButton] })) : (_jsxs(Row, { gap: "1", align: "center", children: [input, regenerateButton] })), collisionMessage ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-collision", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", tone: "muted", children: collisionMessage })] })) : null] }));
261
+ }
102
262
  function asRecord(value) {
103
263
  return value !== null && typeof value === 'object' && !Array.isArray(value)
104
264
  ? value
@@ -522,10 +682,7 @@ function AssetField({ field, value, onChange, readOnly, }) {
522
682
  // is the belt to the picker's stopPropagation braces: the field cannot react at all.
523
683
  const dragProps = canUpload && !open ? { onDragEnter, onDragOver, onDragLeave, onDrop } : {};
524
684
  const selectButton = !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(ImagePlus, {}), " ", translate('common.select')] })) : null;
525
- 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 ? (
526
- // Gallery with per-use sub-fields (EC-301): a card per placement — thumbnail +
527
- // its own sub-field form — reusing the object/repeater item rendering.
528
- _jsxs(Stack, { gap: "2", children: [entries.length > 0 ? (_jsx(Stack, { gap: "2", children: entries.map((entry, i) => {
685
+ 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) => {
529
686
  const aid = readAssetId(entry);
530
687
  if (!aid)
531
688
  return null;
@@ -534,12 +691,7 @@ function AssetField({ field, value, onChange, readOnly, }) {
534
691
  nextEntries[i] = assetNode(aid, next);
535
692
  onChange(nextEntries);
536
693
  }, onRemove: readOnly ? undefined : () => onChange(entries.filter((_, j) => j !== i)) }, `${aid}-${i}`));
537
- }) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : many ? (
538
- // Gallery: a grid of focal-aware previews + the single Select entry.
539
- _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 ? (
540
- // With per-use sub-fields the asset + its fields read as ONE grouped unit
541
- // (like an object), so it's clear they belong together (EC-301 feedback).
542
- _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
694
+ }) })) : (_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
543
695
  ? translate('cms.asset.rejected.one', {
544
696
  name: rejected[0],
545
697
  types: describeAccept(field.accept),
@@ -605,25 +757,6 @@ function AssetGalleryItem({ assetId, collectionId, fields, value, onChangeSubs,
605
757
  const title = source.titleOf({ collection: collectionId, id: assetId });
606
758
  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 }) })] }));
607
759
  }
608
- /**
609
- * System columns the upload pipeline owns — never editable metadata, so they're
610
- * excluded from the asset collection's schema-driven options form (EC-301). The
611
- * filename + dimensions already show in the field's info column.
612
- */
613
- const SYSTEM_ASSET_FIELDS = new Set([
614
- 'filename',
615
- 'url',
616
- 'contentType',
617
- 'byteSize',
618
- 'width',
619
- 'height',
620
- 'storageKey',
621
- 'title',
622
- ]);
623
- /** Drop `undefined` values so the persisted meta bag stays JSON/Convex-clean. */
624
- function pruneMeta(meta) {
625
- return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
626
- }
627
760
  /**
628
761
  * The image options dialog (EC-301): the focal point (engine-special visual
629
762
  * control) + the asset's **schema-driven** metadata, rendered from the
@@ -638,9 +771,9 @@ function AssetOptionsDialog({ assetId, readOnly, open, onOpenChange, }) {
638
771
  const assets = useOptionalAssets();
639
772
  const record = useAssetRecord(assetId);
640
773
  const metaFields = useMemo(() => {
641
- const assetId = source.assetCollectionId ?? 'asset';
642
- const collection = source.collections.find((c) => c.id === assetId);
643
- return (collection?.fields ?? []).filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly);
774
+ const assetCollectionId = source.assetCollectionId ?? 'asset';
775
+ const collection = source.collections.find((c) => c.id === assetCollectionId);
776
+ return collection ? assetMetaFieldsOf(collection) : [];
644
777
  }, [source]);
645
778
  // Local edit buffers, seeded once per open from the record (so an external edit
646
779
  // still wins between opens). Metadata is non-localized in v1 (the asset