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"} +

    +
    +
    + +
    +