From 8e8499212509a29cf59648a98dbe7ccd9da2b3e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 09:46:56 +0000 Subject: [PATCH] feat(website): landing page for the framework at / MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the plain overview at `/` with a marketing landing page for Prescriptive itself, following the `landing-page` spec's own anatomy: hero, social proof, feature grid, the layer taxonomy, "how it works" steps, an Agent Skill section, a token/theming proof, an FAQ, a closing CTA, and a footer — with one repeated primary action ("Get started"). The page doubles as a live proof of the framework: - composed from the spec components (Button, Badge, Switch, Progress, Alert, Steps, LogoCloud) and painted only with the semantic token roles, so a theme change re-skins it; - every figure it quotes (spec counts per layer, behaviors, tokens, themes) is read from the generated registries, never hardcoded; - the theme strip applies the same presets as the settings panel, and the role swatches read the live --ux-* variables; - the hero pairs a real excerpt of the button spec's `## API` contract with a live implementation of it. The docs rail steps aside while the landing page is up (its toggle gives way to the marketing nav) and returns on entering the catalogue. The FAQ disclosures use native details/summary so expanded state, keyboard operation, and announcements come from the platform. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NDx6vi5ACxYJNV1tNhL1Fs --- website/README.md | 22 +- website/src/App.res | 144 ++------ website/src/Landing.res | 790 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 849 insertions(+), 107 deletions(-) create mode 100644 website/src/Landing.res diff --git a/website/README.md b/website/README.md index 2830244..2f94da8 100644 --- a/website/README.md +++ b/website/README.md @@ -1,8 +1,8 @@ # Prescriptive — Website -An interactive catalogue for the specs in this repository. Browse every -spec in the sidebar and see a **live implementation rendered with -[Xote](https://xote.dev)** for each one. +A **landing page** for the framework at `/`, plus an interactive catalogue for +the specs in this repository: browse every spec in the sidebar and see a **live +implementation rendered with [Xote](https://xote.dev)** for each one. ## Stack @@ -39,6 +39,8 @@ src/ReativaSource.res the same examples' reativa (OCaml) source │ ▼ src/App.res sidebar + router (Xote Router) + the example block +src/Landing.res the landing page at `/` — composed from the spec + components and painted with the token roles src/Playground.res per-spec knobs + the live, knob-driven preview src/{Button,Badge,Input,Field,Avatar,Switch,Spinner,Kbd,Separator,Backdrop, Link,IconButton}.res reusable components, one per file, referenced @@ -64,6 +66,20 @@ Each spec's example is a small self-contained Xote component in `Examples.res`; `Examples.get(id)` maps a spec `id` to its rendered node. Specs without an example fall back to a graceful placeholder. +## The landing page + +`/` is a marketing page for the framework itself, and doubles as a live proof of +it. It follows the [`landing-page`](../specs/pages/landing-page.md) spec's +anatomy — hero, social proof, feature grid, "how it works" steps, FAQ, closing +CTA, footer — with one repeated primary action. It is assembled from the same +spec components the catalogue documents (`Button`, `Badge`, `Switch`, +`Progress`, `Alert`, `Steps`, `LogoCloud`, …), every figure it quotes is read +from the generated registries (`SpecsData`, `TraitsData`, `TokensData`, +`ThemesData`) rather than hardcoded, and its theme strip applies the same +presets as the settings panel — so re-skinning the page is a token change you +can watch happen. The docs sidebar hides while the landing page is up and +returns when you enter the catalogue. + ## The example block Each detail page shows **one live surface**, never a preview/playground split: diff --git a/website/src/App.res b/website/src/App.res index 57587a5..dfc355d 100644 --- a/website/src/App.res +++ b/website/src/App.res @@ -28,6 +28,12 @@ let isDesktop: unit => bool = %raw(`() => window.innerWidth >= 1024`) let sidebarOpen = Signal.make(isDesktop()) let spotlightOpen = Signal.make(false) +// The landing page is the marketing front door: it owns the full width and +// carries its own nav and footer, so the docs rail steps aside while it's up +// and returns as soon as you enter the catalogue. +let onLanding = () => Computed.make(() => Signal.get(Router.location()).pathname == "/") +let notLanding = () => Computed.make(() => Signal.get(Router.location()).pathname != "/") + module LayerBadge = { @jsx.component let make = (~layer) => @@ -254,15 +260,33 @@ module Sidebar = { module Topbar = { @jsx.component - let make = () => + let make = () => { + let landing = onLanding() + let docs = notLanding() + // On the landing page the rail is hidden, so its toggle gives way to the + // marketing nav. + let navLink = (to, label) => + + label +
- Signal.update(sidebarOpen, v => !v)}> - - - - "X" + + Signal.update(sidebarOpen, v => !v)}> + + + + + "P" + + +
+ } } module Spotlight = { @@ -379,101 +404,6 @@ module Spotlight = { } } -module Home = { - @jsx.component - let make = () => { - // A stat card links into its layer (first spec) and lifts on hover. - let stat = (layer, title) => { - let dest = switch inLayer(layer)->Array.get(0) { - | Some(a) => "/a/" ++ a.id - | None => "/" - } - -
- - {inLayer(layer)->Array.length->Int.toString} - - -
-
- {title} - -
-
- } - // A featured entry: title + one-liner + layer, lifts on hover. - let feature = id => - switch byId(id) { - | Some(a) => - -
- {a.title} - -
- {a.summary} -
- | None => View.null() - } -
-
-
- "Monochrome · Xote · ReScript" -

- "User Experience" -
- "Specs" -

-

- "A technology-agnostic catalogue of UI patterns. Browse every spec in the sidebar and see a live implementation rendered with " - "Xote" - "." -

-
- - - - - - - - - -
-
-
-
-
- {stat("element", "Elements")} - {stat("component", "Components")} - {stat("block", "Blocks")} - {stat("page", "Pages")} - {stat("flow", "Flows")} -
-

- "Start here" -

-
- {feature("button")} - {feature("alert")} - {feature("dashboard")} -
-

- "Press " - "⌘K" - " to search anything, or open the " - - "Get Started guide" - - "." -

-
-
- } -} - module Guide = { module Section = { @jsx.component @@ -1128,7 +1058,7 @@ let make = () => { None }) let routes = Router.routes([ - {pattern: "/", render: _ => }, + {pattern: "/", render: _ => }, {pattern: "/guide", render: _ => }, {pattern: "/showcase", render: _ => }, {pattern: "/kitchen-sink", render: _ => }, @@ -1136,12 +1066,18 @@ let make = () => { {pattern: "/a/:id", render: params => Dict.get("id")->Option.getOr("")} />}, {pattern: "/t/:id", render: params => Dict.get("id")->Option.getOr("")} />}, ]) + // The docs rail (and its mobile scrim) belong to the catalogue, not to the + // landing page. + let docs = notLanding() + let scrim = Computed.make(() => + Signal.get(sidebarOpen) && Signal.get(Router.location()).pathname != "/" + )
- + // Dim the page behind the mobile drawer. - +
Signal.set(sidebarOpen, false)} diff --git a/website/src/Landing.res b/website/src/Landing.res new file mode 100644 index 0000000..5457ba6 --- /dev/null +++ b/website/src/Landing.res @@ -0,0 +1,790 @@ +// The landing page for Prescriptive itself — and a live proof of the framework: +// it follows the `landing-page` spec's anatomy (hero → social proof → how it +// works → features → proof → FAQ → closing CTA → footer), it is composed from +// the spec components (`@prescriptive/xote`), and every surface is painted with +// the semantic token roles (bg-paper, text-ink, bg-action…), so picking a theme +// in the strip below re-skins the whole page live. +// +// One primary action, repeated at each decision point: "Get started". + +let repo = "https://github.com/brnrdog/prescriptive" + +// --- Catalogue figures, read from the generated registries so the page can't +// --- claim a number the catalogue doesn't back. +let inLayer = layer => SpecsData.all->Array.filter(a => a.layer == layer) +let count = layer => inLayer(layer)->Array.length->Int.toString +let specCount = SpecsData.all->Array.length->Int.toString +let traitCount = TraitsData.all->Array.length->Int.toString +let tokenCount = + TokensData.all->Array.reduce(0, (n, g) => n + Array.length(g.tokens))->Int.toString +let themeCount = ThemesData.all->Array.length->Int.toString + +// Where a layer links into the catalogue — its first spec. +let firstOf = layer => + switch inLayer(layer)->Array.get(0) { + | Some(a) => "/a/" ++ a.id + | None => "/" + } + +// The catalogue's front door: Button is the canonical first spec to read, with +// the first element as a fallback if it ever leaves the catalogue. +let entry = SpecsData.all->Array.some(a => a.id == "button") ? "/a/button" : firstOf("element") + +// A real excerpt of the button spec's `## API` block — the contract an +// implementation (or an agent) is held to. +let contract = `{ + "props": [ + { "name": "variant", "type": "enum", + "values": ["primary", "secondary", + "ghost", "destructive"], + "default": "primary" }, + { "name": "size", "type": "enum", + "values": ["sm", "md", "lg"] }, + { "name": "loading", "type": "boolean", + "default": "false" } + ], + "slots": ["label", "leadingIcon", "trailingIcon"], + "events": ["onActivate"], + "a11y": { + "role": "button", + "keyboard": ["Enter", "Space"], + "announces": ["disabled", "busy"] + }, + "states": ["default", "hover", "focus-visible", + "active", "disabled", "loading"], + "tokens": ["color.action.*", "radius.md", + "space.inline.*", "font.weight.medium"] +}` + +// The semantic color roles a spec's contract is allowed to name. Each chip +// reads the live --ux-* variable, so a theme change repaints them in place. +let roles = [ + ("action", "--ux-color-action-default", "Primary action"), + ("on-action", "--ux-color-action-onAction", "Text on an action"), + ("action-subtle", "--ux-color-action-subtle", "Quiet action wash"), + ("ink", "--ux-color-ink", "Body text"), + ("muted", "--ux-color-muted", "Secondary text"), + ("surface", "--ux-color-surface", "Raised surface"), + ("paper", "--ux-color-paper", "Page background"), + ("border", "--ux-color-border", "Hairlines"), + ("status-success", "--ux-color-status-success", "Success"), + ("status-danger", "--ux-color-status-danger", "Danger"), +] + +// A full-width band with the page's reading measure inside it. +module Section = { + @jsx.component + let make = (~extraClass: string="", ~children: View.node) => +
{children}
+} + +// Eyebrow + title + lede, the heading shape every band below repeats. +module Head = { + @jsx.component + let make = (~eyebrow: string, ~title: string, ~desc: string="") => +
+

+ eyebrow +

+

+ title +

+ {desc == "" + ? View.null() + :

desc

} +
+} + +// A dark code surface with a filename gutter — used for the contract and for +// the agent-skill setup. +module Code = { + @jsx.component + let make = (~label: string, ~code: string, ~extraClass: string="") => +
+
+ + label +
+
+          code  
+      
+
+} + +// The install line, click-to-copy. +module CopyCommand = { + @jsx.component + let make = (~cmd: string) => { + let copied = Signal.make(false) + + } +} + +// Pick a theme (and light/dark) right from the page — the same presets the +// settings panel applies, so the proof is immediate: nothing here is hardcoded. +module ThemeStrip = { + @jsx.component + let make = () => { + let modeCls = dark => + Computed.make(() => + "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors " ++ ( + Signal.get(Settings.darkMode) == dark + ? "bg-action text-on-action" + : "text-muted hover:text-ink" + ) + ) +
+ t.id} + render={t => { + let cls = Computed.make(() => + "inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors " ++ ( + Signal.get(Settings.presetSel) == t.id + ? "border-action bg-action-subtle text-ink" + : "border-border text-muted hover:border-neutral-400 hover:text-ink" + ) + ) + + }} + /> + + + + +
+ } +} + +// The hero's right-hand pane: real components, live and interactive, rendered +// from the same contracts the left-hand pane shows. +module LiveSurface = { + @jsx.component + let make = () => { + let notify = Signal.make(true) + let progress = Signal.make(72) +
+
+ + + + +
+
+ "stable" + "element" + "v1.2.0" + +
+
+
+
+ +
+
+ } +} + +// The role palette, read straight from the live custom properties — the layer +// specs are allowed to name. Repaints itself when a theme is applied. +module RoleSwatches = { + @jsx.component + let make = () => +
+
+

"Semantic roles"

+ "tokens.json" +
+

+ + "What a contract may reference. Never a hex value — that's the theme's job." + +

+
    + { + let (name, var, desc) = role +
  • + + name + desc +
  • + }} + /> +
+
+} + +// One benefit card. +module Feature = { + @jsx.component + let make = (~icon: string, ~title: string, ~desc: string) => +
+ + + +

title

+

desc

+
+} + +// A layer of the taxonomy: how many specs it holds, what it's for, and a way in. +module LayerCard = { + @jsx.component + let make = (~layer: string, ~title: string, ~desc: string) => + +
+ + {count(layer)} + + + layer + +
+ title + desc + + "Open" + + +
+} + +// The FAQ disclosures. Built on native `details`/`summary` so the expanded +// state, keyboard operation, and the announcement come from the platform — the +// `dismissible`-style state doesn't have to be re-implemented (or mis-announced) +// in script. +module Faq = { + @jsx.component + let make = (~items: array<(string, string)>) => +
+ { + let (q, a) = item +
+ + q + + + + +

a

+
+ }} + /> +
+} + +module Footer = { + @jsx.component + let make = () => { + let internal = (to, label) => +
  • + + label + +
  • + let outbound = (href, label) => +
  • + + label + +
  • + let column = (title, children) => +
    +

    + title +

    +
      {children}
    +
    +
    +
    +
    +
    +
    + + "P" + + + "Prescriptive" + +
    +

    + + "A technology-agnostic catalogue of UX specs — a shared source of truth for humans and AI agents." + +

    +
    + {column( + "Catalogue", + <> + {internal(firstOf("element"), "Elements")} + {internal(firstOf("component"), "Components")} + {internal(firstOf("block"), "Blocks")} + {internal(firstOf("page"), "Pages")} + {internal(firstOf("flow"), "Flows")} + , + )} + {column( + "Explore", + <> + {internal("/guide", "Get started")} + {internal("/showcase", "Examples")} + {internal("/kitchen-sink", "Kitchen sink")} + {internal("/tokens", "Design tokens")} + , + )} + {column( + "Project", + <> + {outbound(repo, "GitHub")} + {outbound(repo ++ "/blob/main/skill/SKILL.md", "Agent Skill")} + {outbound(repo ++ "/blob/main/CONTRIBUTING.md", "Contributing")} + {outbound(repo ++ "/blob/main/CHANGELOG.md", "Changelog")} + {outbound(repo ++ "/blob/main/LICENSE", "MIT License")} + , + )} +
    +
    + "MIT licensed. Specs, tokens, and contracts are free to use." + + "Built with " + "Xote" + " · themed by its own tokens" + +
    +
    +
    + } +} + +@jsx.component +let make = () => +
    + // ------------------------------------------------------------------ Hero -- +
    +
    +
    +
    + + {"Open source · " ++ specCount ++ " specs · MIT"} + +

    + "Specify the pattern once." +
    + "Implement it anywhere." +

    +

    + + "Prescriptive is a catalogue of UX specs — reusable, technology-agnostic definitions of UI patterns. Every pattern carries a machine-readable contract: props, slots, events, states, keyboard behavior, and the design tokens it consumes. People and AI agents build from the same source of truth." + +

    +
    + + + + + + + + + "GitHub" + +
    +
    + + + "Specs, tokens, and contracts — no runtime." + +
    +
    +
    +
    + + "The contract" + + +
    + +
    + + "An implementation" + + "live, and themeable" +
    + +
    +
    +
    +
    + + // --------------------------------------------------------- Social proof -- +
    + +
    + + // ------------------------------------------------------------- The trade -- +
    + +
    + + + + + + +
    +
    + + // ------------------------------------------------------------ The layers -- +
    +
    + +
    + + + + + +
    +
    +

    + "Cross-cutting catalogues" +

    +

    + + {traitCount ++ + " shared behaviors (focus-trap, dismissible, roving-focus…) and a reflow vocabulary every spec binds to, so a dialog and a drawer don't each re-describe the same rules."} + +

    +
    +
    + t.id} + render={t => + + {t.id} + } + /> +
    +
    +
    +
    +
    + + // ---------------------------------------------------------- How it works -- +
    + +
    + +
    +
    + + // ---------------------------------------------------------- Agent Skill -- +
    +
    +
    +
    + +
    + + "Read the skill" + + + + + +
    +
    + build the settings page from the spec` + /> +
    +
    +
    + + // ----------------------------------------------------------- Theme proof -- +
    +
    +
    + +
    +
    + + + + + + +
    +
    + { + let (value, label) = pair +
    +
    label
    +
    + value +
    +
    + }} + /> +
    +
    +
    +
    +
    + + // ------------------------------------------------------------------- FAQ -- +
    +
    +
    +
    + +

    + "Everything else is in the " + + "guide" + + " — or open an issue on " + "GitHub" + "." +

    +
    + +
    +
    +
    + + // ---------------------------------------------------------- Closing CTA -- +
    +
    +

    + "Build your next interface to a contract." +

    +

    + + "Read the guide, pick a pattern, and implement it in your stack — or hand the specs to your agent and review the result." + +

    +
    + + + + + + "Star on GitHub" + +
    +

    + {"MIT licensed · " ++ specCount ++ " specs · no runtime dependency"} +

    +
    +
    + +
    +