@elytracms/studio 0.0.66 → 0.0.68

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 (48) hide show
  1. package/dist/components/builder/property-inspector.d.ts +0 -21
  2. package/dist/components/builder/property-inspector.js +5 -22
  3. package/dist/components/builder/property-inspector.js.map +1 -1
  4. package/dist/components/cms/asset-picker-core.d.ts +52 -0
  5. package/dist/components/cms/asset-picker-core.js +69 -0
  6. package/dist/components/cms/asset-picker-core.js.map +1 -0
  7. package/dist/components/cms/asset-picker.d.ts +16 -4
  8. package/dist/components/cms/asset-picker.js +34 -50
  9. package/dist/components/cms/asset-picker.js.map +1 -1
  10. package/dist/components/cms/detail-editor.js +45 -54
  11. package/dist/components/cms/detail-editor.js.map +1 -1
  12. package/dist/components/cms/prop-visibility.d.ts +3 -0
  13. package/dist/components/cms/prop-visibility.js +31 -0
  14. package/dist/components/cms/prop-visibility.js.map +1 -0
  15. package/dist/components/cms/rich-text/component-embed.d.ts +8 -6
  16. package/dist/components/cms/rich-text/component-embed.js +21 -62
  17. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  18. package/dist/components/cms/rich-text/editor.d.ts +13 -3
  19. package/dist/components/cms/rich-text/editor.js +36 -21
  20. package/dist/components/cms/rich-text/editor.js.map +1 -1
  21. package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
  22. package/dist/components/cms/rich-text/embed-props.js +102 -45
  23. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  24. package/dist/lib/i18n/messages.de.js +1 -2
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +0 -1
  27. package/dist/lib/i18n/messages.en.js +1 -2
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/ui/expandable-surface.d.ts +35 -25
  30. package/dist/ui/expandable-surface.js +41 -21
  31. package/dist/ui/expandable-surface.js.map +1 -1
  32. package/dist/ui/index.d.ts +1 -0
  33. package/dist/ui/index.js +1 -0
  34. package/dist/ui/index.js.map +1 -1
  35. package/dist/ui/surface-geometry.d.ts +68 -0
  36. package/dist/ui/surface-geometry.js +61 -0
  37. package/dist/ui/surface-geometry.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/styles/studio.css +30 -0
  40. package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
  41. package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
  42. package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
  43. package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
  44. package/dist/components/cms/rich-text/embed-inspector.js +0 -48
  45. package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
  46. package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
  47. package/dist/components/cms/use-embed-inspector-host.js +0 -17
  48. package/dist/components/cms/use-embed-inspector-host.js.map +0 -1
@@ -34,10 +34,6 @@ import { VersionCompare } from './version-compare-lazy.js';
34
34
  import { IssuesPane } from './issues-pane.js';
35
35
  import { DetailsPane } from './details-pane.js';
36
36
  import { DocumentUrlPath } from './document-url-path.js';
37
- // 0.0.41: selection-driven embed inspector. Both modules are deliberately TipTap-free
38
- // so the editor runtime stays behind rich-text/lazy (EC-322) — see their doc comments.
39
- import { EmbedInspectorProvider } from './rich-text/embed-inspector-context.js';
40
- import { EmbedInspector } from './rich-text/embed-inspector.js';
41
37
  import { useSetBreadcrumb } from '../breadcrumb-context.js';
42
38
  import { useAiCurrentDocumentTap } from '@elytracms/studio/lib/ai-current-document';
43
39
  import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
@@ -51,7 +47,6 @@ import { usePublishController, publishGate } from './use-publish-controller.js';
51
47
  import { useFieldFocus } from './use-field-focus.js';
52
48
  import { useCompareSelection } from './use-compare-selection.js';
53
49
  import { useLiveUrl } from './use-live-url.js';
54
- import { useEmbedInspectorHost } from './use-embed-inspector-host.js';
55
50
  export function DetailEditor({ projectId, collectionId, documentId, focusPath, }) {
56
51
  // AD-19: the edit shell is a portable surface over a CollectionViewSource, just
57
52
  // like the list. `DetailEditor` is the CMS entry point — it builds the CMS source
@@ -235,10 +230,6 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
235
230
  focusPath,
236
231
  setActiveTab,
237
232
  });
238
- // 0.0.41 — the selection-driven embed-inspector host: a rich-text `componentEmbed`
239
- // selection swaps the inspector sidebar for the embed's prop form (the `api` is fed
240
- // to the rich-text field through the EmbedInspector context).
241
- const { activeEmbed, embedInspectorApi } = useEmbedInspectorHost();
242
233
  // EC-298 — the sticky version-compare selection. Opening it focuses the Compare tab
243
234
  // + the Versions pane; tabbing to the real fields keeps it open (only its ✕ closes
244
235
  // it). `activeTab === COMPARE_TAB` is when the diff is visible.
@@ -492,51 +483,51 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
492
483
  setCompareBase,
493
484
  setCompareTarget,
494
485
  exitCompare,
495
- }, children: _jsx(EmbedInspectorProvider, { value: embedInspectorApi, 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
496
- ? translate('cms.editor.saveFailed')
497
- : explicitSave
498
- ? dirty
499
- ? translate('cms.editor.unsavedChanges')
500
- : translate('cms.editor.saved')
501
- : dirty
502
- ? translate('cms.editor.saving')
503
- : 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) => {
504
- source.save(next);
505
- replaceDraft(next);
506
- } })) : 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
507
- ? 'bg-accent text-accent-foreground'
508
- : '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'
509
- ? translate('cms.editor.loadingPublishedVersion')
510
- : 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) => {
511
- // EC-306: a conditionally/statically hidden group section is skipped
512
- // entirely (its fields are also exempt from validation).
513
- if (hiddenGroups.has(group.name))
514
- return null;
515
- // EC-273/EC-240: drop a fieldset whose every field is hidden — whether
516
- // content-conditionally or for this principal — so the form never
517
- // shows an empty section header.
518
- const visible = group.fields.filter(isFieldShown);
519
- if (visible.length === 0)
520
- return null;
521
- const section = group.section;
522
- // EC-241: a section `title` is a LocalizedString ({ en, de } or a plain
523
- // string) — resolve it against the active UI locale before rendering.
524
- const sectionTitle = section?.title !== undefined ? localizedLabel(section.title, group.name) : undefined;
525
- 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 ??
526
- (group.name === DEFAULT_FIELD_GROUP
527
- ? translate('cms.editor.defaultGroup')
528
- : 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));
529
- }), formAppendix] }) }) }), activeEmbed ? (_jsx(EmbedInspector, { payload: activeEmbed, className: "w-[320px] shrink-0 border-l border-border" })) : (_jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" }))] })), explicitSave ? (_jsx(UnsavedChangesGuard, { when: dirty, onSave: () => {
530
- save();
531
- return true;
532
- }, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
533
- if (!open)
534
- setConfirmAction(null);
535
- }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
536
- 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)
537
- 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
538
- ? 'cms.editor.publishAlsoHasIssues.one'
539
- : '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') })] })] }) })] }) }) }));
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') })] })] }) })] }) }));
540
531
  }
541
532
  /**
542
533
  * EC-300/AD-16 — errors for composition embeds (hard references) whose target isn't