@elytracms/studio 0.0.61 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/components/account/account-settings.js +6 -1
  2. package/dist/components/account/account-settings.js.map +1 -1
  3. package/dist/components/account/credential-panel.js +6 -1
  4. package/dist/components/account/credential-panel.js.map +1 -1
  5. package/dist/components/account/self-account-schema.js +4 -1
  6. package/dist/components/account/self-account-schema.js.map +1 -1
  7. package/dist/components/auth-context.d.ts +14 -0
  8. package/dist/components/auth-context.js +32 -2
  9. package/dist/components/auth-context.js.map +1 -1
  10. package/dist/components/builder/blocks-model.d.ts +7 -0
  11. package/dist/components/builder/blocks-model.js +5 -0
  12. package/dist/components/builder/blocks-model.js.map +1 -1
  13. package/dist/components/builder/blocks-panel.js +2 -2
  14. package/dist/components/builder/blocks-panel.js.map +1 -1
  15. package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
  16. package/dist/components/builder/fixture-thumbnail.js +14 -0
  17. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  18. package/dist/components/builder/property-inspector.d.ts +21 -0
  19. package/dist/components/builder/property-inspector.js +24 -8
  20. package/dist/components/builder/property-inspector.js.map +1 -1
  21. package/dist/components/cms/cms-index.js +6 -3
  22. package/dist/components/cms/cms-index.js.map +1 -1
  23. package/dist/components/cms/detail-editor.d.ts +12 -1
  24. package/dist/components/cms/detail-editor.js +23 -23
  25. package/dist/components/cms/detail-editor.js.map +1 -1
  26. package/dist/components/cms/prop-fields.d.ts +1 -1
  27. package/dist/components/cms/prop-fields.js +14 -0
  28. package/dist/components/cms/prop-fields.js.map +1 -1
  29. package/dist/components/deployment-setup-gate.js +40 -8
  30. package/dist/components/deployment-setup-gate.js.map +1 -1
  31. package/dist/components/operation-error.d.ts +7 -5
  32. package/dist/components/operation-error.js +19 -36
  33. package/dist/components/operation-error.js.map +1 -1
  34. package/dist/components/project-list.js +17 -1
  35. package/dist/components/project-list.js.map +1 -1
  36. package/dist/components/project-switcher.d.ts +10 -0
  37. package/dist/components/project-switcher.js +13 -3
  38. package/dist/components/project-switcher.js.map +1 -1
  39. package/dist/components/session-expired-overlay.d.ts +27 -0
  40. package/dist/components/session-expired-overlay.js +51 -0
  41. package/dist/components/session-expired-overlay.js.map +1 -0
  42. package/dist/components/sign-in-view.d.ts +13 -1
  43. package/dist/components/sign-in-view.js +27 -13
  44. package/dist/components/sign-in-view.js.map +1 -1
  45. package/dist/components/studio-project-screen.js +2 -2
  46. package/dist/components/studio-project-screen.js.map +1 -1
  47. package/dist/components/studio-shell.js +40 -3
  48. package/dist/components/studio-shell.js.map +1 -1
  49. package/dist/components/users/users-schema.d.ts +11 -1
  50. package/dist/components/users/users-schema.js +17 -1
  51. package/dist/components/users/users-schema.js.map +1 -1
  52. package/dist/customization.d.ts +4 -1
  53. package/dist/customization.js +3 -0
  54. package/dist/customization.js.map +1 -1
  55. package/dist/lib/auth-errors.d.ts +58 -28
  56. package/dist/lib/auth-errors.js +93 -15
  57. package/dist/lib/auth-errors.js.map +1 -1
  58. package/dist/lib/i18n/index.d.ts +1 -0
  59. package/dist/lib/i18n/index.js.map +1 -1
  60. package/dist/lib/i18n/messages.de.d.ts +24 -0
  61. package/dist/lib/i18n/messages.de.js +1019 -0
  62. package/dist/lib/i18n/messages.de.js.map +1 -0
  63. package/dist/lib/i18n/messages.en.d.ts +880 -0
  64. package/dist/lib/i18n/messages.en.js +71 -3
  65. package/dist/lib/i18n/messages.en.js.map +1 -1
  66. package/dist/lib/operations/studio-operations.js +7 -0
  67. package/dist/lib/operations/studio-operations.js.map +1 -1
  68. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  69. package/dist/lib/persistence/deployment-readiness.js +16 -1
  70. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  71. package/dist/lib/session-expiry.d.ts +67 -0
  72. package/dist/lib/session-expiry.js +72 -0
  73. package/dist/lib/session-expiry.js.map +1 -0
  74. package/dist/lib/tab-title.d.ts +53 -0
  75. package/dist/lib/tab-title.js +77 -0
  76. package/dist/lib/tab-title.js.map +1 -0
  77. package/dist/lib/workspace/index.d.ts +1 -1
  78. package/dist/lib/workspace/index.js +1 -1
  79. package/dist/lib/workspace/index.js.map +1 -1
  80. package/dist/lib/workspace/switcher-source.d.ts +27 -0
  81. package/dist/lib/workspace/switcher-source.js +30 -0
  82. package/dist/lib/workspace/switcher-source.js.map +1 -1
  83. package/dist/router/index.d.ts +6 -0
  84. package/dist/router/index.js +6 -0
  85. package/dist/router/index.js.map +1 -1
  86. package/dist/router/launcher-visit.d.ts +13 -0
  87. package/dist/router/launcher-visit.js +34 -0
  88. package/dist/router/launcher-visit.js.map +1 -0
  89. package/dist/router/root.js +12 -2
  90. package/dist/router/root.js.map +1 -1
  91. package/dist/ui/dialog.d.ts +15 -0
  92. package/dist/ui/dialog.js +17 -0
  93. package/dist/ui/dialog.js.map +1 -1
  94. package/package.json +6 -2
@@ -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":"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","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"]}
@@ -1,4 +1,14 @@
1
- import type { CollectionDef } from '@elytracms/core/cms-core';
1
+ import type { CollectionDef, DocumentTitleFn } from '@elytracms/core/cms-core';
2
+ /**
3
+ * EC-396 — the display-title fallback shared by the users rail and the account lens
4
+ * (`selfAccountCollectionDef`): a member's **name**, else their **email** — never the
5
+ * raw internal id when an email exists. A function-pattern `titleField` (config-as-code,
6
+ * studio-evaluated), so list rows, breadcrumbs, relation labels, and the account header
7
+ * resolve identity the same way. Returns `''` when NEITHER is present, so
8
+ * `resolveDocumentTitle` degrades to the document id (its `fallbackId`) — the only case a
9
+ * raw id ever surfaces.
10
+ */
11
+ export declare const userTitleFallback: DocumentTitleFn;
2
12
  /**
3
13
  * The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —
4
14
  * NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —
@@ -1,6 +1,21 @@
1
1
  import { collectionDefSchema } from '@elytracms/core/cms-core';
2
2
  import { roleOptions } from '@elytracms/core/persistence';
3
3
  import { msgLabel } from '@elytracms/studio/lib/i18n';
4
+ /**
5
+ * EC-396 — the display-title fallback shared by the users rail and the account lens
6
+ * (`selfAccountCollectionDef`): a member's **name**, else their **email** — never the
7
+ * raw internal id when an email exists. A function-pattern `titleField` (config-as-code,
8
+ * studio-evaluated), so list rows, breadcrumbs, relation labels, and the account header
9
+ * resolve identity the same way. Returns `''` when NEITHER is present, so
10
+ * `resolveDocumentTitle` degrades to the document id (its `fallbackId`) — the only case a
11
+ * raw id ever surfaces.
12
+ */
13
+ export const userTitleFallback = (values) => {
14
+ const name = typeof values.name === 'string' ? values.name.trim() : '';
15
+ if (name)
16
+ return name;
17
+ return typeof values.email === 'string' ? values.email.trim() : '';
18
+ };
4
19
  /**
5
20
  * The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —
6
21
  * NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —
@@ -22,7 +37,8 @@ export const usersCollectionDef = collectionDefSchema.parse({
22
37
  // Users aren't a CMS `kind`; 'document' just satisfies the lens schema. The
23
38
  // source's capabilities (adminOnly, no publish/versions/create) carry the semantics.
24
39
  kind: 'document',
25
- titleField: 'name',
40
+ // EC-396: name → email → id (never the raw id when an email exists).
41
+ titleField: userTitleFallback,
26
42
  // EC-241: built-in rail labels resolve through the message catalog (msgLabel) — a
27
43
  // consumer's translation catalog localizes them without a per-rail override.
28
44
  form: { label: msgLabel('users.rail.label'), labelSingular: msgLabel('users.rail.labelSingular') },
@@ -1 +1 @@
1
- {"version":3,"file":"users-schema.js","sourceRoot":"","sources":["../../../src/components/users/users-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAErD;;;;;;;;;;;;GAYG;AACH,kFAAkF;AAClF,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,EAAE,CAAA;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAAkB,mBAAmB,CAAC,KAAK,CAAC;IACzE,EAAE,EAAE,MAAM;IACV,4EAA4E;IAC5E,qFAAqF;IACrF,IAAI,EAAE,UAAU;IAChB,UAAU,EAAE,MAAM;IAClB,kFAAkF;IAClF,6EAA6E;IAC7E,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE;IAClG,MAAM,EAAE;QACN,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACrH,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACvH;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,CAAC,GAAG,iBAAiB,CAAC;YAC/B,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE;SACpD;KACF;CACF,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { roleOptions } from '@elytracms/core/persistence'\nimport { msgLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —\n * NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —\n * so this `CollectionDef` exists only to drive the portable list/edit surfaces\n * over the users {@link CollectionViewSource}. The rail's *user-ness* comes from\n * the source's capabilities (admin-only, no publish/versions/create), never the\n * schema `kind`.\n *\n * Identity fields (`name`, `email`) are **auth-owned** and read-only here — an\n * admin manages a member's *role* (and, later, project-declared custom fields\n * over the `meta` bag, mirroring assets); the person edits their own name/email\n * via account settings. A v1 default; a project may extend the field set later.\n */\n// Derived from the ONE role policy (EC-240c) — no hardcoded duplicate of the role\n// set. Reflects the shipped default today; a project's custom policy flows through.\nexport const USER_ROLE_OPTIONS = roleOptions()\n\nexport const usersCollectionDef: CollectionDef = collectionDefSchema.parse({\n id: 'user',\n // Users aren't a CMS `kind`; 'document' just satisfies the lens schema. The\n // source's capabilities (adminOnly, no publish/versions/create) carry the semantics.\n kind: 'document',\n titleField: 'name',\n // EC-241: built-in rail labels resolve through the message catalog (msgLabel) — a\n // consumer's translation catalog localizes them without a per-rail override.\n form: { label: msgLabel('users.rail.label'), labelSingular: msgLabel('users.rail.labelSingular') },\n fields: [\n { name: 'name', type: 'text', form: { label: msgLabel('users.field.name.label'), readOnly: true }, filterable: true },\n { name: 'email', type: 'text', form: { label: msgLabel('users.field.email.label'), readOnly: true }, filterable: true },\n {\n name: 'role',\n type: 'select',\n options: [...USER_ROLE_OPTIONS],\n filterable: true,\n form: { label: msgLabel('users.field.role.label') },\n },\n ],\n})\n"]}
1
+ {"version":3,"file":"users-schema.js","sourceRoot":"","sources":["../../../src/components/users/users-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAErD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAoB,CAAC,MAAM,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACtE,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IACrB,OAAO,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACpE,CAAC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,kFAAkF;AAClF,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,EAAE,CAAA;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAAkB,mBAAmB,CAAC,KAAK,CAAC;IACzE,EAAE,EAAE,MAAM;IACV,4EAA4E;IAC5E,qFAAqF;IACrF,IAAI,EAAE,UAAU;IAChB,qEAAqE;IACrE,UAAU,EAAE,iBAAiB;IAC7B,kFAAkF;IAClF,6EAA6E;IAC7E,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE;IAClG,MAAM,EAAE;QACN,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACrH,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACvH;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,CAAC,GAAG,iBAAiB,CAAC;YAC/B,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE;SACpD;KACF;CACF,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef, DocumentTitleFn } from '@elytracms/core/cms-core'\nimport { roleOptions } from '@elytracms/core/persistence'\nimport { msgLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * EC-396 — the display-title fallback shared by the users rail and the account lens\n * (`selfAccountCollectionDef`): a member's **name**, else their **email** — never the\n * raw internal id when an email exists. A function-pattern `titleField` (config-as-code,\n * studio-evaluated), so list rows, breadcrumbs, relation labels, and the account header\n * resolve identity the same way. Returns `''` when NEITHER is present, so\n * `resolveDocumentTitle` degrades to the document id (its `fallbackId`) — the only case a\n * raw id ever surfaces.\n */\nexport const userTitleFallback: DocumentTitleFn = (values) => {\n const name = typeof values.name === 'string' ? values.name.trim() : ''\n if (name) return name\n return typeof values.email === 'string' ? values.email.trim() : ''\n}\n\n/**\n * The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —\n * NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —\n * so this `CollectionDef` exists only to drive the portable list/edit surfaces\n * over the users {@link CollectionViewSource}. The rail's *user-ness* comes from\n * the source's capabilities (admin-only, no publish/versions/create), never the\n * schema `kind`.\n *\n * Identity fields (`name`, `email`) are **auth-owned** and read-only here — an\n * admin manages a member's *role* (and, later, project-declared custom fields\n * over the `meta` bag, mirroring assets); the person edits their own name/email\n * via account settings. A v1 default; a project may extend the field set later.\n */\n// Derived from the ONE role policy (EC-240c) — no hardcoded duplicate of the role\n// set. Reflects the shipped default today; a project's custom policy flows through.\nexport const USER_ROLE_OPTIONS = roleOptions()\n\nexport const usersCollectionDef: CollectionDef = collectionDefSchema.parse({\n id: 'user',\n // Users aren't a CMS `kind`; 'document' just satisfies the lens schema. The\n // source's capabilities (adminOnly, no publish/versions/create) carry the semantics.\n kind: 'document',\n // EC-396: name → email → id (never the raw id when an email exists).\n titleField: userTitleFallback,\n // EC-241: built-in rail labels resolve through the message catalog (msgLabel) — a\n // consumer's translation catalog localizes them without a per-rail override.\n form: { label: msgLabel('users.rail.label'), labelSingular: msgLabel('users.rail.labelSingular') },\n fields: [\n { name: 'name', type: 'text', form: { label: msgLabel('users.field.name.label'), readOnly: true }, filterable: true },\n { name: 'email', type: 'text', form: { label: msgLabel('users.field.email.label'), readOnly: true }, filterable: true },\n {\n name: 'role',\n type: 'select',\n options: [...USER_ROLE_OPTIONS],\n filterable: true,\n form: { label: msgLabel('users.field.role.label') },\n },\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 {};
@@ -1,3 +1,4 @@
1
+ import { EN_MESSAGES } from './i18n/messages.en.js';
1
2
  /**
2
3
  * Session-expiry detection + plain-language mapping (EC-177 gap 9).
3
4
  *
@@ -10,13 +11,13 @@
10
11
  *
11
12
  * This module is the one chokepoint that recognizes that shape on the
12
13
  * structured `OperationError` (the seam every studio failure already flows
13
- * through), so UI surfaces never string-match raw messages themselves: the
14
- * shared banner asks {@link isSessionExpiredError} and renders the
15
- * plain-language copy + a "Sign in again" action instead.
14
+ * through), so UI surfaces never string-match raw messages themselves. Since
15
+ * EC-403 an expired session is an app-level state: `lib/session-expiry.ts`
16
+ * runs this predicate over every failed operation on the lifecycle bus and
17
+ * raises the blocking in-place re-auth overlay (the
18
+ * `error.sessionExpiredTitle` / `error.sessionExpired` catalog keys) — the
19
+ * shared banner only uses it to route expiry AWAY from per-surface rendering.
16
20
  */
17
- /** Editor-facing copy: plain language, no auth/transport jargon. */
18
- export const SESSION_EXPIRED_MESSAGE = "You've been signed out, so your latest changes aren't being saved. Sign in again to keep editing — you'll come back to this screen.";
19
- export const SIGN_IN_AGAIN_LABEL = 'Sign in again';
20
21
  /**
21
22
  * Extract the `code` of a JSON object embedded in a transport error message.
22
23
  * Convex auth failures carry `{"code":"Unauthenticated", …}`, sometimes
@@ -60,15 +61,92 @@ export function isSessionExpiredError(error) {
60
61
  return /could not verify oidc token/i.test(error.message);
61
62
  }
62
63
  /**
63
- * Where the "Sign in again" action navigates: the sign-in screen, carrying the
64
- * current location so a successful sign-in returns the editor to the exact
65
- * screen they were on (the sign-in route's `redirect` search param, EC-150).
66
- * External/protocol-relative targets are rejected, mirroring
67
- * `postSignInTarget`.
64
+ * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The
65
+ * auth adapters (no-touch `lib/auth/**`) return plain-English `message`
66
+ * constants on the EC-362/EC-363 classified reasons; this table names the
67
+ * catalog key that carries each of those messages, so the sign-in screen
68
+ * localizes failures through the SAME catalog as the rest of the chrome
69
+ * (a German catalog covers them with no consumer-side mapper — the
70
+ * pre-EC-375 Naturkinder `authErrorMessage` is retired by this seam).
71
+ *
72
+ * A reason can carry several distinct engine messages (`unavailable` spans
73
+ * the rate-limit, the honest server-error generic, and the playground
74
+ * sign-up refusal), so each reason lists its candidate keys and a failure is
75
+ * recognized by **exact EN-message match** against the catalog entry. That
76
+ * makes English rendering byte-identical by construction and keeps the
77
+ * fallback honest: any unrecognized message (a future reason, a reworded
78
+ * adapter constant, a non-engine adapter) falls through to the raw
79
+ * `result.message` — never a mistranslated or stale mapping.
80
+ * `lib/auth-errors.test.ts` runs the real classifier over every branch to
81
+ * pin that adapter copy and catalog never drift apart silently.
82
+ */
83
+ export const SIGN_IN_ERROR_KEYS = {
84
+ 'invalid-credentials': [
85
+ 'auth.signIn.error.invalidCredentials',
86
+ 'auth.signIn.error.invalidCredentialsSignUp',
87
+ ],
88
+ 'account-exists': ['auth.signIn.error.accountExists'],
89
+ 'deployment-misconfigured': ['auth.signIn.error.deploymentMisconfigured'],
90
+ 'not-invited': ['auth.signIn.error.notInvited'],
91
+ unavailable: [
92
+ 'auth.signIn.error.tooManyAttempts',
93
+ 'auth.signIn.error.serverError',
94
+ 'auth.signIn.error.serverErrorSignUp',
95
+ 'auth.signIn.error.unavailable',
96
+ 'auth.signIn.error.playgroundSignUp',
97
+ ],
98
+ };
99
+ /**
100
+ * Resolve a failure `{ reason, message }` against a reason → candidate-keys
101
+ * table by **exact EN-message match** (the EC-397 recognition rule): the
102
+ * matched key's catalog entry carries the same English, so EN rendering stays
103
+ * byte-identical, and anything unrecognized returns `null` — the caller
104
+ * renders the raw message, never a stale mapping.
105
+ */
106
+ function errorMessageKey(table, failure) {
107
+ const candidates = table[failure.reason];
108
+ return candidates?.find((key) => EN_MESSAGES[key]?.message === failure.message) ?? null;
109
+ }
110
+ /**
111
+ * Resolve a failed {@link SignInResult} to its message-catalog key, or `null`
112
+ * when the failure is not an engine-known shape (render `result.message` raw
113
+ * — the honest fallback). Pure; accepts the failure structurally so callers
114
+ * and tests can probe unknown future reasons without casts.
115
+ */
116
+ export function signInErrorMessageKey(failure) {
117
+ return errorMessageKey(SIGN_IN_ERROR_KEYS, failure);
118
+ }
119
+ /**
120
+ * Engine-owned change-password-failure → message-catalog-key mapping (EC-412)
121
+ * — the {@link SIGN_IN_ERROR_KEYS} design extended to the account credential
122
+ * panel. The reasons are the {@link ChangePasswordResult} failure union;
123
+ * `unavailable` again spans several deliberate messages (the
124
+ * deployment-misconfigured remedy, the EC-377 rate limit, and the honest
125
+ * server-error generic), so recognition is by reason + exact EN-message match
126
+ * and every unrecognized shape falls through to the raw `result.message`.
127
+ * `lib/auth-errors.test.ts` runs the real `classifyChangePasswordError` over
128
+ * every branch to pin adapter copy and catalog together.
129
+ */
130
+ export const CHANGE_PASSWORD_ERROR_KEYS = {
131
+ 'invalid-credentials': ['account.password.error.currentIncorrect'],
132
+ 'weak-password': ['account.password.error.weakPassword'],
133
+ 'not-authenticated': ['account.password.error.notSignedIn'],
134
+ unavailable: [
135
+ 'account.password.error.deploymentMisconfigured',
136
+ 'account.password.error.tooManyAttempts',
137
+ 'account.password.error.serverError',
138
+ ],
139
+ unsupported: ['account.password.error.playgroundUnsupported'],
140
+ };
141
+ /**
142
+ * Resolve a failed {@link ChangePasswordResult} to its message-catalog key, or
143
+ * `null` when the failure is not an engine-known shape (render
144
+ * `result.message` raw — the honest fallback).
68
145
  */
69
- export function signInHref(location) {
70
- const target = `${location.pathname}${location.search ?? ''}`;
71
- const redirect = target.startsWith('/') && !target.startsWith('//') ? target : '/';
72
- return `/sign-in?redirect=${encodeURIComponent(redirect)}`;
146
+ export function changePasswordErrorMessageKey(failure) {
147
+ return errorMessageKey(CHANGE_PASSWORD_ERROR_KEYS, failure);
73
148
  }
149
+ // `signInHref` (the banner's navigating "Sign in again" action) was retired by
150
+ // EC-403: re-auth happens in place via the blocking overlay — a navigation
151
+ // would destroy exactly the unsaved editor state the treatment protects.
74
152
  //# sourceMappingURL=auth-errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-errors.js","sourceRoot":"","sources":["../../src/lib/auth-errors.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AAEH,oEAAoE;AACpE,MAAM,CAAC,MAAM,uBAAuB,GAClC,qIAAqI,CAAA;AAEvI,MAAM,CAAC,MAAM,mBAAmB,GAAG,eAAe,CAAA;AAElD;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,SAAS,CAAA;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;QACjE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAClD,MAAM,IAAI,GAAI,MAA6B,CAAC,IAAI,CAAA;YAChD,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAqB;IACzD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,qBAAqB;QAAE,OAAO,KAAK,CAAA;IAClF,IAAI,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,iBAAiB;QAAE,OAAO,IAAI,CAAA;IACtE,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1D,OAAO,8BAA8B,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,QAA+C;IACxE,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,CAAA;IAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA;IAClF,OAAO,qBAAqB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAA;AAC5D,CAAC","sourcesContent":["import type { OperationError } from './operations'\n\n/**\n * Session-expiry detection + plain-language mapping (EC-177 gap 9).\n *\n * An expired Convex session surfaces as a raw backend blob thrown by the\n * transport — e.g. `{\"code\":\"Unauthenticated\",\"message\":\"Could not verify\n * OIDC token claim…\"}` — which `toOperationError` can only normalize to an\n * `unknown`/`backend-unavailable` `OperationError` whose message *is* that\n * blob. Rendering it verbatim is editor-hostile, and the real problem (edits\n * silently stop persisting) is invisible.\n *\n * This module is the one chokepoint that recognizes that shape on the\n * structured `OperationError` (the seam every studio failure already flows\n * through), so UI surfaces never string-match raw messages themselves: the\n * shared banner asks {@link isSessionExpiredError} and renders the\n * plain-language copy + a \"Sign in again\" action instead.\n */\n\n/** Editor-facing copy: plain language, no auth/transport jargon. */\nexport const SESSION_EXPIRED_MESSAGE =\n \"You've been signed out, so your latest changes aren't being saved. Sign in again to keep editing — you'll come back to this screen.\"\n\nexport const SIGN_IN_AGAIN_LABEL = 'Sign in again'\n\n/**\n * Extract the `code` of a JSON object embedded in a transport error message.\n * Convex auth failures carry `{\"code\":\"Unauthenticated\", …}`, sometimes\n * wrapped in client prefixes (`[CONVEX M(graph:save)] … Server Error: {…}`).\n */\nfunction embeddedJsonCode(message: string): string | undefined {\n const start = message.indexOf('{')\n const end = message.lastIndexOf('}')\n if (start === -1 || end <= start) return undefined\n try {\n const parsed: unknown = JSON.parse(message.slice(start, end + 1))\n if (parsed !== null && typeof parsed === 'object') {\n const code = (parsed as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n } catch {\n // Not a JSON payload — fall through to the marker checks.\n }\n return undefined\n}\n\n/**\n * Does this operation failure mean \"the session is no longer valid\"?\n *\n * Only the transport-shaped codes can carry the backend auth blob: structured\n * studio-level failures (`validation`, `unauthorized` from the role hook,\n * `not-found`, `conflict`) are never session expiry. Within those, detect the\n * Convex unauthenticated shape: an embedded `{\"code\":\"Unauthenticated\"}`\n * payload, the bare `Unauthenticated` server-error marker, or the OIDC claim\n * message it ships with.\n */\nexport function isSessionExpiredError(error: OperationError): boolean {\n if (error.code !== 'unknown' && error.code !== 'backend-unavailable') return false\n if (embeddedJsonCode(error.message) === 'Unauthenticated') return true\n if (/\\bUnauthenticated\\b/.test(error.message)) return true\n return /could not verify oidc token/i.test(error.message)\n}\n\n/**\n * Where the \"Sign in again\" action navigates: the sign-in screen, carrying the\n * current location so a successful sign-in returns the editor to the exact\n * screen they were on (the sign-in route's `redirect` search param, EC-150).\n * External/protocol-relative targets are rejected, mirroring\n * `postSignInTarget`.\n */\nexport function signInHref(location: { pathname: string; search?: string }): string {\n const target = `${location.pathname}${location.search ?? ''}`\n const redirect = target.startsWith('/') && !target.startsWith('//') ? target : '/'\n return `/sign-in?redirect=${encodeURIComponent(redirect)}`\n}\n"]}
1
+ {"version":3,"file":"auth-errors.js","sourceRoot":"","sources":["../../src/lib/auth-errors.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChangePasswordResult, SignInResult } from './auth/auth-adapter'\nimport { EN_MESSAGES } from './i18n/messages.en'\nimport type { OperationError } from './operations'\n\n/**\n * Session-expiry detection + plain-language mapping (EC-177 gap 9).\n *\n * An expired Convex session surfaces as a raw backend blob thrown by the\n * transport — e.g. `{\"code\":\"Unauthenticated\",\"message\":\"Could not verify\n * OIDC token claim…\"}` — which `toOperationError` can only normalize to an\n * `unknown`/`backend-unavailable` `OperationError` whose message *is* that\n * blob. Rendering it verbatim is editor-hostile, and the real problem (edits\n * silently stop persisting) is invisible.\n *\n * This module is the one chokepoint that recognizes that shape on the\n * structured `OperationError` (the seam every studio failure already flows\n * through), so UI surfaces never string-match raw messages themselves. Since\n * EC-403 an expired session is an app-level state: `lib/session-expiry.ts`\n * runs this predicate over every failed operation on the lifecycle bus and\n * raises the blocking in-place re-auth overlay (the\n * `error.sessionExpiredTitle` / `error.sessionExpired` catalog keys) — the\n * shared banner only uses it to route expiry AWAY from per-surface rendering.\n */\n\n/**\n * Extract the `code` of a JSON object embedded in a transport error message.\n * Convex auth failures carry `{\"code\":\"Unauthenticated\", …}`, sometimes\n * wrapped in client prefixes (`[CONVEX M(graph:save)] … Server Error: {…}`).\n */\nfunction embeddedJsonCode(message: string): string | undefined {\n const start = message.indexOf('{')\n const end = message.lastIndexOf('}')\n if (start === -1 || end <= start) return undefined\n try {\n const parsed: unknown = JSON.parse(message.slice(start, end + 1))\n if (parsed !== null && typeof parsed === 'object') {\n const code = (parsed as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n } catch {\n // Not a JSON payload — fall through to the marker checks.\n }\n return undefined\n}\n\n/**\n * Does this operation failure mean \"the session is no longer valid\"?\n *\n * Only the transport-shaped codes can carry the backend auth blob: structured\n * studio-level failures (`validation`, `unauthorized` from the role hook,\n * `not-found`, `conflict`) are never session expiry. Within those, detect the\n * Convex unauthenticated shape: an embedded `{\"code\":\"Unauthenticated\"}`\n * payload, the bare `Unauthenticated` server-error marker, or the OIDC claim\n * message it ships with.\n */\nexport function isSessionExpiredError(error: OperationError): boolean {\n if (error.code !== 'unknown' && error.code !== 'backend-unavailable') return false\n if (embeddedJsonCode(error.message) === 'Unauthenticated') return true\n if (/\\bUnauthenticated\\b/.test(error.message)) return true\n return /could not verify oidc token/i.test(error.message)\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The\n * auth adapters (no-touch `lib/auth/**`) return plain-English `message`\n * constants on the EC-362/EC-363 classified reasons; this table names the\n * catalog key that carries each of those messages, so the sign-in screen\n * localizes failures through the SAME catalog as the rest of the chrome\n * (a German catalog covers them with no consumer-side mapper — the\n * pre-EC-375 Naturkinder `authErrorMessage` is retired by this seam).\n *\n * A reason can carry several distinct engine messages (`unavailable` spans\n * the rate-limit, the honest server-error generic, and the playground\n * sign-up refusal), so each reason lists its candidate keys and a failure is\n * recognized by **exact EN-message match** against the catalog entry. That\n * makes English rendering byte-identical by construction and keeps the\n * fallback honest: any unrecognized message (a future reason, a reworded\n * adapter constant, a non-engine adapter) falls through to the raw\n * `result.message` — never a mistranslated or stale mapping.\n * `lib/auth-errors.test.ts` runs the real classifier over every branch to\n * pin that adapter copy and catalog never drift apart silently.\n */\nexport const SIGN_IN_ERROR_KEYS: Record<SignInFailure['reason'], readonly string[]> = {\n 'invalid-credentials': [\n 'auth.signIn.error.invalidCredentials',\n 'auth.signIn.error.invalidCredentialsSignUp',\n ],\n 'account-exists': ['auth.signIn.error.accountExists'],\n 'deployment-misconfigured': ['auth.signIn.error.deploymentMisconfigured'],\n 'not-invited': ['auth.signIn.error.notInvited'],\n unavailable: [\n 'auth.signIn.error.tooManyAttempts',\n 'auth.signIn.error.serverError',\n 'auth.signIn.error.serverErrorSignUp',\n 'auth.signIn.error.unavailable',\n 'auth.signIn.error.playgroundSignUp',\n ],\n}\n\n/**\n * Resolve a failure `{ reason, message }` against a reason → candidate-keys\n * table by **exact EN-message match** (the EC-397 recognition rule): the\n * matched key's catalog entry carries the same English, so EN rendering stays\n * byte-identical, and anything unrecognized returns `null` — the caller\n * renders the raw message, never a stale mapping.\n */\nfunction errorMessageKey(\n table: Record<string, readonly string[] | undefined>,\n failure: { reason: string; message: string },\n): string | null {\n const candidates = table[failure.reason]\n return candidates?.find((key) => EN_MESSAGES[key]?.message === failure.message) ?? null\n}\n\n/**\n * Resolve a failed {@link SignInResult} to its message-catalog key, or `null`\n * when the failure is not an engine-known shape (render `result.message` raw\n * — the honest fallback). Pure; accepts the failure structurally so callers\n * and tests can probe unknown future reasons without casts.\n */\nexport function signInErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(SIGN_IN_ERROR_KEYS, failure)\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Engine-owned change-password-failure → message-catalog-key mapping (EC-412)\n * — the {@link SIGN_IN_ERROR_KEYS} design extended to the account credential\n * panel. The reasons are the {@link ChangePasswordResult} failure union;\n * `unavailable` again spans several deliberate messages (the\n * deployment-misconfigured remedy, the EC-377 rate limit, and the honest\n * server-error generic), so recognition is by reason + exact EN-message match\n * and every unrecognized shape falls through to the raw `result.message`.\n * `lib/auth-errors.test.ts` runs the real `classifyChangePasswordError` over\n * every branch to pin adapter copy and catalog together.\n */\nexport const CHANGE_PASSWORD_ERROR_KEYS: Record<\n ChangePasswordFailure['reason'],\n readonly string[]\n> = {\n 'invalid-credentials': ['account.password.error.currentIncorrect'],\n 'weak-password': ['account.password.error.weakPassword'],\n 'not-authenticated': ['account.password.error.notSignedIn'],\n unavailable: [\n 'account.password.error.deploymentMisconfigured',\n 'account.password.error.tooManyAttempts',\n 'account.password.error.serverError',\n ],\n unsupported: ['account.password.error.playgroundUnsupported'],\n}\n\n/**\n * Resolve a failed {@link ChangePasswordResult} to its message-catalog key, or\n * `null` when the failure is not an engine-known shape (render\n * `result.message` raw — the honest fallback).\n */\nexport function changePasswordErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(CHANGE_PASSWORD_ERROR_KEYS, failure)\n}\n\n// `signInHref` (the banner's navigating \"Sign in again\" action) was retired by\n// EC-403: re-auth happens in place via the blocking overlay — a navigation\n// would destroy exactly the unsaved editor state the treatment protects.\n"]}
@@ -9,4 +9,5 @@
9
9
  export type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types.js';
10
10
  export { msg } from './types.js';
11
11
  export { EN_MESSAGES } from './messages.en.js';
12
+ export type { MessageKey, TranslationCatalog } from './messages.en.js';
12
13
  export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel, parseAcceptLanguage, setServerLanguagesResolver, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/i18n/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,QAAQ,EACR,eAAe,EACf,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,QAAQ,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/i18n` — studio-UI localization (EC-241).\n *\n * - Public config surface (`StudioI18nConfig`, `MessageCatalog`, `msg`) is re-exported\n * from `@elytracms/studio/customization` so a consumer authors catalogs there.\n * - Studio components localize chrome via `translate`/`useT` and schema labels via\n * `useLabel`.\n */\nexport type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types'\nexport { msg } from './types'\nexport { EN_MESSAGES } from './messages.en'\nexport {\n resolveActiveLocale,\n resolveStudioI18n,\n translate,\n localizedLabel,\n msgLabel,\n resolveMsgLabel,\n useT,\n useUiLocale,\n useLabel,\n parseAcceptLanguage,\n setServerLanguagesResolver,\n setUiLocaleForTests,\n resetStudioI18nForTests,\n} from './i18n'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/i18n/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE3C,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,QAAQ,EACR,eAAe,EACf,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,QAAQ,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/i18n` — studio-UI localization (EC-241).\n *\n * - Public config surface (`StudioI18nConfig`, `MessageCatalog`, `msg`) is re-exported\n * from `@elytracms/studio/customization` so a consumer authors catalogs there.\n * - Studio components localize chrome via `translate`/`useT` and schema labels via\n * `useLabel`.\n */\nexport type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types'\nexport { msg } from './types'\nexport { EN_MESSAGES } from './messages.en'\nexport type { MessageKey, TranslationCatalog } from './messages.en'\nexport {\n resolveActiveLocale,\n resolveStudioI18n,\n translate,\n localizedLabel,\n msgLabel,\n resolveMsgLabel,\n useT,\n useUiLocale,\n useLabel,\n parseAcceptLanguage,\n setServerLanguagesResolver,\n setUiLocaleForTests,\n resetStudioI18nForTests,\n} from './i18n'\n"]}
@@ -0,0 +1,24 @@
1
+ import type { TranslationCatalog } from './messages.en.js';
2
+ /**
3
+ * The engine-shipped **German** studio-chrome catalog (EC-398), importable as
4
+ * `@elytracms/studio/locales/de` and passed by reference into
5
+ * `defineStudio({ i18n: { locales: { de: deMessages } } })` — translations stay
6
+ * code passed directly (EC-241's principle; no registry, no runtime fetch). A
7
+ * consumer overrides individual strings by spreading:
8
+ * `{ ...deMessages, 'some.key': 'eigene Formulierung' }`.
9
+ *
10
+ * Seeded 2026-07-15 as the best-of merge of the two hand-maintained consumer
11
+ * catalogs (Naturkinder wins conflicts) + fresh translations for the keys both
12
+ * lacked (incl. EC-397's sign-in/boot copy). Typed against
13
+ * {@link TranslationCatalog} so an unknown/stale key fails typecheck;
14
+ * `messages.de.test.ts` pins EXACT key parity with the EN source (no missing,
15
+ * no stale, matching `{var}` placeholders) — a new engine string fails the
16
+ * engine build until it is translated here.
17
+ *
18
+ * Sections mirror `messages.en.ts` so the files diff side-by-side.
19
+ *
20
+ * Glossar: Sammlung · Dokument · Feld · Medien · Komponente · Weiterleitung ·
21
+ * Verweis · Version · Entwurf · veröffentlichen · Benutzer · Rolle ·
22
+ * Einstellungen · Bereitstellung.
23
+ */
24
+ export declare const deMessages: TranslationCatalog;