@elytracms/studio 0.0.54 → 0.0.55

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.
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createRootRoute, HeadContent, Navigate, Outlet, Scripts, useLocation, } from '@tanstack/react-router';
3
+ import { AuthProvider, useAuth } from '@elytracms/studio/components/auth-context';
4
+ import { DeploymentSetupGate } from '@elytracms/studio/components/deployment-setup-gate';
5
+ import { StudioProvider } from '@elytracms/studio/components/studio-context';
6
+ import { ThemeProvider } from '@elytracms/studio/components/theme-context';
7
+ import { Text } from '@elytracms/studio/ui';
8
+ import { SIGN_IN_PATH, resolveRouteGuard } from '@elytracms/studio/lib/auth';
9
+ import { activeSlugForLocation, setActiveProjectSlug } from '@elytracms/studio/lib/workspace';
10
+ import { THEME_INIT_SCRIPT } from '@elytracms/studio/lib/theme';
11
+ export function createStudioRootRoute(options) {
12
+ const { appCssUrl, title = 'Elytra', htmlLang = 'en', meta = [], links = [], providers } = options;
13
+ function RootComponent() {
14
+ const location = useLocation();
15
+ // EC-179: the active project selects its Convex backend. Resolve it from the
16
+ // URL (or the sign-in redirect target) and bind it *before* the auth and
17
+ // persistence adapters are selected, so the initial sign-in authenticates
18
+ // against the project the user is heading for. Keying the auth/studio subtree
19
+ // by the active project remounts it on a project switch, so the adapters
20
+ // re-select against the new backend and the session is restored per-backend.
21
+ const activeSlug = activeSlugForLocation(location.pathname, location.search.redirect);
22
+ setActiveProjectSlug(activeSlug);
23
+ return (_jsx(RootDocument, { htmlLang: htmlLang, children: _jsx(ThemeProvider, { children: _jsx(DeploymentSetupGate, { children: _jsx(AuthProvider, { children: _jsx(AuthGate, { children: _jsx(StudioProvider, { children: providers ? providers(_jsx(Outlet, {})) : _jsx(Outlet, {}) }) }) }, activeSlug ?? '__launcher__') }) }) }));
24
+ }
25
+ return createRootRoute({
26
+ head: () => ({
27
+ meta: [
28
+ { charSet: 'utf-8' },
29
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
30
+ { title },
31
+ ...meta,
32
+ ],
33
+ links: [{ rel: 'stylesheet', href: appCssUrl }, ...links],
34
+ }),
35
+ component: RootComponent,
36
+ });
37
+ }
38
+ /**
39
+ * Route guard (EC-150): one chokepoint in the root layout. Unauthenticated
40
+ * visitors on protected paths are redirected to the sign-in screen (with the
41
+ * intended target preserved); signed-in users never see the sign-in screen.
42
+ * The decision logic is the pure, unit-tested `resolveRouteGuard`.
43
+ */
44
+ function AuthGate({ children }) {
45
+ const { status } = useAuth();
46
+ const { pathname } = useLocation();
47
+ const decision = resolveRouteGuard(pathname, status);
48
+ if (decision.kind === 'pending')
49
+ return _jsx(SessionLoading, {});
50
+ if (decision.kind === 'redirect-to-sign-in') {
51
+ return _jsx(Navigate, { to: SIGN_IN_PATH, search: { redirect: decision.redirect }, replace: true });
52
+ }
53
+ if (decision.kind === 'redirect-home')
54
+ return _jsx(Navigate, { to: "/", replace: true });
55
+ return children;
56
+ }
57
+ /** Deterministic session-restore state (also the SSR output for protected paths). */
58
+ function SessionLoading() {
59
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background", children: _jsx(Text, { size: "sm", tone: "muted", children: "Checking session\u2026" }) }));
60
+ }
61
+ function RootDocument({ children, htmlLang }) {
62
+ // Dark-first SSR baseline; THEME_INIT_SCRIPT flips the class pre-hydration
63
+ // from the stored preference, and ThemeProvider keeps it reactive thereafter.
64
+ // suppressHydrationWarning: the script intentionally mutates the class before
65
+ // React hydrates, so a className mismatch on <html> is expected, not a bug.
66
+ // The app stylesheet is emitted via head().links and rendered by <HeadContent />.
67
+ return (_jsxs("html", { lang: htmlLang, className: "dark", suppressHydrationWarning: true, children: [_jsxs("head", { children: [_jsx(HeadContent, {}), _jsx("script", { dangerouslySetInnerHTML: { __html: THEME_INIT_SCRIPT } })] }), _jsxs("body", { className: "min-h-screen bg-background font-sans text-foreground antialiased", children: [children, _jsx(Scripts, {})] })] }));
68
+ }
69
+ //# sourceMappingURL=root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/router/root.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport {\n createRootRoute,\n HeadContent,\n Navigate,\n Outlet,\n Scripts,\n useLocation,\n} from '@tanstack/react-router'\nimport { AuthProvider, useAuth } from '@elytracms/studio/components/auth-context'\nimport { DeploymentSetupGate } from '@elytracms/studio/components/deployment-setup-gate'\nimport { StudioProvider } from '@elytracms/studio/components/studio-context'\nimport { ThemeProvider } from '@elytracms/studio/components/theme-context'\nimport { Text } from '@elytracms/studio/ui'\nimport { SIGN_IN_PATH, resolveRouteGuard } from '@elytracms/studio/lib/auth'\nimport { activeSlugForLocation, setActiveProjectSlug } from '@elytracms/studio/lib/workspace'\nimport { THEME_INIT_SCRIPT } from '@elytracms/studio/lib/theme'\n\n/**\n * Studio root-route boot factory (EC-375; consolidated from the copy-pasted\n * consumer `__root.tsx` by EC-369 finding #7). It assembles the ONE canonical\n * provider/gate stack — ThemeProvider → DeploymentSetupGate → AuthProvider(keyed)\n * → AuthGate → StudioProvider → Outlet — plus the `RootDocument` shell\n * (html/head with the pre-hydration theme script + the chrome stylesheet).\n *\n * Its whole purpose is to make the NEXT required-gate addition automatic on\n * package upgrade: when EC-258 added `DeploymentSetupGate`, every hand-copied\n * `__root.tsx` had to be manually re-synced, and Konditorei's silently missed\n * it for five releases (EC-367). Owning the composition here means a consumer's\n * `__root.tsx` shrinks to a thin shell and picks up new gates for free.\n *\n * The consumer re-exports the returned route as `Route` (the generated route\n * tree imports `Route` from `./routes/__root`):\n *\n * ```tsx\n * import { createStudioRootRoute } from '@elytracms/studio/router/root'\n * import appCss from '../styles/app.css?url'\n * export const Route = createStudioRootRoute({ appCssUrl: appCss })\n * ```\n */\nexport interface StudioRootRouteOptions {\n /**\n * URL of the studio chrome stylesheet, from the consumer's Vite `?url` import\n * (`import appCss from '../styles/app.css?url'`). The one option every consumer\n * must pass — the sheet is project-owned (it carries the delivery theme).\n */\n appCssUrl: string\n /** Document `<title>`. Default: `'Elytra'`. */\n title?: string\n /** `<html lang>`. Default: `'en'`. */\n htmlLang?: string\n /**\n * Extra `<head>` meta entries appended after the defaults (charset, viewport,\n * title). Same shape TanStack Router's `head().meta` takes.\n */\n meta?: Array<Record<string, string>>\n /** Extra `<head>` link entries appended after the app stylesheet. */\n links?: Array<Record<string, string>>\n /**\n * Escape hatch: wrap additional project-specific providers around the routed\n * subtree. Rendered INSIDE `StudioProvider` and around `<Outlet />`, so nested\n * providers see the full studio/auth/theme context and remount with the active\n * project. Return the passed `children` wrapped in your providers.\n */\n providers?: (children: ReactNode) => ReactNode\n}\n\nexport function createStudioRootRoute(options: StudioRootRouteOptions) {\n const { appCssUrl, title = 'Elytra', htmlLang = 'en', meta = [], links = [], providers } = options\n\n function RootComponent() {\n const location = useLocation()\n // EC-179: the active project selects its Convex backend. Resolve it from the\n // URL (or the sign-in redirect target) and bind it *before* the auth and\n // persistence adapters are selected, so the initial sign-in authenticates\n // against the project the user is heading for. Keying the auth/studio subtree\n // by the active project remounts it on a project switch, so the adapters\n // re-select against the new backend and the session is restored per-backend.\n const activeSlug = activeSlugForLocation(\n location.pathname,\n (location.search as { redirect?: string }).redirect,\n )\n setActiveProjectSlug(activeSlug)\n\n return (\n <RootDocument htmlLang={htmlLang}>\n <ThemeProvider>\n {/* EC-258: one deployment-setup gate runs before auth — a misconfigured or\n unreachable backend, an invalid workspace config, or any failing readiness\n precondition (missing auth signing key, required collections, …) is a\n single visible boot screen with copy-pasteable fixes, never a silent\n in-memory fallback or a misleading downstream error. All checks passing\n renders children as before. */}\n <DeploymentSetupGate>\n <AuthProvider key={activeSlug ?? '__launcher__'}>\n <AuthGate>\n <StudioProvider>{providers ? providers(<Outlet />) : <Outlet />}</StudioProvider>\n </AuthGate>\n </AuthProvider>\n </DeploymentSetupGate>\n </ThemeProvider>\n </RootDocument>\n )\n }\n\n return createRootRoute({\n head: () => ({\n meta: [\n { charSet: 'utf-8' },\n { name: 'viewport', content: 'width=device-width, initial-scale=1' },\n { title },\n ...meta,\n ],\n links: [{ rel: 'stylesheet', href: appCssUrl }, ...links],\n }),\n component: RootComponent,\n })\n}\n\n/**\n * Route guard (EC-150): one chokepoint in the root layout. Unauthenticated\n * visitors on protected paths are redirected to the sign-in screen (with the\n * intended target preserved); signed-in users never see the sign-in screen.\n * The decision logic is the pure, unit-tested `resolveRouteGuard`.\n */\nfunction AuthGate({ children }: { children: ReactNode }) {\n const { status } = useAuth()\n const { pathname } = useLocation()\n const decision = resolveRouteGuard(pathname, status)\n\n if (decision.kind === 'pending') return <SessionLoading />\n if (decision.kind === 'redirect-to-sign-in') {\n return <Navigate to={SIGN_IN_PATH} search={{ redirect: decision.redirect }} replace />\n }\n if (decision.kind === 'redirect-home') return <Navigate to=\"/\" replace />\n return children\n}\n\n/** Deterministic session-restore state (also the SSR output for protected paths). */\nfunction SessionLoading() {\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background\">\n <Text size=\"sm\" tone=\"muted\">\n Checking session…\n </Text>\n </div>\n )\n}\n\nfunction RootDocument({ children, htmlLang }: { children: ReactNode; htmlLang: string }) {\n // Dark-first SSR baseline; THEME_INIT_SCRIPT flips the class pre-hydration\n // from the stored preference, and ThemeProvider keeps it reactive thereafter.\n // suppressHydrationWarning: the script intentionally mutates the class before\n // React hydrates, so a className mismatch on <html> is expected, not a bug.\n // The app stylesheet is emitted via head().links and rendered by <HeadContent />.\n return (\n <html lang={htmlLang} className=\"dark\" suppressHydrationWarning>\n <head>\n <HeadContent />\n <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />\n </head>\n <body className=\"min-h-screen bg-background font-sans text-foreground antialiased\">\n {children}\n <Scripts />\n </body>\n </html>\n )\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.54",
4
+ "version": "0.0.55",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/router/index.d.ts",
21
21
  "default": "./dist/router/index.js"
22
22
  },
23
+ "./router/root": {
24
+ "types": "./dist/router/root.d.ts",
25
+ "default": "./dist/router/root.js"
26
+ },
23
27
  "./router/*": {
24
28
  "types": "./dist/router/*.d.ts",
25
29
  "default": "./dist/router/*.js"
@@ -137,7 +141,7 @@
137
141
  "lucide-react": "^1.23.0",
138
142
  "tailwind-merge": "^3.6.0",
139
143
  "zod": "^4.4.3",
140
- "@elytracms/core": "^0.0.54"
144
+ "@elytracms/core": "^0.0.55"
141
145
  },
142
146
  "peerDependencies": {
143
147
  "@tanstack/react-router": "^1.170.0",