@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
@@ -1,9 +1,10 @@
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 { activateProject, configProject, emptyContext } from '@elytracms/core/studio-core';
4
4
  import { getWorkspaceState } from '@elytracms/studio/lib/workspace';
5
5
  import { StudioQueryProvider } from '@elytracms/studio/lib/query';
6
- const StudioContext = createContext(null);
6
+ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
7
+ const { Context: StudioContext, useRequired: useStudio } = createRequiredContext('Studio');
7
8
  /** Resolve the config-declared projects into studio `Project`s (slug-keyed). */
8
9
  function configProjects() {
9
10
  const state = getWorkspaceState();
@@ -43,10 +44,5 @@ export function StudioProvider({ children }) {
43
44
  // studio server read flows through `useOperationQuery`.
44
45
  return (_jsx(StudioContext.Provider, { value: store, children: _jsx(StudioQueryProvider, { children: children }) }));
45
46
  }
46
- export function useStudio() {
47
- const store = useContext(StudioContext);
48
- if (!store)
49
- throw new Error('useStudio must be used within a <StudioProvider>.');
50
- return store;
51
- }
47
+ export { useStudio };
52
48
  //# sourceMappingURL=studio-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"studio-context.js","sourceRoot":"","sources":["../../src/components/studio-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEjF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAoBjE,MAAM,aAAa,GAAG,aAAa,CAAqB,IAAI,CAAC,CAAA;AAE7D,gFAAgF;AAChF,SAAS,cAAc;IACrB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IACtC,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAClC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACpC,oEAAoE;QACpE,mEAAmE;QACnE,4EAA4E;QAC5E,8EAA8E;QAC9E,8EAA8E;QAC9E,OAAO;YACL,GAAG,OAAO;YACV,QAAQ,EAAE;gBACR,GAAG,OAAO,CAAC,QAAQ;gBACnB,mBAAmB,EAAE;oBACnB,GAAG,OAAO,CAAC,QAAQ,CAAC,mBAAmB;oBACvC,UAAU,EAAE,aAAa;iBAC1B;aACF;SACF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAA2B;IAClE,6EAA6E;IAC7E,2DAA2D;IAC3D,MAAM,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAqB,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAA;IACvE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAuB,YAAY,CAAC,CAAA;IAE1E,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,EAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEhG,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,EAAa,EAAE,EAAE;QAC7C,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;IACxF,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,EACnD,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAC1C,CAAA;IAED,0EAA0E;IAC1E,yEAAyE;IACzE,wDAAwD;IACxD,OAAO,CACL,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAClC,KAAC,mBAAmB,cAAE,QAAQ,GAAuB,GAC9B,CAC1B,CAAA;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,KAAK,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;IACvC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;IAChF,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { activateProject, configProject, emptyContext } from '@elytracms/core/studio-core'\nimport type { ActiveProjectContext, Project, ProjectId } from '@elytracms/core/studio-core'\nimport { getWorkspaceState } from '@elytracms/studio/lib/workspace'\nimport { StudioQueryProvider } from '@elytracms/studio/lib/query'\n\n/**\n * Project read store (vision AD-8). Project *identity* is owned by\n * `elytra.config.ts` — the studio reads the project list from the resolved\n * workspace config, never from the backend. The active project id is route-\n * driven; this store holds the config-derived list + the recents context.\n * Lifecycle (create/rename/archive/delete) lives in code/CLI, not the UI.\n */\n\nexport interface StudioStore {\n /** Projects declared in `elytra.config.ts`, in config order. */\n projects: readonly Project[]\n /** Active-project context (recents); the active id itself is route-driven. */\n context: ActiveProjectContext\n getProject(id: ProjectId): Project | undefined\n /** Record a project as active + most-recent (idempotent). */\n activate(id: ProjectId): void\n}\n\nconst StudioContext = createContext<StudioStore | null>(null)\n\n/** Resolve the config-declared projects into studio `Project`s (slug-keyed). */\nfunction configProjects(): readonly Project[] {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return []\n return state.projects.map((entry) => {\n const project = configProject(entry)\n // EC-221: the plugin marketplace UI is cut from v1 (vision §5). The\n // Extensions surface is pulled from the nav by marking the section\n // unavailable — the rail omits unavailable sections. The @elytracms/plugins\n // package, ExtensionsView, and the `/extensions` route all remain in the tree\n // for Milestone E revival (reachable by URL, just not advertised in the nav).\n return {\n ...project,\n settings: {\n ...project.settings,\n sectionAvailability: {\n ...project.settings.sectionAvailability,\n extensions: 'unavailable',\n },\n },\n }\n })\n}\n\nexport function StudioProvider({ children }: { children: ReactNode }) {\n // The workspace config is static for the app's lifetime (bundled at build or\n // injected via env), so the project list is computed once.\n const [projects] = useState<readonly Project[]>(() => configProjects())\n const [context, setContext] = useState<ActiveProjectContext>(emptyContext)\n\n const getProject = useCallback((id: ProjectId) => projects.find((p) => p.id === id), [projects])\n\n const activate = useCallback((id: ProjectId) => {\n setContext((prev) => (prev.activeProjectId === id ? prev : activateProject(prev, id)))\n }, [])\n\n const store = useMemo<StudioStore>(\n () => ({ projects, context, getProject, activate }),\n [projects, context, getProject, activate],\n )\n\n // EC-308/AD-17: the single client query cache mounts here (package-owned,\n // above every per-project provider, surviving project switches) so every\n // studio server read flows through `useOperationQuery`.\n return (\n <StudioContext.Provider value={store}>\n <StudioQueryProvider>{children}</StudioQueryProvider>\n </StudioContext.Provider>\n )\n}\n\nexport function useStudio(): StudioStore {\n const store = useContext(StudioContext)\n if (!store) throw new Error('useStudio must be used within a <StudioProvider>.')\n return store\n}\n"]}
1
+ {"version":3,"file":"studio-context.js","sourceRoot":"","sources":["../../src/components/studio-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEtD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAoB9E,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,qBAAqB,CAAc,QAAQ,CAAC,CAAA;AAEvG,gFAAgF;AAChF,SAAS,cAAc;IACrB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IACtC,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAClC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACpC,oEAAoE;QACpE,mEAAmE;QACnE,4EAA4E;QAC5E,8EAA8E;QAC9E,8EAA8E;QAC9E,OAAO;YACL,GAAG,OAAO;YACV,QAAQ,EAAE;gBACR,GAAG,OAAO,CAAC,QAAQ;gBACnB,mBAAmB,EAAE;oBACnB,GAAG,OAAO,CAAC,QAAQ,CAAC,mBAAmB;oBACvC,UAAU,EAAE,aAAa;iBAC1B;aACF;SACF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAA2B;IAClE,6EAA6E;IAC7E,2DAA2D;IAC3D,MAAM,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAqB,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAA;IACvE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAuB,YAAY,CAAC,CAAA;IAE1E,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,EAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEhG,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,EAAa,EAAE,EAAE;QAC7C,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;IACxF,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,EACnD,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAC1C,CAAA;IAED,0EAA0E;IAC1E,yEAAyE;IACzE,wDAAwD;IACxD,OAAO,CACL,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAClC,KAAC,mBAAmB,cAAE,QAAQ,GAAuB,GAC9B,CAC1B,CAAA;AACH,CAAC;AAED,OAAO,EAAE,SAAS,EAAE,CAAA","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { activateProject, configProject, emptyContext } from '@elytracms/core/studio-core'\nimport type { ActiveProjectContext, Project, ProjectId } from '@elytracms/core/studio-core'\nimport { getWorkspaceState } from '@elytracms/studio/lib/workspace'\nimport { StudioQueryProvider } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Project read store (vision AD-8). Project *identity* is owned by\n * `elytra.config.ts` — the studio reads the project list from the resolved\n * workspace config, never from the backend. The active project id is route-\n * driven; this store holds the config-derived list + the recents context.\n * Lifecycle (create/rename/archive/delete) lives in code/CLI, not the UI.\n */\n\nexport interface StudioStore {\n /** Projects declared in `elytra.config.ts`, in config order. */\n projects: readonly Project[]\n /** Active-project context (recents); the active id itself is route-driven. */\n context: ActiveProjectContext\n getProject(id: ProjectId): Project | undefined\n /** Record a project as active + most-recent (idempotent). */\n activate(id: ProjectId): void\n}\n\nconst { Context: StudioContext, useRequired: useStudio } = createRequiredContext<StudioStore>('Studio')\n\n/** Resolve the config-declared projects into studio `Project`s (slug-keyed). */\nfunction configProjects(): readonly Project[] {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return []\n return state.projects.map((entry) => {\n const project = configProject(entry)\n // EC-221: the plugin marketplace UI is cut from v1 (vision §5). The\n // Extensions surface is pulled from the nav by marking the section\n // unavailable — the rail omits unavailable sections. The @elytracms/plugins\n // package, ExtensionsView, and the `/extensions` route all remain in the tree\n // for Milestone E revival (reachable by URL, just not advertised in the nav).\n return {\n ...project,\n settings: {\n ...project.settings,\n sectionAvailability: {\n ...project.settings.sectionAvailability,\n extensions: 'unavailable',\n },\n },\n }\n })\n}\n\nexport function StudioProvider({ children }: { children: ReactNode }) {\n // The workspace config is static for the app's lifetime (bundled at build or\n // injected via env), so the project list is computed once.\n const [projects] = useState<readonly Project[]>(() => configProjects())\n const [context, setContext] = useState<ActiveProjectContext>(emptyContext)\n\n const getProject = useCallback((id: ProjectId) => projects.find((p) => p.id === id), [projects])\n\n const activate = useCallback((id: ProjectId) => {\n setContext((prev) => (prev.activeProjectId === id ? prev : activateProject(prev, id)))\n }, [])\n\n const store = useMemo<StudioStore>(\n () => ({ projects, context, getProject, activate }),\n [projects, context, getProject, activate],\n )\n\n // EC-308/AD-17: the single client query cache mounts here (package-owned,\n // above every per-project provider, surviving project switches) so every\n // studio server read flows through `useOperationQuery`.\n return (\n <StudioContext.Provider value={store}>\n <StudioQueryProvider>{children}</StudioQueryProvider>\n </StudioContext.Provider>\n )\n}\n\nexport { useStudio }\n"]}
@@ -36,7 +36,7 @@ export function StudioShell({ project, children }) {
36
36
  return next;
37
37
  });
38
38
  }, []);
39
- return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, {}), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
39
+ return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, { projectId: project.id }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
40
40
  }
41
41
  const CRUMB_LINK = 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground';
42
42
  const CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground';
@@ -1 +1 @@
1
- {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n <Link to=\"/\" className=\"grid size-6 shrink-0 place-items-center\" aria-label={translate('shell.allProjects')}>\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n <ProjectSwitcher activeProject={project} />\n <HeaderBreadcrumb project={project} activeViewId={activeViewId} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n project={project}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is the active VIEW's label (EC-237/239, resolved\n * from the rail-derived registry); deeper crumbs are contributed by the active\n * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain\n * text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const projectId: ProjectId = project.id\n const nav = useStudioNav(project)\n const extra = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n if (!view) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label={translate('shell.breadcrumb.label')}>\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{view.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.id }}\n className={CRUMB_LINK}\n >\n {view.label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n <Link to=\"/\" className=\"grid size-6 shrink-0 place-items-center\" aria-label={translate('shell.allProjects')}>\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n <ProjectSwitcher activeProject={project} />\n <HeaderBreadcrumb project={project} activeViewId={activeViewId} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner projectId={project.id} />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n project={project}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is the active VIEW's label (EC-237/239, resolved\n * from the rail-derived registry); deeper crumbs are contributed by the active\n * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain\n * text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const projectId: ProjectId = project.id\n const nav = useStudioNav(project)\n const extra = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n if (!view) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label={translate('shell.breadcrumb.label')}>\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{view.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.id }}\n className={CRUMB_LINK}\n >\n {view.label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
@@ -11,7 +11,8 @@ export interface ThemeStore {
11
11
  setTheme: (theme: Theme) => void;
12
12
  toggle: () => void;
13
13
  }
14
+ declare const useTheme: () => ThemeStore;
14
15
  export declare function ThemeProvider({ children }: {
15
16
  children: ReactNode;
16
17
  }): import("react").JSX.Element;
17
- export declare function useTheme(): ThemeStore;
18
+ export { useTheme };
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { DEFAULT_THEME, applyTheme, readStoredTheme, storeTheme } from '@elytracms/studio/lib/theme';
4
- const ThemeContext = createContext(null);
4
+ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
5
+ const { Context: ThemeContext, useRequired: useTheme } = createRequiredContext('Theme');
5
6
  export function ThemeProvider({ children }) {
6
7
  const [theme, setThemeState] = useState(DEFAULT_THEME);
7
8
  // Sync state to whatever the pre-hydration script already applied.
@@ -17,10 +18,5 @@ export function ThemeProvider({ children }) {
17
18
  const store = useMemo(() => ({ theme, setTheme, toggle: () => setTheme(theme === 'dark' ? 'light' : 'dark') }), [theme, setTheme]);
18
19
  return _jsx(ThemeContext.Provider, { value: store, children: children });
19
20
  }
20
- export function useTheme() {
21
- const store = useContext(ThemeContext);
22
- if (!store)
23
- throw new Error('useTheme must be used within a <ThemeProvider>.');
24
- return store;
25
- }
21
+ export { useTheme };
26
22
  //# sourceMappingURL=theme-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-context.js","sourceRoot":"","sources":["../../src/components/theme-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE5F,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAepG,MAAM,YAAY,GAAG,aAAa,CAAoB,IAAI,CAAC,CAAA;AAE3D,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAQ,aAAa,CAAC,CAAA;IAE7D,mEAAmE;IACnE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;QAChC,aAAa,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;IACnG,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAAW,EAAE,EAAE;QAC3C,aAAa,CAAC,IAAI,CAAC,CAAA;QACnB,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAA;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EACxF,CAAC,KAAK,EAAE,QAAQ,CAAC,CAClB,CAAA;IAED,OAAO,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAyB,CAAA;AAChF,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IAC9E,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { DEFAULT_THEME, applyTheme, readStoredTheme, storeTheme } from '@elytracms/studio/lib/theme'\nimport type { Theme } from '@elytracms/studio/lib/theme'\n\n/**\n * Theme store: owns the active light/dark theme, persists the choice, and keeps\n * the `dark` class on the document element in sync. The pre-hydration script\n * (THEME_INIT_SCRIPT) sets the initial class; this provider syncs React state to\n * it on mount so toggling is reactive.\n */\nexport interface ThemeStore {\n theme: Theme\n setTheme: (theme: Theme) => void\n toggle: () => void\n}\n\nconst ThemeContext = createContext<ThemeStore | null>(null)\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n const [theme, setThemeState] = useState<Theme>(DEFAULT_THEME)\n\n // Sync state to whatever the pre-hydration script already applied.\n useEffect(() => {\n const stored = readStoredTheme()\n setThemeState(stored ?? (document.documentElement.classList.contains('dark') ? 'dark' : 'light'))\n }, [])\n\n const setTheme = useCallback((next: Theme) => {\n setThemeState(next)\n storeTheme(next)\n applyTheme(next, document.documentElement)\n }, [])\n\n const store = useMemo<ThemeStore>(\n () => ({ theme, setTheme, toggle: () => setTheme(theme === 'dark' ? 'light' : 'dark') }),\n [theme, setTheme],\n )\n\n return <ThemeContext.Provider value={store}>{children}</ThemeContext.Provider>\n}\n\nexport function useTheme(): ThemeStore {\n const store = useContext(ThemeContext)\n if (!store) throw new Error('useTheme must be used within a <ThemeProvider>.')\n return store\n}\n"]}
1
+ {"version":3,"file":"theme-context.js","sourceRoot":"","sources":["../../src/components/theme-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEjE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAEpG,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAc9E,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,qBAAqB,CAAa,OAAO,CAAC,CAAA;AAEnG,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAQ,aAAa,CAAC,CAAA;IAE7D,mEAAmE;IACnE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;QAChC,aAAa,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;IACnG,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAAW,EAAE,EAAE;QAC3C,aAAa,CAAC,IAAI,CAAC,CAAA;QACnB,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAA;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EACxF,CAAC,KAAK,EAAE,QAAQ,CAAC,CAClB,CAAA;IAED,OAAO,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAyB,CAAA;AAChF,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { DEFAULT_THEME, applyTheme, readStoredTheme, storeTheme } from '@elytracms/studio/lib/theme'\nimport type { Theme } from '@elytracms/studio/lib/theme'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Theme store: owns the active light/dark theme, persists the choice, and keeps\n * the `dark` class on the document element in sync. The pre-hydration script\n * (THEME_INIT_SCRIPT) sets the initial class; this provider syncs React state to\n * it on mount so toggling is reactive.\n */\nexport interface ThemeStore {\n theme: Theme\n setTheme: (theme: Theme) => void\n toggle: () => void\n}\n\nconst { Context: ThemeContext, useRequired: useTheme } = createRequiredContext<ThemeStore>('Theme')\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n const [theme, setThemeState] = useState<Theme>(DEFAULT_THEME)\n\n // Sync state to whatever the pre-hydration script already applied.\n useEffect(() => {\n const stored = readStoredTheme()\n setThemeState(stored ?? (document.documentElement.classList.contains('dark') ? 'dark' : 'light'))\n }, [])\n\n const setTheme = useCallback((next: Theme) => {\n setThemeState(next)\n storeTheme(next)\n applyTheme(next, document.documentElement)\n }, [])\n\n const store = useMemo<ThemeStore>(\n () => ({ theme, setTheme, toggle: () => setTheme(theme === 'dark' ? 'light' : 'dark') }),\n [theme, setTheme],\n )\n\n return <ThemeContext.Provider value={store}>{children}</ThemeContext.Provider>\n}\n\nexport { useTheme }\n"]}
@@ -0,0 +1,65 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { OperationError, OperationResult } from '@elytracms/studio/lib/operations';
3
+ /**
4
+ * The shared hydration state machine (EC-329) behind both
5
+ * `ProjectSessionProvider` and `CmsSessionGate`: playground short-circuit,
6
+ * phase union, attempt-driven retry, cancelled-flag cleanup — a specific
7
+ * multi-part shape the two providers used to hand-roll identically. Real
8
+ * asymmetries between them stay explicit HOOK OPTIONS rather than being
9
+ * flattened away:
10
+ *
11
+ * - `ProjectSessionProvider`'s playground branch calls its `onResolved` side
12
+ * effect (`configureContentSession(null)`); `CmsSessionGate` passes no
13
+ * `onResolved` at all (it owns no such side effect — `loadCms`'s own body
14
+ * already calls it). `enabled` toggles whether `onResolved` fires in the
15
+ * playground fast-path — the ONE point of divergence between the two.
16
+ * - `onResolved` runs synchronously BEFORE the phase flips to `'ready'` (both
17
+ * the playground fast-path and the real load), so children reading a
18
+ * module global (e.g. `configureContentSession`) see it updated on the
19
+ * SAME render they first see `'ready'` — a bare `loader().then(setPhase)`
20
+ * cannot guarantee that ordering.
21
+ */
22
+ export type HydrationPhase<T> = {
23
+ kind: 'loading';
24
+ } | {
25
+ kind: 'ready';
26
+ value: T;
27
+ } | {
28
+ kind: 'error';
29
+ error: OperationError;
30
+ };
31
+ export interface UseHydrationPhaseOptions<T> {
32
+ /** Playground (fixture) sessions resolve synchronously to `playgroundValue` — no loader call. */
33
+ playground: boolean;
34
+ /** The value `phase` resolves to in playground mode. */
35
+ playgroundValue: T;
36
+ /** Call `onResolved` for the playground fast-path too (default `false`). */
37
+ enabled?: boolean;
38
+ /**
39
+ * Run synchronously before the phase flips to `'ready'` — in BOTH the
40
+ * playground fast-path (when `enabled`) and the real load. Read via a ref
41
+ * internally, so an inline (non-memoized) callback never re-triggers a fetch.
42
+ */
43
+ onResolved?: (value: T) => void;
44
+ }
45
+ export interface UseHydrationPhaseResult<T> {
46
+ phase: HydrationPhase<T>;
47
+ /** Re-run the loader (the retry button's handler). */
48
+ retry: () => void;
49
+ }
50
+ /**
51
+ * `loader`'s IDENTITY is the refetch trigger — memoize it (`useCallback`) over
52
+ * whatever inputs should re-hydrate (e.g. `projectId`), exactly like the
53
+ * original two effects keyed their dependency arrays.
54
+ */
55
+ export declare function useHydrationPhase<T>(loader: () => Promise<OperationResult<T>>, options: UseHydrationPhaseOptions<T>): UseHydrationPhaseResult<T>;
56
+ /**
57
+ * Renders `phase`'s loading/error frames, or `children(value)` once ready —
58
+ * the identical 3-way branch both `ProjectSessionProvider` and
59
+ * `CmsSessionGate` used to repeat verbatim.
60
+ */
61
+ export declare function HydrationBoundary<T>({ phase, retry, children, }: {
62
+ phase: HydrationPhase<T>;
63
+ retry: () => void;
64
+ children: (value: T) => ReactNode;
65
+ }): ReactNode;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Button, Stack, Text } from '@elytracms/studio/ui';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
5
+ import { OperationErrorBanner } from './operation-error';
6
+ /**
7
+ * `loader`'s IDENTITY is the refetch trigger — memoize it (`useCallback`) over
8
+ * whatever inputs should re-hydrate (e.g. `projectId`), exactly like the
9
+ * original two effects keyed their dependency arrays.
10
+ */
11
+ export function useHydrationPhase(loader, options) {
12
+ const { playground, playgroundValue, enabled = false } = options;
13
+ const onResolvedRef = useRef(options.onResolved);
14
+ onResolvedRef.current = options.onResolved;
15
+ const [phase, setPhase] = useState(() => playground ? { kind: 'ready', value: playgroundValue } : { kind: 'loading' });
16
+ const [attempt, setAttempt] = useState(0);
17
+ useEffect(() => {
18
+ if (playground) {
19
+ if (enabled)
20
+ onResolvedRef.current?.(playgroundValue);
21
+ return;
22
+ }
23
+ let cancelled = false;
24
+ setPhase({ kind: 'loading' });
25
+ void loader().then((result) => {
26
+ if (cancelled)
27
+ return;
28
+ if (!result.ok) {
29
+ setPhase({ kind: 'error', error: result.error });
30
+ return;
31
+ }
32
+ onResolvedRef.current?.(result.value);
33
+ setPhase({ kind: 'ready', value: result.value });
34
+ });
35
+ return () => {
36
+ cancelled = true;
37
+ };
38
+ // `onResolved` is read via the ref above (see the doc comment) — its
39
+ // identity intentionally does not retrigger this effect.
40
+ }, [loader, playground, attempt, enabled, playgroundValue]);
41
+ const retry = useCallback(() => setAttempt((n) => n + 1), []);
42
+ return { phase, retry };
43
+ }
44
+ /**
45
+ * Renders `phase`'s loading/error frames, or `children(value)` once ready —
46
+ * the identical 3-way branch both `ProjectSessionProvider` and
47
+ * `CmsSessionGate` used to repeat verbatim.
48
+ */
49
+ export function HydrationBoundary({ phase, retry, children, }) {
50
+ if (phase.kind === 'loading')
51
+ return _jsx(SessionHydrating, {});
52
+ if (phase.kind === 'error')
53
+ return _jsx(SessionHydrationError, { error: phase.error, onRetry: retry });
54
+ return _jsx(_Fragment, { children: children(phase.value) });
55
+ }
56
+ /** Deterministic loading frame while persisted state is fetched (mirrors BackendGate). */
57
+ function SessionHydrating() {
58
+ return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", "data-testid": "session-hydrating", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('shell.session.loading') }) }));
59
+ }
60
+ /** A failed hydration is a visible, retryable state — never a fixture fallback. */
61
+ function SessionHydrationError({ error, onRetry, }) {
62
+ return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", children: _jsxs(Stack, { gap: "3", className: "w-full max-w-xl", children: [_jsx(Text, { size: "sm", weight: "semibold", children: translate('shell.session.error.title') }), _jsx(OperationErrorBanner, { error: error }), _jsx(Text, { size: "xs", tone: "muted", children: translate('shell.session.error.body') }), _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: translate('common.retry') })] }) }));
63
+ }
64
+ //# sourceMappingURL=use-hydration-phase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-hydration-phase.js","sourceRoot":"","sources":["../../src/components/use-hydration-phase.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AA+CxD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAyC,EACzC,OAAoC;IAEpC,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IAChE,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAChD,aAAa,CAAC,OAAO,GAAG,OAAO,CAAC,UAAU,CAAA;IAE1C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAoB,GAAG,EAAE,CACzD,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAC7E,CAAA;IACD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEzC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,OAAO;gBAAE,aAAa,CAAC,OAAO,EAAE,CAAC,eAAe,CAAC,CAAA;YACrD,OAAM;QACR,CAAC;QACD,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAC7B,KAAK,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC5B,IAAI,SAAS;gBAAE,OAAM;YACrB,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;gBAChD,OAAM;YACR,CAAC;YACD,aAAa,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACrC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;QACD,qEAAqE;QACrE,yDAAyD;IAC3D,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAA;IAE3D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAI,EACnC,KAAK,EACL,KAAK,EACL,QAAQ,GAKT;IACC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,gBAAgB,KAAG,CAAA;IACzD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,KAAC,qBAAqB,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAI,CAAA;IAChG,OAAO,4BAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAI,CAAA;AACrC,CAAC;AAED,0FAA0F;AAC1F,SAAS,gBAAgB;IACvB,OAAO,CACL,cAAK,SAAS,EAAC,oDAAoD,iBAAa,mBAAmB,YACjG,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,uBAAuB,CAAC,GAC9B,GACH,CACP,CAAA;AACH,CAAC;AAED,mFAAmF;AACnF,SAAS,qBAAqB,CAAC,EAC7B,KAAK,EACL,OAAO,GAIR;IACC,OAAO,CACL,cAAK,SAAS,EAAC,oDAAoD,YACjE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,iBAAiB,aACxC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,2BAA2B,CAAC,GAClC,EACP,KAAC,oBAAoB,IAAC,KAAK,EAAE,KAAK,GAAI,EACtC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,0BAA0B,CAAC,GACjC,EACP,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,YAAY,YACzE,SAAS,CAAC,cAAc,CAAC,GACnB,IACH,GACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * The shared hydration state machine (EC-329) behind both\n * `ProjectSessionProvider` and `CmsSessionGate`: playground short-circuit,\n * phase union, attempt-driven retry, cancelled-flag cleanup — a specific\n * multi-part shape the two providers used to hand-roll identically. Real\n * asymmetries between them stay explicit HOOK OPTIONS rather than being\n * flattened away:\n *\n * - `ProjectSessionProvider`'s playground branch calls its `onResolved` side\n * effect (`configureContentSession(null)`); `CmsSessionGate` passes no\n * `onResolved` at all (it owns no such side effect — `loadCms`'s own body\n * already calls it). `enabled` toggles whether `onResolved` fires in the\n * playground fast-path — the ONE point of divergence between the two.\n * - `onResolved` runs synchronously BEFORE the phase flips to `'ready'` (both\n * the playground fast-path and the real load), so children reading a\n * module global (e.g. `configureContentSession`) see it updated on the\n * SAME render they first see `'ready'` — a bare `loader().then(setPhase)`\n * cannot guarantee that ordering.\n */\nexport type HydrationPhase<T> =\n | { kind: 'loading' }\n | { kind: 'ready'; value: T }\n | { kind: 'error'; error: OperationError }\n\nexport interface UseHydrationPhaseOptions<T> {\n /** Playground (fixture) sessions resolve synchronously to `playgroundValue` — no loader call. */\n playground: boolean\n /** The value `phase` resolves to in playground mode. */\n playgroundValue: T\n /** Call `onResolved` for the playground fast-path too (default `false`). */\n enabled?: boolean\n /**\n * Run synchronously before the phase flips to `'ready'` — in BOTH the\n * playground fast-path (when `enabled`) and the real load. Read via a ref\n * internally, so an inline (non-memoized) callback never re-triggers a fetch.\n */\n onResolved?: (value: T) => void\n}\n\nexport interface UseHydrationPhaseResult<T> {\n phase: HydrationPhase<T>\n /** Re-run the loader (the retry button's handler). */\n retry: () => void\n}\n\n/**\n * `loader`'s IDENTITY is the refetch trigger — memoize it (`useCallback`) over\n * whatever inputs should re-hydrate (e.g. `projectId`), exactly like the\n * original two effects keyed their dependency arrays.\n */\nexport function useHydrationPhase<T>(\n loader: () => Promise<OperationResult<T>>,\n options: UseHydrationPhaseOptions<T>,\n): UseHydrationPhaseResult<T> {\n const { playground, playgroundValue, enabled = false } = options\n const onResolvedRef = useRef(options.onResolved)\n onResolvedRef.current = options.onResolved\n\n const [phase, setPhase] = useState<HydrationPhase<T>>(() =>\n playground ? { kind: 'ready', value: playgroundValue } : { kind: 'loading' },\n )\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) {\n if (enabled) onResolvedRef.current?.(playgroundValue)\n return\n }\n let cancelled = false\n setPhase({ kind: 'loading' })\n void loader().then((result) => {\n if (cancelled) return\n if (!result.ok) {\n setPhase({ kind: 'error', error: result.error })\n return\n }\n onResolvedRef.current?.(result.value)\n setPhase({ kind: 'ready', value: result.value })\n })\n return () => {\n cancelled = true\n }\n // `onResolved` is read via the ref above (see the doc comment) — its\n // identity intentionally does not retrigger this effect.\n }, [loader, playground, attempt, enabled, playgroundValue])\n\n const retry = useCallback(() => setAttempt((n) => n + 1), [])\n return { phase, retry }\n}\n\n/**\n * Renders `phase`'s loading/error frames, or `children(value)` once ready —\n * the identical 3-way branch both `ProjectSessionProvider` and\n * `CmsSessionGate` used to repeat verbatim.\n */\nexport function HydrationBoundary<T>({\n phase,\n retry,\n children,\n}: {\n phase: HydrationPhase<T>\n retry: () => void\n children: (value: T) => ReactNode\n}): ReactNode {\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') return <SessionHydrationError error={phase.error} onRetry={retry} />\n return <>{children(phase.value)}</>\n}\n\n/** Deterministic loading frame while persisted state is fetched (mirrors BackendGate). */\nfunction SessionHydrating() {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\" data-testid=\"session-hydrating\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.session.loading')}\n </Text>\n </div>\n )\n}\n\n/** A failed hydration is a visible, retryable state — never a fixture fallback. */\nfunction SessionHydrationError({\n error,\n onRetry,\n}: {\n error: OperationError\n onRetry: () => void\n}) {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\">\n <Stack gap=\"3\" className=\"w-full max-w-xl\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('shell.session.error.title')}\n </Text>\n <OperationErrorBanner error={error} />\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.session.error.body')}\n </Text>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('common.retry')}\n </Button>\n </Stack>\n </div>\n )\n}\n"]}
@@ -1,6 +1,14 @@
1
1
  import type { CmsDocument, FieldDef } from '@elytracms/core/cms-core';
2
2
  import type { SaveUserInput, UserRecord } from '@elytracms/core/persistence';
3
- import type { CollectionViewSource, FieldAccess } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ import type { CollectionViewCapabilities, CollectionViewSource, FieldAccess } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ /**
5
+ * The capability profile of the users rail (EC-240): admin-only, no
6
+ * publish/versions/create/import (identities are minted by sign-up and their
7
+ * bytes are auth-owned), remove allowed (revoke membership). Hoisted to a
8
+ * module-level constant (no prop/state dependency, EC-327) — mirrors
9
+ * `ASSET_COLLECTION_CAPABILITIES`.
10
+ */
11
+ export declare const USER_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
4
12
  /** Project the auth-identity + membership join into the lens's `values` shape. */
5
13
  export declare function userToDocument(record: UserRecord): CmsDocument;
6
14
  /**
@@ -24,5 +32,9 @@ export declare function documentToSaveInput(doc: CmsDocument): SaveUserInput;
24
32
  * read-only; an admin edits a member's role (and future custom `meta` fields).
25
33
  *
26
34
  * Must be mounted under a `UsersProvider` (for {@link useUsers}).
35
+ *
36
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
37
+ * plumbing and the assembled source; this hook supplies only the users-store
38
+ * reads/writes, the capability profile, and the principal-based `fieldAccess`.
27
39
  */
28
40
  export declare function useUsersCollectionViewSource(projectId: string): CollectionViewSource;
@@ -1,12 +1,32 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback } from 'react';
2
2
  import { studioPath } from '@elytracms/studio/router';
3
- import { defaultViewState } from '@elytracms/studio/lib/cms-views';
4
3
  import { useAuth } from '../auth-context';
5
4
  import { useUsers } from './users-store';
6
5
  import { usersCollectionDef } from './users-schema';
6
+ import { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source';
7
7
  /** The users rail is not localized — one implicit locale keeps the lens honest. */
8
8
  const USERS_LOCALE = 'en';
9
9
  const USERS_LOCALE_CONFIG = { default: USERS_LOCALE, locales: [USERS_LOCALE] };
10
+ /**
11
+ * The capability profile of the users rail (EC-240): admin-only, no
12
+ * publish/versions/create/import (identities are minted by sign-up and their
13
+ * bytes are auth-owned), remove allowed (revoke membership). Hoisted to a
14
+ * module-level constant (no prop/state dependency, EC-327) — mirrors
15
+ * `ASSET_COLLECTION_CAPABILITIES`.
16
+ */
17
+ export const USER_COLLECTION_CAPABILITIES = {
18
+ publish: false,
19
+ versions: false,
20
+ locales: false,
21
+ inlineEdit: false,
22
+ create: false, // identities are minted by sign-up, never here
23
+ remove: true, // revoke membership (does not delete the auth identity)
24
+ bulk: false,
25
+ liveUrl: false,
26
+ hierarchy: false,
27
+ importExport: false, // credential records are never dumped to raw JSON
28
+ adminOnly: true,
29
+ };
10
30
  /** The built-in field names — everything else in `values` is a custom `meta` field. */
11
31
  const IDENTITY_FIELDS = new Set(['name', 'email', 'role']);
12
32
  /** Project the auth-identity + membership join into the lens's `values` shape. */
@@ -41,14 +61,21 @@ export function userFieldAccess(field, record, currentUserId) {
41
61
  /** Map an edited lens document back to the write contract (role + custom `meta`). */
42
62
  export function documentToSaveInput(doc) {
43
63
  const values = doc.values ?? {};
44
- const meta = {};
45
- for (const [key, value] of Object.entries(values)) {
46
- if (!IDENTITY_FIELDS.has(key))
47
- meta[key] = value;
48
- }
64
+ const meta = metaBagProjection(values, IDENTITY_FIELDS);
49
65
  const role = typeof values.role === 'string' ? values.role : 'viewer';
50
66
  return { id: doc.id, roles: [role], ...(Object.keys(meta).length > 0 ? { meta } : {}) };
51
67
  }
68
+ /** `validate` for the users rail: no-op — a member's identity fields aren't
69
+ * schema-validated the way CMS documents are (240a-era; a registry-driven
70
+ * validation may land alongside a future custom-field schema). */
71
+ function noValidation() {
72
+ return [];
73
+ }
74
+ /** `create` when `capabilities.create === false` — never reached (identities
75
+ * are minted by sign-up, not the users rail). */
76
+ function createUnsupported() {
77
+ throw new Error('users are created by sign-up, not the users rail');
78
+ }
52
79
  /**
53
80
  * Assemble a {@link CollectionViewSource} for the **users rail** (EC-240, AD-19) —
54
81
  * the reuse proof that the portable list/edit surfaces serve a non-`cmsDocuments`
@@ -59,6 +86,10 @@ export function documentToSaveInput(doc) {
59
86
  * read-only; an admin edits a member's role (and future custom `meta` fields).
60
87
  *
61
88
  * Must be mounted under a `UsersProvider` (for {@link useUsers}).
89
+ *
90
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
91
+ * plumbing and the assembled source; this hook supplies only the users-store
92
+ * reads/writes, the capability profile, and the principal-based `fieldAccess`.
62
93
  */
63
94
  export function useUsersCollectionViewSource(projectId) {
64
95
  const users = useUsers();
@@ -66,8 +97,6 @@ export function useUsersCollectionViewSource(projectId) {
66
97
  // their OWN record (self ≠ other; the server floor enforces it regardless).
67
98
  const auth = useAuth();
68
99
  const currentUserId = auth.session?.user?.id;
69
- const [viewState, setViewStateRaw] = useState(() => defaultViewState(usersCollectionDef));
70
- const [lastError, setLastError] = useState(null);
71
100
  const documents = useCallback(() => users.users.map(userToDocument), [users.users]);
72
101
  const get = useCallback((ref) => {
73
102
  const record = users.users.find((u) => u.id === ref.id);
@@ -77,81 +106,33 @@ export function useUsersCollectionViewSource(projectId) {
77
106
  const record = users.users.find((u) => u.id === ref.id);
78
107
  return record?.name || record?.email || ref.id;
79
108
  }, [users.users]);
80
- const setViewState = useCallback((update) => {
81
- setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update));
82
- }, []);
83
- const save = useCallback((doc) => {
84
- void users.save(documentToSaveInput(doc)).then((result) => {
85
- if (!result.ok)
86
- setLastError({ code: 'unknown', message: result.message });
87
- });
88
- }, [users]);
89
- const remove = useCallback((ref) => {
90
- void users.remove(ref.id).then((result) => {
91
- if (!result.ok)
92
- setLastError({ code: 'unknown', message: result.message });
93
- });
94
- }, [users]);
95
- return useMemo(() => {
96
- const capabilities = {
97
- publish: false,
98
- versions: false,
99
- locales: false,
100
- inlineEdit: false,
101
- create: false, // identities are minted by sign-up, never here
102
- remove: true, // revoke membership (does not delete the auth identity)
103
- bulk: false,
104
- liveUrl: false,
105
- hierarchy: false,
106
- importExport: false, // credential records are never dumped to raw JSON
107
- adminOnly: true,
108
- };
109
- const source = {
110
- collectionId: usersCollectionDef.id,
111
- schema: usersCollectionDef,
112
- capabilities,
113
- projectId,
114
- localeConfig: USERS_LOCALE_CONFIG,
115
- activeLocale: USERS_LOCALE,
116
- documents,
117
- get,
118
- titleOf,
119
- // Users carry no relation fields in v1, so there are no cross-collection
120
- // candidates to resolve; the lens is the only collection in scope.
121
- referenceCandidates: () => [],
122
- collections: [usersCollectionDef],
123
- // The users rail now has a per-member detail surface (EC-240): rows link to
124
- // it, and the edit shell's back-crumb / post-delete nav return to the list.
125
- detailPath: (id) => studioPath.user(id),
126
- listPath: studioPath.users(),
127
- isFieldReadOnly: (field) => field.form?.readOnly ?? false,
128
- // Principal-based tri-state: `role` is read-only for self, editable for an
129
- // admin acting on another; identity fields follow their schema readOnly.
130
- fieldAccess: (field, record) => userFieldAccess(field, record, currentUserId),
131
- validate: () => [],
132
- lastError,
133
- clearLastError: () => setLastError(null),
134
- viewState: () => viewState,
135
- setViewState,
136
- // create is gated off (capabilities.create === false); never reached.
137
- create: () => {
138
- throw new Error('users are created by sign-up, not the users rail');
139
- },
140
- save,
141
- remove,
142
- };
143
- return source;
144
- }, [
109
+ const fieldAccess = useCallback((field, record) => userFieldAccess(field, record, currentUserId), [currentUserId]);
110
+ const save = useCallback((doc) => users.save(documentToSaveInput(doc)), [users]);
111
+ const remove = useCallback((ref) => users.remove(ref.id), [users]);
112
+ return useRailViewSource({
145
113
  projectId,
146
- currentUserId,
114
+ schema: usersCollectionDef,
115
+ capabilities: USER_COLLECTION_CAPABILITIES,
116
+ localeConfig: USERS_LOCALE_CONFIG,
147
117
  documents,
148
118
  get,
149
119
  titleOf,
150
- setViewState,
120
+ // No `referenceCandidates`/`collections` override: users carry no relation
121
+ // fields in v1, so there are no cross-collection candidates to resolve; the
122
+ // lens is the only collection in scope (both factory defaults apply).
123
+ // The users rail now has a per-member detail surface (EC-240): rows link to
124
+ // it, and the edit shell's back-crumb / post-delete nav return to the list.
125
+ detailPath: (id) => studioPath.user(id),
126
+ listPath: studioPath.users(),
127
+ // Principal-based tri-state: `role` is read-only for self, editable for an
128
+ // admin acting on another; identity fields follow their schema readOnly.
129
+ fieldAccess,
130
+ validate: noValidation,
131
+ create: createUnsupported,
151
132
  save,
152
133
  remove,
153
- viewState,
154
- lastError,
155
- ]);
134
+ normalizeSaveResult: normalizeOkResult,
135
+ normalizeRemoveResult: normalizeOkResult,
136
+ });
156
137
  }
157
138
  //# sourceMappingURL=users-collection-view-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"users-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/users/users-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type { CmsDocument, DocumentRef, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { ProjectRole, SaveUserInput, UserRecord } 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 FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAuth } from '../auth-context'\nimport { useUsers } from './users-store'\nimport { usersCollectionDef } from './users-schema'\n\n/** The users rail is not localized — one implicit locale keeps the lens honest. */\nconst USERS_LOCALE: Locale = 'en'\nconst USERS_LOCALE_CONFIG: LocaleConfig = { default: USERS_LOCALE, locales: [USERS_LOCALE] }\n\n/** The built-in field names — everything else in `values` is a custom `meta` field. */\nconst IDENTITY_FIELDS = new Set(['name', 'email', 'role'])\n\n/** Project the auth-identity + membership join into the lens's `values` shape. */\nexport function userToDocument(record: UserRecord): CmsDocument {\n return {\n collection: usersCollectionDef.id,\n id: record.id,\n values: {\n // Display title falls through empties (name → email → id) so a nameless,\n // blank-email member never renders as a blank row; `email` keeps its real value.\n name: record.name || record.email || record.id,\n email: record.email,\n role: record.roles[0] ?? 'viewer',\n ...(record.meta ?? {}),\n },\n }\n}\n\n/**\n * Principal-based field access for the users rail (EC-240, AD-18). `role` is\n * authority: **read-only when you are editing YOURSELF** — you don't change your\n * own role (the server integrity floor also guards it) — and **editable otherwise**\n * (the rail is admin-only, so a non-self editor is an admin acting on someone else).\n * Identity fields (name/email) follow their schema `form.readOnly`; everything else\n * is editable. Pure, so the self/other rule is unit-testable without the auth hook.\n */\nexport function userFieldAccess(\n field: FieldDef,\n record: CmsDocument | undefined,\n currentUserId: string | undefined,\n): FieldAccess {\n if (field.name === 'role') {\n return record?.id === currentUserId ? 'read' : 'edit'\n }\n return field.form?.readOnly ? 'read' : 'edit'\n}\n\n/** Map an edited lens document back to the write contract (role + custom `meta`). */\nexport function documentToSaveInput(doc: CmsDocument): SaveUserInput {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!IDENTITY_FIELDS.has(key)) meta[key] = value\n }\n const role = typeof values.role === 'string' ? (values.role as ProjectRole) : 'viewer'\n return { id: doc.id, roles: [role], ...(Object.keys(meta).length > 0 ? { meta } : {}) }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **users rail** (EC-240, AD-19) —\n * the reuse proof that the portable list/edit surfaces serve a non-`cmsDocuments`\n * rail. Mirrors {@link useCmsCollectionViewSource}, over the users store instead of\n * the CMS store, with the users capability profile: admin-only, no\n * publish/versions/create/import (identities are minted by sign-up and their bytes\n * are auth-owned), remove allowed (revoke membership). Identity fields are\n * read-only; an admin edits a member's role (and future custom `meta` fields).\n *\n * Must be mounted under a `UsersProvider` (for {@link useUsers}).\n */\nexport function useUsersCollectionViewSource(projectId: string): CollectionViewSource {\n const users = useUsers()\n // The signed-in principal — so `role` locks to read-only when an admin opens\n // their OWN record (self ≠ other; the server floor enforces it regardless).\n const auth = useAuth()\n const currentUserId = auth.session?.user?.id\n const [viewState, setViewStateRaw] = useState<TableViewState>(() =>\n defaultViewState(usersCollectionDef),\n )\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const documents = useCallback(() => users.users.map(userToDocument), [users.users])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = users.users.find((u) => u.id === ref.id)\n return record ? userToDocument(record) : undefined\n },\n [users.users],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = users.users.find((u) => u.id === ref.id)\n return record?.name || record?.email || ref.id\n },\n [users.users],\n )\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n\n const save = useCallback(\n (doc: CmsDocument) => {\n void users.save(documentToSaveInput(doc)).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [users],\n )\n const remove = useCallback(\n (ref: DocumentRef) => {\n void users.remove(ref.id).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [users],\n )\n\n return useMemo(() => {\n const capabilities: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false, // identities are minted by sign-up, never here\n remove: true, // revoke membership (does not delete the auth identity)\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false, // credential records are never dumped to raw JSON\n adminOnly: true,\n }\n const source: CollectionViewSource = {\n collectionId: usersCollectionDef.id,\n schema: usersCollectionDef,\n capabilities,\n projectId,\n localeConfig: USERS_LOCALE_CONFIG,\n activeLocale: USERS_LOCALE,\n documents,\n get,\n titleOf,\n // Users carry no relation fields in v1, so there are no cross-collection\n // candidates to resolve; the lens is the only collection in scope.\n referenceCandidates: () => [],\n collections: [usersCollectionDef],\n // The users rail now has a per-member detail surface (EC-240): rows link to\n // it, and the edit shell's back-crumb / post-delete nav return to the list.\n detailPath: (id: string) => studioPath.user(id),\n listPath: studioPath.users(),\n isFieldReadOnly: (field: FieldDef) => field.form?.readOnly ?? false,\n // Principal-based tri-state: `role` is read-only for self, editable for an\n // admin acting on another; identity fields follow their schema readOnly.\n fieldAccess: (field: FieldDef, record?: CmsDocument): FieldAccess =>\n userFieldAccess(field, record, currentUserId),\n validate: () => [],\n lastError,\n clearLastError: () => setLastError(null),\n viewState: () => viewState,\n setViewState,\n // create is gated off (capabilities.create === false); never reached.\n create: () => {\n throw new Error('users are created by sign-up, not the users rail')\n },\n save,\n remove,\n }\n return source\n }, [\n projectId,\n currentUserId,\n documents,\n get,\n titleOf,\n setViewState,\n save,\n remove,\n viewState,\n lastError,\n ])\n}\n"]}
1
+ {"version":3,"file":"users-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/users/users-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback } from 'react'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { ProjectRole, SaveUserInput, UserRecord } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAuth } from '../auth-context'\nimport { useUsers } from './users-store'\nimport { usersCollectionDef } from './users-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The users rail is not localized — one implicit locale keeps the lens honest. */\nconst USERS_LOCALE: Locale = 'en'\nconst USERS_LOCALE_CONFIG: LocaleConfig = { default: USERS_LOCALE, locales: [USERS_LOCALE] }\n\n/**\n * The capability profile of the users rail (EC-240): admin-only, no\n * publish/versions/create/import (identities are minted by sign-up and their\n * bytes are auth-owned), remove allowed (revoke membership). Hoisted to a\n * module-level constant (no prop/state dependency, EC-327) — mirrors\n * `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const USER_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false, // identities are minted by sign-up, never here\n remove: true, // revoke membership (does not delete the auth identity)\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false, // credential records are never dumped to raw JSON\n adminOnly: true,\n}\n\n/** The built-in field names — everything else in `values` is a custom `meta` field. */\nconst IDENTITY_FIELDS = new Set(['name', 'email', 'role'])\n\n/** Project the auth-identity + membership join into the lens's `values` shape. */\nexport function userToDocument(record: UserRecord): CmsDocument {\n return {\n collection: usersCollectionDef.id,\n id: record.id,\n values: {\n // Display title falls through empties (name → email → id) so a nameless,\n // blank-email member never renders as a blank row; `email` keeps its real value.\n name: record.name || record.email || record.id,\n email: record.email,\n role: record.roles[0] ?? 'viewer',\n ...(record.meta ?? {}),\n },\n }\n}\n\n/**\n * Principal-based field access for the users rail (EC-240, AD-18). `role` is\n * authority: **read-only when you are editing YOURSELF** — you don't change your\n * own role (the server integrity floor also guards it) — and **editable otherwise**\n * (the rail is admin-only, so a non-self editor is an admin acting on someone else).\n * Identity fields (name/email) follow their schema `form.readOnly`; everything else\n * is editable. Pure, so the self/other rule is unit-testable without the auth hook.\n */\nexport function userFieldAccess(\n field: FieldDef,\n record: CmsDocument | undefined,\n currentUserId: string | undefined,\n): FieldAccess {\n if (field.name === 'role') {\n return record?.id === currentUserId ? 'read' : 'edit'\n }\n return field.form?.readOnly ? 'read' : 'edit'\n}\n\n/** Map an edited lens document back to the write contract (role + custom `meta`). */\nexport function documentToSaveInput(doc: CmsDocument): SaveUserInput {\n const values = doc.values ?? {}\n const meta = metaBagProjection(values, IDENTITY_FIELDS)\n const role = typeof values.role === 'string' ? (values.role as ProjectRole) : 'viewer'\n return { id: doc.id, roles: [role], ...(Object.keys(meta).length > 0 ? { meta } : {}) }\n}\n\n/** `validate` for the users rail: no-op — a member's identity fields aren't\n * schema-validated the way CMS documents are (240a-era; a registry-driven\n * validation may land alongside a future custom-field schema). */\nfunction noValidation(): CmsValidationIssue[] {\n return []\n}\n\n/** `create` when `capabilities.create === false` — never reached (identities\n * are minted by sign-up, not the users rail). */\nfunction createUnsupported(): DocumentRef {\n throw new Error('users are created by sign-up, not the users rail')\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **users rail** (EC-240, AD-19) —\n * the reuse proof that the portable list/edit surfaces serve a non-`cmsDocuments`\n * rail. Mirrors {@link useCmsCollectionViewSource}, over the users store instead of\n * the CMS store, with the users capability profile: admin-only, no\n * publish/versions/create/import (identities are minted by sign-up and their bytes\n * are auth-owned), remove allowed (revoke membership). Identity fields are\n * read-only; an admin edits a member's role (and future custom `meta` fields).\n *\n * Must be mounted under a `UsersProvider` (for {@link useUsers}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the users-store\n * reads/writes, the capability profile, and the principal-based `fieldAccess`.\n */\nexport function useUsersCollectionViewSource(projectId: string): CollectionViewSource {\n const users = useUsers()\n // The signed-in principal — so `role` locks to read-only when an admin opens\n // their OWN record (self ≠ other; the server floor enforces it regardless).\n const auth = useAuth()\n const currentUserId = auth.session?.user?.id\n\n const documents = useCallback(() => users.users.map(userToDocument), [users.users])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = users.users.find((u) => u.id === ref.id)\n return record ? userToDocument(record) : undefined\n },\n [users.users],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = users.users.find((u) => u.id === ref.id)\n return record?.name || record?.email || ref.id\n },\n [users.users],\n )\n const fieldAccess = useCallback(\n (field: FieldDef, record?: CmsDocument): FieldAccess => userFieldAccess(field, record, currentUserId),\n [currentUserId],\n )\n const save = useCallback((doc: CmsDocument) => users.save(documentToSaveInput(doc)), [users])\n const remove = useCallback((ref: DocumentRef) => users.remove(ref.id), [users])\n\n return useRailViewSource({\n projectId,\n schema: usersCollectionDef,\n capabilities: USER_COLLECTION_CAPABILITIES,\n localeConfig: USERS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: users carry no relation\n // fields in v1, so there are no cross-collection candidates to resolve; the\n // lens is the only collection in scope (both factory defaults apply).\n // The users rail now has a per-member detail surface (EC-240): rows link to\n // it, and the edit shell's back-crumb / post-delete nav return to the list.\n detailPath: (id: string) => studioPath.user(id),\n listPath: studioPath.users(),\n // Principal-based tri-state: `role` is read-only for self, editable for an\n // admin acting on another; identity fields follow their schema readOnly.\n fieldAccess,\n validate: noValidation,\n create: createUnsupported,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
@@ -33,8 +33,9 @@ export interface UsersStore {
33
33
  /** Re-list from the backend (the public escape hatch). */
34
34
  refresh(): Promise<void>;
35
35
  }
36
+ declare const useUsers: () => UsersStore;
36
37
  export declare function UsersProvider({ projectId, children, }: {
37
38
  projectId: string;
38
39
  children: ReactNode;
39
40
  }): import("react").JSX.Element;
40
- export declare function useUsers(): UsersStore;
41
+ export { useUsers };