@elytracms/studio 0.0.62 → 0.0.64

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 (110) 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/canvas-chrome-css.d.ts +54 -0
  12. package/dist/components/builder/canvas-chrome-css.js +164 -0
  13. package/dist/components/builder/canvas-chrome-css.js.map +1 -0
  14. package/dist/components/builder/canvas-chrome.d.ts +122 -18
  15. package/dist/components/builder/canvas-chrome.js +148 -82
  16. package/dist/components/builder/canvas-chrome.js.map +1 -1
  17. package/dist/components/builder/canvas-render-context.d.ts +13 -0
  18. package/dist/components/builder/canvas-render-context.js +7 -5
  19. package/dist/components/builder/canvas-render-context.js.map +1 -1
  20. package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
  21. package/dist/components/builder/canvas-stylesheet.js +16 -9
  22. package/dist/components/builder/canvas-stylesheet.js.map +1 -1
  23. package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
  24. package/dist/components/builder/canvas-theme-notice.js +46 -0
  25. package/dist/components/builder/canvas-theme-notice.js.map +1 -0
  26. package/dist/components/builder/canvas.js +17 -10
  27. package/dist/components/builder/canvas.js.map +1 -1
  28. package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
  29. package/dist/components/builder/fixture-thumbnail.js +14 -0
  30. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  31. package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
  32. package/dist/components/builder/rich-text-toolbar.js +24 -17
  33. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  34. package/dist/components/builder/spacing-handles.d.ts +31 -0
  35. package/dist/components/builder/spacing-handles.js +49 -37
  36. package/dist/components/builder/spacing-handles.js.map +1 -1
  37. package/dist/components/cms/cms-index.js +6 -3
  38. package/dist/components/cms/cms-index.js.map +1 -1
  39. package/dist/components/components/component-preview.d.ts +2 -1
  40. package/dist/components/components/component-preview.js +24 -25
  41. package/dist/components/components/component-preview.js.map +1 -1
  42. package/dist/components/deployment-setup-gate.js +40 -8
  43. package/dist/components/deployment-setup-gate.js.map +1 -1
  44. package/dist/components/operation-error.d.ts +7 -5
  45. package/dist/components/operation-error.js +19 -36
  46. package/dist/components/operation-error.js.map +1 -1
  47. package/dist/components/project-list.js +17 -1
  48. package/dist/components/project-list.js.map +1 -1
  49. package/dist/components/project-switcher.d.ts +10 -0
  50. package/dist/components/project-switcher.js +13 -3
  51. package/dist/components/project-switcher.js.map +1 -1
  52. package/dist/components/session-expired-overlay.d.ts +27 -0
  53. package/dist/components/session-expired-overlay.js +51 -0
  54. package/dist/components/session-expired-overlay.js.map +1 -0
  55. package/dist/components/sign-in-view.d.ts +13 -1
  56. package/dist/components/sign-in-view.js +27 -13
  57. package/dist/components/sign-in-view.js.map +1 -1
  58. package/dist/components/studio-project-screen.js +2 -2
  59. package/dist/components/studio-project-screen.js.map +1 -1
  60. package/dist/components/studio-shell.js +40 -3
  61. package/dist/components/studio-shell.js.map +1 -1
  62. package/dist/customization.d.ts +4 -1
  63. package/dist/customization.js +3 -0
  64. package/dist/customization.js.map +1 -1
  65. package/dist/lib/auth-errors.d.ts +58 -28
  66. package/dist/lib/auth-errors.js +93 -15
  67. package/dist/lib/auth-errors.js.map +1 -1
  68. package/dist/lib/i18n/index.d.ts +1 -0
  69. package/dist/lib/i18n/index.js.map +1 -1
  70. package/dist/lib/i18n/messages.de.d.ts +24 -0
  71. package/dist/lib/i18n/messages.de.js +1023 -0
  72. package/dist/lib/i18n/messages.de.js.map +1 -0
  73. package/dist/lib/i18n/messages.en.d.ts +884 -0
  74. package/dist/lib/i18n/messages.en.js +76 -3
  75. package/dist/lib/i18n/messages.en.js.map +1 -1
  76. package/dist/lib/operations/studio-operations.js +7 -0
  77. package/dist/lib/operations/studio-operations.js.map +1 -1
  78. package/dist/lib/persistence/backend-mode.d.ts +7 -0
  79. package/dist/lib/persistence/backend-mode.js +9 -0
  80. package/dist/lib/persistence/backend-mode.js.map +1 -1
  81. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  82. package/dist/lib/persistence/deployment-readiness.js +16 -1
  83. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  84. package/dist/lib/persistence/index.d.ts +1 -1
  85. package/dist/lib/persistence/index.js +1 -1
  86. package/dist/lib/persistence/index.js.map +1 -1
  87. package/dist/lib/session-expiry.d.ts +67 -0
  88. package/dist/lib/session-expiry.js +72 -0
  89. package/dist/lib/session-expiry.js.map +1 -0
  90. package/dist/lib/tab-title.d.ts +53 -0
  91. package/dist/lib/tab-title.js +77 -0
  92. package/dist/lib/tab-title.js.map +1 -0
  93. package/dist/lib/workspace/index.d.ts +1 -1
  94. package/dist/lib/workspace/index.js +1 -1
  95. package/dist/lib/workspace/index.js.map +1 -1
  96. package/dist/lib/workspace/switcher-source.d.ts +27 -0
  97. package/dist/lib/workspace/switcher-source.js +30 -0
  98. package/dist/lib/workspace/switcher-source.js.map +1 -1
  99. package/dist/router/index.d.ts +6 -0
  100. package/dist/router/index.js +6 -0
  101. package/dist/router/index.js.map +1 -1
  102. package/dist/router/launcher-visit.d.ts +13 -0
  103. package/dist/router/launcher-visit.js +34 -0
  104. package/dist/router/launcher-visit.js.map +1 -0
  105. package/dist/router/root.js +12 -2
  106. package/dist/router/root.js.map +1 -1
  107. package/dist/ui/dialog.d.ts +15 -0
  108. package/dist/ui/dialog.js +17 -0
  109. package/dist/ui/dialog.js.map +1 -1
  110. package/package.json +6 -2
@@ -4,9 +4,15 @@ import { useNavigate, useSearch } from '@tanstack/react-router';
4
4
  import { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { useAuth } from '@elytracms/studio/components/auth-context';
6
6
  import { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth';
7
+ import { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors';
8
+ import { useT } from '@elytracms/studio/lib/i18n';
7
9
  import { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace';
8
- /** The project whose backend this sign-in authenticates against (EC-179). */
9
- function activeProjectName() {
10
+ /**
11
+ * The project whose backend this sign-in authenticates against (EC-179).
12
+ * Exported for the session-expired re-auth overlay (EC-403), which mounts
13
+ * {@link SignInScreen} with the same subtext.
14
+ */
15
+ export function activeProjectName() {
10
16
  const state = getWorkspaceState();
11
17
  if (state.kind !== 'config')
12
18
  return null;
@@ -32,7 +38,8 @@ export function SignInView({ brandMark, productName }) {
32
38
  * Provider-free presentational sign-in screen. Owns only local form state and
33
39
  * calls injected callbacks — no context hooks — so it static-renders in tests.
34
40
  */
35
- export function SignInScreen({ projectName, showModeToggle, devCredentials, brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
41
+ export function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
42
+ const t = useT();
36
43
  const [email, setEmail] = useState('');
37
44
  const [password, setPassword] = useState('');
38
45
  const [phase, setPhase] = useState('idle');
@@ -50,26 +57,33 @@ export function SignInScreen({ projectName, showModeToggle, devCredentials, bran
50
57
  // A fresh backend has no account yet: nudge sign-in → create, and vice versa.
51
58
  if (result.reason === 'account-exists')
52
59
  setMode('signin');
53
- setError(result.message);
60
+ // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize
61
+ // through the message catalog; an unrecognized shape renders the adapter's
62
+ // raw English message — honest, never a stale mapping.
63
+ const errorKey = signInErrorMessageKey(result);
64
+ setError(errorKey ? t(errorKey) : result.message);
54
65
  setPhase('idle');
55
66
  }
56
67
  const submitting = phase === 'submitting';
57
68
  const creating = mode === 'signup';
58
- return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Stack, { gap: "4", className: "w-full max-w-sm", children: [_jsxs(Stack, { gap: "1", align: "center", children: [brandMark ?? _jsx("span", { className: "size-8 rounded-lg bg-primary shadow-sm", "aria-hidden": true }), _jsx(Heading, { size: "h2", children: creating ? 'Create your account' : `Sign in to ${productName}` }), _jsx(Text, { size: "sm", tone: "muted", children: projectName
59
- ? `${creating ? 'Register on' : 'Continue to'} ${projectName}.`
60
- : 'Use your studio account to continue.' })] }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-5", children: _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "4", children: [_jsx(Field, { label: "Email", htmlFor: "sign-in-email", required: true, children: _jsx(Input, { id: "sign-in-email", type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "you@example.com", disabled: submitting }) }), _jsx(Field, { label: "Password", htmlFor: "sign-in-password", required: true, error: error, children: _jsx(Input, { id: "sign-in-password", type: "password", autoComplete: creating ? 'new-password' : 'current-password', value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting }) }), _jsx(Button, { type: "submit", disabled: submitting, className: "w-full", children: submitting
69
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Stack, { gap: "4", className: "w-full max-w-sm", children: [notice, _jsxs(Stack, { gap: "1", align: "center", children: [brandMark ?? _jsx("span", { className: "size-8 rounded-lg bg-primary shadow-sm", "aria-hidden": true }), _jsx(Heading, { size: "h2", children: creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName }) }), _jsx(Text, { size: "sm", tone: "muted", children: projectName
70
+ ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {
71
+ project: projectName,
72
+ })
73
+ : t('auth.signIn.subtext.generic') })] }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-5", children: _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "4", children: [_jsx(Field, { label: t('auth.signIn.emailLabel'), htmlFor: "sign-in-email", required: true, children: _jsx(Input, { id: "sign-in-email", type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: t('auth.signIn.emailPlaceholder'), disabled: submitting }) }), _jsx(Field, { label: t('auth.signIn.passwordLabel'), htmlFor: "sign-in-password", required: true, error: error, children: _jsx(Input, { id: "sign-in-password", type: "password", autoComplete: creating ? 'new-password' : 'current-password', value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting }) }), _jsx(Button, { type: "submit", disabled: submitting, className: "w-full", children: submitting
61
74
  ? creating
62
- ? 'Creating account…'
63
- : 'Signing in…'
75
+ ? t('auth.signIn.submittingCreate')
76
+ : t('auth.signIn.submitting')
64
77
  : creating
65
- ? 'Create account'
66
- : 'Sign in' })] }) }) }) }), showModeToggle ? (_jsxs(Text, { size: "sm", tone: "muted", className: "text-center", children: [creating ? 'Already have an account on this backend? ' : 'First time on this backend? ', _jsx("button", { type: "button", className: "font-medium text-primary underline-offset-2 hover:underline", onClick: () => {
78
+ ? t('auth.signIn.submitCreate')
79
+ : t('auth.signIn.submit') })] }) }) }) }), showModeToggle ? (_jsxs(Text, { size: "sm", tone: "muted", className: "text-center", children: [creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime'), ' ', _jsx("button", { type: "button", className: "font-medium text-primary underline-offset-2 hover:underline", onClick: () => {
67
80
  setMode(creating ? 'signin' : 'signup');
68
81
  setError(null);
69
- }, children: creating ? 'Sign in' : 'Create an account' })] })) : null, devCredentials ? _jsx(DevCredentialsHint, { entries: devCredentials }) : null] }) }));
82
+ }, children: creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount') })] })) : null, devCredentials ? _jsx(DevCredentialsHint, { entries: devCredentials }) : null] }) }));
70
83
  }
71
84
  /** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */
72
85
  function DevCredentialsHint({ entries }) {
73
- return (_jsx(Card, { className: "border-dashed", children: _jsx(CardContent, { className: "pt-4", children: _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", tone: "muted", className: "font-medium", children: "Playground mode \u2014 seeded dev accounts" }), entries.map((entry) => (_jsxs(Text, { size: "xs", tone: "muted", className: "font-mono", children: [entry.user.email, " / ", entry.password] }, entry.user.id)))] }) }) }));
86
+ const t = useT();
87
+ return (_jsx(Card, { className: "border-dashed", children: _jsx(CardContent, { className: "pt-4", children: _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", tone: "muted", className: "font-medium", children: t('auth.signIn.devAccounts.title') }), entries.map((entry) => (_jsxs(Text, { size: "xs", tone: "muted", className: "font-mono", children: [entry.user.email, " / ", entry.password] }, entry.user.id)))] }) }) }));
74
88
  }
75
89
  //# sourceMappingURL=sign-in-view.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACpG,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAA;AACnE,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE9F,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAqCzF,6EAA6E;AAC7E,SAAS,iBAAiB;IACxB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACxC,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAA;IACnC,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC1F,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAA;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,SAAS,EAAE,WAAW,EAAmB;IACpE,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,iFAAiF;IACjF,2EAA2E;IAC3E,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAA0B,CAAA;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAEhC,OAAO,CACL,KAAC,YAAY,IACX,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,QAAQ,EAAE,KAAK,QAAQ,EACvC,cAAc,EAAE,QAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,EAC1E,SAAS,EAAE,SAAS,EACpB,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CACtC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EAEjF,eAAe,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC,GACnE,CACH,CAAA;AACH,CAAC;AAgBD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,WAAW,EACX,cAAc,EACd,cAAc,EACd,SAAS,EACT,WAAW,GAAG,QAAQ,EACtB,YAAY,EACZ,eAAe,GACG;IAClB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAY,MAAM,CAAC,CAAA;IACrD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAW,QAAQ,CAAC,CAAA;IACpD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAEvD,KAAK,UAAU,YAAY,CAAC,KAAiC;QAC3D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,QAAQ,CAAC,YAAY,CAAC,CAAA;QACtB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAA;QACxD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,MAAM,eAAe,EAAE,CAAA;YACvB,OAAM;QACR,CAAC;QACD,8EAA8E;QAC9E,IAAI,MAAM,CAAC,MAAM,KAAK,gBAAgB;YAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;QACzD,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,KAAK,YAAY,CAAA;IACzC,MAAM,QAAQ,GAAG,IAAI,KAAK,QAAQ,CAAA;IAElC,OAAO,CACL,cAAK,SAAS,EAAC,qDAAqD,YAClE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,iBAAiB,aACxC,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aAC1B,SAAS,IAAI,eAAM,SAAS,EAAC,wCAAwC,wBAAe,EACrF,KAAC,OAAO,IAAC,IAAI,EAAC,IAAI,YACf,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,cAAc,WAAW,EAAE,GACvD,EACV,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,WAAW;gCACV,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,IAAI,WAAW,GAAG;gCAC/D,CAAC,CAAC,sCAAsC,GACrC,IACD,EAER,KAAC,IAAI,cACH,KAAC,WAAW,IAAC,SAAS,EAAC,MAAM,YAC3B,eAAM,QAAQ,EAAE,YAAY,EAAE,UAAU,kBACtC,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,KAAK,IAAC,KAAK,EAAC,OAAO,EAAC,OAAO,EAAC,eAAe,EAAC,QAAQ,kBACnD,KAAC,KAAK,IACJ,EAAE,EAAC,eAAe,EAClB,IAAI,EAAC,OAAO,EACZ,YAAY,EAAC,OAAO,EACpB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAC,iBAAiB,EAC7B,QAAQ,EAAE,UAAU,GACpB,GACI,EACR,KAAC,KAAK,IAAC,KAAK,EAAC,UAAU,EAAC,OAAO,EAAC,kBAAkB,EAAC,QAAQ,QAAC,KAAK,EAAE,KAAK,YACtE,KAAC,KAAK,IACJ,EAAE,EAAC,kBAAkB,EACrB,IAAI,EAAC,UAAU,EACf,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB,EAC5D,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5C,QAAQ,EAAE,UAAU,GACpB,GACI,EACR,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAC,QAAQ,YAC3D,UAAU;4CACT,CAAC,CAAC,QAAQ;gDACR,CAAC,CAAC,mBAAmB;gDACrB,CAAC,CAAC,aAAa;4CACjB,CAAC,CAAC,QAAQ;gDACR,CAAC,CAAC,gBAAgB;gDAClB,CAAC,CAAC,SAAS,GACR,IACH,GACH,GACK,GACT,EAEN,cAAc,CAAC,CAAC,CAAC,CAChB,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,aAAa,aACjD,QAAQ,CAAC,CAAC,CAAC,2CAA2C,CAAC,CAAC,CAAC,8BAA8B,EACxF,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,6DAA6D,EACvE,OAAO,EAAE,GAAG,EAAE;gCACZ,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;gCACvC,QAAQ,CAAC,IAAI,CAAC,CAAA;4BAChB,CAAC,YAEA,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB,GACpC,IACJ,CACR,CAAC,CAAC,CAAC,IAAI,EAEP,cAAc,CAAC,CAAC,CAAC,KAAC,kBAAkB,IAAC,OAAO,EAAE,cAAc,GAAI,CAAC,CAAC,CAAC,IAAI,IAClE,GACJ,CACP,CAAA;AACH,CAAC;AAED,oFAAoF;AACpF,SAAS,kBAAkB,CAAC,EAAE,OAAO,EAAyC;IAC5E,OAAO,CACL,KAAC,IAAI,IAAC,SAAS,EAAC,eAAe,YAC7B,KAAC,WAAW,IAAC,SAAS,EAAC,MAAM,YAC3B,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,aAAa,2DAE7C,EACN,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACtB,MAAC,IAAI,IAAqB,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,WAAW,aACnE,KAAK,CAAC,IAAI,CAAC,KAAK,SAAK,KAAK,CAAC,QAAQ,KAD3B,KAAK,CAAC,IAAI,CAAC,EAAE,CAEjB,CACR,CAAC,IACI,GACI,GACT,CACR,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/** The project whose backend this sign-in authenticates against (EC-179). */\nfunction activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n setError(result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? 'Create your account' : `Sign in to ${productName}`}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? `${creating ? 'Register on' : 'Continue to'} ${projectName}.`\n : 'Use your studio account to continue.'}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label=\"Email\" htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@example.com\"\n disabled={submitting}\n />\n </Field>\n <Field label=\"Password\" htmlFor=\"sign-in-password\" required error={error}>\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? 'Creating account…'\n : 'Signing in…'\n : creating\n ? 'Create account'\n : 'Sign in'}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? 'Already have an account on this backend? ' : 'First time on this backend? '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? 'Sign in' : 'Create an account'}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </div>\n )\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n Playground mode — seeded dev accounts\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n *\n * All copy resolves through the studio message catalog (`auth.signIn.*`,\n * EC-397), including the engine-classified auth failures — see\n * `signInErrorMessageKey`; an unrecognized failure renders `result.message`\n * raw (honest fallback).\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/**\n * The project whose backend this sign-in authenticates against (EC-179).\n * Exported for the session-expired re-auth overlay (EC-403), which mounts\n * {@link SignInScreen} with the same subtext.\n */\nexport function activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n /**\n * Contextual notice rendered above the heading — the session-expired\n * re-auth overlay (EC-403) explains WHY the screen appeared here. The\n * normal `/sign-in` route passes none.\n */\n notice?: ReactNode\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n notice,\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const t = useT()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize\n // through the message catalog; an unrecognized shape renders the adapter's\n // raw English message — honest, never a stale mapping.\n const errorKey = signInErrorMessageKey(result)\n setError(errorKey ? t(errorKey) : result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n {notice}\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName })}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {\n project: projectName,\n })\n : t('auth.signIn.subtext.generic')}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label={t('auth.signIn.emailLabel')} htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t('auth.signIn.emailPlaceholder')}\n disabled={submitting}\n />\n </Field>\n <Field\n label={t('auth.signIn.passwordLabel')}\n htmlFor=\"sign-in-password\"\n required\n error={error}\n >\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? t('auth.signIn.submittingCreate')\n : t('auth.signIn.submitting')\n : creating\n ? t('auth.signIn.submitCreate')\n : t('auth.signIn.submit')}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime')}{' '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount')}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </div>\n )\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n const t = useT()\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n {t('auth.signIn.devAccounts.title')}\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
- import { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router';
3
+ import { launcherVisitState, Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch, } from '@elytracms/studio/router';
4
4
  import { matchStudioRoute } from '@elytracms/studio/router/match';
5
5
  import { studioSearch } from '@elytracms/studio/router/search';
6
6
  import { Button, Heading, Stack, Text } from '@elytracms/studio/ui';
@@ -93,7 +93,7 @@ function StudioSectionResolver({ project, match, search, }) {
93
93
  return _jsx(_Fragment, { children: view.render({ project, projectId, deepLink, search }) });
94
94
  }
95
95
  function ProjectNotFound() {
96
- return (_jsx("div", { className: "grid min-h-dvh place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { size: "display", children: translate('shell.projectNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.projectNotFound.body') }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/", children: translate('shell.backToProjects') }) })] }) }));
96
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { size: "display", children: translate('shell.projectNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.projectNotFound.body') }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/", state: launcherVisitState, children: translate('shell.backToProjects') }) })] }) }));
97
97
  }
98
98
  function StudioRouteNotFound() {
99
99
  return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Stack, { gap: "2", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { level: 2, size: "h2", children: translate('shell.routeNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.routeNotFound.body') })] }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { ProjectSessionProvider } from './project-session'\nimport { flatPath } from './app-views/module'\nimport { landingViewId, useStudioNav } from './app-views/sidebar'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from\n * the section rail (EC-237/239). The bare project URL redirects to the first\n * navigable rail view; otherwise the matched view id is resolved from the registry\n * (rail views + always-on built-ins) and its `parsePath` interprets the trailing\n * segments into a deep-link the view renders. An unknown view id or an unparseable\n * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const nav = useStudioNav(project)\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first navigable rail view.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: landingViewId(nav, project) }}\n replace\n />\n )\n }\n\n const view = nav.registry.get(match.viewId)\n if (!view) return <StudioRouteNotFound />\n\n // A flat view (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (view.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{view.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">{translate('shell.projectNotFound.title')}</Heading>\n <Text tone=\"muted\">{translate('shell.projectNotFound.body')}</Text>\n <Button asChild>\n <Link to=\"/\">{translate('shell.backToProjects')}</Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('shell.routeNotFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('shell.routeNotFound.body')}</Text>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport {\n launcherVisitState,\n Link,\n Navigate,\n STUDIO_PROJECT_ROUTE,\n useParams,\n useSearch,\n} from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { ProjectSessionProvider } from './project-session'\nimport { flatPath } from './app-views/module'\nimport { landingViewId, useStudioNav } from './app-views/sidebar'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from\n * the section rail (EC-237/239). The bare project URL redirects to the first\n * navigable rail view; otherwise the matched view id is resolved from the registry\n * (rail views + always-on built-ins) and its `parsePath` interprets the trailing\n * segments into a deep-link the view renders. An unknown view id or an unparseable\n * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const nav = useStudioNav(project)\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first navigable rail view.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: landingViewId(nav, project) }}\n replace\n />\n )\n }\n\n const view = nav.registry.get(match.viewId)\n if (!view) return <StudioRouteNotFound />\n\n // A flat view (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (view.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{view.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">{translate('shell.projectNotFound.title')}</Heading>\n <Text tone=\"muted\">{translate('shell.projectNotFound.body')}</Text>\n <Button asChild>\n {/* EC-401: an explicit \"back to projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <Link to=\"/\" state={launcherVisitState}>\n {translate('shell.backToProjects')}\n </Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('shell.routeNotFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('shell.routeNotFound.body')}</Text>\n </Stack>\n </div>\n )\n}\n"]}
@@ -1,17 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
2
+ import { launcherVisitState, Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
3
3
  import { ChevronRight } from 'lucide-react';
4
4
  import { useCallback, useEffect, useState } from 'react';
5
5
  import { Row, Spacer, Toaster } from '@elytracms/studio/ui';
6
6
  import { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail';
7
+ import { composeTabTitle, studioBaseTitle, tabTitleParts } from '@elytracms/studio/lib/tab-title';
8
+ import { isSingleProjectWorkspace } from '@elytracms/studio/lib/workspace';
7
9
  import { translate } from '@elytracms/studio/lib/i18n';
8
10
  import { CurrentUserMenu } from './current-user-menu.js';
9
11
  import { DeliveryHealthBanner } from './delivery-health-banner.js';
10
12
  import { PlaygroundBadge } from './playground-badge.js';
11
13
  import { DevLoginBadge } from './dev-login-badge.js';
12
14
  import { AiPanelToggle } from './ai/ai-panel-toggle.js';
13
- import { ProjectSwitcher } from './project-switcher.js';
15
+ import { ActiveProjectName, ProjectSwitcher } from './project-switcher.js';
14
16
  import { SectionNav } from './section-nav.js';
17
+ import { useSwitcherEntries } from './workspace-projects.js';
15
18
  import { activeViewIdFromPath, useStudioNav } from './app-views/sidebar.js';
16
19
  import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context.js';
17
20
  /**
@@ -24,6 +27,11 @@ import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context.js'
24
27
  export function StudioShell({ project, children }) {
25
28
  const { pathname } = useLocation();
26
29
  const activeViewId = activeViewIdFromPath(pathname);
30
+ // EC-401: the shared single-project predicate — the same one that drives the
31
+ // launcher's auto-forward — decides whether the header slot carries the
32
+ // switcher dropdown or just the project's name.
33
+ const { entries } = useSwitcherEntries();
34
+ const singleProject = isSingleProjectWorkspace(entries);
27
35
  // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the
28
36
  // stored preference is read after mount (no flash script needed for a rail).
29
37
  const [railCollapsed, setRailCollapsed] = useState(false);
@@ -37,7 +45,36 @@ export function StudioShell({ project, children }) {
37
45
  return next;
38
46
  });
39
47
  }, []);
40
- return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(AiPanelToggle, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, { projectId: project.id }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
48
+ return (_jsxs(BreadcrumbProvider, { children: [_jsx(StudioTabTitle, { project: project, activeViewId: activeViewId }), _jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", state: launcherVisitState, className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), singleProject ? (_jsx(ActiveProjectName, { project: project })) : (_jsx(ProjectSwitcher, { activeProject: project })), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(AiPanelToggle, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, { projectId: project.id }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
49
+ }
50
+ /**
51
+ * Per-view browser tab title (EC-402). The studio is ONE catch-all splat route,
52
+ * so per-view titles cannot come from per-route `head()` — this effect composes
53
+ * `deepest crumb · view label · project name · base title` from the exact
54
+ * sources the header breadcrumb renders (no new data flow, AD-17): the
55
+ * contributed trail, the nav registry's localized view label, `project.name`,
56
+ * and the `createStudioRootRoute` base title. Assignment is effect-only
57
+ * (SSR-safe); unmounting (launcher, sign-in, project switch) restores the base
58
+ * title, so non-project surfaces keep the consumer's branding untouched.
59
+ */
60
+ function StudioTabTitle({ project, activeViewId, }) {
61
+ const nav = useStudioNav(project);
62
+ const trail = useBreadcrumbTrail();
63
+ const view = activeViewId ? nav.registry.get(activeViewId) : undefined;
64
+ const title = composeTabTitle(tabTitleParts({
65
+ breadcrumb: trail,
66
+ viewLabel: view?.label,
67
+ projectName: project.name,
68
+ baseTitle: studioBaseTitle(),
69
+ }));
70
+ useEffect(() => {
71
+ document.title = title;
72
+ }, [title]);
73
+ // Leaving the project shell restores the base title (launcher/sign-in keep it).
74
+ useEffect(() => () => {
75
+ document.title = studioBaseTitle();
76
+ }, []);
77
+ return null;
41
78
  }
42
79
  const CRUMB_LINK = 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground';
43
80
  const CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground';
@@ -1 +1 @@
1
- {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { AiPanelToggle } from './ai/ai-panel-toggle'\nimport { ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n <Link to=\"/\" className=\"grid size-6 shrink-0 place-items-center\" aria-label={translate('shell.allProjects')}>\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n <ProjectSwitcher activeProject={project} />\n <HeaderBreadcrumb project={project} activeViewId={activeViewId} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-199: AI assistant toggle — renders nothing unless `defineStudio({ ai })`. */}\n <AiPanelToggle />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner projectId={project.id} />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n project={project}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is the active VIEW's label (EC-237/239, resolved\n * from the rail-derived registry); deeper crumbs are contributed by the active\n * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain\n * text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const projectId: ProjectId = project.id\n const nav = useStudioNav(project)\n const extra = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n if (!view) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label={translate('shell.breadcrumb.label')}>\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{view.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.id }}\n className={CRUMB_LINK}\n >\n {view.label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGxD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AACjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAA;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAO7E;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAoB;IACjE,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,CAAA;IAClC,MAAM,YAAY,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAA;IAEnD,6EAA6E;IAC7E,wEAAwE;IACxE,gDAAgD;IAChD,MAAM,EAAE,OAAO,EAAE,GAAG,kBAAkB,EAAE,CAAA;IACxC,MAAM,aAAa,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAA;IAEvD,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,gBAAgB,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACvC,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;QAClC,gBAAgB,CAAC,CAAC,IAAI,EAAE,EAAE;YACxB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAA;YAClB,kBAAkB,CAAC,IAAI,CAAC,CAAA;YACxB,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,MAAC,kBAAkB,eAEjB,KAAC,cAAc,IAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,GAAI,EAChE,eAAK,SAAS,EAAC,mCAAmC,aAEhD,kBAAQ,SAAS,EAAC,mEAAmE,aAGnF,KAAC,IAAI,IACH,EAAE,EAAC,GAAG,EACN,KAAK,EAAE,kBAAkB,EACzB,SAAS,EAAC,yCAAyC,gBACvC,SAAS,CAAC,mBAAmB,CAAC,YAE1C,eAAM,SAAS,EAAC,2CAA2C,GAAG,GACzD,EACN,aAAa,CAAC,CAAC,CAAC,CACf,KAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,GAAI,CACxC,CAAC,CAAC,CAAC,CACF,KAAC,eAAe,IAAC,aAAa,EAAE,OAAO,GAAI,CAC5C,EACD,KAAC,gBAAgB,IAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,GAAI,EAClE,KAAC,MAAM,KAAG,EAEV,KAAC,eAAe,KAAG,EAEnB,KAAC,aAAa,KAAG,EAEjB,KAAC,aAAa,KAAG,EAEjB,KAAC,eAAe,IAAC,SAAS,EAAE,OAAO,CAAC,EAAE,GAAI,IACnC,EAGT,KAAC,oBAAoB,IAAC,SAAS,EAAE,OAAO,CAAC,EAAE,GAAI,EAE/C,eAAK,SAAS,EAAC,qBAAqB,aAClC,KAAC,UAAU,IACT,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,aAAa,EACxB,iBAAiB,EAAE,UAAU,GAC7B,EAEF,eAAM,SAAS,EAAC,8CAA8C,YAC5D,cAAK,SAAS,EAAC,8BAA8B,YAAE,QAAQ,GAAO,GACzD,IACH,IACF,EAEN,KAAC,OAAO,KAAG,IACQ,CACtB,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,EACtB,OAAO,EACP,YAAY,GAIb;IACC,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtE,MAAM,KAAK,GAAG,eAAe,CAC3B,aAAa,CAAC;QACZ,UAAU,EAAE,KAAK;QACjB,SAAS,EAAE,IAAI,EAAE,KAAK;QACtB,WAAW,EAAE,OAAO,CAAC,IAAI;QACzB,SAAS,EAAE,eAAe,EAAE;KAC7B,CAAC,CACH,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAA;IACxB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,gFAAgF;IAChF,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,QAAQ,CAAC,KAAK,GAAG,eAAe,EAAE,CAAA;IACpC,CAAC,EACD,EAAE,CACH,CAAA;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,GACd,sFAAsF,CAAA;AACxF,MAAM,aAAa,GAAG,oDAAoD,CAAA;AAE1E;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,EACxB,OAAO,EACP,YAAY,GAIb;IACC,MAAM,SAAS,GAAc,OAAO,CAAC,EAAE,CAAA;IACvC,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,CAAA;IAExC,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,iBAAiB,gBAAa,SAAS,CAAC,wBAAwB,CAAC,aACtF,KAAC,YAAY,IAAC,SAAS,EAAC,4CAA4C,wBAAe,EAClF,aAAa,CAAC,CAAC,CAAC,CACf,eAAM,SAAS,EAAE,aAAa,YAAG,IAAI,CAAC,KAAK,GAAQ,CACpD,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,EACtC,SAAS,EAAE,UAAU,YAEpB,IAAI,CAAC,KAAK,GACN,CACR,EACA,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBACzB,MAAM,MAAM,GAAG,KAAK,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;gBACzC,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAgC,SAAS,EAAC,SAAS,aAC7D,KAAC,YAAY,IAAC,SAAS,EAAC,4CAA4C,wBAAe,EAClF,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAGpB,KAAC,IAAI,IAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAO,EAAE,SAAS,EAAE,UAAU,YAC3D,IAAI,CAAC,KAAK,GACN,CACR,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,YAAG,IAAI,CAAC,KAAK,GAAQ,CAC1E,KAVe,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,CAWpC,CACP,CAAA;YACH,CAAC,CAAC,IACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import { launcherVisitState, Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { composeTabTitle, studioBaseTitle, tabTitleParts } from '@elytracms/studio/lib/tab-title'\nimport { isSingleProjectWorkspace } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { AiPanelToggle } from './ai/ai-panel-toggle'\nimport { ActiveProjectName, ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { useSwitcherEntries } from './workspace-projects'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n\n // EC-401: the shared single-project predicate — the same one that drives the\n // launcher's auto-forward — decides whether the header slot carries the\n // switcher dropdown or just the project's name.\n const { entries } = useSwitcherEntries()\n const singleProject = isSingleProjectWorkspace(entries)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n {/* EC-402: per-view tab title, fed by the same breadcrumb/nav data as the header. */}\n <StudioTabTitle project={project} activeViewId={activeViewId} />\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <Link\n to=\"/\"\n state={launcherVisitState}\n className=\"grid size-6 shrink-0 place-items-center\"\n aria-label={translate('shell.allProjects')}\n >\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n {singleProject ? (\n <ActiveProjectName project={project} />\n ) : (\n <ProjectSwitcher activeProject={project} />\n )}\n <HeaderBreadcrumb project={project} activeViewId={activeViewId} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-199: AI assistant toggle — renders nothing unless `defineStudio({ ai })`. */}\n <AiPanelToggle />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner projectId={project.id} />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n project={project}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\n/**\n * Per-view browser tab title (EC-402). The studio is ONE catch-all splat route,\n * so per-view titles cannot come from per-route `head()` — this effect composes\n * `deepest crumb · view label · project name · base title` from the exact\n * sources the header breadcrumb renders (no new data flow, AD-17): the\n * contributed trail, the nav registry's localized view label, `project.name`,\n * and the `createStudioRootRoute` base title. Assignment is effect-only\n * (SSR-safe); unmounting (launcher, sign-in, project switch) restores the base\n * title, so non-project surfaces keep the consumer's branding untouched.\n */\nfunction StudioTabTitle({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const nav = useStudioNav(project)\n const trail = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n const title = composeTabTitle(\n tabTitleParts({\n breadcrumb: trail,\n viewLabel: view?.label,\n projectName: project.name,\n baseTitle: studioBaseTitle(),\n }),\n )\n\n useEffect(() => {\n document.title = title\n }, [title])\n\n // Leaving the project shell restores the base title (launcher/sign-in keep it).\n useEffect(\n () => () => {\n document.title = studioBaseTitle()\n },\n [],\n )\n\n return null\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is the active VIEW's label (EC-237/239, resolved\n * from the rail-derived registry); deeper crumbs are contributed by the active\n * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain\n * text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const projectId: ProjectId = project.id\n const nav = useStudioNav(project)\n const extra = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n if (!view) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label={translate('shell.breadcrumb.label')}>\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{view.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.id }}\n className={CRUMB_LINK}\n >\n {view.label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
@@ -50,6 +50,9 @@ export { colorFieldRenderer } from './components/cms/field-renderer.js';
50
50
  * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({
51
51
  * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator
52
52
  * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.
53
+ * Prefer `TranslationCatalog` (EC-398) when authoring: it is typed against the EN key
54
+ * set, so unknown/stale keys fail typecheck — engine-shipped locales
55
+ * (`@elytracms/studio/locales/de`) and spread-overrides of them use it.
53
56
  */
54
57
  export { msg } from './lib/i18n/index.js';
55
- export type { MessageCatalog, SourceMessage, StudioI18nConfig } from './lib/i18n/index.js';
58
+ export type { MessageCatalog, MessageKey, SourceMessage, StudioI18nConfig, TranslationCatalog, } from './lib/i18n/index.js';
@@ -47,6 +47,9 @@ export { colorFieldRenderer } from './components/cms/field-renderer.js';
47
47
  * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({
48
48
  * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator
49
49
  * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.
50
+ * Prefer `TranslationCatalog` (EC-398) when authoring: it is typed against the EN key
51
+ * set, so unknown/stale keys fail typecheck — engine-shipped locales
52
+ * (`@elytracms/studio/locales/de`) and spread-overrides of them use it.
50
53
  */
51
54
  export { msg } from './lib/i18n/index.js';
52
55
  //# sourceMappingURL=customization.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAOnE,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;GAIG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n *\n * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n StudioCanvasTheme,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n/**\n * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({\n * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator\n * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.\n */\nexport { msg } from './lib/i18n'\nexport type { MessageCatalog, SourceMessage, StudioI18nConfig } from './lib/i18n'\n"]}
1
+ {"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAOnE,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;;;;GAOG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n *\n * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n StudioCanvasTheme,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n/**\n * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({\n * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator\n * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.\n * Prefer `TranslationCatalog` (EC-398) when authoring: it is typed against the EN key\n * set, so unknown/stale keys fail typecheck — engine-shipped locales\n * (`@elytracms/studio/locales/de`) and spread-overrides of them use it.\n */\nexport { msg } from './lib/i18n'\nexport type {\n MessageCatalog,\n MessageKey,\n SourceMessage,\n StudioI18nConfig,\n TranslationCatalog,\n} from './lib/i18n'\n"]}
@@ -1,23 +1,5 @@
1
+ import type { ChangePasswordResult, SignInResult } from './auth/auth-adapter.js';
1
2
  import type { OperationError } from './operations/index.js';
2
- /**
3
- * Session-expiry detection + plain-language mapping (EC-177 gap 9).
4
- *
5
- * An expired Convex session surfaces as a raw backend blob thrown by the
6
- * transport — e.g. `{"code":"Unauthenticated","message":"Could not verify
7
- * OIDC token claim…"}` — which `toOperationError` can only normalize to an
8
- * `unknown`/`backend-unavailable` `OperationError` whose message *is* that
9
- * blob. Rendering it verbatim is editor-hostile, and the real problem (edits
10
- * silently stop persisting) is invisible.
11
- *
12
- * This module is the one chokepoint that recognizes that shape on the
13
- * structured `OperationError` (the seam every studio failure already flows
14
- * through), so UI surfaces never string-match raw messages themselves: the
15
- * shared banner asks {@link isSessionExpiredError} and renders the
16
- * plain-language copy + a "Sign in again" action instead.
17
- */
18
- /** Editor-facing copy: plain language, no auth/transport jargon. */
19
- export declare const SESSION_EXPIRED_MESSAGE = "You've been signed out, so your latest changes aren't being saved. Sign in again to keep editing \u2014 you'll come back to this screen.";
20
- export declare const SIGN_IN_AGAIN_LABEL = "Sign in again";
21
3
  /**
22
4
  * Does this operation failure mean "the session is no longer valid"?
23
5
  *
@@ -29,14 +11,62 @@ export declare const SIGN_IN_AGAIN_LABEL = "Sign in again";
29
11
  * message it ships with.
30
12
  */
31
13
  export declare function isSessionExpiredError(error: OperationError): boolean;
14
+ type SignInFailure = Extract<SignInResult, {
15
+ ok: false;
16
+ }>;
17
+ /**
18
+ * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The
19
+ * auth adapters (no-touch `lib/auth/**`) return plain-English `message`
20
+ * constants on the EC-362/EC-363 classified reasons; this table names the
21
+ * catalog key that carries each of those messages, so the sign-in screen
22
+ * localizes failures through the SAME catalog as the rest of the chrome
23
+ * (a German catalog covers them with no consumer-side mapper — the
24
+ * pre-EC-375 Naturkinder `authErrorMessage` is retired by this seam).
25
+ *
26
+ * A reason can carry several distinct engine messages (`unavailable` spans
27
+ * the rate-limit, the honest server-error generic, and the playground
28
+ * sign-up refusal), so each reason lists its candidate keys and a failure is
29
+ * recognized by **exact EN-message match** against the catalog entry. That
30
+ * makes English rendering byte-identical by construction and keeps the
31
+ * fallback honest: any unrecognized message (a future reason, a reworded
32
+ * adapter constant, a non-engine adapter) falls through to the raw
33
+ * `result.message` — never a mistranslated or stale mapping.
34
+ * `lib/auth-errors.test.ts` runs the real classifier over every branch to
35
+ * pin that adapter copy and catalog never drift apart silently.
36
+ */
37
+ export declare const SIGN_IN_ERROR_KEYS: Record<SignInFailure['reason'], readonly string[]>;
38
+ /**
39
+ * Resolve a failed {@link SignInResult} to its message-catalog key, or `null`
40
+ * when the failure is not an engine-known shape (render `result.message` raw
41
+ * — the honest fallback). Pure; accepts the failure structurally so callers
42
+ * and tests can probe unknown future reasons without casts.
43
+ */
44
+ export declare function signInErrorMessageKey(failure: {
45
+ reason: string;
46
+ message: string;
47
+ }): string | null;
48
+ type ChangePasswordFailure = Extract<ChangePasswordResult, {
49
+ ok: false;
50
+ }>;
51
+ /**
52
+ * Engine-owned change-password-failure → message-catalog-key mapping (EC-412)
53
+ * — the {@link SIGN_IN_ERROR_KEYS} design extended to the account credential
54
+ * panel. The reasons are the {@link ChangePasswordResult} failure union;
55
+ * `unavailable` again spans several deliberate messages (the
56
+ * deployment-misconfigured remedy, the EC-377 rate limit, and the honest
57
+ * server-error generic), so recognition is by reason + exact EN-message match
58
+ * and every unrecognized shape falls through to the raw `result.message`.
59
+ * `lib/auth-errors.test.ts` runs the real `classifyChangePasswordError` over
60
+ * every branch to pin adapter copy and catalog together.
61
+ */
62
+ export declare const CHANGE_PASSWORD_ERROR_KEYS: Record<ChangePasswordFailure['reason'], readonly string[]>;
32
63
  /**
33
- * Where the "Sign in again" action navigates: the sign-in screen, carrying the
34
- * current location so a successful sign-in returns the editor to the exact
35
- * screen they were on (the sign-in route's `redirect` search param, EC-150).
36
- * External/protocol-relative targets are rejected, mirroring
37
- * `postSignInTarget`.
64
+ * Resolve a failed {@link ChangePasswordResult} to its message-catalog key, or
65
+ * `null` when the failure is not an engine-known shape (render
66
+ * `result.message` raw — the honest fallback).
38
67
  */
39
- export declare function signInHref(location: {
40
- pathname: string;
41
- search?: string;
42
- }): string;
68
+ export declare function changePasswordErrorMessageKey(failure: {
69
+ reason: string;
70
+ message: string;
71
+ }): string | null;
72
+ export {};