@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,139 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { defaultViewState } from '@elytracms/studio/lib/cms-views';
3
+ /** The default normalizer for an {@link OkResult}: a failure maps to a
4
+ * surfaced {@link OperationError}; a success maps to no error at all. */
5
+ export function normalizeOkResult(result) {
6
+ return result.ok ? null : { code: 'unknown', message: result.message };
7
+ }
8
+ /**
9
+ * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):
10
+ * every entry whose key is NOT one of the rail's canonical/identity field
11
+ * names round-trips through the record's open `meta` bag. Shared by the users
12
+ * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the
13
+ * assets rail's projection is schema-FIELDS-driven inclusion (iterating the
14
+ * schema's declared fields, not the document's value keys), a genuinely
15
+ * different shape that stays bespoke in `assets-collection-view-source.ts`.
16
+ */
17
+ export function metaBagProjection(values, canonicalNames) {
18
+ const meta = {};
19
+ for (const [key, value] of Object.entries(values)) {
20
+ if (!canonicalNames.has(key))
21
+ meta[key] = value;
22
+ }
23
+ return meta;
24
+ }
25
+ /** Wrap a raw store write (save/remove) so a normalized failure surfaces via
26
+ * `setError` — the factory's error-mapping plumbing, pulled out as a plain
27
+ * function (no hook) so it is unit-testable without a `renderHook` harness. */
28
+ export function createErrorMappedWriter(write, normalize, setError) {
29
+ return (arg) => {
30
+ void write(arg).then((result) => {
31
+ const error = normalize(result);
32
+ if (error)
33
+ setError(error);
34
+ });
35
+ };
36
+ }
37
+ /**
38
+ * Pure assembly of a {@link CollectionViewSource} from a rail's resolved
39
+ * fields + state. Exported (and taking plain data/functions, no hook) so it is
40
+ * directly unit-testable — this workspace runs vitest in `environment: 'node'`
41
+ * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`
42
+ * wrapper around it.
43
+ */
44
+ export function assembleRailViewSource(input) {
45
+ const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, validate, create, lastError, clearLastError, viewState, setViewState, save, remove, } = input;
46
+ return {
47
+ collectionId: schema.id,
48
+ schema,
49
+ capabilities,
50
+ projectId,
51
+ localeConfig,
52
+ activeLocale: localeConfig.default,
53
+ documents,
54
+ get,
55
+ titleOf,
56
+ referenceCandidates: referenceCandidates ?? (() => []),
57
+ collections: collections ?? [schema],
58
+ ...(detailPath ? { detailPath } : {}),
59
+ ...(listPath !== undefined ? { listPath } : {}),
60
+ isFieldReadOnly: isFieldReadOnly ?? ((field) => field.form?.readOnly ?? false),
61
+ ...(fieldAccess ? { fieldAccess } : {}),
62
+ validate,
63
+ lastError,
64
+ clearLastError,
65
+ viewState: () => viewState,
66
+ setViewState,
67
+ create,
68
+ save,
69
+ remove,
70
+ };
71
+ }
72
+ /**
73
+ * The shared scaffold factory (EC-327) behind the assets/users/redirects rail
74
+ * `CollectionViewSource` hooks: `viewState`/`lastError` state, the
75
+ * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via
76
+ * the rail's own result normalizer), and the final assembled source. See the
77
+ * module doc for what stays a per-rail parameter rather than being absorbed
78
+ * here.
79
+ */
80
+ export function useRailViewSource(adapter) {
81
+ const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, validate, create, save: rawSave, remove: rawRemove, normalizeSaveResult, normalizeRemoveResult, } = adapter;
82
+ const [viewState, setViewStateRaw] = useState(() => defaultViewState(schema));
83
+ const [lastError, setLastError] = useState(null);
84
+ const setViewState = useCallback((update) => {
85
+ setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update));
86
+ }, []);
87
+ const clearLastError = useCallback(() => setLastError(null), []);
88
+ const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [
89
+ rawSave,
90
+ normalizeSaveResult,
91
+ ]);
92
+ const remove = useCallback(createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError), [rawRemove, normalizeRemoveResult]);
93
+ return useMemo(() => assembleRailViewSource({
94
+ projectId,
95
+ schema,
96
+ capabilities,
97
+ localeConfig,
98
+ documents,
99
+ get,
100
+ titleOf,
101
+ referenceCandidates,
102
+ collections,
103
+ detailPath,
104
+ listPath,
105
+ isFieldReadOnly,
106
+ fieldAccess,
107
+ validate,
108
+ create,
109
+ lastError,
110
+ clearLastError,
111
+ viewState,
112
+ setViewState,
113
+ save,
114
+ remove,
115
+ }), [
116
+ projectId,
117
+ schema,
118
+ capabilities,
119
+ localeConfig,
120
+ documents,
121
+ get,
122
+ titleOf,
123
+ referenceCandidates,
124
+ collections,
125
+ detailPath,
126
+ listPath,
127
+ isFieldReadOnly,
128
+ fieldAccess,
129
+ validate,
130
+ create,
131
+ lastError,
132
+ clearLastError,
133
+ viewState,
134
+ setViewState,
135
+ save,
136
+ remove,
137
+ ]);
138
+ }
139
+ //# sourceMappingURL=rail-view-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult>(\n write: (arg: TArg) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg) => void {\n return (arg: TArg) => {\n void write(arg).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n save(doc: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n ],\n )\n}\n"]}
@@ -1,6 +1,14 @@
1
1
  import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
2
  import type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence';
3
- import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ import type { CollectionViewCapabilities, CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ /**
5
+ * The capability profile of the redirects rail (EC-303): content-gated, create
6
+ * + remove, no publish/versions/locales — redirects are CONTENT (an editor
7
+ * owns them), so unlike the users rail this source allows `create`, is NOT
8
+ * admin-only, and needs no principal field-access policy. Hoisted to a
9
+ * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.
10
+ */
11
+ export declare const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
4
12
  /** Project a stored redirect into the lens's `values` shape. */
5
13
  export declare function redirectToDocument(entry: RedirectEntry): CmsDocument;
6
14
  /** Map an edited lens document back to the redirect write contract. */
@@ -21,5 +29,9 @@ export declare function documentToSaveRedirect(doc: CmsDocument): SaveRedirectIn
21
29
  * names+types are contract-locked, so the projection helpers still read the same
22
30
  * canonical values; a mount's *additive* fields (Tier 2) round-trip through the
23
31
  * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).
32
+ *
33
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
34
+ * plumbing and the assembled source; this hook supplies only the redirects-store
35
+ * reads/writes, the capability profile, and the registry-driven `validate`.
24
36
  */
25
37
  export declare function useRedirectsCollectionViewSource(projectId: string, schema?: CollectionDef): CollectionViewSource;
@@ -1,15 +1,35 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback, useMemo } from 'react';
2
2
  import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
3
3
  import { studioPath } from '@elytracms/studio/router';
4
- import { defaultViewState } from '@elytracms/studio/lib/cms-views';
5
4
  import { useRedirects } from './redirects-store';
6
5
  import { redirectsCollectionDef } from './redirects-schema';
6
+ import { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source';
7
7
  /** The redirects rail is not localized — one implicit locale keeps the lens honest. */
8
8
  const REDIRECTS_LOCALE = 'en';
9
9
  const REDIRECTS_LOCALE_CONFIG = {
10
10
  default: REDIRECTS_LOCALE,
11
11
  locales: [REDIRECTS_LOCALE],
12
12
  };
13
+ /**
14
+ * The capability profile of the redirects rail (EC-303): content-gated, create
15
+ * + remove, no publish/versions/locales — redirects are CONTENT (an editor
16
+ * owns them), so unlike the users rail this source allows `create`, is NOT
17
+ * admin-only, and needs no principal field-access policy. Hoisted to a
18
+ * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.
19
+ */
20
+ export const REDIRECT_COLLECTION_CAPABILITIES = {
21
+ publish: false,
22
+ versions: false,
23
+ locales: false,
24
+ inlineEdit: false,
25
+ create: true, // editors create redirects
26
+ remove: true,
27
+ bulk: false,
28
+ liveUrl: false,
29
+ hierarchy: false,
30
+ importExport: false,
31
+ adminOnly: false, // content, not administration
32
+ };
13
33
  /**
14
34
  * The canonical redirect fields — the storage/delivery contract of the primitive
15
35
  * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311
@@ -39,11 +59,7 @@ export function documentToSaveRedirect(doc) {
39
59
  // additive `nav.redirects({ fields })` override — persist it in the meta bag. The
40
60
  // editor holds the complete additive set, so this is the whole bag (replace, not
41
61
  // merge). Omit `meta` entirely for the default rail so its saves are unchanged.
42
- const meta = {};
43
- for (const [key, value] of Object.entries(values)) {
44
- if (!CANONICAL_REDIRECT_FIELDS.has(key))
45
- meta[key] = value;
46
- }
62
+ const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS);
47
63
  return {
48
64
  id: doc.id,
49
65
  from: typeof values.from === 'string' ? values.from : '',
@@ -69,11 +85,13 @@ export function documentToSaveRedirect(doc) {
69
85
  * names+types are contract-locked, so the projection helpers still read the same
70
86
  * canonical values; a mount's *additive* fields (Tier 2) round-trip through the
71
87
  * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).
88
+ *
89
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
90
+ * plumbing and the assembled source; this hook supplies only the redirects-store
91
+ * reads/writes, the capability profile, and the registry-driven `validate`.
72
92
  */
73
93
  export function useRedirectsCollectionViewSource(projectId, schema = redirectsCollectionDef) {
74
94
  const store = useRedirects();
75
- const [viewState, setViewStateRaw] = useState(() => defaultViewState(schema));
76
- const [lastError, setLastError] = useState(null);
77
95
  // EC-311: run the (possibly customized) schema's validation — required/pattern and
78
96
  // custom `validate` fns on an overridden field must actually fire in the editor.
79
97
  const registry = useMemo(() => createCollectionRegistry([schema]), [schema]);
@@ -83,75 +101,29 @@ export function useRedirectsCollectionViewSource(projectId, schema = redirectsCo
83
101
  return record ? redirectToDocument(record) : undefined;
84
102
  }, [store]);
85
103
  const titleOf = useCallback((ref) => store.get(ref.id)?.from || '(new redirect)', [store]);
86
- const setViewState = useCallback((update) => {
87
- setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update));
88
- }, []);
104
+ const validate = useCallback((doc) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }), [registry]);
89
105
  const create = useCallback(() => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }), [store]);
90
- const save = useCallback((doc) => {
91
- void store.save(documentToSaveRedirect(doc)).then((result) => {
92
- if (!result.ok)
93
- setLastError({ code: 'unknown', message: result.message });
94
- });
95
- }, [store]);
96
- const remove = useCallback((ref) => {
97
- void store.remove(ref.id).then((result) => {
98
- if (!result.ok)
99
- setLastError({ code: 'unknown', message: result.message });
100
- });
101
- }, [store]);
102
- return useMemo(() => {
103
- const capabilities = {
104
- publish: false,
105
- versions: false,
106
- locales: false,
107
- inlineEdit: false,
108
- create: true, // editors create redirects
109
- remove: true,
110
- bulk: false,
111
- liveUrl: false,
112
- hierarchy: false,
113
- importExport: false,
114
- adminOnly: false, // content, not administration
115
- };
116
- const source = {
117
- collectionId: schema.id,
118
- schema,
119
- capabilities,
120
- projectId,
121
- localeConfig: REDIRECTS_LOCALE_CONFIG,
122
- activeLocale: REDIRECTS_LOCALE,
123
- documents,
124
- get,
125
- titleOf,
126
- // Redirects carry no relation/asset fields, so nothing cross-collection to resolve.
127
- referenceCandidates: () => [],
128
- collections: [schema],
129
- detailPath: (id) => studioPath.redirect(id),
130
- listPath: studioPath.redirects(),
131
- isFieldReadOnly: (field) => field.form?.readOnly ?? false,
132
- validate: (doc) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),
133
- lastError,
134
- clearLastError: () => setLastError(null),
135
- viewState: () => viewState,
136
- setViewState,
137
- create,
138
- save,
139
- remove,
140
- };
141
- return source;
142
- }, [
106
+ const save = useCallback((doc) => store.save(documentToSaveRedirect(doc)), [store]);
107
+ const remove = useCallback((ref) => store.remove(ref.id), [store]);
108
+ return useRailViewSource({
143
109
  projectId,
144
110
  schema,
145
- registry,
111
+ capabilities: REDIRECT_COLLECTION_CAPABILITIES,
112
+ localeConfig: REDIRECTS_LOCALE_CONFIG,
146
113
  documents,
147
114
  get,
148
115
  titleOf,
149
- setViewState,
116
+ // No `referenceCandidates`/`collections` override: redirects carry no
117
+ // relation/asset fields, so nothing cross-collection to resolve (the
118
+ // factory's `() => []` / `[schema]` defaults apply).
119
+ detailPath: (id) => studioPath.redirect(id),
120
+ listPath: studioPath.redirects(),
121
+ validate,
150
122
  create,
151
123
  save,
152
124
  remove,
153
- viewState,
154
- lastError,
155
- ]);
125
+ normalizeSaveResult: normalizeOkResult,
126
+ normalizeRemoveResult: normalizeOkResult,
127
+ });
156
128
  }
157
129
  //# sourceMappingURL=redirects-collection-view-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!CANONICAL_REDIRECT_FIELDS.has(key)) meta[key] = value\n }\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.redirects.map(redirectToDocument),\n [store.redirects],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback(\n (doc: CmsDocument) => {\n void store.save(documentToSaveRedirect(doc)).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n const remove = useCallback(\n (ref: DocumentRef) => {\n void store.remove(ref.id).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n\n return useMemo(() => {\n const capabilities: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n }\n const source: CollectionViewSource = {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n activeLocale: REDIRECTS_LOCALE,\n documents,\n get,\n titleOf,\n // Redirects carry no relation/asset fields, so nothing cross-collection to resolve.\n referenceCandidates: () => [],\n collections: [schema],\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n isFieldReadOnly: (field: FieldDef) => field.form?.readOnly ?? false,\n validate: (doc: CmsDocument) =>\n validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n lastError,\n clearLastError: () => setLastError(null),\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n }\n return source\n }, [\n projectId,\n schema,\n registry,\n documents,\n get,\n titleOf,\n setViewState,\n create,\n save,\n remove,\n viewState,\n lastError,\n ])\n}\n"]}
1
+ {"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The capability profile of the redirects rail (EC-303): content-gated, create\n * + remove, no publish/versions/locales — redirects are CONTENT (an editor\n * owns them), so unlike the users rail this source allows `create`, is NOT\n * admin-only, and needs no principal field-access policy. Hoisted to a\n * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS)\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the redirects-store\n * reads/writes, the capability profile, and the registry-driven `validate`.\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback((doc: CmsDocument) => store.save(documentToSaveRedirect(doc)), [store])\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: REDIRECT_COLLECTION_CAPABILITIES,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: redirects carry no\n // relation/asset fields, so nothing cross-collection to resolve (the\n // factory's `() => []` / `[schema]` defaults apply).\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
@@ -39,8 +39,9 @@ export interface RedirectsStore {
39
39
  /** Re-list from the backend. */
40
40
  refresh(): Promise<void>;
41
41
  }
42
+ declare const useRedirects: () => RedirectsStore;
42
43
  export declare function RedirectsProvider({ projectId, children, }: {
43
44
  projectId: string;
44
45
  children: ReactNode;
45
46
  }): import("react").JSX.Element;
46
- export declare function useRedirects(): RedirectsStore;
47
+ export { useRedirects };
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useMemo, useState } from 'react';
2
+ import { useCallback, useMemo, useState } from 'react';
3
3
  import { getStudioOperations } from '@elytracms/studio/lib/operations';
4
4
  import { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query';
5
- const RedirectsContext = createContext(null);
5
+ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
6
+ const { Context: RedirectsContext, useRequired: useRedirects } = createRequiredContext('Redirects');
6
7
  const EMPTY_REDIRECTS = [];
7
8
  let draftCounter = 0;
8
9
  /** A collision-resistant client id for a new redirect (used as its real id on save). */
@@ -114,10 +115,5 @@ export function RedirectsProvider({ projectId, children, }) {
114
115
  }), [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh]);
115
116
  return _jsx(RedirectsContext.Provider, { value: store, children: children });
116
117
  }
117
- export function useRedirects() {
118
- const store = useContext(RedirectsContext);
119
- if (!store)
120
- throw new Error('useRedirects must be used within a <RedirectsProvider>.');
121
- return store;
122
- }
118
+ export { useRedirects };
123
119
  //# sourceMappingURL=redirects-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst RedirectsContext = createContext<RedirectsStore | null>(null)\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const { status, errorMessage } = listStoreState(query)\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n // New redirects default to enabled — the reason you create one is for it to\n // work; `enabled` is really the switch for *pausing* an existing redirect. A\n // half-filled draft can't go live anyway (save requires from/to).\n enabled: true,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n ...(input.meta !== undefined ? { meta: input.meta } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport function useRedirects(): RedirectsStore {\n const store = useContext(RedirectsContext)\n if (!store) throw new Error('useRedirects must be used within a <RedirectsProvider>.')\n return store\n}\n"]}
1
+ {"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RedirectsContext, useRequired: useRedirects } =\n createRequiredContext<RedirectsStore>('Redirects')\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const { status, errorMessage } = listStoreState(query)\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n // New redirects default to enabled — the reason you create one is for it to\n // work; `enabled` is really the switch for *pausing* an existing redirect. A\n // half-filled draft can't go live anyway (save requires from/to).\n enabled: true,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n ...(input.meta !== undefined ? { meta: input.meta } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport { useRedirects }\n"]}
@@ -16,7 +16,16 @@ export interface SectionStateViewProps {
16
16
  }
17
17
  /** Loading uses a skeleton; the rest use a centered placeholder. Consistent across sections. */
18
18
  export declare function SectionStateView({ state, title, description, action, className, }: SectionStateViewProps): import("react").JSX.Element;
19
- /** Shimmer skeleton standing in for an in-flight section load. */
20
- export declare function SectionSkeleton({ className }: {
19
+ export interface SectionSkeletonProps {
21
20
  className?: string;
22
- }): import("react").JSX.Element;
21
+ /**
22
+ * Section-specific loading heading (e.g. "Loading assets…", EC-327). Renders
23
+ * as real text instead of the generic shimmer bar; omitted keeps the bare
24
+ * shimmer (the pre-EC-327 look, still used by callers with no rail-specific
25
+ * copy to show).
26
+ */
27
+ label?: string;
28
+ description?: ReactNode;
29
+ }
30
+ /** Shimmer skeleton standing in for an in-flight section load. */
31
+ export declare function SectionSkeleton({ className, label, description }: SectionSkeletonProps): import("react").JSX.Element;
@@ -28,8 +28,12 @@ const PRESETS = {
28
28
  };
29
29
  /** Loading uses a skeleton; the rest use a centered placeholder. Consistent across sections. */
30
30
  export function SectionStateView({ state, title, description, action, className, }) {
31
- if (state === 'loading')
32
- return _jsx(SectionSkeleton, { className: className });
31
+ // `title`/`description` thread through to the skeleton's optional label slot
32
+ // (EC-327) — without this, a caller's section-specific loading copy (e.g. "Loading
33
+ // assets…") would be silently dropped the moment `state` was 'loading'.
34
+ if (state === 'loading') {
35
+ return _jsx(SectionSkeleton, { className: className, label: title, description: description });
36
+ }
33
37
  const preset = PRESETS[state];
34
38
  const Icon = preset.icon;
35
39
  // `state` is narrowed to a non-loading state here, so each has a title/description/badge key.
@@ -44,7 +48,7 @@ export function SectionStateView({ state, title, description, action, className,
44
48
  return (_jsx("div", { "data-section-state": state, className: cn('grid min-h-[18rem] place-items-center p-8', className), children: _jsxs(Stack, { gap: "3", align: "center", className: cn('max-w-sm rounded-xl border border-dashed px-8 py-10 text-center', tone), children: [_jsx("span", { className: "grid size-11 place-items-center rounded-lg border border-border bg-background text-muted-foreground", children: _jsx(Icon, { className: "size-5" }) }), _jsxs(Stack, { gap: "1", align: "center", children: [_jsx(Heading, { level: 2, size: "h3", children: title ?? presetTitle }), _jsx(Text, { size: "sm", tone: "muted", children: description ?? presetDescription })] }), _jsx(Badge, { variant: preset.badgeVariant, children: badgeLabel }), action ? _jsx("div", { className: "pt-1", children: action }) : null] }) }));
45
49
  }
46
50
  /** Shimmer skeleton standing in for an in-flight section load. */
47
- export function SectionSkeleton({ className }) {
48
- return (_jsxs("div", { "data-section-state": "loading", className: cn('space-y-4 p-6', className), "aria-busy": true, children: [_jsx("div", { className: "h-7 w-48 animate-pulse rounded-md bg-muted" }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "h-28 animate-pulse rounded-xl bg-muted/70" }, i))) })] }));
51
+ export function SectionSkeleton({ className, label, description }) {
52
+ return (_jsxs("div", { "data-section-state": "loading", className: cn('space-y-4 p-6', className), "aria-busy": true, children: [label ? (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { weight: "medium", children: label }), description ? (_jsx(Text, { size: "sm", tone: "muted", children: description })) : null] })) : (_jsx("div", { className: "h-7 w-48 animate-pulse rounded-md bg-muted" })), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "h-28 animate-pulse rounded-xl bg-muted/70" }, i))) })] }));
49
53
  }
50
54
  //# sourceMappingURL=section-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"section-state.js","sourceRoot":"","sources":["../../src/components/section-state.tsx"],"names":[],"mappings":"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","sourcesContent":["import { CircleAlert, Clock, Inbox, Loader2, Lock, type LucideIcon } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { SectionAvailability, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Resolve the runtime state of a section (brief §8.1, §11). A `forced` value\n * (e.g. from a `?state=` param) wins, making every state reachable\n * deterministically; otherwise availability decides.\n */\nexport function resolveSectionState(\n availability: SectionAvailability,\n forced?: SectionState | null,\n): SectionState {\n if (forced) return forced\n if (availability === 'planned') return 'planned'\n if (availability === 'unavailable') return 'unavailable'\n return 'working'\n}\n\ninterface StatePreset {\n icon: LucideIcon\n badgeVariant: 'outline' | 'warning' | 'error' | 'info'\n spin?: boolean\n}\n\n// Structural (icon/variant) only; the title/description/badge copy is resolved at\n// the render site via `translate` so it reflects the active UI locale (EC-241).\nconst PRESETS: Record<Exclude<SectionState, 'working'>, StatePreset> = {\n empty: { icon: Inbox, badgeVariant: 'outline' },\n loading: { icon: Loader2, badgeVariant: 'info', spin: true },\n error: { icon: CircleAlert, badgeVariant: 'error' },\n unavailable: { icon: Lock, badgeVariant: 'outline' },\n planned: { icon: Clock, badgeVariant: 'warning' },\n}\n\nexport interface SectionStateViewProps {\n state: Exclude<SectionState, 'working'>\n /** Section-specific heading, e.g. \"CMS\" — overrides the generic preset title. */\n title?: string\n description?: ReactNode\n action?: ReactNode\n className?: string\n}\n\n/** Loading uses a skeleton; the rest use a centered placeholder. Consistent across sections. */\nexport function SectionStateView({\n state,\n title,\n description,\n action,\n className,\n}: SectionStateViewProps) {\n if (state === 'loading') return <SectionSkeleton className={className} />\n\n const preset = PRESETS[state]\n const Icon = preset.icon\n // `state` is narrowed to a non-loading state here, so each has a title/description/badge key.\n const presetTitle = translate(`shell.sectionState.${state}.title`)\n const presetDescription = translate(`shell.sectionState.${state}.description`)\n const badgeLabel = translate(`shell.sectionState.badge.${state}`)\n const tone =\n state === 'error'\n ? 'border-destructive/30 bg-destructive/5'\n : state === 'planned'\n ? 'border-warning/30 bg-warning/5'\n : 'border-border bg-card/40'\n\n return (\n <div\n data-section-state={state}\n className={cn('grid min-h-[18rem] place-items-center p-8', className)}\n >\n <Stack\n gap=\"3\"\n align=\"center\"\n className={cn('max-w-sm rounded-xl border border-dashed px-8 py-10 text-center', tone)}\n >\n <span className=\"grid size-11 place-items-center rounded-lg border border-border bg-background text-muted-foreground\">\n <Icon className=\"size-5\" />\n </span>\n <Stack gap=\"1\" align=\"center\">\n <Heading level={2} size=\"h3\">\n {title ?? presetTitle}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {description ?? presetDescription}\n </Text>\n </Stack>\n <Badge variant={preset.badgeVariant}>{badgeLabel}</Badge>\n {action ? <div className=\"pt-1\">{action}</div> : null}\n </Stack>\n </div>\n )\n}\n\n/** Shimmer skeleton standing in for an in-flight section load. */\nexport function SectionSkeleton({ className }: { className?: string }) {\n return (\n <div data-section-state=\"loading\" className={cn('space-y-4 p-6', className)} aria-busy>\n <div className=\"h-7 w-48 animate-pulse rounded-md bg-muted\" />\n <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-28 animate-pulse rounded-xl bg-muted/70\" />\n ))}\n </div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"section-state.js","sourceRoot":"","sources":["../../src/components/section-state.tsx"],"names":[],"mappings":"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","sourcesContent":["import { CircleAlert, Clock, Inbox, Loader2, Lock, type LucideIcon } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { SectionAvailability, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Resolve the runtime state of a section (brief §8.1, §11). A `forced` value\n * (e.g. from a `?state=` param) wins, making every state reachable\n * deterministically; otherwise availability decides.\n */\nexport function resolveSectionState(\n availability: SectionAvailability,\n forced?: SectionState | null,\n): SectionState {\n if (forced) return forced\n if (availability === 'planned') return 'planned'\n if (availability === 'unavailable') return 'unavailable'\n return 'working'\n}\n\ninterface StatePreset {\n icon: LucideIcon\n badgeVariant: 'outline' | 'warning' | 'error' | 'info'\n spin?: boolean\n}\n\n// Structural (icon/variant) only; the title/description/badge copy is resolved at\n// the render site via `translate` so it reflects the active UI locale (EC-241).\nconst PRESETS: Record<Exclude<SectionState, 'working'>, StatePreset> = {\n empty: { icon: Inbox, badgeVariant: 'outline' },\n loading: { icon: Loader2, badgeVariant: 'info', spin: true },\n error: { icon: CircleAlert, badgeVariant: 'error' },\n unavailable: { icon: Lock, badgeVariant: 'outline' },\n planned: { icon: Clock, badgeVariant: 'warning' },\n}\n\nexport interface SectionStateViewProps {\n state: Exclude<SectionState, 'working'>\n /** Section-specific heading, e.g. \"CMS\" — overrides the generic preset title. */\n title?: string\n description?: ReactNode\n action?: ReactNode\n className?: string\n}\n\n/** Loading uses a skeleton; the rest use a centered placeholder. Consistent across sections. */\nexport function SectionStateView({\n state,\n title,\n description,\n action,\n className,\n}: SectionStateViewProps) {\n // `title`/`description` thread through to the skeleton's optional label slot\n // (EC-327) — without this, a caller's section-specific loading copy (e.g. \"Loading\n // assets…\") would be silently dropped the moment `state` was 'loading'.\n if (state === 'loading') {\n return <SectionSkeleton className={className} label={title} description={description} />\n }\n\n const preset = PRESETS[state]\n const Icon = preset.icon\n // `state` is narrowed to a non-loading state here, so each has a title/description/badge key.\n const presetTitle = translate(`shell.sectionState.${state}.title`)\n const presetDescription = translate(`shell.sectionState.${state}.description`)\n const badgeLabel = translate(`shell.sectionState.badge.${state}`)\n const tone =\n state === 'error'\n ? 'border-destructive/30 bg-destructive/5'\n : state === 'planned'\n ? 'border-warning/30 bg-warning/5'\n : 'border-border bg-card/40'\n\n return (\n <div\n data-section-state={state}\n className={cn('grid min-h-[18rem] place-items-center p-8', className)}\n >\n <Stack\n gap=\"3\"\n align=\"center\"\n className={cn('max-w-sm rounded-xl border border-dashed px-8 py-10 text-center', tone)}\n >\n <span className=\"grid size-11 place-items-center rounded-lg border border-border bg-background text-muted-foreground\">\n <Icon className=\"size-5\" />\n </span>\n <Stack gap=\"1\" align=\"center\">\n <Heading level={2} size=\"h3\">\n {title ?? presetTitle}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {description ?? presetDescription}\n </Text>\n </Stack>\n <Badge variant={preset.badgeVariant}>{badgeLabel}</Badge>\n {action ? <div className=\"pt-1\">{action}</div> : null}\n </Stack>\n </div>\n )\n}\n\nexport interface SectionSkeletonProps {\n className?: string\n /**\n * Section-specific loading heading (e.g. \"Loading assets…\", EC-327). Renders\n * as real text instead of the generic shimmer bar; omitted keeps the bare\n * shimmer (the pre-EC-327 look, still used by callers with no rail-specific\n * copy to show).\n */\n label?: string\n description?: ReactNode\n}\n\n/** Shimmer skeleton standing in for an in-flight section load. */\nexport function SectionSkeleton({ className, label, description }: SectionSkeletonProps) {\n return (\n <div data-section-state=\"loading\" className={cn('space-y-4 p-6', className)} aria-busy>\n {label ? (\n <Stack gap=\"1\">\n <Text weight=\"medium\">{label}</Text>\n {description ? (\n <Text size=\"sm\" tone=\"muted\">\n {description}\n </Text>\n ) : null}\n </Stack>\n ) : (\n <div className=\"h-7 w-48 animate-pulse rounded-md bg-muted\" />\n )}\n <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-28 animate-pulse rounded-xl bg-muted/70\" />\n ))}\n </div>\n </div>\n )\n}\n"]}
@@ -16,7 +16,8 @@ export interface StudioStore {
16
16
  /** Record a project as active + most-recent (idempotent). */
17
17
  activate(id: ProjectId): void;
18
18
  }
19
+ declare const useStudio: () => StudioStore;
19
20
  export declare function StudioProvider({ children }: {
20
21
  children: ReactNode;
21
22
  }): import("react").JSX.Element;
22
- export declare function useStudio(): StudioStore;
23
+ export { useStudio };