@elytracms/studio 0.0.62 → 0.0.63

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 (78) hide show
  1. package/dist/components/account/credential-panel.js +6 -1
  2. package/dist/components/account/credential-panel.js.map +1 -1
  3. package/dist/components/auth-context.d.ts +14 -0
  4. package/dist/components/auth-context.js +32 -2
  5. package/dist/components/auth-context.js.map +1 -1
  6. package/dist/components/builder/blocks-model.d.ts +7 -0
  7. package/dist/components/builder/blocks-model.js +5 -0
  8. package/dist/components/builder/blocks-model.js.map +1 -1
  9. package/dist/components/builder/blocks-panel.js +2 -2
  10. package/dist/components/builder/blocks-panel.js.map +1 -1
  11. package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
  12. package/dist/components/builder/fixture-thumbnail.js +14 -0
  13. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  14. package/dist/components/cms/cms-index.js +6 -3
  15. package/dist/components/cms/cms-index.js.map +1 -1
  16. package/dist/components/deployment-setup-gate.js +40 -8
  17. package/dist/components/deployment-setup-gate.js.map +1 -1
  18. package/dist/components/operation-error.d.ts +7 -5
  19. package/dist/components/operation-error.js +19 -36
  20. package/dist/components/operation-error.js.map +1 -1
  21. package/dist/components/project-list.js +17 -1
  22. package/dist/components/project-list.js.map +1 -1
  23. package/dist/components/project-switcher.d.ts +10 -0
  24. package/dist/components/project-switcher.js +13 -3
  25. package/dist/components/project-switcher.js.map +1 -1
  26. package/dist/components/session-expired-overlay.d.ts +27 -0
  27. package/dist/components/session-expired-overlay.js +51 -0
  28. package/dist/components/session-expired-overlay.js.map +1 -0
  29. package/dist/components/sign-in-view.d.ts +13 -1
  30. package/dist/components/sign-in-view.js +27 -13
  31. package/dist/components/sign-in-view.js.map +1 -1
  32. package/dist/components/studio-project-screen.js +2 -2
  33. package/dist/components/studio-project-screen.js.map +1 -1
  34. package/dist/components/studio-shell.js +40 -3
  35. package/dist/components/studio-shell.js.map +1 -1
  36. package/dist/customization.d.ts +4 -1
  37. package/dist/customization.js +3 -0
  38. package/dist/customization.js.map +1 -1
  39. package/dist/lib/auth-errors.d.ts +58 -28
  40. package/dist/lib/auth-errors.js +93 -15
  41. package/dist/lib/auth-errors.js.map +1 -1
  42. package/dist/lib/i18n/index.d.ts +1 -0
  43. package/dist/lib/i18n/index.js.map +1 -1
  44. package/dist/lib/i18n/messages.de.d.ts +24 -0
  45. package/dist/lib/i18n/messages.de.js +1019 -0
  46. package/dist/lib/i18n/messages.de.js.map +1 -0
  47. package/dist/lib/i18n/messages.en.d.ts +880 -0
  48. package/dist/lib/i18n/messages.en.js +71 -3
  49. package/dist/lib/i18n/messages.en.js.map +1 -1
  50. package/dist/lib/operations/studio-operations.js +7 -0
  51. package/dist/lib/operations/studio-operations.js.map +1 -1
  52. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  53. package/dist/lib/persistence/deployment-readiness.js +16 -1
  54. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  55. package/dist/lib/session-expiry.d.ts +67 -0
  56. package/dist/lib/session-expiry.js +72 -0
  57. package/dist/lib/session-expiry.js.map +1 -0
  58. package/dist/lib/tab-title.d.ts +53 -0
  59. package/dist/lib/tab-title.js +77 -0
  60. package/dist/lib/tab-title.js.map +1 -0
  61. package/dist/lib/workspace/index.d.ts +1 -1
  62. package/dist/lib/workspace/index.js +1 -1
  63. package/dist/lib/workspace/index.js.map +1 -1
  64. package/dist/lib/workspace/switcher-source.d.ts +27 -0
  65. package/dist/lib/workspace/switcher-source.js +30 -0
  66. package/dist/lib/workspace/switcher-source.js.map +1 -1
  67. package/dist/router/index.d.ts +6 -0
  68. package/dist/router/index.js +6 -0
  69. package/dist/router/index.js.map +1 -1
  70. package/dist/router/launcher-visit.d.ts +13 -0
  71. package/dist/router/launcher-visit.js +34 -0
  72. package/dist/router/launcher-visit.js.map +1 -0
  73. package/dist/router/root.js +12 -2
  74. package/dist/router/root.js.map +1 -1
  75. package/dist/ui/dialog.d.ts +15 -0
  76. package/dist/ui/dialog.js +17 -0
  77. package/dist/ui/dialog.js.map +1 -1
  78. package/package.json +6 -2
@@ -5,7 +5,7 @@ import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router
5
5
  import { useCms } from '@elytracms/studio/components/cms/cms-store';
6
6
  import { focusCollectionList } from './collection-focus.js';
7
7
  import { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui';
8
- import { localizedLabel } from '@elytracms/studio/lib/i18n';
8
+ import { localizedLabel, useT } from '@elytracms/studio/lib/i18n';
9
9
  /**
10
10
  * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when
11
11
  * no collection is selected. Takes `projectId` as a prop; the splat resolver
@@ -14,13 +14,16 @@ import { localizedLabel } from '@elytracms/studio/lib/i18n';
14
14
  */
15
15
  export function CmsIndex({ projectId, focusCollections, }) {
16
16
  const cms = useCms();
17
+ const t = useT();
17
18
  const collections = focusCollectionList(cms.collections, focusCollections);
18
- return (_jsx("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6", children: _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: "Content" }), _jsx(Text, { tone: "muted", children: "Schema-driven collections. Open a collection to browse its documents as a table and edit them in the detail editor." })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: collections.map((collection) => {
19
+ return (_jsx("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6", children: _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: t('cms.index.heading') }), _jsx(Text, { tone: "muted", children: t('cms.index.description') })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: collections.map((collection) => {
19
20
  // EC-217: a singleton reads as one fixed document — a distinct icon
20
21
  // and a "Single document" note instead of a (always-1) row count.
21
22
  const singleton = isSingleton(collection);
22
23
  const Icon = singleton ? FileCog : Database;
23
- return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.cmsCollection(collection.id) }, children: _jsxs(Card, { className: "flex items-center gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-4" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: localizedLabel(collection.form?.label, collection.id) }), _jsxs(Code, { tone: "muted", className: "w-fit", children: [collection.id, " \u00B7 ", collection.fields.length, " fields"] })] }), _jsx(Spacer, {}), singleton ? (_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", children: "Single document" })) : (_jsx(Text, { size: "sm", tone: "muted", mono: true, children: cms.documentsOf(collection.id).length }))] }) }, collection.id));
24
+ return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.cmsCollection(collection.id) }, children: _jsxs(Card, { className: "flex items-center gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-4" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: localizedLabel(collection.form?.label, collection.id) }), _jsxs(Code, { tone: "muted", className: "w-fit", children: [collection.id, " \u00B7", ' ', t(collection.fields.length === 1
25
+ ? 'cms.index.fieldCount.one'
26
+ : 'cms.index.fieldCount.other', { count: collection.fields.length })] })] }), _jsx(Spacer, {}), singleton ? (_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", children: t('cms.index.singleDocument') })) : (_jsx(Text, { size: "sm", tone: "muted", mono: true, children: cms.documentsOf(collection.id).length }))] }) }, collection.id));
24
27
  }) })] }) }));
25
28
  }
26
29
  //# sourceMappingURL=cms-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,SAAS,EACT,gBAAgB,GAIjB;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;IAE1E,OAAO,CACL,cAAK,SAAS,EAAC,qCAAqC,YAClD,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,wBAAkB,EACzC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,oIAGX,IACD,EACR,cAAK,SAAS,EAAC,2BAA2B,YACvC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;wBAC9B,oEAAoE;wBACpE,kEAAkE;wBAClE,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;wBACzC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;wBAC3C,OAAO,CACL,KAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,YAEtE,MAAC,IAAI,IAAC,SAAS,EAAC,uEAAuE,aACrF,eAAM,SAAS,EAAC,mFAAmF,YACjG,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,GAAG,GACtB,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,SAAS,aACnC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC,GACjD,EACP,MAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,aACjC,UAAU,CAAC,EAAE,cAAK,UAAU,CAAC,MAAM,CAAC,MAAM,eACtC,IACD,EACR,KAAC,MAAM,KAAG,EACT,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,mBAAmB,gCAEnD,CACR,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,kBAC9B,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,GACjC,CACR,IACI,IA1BF,UAAU,CAAC,EAAE,CA2Bb,CACR,CAAA;oBACH,CAAC,CAAC,GACE,IACA,GACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Database, FileCog } from 'lucide-react'\nimport { isSingleton } from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { useCms } from '@elytracms/studio/components/cms/cms-store'\nimport { focusCollectionList } from './collection-focus'\nimport { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when\n * no collection is selected. Takes `projectId` as a prop; the splat resolver\n * mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`\n * (EC-239) narrows the grid for a focused CMS module instance.\n */\nexport function CmsIndex({\n projectId,\n focusCollections,\n}: {\n projectId: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n const collections = focusCollectionList(cms.collections, focusCollections)\n\n return (\n <div className=\"mx-auto w-full max-w-3xl p-4 sm:p-6\">\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">Content</Heading>\n <Text tone=\"muted\">\n Schema-driven collections. Open a collection to browse its documents as a table and edit\n them in the detail editor.\n </Text>\n </Stack>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {collections.map((collection) => {\n // EC-217: a singleton reads as one fixed document — a distinct icon\n // and a \"Single document\" note instead of a (always-1) row count.\n const singleton = isSingleton(collection)\n const Icon = singleton ? FileCog : Database\n return (\n <Link\n key={collection.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsCollection(collection.id) }}\n >\n <Card className=\"flex items-center gap-3 p-4 transition-colors hover:border-primary/30\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" />\n </span>\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {localizedLabel(collection.form?.label, collection.id)}\n </Text>\n <Code tone=\"muted\" className=\"w-fit\">\n {collection.id} · {collection.fields.length} fields\n </Code>\n </Stack>\n <Spacer />\n {singleton ? (\n <Text size=\"xs\" tone=\"muted\" className=\"whitespace-nowrap\">\n Single document\n </Text>\n ) : (\n <Text size=\"sm\" tone=\"muted\" mono>\n {cms.documentsOf(collection.id).length}\n </Text>\n )}\n </Card>\n </Link>\n )\n })}\n </div>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Database, FileCog } from 'lucide-react'\nimport { isSingleton } from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { useCms } from '@elytracms/studio/components/cms/cms-store'\nimport { focusCollectionList } from './collection-focus'\nimport { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { localizedLabel, useT } from '@elytracms/studio/lib/i18n'\n\n/**\n * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when\n * no collection is selected. Takes `projectId` as a prop; the splat resolver\n * mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`\n * (EC-239) narrows the grid for a focused CMS module instance.\n */\nexport function CmsIndex({\n projectId,\n focusCollections,\n}: {\n projectId: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n const t = useT()\n const collections = focusCollectionList(cms.collections, focusCollections)\n\n return (\n <div className=\"mx-auto w-full max-w-3xl p-4 sm:p-6\">\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">{t('cms.index.heading')}</Heading>\n <Text tone=\"muted\">{t('cms.index.description')}</Text>\n </Stack>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {collections.map((collection) => {\n // EC-217: a singleton reads as one fixed document — a distinct icon\n // and a \"Single document\" note instead of a (always-1) row count.\n const singleton = isSingleton(collection)\n const Icon = singleton ? FileCog : Database\n return (\n <Link\n key={collection.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsCollection(collection.id) }}\n >\n <Card className=\"flex items-center gap-3 p-4 transition-colors hover:border-primary/30\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" />\n </span>\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {localizedLabel(collection.form?.label, collection.id)}\n </Text>\n <Code tone=\"muted\" className=\"w-fit\">\n {collection.id} ·{' '}\n {t(\n collection.fields.length === 1\n ? 'cms.index.fieldCount.one'\n : 'cms.index.fieldCount.other',\n { count: collection.fields.length },\n )}\n </Code>\n </Stack>\n <Spacer />\n {singleton ? (\n <Text size=\"xs\" tone=\"muted\" className=\"whitespace-nowrap\">\n {t('cms.index.singleDocument')}\n </Text>\n ) : (\n <Text size=\"sm\" tone=\"muted\" mono>\n {cms.documentsOf(collection.id).length}\n </Text>\n )}\n </Card>\n </Link>\n )\n })}\n </div>\n </Stack>\n </div>\n )\n}\n"]}
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui';
4
4
  import { resolveBackendConfig, selectPersistenceAdapter, studioDeploymentReadiness, } from '@elytracms/studio/lib/persistence';
5
- import { getWorkspaceState, resolveDeploymentCollections, resolveDeploymentUrl, } from '@elytracms/studio/lib/workspace';
5
+ import { getWorkspaceState, resolveDeploymentAi, resolveDeploymentCollections, resolveDeploymentUrl, } from '@elytracms/studio/lib/workspace';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
7
  /**
8
8
  * Deployment-setup boot gate (EC-258) — the single readiness surface that
@@ -24,11 +24,18 @@ import { translate } from '@elytracms/studio/lib/i18n';
24
24
  */
25
25
  export function DeploymentSetupGate({ children }) {
26
26
  const config = useMemo(() => resolveBackendConfig(), []);
27
+ // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the
28
+ // AI route exists — its provider-key expectation joins the deployment-env checks.
29
+ const aiRouteConfigured = useMemo(() => {
30
+ const ai = resolveDeploymentAi();
31
+ return Boolean(ai && ai.enabled !== false);
32
+ }, []);
27
33
  const staticReport = useCallback(() => studioDeploymentReadiness({
28
34
  backend: config,
29
35
  workspace: getWorkspaceState(),
30
36
  collections: resolveDeploymentCollections(),
31
- }), [config]);
37
+ aiRouteConfigured,
38
+ }), [config, aiRouteConfigured]);
32
39
  const [state, setState] = useState(() => config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() });
33
40
  const check = useCallback(() => {
34
41
  if (config.kind !== 'convex') {
@@ -44,6 +51,7 @@ export function DeploymentSetupGate({ children }) {
44
51
  workspace: getWorkspaceState(),
45
52
  collections: resolveDeploymentCollections(),
46
53
  reachable,
54
+ aiRouteConfigured,
47
55
  }),
48
56
  });
49
57
  setState({ phase: 'checking' });
@@ -51,7 +59,7 @@ export function DeploymentSetupGate({ children }) {
51
59
  .then((adapter) => adapter.status())
52
60
  .then((status) => resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }))
53
61
  .catch((error) => resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }));
54
- }, [config, staticReport]);
62
+ }, [config, staticReport, aiRouteConfigured]);
55
63
  useEffect(() => {
56
64
  if (config.kind === 'convex')
57
65
  check();
@@ -64,12 +72,15 @@ export function DeploymentSetupGate({ children }) {
64
72
  return _jsx(_Fragment, { children: children });
65
73
  }
66
74
  /**
67
- * The checks that BLOCK boot: a failed deployment- or app-level precondition. The
68
- * per-user bucket ("not a member") is not a boot blocker — it is surfaced post-auth
69
- * ("ask an admin"), and at boot it is `unknown` (pre-auth) anyway.
75
+ * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level
76
+ * precondition. The per-user bucket ("not a member") is not a boot blocker — it is
77
+ * surfaced post-auth ("ask an admin"), and at boot it is `unknown` (pre-auth) anyway.
78
+ * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),
79
+ * so they never block in practice; `warn` findings never block anywhere.
70
80
  */
71
81
  function blockingFailures(report) {
72
- return report.checks.filter((c) => c.status === 'fail' && (c.bucket === 'deployment' || c.bucket === 'app'));
82
+ return report.checks.filter((c) => c.status === 'fail' &&
83
+ (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'));
73
84
  }
74
85
  /** Deterministic boot state while the backend probe is in flight (also SSR output). */
75
86
  function DeploymentChecking() {
@@ -81,7 +92,28 @@ function DeploymentChecking() {
81
92
  */
82
93
  export function DeploymentSetupScreen({ report, onRetry, }) {
83
94
  const failures = report.checks.filter((c) => c.status === 'fail');
84
- return (_jsx(BootScreenFrame, { children: _jsxs(Card, { className: "max-w-2xl border-destructive/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: translate('shell.deploymentSetup.title') }) }), _jsx(CardContent, { children: _jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", tone: "muted", children: translate('shell.deploymentSetup.description') }), _jsx(Stack, { gap: "2", role: "list", "aria-label": translate('shell.deploymentSetup.issuesLabel'), children: failures.map((check) => (_jsx(CheckRow, { check: check }, check.id))) }), onRetry ? (_jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: translate('shell.deploymentSetup.retry') })) : null] }) })] }) }));
95
+ const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env');
96
+ return (_jsx(BootScreenFrame, { children: _jsxs(Card, { className: "max-w-2xl border-destructive/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: translate('shell.deploymentSetup.title') }) }), _jsx(CardContent, { children: _jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", tone: "muted", children: translate('shell.deploymentSetup.description') }), _jsx(Stack, { gap: "2", role: "list", "aria-label": translate('shell.deploymentSetup.issuesLabel'), children: failures.map((check) => (_jsx(CheckRow, { check: check }, check.id))) }), envChecks.length > 0 ? _jsx(ExpectedEnvSection, { checks: envChecks }) : null, onRetry ? (_jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: translate('shell.deploymentSetup.retry') })) : null] }) })] }) }));
97
+ }
98
+ /**
99
+ * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the
100
+ * studio and CLI tell one story. Presence only: each row is a var name + set/missing/
101
+ * unknown status text; values never exist in the report. In the browser the presence
102
+ * sources aren't readable pre-auth, so rows typically read `unknown` and point at
103
+ * `elytra doctor` — which is exactly the honest answer.
104
+ */
105
+ function ExpectedEnvSection({ checks }) {
106
+ return (_jsxs(Stack, { gap: "2", children: [_jsx(Text, { size: "sm", className: "font-medium", children: translate('shell.deploymentSetup.envSection') }), _jsx(Text, { size: "xs", tone: "muted", children: translate('shell.deploymentSetup.envSectionHint') }), _jsx(Stack, { gap: "1", role: "list", "aria-label": translate('shell.deploymentSetup.envSection'), children: checks.map((check) => (_jsx(EnvCheckRow, { check: check }, check.id))) })] }));
107
+ }
108
+ const ENV_STATUS_GLYPH = {
109
+ ok: '✓',
110
+ warn: '!',
111
+ fail: '✗',
112
+ unknown: '?',
113
+ };
114
+ function EnvCheckRow({ check }) {
115
+ const failing = check.status === 'fail';
116
+ return (_jsxs("div", { role: "listitem", className: `rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`, children: [_jsxs(Text, { size: "xs", mono: true, className: failing ? 'text-destructive' : undefined, children: [ENV_STATUS_GLYPH[check.status], " [", check.id, "]"] }), _jsx(Text, { size: "xs", tone: "muted", children: check.detail }), check.fix && (check.status === 'fail' || check.status === 'warn') ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
85
117
  }
86
118
  function CheckRow({ check }) {
87
119
  return (_jsxs("div", { role: "listitem", className: "rounded-md border border-destructive/30 bg-destructive/5 p-2", children: [_jsxs(Text, { size: "xs", mono: true, className: "text-destructive", children: ["[", check.id, "]"] }), _jsx(Text, { size: "sm", children: check.detail }), check.fix ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n }),\n [config],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment- or app-level precondition. The\n * per-user bucket (\"not a member\") is not a boot blocker — it is surfaced post-auth\n * (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) => c.status === 'fail' && (c.bucket === 'deployment' || c.bucket === 'app'),\n )\n}\n\n/** Deterministic boot state while the backend probe is in flight (also SSR output). */\nfunction DeploymentChecking() {\n return (\n <BootScreenFrame>\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.checking')}\n </Text>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
1
+ {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/** Deterministic boot state while the backend probe is in flight (also SSR output). */\nfunction DeploymentChecking() {\n return (\n <BootScreenFrame>\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.checking')}\n </Text>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
@@ -5,13 +5,15 @@ import type { OperationError } from '@elytracms/studio/lib/operations';
5
5
  * `OperationError` — including its validation issues — is always *visible*,
6
6
  * never a silent failure.
7
7
  *
8
- * Session expiry (EC-177 gap 9) is special-cased: the backend's raw
9
- * unauthenticated blob is recognized at the lib chokepoint
10
- * (`isSessionExpiredError`) and rendered as plain language with a
11
- * "Sign in again" action instead of the verbatim transport message.
8
+ * Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:
9
+ * an expired session is an app-level state, so the banner funnels the error
10
+ * into the global session-expiry chokepoint (which raises the blocking
11
+ * in-place re-auth overlay) and renders nothing itself — the per-surface
12
+ * banner path remains only for non-expiry failures. The raw OIDC blob is
13
+ * never shown either way.
12
14
  */
13
15
  export declare function OperationErrorBanner({ error, onDismiss, className, }: {
14
16
  error: OperationError;
15
17
  onDismiss?: () => void;
16
18
  className?: string;
17
- }): import("react").JSX.Element;
19
+ }): import("react").JSX.Element | null;
@@ -1,55 +1,38 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { CircleAlert, LogIn, X } from 'lucide-react';
3
- import { SESSION_EXPIRED_MESSAGE, SIGN_IN_AGAIN_LABEL, isSessionExpiredError, signInHref, } from '@elytracms/studio/lib/auth-errors';
2
+ import { useEffect } from 'react';
3
+ import { CircleAlert, X } from 'lucide-react';
4
+ import { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors';
5
+ import { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry';
4
6
  import { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui';
5
7
  import { operationErrorLabel } from '@elytracms/studio/lib/persona/labels';
6
8
  import { translate } from '@elytracms/studio/lib/i18n';
7
- import { useOptionalAuth } from './auth-context.js';
8
9
  /**
9
10
  * Shared error surface for failed/denied operations (EC-141). Every studio
10
11
  * surface renders rejected commands through this banner so a structured
11
12
  * `OperationError` — including its validation issues — is always *visible*,
12
13
  * never a silent failure.
13
14
  *
14
- * Session expiry (EC-177 gap 9) is special-cased: the backend's raw
15
- * unauthenticated blob is recognized at the lib chokepoint
16
- * (`isSessionExpiredError`) and rendered as plain language with a
17
- * "Sign in again" action instead of the verbatim transport message.
15
+ * Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:
16
+ * an expired session is an app-level state, so the banner funnels the error
17
+ * into the global session-expiry chokepoint (which raises the blocking
18
+ * in-place re-auth overlay) and renders nothing itself — the per-surface
19
+ * banner path remains only for non-expiry failures. The raw OIDC blob is
20
+ * never shown either way.
18
21
  */
19
22
  export function OperationErrorBanner({ error, onDismiss, className, }) {
20
- if (isSessionExpiredError(error)) {
21
- return _jsx(SessionExpiredBanner, { onDismiss: onDismiss, className: className });
22
- }
23
+ // Funnel every error into the EC-403 chokepoint (belt-and-braces for a
24
+ // failure that reached this surface without passing the operations event
25
+ // bus). Deduped per error instance there, so re-rendering a stale
26
+ // persistError after re-auth never re-raises the overlay.
27
+ useEffect(() => {
28
+ reportSessionExpiredError(error);
29
+ }, [error]);
30
+ if (isSessionExpiredError(error))
31
+ return null;
23
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",
24
33
  // The raw graph path (`pages.0.root.slots…`) is structural
25
34
  // machine addressing — kept inspectable on hover, never shown
26
35
  // as visible copy to a non-technical editor (EC-192).
27
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] }) }));
28
37
  }
29
- /**
30
- * The session-expired rendering of the banner: plain-language copy plus a
31
- * "Sign in again" action that clears the stale session and routes to the
32
- * sign-in screen, preserving the current location so a successful sign-in
33
- * returns the editor to this exact screen.
34
- */
35
- function SessionExpiredBanner({ onDismiss, className, }) {
36
- const auth = useOptionalAuth();
37
- async function handleSignInAgain() {
38
- if (typeof window === 'undefined')
39
- return;
40
- const href = signInHref(window.location);
41
- // Clear the stale (expired) session first: the route guard sends
42
- // "signed-in" sessions away from /sign-in, so navigating with the dead
43
- // session still bound would bounce straight back.
44
- try {
45
- await auth?.signOut();
46
- }
47
- catch {
48
- // Even a failed sign-out must not strand the editor — proceed to the
49
- // sign-in screen; the backend session is invalid either way.
50
- }
51
- window.location.assign(href);
52
- }
53
- 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" }), _jsx(Stack, { gap: "1", className: "min-w-0 flex-1", children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "error", children: translate('error.signedOut') }), _jsx(Text, { size: "sm", className: "text-destructive", children: SESSION_EXPIRED_MESSAGE })] }) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => void handleSignInAgain(), className: "shrink-0", children: [_jsx(LogIn, {}), " ", SIGN_IN_AGAIN_LABEL] }), onDismiss ? (_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('error.dismiss'), onClick: onDismiss, className: "shrink-0", children: _jsx(X, {}) })) : null] }) }));
54
- }
55
38
  //# sourceMappingURL=operation-error.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"operation-error.js","sourceRoot":"","sources":["../../src/components/operation-error.tsx"],"names":[],"mappings":"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","sourcesContent":["import { CircleAlert, LogIn, X } from 'lucide-react'\nimport type { OperationError } from '@elytracms/studio/lib/operations'\nimport {\n SESSION_EXPIRED_MESSAGE,\n SIGN_IN_AGAIN_LABEL,\n isSessionExpiredError,\n signInHref,\n} from '@elytracms/studio/lib/auth-errors'\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'\nimport { useOptionalAuth } from './auth-context'\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) is special-cased: the backend's raw\n * unauthenticated blob is recognized at the lib chokepoint\n * (`isSessionExpiredError`) and rendered as plain language with a\n * \"Sign in again\" action instead of the verbatim transport message.\n */\nexport function OperationErrorBanner({\n error,\n onDismiss,\n className,\n}: {\n error: OperationError\n onDismiss?: () => void\n className?: string\n}) {\n if (isSessionExpiredError(error)) {\n return <SessionExpiredBanner onDismiss={onDismiss} className={className} />\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\n/**\n * The session-expired rendering of the banner: plain-language copy plus a\n * \"Sign in again\" action that clears the stale session and routes to the\n * sign-in screen, preserving the current location so a successful sign-in\n * returns the editor to this exact screen.\n */\nfunction SessionExpiredBanner({\n onDismiss,\n className,\n}: {\n onDismiss?: () => void\n className?: string\n}) {\n const auth = useOptionalAuth()\n\n async function handleSignInAgain() {\n if (typeof window === 'undefined') return\n const href = signInHref(window.location)\n // Clear the stale (expired) session first: the route guard sends\n // \"signed-in\" sessions away from /sign-in, so navigating with the dead\n // session still bound would bounce straight back.\n try {\n await auth?.signOut()\n } catch {\n // Even a failed sign-out must not strand the editor — proceed to the\n // sign-in screen; the backend session is invalid either way.\n }\n window.location.assign(href)\n }\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\">{translate('error.signedOut')}</Badge>\n <Text size=\"sm\" className=\"text-destructive\">\n {SESSION_EXPIRED_MESSAGE}\n </Text>\n </Row>\n </Stack>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void handleSignInAgain()}\n className=\"shrink-0\"\n >\n <LogIn /> {SIGN_IN_AGAIN_LABEL}\n </Button>\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,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,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
2
+ import { isExplicitLauncherVisit, Link, Navigate, STUDIO_PROJECT_ROUTE, studioPath, useLocation, } from '@elytracms/studio/router';
3
3
  import { Globe, LogIn, TerminalSquare } from 'lucide-react';
4
4
  import { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { hasProjectSession } from '@elytracms/studio/lib/auth';
6
+ import { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace';
6
7
  import { translate } from '@elytracms/studio/lib/i18n';
7
8
  import { PlaygroundBadge } from './playground-badge.js';
8
9
  import { SignInNeededBadge } from './sign-in-needed-badge.js';
@@ -24,7 +25,22 @@ export function ProjectLauncher({ forcedState }) {
24
25
  const hasConfig = workspace.kind === 'config';
25
26
  const visible = entries;
26
27
  const recents = useRecentProjects(entries);
28
+ const location = useLocation();
27
29
  const forced = forcedState && forcedState !== 'working' ? forcedState : null;
30
+ // EC-401: a single-project workspace skips the launcher hop — the bare `/`
31
+ // hit forwards straight into the sole project (replace, so Back never loops
32
+ // through the launcher). Explicit "all projects" navigations carry the
33
+ // history-state marker and keep the launcher visible, as do forced `?state=`
34
+ // verification states and a lone UNPROVISIONED entry (its setup gate lives
35
+ // here). Activation/recents are recorded by the project screen on arrival.
36
+ const autoForward = launcherAutoForwardTarget({
37
+ entries,
38
+ forcedState: forcedState != null,
39
+ explicitVisit: isExplicitLauncherVisit(location.state),
40
+ });
41
+ if (autoForward) {
42
+ return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: { projectId: autoForward.id, _splat: studioPath.index() }, replace: true }));
43
+ }
28
44
  return (_jsxs("div", { className: "mx-auto w-full max-w-6xl px-6 py-8", children: [_jsxs(Row, { gap: "3", className: "mb-6", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { size: "display", children: translate('shell.projects') }), _jsx(PlaygroundBadge, {}), hasConfig ? (_jsxs(Badge, { variant: "outline", children: [_jsx(TerminalSquare, { className: "size-3" }), " ", workspace.source] })) : null] }), _jsx(Text, { tone: "muted", children: hasConfig
29
45
  ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
30
46
  : '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
@@ -1 +1 @@
1
- {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } 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 { 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\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\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":"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","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n Navigate,\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 return (\n <Navigate\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"]}
@@ -9,3 +9,13 @@ import type { Project } from '@elytracms/core/studio-core';
9
9
  export declare function ProjectSwitcher({ activeProject }: {
10
10
  activeProject: Project;
11
11
  }): import("react").JSX.Element;
12
+ /**
13
+ * The switcher slot's single-project face (EC-401): the active project's name
14
+ * as a plain heading — same identity mark as the switcher trigger, but no
15
+ * button, no chevron, no dropdown. `StudioShell` renders this instead of
16
+ * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the
17
+ * switcher returns the moment a second workspace entry exists.
18
+ */
19
+ export declare function ActiveProjectName({ project }: {
20
+ project: Project;
21
+ }): import("react").JSX.Element;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
2
+ import { launcherVisitState, STUDIO_PROJECT_ROUTE, studioPath, useNavigate, } from '@elytracms/studio/router';
3
3
  import { Check, ChevronsUpDown, FolderOpen } from 'lucide-react';
4
4
  import { hasProjectSession } from '@elytracms/studio/lib/auth';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
6
+ import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Heading, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
7
7
  import { useStudio } from './studio-context.js';
8
8
  import { useRecentProjects, useSwitcherEntries } from './workspace-projects.js';
9
9
  import { SignInNeededBadge } from './sign-in-needed-badge.js';
@@ -24,7 +24,17 @@ export function ProjectSwitcher({ activeProject }) {
24
24
  studio.activate(project.id);
25
25
  navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } });
26
26
  }
27
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/' }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
27
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/', state: launcherVisitState }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
28
+ }
29
+ /**
30
+ * The switcher slot's single-project face (EC-401): the active project's name
31
+ * as a plain heading — same identity mark as the switcher trigger, but no
32
+ * button, no chevron, no dropdown. `StudioShell` renders this instead of
33
+ * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the
34
+ * switcher returns the moment a second workspace entry exists.
35
+ */
36
+ export function ActiveProjectName({ project }) {
37
+ return (_jsxs(Heading, { level: 1, size: "h4", className: "flex h-8 min-w-0 items-center gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: project.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate", children: project.name })] }));
28
38
  }
29
39
  function ProjectMenuItem({ project, active, onSelect, }) {
30
40
  // Per-backend sign-in state (EC-179): the active project is signed-in by