@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,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, CircleAlert, CircleCheck, ExternalLink, Eye, EyeOff, GitCompareArrows, History, Languages, Link2, Loader2, Lock, MoreHorizontal, Tag, Trash2, X, } from 'lucide-react';
3
+ import { Check, ChevronDown, CircleAlert, CircleCheck, ExternalLink, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, clearHiddenValues, documentKey, groupOrder, groupSectionOf, hasAliases, hiddenGroupNames, isBlocksField, isFieldHidden, isFieldLocalized, isSingleton, resolveDocumentForLocale, resolveDocumentTitle, resolveField, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, urlForDocument, } from '@elytracms/core/cms-core';
6
6
  import { blockRootsOf } from '@elytracms/core/project-graph';
@@ -8,12 +8,16 @@ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter,
8
8
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
9
9
  import { collectEmbedRefs, resolveReuseCapability } from '../builder/reuse';
10
10
  import { cn } from '@elytracms/studio/lib/utils';
11
+ import { useOperationCache } from '@elytracms/studio/lib/query';
11
12
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
12
- import { LocaleSwitcher, displayedFieldFallback, hasLocaleVariant, localeChipLabel, setStudioLocale, } from '@elytracms/studio/lib/locale';
13
+ import { LocaleSwitcher, activeLocaleValues, displayedFieldFallback, fieldConnection, fillFieldFromBase, resetFieldToBase, setFieldValue, setStudioLocale, } from '@elytracms/studio/lib/locale';
13
14
  import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
14
- import { FallbackChip, fallbackTitle } from './cell';
15
+ import { FallbackChip } from './cell';
16
+ import { FieldConnectionControl } from './field-connection';
15
17
  import { FieldControl } from './field-control';
16
- import { CompositionFieldEditor } from '../builder/composition-field-editor';
18
+ // EC-322: the lazy-wrapped page builder — the heavy canvas subtree code-splits out
19
+ // of the detail-view critical path and downloads only when a `blocks` field opens.
20
+ import { CompositionFieldEditor } from '../builder/composition-field-editor-lazy';
17
21
  import { fieldLabel } from './cms-store';
18
22
  import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source';
19
23
  import { CollectionViewProvider, useCollectionView } from '../collection-view/collection-view-context';
@@ -25,14 +29,16 @@ import { ReferencesPanel } from '../references/references-panel';
25
29
  import { InspectorSidebar } from '../inspector/inspector-sidebar';
26
30
  import { DocumentEditorProvider } from './document-editor-context';
27
31
  import { VersionsPane } from './versions-pane';
28
- import { VersionCompare } from './version-compare';
32
+ // EC-322: the lazy-wrapped compare view — its diff UI code-splits off the detail-view
33
+ // critical path and loads only when the Compare tab opens.
34
+ import { VersionCompare } from './version-compare-lazy';
35
+ import { useDocumentVersionsQuery } from './use-document-versions';
36
+ import { discardDraftStatus } from './discard-draft';
29
37
  import { IssuesPane } from './issues-pane';
30
38
  import { DetailsPane } from './details-pane';
31
39
  import { useSetBreadcrumb } from '../breadcrumb-context';
32
40
  import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
33
41
  import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
34
- /** The conventional slug field name (mirrors `url-for-document`'s `SLUG_FIELD`). */
35
- const SLUG_FIELD = 'slug';
36
42
  export function DetailEditor({ projectId, collectionId, documentId, focusPath, }) {
37
43
  // AD-19: the edit shell is a portable surface over a CollectionViewSource, just
38
44
  // like the list. `DetailEditor` is the CMS entry point — it builds the CMS source
@@ -144,7 +150,59 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
144
150
  const viewReadOnly = perspective === 'published';
145
151
  const viewDoc = viewReadOnly && publishedSnapshot.status === 'ready' ? publishedSnapshot.doc : draft;
146
152
  const publishedPending = viewReadOnly && publishedSnapshot.status !== 'ready';
147
- const dirty = JSON.stringify(draft) !== JSON.stringify(doc);
153
+ // EC-316 — "Discard changes": revert the draft to exactly the published
154
+ // content. Only meaningful for a publishing + versioned rail AND a published
155
+ // document, so the history query is gated off otherwise (never a wasted
156
+ // fetch on e.g. the users rail or a never-published draft).
157
+ const canDiscard = canPublish && canVersions;
158
+ const { versionsQuery: discardVersionsQuery, versionKey: discardVersionKey } = useDocumentVersionsQuery(ref, {
159
+ enabled: canDiscard && published,
160
+ });
161
+ const discardCache = useOperationCache();
162
+ const [discardConfirm, setDiscardConfirm] = useState(false);
163
+ const [discardBusy, setDiscardBusy] = useState(false);
164
+ const discardView = discardVersionsQuery.data ?? null;
165
+ const publishedVersionNumber = discardView?.publishedVersion;
166
+ const publishedSnapshotForDiscard = publishedVersionNumber !== undefined
167
+ ? discardView?.versions.find((v) => v.version === publishedVersionNumber)?.snapshot
168
+ : undefined;
169
+ // One pure decision (tested in discard-draft.test.ts): hidden / disabled-
170
+ // with-reason / ready. Its semantic equality is the SAME comparator the
171
+ // Versions pane's publish gate uses, so the two never contradict (EC-298).
172
+ const discardStatus = discardDraftStatus({
173
+ canDiscard,
174
+ published,
175
+ viewReadOnly,
176
+ publishedSnapshot: publishedSnapshotForDiscard,
177
+ collection,
178
+ draft,
179
+ });
180
+ const discardDisabledReason = discardStatus === 'view-read-only'
181
+ ? translate('cms.editor.switchToDraftToDiscard')
182
+ : discardStatus === 'up-to-date'
183
+ ? translate('cms.editor.discardDraftUpToDateHint')
184
+ : discardStatus === 'loading'
185
+ ? translate('common.loading')
186
+ : undefined;
187
+ // Reuses the EC-298 restore machinery (the same op the Compare view's
188
+ // "Restore version" button calls) — never a parallel write path: reverting
189
+ // the draft IS restoring the published version's snapshot, whole-document.
190
+ async function handleDiscardDraft() {
191
+ if (publishedVersionNumber === undefined)
192
+ return;
193
+ setDiscardBusy(true);
194
+ try {
195
+ const restored = await source.versioning?.restore(ref, publishedVersionNumber, { kind: 'all' });
196
+ if (restored)
197
+ replaceDraft(restored);
198
+ await discardCache.invalidate(discardVersionKey);
199
+ }
200
+ finally {
201
+ setDiscardBusy(false);
202
+ setDiscardConfirm(false);
203
+ }
204
+ }
205
+ const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(doc), [draft, doc]);
148
206
  const schemaIssues = useMemo(() => source.validate(draft), [source, draft]);
149
207
  // EC-300/AD-16: composition embeds (hard references) whose target isn't published
150
208
  // would render empty on the live site. Surface them as errors in the Issues panel,
@@ -327,29 +385,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
327
385
  }
328
386
  return viewDoc.values[field.name];
329
387
  }
388
+ // EC-176/EC-318: the present/absent-key write is single-sourced in `setFieldValue`
389
+ // — a localized field writes `localized[locale]`, clearing (value === undefined)
390
+ // DELETES the key (reconnecting to base). `fill from base` / `reset to base` below
391
+ // reuse the same foundation, so typing, filling, and resetting can never diverge.
330
392
  function setValue(field, value) {
331
- setDraft((d) => {
332
- if (isFieldLocalized(collection, field.name)) {
333
- const perLocale = { ...(d.localized?.[locale] ?? {}) };
334
- if (value === undefined)
335
- delete perLocale[field.name];
336
- else
337
- perLocale[field.name] = value;
338
- return { ...d, localized: { ...d.localized, [locale]: perLocale } };
339
- }
340
- const values = { ...d.values };
341
- if (value === undefined)
342
- delete values[field.name];
343
- else
344
- values[field.name] = value;
345
- return { ...d, values };
346
- });
393
+ setDraft((d) => setFieldValue(collection, d, field.name, locale, value));
347
394
  }
348
395
  // EC-273: conditional-visibility context — the draft's values for the active
349
396
  // locale, against which a field's `hidden(values)` predicate is evaluated. A
350
397
  // hidden field's row is omitted (and it is not validated). Recomputed as the
351
398
  // draft changes, so a field shows/hides live as the editor types.
352
- const conditionValues = { ...draft.values, ...(draft.localized?.[locale] ?? {}) };
399
+ const conditionValues = activeLocaleValues(draft, locale);
353
400
  const isHidden = (field) => isFieldHidden(field, { values: conditionValues });
354
401
  // Visible = not content-hidden AND not principal-hidden (EC-240 access === 'hidden').
355
402
  const isFieldShown = (field) => !isHidden(field) && accessOf(field) !== 'hidden';
@@ -375,24 +422,15 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
375
422
  const fieldLevelReadOnly = access === 'read';
376
423
  const fieldReadOnly = viewReadOnly || fieldLevelReadOnly;
377
424
  const localized = isFieldLocalized(collection, field.name);
378
- // Fallback visibility (EC-176): the control DISPLAYS the fallback value, and
379
- // this marker says so on the exact field.
380
- const fallback = localized
381
- ? displayedFieldFallback(collection, viewDoc, field.name, locale, source.localeConfig)
425
+ // EC-318 — connected fields: the per-locale connection state of a localized
426
+ // field (connected/diverged + fill-from-base), DERIVED from the draft, never
427
+ // stored. Null for a non-localized field or the base-locale view. Suppressed
428
+ // while the row is read-only (Published view or a field-level `read` grant) —
429
+ // there is nothing to connect or diverge there.
430
+ const connection = localized
431
+ ? fieldConnection(collection, draft, field.name, locale, source.localeConfig)
382
432
  : null;
383
- // The variant was created in this editing session (the saved document doesn't
384
- // have it yet): editing a fallback-shown value is an explicit translation act.
385
- // Moot (and hidden) while viewing the read-only published snapshot.
386
- const newlyTranslated = !viewReadOnly &&
387
- localized &&
388
- hasLocaleVariant(draft, field.name, locale) &&
389
- !hasLocaleVariant(doc, field.name, locale);
390
- return (_jsxs(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, fallback ? (_jsx(Badge, { variant: "warning", title: fallbackTitle(fallback), "data-testid": `field-fallback-${field.name}`, children: translate('cms.editor.fieldShowingFallback', {
391
- locale: localeChipLabel(fallback.sourceLocale),
392
- }) })) : null, newlyTranslated ? (_jsxs(Badge, { variant: "success", "data-testid": `field-translated-${field.name}`, children: [_jsx(Languages, { className: "size-3" }), ' ', translate('cms.editor.fieldTranslatedTo', { locale: localeChipLabel(locale) })] })) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {})] }), htmlFor: `f-${field.name}`, hint: fallback ? (translate('cms.editor.fieldFallbackHint', {
393
- source: localeChipLabel(fallback.sourceLocale),
394
- locale: localeChipLabel(locale),
395
- })) : (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 === SLUG_FIELD && routableCollection ? _jsx(PathPreview, { url: draftUrl }) : null] }, field.name));
433
+ 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));
396
434
  }
397
435
  function save() {
398
436
  source.save(draft);
@@ -435,12 +473,25 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
435
473
  // memoized: its render closes over per-render edit state (draft, highlight,
436
474
  // field errors) via `renderFieldRow`, and the inspector re-renders with the rest
437
475
  // of the editor anyway, so a stable pane identity would buy nothing here.
438
- const detailsPane = hasAliases(collection) || visibleSidebarFields.length > 0
476
+ // 0.0.39: whether a deployment route addresses this collection — gates the header's
477
+ // "View live" AND the Details pane's document-level URL row (EC-315 hybrid split).
478
+ // A cheap synchronous accessor, resolved on every render (NOT frozen in a `useMemo`
479
+ // with a static dep — the same first-paint-empty-routes trap the SlugField fix
480
+ // removed: routes arrive after the workspace/config loads).
481
+ const routableCollection = resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id);
482
+ const detailsPane = hasAliases(collection) || visibleSidebarFields.length > 0 || routableCollection
439
483
  ? {
440
484
  id: 'details',
441
485
  label: translate('cms.editor.detailsPane'),
442
486
  icon: _jsx(Tag, { className: "size-3.5" }),
443
- render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, onAliasChange: (alias) => setDraft((d) => {
487
+ render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, urlContext: routableCollection
488
+ ? {
489
+ projectId,
490
+ routes: resolveDeploymentRoutes(),
491
+ localeConfig: source.localeConfig,
492
+ locale,
493
+ }
494
+ : undefined, onAliasChange: (alias) => setDraft((d) => {
444
495
  const next = { ...d };
445
496
  if (alias === undefined)
446
497
  delete next.alias;
@@ -462,6 +513,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
462
513
  const [publishState, setPublishState] = useState('idle');
463
514
  // EC-300/AD-16: confirm-through guard when the page embeds unpublished reusables.
464
515
  const [publishConfirm, setPublishConfirm] = useState(false);
516
+ // 0.0.39: the split-button "Publish & close" intent — remembered across the
517
+ // confirm-through dialog so a successful publish returns to the list, matching the
518
+ // retired EC-316 "Save & close". Cleared on cancel or a publish failure (no navigate).
519
+ const closeAfterPublishRef = useRef(false);
465
520
  // Gate publish on the confirm-through dialog when there are unpublished hard
466
521
  // references (they'd render empty on the live site); schema errors already
467
522
  // disable the button, so they never reach here.
@@ -472,6 +527,13 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
472
527
  }
473
528
  void doPublish();
474
529
  }
530
+ // 0.0.39: publish, then return to the rail's list (rail-owned `listPath`, AD-19),
531
+ // the same navigation the removed "Save & close" did. Shares handlePublish's exact
532
+ // gating + confirm-through; the close happens only after the publish resolves.
533
+ function handlePublishAndClose() {
534
+ closeAfterPublishRef.current = true;
535
+ handlePublish();
536
+ }
475
537
  async function doPublish() {
476
538
  // EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME.
477
539
  // While editing, the draft keeps them so an accidental hide (e.g. flipping a
@@ -497,8 +559,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
497
559
  await source.publishing?.publish(ref);
498
560
  setPublishState('success');
499
561
  setTimeout(() => setPublishState('idle'), 1800);
562
+ // "Publish & close": return to the list once the publish landed (never on a
563
+ // failure). Rail-owned `listPath`, falling back to the project index (AD-19).
564
+ if (closeAfterPublishRef.current) {
565
+ closeAfterPublishRef.current = false;
566
+ void navigate({
567
+ to: STUDIO_PROJECT_ROUTE,
568
+ params: { projectId, _splat: source.listPath ?? studioPath.index() },
569
+ });
570
+ }
500
571
  }
501
572
  catch (error) {
573
+ closeAfterPublishRef.current = false;
502
574
  setPublishState('idle');
503
575
  toast.error(error instanceof Error
504
576
  ? translate('cms.editor.publishFailedReason', { error: error.message })
@@ -560,7 +632,6 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
560
632
  // URL visitors actually get; `undefined` until the doc is published (and the
561
633
  // pool has loaded). The collection being routable gates the disabled hint below.
562
634
  const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
563
- const routableCollection = useMemo(() => resolveDeploymentRoutes().some((route) => route.document?.collection === collection.id), [collection.id]);
564
635
  const liveUrl = useMemo(() => {
565
636
  const published = publishedDocs.byId.get(doc.id);
566
637
  if (!published)
@@ -571,12 +642,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
571
642
  localeConfig: source.localeConfig,
572
643
  }, collection, published, { siblings: [...publishedDocs.byId.values()], locale });
573
644
  }, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
574
- // EC-246d: the DRAFT path — derived from the current edit (slug + parent chain),
575
- // not the published pool — so the slug field shows the URL forming live, before
576
- // the first publish. Read-only: the slug is the single editable source, the path
577
- // is always derived (never a second stored field). `undefined` until a slug
578
- // resolves a route (a fresh hierarchy page with no slug has no path yet).
579
- const draftUrl = useMemo(() => urlForDocument({ routes: resolveDeploymentRoutes(), localeConfig: source.localeConfig }, collection, viewDoc, { siblings: source.documents(), locale }), [collection, viewDoc, locale, source]);
645
+ // EC-246d/EC-315: the DRAFT composed path moved INTO the slug control itself
646
+ // (`SlugField`), which derives + decomposes it from the same routes; the editor no
647
+ // longer computes a separate `draftUrl`. `liveUrl` (published) still gates the
648
+ // header's "View live" button — the sole open-live, so draft never links out.
580
649
  // EC-240: hide the tab bar when there is nothing to navigate — a single content
581
650
  // tab with no References/Compare (the users rail). When framework tabs ARE
582
651
  // present we KEEP the content tab(s) so there is always a way back to the form;
@@ -586,6 +655,18 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
586
655
  const hasFrameworkTabs = showReferences || compare !== null;
587
656
  const showContentTabs = tabs.length > 1 || hasFrameworkTabs;
588
657
  const showLensRow = showContentTabs || canLocales || canPublish;
658
+ // 0.0.39 split Publish button — the shared gating/label, computed once so the
659
+ // primary "Publish" and the "Publish & close" item can never diverge.
660
+ const publishDisabled = hasErrors || viewReadOnly || publishState === 'pending';
661
+ const publishTitle = viewReadOnly
662
+ ? translate('cms.editor.switchToDraftToPublish')
663
+ : hasErrors
664
+ ? translate(errorCount === 1 ? 'cms.editor.fixIssuesToPublish.one' : 'cms.editor.fixIssuesToPublish.other', { count: errorCount })
665
+ : undefined;
666
+ const publishButtonLabel = publishState === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.publishing')] })) : publishState === 'success' ? (_jsxs(_Fragment, { children: [_jsx(CircleCheck, {}), " ", translate('cms.editor.publishedState')] })) : (_jsxs(_Fragment, { children: [_jsx(Eye, {}), " ", translate('cms.editor.publish')] }));
667
+ // Offer "Publish & close" only when there IS a list to return to — the same gate
668
+ // the removed EC-316 "Save & close" used (never a singleton or a list-less rail).
669
+ const canPublishClose = !singleton && source.listPath !== undefined;
589
670
  return (_jsx(DocumentEditorProvider, { value: {
590
671
  projectId,
591
672
  collection,
@@ -614,24 +695,11 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
614
695
  ? translate('cms.editor.loadingLiveUrl')
615
696
  : !published
616
697
  ? translate('cms.editor.publishToViewLive')
617
- : translate('cms.editor.setFrontendUrlHint'), children: [_jsx(ExternalLink, {}), " ", translate('cms.editor.viewLive')] }))) : null, canPublish ? (_jsx(Button, { size: "sm", onClick: handlePublish, disabled: hasErrors || viewReadOnly || publishState === 'pending', title: viewReadOnly
618
- ? translate('cms.editor.switchToDraftToPublish')
619
- : hasErrors
620
- ? translate(errorCount === 1
621
- ? 'cms.editor.fixIssuesToPublish.one'
622
- : 'cms.editor.fixIssuesToPublish.other', { count: errorCount })
623
- : undefined, children: publishState === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.publishing')] })) : publishState === 'success' ? (_jsxs(_Fragment, { children: [_jsx(CircleCheck, {}), " ", translate('cms.editor.publishedState')] })) : (_jsxs(_Fragment, { children: [_jsx(Eye, {}), " ", translate('cms.editor.publish')] })) })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : 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: [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
698
+ : translate('cms.editor.setFrontendUrlHint'), children: [_jsx(ExternalLink, {}), " ", translate('cms.editor.viewLive')] }))) : 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, 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
624
699
  ? 'bg-accent text-accent-foreground'
625
700
  : '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'
626
701
  ? translate('cms.editor.loadingPublishedVersion')
627
- : translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (
628
- // EC-298: the version compare view takes over the main area, with the
629
- // shared inspector (Versions pane) staying on as the navigator — an
630
- // in-app surface, not a modal, so arbitrary versions can be browsed.
631
- _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 ? (
632
- // EC-269: mounted only while this tab is active → reference data loads on
633
- // demand and is torn down on leave (no fetching during normal editing).
634
- _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: _jsx(Stack, { gap: "6", children: groups.map((group) => {
702
+ : 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: _jsx(Stack, { gap: "6", children: groups.map((group) => {
635
703
  // EC-306: a conditionally/statically hidden group section is skipped
636
704
  // entirely (its fields are also exempt from validation).
637
705
  if (hiddenGroups.has(group.name))
@@ -656,10 +724,16 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
656
724
  }, description: translate('cms.editor.unsavedChangesGuard') })) : null, confirmAction !== null ? (_jsx(ReferenceConfirmDialog, { projectId: projectId, action: confirmAction, targets: confirmTargets, open: true, onOpenChange: (open) => {
657
725
  if (!open)
658
726
  setConfirmAction(null);
659
- }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: publishConfirm, onOpenChange: (open) => { if (!open)
660
- setPublishConfirm(false); }, 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
727
+ }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
728
+ 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) {
729
+ setPublishConfirm(false);
730
+ closeAfterPublishRef.current = false;
731
+ } }, 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
661
732
  ? 'cms.editor.publishAlsoHasIssues.one'
662
- : 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setPublishConfirm(false), children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
733
+ : 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
734
+ setPublishConfirm(false);
735
+ closeAfterPublishRef.current = false;
736
+ }, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: () => {
663
737
  setPublishConfirm(false);
664
738
  void doPublish();
665
739
  }, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }));
@@ -720,16 +794,6 @@ function PerspectiveButton({ label, active, accent, disabled, title, onClick, })
720
794
  : 'bg-background text-foreground shadow-sm'
721
795
  : 'text-muted-foreground hover:text-foreground'), children: label }));
722
796
  }
723
- /**
724
- * EC-246d: the slug's derived route path, shown read-only directly under the slug
725
- * input. The slug is the single editable source; the path is composed by the route
726
- * resolver (`urlForDocument`) from slug + parent chain and never stored. Draft-aware,
727
- * so it updates as the editor types. Until a slug resolves a route (a fresh
728
- * hierarchy page), a hint explains where the URL comes from.
729
- */
730
- function PathPreview({ url }) {
731
- return (_jsxs(Row, { gap: "1", className: "text-xs text-muted-foreground", "data-testid": "slug-path-preview", children: [_jsx(Link2, { className: "size-3 shrink-0", "aria-hidden": true }), url ? (_jsxs(Text, { as: "span", size: "xs", tone: "muted", className: "min-w-0 truncate", title: url.path, children: ["Served at ", _jsx("span", { className: "font-medium text-foreground/80", children: url.path })] })) : (_jsx(Text, { as: "span", size: "xs", tone: "muted", children: translate('cms.editor.slugUrlHint') }))] }));
732
- }
733
797
  /** The implicit fieldset that holds fields with no declared `form.group` (localized via `cms.editor.defaultGroup`). */
734
798
  const DEFAULT_FIELD_GROUP = 'fields';
735
799
  function groupFields(fields, collection) {