@elytracms/studio 0.0.62 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account/credential-panel.js +6 -1
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/auth-context.d.ts +14 -0
- package/dist/components/auth-context.js +32 -2
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -0
- package/dist/components/builder/blocks-model.js +5 -0
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +2 -2
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
- package/dist/components/builder/fixture-thumbnail.js +14 -0
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/cms/cms-index.js +6 -3
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +40 -8
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/operation-error.d.ts +7 -5
- package/dist/components/operation-error.js +19 -36
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.js +17 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +10 -0
- package/dist/components/project-switcher.js +13 -3
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +27 -0
- package/dist/components/session-expired-overlay.js +51 -0
- package/dist/components/session-expired-overlay.js.map +1 -0
- package/dist/components/sign-in-view.d.ts +13 -1
- package/dist/components/sign-in-view.js +27 -13
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-project-screen.js +2 -2
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +40 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/customization.d.ts +4 -1
- package/dist/customization.js +3 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +58 -28
- package/dist/lib/auth-errors.js +93 -15
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +1 -0
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +24 -0
- package/dist/lib/i18n/messages.de.js +1019 -0
- package/dist/lib/i18n/messages.de.js.map +1 -0
- package/dist/lib/i18n/messages.en.d.ts +880 -0
- package/dist/lib/i18n/messages.en.js +71 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +7 -0
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
- package/dist/lib/persistence/deployment-readiness.js +16 -1
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +67 -0
- package/dist/lib/session-expiry.js +72 -0
- package/dist/lib/session-expiry.js.map +1 -0
- package/dist/lib/tab-title.d.ts +53 -0
- package/dist/lib/tab-title.js +77 -0
- package/dist/lib/tab-title.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +27 -0
- package/dist/lib/workspace/switcher-source.js +30 -0
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/index.js.map +1 -1
- package/dist/router/launcher-visit.d.ts +13 -0
- package/dist/router/launcher-visit.js +34 -0
- package/dist/router/launcher-visit.js.map +1 -0
- package/dist/router/root.js +12 -2
- package/dist/router/root.js.map +1 -1
- package/dist/ui/dialog.d.ts +15 -0
- package/dist/ui/dialog.js +17 -0
- package/dist/ui/dialog.js.map +1 -1
- package/package.json +6 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={() => navigate({ to: '/' })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The blocking in-place re-auth overlay (EC-403). Mounted once in the studio
|
|
3
|
+
* root (inside `StudioProvider`, over every route) and shown while the auth
|
|
4
|
+
* store reports `sessionExpired`: the backend session died mid-flight, so all
|
|
5
|
+
* editing is blocked behind a non-dismissible modal that mounts the SAME
|
|
6
|
+
* `SignInScreen` composition the `/sign-in` route uses — **without any
|
|
7
|
+
* navigation**. The React tree beneath stays alive, so every unsaved editor
|
|
8
|
+
* state survives; a successful sign-in applies the fresh session (clearing the
|
|
9
|
+
* flag in the auth store) and editing resumes exactly where it stopped.
|
|
10
|
+
* Pending failed writes keep their normal retry paths (a `persistError`
|
|
11
|
+
* clears on the next successful save).
|
|
12
|
+
*
|
|
13
|
+
* Re-auth is strictly sign-in (no create-account toggle — a brand-new account
|
|
14
|
+
* cannot resume an existing editing session). On success the studio query
|
|
15
|
+
* cache is invalidated wholesale so reads that failed or staled while the
|
|
16
|
+
* session was dead recover without a reload.
|
|
17
|
+
*
|
|
18
|
+
* Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
|
|
19
|
+
* focus-trapped, Escape/outside interaction consumed, studio beneath inert.
|
|
20
|
+
*/
|
|
21
|
+
export declare function SessionExpiredOverlay(): import("react").JSX.Element | null;
|
|
22
|
+
/**
|
|
23
|
+
* Why the sign-in screen appeared in place (EC-403): the session-expired copy
|
|
24
|
+
* rendered above the form via the `SignInScreen` notice slot. Pure — exported
|
|
25
|
+
* for the provider-free static-render smoke test.
|
|
26
|
+
*/
|
|
27
|
+
export declare function SessionExpiredNotice(): import("react").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CircleAlert } from 'lucide-react';
|
|
3
|
+
import { LOCAL_DEV_CREDENTIALS, authMode } from '@elytracms/studio/lib/auth';
|
|
4
|
+
import { useT } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
import { useOperationCache } from '@elytracms/studio/lib/query';
|
|
6
|
+
import { Dialog, DialogBlockingContent, DialogTitle, Row, Stack, Text } from '@elytracms/studio/ui';
|
|
7
|
+
import { useAuth } from './auth-context.js';
|
|
8
|
+
import { SignInScreen, activeProjectName } from './sign-in-view.js';
|
|
9
|
+
/**
|
|
10
|
+
* The blocking in-place re-auth overlay (EC-403). Mounted once in the studio
|
|
11
|
+
* root (inside `StudioProvider`, over every route) and shown while the auth
|
|
12
|
+
* store reports `sessionExpired`: the backend session died mid-flight, so all
|
|
13
|
+
* editing is blocked behind a non-dismissible modal that mounts the SAME
|
|
14
|
+
* `SignInScreen` composition the `/sign-in` route uses — **without any
|
|
15
|
+
* navigation**. The React tree beneath stays alive, so every unsaved editor
|
|
16
|
+
* state survives; a successful sign-in applies the fresh session (clearing the
|
|
17
|
+
* flag in the auth store) and editing resumes exactly where it stopped.
|
|
18
|
+
* Pending failed writes keep their normal retry paths (a `persistError`
|
|
19
|
+
* clears on the next successful save).
|
|
20
|
+
*
|
|
21
|
+
* Re-auth is strictly sign-in (no create-account toggle — a brand-new account
|
|
22
|
+
* cannot resume an existing editing session). On success the studio query
|
|
23
|
+
* cache is invalidated wholesale so reads that failed or staled while the
|
|
24
|
+
* session was dead recover without a reload.
|
|
25
|
+
*
|
|
26
|
+
* Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
|
|
27
|
+
* focus-trapped, Escape/outside interaction consumed, studio beneath inert.
|
|
28
|
+
*/
|
|
29
|
+
export function SessionExpiredOverlay() {
|
|
30
|
+
const auth = useAuth();
|
|
31
|
+
const t = useT();
|
|
32
|
+
const cache = useOperationCache();
|
|
33
|
+
if (!auth.sessionExpired)
|
|
34
|
+
return null;
|
|
35
|
+
return (_jsx(Dialog, { open: true, children: _jsxs(DialogBlockingContent, { "aria-describedby": undefined, children: [_jsx(DialogTitle, { className: "sr-only", children: t('error.sessionExpiredTitle') }), _jsx(SignInScreen, { notice: _jsx(SessionExpiredNotice, {}), projectName: activeProjectName(), showModeToggle: false, devCredentials: authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null, authenticate: (_mode, email, password) => auth.signIn(email, password), onAuthenticated: () =>
|
|
36
|
+
// The session was dead for an unknown span: reads may have failed
|
|
37
|
+
// (retry: false) or gone stale. One wholesale invalidation lets
|
|
38
|
+
// every active query refetch under the fresh session — resume,
|
|
39
|
+
// not reload. (The empty key prefix matches all queries.)
|
|
40
|
+
cache.invalidate([]) })] }) }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Why the sign-in screen appeared in place (EC-403): the session-expired copy
|
|
44
|
+
* rendered above the form via the `SignInScreen` notice slot. Pure — exported
|
|
45
|
+
* for the provider-free static-render smoke test.
|
|
46
|
+
*/
|
|
47
|
+
export function SessionExpiredNotice() {
|
|
48
|
+
const t = useT();
|
|
49
|
+
return (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2", children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "font-medium text-destructive", children: t('error.sessionExpiredTitle') }), _jsx(Text, { size: "sm", tone: "muted", children: t('error.sessionExpired') })] })] }) }));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=session-expired-overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-expired-overlay.js","sourceRoot":"","sources":["../../src/components/session-expired-overlay.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IAEjC,IAAI,CAAC,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAA;IAErC,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,kBACV,MAAC,qBAAqB,wBAAmB,SAAS,aAChD,KAAC,WAAW,IAAC,SAAS,EAAC,SAAS,YAAE,CAAC,CAAC,2BAA2B,CAAC,GAAe,EAC/E,KAAC,YAAY,IACX,MAAM,EAAE,KAAC,oBAAoB,KAAG,EAChC,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,KAAK,EACrB,cAAc,EAAE,QAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,EAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EACtE,eAAe,EAAE,GAAG,EAAE;oBACpB,kEAAkE;oBAClE,gEAAgE;oBAChE,+DAA+D;oBAC/D,0DAA0D;oBAC1D,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAEtB,IACoB,GACjB,CACV,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,qEAAqE,YAE/E,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,8BAA8B,YACrD,CAAC,CAAC,2BAA2B,CAAC,GAC1B,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,CAAC,CAAC,sBAAsB,CAAC,GACrB,IACD,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { CircleAlert } from 'lucide-react'\nimport { LOCAL_DEV_CREDENTIALS, authMode } from '@elytracms/studio/lib/auth'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport { Dialog, DialogBlockingContent, DialogTitle, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from './auth-context'\nimport { SignInScreen, activeProjectName } from './sign-in-view'\n\n/**\n * The blocking in-place re-auth overlay (EC-403). Mounted once in the studio\n * root (inside `StudioProvider`, over every route) and shown while the auth\n * store reports `sessionExpired`: the backend session died mid-flight, so all\n * editing is blocked behind a non-dismissible modal that mounts the SAME\n * `SignInScreen` composition the `/sign-in` route uses — **without any\n * navigation**. The React tree beneath stays alive, so every unsaved editor\n * state survives; a successful sign-in applies the fresh session (clearing the\n * flag in the auth store) and editing resumes exactly where it stopped.\n * Pending failed writes keep their normal retry paths (a `persistError`\n * clears on the next successful save).\n *\n * Re-auth is strictly sign-in (no create-account toggle — a brand-new account\n * cannot resume an existing editing session). On success the studio query\n * cache is invalidated wholesale so reads that failed or staled while the\n * session was dead recover without a reload.\n *\n * Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,\n * focus-trapped, Escape/outside interaction consumed, studio beneath inert.\n */\nexport function SessionExpiredOverlay() {\n const auth = useAuth()\n const t = useT()\n const cache = useOperationCache()\n\n if (!auth.sessionExpired) return null\n\n return (\n <Dialog open>\n <DialogBlockingContent aria-describedby={undefined}>\n <DialogTitle className=\"sr-only\">{t('error.sessionExpiredTitle')}</DialogTitle>\n <SignInScreen\n notice={<SessionExpiredNotice />}\n projectName={activeProjectName()}\n showModeToggle={false}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n authenticate={(_mode, email, password) => auth.signIn(email, password)}\n onAuthenticated={() =>\n // The session was dead for an unknown span: reads may have failed\n // (retry: false) or gone stale. One wholesale invalidation lets\n // every active query refetch under the fresh session — resume,\n // not reload. (The empty key prefix matches all queries.)\n cache.invalidate([])\n }\n />\n </DialogBlockingContent>\n </Dialog>\n )\n}\n\n/**\n * Why the sign-in screen appeared in place (EC-403): the session-expired copy\n * rendered above the form via the `SignInScreen` notice slot. Pure — exported\n * for the provider-free static-render smoke test.\n */\nexport function SessionExpiredNotice() {\n const t = useT()\n return (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2\"\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"font-medium text-destructive\">\n {t('error.sessionExpiredTitle')}\n </Text>\n <Text size=\"sm\" tone=\"muted\">\n {t('error.sessionExpired')}\n </Text>\n </Stack>\n </Row>\n </div>\n )\n}\n"]}
|
|
@@ -14,6 +14,12 @@ export interface SignInViewProps {
|
|
|
14
14
|
/** Product name in the "Sign in to …" heading. Default: `'Elytra'`. */
|
|
15
15
|
productName?: string;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The project whose backend this sign-in authenticates against (EC-179).
|
|
19
|
+
* Exported for the session-expired re-auth overlay (EC-403), which mounts
|
|
20
|
+
* {@link SignInScreen} with the same subtext.
|
|
21
|
+
*/
|
|
22
|
+
export declare function activeProjectName(): string | null;
|
|
17
23
|
/**
|
|
18
24
|
* The `/sign-in` route component. Container: reads the router (post-sign-in
|
|
19
25
|
* redirect target from the route search) + auth seams and hands the pure
|
|
@@ -26,6 +32,12 @@ interface SignInScreenProps {
|
|
|
26
32
|
showModeToggle: boolean;
|
|
27
33
|
/** Seeded dev accounts to list (playground mode), or null to hide the hint. */
|
|
28
34
|
devCredentials: readonly DevCredential[] | null;
|
|
35
|
+
/**
|
|
36
|
+
* Contextual notice rendered above the heading — the session-expired
|
|
37
|
+
* re-auth overlay (EC-403) explains WHY the screen appeared here. The
|
|
38
|
+
* normal `/sign-in` route passes none.
|
|
39
|
+
*/
|
|
40
|
+
notice?: ReactNode;
|
|
29
41
|
brandMark?: ReactNode;
|
|
30
42
|
productName?: string;
|
|
31
43
|
/** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */
|
|
@@ -37,5 +49,5 @@ interface SignInScreenProps {
|
|
|
37
49
|
* Provider-free presentational sign-in screen. Owns only local form state and
|
|
38
50
|
* calls injected callbacks — no context hooks — so it static-renders in tests.
|
|
39
51
|
*/
|
|
40
|
-
export declare function SignInScreen({ projectName, showModeToggle, devCredentials, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
|
|
52
|
+
export declare function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
|
|
41
53
|
export {};
|
|
@@ -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
|
-
/**
|
|
9
|
-
|
|
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
|
-
|
|
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 ? '
|
|
59
|
-
?
|
|
60
|
-
|
|
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
|
-
? '
|
|
63
|
-
: '
|
|
75
|
+
? t('auth.signIn.submittingCreate')
|
|
76
|
+
: t('auth.signIn.submitting')
|
|
64
77
|
: creating
|
|
65
|
-
? '
|
|
66
|
-
: '
|
|
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 ? '
|
|
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
|
-
|
|
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":"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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 { 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":";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,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AA0CzF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,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;AAsBD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,WAAW,EACX,cAAc,EACd,cAAc,EACd,MAAM,EACN,SAAS,EACT,WAAW,GAAG,QAAQ,EACtB,YAAY,EACZ,eAAe,GACG;IAClB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,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,sEAAsE;QACtE,2EAA2E;QAC3E,uDAAuD;QACvD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAA;QAC9C,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACjD,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,aACvC,MAAM,EACP,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,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,EAAE,EAAE,WAAW,EAAE,CAAC,GAC9E,EACV,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,WAAW;gCACV,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,8BAA8B,EAAE;oCAC5E,OAAO,EAAE,WAAW;iCACrB,CAAC;gCACJ,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,GAC/B,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,EAAE,CAAC,CAAC,wBAAwB,CAAC,EAAE,OAAO,EAAC,eAAe,EAAC,QAAQ,kBACzE,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,EAAE,CAAC,CAAC,8BAA8B,CAAC,EAC9C,QAAQ,EAAE,UAAU,GACpB,GACI,EACR,KAAC,KAAK,IACJ,KAAK,EAAE,CAAC,CAAC,2BAA2B,CAAC,EACrC,OAAO,EAAC,kBAAkB,EAC1B,QAAQ,QACR,KAAK,EAAE,KAAK,YAEZ,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,CAAC,CAAC,8BAA8B,CAAC;gDACnC,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC;4CAC/B,CAAC,CAAC,QAAQ;gDACR,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC;gDAC/B,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,GACtB,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,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,8BAA8B,CAAC,EAAE,GAAG,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,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kCAAkC,CAAC,GAC3E,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,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,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,YACjD,CAAC,CAAC,+BAA+B,CAAC,GAC9B,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 { 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":"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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"]}
|
package/dist/customization.d.ts
CHANGED
|
@@ -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';
|
package/dist/customization.js
CHANGED
|
@@ -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
|