diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json index 6916f27..6f26481 100644 --- a/artifacts/api-server/package.json +++ b/artifacts/api-server/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@clerk/express": "^2.1.61", "@workspace/api-zod": "workspace:*", "@workspace/db": "workspace:*", "cookie-parser": "^1.4.7", diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts index 0d60a0d..6375d64 100644 --- a/artifacts/api-server/src/app.ts +++ b/artifacts/api-server/src/app.ts @@ -4,6 +4,7 @@ import cors from "cors"; import pinoHttp from "pino-http"; import router from "./routes"; import { logger } from "./lib/logger"; +import { installStagingAccess } from "./middleware/staging-access"; const app: Express = express(); @@ -30,18 +31,22 @@ app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +installStagingAccess(app); + app.use("/api", router); // Railway runs the API and web client as a single service. This keeps relative // /api requests on the canonical domain when the built Vite client is served. -const clientDist = process.env["CLIENT_DIST"] ?? path.resolve( - import.meta.dirname, - "..", - "..", - "structured-liquidity", - "dist", - "public", -); +const clientDist = + process.env["CLIENT_DIST"] ?? + path.resolve( + import.meta.dirname, + "..", + "..", + "structured-liquidity", + "dist", + "public", + ); app.use(express.static(clientDist)); app.use((req, res, next) => { diff --git a/artifacts/api-server/src/middleware/staging-access.ts b/artifacts/api-server/src/middleware/staging-access.ts new file mode 100644 index 0000000..ae41674 --- /dev/null +++ b/artifacts/api-server/src/middleware/staging-access.ts @@ -0,0 +1,230 @@ +import { clerkClient, clerkMiddleware, getAuth } from "@clerk/express"; +import type { Express, NextFunction, Request, Response } from "express"; + +const STAGING_ROBOTS = "noindex, nofollow, noarchive, nosnippet"; +const AUTHORIZATION_CACHE_TTL_MS = 60_000; + +type StagingAccessConfig = { + allowedEmails: Set; + authorizedParties: string[]; + canonicalOrigin: string; + publishableKey: string; + secretKey: string; + signInUrl: string; +}; + +type CachedAuthorization = { + allowed: boolean; + expiresAt: number; +}; + +const authorizationCache = new Map(); + +function parseCsv(value: string | undefined): string[] { + return (value ?? "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function parseHttpsUrl(value: string, name: string): string { + let url: URL; + + try { + url = new URL(value); + } catch { + throw new Error(`${name} must be an absolute URL`); + } + + if (url.protocol !== "https:") { + throw new Error(`${name} must use https`); + } + + return ( + url.origin + (url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "")) + ); +} + +function readConfig(): StagingAccessConfig { + const publishableKey = process.env["CLERK_PUBLISHABLE_KEY"]?.trim(); + const secretKey = process.env["CLERK_SECRET_KEY"]?.trim(); + const signInUrlValue = process.env["CLERK_SIGN_IN_URL"]?.trim(); + const canonicalOriginValue = process.env["STAGING_CANONICAL_ORIGIN"]?.trim(); + const allowedEmails = new Set( + parseCsv(process.env["STAGING_ALLOWED_EMAILS"]).map((email) => + email.toLowerCase(), + ), + ); + const authorizedParties = parseCsv( + process.env["STAGING_AUTHORIZED_PARTIES"], + ).map((party) => parseHttpsUrl(party, "STAGING_AUTHORIZED_PARTIES")); + + if ( + !publishableKey || + !secretKey || + !signInUrlValue || + !canonicalOriginValue + ) { + throw new Error( + "Staging auth requires CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, CLERK_SIGN_IN_URL, and STAGING_CANONICAL_ORIGIN", + ); + } + + if (allowedEmails.size === 0 || authorizedParties.length === 0) { + throw new Error( + "Staging auth requires non-empty STAGING_ALLOWED_EMAILS and STAGING_AUTHORIZED_PARTIES", + ); + } + + const canonicalOrigin = parseHttpsUrl( + canonicalOriginValue, + "STAGING_CANONICAL_ORIGIN", + ); + const signInUrl = parseHttpsUrl(signInUrlValue, "CLERK_SIGN_IN_URL"); + + if (!authorizedParties.includes(canonicalOrigin)) { + throw new Error( + "STAGING_AUTHORIZED_PARTIES must include STAGING_CANONICAL_ORIGIN exactly", + ); + } + + return { + allowedEmails, + authorizedParties, + canonicalOrigin, + publishableKey, + secretKey, + signInUrl, + }; +} + +function isHealthRequest(req: Request): boolean { + return req.path === "/api/health" || req.path === "/api/healthz"; +} + +function isApiRequest(req: Request): boolean { + return req.path === "/api" || req.path.startsWith("/api/"); +} + +function isPublicAuthSurfaceRequest(req: Request): boolean { + return ( + req.path === "/sign-in" || + req.path.startsWith("/sign-in/") || + req.path === "/sign-up" || + req.path.startsWith("/sign-up/") || + req.path.startsWith("/assets/") || + req.path === "/favicon.svg" || + req.path === "/favicon.ico" + ); +} + +function signInRedirect(config: StagingAccessConfig, req: Request): string { + const signInUrl = new URL(config.signInUrl); + const returnUrl = new URL(req.originalUrl || "/", config.canonicalOrigin); + signInUrl.searchParams.set("redirect_url", returnUrl.toString()); + return signInUrl.toString(); +} + +async function isAllowedUser( + userId: string, + allowedEmails: Set, +): Promise { + const cached = authorizationCache.get(userId); + const now = Date.now(); + + if (cached && cached.expiresAt > now) { + return cached.allowed; + } + + const user = await clerkClient.users.getUser(userId); + const allowed = user.emailAddresses.some((email) => + allowedEmails.has(email.emailAddress.toLowerCase()), + ); + + authorizationCache.set(userId, { + allowed, + expiresAt: now + AUTHORIZATION_CACHE_TTL_MS, + }); + + return allowed; +} + +export function installStagingAccess(app: Express): void { + if (process.env["STAGING_AUTH_REQUIRED"] !== "true") return; + + const config = readConfig(); + const canonicalHostname = new URL(config.canonicalOrigin).hostname; + + app.use((req, res, next) => { + res.setHeader("X-Robots-Tag", STAGING_ROBOTS); + res.setHeader("Referrer-Policy", "same-origin"); + + if (!isHealthRequest(req)) { + res.setHeader("Cache-Control", "private, no-store"); + } + + next(); + }); + + app.get("/robots.txt", (_req, res) => { + res.type("text/plain").send("User-agent: *\nDisallow: /\n"); + }); + + app.use((req, res, next) => { + if ( + !isHealthRequest(req) && + !isApiRequest(req) && + req.hostname !== canonicalHostname + ) { + const destination = new URL( + req.originalUrl || "/", + config.canonicalOrigin, + ); + res.redirect(308, destination.toString()); + return; + } + + next(); + }); + + app.use( + clerkMiddleware({ + authorizedParties: config.authorizedParties, + publishableKey: config.publishableKey, + secretKey: config.secretKey, + signInUrl: config.signInUrl, + }), + ); + + app.use(async (req: Request, res: Response, next: NextFunction) => { + if (isHealthRequest(req) || isPublicAuthSurfaceRequest(req)) { + next(); + return; + } + + const { userId } = getAuth(req); + + if (!userId) { + if (isApiRequest(req)) { + res.status(401).json({ error: "Authentication required" }); + return; + } + + res.redirect(302, signInRedirect(config, req)); + return; + } + + try { + if (!(await isAllowedUser(userId, config.allowedEmails))) { + res + .status(403) + .send("This account does not have access to Structured staging."); + return; + } + + next(); + } catch (error) { + next(error); + } + }); +} diff --git a/artifacts/structured-liquidity/package.json b/artifacts/structured-liquidity/package.json index 7b914e1..64d3a2e 100644 --- a/artifacts/structured-liquidity/package.json +++ b/artifacts/structured-liquidity/package.json @@ -74,5 +74,8 @@ "vite": "catalog:", "wouter": "^3.3.5", "zod": "catalog:" + }, + "dependencies": { + "@clerk/react": "^6.14.5" } } diff --git a/artifacts/structured-liquidity/public/r/ai-chat.json b/artifacts/structured-liquidity/public/r/ai-chat.json index 6fed79f..56ed391 100644 --- a/artifacts/structured-liquidity/public/r/ai-chat.json +++ b/artifacts/structured-liquidity/public/r/ai-chat.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/ui/ai-chat.tsx", - "content": "import * as React from \"react\";\nimport { Sparkles, Paperclip, Mic, ArrowUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ChatMessage {\n from: \"bot\" | \"me\";\n text: React.ReactNode;\n}\n\nconst DEFAULT_MESSAGES: ChatMessage[] = [\n { from: \"bot\", text: \"Hey, I'm Vector. Ask me to draft, summarize, or name anything.\" },\n { from: \"me\", text: \"Write a title for a late-night ambient mix.\" },\n { from: \"bot\", text: 'How about \"Low Tide, 3AM\"? I can give you a few more.' },\n];\n\nconst DEFAULT_SUGGESTIONS = [\"More title ideas\", \"Make it moodier\", \"Write a blurb\"];\n\nexport interface AIChatProps extends React.HTMLAttributes {\n name?: string;\n role?: string;\n placeholder?: string;\n messages?: ChatMessage[];\n suggestions?: string[];\n}\n\n/** Structured Liquidity AI chat — a rigid conversation panel with glass bubbles, animated by the kit script. */\nexport const AIChat = React.forwardRef(\n (\n {\n name = \"Vector\",\n role = \"AI assistant\",\n placeholder = \"Ask Vector anything\",\n messages = DEFAULT_MESSAGES,\n suggestions = DEFAULT_SUGGESTIONS,\n className,\n ...props\n },\n ref,\n ) => (\n
\n
\n \n \n \n \n {name}\n {role}\n \n \n
\n
\n {messages.map((m, i) => (\n
\n {m.from === \"bot\" && (\n \n \n \n )}\n
{m.text}
\n
\n ))}\n
\n
\n {suggestions.map((s, i) => (\n \n ))}\n
\n
\n \n \n \n \n \n \n \n \n \n
\n ),\n);\nAIChat.displayName = \"AIChat\";\n", + "content": "import * as React from \"react\";\nimport { Sparkles, Paperclip, Mic, ArrowUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ChatMessage {\n from: \"bot\" | \"me\";\n text: React.ReactNode;\n}\n\nconst DEFAULT_MESSAGES: ChatMessage[] = [\n {\n from: \"bot\",\n text: \"Hey, I'm Vector. Ask me to draft, summarize, or name anything.\",\n },\n { from: \"me\", text: \"Summarize the latest transit observation.\" },\n {\n from: \"bot\",\n text: \"One stable orbit, two anomalies, and a clean signal window at 03:20 UTC.\",\n },\n];\n\nconst DEFAULT_SUGGESTIONS = [\"Show anomalies\", \"Compare orbits\", \"Draft a log\"];\n\nexport interface AIChatProps extends React.HTMLAttributes {\n name?: string;\n role?: string;\n placeholder?: string;\n messages?: ChatMessage[];\n suggestions?: string[];\n}\n\n/** Structured Liquidity AI chat — a rigid conversation panel with glass bubbles, animated by the kit script. */\nexport const AIChat = React.forwardRef(\n (\n {\n name = \"Vector\",\n role = \"AI assistant\",\n placeholder = \"Ask Vector anything\",\n messages = DEFAULT_MESSAGES,\n suggestions = DEFAULT_SUGGESTIONS,\n className,\n ...props\n },\n ref,\n ) => (\n
\n
\n \n \n \n \n {name}\n {role}\n \n \n
\n
\n {messages.map((m, i) => (\n
\n {m.from === \"bot\" && (\n \n \n \n )}\n
{m.text}
\n
\n ))}\n
\n
\n {suggestions.map((s, i) => (\n \n ))}\n
\n
\n \n \n \n \n \n \n \n \n \n
\n ),\n);\nAIChat.displayName = \"AIChat\";\n", "type": "registry:ui", "target": "components/ui/ai-chat.tsx" } diff --git a/artifacts/structured-liquidity/public/r/media-player.json b/artifacts/structured-liquidity/public/r/media-player.json index 1922e68..5c286ea 100644 --- a/artifacts/structured-liquidity/public/r/media-player.json +++ b/artifacts/structured-liquidity/public/r/media-player.json @@ -17,7 +17,7 @@ "files": [ { "path": "registry/ui/media-player.tsx", - "content": "import * as React from \"react\";\nimport {\n Radio,\n Shuffle,\n SkipBack,\n Pause,\n SkipForward,\n Repeat,\n Volume2,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Waveform } from \"@/components/ui/waveform\";\n\nexport interface MediaPlayerProps extends Omit<\n React.HTMLAttributes,\n \"onVolumeChange\"\n> {\n eyebrow?: string;\n title?: string;\n artist?: string;\n badge?: React.ReactNode;\n elapsed?: string;\n duration?: string;\n /** waveform bar count */\n bars?: number;\n /** leading bars read as elapsed */\n played?: number;\n /** volume fill, 0–100 */\n volume?: number;\n onVolumeChange?: (value: number) => void;\n}\n\n/** Structured Liquidity media player — a rigid \"now playing\" card over a glass waveform. */\nexport const MediaPlayer = React.forwardRef(\n (\n {\n eyebrow = \"Now playing\",\n title = \"Gracias a la Vida\",\n artist = \"Violeta Parra\",\n badge = \"Live\",\n elapsed = \"1:48\",\n duration = \"4:38\",\n bars = 56,\n played = 22,\n volume = 72,\n onVolumeChange,\n className,\n ...props\n },\n ref,\n ) => {\n const [internalVolume, setInternalVolume] = React.useState(volume);\n React.useEffect(() => setInternalVolume(volume), [volume]);\n const level = internalVolume;\n const updateVolume = (next: number) => {\n setInternalVolume(next);\n onVolumeChange?.(next);\n };\n\n return (\n
\n
\n \n \n \n
\n {eyebrow}\n {title}\n {artist}\n
\n {badge != null && {badge}}\n
\n
\n \n
\n {elapsed}\n {duration}\n
\n
\n
\n
\n \n \n \n \n \n \n \n
\n
\n \n
\n );\n },\n);\nMediaPlayer.displayName = \"MediaPlayer\";\n", + "content": "import * as React from \"react\";\nimport {\n Radio,\n Shuffle,\n SkipBack,\n Pause,\n SkipForward,\n Repeat,\n Volume2,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Waveform } from \"@/components/ui/waveform\";\n\nexport interface MediaPlayerProps extends Omit<\n React.HTMLAttributes,\n \"onVolumeChange\"\n> {\n eyebrow?: string;\n title?: string;\n artist?: string;\n badge?: React.ReactNode;\n elapsed?: string;\n duration?: string;\n /** waveform bar count */\n bars?: number;\n /** leading bars read as elapsed */\n played?: number;\n /** volume fill, 0–100 */\n volume?: number;\n onVolumeChange?: (value: number) => void;\n}\n\n/** Structured Liquidity media player — a rigid \"now playing\" card over a glass waveform. */\nexport const MediaPlayer = React.forwardRef(\n (\n {\n eyebrow = \"Now playing\",\n title = \"Transit Echo 04\",\n artist = \"Kepler Station Archive\",\n badge = \"Live\",\n elapsed = \"1:48\",\n duration = \"4:38\",\n bars = 56,\n played = 22,\n volume = 72,\n onVolumeChange,\n className,\n ...props\n },\n ref,\n ) => {\n const [internalVolume, setInternalVolume] = React.useState(volume);\n React.useEffect(() => setInternalVolume(volume), [volume]);\n const level = internalVolume;\n const updateVolume = (next: number) => {\n setInternalVolume(next);\n onVolumeChange?.(next);\n };\n\n return (\n
\n
\n \n \n \n
\n {eyebrow}\n {title}\n {artist}\n
\n {badge != null && {badge}}\n
\n
\n \n
\n {elapsed}\n {duration}\n
\n
\n
\n
\n \n \n \n \n \n \n \n
\n
\n \n
\n );\n },\n);\nMediaPlayer.displayName = \"MediaPlayer\";\n", "type": "registry:ui", "target": "components/ui/media-player.tsx" } diff --git a/artifacts/structured-liquidity/public/r/mobile-nav.json b/artifacts/structured-liquidity/public/r/mobile-nav.json index b7d5c9b..ce120ec 100644 --- a/artifacts/structured-liquidity/public/r/mobile-nav.json +++ b/artifacts/structured-liquidity/public/r/mobile-nav.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/ui/mobile-nav.tsx", - "content": "import type { CSSProperties } from \"react\";\nimport * as React from \"react\";\nimport {\n Menu,\n X,\n Search,\n Compass,\n Library,\n Radio,\n Heart,\n House,\n User,\n Disc3,\n Music,\n BarChart3,\n Sparkles,\n ChevronLeft,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction PhoneScreen() {\n return (\n
\n
\n
\n
\n
\n );\n}\n\n/** Mobile navigation — a phone frame with a top drawer and one floating liquid-glass tab bar. */\nexport const MobileTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [menuOpen, setMenuOpen] = React.useState(false);\n const [active, setActive] = React.useState(0);\n const select = (index: number) => {\n setActive(index);\n setMenuOpen(false);\n };\n\n return (\n
\n
\n
\n setMenuOpen((open) => !open)}\n >\n \n \n \n \n \n universe\n \n \n \n \n
\n
\n
\n select(0)}\n >\n \n Discover\n \n select(1)}\n >\n \n Library\n \n select(2)}\n >\n \n Radio\n \n select(3)}\n >\n \n Saved\n \n
\n
\n \n \n \n select(0)}\n >\n \n Home\n \n select(1)}\n >\n \n Search\n \n select(2)}\n >\n \n Browse\n \n select(3)}\n >\n \n Saved\n \n select(4)}\n >\n \n You\n \n \n
\n
\n );\n});\nMobileTabBar.displayName = \"MobileTabBar\";\n\n/** Mobile navigation — a floating glass tab bar with a sliding active marker. */\nexport const GlassTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n
\n
\n \n \n \n setActive(0)}\n >\n \n Playback\n \n setActive(1)}\n >\n \n Music\n \n setActive(2)}\n >\n \n Billboard\n \n setActive(3)}\n >\n \n Lyriq\n \n \n
\n
\n );\n});\nGlassTabBar.displayName = \"GlassTabBar\";\n\n/** Mobile navigation — a segmented sub-nav header with a sliding marker (wired by the kit script). */\nexport const SegmentedNav = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n
\n
\n
\n \n Library\n \n \n \n
\n \n \n setActive(0)}\n >\n Playlists\n \n setActive(1)}\n >\n Songs\n \n
\n \n
\n
\n );\n});\nSegmentedNav.displayName = \"SegmentedNav\";\n", + "content": "import type { CSSProperties } from \"react\";\nimport * as React from \"react\";\nimport {\n Menu,\n X,\n Search,\n Compass,\n Library,\n Radio,\n Heart,\n House,\n User,\n Disc3,\n Music,\n BarChart3,\n Sparkles,\n ChevronLeft,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction PhoneScreen() {\n return (\n
\n
\n
\n
\n
\n );\n}\n\n/** Mobile navigation — a phone frame with a top drawer and one floating liquid-glass tab bar. */\nexport const MobileTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [menuOpen, setMenuOpen] = React.useState(false);\n const [active, setActive] = React.useState(0);\n const select = (index: number) => {\n setActive(index);\n setMenuOpen(false);\n };\n\n return (\n
\n
\n
\n setMenuOpen((open) => !open)}\n >\n \n \n \n \n \n Kepler\n \n \n \n \n
\n
\n
\n select(0)}\n >\n \n Mission\n \n select(1)}\n >\n \n Signals\n \n select(2)}\n >\n \n Atlas\n \n select(3)}\n >\n \n Archive\n \n
\n
\n \n \n \n select(0)}\n >\n \n Mission\n \n select(1)}\n >\n \n Signals\n \n select(2)}\n >\n \n Atlas\n \n select(3)}\n >\n \n Archive\n \n select(4)}\n >\n \n Crew\n \n \n
\n
\n );\n});\nMobileTabBar.displayName = \"MobileTabBar\";\n\n/** Mobile navigation — a floating glass tab bar with a sliding active marker. */\nexport const GlassTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n
\n
\n \n \n \n setActive(0)}\n >\n \n Mission\n \n setActive(1)}\n >\n \n Signals\n \n setActive(2)}\n >\n \n Atlas\n \n setActive(3)}\n >\n \n Archive\n \n \n
\n
\n );\n});\nGlassTabBar.displayName = \"GlassTabBar\";\n\n/** Mobile navigation — a segmented sub-nav header with a sliding marker (wired by the kit script). */\nexport const SegmentedNav = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n
\n
\n
\n \n Signal archive\n \n \n \n
\n \n \n setActive(0)}\n >\n Observations\n \n setActive(1)}\n >\n Transits\n \n
\n \n
\n
\n );\n});\nSegmentedNav.displayName = \"SegmentedNav\";\n", "type": "registry:ui", "target": "components/ui/mobile-nav.tsx" } diff --git a/artifacts/structured-liquidity/public/r/site-templates.json b/artifacts/structured-liquidity/public/r/site-templates.json index 3a16c5e..70afd09 100644 --- a/artifacts/structured-liquidity/public/r/site-templates.json +++ b/artifacts/structured-liquidity/public/r/site-templates.json @@ -12,7 +12,7 @@ "files": [ { "path": "registry/templates/site-templates.tsx", - "content": "import * as React from \"react\";\nimport { FrameworkAttribution } from \"@/components/ui/attribution\";\nimport { SiteContent, SiteShell } from \"@/components/ui/site-shell\";\n\ninterface TemplateProps {\n navigation?: React.ReactNode;\n footer?: React.ReactNode;\n children: React.ReactNode;\n}\n\ninterface MarketingSiteTemplateProps extends TemplateProps {\n hero: React.ReactNode;\n proof?: React.ReactNode;\n}\n\n/** Landing template distilled from Interspace, 2 Days Early, and Bumble Bee. */\nexport function MarketingSiteTemplate({\n navigation,\n hero,\n proof,\n children,\n footer,\n}: MarketingSiteTemplateProps) {\n return (\n
\n {navigation}\n \n {hero}\n {proof}\n {children}\n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface DashboardTemplateProps extends TemplateProps {\n metrics?: React.ReactNode;\n filters?: React.ReactNode;\n}\n\n/** Data-heavy template distilled from Interspace portfolio and index views. */\nexport function DashboardTemplate({\n navigation,\n metrics,\n filters,\n children,\n footer,\n}: DashboardTemplateProps) {\n return (\n
\n {navigation}\n \n {metrics}\n {filters}\n
{children}
\n
\n \n {footer ?? }\n \n
\n );\n}\n\ninterface GuidedFlowTemplateProps extends TemplateProps {\n steps: React.ReactNode;\n}\n\n/** Multi-step conversion template distilled from Bumble Bee enrollment. */\nexport function GuidedFlowTemplate({\n navigation,\n steps,\n children,\n footer,\n}: GuidedFlowTemplateProps) {\n return (\n
\n {navigation}\n \n \n
{children}
\n
\n \n {footer ?? }\n \n
\n );\n}\n\ninterface ImmersiveAppTemplateProps extends TemplateProps {\n hud?: React.ReactNode;\n controls?: React.ReactNode;\n}\n\n/** Full-viewport app template distilled from Cosmograph and Universe. */\nexport function ImmersiveAppTemplate({\n navigation,\n hud,\n controls,\n children,\n footer,\n}: ImmersiveAppTemplateProps) {\n return (\n
\n {navigation}\n
\n {children}\n {hud}\n {controls}\n
\n \n {footer ?? }\n \n
\n );\n}\n\n", + "content": "import * as React from \"react\";\nimport { FrameworkAttribution } from \"@/components/ui/attribution\";\nimport { SiteContent, SiteShell } from \"@/components/ui/site-shell\";\n\ninterface TemplateProps {\n navigation?: React.ReactNode;\n footer?: React.ReactNode;\n children: React.ReactNode;\n}\n\ninterface MarketingSiteTemplateProps extends TemplateProps {\n hero: React.ReactNode;\n proof?: React.ReactNode;\n}\n\n/** Landing template for a concise public narrative, proof, and conversion path. */\nexport function MarketingSiteTemplate({\n navigation,\n hero,\n proof,\n children,\n footer,\n}: MarketingSiteTemplateProps) {\n return (\n
\n {navigation}\n \n {hero}\n {proof}\n {children}\n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface DashboardTemplateProps extends TemplateProps {\n metrics?: React.ReactNode;\n filters?: React.ReactNode;\n}\n\n/** Data-heavy template for metrics, filters, and responsive evidence views. */\nexport function DashboardTemplate({\n navigation,\n metrics,\n filters,\n children,\n footer,\n}: DashboardTemplateProps) {\n return (\n
\n {navigation}\n \n {metrics}\n {filters}\n
{children}
\n
\n \n {footer ?? }\n \n
\n );\n}\n\ninterface GuidedFlowTemplateProps extends TemplateProps {\n steps: React.ReactNode;\n}\n\n/** Multi-step conversion template with persistent progress and recovery states. */\nexport function GuidedFlowTemplate({\n navigation,\n steps,\n children,\n footer,\n}: GuidedFlowTemplateProps) {\n return (\n
\n {navigation}\n \n \n
{children}
\n
\n \n {footer ?? }\n \n
\n );\n}\n\ninterface ImmersiveAppTemplateProps extends TemplateProps {\n hud?: React.ReactNode;\n controls?: React.ReactNode;\n}\n\n/** Full-viewport app template for a canvas, HUD overlays, and command controls. */\nexport function ImmersiveAppTemplate({\n navigation,\n hud,\n controls,\n children,\n footer,\n}: ImmersiveAppTemplateProps) {\n return (\n
\n {navigation}\n
\n {children}\n {hud}\n {controls}\n
\n \n {footer ?? }\n \n
\n );\n}\n", "type": "registry:block", "target": "components/templates/site-templates.tsx" } diff --git a/artifacts/structured-liquidity/src/App.tsx b/artifacts/structured-liquidity/src/App.tsx index ba2f205..92ee9db 100644 --- a/artifacts/structured-liquidity/src/App.tsx +++ b/artifacts/structured-liquidity/src/App.tsx @@ -11,14 +11,36 @@ import { Adopt } from "@/components/site/Adopt"; import { FooterCta } from "@/components/site/FooterCta"; import { Footer } from "@/components/site/Footer"; import { Tweaker } from "@/components/site/Tweaker"; +import { AuthPage } from "@/components/site/AuthPage"; export function App() { + if ( + window.location.pathname === "/sign-in" || + window.location.pathname.startsWith("/sign-in/") || + window.location.pathname === "/sign-up" || + window.location.pathname.startsWith("/sign-up/") + ) { + return ; + } + + const isStaging = + window.location.hostname === "staging.structured.glass" || + import.meta.env.VITE_DEPLOYMENT_ENVIRONMENT === "staging"; + useEffect(() => { initBehaviors(); }, []); return ( + {isStaging ? ( +
+ Staging +
+ ) : null}
diff --git a/artifacts/structured-liquidity/src/components/site/Nav.tsx b/artifacts/structured-liquidity/src/components/site/Nav.tsx index 179766a..e73b45b 100644 --- a/artifacts/structured-liquidity/src/components/site/Nav.tsx +++ b/artifacts/structured-liquidity/src/components/site/Nav.tsx @@ -1,5 +1,13 @@ -import { useCallback, useEffect, useRef } from "react"; -import { Bot, Blocks, Github, Sparkles, Waypoints } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + Bot, + Blocks, + Github, + Menu, + Sparkles, + Waypoints, + X, +} from "lucide-react"; import { Hypercube } from "./liquid"; import { InstallButton } from "./InstallButton"; @@ -14,6 +22,7 @@ const NAV_ITEMS = [ export function Nav() { const navRef = useRef(null); const linksRef = useRef(null); + const [mobileOpen, setMobileOpen] = useState(false); const positionMarker = useCallback((index: number) => { const links = linksRef.current; @@ -33,7 +42,10 @@ export function Nav() { const page = document.documentElement; const distance = Math.max(1, page.scrollHeight - window.innerHeight); const progress = Math.min(1, Math.max(0, window.scrollY / distance)); - navRef.current?.style.setProperty("--page-progress", `${progress * 100}%`); + navRef.current?.style.setProperty( + "--page-progress", + `${progress * 100}%`, + ); }; const requestUpdate = () => { if (!frame) frame = window.requestAnimationFrame(update); @@ -48,19 +60,39 @@ export function Nav() { }; }, [positionMarker]); + useEffect(() => { + if (!mobileOpen) return; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setMobileOpen(false); + }; + window.addEventListener("keydown", closeOnEscape); + return () => window.removeEventListener("keydown", closeOnEscape); + }, [mobileOpen]); + return (
-
); diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx index cd9eb22..9ca1f98 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx @@ -61,12 +61,12 @@ export function KitContentData() {
Empty - - No tracks yet + + No signals yet - Import audio to start a library. + Connect an instrument to begin the survey. - +
@@ -76,8 +76,8 @@ export function KitContentData() { U - universe.audio - Audio platform · Live + Kepler Station + Orbital survey · Live
@@ -87,7 +87,9 @@ export function KitDataDisplay() {
Build failed - Border weight must be ≥ 1px. + + Border weight must be ≥ 1px. +
@@ -97,14 +99,25 @@ export function KitDataDisplay() { Avatar
SL - + JD A - B - C - +5 + + B + + + C + + + +5 +
@@ -119,23 +132,23 @@ export function KitDataDisplay() { - U + KS
- universe.audio + Kepler Station
- Built on SL + Survey nominal
} >
@@ -167,7 +180,11 @@ export function KitDataDisplay() {
Waveform - +
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx index 69046d3..d07fc72 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx @@ -1,18 +1,29 @@ +import { ButtonGroup, ButtonGroupItem } from "@/components/ui/button-group"; import { - ButtonGroup, - ButtonGroupItem, -} from "@/components/ui/button-group"; -import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field"; -import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; + Field, + FieldLabel, + FieldDescription, + FieldError, +} from "@/components/ui/field"; +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group"; import { Kbd } from "@/components/ui/kbd"; import { Spinner } from "@/components/ui/spinner"; export function KitFieldsControls() { return (
-
Fields & controlsButton group · Field · Input group · Kbd · Spinner
+
+ Fields & controls + + + Button group · Field · Input group · Kbd · Spinner + +
-
Button group @@ -26,12 +37,12 @@ export function KitFieldsControls() { Field Workspace URL - + Already taken, try another. Display name - + Shown on your public profile.
@@ -40,10 +51,10 @@ export function KitFieldsControls() { Input group https:// - + - + ⌘K
@@ -51,24 +62,47 @@ export function KitFieldsControls() {
Kbd
- Command - K + + Command + + + K +
- Save - S + + Save + + + S +
Spinner -
+
- +
-
); diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx index 184282e..d6b946c 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx @@ -337,14 +337,14 @@ export function KitFormsInputs() {
Fieldset
- Billing address + Station coordinates - - + + - - + +
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx index 5ebedff..5e8569a 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx @@ -45,7 +45,7 @@ export function KitLayoutStructure() { Purpose - By operators. For operators. + Signals, ordered for the mission.
@@ -55,12 +55,12 @@ export function KitLayoutStructure() { @@ -93,13 +93,13 @@ export function KitLayoutStructure() {
Scroll area - Track 01 · Aurora - Track 02 · Meniscus - Track 03 · Offset - Track 04 · Specular - Track 05 · Viscosity - Track 06 · Containment - Track 07 · Refraction + Orbit 01 · Periapsis + Orbit 02 · Transit + Orbit 03 · Eclipse + Orbit 04 · Relay + Orbit 05 · Survey + Orbit 06 · Return + Orbit 07 · Archive
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx index 35a55f8..51a3819 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx @@ -32,18 +32,37 @@ export function KitMenusOverlays() {
Menus & overlays - Alert dialog · Context menu · Drawer · Navigation menu + + Alert dialog · Context menu · Drawer · Navigation menu +
-
Alert dialog -

A blocking confirm for destructive actions.

+

+ A blocking confirm for destructive actions. +

- Delete project + + Delete project + -

Delete project?

-

This permanently removes the project and all of its data. This action cannot be undone.

+

+ Delete project? +

+

+ This permanently removes the project and all of its data. This + action cannot be undone. +

Cancel Delete project @@ -54,7 +73,10 @@ export function KitMenusOverlays() {
Context menu -
+
Right-click surface @@ -70,12 +92,30 @@ export function KitMenusOverlays() {
Drawer -

A panel that rises from the bottom edge.

+

+ A panel that rises from the bottom edge. +

- Open drawer + + Open drawer + -

Quick settings

-

A panel that rises from the bottom edge, settling into its flat shadow.

+

+ Quick settings +

+

+ A panel that rises from the bottom edge, settling into its flat + shadow. +

Close Save @@ -90,9 +130,11 @@ export function KitMenusOverlays() { Products - universe.audio - Structured Liquidity - Interspace + + Mission control + + Orbital atlas + Signal archive @@ -104,12 +146,18 @@ export function KitMenusOverlays() { - Pricing + + Pricing + - Hover an item for its flyout. + + Hover an item for its flyout. +
-
); diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx index 63d5e0b..64cb750 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx @@ -38,8 +38,8 @@ export function KitNavDisclosure() { Is it tied to one product?

- No. universe.audio is only one example; the language is - product-agnostic and travels from product to product by its + No. Kepler Station is a fictional specimen world; the language + is product-agnostic and travels between products through its tokens.

diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx index b876bed..7d06228 100644 --- a/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx +++ b/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx @@ -78,7 +78,9 @@ export function KitPatterns() {
Site shell
- Navigation and footer use the shared site frame + + Navigation and footer use the shared site frame + Body surface aligns to the same frame
@@ -124,10 +126,10 @@ export function KitPatterns() { 26 results @@ -173,11 +175,11 @@ export function KitPatterns() { Verified family review - “The same familiar faces, thoughtful updates, and a place our - child is excited to return to.” + “The transit log made a complex orbital shift immediately clear to + every crew on the station.” - Brooklyn parent Daycare portal + Mission review Flight archive
@@ -186,10 +188,10 @@ export function KitPatterns() { Logo grid {[ - ["IV", "Advisory"], - ["2D", "Syndicate"], - ["BB", "Daycare"], - ["CG", "Research"], + ["KS", "Station"], + ["AT", "Atlas"], + ["SR", "Survey"], + ["RL", "Relay"], ].map(([mark, label]) => ( {mark} @@ -206,11 +208,11 @@ export function KitPatterns() { 01 - Investor update published + Transit report published Today · 09:42 - Operating notes and portfolio metrics are ready for review. + Orbital notes and instrument readings are ready for review. @@ -248,7 +250,7 @@ export function KitPatterns() { Pricing card - Operator + Expedition Recommended @@ -273,7 +275,7 @@ export function KitPatterns() { - +
@@ -281,32 +283,32 @@ export function KitPatterns() {
Unit economics chart ); } - diff --git a/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx b/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx index 356b87a..7187208 100644 --- a/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx +++ b/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx @@ -8,12 +8,18 @@ interface ChatMessage { } const DEFAULT_MESSAGES: ChatMessage[] = [ - { from: "bot", text: "Hey, I'm Vector. Ask me to draft, summarize, or name anything." }, - { from: "me", text: "Write a title for a late-night ambient mix." }, - { from: "bot", text: 'How about "Low Tide, 3AM"? I can give you a few more.' }, + { + from: "bot", + text: "Hey, I'm Vector. Ask me to draft, summarize, or name anything.", + }, + { from: "me", text: "Summarize the latest transit observation." }, + { + from: "bot", + text: "One stable orbit, two anomalies, and a clean signal window at 03:20 UTC.", + }, ]; -const DEFAULT_SUGGESTIONS = ["More title ideas", "Make it moodier", "Write a blurb"]; +const DEFAULT_SUGGESTIONS = ["Show anomalies", "Compare orbits", "Draft a log"]; export interface AIChatProps extends React.HTMLAttributes { name?: string; diff --git a/artifacts/structured-liquidity/src/components/ui/media-player.tsx b/artifacts/structured-liquidity/src/components/ui/media-player.tsx index 3fcc1f7..1a96baa 100644 --- a/artifacts/structured-liquidity/src/components/ui/media-player.tsx +++ b/artifacts/structured-liquidity/src/components/ui/media-player.tsx @@ -37,8 +37,8 @@ export const MediaPlayer = React.forwardRef( ( { eyebrow = "Now playing", - title = "Gracias a la Vida", - artist = "Violeta Parra", + title = "Transit Echo 04", + artist = "Kepler Station Archive", badge = "Live", elapsed = "1:48", duration = "4:38", diff --git a/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx b/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx index af4bc95..ae029a8 100644 --- a/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx +++ b/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx @@ -60,7 +60,7 @@ export const MobileTabBar = React.forwardRef< style={{ "--s": "18px" } as CSSProperties} aria-hidden="true" /> - universe + Kepler
@@ -115,7 +115,7 @@ export const MobileTabBar = React.forwardRef< onClick={() => select(0)} > - Home + Mission
@@ -178,7 +178,7 @@ export const GlassTabBar = React.forwardRef< onClick={() => setActive(0)} > - Playback + Mission
@@ -224,7 +224,7 @@ export const SegmentedNav = React.forwardRef< - Library + Signal archive @@ -243,7 +243,7 @@ export const SegmentedNav = React.forwardRef< aria-selected={active === 0} onClick={() => setActive(0)} > - Playlists + Observations diff --git a/artifacts/structured-liquidity/src/index.css b/artifacts/structured-liquidity/src/index.css index deab317..6f37697 100644 --- a/artifacts/structured-liquidity/src/index.css +++ b/artifacts/structured-liquidity/src/index.css @@ -30,3 +30,209 @@ --font-body: var(--body); --font-mono: var(--mono); } + +.staging-environment-label { + position: fixed; + z-index: 10000; + right: 0.75rem; + bottom: 0.75rem; + padding: 0.45rem 0.65rem; + border: var(--border-w) solid rgb(var(--edge)); + background: var(--accent); + color: var(--accent-ink); + box-shadow: 3px 3px 0 0 var(--hard-shadow); + font-family: var(--mono); + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.12em; + line-height: 1; + text-transform: uppercase; +} + +.auth-page { + min-height: 100svh; + display: grid; + place-items: center; + padding: clamp(1rem, 4vw, 4rem); + background-color: var(--bg); + background-image: + linear-gradient(rgba(var(--edge), 0.055) 1px, transparent 1px), + linear-gradient(90deg, rgba(var(--edge), 0.055) 1px, transparent 1px); + background-size: 64px 64px; +} + +.auth-frame { + width: min(100%, 1120px); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.82fr); + border: var(--border-w) solid rgb(var(--edge)); + background: var(--bg-2); + box-shadow: var(--hard-x) var(--hard-y) 0 0 var(--hard-shadow); +} + +.auth-copy { + min-height: 580px; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: clamp(2rem, 5vw, 5rem); + border-right: var(--border-w) solid rgb(var(--edge)); +} + +.auth-kicker { + margin-bottom: clamp(3rem, 10vh, 7rem); + font-family: var(--mono); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.auth-copy h1 { + max-width: 8ch; + margin: 0; + font-family: var(--display); + font-size: clamp(3.1rem, 6vw, 6rem); + font-weight: 760; + letter-spacing: -0.065em; + line-height: 0.9; +} + +.auth-copy > p { + max-width: 42ch; + margin: 1.75rem 0 0; + color: var(--ink-dim); + font-family: var(--body); + font-size: 1rem; +} + +.auth-notes { + width: 100%; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + margin: auto 0 0; + padding-top: 2rem; + border-top: 1px solid rgba(var(--edge), 0.28); +} + +.auth-notes div { + min-width: 0; +} + +.auth-notes dt, +.auth-notes dd { + margin: 0; + font-family: var(--mono); + font-size: 0.7rem; +} + +.auth-notes dt { + margin-bottom: 0.45rem; + color: var(--ink-dim); + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.auth-notes dd { + overflow-wrap: anywhere; +} + +.auth-widget-shell { + display: grid; + place-items: center; + min-width: 0; + padding: clamp(1.5rem, 4vw, 3.5rem); + background: color-mix(in srgb, var(--accent) 9%, var(--bg)); +} + +.structured-auth-root, +.structured-auth-card-box { + width: 100%; +} + +.structured-auth-card { + width: 100%; + border: var(--border-w) solid rgb(var(--edge)); + border-radius: 0; + box-shadow: 5px 5px 0 0 var(--hard-shadow); +} + +.structured-auth-title { + font-weight: 760; + letter-spacing: -0.035em; +} + +.structured-auth-subtitle { + color: var(--ink-dim); +} + +.structured-auth-social, +.structured-auth-primary, +.structured-auth-input { + min-height: 44px; + border: 2px solid rgb(var(--edge)); + border-radius: 0; + box-shadow: none; +} + +.structured-auth-social:hover, +.structured-auth-primary:hover { + transform: translate(1px, 1px); +} + +.structured-auth-social:focus-visible, +.structured-auth-primary:focus-visible, +.structured-auth-input:focus-visible, +.structured-auth-link:focus-visible { + outline: 3px solid var(--accent); + outline-offset: 2px; +} + +.structured-auth-link { + color: #5f3fc4; + font-weight: 700; +} + +.structured-auth-divider { + background: rgba(var(--edge), 0.3); +} + +@media (max-width: 760px) { + .auth-page { + place-items: start stretch; + padding: 0; + } + + .auth-frame { + grid-template-columns: 1fr; + border-right: 0; + border-left: 0; + box-shadow: none; + } + + .auth-copy { + min-height: auto; + padding: 2rem 1.25rem 1.75rem; + border-right: 0; + border-bottom: var(--border-w) solid rgb(var(--edge)); + } + + .auth-kicker { + margin-bottom: 3rem; + } + + .auth-copy h1 { + max-width: 9ch; + font-size: clamp(3rem, 16vw, 4.6rem); + } + + .auth-notes { + margin-top: 3rem; + } + + .auth-widget-shell { + padding: 1.25rem; + } +} diff --git a/artifacts/structured-liquidity/src/main.tsx b/artifacts/structured-liquidity/src/main.tsx index c1f2205..b091c02 100644 --- a/artifacts/structured-liquidity/src/main.tsx +++ b/artifacts/structured-liquidity/src/main.tsx @@ -1,13 +1,26 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { ClerkProvider } from "@clerk/react"; import "./index.css"; import { App } from "./App"; const rootEl = document.getElementById("root"); if (!rootEl) throw new Error("Root element #root not found"); -createRoot(rootEl).render( - +const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY?.trim(); + +const app = clerkPublishableKey ? ( + - , + +) : ( + ); + +createRoot(rootEl).render({app}); diff --git a/artifacts/structured-liquidity/src/styles/10-tokens-base.css b/artifacts/structured-liquidity/src/styles/10-tokens-base.css index 08bce7e..b4b266a 100644 --- a/artifacts/structured-liquidity/src/styles/10-tokens-base.css +++ b/artifacts/structured-liquidity/src/styles/10-tokens-base.css @@ -32,6 +32,7 @@ --rule-hairline: 1px; --diagram-line: color-mix(in srgb, var(--ink) 72%, transparent); --diagram-muted: color-mix(in srgb, var(--ink) 42%, transparent); + --grid-line: rgba(17, 17, 17, 0.065); /* ---- light-first paper palette ---- */ --bg: #f2f0ea; @@ -61,6 +62,7 @@ --neg-ink: #ffffff; --glass-tint: 255 255 255; --glass-alpha: 0.72; + --grid-line: rgba(17, 17, 17, 0.065); } [data-mode="dark"] { @@ -76,6 +78,7 @@ --neg-ink: #ffffff; --glass-tint: 255 255 255; --glass-alpha: 0.08; + --grid-line: rgba(246, 242, 248, 0.07); } * { @@ -131,10 +134,10 @@ body { position: absolute; inset: 0; background-image: - linear-gradient(rgba(var(--edge), 0.045) 1px, transparent 1px), - linear-gradient(90deg, rgba(var(--edge), 0.045) 1px, transparent 1px); + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); background-size: 64px 64px; - opacity: 0.7; + opacity: 1; } .grain { diff --git a/artifacts/structured-liquidity/src/styles/40-inline.css b/artifacts/structured-liquidity/src/styles/40-inline.css index 7d22855..2632a52 100644 --- a/artifacts/structured-liquidity/src/styles/40-inline.css +++ b/artifacts/structured-liquidity/src/styles/40-inline.css @@ -389,21 +389,23 @@ .kit-filter:hover{color:var(--ink);transform:translate(-1px,-1px);box-shadow:4px 4px 0 0 var(--hard-shadow);} .kit-filter.active{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);} .kit-filter.active:hover{color:var(--accent-ink);} - /* Masonry columns: uniform-width cards pack tight vertically (no ragged empty - space under short cards) and get a clear horizontal gap between columns. */ + /* Masonry columns: one on mobile, two on ordinary laptop viewports, and three + only once a wide display can preserve useful specimen width. */ .kit-grid.gallery{display:block;column-count:2;column-gap:1.4rem;} .kit-grid.gallery .kit-cell{width:100%;margin:0 0 1.4rem;padding:1.5rem 1.6rem; -webkit-column-break-inside:avoid;break-inside:avoid;} + @media (min-width:1440px){.kit-grid.gallery{column-count:3;}} @media (max-width:600px){.kit-grid.gallery{column-count:1;}} .kit-cell.is-hidden{display:none;} - /* Two-axis gallery filters: a primary "Type" row + a secondary "Kind" row */ + /* Two useful axes: Category describes the job; Type describes implementation + scope (primitive, composite, pattern, or template). Type stays subordinate. */ .kit-filterbars{display:flex;flex-direction:column;gap:0.6rem;margin:0.2rem 0 1.5rem;} .kit-filter-row{display:flex;align-items:baseline;gap:0.7rem;flex-wrap:wrap;} .kit-filter-axis{font-family:var(--mono);font-size:0.62rem;letter-spacing:0.16em;text-transform:uppercase;color:var(--ink);min-width:3rem;flex:0 0 auto;padding-top:0.55rem;display:flex;align-items:center;gap:0.45rem;} .kit-filter-axis::before{content:"";width:0.5rem;height:0.5rem;background:var(--accent);flex:0 0 auto;} .kit-filterbars .kit-filters{margin:0;flex:1 1 auto;} - /* The secondary "Kind" axis is deliberately subordinate to "Type": a hollow + /* The secondary Type axis is deliberately subordinate to Category: a hollow axis marker, smaller/flatter chips, and an accent-OUTLINE active state (vs the primary row's solid accent fill). Distinct hierarchy, one accent. */ .kit-filter-row.is-secondary{padding-left:0.1rem;} @@ -1424,7 +1426,7 @@ body { } /* ============================================================ - PRECISION PASS — one editorial grid, full-height specimens, + PRECISION PASS — one editorial grid, compact specimens, higher-fidelity liquid state, and legible accent interaction. ============================================================ */ .nav-cluster, @@ -1452,7 +1454,7 @@ body { min-height: 100%; padding: 0.58rem 0.72rem; border-width: 1px; - background: var(--bg-2); + background: color-mix(in srgb, var(--ink) 10%, var(--bg-2)); box-shadow: none; color: var(--ink); font-size: 0.74rem; @@ -1498,20 +1500,22 @@ body { .tenet ul { margin-top: 0; } #components .kit-grid.gallery { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: stretch; - gap: 1px; - column-count: auto; + display: block; + column-count: 2; + column-gap: 1px; +} +@media (min-width: 1440px) { + #components .kit-grid.gallery { column-count: 3; } } #components .kit-grid.gallery .kit-cell { display: flex; flex-direction: column; - width: auto; - height: 100%; + width: 100%; + height: auto; min-width: 0; - margin: 0; - break-inside: auto; + margin: 0 0 1px; + break-inside: avoid; + -webkit-column-break-inside: avoid; } .sl-button-specimen { flex-direction: column; @@ -1519,18 +1523,32 @@ body { justify-content: flex-start; flex-wrap: nowrap; } -.kit-filter.active { +.kit-filters[data-axis="cat"] .kit-filter { + background: color-mix(in srgb, var(--ink) 10%, var(--bg-2)); + border-color: color-mix(in srgb, var(--ink) 58%, transparent); + box-shadow: 2px 2px 0 0 var(--hard-shadow); +} +.kit-filters[data-axis="cat"] .kit-filter:hover { + background: color-mix(in srgb, var(--ink) 15%, var(--bg-2)); + box-shadow: 2px 2px 0 0 var(--hard-shadow); +} +.kit-filters[data-axis="cat"] .kit-filter.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); } -.kit-filter.active:hover { color: var(--accent-ink); } -[data-mode="dark"] .kit-filter.active { +.kit-filters[data-axis="cat"] .kit-filter.active:hover { color: var(--accent-ink); } +.kit-filters[data-axis="kind"] .kit-filter { + background: color-mix(in srgb, var(--ink) 6%, var(--bg-2)); + border-color: color-mix(in srgb, var(--ink) 42%, transparent); + box-shadow: 1px 1px 0 0 color-mix(in srgb, var(--hard-shadow) 70%, transparent); +} +[data-mode="dark"] .kit-filters[data-axis="cat"] .kit-filter.active { background: color-mix(in srgb, var(--accent) 54%, var(--bg-2)); color: #fff; border-color: color-mix(in srgb, var(--accent) 76%, white); } -[data-mode="dark"] .kit-filter.active:hover { color: #fff; } +[data-mode="dark"] .kit-filters[data-axis="cat"] .kit-filter.active:hover { color: #fff; } [data-mode="dark"] .kit-filters[data-axis="kind"] .kit-filter.active { background: color-mix(in srgb, var(--accent) 32%, transparent); color: #fff; @@ -1555,7 +1573,11 @@ body { .nav .nav-utility { padding-inline: 0.58rem; } } @media (max-width: 600px) { - #components .kit-grid.gallery { grid-template-columns: 1fr; } + html, + body { + overflow-x: clip; + } + #components .kit-grid.gallery { column-count: 1; } .nav .nav-utility { font-size: 0; gap: 0; } .tenet { display: flex; @@ -1625,3 +1647,337 @@ body { text-align: left; } } +/* ============================================================ + MOBILE AUDIT — labeled navigation, left-anchored hierarchy, + and specimens that stay inside the scientific grid. + ============================================================ */ +.nav-mobile-trigger, +.nav-mobile-menu { + display: none; +} + +@media (max-width: 780px) { + .nav { + padding: 0.42rem 0.48rem 0.42rem 0.7rem; + overflow: visible; + } + .nav .brand .name { + display: inline; + font-size: 0.88rem; + } + .nav-cluster { + display: none; + } + .nav-mobile-trigger { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + gap: 0.45rem; + padding: 0.58rem 0.72rem; + border: 1px solid rgb(var(--edge)); + background: color-mix(in srgb, var(--ink) 9%, var(--bg-2)); + color: var(--ink); + font: 700 0.74rem/1 var(--body); + cursor: pointer; + } + .nav-mobile-trigger svg { + width: 1rem; + height: 1rem; + } + .nav-mobile-trigger[aria-expanded="true"] { + background: var(--accent); + color: var(--accent-ink); + } + .nav-mobile-menu { + position: absolute; + top: calc(100% + 0.45rem); + left: 0; + right: 0; + display: block; + max-height: 0; + overflow: hidden; + visibility: hidden; + opacity: 0; + transform: translateY(-0.5rem); + border: 0 solid rgb(var(--edge)); + background: color-mix(in srgb, var(--bg-2) 96%, transparent); + backdrop-filter: blur(var(--glass-blur)) saturate(150%); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%); + transition: + opacity var(--motion-fast) var(--ease-liquid), + transform var(--motion) var(--ease-liquid-over), + visibility 0s linear var(--motion); + } + .nav-mobile-menu[data-open="true"] { + max-height: 28rem; + visibility: visible; + opacity: 1; + transform: none; + border-width: 1px; + box-shadow: 5px 5px 0 var(--hard-shadow); + transition-delay: 0s; + } + .nav-mobile-links { + min-height: 0; + display: grid; + grid-template-columns: 1fr 1fr; + overflow: hidden; + } + .nav-mobile-links a { + display: flex; + min-height: 50px; + align-items: center; + gap: 0.65rem; + padding: 0.75rem; + border-right: 1px solid color-mix(in srgb, var(--ink) 24%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--ink) 24%, transparent); + color: var(--ink); + font: 700 0.82rem/1.2 var(--body); + text-decoration: none; + } + .nav-mobile-links a:nth-child(2n) { + border-right: 0; + } + .nav-mobile-links svg { + width: 1rem; + height: 1rem; + color: var(--accent); + } + .nav-mobile-utilities { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + padding: 0.65rem; + } + .nav-mobile-utilities .nav-utility { + min-height: 44px; + justify-content: center; + padding: 0.65rem; + font-size: 0.78rem; + } + .section-head, + .section-head .eyebrow, + .section-head .section-title, + .section-head .lead, + .tenet, + .tenet h3, + .tenet > p, + .tenet li, + .kit-group-head, + .kit-cell, + .foot { + text-align: left; + } + .kit-group > .kit-group-head { + align-items: flex-start; + flex-direction: column; + gap: 0.4rem; + } + .kit-group-head .kg-rule { + width: 100%; + flex: none; + } + .kit-group-head .kg-count { + line-height: 1.5; + } +} + +@media (max-width: 600px) { + section { + padding-block: clamp(2.8rem, 12vw, 4rem); + } + .wrap { + width: calc(100vw - 1.5rem); + } + .hero { + padding-top: 6.4rem; + } + .hero-cube-wrap { + right: -7.5rem; + } + #components .kit-grid.gallery, + .kit-grid { + width: 100%; + min-width: 0; + } + #components .kit-grid.gallery .kit-cell, + .kit-cell { + width: 100%; + min-width: 0; + max-width: 100%; + padding: 1rem; + overflow: hidden; + } + .kit-cell > *, + .kit-row, + .kit-col { + min-width: 0; + max-width: 100%; + } + .browser-bar { + min-width: 0; + } + .browser-bar .url { + min-width: 0; + margin-left: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + #adopt .kit-cell p, + #adopt .kit-cell code { + overflow-wrap: anywhere; + word-break: break-word; + } + .sl-button-specimen .sl-btn { + width: 100%; + justify-content: flex-start; + } + .sl-navbar { + align-items: flex-start; + } + .sl-navbar .nb-brand { + width: 100%; + } + .sl-navbar .nb-links { + width: 100%; + margin-left: 0; + } + .sl-navbar .nb-link { + min-width: 0; + padding-inline: 0.35rem; + font-size: 0.68rem; + } + .sl-navbar .nb-link svg { + display: none; + } + .sl-navmenu { + display: grid; + grid-template-columns: 1fr; + width: 100%; + } + .sl-navmenu .nm-item { + min-width: 0; + } + .sl-navmenu .nm-item > button { + width: 100%; + min-height: 44px; + justify-content: space-between; + border-left: 0; + border-top: 1px solid rgb(var(--edge) / 0.25); + } + .sl-navmenu .nm-item:first-child > button { + border-top: 0; + } + .sl-navmenu .nm-panel { + left: 0; + right: 0; + width: 100%; + min-width: 0; + } + .sl-profile-card-facts > div { + grid-template-columns: 1fr; + gap: 0.2rem; + } + .sl-profile-card-facts dd { + text-align: left; + } + .sl-dialog .actions { + align-items: stretch; + flex-direction: column; + justify-content: flex-start; + } + .sl-dialog .actions > * { + width: 100%; + } + .sl-toast-stack { + left: 0.75rem; + right: 0.75rem; + width: auto; + } + .sl-otp { + width: 100%; + gap: 0.32rem; + } + .sl-otp input { + width: calc((100% - 1.6rem) / 6); + min-width: 0; + } + .sl-table-wrap, + .sl-table-container { + max-width: 100%; + overflow-x: auto; + } + .sl-player, + .sl-player .pl-top, + .sl-player .pl-meta, + .sl-player .pl-progress, + .sl-player .pl-controls { + min-width: 0; + max-width: 100%; + } + .sl-player { + overflow: hidden; + } + .sl-mnav, + .sl-gnav, + .sl-snav, + .mn-phone { + max-width: 100%; + } + .gnav-tab { + min-width: 0; + font-size: 0.5rem; + letter-spacing: 0; + } + .gnav-tab svg { + width: 16px; + height: 16px; + } + .sl-fab-dock { + bottom: max(0.65rem, env(safe-area-inset-bottom)); + gap: 0.45rem; + } + .sl-tweaks-fab { + min-height: 44px; + padding: 0.55rem 0.68rem; + } + .twk-panel { + right: 0.75rem; + bottom: calc(4.4rem + env(safe-area-inset-bottom)); + width: calc(100vw - 1.5rem); + } + .cta-panel h2 { + max-width: 100%; + font-size: clamp(2rem, 11.5vw, 3.1rem); + overflow-wrap: anywhere; + } + .sl-overlay { + overflow: hidden; + } +} + +@media (max-width: 360px) { + .nav .brand .name { + display: none; + } + .nav-mobile-links { + grid-template-columns: 1fr; + } + .nav-mobile-links a { + border-right: 0; + } + .nav-mobile-utilities { + grid-template-columns: 1fr; + } + .gnav-tab span { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .nav-mobile-menu { + transition: none; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e35854..572f986 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: artifacts/api-server: dependencies: + '@clerk/express': + specifier: ^2.1.61 + version: 2.1.61(express@5.2.1) '@workspace/api-zod': specifier: workspace:* version: link:../../lib/api-zod @@ -314,6 +317,10 @@ importers: version: 3.25.76 artifacts/structured-liquidity: + dependencies: + '@clerk/react': + specifier: ^6.14.5 + version: 6.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) devDependencies: '@hookform/resolvers': specifier: ^3.10.0 @@ -640,6 +647,35 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@clerk/backend@3.16.10': + resolution: {integrity: sha512-rYGgr3sshL6Jaq5bvw3hGYnvOj31o6Yie/jWYiNUnlET9RFH82GywxWIGv6YVGSdYIFVm6DnaCyKqXc/hff/hA==} + engines: {node: '>=20.9.0'} + + '@clerk/express@2.1.61': + resolution: {integrity: sha512-8I+VvIfobwWmV3fDttDJsZyw1xElh9EyojxKvchjZ+V/aXroNxPYr3JlX/6VqK4uf0Nb8/yyYSevzo5qH0ar6g==} + engines: {node: '>=20.9.0'} + peerDependencies: + express: ^4.17.0 || ^5.0.0 + + '@clerk/react@6.14.5': + resolution: {integrity: sha512-J8VYDmlRqe2+fq49+FtsbQHJG6p5TcCjJ5myA0sBZ5ht8m2HsgJ9OV6qmIpupLmnDgu9Cp/9Pe/JpUM1JAxAjA==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + + '@clerk/shared@4.29.3': + resolution: {integrity: sha512-sc9CNFDR3q+tJ8N5MkGcHGiXlgupQ6DMcZlXdk88kfo1Pl7G9EFK8e4mzp5D9OiYGRJ7rPnZCKiWoyInD59QXw==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@commander-js/extra-typings@14.0.0': resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==} peerDependencies: @@ -2191,6 +2227,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@tabby_ai/hijri-converter@1.0.5': resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} engines: {node: '>=16.0.0'} @@ -2632,6 +2671,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2874,6 +2917,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} @@ -2970,6 +3016,9 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + globby@16.1.0: resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} engines: {node: '>=20'} @@ -3067,6 +3116,10 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3688,6 +3741,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -4052,6 +4108,42 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@clerk/backend@3.16.10': + dependencies: + '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + standardwebhooks: 1.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/express@2.1.61(express@5.2.1)': + dependencies: + '@clerk/backend': 3.16.10 + '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + express: 5.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/react@6.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@clerk/shared@4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.100.9 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.7 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@commander-js/extra-typings@14.0.0(commander@14.0.3)': dependencies: commander: 14.0.3 @@ -5386,6 +5478,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@stablelib/base64@1.0.1': {} + '@tabby_ai/hijri-converter@1.0.5': {} '@tailwindcss/node@4.3.0': @@ -5798,6 +5892,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -6066,6 +6162,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-uri@3.1.2: {} fastq@1.20.1: @@ -6161,6 +6259,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + globby@16.1.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -6233,6 +6333,8 @@ snapshots: joycon@3.1.1: {} + js-cookie@3.0.7: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -6871,6 +6973,11 @@ snapshots: split2@4.2.0: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.2: {} string-argv@0.3.2: {}