From 3decc28242febef91b15c39d8626863d87e51bc2 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Tue, 30 Dec 2025 14:25:19 +0100 Subject: [PATCH] website --- website/.gitignore | 6 + website/components.json | 22 ++++ website/package.json | 46 ++++++++ website/public/favicon.ico | Bin 0 -> 16958 bytes website/react-router.config.ts | 6 + website/src/entry.client.tsx | 7 ++ website/src/entry.server.tsx | 163 +++++++++++++++++++++++++++ website/src/lib/errors.ts | 34 ++++++ website/src/lib/utils.ts | 10 ++ website/src/root.tsx | 63 +++++++++++ website/src/routes.ts | 4 + website/src/routes/_index.tsx | 9 ++ website/src/routes/defer-example.tsx | 24 ++++ website/src/styles/globals.css | 143 +++++++++++++++++++++++ website/tsconfig.json | 28 +++++ website/vite.config.ts | 47 ++++++++ 16 files changed, 612 insertions(+) create mode 100644 website/.gitignore create mode 100644 website/components.json create mode 100644 website/package.json create mode 100644 website/public/favicon.ico create mode 100644 website/react-router.config.ts create mode 100644 website/src/entry.client.tsx create mode 100644 website/src/entry.server.tsx create mode 100644 website/src/lib/errors.ts create mode 100644 website/src/lib/utils.ts create mode 100644 website/src/root.tsx create mode 100644 website/src/routes.ts create mode 100644 website/src/routes/_index.tsx create mode 100644 website/src/routes/defer-example.tsx create mode 100644 website/src/styles/globals.css create mode 100644 website/tsconfig.json create mode 100644 website/vite.config.ts diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..87fa0f3 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,6 @@ +node_modules +/.cache +/build +.env +.react-router +.DS_Store \ No newline at end of file diff --git a/website/components.json b/website/components.json new file mode 100644 index 0000000..291aaa0 --- /dev/null +++ b/website/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "website/src/components", + "utils": "website/src/lib/utils", + "ui": "website/src/components/ui", + "lib": "website/src/lib", + "hooks": "website/src/lib/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..672f967 --- /dev/null +++ b/website/package.json @@ -0,0 +1,46 @@ +{ + "name": "website", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev --port 8044", + "start": "node ./build/server/server.js", + "typecheck": "react-router typegen && tsc" + }, + "exports": { + "./*": "./*" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@react-router/fs-routes": "^7.11.0", + "@react-router/node": "^7.11.0", + "@sentry/node": "^10.32.1", + "@tailwindcss/typography": "^0.5.19", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "isbot": "^5.1.32", + "lucide-react": "^0.562.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router": "^7.11.0", + "sentries": "^0.2.2", + "tailwind-merge": "^3.4.0", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@react-router/dev": "^7.11.0", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "@xmorse/deployment-utils": "^0.7.2", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.3.0", + "vite-plugin-environment": "^1.1.3", + "vite-tsconfig-paths": "^6.0.3", + "vitest": "^4.0.16" + } +} diff --git a/website/public/favicon.ico b/website/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 zcmeI3+jCXb9mnJN2h^uNlXH@jlam{_a8F3W{T}Wih>9YJpaf7TUbu)A5fv|h7OMfR zR;q$lr&D!wv|c)`wcw1?>4QT1(&|jdsrI2h`Rn)dTW5t$8pz=s3_5L?#oBxAowe8R z_WfPfN?F+@`q$D@rvC?(W!uWieppskmQ~YG*>*L?{img@tWpnYXZslxeh#TSUS3{q z1Ju6JcfQSbQuORq69@YK(X-3c9vC2c2a2z~zw=F=50@pm0PUiCAm!bAT?2jpM`(^b zC|2&Ngngt^<>oCv#?P(AZ`5_84x#QBPulix)TpkIAUp=(KgGo4CVS~Sxt zVoR4>r5g9%bDh7hi0|v$={zr>CHd`?-l4^Ld(Z9PNz9piFY+llUw_x4ou7Vf-q%$g z)&)J4>6Ft~RZ(uV>dJD|`nxI1^x{X@Z5S<=vf;V3w_(*O-7}W<=e$=}CB9_R;)m9)d7`d_xx+nl^Bg|%ew=?uoKO8w zeQU7h;~8s!@9-k>7Cx}1SDQ7m(&miH zs8!l*wOJ!GHbdh)pD--&W3+w`9YJ=;m^FtMY=`mTq8pyV!-@L6smwp3(q?G>=_4v^ zn(ikLue7!y70#2uhqUVpb7fp!=xu2{aM^1P^pts#+feZv8d~)2sf`sjXLQCEj;pdI z%~f`JOO;*KnziMv^i_6+?mL?^wrE_&=IT9o1i!}Sd4Sx4O@w~1bi1)8(sXvYR-1?7~Zr<=SJ1Cw!i~yfi=4h6o3O~(-Sb2Ilwq%g$+V` z>(C&N1!FV5rWF&iwt8~b)=jIn4b!XbrWrZgIHTISrdHcpjjx=TwJXI7_%Ks4oFLl9 zNT;!%!P4~xH85njXdfqgnIxIFOOKW`W$fxU%{{5wZkVF^G=JB$oUNU5dQSL&ZnR1s z*ckJ$R`eCUJsWL>j6*+|2S1TL_J|Fl&kt=~XZF=+=iT0Xq1*KU-NuH%NAQff$LJp3 zU_*a;@7I0K{mqwux87~vwsp<}@P>KNDb}3U+6$rcZ114|QTMUSk+rhPA(b{$>pQTc zIQri{+U>GMzsCy0Mo4BfWXJlkk;RhfpWpAB{=Rtr*d1MNC+H3Oi5+3D$gUI&AjV-1 z=0ZOox+bGyHe=yk-yu%=+{~&46C$ut^ZN+ysx$NH}*F43)3bKkMsxGyIl#>7Yb8W zO{}&LUO8Ow{7>!bvSq?X{15&Y|4}0w2=o_^0ZzYgB+4HhZ4>s*mW&?RQ6&AY|CPcx z$*LjftNS|H)ePYnIKNg{ck*|y7EJ&Co0ho0K`!{ENPkASeKy-JWE}dF_%}j)Z5a&q zXAI2gPu6`s-@baW=*+keiE$ALIs5G6_X_6kgKK8n3jH2-H9`6bo)Qn1 zZ2x)xPt1=`9V|bE4*;j9$X20+xQCc$rEK|9OwH-O+Q*k`ZNw}K##SkY z3u}aCV%V|j@!gL5(*5fuWo>JFjeU9Qqk`$bdwH8(qZovE2tA7WUpoCE=VKm^eZ|vZ z(k<+j*mGJVah>8CkAsMD6#I$RtF;#57Wi`c_^k5?+KCmX$;Ky2*6|Q^bJ8+s%2MB}OH-g$Ev^ zO3uqfGjuN%CZiu<`aCuKCh{kK!dDZ+CcwgIeU2dsDfz+V>V3BDb~)~ zO!2l!_)m;ZepR~sL+-~sHS7;5ZB|~uUM&&5vDda2b z)CW8S6GI*oF><|ZeY5D^+Mcsri)!tmrM33qvwI4r9o@(GlW!u2R>>sB|E#%W`c*@5 z|0iA|`{6aA7D4Q?vc1{vT-#yytn07`H!QIO^1+X7?zG3%y0gPdIPUJ#s*DNAwd}m1_IMN1^T&be~+E z_z%1W^9~dl|Me9U6+3oNyuMDkF*z_;dOG(Baa*yq;TRiw{EO~O_S6>e*L(+Cdu(TM z@o%xTCV%hi&p)x3_inIF!b|W4|AF5p?y1j)cr9RG@v%QVaN8&LaorC-kJz_ExfVHB za!mtuee#Vb?dh&bwrfGHYAiX&&|v$}U*UBM;#F!N=x>x|G5s0zOa9{(`=k4v^6iK3 z8d&=O@xhDs{;v7JQ%eO;!Bt`&*MH&d zp^K#dkq;jnJz%%bsqwlaKA5?fy zS5JDbO#BgSAdi8NM zDo2SifX6^Z;vn>cBh-?~r_n9qYvP|3ihrnqq6deS-#>l#dV4mX|G%L8|EL;$U+w69 z;rTK3FW$ewUfH|R-Z;3;jvpfiDm?Fvyu9PeR>wi|E8>&j2Z@2h`U}|$>2d`BPV3pz#ViIzH8v6pP^L-p!GbLv<;(p>}_6u&E6XO5- zJ8JEvJ1)0>{iSd|kOQn#?0rTYL=KSmgMHCf$Qbm;7|8d(goD&T-~oCDuZf57iP#_Y zmxaoOSjQsm*^u+m$L9AMqwi=6bpdiAY6k3akjGN{xOZ`_J<~Puyzpi7yhhKrLmXV; z@ftONPy;Uw1F#{_fyGbk04yLE01v=i_5`RqQP+SUH0nb=O?l!J)qCSTdsbmjFJrTm zx4^ef@qt{B+TV_OHOhtR?XT}1Etm(f21;#qyyW6FpnM+S7*M1iME?9fe8d-`Q#InN z?^y{C_|8bxgUE@!o+Z72C)BrS&5D`gb-X8kq*1G7Uld-z19V}HY~mK#!o9MC-*#^+ znEsdc-|jj0+%cgBMy(cEkq4IQ1D*b;17Lyp>Utnsz%LRTfjQKL*vo(yJxwtw^)l|! z7jhIDdtLB}mpkOIG&4@F+9cYkS5r%%jz}I0R#F4oBMf-|Jmmk* zk^OEzF%}%5{a~kGYbFjV1n>HKC+a`;&-n*v_kD2DPP~n5(QE3C;30L<32GB*qV2z$ zWR1Kh=^1-q)P37WS6YWKlUSDe=eD^u_CV+P)q!3^{=$#b^auGS7m8zFfFS<>(e~)TG z&uwWhSoetoe!1^%)O}=6{SUcw-UQmw+i8lokRASPsbT=H|4D|( zk^P7>TUEFho!3qXSWn$m2{lHXw zD>eN6-;wwq9(?@f^F4L2Ny5_6!d~iiA^s~(|B*lbZir-$&%)l>%Q(36yOIAu|326K ztmBWz|MLA{Kj(H_{w2gd*nZ6a@ma(w==~EHIscEk|C=NGJa%Ruh4_+~f|%rt{I5v* zIX@F?|KJID56-ivb+PLo(9hn_CdK{irOcL15>JNQFY112^$+}JPyI{uQ~$&E*=ri; z`d^fH?4f=8vKHT4!p9O*fX(brB75Y9?e>T9=X#Fc@V#%@5^)~#zu5I(=>LQA-EGTS zecy*#6gG+8lapch#Hh%vl(+}J;Q!hC1OKoo;#h3#V%5Js)tQ)|>pTT@1ojd+F9Gey zg`B)zm`|Mo%tH31s4=<+`Pu|B3orXwNyIcNN>;fBkIj^X8P}RXhF= zXQK1u5RLN7k#_Q(KznJrALtMM13!vhfr025ar?@-%{l|uWt@NEd<$~n>RQL{ z+o;->n)+~0tt(u|o_9h!T`%M8%)w2awpV9b*xz9Pl-daUJm3y-HT%xg`^mFd6LBeL z!0~s;zEr)Bn9x)I(wx`;JVwvRcc^io2XX(Nn3vr3dgbrr@YJ?K3w18P*52^ieBCQP z=Up1V$N2~5ppJHRTeY8QfM(7Yv&RG7oWJAyv?c3g(29)P)u;_o&w|&)HGDIinXT~p z3;S|e$=&Tek9Wn!`cdY+d-w@o`37}x{(hl>ykB|%9yB$CGdIcl7Z?d&lJ%}QHck77 zJPR%C+s2w1_Dl_pxu6$Zi!`HmoD-%7OD@7%lKLL^Ixd9VlRSW*o&$^iQ2z+}hTgH) z#91TO#+jH<`w4L}XWOt(`gqM*uTUcky`O(mEyU|4dJoy6*UZJ7%*}ajuos%~>&P2j zk23f5<@GeV?(?`l=ih+D8t`d72xrUjv0wsg;%s1@*2p?TQ;n2$pV7h?_T%sL>iL@w zZ{lmc<|B7!e&o!zs6RW+u8+aDyUdG>ZS(v&rT$QVymB7sEC@VsK1dg^3F@K90-wYB zX!we79qx`(6LA>F$~{{xE8-3Wzyfe`+Lsce(?uj{k@lb97YTJt#>l*Z&LyKX@zjmu?UJC9w~;|NsB{%7G}y*uNDBxirfC EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/website/react-router.config.ts b/website/react-router.config.ts new file mode 100644 index 0000000..afe7ca6 --- /dev/null +++ b/website/react-router.config.ts @@ -0,0 +1,6 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + appDirectory: "src", + prerender: ["/", "/defer-example"], +} satisfies Config; diff --git a/website/src/entry.client.tsx b/website/src/entry.client.tsx new file mode 100644 index 0000000..e13f9b4 --- /dev/null +++ b/website/src/entry.client.tsx @@ -0,0 +1,7 @@ +import { startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +startTransition(() => { + hydrateRoot(document, ); +}); diff --git a/website/src/entry.server.tsx b/website/src/entry.server.tsx new file mode 100644 index 0000000..31814ba --- /dev/null +++ b/website/src/entry.server.tsx @@ -0,0 +1,163 @@ +import { PassThrough } from "node:stream"; +import { createReadableStreamFromReadable } from "@react-router/node"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; +import type { + ActionFunctionArgs, + AppLoadContext, + EntryContext, + LoaderFunctionArgs, +} from "react-router"; +import { isRouteErrorResponse, ServerRouter } from "react-router"; +import { notifyError } from "./lib/errors"; + +export const streamTimeout = 60 * 20 * 1_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + reactRouterContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + reactRouterContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + reactRouterContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + reactRouterContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + let timeoutId: NodeJS.Timeout; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + clearTimeout(timeoutId); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + clearTimeout(timeoutId); + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + timeoutId = setTimeout(abort, streamTimeout); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + reactRouterContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + let timeoutId: NodeJS.Timeout; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + clearTimeout(timeoutId); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + clearTimeout(timeoutId); + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + timeoutId = setTimeout(abort, streamTimeout); + }); +} + +export function handleError( + error: any, + { request, params, context }: LoaderFunctionArgs | ActionFunctionArgs +) { + // https://github.com/remix-run/remix/discussions/8933 + if (request.signal.aborted || isRouteErrorResponse(error)) { + return; + } + if (error?.["status"] === 404) { + return; + } + if (error instanceof Error) { + notifyError(error, "unhandled remix error"); + } else { + console.error("error", error); + } +} diff --git a/website/src/lib/errors.ts b/website/src/lib/errors.ts new file mode 100644 index 0000000..0dd67a6 --- /dev/null +++ b/website/src/lib/errors.ts @@ -0,0 +1,34 @@ +import { captureException, flush, init } from "sentries"; + +init({ + dsn: "https://3e3f1075fec9ee2de1e0f79026b5f734@o4508014272446464.ingest.de.sentry.io/4508014292697168", + integrations: [], + tracesSampleRate: 0.01, + profilesSampleRate: 0.01, + beforeSend(event) { + if (process.env.NODE_ENV === "development") { + return null; + } + if (process.env.BYTECODE_RUN) { + return null; + } + if (event?.["name"] === "AbortError") { + return null; + } + + return event; + }, +}); + +export async function notifyError(error: any, msg?: string) { + console.error(msg, error); + captureException(error, { extra: { msg } }); + await flush(1000); +} + +export class AppError extends Error { + constructor(message: string) { + super(message); + this.name = "AppError"; + } +} diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts new file mode 100644 index 0000000..b13e418 --- /dev/null +++ b/website/src/lib/utils.ts @@ -0,0 +1,10 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export const sleep = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/website/src/root.tsx b/website/src/root.tsx new file mode 100644 index 0000000..edb45f8 --- /dev/null +++ b/website/src/root.tsx @@ -0,0 +1,63 @@ +import "website/src/styles/globals.css"; +import { Route } from "./+types/root"; +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + if (isRouteErrorResponse(error)) { + return ( +
+

+ {error.status} {error.statusText} +

+

{error.data}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
+          {error.stack}
+        
+
+ ); + } else { + return ( +
+

Unknown Error

+
+ ); + } +} + +export default function App() { + return ; +} diff --git a/website/src/routes.ts b/website/src/routes.ts new file mode 100644 index 0000000..30b5e6f --- /dev/null +++ b/website/src/routes.ts @@ -0,0 +1,4 @@ +import type { RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; diff --git a/website/src/routes/_index.tsx b/website/src/routes/_index.tsx new file mode 100644 index 0000000..20a6535 --- /dev/null +++ b/website/src/routes/_index.tsx @@ -0,0 +1,9 @@ +import { redirect } from 'react-router'; + +export const loader = () => { + return redirect('https://github.com/remorses/playwriter'); +}; + +export default function Index() { + return null; +} diff --git a/website/src/routes/defer-example.tsx b/website/src/routes/defer-example.tsx new file mode 100644 index 0000000..e29848a --- /dev/null +++ b/website/src/routes/defer-example.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { sleep } from "../lib/utils"; +import { Route } from "./+types/defer-example"; + +async function getProjectLocation() { + return Promise.resolve().then(() => sleep(1000).then(() => "hi")); +} + +export async function loader({}: Route.LoaderArgs) { + return { + project: getProjectLocation(), + }; +} + +export default function ProjectRoute({ loaderData }: Route.ComponentProps) { + const location = React.use(loaderData.project); + + return ( +
+

Let's locate your project

+

Your project is at {location}.

+
+ ); +} diff --git a/website/src/styles/globals.css b/website/src/styles/globals.css new file mode 100644 index 0000000..7ba0a5f --- /dev/null +++ b/website/src/styles/globals.css @@ -0,0 +1,143 @@ +@import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); + +@plugin "@tailwindcss/typography"; + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.637 0.237 25.331); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.871 0.006 286.286); + --ring: oklch(0.871 0.006 286.286); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.871 0.006 286.286); + --sidebar-primary: oklch(0.37 0.013 285.805); + --sidebar-primary-foreground: oklch(0.871 0.006 286.286); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.871 0.006 286.286); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.871 0.006 286.286); +} +.dark { + --background: oklch(0.21 0.006 285.885); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.637 0.237 25.331); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.274 0.006 286.033); + --input: oklch(0.274 0.006 286.033); + --ring: oklch(0.442 0.017 285.786); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: #000; + --sidebar-foreground: oklch(0.871 0.006 286.286); + --sidebar-primary: oklch(0.37 0.013 285.805); + --sidebar-primary-foreground: oklch(0.871 0.006 286.286); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.871 0.006 286.286); + --sidebar-border: oklch(0.274 0.006 286.033); + --sidebar-ring: oklch(0.442 0.017 285.786); +} + +@theme inline { + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom scrollbar styles: shadcn-inspired */ +::-webkit-scrollbar { + width: 6px; + height: 6px; + background: transparent; + border-radius: var(--radius-md); +} + +::-webkit-scrollbar-thumb { + background: var(--color-muted); + border-radius: var(--radius-md); + border: none; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-muted-foreground); +} diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000..9ecab99 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,28 @@ +{ + "include": ["src"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@react-router/node", "vite/client", "@types/node"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "allowArbitraryExtensions": true, + "allowImportingTsExtensions": true, + "noImplicitAny": false, + "useUnknownInCatchVariables": false, + "forceConsistentCasingInFileNames": true, + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "website/*": ["./*"] + }, + "noEmit": true + } +} diff --git a/website/vite.config.ts b/website/vite.config.ts new file mode 100644 index 0000000..3f2ca80 --- /dev/null +++ b/website/vite.config.ts @@ -0,0 +1,47 @@ +/// +import { reactRouter } from "@react-router/dev/vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import EnvironmentPlugin from "vite-plugin-environment"; +import { defineConfig } from "vite"; +import { + viteExternalsPlugin, + enablePreserveModulesPlugin, +} from "@xmorse/deployment-utils/dist/vite-externals-plugin.js"; +import { reactRouterServerPlugin } from "@xmorse/deployment-utils/dist/react-router.js"; +import tsconfigPaths from "vite-tsconfig-paths"; + +const NODE_ENV = JSON.stringify(process.env.NODE_ENV || "production"); + +export default defineConfig({ + clearScreen: false, + define: { + "process.env.NODE_ENV": NODE_ENV, + }, + test: { + pool: "threads", + exclude: ["**/dist/**", "**/esm/**", "**/node_modules/**", "**/e2e/**"], + poolOptions: { + threads: { + isolate: false, + }, + }, + }, + plugins: [ + EnvironmentPlugin("all", { prefix: "PUBLIC" }), + EnvironmentPlugin("all", { prefix: "NEXT_PUBLIC" }), + process.env.VITEST ? react() : reactRouter(), + tsconfigPaths(), + // viteExternalsPlugin({ + // externals: ["@sentry/node"], + // }), + // enablePreserveModulesPlugin(), + reactRouterServerPlugin({ port: process.env.PORT || '8044' }), + tailwindcss(), + ], + build: { + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});