diff --git a/AGENTS.md b/AGENTS.md
index 051982a20..20a6c4e85 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -154,14 +154,14 @@ let default: unit => React.element
## Styling
- **Tailwind CSS v4** configured via the Vite plugin (`@tailwindcss/vite`). There is no `tailwind.config.js`.
-- All Tailwind configuration is in `styles/main.css` using CSS-native `@theme` blocks.
+- Tailwind design tokens and custom utilities live in `styles/_theme.css`. It is imported by `styles/main.css` and referenced by route-specific stylesheets.
- **LightningCSS** is used as the CSS transformer.
-- The project defines custom design tokens in `styles/main.css`:
+- The project defines custom design tokens in `styles/_theme.css`:
- Custom colors: `gray-*`, `fire-*`, `sky-*`, `berry-*`, `water`, `turtle`, `orange-*`
- Custom font sizes: `text-11` through `text-68`
- Custom utility classes: `hl-title`, `hl-1`–`hl-5`, `body-lg`, `body-md`, `body-sm`, `captions`
- Fonts: Inter (sans), Roboto Mono (mono)
-- Use existing custom utilities and design tokens. Check `styles/main.css` before creating new ones.
+- Use existing custom utilities and design tokens. Check `styles/_theme.css` before creating new ones.
## Testing
diff --git a/apps/docs/app/DocsRoot.res b/apps/docs/app/DocsRoot.res
index f45def69d..3e38c1f3d 100644
--- a/apps/docs/app/DocsRoot.res
+++ b/apps/docs/app/DocsRoot.res
@@ -11,7 +11,6 @@ let default = () => {
{CypressBootstrap.element()}
-
diff --git a/apps/docs/app/DocsRoot.resi b/apps/docs/app/DocsRoot.resi
index 98c027984..e9a4c6ad1 100644
--- a/apps/docs/app/DocsRoot.resi
+++ b/apps/docs/app/DocsRoot.resi
@@ -1,4 +1,5 @@
/** Shared shell, token styles, and the stable Cypress bootstrap slot.
Runtime grammars belong to content routes and the playground. */
+/** Keep server-rendered content visible; head stylesheets handle render blocking. */
@react.component
let default: unit => Jsx.element
diff --git a/apps/docs/app/DocsRoutes.res b/apps/docs/app/DocsRoutes.res
index 326a19350..35886a279 100644
--- a/apps/docs/app/DocsRoutes.res
+++ b/apps/docs/app/DocsRoutes.res
@@ -73,7 +73,7 @@ let syntaxLookupDetailRoutes =
)
let default = [
- index("./routes/LandingPageRoute.jsx"),
+ layout("./layouts/HomepageLayoutRoute.jsx", [index("./routes/LandingPageRoute.jsx")]),
route("try", "./routes/TryRoute.jsx"),
layout(
"./layouts/ContentLayoutRoute.jsx",
diff --git a/apps/docs/app/DocsRoutes.resi b/apps/docs/app/DocsRoutes.resi
index c60e74bfd..ba1514a46 100644
--- a/apps/docs/app/DocsRoutes.resi
+++ b/apps/docs/app/DocsRoutes.resi
@@ -10,5 +10,5 @@ let docsReactRoutes: array
let docsGuidesRoutes: array
let communityRoutes: array
let syntaxLookupDetailRoutes: array
-/** Keep home, the lazy playground, and not-found outside the content highlighting boundary. */
+/** Keep homepage, playground, and content styles scoped to their route groups. */
let default: array
diff --git a/apps/docs/app/layouts/ContentLayoutRoute.res b/apps/docs/app/layouts/ContentLayoutRoute.res
index 5d0015195..a03d7b926 100644
--- a/apps/docs/app/layouts/ContentLayoutRoute.res
+++ b/apps/docs/app/layouts/ContentLayoutRoute.res
@@ -1,3 +1,10 @@
+@module("../../styles/content.css?url")
+external contentCss: string = "default"
+
+type stylesheet = {rel: string, href: string}
+
+let links = () => [{rel: "stylesheet", href: contentCss}]
+
// Route-module initialization runs before any content children render.
let () = ContentHighlighting.register(HighlightLanguages.defaultInstance)
diff --git a/apps/docs/app/layouts/ContentLayoutRoute.resi b/apps/docs/app/layouts/ContentLayoutRoute.resi
index a5c43f841..381da2b21 100644
--- a/apps/docs/app/layouts/ContentLayoutRoute.resi
+++ b/apps/docs/app/layouts/ContentLayoutRoute.resi
@@ -1,3 +1,7 @@
-/** Own runtime highlighting for content routes without adding a DOM wrapper. */
+type stylesheet = {rel: string, href: string}
+
+let links: unit => array
+
+/** Own content styles and runtime highlighting without adding a DOM wrapper. */
@react.component
let default: unit => React.element
diff --git a/apps/docs/app/layouts/HomepageLayoutRoute.res b/apps/docs/app/layouts/HomepageLayoutRoute.res
new file mode 100644
index 000000000..c8bdddb7d
--- /dev/null
+++ b/apps/docs/app/layouts/HomepageLayoutRoute.res
@@ -0,0 +1,9 @@
+@module("../../styles/homepage.css?url")
+external homepageCss: string = "default"
+
+type stylesheet = {rel: string, href: string}
+
+let links = () => [{rel: "stylesheet", href: homepageCss}]
+
+@react.component
+let default = () =>
diff --git a/apps/docs/app/layouts/HomepageLayoutRoute.resi b/apps/docs/app/layouts/HomepageLayoutRoute.resi
new file mode 100644
index 000000000..bf0517cf9
--- /dev/null
+++ b/apps/docs/app/layouts/HomepageLayoutRoute.resi
@@ -0,0 +1,7 @@
+type stylesheet = {rel: string, href: string}
+
+let links: unit => array
+
+/** Own homepage styles without adding a DOM wrapper. */
+@react.component
+let default: unit => React.element
diff --git a/apps/docs/app/routes/TryRoute.res b/apps/docs/app/routes/TryRoute.res
index d3636e6f6..22785fe5b 100644
--- a/apps/docs/app/routes/TryRoute.res
+++ b/apps/docs/app/routes/TryRoute.res
@@ -1,3 +1,10 @@
+@module("../../styles/playground.css?url")
+external playgroundCss: string = "default"
+
+type stylesheet = {rel: string, href: string}
+
+let links = () => [{rel: "stylesheet", href: playgroundCss}]
+
type props = {
bundleBaseUrl: string,
versions: array,
diff --git a/apps/docs/app/routes/TryRoute.resi b/apps/docs/app/routes/TryRoute.resi
index 28764a056..65e640435 100644
--- a/apps/docs/app/routes/TryRoute.resi
+++ b/apps/docs/app/routes/TryRoute.resi
@@ -1,3 +1,7 @@
+type stylesheet = {rel: string, href: string}
+
+let links: unit => array
+
type props = {
bundleBaseUrl: string,
versions: array,
diff --git a/apps/docs/cypress.homepage.config.mjs b/apps/docs/cypress.homepage.config.mjs
index 93fe3f1b2..484a30a2f 100644
--- a/apps/docs/cypress.homepage.config.mjs
+++ b/apps/docs/cypress.homepage.config.mjs
@@ -9,7 +9,7 @@ export default defineConfig({
e2e: {
...config.e2e,
baseUrl: "http://127.0.0.1:4173",
- specPattern: ["e2e/homepage/**/*.cy.jsx", "e2e/homepage/**/*.cy.js"],
+ specPattern: "e2e/homepage/**/*.cy.jsx",
excludeSpecPattern: [],
supportFile: "e2e/homepage/HomepageSupport.jsx",
screenshotOnRunFailure: true,
diff --git a/apps/docs/e2e/Playground.cy.res b/apps/docs/e2e/Playground.cy.res
index 1b77dc10e..f4fd5eb09 100644
--- a/apps/docs/e2e/Playground.cy.res
+++ b/apps/docs/e2e/Playground.cy.res
@@ -66,6 +66,8 @@ describe("Playground", () => {
it("should highlight JavaScript after a direct playground load", () => {
visit("/try")
get(".cm-editor")->shouldBeVisible->ignore
+ get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
+ get(".cm-editor")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
get("pre.whitespace-pre-wrap")->shouldContainText("react/jsx-runtime")->ignore
get("pre code.lang-js span[class^='hljs-']")->should("exist")->ignore
})
@@ -134,6 +136,10 @@ describe("Playground", () => {
// Verify playground shell is in light mode
get("main")->shouldWithValue("have.class", "playground-theme-light")->ignore
+ get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(250, 251, 252)")->ignore
+ get(".cm-editor")
+ ->shouldWithKeyValue("have.css", "background-color", "rgb(255, 255, 255)")
+ ->ignore
cyWindow()
->its("localStorage")
->invokeWithArg("getItem", "playgroundTheme")
@@ -150,6 +156,8 @@ describe("Playground", () => {
// Verify playground shell is back to dark mode
get("main")->shouldWithValue("have.class", "playground-theme-dark")->ignore
+ get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
+ get(".cm-editor")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
cyWindow()
->its("localStorage")
->invokeWithArg("getItem", "playgroundTheme")
diff --git a/apps/docs/e2e/bindings/Cypress.res b/apps/docs/e2e/bindings/Cypress.res
index 793df1e4c..be8c3c607 100644
--- a/apps/docs/e2e/bindings/Cypress.res
+++ b/apps/docs/e2e/bindings/Cypress.res
@@ -15,6 +15,9 @@ type rec window = {
}
and clipboard
type response = {status: int, body: string, headers: Dict.t}
+type replyResponse = {statusCode: int, headers: Dict.t, body: string}
+type networkError = {forceNetworkError: bool}
+type fetchResponse
type automation = {command: string, params?: {permissions: array, origin: string}}
type request = {url: string, resourceType: string}
type routeMatcher = {resourceType?: string, pathname?: string}
@@ -59,6 +62,10 @@ external interceptStatic: (searchRouteMatcher, staticResponse) => chain =
@val @scope("cy")
external interceptPattern: (RegExp.t, request => unit) => chain = "intercept"
@val @scope("cy")
+external interceptFailurePattern: (RegExp.t, networkError) => chain = "intercept"
+@val @scope("cy")
+external interceptFailureString: (string, networkError) => chain = "intercept"
+@val @scope("cy")
external interceptDeferred: (RegExp.t, unit => promise) => chain = "intercept"
@val @scope("cy")
external interceptDeferredRequest: (routeMatcher, request => promise) => chain =
@@ -110,6 +117,9 @@ external shouldProperty: (chain, @as("have.prop") _, string, string) =
"should"
@send external attribute: (chain, @as("attr") _, string) => chain = "invoke"
@send external propertyInt: (chain<'a>, string) => chain = "its"
+@send external propertyString: (chain<'a>, string) => chain = "its"
+@send
+external shouldPropertyExist: (chain<'a>, @as("have.property") _, string) => chain<'a> = "should"
@send external shouldSatisfy: (chain<'a>, 'a => unit) => chain<'a> = "should"
@send external click: chain => chain = "click"
@send external first: chain => chain = "first"
@@ -127,6 +137,8 @@ external containsChildRegex: (chain, string, RegExp.t) => chain int = "naturalWidth"
@send external readText: clipboard => promise = "readText"
@send external destroy: request => unit = "destroy"
+@set external setRequestAlias: (request, string) => unit = "alias"
+@send external reply: (request, replyResponse) => unit = "reply"
@val external expect: ('a, ~message: string=?) => assertion = "expect"
@send @scope("to") external equal: (assertion, 'a) => unit = "equal"
@@ -164,6 +176,7 @@ external addEventListenerOnce: (Dom.document, string, unit => unit, listenerOpti
@get external baseURI: Dom.element => string = "baseURI"
@get external currentSrc: Dom.element => string = "currentSrc"
@get external parentElement: Dom.element => Nullable.t = "parentElement"
+@get external elementHref: Dom.element => string = "href"
@send external decode: Dom.element => promise = "decode"
@val @scope("Array") external elementsFrom: elementList => array = "from"
@@ -183,6 +196,9 @@ external addEventListenerOnce: (Dom.document, string, unit => unit, listenerOpti
@get external fontLoaded: fontFace => promise = "loaded"
@send external windowRequestAnimationFrame: (window, float => unit) => int = "requestAnimationFrame"
+@send external windowFetch: (window, string) => promise = "fetch"
+@get external fetchStatus: fetchResponse => int = "status"
+@send external responseText: fetchResponse => promise = "text"
@get external styleSheets: Dom.document => styleSheetList = "styleSheets"
@val @scope("Array") external styleSheetsFrom: styleSheetList => array = "from"
@@ -190,7 +206,10 @@ external addEventListenerOnce: (Dom.document, string, unit => unit, listenerOpti
@val @scope("Array") external rulesFrom: cssRuleList => array = "from"
@get external ruleType: cssRule => int = "type"
@get external nestedRules: cssRule => Nullable.t = "cssRules"
+@get external cssText: cssRule => string = "cssText"
+@get external selectorText: cssRule => string = "selectorText"
@get external ruleStyle: cssRule => cssStyle = "style"
+@get external boxSizing: cssStyle => string = "boxSizing"
@get external fontFamily: cssStyle => string = "fontFamily"
@get external fontWeight: cssStyle => string = "fontWeight"
@get external fontStyle: cssStyle => string = "fontStyle"
diff --git a/apps/docs/e2e/homepage/HomepageInitialContent.cy.res b/apps/docs/e2e/homepage/HomepageInitialContent.cy.res
new file mode 100644
index 000000000..9116e5dd9
--- /dev/null
+++ b/apps/docs/e2e/homepage/HomepageInitialContent.cy.res
@@ -0,0 +1,45 @@
+open Cypress
+open HomepageHelpers
+
+let visitWithoutHydration = path => {
+ // Cypress needs JavaScript itself; empty application modules keep the AUT prerender-only.
+ interceptPattern(/\/assets\/[^/]+\.js(?:\?.*)?$/, request => {
+ if /\/entry\.client-[^/]+\.js/->RegExp.test(request.url) {
+ request->setRequestAlias("clientEntry")
+ }
+ request->reply({
+ statusCode: 200,
+ headers: Dict.fromArray([("content-type", "text/javascript")]),
+ body: "",
+ })
+ })->ignore
+ visit(path)
+ wait("@clientEntry")->propertyString("response.body")->shouldEqual("")->ignore
+}
+
+it("homepage paragraphs render without hydration or downloaded fonts", () => {
+ interceptFailurePattern(/\.(?:woff2?|ttf|otf)(?:\?|$)/, {forceNetworkError: true})->ignore
+ visitWithoutHydration("/")
+ get("html")->shouldCss("opacity", "1")->ignore
+ containsIn("p", "ReScript is a strongly typed language that compiles to clean,")
+ ->should("be.visible")
+ ->shouldCss("color", "rgb(105, 107, 125)")
+ ->ignore
+ containsIn("p", "Its fast compiler and static type system")->should("be.visible")->ignore
+})
+
+["/", "/docs/manual/introduction/"]->Array.forEach(path => {
+ it(`${path} remains readable when the shared stylesheet fails`, () => {
+ interceptFailureString("**/assets/main-*.css", {forceNetworkError: true})
+ ->as_("sharedStylesheet")
+ ->ignore
+ visitWithoutHydration(path)
+ wait("@sharedStylesheet")->shouldPropertyExist("error")->ignore
+ get("html")->shouldCss("opacity", "1")->ignore
+ if path === "/" {
+ containsIn("h1", headline)->should("be.visible")->ignore
+ } else {
+ containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore
+ }
+ })
+})
diff --git a/apps/docs/e2e/homepage/HomepageRouteCss.cy.res b/apps/docs/e2e/homepage/HomepageRouteCss.cy.res
new file mode 100644
index 000000000..1b3b679d4
--- /dev/null
+++ b/apps/docs/e2e/homepage/HomepageRouteCss.cy.res
@@ -0,0 +1,183 @@
+open Cypress
+open HomepageHelpers
+
+type foundationCounts = {
+ fonts: int,
+ uniqueFonts: int,
+ tokens: array,
+ resets: int,
+}
+
+let homepageLayoutSelectors = [
+ ".max-w-1060",
+ ".md\\:grid-cols-10",
+ ".md\\:col-span-6",
+ ".min-h-148",
+]
+
+let expectDesktopLogo = () => {
+ get(`a[aria-label="homepage"]`)
+ ->shouldCss("width", "128px")
+ ->shouldCss("height", "40px")
+ ->ignore
+ get(`a[aria-label="homepage"] img[alt="ReScript Home"]`)->should("be.visible")->ignore
+}
+
+let loadedStyles = () =>
+ cyWindow()->thenPromise(async window => {
+ let styles = await window.document
+ ->querySelectorAll(`link[rel="stylesheet"]`)
+ ->elementsFrom
+ ->Array.map(async link => {
+ let response = await window->windowFetch(link->elementHref)
+ expect(response->fetchStatus, ~message=link->elementHref)->equal(200)
+ await response->responseText
+ })
+ ->Promise.all
+ styles->Array.join("\n")
+ })
+
+let rec flattenRules = rules =>
+ rules->Array.flatMap(rule =>
+ switch rule->nestedRules->Nullable.toOption {
+ | Some(nested) => [rule]->Array.concat(nested->rulesFrom->flattenRules)
+ | None => [rule]
+ }
+ )
+
+let foundationCounts = window => {
+ let rules =
+ window.document
+ ->styleSheets
+ ->styleSheetsFrom
+ ->Array.flatMap(sheet => sheet->sheetRules->rulesFrom->flattenRules)
+ let fonts = rules->Array.filter(rule => rule->ruleType === 5)->Array.map(rule => rule->cssText)
+ let styles = rules->Array.filter(rule => rule->ruleType === 1)
+ let tokens =
+ ["--font-sans", "--color-gray-90", "--color-fire", "--text-48"]->Array.map(token =>
+ styles->Array.filter(rule => rule->ruleStyle->propertyValue(token) !== "")->Array.length
+ )
+ let uniqueFonts =
+ fonts->Array.reduce([], (unique, font) =>
+ unique->Array.includes(font) ? unique : unique->Array.concat([font])
+ )
+ let resets = styles->Array.filter(rule =>
+ rule
+ ->selectorText
+ ->String.split(",")
+ ->Array.some(selector => selector->String.trim === "*") &&
+ rule->ruleStyle->boxSizing === "border-box"
+ )
+ {
+ fonts: fonts->Array.length,
+ uniqueFonts: uniqueFonts->Array.length,
+ tokens,
+ resets: resets->Array.length,
+ }
+}
+
+let expectContentStyles = () =>
+ loadedStyles()->then(styles => {
+ expect(styles)->include_(".markdown-body")
+ expect(styles)->notInclude(".playground-theme")
+ homepageLayoutSelectors->Array.forEach(selector => expect(styles)->notInclude(selector))
+ })
+
+it("homepage styles exclude content, search, and playground rules", () => {
+ visit("/")
+ containsIn("h1", headline)->should("be.visible")->ignore
+ loadedStyles()
+ ->then(styles => {
+ expect(styles)->include_(".gallery-selector")
+ homepageLayoutSelectors->Array.forEach(selector => expect(styles)->include_(selector))
+ [".markdown-body", ".playground-theme", ".DocSearch-Modal"]->Array.forEach(
+ selector => expect(styles)->notInclude(selector),
+ )
+ })
+ ->ignore
+})
+
+it("shared foundations are emitted once across route navigation", () => {
+ visit("/")
+ cyWindow()
+ ->thenMap(foundationCounts)
+ ->then(initial => {
+ expect(initial.fonts)->greaterThan(0)
+ expect(initial.uniqueFonts)->equal(initial.fonts)
+ expect(initial.tokens)->deepEqual([1, 1, 1, 1])
+ expect(initial.resets)->equal(1)
+ containsInRegex("a", /^Docs$/)->click->ignore
+ containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore
+ cyWindow()->thenMap(foundationCounts)->shouldDeepEqual(initial)->ignore
+ get(`a[aria-label="homepage"]`)->click->ignore
+ containsIn("h1", headline)->shouldCss("font-size", "68px")->ignore
+ cyWindow()->thenMap(foundationCounts)->shouldDeepEqual(initial)->ignore
+ })
+ ->ignore
+})
+
+it("desktop navigation keeps its responsive logo across route stylesheets", () => {
+ visit("/")
+ expectDesktopLogo()
+ containsInRegex("a", /^Docs$/)->click->ignore
+ containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore
+ expectDesktopLogo()
+ get(`a[aria-label="homepage"]`)->click->ignore
+ containsIn("h1", headline)->should("be.visible")->ignore
+ expectDesktopLogo()
+})
+
+it("documentation styles are prefetched only after navigation intent", () => {
+ visit("/")
+ let prefetch = `link[rel="prefetch"][as="style"][href*="/content-"]`
+ get(prefetch)->should("not.exist")->ignore
+ containsInRegex("a", /^Get started$/)->focusElement->ignore
+ get(prefetch)->shouldInt("have.length", 1)->ignore
+ containsInRegex("a", /^Docs$/)->focusElement->ignore
+ get(prefetch)->should("not.exist")->ignore
+})
+
+[
+ ("/docs/manual/introduction/", "ReScript"),
+ ("/brand/", "Brand Assets"),
+ ("/packages/", "Libraries & Bindings"),
+]->Array.forEach(((path, title)) => {
+ it(`cold ${path} loads its content styles`, () => {
+ visit(path)
+ containsIn("h1", title)
+ ->should("be.visible")
+ ->shouldCss("font-weight", "600")
+ ->shouldCss("font-size", "48px")
+ ->ignore
+ expectContentStyles()->ignore
+ })
+})
+
+it("cold blog styles preserve article typography", () => {
+ visit("/blog/")
+ get("h2")
+ ->first
+ ->should("be.visible")
+ ->shouldCss("font-size", "48px")
+ ->shouldCss("font-weight", "600")
+ ->ignore
+ expectContentStyles()->ignore
+})
+
+it("mobile documentation drawer retains its layout after navigation", () => {
+ viewport(375, 812)
+ visit("/")
+ containsInRegex("a", /^Docs$/)->click->ignore
+ containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore
+ get(`button[aria-label="Toggle navigation menu"]`)->click->ignore
+ get("dialog#mobile-tertiary-drawer")
+ ->as_("drawer")
+ ->should("be.visible")
+ ->shouldCss("background-color", "rgb(255, 255, 255)")
+ ->shouldCss("margin-left", "0px")
+ ->ignore
+ alias("@drawer")->containsChildRegex("a", /^Installation$/)->click->ignore
+ containsInRegex("h1", /^Installation$/)->should("be.visible")->ignore
+ realPressKey("Escape")->ignore
+ alias("@drawer")->should("not.be.visible")->ignore
+})
diff --git a/apps/docs/src/components/LandingPageIntro.res b/apps/docs/src/components/LandingPageIntro.res
index fcf9d1163..17e368c4d 100644
--- a/apps/docs/src/components/LandingPageIntro.res
+++ b/apps/docs/src/components/LandingPageIntro.res
@@ -18,7 +18,7 @@ let make = () => {
confidence as your codebase grows.`)}
{React.string("Get started")}
diff --git a/apps/docs/src/components/LandingPageIntro.resi b/apps/docs/src/components/LandingPageIntro.resi
index 1ca44ce26..26c2771b5 100644
--- a/apps/docs/src/components/LandingPageIntro.resi
+++ b/apps/docs/src/components/LandingPageIntro.resi
@@ -1,2 +1,3 @@
+/** Prefetch installation content only after hover or focus intent. */
@react.component
let make: unit => React.element
diff --git a/apps/docs/styles/_shared-sources.css b/apps/docs/styles/_shared-sources.css
new file mode 100644
index 000000000..ffaf36f90
--- /dev/null
+++ b/apps/docs/styles/_shared-sources.css
@@ -0,0 +1,5 @@
+/* Repeat shell utilities in each route so responsive variants follow every base utility. */
+@source "../app/DocsRoot.{res,jsx}";
+@source "../app/routes/NotFoundRoute.{res,jsx}";
+@source "../src/components/{NavbarPrimary,NavbarMobileOverlay,NavbarUtils,Search,SearchNotice}.{res,jsx}";
+@source "../../../packages/shared/src/Icon.{res,jsx}";
diff --git a/apps/docs/styles/_theme.css b/apps/docs/styles/_theme.css
new file mode 100644
index 000000000..5982f28f0
--- /dev/null
+++ b/apps/docs/styles/_theme.css
@@ -0,0 +1,252 @@
+/* Inline defaults keep route-only utilities independent of root source detection. */
+@import "tailwindcss/theme.css" layer(theme) theme(inline);
+
+/* Emit custom tokens once at the root, including those referenced by lazy routes. */
+@theme static {
+ --radius-*: initial;
+ --radius-none: 0;
+ --radius-sm: 0.125rem;
+ --radius: 0.25rem;
+ --radius-lg: 0.5rem;
+ --radius-xl: 1rem;
+ --radius-full: 9999px;
+
+ --breakpoint-*: initial;
+ --breakpoint-xs: 599px;
+ --breakpoint-sm: 576px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 1024px;
+ --breakpoint-xl: 1440px;
+
+ --text-*: initial;
+ --text-11: 0.6875rem;
+ --text-12: 0.75rem;
+ --text-14: 0.875rem;
+ --text-16: 1rem;
+ --text-18: 1.125rem;
+ --text-24: 1.5rem;
+ --text-32: 2rem;
+ --text-48: 3rem;
+ --text-68: 4.25rem;
+
+ --font-weight-*: initial;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+
+ --leading-*: initial;
+ --leading-2: 1.35;
+ --leading-4: 1.5;
+ --leading-5: 1.75;
+ --leading-none: 1;
+ --leading-tight: 1.25;
+
+ --tracking-*: initial;
+ --tracking-tighter: -0.03em;
+ --tracking-tight: -0.02em;
+ --tracking-normal: 0;
+ --tracking-wide: 0.075em;
+ --tracking-widest: 0.15em;
+
+ --z-index-*: initial;
+ --z-index-0: 0;
+ --z-index-1: 1;
+ --z-index-2: 2;
+ --z-index-3: 3;
+ --z-index-4: 4;
+ --z-index-5: 5;
+ --z-index-10: 10;
+ --z-index-20: 20;
+ --z-index-25: 25;
+ --z-index-30: 30;
+ --z-index-40: 40;
+ --z-index-50: 50;
+ --z-index-75: 75;
+ --z-index-100: 100;
+ --z-index-auto: auto;
+
+ --font-*: initial;
+ --font-sans:
+ Inter, SF Pro Text, Roboto, -apple-system, BlinkMacSystemFont,
+ Helvetica Neue, Arial, sans-serif;
+ --font-mono: Roboto Mono, SFMono-Regular, Menlo, Segoe UI, Courier, monospace;
+
+ --color-gray-5: #fafbfc;
+ --color-gray-10: #fafbfc;
+ --color-gray-20: #edf0f2;
+ --color-gray-30: #cdcdd6;
+ --color-gray-40: #979aad;
+ --color-gray-60: #696b7d;
+ --color-gray-70: #3c3d4e;
+ --color-gray-80: #232538;
+ --color-gray-90: #14162c;
+ --color-gray-95: #14162c;
+ --color-gray-100: #0b0d22;
+ --color-gray-5-tr: rgba(1, 20, 29, 0.02);
+ --color-gray-10-tr: rgba(1, 20, 38, 0.02);
+ --color-gray-60-tr: rgba(1, 4, 39, 0.6);
+ --color-gray-80-tr: rgba(1, 4, 39, 0.8);
+ --color-gray-90-tr: rgba(1, 4, 39, 0.95);
+
+ --color-white: #ffffff;
+ --color-white-80-tr: rgba(255, 255, 255, 0.8);
+ --color-white-60-tr: rgba(255, 255, 255, 0.6);
+ --color-white-40-tr: rgba(255, 255, 255, 0.4);
+
+ --color-fire-5: #fcf1f1;
+ --color-fire-10: #fbb8bb;
+ --color-fire-30: #f4646a;
+ --color-fire-50: #e6484f;
+ --color-fire-70: #c3373d;
+ --color-fire-90: #790c10;
+ --color-fire-100: #211332;
+ --color-fire: #e6484f;
+ --color-fire-dark: #e06c75;
+
+ --color-sky-5: #ecf1fc;
+ --color-sky-10: #dde8fd;
+ --color-sky-30: #638fe6;
+ --color-sky-70: #2258c3;
+ --color-sky-90: #0c2e6f;
+ --color-sky: #376fdd;
+
+ --color-berry-15: rgba(171, 94, 163, 0.15);
+ --color-berry-40: #a766d0;
+ --color-berry: #b151dd;
+ --color-berry-dark-50: #b984db;
+
+ --color-water: #5e5ede;
+ --color-water-dark: #637cc1;
+
+ --color-ocean-dark: #8aaec8;
+
+ --color-turtle: #02a875;
+ --color-turtle-dark: #388b72;
+
+ --color-orange-10: rgba(224, 172, 0, 0.1);
+ --color-orange-15: rgba(224, 172, 0, 0.15);
+ --color-orange: #dd8c1b;
+ --color-orange-dark: #d59b74;
+
+ --height-18: 4.5rem;
+ --height-inherit: inherit;
+
+ --min-width-320: 20rem;
+
+ --container-320: 20rem;
+ --container-400: 25rem;
+ --container-576: 36rem;
+ --container-740: 46.25rem;
+ --container-1060: 66.25rem;
+ --container-1280: 80rem;
+ --container-sm: 30rem;
+ --container-md: 40rem;
+ --container-xl: 66.25rem;
+ --container-none: none;
+ --container-full: 100%;
+
+ --inset-16: 4rem;
+ --inset-18: 4.5rem;
+
+ --spacing-2\/3: 66.666667%;
+ --spacing-9\/16: 56.25%;
+ --spacing-0_75: 0.135rem;
+
+ --animate-pulse: pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1);
+
+ --shadow-sm: 0 0.5px 0.5px 0 rgba(0, 0, 0, 0.05);
+
+ @keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+ }
+}
+
+@utility hl-title {
+ @apply text-[2.5rem] tracking-tighter leading-tight font-semibold text-gray-80;
+
+ @media (width >=theme(--breakpoint-xs)) {
+ @apply text-48;
+ }
+
+ @media (width >=theme(--breakpoint-md)) {
+ @apply text-68;
+ }
+
+ @media (width >=theme(--breakpoint-lg)) {
+ @apply font-bold;
+ }
+}
+
+@utility hl-1 {
+ @layer components {
+ @apply text-48 font-semibold tracking-tighter leading-tight text-black;
+ }
+}
+
+@utility hl-2 {
+ @layer components {
+ @apply text-32 font-semibold tracking-tighter leading-tight text-black;
+ }
+}
+
+@utility hl-3 {
+ @layer components {
+ @apply text-24 font-semibold tracking-tight leading-tight text-black;
+ }
+}
+
+@utility hl-4 {
+ @layer components {
+ @apply text-18 font-semibold tracking-tight leading-tight text-gray-80;
+ }
+}
+
+@utility hl-5 {
+ @layer components {
+ @apply text-14 font-bold tracking-normal leading-tight text-gray-80;
+ }
+}
+
+@utility hl-overline {
+ @layer components {
+ @apply text-11 font-semibold tracking-wide leading-tight uppercase text-gray-80;
+ }
+}
+
+@utility body-lg {
+ @layer components {
+ @apply text-18 font-normal tracking-tight leading-4;
+ }
+}
+
+@utility body-button {
+ @layer components {
+ @apply text-16 font-semibold tracking-normal leading-2;
+ }
+}
+
+@utility body-md {
+ @layer components {
+ @apply text-16 font-normal tracking-tight leading-4;
+ }
+}
+
+@utility body-sm {
+ @layer components {
+ @apply text-14 font-medium tracking-normal leading-2;
+ }
+}
+
+@utility captions {
+ @layer components {
+ @apply text-12 font-medium tracking-normal leading-2;
+ }
+}
diff --git a/apps/docs/styles/content.css b/apps/docs/styles/content.css
new file mode 100644
index 000000000..bee0e3cd0
--- /dev/null
+++ b/apps/docs/styles/content.css
@@ -0,0 +1,89 @@
+@reference "./_theme.css";
+@import "tailwindcss/utilities.css" layer(utilities) source(none);
+@import "./_shared-sources.css";
+@import "./_markdown.css" layer(base);
+
+@source "../app/routes/**/*.{res,jsx}";
+@source not "../app/routes/LandingPage*.{res,jsx}";
+@source not "../app/routes/TryRoute.{res,jsx}";
+@source "../app/layouts/**/*.{res,jsx}";
+@source "../src/**/*.{res,jsx,js,mjs}";
+@source not "../src/components/LandingPage*.{res,jsx}";
+@source "../markdown-pages/**/*.mdx";
+@source "../../../packages/shared/src/**/*.{res,jsx}";
+
+@layer components {
+ a > code {
+ @apply text-fire;
+ }
+}
+
+.wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.version-popover[popover] {
+ inset: unset;
+ width: 100%;
+ height: auto;
+ /* padding: 0; */
+ background: transparent;
+ border: none;
+ position: fixed;
+ top: 0px;
+ right: 0px;
+ pointer-events: none;
+
+ overlay: none;
+ /* this alone would do the trick, but its not supported yet in all major browsers*/
+}
+
+.menu {
+ visibility: hidden;
+ position: absolute;
+ left: 0;
+ top: 100%;
+ opacity: 0;
+ transition:
+ visibility 0s ease,
+ opacity 0.3s ease,
+ transform 0.3s ease;
+}
+
+.version-popover[popover]:popover-open ~ .menu {
+ opacity: 1;
+ visibility: visible;
+}
+
+.trigger {
+ svg {
+ display: inline-block;
+ height: 0.75rem;
+ width: 0.75rem;
+ transform: rotate(0);
+ }
+}
+
+.version-popover[popover]:popover-open ~ .trigger {
+ svg {
+ transform: rotate(180deg);
+ }
+}
+
+#sidebar[popover] {
+ inset: unset;
+ width: unset;
+ border: none;
+ position: relative;
+ top: 0px;
+ left: 0px;
+ overlay: none;
+ height: 100%;
+ @apply min-w-64 bg-white border-gray-20 border-r-2 overflow-y-scroll h-full md:block;
+}
+
+#mobile-tertiary-drawer {
+ max-width: 100%;
+ margin: 0;
+}
diff --git a/apps/docs/styles/homepage.css b/apps/docs/styles/homepage.css
new file mode 100644
index 000000000..675a10b53
--- /dev/null
+++ b/apps/docs/styles/homepage.css
@@ -0,0 +1,30 @@
+@reference "./_theme.css";
+@import "tailwindcss/utilities.css" layer(utilities) source(none);
+@import "./_shared-sources.css";
+
+@source "../app/routes/LandingPage*.{res,jsx}";
+@source "../src/components/LandingPage*.{res,jsx}";
+@source "../src/components/{Footer,Button,ImageGallery,ResponsiveImage}.{res,jsx}";
+
+.gallery-selector::after {
+ content: "";
+ inline-size: 100%;
+ block-size: 1px;
+ background-color: currentColor;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .gallery-photo {
+ animation: gallery-fade-in 1s ease-in-out;
+ }
+}
+
+@keyframes gallery-fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
diff --git a/apps/docs/styles/main.css b/apps/docs/styles/main.css
index 045eb257d..fefc0ceca 100644
--- a/apps/docs/styles/main.css
+++ b/apps/docs/styles/main.css
@@ -1,180 +1,10 @@
-@import "./_markdown.css" layer(base);
-@import "./_fonts.css" layer(base);
-
-@import "tailwindcss";
-
-@source '../src/**/*.{jsx,res}';
-@source '../pages/**/*.{jsx,mdx}';
-@source '../app/**/*.{jsx,mdx}';
-/* Include workspace packages that render inside the docs app, e.g. the playground. */
-@source '../../../packages/*/src/**/*.{jsx,res}';
-
-@theme {
- --radius-*: initial;
- --radius-none: 0;
- --radius-sm: 0.125rem;
- --radius: 0.25rem;
- --radius-lg: 0.5rem;
- --radius-xl: 1rem;
- --radius-full: 9999px;
-
- --breakpoint-*: initial;
- --breakpoint-xs: 599px;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 1024px;
- --breakpoint-xl: 1440px;
-
- --text-*: initial;
- --text-11: 0.6875rem;
- --text-12: 0.75rem;
- --text-14: 0.875rem;
- --text-16: 1rem;
- --text-18: 1.125rem;
- --text-24: 1.5rem;
- --text-32: 2rem;
- --text-48: 3rem;
- --text-68: 4.25rem;
-
- --font-weight-*: initial;
- --font-weight-normal: 400;
- --font-weight-medium: 500;
- --font-weight-semibold: 600;
- --font-weight-bold: 700;
-
- --leading-*: initial;
- --leading-2: 1.35;
- --leading-4: 1.5;
- --leading-5: 1.75;
- --leading-none: 1;
- --leading-tight: 1.25;
-
- --tracking-*: initial;
- --tracking-tighter: -0.03em;
- --tracking-tight: -0.02em;
- --tracking-normal: 0;
- --tracking-wide: 0.075em;
- --tracking-widest: 0.15em;
-
- --z-index-*: initial;
- --z-index-0: 0;
- --z-index-1: 1;
- --z-index-2: 2;
- --z-index-3: 3;
- --z-index-4: 4;
- --z-index-5: 5;
- --z-index-10: 10;
- --z-index-20: 20;
- --z-index-25: 25;
- --z-index-30: 30;
- --z-index-40: 40;
- --z-index-50: 50;
- --z-index-75: 75;
- --z-index-100: 100;
- --z-index-auto: auto;
-
- --font-*: initial;
- --font-sans:
- Inter, SF Pro Text, Roboto, -apple-system, BlinkMacSystemFont,
- Helvetica Neue, Arial, sans-serif;
- --font-mono: Roboto Mono, SFMono-Regular, Menlo, Segoe UI, Courier, monospace;
-
- --color-gray-5: #fafbfc;
- --color-gray-10: #fafbfc;
- --color-gray-20: #edf0f2;
- --color-gray-30: #cdcdd6;
- --color-gray-40: #979aad;
- --color-gray-60: #696b7d;
- --color-gray-70: #3c3d4e;
- --color-gray-80: #232538;
- --color-gray-90: #14162c;
- --color-gray-95: #14162c;
- --color-gray-100: #0b0d22;
- --color-gray-5-tr: rgba(1, 20, 29, 0.02);
- --color-gray-10-tr: rgba(1, 20, 38, 0.02);
- --color-gray-60-tr: rgba(1, 4, 39, 0.6);
- --color-gray-80-tr: rgba(1, 4, 39, 0.8);
- --color-gray-90-tr: rgba(1, 4, 39, 0.95);
-
- --color-white: #ffffff;
- --color-white-80-tr: rgba(255, 255, 255, 0.8);
- --color-white-60-tr: rgba(255, 255, 255, 0.6);
- --color-white-40-tr: rgba(255, 255, 255, 0.4);
-
- --color-fire-5: #fcf1f1;
- --color-fire-10: #fbb8bb;
- --color-fire-30: #f4646a;
- --color-fire-50: #e6484f;
- --color-fire-70: #c3373d;
- --color-fire-90: #790c10;
- --color-fire-100: #211332;
- --color-fire: #e6484f;
- --color-fire-dark: #e06c75;
-
- --color-sky-5: #ecf1fc;
- --color-sky-10: #dde8fd;
- --color-sky-30: #638fe6;
- --color-sky-70: #2258c3;
- --color-sky-90: #0c2e6f;
- --color-sky: #376fdd;
-
- --color-berry-15: rgba(171, 94, 163, 0.15);
- --color-berry-40: #a766d0;
- --color-berry: #b151dd;
- --color-berry-dark-50: #b984db;
-
- --color-water: #5e5ede;
- --color-water-dark: #637cc1;
-
- --color-ocean-dark: #8aaec8;
-
- --color-turtle: #02a875;
- --color-turtle-dark: #388b72;
-
- --color-orange-10: rgba(224, 172, 0, 0.1);
- --color-orange-15: rgba(224, 172, 0, 0.15);
- --color-orange: #dd8c1b;
- --color-orange-dark: #d59b74;
-
- --height-18: 4.5rem;
- --height-inherit: inherit;
-
- --min-width-320: 20rem;
-
- --container-320: 20rem;
- --container-400: 25rem;
- --container-576: 36rem;
- --container-740: 46.25rem;
- --container-1060: 66.25rem;
- --container-1280: 80rem;
- --container-sm: 30rem;
- --container-md: 40rem;
- --container-xl: 66.25rem;
- --container-none: none;
- --container-full: 100%;
+@layer base, theme, components, utilities;
- --inset-16: 4rem;
- --inset-18: 4.5rem;
-
- --spacing-2\/3: 66.666667%;
- --spacing-9\/16: 56.25%;
- --spacing-0_75: 0.135rem;
-
- --animate-pulse: pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1);
-
- --shadow-sm: 0 0.5px 0.5px 0 rgba(0, 0, 0, 0.05);
-
- @keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0.5;
- }
- }
-}
+@import "./_fonts.css" layer(base);
+@import "./_theme.css";
+@import "tailwindcss/preflight.css" layer(base);
+@import "tailwindcss/utilities.css" layer(utilities) source(none);
+@import "./_shared-sources.css";
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
@@ -194,324 +24,6 @@
}
}
-@layer components {
- .playground-theme {
- --playground-bg: var(--color-gray-100);
- --playground-surface: var(--color-gray-100);
- --playground-surface-border: transparent;
- --playground-control-bg: var(--color-gray-100);
- --playground-control-border: transparent;
- --playground-elevated-bg: var(--color-gray-100);
- --playground-overlay-bg: var(--color-gray-90);
- --playground-overlay-border: var(--color-gray-70);
- --playground-text-primary: var(--color-gray-20);
- --playground-text-secondary: var(--color-gray-40);
- --playground-border: var(--color-gray-60);
- --playground-border-strong: var(--color-gray-80);
- --playground-hover-surface: var(--color-gray-40);
- --playground-hover-elevated: var(--color-gray-80);
- --playground-active-surface: var(--color-gray-40);
- --playground-input-bg: var(--color-gray-90);
- --playground-placeholder: rgba(237, 240, 242, 0.5);
- --playground-divider-bg: var(--color-gray-70);
- --playground-divider-border: transparent;
- --playground-divider-opacity: 0.3;
- --playground-divider-hover-opacity: 0.5;
- --playground-divider-handle: var(--color-gray-20);
- --playground-scrollbar-track: var(--color-gray-100);
- --playground-scrollbar-thumb: var(--color-gray-70);
- --playground-scrollbar-thumb-hover: var(--color-gray-60);
- --playground-toggle-track: #374151;
- --playground-toggle-track-border: #4b5563;
- --playground-toggle-thumb: var(--color-white);
- --playground-toggle-label: #d1d5db;
- --playground-tab-active: var(--color-white);
- --playground-editor-bg: var(--color-gray-100);
- --playground-editor-text: var(--color-gray-20);
- --playground-editor-cursor: var(--color-orange);
- --playground-editor-active-line: rgba(255, 255, 255, 0.02);
- --playground-editor-gutter-bg: transparent;
- --playground-editor-gutter-text: var(--color-gray-60);
- --playground-editor-gutter-border: transparent;
- --playground-editor-active-gutter-bg: transparent;
- --playground-editor-active-gutter-text: var(--color-white);
- --playground-editor-selection: rgba(255, 255, 255, 0.2);
- --playground-editor-selection-match: #aafe661a;
- --playground-editor-syntax-keyword: var(--color-berry-dark-50);
- --playground-editor-syntax-variable: var(--color-gray-30);
- --playground-editor-syntax-type: var(--color-orange-dark);
- --playground-editor-syntax-string: var(--color-turtle-dark);
- --playground-editor-syntax-comment: var(--color-gray-60);
- --playground-editor-syntax-namespace-def: var(--color-orange);
- --playground-editor-syntax-namespace: var(--color-water-dark);
- --playground-editor-syntax-property: var(--color-ocean-dark);
- --playground-editor-syntax-attribute: #bcc9ab;
- }
-
- .playground-theme-light {
- --playground-bg: var(--color-gray-5);
- --playground-surface: var(--color-white);
- --playground-surface-border: var(--color-gray-20);
- --playground-control-bg: #f3f5f7;
- --playground-control-border: var(--color-gray-20);
- --playground-elevated-bg: var(--color-white);
- --playground-overlay-bg: var(--color-white);
- --playground-overlay-border: var(--color-gray-30);
- --playground-text-primary: var(--color-gray-80);
- --playground-text-secondary: var(--color-gray-80);
- --playground-border: var(--color-gray-30);
- --playground-border-strong: var(--color-gray-60);
- --playground-hover-surface: var(--color-gray-20);
- --playground-hover-elevated: var(--color-gray-10);
- --playground-active-surface: var(--color-gray-20);
- --playground-input-bg: var(--color-gray-10);
- --playground-placeholder: var(--color-gray-60);
- --playground-divider-bg: #f5f7f9;
- --playground-divider-border: var(--color-gray-20);
- --playground-divider-opacity: 1;
- --playground-divider-hover-opacity: 1;
- --playground-divider-handle: var(--color-gray-40);
- --playground-scrollbar-track: #f5f7f9;
- --playground-scrollbar-thumb: var(--color-gray-30);
- --playground-scrollbar-thumb-hover: var(--color-gray-40);
- --playground-toggle-track: var(--color-gray-30);
- --playground-toggle-track-border: var(--color-gray-40);
- --playground-toggle-thumb: var(--color-white);
- --playground-toggle-label: var(--color-gray-80);
- --playground-tab-active: var(--color-gray-80);
- --playground-editor-bg: var(--color-white);
- --playground-editor-text: var(--color-gray-80);
- --playground-editor-cursor: var(--color-sky-70);
- --playground-editor-active-line: rgba(34, 88, 195, 0.07);
- --playground-editor-gutter-bg: var(--color-gray-10);
- --playground-editor-gutter-text: var(--color-gray-60);
- --playground-editor-gutter-border: var(--color-gray-20);
- --playground-editor-active-gutter-bg: var(--color-gray-20);
- --playground-editor-active-gutter-text: var(--color-gray-80);
- --playground-editor-selection: rgba(34, 88, 195, 0.22);
- --playground-editor-selection-match: rgba(94, 94, 222, 0.18);
- --playground-editor-syntax-keyword: var(--color-berry);
- --playground-editor-syntax-variable: var(--color-gray-80);
- --playground-editor-syntax-type: var(--color-orange);
- --playground-editor-syntax-string: var(--color-turtle);
- --playground-editor-syntax-comment: var(--color-gray-60);
- --playground-editor-syntax-namespace-def: var(--color-orange);
- --playground-editor-syntax-namespace: var(--color-water);
- --playground-editor-syntax-property: var(--color-sky);
- --playground-editor-syntax-attribute: #4f5f78;
- }
-
- .playground-main {
- background-color: var(--playground-bg);
- color: var(--playground-text-secondary);
- }
-
- .playground-surface {
- background-color: var(--playground-surface);
- border-color: var(--playground-surface-border);
- }
-
- .playground-text-primary {
- color: var(--playground-text-primary) !important;
- }
-
- .playground-text-secondary {
- color: var(--playground-text-secondary) !important;
- }
-
- .playground-select {
- background-color: var(--playground-elevated-bg);
- border-color: var(--playground-border);
- color: var(--playground-text-primary) !important;
- }
-
- .playground-selection-option {
- background-color: var(--playground-elevated-bg);
- border: 1px solid var(--playground-border);
- color: var(--playground-text-primary);
- }
-
- .playground-selection-option:hover {
- background-color: var(--playground-hover-surface);
- }
-
- .playground-selection-option-active {
- background-color: var(--color-fire);
- border-color: var(--color-fire);
- color: var(--color-white);
- }
-
- .playground-selection-option-active:hover {
- background-color: var(--color-fire);
- }
-
- .playground-overlay {
- background-color: var(--playground-overlay-bg);
- border-color: var(--playground-overlay-border);
- color: var(--playground-text-primary);
- }
-
- .playground-field {
- border-color: var(--playground-border);
- }
-
- .playground-field-active {
- border-color: var(--playground-text-primary);
- }
-
- .playground-icon-button:hover {
- background-color: var(--playground-hover-surface);
- }
-
- .playground-input {
- background-color: var(--playground-input-bg);
- color: var(--playground-text-primary);
- }
-
- .playground-input::placeholder {
- color: var(--playground-placeholder);
- }
-
- .playground-chip-active {
- background-color: var(--playground-active-surface);
- }
-
- .playground-suggestion-active {
- background-color: var(--playground-active-surface);
- }
-
- .playground-control-panel {
- background-color: var(--playground-control-bg);
- border-bottom: 1px solid var(--playground-control-border);
- }
-
- .playground-toast {
- background-color: var(--playground-overlay-bg);
- border-color: var(--playground-overlay-border);
- color: var(--playground-text-primary);
- }
-
- .playground-toggle-label {
- color: var(--playground-toggle-label);
- }
-
- .playground-toggle-track {
- background-color: var(--playground-toggle-track);
- border-color: var(--playground-toggle-track-border);
- }
-
- .playground-toggle-track::after {
- background-color: var(--playground-toggle-thumb);
- border-color: var(--playground-toggle-track-border);
- }
-
- .playground-tab-active {
- color: var(--playground-tab-active);
- border-top-color: var(--color-sky-70) !important;
- }
-
- .playground-editor-shell {
- background-color: var(--playground-editor-bg);
- }
-
- .playground-divider {
- background-color: var(--playground-divider-bg);
- border: 1px solid var(--playground-divider-border);
- opacity: var(--playground-divider-opacity);
- }
-
- .playground-divider:hover {
- opacity: var(--playground-divider-hover-opacity);
- background-color: var(--playground-hover-surface);
- }
-
- .playground-divider-handle {
- color: var(--playground-divider-handle);
- }
-}
-
-@utility hl-title {
- @apply text-[2.5rem] tracking-tighter leading-tight font-semibold text-gray-80;
-
- @media (width >=theme(--breakpoint-xs)) {
- @apply text-48;
- }
-
- @media (width >=theme(--breakpoint-md)) {
- @apply text-68;
- }
-
- @media (width >=theme(--breakpoint-lg)) {
- @apply font-bold;
- }
-}
-
-@utility hl-1 {
- @layer components {
- @apply text-48 font-semibold tracking-tighter leading-tight text-black;
- }
-}
-
-@utility hl-2 {
- @layer components {
- @apply text-32 font-semibold tracking-tighter leading-tight text-black;
- }
-}
-
-@utility hl-3 {
- @layer components {
- @apply text-24 font-semibold tracking-tight leading-tight text-black;
- }
-}
-
-@utility hl-4 {
- @layer components {
- @apply text-18 font-semibold tracking-tight leading-tight text-gray-80;
- }
-}
-
-@utility hl-5 {
- @layer components {
- @apply text-14 font-bold tracking-normal leading-tight text-gray-80;
- }
-}
-
-@utility hl-overline {
- @layer components {
- @apply text-11 font-semibold tracking-wide leading-tight uppercase text-gray-80;
- }
-}
-
-@utility body-lg {
- @layer components {
- @apply text-18 font-normal tracking-tight leading-4;
- }
-}
-
-@utility body-button {
- @layer components {
- @apply text-16 font-semibold tracking-normal leading-2;
- }
-}
-
-@utility body-md {
- @layer components {
- @apply text-16 font-normal tracking-tight leading-4;
- }
-}
-
-@utility body-sm {
- @layer components {
- @apply text-14 font-medium tracking-normal leading-2;
- }
-}
-
-@utility captions {
- @layer components {
- @apply text-12 font-medium tracking-normal leading-2;
- }
-}
-
@layer components {
/* @import "./_typography.css"; */
@@ -531,124 +43,6 @@
background: transparent;
/* Chrome/Safari/Webkit */
}
-
- .playground-scrollbar {
- scrollbar-width: thin;
- scrollbar-color: var(--playground-scrollbar-thumb)
- var(--playground-scrollbar-track);
- scrollbar-gutter: stable; /* Reserve only at the scrollbar edge to avoid padding shifts */
- }
-
- .playground-scrollbar::-webkit-scrollbar {
- width: 0.65rem;
- height: 0.65rem;
- }
-
- .playground-scrollbar::-webkit-scrollbar-track {
- background: var(--playground-scrollbar-track);
- }
-
- .playground-scrollbar::-webkit-scrollbar-thumb {
- background-color: var(--playground-scrollbar-thumb);
- border-radius: 9999px;
- border: 2px solid var(--playground-scrollbar-track);
- }
-
- .playground-scrollbar::-webkit-scrollbar-thumb:hover {
- background-color: var(--playground-scrollbar-thumb-hover);
- }
-
- a > code {
- @apply text-fire;
- }
-}
-
-.wrapper {
- position: relative;
- display: inline-block;
-}
-
-.gallery-selector::after {
- content: "";
- inline-size: 100%;
- block-size: 1px;
- background-color: currentColor;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .gallery-photo {
- animation: gallery-fade-in 1s ease-in-out;
- }
-}
-
-@keyframes gallery-fade-in {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-.version-popover[popover] {
- inset: unset;
- width: 100%;
- height: auto;
- /* padding: 0; */
- background: transparent;
- border: none;
- position: fixed;
- top: 0px;
- right: 0px;
- pointer-events: none;
-
- overlay: none;
- /* this alone would do the trick, but its not supported yet in all major browsers*/
-}
-
-.menu {
- visibility: hidden;
- position: absolute;
- left: 0;
- top: 100%;
- opacity: 0;
- transition:
- visibility 0s ease,
- opacity 0.3s ease,
- transform 0.3s ease;
-}
-
-.version-popover[popover]:popover-open ~ .menu {
- opacity: 1;
- visibility: visible;
-}
-
-.trigger {
- svg {
- display: inline-block;
- height: 0.75rem;
- width: 0.75rem;
- transform: rotate(0);
- }
-}
-
-.version-popover[popover]:popover-open ~ .trigger {
- svg {
- transform: rotate(180deg);
- }
-}
-
-#sidebar[popover] {
- inset: unset;
- width: unset;
- border: none;
- position: relative;
- top: 0px;
- left: 0px;
- overlay: none;
- height: 100%;
- @apply min-w-64 bg-white border-gray-20 border-r-2 overflow-y-scroll h-full md:block;
}
/* When any dialog is open as a modal, lock the body scroll */
@@ -661,19 +55,8 @@ body:has(dialog[open]) {
margin: 0;
}
-#mobile-tertiary-drawer {
- max-width: 100%;
- margin: 0;
-}
-
body {
scrollbar-gutter: stable;
height: 100%;
min-height: 100vh;
}
-
-/* This has to stay at the end! */
-/* This is to prevent FOUC (flash of unstyled content) */
-html {
- opacity: 1;
-}
diff --git a/apps/docs/styles/playground.css b/apps/docs/styles/playground.css
new file mode 100644
index 000000000..dd6955bee
--- /dev/null
+++ b/apps/docs/styles/playground.css
@@ -0,0 +1,271 @@
+@reference "./_theme.css";
+@import "tailwindcss/utilities.css" layer(utilities) source(none);
+@import "./_shared-sources.css";
+
+@source "../app/routes/TryRoute.{res,jsx}";
+@source "../../../packages/playground/src/**/*.{res,jsx}";
+@source "../../../packages/shared/src/**/*.{res,jsx}";
+
+@layer components {
+ .playground-theme {
+ --playground-bg: var(--color-gray-100);
+ --playground-surface: var(--color-gray-100);
+ --playground-surface-border: transparent;
+ --playground-control-bg: var(--color-gray-100);
+ --playground-control-border: transparent;
+ --playground-elevated-bg: var(--color-gray-100);
+ --playground-overlay-bg: var(--color-gray-90);
+ --playground-overlay-border: var(--color-gray-70);
+ --playground-text-primary: var(--color-gray-20);
+ --playground-text-secondary: var(--color-gray-40);
+ --playground-border: var(--color-gray-60);
+ --playground-border-strong: var(--color-gray-80);
+ --playground-hover-surface: var(--color-gray-40);
+ --playground-hover-elevated: var(--color-gray-80);
+ --playground-active-surface: var(--color-gray-40);
+ --playground-input-bg: var(--color-gray-90);
+ --playground-placeholder: rgba(237, 240, 242, 0.5);
+ --playground-divider-bg: var(--color-gray-70);
+ --playground-divider-border: transparent;
+ --playground-divider-opacity: 0.3;
+ --playground-divider-hover-opacity: 0.5;
+ --playground-divider-handle: var(--color-gray-20);
+ --playground-scrollbar-track: var(--color-gray-100);
+ --playground-scrollbar-thumb: var(--color-gray-70);
+ --playground-scrollbar-thumb-hover: var(--color-gray-60);
+ --playground-toggle-track: #374151;
+ --playground-toggle-track-border: #4b5563;
+ --playground-toggle-thumb: var(--color-white);
+ --playground-toggle-label: #d1d5db;
+ --playground-tab-active: var(--color-white);
+ --playground-editor-bg: var(--color-gray-100);
+ --playground-editor-text: var(--color-gray-20);
+ --playground-editor-cursor: var(--color-orange);
+ --playground-editor-active-line: rgba(255, 255, 255, 0.02);
+ --playground-editor-gutter-bg: transparent;
+ --playground-editor-gutter-text: var(--color-gray-60);
+ --playground-editor-gutter-border: transparent;
+ --playground-editor-active-gutter-bg: transparent;
+ --playground-editor-active-gutter-text: var(--color-white);
+ --playground-editor-selection: rgba(255, 255, 255, 0.2);
+ --playground-editor-selection-match: #aafe661a;
+ --playground-editor-syntax-keyword: var(--color-berry-dark-50);
+ --playground-editor-syntax-variable: var(--color-gray-30);
+ --playground-editor-syntax-type: var(--color-orange-dark);
+ --playground-editor-syntax-string: var(--color-turtle-dark);
+ --playground-editor-syntax-comment: var(--color-gray-60);
+ --playground-editor-syntax-namespace-def: var(--color-orange);
+ --playground-editor-syntax-namespace: var(--color-water-dark);
+ --playground-editor-syntax-property: var(--color-ocean-dark);
+ --playground-editor-syntax-attribute: #bcc9ab;
+ }
+
+ .playground-theme-light {
+ --playground-bg: var(--color-gray-5);
+ --playground-surface: var(--color-white);
+ --playground-surface-border: var(--color-gray-20);
+ --playground-control-bg: #f3f5f7;
+ --playground-control-border: var(--color-gray-20);
+ --playground-elevated-bg: var(--color-white);
+ --playground-overlay-bg: var(--color-white);
+ --playground-overlay-border: var(--color-gray-30);
+ --playground-text-primary: var(--color-gray-80);
+ --playground-text-secondary: var(--color-gray-80);
+ --playground-border: var(--color-gray-30);
+ --playground-border-strong: var(--color-gray-60);
+ --playground-hover-surface: var(--color-gray-20);
+ --playground-hover-elevated: var(--color-gray-10);
+ --playground-active-surface: var(--color-gray-20);
+ --playground-input-bg: var(--color-gray-10);
+ --playground-placeholder: var(--color-gray-60);
+ --playground-divider-bg: #f5f7f9;
+ --playground-divider-border: var(--color-gray-20);
+ --playground-divider-opacity: 1;
+ --playground-divider-hover-opacity: 1;
+ --playground-divider-handle: var(--color-gray-40);
+ --playground-scrollbar-track: #f5f7f9;
+ --playground-scrollbar-thumb: var(--color-gray-30);
+ --playground-scrollbar-thumb-hover: var(--color-gray-40);
+ --playground-toggle-track: var(--color-gray-30);
+ --playground-toggle-track-border: var(--color-gray-40);
+ --playground-toggle-thumb: var(--color-white);
+ --playground-toggle-label: var(--color-gray-80);
+ --playground-tab-active: var(--color-gray-80);
+ --playground-editor-bg: var(--color-white);
+ --playground-editor-text: var(--color-gray-80);
+ --playground-editor-cursor: var(--color-sky-70);
+ --playground-editor-active-line: rgba(34, 88, 195, 0.07);
+ --playground-editor-gutter-bg: var(--color-gray-10);
+ --playground-editor-gutter-text: var(--color-gray-60);
+ --playground-editor-gutter-border: var(--color-gray-20);
+ --playground-editor-active-gutter-bg: var(--color-gray-20);
+ --playground-editor-active-gutter-text: var(--color-gray-80);
+ --playground-editor-selection: rgba(34, 88, 195, 0.22);
+ --playground-editor-selection-match: rgba(94, 94, 222, 0.18);
+ --playground-editor-syntax-keyword: var(--color-berry);
+ --playground-editor-syntax-variable: var(--color-gray-80);
+ --playground-editor-syntax-type: var(--color-orange);
+ --playground-editor-syntax-string: var(--color-turtle);
+ --playground-editor-syntax-comment: var(--color-gray-60);
+ --playground-editor-syntax-namespace-def: var(--color-orange);
+ --playground-editor-syntax-namespace: var(--color-water);
+ --playground-editor-syntax-property: var(--color-sky);
+ --playground-editor-syntax-attribute: #4f5f78;
+ }
+
+ .playground-main {
+ background-color: var(--playground-bg);
+ color: var(--playground-text-secondary);
+ }
+
+ .playground-surface {
+ background-color: var(--playground-surface);
+ border-color: var(--playground-surface-border);
+ }
+
+ .playground-text-primary {
+ color: var(--playground-text-primary) !important;
+ }
+
+ .playground-text-secondary {
+ color: var(--playground-text-secondary) !important;
+ }
+
+ .playground-select {
+ background-color: var(--playground-elevated-bg);
+ border-color: var(--playground-border);
+ color: var(--playground-text-primary) !important;
+ }
+
+ .playground-selection-option {
+ background-color: var(--playground-elevated-bg);
+ border: 1px solid var(--playground-border);
+ color: var(--playground-text-primary);
+ }
+
+ .playground-selection-option:hover {
+ background-color: var(--playground-hover-surface);
+ }
+
+ .playground-selection-option-active {
+ background-color: var(--color-fire);
+ border-color: var(--color-fire);
+ color: var(--color-white);
+ }
+
+ .playground-selection-option-active:hover {
+ background-color: var(--color-fire);
+ }
+
+ .playground-overlay {
+ background-color: var(--playground-overlay-bg);
+ border-color: var(--playground-overlay-border);
+ color: var(--playground-text-primary);
+ }
+
+ .playground-field {
+ border-color: var(--playground-border);
+ }
+
+ .playground-field-active {
+ border-color: var(--playground-text-primary);
+ }
+
+ .playground-icon-button:hover {
+ background-color: var(--playground-hover-surface);
+ }
+
+ .playground-input {
+ background-color: var(--playground-input-bg);
+ color: var(--playground-text-primary);
+ }
+
+ .playground-input::placeholder {
+ color: var(--playground-placeholder);
+ }
+
+ .playground-chip-active {
+ background-color: var(--playground-active-surface);
+ }
+
+ .playground-suggestion-active {
+ background-color: var(--playground-active-surface);
+ }
+
+ .playground-control-panel {
+ background-color: var(--playground-control-bg);
+ border-bottom: 1px solid var(--playground-control-border);
+ }
+
+ .playground-toast {
+ background-color: var(--playground-overlay-bg);
+ border-color: var(--playground-overlay-border);
+ color: var(--playground-text-primary);
+ }
+
+ .playground-toggle-label {
+ color: var(--playground-toggle-label);
+ }
+
+ .playground-toggle-track {
+ background-color: var(--playground-toggle-track);
+ border-color: var(--playground-toggle-track-border);
+ }
+
+ .playground-toggle-track::after {
+ background-color: var(--playground-toggle-thumb);
+ border-color: var(--playground-toggle-track-border);
+ }
+
+ .playground-tab-active {
+ color: var(--playground-tab-active);
+ border-top-color: var(--color-sky-70) !important;
+ }
+
+ .playground-editor-shell {
+ background-color: var(--playground-editor-bg);
+ }
+
+ .playground-divider {
+ background-color: var(--playground-divider-bg);
+ border: 1px solid var(--playground-divider-border);
+ opacity: var(--playground-divider-opacity);
+ }
+
+ .playground-divider:hover {
+ opacity: var(--playground-divider-hover-opacity);
+ background-color: var(--playground-hover-surface);
+ }
+
+ .playground-divider-handle {
+ color: var(--playground-divider-handle);
+ }
+}
+
+@layer components {
+ .playground-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: var(--playground-scrollbar-thumb)
+ var(--playground-scrollbar-track);
+ scrollbar-gutter: stable; /* Reserve only at the scrollbar edge to avoid padding shifts */
+ }
+
+ .playground-scrollbar::-webkit-scrollbar {
+ width: 0.65rem;
+ height: 0.65rem;
+ }
+
+ .playground-scrollbar::-webkit-scrollbar-track {
+ background: var(--playground-scrollbar-track);
+ }
+
+ .playground-scrollbar::-webkit-scrollbar-thumb {
+ background-color: var(--playground-scrollbar-thumb);
+ border-radius: 9999px;
+ border: 2px solid var(--playground-scrollbar-track);
+ }
+
+ .playground-scrollbar::-webkit-scrollbar-thumb:hover {
+ background-color: var(--playground-scrollbar-thumb-hover);
+ }
+}
diff --git a/apps/docs/styles/search.css b/apps/docs/styles/search.css
index fa6ddef6a..9ff7d5318 100644
--- a/apps/docs/styles/search.css
+++ b/apps/docs/styles/search.css
@@ -1,3 +1,3 @@
/* Reuse tokens and utilities without duplicating the global theme or preflight. */
-@reference "./main.css";
+@reference "./_theme.css";
@import "./_docsearch.css" layer(base);
diff --git a/apps/docs/styles/test-utilities.css b/apps/docs/styles/test-utilities.css
new file mode 100644
index 000000000..096e01272
--- /dev/null
+++ b/apps/docs/styles/test-utilities.css
@@ -0,0 +1,12 @@
+@reference "./_theme.css";
+@import "tailwindcss/utilities.css" layer(utilities) source(none);
+@import "./_shared-sources.css";
+
+/* Tests render every route together, so their final utility sheet needs the complete union. */
+@source "../app/**/*.{res,jsx}";
+@source "../src/**/*.{res,jsx,js,mjs}";
+@source "../markdown-pages/**/*.mdx";
+@source "../../../packages/shared/src/**/*.{res,jsx}";
+@source "../../../packages/playground/src/**/*.{res,jsx}";
+@source "../__tests__/**/*.{res,jsx}";
+@source "../test-utils/**/*.{res,jsx}";
diff --git a/apps/docs/vitest.setup.mjs b/apps/docs/vitest.setup.mjs
index acb31a714..7c5201827 100644
--- a/apps/docs/vitest.setup.mjs
+++ b/apps/docs/vitest.setup.mjs
@@ -1,4 +1,8 @@
import "./styles/main.css";
+import "./styles/homepage.css";
+import "./styles/content.css";
+import "./styles/playground.css";
+import "./styles/test-utilities.css";
import "./styles/_hljs.css";
import "./styles/utils.css";
import "./styles/test-overrides.css";