@elytracms/studio 0.0.62 → 0.0.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/canvas-chrome-css.d.ts +54 -0
- package/dist/components/builder/canvas-chrome-css.js +164 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -0
- package/dist/components/builder/canvas-chrome.d.ts +122 -18
- package/dist/components/builder/canvas-chrome.js +148 -82
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +13 -0
- package/dist/components/builder/canvas-render-context.js +7 -5
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
- package/dist/components/builder/canvas-stylesheet.js +16 -9
- package/dist/components/builder/canvas-stylesheet.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
- package/dist/components/builder/canvas-theme-notice.js +46 -0
- package/dist/components/builder/canvas-theme-notice.js.map +1 -0
- package/dist/components/builder/canvas.js +17 -10
- package/dist/components/builder/canvas.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/builder/rich-text-toolbar.d.ts +23 -0
- package/dist/components/builder/rich-text-toolbar.js +24 -17
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +31 -0
- package/dist/components/builder/spacing-handles.js +49 -37
- package/dist/components/builder/spacing-handles.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/components/component-preview.d.ts +2 -1
- package/dist/components/components/component-preview.js +24 -25
- package/dist/components/components/component-preview.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 +1023 -0
- package/dist/lib/i18n/messages.de.js.map +1 -0
- package/dist/lib/i18n/messages.en.d.ts +884 -0
- package/dist/lib/i18n/messages.en.js +76 -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/backend-mode.d.ts +7 -0
- package/dist/lib/persistence/backend-mode.js +9 -0
- package/dist/lib/persistence/backend-mode.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/persistence/index.d.ts +1 -1
- package/dist/lib/persistence/index.js +1 -1
- package/dist/lib/persistence/index.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":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n }),\n [config],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment- or app-level precondition. The\n * per-user bucket (\"not a member\") is not a boot blocker — it is surfaced post-auth\n * (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) => c.status === 'fail' && (c.bucket === 'deployment' || c.bucket === 'app'),\n )\n}\n\n/** Deterministic boot state while the backend probe is in flight (also SSR output). */\nfunction DeploymentChecking() {\n return (\n <BootScreenFrame>\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.checking')}\n </Text>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
|
|
1
|
+
{"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEjE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC1G,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAMtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA2B;IACvE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,oBAAoB,EAAE,EAAE,EAAE,CAAC,CAAA;IACxD,iFAAiF;IACjF,kFAAkF;IAClF,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,EAAE;QACrC,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK,CAAC,CAAA;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,YAAY,GAAG,WAAW,CAC9B,GAA8B,EAAE,CAC9B,yBAAyB,CAAC;QACxB,OAAO,EAAE,MAAM;QACf,SAAS,EAAE,iBAAiB,EAAE;QAC9B,WAAW,EAAE,4BAA4B,EAAE;QAC3C,iBAAiB;KAClB,CAAC,EACJ,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAC5B,CAAA;IAED,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAY,GAAG,EAAE,CACjD,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CACjG,CAAA;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,CAAA;YACvD,OAAM;QACR,CAAC;QACD,MAAM,YAAY,GAAG,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACrD,MAAM,OAAO,GAAG,CAAC,SAAkD,EAAE,EAAE,CACrE,QAAQ,CAAC;YACP,KAAK,EAAE,UAAU;YACjB,MAAM,EAAE,yBAAyB,CAAC;gBAChC,OAAO,EAAE,MAAM;gBACf,YAAY;gBACZ,SAAS,EAAE,iBAAiB,EAAE;gBAC9B,WAAW,EAAE,4BAA4B,EAAE;gBAC3C,SAAS;gBACT,iBAAiB;aAClB,CAAC;SACH,CAAC,CAAA;QACJ,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAA;QAC/B,KAAK,wBAAwB,EAAE;aAC5B,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;aACnC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACf,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAC9F;aACA,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CACxB,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAC9F,CAAA;IACL,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,iBAAiB,CAAC,CAAC,CAAA;IAE7C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,KAAK,EAAE,CAAA;IACvC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAExB,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,KAAC,kBAAkB,KAAG,CAAA;IAC7D,IAAI,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,CACL,KAAC,qBAAqB,IACpB,MAAM,EAAE,KAAK,CAAC,MAAM,EACpB,OAAO,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,GACrD,CACH,CAAA;IACH,CAAC;IACD,OAAO,4BAAG,QAAQ,GAAI,CAAA;AACxB,CAAC;AAID;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,MAAiC;IACzD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CACzB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,MAAM,KAAK,MAAM;QACnB,CAAC,CAAC,CAAC,MAAM,KAAK,YAAY,IAAI,CAAC,CAAC,MAAM,KAAK,gBAAgB,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,CACrF,CAAA;AACH,CAAC;AAED,uFAAuF;AACvF,SAAS,kBAAkB;IACzB,OAAO,CACL,KAAC,eAAe,cACd,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,gCAAgC,CAAC,GACvC,GACS,CACnB,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,GAIR;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAA;IACjE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,gBAAgB,CAAC,CAAA;IAC5E,OAAO,CACL,KAAC,eAAe,cACd,MAAC,IAAI,IAAC,SAAS,EAAC,iCAAiC,aAC/C,KAAC,UAAU,cACT,KAAC,SAAS,cAAE,SAAS,CAAC,6BAA6B,CAAC,GAAa,GACtD,EACb,KAAC,WAAW,cACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,mCAAmC,CAAC,GAC1C,EACP,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,MAAM,gBAAa,SAAS,CAAC,mCAAmC,CAAC,YAClF,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACvB,KAAC,QAAQ,IAAgB,KAAK,EAAE,KAAK,IAAtB,KAAK,CAAC,EAAE,CAAkB,CAC1C,CAAC,GACI,EACP,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAC,kBAAkB,IAAC,MAAM,EAAE,SAAS,GAAI,CAAC,CAAC,CAAC,IAAI,EACvE,OAAO,CAAC,CAAC,CAAC,CACT,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,YAAY,YACzE,SAAS,CAAC,6BAA6B,CAAC,GAClC,CACV,CAAC,CAAC,CAAC,IAAI,IACF,GACI,IACT,GACS,CACnB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,EAAE,MAAM,EAAgC;IAClE,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,aAAa,YACpC,SAAS,CAAC,kCAAkC,CAAC,GACzC,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,sCAAsC,CAAC,GAC7C,EACP,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,MAAM,gBAAa,SAAS,CAAC,kCAAkC,CAAC,YACjF,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,KAAC,WAAW,IAAgB,KAAK,EAAE,KAAK,IAAtB,KAAK,CAAC,EAAE,CAAkB,CAC7C,CAAC,GACI,IACF,CACT,CAAA;AACH,CAAC;AAED,MAAM,gBAAgB,GAA6C;IACjE,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,OAAO,EAAE,GAAG;CACb,CAAA;AAED,SAAS,WAAW,CAAC,EAAE,KAAK,EAA6B;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,MAAM,CAAA;IACvC,OAAO,CACL,eACE,IAAI,EAAC,UAAU,EACf,SAAS,EAAE,yBAAyB,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,2BAA2B,EAAE,aAEtH,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,QAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,aACrE,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC,QAAI,KAAK,CAAC,EAAE,SACtC,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,MAAM,GACR,EACN,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CACnE,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,MAAM,aAC1C,SAAS,CAAC,gCAAgC,CAAC,QAAG,KAAC,IAAI,IAAC,SAAS,EAAC,WAAW,YAAE,KAAK,CAAC,GAAG,GAAQ,IACxF,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,EAAE,KAAK,EAA6B;IACpD,OAAO,CACL,eAAK,IAAI,EAAC,UAAU,EAAC,SAAS,EAAC,8DAA8D,aAC3F,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,QAAC,SAAS,EAAC,kBAAkB,kBAC7C,KAAK,CAAC,EAAE,SACL,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,YAAE,KAAK,CAAC,MAAM,GAAQ,EACpC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CACX,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,MAAM,aAC1C,SAAS,CAAC,gCAAgC,CAAC,QAAG,KAAC,IAAI,IAAC,SAAS,EAAC,WAAW,YAAE,KAAK,CAAC,GAAG,GAAQ,IACxF,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,EAAE,QAAQ,EAA2B;IAC5D,OAAO,cAAK,SAAS,EAAC,qDAAqD,YAAE,QAAQ,GAAO,CAAA;AAC9F,CAAC","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/** Deterministic boot state while the backend probe is in flight (also SSR output). */\nfunction DeploymentChecking() {\n return (\n <BootScreenFrame>\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.checking')}\n </Text>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
|
|
@@ -5,13 +5,15 @@ import type { OperationError } from '@elytracms/studio/lib/operations';
|
|
|
5
5
|
* `OperationError` — including its validation issues — is always *visible*,
|
|
6
6
|
* never a silent failure.
|
|
7
7
|
*
|
|
8
|
-
* Session expiry (EC-177 gap 9) is
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:
|
|
9
|
+
* an expired session is an app-level state, so the banner funnels the error
|
|
10
|
+
* into the global session-expiry chokepoint (which raises the blocking
|
|
11
|
+
* in-place re-auth overlay) and renders nothing itself — the per-surface
|
|
12
|
+
* banner path remains only for non-expiry failures. The raw OIDC blob is
|
|
13
|
+
* never shown either way.
|
|
12
14
|
*/
|
|
13
15
|
export declare function OperationErrorBanner({ error, onDismiss, className, }: {
|
|
14
16
|
error: OperationError;
|
|
15
17
|
onDismiss?: () => void;
|
|
16
18
|
className?: string;
|
|
17
|
-
}): import("react").JSX.Element;
|
|
19
|
+
}): import("react").JSX.Element | null;
|
|
@@ -1,55 +1,38 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { CircleAlert, X } from 'lucide-react';
|
|
4
|
+
import { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors';
|
|
5
|
+
import { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry';
|
|
4
6
|
import { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui';
|
|
5
7
|
import { operationErrorLabel } from '@elytracms/studio/lib/persona/labels';
|
|
6
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
-
import { useOptionalAuth } from './auth-context.js';
|
|
8
9
|
/**
|
|
9
10
|
* Shared error surface for failed/denied operations (EC-141). Every studio
|
|
10
11
|
* surface renders rejected commands through this banner so a structured
|
|
11
12
|
* `OperationError` — including its validation issues — is always *visible*,
|
|
12
13
|
* never a silent failure.
|
|
13
14
|
*
|
|
14
|
-
* Session expiry (EC-177 gap 9) is
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:
|
|
16
|
+
* an expired session is an app-level state, so the banner funnels the error
|
|
17
|
+
* into the global session-expiry chokepoint (which raises the blocking
|
|
18
|
+
* in-place re-auth overlay) and renders nothing itself — the per-surface
|
|
19
|
+
* banner path remains only for non-expiry failures. The raw OIDC blob is
|
|
20
|
+
* never shown either way.
|
|
18
21
|
*/
|
|
19
22
|
export function OperationErrorBanner({ error, onDismiss, className, }) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// Funnel every error into the EC-403 chokepoint (belt-and-braces for a
|
|
24
|
+
// failure that reached this surface without passing the operations event
|
|
25
|
+
// bus). Deduped per error instance there, so re-rendering a stale
|
|
26
|
+
// persistError after re-auth never re-raises the overlay.
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
reportSessionExpiredError(error);
|
|
29
|
+
}, [error]);
|
|
30
|
+
if (isSessionExpiredError(error))
|
|
31
|
+
return null;
|
|
23
32
|
return (_jsx("div", { role: "alert", className: 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? ''), children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "error", title: error.code, "data-code": error.code, children: operationErrorLabel(error.code) }), _jsx(Text, { size: "sm", className: "text-destructive", children: error.message })] }), error.code === 'validation' && error.issues.length > 0 ? (_jsx(Stack, { gap: "none", children: error.issues.map((issue, i) => (_jsx(Text, { size: "xs", tone: "muted",
|
|
24
33
|
// The raw graph path (`pages.0.root.slots…`) is structural
|
|
25
34
|
// machine addressing — kept inspectable on hover, never shown
|
|
26
35
|
// as visible copy to a non-technical editor (EC-192).
|
|
27
36
|
title: issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined, children: issue.message }, `${issue.code}-${i}`))) })) : null] }), onDismiss ? (_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('error.dismiss'), onClick: onDismiss, className: "shrink-0", children: _jsx(X, {}) })) : null] }) }));
|
|
28
37
|
}
|
|
29
|
-
/**
|
|
30
|
-
* The session-expired rendering of the banner: plain-language copy plus a
|
|
31
|
-
* "Sign in again" action that clears the stale session and routes to the
|
|
32
|
-
* sign-in screen, preserving the current location so a successful sign-in
|
|
33
|
-
* returns the editor to this exact screen.
|
|
34
|
-
*/
|
|
35
|
-
function SessionExpiredBanner({ onDismiss, className, }) {
|
|
36
|
-
const auth = useOptionalAuth();
|
|
37
|
-
async function handleSignInAgain() {
|
|
38
|
-
if (typeof window === 'undefined')
|
|
39
|
-
return;
|
|
40
|
-
const href = signInHref(window.location);
|
|
41
|
-
// Clear the stale (expired) session first: the route guard sends
|
|
42
|
-
// "signed-in" sessions away from /sign-in, so navigating with the dead
|
|
43
|
-
// session still bound would bounce straight back.
|
|
44
|
-
try {
|
|
45
|
-
await auth?.signOut();
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Even a failed sign-out must not strand the editor — proceed to the
|
|
49
|
-
// sign-in screen; the backend session is invalid either way.
|
|
50
|
-
}
|
|
51
|
-
window.location.assign(href);
|
|
52
|
-
}
|
|
53
|
-
return (_jsx("div", { role: "alert", className: 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? ''), children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsx(Stack, { gap: "1", className: "min-w-0 flex-1", children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "error", children: translate('error.signedOut') }), _jsx(Text, { size: "sm", className: "text-destructive", children: SESSION_EXPIRED_MESSAGE })] }) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => void handleSignInAgain(), className: "shrink-0", children: [_jsx(LogIn, {}), " ", SIGN_IN_AGAIN_LABEL] }), onDismiss ? (_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('error.dismiss'), onClick: onDismiss, className: "shrink-0", children: _jsx(X, {}) })) : null] }) }));
|
|
54
|
-
}
|
|
55
38
|
//# sourceMappingURL=operation-error.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-error.js","sourceRoot":"","sources":["../../src/components/operation-error.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"operation-error.js","sourceRoot":"","sources":["../../src/components/operation-error.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAA;AAChF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAA;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,EACnC,KAAK,EACL,SAAS,EACT,SAAS,GAKV;IACC,uEAAuE;IACvE,yEAAyE;IACzE,kEAAkE;IAClE,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE;QACb,yBAAyB,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7C,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EACP,sEAAsE,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,YAG5F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,KAAK,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAE,KAAK,CAAC,IAAI,eAAa,KAAK,CAAC,IAAI,YAC5D,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,GAC1B,EACR,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACzC,KAAK,CAAC,OAAO,GACT,IACH,EACL,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACxD,KAAC,KAAK,IAAC,GAAG,EAAC,MAAM,YACd,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9B,KAAC,IAAI,IAEH,IAAI,EAAC,IAAI,EACT,IAAI,EAAC,OAAO;gCACZ,2DAA2D;gCAC3D,8DAA8D;gCAC9D,sDAAsD;gCACtD,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,YAE5E,KAAK,CAAC,OAAO,IART,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,CASpB,CACR,CAAC,GACI,CACT,CAAC,CAAC,CAAC,IAAI,IACF,EACP,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,eAAe,CAAC,EACtC,OAAO,EAAE,SAAS,EAClB,SAAS,EAAC,UAAU,YAEpB,KAAC,CAAC,KAAG,GACE,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect } from 'react'\nimport { CircleAlert, X } from 'lucide-react'\nimport type { OperationError } from '@elytracms/studio/lib/operations'\nimport { isSessionExpiredError } from '@elytracms/studio/lib/auth-errors'\nimport { reportSessionExpiredError } from '@elytracms/studio/lib/session-expiry'\nimport { Badge, Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { operationErrorLabel } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Shared error surface for failed/denied operations (EC-141). Every studio\n * surface renders rejected commands through this banner so a structured\n * `OperationError` — including its validation issues — is always *visible*,\n * never a silent failure.\n *\n * Session expiry (EC-177 gap 9, reworked by EC-403) is routed, not rendered:\n * an expired session is an app-level state, so the banner funnels the error\n * into the global session-expiry chokepoint (which raises the blocking\n * in-place re-auth overlay) and renders nothing itself — the per-surface\n * banner path remains only for non-expiry failures. The raw OIDC blob is\n * never shown either way.\n */\nexport function OperationErrorBanner({\n error,\n onDismiss,\n className,\n}: {\n error: OperationError\n onDismiss?: () => void\n className?: string\n}) {\n // Funnel every error into the EC-403 chokepoint (belt-and-braces for a\n // failure that reached this surface without passing the operations event\n // bus). Deduped per error instance there, so re-rendering a stale\n // persistError after re-auth never re-raises the overlay.\n useEffect(() => {\n reportSessionExpiredError(error)\n }, [error])\n\n if (isSessionExpiredError(error)) return null\n\n return (\n <div\n role=\"alert\"\n className={\n 'rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 ' + (className ?? '')\n }\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"error\" title={error.code} data-code={error.code}>\n {operationErrorLabel(error.code)}\n </Badge>\n <Text size=\"sm\" className=\"text-destructive\">\n {error.message}\n </Text>\n </Row>\n {error.code === 'validation' && error.issues.length > 0 ? (\n <Stack gap=\"none\">\n {error.issues.map((issue, i) => (\n <Text\n key={`${issue.code}-${i}`}\n size=\"xs\"\n tone=\"muted\"\n // The raw graph path (`pages.0.root.slots…`) is structural\n // machine addressing — kept inspectable on hover, never shown\n // as visible copy to a non-technical editor (EC-192).\n title={issue.path && issue.path.length > 0 ? issue.path.join('.') : undefined}\n >\n {issue.message}\n </Text>\n ))}\n </Stack>\n ) : null}\n </Stack>\n {onDismiss ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('error.dismiss')}\n onClick={onDismiss}\n className=\"shrink-0\"\n >\n <X />\n </Button>\n ) : null}\n </Row>\n </div>\n )\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
2
|
+
import { isExplicitLauncherVisit, Link, Navigate, STUDIO_PROJECT_ROUTE, studioPath, useLocation, } from '@elytracms/studio/router';
|
|
3
3
|
import { Globe, LogIn, TerminalSquare } from 'lucide-react';
|
|
4
4
|
import { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
5
5
|
import { hasProjectSession } from '@elytracms/studio/lib/auth';
|
|
6
|
+
import { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace';
|
|
6
7
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
8
|
import { PlaygroundBadge } from './playground-badge.js';
|
|
8
9
|
import { SignInNeededBadge } from './sign-in-needed-badge.js';
|
|
@@ -24,7 +25,22 @@ export function ProjectLauncher({ forcedState }) {
|
|
|
24
25
|
const hasConfig = workspace.kind === 'config';
|
|
25
26
|
const visible = entries;
|
|
26
27
|
const recents = useRecentProjects(entries);
|
|
28
|
+
const location = useLocation();
|
|
27
29
|
const forced = forcedState && forcedState !== 'working' ? forcedState : null;
|
|
30
|
+
// EC-401: a single-project workspace skips the launcher hop — the bare `/`
|
|
31
|
+
// hit forwards straight into the sole project (replace, so Back never loops
|
|
32
|
+
// through the launcher). Explicit "all projects" navigations carry the
|
|
33
|
+
// history-state marker and keep the launcher visible, as do forced `?state=`
|
|
34
|
+
// verification states and a lone UNPROVISIONED entry (its setup gate lives
|
|
35
|
+
// here). Activation/recents are recorded by the project screen on arrival.
|
|
36
|
+
const autoForward = launcherAutoForwardTarget({
|
|
37
|
+
entries,
|
|
38
|
+
forcedState: forcedState != null,
|
|
39
|
+
explicitVisit: isExplicitLauncherVisit(location.state),
|
|
40
|
+
});
|
|
41
|
+
if (autoForward) {
|
|
42
|
+
return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: { projectId: autoForward.id, _splat: studioPath.index() }, replace: true }));
|
|
43
|
+
}
|
|
28
44
|
return (_jsxs("div", { className: "mx-auto w-full max-w-6xl px-6 py-8", children: [_jsxs(Row, { gap: "3", className: "mb-6", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { size: "display", children: translate('shell.projects') }), _jsx(PlaygroundBadge, {}), hasConfig ? (_jsxs(Badge, { variant: "outline", children: [_jsx(TerminalSquare, { className: "size-3" }), " ", workspace.source] })) : null] }), _jsx(Text, { tone: "muted", children: hasConfig
|
|
29
45
|
? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
|
|
30
46
|
: 'Open and manage your Elytra projects.' })] }), _jsx(Spacer, {})] }), recents.length > 0 && !forced ? (_jsxs(Stack, { gap: "2", className: "mb-6", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, className: "uppercase", children: translate('shell.projectList.recent') }), _jsx(Row, { gap: "2", wrap: true, children: recents.map((p) => (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: p.id, _splat: studioPath.index() }, onClick: () => studio.activate(p.id), className: "inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: p.name.charAt(0).toUpperCase() }), p.name] }, p.id))) })] })) : null, forced ? (_jsx(SectionStateView, { state: forced, title: forced === 'empty' ? translate('shell.projectList.empty.title') : undefined })) : visible.length === 0 ? (_jsx(SectionStateView, { state: "empty", title: translate('shell.projectList.empty.title'), description: hasConfig
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n Navigate,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useLocation,\n} from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n const location = useLocation()\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n // EC-401: a single-project workspace skips the launcher hop — the bare `/`\n // hit forwards straight into the sole project (replace, so Back never loops\n // through the launcher). Explicit \"all projects\" navigations carry the\n // history-state marker and keep the launcher visible, as do forced `?state=`\n // verification states and a lone UNPROVISIONED entry (its setup gate lives\n // here). Activation/recents are recorded by the project screen on arrival.\n const autoForward = launcherAutoForwardTarget({\n entries,\n forcedState: forcedState != null,\n explicitVisit: isExplicitLauncherVisit(location.state),\n })\n if (autoForward) {\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: autoForward.id, _splat: studioPath.index() }}\n replace\n />\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
|
|
@@ -9,3 +9,13 @@ import type { Project } from '@elytracms/core/studio-core';
|
|
|
9
9
|
export declare function ProjectSwitcher({ activeProject }: {
|
|
10
10
|
activeProject: Project;
|
|
11
11
|
}): import("react").JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* The switcher slot's single-project face (EC-401): the active project's name
|
|
14
|
+
* as a plain heading — same identity mark as the switcher trigger, but no
|
|
15
|
+
* button, no chevron, no dropdown. `StudioShell` renders this instead of
|
|
16
|
+
* {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the
|
|
17
|
+
* switcher returns the moment a second workspace entry exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ActiveProjectName({ project }: {
|
|
20
|
+
project: Project;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
2
|
+
import { launcherVisitState, STUDIO_PROJECT_ROUTE, studioPath, useNavigate, } from '@elytracms/studio/router';
|
|
3
3
|
import { Check, ChevronsUpDown, FolderOpen } from 'lucide-react';
|
|
4
4
|
import { hasProjectSession } from '@elytracms/studio/lib/auth';
|
|
5
5
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
-
import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
+
import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Heading, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
7
7
|
import { useStudio } from './studio-context.js';
|
|
8
8
|
import { useRecentProjects, useSwitcherEntries } from './workspace-projects.js';
|
|
9
9
|
import { SignInNeededBadge } from './sign-in-needed-badge.js';
|
|
@@ -24,7 +24,17 @@ export function ProjectSwitcher({ activeProject }) {
|
|
|
24
24
|
studio.activate(project.id);
|
|
25
25
|
navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } });
|
|
26
26
|
}
|
|
27
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/' }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
|
|
27
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/', state: launcherVisitState }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The switcher slot's single-project face (EC-401): the active project's name
|
|
31
|
+
* as a plain heading — same identity mark as the switcher trigger, but no
|
|
32
|
+
* button, no chevron, no dropdown. `StudioShell` renders this instead of
|
|
33
|
+
* {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the
|
|
34
|
+
* switcher returns the moment a second workspace entry exists.
|
|
35
|
+
*/
|
|
36
|
+
export function ActiveProjectName({ project }) {
|
|
37
|
+
return (_jsxs(Heading, { level: 1, size: "h4", className: "flex h-8 min-w-0 items-center gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: project.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate", children: project.name })] }));
|
|
28
38
|
}
|
|
29
39
|
function ProjectMenuItem({ project, active, onSelect, }) {
|
|
30
40
|
// Per-backend sign-in state (EC-179): the active project is signed-in by
|
|
@@ -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 {};
|