Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion apps/docs/app/DocsRoot.res
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ let default = () => {
<html lang="en">
<head>
{CypressBootstrap.element()}
<style> {React.string("html {opacity:0;}")} </style>
<link rel="preload" href={mainCss} as_="style" />
<link rel="stylesheet" href={mainCss} />
<link rel="stylesheet" href={hljsCss} />
Expand Down
1 change: 1 addition & 0 deletions apps/docs/app/DocsRoot.resi
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion apps/docs/app/DocsRoutes.res
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/DocsRoutes.resi
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ let docsReactRoutes: array<ReactRouter.Routes.t>
let docsGuidesRoutes: array<ReactRouter.Routes.t>
let communityRoutes: array<ReactRouter.Routes.t>
let syntaxLookupDetailRoutes: array<ReactRouter.Routes.t>
/** 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<ReactRouter.Routes.t>
7 changes: 7 additions & 0 deletions apps/docs/app/layouts/ContentLayoutRoute.res
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
6 changes: 5 additions & 1 deletion apps/docs/app/layouts/ContentLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -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<stylesheet>

/** Own content styles and runtime highlighting without adding a DOM wrapper. */
@react.component
let default: unit => React.element
9 changes: 9 additions & 0 deletions apps/docs/app/layouts/HomepageLayoutRoute.res
Original file line number Diff line number Diff line change
@@ -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 = () => <ReactRouter.Outlet />
7 changes: 7 additions & 0 deletions apps/docs/app/layouts/HomepageLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type stylesheet = {rel: string, href: string}

let links: unit => array<stylesheet>

/** Own homepage styles without adding a DOM wrapper. */
@react.component
let default: unit => React.element
7 changes: 7 additions & 0 deletions apps/docs/app/routes/TryRoute.res
Original file line number Diff line number Diff line change
@@ -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<string>,
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/app/routes/TryRoute.resi
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
type stylesheet = {rel: string, href: string}

let links: unit => array<stylesheet>

type props = {
bundleBaseUrl: string,
versions: array<string>,
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/cypress.homepage.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/e2e/Playground.cy.res
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down
19 changes: 19 additions & 0 deletions apps/docs/e2e/bindings/Cypress.res
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type rec window = {
}
and clipboard
type response = {status: int, body: string, headers: Dict.t<string>}
type replyResponse = {statusCode: int, headers: Dict.t<string>, body: string}
type networkError = {forceNetworkError: bool}
type fetchResponse
type automation = {command: string, params?: {permissions: array<string>, origin: string}}
type request = {url: string, resourceType: string}
type routeMatcher = {resourceType?: string, pathname?: string}
Expand Down Expand Up @@ -59,6 +62,10 @@ external interceptStatic: (searchRouteMatcher, staticResponse) => chain<unit> =
@val @scope("cy")
external interceptPattern: (RegExp.t, request => unit) => chain<unit> = "intercept"
@val @scope("cy")
external interceptFailurePattern: (RegExp.t, networkError) => chain<unit> = "intercept"
@val @scope("cy")
external interceptFailureString: (string, networkError) => chain<unit> = "intercept"
@val @scope("cy")
external interceptDeferred: (RegExp.t, unit => promise<unit>) => chain<unit> = "intercept"
@val @scope("cy")
external interceptDeferredRequest: (routeMatcher, request => promise<unit>) => chain<unit> =
Expand Down Expand Up @@ -110,6 +117,9 @@ external shouldProperty: (chain<elements>, @as("have.prop") _, string, string) =
"should"
@send external attribute: (chain<elements>, @as("attr") _, string) => chain<string> = "invoke"
@send external propertyInt: (chain<'a>, string) => chain<int> = "its"
@send external propertyString: (chain<'a>, string) => chain<string> = "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<elements> => chain<elements> = "click"
@send external first: chain<elements> => chain<elements> = "first"
Expand All @@ -127,6 +137,8 @@ external containsChildRegex: (chain<elements>, string, RegExp.t) => chain<elemen
@get external naturalWidth: Dom.element => int = "naturalWidth"
@send external readText: clipboard => promise<string> = "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"
Expand Down Expand Up @@ -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<Dom.element> = "parentElement"
@get external elementHref: Dom.element => string = "href"
@send external decode: Dom.element => promise<unit> = "decode"

@val @scope("Array") external elementsFrom: elementList => array<Dom.element> = "from"
Expand All @@ -183,14 +196,20 @@ external addEventListenerOnce: (Dom.document, string, unit => unit, listenerOpti
@get external fontLoaded: fontFace => promise<fontFace> = "loaded"

@send external windowRequestAnimationFrame: (window, float => unit) => int = "requestAnimationFrame"
@send external windowFetch: (window, string) => promise<fetchResponse> = "fetch"
@get external fetchStatus: fetchResponse => int = "status"
@send external responseText: fetchResponse => promise<string> = "text"

@get external styleSheets: Dom.document => styleSheetList = "styleSheets"
@val @scope("Array") external styleSheetsFrom: styleSheetList => array<styleSheet> = "from"
@get external sheetRules: styleSheet => cssRuleList = "cssRules"
@val @scope("Array") external rulesFrom: cssRuleList => array<cssRule> = "from"
@get external ruleType: cssRule => int = "type"
@get external nestedRules: cssRule => Nullable.t<cssRuleList> = "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"
Expand Down
45 changes: 45 additions & 0 deletions apps/docs/e2e/homepage/HomepageInitialContent.cy.res
Original file line number Diff line number Diff line change
@@ -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
}
})
})
Loading
Loading