@elytracms/studio 0.0.80 → 0.0.82

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 (196) hide show
  1. package/convex/contract-parity.test.ts +1 -1
  2. package/convex/functions.ts +13 -3
  3. package/convex/http-routes.test.ts +143 -0
  4. package/convex/http.ts +19 -9
  5. package/convex/redirect-revalidate.test.ts +130 -0
  6. package/convex/revalidate.ts +9 -7
  7. package/convex/schema.ts +0 -19
  8. package/convex/triggers.ts +33 -3
  9. package/convex/users.ts +1 -1
  10. package/dist/components/ai/ai-chat-panel.js +26 -4
  11. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  12. package/dist/components/ai/proposal-card.d.ts +27 -0
  13. package/dist/components/ai/proposal-card.js +31 -3
  14. package/dist/components/ai/proposal-card.js.map +1 -1
  15. package/dist/components/ai/proposal-issues.d.ts +55 -0
  16. package/dist/components/ai/proposal-issues.js +69 -0
  17. package/dist/components/ai/proposal-issues.js.map +1 -0
  18. package/dist/components/ai/translate-document-dialog.js +38 -3
  19. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  20. package/dist/components/app-views/default-modules.d.ts +141 -44
  21. package/dist/components/app-views/default-modules.js +229 -65
  22. package/dist/components/app-views/default-modules.js.map +1 -1
  23. package/dist/components/app-views/module.d.ts +38 -0
  24. package/dist/components/app-views/module.js.map +1 -1
  25. package/dist/components/app-views/sidebar.d.ts +5 -2
  26. package/dist/components/app-views/sidebar.js +16 -5
  27. package/dist/components/app-views/sidebar.js.map +1 -1
  28. package/dist/components/app-views/studio-config.d.ts +28 -1
  29. package/dist/components/app-views/studio-config.js +1 -1
  30. package/dist/components/app-views/studio-config.js.map +1 -1
  31. package/dist/components/builder/canvas-render-context.js +10 -2
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/clipboard-actions.js +5 -3
  34. package/dist/components/builder/clipboard-actions.js.map +1 -1
  35. package/dist/components/builder/reuse-context.js +7 -1
  36. package/dist/components/builder/reuse-context.js.map +1 -1
  37. package/dist/components/builder/reuse.d.ts +94 -16
  38. package/dist/components/builder/reuse.js +201 -19
  39. package/dist/components/builder/reuse.js.map +1 -1
  40. package/dist/components/cms/bulk-actions.js +10 -5
  41. package/dist/components/cms/bulk-actions.js.map +1 -1
  42. package/dist/components/cms/cell-edit.d.ts +18 -3
  43. package/dist/components/cms/cell-edit.js +29 -7
  44. package/dist/components/cms/cell-edit.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +54 -14
  46. package/dist/components/cms/cms-store.js +142 -45
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.js +2 -2
  49. package/dist/components/cms/collection-table.js.map +1 -1
  50. package/dist/components/cms/detail-editor.js +15 -14
  51. package/dist/components/cms/detail-editor.js.map +1 -1
  52. package/dist/components/cms/document-editor-context.d.ts +12 -1
  53. package/dist/components/cms/document-editor-context.js.map +1 -1
  54. package/dist/components/cms/field-control.js +22 -8
  55. package/dist/components/cms/field-control.js.map +1 -1
  56. package/dist/components/cms/field-renderer.js +8 -1
  57. package/dist/components/cms/field-renderer.js.map +1 -1
  58. package/dist/components/cms/gate-validation.d.ts +36 -0
  59. package/dist/components/cms/gate-validation.js +17 -0
  60. package/dist/components/cms/gate-validation.js.map +1 -0
  61. package/dist/components/cms/import-export.js +2 -1
  62. package/dist/components/cms/import-export.js.map +1 -1
  63. package/dist/components/cms/inline-field-errors.d.ts +8 -0
  64. package/dist/components/cms/inline-field-errors.js +52 -0
  65. package/dist/components/cms/inline-field-errors.js.map +1 -0
  66. package/dist/components/cms/issues-pane.d.ts +4 -2
  67. package/dist/components/cms/issues-pane.js +13 -7
  68. package/dist/components/cms/issues-pane.js.map +1 -1
  69. package/dist/components/cms/slug-collision-report.d.ts +41 -0
  70. package/dist/components/cms/slug-collision-report.js +52 -0
  71. package/dist/components/cms/slug-collision-report.js.map +1 -0
  72. package/dist/components/components/transfer-dialog.js +10 -12
  73. package/dist/components/components/transfer-dialog.js.map +1 -1
  74. package/dist/components/deployment-setup-gate.d.ts +24 -0
  75. package/dist/components/deployment-setup-gate.js +29 -4
  76. package/dist/components/deployment-setup-gate.js.map +1 -1
  77. package/dist/components/issue-severity.d.ts +73 -0
  78. package/dist/components/issue-severity.js +56 -0
  79. package/dist/components/issue-severity.js.map +1 -0
  80. package/dist/components/nav-issues.d.ts +68 -0
  81. package/dist/components/nav-issues.js +78 -0
  82. package/dist/components/nav-issues.js.map +1 -0
  83. package/dist/components/operation-error.js +3 -3
  84. package/dist/components/operation-error.js.map +1 -1
  85. package/dist/components/project-list.d.ts +9 -3
  86. package/dist/components/project-list.js +11 -5
  87. package/dist/components/project-list.js.map +1 -1
  88. package/dist/components/project-switcher.d.ts +3 -1
  89. package/dist/components/project-switcher.js +4 -2
  90. package/dist/components/project-switcher.js.map +1 -1
  91. package/dist/components/redirects/redirects-store.d.ts +21 -0
  92. package/dist/components/redirects/redirects-store.js +17 -4
  93. package/dist/components/redirects/redirects-store.js.map +1 -1
  94. package/dist/components/section-nav.js +75 -10
  95. package/dist/components/section-nav.js.map +1 -1
  96. package/dist/customization.d.ts +7 -0
  97. package/dist/customization.js.map +1 -1
  98. package/dist/lib/ai/apply-content.js +6 -7
  99. package/dist/lib/ai/apply-content.js.map +1 -1
  100. package/dist/lib/ai/chat-context.d.ts +32 -26
  101. package/dist/lib/ai/chat-context.js +81 -9
  102. package/dist/lib/ai/chat-context.js.map +1 -1
  103. package/dist/lib/ai/constraints.d.ts +42 -0
  104. package/dist/lib/ai/constraints.js +259 -0
  105. package/dist/lib/ai/constraints.js.map +1 -0
  106. package/dist/lib/ai/index.d.ts +2 -0
  107. package/dist/lib/ai/index.js +2 -0
  108. package/dist/lib/ai/index.js.map +1 -1
  109. package/dist/lib/ai/proposal-validation.d.ts +26 -0
  110. package/dist/lib/ai/proposal-validation.js +17 -0
  111. package/dist/lib/ai/proposal-validation.js.map +1 -0
  112. package/dist/lib/ai/shape.js +60 -13
  113. package/dist/lib/ai/shape.js.map +1 -1
  114. package/dist/lib/ai/tools.d.ts +8 -4
  115. package/dist/lib/ai/tools.js +43 -14
  116. package/dist/lib/ai/tools.js.map +1 -1
  117. package/dist/lib/ai/translate-document.d.ts +35 -1
  118. package/dist/lib/ai/translate-document.js +16 -1
  119. package/dist/lib/ai/translate-document.js.map +1 -1
  120. package/dist/lib/ai/types.d.ts +11 -6
  121. package/dist/lib/ai/types.js.map +1 -1
  122. package/dist/lib/ai/update-fields.d.ts +8 -6
  123. package/dist/lib/ai/update-fields.js +9 -22
  124. package/dist/lib/ai/update-fields.js.map +1 -1
  125. package/dist/lib/clipboard/paste-issues.d.ts +5 -0
  126. package/dist/lib/clipboard/paste-issues.js +16 -6
  127. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  128. package/dist/lib/cms/document-gate.d.ts +37 -0
  129. package/dist/lib/cms/document-gate.js +40 -0
  130. package/dist/lib/cms/document-gate.js.map +1 -0
  131. package/dist/lib/cms-views/view-model.d.ts +5 -0
  132. package/dist/lib/cms-views/view-model.js +10 -4
  133. package/dist/lib/cms-views/view-model.js.map +1 -1
  134. package/dist/lib/data/collection-content.d.ts +6 -1
  135. package/dist/lib/data/collection-content.js +51 -15
  136. package/dist/lib/data/collection-content.js.map +1 -1
  137. package/dist/lib/i18n/i18n.d.ts +51 -16
  138. package/dist/lib/i18n/i18n.js +119 -43
  139. package/dist/lib/i18n/i18n.js.map +1 -1
  140. package/dist/lib/i18n/index.d.ts +8 -1
  141. package/dist/lib/i18n/index.js +1 -1
  142. package/dist/lib/i18n/index.js.map +1 -1
  143. package/dist/lib/i18n/messages.de.js +200 -0
  144. package/dist/lib/i18n/messages.de.js.map +1 -1
  145. package/dist/lib/i18n/messages.en.d.ts +162 -0
  146. package/dist/lib/i18n/messages.en.js +226 -0
  147. package/dist/lib/i18n/messages.en.js.map +1 -1
  148. package/dist/lib/i18n/types.d.ts +15 -9
  149. package/dist/lib/i18n/types.js.map +1 -1
  150. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  151. package/dist/lib/locale/binding-fallback.js +8 -1
  152. package/dist/lib/locale/binding-fallback.js.map +1 -1
  153. package/dist/lib/locale/locale-store.d.ts +41 -9
  154. package/dist/lib/locale/locale-store.js +34 -15
  155. package/dist/lib/locale/locale-store.js.map +1 -1
  156. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  157. package/dist/lib/locale/locale-switcher.js +4 -2
  158. package/dist/lib/locale/locale-switcher.js.map +1 -1
  159. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  160. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  161. package/dist/lib/operations/studio-operations.js +19 -2
  162. package/dist/lib/operations/studio-operations.js.map +1 -1
  163. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  164. package/dist/lib/persistence/convex-adapter.js +0 -55
  165. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  166. package/dist/lib/persistence/deployment-readiness.js +10 -0
  167. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  168. package/dist/lib/persona/labels.d.ts +85 -9
  169. package/dist/lib/persona/labels.js +198 -58
  170. package/dist/lib/persona/labels.js.map +1 -1
  171. package/dist/lib/routes/route-issues.d.ts +9 -0
  172. package/dist/lib/routes/route-issues.js +117 -0
  173. package/dist/lib/routes/route-issues.js.map +1 -0
  174. package/dist/lib/webhooks/dispatcher.d.ts +5 -2
  175. package/dist/lib/webhooks/dispatcher.js.map +1 -1
  176. package/dist/lib/webhooks/tags.js +150 -17
  177. package/dist/lib/webhooks/tags.js.map +1 -1
  178. package/dist/lib/workspace/config-issues.d.ts +56 -0
  179. package/dist/lib/workspace/config-issues.js +72 -0
  180. package/dist/lib/workspace/config-issues.js.map +1 -0
  181. package/dist/lib/workspace/index.d.ts +24 -8
  182. package/dist/lib/workspace/index.js +28 -18
  183. package/dist/lib/workspace/index.js.map +1 -1
  184. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  185. package/dist/lib/workspace/switcher-source.js.map +1 -1
  186. package/dist/lib/workspace/workspace-source.js +27 -0
  187. package/dist/lib/workspace/workspace-source.js.map +1 -1
  188. package/dist/previews/manifest-format.d.ts +13 -6
  189. package/dist/previews/manifest-format.js +9 -2
  190. package/dist/previews/manifest-format.js.map +1 -1
  191. package/dist/router/root.d.ts +36 -1
  192. package/dist/router/root.js +54 -7
  193. package/dist/router/root.js.map +1 -1
  194. package/package.json +2 -2
  195. package/convex/cliTokens.ts +0 -63
  196. package/convex/sync.ts +0 -297
@@ -0,0 +1,56 @@
1
+ import { CircleAlert, TriangleAlert } from 'lucide-react';
2
+ /**
3
+ * The per-severity palette, as a pure mapping onto the shared token palette —
4
+ * unit-testable without a DOM (the `statusDotToneClass` pattern).
5
+ *
6
+ * The values are the document Issues pane's, which had them first: destructive
7
+ * border/fill/text for an error, warning border/fill/text for a warning. A surface
8
+ * applies `card` to the finding's container and `text` to at least its glyph; whether
9
+ * the tint also carries to the body copy is the surface's call (the pane keeps its
10
+ * body `muted`, the rail and the proposal card tint the whole row), because that is
11
+ * layout, not severity.
12
+ */
13
+ export function issueSeverityStyle(severity) {
14
+ return severity === 'error'
15
+ ? { card: 'border-destructive/30 bg-destructive/5', text: 'text-destructive' }
16
+ : { card: 'border-warning/30 bg-warning/5', text: 'text-warning' };
17
+ }
18
+ /** The glyph for a severity — `CircleAlert` for an error, `TriangleAlert` for a warning. */
19
+ export function issueSeverityIcon(severity) {
20
+ return severity === 'error' ? CircleAlert : TriangleAlert;
21
+ }
22
+ /**
23
+ * **The rank rule: `error` outranks `warning`.** Stated once here because two
24
+ * surfaces express it differently — the collapsed rail shows one aggregate glyph
25
+ * ({@link highestIssueSeverity}), the proposal card shows every finding and sorts
26
+ * them ({@link orderBySeverity}) — and a rule expressed twice drifts.
27
+ */
28
+ function isError(issue) {
29
+ return issue.severity === 'error';
30
+ }
31
+ /**
32
+ * The severity ONE aggregate glyph should report for a mixed list: **the highest
33
+ * present**.
34
+ *
35
+ * An icons-only rail may over-state the mildest finding, but it must never
36
+ * under-report a hard error down to an advisory — the reader has no second signal
37
+ * there, and expanding immediately corrects any over-statement.
38
+ *
39
+ * An empty list has no severity to report; callers do not render the widget at all
40
+ * in that case, and `'warning'` is the conservative answer for one that does.
41
+ */
42
+ export function highestIssueSeverity(issues) {
43
+ return issues.some(isError) ? 'error' : 'warning';
44
+ }
45
+ /**
46
+ * The same rank as ordering: **errors first**, warnings after, stable within each
47
+ * severity.
48
+ *
49
+ * For a surface that shows every finding rather than one glyph. Where the card's
50
+ * primary action commits the change and the reader scans top-down, the finding that
51
+ * should give pause must not sit below one that should merely inform.
52
+ */
53
+ export function orderBySeverity(issues) {
54
+ return [...issues.filter(isError), ...issues.filter((issue) => !isError(issue))];
55
+ }
56
+ //# sourceMappingURL=issue-severity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-severity.js","sourceRoot":"","sources":["../../src/components/issue-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAiCzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAuB;IAMxD,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,wCAAwC,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC9E,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;AACtE,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,QAAuB;IACvD,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAA;AAC3D,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAkC;IACjD,OAAO,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAA;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA8C;IAE9C,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAoB;IAEpB,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { CircleAlert, TriangleAlert } from 'lucide-react'\n\n/**\n * EC-507 — the studio's ONE issue-severity treatment.\n *\n * Three surfaces render a finding by severity: the document Issues pane\n * (`components/cms/issues-pane.tsx`, EC-283), the section-rail foot\n * (`components/nav-issues.tsx`, EC-493) and the AI proposal card\n * (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but\n * not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`\n * beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with\n * nothing to do with navigation, and the original stayed inlined in the pane — three\n * expressions of one rule, and no obvious home for the fourth surface to import from.\n * This module is that home. The `nav*` names are retired.\n *\n * Deliberately NOT here: the label/copy vocabulary. `cmsIssueLabel`, the message\n * catalog and `issueBodyDescriptor` are the EC-192 persona boundary — a separate\n * concern that happens to be rendered by the same cards. Presentation and copy stay\n * apart.\n *\n * Also not here: the `data-*` attributes each surface emits\n * (`data-nav-issue-severity`, `data-proposal-issue-severity`). Those are per-surface\n * test/inspection hooks, not part of the shared rule.\n */\n\n/**\n * The two values every contributing producer publishes — `CmsValidationIssue.severity`,\n * `CustomizationIssue.severity` and the project-graph `ValidationIssue.severity` are all\n * `'error' | 'warning'`. A surface carries the field across rather than re-deriving a\n * local notion of \"bad\".\n */\nexport type IssueSeverity = 'error' | 'warning'\n\n/**\n * The per-severity palette, as a pure mapping onto the shared token palette —\n * unit-testable without a DOM (the `statusDotToneClass` pattern).\n *\n * The values are the document Issues pane's, which had them first: destructive\n * border/fill/text for an error, warning border/fill/text for a warning. A surface\n * applies `card` to the finding's container and `text` to at least its glyph; whether\n * the tint also carries to the body copy is the surface's call (the pane keeps its\n * body `muted`, the rail and the proposal card tint the whole row), because that is\n * layout, not severity.\n */\nexport function issueSeverityStyle(severity: IssueSeverity): {\n /** Border + fill for the finding's card. */\n card: string\n /** Icon + message tone. */\n text: string\n} {\n return severity === 'error'\n ? { card: 'border-destructive/30 bg-destructive/5', text: 'text-destructive' }\n : { card: 'border-warning/30 bg-warning/5', text: 'text-warning' }\n}\n\n/** The glyph for a severity — `CircleAlert` for an error, `TriangleAlert` for a warning. */\nexport function issueSeverityIcon(severity: IssueSeverity) {\n return severity === 'error' ? CircleAlert : TriangleAlert\n}\n\n/**\n * **The rank rule: `error` outranks `warning`.** Stated once here because two\n * surfaces express it differently — the collapsed rail shows one aggregate glyph\n * ({@link highestIssueSeverity}), the proposal card shows every finding and sorts\n * them ({@link orderBySeverity}) — and a rule expressed twice drifts.\n */\nfunction isError(issue: { severity: IssueSeverity }): boolean {\n return issue.severity === 'error'\n}\n\n/**\n * The severity ONE aggregate glyph should report for a mixed list: **the highest\n * present**.\n *\n * An icons-only rail may over-state the mildest finding, but it must never\n * under-report a hard error down to an advisory — the reader has no second signal\n * there, and expanding immediately corrects any over-statement.\n *\n * An empty list has no severity to report; callers do not render the widget at all\n * in that case, and `'warning'` is the conservative answer for one that does.\n */\nexport function highestIssueSeverity(\n issues: readonly { severity: IssueSeverity }[],\n): IssueSeverity {\n return issues.some(isError) ? 'error' : 'warning'\n}\n\n/**\n * The same rank as ordering: **errors first**, warnings after, stable within each\n * severity.\n *\n * For a surface that shows every finding rather than one glyph. Where the card's\n * primary action commits the change and the reader scans top-down, the finding that\n * should give pause must not sit below one that should merely inform.\n */\nexport function orderBySeverity<T extends { severity: IssueSeverity }>(\n issues: readonly T[],\n): T[] {\n return [...issues.filter(isError), ...issues.filter((issue) => !isError(issue))]\n}\n"]}
@@ -0,0 +1,68 @@
1
+ import type { CmsValidationIssue } from '@elytracms/core/cms-core';
2
+ import type { CustomizationIssue } from '@elytracms/core/studio-core';
3
+ import type { IssueSeverity } from './issue-severity.js';
4
+ /**
5
+ * What the rail foot needs to render one finding: a stable key, a message, and the
6
+ * finding's severity. The least common shape across its contributing sources —
7
+ * customization issues (`CustomizationIssue`) and URL-map validation
8
+ * (`CmsValidationIssue`, EC-469) — so a new source joins by mapping to this, not by
9
+ * pretending to be a customization issue.
10
+ *
11
+ * `severity` was added by EC-493: both mappers used to drop it and the widget painted
12
+ * every finding amber, which stopped being a style nit once EC-488 re-derived
13
+ * `shadowedMountSpaces` from `error` to `warning` ON THE REASONING that the two mean
14
+ * different things, and EC-482 shipped one producer (`hierarchyMountConflicts`)
15
+ * emitting both into this very list.
16
+ */
17
+ export interface NavIssueItem {
18
+ key: string;
19
+ message: string;
20
+ severity: IssueSeverity;
21
+ }
22
+ /**
23
+ * Customization issues, as rail-foot items — localized (EC-478). The producers
24
+ * (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a
25
+ * catalog key + identifier values rather than a sentence, because the EC-236 seam
26
+ * lives in `packages/core` and may not import the studio catalog;
27
+ * {@link customizationIssueMessage} resolves them here, so this list renders wholly in
28
+ * the active locale instead of half of it.
29
+ *
30
+ * EC-493: `severity` rides along. It used to be dropped here, which is half of why an
31
+ * error and a warning were indistinguishable at the foot — `CustomizationIssue` has
32
+ * carried the field since EC-236, the mapper simply did not read it.
33
+ */
34
+ export declare function customizationItems(issues: readonly CustomizationIssue[]): NavIssueItem[];
35
+ /**
36
+ * URL-map validation issues (EC-469), as rail-foot items.
37
+ *
38
+ * Copy follows the issues-pane convention (EC-192): the localized plain-language label,
39
+ * then the detail — both localized since EC-487, which gave `CmsValidationIssue` the
40
+ * same catalog-reference carriage EC-478 gave `CustomizationIssue`.
41
+ *
42
+ * EC-493: `severity` rides along, the other half of the fix. It matters most here:
43
+ * EC-482's `hierarchyMountConflicts` emits `error` for two mounts on the same path and
44
+ * `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived
45
+ * `shadowedMountSpaces` to `warning` precisely because the fall-through made it
46
+ * conditional. Both land in this list.
47
+ */
48
+ export declare function routeIssueItems(issues: readonly CmsValidationIssue[]): NavIssueItem[];
49
+ /**
50
+ * Visible project-level issues at the rail foot — the seam's "never silent" (EC-236,
51
+ * extended by EC-467's reuse issues, EC-469's URL-map issues and EC-479's config
52
+ * issues).
53
+ *
54
+ * EC-493: an `error` and a `warning` render distinguishably here, in the treatment the
55
+ * document Issues pane already uses. Each finding is its own tinted card (as in the
56
+ * pane) rather than one shared amber box, because a shared box cannot express a mixed
57
+ * list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved
58
+ * that treatment to `components/issue-severity.ts`, the one home all three surfaces
59
+ * import from.
60
+ *
61
+ * `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the
62
+ * machine-readable form of the same distinction — what the smoke test pins, and what an
63
+ * agent inspecting the DOM reads without decoding Tailwind classes.
64
+ */
65
+ export declare function NavIssues({ issues, collapsed }: {
66
+ issues: NavIssueItem[];
67
+ collapsed: boolean;
68
+ }): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '@elytracms/studio/lib/utils';
3
+ import { translate } from '@elytracms/studio/lib/i18n';
4
+ import { cmsIssueLabel, cmsValidationIssueMessage, customizationIssueMessage, } from '@elytracms/studio/lib/persona/labels';
5
+ import { Row, Stack, Text } from '@elytracms/studio/ui';
6
+ import { highestIssueSeverity, issueSeverityIcon, issueSeverityStyle } from './issue-severity.js';
7
+ /**
8
+ * Customization issues, as rail-foot items — localized (EC-478). The producers
9
+ * (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a
10
+ * catalog key + identifier values rather than a sentence, because the EC-236 seam
11
+ * lives in `packages/core` and may not import the studio catalog;
12
+ * {@link customizationIssueMessage} resolves them here, so this list renders wholly in
13
+ * the active locale instead of half of it.
14
+ *
15
+ * EC-493: `severity` rides along. It used to be dropped here, which is half of why an
16
+ * error and a warning were indistinguishable at the foot — `CustomizationIssue` has
17
+ * carried the field since EC-236, the mapper simply did not read it.
18
+ */
19
+ export function customizationItems(issues) {
20
+ return issues.map((issue, index) => ({
21
+ key: `${issue.point}:${issue.id ?? index}`,
22
+ message: customizationIssueMessage(issue),
23
+ severity: issue.severity,
24
+ }));
25
+ }
26
+ /**
27
+ * URL-map validation issues (EC-469), as rail-foot items.
28
+ *
29
+ * Copy follows the issues-pane convention (EC-192): the localized plain-language label,
30
+ * then the detail — both localized since EC-487, which gave `CmsValidationIssue` the
31
+ * same catalog-reference carriage EC-478 gave `CustomizationIssue`.
32
+ *
33
+ * EC-493: `severity` rides along, the other half of the fix. It matters most here:
34
+ * EC-482's `hierarchyMountConflicts` emits `error` for two mounts on the same path and
35
+ * `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived
36
+ * `shadowedMountSpaces` to `warning` precisely because the fall-through made it
37
+ * conditional. Both land in this list.
38
+ */
39
+ export function routeIssueItems(issues) {
40
+ return issues.map((issue, index) => ({
41
+ key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,
42
+ message: `${cmsIssueLabel(issue.code, issue.severity)} — ${cmsValidationIssueMessage(issue)}`,
43
+ severity: issue.severity,
44
+ }));
45
+ }
46
+ /**
47
+ * Visible project-level issues at the rail foot — the seam's "never silent" (EC-236,
48
+ * extended by EC-467's reuse issues, EC-469's URL-map issues and EC-479's config
49
+ * issues).
50
+ *
51
+ * EC-493: an `error` and a `warning` render distinguishably here, in the treatment the
52
+ * document Issues pane already uses. Each finding is its own tinted card (as in the
53
+ * pane) rather than one shared amber box, because a shared box cannot express a mixed
54
+ * list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved
55
+ * that treatment to `components/issue-severity.ts`, the one home all three surfaces
56
+ * import from.
57
+ *
58
+ * `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the
59
+ * machine-readable form of the same distinction — what the smoke test pins, and what an
60
+ * agent inspecting the DOM reads without decoding Tailwind classes.
61
+ */
62
+ export function NavIssues({ issues, collapsed }) {
63
+ if (collapsed) {
64
+ const severity = highestIssueSeverity(issues);
65
+ const Icon = issueSeverityIcon(severity);
66
+ return (_jsx("div", { className: cn('grid h-9 place-items-center', issueSeverityStyle(severity).text), "data-nav-issue-severity": severity, title: issues.map((issue) => issue.message).join('\n'), children: _jsx(Icon, { className: "size-4", "aria-label": translate('shell.nav.issuesCount', { count: issues.length }) }) }));
67
+ }
68
+ return (_jsx(Stack, { gap: "1", className: "mb-1", children: issues.map((issue) => (_jsx(NavIssueCard, { issue: issue }, issue.key))) }));
69
+ }
70
+ /** One finding at the rail foot, tinted + glyphed by its severity. */
71
+ function NavIssueCard({ issue }) {
72
+ const style = issueSeverityStyle(issue.severity);
73
+ const Icon = issueSeverityIcon(issue.severity);
74
+ return (_jsxs(Row, { gap: "1", align: "start",
75
+ // `gap-1.5` (the pre-EC-493 icon/text spacing) merged over the primitive's `gap-1`.
76
+ className: cn('gap-1.5 rounded-md border px-2 py-1.5', style.card, style.text), "data-nav-issue-severity": issue.severity, title: issue.message, children: [_jsx(Icon, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 leading-snug", children: issue.message })] }));
77
+ }
78
+ //# sourceMappingURL=nav-issues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-issues.js","sourceRoot":"","sources":["../../src/components/nav-issues.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n cmsIssueLabel,\n cmsValidationIssueMessage,\n customizationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\nimport { Row, Stack, Text } from '@elytracms/studio/ui'\nimport { highestIssueSeverity, issueSeverityIcon, issueSeverityStyle } from './issue-severity'\nimport type { IssueSeverity } from './issue-severity'\n\n/**\n * What the rail foot needs to render one finding: a stable key, a message, and the\n * finding's severity. The least common shape across its contributing sources —\n * customization issues (`CustomizationIssue`) and URL-map validation\n * (`CmsValidationIssue`, EC-469) — so a new source joins by mapping to this, not by\n * pretending to be a customization issue.\n *\n * `severity` was added by EC-493: both mappers used to drop it and the widget painted\n * every finding amber, which stopped being a style nit once EC-488 re-derived\n * `shadowedMountSpaces` from `error` to `warning` ON THE REASONING that the two mean\n * different things, and EC-482 shipped one producer (`hierarchyMountConflicts`)\n * emitting both into this very list.\n */\nexport interface NavIssueItem {\n key: string\n message: string\n severity: IssueSeverity\n}\n\n/**\n * Customization issues, as rail-foot items — localized (EC-478). The producers\n * (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a\n * catalog key + identifier values rather than a sentence, because the EC-236 seam\n * lives in `packages/core` and may not import the studio catalog;\n * {@link customizationIssueMessage} resolves them here, so this list renders wholly in\n * the active locale instead of half of it.\n *\n * EC-493: `severity` rides along. It used to be dropped here, which is half of why an\n * error and a warning were indistinguishable at the foot — `CustomizationIssue` has\n * carried the field since EC-236, the mapper simply did not read it.\n */\nexport function customizationItems(issues: readonly CustomizationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => ({\n key: `${issue.point}:${issue.id ?? index}`,\n message: customizationIssueMessage(issue),\n severity: issue.severity,\n }))\n}\n\n/**\n * URL-map validation issues (EC-469), as rail-foot items.\n *\n * Copy follows the issues-pane convention (EC-192): the localized plain-language label,\n * then the detail — both localized since EC-487, which gave `CmsValidationIssue` the\n * same catalog-reference carriage EC-478 gave `CustomizationIssue`.\n *\n * EC-493: `severity` rides along, the other half of the fix. It matters most here:\n * EC-482's `hierarchyMountConflicts` emits `error` for two mounts on the same path and\n * `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived\n * `shadowedMountSpaces` to `warning` precisely because the fall-through made it\n * conditional. Both land in this list.\n */\nexport function routeIssueItems(issues: readonly CmsValidationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => ({\n key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,\n message: `${cmsIssueLabel(issue.code, issue.severity)} — ${cmsValidationIssueMessage(issue)}`,\n severity: issue.severity,\n }))\n}\n\n/**\n * Visible project-level issues at the rail foot — the seam's \"never silent\" (EC-236,\n * extended by EC-467's reuse issues, EC-469's URL-map issues and EC-479's config\n * issues).\n *\n * EC-493: an `error` and a `warning` render distinguishably here, in the treatment the\n * document Issues pane already uses. Each finding is its own tinted card (as in the\n * pane) rather than one shared amber box, because a shared box cannot express a mixed\n * list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved\n * that treatment to `components/issue-severity.ts`, the one home all three surfaces\n * import from.\n *\n * `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the\n * machine-readable form of the same distinction — what the smoke test pins, and what an\n * agent inspecting the DOM reads without decoding Tailwind classes.\n */\nexport function NavIssues({ issues, collapsed }: { issues: NavIssueItem[]; collapsed: boolean }) {\n if (collapsed) {\n const severity = highestIssueSeverity(issues)\n const Icon = issueSeverityIcon(severity)\n return (\n <div\n className={cn('grid h-9 place-items-center', issueSeverityStyle(severity).text)}\n data-nav-issue-severity={severity}\n title={issues.map((issue) => issue.message).join('\\n')}\n >\n <Icon\n className=\"size-4\"\n aria-label={translate('shell.nav.issuesCount', { count: issues.length })}\n />\n </div>\n )\n }\n return (\n <Stack gap=\"1\" className=\"mb-1\">\n {issues.map((issue) => (\n <NavIssueCard key={issue.key} issue={issue} />\n ))}\n </Stack>\n )\n}\n\n/** One finding at the rail foot, tinted + glyphed by its severity. */\nfunction NavIssueCard({ issue }: { issue: NavIssueItem }) {\n const style = issueSeverityStyle(issue.severity)\n const Icon = issueSeverityIcon(issue.severity)\n return (\n <Row\n gap=\"1\"\n align=\"start\"\n // `gap-1.5` (the pre-EC-493 icon/text spacing) merged over the primitive's `gap-1`.\n className={cn('gap-1.5 rounded-md border px-2 py-1.5', style.card, style.text)}\n data-nav-issue-severity={issue.severity}\n title={issue.message}\n >\n <Icon className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 leading-snug\">\n {issue.message}\n </Text>\n </Row>\n )\n}\n"]}
@@ -4,7 +4,7 @@ import { CircleAlert, X } from 'lucide-react';
4
4
  import { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors';
5
5
  import { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry';
6
6
  import { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui';
7
- import { operationErrorLabel } from '@elytracms/studio/lib/persona/labels';
7
+ import { operationErrorLabel, operationErrorMessage, operationIssueMessage, } from '@elytracms/studio/lib/persona/labels';
8
8
  import { translate } from '@elytracms/studio/lib/i18n';
9
9
  /**
10
10
  * Shared error surface for failed/denied operations (EC-141). Every studio
@@ -29,10 +29,10 @@ export function OperationErrorBanner({ error, onDismiss, className, }) {
29
29
  }, [error]);
30
30
  if (isSessionExpiredError(error))
31
31
  return null;
32
- return (_jsx("div", { role: "alert", className: 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? ''), children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "error", title: error.code, "data-code": error.code, children: operationErrorLabel(error.code) }), _jsx(Text, { size: "sm", className: "text-destructive", children: error.message })] }), error.code === 'validation' && error.issues.length > 0 ? (_jsx(Stack, { gap: "none", children: error.issues.map((issue, i) => (_jsx(Text, { size: "xs", tone: "muted",
32
+ return (_jsx("div", { role: "alert", className: 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? ''), children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "error", title: error.code, "data-code": error.code, children: operationErrorLabel(error.code) }), _jsx(Text, { size: "sm", className: "text-destructive", children: operationErrorMessage(error) })] }), error.code === 'validation' && error.issues.length > 0 ? (_jsx(Stack, { gap: "none", children: error.issues.map((issue, i) => (_jsx(Text, { size: "xs", tone: "muted",
33
33
  // The raw graph path (`pages.0.root.slots…`) is structural
34
34
  // machine addressing — kept inspectable on hover, never shown
35
35
  // as visible copy to a non-technical editor (EC-192).
36
- title: issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined, children: issue.message }, `${issue.code}-${i}`))) })) : null] }), onDismiss ? (_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('error.dismiss'), onClick: onDismiss, className: "shrink-0", children: _jsx(X, {}) })) : null] }) }));
36
+ title: issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined, children: operationIssueMessage(issue) }, `${issue.code}-${i}`))) })) : null] }), onDismiss ? (_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('error.dismiss'), onClick: onDismiss, className: "shrink-0", children: _jsx(X, {}) })) : null] }) }));
37
37
  }
38
38
  //# sourceMappingURL=operation-error.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"operation-error.js","sourceRoot":"","sources":["../../src/components/operation-error.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAA;AAChF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAA;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,EACnC,KAAK,EACL,SAAS,EACT,SAAS,GAKV;IACC,uEAAuE;IACvE,yEAAyE;IACzE,kEAAkE;IAClE,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE;QACb,yBAAyB,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7C,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EACP,sEAAsE,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,YAG5F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,KAAK,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAE,KAAK,CAAC,IAAI,eAAa,KAAK,CAAC,IAAI,YAC5D,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,GAC1B,EACR,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACzC,KAAK,CAAC,OAAO,GACT,IACH,EACL,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACxD,KAAC,KAAK,IAAC,GAAG,EAAC,MAAM,YACd,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9B,KAAC,IAAI,IAEH,IAAI,EAAC,IAAI,EACT,IAAI,EAAC,OAAO;gCACZ,2DAA2D;gCAC3D,8DAA8D;gCAC9D,sDAAsD;gCACtD,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,YAE5E,KAAK,CAAC,OAAO,IART,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,CASpB,CACR,CAAC,GACI,CACT,CAAC,CAAC,CAAC,IAAI,IACF,EACP,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,eAAe,CAAC,EACtC,OAAO,EAAE,SAAS,EAClB,SAAS,EAAC,UAAU,YAEpB,KAAC,CAAC,KAAG,GACE,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect } from 'react'\nimport { CircleAlert, X } from 'lucide-react'\nimport type { OperationError } from '@elytracms/studio/lib/operations'\nimport { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors'\nimport { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry'\nimport { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { operationErrorLabel } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Shared error surface for failed/denied operations (EC-141). Every studio\n * surface renders rejected commands through this banner so a structured\n * `OperationError` — including its validation issues — is always *visible*,\n * never a silent failure.\n *\n * Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:\n * an expired session is an app-level state, so the banner funnels the error\n * into the global session-expiry chokepoint (which raises the blocking\n * in-place re-auth overlay) and renders nothing itself — the per-surface\n * banner path remains only for non-expiry failures. The raw OIDC blob is\n * never shown either way.\n */\nexport function OperationErrorBanner({\n error,\n onDismiss,\n className,\n}: {\n error: OperationError\n onDismiss?: () => void\n className?: string\n}) {\n // Funnel every error into the EC-403 chokepoint (belt-and-braces for a\n // failure that reached this surface without passing the operations event\n // bus). Deduped per error instance there, so re-rendering a stale\n // persistError after re-auth never re-raises the overlay.\n useEffect(() => {\n reportSessionExpiredError(error)\n }, [error])\n\n if (isSessionExpiredError(error)) return null\n\n return (\n <div\n role=\"alert\"\n className={\n 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? '')\n }\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"error\" title={error.code} data-code={error.code}>\n {operationErrorLabel(error.code)}\n </Badge>\n <Text size=\"sm\" className=\"text-destructive\">\n {error.message}\n </Text>\n </Row>\n {error.code === 'validation' && error.issues.length > 0 ? (\n <Stack gap=\"none\">\n {error.issues.map((issue, i) => (\n <Text\n key={`${issue.code}-${i}`}\n size=\"xs\"\n tone=\"muted\"\n // The raw graph path (`pages.0.root.slots…`) is structural\n // machine addressing — kept inspectable on hover, never shown\n // as visible copy to a non-technical editor (EC-192).\n title={issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined}\n >\n {issue.message}\n </Text>\n ))}\n </Stack>\n ) : null}\n </Stack>\n {onDismiss ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('error.dismiss')}\n onClick={onDismiss}\n className=\"shrink-0\"\n >\n <X />\n </Button>\n ) : null}\n </Row>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"operation-error.js","sourceRoot":"","sources":["../../src/components/operation-error.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAA;AAChF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,EACnC,KAAK,EACL,SAAS,EACT,SAAS,GAKV;IACC,uEAAuE;IACvE,yEAAyE;IACzE,kEAAkE;IAClE,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE;QACb,yBAAyB,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7C,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EACP,sEAAsE,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,YAG5F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,KAAK,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAE,KAAK,CAAC,IAAI,eAAa,KAAK,CAAC,IAAI,YAC5D,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,GAC1B,EACR,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YAKzC,qBAAqB,CAAC,KAAK,CAAC,GACxB,IACH,EACL,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACxD,KAAC,KAAK,IAAC,GAAG,EAAC,MAAM,YACd,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9B,KAAC,IAAI,IAEH,IAAI,EAAC,IAAI,EACT,IAAI,EAAC,OAAO;gCACZ,2DAA2D;gCAC3D,8DAA8D;gCAC9D,sDAAsD;gCACtD,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,YAE5E,qBAAqB,CAAC,KAAK,CAAC,IARxB,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,CASpB,CACR,CAAC,GACI,CACT,CAAC,CAAC,CAAC,IAAI,IACF,EACP,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,eAAe,CAAC,EACtC,OAAO,EAAE,SAAS,EAClB,SAAS,EAAC,UAAU,YAEpB,KAAC,CAAC,KAAG,GACE,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect } from 'react'\nimport { CircleAlert, X } from 'lucide-react'\nimport type { OperationError } from '@elytracms/studio/lib/operations'\nimport { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors'\nimport { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry'\nimport { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport {\n operationErrorLabel,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Shared error surface for failed/denied operations (EC-141). Every studio\n * surface renders rejected commands through this banner so a structured\n * `OperationError` — including its validation issues — is always *visible*,\n * never a silent failure.\n *\n * Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:\n * an expired session is an app-level state, so the banner funnels the error\n * into the global session-expiry chokepoint (which raises the blocking\n * in-place re-auth overlay) and renders nothing itself — the per-surface\n * banner path remains only for non-expiry failures. The raw OIDC blob is\n * never shown either way.\n */\nexport function OperationErrorBanner({\n error,\n onDismiss,\n className,\n}: {\n error: OperationError\n onDismiss?: () => void\n className?: string\n}) {\n // Funnel every error into the EC-403 chokepoint (belt-and-braces for a\n // failure that reached this surface without passing the operations event\n // bus). Deduped per error instance there, so re-rendering a stale\n // persistError after re-auth never re-raises the overlay.\n useEffect(() => {\n reportSessionExpiredError(error)\n }, [error])\n\n if (isSessionExpiredError(error)) return null\n\n return (\n <div\n role=\"alert\"\n className={\n 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? '')\n }\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"error\" title={error.code} data-code={error.code}>\n {operationErrorLabel(error.code)}\n </Badge>\n <Text size=\"sm\" className=\"text-destructive\">\n {/* EC-494: the envelope carries its own catalog ref across the wire,\n so this sentence localizes instead of sitting in English under a\n localized badge. Ref-less (a persistence-layer or hook-authored\n message) falls back to that English, visibly. */}\n {operationErrorMessage(error)}\n </Text>\n </Row>\n {error.code === 'validation' && error.issues.length > 0 ? (\n <Stack gap=\"none\">\n {error.issues.map((issue, i) => (\n <Text\n key={`${issue.code}-${i}`}\n size=\"xs\"\n tone=\"muted\"\n // The raw graph path (`pages.0.root.slots…`) is structural\n // machine addressing — kept inspectable on hover, never shown\n // as visible copy to a non-technical editor (EC-192).\n title={issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined}\n >\n {operationIssueMessage(issue)}\n </Text>\n ))}\n </Stack>\n ) : null}\n </Stack>\n {onDismiss ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('error.dismiss')}\n onClick={onDismiss}\n className=\"shrink-0\"\n >\n <X />\n </Button>\n ) : null}\n </Row>\n </div>\n )\n}\n"]}
@@ -8,8 +8,14 @@ export interface ProjectLauncherProps {
8
8
  * Studio's front door: lists the workspace's projects and routes into a
9
9
  * project's shell. With a workspace config the list is config-driven
10
10
  * (configured projects ∩ grants, plus explicit "not provisioned" states);
11
- * project *creation* is code-first — `elytra.config.ts` + `builder init
12
- * --project <slug>` — so there is no in-UI create flow. Without a config the
13
- * adapter-backed list (and the starter-kit gallery) behaves as before.
11
+ * project *creation* is code-first — declare it in `elytra.config.ts`, then
12
+ * provision the backend with `elytra init` — so there is no in-UI create flow.
13
+ * Without a config the adapter-backed list (and the starter-kit gallery)
14
+ * behaves as before.
15
+ *
16
+ * EC-470: this used to instruct `builder init --project <slug>`. No `builder`
17
+ * bin was ever declared, so that was an unrunnable command shown as product UI.
18
+ * The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`
19
+ * reports what is still missing).
14
20
  */
15
21
  export declare function ProjectLauncher({ forcedState }: ProjectLauncherProps): import("react").JSX.Element;
@@ -15,9 +15,15 @@ import { useRecentProjects, useSwitcherEntries } from './workspace-projects.js';
15
15
  * Studio's front door: lists the workspace's projects and routes into a
16
16
  * project's shell. With a workspace config the list is config-driven
17
17
  * (configured projects ∩ grants, plus explicit "not provisioned" states);
18
- * project *creation* is code-first — `elytra.config.ts` + `builder init
19
- * --project <slug>` — so there is no in-UI create flow. Without a config the
20
- * adapter-backed list (and the starter-kit gallery) behaves as before.
18
+ * project *creation* is code-first — declare it in `elytra.config.ts`, then
19
+ * provision the backend with `elytra init` — so there is no in-UI create flow.
20
+ * Without a config the adapter-backed list (and the starter-kit gallery)
21
+ * behaves as before.
22
+ *
23
+ * EC-470: this used to instruct `builder init --project <slug>`. No `builder`
24
+ * bin was ever declared, so that was an unrunnable command shown as product UI.
25
+ * The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`
26
+ * reports what is still missing).
21
27
  */
22
28
  export function ProjectLauncher({ forcedState }) {
23
29
  const studio = useStudio();
@@ -49,7 +55,7 @@ export function ProjectLauncher({ forcedState }) {
49
55
  ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
50
56
  : 'Open and manage your Elytra projects.' })] }), _jsx(Spacer, {})] }), recents.length > 0 && !forced ? (_jsxs(Stack, { gap: "2", className: "mb-6", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, className: "uppercase", children: translate('shell.projectList.recent') }), _jsx(Row, { gap: "2", wrap: true, children: recents.map((p) => (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: p.id, _splat: studioPath.index() }, onClick: () => studio.activate(p.id), className: "inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: p.name.charAt(0).toUpperCase() }), p.name] }, p.id))) })] })) : null, forced ? (_jsx(SectionStateView, { state: forced, title: forced === 'empty' ? translate('shell.projectList.empty.title') : undefined })) : visible.length === 0 ? (_jsx(SectionStateView, { state: "empty", title: translate('shell.projectList.empty.title'), description: hasConfig
51
57
  ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'
52
- : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: visible.map((entry) => entry.kind === 'project' ? (_jsx(ProjectCard, { project: entry.project }, entry.project.id)) : (_jsx(UnprovisionedCard, { config: entry.config }, `config:${entry.config.slug}`))) }))] }));
58
+ : 'No active projects. Projects are code: scaffold a new one with `npm create elytra@latest`, or declare it in elytra.config.ts, then run `elytra init` to provision its backend.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: visible.map((entry) => entry.kind === 'project' ? (_jsx(ProjectCard, { project: entry.project }, entry.project.id)) : (_jsx(UnprovisionedCard, { config: entry.config }, `config:${entry.config.slug}`))) }))] }));
53
59
  }
54
60
  function ProjectCard({ project }) {
55
61
  const studio = useStudio();
@@ -63,6 +69,6 @@ function ProjectCard({ project }) {
63
69
  * deterministic state pointing at the CLI provisioning flow — never an error.
64
70
  */
65
71
  function UnprovisionedCard({ config }) {
66
- return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsx(Text, { size: "sm", tone: "muted", children: "Declared in the workspace config, but no backend project exists yet. Provision it from your repo:" }), _jsxs(Code, { className: "block w-fit max-w-full truncate text-xs", children: ["builder init --project ", config.slug] })] }));
72
+ return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsxs(Text, { size: "sm", tone: "muted", children: ["Declared in the workspace config, but no backend project exists yet. Point", ' ', _jsx(Code, { tone: "muted", children: "studio/.env" }), " at this project\u2019s Convex deployment, then provision it from that project\u2019s repo:"] }), _jsx(Code, { className: "block w-fit max-w-full truncate text-xs", children: "elytra init" }), _jsxs(Text, { size: "xs", tone: "muted", children: [_jsx(Code, { tone: "muted", children: "elytra doctor" }), " reports what is still missing."] })] }));
67
73
  }
68
74
  //# sourceMappingURL=project-list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n NavigateOnce,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useLocation,\n} from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n const location = useLocation()\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n // EC-401: a single-project workspace skips the launcher hop — the bare `/`\n // hit forwards straight into the sole project (replace, so Back never loops\n // through the launcher). Explicit \"all projects\" navigations carry the\n // history-state marker and keep the launcher visible, as do forced `?state=`\n // verification states and a lone UNPROVISIONED entry (its setup gate lives\n // here). Activation/recents are recorded by the project screen on arrival.\n const autoForward = launcherAutoForwardTarget({\n entries,\n forcedState: forcedState != null,\n explicitVisit: isExplicitLauncherVisit(location.state),\n })\n if (autoForward) {\n // EC-441: `NavigateOnce`, not `<Navigate>` — on a slow boot the launcher\n // stays mounted while the project splat's modules load, re-rendering on\n // every router-store tick; a per-render re-navigate compounded into the\n // \"Maximum update depth exceeded\" rejection (104 re-fires on one cold load).\n return (\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: autoForward.id, _splat: studioPath.index() }}\n replace\n />\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,uBAAuB,EACvB,IAAI,EACJ,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,WAAW,GACZ,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAO5E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,WAAW,EAAwB;IACnE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,kBAAkB,EAAE,CAAA;IAEnD,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAA;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAA;IACvB,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC1C,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAE9B,MAAM,MAAM,GAAG,WAAW,IAAI,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA;IAE5E,2EAA2E;IAC3E,4EAA4E;IAC5E,uEAAuE;IACvE,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,WAAW,GAAG,yBAAyB,CAAC;QAC5C,OAAO;QACP,WAAW,EAAE,WAAW,IAAI,IAAI;QAChC,aAAa,EAAE,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC;KACvD,CAAC,CAAA;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,6EAA6E;QAC7E,OAAO,CACL,KAAC,YAAY,IACX,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EACjE,OAAO,SACP,CACH,CAAA;IACH,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,oCAAoC,aACjD,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,MAAM,aAC3B,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,YAAE,SAAS,CAAC,gBAAgB,CAAC,GAAW,EAE/D,KAAC,eAAe,KAAG,EAClB,SAAS,CAAC,CAAC,CAAC,CAEX,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,cAAc,IAAC,SAAS,EAAC,QAAQ,GAAG,OAAE,SAAS,CAAC,MAAM,IACjD,CACT,CAAC,CAAC,CAAC,IAAI,IACJ,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YACf,SAAS;oCACR,CAAC,CAAC,gGAAgG;oCAClG,CAAC,CAAC,uCAAuC,GACtC,IACD,EACR,KAAC,MAAM,KAAG,IACN,EAEL,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAC/B,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,MAAM,aAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,QAAC,SAAS,EAAC,WAAW,YACpD,SAAS,CAAC,0BAA0B,CAAC,GACjC,EACP,KAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,kBACd,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAClB,MAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EACvD,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EACpC,SAAS,EAAC,qJAAqJ,aAE/J,eAAM,SAAS,EAAC,yGAAyG,YACtH,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAC1B,EACN,CAAC,CAAC,IAAI,KATF,CAAC,CAAC,EAAE,CAUJ,CACR,CAAC,GACE,IACA,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,MAAM,CAAC,CAAC,CAAC,CACR,KAAC,gBAAgB,IACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC,CAAC,CAAC,SAAS,GAClF,CACH,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACzB,KAAC,gBAAgB,IACf,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,SAAS,CAAC,+BAA+B,CAAC,EACjD,WAAW,EACT,SAAS;oBACP,CAAC,CAAC,2HAA2H;oBAC7H,CAAC,CAAC,gLAAgL,GAEtL,CACH,CAAC,CAAC,CAAC,CACF,cAAK,SAAS,EAAC,0CAA0C,YACtD,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACrB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CACzB,KAAC,WAAW,IAAwB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAxC,KAAK,CAAC,OAAO,CAAC,EAAE,CAA4B,CAC/D,CAAC,CAAC,CAAC,CACF,KAAC,iBAAiB,IAAqC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAnD,UAAU,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAA0B,CAChF,CACF,GACG,CACP,IACG,CACP,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,OAAO,EAAwB;IACpD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,+EAA+E;IAC/E,+EAA+E;IAC/E,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAC,mEAAmE,aACjF,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,eAAM,SAAS,EAAC,yGAAyG,YACtH,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAChC,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,SAAS,aAChC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,OAAO,CAAC,IAAI,GACR,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,2BAA2B,YACrD,OAAO,CAAC,IAAI,GACR,IACD,EACR,KAAC,MAAM,KAAG,EACT,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,iBAAiB,KAAG,IAClC,EAEN,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,uBAAuB,aAC5C,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,KAAK,IAAC,SAAS,EAAC,QAAQ,GAAG,OAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,IACvD,EACR,KAAC,MAAM,KAAG,IACN,EAEN,KAAC,MAAM,IAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAC,IAAI,EAAC,OAAO,QAAC,SAAS,EAAC,MAAM,YACnF,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EAC7D,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,YAEzC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAC9C,8BACE,KAAC,KAAK,KAAG,OAAE,SAAS,CAAC,aAAa,CAAC,IAClC,CACJ,GACI,GACA,IACJ,CACR,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,EAAE,MAAM,EAAwC;IACzE,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAC,uCAAuC,aACrD,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,eAAM,SAAS,EAAC,0HAA0H,YACvI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAC/B,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,SAAS,aAChC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,MAAM,CAAC,IAAI,GACP,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,2BAA2B,YACrD,MAAM,CAAC,IAAI,GACP,IACD,EACR,KAAC,MAAM,KAAG,EACV,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,SAAS,CAAC,8BAA8B,CAAC,GAAS,IACxE,EAEN,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,2FACiD,GAAG,EAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,4BAAmB,mGAEhC,EACP,KAAC,IAAI,IAAC,SAAS,EAAC,yCAAyC,4BAAmB,EAC5E,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,aAC1B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,8BAAqB,uCAClC,IACF,CACR,CAAA;AACH,CAAC","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n NavigateOnce,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useLocation,\n} from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — declare it in `elytra.config.ts`, then\n * provision the backend with `elytra init` — so there is no in-UI create flow.\n * Without a config the adapter-backed list (and the starter-kit gallery)\n * behaves as before.\n *\n * EC-470: this used to instruct `builder init --project <slug>`. No `builder`\n * bin was ever declared, so that was an unrunnable command shown as product UI.\n * The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`\n * reports what is still missing).\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n const location = useLocation()\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n // EC-401: a single-project workspace skips the launcher hop — the bare `/`\n // hit forwards straight into the sole project (replace, so Back never loops\n // through the launcher). Explicit \"all projects\" navigations carry the\n // history-state marker and keep the launcher visible, as do forced `?state=`\n // verification states and a lone UNPROVISIONED entry (its setup gate lives\n // here). Activation/recents are recorded by the project screen on arrival.\n const autoForward = launcherAutoForwardTarget({\n entries,\n forcedState: forcedState != null,\n explicitVisit: isExplicitLauncherVisit(location.state),\n })\n if (autoForward) {\n // EC-441: `NavigateOnce`, not `<Navigate>` — on a slow boot the launcher\n // stays mounted while the project splat's modules load, re-rendering on\n // every router-store tick; a per-render re-navigate compounded into the\n // \"Maximum update depth exceeded\" rejection (104 re-fires on one cold load).\n return (\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: autoForward.id, _splat: studioPath.index() }}\n replace\n />\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Projects are code: scaffold a new one with `npm create elytra@latest`, or declare it in elytra.config.ts, then run `elytra init` to provision its backend.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Point{' '}\n <Code tone=\"muted\">studio/.env</Code> at this project’s Convex deployment, then provision it\n from that project’s repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">elytra init</Code>\n <Text size=\"xs\" tone=\"muted\">\n <Code tone=\"muted\">elytra doctor</Code> reports what is still missing.\n </Text>\n </Card>\n )\n}\n"]}
@@ -4,7 +4,9 @@ import type { Project } from '@elytracms/core/studio-core';
4
4
  * the list is config-driven — configured projects ∩ the user's grants, plus
5
5
  * explicit "not provisioned" states for config entries that have no backend
6
6
  * project yet. Without a config it lists the adapter-backed projects exactly
7
- * as before. Project *creation* lives in code + CLI (`builder init`), not here.
7
+ * as before. Project *creation* lives in code + CLI (`elytra.config.ts` +
8
+ * `elytra init`), not here — EC-470 retired the `builder init` this named,
9
+ * which was never a declared bin.
8
10
  */
9
11
  export declare function ProjectSwitcher({ activeProject }: {
10
12
  activeProject: Project;
@@ -12,7 +12,9 @@ import { SignInNeededBadge } from './sign-in-needed-badge.js';
12
12
  * the list is config-driven — configured projects ∩ the user's grants, plus
13
13
  * explicit "not provisioned" states for config entries that have no backend
14
14
  * project yet. Without a config it lists the adapter-backed projects exactly
15
- * as before. Project *creation* lives in code + CLI (`builder init`), not here.
15
+ * as before. Project *creation* lives in code + CLI (`elytra.config.ts` +
16
+ * `elytra init`), not here — EC-470 retired the `builder init` this named,
17
+ * which was never a declared bin.
16
18
  */
17
19
  export function ProjectSwitcher({ activeProject }) {
18
20
  const studio = useStudio();
@@ -47,6 +49,6 @@ function ProjectMenuItem({ project, active, onSelect, }) {
47
49
  * non-navigating state pointing at the CLI provisioning flow — never an error.
48
50
  */
49
51
  function UnprovisionedMenuItem({ config }) {
50
- return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsxs(Code, { tone: "muted", className: "w-fit max-w-full truncate text-[0.7rem]", children: ["builder init --project ", config.slug] })] }) }));
52
+ return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate text-[0.7rem]", children: "elytra init" })] }) }));
51
53
  }
52
54
  //# sourceMappingURL=project-switcher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
1
+ {"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`elytra.config.ts` +\n * `elytra init`), not here — EC-470 retired the `builder init` this named,\n * which was never a declared bin.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n elytra init\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
@@ -40,6 +40,27 @@ export interface RedirectsStore {
40
40
  refresh(): Promise<void>;
41
41
  }
42
42
  declare const useRedirects: () => RedirectsStore;
43
+ /**
44
+ * The project's redirects, read through the shared operations cache — the ONE query
45
+ * this rail uses, without needing {@link RedirectsProvider}.
46
+ *
47
+ * Extracted (EC-469) so the section rail's URL-map validation can read redirects from
48
+ * outside the redirects module while sharing this exact query key. One key, one cache
49
+ * entry (AD-17): the rail's foot and the rail itself are the same query, so opening
50
+ * Redirects hits a warm cache, and `save`/`remove`'s `cache.patch` below updates both
51
+ * at once — a fixed loop stops being reported the moment it is fixed.
52
+ */
53
+ export declare function useRedirectList(projectId: string): import("@tanstack/react-query").UseQueryResult<{
54
+ createdAt: string;
55
+ updatedAt: string;
56
+ id: string;
57
+ projectId: string;
58
+ from: string;
59
+ to: string;
60
+ permanent: boolean;
61
+ enabled: boolean;
62
+ meta?: Record<string, unknown> | undefined;
63
+ }[]>;
43
64
  export declare function RedirectsProvider({ projectId, children, }: {
44
65
  projectId: string;
45
66
  children: ReactNode;
@@ -20,14 +20,27 @@ async function loadRedirects(operations, projectId) {
20
20
  throw new Error(result.error.message);
21
21
  return [...result.value];
22
22
  }
23
+ /**
24
+ * The project's redirects, read through the shared operations cache — the ONE query
25
+ * this rail uses, without needing {@link RedirectsProvider}.
26
+ *
27
+ * Extracted (EC-469) so the section rail's URL-map validation can read redirects from
28
+ * outside the redirects module while sharing this exact query key. One key, one cache
29
+ * entry (AD-17): the rail's foot and the rail itself are the same query, so opening
30
+ * Redirects hits a warm cache, and `save`/`remove`'s `cache.patch` below updates both
31
+ * at once — a fixed loop stops being reported the moment it is fixed.
32
+ */
33
+ export function useRedirectList(projectId) {
34
+ return useOperationQuery({
35
+ queryKey: redirectsQueryKey(projectId),
36
+ loader: (operations) => loadRedirects(operations, projectId),
37
+ });
38
+ }
23
39
  function byFrom(a, b) {
24
40
  return a.from < b.from ? -1 : a.from > b.from ? 1 : 0;
25
41
  }
26
42
  export function RedirectsProvider({ projectId, children, }) {
27
- const query = useOperationQuery({
28
- queryKey: redirectsQueryKey(projectId),
29
- loader: (operations) => loadRedirects(operations, projectId),
30
- });
43
+ const query = useRedirectList(projectId);
31
44
  const cache = useOperationCache();
32
45
  const refetch = query.refetch;
33
46
  const persisted = query.data ?? EMPTY_REDIRECTS;