@elytracms/studio 0.0.70 → 0.0.71

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 (61) hide show
  1. package/dist/components/account/self-account-view-source.js +1 -0
  2. package/dist/components/account/self-account-view-source.js.map +1 -1
  3. package/dist/components/assets/assets-collection-view-source.js +1 -0
  4. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  5. package/dist/components/cms/collection-table.js +19 -2
  6. package/dist/components/cms/collection-table.js.map +1 -1
  7. package/dist/components/cms/detail-editor.js +73 -51
  8. package/dist/components/cms/detail-editor.js.map +1 -1
  9. package/dist/components/cms/duplicate-document.d.ts +45 -0
  10. package/dist/components/cms/duplicate-document.js +141 -0
  11. package/dist/components/cms/duplicate-document.js.map +1 -0
  12. package/dist/components/cms/field-anchors.d.ts +67 -0
  13. package/dist/components/cms/field-anchors.js +91 -0
  14. package/dist/components/cms/field-anchors.js.map +1 -0
  15. package/dist/components/cms/issue-copy.d.ts +45 -0
  16. package/dist/components/cms/issue-copy.js +89 -0
  17. package/dist/components/cms/issue-copy.js.map +1 -0
  18. package/dist/components/cms/issues-pane.d.ts +11 -2
  19. package/dist/components/cms/issues-pane.js +22 -4
  20. package/dist/components/cms/issues-pane.js.map +1 -1
  21. package/dist/components/cms/object-array-field.d.ts +2 -0
  22. package/dist/components/cms/object-array-field.js +75 -21
  23. package/dist/components/cms/object-array-field.js.map +1 -1
  24. package/dist/components/cms/row-actions.d.ts +10 -3
  25. package/dist/components/cms/row-actions.js +8 -0
  26. package/dist/components/cms/row-actions.js.map +1 -1
  27. package/dist/components/cms/use-field-focus.d.ts +25 -9
  28. package/dist/components/cms/use-field-focus.js +80 -15
  29. package/dist/components/cms/use-field-focus.js.map +1 -1
  30. package/dist/components/collection-view/cms-collection-view-source.js +1 -0
  31. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  32. package/dist/components/redirects/redirects-collection-view-source.js +1 -0
  33. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  34. package/dist/components/users/users-collection-view-source.js +1 -0
  35. package/dist/components/users/users-collection-view-source.js.map +1 -1
  36. package/dist/customization.d.ts +9 -0
  37. package/dist/customization.js +8 -0
  38. package/dist/customization.js.map +1 -1
  39. package/dist/lib/collection-view/collection-view-source.d.ts +8 -0
  40. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  41. package/dist/lib/i18n/messages.de.js +38 -1
  42. package/dist/lib/i18n/messages.de.js.map +1 -1
  43. package/dist/lib/i18n/messages.en.d.ts +34 -0
  44. package/dist/lib/i18n/messages.en.js +41 -1
  45. package/dist/lib/i18n/messages.en.js.map +1 -1
  46. package/dist/lib/persona/labels.d.ts +5 -1
  47. package/dist/lib/persona/labels.js +43 -30
  48. package/dist/lib/persona/labels.js.map +1 -1
  49. package/dist/previews/enumerate.d.ts +68 -0
  50. package/dist/previews/enumerate.js +78 -0
  51. package/dist/previews/enumerate.js.map +1 -0
  52. package/dist/previews/harness.d.ts +50 -0
  53. package/dist/previews/harness.js +123 -0
  54. package/dist/previews/harness.js.map +1 -0
  55. package/dist/previews/manifest-format.d.ts +57 -0
  56. package/dist/previews/manifest-format.js +59 -0
  57. package/dist/previews/manifest-format.js.map +1 -0
  58. package/dist/ui/field.d.ts +24 -1
  59. package/dist/ui/field.js +23 -2
  60. package/dist/ui/field.js.map +1 -1
  61. package/package.json +10 -6
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Link, STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
3
- import { Check, ChevronDown, CircleAlert, CircleCheck, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
3
+ import { Check, ChevronDown, CircleAlert, CircleCheck, Copy, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
4
4
  import { useCallback, useMemo, useState } from 'react';
5
5
  import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, documentKey, groupOrder, groupSectionOf, hasAliases, hiddenGroupNames, isBlocksField, isFieldHidden, isFieldLocalized, isSingleton, resolveDocumentForLocale, resolveDocumentTitle, resolveField, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, } from '@elytracms/core/cms-core';
6
6
  import { blockRootsOf } from '@elytracms/core/project-graph';
@@ -13,6 +13,7 @@ import { LocaleSwitcher, activeLocaleValues, displayedFieldFallback, fieldConnec
13
13
  import { FallbackChip } from './cell.js';
14
14
  import { FieldConnectionControl } from './field-connection.js';
15
15
  import { FieldControl } from './field-control.js';
16
+ import { duplicateFromSource } from './duplicate-document.js';
16
17
  // EC-322: the lazy-wrapped page builder — the heavy canvas subtree code-splits out
17
18
  // of the detail-view critical path and downloads only when a `blocks` field opens.
18
19
  import { CompositionFieldEditor } from '../builder/composition-field-editor-lazy.js';
@@ -44,6 +45,7 @@ import { usePublishedPerspective } from './use-published-perspective.js';
44
45
  import { useSaveMode } from './use-save-mode.js';
45
46
  import { useDiscardController } from './use-discard-controller.js';
46
47
  import { usePublishController, publishGate } from './use-publish-controller.js';
48
+ import { FieldFocusRequestContext, FieldPathContext } from './field-anchors.js';
47
49
  import { useFieldFocus } from './use-field-focus.js';
48
50
  import { useCompareSelection } from './use-compare-selection.js';
49
51
  import { useLiveUrl } from './use-live-url.js';
@@ -221,10 +223,12 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
221
223
  // not a field-row — it takes over its tab with the full page-builder canvas.
222
224
  // Convention: such a field lives on its own dedicated fieldset tab.
223
225
  const canvasField = tabFields.find(isBlocksField);
224
- // EC-172 / EC-283 — the field-focus/highlight controller (jump-to-field from the
225
- // reference deep-link `focusPath` and the Issues pane). Owns the highlight + the
226
- // controlled inspector-pane state and the scroll/clear timers.
227
- const { highlightedField, inspectorPaneId, setInspectorPaneId, focusField } = useFieldFocus({
226
+ // EC-172 / EC-283 / EC-443 — the field-focus/highlight controller (jump-to-field
227
+ // from the reference deep-link `focusPath` and the Issues pane), now path-aware:
228
+ // `focusRequest` is provided below via `FieldFocusRequestContext` so nested
229
+ // array/object controls can auto-expand and ring the exact target. Owns the
230
+ // highlight + the controlled inspector-pane state and the scroll/clear timers.
231
+ const { highlightedField, focusRequest, inspectorPaneId, setInspectorPaneId, focusField } = useFieldFocus({
228
232
  collection,
229
233
  source,
230
234
  focusPath,
@@ -314,7 +318,7 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
314
318
  const connection = localized
315
319
  ? fieldConnection(collection, draft, field.name, locale, source.localeConfig)
316
320
  : null;
317
- return (_jsx(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {}), connection && !fieldReadOnly ? (_jsx(FieldConnectionControl, { connection: connection, fieldName: field.name, locale: locale, onFillFromBase: () => setDraft((d) => fillFieldFromBase(collection, d, field.name, locale, source.localeConfig)), onResetToBase: () => setDraft((d) => resetFieldToBase(collection, d, field.name, locale)) })) : null] }), htmlFor: `f-${field.name}`, hint: localizedLabel(field.form?.description) || undefined, error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: _jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }) }, field.name));
321
+ return (_jsx(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {}), connection && !fieldReadOnly ? (_jsx(FieldConnectionControl, { connection: connection, fieldName: field.name, locale: locale, onFillFromBase: () => setDraft((d) => fillFieldFromBase(collection, d, field.name, locale, source.localeConfig)), onResetToBase: () => setDraft((d) => resetFieldToBase(collection, d, field.name, locale)) })) : null] }), htmlFor: `f-${field.name}`, fieldPath: field.name, hint: localizedLabel(field.form?.description) || undefined, error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: _jsx(FieldPathContext.Provider, { value: field.name, children: _jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }) }) }, field.name));
318
322
  }
319
323
  // The frame-owned `Versions` pane (EC-224) — appended after any view-supplied
320
324
  // pane (the canvas's `Properties`). One instance feeds both the form view's
@@ -390,6 +394,24 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
390
394
  dirty,
391
395
  hasUnpublishedRefs,
392
396
  });
397
+ // EC-442 (amended): Duplicate in the header `…` menu — gated on the explicit
398
+ // `duplicate` capability (CMS collections only, owner call 2026-07-17) + a
399
+ // detail surface to land on, and never a singleton (its shape IS "exactly one
400
+ // document"; a copy would be EC-274 drift by construction, mirroring the
401
+ // hidden Delete).
402
+ const canDuplicate = !singleton && source.capabilities.duplicate && source.detailPath !== undefined;
403
+ function handleDuplicate() {
404
+ // Copy the CURRENT draft buffer — what the editor sees — not the possibly
405
+ // autosave-lagged stored row. The duplicate is a fresh DRAFT, never
406
+ // auto-published (live-is-published-only), created through the seam's
407
+ // existing create+save write paths (`duplicateFromSource`).
408
+ const newRef = duplicateFromSource(source, draft);
409
+ toast.success(translate('cms.editor.duplicateCreated'));
410
+ const splat = source.detailPath?.(newRef.id);
411
+ if (splat === undefined)
412
+ return;
413
+ void navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: splat } });
414
+ }
393
415
  function handleConfirmedAction() {
394
416
  if (confirmAction === 'unpublish') {
395
417
  setConfirmAction(null);
@@ -483,51 +505,51 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
483
505
  setCompareBase,
484
506
  setCompareTarget,
485
507
  exitCompare,
486
- }, children: _jsxs("div", { className: cn('flex h-full min-h-0 flex-col', viewReadOnly && 'bg-info/[0.04]'), children: [_jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", align: "start", children: [_jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: singleton ? localizedLabel(collection.form?.label, collection.id) : title || doc.id }), singleton ? null : _jsx(FallbackChip, { fallback: titleFallback, testId: "doc-title-fallback" })] }), routableCollection ? (_jsx(DocumentUrlPath, { projectId: projectId, collection: collection, doc: draft, siblings: source.documents(), routes: resolveDeploymentRoutes(), localeConfig: source.localeConfig, locale: locale, live: headerLive })) : null] }), _jsx(Spacer, {}), _jsxs(Stack, { gap: "none", align: "end", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", "data-testid": "autosave-status", children: source.lastError
487
- ? translate('cms.editor.saveFailed')
488
- : explicitSave
489
- ? dirty
490
- ? translate('cms.editor.unsavedChanges')
491
- : translate('cms.editor.saved')
492
- : dirty
493
- ? translate('cms.editor.saving')
494
- : translate('cms.editor.saved') }), source.lastError?.message ? (_jsx(Text, { size: "xs", className: "max-w-64 truncate text-destructive", title: source.lastError.message, "data-testid": "save-error-message", children: source.lastError.message })) : null] }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !viewReadOnly ? (_jsx(TranslateDocumentAction, { projectId: projectId, collection: collection, docRef: ref, document: draft, localeConfig: source.localeConfig, localesEnabled: canLocales, onApplied: (next) => {
495
- source.save(next);
496
- replaceDraft(next);
497
- } })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
498
- ? 'bg-accent text-accent-foreground'
499
- : 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: perspective === 'draft', onClick: () => setPerspective('draft') }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective('draft'), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
500
- ? translate('cms.editor.loadingPublishedVersion')
501
- : translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => setValue(canvasField, v), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${restoreNonce}:${perspective}`) })) : activeTab === REFERENCES_TAB ? (_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsxs(Stack, { gap: "6", children: [groups.map((group) => {
502
- // EC-306: a conditionally/statically hidden group section is skipped
503
- // entirely (its fields are also exempt from validation).
504
- if (hiddenGroups.has(group.name))
505
- return null;
506
- // EC-273/EC-240: drop a fieldset whose every field is hidden — whether
507
- // content-conditionally or for this principal — so the form never
508
- // shows an empty section header.
509
- const visible = group.fields.filter(isFieldShown);
510
- if (visible.length === 0)
511
- return null;
512
- const section = group.section;
513
- // EC-241: a section `title` is a LocalizedString ({ en, de } or a plain
514
- // string) — resolve it against the active UI locale before rendering.
515
- const sectionTitle = section?.title !== undefined ? localizedLabel(section.title, group.name) : undefined;
516
- return (_jsx(CollapsibleSection, { collapsible: section?.collapsible, defaultOpen: section?.defaultOpen, title: _jsx(Text, { size: "sm", weight: "semibold", className: sectionTitle || group.name === DEFAULT_FIELD_GROUP ? undefined : 'capitalize', children: sectionTitle ??
517
- (group.name === DEFAULT_FIELD_GROUP
518
- ? translate('cms.editor.defaultGroup')
519
- : group.name) }), children: _jsx("div", { className: "grid grid-cols-1 gap-4 p-4 md:grid-cols-12", children: visible.map((field) => (_jsx("div", { className: cn('col-span-1', fieldWidthColSpan(field.form?.width)), children: renderFieldRow(field) }, field.name))) }) }, group.name));
520
- }), formAppendix] }) }) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })), explicitSave ? (_jsx(UnsavedChangesGuard, { when: dirty, onSave: () => {
521
- save();
522
- return true;
523
- }, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
524
- if (!open)
525
- setConfirmAction(null);
526
- }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
527
- setDiscardConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.discardDraftTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.discardDraftDescription') })] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDiscardConfirm(false), disabled: discardBusy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", "data-testid": "discard-draft-confirm", disabled: discardBusy, onClick: () => void handleDiscardDraft(), children: discardBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.discardingDraft')] })) : (_jsxs(_Fragment, { children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraftConfirm')] })) })] })] }) }), _jsx(Dialog, { open: publishConfirm, onOpenChange: (open) => { if (!open)
528
- cancelPublishConfirm(); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.publishUnpublishedRefsTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.publishUnpublishedRefsDescription') })] }), _jsxs(Stack, { gap: "1", children: [referenceIssues.map((issue, i) => (_jsxs(Text, { size: "xs", tone: "muted", children: ["\u2022 ", issue.message] }, `${issue.meta?.id ?? i}`))), schemaIssues.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", className: "mt-1", children: translate(schemaIssues.length === 1
529
- ? 'cms.editor.publishAlsoHasIssues.one'
530
- : 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: cancelPublishConfirm, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: confirmPublish, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }));
508
+ }, children: _jsx(FieldFocusRequestContext.Provider, { value: focusRequest, children: _jsxs("div", { className: cn('flex h-full min-h-0 flex-col', viewReadOnly && 'bg-info/[0.04]'), children: [_jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", align: "start", children: [_jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: singleton ? localizedLabel(collection.form?.label, collection.id) : title || doc.id }), singleton ? null : _jsx(FallbackChip, { fallback: titleFallback, testId: "doc-title-fallback" })] }), routableCollection ? (_jsx(DocumentUrlPath, { projectId: projectId, collection: collection, doc: draft, siblings: source.documents(), routes: resolveDeploymentRoutes(), localeConfig: source.localeConfig, locale: locale, live: headerLive })) : null] }), _jsx(Spacer, {}), _jsxs(Stack, { gap: "none", align: "end", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", "data-testid": "autosave-status", children: source.lastError
509
+ ? translate('cms.editor.saveFailed')
510
+ : explicitSave
511
+ ? dirty
512
+ ? translate('cms.editor.unsavedChanges')
513
+ : translate('cms.editor.saved')
514
+ : dirty
515
+ ? translate('cms.editor.saving')
516
+ : translate('cms.editor.saved') }), source.lastError?.message ? (_jsx(Text, { size: "xs", className: "max-w-64 truncate text-destructive", title: source.lastError.message, "data-testid": "save-error-message", children: source.lastError.message })) : null] }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !viewReadOnly ? (_jsx(TranslateDocumentAction, { projectId: projectId, collection: collection, docRef: ref, document: draft, localeConfig: source.localeConfig, localesEnabled: canLocales, onApplied: (next) => {
517
+ source.save(next);
518
+ replaceDraft(next);
519
+ } })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [canDuplicate ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-duplicate", onClick: handleDuplicate, children: [_jsx(Copy, {}), " ", translate('common.duplicate')] })) : null, discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
520
+ ? 'bg-accent text-accent-foreground'
521
+ : 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: perspective === 'draft', onClick: () => setPerspective('draft') }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective('draft'), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
522
+ ? translate('cms.editor.loadingPublishedVersion')
523
+ : translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => setValue(canvasField, v), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${restoreNonce}:${perspective}`) })) : activeTab === REFERENCES_TAB ? (_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsxs(Stack, { gap: "6", children: [groups.map((group) => {
524
+ // EC-306: a conditionally/statically hidden group section is skipped
525
+ // entirely (its fields are also exempt from validation).
526
+ if (hiddenGroups.has(group.name))
527
+ return null;
528
+ // EC-273/EC-240: drop a fieldset whose every field is hidden — whether
529
+ // content-conditionally or for this principal — so the form never
530
+ // shows an empty section header.
531
+ const visible = group.fields.filter(isFieldShown);
532
+ if (visible.length === 0)
533
+ return null;
534
+ const section = group.section;
535
+ // EC-241: a section `title` is a LocalizedString ({ en, de } or a plain
536
+ // string) — resolve it against the active UI locale before rendering.
537
+ const sectionTitle = section?.title !== undefined ? localizedLabel(section.title, group.name) : undefined;
538
+ return (_jsx(CollapsibleSection, { collapsible: section?.collapsible, defaultOpen: section?.defaultOpen, title: _jsx(Text, { size: "sm", weight: "semibold", className: sectionTitle || group.name === DEFAULT_FIELD_GROUP ? undefined : 'capitalize', children: sectionTitle ??
539
+ (group.name === DEFAULT_FIELD_GROUP
540
+ ? translate('cms.editor.defaultGroup')
541
+ : group.name) }), children: _jsx("div", { className: "grid grid-cols-1 gap-4 p-4 md:grid-cols-12", children: visible.map((field) => (_jsx("div", { className: cn('col-span-1', fieldWidthColSpan(field.form?.width)), children: renderFieldRow(field) }, field.name))) }) }, group.name));
542
+ }), formAppendix] }) }) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })), explicitSave ? (_jsx(UnsavedChangesGuard, { when: dirty, onSave: () => {
543
+ save();
544
+ return true;
545
+ }, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
546
+ if (!open)
547
+ setConfirmAction(null);
548
+ }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
549
+ setDiscardConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.discardDraftTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.discardDraftDescription') })] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDiscardConfirm(false), disabled: discardBusy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", "data-testid": "discard-draft-confirm", disabled: discardBusy, onClick: () => void handleDiscardDraft(), children: discardBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.discardingDraft')] })) : (_jsxs(_Fragment, { children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraftConfirm')] })) })] })] }) }), _jsx(Dialog, { open: publishConfirm, onOpenChange: (open) => { if (!open)
550
+ cancelPublishConfirm(); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.publishUnpublishedRefsTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.publishUnpublishedRefsDescription') })] }), _jsxs(Stack, { gap: "1", children: [referenceIssues.map((issue, i) => (_jsxs(Text, { size: "xs", tone: "muted", children: ["\u2022 ", issue.message] }, `${issue.meta?.id ?? i}`))), schemaIssues.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", className: "mt-1", children: translate(schemaIssues.length === 1
551
+ ? 'cms.editor.publishAlsoHasIssues.one'
552
+ : 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: cancelPublishConfirm, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: confirmPublish, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }) }));
531
553
  }
532
554
  /**
533
555
  * EC-300/AD-16 — errors for composition embeds (hard references) whose target isn't