@elytracms/studio 0.0.42 → 0.0.44

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 (229) hide show
  1. package/dist/components/app-views/studio-config.d.ts +12 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/asset-detail.js +1 -1
  4. package/dist/components/assets/asset-detail.js.map +1 -1
  5. package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
  6. package/dist/components/assets/assets-collection-view-source.js +42 -51
  7. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  8. package/dist/components/assets/assets-module.js +19 -6
  9. package/dist/components/assets/assets-module.js.map +1 -1
  10. package/dist/components/assets/assets-store.d.ts +2 -1
  11. package/dist/components/assets/assets-store.js +4 -8
  12. package/dist/components/assets/assets-store.js.map +1 -1
  13. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  14. package/dist/components/assets/assets-view-preference.js +12 -13
  15. package/dist/components/assets/assets-view-preference.js.map +1 -1
  16. package/dist/components/auth-context.d.ts +2 -1
  17. package/dist/components/auth-context.js +6 -8
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/builder-store.d.ts +2 -2
  20. package/dist/components/builder/builder-store.js +4 -8
  21. package/dist/components/builder/builder-store.js.map +1 -1
  22. package/dist/components/cms/bulk-actions.js +2 -1
  23. package/dist/components/cms/bulk-actions.js.map +1 -1
  24. package/dist/components/cms/cell-edit.js +2 -1
  25. package/dist/components/cms/cell-edit.js.map +1 -1
  26. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  27. package/dist/components/cms/cms-composition-validators.js +75 -0
  28. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  29. package/dist/components/cms/cms-labels.d.ts +15 -0
  30. package/dist/components/cms/cms-labels.js +27 -0
  31. package/dist/components/cms/cms-labels.js.map +1 -0
  32. package/dist/components/cms/cms-store.d.ts +23 -10
  33. package/dist/components/cms/cms-store.js +87 -165
  34. package/dist/components/cms/cms-store.js.map +1 -1
  35. package/dist/components/cms/collection-table.js +1 -1
  36. package/dist/components/cms/collection-table.js.map +1 -1
  37. package/dist/components/cms/detail-editor.js +111 -348
  38. package/dist/components/cms/detail-editor.js.map +1 -1
  39. package/dist/components/cms/discard-draft.d.ts +18 -0
  40. package/dist/components/cms/discard-draft.js +18 -0
  41. package/dist/components/cms/discard-draft.js.map +1 -1
  42. package/dist/components/cms/document-editor-context.d.ts +2 -1
  43. package/dist/components/cms/document-editor-context.js +4 -9
  44. package/dist/components/cms/document-editor-context.js.map +1 -1
  45. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  46. package/dist/components/cms/document-ref-utils.js +47 -0
  47. package/dist/components/cms/document-ref-utils.js.map +1 -0
  48. package/dist/components/cms/document-staging.d.ts +8 -3
  49. package/dist/components/cms/document-staging.js +81 -21
  50. package/dist/components/cms/document-staging.js.map +1 -1
  51. package/dist/components/cms/field-control.js +1 -1
  52. package/dist/components/cms/field-control.js.map +1 -1
  53. package/dist/components/cms/issues-pane.js +1 -1
  54. package/dist/components/cms/issues-pane.js.map +1 -1
  55. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  56. package/dist/components/cms/rich-text/block-commands.js +47 -0
  57. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  58. package/dist/components/cms/rich-text/slash-command.js +25 -8
  59. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  60. package/dist/components/cms/rich-text/toolbar.js +3 -2
  61. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  62. package/dist/components/cms/use-compare-selection.d.ts +27 -0
  63. package/dist/components/cms/use-compare-selection.js +29 -0
  64. package/dist/components/cms/use-compare-selection.js.map +1 -0
  65. package/dist/components/cms/use-discard-controller.d.ts +44 -0
  66. package/dist/components/cms/use-discard-controller.js +51 -0
  67. package/dist/components/cms/use-discard-controller.js.map +1 -0
  68. package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
  69. package/dist/components/cms/use-embed-inspector-host.js +17 -0
  70. package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
  71. package/dist/components/cms/use-field-focus.d.ts +38 -0
  72. package/dist/components/cms/use-field-focus.js +70 -0
  73. package/dist/components/cms/use-field-focus.js.map +1 -0
  74. package/dist/components/cms/use-live-url.d.ts +41 -0
  75. package/dist/components/cms/use-live-url.js +55 -0
  76. package/dist/components/cms/use-live-url.js.map +1 -0
  77. package/dist/components/cms/use-publish-controller.d.ts +49 -0
  78. package/dist/components/cms/use-publish-controller.js +102 -0
  79. package/dist/components/cms/use-publish-controller.js.map +1 -0
  80. package/dist/components/cms/use-published-perspective.d.ts +67 -0
  81. package/dist/components/cms/use-published-perspective.js +67 -0
  82. package/dist/components/cms/use-published-perspective.js.map +1 -0
  83. package/dist/components/cms/use-restore-version.d.ts +57 -0
  84. package/dist/components/cms/use-restore-version.js +66 -0
  85. package/dist/components/cms/use-restore-version.js.map +1 -0
  86. package/dist/components/cms/use-save-mode.d.ts +35 -0
  87. package/dist/components/cms/use-save-mode.js +54 -0
  88. package/dist/components/cms/use-save-mode.js.map +1 -0
  89. package/dist/components/cms/version-compare.js +14 -17
  90. package/dist/components/cms/version-compare.js.map +1 -1
  91. package/dist/components/cms/versions-model.d.ts +31 -0
  92. package/dist/components/cms/versions-model.js +30 -0
  93. package/dist/components/cms/versions-model.js.map +1 -1
  94. package/dist/components/cms/versions-pane.js +21 -19
  95. package/dist/components/cms/versions-pane.js.map +1 -1
  96. package/dist/components/cms/view-bar.js +9 -6
  97. package/dist/components/cms/view-bar.js.map +1 -1
  98. package/dist/components/collab/collab-context.d.ts +2 -1
  99. package/dist/components/collab/collab-context.js +4 -8
  100. package/dist/components/collab/collab-context.js.map +1 -1
  101. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  102. package/dist/components/collection-view/collection-view-context.js +4 -9
  103. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  104. package/dist/components/components/component-detail.js +2 -5
  105. package/dist/components/components/component-detail.js.map +1 -1
  106. package/dist/components/components/component-preview.js +8 -4
  107. package/dist/components/components/component-preview.js.map +1 -1
  108. package/dist/components/components/registry-browser.js +24 -5
  109. package/dist/components/components/registry-browser.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.d.ts +21 -6
  111. package/dist/components/components/transfer-dialog.js +120 -31
  112. package/dist/components/components/transfer-dialog.js.map +1 -1
  113. package/dist/components/components/transfer.d.ts +74 -8
  114. package/dist/components/components/transfer.js +87 -2
  115. package/dist/components/components/transfer.js.map +1 -1
  116. package/dist/components/delivery-health-banner.d.ts +12 -6
  117. package/dist/components/delivery-health-banner.js +27 -29
  118. package/dist/components/delivery-health-banner.js.map +1 -1
  119. package/dist/components/dev-login-badge.d.ts +0 -2
  120. package/dist/components/dev-login-badge.js +1 -3
  121. package/dist/components/dev-login-badge.js.map +1 -1
  122. package/dist/components/playground-badge.d.ts +0 -2
  123. package/dist/components/playground-badge.js +0 -2
  124. package/dist/components/playground-badge.js.map +1 -1
  125. package/dist/components/project-list.js +2 -6
  126. package/dist/components/project-list.js.map +1 -1
  127. package/dist/components/project-session.d.ts +6 -2
  128. package/dist/components/project-session.js +32 -83
  129. package/dist/components/project-session.js.map +1 -1
  130. package/dist/components/project-switcher.js +2 -6
  131. package/dist/components/project-switcher.js.map +1 -1
  132. package/dist/components/rail-store-status.d.ts +7 -5
  133. package/dist/components/rail-store-status.js +12 -9
  134. package/dist/components/rail-store-status.js.map +1 -1
  135. package/dist/components/rail-view-source.d.ts +111 -0
  136. package/dist/components/rail-view-source.js +139 -0
  137. package/dist/components/rail-view-source.js.map +1 -0
  138. package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
  139. package/dist/components/redirects/redirects-collection-view-source.js +42 -70
  140. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  141. package/dist/components/redirects/redirects-store.d.ts +2 -1
  142. package/dist/components/redirects/redirects-store.js +4 -8
  143. package/dist/components/redirects/redirects-store.js.map +1 -1
  144. package/dist/components/section-state.d.ts +12 -3
  145. package/dist/components/section-state.js +8 -4
  146. package/dist/components/section-state.js.map +1 -1
  147. package/dist/components/studio-context.d.ts +2 -1
  148. package/dist/components/studio-context.js +4 -8
  149. package/dist/components/studio-context.js.map +1 -1
  150. package/dist/components/studio-shell.js +1 -1
  151. package/dist/components/studio-shell.js.map +1 -1
  152. package/dist/components/theme-context.d.ts +2 -1
  153. package/dist/components/theme-context.js +4 -8
  154. package/dist/components/theme-context.js.map +1 -1
  155. package/dist/components/use-hydration-phase.d.ts +65 -0
  156. package/dist/components/use-hydration-phase.js +64 -0
  157. package/dist/components/use-hydration-phase.js.map +1 -0
  158. package/dist/components/users/users-collection-view-source.d.ts +13 -1
  159. package/dist/components/users/users-collection-view-source.js +60 -79
  160. package/dist/components/users/users-collection-view-source.js.map +1 -1
  161. package/dist/components/users/users-store.d.ts +2 -1
  162. package/dist/components/users/users-store.js +4 -8
  163. package/dist/components/users/users-store.js.map +1 -1
  164. package/dist/components/workspace-projects.d.ts +14 -0
  165. package/dist/components/workspace-projects.js +21 -0
  166. package/dist/components/workspace-projects.js.map +1 -1
  167. package/dist/lib/ai/anthropic-provider.d.ts +29 -0
  168. package/dist/lib/ai/anthropic-provider.js +59 -0
  169. package/dist/lib/ai/anthropic-provider.js.map +1 -0
  170. package/dist/lib/ai/apply-content.d.ts +9 -0
  171. package/dist/lib/ai/apply-content.js +170 -0
  172. package/dist/lib/ai/apply-content.js.map +1 -0
  173. package/dist/lib/ai/config.d.ts +58 -0
  174. package/dist/lib/ai/config.js +134 -0
  175. package/dist/lib/ai/config.js.map +1 -0
  176. package/dist/lib/ai/fake-provider.d.ts +34 -0
  177. package/dist/lib/ai/fake-provider.js +31 -0
  178. package/dist/lib/ai/fake-provider.js.map +1 -0
  179. package/dist/lib/ai/index.d.ts +22 -0
  180. package/dist/lib/ai/index.js +23 -0
  181. package/dist/lib/ai/index.js.map +1 -0
  182. package/dist/lib/ai/knowledge.d.ts +39 -0
  183. package/dist/lib/ai/knowledge.js +60 -0
  184. package/dist/lib/ai/knowledge.js.map +1 -0
  185. package/dist/lib/ai/provenance.d.ts +20 -0
  186. package/dist/lib/ai/provenance.js +15 -0
  187. package/dist/lib/ai/provenance.js.map +1 -0
  188. package/dist/lib/ai/route.d.ts +24 -0
  189. package/dist/lib/ai/route.js +90 -0
  190. package/dist/lib/ai/route.js.map +1 -0
  191. package/dist/lib/ai/shape.d.ts +35 -0
  192. package/dist/lib/ai/shape.js +113 -0
  193. package/dist/lib/ai/shape.js.map +1 -0
  194. package/dist/lib/ai/types.d.ts +227 -0
  195. package/dist/lib/ai/types.js +2 -0
  196. package/dist/lib/ai/types.js.map +1 -0
  197. package/dist/lib/data/data-store.d.ts +2 -1
  198. package/dist/lib/data/data-store.js +4 -8
  199. package/dist/lib/data/data-store.js.map +1 -1
  200. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  201. package/dist/lib/delivery/instant-publish.js +0 -7
  202. package/dist/lib/delivery/instant-publish.js.map +1 -1
  203. package/dist/lib/diff-view-preference.js +4 -4
  204. package/dist/lib/diff-view-preference.js.map +1 -1
  205. package/dist/lib/i18n/messages.en.js +5 -2
  206. package/dist/lib/i18n/messages.en.js.map +1 -1
  207. package/dist/lib/local-preference.d.ts +37 -0
  208. package/dist/lib/local-preference.js +36 -0
  209. package/dist/lib/local-preference.js.map +1 -1
  210. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  211. package/dist/lib/persistence/convex-adapter.js +20 -0
  212. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  213. package/dist/lib/rail.js +7 -6
  214. package/dist/lib/rail.js.map +1 -1
  215. package/dist/lib/required-context.d.ts +24 -0
  216. package/dist/lib/required-context.js +28 -0
  217. package/dist/lib/required-context.js.map +1 -0
  218. package/dist/lib/theme.js +11 -4
  219. package/dist/lib/theme.js.map +1 -1
  220. package/dist/lib/workspace/index.d.ts +9 -0
  221. package/dist/lib/workspace/index.js +11 -0
  222. package/dist/lib/workspace/index.js.map +1 -1
  223. package/dist/ui/index.d.ts +1 -0
  224. package/dist/ui/index.js +1 -0
  225. package/dist/ui/index.js.map +1 -1
  226. package/dist/ui/segmented-control.d.ts +46 -0
  227. package/dist/ui/segmented-control.js +13 -0
  228. package/dist/ui/segmented-control.js.map +1 -0
  229. package/package.json +8 -2
@@ -0,0 +1,17 @@
1
+ import { useMemo, useState } from 'react';
2
+ /**
3
+ * 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node
4
+ * selection in a rich-text field swaps the inspector sidebar for the embed's prop
5
+ * form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node
6
+ * id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed
7
+ * by the rich-text field through the EmbedInspector context (the returned api).
8
+ */
9
+ export function useEmbedInspectorHost() {
10
+ const [activeEmbed, setActiveEmbed] = useState(null);
11
+ const embedInspectorApi = useMemo(() => ({
12
+ setActiveEmbed: (payload) => setActiveEmbed(payload),
13
+ clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),
14
+ }), []);
15
+ return { activeEmbed, embedInspectorApi };
16
+ }
17
+ //# sourceMappingURL=use-embed-inspector-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-embed-inspector-host.js","sourceRoot":"","sources":["../../../src/components/cms/use-embed-inspector-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGzC;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IAInC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAA+B,IAAI,CAAC,CAAA;IAClF,MAAM,iBAAiB,GAAG,OAAO,CAC/B,GAAG,EAAE,CAAC,CAAC;QACL,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC;QACpD,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;KAChG,CAAC,EACF,EAAE,CACH,CAAA;IACD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAC3C,CAAC","sourcesContent":["import { useMemo, useState } from 'react'\nimport type { EmbedInspectorApi, EmbedInspectorPayload } from './rich-text/embed-inspector-context'\n\n/**\n * 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node\n * selection in a rich-text field swaps the inspector sidebar for the embed's prop\n * form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node\n * id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed\n * by the rich-text field through the EmbedInspector context (the returned api).\n */\nexport function useEmbedInspectorHost(): {\n activeEmbed: EmbedInspectorPayload | null\n embedInspectorApi: EmbedInspectorApi\n} {\n const [activeEmbed, setActiveEmbed] = useState<EmbedInspectorPayload | null>(null)\n const embedInspectorApi = useMemo<EmbedInspectorApi>(\n () => ({\n setActiveEmbed: (payload) => setActiveEmbed(payload),\n clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),\n }),\n [],\n )\n return { activeEmbed, embedInspectorApi }\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core';
2
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ /**
4
+ * EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
5
+ * the reserved `sidebar` group renders in the Details inspector pane (open that
6
+ * pane, EC-285); any other field lives on a content tab (switch to it). Returns
7
+ * `null` for an unknown field name (no-op). Directly testable, no DOM.
8
+ */
9
+ export declare function focusTarget(collection: CollectionDef, fieldName: string): {
10
+ field: FieldDef;
11
+ paneId: 'details';
12
+ } | {
13
+ field: FieldDef;
14
+ tab: string;
15
+ } | null;
16
+ export interface FieldFocus {
17
+ /** The field ring-highlighted right now (or null). */
18
+ highlightedField: string | null;
19
+ /** The inspector sidebar's active pane, controlled so a jump can open Details. */
20
+ inspectorPaneId: string | undefined;
21
+ setInspectorPaneId: (id: string | undefined) => void;
22
+ /** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */
23
+ focusField: (fieldName: string, localeHint?: string) => void;
24
+ }
25
+ /**
26
+ * EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
27
+ * switches to its tab (or opens the Details pane for a sidebar field) and the
28
+ * given locale, scrolls its control into view, and ring-highlights it briefly.
29
+ * Shared by the reference deep-link (`focusPath`) and the Issues pane's
30
+ * jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
31
+ * timers (cleaned up on unmount, and cleared between back-to-back jumps).
32
+ */
33
+ export declare function useFieldFocus(input: {
34
+ collection: CollectionDef;
35
+ source: CollectionViewSource;
36
+ focusPath: string | undefined;
37
+ setActiveTab: (tab: string) => void;
38
+ }): FieldFocus;
@@ -0,0 +1,70 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core';
3
+ import { setStudioLocale } from '@elytracms/studio/lib/locale';
4
+ import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
5
+ /**
6
+ * EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
7
+ * the reserved `sidebar` group renders in the Details inspector pane (open that
8
+ * pane, EC-285); any other field lives on a content tab (switch to it). Returns
9
+ * `null` for an unknown field name (no-op). Directly testable, no DOM.
10
+ */
11
+ export function focusTarget(collection, fieldName) {
12
+ const field = collection.fields.find((f) => f.name === fieldName);
13
+ if (!field)
14
+ return null;
15
+ if (field.form?.group === SIDEBAR_FIELD_GROUP)
16
+ return { field, paneId: 'details' };
17
+ const fieldTabs = tabOrder(collection.form);
18
+ return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' };
19
+ }
20
+ /**
21
+ * EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
22
+ * switches to its tab (or opens the Details pane for a sidebar field) and the
23
+ * given locale, scrolls its control into view, and ring-highlights it briefly.
24
+ * Shared by the reference deep-link (`focusPath`) and the Issues pane's
25
+ * jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
26
+ * timers (cleaned up on unmount, and cleared between back-to-back jumps).
27
+ */
28
+ export function useFieldFocus(input) {
29
+ const { collection, source, focusPath, setActiveTab } = input;
30
+ const [highlightedField, setHighlightedField] = useState(null);
31
+ const [inspectorPaneId, setInspectorPaneId] = useState();
32
+ const focusTimersRef = useRef([]);
33
+ const focusField = useCallback((fieldName, localeHint) => {
34
+ const target = focusTarget(collection, fieldName);
35
+ if (!target)
36
+ return;
37
+ // A sidebar-group field renders in the Details pane; any other on its tab.
38
+ if ('paneId' in target)
39
+ setInspectorPaneId(target.paneId);
40
+ else
41
+ setActiveTab(target.tab);
42
+ if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {
43
+ setStudioLocale(localeHint);
44
+ }
45
+ setHighlightedField(target.field.name);
46
+ // Clear any timers from a prior jump so back-to-back jumps don't fight.
47
+ focusTimersRef.current.forEach((t) => window.clearTimeout(t));
48
+ const scrollTimer = window.setTimeout(() => {
49
+ // Some controls (relation/asset pickers) don't render the input id —
50
+ // the field label (`htmlFor`) is always present.
51
+ const control = document.getElementById(`f-${target.field.name}`) ??
52
+ document.querySelector(`label[for="${CSS.escape(`f-${target.field.name}`)}"]`);
53
+ control?.scrollIntoView({ block: 'center', behavior: 'smooth' });
54
+ }, 60);
55
+ const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200);
56
+ focusTimersRef.current = [scrollTimer, clearTimer];
57
+ }, [collection, source.localeConfig, setActiveTab]);
58
+ useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), []);
59
+ // Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.
60
+ useEffect(() => {
61
+ if (!focusPath)
62
+ return;
63
+ const parsed = parseDocumentFieldPath(focusPath);
64
+ if (!parsed)
65
+ return;
66
+ focusField(parsed.field, parsed.locale);
67
+ }, [focusPath, focusField]);
68
+ return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField };
69
+ }
70
+ //# sourceMappingURL=use-field-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-field-focus.js","sourceRoot":"","sources":["../../../src/components/cms/use-field-focus.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { setStudioLocale } from '@elytracms/studio/lib/locale'\nimport { parseDocumentFieldPath } from '@elytracms/studio/lib/references'\n\n/**\n * EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in\n * the reserved `sidebar` group renders in the Details inspector pane (open that\n * pane, EC-285); any other field lives on a content tab (switch to it). Returns\n * `null` for an unknown field name (no-op). Directly testable, no DOM.\n */\nexport function focusTarget(\n collection: CollectionDef,\n fieldName: string,\n): { field: FieldDef; paneId: 'details' } | { field: FieldDef; tab: string } | null {\n const field = collection.fields.find((f) => f.name === fieldName)\n if (!field) return null\n if (field.form?.group === SIDEBAR_FIELD_GROUP) return { field, paneId: 'details' }\n const fieldTabs = tabOrder(collection.form)\n return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' }\n}\n\nexport interface FieldFocus {\n /** The field ring-highlighted right now (or null). */\n highlightedField: string | null\n /** The inspector sidebar's active pane, controlled so a jump can open Details. */\n inspectorPaneId: string | undefined\n setInspectorPaneId: (id: string | undefined) => void\n /** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */\n focusField: (fieldName: string, localeHint?: string) => void\n}\n\n/**\n * EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field\n * switches to its tab (or opens the Details pane for a sidebar field) and the\n * given locale, scrolls its control into view, and ring-highlights it briefly.\n * Shared by the reference deep-link (`focusPath`) and the Issues pane's\n * jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear\n * timers (cleaned up on unmount, and cleared between back-to-back jumps).\n */\nexport function useFieldFocus(input: {\n collection: CollectionDef\n source: CollectionViewSource\n focusPath: string | undefined\n setActiveTab: (tab: string) => void\n}): FieldFocus {\n const { collection, source, focusPath, setActiveTab } = input\n const [highlightedField, setHighlightedField] = useState<string | null>(null)\n const [inspectorPaneId, setInspectorPaneId] = useState<string>()\n const focusTimersRef = useRef<number[]>([])\n\n const focusField = useCallback(\n (fieldName: string, localeHint?: string) => {\n const target = focusTarget(collection, fieldName)\n if (!target) return\n // A sidebar-group field renders in the Details pane; any other on its tab.\n if ('paneId' in target) setInspectorPaneId(target.paneId)\n else setActiveTab(target.tab)\n if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {\n setStudioLocale(localeHint)\n }\n setHighlightedField(target.field.name)\n // Clear any timers from a prior jump so back-to-back jumps don't fight.\n focusTimersRef.current.forEach((t) => window.clearTimeout(t))\n const scrollTimer = window.setTimeout(() => {\n // Some controls (relation/asset pickers) don't render the input id —\n // the field label (`htmlFor`) is always present.\n const control =\n document.getElementById(`f-${target.field.name}`) ??\n document.querySelector(`label[for=\"${CSS.escape(`f-${target.field.name}`)}\"]`)\n control?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n }, 60)\n const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200)\n focusTimersRef.current = [scrollTimer, clearTimer]\n },\n [collection, source.localeConfig, setActiveTab],\n )\n useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), [])\n\n // Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.\n useEffect(() => {\n if (!focusPath) return\n const parsed = parseDocumentFieldPath(focusPath)\n if (!parsed) return\n focusField(parsed.field, parsed.locale)\n }, [focusPath, focusField])\n\n return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core';
2
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ /** The header's live-link descriptor: an `href` (open live) + a title, or a muted
4
+ * reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */
5
+ export interface HeaderLive {
6
+ href?: string;
7
+ title: string;
8
+ }
9
+ /**
10
+ * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
11
+ * decision. Only a routable collection shows the row; its leaf opens the live page
12
+ * when the doc is published AND a frontendUrl is set (an absolute `href`),
13
+ * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
14
+ * first, or set the frontendUrl). The draft-derived path itself updates as you
15
+ * type — only the OPEN-live action is published-gated, preserving draft/live.
16
+ */
17
+ export declare function resolveHeaderLive(input: {
18
+ routableCollection: boolean;
19
+ liveUrl: {
20
+ href?: string;
21
+ path: string;
22
+ } | undefined;
23
+ loading: boolean;
24
+ published: boolean;
25
+ }): HeaderLive | undefined;
26
+ /**
27
+ * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
28
+ * published snapshot + its published ancestors), never the draft, so an unpublished
29
+ * slug/parent edit can't leak into the live URL. Resolves the header's live-link
30
+ * descriptor for the composed-URL row; `undefined` for a non-routable collection.
31
+ */
32
+ export declare function useLiveUrl(input: {
33
+ source: CollectionViewSource;
34
+ collection: CollectionDef;
35
+ doc: CmsDocument;
36
+ locale: Locale;
37
+ published: boolean;
38
+ routableCollection: boolean;
39
+ }): {
40
+ headerLive: HeaderLive | undefined;
41
+ };
@@ -0,0 +1,55 @@
1
+ import { useMemo } from 'react';
2
+ import { urlForDocument } from '@elytracms/core/cms-core';
3
+ import { translate } from '@elytracms/studio/lib/i18n';
4
+ import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
5
+ import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
6
+ /**
7
+ * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
8
+ * decision. Only a routable collection shows the row; its leaf opens the live page
9
+ * when the doc is published AND a frontendUrl is set (an absolute `href`),
10
+ * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
11
+ * first, or set the frontendUrl). The draft-derived path itself updates as you
12
+ * type — only the OPEN-live action is published-gated, preserving draft/live.
13
+ */
14
+ export function resolveHeaderLive(input) {
15
+ if (!input.routableCollection)
16
+ return undefined;
17
+ if (input.liveUrl?.href) {
18
+ return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) };
19
+ }
20
+ return {
21
+ title: input.loading
22
+ ? translate('cms.editor.loadingLiveUrl')
23
+ : !input.published
24
+ ? translate('cms.editor.publishToViewLive')
25
+ : translate('cms.editor.setFrontendUrlHint'),
26
+ };
27
+ }
28
+ /**
29
+ * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
30
+ * published snapshot + its published ancestors), never the draft, so an unpublished
31
+ * slug/parent edit can't leak into the live URL. Resolves the header's live-link
32
+ * descriptor for the composed-URL row; `undefined` for a non-routable collection.
33
+ */
34
+ export function useLiveUrl(input) {
35
+ const { source, collection, doc, locale, published, routableCollection } = input;
36
+ const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
37
+ const liveUrl = useMemo(() => {
38
+ const publishedDoc = publishedDocs.byId.get(doc.id);
39
+ if (!publishedDoc)
40
+ return undefined;
41
+ return urlForDocument({
42
+ routes: resolveDeploymentRoutes(),
43
+ frontendUrl: resolveDeploymentFrontendUrl(),
44
+ localeConfig: source.localeConfig,
45
+ }, collection, publishedDoc, { siblings: [...publishedDocs.byId.values()], locale });
46
+ }, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
47
+ const headerLive = resolveHeaderLive({
48
+ routableCollection,
49
+ liveUrl,
50
+ loading: publishedDocs.status === 'loading',
51
+ published,
52
+ });
53
+ return { headerLive };
54
+ }
55
+ //# sourceMappingURL=use-live-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAA;AACvG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AASrF;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAKjC;IACC,IAAI,CAAC,KAAK,CAAC,kBAAkB;QAAE,OAAO,SAAS,CAAA;IAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC/G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO;YAClB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC;YACxC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS;gBAChB,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC;gBAC3C,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC;KACjD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAO1B;IACC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IAChF,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,CAAC,YAAY;YAAE,OAAO,SAAS,CAAA;QACnC,OAAO,cAAc,CACnB;YACE,MAAM,EAAE,uBAAuB,EAAE;YACjC,WAAW,EAAE,4BAA4B,EAAE;YAC3C,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,EACD,UAAU,EACV,YAAY,EACZ,EAAE,QAAQ,EAAE,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CACvD,CAAA;IACH,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;IAEpE,MAAM,UAAU,GAAG,iBAAiB,CAAC;QACnC,kBAAkB;QAClB,OAAO;QACP,OAAO,EAAE,aAAa,CAAC,MAAM,KAAK,SAAS;QAC3C,SAAS;KACV,CAAC,CAAA;IACF,OAAO,EAAE,UAAU,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { urlForDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\n\n/** The header's live-link descriptor: an `href` (open live) + a title, or a muted\n * reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */\nexport interface HeaderLive {\n href?: string\n title: string\n}\n\n/**\n * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure\n * decision. Only a routable collection shows the row; its leaf opens the live page\n * when the doc is published AND a frontendUrl is set (an absolute `href`),\n * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish\n * first, or set the frontendUrl). The draft-derived path itself updates as you\n * type — only the OPEN-live action is published-gated, preserving draft/live.\n */\nexport function resolveHeaderLive(input: {\n routableCollection: boolean\n liveUrl: { href?: string; path: string } | undefined\n loading: boolean\n published: boolean\n}): HeaderLive | undefined {\n if (!input.routableCollection) return undefined\n if (input.liveUrl?.href) {\n return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) }\n }\n return {\n title: input.loading\n ? translate('cms.editor.loadingLiveUrl')\n : !input.published\n ? translate('cms.editor.publishToViewLive')\n : translate('cms.editor.setFrontendUrlHint'),\n }\n}\n\n/**\n * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned\n * published snapshot + its published ancestors), never the draft, so an unpublished\n * slug/parent edit can't leak into the live URL. Resolves the header's live-link\n * descriptor for the composed-URL row; `undefined` for a non-routable collection.\n */\nexport function useLiveUrl(input: {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n published: boolean\n routableCollection: boolean\n}): { headerLive: HeaderLive | undefined } {\n const { source, collection, doc, locale, published, routableCollection } = input\n const publishedDocs = usePublishedDocuments(source.projectId, collection.id)\n const liveUrl = useMemo(() => {\n const publishedDoc = publishedDocs.byId.get(doc.id)\n if (!publishedDoc) return undefined\n return urlForDocument(\n {\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: source.localeConfig,\n },\n collection,\n publishedDoc,\n { siblings: [...publishedDocs.byId.values()], locale },\n )\n }, [publishedDocs, collection, doc.id, locale, source.localeConfig])\n\n const headerLive = resolveHeaderLive({\n routableCollection,\n liveUrl,\n loading: publishedDocs.status === 'loading',\n published,\n })\n return { headerLive }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core';
2
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ export type PublishState = 'idle' | 'pending' | 'success';
4
+ /**
5
+ * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
6
+ * primary "Publish" and the "Publish & close" item can never diverge. A pure
7
+ * decision (directly testable): the Published perspective is read-only (switch to
8
+ * Draft to publish), schema errors block (with a count-aware hint), and an
9
+ * in-flight publish disables re-entry.
10
+ */
11
+ export declare function publishGate(input: {
12
+ hasErrors: boolean;
13
+ viewReadOnly: boolean;
14
+ publishState: PublishState;
15
+ errorCount: number;
16
+ }): {
17
+ disabled: boolean;
18
+ title: string | undefined;
19
+ };
20
+ export interface PublishController {
21
+ publishState: PublishState;
22
+ /** Is the confirm-through dialog (unpublished hard refs) open? */
23
+ publishConfirm: boolean;
24
+ /** Publish (opening the confirm-through dialog first if there are unpublished refs). */
25
+ handlePublish: () => void;
26
+ /** Publish and, on success, return to the rail's list (0.0.39 "Publish & close"). */
27
+ handlePublishAndClose: () => void;
28
+ /** Confirm-through: publish despite the unpublished hard references. */
29
+ confirmPublish: () => void;
30
+ /** Dismiss the confirm-through dialog (also clears a pending "& close" intent). */
31
+ cancelPublishConfirm: () => void;
32
+ }
33
+ /**
34
+ * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator
35
+ * state, the EC-300 confirm-through guard (unpublished hard references would render
36
+ * empty on the live site, so publishing confirms rather than blocks), and the
37
+ * 0.0.39 "Publish & close" navigation (remembered across the confirm dialog via a
38
+ * ref, so a successful publish returns to the rail-owned list).
39
+ */
40
+ export declare function usePublishController(input: {
41
+ source: CollectionViewSource;
42
+ docRef: DocumentRef;
43
+ collection: CollectionDef;
44
+ projectId: string;
45
+ draft: CmsDocument;
46
+ setDraft: (doc: CmsDocument) => void;
47
+ dirty: boolean;
48
+ hasUnpublishedRefs: boolean;
49
+ }): PublishController;
@@ -0,0 +1,102 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
3
+ import { clearHiddenValues } from '@elytracms/core/cms-core';
4
+ import { toast } from '@elytracms/studio/ui';
5
+ import { translate } from '@elytracms/studio/lib/i18n';
6
+ /**
7
+ * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
8
+ * primary "Publish" and the "Publish & close" item can never diverge. A pure
9
+ * decision (directly testable): the Published perspective is read-only (switch to
10
+ * Draft to publish), schema errors block (with a count-aware hint), and an
11
+ * in-flight publish disables re-entry.
12
+ */
13
+ export function publishGate(input) {
14
+ const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending';
15
+ const title = input.viewReadOnly
16
+ ? translate('cms.editor.switchToDraftToPublish')
17
+ : input.hasErrors
18
+ ? translate(input.errorCount === 1
19
+ ? 'cms.editor.fixIssuesToPublish.one'
20
+ : 'cms.editor.fixIssuesToPublish.other', { count: input.errorCount })
21
+ : undefined;
22
+ return { disabled, title };
23
+ }
24
+ /**
25
+ * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator
26
+ * state, the EC-300 confirm-through guard (unpublished hard references would render
27
+ * empty on the live site, so publishing confirms rather than blocks), and the
28
+ * 0.0.39 "Publish & close" navigation (remembered across the confirm dialog via a
29
+ * ref, so a successful publish returns to the rail-owned list).
30
+ */
31
+ export function usePublishController(input) {
32
+ const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input;
33
+ const navigate = useNavigate();
34
+ const [publishState, setPublishState] = useState('idle');
35
+ const [publishConfirm, setPublishConfirm] = useState(false);
36
+ // The split-button "Publish & close" intent — remembered across the confirm-through
37
+ // dialog so a successful publish returns to the list. Cleared on cancel or failure.
38
+ const closeAfterPublishRef = useRef(false);
39
+ const doPublish = useCallback(async () => {
40
+ // EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME —
41
+ // into the draft AND the snapshot, so both go clean together. Also captures the
42
+ // latest edit if Publish fires inside the autosave debounce window; the serial
43
+ // persister runs the upsert before the publish so the snapshot is this draft.
44
+ const cleaned = clearHiddenValues(collection, draft);
45
+ if (cleaned !== draft)
46
+ setDraft(cleaned);
47
+ if (cleaned !== draft || dirty)
48
+ source.save(cleaned);
49
+ setPublishState('pending');
50
+ try {
51
+ await source.publishing?.publish(docRef);
52
+ setPublishState('success');
53
+ setTimeout(() => setPublishState('idle'), 1800);
54
+ // "Publish & close": return to the list once the publish landed (never on a
55
+ // failure). Rail-owned `listPath`, falling back to the project index (AD-19).
56
+ if (closeAfterPublishRef.current) {
57
+ closeAfterPublishRef.current = false;
58
+ void navigate({
59
+ to: STUDIO_PROJECT_ROUTE,
60
+ params: { projectId, _splat: source.listPath ?? studioPath.index() },
61
+ });
62
+ }
63
+ }
64
+ catch (error) {
65
+ closeAfterPublishRef.current = false;
66
+ setPublishState('idle');
67
+ toast.error(error instanceof Error
68
+ ? translate('cms.editor.publishFailedReason', { error: error.message })
69
+ : translate('cms.editor.publishFailed'));
70
+ }
71
+ }, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId]);
72
+ // Gate publish on the confirm-through dialog when there are unpublished hard
73
+ // references; schema errors already disable the button, so they never reach here.
74
+ const handlePublish = useCallback(() => {
75
+ if (hasUnpublishedRefs) {
76
+ setPublishConfirm(true);
77
+ return;
78
+ }
79
+ void doPublish();
80
+ }, [hasUnpublishedRefs, doPublish]);
81
+ const handlePublishAndClose = useCallback(() => {
82
+ closeAfterPublishRef.current = true;
83
+ handlePublish();
84
+ }, [handlePublish]);
85
+ const confirmPublish = useCallback(() => {
86
+ setPublishConfirm(false);
87
+ void doPublish();
88
+ }, [doPublish]);
89
+ const cancelPublishConfirm = useCallback(() => {
90
+ setPublishConfirm(false);
91
+ closeAfterPublishRef.current = false;
92
+ }, []);
93
+ return {
94
+ publishState,
95
+ publishConfirm,
96
+ handlePublish,
97
+ handlePublishAndClose,
98
+ confirmPublish,
99
+ cancelPublishConfirm,
100
+ };
101
+ }
102
+ //# sourceMappingURL=use-publish-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { clearHiddenValues } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Is the confirm-through dialog (unpublished hard refs) open? */\n publishConfirm: boolean\n /** Publish (opening the confirm-through dialog first if there are unpublished refs). */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n /** Confirm-through: publish despite the unpublished hard references. */\n confirmPublish: () => void\n /** Dismiss the confirm-through dialog (also clears a pending \"& close\" intent). */\n cancelPublishConfirm: () => void\n}\n\n/**\n * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator\n * state, the EC-300 confirm-through guard (unpublished hard references would render\n * empty on the live site, so publishing confirms rather than blocks), and the\n * 0.0.39 \"Publish & close\" navigation (remembered across the confirm dialog via a\n * ref, so a successful publish returns to the rail-owned list).\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n collection: CollectionDef\n projectId: string\n draft: CmsDocument\n setDraft: (doc: CmsDocument) => void\n dirty: boolean\n hasUnpublishedRefs: boolean\n}): PublishController {\n const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n const [publishConfirm, setPublishConfirm] = useState(false)\n // The split-button \"Publish & close\" intent — remembered across the confirm-through\n // dialog so a successful publish returns to the list. Cleared on cancel or failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n // EC-273: clean conditionally-hidden values from the draft AT PUBLISH TIME —\n // into the draft AND the snapshot, so both go clean together. Also captures the\n // latest edit if Publish fires inside the autosave debounce window; the serial\n // persister runs the upsert before the publish so the snapshot is this draft.\n const cleaned = clearHiddenValues(collection, draft)\n if (cleaned !== draft) setDraft(cleaned)\n if (cleaned !== draft || dirty) source.save(cleaned)\n setPublishState('pending')\n try {\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n }\n }, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId])\n\n // Gate publish on the confirm-through dialog when there are unpublished hard\n // references; schema errors already disable the button, so they never reach here.\n const handlePublish = useCallback(() => {\n if (hasUnpublishedRefs) {\n setPublishConfirm(true)\n return\n }\n void doPublish()\n }, [hasUnpublishedRefs, doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n const confirmPublish = useCallback(() => {\n setPublishConfirm(false)\n void doPublish()\n }, [doPublish])\n\n const cancelPublishConfirm = useCallback(() => {\n setPublishConfirm(false)\n closeAfterPublishRef.current = false\n }, [])\n\n return {\n publishState,\n publishConfirm,\n handlePublish,\n handlePublishAndClose,\n confirmPublish,\n cancelPublishConfirm,\n }\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import type { QueryKey } from '@tanstack/react-query';
2
+ import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
3
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ import type { DocumentVersionsView } from './cms-store';
5
+ import type { PublishedSnapshotState } from './versions-model';
6
+ export type Perspective = 'draft' | 'published';
7
+ /**
8
+ * EC-226 — which document the fieldset renders, given the active perspective and
9
+ * the pinned published snapshot's load state. A pure decision so "never present
10
+ * the draft AS published" is single-sourced and directly testable:
11
+ *
12
+ * - Draft perspective → the live draft, editable.
13
+ * - Published perspective, snapshot READY → the pinned snapshot, read-only.
14
+ * - Published perspective, snapshot NOT ready → still the draft as the fallback
15
+ * render target, but flagged `publishedPending` so the shell shows a placeholder
16
+ * instead (the draft must never be shown labelled as the published content).
17
+ */
18
+ export declare function resolveViewDoc(input: {
19
+ perspective: Perspective;
20
+ publishedSnapshot: PublishedSnapshotState;
21
+ draft: CmsDocument;
22
+ }): {
23
+ viewReadOnly: boolean;
24
+ viewDoc: CmsDocument;
25
+ publishedPending: boolean;
26
+ };
27
+ export interface PublishedPerspective {
28
+ perspective: Perspective;
29
+ setPerspective: (perspective: Perspective) => void;
30
+ /** Is the document published (a version pointer is set)? */
31
+ published: boolean;
32
+ /** A publishing AND versioned rail — gates the version-history query + Discard. */
33
+ canDiscard: boolean;
34
+ /** The recorded version history (null while loading / disabled). */
35
+ versionsView: DocumentVersionsView | null;
36
+ /** The shared version-history cache key ({@link documentVersionsKey}). */
37
+ versionKey: QueryKey;
38
+ /** The pinned published version number, or undefined while loading / none pinned. */
39
+ publishedVersionNumber: number | undefined;
40
+ /** The pinned published version's snapshot, or undefined while loading / none. */
41
+ publishedSnapshotDoc: CmsDocument | undefined;
42
+ /** The loaded state of the pinned published snapshot. */
43
+ publishedSnapshot: PublishedSnapshotState;
44
+ /** Is the editor viewing the read-only Published snapshot? */
45
+ viewReadOnly: boolean;
46
+ /** The document the fieldset renders (draft or the pinned snapshot). */
47
+ viewDoc: CmsDocument;
48
+ /** Is the Published snapshot still loading (show a placeholder, not the draft)? */
49
+ publishedPending: boolean;
50
+ }
51
+ /**
52
+ * EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned
53
+ * published snapshot, both derived from the ONE shared version-history query
54
+ * (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw
55
+ * `source.versioning.load` call). The Discard-draft gating and the Published
56
+ * perspective read exactly the same `versions`/`publishedVersion` shape, so a
57
+ * second bespoke fetch on perspective switch would just re-read this. Gated the
58
+ * same way Discard always was — a publishing + versioned rail AND a published
59
+ * document — which is also exactly when the Published perspective is reachable.
60
+ */
61
+ export declare function usePublishedPerspective(input: {
62
+ ref: DocumentRef;
63
+ source: CollectionViewSource;
64
+ canPublish: boolean;
65
+ canVersions: boolean;
66
+ draft: CmsDocument;
67
+ }): PublishedPerspective;
@@ -0,0 +1,67 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useDocumentVersionsQuery } from './use-document-versions';
3
+ import { derivePublishedSnapshot, publishedVersionSnapshot } from './versions-model';
4
+ /**
5
+ * EC-226 — which document the fieldset renders, given the active perspective and
6
+ * the pinned published snapshot's load state. A pure decision so "never present
7
+ * the draft AS published" is single-sourced and directly testable:
8
+ *
9
+ * - Draft perspective → the live draft, editable.
10
+ * - Published perspective, snapshot READY → the pinned snapshot, read-only.
11
+ * - Published perspective, snapshot NOT ready → still the draft as the fallback
12
+ * render target, but flagged `publishedPending` so the shell shows a placeholder
13
+ * instead (the draft must never be shown labelled as the published content).
14
+ */
15
+ export function resolveViewDoc(input) {
16
+ const viewReadOnly = input.perspective === 'published';
17
+ const ready = input.publishedSnapshot.status === 'ready';
18
+ return {
19
+ viewReadOnly,
20
+ viewDoc: viewReadOnly && input.publishedSnapshot.status === 'ready' ? input.publishedSnapshot.doc : input.draft,
21
+ publishedPending: viewReadOnly && !ready,
22
+ };
23
+ }
24
+ /**
25
+ * EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned
26
+ * published snapshot, both derived from the ONE shared version-history query
27
+ * (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw
28
+ * `source.versioning.load` call). The Discard-draft gating and the Published
29
+ * perspective read exactly the same `versions`/`publishedVersion` shape, so a
30
+ * second bespoke fetch on perspective switch would just re-read this. Gated the
31
+ * same way Discard always was — a publishing + versioned rail AND a published
32
+ * document — which is also exactly when the Published perspective is reachable.
33
+ */
34
+ export function usePublishedPerspective(input) {
35
+ const { ref, source, canPublish, canVersions, draft } = input;
36
+ const published = source.publishing?.isPublished(ref) ?? false;
37
+ const [perspective, setPerspective] = useState('draft');
38
+ // Snap back to Draft if the document is unpublished while viewing Published.
39
+ useEffect(() => {
40
+ if (!published)
41
+ setPerspective('draft');
42
+ }, [published]);
43
+ const canDiscard = canPublish && canVersions;
44
+ const { versionsQuery, versionKey } = useDocumentVersionsQuery(ref, {
45
+ enabled: canDiscard && published,
46
+ });
47
+ const versionsView = versionsQuery.data ?? null;
48
+ const publishedVersionNumber = versionsView?.publishedVersion;
49
+ const publishedSnapshotDoc = publishedVersionSnapshot(versionsView);
50
+ const publishedSnapshot = derivePublishedSnapshot(versionsView);
51
+ const { viewReadOnly, viewDoc, publishedPending } = resolveViewDoc({ perspective, publishedSnapshot, draft });
52
+ return {
53
+ perspective,
54
+ setPerspective,
55
+ published,
56
+ canDiscard,
57
+ versionsView,
58
+ versionKey,
59
+ publishedVersionNumber,
60
+ publishedSnapshotDoc,
61
+ publishedSnapshot,
62
+ viewReadOnly,
63
+ viewDoc,
64
+ publishedPending,
65
+ };
66
+ }
67
+ //# sourceMappingURL=use-published-perspective.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-published-perspective.js","sourceRoot":"","sources":["../../../src/components/cms/use-published-perspective.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAK3C,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAKpF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,KAI9B;IACC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,KAAK,WAAW,CAAA;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,iBAAiB,CAAC,MAAM,KAAK,OAAO,CAAA;IACxD,OAAO;QACL,YAAY;QACZ,OAAO,EAAE,YAAY,IAAI,KAAK,CAAC,iBAAiB,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK;QAC/G,gBAAgB,EAAE,YAAY,IAAI,CAAC,KAAK;KACzC,CAAA;AACH,CAAC;AA2BD;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAMvC;IACC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IAC7D,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,KAAK,CAAA;IAC9D,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAc,OAAO,CAAC,CAAA;IACpE,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS;YAAE,cAAc,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,MAAM,UAAU,GAAG,UAAU,IAAI,WAAW,CAAA;IAC5C,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,wBAAwB,CAAC,GAAG,EAAE;QAClE,OAAO,EAAE,UAAU,IAAI,SAAS;KACjC,CAAC,CAAA;IACF,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,IAAI,IAAI,CAAA;IAC/C,MAAM,sBAAsB,GAAG,YAAY,EAAE,gBAAgB,CAAA;IAC7D,MAAM,oBAAoB,GAAG,wBAAwB,CAAC,YAAY,CAAC,CAAA;IACnE,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAA;IAC/D,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAAC,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAA;IAE7G,OAAO;QACL,WAAW;QACX,cAAc;QACd,SAAS;QACT,UAAU;QACV,YAAY;QACZ,UAAU;QACV,sBAAsB;QACtB,oBAAoB;QACpB,iBAAiB;QACjB,YAAY;QACZ,OAAO;QACP,gBAAgB;KACjB,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport type { DocumentVersionsView } from './cms-store'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { derivePublishedSnapshot, publishedVersionSnapshot } from './versions-model'\nimport type { PublishedSnapshotState } from './versions-model'\n\nexport type Perspective = 'draft' | 'published'\n\n/**\n * EC-226 — which document the fieldset renders, given the active perspective and\n * the pinned published snapshot's load state. A pure decision so \"never present\n * the draft AS published\" is single-sourced and directly testable:\n *\n * - Draft perspective → the live draft, editable.\n * - Published perspective, snapshot READY → the pinned snapshot, read-only.\n * - Published perspective, snapshot NOT ready → still the draft as the fallback\n * render target, but flagged `publishedPending` so the shell shows a placeholder\n * instead (the draft must never be shown labelled as the published content).\n */\nexport function resolveViewDoc(input: {\n perspective: Perspective\n publishedSnapshot: PublishedSnapshotState\n draft: CmsDocument\n}): { viewReadOnly: boolean; viewDoc: CmsDocument; publishedPending: boolean } {\n const viewReadOnly = input.perspective === 'published'\n const ready = input.publishedSnapshot.status === 'ready'\n return {\n viewReadOnly,\n viewDoc: viewReadOnly && input.publishedSnapshot.status === 'ready' ? input.publishedSnapshot.doc : input.draft,\n publishedPending: viewReadOnly && !ready,\n }\n}\n\nexport interface PublishedPerspective {\n perspective: Perspective\n setPerspective: (perspective: Perspective) => void\n /** Is the document published (a version pointer is set)? */\n published: boolean\n /** A publishing AND versioned rail — gates the version-history query + Discard. */\n canDiscard: boolean\n /** The recorded version history (null while loading / disabled). */\n versionsView: DocumentVersionsView | null\n /** The shared version-history cache key ({@link documentVersionsKey}). */\n versionKey: QueryKey\n /** The pinned published version number, or undefined while loading / none pinned. */\n publishedVersionNumber: number | undefined\n /** The pinned published version's snapshot, or undefined while loading / none. */\n publishedSnapshotDoc: CmsDocument | undefined\n /** The loaded state of the pinned published snapshot. */\n publishedSnapshot: PublishedSnapshotState\n /** Is the editor viewing the read-only Published snapshot? */\n viewReadOnly: boolean\n /** The document the fieldset renders (draft or the pinned snapshot). */\n viewDoc: CmsDocument\n /** Is the Published snapshot still loading (show a placeholder, not the draft)? */\n publishedPending: boolean\n}\n\n/**\n * EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective plus the pinned\n * published snapshot, both derived from the ONE shared version-history query\n * (AD-17: the single operations-backed cache, `use-document-versions.ts` — no raw\n * `source.versioning.load` call). The Discard-draft gating and the Published\n * perspective read exactly the same `versions`/`publishedVersion` shape, so a\n * second bespoke fetch on perspective switch would just re-read this. Gated the\n * same way Discard always was — a publishing + versioned rail AND a published\n * document — which is also exactly when the Published perspective is reachable.\n */\nexport function usePublishedPerspective(input: {\n ref: DocumentRef\n source: CollectionViewSource\n canPublish: boolean\n canVersions: boolean\n draft: CmsDocument\n}): PublishedPerspective {\n const { ref, source, canPublish, canVersions, draft } = input\n const published = source.publishing?.isPublished(ref) ?? false\n const [perspective, setPerspective] = useState<Perspective>('draft')\n // Snap back to Draft if the document is unpublished while viewing Published.\n useEffect(() => {\n if (!published) setPerspective('draft')\n }, [published])\n\n const canDiscard = canPublish && canVersions\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(ref, {\n enabled: canDiscard && published,\n })\n const versionsView = versionsQuery.data ?? null\n const publishedVersionNumber = versionsView?.publishedVersion\n const publishedSnapshotDoc = publishedVersionSnapshot(versionsView)\n const publishedSnapshot = derivePublishedSnapshot(versionsView)\n const { viewReadOnly, viewDoc, publishedPending } = resolveViewDoc({ perspective, publishedSnapshot, draft })\n\n return {\n perspective,\n setPerspective,\n published,\n canDiscard,\n versionsView,\n versionKey,\n publishedVersionNumber,\n publishedSnapshotDoc,\n publishedSnapshot,\n viewReadOnly,\n viewDoc,\n publishedPending,\n }\n}\n"]}