From 50ff03c7e94df6e64de98ed966e6769853321c0d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 14:20:39 +0000 Subject: [PATCH 01/19] Align the site with the LeanCode palette, from one source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing page, the docs, their social cards, the Flutter demos and the logos each carried their own copy of the colors — nine in all, across CSS, Dart, TypeScript and SVG — and they had drifted: three different yellows, a #050505 ground instead of black, blue-tinted grays where the design system wants warm ones. Name the design system's swatches once in docs_app/palette.json, together with the light and dark --af-* themes built from them, and let scripts/palette.mjs write every copy: af-tokens.css for the landing page and the docs, palette.generated.ts for the OG cards and the logo mark, palette.generated.dart for the live demos, the landing page's theme-color, and the fill of the SVG shapes marked data-palette. The generated files are committed and `palette:check` fails the docs CI when they drift, the same way examples:check guards the manifest. While at it: the Fumadocs variables become @theme inline references to the tokens instead of a third literal copy, alpha variants derive through color-mix() and withAlpha(), and the unused --af-accent-2 goes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- .github/workflows/docs.yml | 6 + docs_app/.gitignore | 1 + docs_app/.prettierignore | 4 + docs_app/README.md | 42 +-- docs_app/app/af-tokens.css | 82 ++++++ docs_app/app/global.css | 126 +++------ docs_app/app/icon.svg | 4 +- docs_app/app/og/[...slug]/route.tsx | 10 +- docs_app/app/og/landing.png/route.tsx | 10 +- docs_app/components/logo.tsx | 5 +- docs_app/eslint.config.mjs | 2 + .../flutter/lib/support/island_frame.dart | 43 ++-- .../lib/support/palette.generated.dart | 134 ++++++++++ docs_app/lib/palette.generated.ts | 129 ++++++++++ docs_app/lib/palette.ts | 17 ++ docs_app/package.json | 4 +- docs_app/palette.json | 93 +++++++ docs_app/public/logo-dark.svg | 4 +- docs_app/public/logo-light.svg | 4 +- docs_app/scripts/landing.mjs | 2 +- docs_app/scripts/palette.mjs | 243 ++++++++++++++++++ landing/README.md | 3 +- landing/lib/main.server.dart | 7 +- landing/lib/palette.generated.dart | 6 + landing/web/af-tokens.css | 82 ++++++ landing/web/landing-icon.svg | 4 +- landing/web/landing.css | 70 ++--- 27 files changed, 937 insertions(+), 200 deletions(-) create mode 100644 docs_app/app/af-tokens.css create mode 100644 docs_app/flutter/lib/support/palette.generated.dart create mode 100644 docs_app/lib/palette.generated.ts create mode 100644 docs_app/lib/palette.ts create mode 100644 docs_app/palette.json create mode 100644 docs_app/scripts/palette.mjs create mode 100644 landing/lib/palette.generated.dart create mode 100644 landing/web/af-tokens.css diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fb51bff..4a71894 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -100,6 +100,12 @@ jobs: - name: Check the example manifest run: npm run examples:check + # Fails when a generated copy of the palette (the landing page's and the + # docs' tokens, the social cards, the Flutter demos, the logos) no longer + # matches palette.json; `npm run palette:generate` rewrites them. + - name: Check the palette + run: npm run palette:check + - name: Generate the example sources run: npm run examples:generate diff --git a/docs_app/.gitignore b/docs_app/.gitignore index 92dd5f2..5485c54 100644 --- a/docs_app/.gitignore +++ b/docs_app/.gitignore @@ -8,6 +8,7 @@ public/index.html public/landing.css public/landing.js public/landing-icon.svg +public/af-tokens.css # Written by `next dev` (agentRules). Regenerated on every run. AGENTS.md CLAUDE.md diff --git a/docs_app/.prettierignore b/docs_app/.prettierignore index c895d6c..2863ae5 100644 --- a/docs_app/.prettierignore +++ b/docs_app/.prettierignore @@ -14,10 +14,14 @@ flutter/ # Build output and codegen. public/flutter-examples/ lib/flutter-examples/manifest.generated.ts +# The palette, written by `npm run palette:generate` from palette.json. +app/af-tokens.css +lib/palette.generated.ts # The landing page, built by Jaspr in ../landing and copied here. public/index.html public/landing.* public/landing-icon.svg +public/af-tokens.css # Landing-page MDX: the Dart fences are formatted by `npm run examples:format`. content/ diff --git a/docs_app/README.md b/docs_app/README.md index 56760d7..b00593c 100644 --- a/docs_app/README.md +++ b/docs_app/README.md @@ -25,17 +25,18 @@ changes; `npm run dev` without it serves the docs and 404s on `/`. ## Layout -| Path | What lives there | -| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -| `../docs/*.mdx`, `../docs/meta.json` | The documentation pages and their order. Section separators are `---Name---` entries in `meta.json`. | -| `content/landing/*.mdx` | The landing page's live demos: the Dart the Jaspr site shows and compiles, written like any docs example. | -| `../landing/` | The landing page, a Jaspr static site. `scripts/landing.mjs` builds it and copies the result into `public/`. | -| `app/docs/` | The notebook layout and the page renderer for `/docs/*`. | -| `app/global.css` | The design system: tokens (`--af-*`), the Fumadocs variables they map onto, the example frame. | -| `lib/shared.ts` | Routes, external URLs, site copy. | -| `lib/layout.shared.tsx` | The header: logo (`components/nav-title.tsx`, a plain link to the landing page), links, GitHub. | -| `components/mdx.tsx` | The components MDX can use: Fumadocs' set, plus `Tabs`, `Steps`, `Accordions`, `TypeTable` and `AdvancedFormsExample`. | -| `app/og/` | Branded Open Graph images, one per docs page. | +| Path | What lives there | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `../docs/*.mdx`, `../docs/meta.json` | The documentation pages and their order. Section separators are `---Name---` entries in `meta.json`. | +| `content/landing/*.mdx` | The landing page's live demos: the Dart the Jaspr site shows and compiles, written like any docs example. | +| `../landing/` | The landing page, a Jaspr static site. `scripts/landing.mjs` builds it and copies the result into `public/`. | +| `app/docs/` | The notebook layout and the page renderer for `/docs/*`. | +| `palette.json`, `scripts/palette.mjs` | The palette — the LeanCode swatches and the light and dark `--af-*` tokens — and the generator that writes every copy of it. | +| `app/global.css` | The stylesheet: the Fumadocs variables mapped onto the `--af-*` tokens (generated into `app/af-tokens.css`), the example frame. | +| `lib/shared.ts` | Routes, external URLs, site copy. | +| `lib/layout.shared.tsx` | The header: logo (`components/nav-title.tsx`, a plain link to the landing page), links, GitHub. | +| `components/mdx.tsx` | The components MDX can use: Fumadocs' set, plus `Tabs`, `Steps`, `Accordions`, `TypeTable` and `AdvancedFormsExample`. | +| `app/og/` | Branded Open Graph images, one per docs page. | ## Live Flutter examples @@ -134,14 +135,19 @@ debug asset server sends no CORS headers, unlike the release one. ## Design -The stylesheet is hand-written on top of the Fumadocs preset. Tokens are CSS custom properties prefixed `--af-` at the -top of `app/global.css` — the ciach palette: `#050505` ground, `#101013` surfaces, `#edff2f` accent, Space Grotesk and -JetBrains Mono — and the `--color-fd-*` variables Fumadocs paints with are re-pointed at them. Dark is the design and -the default; light is a paper variant where the accent is a fill with dark ink on it, the way leancode.co does it. - -Code blocks and live examples share one window frame: a title bar with three dots (the first one lime), a mono title, +The colors are the LeanCode design system — black ground, warm surfaces, one CTA yellow `#f0ff00` — and live in one +place, `palette.json`: the swatches as the design system names them, and the light and dark themes as `--af-*` tokens +built from them. `npm run palette:generate` writes every copy a consumer needs (`app/af-tokens.css` and the landing +page's twin, `lib/palette.generated.ts` for the social cards and the logo, `flutter/lib/support/palette.generated.dart` +for the live demos, the landing page's `theme-color`) and recolors the shapes marked `data-palette` in the logo and icon +SVGs; `npm run palette:check` fails CI when they drift. The stylesheet is hand-written on top of the Fumadocs preset, +with the `--color-fd-*` variables Fumadocs paints with pointed at the tokens. Space Grotesk and JetBrains Mono. Dark is +the design and the default; light is a paper variant where the accent is a fill with black ink on it, the way +leancode.co does it. + +Code blocks and live examples share one window frame: a title bar with three dots (the first one yellow), a mono title, and the content below. The landing page in `../landing/web/landing.css` uses the same tokens and the same frame, so the -homepage and the docs read as one site — change a colour in one place and mirror it in the other. +homepage and the docs read as one site. ## Deployment diff --git a/docs_app/app/af-tokens.css b/docs_app/app/af-tokens.css new file mode 100644 index 0000000..14cdbf5 --- /dev/null +++ b/docs_app/app/af-tokens.css @@ -0,0 +1,82 @@ +/* Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do not edit. Run `npm run palette:generate` in docs_app after changing the palette. */ + +:root { + /* Page background. */ + --af-bg: #ffffff; + + /* Section background: footer, code panels. */ + --af-bg-2: #f7f7f5; + + /* Cards and frames. */ + --af-surface: #ffffff; + + /* Bars and chips on a surface. */ + --af-surface-2: #f2f2ef; + + /* The hairline. */ + --af-border: #e5e5e5; + + /* The stronger hairline, for controls. */ + --af-border-2: #cfcfcb; + + /* Headings and primary text. */ + --af-text: #000000; + + /* Body copy. */ + --af-text-2: #3f3f3c; + + /* Captions and metadata. */ + --af-muted: #757575; + + /* The CTA yellow, as a fill. */ + --af-accent: #f0ff00; + + /* Text on the accent. */ + --af-accent-ink: #000000; + + /* The accent as text. */ + --af-accent-text: #606a00; + + /* The primary button's hover fill. */ + --af-accent-hover: #f5ff4d; + + /* The accent as a wash behind icons, pills and glows. */ + --af-accent-soft: rgba(240, 255, 0, 0.45); + + /* The docs' primary action fill. */ + --af-primary: #000000; + + /* Text on the primary fill. */ + --af-primary-ink: #f0ff00; + + /* The scrim behind dialogs. */ + --af-overlay: rgba(0, 0, 0, 0.3); + + /* Error signal. */ + --af-danger: #be2119; + + /* Success signal. */ + --af-ok: #527b28; +} + +.dark { + --af-bg: #000000; + --af-bg-2: #0a0a08; + --af-surface: #151513; + --af-surface-2: #1d1d1a; + --af-border: rgba(255, 255, 255, 0.12); + --af-border-2: rgba(255, 255, 255, 0.2); + --af-text: #ffffff; + --af-text-2: #d8d8d4; + --af-muted: #a3a3a0; + --af-accent: #f0ff00; + --af-accent-ink: #000000; + --af-accent-text: #f0ff00; + --af-accent-hover: #f5ff4d; + --af-accent-soft: rgba(240, 255, 0, 0.06); + --af-primary: #f0ff00; + --af-primary-ink: #000000; + --af-overlay: rgba(0, 0, 0, 0.6); + --af-danger: #e64239; + --af-ok: #80c340; +} diff --git a/docs_app/app/global.css b/docs_app/app/global.css index e958534..97d679e 100644 --- a/docs_app/app/global.css +++ b/docs_app/app/global.css @@ -3,35 +3,20 @@ * model: Claude Fable 5.1 * harness: Claude Code * - * The LeanCode design system as used on ciach.leancode.co — near-black - * surfaces, one lime accent, Space Grotesk and JetBrains Mono — mapped onto - * the variables Fumadocs paints with. Dark is the design; light is a paper - * variant that keeps the same accent as a fill (black ink on lime), the way - * leancode.co does it. + * The LeanCode design system — black surfaces, one CTA yellow, Space Grotesk + * and JetBrains Mono — mapped onto the variables Fumadocs paints with. Dark is + * the design; light is a paper variant that keeps the same accent as a fill + * (black ink on yellow), the way leancode.co does it. The colors themselves + * are the `--af-*` variables of af-tokens.css, generated from ../palette.json + * and shared with the landing page; this file holds everything else. */ @import "tailwindcss"; @import "fumadocs-ui/css/preset.css"; +@import "./af-tokens.css"; /* ---------- Tokens ---------- */ :root { - --af-bg: #f6f6f2; - --af-bg-2: #ffffff; - --af-surface: #ffffff; - --af-surface-2: #efefe9; - --af-border: #e2e2dc; - --af-border-2: #cfcfc7; - --af-text: #0b0b0d; - --af-text-2: #4b4b52; - --af-muted: #7d7d84; - --af-accent: #edff2f; - --af-accent-2: #c9dc00; - --af-accent-ink: #0b0c00; - /* Accent used *as text* on a light ground has to be darker to read. */ - --af-accent-text: #5f6e00; - --af-accent-soft: rgba(237, 255, 47, 0.45); - --af-danger: #d93636; - --af-ok: #1f9d55; --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.25); --af-radius: 14px; --af-radius-sm: 8px; @@ -40,78 +25,43 @@ } .dark { - --af-bg: #050505; - --af-bg-2: #0b0b0d; - --af-surface: #101013; - --af-surface-2: #16161b; - --af-border: #23232b; - --af-border-2: #33333e; - --af-text: #f4f4f1; - --af-text-2: #b7b7b3; - --af-muted: #7d7d84; - --af-accent: #edff2f; - --af-accent-2: #c9dc00; - --af-accent-ink: #0b0c00; - --af-accent-text: #edff2f; - --af-accent-soft: rgba(237, 255, 47, 0.12); - --af-danger: #ff5d5d; - --af-ok: #58e08a; --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.9); } -/* Fumadocs paints with these. Light values in @theme so Tailwind emits the - utilities; the dark block re-points the same variables. */ -@theme { - --color-fd-background: #f6f6f2; - --color-fd-foreground: #0b0b0d; - --color-fd-muted: #efefe9; - --color-fd-muted-foreground: #55555c; - --color-fd-popover: #ffffff; - --color-fd-popover-foreground: #0b0b0d; - --color-fd-card: #ffffff; - --color-fd-card-foreground: #0b0b0d; - --color-fd-border: #e2e2dc; - --color-fd-primary: #0b0b0d; - --color-fd-primary-foreground: #edff2f; - --color-fd-secondary: #efefe9; - --color-fd-secondary-foreground: #0b0b0d; - --color-fd-accent: rgba(11, 11, 13, 0.06); - --color-fd-accent-foreground: #0b0b0d; - --color-fd-ring: #0b0b0d; - --color-fd-overlay: rgba(0, 0, 0, 0.3); -} - -.dark { - --color-fd-background: #050505; - --color-fd-foreground: #f4f4f1; - --color-fd-muted: #16161b; - --color-fd-muted-foreground: #a8a8a4; - --color-fd-popover: #101013; - --color-fd-popover-foreground: #f4f4f1; - --color-fd-card: #101013; - --color-fd-card-foreground: #f4f4f1; - --color-fd-border: #23232b; - --color-fd-primary: #edff2f; - --color-fd-primary-foreground: #0b0c00; - --color-fd-secondary: #16161b; - --color-fd-secondary-foreground: #f4f4f1; - --color-fd-accent: rgba(255, 255, 255, 0.06); - --color-fd-accent-foreground: #f4f4f1; - --color-fd-ring: #edff2f; - --color-fd-overlay: rgba(0, 0, 0, 0.6); -} - -@theme static { +/* Fumadocs paints with these. Inline, so the utilities point at the `--af-*` + variables and follow the theme with them; no dark block needed. */ +@theme inline { + --color-fd-background: var(--af-bg); + --color-fd-foreground: var(--af-text); + --color-fd-muted: var(--af-surface-2); + --color-fd-muted-foreground: var(--af-muted); + --color-fd-popover: var(--af-surface); + --color-fd-popover-foreground: var(--af-text); + --color-fd-card: var(--af-surface); + --color-fd-card-foreground: var(--af-text); + --color-fd-border: var(--af-border); + --color-fd-primary: var(--af-primary); + --color-fd-primary-foreground: var(--af-primary-ink); + --color-fd-secondary: var(--af-surface-2); + --color-fd-secondary-foreground: var(--af-text); + --color-fd-accent: color-mix(in srgb, var(--af-text) 6%, transparent); + --color-fd-accent-foreground: var(--af-text); + --color-fd-ring: var(--af-primary); + --color-fd-overlay: var(--af-overlay); +} + +@theme inline static { + /* Info and warning are the code palette's blue and amber, not brand colors. */ --color-fd-info: #82aaff; --color-fd-warning: #ffcb6b; - --color-fd-error: #ff5d5d; - --color-fd-success: #58e08a; - --color-fd-idea: #edff2f; + --color-fd-error: var(--af-danger); + --color-fd-success: var(--af-ok); + --color-fd-idea: var(--af-accent); /* Diff markers in code blocks; the preset's Shiki styles apply these. */ - --color-fd-diff-remove: rgba(255, 93, 93, 0.12); - --color-fd-diff-remove-symbol: #ff5d5d; - --color-fd-diff-add: rgba(88, 224, 138, 0.12); - --color-fd-diff-add-symbol: #58e08a; + --color-fd-diff-remove: color-mix(in srgb, var(--af-danger) 12%, transparent); + --color-fd-diff-remove-symbol: var(--af-danger); + --color-fd-diff-add: color-mix(in srgb, var(--af-ok) 12%, transparent); + --color-fd-diff-add-symbol: var(--af-ok); } @theme inline { diff --git a/docs_app/app/icon.svg b/docs_app/app/icon.svg index ba21bea..8c45ed3 100644 --- a/docs_app/app/icon.svg +++ b/docs_app/app/icon.svg @@ -2,6 +2,6 @@ - - + + diff --git a/docs_app/app/og/[...slug]/route.tsx b/docs_app/app/og/[...slug]/route.tsx index eb1fec4..24b9be7 100644 --- a/docs_app/app/og/[...slug]/route.tsx +++ b/docs_app/app/og/[...slug]/route.tsx @@ -7,16 +7,12 @@ import { getPageImageUrl, source } from "@/lib/source" import { notFound } from "next/navigation" import { ImageResponse } from "next/og" import { LogoMark } from "@/components/logo" +import { dark, withAlpha } from "@/lib/palette" import { appName } from "@/lib/shared" export const revalidate = false -const ink = "#050505" -const surface = "#101013" -const border = "#23232b" -const text = "#f4f4f1" -const muted = "#b7b7b3" -const accent = "#edff2f" +const { bg: ink, surface, border, text, muted, accent } = dark export async function GET(_req: Request, { params }: RouteContext<"/og/[...slug]">) { const { slug } = await params @@ -32,7 +28,7 @@ export async function GET(_req: Request, { params }: RouteContext<"/og/[...slug] flexDirection: "column", justifyContent: "space-between", padding: 64, - background: `radial-gradient(60% 50% at 85% 10%, rgba(237, 255, 47, 0.18), transparent 65%), ${ink}`, + background: `radial-gradient(60% 50% at 85% 10%, ${withAlpha(accent, 0.18)}, transparent 65%), ${ink}`, color: text, fontFamily: "sans-serif", }}> diff --git a/docs_app/app/og/landing.png/route.tsx b/docs_app/app/og/landing.png/route.tsx index ff178dd..5094dd2 100644 --- a/docs_app/app/og/landing.png/route.tsx +++ b/docs_app/app/og/landing.png/route.tsx @@ -5,6 +5,7 @@ */ import { ImageResponse } from "next/og" import { LogoMark } from "@/components/logo" +import { dark, withAlpha } from "@/lib/palette" import { appName, description } from "@/lib/shared" /** @@ -14,12 +15,7 @@ import { appName, description } from "@/lib/shared" */ export const dynamic = "force-static" -const ink = "#050505" -const surface = "#101013" -const border = "#23232b" -const text = "#f4f4f1" -const muted = "#b7b7b3" -const accent = "#edff2f" +const { bg: ink, surface, border, text, muted, accent } = dark export function GET() { return new ImageResponse( @@ -31,7 +27,7 @@ export function GET() { flexDirection: "column", justifyContent: "space-between", padding: 64, - background: `radial-gradient(60% 50% at 85% 10%, rgba(237, 255, 47, 0.18), transparent 65%), ${ink}`, + background: `radial-gradient(60% 50% at 85% 10%, ${withAlpha(accent, 0.18)}, transparent 65%), ${ink}`, color: text, fontFamily: "sans-serif", }}> diff --git a/docs_app/components/logo.tsx b/docs_app/components/logo.tsx index a7ad58a..0968d0f 100644 --- a/docs_app/components/logo.tsx +++ b/docs_app/components/logo.tsx @@ -5,6 +5,7 @@ */ import logoDark from "@/public/logo-dark.svg" import logoLight from "@/public/logo-light.svg" +import { swatches } from "@/lib/palette" import { appName } from "@/lib/shared" import { cn } from "@/lib/cn" @@ -48,8 +49,8 @@ export function LogoMark({ size = 56 }: { size?: number }) { transform="matrix(0.5 0.866025 0.866025 -0.5 209.527 183.354)" fill="white" /> - - + + ) } diff --git a/docs_app/eslint.config.mjs b/docs_app/eslint.config.mjs index f9c05de..2bdf6dd 100644 --- a/docs_app/eslint.config.mjs +++ b/docs_app/eslint.config.mjs @@ -23,10 +23,12 @@ const config = [ "flutter/**", "public/flutter-examples/**", "lib/flutter-examples/manifest.generated.ts", + "lib/palette.generated.ts", // The landing page, built by Jaspr in ../landing and copied here. "public/index.html", "public/landing.*", "public/landing-icon.svg", + "public/af-tokens.css", ], }, { diff --git a/docs_app/flutter/lib/support/island_frame.dart b/docs_app/flutter/lib/support/island_frame.dart index c367fb4..5d3b5df 100644 --- a/docs_app/flutter/lib/support/island_frame.dart +++ b/docs_app/flutter/lib/support/island_frame.dart @@ -5,6 +5,7 @@ * edited-by: Claude Fable 5.1 (Claude Code) */ import 'package:advanced_forms_docs_islands/support/example_log.dart'; +import 'package:advanced_forms_docs_islands/support/palette.generated.dart'; import 'package:flutter/material.dart'; /// The part of an island that has nothing to do with the browser. @@ -88,32 +89,34 @@ class _IslandFrameState extends State { /// The LeanCode design system, as a Material theme. /// -/// The same tokens the docs page paints with: near-black surfaces, one lime -/// accent, rounded filled inputs. In the light variant the accent becomes a -/// fill with dark ink on it — a lime *text* would not be legible on paper. +/// The same `--af-*` tokens the docs page paints with, from +/// `palette.generated.dart`: black surfaces, one CTA yellow, rounded filled +/// inputs. In the light variant the accent becomes a fill with black ink on +/// it — a yellow *text* would not be legible on paper. ThemeData islandTheme(Brightness brightness) { final dark = brightness == Brightness.dark; + final palette = dark ? afDark : afLight; - const accent = Color(0xFFEDFF2F); - const accentInk = Color(0xFF0B0C00); - final ink = dark ? const Color(0xFFF4F4F1) : const Color(0xFF0B0B0D); - final ink2 = dark ? const Color(0xFFB7B7B3) : const Color(0xFF4B4B52); - const muted = Color(0xFF7D7D84); - final surface = dark ? const Color(0xFF101013) : Colors.white; - final surface2 = dark ? const Color(0xFF16161B) : const Color(0xFFEFEFE9); - final border = dark ? const Color(0xFF33333E) : const Color(0xFFCFCFC7); - final borderSoft = dark ? const Color(0xFF23232B) : const Color(0xFFE2E2DC); - final danger = dark ? const Color(0xFFFF5D5D) : const Color(0xFFD93636); - final ok = dark ? const Color(0xFF58E08A) : const Color(0xFF1F9D55); + final accent = palette.accent; + final accentInk = palette.accentInk; + final ink = palette.text; + final ink2 = palette.text2; + final muted = palette.muted; + final surface = palette.surface; + final surface2 = palette.surface2; + final border = palette.border2; + final borderSoft = palette.border; + final danger = palette.danger; + final ok = palette.ok; - final primary = dark ? accent : const Color(0xFF0B0B0D); - final onPrimary = dark ? accentInk : accent; + final primary = palette.primary; + final onPrimary = palette.primaryInk; final scheme = ColorScheme( brightness: brightness, primary: primary, onPrimary: onPrimary, - primaryContainer: accent.withValues(alpha: dark ? 0.16 : 0.45), + primaryContainer: palette.accentSoft, onPrimaryContainer: ink, secondary: accent, onSecondary: accentInk, @@ -137,7 +140,7 @@ ThemeData islandTheme(Brightness brightness) { scrim: Colors.black, inverseSurface: ink, onInverseSurface: surface, - inversePrimary: dark ? const Color(0xFF0B0B0D) : accent, + inversePrimary: (dark ? afLight : afDark).primary, ); final rounded = RoundedRectangleBorder( @@ -161,10 +164,10 @@ ThemeData islandTheme(Brightness brightness) { fillColor: surface2, isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), - hintStyle: const TextStyle(color: muted), + hintStyle: TextStyle(color: muted), labelStyle: TextStyle(color: ink2), floatingLabelStyle: TextStyle(color: primary), - helperStyle: const TextStyle(color: muted), + helperStyle: TextStyle(color: muted), errorStyle: TextStyle(color: danger), prefixIconColor: muted, suffixIconColor: muted, diff --git a/docs_app/flutter/lib/support/palette.generated.dart b/docs_app/flutter/lib/support/palette.generated.dart new file mode 100644 index 0000000..1693f21 --- /dev/null +++ b/docs_app/flutter/lib/support/palette.generated.dart @@ -0,0 +1,134 @@ +// Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do +// not edit. Run `npm run palette:generate` in docs_app after changing the +// palette. + +import 'dart:ui' show Color; + +/// The site's `--af-*` tokens for one theme, as the docs and the landing +/// page paint them. +class AfPalette { + const AfPalette({ + required this.bg, + required this.bg2, + required this.surface, + required this.surface2, + required this.border, + required this.border2, + required this.text, + required this.text2, + required this.muted, + required this.accent, + required this.accentInk, + required this.accentText, + required this.accentHover, + required this.accentSoft, + required this.primary, + required this.primaryInk, + required this.overlay, + required this.danger, + required this.ok, + }); + + /// Page background. + final Color bg; + + /// Section background: footer, code panels. + final Color bg2; + + /// Cards and frames. + final Color surface; + + /// Bars and chips on a surface. + final Color surface2; + + /// The hairline. + final Color border; + + /// The stronger hairline, for controls. + final Color border2; + + /// Headings and primary text. + final Color text; + + /// Body copy. + final Color text2; + + /// Captions and metadata. + final Color muted; + + /// The CTA yellow, as a fill. + final Color accent; + + /// Text on the accent. + final Color accentInk; + + /// The accent as text. + final Color accentText; + + /// The primary button's hover fill. + final Color accentHover; + + /// The accent as a wash behind icons, pills and glows. + final Color accentSoft; + + /// The docs' primary action fill. + final Color primary; + + /// Text on the primary fill. + final Color primaryInk; + + /// The scrim behind dialogs. + final Color overlay; + + /// Error signal. + final Color danger; + + /// Success signal. + final Color ok; +} + +/// The light theme. +const afLight = AfPalette( + bg: Color(0xFFFFFFFF), + bg2: Color(0xFFF7F7F5), + surface: Color(0xFFFFFFFF), + surface2: Color(0xFFF2F2EF), + border: Color(0xFFE5E5E5), + border2: Color(0xFFCFCFCB), + text: Color(0xFF000000), + text2: Color(0xFF3F3F3C), + muted: Color(0xFF757575), + accent: Color(0xFFF0FF00), + accentInk: Color(0xFF000000), + accentText: Color(0xFF606A00), + accentHover: Color(0xFFF5FF4D), + accentSoft: Color(0x73F0FF00), + primary: Color(0xFF000000), + primaryInk: Color(0xFFF0FF00), + overlay: Color(0x4D000000), + danger: Color(0xFFBE2119), + ok: Color(0xFF527B28), +); + +/// The dark theme. +const afDark = AfPalette( + bg: Color(0xFF000000), + bg2: Color(0xFF0A0A08), + surface: Color(0xFF151513), + surface2: Color(0xFF1D1D1A), + border: Color(0x1FFFFFFF), + border2: Color(0x33FFFFFF), + text: Color(0xFFFFFFFF), + text2: Color(0xFFD8D8D4), + muted: Color(0xFFA3A3A0), + accent: Color(0xFFF0FF00), + accentInk: Color(0xFF000000), + accentText: Color(0xFFF0FF00), + accentHover: Color(0xFFF5FF4D), + accentSoft: Color(0x0FF0FF00), + primary: Color(0xFFF0FF00), + primaryInk: Color(0xFF000000), + overlay: Color(0x99000000), + danger: Color(0xFFE64239), + ok: Color(0xFF80C340), +); diff --git a/docs_app/lib/palette.generated.ts b/docs_app/lib/palette.generated.ts new file mode 100644 index 0000000..e03cb84 --- /dev/null +++ b/docs_app/lib/palette.generated.ts @@ -0,0 +1,129 @@ +// Generated by scripts/palette.mjs from palette.json — do not edit. Run `npm run palette:generate` in docs_app after changing the palette. + +/** The LeanCode swatches, as `#rrggbb`. */ +export const swatches = { + /** Primary surface. LeanCode is black-first; yellow is the exception. */ + black: "#000000", + /** Text and headings on black; the light document surface. */ + white: "#ffffff", + /** Call to action and emphasis only. Text on it is always black. */ + ctaYellow: "#f0ff00", + /** Body copy on dark surfaces. */ + bodyGray: "#d8d8d4", + /** Secondary text, captions, metadata on dark. */ + mutedGray: "#a3a3a0", + /** Secondary text, captions, metadata on light. */ + mutedOnLight: "#757575", + /** Cards and panels on black, warm-tinted. */ + surface: "#151513", + /** A second surface step on black. */ + surface2: "#1d1d1a", + /** The 1px hairline on white. */ + lineOnLight: "#e5e5e5", + /** Error signal, sparingly. */ + error: "#e64239", + /** Success signal, sparingly. */ + success: "#80c340", + /** Site tint: a section background a step above black. */ + nearBlack: "#0a0a08", + /** Site tint: a section background a step below white. */ + paper: "#f7f7f5", + /** Site tint: a second surface step on white. */ + paper2: "#f2f2ef", + /** Site tint: body copy on light surfaces. */ + bodyOnLight: "#3f3f3c", + /** Site tint: the stronger hairline on white, for controls. */ + lineStrongOnLight: "#cfcfcb", + /** Site tint: the CTA yellow lifted, for the primary button's hover. */ + ctaYellowLight: "#f5ff4d", + /** Site tint: the CTA yellow as text on white, which yellow itself never is. */ + ctaYellowOnLight: "#606a00", + /** Site tint: the error signal as text on white. */ + errorOnLight: "#be2119", + /** Site tint: the success signal as text on white. */ + successOnLight: "#527b28", +} as const + +/** The site's `--af-*` tokens per theme, as CSS colors. */ +export const themes = { + light: { + /** Page background. */ + bg: "#ffffff", + /** Section background: footer, code panels. */ + bg2: "#f7f7f5", + /** Cards and frames. */ + surface: "#ffffff", + /** Bars and chips on a surface. */ + surface2: "#f2f2ef", + /** The hairline. */ + border: "#e5e5e5", + /** The stronger hairline, for controls. */ + border2: "#cfcfcb", + /** Headings and primary text. */ + text: "#000000", + /** Body copy. */ + text2: "#3f3f3c", + /** Captions and metadata. */ + muted: "#757575", + /** The CTA yellow, as a fill. */ + accent: "#f0ff00", + /** Text on the accent. */ + accentInk: "#000000", + /** The accent as text. */ + accentText: "#606a00", + /** The primary button's hover fill. */ + accentHover: "#f5ff4d", + /** The accent as a wash behind icons, pills and glows. */ + accentSoft: "rgba(240, 255, 0, 0.45)", + /** The docs' primary action fill. */ + primary: "#000000", + /** Text on the primary fill. */ + primaryInk: "#f0ff00", + /** The scrim behind dialogs. */ + overlay: "rgba(0, 0, 0, 0.3)", + /** Error signal. */ + danger: "#be2119", + /** Success signal. */ + ok: "#527b28", + }, + dark: { + /** Page background. */ + bg: "#000000", + /** Section background: footer, code panels. */ + bg2: "#0a0a08", + /** Cards and frames. */ + surface: "#151513", + /** Bars and chips on a surface. */ + surface2: "#1d1d1a", + /** The hairline. */ + border: "rgba(255, 255, 255, 0.12)", + /** The stronger hairline, for controls. */ + border2: "rgba(255, 255, 255, 0.2)", + /** Headings and primary text. */ + text: "#ffffff", + /** Body copy. */ + text2: "#d8d8d4", + /** Captions and metadata. */ + muted: "#a3a3a0", + /** The CTA yellow, as a fill. */ + accent: "#f0ff00", + /** Text on the accent. */ + accentInk: "#000000", + /** The accent as text. */ + accentText: "#f0ff00", + /** The primary button's hover fill. */ + accentHover: "#f5ff4d", + /** The accent as a wash behind icons, pills and glows. */ + accentSoft: "rgba(240, 255, 0, 0.06)", + /** The docs' primary action fill. */ + primary: "#f0ff00", + /** Text on the primary fill. */ + primaryInk: "#000000", + /** The scrim behind dialogs. */ + overlay: "rgba(0, 0, 0, 0.6)", + /** Error signal. */ + danger: "#e64239", + /** Success signal. */ + ok: "#80c340", + }, +} as const diff --git a/docs_app/lib/palette.ts b/docs_app/lib/palette.ts new file mode 100644 index 0000000..4e1baef --- /dev/null +++ b/docs_app/lib/palette.ts @@ -0,0 +1,17 @@ +/* + * AI-Provenance: + * model: Claude Fable 5.1 + * harness: Claude Code + */ +import { swatches, themes } from "@/lib/palette.generated" + +export { swatches, themes } + +/** The dark theme: what the social cards are drawn in, whatever the reader's theme. */ +export const dark = themes.dark + +/** `#rrggbb` at an alpha, as `rgba()` — the one color form every renderer of ours understands. */ +export function withAlpha(hex: string, alpha: number): string { + const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16)) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} diff --git a/docs_app/package.json b/docs_app/package.json index 1301013..a8f28bc 100644 --- a/docs_app/package.json +++ b/docs_app/package.json @@ -18,7 +18,9 @@ "examples:format:check": "node scripts/format-snippets.mjs --check", "examples:build": "node scripts/flutter-examples.mjs build", "examples:serve": "cd flutter && flutter run -d web-server --web-port 5333", - "landing:build": "node scripts/landing.mjs" + "landing:build": "node scripts/landing.mjs", + "palette:generate": "node scripts/palette.mjs generate", + "palette:check": "node scripts/palette.mjs check" }, "dependencies": { "cnfast": "^0.1.0", diff --git a/docs_app/palette.json b/docs_app/palette.json new file mode 100644 index 0000000..159e724 --- /dev/null +++ b/docs_app/palette.json @@ -0,0 +1,93 @@ +{ + "swatches": { + "black": { "hex": "#000000", "note": "Primary surface. LeanCode is black-first; yellow is the exception." }, + "white": { "hex": "#ffffff", "note": "Text and headings on black; the light document surface." }, + "ctaYellow": { "hex": "#f0ff00", "note": "Call to action and emphasis only. Text on it is always black." }, + "bodyGray": { "hex": "#d8d8d4", "note": "Body copy on dark surfaces." }, + "mutedGray": { "hex": "#a3a3a0", "note": "Secondary text, captions, metadata on dark." }, + "mutedOnLight": { "hex": "#757575", "note": "Secondary text, captions, metadata on light." }, + "surface": { "hex": "#151513", "note": "Cards and panels on black, warm-tinted." }, + "surface2": { "hex": "#1d1d1a", "note": "A second surface step on black." }, + "lineOnLight": { "hex": "#e5e5e5", "note": "The 1px hairline on white." }, + "error": { "hex": "#e64239", "note": "Error signal, sparingly." }, + "success": { "hex": "#80c340", "note": "Success signal, sparingly." }, + + "nearBlack": { "hex": "#0a0a08", "note": "Site tint: a section background a step above black." }, + "paper": { "hex": "#f7f7f5", "note": "Site tint: a section background a step below white." }, + "paper2": { "hex": "#f2f2ef", "note": "Site tint: a second surface step on white." }, + "bodyOnLight": { "hex": "#3f3f3c", "note": "Site tint: body copy on light surfaces." }, + "lineStrongOnLight": { "hex": "#cfcfcb", "note": "Site tint: the stronger hairline on white, for controls." }, + "ctaYellowLight": { "hex": "#f5ff4d", "note": "Site tint: the CTA yellow lifted, for the primary button's hover." }, + "ctaYellowOnLight": { + "hex": "#606a00", + "note": "Site tint: the CTA yellow as text on white, which yellow itself never is." + }, + "errorOnLight": { "hex": "#be2119", "note": "Site tint: the error signal as text on white." }, + "successOnLight": { "hex": "#527b28", "note": "Site tint: the success signal as text on white." } + }, + "tokens": { + "bg": "Page background.", + "bg2": "Section background: footer, code panels.", + "surface": "Cards and frames.", + "surface2": "Bars and chips on a surface.", + "border": "The hairline.", + "border2": "The stronger hairline, for controls.", + "text": "Headings and primary text.", + "text2": "Body copy.", + "muted": "Captions and metadata.", + "accent": "The CTA yellow, as a fill.", + "accentInk": "Text on the accent.", + "accentText": "The accent as text.", + "accentHover": "The primary button's hover fill.", + "accentSoft": "The accent as a wash behind icons, pills and glows.", + "primary": "The docs' primary action fill.", + "primaryInk": "Text on the primary fill.", + "overlay": "The scrim behind dialogs.", + "danger": "Error signal.", + "ok": "Success signal." + }, + "themes": { + "light": { + "bg": "white", + "bg2": "paper", + "surface": "white", + "surface2": "paper2", + "border": "lineOnLight", + "border2": "lineStrongOnLight", + "text": "black", + "text2": "bodyOnLight", + "muted": "mutedOnLight", + "accent": "ctaYellow", + "accentInk": "black", + "accentText": "ctaYellowOnLight", + "accentHover": "ctaYellowLight", + "accentSoft": { "of": "ctaYellow", "alpha": 0.45 }, + "primary": "black", + "primaryInk": "ctaYellow", + "overlay": { "of": "black", "alpha": 0.3 }, + "danger": "errorOnLight", + "ok": "successOnLight" + }, + "dark": { + "bg": "black", + "bg2": "nearBlack", + "surface": "surface", + "surface2": "surface2", + "border": { "of": "white", "alpha": 0.12 }, + "border2": { "of": "white", "alpha": 0.2 }, + "text": "white", + "text2": "bodyGray", + "muted": "mutedGray", + "accent": "ctaYellow", + "accentInk": "black", + "accentText": "ctaYellow", + "accentHover": "ctaYellowLight", + "accentSoft": { "of": "ctaYellow", "alpha": 0.06 }, + "primary": "ctaYellow", + "primaryInk": "black", + "overlay": { "of": "black", "alpha": 0.6 }, + "danger": "error", + "ok": "success" + } + } +} diff --git a/docs_app/public/logo-dark.svg b/docs_app/public/logo-dark.svg index ab96eb7..b532098 100644 --- a/docs_app/public/logo-dark.svg +++ b/docs_app/public/logo-dark.svg @@ -3,6 +3,6 @@ - - + + diff --git a/docs_app/public/logo-light.svg b/docs_app/public/logo-light.svg index f02ae69..65b21b6 100644 --- a/docs_app/public/logo-light.svg +++ b/docs_app/public/logo-light.svg @@ -3,6 +3,6 @@ - - + + diff --git a/docs_app/scripts/landing.mjs b/docs_app/scripts/landing.mjs index 82e3e4a..5b98b77 100644 --- a/docs_app/scripts/landing.mjs +++ b/docs_app/scripts/landing.mjs @@ -28,7 +28,7 @@ const output = path.join(landing, "build", "jaspr") const publicDir = path.join(docsApp, "public") /** What ends up in public/. Everything else in build/jaspr is build tooling. */ -const artifacts = ["index.html", "landing.css", "landing.js", "landing-icon.svg"] +const artifacts = ["index.html", "af-tokens.css", "landing.css", "landing.js", "landing-icon.svg"] function run(command, args, options = {}) { const result = spawnSync(command, args, { stdio: "inherit", cwd: landing, ...options }) diff --git a/docs_app/scripts/palette.mjs b/docs_app/scripts/palette.mjs new file mode 100644 index 0000000..4bf3a2b --- /dev/null +++ b/docs_app/scripts/palette.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +/* + * AI-Provenance: + * model: Claude Fable 5.1 + * harness: Claude Code + */ + +/** + * Writes every copy of the site's palette from palette.json, so the landing + * page, the docs, the social cards, the Flutter demos and the logos cannot + * disagree about a color. palette.json names the LeanCode swatches and maps + * them onto the `--af-*` tokens of the light and dark themes; this script + * turns that into each consumer's language. + * + * generate write the generated files and recolor the marked SVG elements + * check fail if any of them differs from what palette.json says + * + * Generated (committed, so every checkout builds without running this): + * app/af-tokens.css, ../landing/web/af-tokens.css the tokens as CSS variables + * lib/palette.generated.ts the swatches and tokens for the OG cards and the logo + * flutter/lib/support/palette.generated.dart the tokens as Flutter Colors, for the live demos + * ../landing/lib/palette.generated.dart the theme-color of the landing page + * + * Recolored in place: SVG elements carrying `data-palette=""` get that + * swatch as their fill. The logos and icons stay single files of artwork; the + * attribute marks which shapes are brand color. + */ +import { readFile, writeFile } from "node:fs/promises" +import { join, relative, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const docsApp = resolve(fileURLToPath(new URL(".", import.meta.url)), "..") +const repoRoot = resolve(docsApp, "..") +const landing = join(repoRoot, "landing") +const sourceFile = join(docsApp, "palette.json") + +const palette = JSON.parse(await readFile(sourceFile, "utf8")) +const tokenNames = Object.keys(palette.tokens) + +for (const [theme, tokens] of Object.entries(palette.themes)) { + const missing = tokenNames.filter(name => !(name in tokens)) + const extra = Object.keys(tokens).filter(name => !(name in palette.tokens)) + if (missing.length || extra.length) { + throw new Error( + `palette.json: theme "${theme}" must define exactly the tokens listed under "tokens"` + + (missing.length ? `; missing ${missing.join(", ")}` : "") + + (extra.length ? `; unknown ${extra.join(", ")}` : ""), + ) + } +} + +// ---------- Colors ---------- + +function swatch(name) { + const entry = palette.swatches[name] + if (!entry) throw new Error(`palette.json: unknown swatch "${name}"`) + if (!/^#[0-9a-f]{6}$/.test(entry.hex)) throw new Error(`palette.json: swatch "${name}" must be lowercase #rrggbb`) + return entry +} + +/** A token's value: the swatch's hex, and the alpha it is used at (1 for solid). */ +function color(value) { + return typeof value === "string" + ? { hex: swatch(value).hex, alpha: 1 } + : { hex: swatch(value.of).hex, alpha: value.alpha } +} + +const channels = hex => [1, 3, 5].map(i => Number.parseInt(hex.slice(i, i + 2), 16)) + +const cssColor = ({ hex, alpha }) => (alpha === 1 ? hex : `rgba(${channels(hex).join(", ")}, ${alpha})`) + +const dartColor = ({ hex, alpha }) => { + const a = Math.round(alpha * 255) + .toString(16) + .padStart(2, "0") + return `Color(0x${(a + hex.slice(1)).toUpperCase()})` +} + +const cssName = name => `--af-${name.replaceAll(/([a-z])([A-Z0-9])/g, "$1-$2").toLowerCase()}` + +// ---------- Outputs ---------- + +const doNotEdit = (from, here) => + `Generated by ${from} from ${here} — do not edit. Run \`npm run palette:generate\` in docs_app after changing the palette.` + +/** The same notice, wrapped for Dart's 80 columns. */ +const dartHeader = lines => lines.map(line => `// ${line}\n`).join("") + +function tokensCss() { + const block = (selector, tokens, notes) => + `${selector} {\n` + + tokenNames + .map( + name => + `${notes ? ` /* ${palette.tokens[name]} */\n` : ""} ${cssName(name)}: ${cssColor(color(tokens[name]))};\n`, + ) + .join(notes ? "\n" : "") + + "}\n" + return ( + `/* ${doNotEdit("docs_app/scripts/palette.mjs", "docs_app/palette.json")} */\n\n` + + block(":root", palette.themes.light, true) + + "\n" + + block(".dark", palette.themes.dark, false) + ) +} + +function paletteTs() { + const swatches = Object.entries(palette.swatches) + .map(([name, { hex, note }]) => ` /** ${note} */\n ${name}: "${hex}",\n`) + .join("") + const theme = tokens => + tokenNames + .map(name => ` /** ${palette.tokens[name]} */\n ${name}: "${cssColor(color(tokens[name]))}",\n`) + .join("") + return ( + `// ${doNotEdit("scripts/palette.mjs", "palette.json")}\n\n` + + `/** The LeanCode swatches, as \`#rrggbb\`. */\n` + + `export const swatches = {\n${swatches}} as const\n\n` + + `/** The site's \`--af-*\` tokens per theme, as CSS colors. */\n` + + `export const themes = {\n` + + ` light: {\n${theme(palette.themes.light)} },\n` + + ` dark: {\n${theme(palette.themes.dark)} },\n` + + `} as const\n` + ) +} + +function flutterDart() { + const fields = tokenNames.map(name => ` /// ${palette.tokens[name]}\n final Color ${name};\n`).join("\n") + const params = tokenNames.map(name => ` required this.${name},\n`).join("") + const theme = tokens => tokenNames.map(name => ` ${name}: ${dartColor(color(tokens[name]))},\n`).join("") + return ( + dartHeader([ + "Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do", + "not edit. Run `npm run palette:generate` in docs_app after changing the", + "palette.", + ]) + + `\nimport 'dart:ui' show Color;\n\n` + + `/// The site's \`--af-*\` tokens for one theme, as the docs and the landing\n` + + `/// page paint them.\n` + + `class AfPalette {\n` + + ` const AfPalette({\n${params} });\n\n` + + `${fields}}\n\n` + + `/// The light theme.\n` + + `const afLight = AfPalette(\n${theme(palette.themes.light)});\n\n` + + `/// The dark theme.\n` + + `const afDark = AfPalette(\n${theme(palette.themes.dark)});\n` + ) +} + +function landingDart() { + return ( + dartHeader([ + "Generated by ../../docs_app/scripts/palette.mjs from", + "../../docs_app/palette.json — do not edit. Run `npm run palette:generate`", + "in docs_app after changing the palette.", + ]) + + `\n/// The dark theme's page background, for \`\`.\n` + + `const themeColor = '${cssColor(color(palette.themes.dark.bg))}';\n` + ) +} + +/** Recolors the elements of an SVG that carry `data-palette=""`. */ +function recolorSvg(file, svg) { + let marked = 0 + const result = svg.replaceAll(/<[a-zA-Z][^>]*\bdata-palette="([^"]+)"[^>]*>/g, (tag, name) => { + marked += 1 + if (!/\bfill="[^"]*"/.test(tag)) throw new Error(`${file}: the element marked data-palette="${name}" has no fill`) + return tag.replace(/\bfill="[^"]*"/, `fill="${swatch(name).hex}"`) + }) + if (marked === 0) throw new Error(`${file}: no element carries data-palette; mark the brand-color shapes`) + return result +} + +const generated = [ + { file: join(docsApp, "app", "af-tokens.css"), content: tokensCss }, + { file: join(landing, "web", "af-tokens.css"), content: tokensCss }, + { file: join(docsApp, "lib", "palette.generated.ts"), content: paletteTs }, + { file: join(docsApp, "flutter", "lib", "support", "palette.generated.dart"), content: flutterDart }, + { file: join(landing, "lib", "palette.generated.dart"), content: landingDart }, +] + +const svgs = [ + join(docsApp, "app", "icon.svg"), + join(docsApp, "public", "logo-dark.svg"), + join(docsApp, "public", "logo-light.svg"), + join(landing, "web", "landing-icon.svg"), +] + +/** Every output with what it should contain and what it contains now. */ +async function outputs() { + const current = async file => { + try { + return await readFile(file, "utf8") + } catch (error) { + if (error.code === "ENOENT") return null + throw error + } + } + const files = [] + for (const { file, content } of generated) { + files.push({ file, expected: content(), actual: await current(file) }) + } + for (const file of svgs) { + const actual = await current(file) + if (actual === null) throw new Error(`${file} is missing`) + files.push({ file, expected: recolorSvg(relative(repoRoot, file), actual), actual }) + } + return files +} + +const label = file => relative(repoRoot, file) + +async function generate() { + for (const { file, expected, actual } of await outputs()) { + if (expected === actual) continue + await writeFile(file, expected) + console.log(`wrote ${label(file)}`) + } + console.log(`palette: ${generated.length} generated files and ${svgs.length} SVGs follow palette.json`) +} + +async function check() { + const files = await outputs() + const stale = files.filter(({ expected, actual }) => expected !== actual) + if (stale.length === 0) { + console.log(`palette: ${generated.length} generated files and ${svgs.length} SVGs are up to date`) + return + } + throw new Error( + "The palette's generated files are out of date — run `npm run palette:generate` in docs_app:\n" + + stale.map(({ file }) => ` ${label(file)}`).join("\n"), + ) +} + +const command = process.argv[2] +try { + if (command === "generate") await generate() + else if (command === "check") await check() + else throw new Error("usage: node scripts/palette.mjs ") +} catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exit(1) +} diff --git a/landing/README.md b/landing/README.md index c7eea1d..36d2501 100644 --- a/landing/README.md +++ b/landing/README.md @@ -30,7 +30,8 @@ From `../docs_app`, `npm run landing:build` runs the build and copies those four | `lib/examples.dart` | Reads the demos from `../docs_app/content/landing/*.mdx` and maps them to the compiled bundle. | | `lib/highlight.dart` | Build-time Dart syntax highlighting (`syntax_highlight_lite`) into `tk-*` spans. | | `lib/site.dart` | URLs, copy, the package version from `../pubspec.yaml`. `SITE_URL` (a `--dart-define`) for previews. | -| `web/landing.css` | The stylesheet: the same `--af-*` tokens as `docs_app/app/global.css`, dark by default. | +| `web/af-tokens.css` | The `--af-*` color tokens, generated from `../docs_app/palette.json` by `npm run palette:generate` there; the docs use the same file. | +| `web/landing.css` | The stylesheet: everything but the colors, dark by default. | | `web/landing.js` | The client: Flutter islands, copy buttons, theme toggle. No framework, no build step. | ## Live demos diff --git a/landing/lib/main.server.dart b/landing/lib/main.server.dart index 8fb3943..59197d2 100644 --- a/landing/lib/main.server.dart +++ b/landing/lib/main.server.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:advanced_forms_landing/app.dart'; import 'package:advanced_forms_landing/highlight.dart'; +import 'package:advanced_forms_landing/palette.generated.dart'; import 'package:advanced_forms_landing/site.dart'; import 'package:jaspr/dom.dart'; import 'package:jaspr/server.dart'; @@ -23,11 +24,11 @@ Future main() async { Document( lang: 'en', title: seoTitle, - meta: const { + meta: { 'description': description, 'author': 'LeanCode', 'robots': 'index, follow, max-image-preview:large', - 'theme-color': '#050505', + 'theme-color': themeColor, 'application-name': siteName, 'generator': 'Jaspr', 'twitter:card': 'summary_large_image', @@ -58,6 +59,8 @@ Future main() async { '?family=Space+Grotesk:wght@400;500;600;700' '&family=JetBrains+Mono:wght@400;600&display=swap', ), + // The colors, generated from docs_app/palette.json; then the rest. + const link(rel: 'stylesheet', href: '/af-tokens.css'), const link(rel: 'stylesheet', href: '/landing.css'), // Applied before paint so a light-theme visitor never sees a dark flash. const script( diff --git a/landing/lib/palette.generated.dart b/landing/lib/palette.generated.dart new file mode 100644 index 0000000..8032f32 --- /dev/null +++ b/landing/lib/palette.generated.dart @@ -0,0 +1,6 @@ +// Generated by ../../docs_app/scripts/palette.mjs from +// ../../docs_app/palette.json — do not edit. Run `npm run palette:generate` +// in docs_app after changing the palette. + +/// The dark theme's page background, for ``. +const themeColor = '#000000'; diff --git a/landing/web/af-tokens.css b/landing/web/af-tokens.css new file mode 100644 index 0000000..14cdbf5 --- /dev/null +++ b/landing/web/af-tokens.css @@ -0,0 +1,82 @@ +/* Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do not edit. Run `npm run palette:generate` in docs_app after changing the palette. */ + +:root { + /* Page background. */ + --af-bg: #ffffff; + + /* Section background: footer, code panels. */ + --af-bg-2: #f7f7f5; + + /* Cards and frames. */ + --af-surface: #ffffff; + + /* Bars and chips on a surface. */ + --af-surface-2: #f2f2ef; + + /* The hairline. */ + --af-border: #e5e5e5; + + /* The stronger hairline, for controls. */ + --af-border-2: #cfcfcb; + + /* Headings and primary text. */ + --af-text: #000000; + + /* Body copy. */ + --af-text-2: #3f3f3c; + + /* Captions and metadata. */ + --af-muted: #757575; + + /* The CTA yellow, as a fill. */ + --af-accent: #f0ff00; + + /* Text on the accent. */ + --af-accent-ink: #000000; + + /* The accent as text. */ + --af-accent-text: #606a00; + + /* The primary button's hover fill. */ + --af-accent-hover: #f5ff4d; + + /* The accent as a wash behind icons, pills and glows. */ + --af-accent-soft: rgba(240, 255, 0, 0.45); + + /* The docs' primary action fill. */ + --af-primary: #000000; + + /* Text on the primary fill. */ + --af-primary-ink: #f0ff00; + + /* The scrim behind dialogs. */ + --af-overlay: rgba(0, 0, 0, 0.3); + + /* Error signal. */ + --af-danger: #be2119; + + /* Success signal. */ + --af-ok: #527b28; +} + +.dark { + --af-bg: #000000; + --af-bg-2: #0a0a08; + --af-surface: #151513; + --af-surface-2: #1d1d1a; + --af-border: rgba(255, 255, 255, 0.12); + --af-border-2: rgba(255, 255, 255, 0.2); + --af-text: #ffffff; + --af-text-2: #d8d8d4; + --af-muted: #a3a3a0; + --af-accent: #f0ff00; + --af-accent-ink: #000000; + --af-accent-text: #f0ff00; + --af-accent-hover: #f5ff4d; + --af-accent-soft: rgba(240, 255, 0, 0.06); + --af-primary: #f0ff00; + --af-primary-ink: #000000; + --af-overlay: rgba(0, 0, 0, 0.6); + --af-danger: #e64239; + --af-ok: #80c340; +} diff --git a/landing/web/landing-icon.svg b/landing/web/landing-icon.svg index ba21bea..8c45ed3 100644 --- a/landing/web/landing-icon.svg +++ b/landing/web/landing-icon.svg @@ -2,6 +2,6 @@ - - + + diff --git a/landing/web/landing.css b/landing/web/landing.css index 075031d..47185d0 100644 --- a/landing/web/landing.css +++ b/landing/web/landing.css @@ -3,32 +3,17 @@ * model: Claude Fable 5.1 * harness: Claude Code * - * The advanced_forms homepage — the LeanCode design system as used on - * ciach.leancode.co: near-black surfaces, one lime accent, Space Grotesk and - * JetBrains Mono. Dark is the design; light is a paper variant where the - * accent is a fill with dark ink on it. The docs app under /docs uses the same - * tokens, so the two read as one site. + * The advanced_forms homepage — the LeanCode design system: black surfaces, + * one CTA yellow, Space Grotesk and JetBrains Mono. Dark is the design; light + * is a paper variant where the accent is a fill with black ink on it. The + * colors are the `--af-*` variables of af-tokens.css, generated from + * docs_app/palette.json and shared with the docs app under /docs, so the two + * read as one site; this file holds everything else. */ /* ---------- Tokens ---------- */ :root { - --af-bg: #f6f6f2; - --af-bg-2: #ffffff; - --af-surface: #ffffff; - --af-surface-2: #efefe9; - --af-border: #e2e2dc; - --af-border-2: #cfcfc7; - --af-text: #0b0b0d; - --af-text-2: #4b4b52; - --af-muted: #7d7d84; - --af-accent: #edff2f; - --af-accent-2: #c9dc00; - --af-accent-ink: #0b0c00; - --af-accent-text: #5f6e00; - --af-accent-soft: rgba(237, 255, 47, 0.45); - --af-danger: #d93636; - --af-ok: #1f9d55; --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.25); --af-radius: 14px; --af-header-h: 3.5rem; @@ -51,19 +36,6 @@ } .dark { - --af-bg: #050505; - --af-bg-2: #0b0b0d; - --af-surface: #101013; - --af-surface-2: #16161b; - --af-border: #23232b; - --af-border-2: #33333e; - --af-text: #f4f4f1; - --af-text-2: #b7b7b3; - --af-muted: #7d7d84; - --af-accent-text: #edff2f; - --af-accent-soft: rgba(237, 255, 47, 0.12); - --af-danger: #ff5d5d; - --af-ok: #58e08a; --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.9); /* Code tokens: Material Palenight, the palette ciach uses. */ @@ -339,7 +311,7 @@ h3 code { font-size: 0.8rem; font-weight: 500; color: var(--af-text-2); - background: rgba(127, 127, 127, 0.04); + background: color-mix(in srgb, var(--af-text) 4%, transparent); white-space: nowrap; } @@ -350,7 +322,7 @@ h3 code { } a.af-pill-accent:hover { - border-color: var(--af-accent-2); + border-color: var(--af-accent); } .af-button { @@ -382,7 +354,7 @@ a.af-pill-accent:hover { } .af-button-primary:hover { - background: #f6ff6b; + background: var(--af-accent-hover); } .af-button-secondary { @@ -392,7 +364,7 @@ a.af-pill-accent:hover { } .af-button-secondary:hover { - border-color: var(--af-accent-2); + border-color: var(--af-accent); } .af-card { @@ -431,18 +403,26 @@ a.af-pill-accent:hover { inset: 0; z-index: -1; background: - radial-gradient(55% 45% at 72% 18%, rgba(237, 255, 47, 0.55), transparent 65%), - radial-gradient(40% 40% at 10% 90%, rgba(237, 255, 47, 0.25), transparent 60%), - repeating-linear-gradient(-58deg, transparent 0 148px, rgba(11, 11, 13, 0.06) 148px 149px); + radial-gradient(55% 45% at 72% 18%, color-mix(in srgb, var(--af-accent) 55%, transparent), transparent 65%), + radial-gradient(40% 40% at 10% 90%, color-mix(in srgb, var(--af-accent) 25%, transparent), transparent 60%), + repeating-linear-gradient( + -58deg, + transparent 0 148px, + color-mix(in srgb, var(--af-text) 6%, transparent) 148px 149px + ); mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); } .dark .af-hero-bg { background: - radial-gradient(55% 45% at 72% 18%, rgba(237, 255, 47, 0.16), transparent 65%), - radial-gradient(40% 40% at 10% 90%, rgba(237, 255, 47, 0.07), transparent 60%), - repeating-linear-gradient(-58deg, transparent 0 148px, rgba(237, 255, 47, 0.09) 148px 149px); + radial-gradient(55% 45% at 72% 18%, color-mix(in srgb, var(--af-accent) 16%, transparent), transparent 65%), + radial-gradient(40% 40% at 10% 90%, color-mix(in srgb, var(--af-accent) 7%, transparent), transparent 60%), + repeating-linear-gradient( + -58deg, + transparent 0 148px, + color-mix(in srgb, var(--af-accent) 9%, transparent) 148px 149px + ); } .af-hero-grid { @@ -581,7 +561,7 @@ a.af-pill-accent:hover { .af-copy-button:hover { color: var(--af-text); - border-color: var(--af-accent-2); + border-color: var(--af-accent); } .af-copy-done { From 9bd3648d9ee9d41d6bdfa70338d53a00dcc9e3ec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 15:46:51 +0000 Subject: [PATCH 02/19] Landing: move the stylesheet into @css getters next to the components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit web/landing.css held every rule for the page in one 1,350-line file, and main.server.dart read, minified and inlined it at render time. The styles now live in Dart, the way the ciach website does it: each component declares the rules for the classes it renders in a `@css static List get styles` next to its `build`, and lib/styles.dart holds what is not any one component's — the @font-face rules, the --af-* tokens for both themes, the reset, the container and skip-link utilities and the reduced-motion rule. The token colours for highlighted code sit in highlight.dart, next to the code that emits the classes. jaspr_builder bundles the getters into lib/main.server.options.dart, and the entrypoint passes `defaultServerOptions`, so Jaspr renders them as the one `'); - } - - /// Drops comments and the whitespace the grammar does not need. Strings in - /// the sheet are font names and URLs, which a single space cannot harm. - static String _minify(String css) => css - .replaceAll(RegExp(r'/\*[\s\S]*?\*/'), '') - .replaceAll(RegExp(r'\s+'), ' ') - .replaceAllMapped(RegExp(r'\s*([{};,>])\s*'), (match) => match[1]!) - .replaceAll(RegExp(r':\s+'), ':') - .trim(); -} - Map _structuredData(String version) { const publisher = { '@type': 'Organization', diff --git a/landing/lib/styles.dart b/landing/lib/styles.dart new file mode 100644 index 0000000..7a9c731 --- /dev/null +++ b/landing/lib/styles.dart @@ -0,0 +1,310 @@ +/// Design tokens and the site-wide rules: the fonts, the reset, typography and +/// shared utilities. Everything else lives next to the component it styles, in +/// `@css` getters; Jaspr collects all of them into one stylesheet, global rules +/// first, and inlines it into the page's ``. +/// +/// The LeanCode design system as used on ciach.leancode.co: near-black +/// surfaces, one lime accent, Space Grotesk and JetBrains Mono. Dark is the +/// design; light is a paper variant where the accent is a fill with dark ink on +/// it. The docs app under /docs uses the same `--af-*` tokens (see +/// `docs_app/app/global.css`), so the two read as one site. +library; + +import 'package:jaspr/dom.dart'; + +// ---------- Tokens ---------- +// +// The palette and metrics are CSS custom properties: light on `:root`, dark +// where `` carries the `dark` class. The values below are the variables; +// the definitions sit in [styles]. + +const bgColor = Color.variable('--af-bg'); +const bg2Color = Color.variable('--af-bg-2'); +const surfaceColor = Color.variable('--af-surface'); +const surface2Color = Color.variable('--af-surface-2'); +const borderColor = Color.variable('--af-border'); +const border2Color = Color.variable('--af-border-2'); +const textColor = Color.variable('--af-text'); +const text2Color = Color.variable('--af-text-2'); +const mutedColor = Color.variable('--af-muted'); +const accentColor = Color.variable('--af-accent'); +const accent2Color = Color.variable('--af-accent-2'); +const accentInkColor = Color.variable('--af-accent-ink'); + +/// The accent as text: the accent itself on dark, a readable olive on light. +const accentTextColor = Color.variable('--af-accent-text'); +const accentSoftColor = Color.variable('--af-accent-soft'); +const dangerColor = Color.variable('--af-danger'); +const okColor = Color.variable('--af-ok'); + +const fontSans = FontFamily.variable('--font-sans'); +const fontMono = FontFamily.variable('--font-mono'); + +const radius = Unit.variable('--af-radius'); +const headerHeight = Unit.variable('--af-header-h'); + +/// `var(--af-shadow)`, for the `box-shadow` slot of a `raw` map: `BoxShadow` +/// has no variable form. +const shadow = 'var(--af-shadow)'; + +/// Marks a link inside running text by more than its colour: underlined in +/// the border colour, a little below the baseline. +const underlinedLink = { + 'text-decoration': 'underline', + 'text-decoration-color': 'var(--af-border-2)', + 'text-underline-offset': '3px', +}; + +/// A 1px solid border in [color]. +Border hairline(Color color) => Border.all(color: color, width: 1.px); + +/// One side of a [hairline], for `Border.only`. +BorderSide hairlineSide(Color color) => BorderSide(color: color, width: 1.px); + +/// `color-mix(in srgb, %, )`: [color] at [percent] +/// opacity over [with], which defaults to transparent. Jaspr has no typed form, +/// so it goes into `raw` maps. +String colorMix(Color color, int percent, {Color? with_}) => + 'color-mix(in srgb, ${color.value} $percent%, ' + '${with_?.value ?? 'transparent'})'; + +/// Everything the page needs before any component draws: fonts, tokens, the +/// reset, utilities and the motion preference. +@css +List get styles => [ + ..._fonts, + ..._tokens, + ..._reset, + ..._utilities, + ..._motion, +]; + +// ---------- Fonts ---------- + +const _latin = + 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, ' + 'U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, ' + 'U+2212, U+2215, U+FEFF, U+FFFD'; +const _latinExt = + 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, ' + 'U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, ' + 'U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF'; + +/// The two variable fonts, self-hosted from `web/fonts/` (SIL Open Font +/// License, the texts sit alongside the files), so no third-party stylesheet +/// blocks the first paint. Latin and Latin Extended subsets cover the site's +/// text; `swap` shows the fallback stack until they arrive, and the `` +/// preloads the Latin two. +List get _fonts => [ + _fontFace('Space Grotesk', '300 700', 'space-grotesk-latin', _latin), + _fontFace('Space Grotesk', '300 700', 'space-grotesk-latin-ext', _latinExt), + _fontFace('JetBrains Mono', '100 800', 'jetbrains-mono-latin', _latin), + _fontFace('JetBrains Mono', '100 800', 'jetbrains-mono-latin-ext', _latinExt), +]; + +/// One `@font-face`. Jaspr's `css.fontFace` knows neither weight ranges, +/// `font-display` nor `unicode-range`, so the at-rule is spelled out: `css()` +/// takes any selector text, and an at-rule with a declaration block renders +/// the same way a rule does. +StyleRule _fontFace(String family, String weights, String file, String range) => + css('@font-face').styles( + raw: { + 'font-family': '"$family"', + 'font-style': 'normal', + 'font-weight': weights, + 'font-display': 'swap', + 'src': 'url("/fonts/$file.woff2") format("woff2")', + 'unicode-range': range, + }, + ); + +// ---------- Tokens ---------- + +List get _tokens => [ + css(':root').styles( + raw: { + '--af-bg': '#f6f6f2', + '--af-bg-2': '#ffffff', + '--af-surface': '#ffffff', + '--af-surface-2': '#efefe9', + '--af-border': '#e2e2dc', + '--af-border-2': '#cfcfc7', + '--af-text': '#0b0b0d', + '--af-text-2': '#4b4b52', + '--af-muted': '#66666d', + '--af-accent': '#edff2f', + '--af-accent-2': '#c9dc00', + '--af-accent-ink': '#0b0c00', + '--af-accent-text': '#5f6e00', + '--af-accent-soft': 'rgba(237, 255, 47, 0.45)', + '--af-danger': '#d93636', + '--af-ok': '#15773f', + '--af-shadow': '0 24px 60px -32px rgba(0, 0, 0, 0.25)', + '--af-radius': '14px', + '--af-header-h': '3.5rem', + '--af-container': '1160px', + '--font-sans': + '"Space Grotesk", ui-sans-serif, system-ui, -apple-system, ' + '"Segoe UI", Roboto, sans-serif', + '--font-mono': + '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, ' + 'monospace', + // Code tokens: GitHub Light. + '--tk-keyword': '#cf222e', + '--tk-type': '#953800', + '--tk-string': '#0a3069', + '--tk-number': '#0550ae', + '--tk-comment': '#656d76', + '--tk-annotation': '#8250df', + '--tk-function': '#8250df', + '--tk-operator': '#0550ae', + 'color-scheme': 'light', + }, + ), + css('.dark').styles( + raw: { + '--af-bg': '#050505', + '--af-bg-2': '#0b0b0d', + '--af-surface': '#101013', + '--af-surface-2': '#16161b', + '--af-border': '#23232b', + '--af-border-2': '#33333e', + '--af-text': '#f4f4f1', + '--af-text-2': '#b7b7b3', + '--af-muted': '#84848d', + '--af-accent-text': '#edff2f', + '--af-accent-soft': 'rgba(237, 255, 47, 0.12)', + '--af-danger': '#ff5d5d', + '--af-ok': '#58e08a', + '--af-shadow': '0 24px 60px -32px rgba(0, 0, 0, 0.9)', + // Code tokens: Material Palenight, the palette ciach uses. + '--tk-keyword': '#c792ea', + '--tk-type': '#ffcb6b', + '--tk-string': '#c3e88d', + '--tk-number': '#f78c6c', + '--tk-comment': '#767eaa', + '--tk-annotation': '#ffcb6b', + '--tk-function': '#82aaff', + '--tk-operator': '#89ddff', + 'color-scheme': 'dark', + }, + ), +]; + +// ---------- Base ---------- + +List get _reset => [ + css('*, *::before, *::after').styles(boxSizing: .borderBox), + css('html').styles( + raw: { + 'scroll-behavior': 'smooth', + 'scroll-padding-top': 'calc(var(--af-header-h) + 16px)', + '-webkit-text-size-adjust': '100%', + 'scrollbar-gutter': 'stable', + }, + ), + css('body').styles( + margin: .zero, + color: textColor, + fontFamily: fontSans, + fontSize: 1.0625.rem, + lineHeight: const .expression('1.6'), + backgroundColor: bgColor, + raw: { + '-webkit-font-smoothing': 'antialiased', + 'text-rendering': 'optimizeLegibility', + }, + ), + css('h1, h2, h3').styles( + margin: .zero, + fontWeight: .w600, + letterSpacing: (-0.02).em, + lineHeight: const .expression('1.15'), + raw: {'text-wrap': 'balance'}, + ), + css('p').styles(margin: .zero), + css('a').styles(color: .inherit, textDecoration: .none), + css('ul, ol').styles(padding: .zero, margin: .zero, listStyle: .none), + css('button').styles( + padding: .zero, + cursor: .pointer, + color: .inherit, + raw: {'font': 'inherit', 'background': 'none', 'border': '0'}, + ), + css('svg').styles( + display: .inlineBlock, + flex: .none, + raw: {'vertical-align': 'middle'}, + ), + css( + 'code, pre, kbd', + ).styles(fontFamily: fontMono, raw: {'font-feature-settings': '"liga" 0'}), + // Inline code chips. + css('p code, li code, h3 code').styles( + padding: .symmetric(vertical: 0.1.em, horizontal: 0.4.em), + border: hairline(borderColor), + radius: .circular(6.px), + color: textColor, + fontSize: 0.85.em, + whiteSpace: .noWrap, + backgroundColor: surface2Color, + ), + css(':focus-visible').styles( + radius: .circular(4.px), + outline: Outline( + color: accentTextColor, + style: .solid, + width: OutlineWidth(2.px), + offset: 3.px, + ), + ), + css( + '::selection', + ).styles(color: accentInkColor, backgroundColor: accentColor), +]; + +// ---------- Utilities ---------- + +List get _utilities => [ + css('.af-container').styles( + width: const .expression('min(100% - 2.5rem, var(--af-container))'), + raw: {'margin-inline': 'auto'}, + ), + // Grid and flex children may hold wide code samples; let them shrink and + // scroll instead of stretching the page. + css( + '.af-landing > *, .af-hero-grid > *, .af-feature-grid > *, ' + '.af-example-body > *', + ).styles(minWidth: .zero), + css('.af-skip-link').styles( + position: .fixed(top: 12.px, left: 12.px), + zIndex: const ZIndex(100), + padding: .symmetric(vertical: 0.6.rem, horizontal: 1.rem), + radius: .circular(8.px), + transition: Transition('transform', duration: 200.ms, curve: .ease), + transform: .translate(y: (-200).percent), + color: accentInkColor, + fontWeight: .w600, + backgroundColor: accentColor, + ), + css('.af-skip-link:focus').styles(transform: const .translate(y: .zero)), + css('.af-accent').styles(color: accentTextColor), +]; + +// ---------- Motion ---------- + +List get _motion => [ + // The live dot in the hero and the "attaching" status of an example frame. + css.keyframes('af-pulse', {'50%': const Styles(opacity: 0.3)}), + // Motion is a garnish here; readers who asked for less of it get none. + css.media(const MediaQuery.raw('(prefers-reduced-motion: reduce)'), [ + css('html').styles(raw: {'scroll-behavior': 'auto'}), + css('*, *::before, *::after').styles( + raw: { + 'animation-duration': '0.01ms !important', + 'animation-iteration-count': '1 !important', + 'transition-duration': '0.01ms !important', + }, + ), + ]), +]; diff --git a/landing/web/landing.css b/landing/web/landing.css deleted file mode 100644 index 27b79c8..0000000 --- a/landing/web/landing.css +++ /dev/null @@ -1,1351 +0,0 @@ -/* - * AI-Provenance: - * model: Claude Fable 5.1 - * harness: Claude Code - * - * The advanced_forms homepage — the LeanCode design system as used on - * ciach.leancode.co: near-black surfaces, one lime accent, Space Grotesk and - * JetBrains Mono. Dark is the design; light is a paper variant where the - * accent is a fill with dark ink on it. The docs app under /docs uses the same - * tokens, so the two read as one site. - */ - -/* ---------- Fonts ---------- */ - -/* The two variable fonts, self-hosted from fonts/ (SIL Open Font License, the - texts sit alongside the files), so no third-party stylesheet blocks the first - paint. Latin and Latin Extended subsets cover the site's text; `swap` shows - the fallback stack until they arrive, and the preloads the Latin two. */ -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 300 700; - font-display: swap; - src: url("/fonts/space-grotesk-latin.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, - U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 300 700; - font-display: swap; - src: url("/fonts/space-grotesk-latin-ext.woff2") format("woff2"); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, - U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} - -@font-face { - font-family: "JetBrains Mono"; - font-style: normal; - font-weight: 100 800; - font-display: swap; - src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, - U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -@font-face { - font-family: "JetBrains Mono"; - font-style: normal; - font-weight: 100 800; - font-display: swap; - src: url("/fonts/jetbrains-mono-latin-ext.woff2") format("woff2"); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, - U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} - -/* ---------- Tokens ---------- */ - -:root { - --af-bg: #f6f6f2; - --af-bg-2: #ffffff; - --af-surface: #ffffff; - --af-surface-2: #efefe9; - --af-border: #e2e2dc; - --af-border-2: #cfcfc7; - --af-text: #0b0b0d; - --af-text-2: #4b4b52; - --af-muted: #66666d; - --af-accent: #edff2f; - --af-accent-2: #c9dc00; - --af-accent-ink: #0b0c00; - --af-accent-text: #5f6e00; - --af-accent-soft: rgba(237, 255, 47, 0.45); - --af-danger: #d93636; - --af-ok: #15773f; - --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.25); - --af-radius: 14px; - --af-header-h: 3.5rem; - --af-container: 1160px; - - --font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - - /* Code tokens: GitHub Light. */ - --tk-keyword: #cf222e; - --tk-type: #953800; - --tk-string: #0a3069; - --tk-number: #0550ae; - --tk-comment: #656d76; - --tk-annotation: #8250df; - --tk-function: #8250df; - --tk-operator: #0550ae; - - color-scheme: light; -} - -.dark { - --af-bg: #050505; - --af-bg-2: #0b0b0d; - --af-surface: #101013; - --af-surface-2: #16161b; - --af-border: #23232b; - --af-border-2: #33333e; - --af-text: #f4f4f1; - --af-text-2: #b7b7b3; - --af-muted: #84848d; - --af-accent-text: #edff2f; - --af-accent-soft: rgba(237, 255, 47, 0.12); - --af-danger: #ff5d5d; - --af-ok: #58e08a; - --af-shadow: 0 24px 60px -32px rgba(0, 0, 0, 0.9); - - /* Code tokens: Material Palenight, the palette ciach uses. */ - --tk-keyword: #c792ea; - --tk-type: #ffcb6b; - --tk-string: #c3e88d; - --tk-number: #f78c6c; - --tk-comment: #767eaa; - --tk-annotation: #ffcb6b; - --tk-function: #82aaff; - --tk-operator: #89ddff; - - color-scheme: dark; -} - -/* ---------- Base ---------- */ - -*, -*::before, -*::after { - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; - scroll-padding-top: calc(var(--af-header-h) + 16px); - -webkit-text-size-adjust: 100%; - scrollbar-gutter: stable; -} - -body { - margin: 0; - background: var(--af-bg); - color: var(--af-text); - font-family: var(--font-sans); - font-size: 1.0625rem; - line-height: 1.6; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} - -h1, -h2, -h3 { - margin: 0; - font-weight: 600; - letter-spacing: -0.02em; - line-height: 1.15; - text-wrap: balance; -} - -p { - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -ul, -ol { - margin: 0; - padding: 0; - list-style: none; -} - -button { - font: inherit; - color: inherit; - background: none; - border: 0; - padding: 0; - cursor: pointer; -} - -svg { - display: inline-block; - vertical-align: middle; - flex: none; -} - -code, -pre, -kbd { - font-family: var(--font-mono); - font-feature-settings: "liga" 0; -} - -p code, -li code, -h3 code { - padding: 0.1em 0.4em; - border: 1px solid var(--af-border); - border-radius: 6px; - background: var(--af-surface-2); - font-size: 0.85em; - color: var(--af-text); - white-space: nowrap; -} - -:focus-visible { - outline: 2px solid var(--af-accent-text); - outline-offset: 3px; - border-radius: 4px; -} - -::selection { - background: var(--af-accent); - color: var(--af-accent-ink); -} - -/* Theme-specific children: the logo has one file per theme. */ -.af-theme-dark { - display: none; -} - -.dark .af-theme-light { - display: none; -} - -.dark .af-theme-dark { - display: inline; -} - -.af-container { - width: min(100% - 2.5rem, var(--af-container)); - margin-inline: auto; -} - -.af-landing > *, -.af-hero-grid > *, -.af-feature-grid > *, -.af-example-body > * { - min-width: 0; -} - -.af-skip-link { - position: fixed; - top: 12px; - left: 12px; - z-index: 100; - padding: 0.6rem 1rem; - border-radius: 8px; - background: var(--af-accent); - color: var(--af-accent-ink); - font-weight: 600; - transform: translateY(-200%); - transition: transform 0.2s ease; -} - -.af-skip-link:focus { - transform: translateY(0); -} - -.af-accent { - color: var(--af-accent-text); -} - -/* ---------- Header ---------- */ - -.af-header { - position: sticky; - top: 0; - z-index: 50; - height: var(--af-header-h); - border-bottom: 1px solid var(--af-border); - background: color-mix(in srgb, var(--af-bg) 80%, transparent); - backdrop-filter: saturate(140%) blur(14px); - -webkit-backdrop-filter: saturate(140%) blur(14px); -} - -.af-nav { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - height: 100%; -} - -.af-brand { - display: inline-flex; - align-items: center; -} - -.af-logo { - display: inline-flex; - align-items: center; -} - -.af-logo img { - height: 34px; - width: auto; - vertical-align: middle; -} - -.af-footer-brand .af-logo img { - height: 44px; -} - -.af-nav-links { - display: flex; - align-items: center; - gap: 0.25rem; -} - -.af-nav-links a, -.af-theme-toggle { - display: inline-flex; - align-items: center; - gap: 0.45rem; - padding: 0.5rem 0.85rem; - border-radius: 999px; - font-size: 0.95rem; - font-weight: 500; - color: var(--af-text-2); - transition: - background-color 0.15s ease, - color 0.15s ease; -} - -.af-nav-links a:hover, -.af-theme-toggle:hover { - background: var(--af-surface-2); - color: var(--af-text); -} - -.af-theme-toggle { - padding: 0.5rem; -} - -.af-theme-moon { - display: none; -} - -.dark .af-theme-sun { - display: none; -} - -.dark .af-theme-moon { - display: inline-flex; -} - -/* ---------- Shared vocabulary ---------- */ - -.af-eyebrow { - display: inline-flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 1rem; - font-family: var(--font-mono); - font-size: 0.78rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--af-accent-text); -} - -.af-eyebrow::before { - content: ""; - width: 1.5rem; - height: 2px; - background: var(--af-accent); -} - -.af-pill { - display: inline-flex; - align-items: center; - gap: 0.35rem; - padding: 0.3rem 0.7rem; - border: 1px solid var(--af-border-2); - border-radius: 999px; - font-size: 0.8rem; - font-weight: 500; - color: var(--af-text-2); - background: rgba(127, 127, 127, 0.04); - white-space: nowrap; -} - -.af-pill-accent { - border-color: color-mix(in srgb, var(--af-accent) 45%, var(--af-border-2)); - background: var(--af-accent-soft); - color: var(--af-accent-text); -} - -a.af-pill-accent:hover { - border-color: var(--af-accent-2); -} - -.af-button { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.2rem; - border: 1px solid transparent; - border-radius: 999px; - font-weight: 600; - font-size: 0.95rem; - line-height: 1; - white-space: nowrap; - cursor: pointer; - transition: - transform 0.15s ease, - background-color 0.15s ease, - border-color 0.15s ease, - color 0.15s ease; -} - -.af-button:hover { - transform: translateY(-1px); -} - -.af-button-primary { - background: var(--af-accent); - color: var(--af-accent-ink); -} - -.af-button-primary:hover { - background: #f6ff6b; -} - -.af-button-secondary { - border-color: var(--af-border-2); - background: var(--af-surface); - color: var(--af-text); -} - -.af-button-secondary:hover { - border-color: var(--af-accent-2); -} - -.af-card { - padding: 1.5rem; - border: 1px solid var(--af-border); - border-radius: var(--af-radius); - background: var(--af-surface); - transition: - border-color 0.2s ease, - transform 0.2s ease; -} - -.af-card h3 { - margin: 0 0 0.5rem; - font-size: 1.15rem; - font-weight: 600; -} - -.af-card p { - margin: 0; - font-size: 0.95rem; - color: var(--af-text-2); -} - -/* ---------- Hero ---------- */ - -.af-hero { - position: relative; - padding: clamp(3rem, 8vw, 6rem) 0 clamp(3rem, 6vw, 4.5rem); - overflow: hidden; - isolation: isolate; -} - -.af-hero-bg { - position: absolute; - inset: 0; - z-index: -1; - background: - radial-gradient(55% 45% at 72% 18%, rgba(237, 255, 47, 0.55), transparent 65%), - radial-gradient(40% 40% at 10% 90%, rgba(237, 255, 47, 0.25), transparent 60%), - repeating-linear-gradient(-58deg, transparent 0 148px, rgba(11, 11, 13, 0.06) 148px 149px); - mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); -} - -.dark .af-hero-bg { - background: - radial-gradient(55% 45% at 72% 18%, rgba(237, 255, 47, 0.16), transparent 65%), - radial-gradient(40% 40% at 10% 90%, rgba(237, 255, 47, 0.07), transparent 60%), - repeating-linear-gradient(-58deg, transparent 0 148px, rgba(237, 255, 47, 0.09) 148px 149px); -} - -.af-hero-grid { - display: grid; - gap: 3rem; - align-items: center; -} - -.af-hero-badges { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1.5rem; -} - -.af-hero h1 { - font-size: clamp(2.4rem, 5.2vw, 4rem); - font-weight: 700; - line-height: 1.02; - letter-spacing: -0.035em; -} - -:root:not(.dark) .af-hero h1 .af-accent { - color: var(--af-text); - background: linear-gradient(transparent 55%, var(--af-accent) 55%); - padding: 0 0.1em; - margin: 0 -0.1em; -} - -.af-hero-lead { - max-width: 38rem; - margin-top: 1.5rem; - font-size: clamp(1.1rem, 1.6vw, 1.3rem); - color: var(--af-text-2); -} - -.af-install { - margin-top: 2rem; -} - -.af-install-command { - display: flex; - align-items: center; - gap: 0.75rem; - max-width: 34rem; - padding: 0.5rem 0.5rem 0.5rem 1rem; - border: 1px solid var(--af-border-2); - border-radius: var(--af-radius); - background: var(--af-surface); - box-shadow: var(--af-shadow); -} - -.af-install-command code { - flex: 1; - min-width: 0; - font-size: 0.95rem; - overflow-x: auto; - white-space: nowrap; -} - -.af-prompt { - color: var(--af-accent-text); - font-family: var(--font-mono); - font-weight: 600; - user-select: none; -} - -.af-install-alt { - margin-top: 0.6rem; - font-size: 0.85rem; - color: var(--af-muted); -} - -.af-install-alt a { - color: var(--af-text-2); - text-decoration: underline; - text-decoration-color: var(--af-border-2); - text-underline-offset: 3px; -} - -.af-hero-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-top: 1.75rem; -} - -.af-hero-actions.af-center { - justify-content: center; -} - -.af-demo-label { - display: flex; - align-items: center; - gap: 0.6rem; - margin-bottom: 0.75rem; - font-family: var(--font-mono); - font-size: 0.78rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--af-accent-text); -} - -.af-demo-dot { - flex: none; - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--af-ok); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--af-ok) 25%, transparent); - animation: af-pulse 1.6s ease-in-out infinite; -} - -@keyframes af-pulse { - 50% { - opacity: 0.3; - } -} - -/* Copy buttons (install command, source files). */ -.af-copy-button { - display: inline-flex; - align-items: center; - gap: 0.4rem; - padding: 0.45rem 0.75rem; - border: 1px solid var(--af-border-2); - border-radius: 8px; - background: var(--af-surface); - color: var(--af-text-2); - font: 600 0.78rem/1 var(--font-sans); - transition: - color 0.15s ease, - border-color 0.15s ease; -} - -.af-copy-button:hover { - color: var(--af-text); - border-color: var(--af-accent-2); -} - -.af-copy-done { - display: none; - color: var(--af-ok); -} - -[data-copied="true"] { - border-color: var(--af-ok) !important; - color: var(--af-ok) !important; -} - -[data-copied="true"] .af-copy-idle { - display: none; -} - -[data-copied="true"] .af-copy-done { - display: inline-flex; -} - -/* ---------- Sections ---------- */ - -.af-section { - padding: clamp(3.5rem, 7vw, 6rem) 0; - border-top: 1px solid var(--af-border); -} - -.af-section-head { - max-width: 44rem; - margin-bottom: 2.5rem; -} - -.af-section h2 { - font-size: clamp(1.9rem, 3.6vw, 2.75rem); -} - -.af-lead { - margin-top: 1rem; - font-size: 1.125rem; - color: var(--af-text-2); -} - -.af-section-more { - margin-top: 1.5rem; - font-weight: 500; -} - -.af-section-more a { - color: var(--af-accent-text); -} - -.af-section-more a:hover { - text-decoration: underline; -} - -.af-section-demo { - margin-top: 2.5rem; -} - -.af-checklist { - display: grid; - gap: 0.75rem; - max-width: 52rem; - margin-top: 2rem; -} - -.af-checklist li { - position: relative; - padding-left: 1.6rem; - color: var(--af-text-2); -} - -.af-checklist li strong { - color: var(--af-text); - font-weight: 600; -} - -.af-checklist li::before { - content: ""; - position: absolute; - top: 0.7em; - left: 0; - width: 0.9rem; - height: 2px; - background: var(--af-accent); -} - -.af-feature-grid { - display: grid; - gap: 1rem; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); -} - -.af-feature { - display: flex; - flex-direction: column; - gap: 0.6rem; -} - -.af-feature:hover, -.af-mode:hover { - border-color: var(--af-border-2); - transform: translateY(-2px); -} - -.af-feature-icon { - display: inline-grid; - place-items: center; - width: 42px; - height: 42px; - margin-bottom: 0.5rem; - border: 1px solid color-mix(in srgb, var(--af-accent) 35%, var(--af-border)); - border-radius: 12px; - background: var(--af-accent-soft); - color: var(--af-accent-text); -} - -.af-feature p { - flex: 1; -} - -.af-rules { - display: grid; - gap: 1rem; - counter-reset: rule; -} - -.af-rule { - position: relative; - padding-left: 3.25rem; -} - -.af-rule::before { - counter-increment: rule; - content: counter(rule, decimal-leading-zero); - position: absolute; - left: 0; - top: 0.15rem; - font-family: var(--font-mono); - font-size: 0.85rem; - font-weight: 600; - color: var(--af-accent-text); -} - -.af-rule h3 { - font-size: 1.1rem; - margin-bottom: 0.35rem; -} - -.af-rule p { - color: var(--af-text-2); - font-size: 0.98rem; -} - -.af-modes { - margin-top: 2.5rem; -} - -.af-mode { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.af-mode-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 0.75rem; -} - -.af-mode h3 { - margin: 0.25rem 0 0; -} - -.af-mode h3 code { - font-size: 0.95rem; - font-weight: 600; -} - -.af-mode-when { - color: var(--af-text) !important; - font-weight: 600; -} - -.af-band { - display: grid; - gap: 2rem; - align-items: center; - padding: 2rem; - border: 1px solid var(--af-border); - border-radius: var(--af-radius); - background: - radial-gradient(60% 80% at 100% 0%, var(--af-accent-soft), transparent 70%), - var(--af-surface); -} - -.af-band > * { - min-width: 0; -} - -.af-band h2 { - font-size: clamp(1.5rem, 2.6vw, 2rem); -} - -.af-band .af-hero-actions { - margin-top: 0; -} - -/* ---------- Footer ---------- */ - -.af-footer { - border-top: 1px solid var(--af-border); - background: var(--af-bg-2); - font-size: 1rem; -} - -.af-cta { - padding: clamp(3.5rem, 7vw, 5.5rem) 0; - border-bottom: 1px solid var(--af-border); - background: - radial-gradient(50% 60% at 50% 100%, var(--af-accent-soft), transparent 70%), - var(--af-bg-2); -} - -.af-cta-inner { - max-width: 40rem; - text-align: center; -} - -.af-cta h2 { - font-size: clamp(1.9rem, 3.6vw, 2.75rem); -} - -.af-cta p { - margin-top: 1rem; - color: var(--af-text-2); - font-size: 1.1rem; -} - -.af-footer-grid { - display: grid; - gap: 2.5rem; - padding: 3.5rem 0; -} - -.af-footer-grid h3 { - margin: 0 0 0.9rem; - font-family: var(--font-mono); - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--af-muted); -} - -.af-footer-grid ul { - display: grid; - gap: 0.5rem; -} - -.af-footer-grid li a { - color: var(--af-text-2); -} - -.af-footer-grid li a:hover { - color: var(--af-accent-text); -} - -.af-footer-brand p { - max-width: 24rem; - margin-top: 1rem; - color: var(--af-text-2); - font-size: 0.95rem; -} - -.af-footer-brand p a { - color: var(--af-text); - text-decoration: underline; - text-decoration-color: var(--af-border-2); - text-underline-offset: 3px; -} - -.af-footer-bottom { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 0.75rem; - padding: 1.5rem 0 2rem; - border-top: 1px solid var(--af-border); - font-size: 0.85rem; - color: var(--af-muted); -} - -.af-footer-bottom a { - color: var(--af-text-2); - text-decoration: underline; - text-decoration-color: var(--af-border-2); - text-underline-offset: 3px; -} - -/* ---------- Live examples ---------- */ - -.af-example { - margin: 0; - border: 1px solid var(--af-border); - border-radius: var(--af-radius); - background: var(--af-surface); - box-shadow: var(--af-shadow); - overflow: hidden; -} - -.af-example-bar { - display: flex; - align-items: center; - gap: 0.75rem; - min-height: 2.6rem; - padding: 0.4rem 0.75rem 0.4rem 0.9rem; - border-bottom: 1px solid var(--af-border); - background: var(--af-surface-2); -} - -.af-dots { - display: inline-flex; - gap: 0.4rem; - flex: none; -} - -.af-dots span { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--af-border-2); -} - -.af-dots span:first-child { - background: var(--af-accent); -} - -.af-example-title { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: center; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--af-muted); -} - -.af-example-status { - display: inline-flex; - align-items: center; - gap: 0.4rem; - flex: none; - font-family: var(--font-mono); - font-size: 0.68rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--af-muted); -} - -.af-example-status::before { - content: ""; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--af-muted); -} - -.af-example-status[data-status="ready"] { - color: var(--af-ok); -} - -.af-example-status[data-status="ready"]::before { - background: var(--af-ok); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--af-ok) 25%, transparent); -} - -.af-example-status[data-status="attaching"]::before { - background: var(--af-accent); - animation: af-pulse 1s ease-in-out infinite; -} - -.af-example-status[data-status="failed"]::before, -.af-example-status[data-status="evicted"]::before { - background: var(--af-danger); -} - -.af-example-stage { - position: relative; - background: var(--af-surface); -} - -.af-example-stage[data-settled="false"] { - min-height: 8rem; -} - -.af-example-notice { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - text-align: center; - font-size: 0.8rem; - color: var(--af-muted); -} - -.af-example-notice button { - display: block; - margin: 0.6rem auto 0; - padding: 0.35rem 0.9rem; - border-radius: 999px; - background: var(--af-accent); - color: var(--af-accent-ink); - font-weight: 600; - font-size: 0.8rem; -} - -.af-example-gate { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: color-mix(in srgb, var(--af-surface) 55%, transparent); - backdrop-filter: blur(1px); -} - -.af-example-gate span { - padding: 0.45rem 1.1rem; - border-radius: 999px; - background: var(--af-accent); - color: var(--af-accent-ink); - font-weight: 600; - font-size: 0.875rem; -} - -.af-example-gate-done { - position: absolute; - top: 0.35rem; - right: 0.35rem; - padding: 0.15rem 0.55rem; - border-radius: 6px; - background: var(--af-surface-2); - color: var(--af-text-2); - font-size: 0.75rem; -} - -.af-example-caption { - padding: 0.75rem 0.9rem; - border-top: 1px solid var(--af-border); - font-size: 0.85rem; - color: var(--af-muted); -} - -.af-example-caption a { - color: var(--af-text-2); - text-decoration: underline; - text-decoration-color: var(--af-border-2); - text-underline-offset: 3px; -} - -/* Source, one file at a time. Radio inputs and CSS switch the panels. */ -.af-example-code { - padding: 0 0.75rem 0.75rem; -} - -.af-tab-input { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - overflow: hidden; - clip: rect(0 0 0 0); - opacity: 0; -} - -.af-example-toolbar { - display: flex; - align-items: center; - gap: 0.25rem; - padding: 0.35rem 0.35rem 0; -} - -.af-example-tabs { - display: flex; - align-items: center; - gap: 0.25rem; - flex: 1; - min-width: 0; - overflow-x: auto; - scrollbar-width: none; -} - -.af-example-tab { - flex: none; - padding: 0.45rem 0.8rem; - border: 1px solid transparent; - border-bottom: 0; - border-radius: 8px 8px 0 0; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--af-muted); - cursor: pointer; - white-space: nowrap; - transition: - color 0.15s ease, - background-color 0.15s ease; -} - -.af-example-tab:hover { - color: var(--af-text); -} - -.af-example-copy { - /* Beside the tab strip, not in it, so it stays put when the strip scrolls. */ - flex: none; - display: inline-grid; - place-items: center; - width: 30px; - height: 30px; - border-radius: 6px; - color: var(--af-muted); - background: var(--af-surface); -} - -.af-example-copy:hover { - color: var(--af-text); - background: var(--af-surface-2); -} - -.af-example-copy .af-copy-idle, -.af-example-copy .af-copy-done { - display: none; -} - -.af-example-copy .af-copy-idle { - display: inline-flex; -} - -.af-example-copy[data-copied="true"] .af-copy-idle { - display: none; -} - -.af-example-copy[data-copied="true"] .af-copy-done { - display: inline-flex; -} - -.af-code-panel { - display: none; - margin-top: -1px; - border: 1px solid var(--af-border); - border-radius: 0 var(--af-radius) var(--af-radius) var(--af-radius); - background: var(--af-bg-2); - overflow: hidden; -} - -.af-code-panel pre { - margin: 0; - padding: 0.9rem 1rem; - max-height: 420px; - overflow: auto; - font-size: 0.8125rem; - line-height: 1.65; - tab-size: 2; - scrollbar-width: thin; - scrollbar-color: var(--af-border-2) transparent; -} - -.af-code-panel code { - display: block; - min-width: max-content; -} - -/* The checked radio picks its tab (the label right after it) and its panel, - for up to eight files. */ -.af-tab-input:checked + .af-example-tab { - color: var(--af-text); - background: var(--af-bg-2); - border-color: var(--af-border); - box-shadow: inset 0 2px 0 var(--af-accent); -} - -.af-example-code:has(.af-tab-input:nth-of-type(1):checked) .af-code-panel:nth-child(1), -.af-example-code:has(.af-tab-input:nth-of-type(2):checked) .af-code-panel:nth-child(2), -.af-example-code:has(.af-tab-input:nth-of-type(3):checked) .af-code-panel:nth-child(3), -.af-example-code:has(.af-tab-input:nth-of-type(4):checked) .af-code-panel:nth-child(4), -.af-example-code:has(.af-tab-input:nth-of-type(5):checked) .af-code-panel:nth-child(5), -.af-example-code:has(.af-tab-input:nth-of-type(6):checked) .af-code-panel:nth-child(6), -.af-example-code:has(.af-tab-input:nth-of-type(7):checked) .af-code-panel:nth-child(7), -.af-example-code:has(.af-tab-input:nth-of-type(8):checked) .af-code-panel:nth-child(8) { - display: block; -} - -/* The radio is clipped away; its label shows the keyboard focus instead. */ -.af-tab-input:focus-visible + .af-example-tab { - outline: 2px solid var(--af-accent-text); - outline-offset: -2px; -} - -/* Code tokens. */ -.tk-keyword { color: var(--tk-keyword); } -.tk-type { color: var(--tk-type); } -.tk-string { color: var(--tk-string); } -.tk-number { color: var(--tk-number); } -.tk-comment { color: var(--tk-comment); } -.tk-doc { color: var(--tk-comment); font-style: italic; } -.tk-annotation { color: var(--tk-annotation); } -.tk-function { color: var(--tk-function); } -.tk-operator { color: var(--tk-operator); } - -/* ---------- Responsive ---------- */ - -@media (min-width: 760px) { - .af-footer-grid { - grid-template-columns: 1fr 1fr; - } - - .af-band { - grid-template-columns: 1.3fr 1fr; - padding: 2.5rem; - } -} - -@media (min-width: 960px) { - .af-rules { - grid-template-columns: repeat(3, 1fr); - gap: 2rem; - } - - .af-example[data-layout="split"] .af-example-body { - display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); - align-items: start; - } - - .af-example[data-layout="split"] .af-example-stage { - order: 2; - position: sticky; - top: calc(var(--af-header-h) + 1.5rem); - border-left: 1px solid var(--af-border); - } - - .af-example[data-layout="split"] .af-example-code { - order: 1; - padding: 0.75rem; - } -} - -@media (min-width: 1000px) { - .af-hero-grid { - grid-template-columns: 1.05fr 1fr; - } - - .af-footer-grid { - grid-template-columns: 1.6fr 1fr 1fr 1.4fr; - } -} - -@media (max-width: 540px) { - .af-nav-links a, - .af-theme-toggle { - padding: 0.5rem 0.55rem; - font-size: 0.9rem; - } - - .af-nav-links { - gap: 0; - } - - /* Examples is reachable from the docs; the row has to fit next to the logo. */ - .af-nav-secondary { - display: none; - } - - .af-logo img { - height: 28px; - } - - /* One row still: the prompt goes, the button shrinks to its icon, and the - command scrolls sideways if a screen is narrower than it. */ - .af-install-command { - gap: 0.5rem; - padding: 0.4rem 0.4rem 0.4rem 0.85rem; - } - - .af-install-command code { - font-size: 0.875rem; - } - - .af-install-command .af-prompt { - display: none; - } - - .af-install-command .af-copy-label { - display: none; - } - - .af-install-command .af-copy-button { - padding: 0.5rem; - } -} - -@media (prefers-reduced-motion: reduce) { - html { - scroll-behavior: auto; - } - - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} From 87dde22997a5a33f91cd5c5468961c1652f83d5b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 16:25:48 +0000 Subject: [PATCH 03/19] Generate the whole palette into Dart, as ciach does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: the landing page's generated Dart held one color, and islandTheme copied its tokens into locals one line at a time. Both Dart consumers now get the same shape as ciach's landing page — an `enum Palette` of the swatches with color, hex and alpha(), and an AfTheme built from it for each theme — in Jaspr's Color for the landing and dart:ui's for the Flutter demos; the landing's theme-color derives from afDark. islandTheme destructures the theme with an object pattern. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- docs_app/README.md | 10 +- .../flutter/lib/support/island_frame.dart | 36 +-- .../lib/support/palette.generated.dart | 160 +++++++++---- docs_app/scripts/palette.mjs | 111 ++++++--- landing/README.md | 1 + landing/lib/palette.generated.dart | 210 +++++++++++++++++- 6 files changed, 435 insertions(+), 93 deletions(-) diff --git a/docs_app/README.md b/docs_app/README.md index f9d25f2..2fd962b 100644 --- a/docs_app/README.md +++ b/docs_app/README.md @@ -139,11 +139,11 @@ The colors are the LeanCode design system — black ground, warm surfaces, one C place, `palette.json`: the swatches as the design system names them, and the light and dark themes as `--af-*` tokens built from them. `npm run palette:generate` writes every copy a consumer needs (`app/af-tokens.css` and the landing page's twin, `lib/palette.generated.ts` for the social cards and the logo, `flutter/lib/support/palette.generated.dart` -for the live demos, the landing page's `theme-color`) and recolors the shapes marked `data-palette` in the logo and icon -SVGs; `npm run palette:check` fails CI when they drift. The stylesheet is hand-written on top of the Fumadocs preset, -with the `--color-fd-*` variables Fumadocs paints with pointed at the tokens. Space Grotesk and JetBrains Mono. Dark is -the design and the default; light is a paper variant where the accent is a fill with black ink on it, the way -leancode.co does it. +for the live demos, `../landing/lib/palette.generated.dart` for the landing page) and recolors the shapes marked +`data-palette` in the logo and icon SVGs; `npm run palette:check` fails CI when they drift. The stylesheet is +hand-written on top of the Fumadocs preset, with the `--color-fd-*` variables Fumadocs paints with pointed at the +tokens. Space Grotesk and JetBrains Mono. Dark is the design and the default; light is a paper variant where the accent +is a fill with black ink on it, the way leancode.co does it. Code blocks and live examples share one window frame: a title bar with three dots (the first one yellow), a mono title, and the content below. The landing page in `../landing/web/landing.css` uses the same tokens and the same frame, so the diff --git a/docs_app/flutter/lib/support/island_frame.dart b/docs_app/flutter/lib/support/island_frame.dart index 5d3b5df..2b1f8cb 100644 --- a/docs_app/flutter/lib/support/island_frame.dart +++ b/docs_app/flutter/lib/support/island_frame.dart @@ -95,28 +95,30 @@ class _IslandFrameState extends State { /// it — a yellow *text* would not be legible on paper. ThemeData islandTheme(Brightness brightness) { final dark = brightness == Brightness.dark; - final palette = dark ? afDark : afLight; - - final accent = palette.accent; - final accentInk = palette.accentInk; - final ink = palette.text; - final ink2 = palette.text2; - final muted = palette.muted; - final surface = palette.surface; - final surface2 = palette.surface2; - final border = palette.border2; - final borderSoft = palette.border; - final danger = palette.danger; - final ok = palette.ok; - - final primary = palette.primary; - final onPrimary = palette.primaryInk; + final AfTheme( + :accent, + :accentInk, + :accentSoft, + text: ink, + text2: ink2, + :muted, + :surface, + :surface2, + border2: border, + border: borderSoft, + :danger, + :ok, + :primary, + primaryInk: onPrimary, + ) = dark + ? afDark + : afLight; final scheme = ColorScheme( brightness: brightness, primary: primary, onPrimary: onPrimary, - primaryContainer: palette.accentSoft, + primaryContainer: accentSoft, onPrimaryContainer: ink, secondary: accent, onSecondary: accentInk, diff --git a/docs_app/flutter/lib/support/palette.generated.dart b/docs_app/flutter/lib/support/palette.generated.dart index be14aaa..7f47af6 100644 --- a/docs_app/flutter/lib/support/palette.generated.dart +++ b/docs_app/flutter/lib/support/palette.generated.dart @@ -4,10 +4,86 @@ import 'dart:ui' show Color; +/// The LeanCode swatches, as `palette.json` names them, each a packed +/// `0xRRGGBB`. +enum Palette { + /// Primary surface. LeanCode is black-first; yellow is the exception. + black(0x000000), + + /// Text and headings on black; the light document surface. + white(0xffffff), + + /// Call to action and emphasis only. Text on it is always black. + ctaYellow(0xf0ff00), + + /// Body copy on dark surfaces. + bodyGray(0xd8d8d4), + + /// Secondary text, captions, metadata on dark. + mutedGray(0xa3a3a0), + + /// Secondary text, captions, metadata on light: the design system's #757575, + /// a step darker so it also reads AA on paper. + mutedOnLight(0x6f6f6f), + + /// Cards and panels on black, warm-tinted. + surface(0x151513), + + /// A second surface step on black. + surface2(0x1d1d1a), + + /// The 1px hairline on white. + lineOnLight(0xe5e5e5), + + /// Error signal, sparingly. + error(0xe64239), + + /// Success signal, sparingly. + success(0x80c340), + + /// Site tint: a section background a step above black. + nearBlack(0x0a0a08), + + /// Site tint: a section background a step below white. + paper(0xf7f7f5), + + /// Site tint: a second surface step on white. + paper2(0xf2f2ef), + + /// Site tint: body copy on light surfaces. + bodyOnLight(0x3f3f3c), + + /// Site tint: the stronger hairline on white, for controls. + lineStrongOnLight(0xcfcfcb), + + /// Site tint: the CTA yellow lifted, for the primary button's hover. + ctaYellowLight(0xf5ff4d), + + /// Site tint: the CTA yellow as text on white, which yellow itself never is. + ctaYellowOnLight(0x606a00), + + /// Site tint: the error signal as text on white. + errorOnLight(0xbe2119), + + /// Site tint: the success signal as text on white. + successOnLight(0x527b28); + + const Palette(this.rgb); + + final int rgb; + + Color get color => Color(0xff000000 | rgb); + + /// `#rrggbb`. + String get hex => '#${rgb.toRadixString(16).padLeft(6, '0')}'; + + Color alpha(double alpha) => color.withValues(alpha: alpha); +} + /// The site's `--af-*` tokens for one theme, as the docs and the landing /// page paint them. -class AfPalette { - const AfPalette({ +class AfTheme { + const AfTheme({ required this.bg, required this.bg2, required this.surface, @@ -88,47 +164,47 @@ class AfPalette { } /// The light theme. -const afLight = AfPalette( - bg: Color(0xFFFFFFFF), - bg2: Color(0xFFF7F7F5), - surface: Color(0xFFFFFFFF), - surface2: Color(0xFFF2F2EF), - border: Color(0xFFE5E5E5), - border2: Color(0xFFCFCFCB), - text: Color(0xFF000000), - text2: Color(0xFF3F3F3C), - muted: Color(0xFF6F6F6F), - accent: Color(0xFFF0FF00), - accentInk: Color(0xFF000000), - accentText: Color(0xFF606A00), - accentHover: Color(0xFFF5FF4D), - accentSoft: Color(0x73F0FF00), - primary: Color(0xFF000000), - primaryInk: Color(0xFFF0FF00), - overlay: Color(0x4D000000), - danger: Color(0xFFBE2119), - ok: Color(0xFF527B28), +final afLight = AfTheme( + bg: Palette.white.color, + bg2: Palette.paper.color, + surface: Palette.white.color, + surface2: Palette.paper2.color, + border: Palette.lineOnLight.color, + border2: Palette.lineStrongOnLight.color, + text: Palette.black.color, + text2: Palette.bodyOnLight.color, + muted: Palette.mutedOnLight.color, + accent: Palette.ctaYellow.color, + accentInk: Palette.black.color, + accentText: Palette.ctaYellowOnLight.color, + accentHover: Palette.ctaYellowLight.color, + accentSoft: Palette.ctaYellow.alpha(0.45), + primary: Palette.black.color, + primaryInk: Palette.ctaYellow.color, + overlay: Palette.black.alpha(0.3), + danger: Palette.errorOnLight.color, + ok: Palette.successOnLight.color, ); /// The dark theme. -const afDark = AfPalette( - bg: Color(0xFF000000), - bg2: Color(0xFF0A0A08), - surface: Color(0xFF151513), - surface2: Color(0xFF1D1D1A), - border: Color(0x1FFFFFFF), - border2: Color(0x33FFFFFF), - text: Color(0xFFFFFFFF), - text2: Color(0xFFD8D8D4), - muted: Color(0xFFA3A3A0), - accent: Color(0xFFF0FF00), - accentInk: Color(0xFF000000), - accentText: Color(0xFFF0FF00), - accentHover: Color(0xFFF5FF4D), - accentSoft: Color(0x0FF0FF00), - primary: Color(0xFFF0FF00), - primaryInk: Color(0xFF000000), - overlay: Color(0x99000000), - danger: Color(0xFFE64239), - ok: Color(0xFF80C340), +final afDark = AfTheme( + bg: Palette.black.color, + bg2: Palette.nearBlack.color, + surface: Palette.surface.color, + surface2: Palette.surface2.color, + border: Palette.white.alpha(0.12), + border2: Palette.white.alpha(0.2), + text: Palette.white.color, + text2: Palette.bodyGray.color, + muted: Palette.mutedGray.color, + accent: Palette.ctaYellow.color, + accentInk: Palette.black.color, + accentText: Palette.ctaYellow.color, + accentHover: Palette.ctaYellowLight.color, + accentSoft: Palette.ctaYellow.alpha(0.06), + primary: Palette.ctaYellow.color, + primaryInk: Palette.black.color, + overlay: Palette.black.alpha(0.6), + danger: Palette.error.color, + ok: Palette.success.color, ); diff --git a/docs_app/scripts/palette.mjs b/docs_app/scripts/palette.mjs index 4bf3a2b..14ea95a 100644 --- a/docs_app/scripts/palette.mjs +++ b/docs_app/scripts/palette.mjs @@ -18,8 +18,8 @@ * Generated (committed, so every checkout builds without running this): * app/af-tokens.css, ../landing/web/af-tokens.css the tokens as CSS variables * lib/palette.generated.ts the swatches and tokens for the OG cards and the logo - * flutter/lib/support/palette.generated.dart the tokens as Flutter Colors, for the live demos - * ../landing/lib/palette.generated.dart the theme-color of the landing page + * flutter/lib/support/palette.generated.dart the swatches and tokens as Flutter Colors, for the live demos + * ../landing/lib/palette.generated.dart the same as Jaspr Colors, and the landing page's theme-color * * Recolored in place: SVG elements carrying `data-palette=""` get that * swatch as their fill. The logos and icons stay single files of artwork; the @@ -69,13 +69,6 @@ const channels = hex => [1, 3, 5].map(i => Number.parseInt(hex.slice(i, i + 2), const cssColor = ({ hex, alpha }) => (alpha === 1 ? hex : `rgba(${channels(hex).join(", ")}, ${alpha})`) -const dartColor = ({ hex, alpha }) => { - const a = Math.round(alpha * 255) - .toString(16) - .padStart(2, "0") - return `Color(0x${(a + hex.slice(1)).toUpperCase()})` -} - const cssName = name => `--af-${name.replaceAll(/([a-z])([A-Z0-9])/g, "$1-$2").toLowerCase()}` // ---------- Outputs ---------- @@ -86,6 +79,23 @@ const doNotEdit = (from, here) => /** The same notice, wrapped for Dart's 80 columns. */ const dartHeader = lines => lines.map(line => `// ${line}\n`).join("") +/** `text` as a Dart doc comment at `indent`, wrapped to 80 columns. */ +function doc(text, indent = " ") { + const width = 80 - indent.length - "/// ".length + const lines = [] + let line = "" + for (const word of text.split(" ")) { + if (line && line.length + 1 + word.length > width) { + lines.push(line) + line = word + } else { + line = line ? `${line} ${word}` : word + } + } + if (line) lines.push(line) + return lines.map(l => `${indent}/// ${l}\n`).join("") +} + function tokensCss() { const block = (selector, tokens, notes) => `${selector} {\n` + @@ -124,39 +134,84 @@ function paletteTs() { ) } -function flutterDart() { - const fields = tokenNames.map(name => ` /// ${palette.tokens[name]}\n final Color ${name};\n`).join("\n") +/** + * The palette as Dart: `enum Palette` names the swatches, `AfTheme` holds one + * theme's tokens, and `afLight`/`afDark` build them from the enum — the same + * shape ciach's landing page uses. `members` are the enum's color accessors, + * which differ between Flutter's and Jaspr's `Color`. + */ +function dart({ header, import: importLine, members, trailer = "" }) { + const swatches = Object.entries(palette.swatches) + .map(([name, { hex, note }]) => `${doc(note)} ${name}(0x${hex.slice(1)}),\n`) + .join("\n") + .replace(/,\n$/, ";\n") + const fields = tokenNames.map(name => `${doc(palette.tokens[name])} final Color ${name};\n`).join("\n") const params = tokenNames.map(name => ` required this.${name},\n`).join("") - const theme = tokens => tokenNames.map(name => ` ${name}: ${dartColor(color(tokens[name]))},\n`).join("") + const theme = tokens => + tokenNames + .map(name => { + const value = tokens[name] + const swatch = + typeof value === "string" ? `Palette.${value}.color` : `Palette.${value.of}.alpha(${value.alpha})` + return ` ${name}: ${swatch},\n` + }) + .join("") return ( - dartHeader([ - "Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do", - "not edit. Run `npm run palette:generate` in docs_app after changing the", - "palette.", - ]) + - `\nimport 'dart:ui' show Color;\n\n` + + dartHeader(header) + + `\n${importLine}\n\n` + + `/// The LeanCode swatches, as \`palette.json\` names them, each a packed\n` + + `/// \`0xRRGGBB\`.\n` + + `enum Palette {\n${swatches}\n` + + ` const Palette(this.rgb);\n\n` + + ` final int rgb;\n\n` + + `${members}}\n\n` + `/// The site's \`--af-*\` tokens for one theme, as the docs and the landing\n` + `/// page paint them.\n` + - `class AfPalette {\n` + - ` const AfPalette({\n${params} });\n\n` + + `class AfTheme {\n` + + ` const AfTheme({\n${params} });\n\n` + `${fields}}\n\n` + `/// The light theme.\n` + - `const afLight = AfPalette(\n${theme(palette.themes.light)});\n\n` + + `final afLight = AfTheme(\n${theme(palette.themes.light)});\n\n` + `/// The dark theme.\n` + - `const afDark = AfPalette(\n${theme(palette.themes.dark)});\n` + `final afDark = AfTheme(\n${theme(palette.themes.dark)});\n` + + trailer ) } +function flutterDart() { + return dart({ + header: [ + "Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do", + "not edit. Run `npm run palette:generate` in docs_app after changing the", + "palette.", + ], + import: "import 'dart:ui' show Color;", + members: + ` Color get color => Color(0xff000000 | rgb);\n\n` + + ` /// \`#rrggbb\`.\n` + + ` String get hex => '#\${rgb.toRadixString(16).padLeft(6, '0')}';\n\n` + + ` Color alpha(double alpha) => color.withValues(alpha: alpha);\n`, + }) +} + function landingDart() { - return ( - dartHeader([ + return dart({ + header: [ "Generated by ../../docs_app/scripts/palette.mjs from", "../../docs_app/palette.json — do not edit. Run `npm run palette:generate`", "in docs_app after changing the palette.", - ]) + - `\n/// The dark theme's page background, for \`\`.\n` + - `const themeColor = '${cssColor(color(palette.themes.dark.bg))}';\n` - ) + ], + import: "import 'package:jaspr/dom.dart';", + members: + ` Color get color => Color.value(rgb);\n\n` + + ` /// \`#rrggbb\`.\n` + + ` String get hex => color.value;\n\n` + + ` Color alpha(double alpha) =>\n` + + ` Color.rgba(rgb >> 16 & 0xff, rgb >> 8 & 0xff, rgb & 0xff, alpha);\n`, + trailer: + `\n/// The dark theme's page background, for \`\`.\n` + + `final themeColor = afDark.bg.value;\n`, + }) } /** Recolors the elements of an SVG that carry `data-palette=""`. */ diff --git a/landing/README.md b/landing/README.md index 2ff3561..09d57ea 100644 --- a/landing/README.md +++ b/landing/README.md @@ -31,6 +31,7 @@ inlined into `index.html`, so it is not copied on its own. | `lib/examples.dart` | Reads the demos from `../docs_app/content/landing/*.mdx` and maps them to the compiled bundle. | | `lib/highlight.dart` | Build-time Dart syntax highlighting (`syntax_highlight_lite`) into `tk-*` spans. | | `lib/site.dart` | URLs, copy, the package version from `../pubspec.yaml`. `SITE_URL` (a `--dart-define`) for previews. | +| `lib/palette.generated.dart` | The palette as Dart — `enum Palette`, `afLight`/`afDark` — generated from `../docs_app/palette.json`. | | `web/af-tokens.css` | The `--af-*` colors, generated from `../docs_app/palette.json` by `npm run palette:generate` there. Inlined. | | `web/landing.css` | The stylesheet: everything but the colors, dark by default. Inlined at build, after the tokens. | | `web/fonts/` | Space Grotesk and JetBrains Mono, self-hosted (see its README), so no third-party request blocks paint. | diff --git a/landing/lib/palette.generated.dart b/landing/lib/palette.generated.dart index 8032f32..48dfa35 100644 --- a/landing/lib/palette.generated.dart +++ b/landing/lib/palette.generated.dart @@ -2,5 +2,213 @@ // ../../docs_app/palette.json — do not edit. Run `npm run palette:generate` // in docs_app after changing the palette. +import 'package:jaspr/dom.dart'; + +/// The LeanCode swatches, as `palette.json` names them, each a packed +/// `0xRRGGBB`. +enum Palette { + /// Primary surface. LeanCode is black-first; yellow is the exception. + black(0x000000), + + /// Text and headings on black; the light document surface. + white(0xffffff), + + /// Call to action and emphasis only. Text on it is always black. + ctaYellow(0xf0ff00), + + /// Body copy on dark surfaces. + bodyGray(0xd8d8d4), + + /// Secondary text, captions, metadata on dark. + mutedGray(0xa3a3a0), + + /// Secondary text, captions, metadata on light: the design system's #757575, + /// a step darker so it also reads AA on paper. + mutedOnLight(0x6f6f6f), + + /// Cards and panels on black, warm-tinted. + surface(0x151513), + + /// A second surface step on black. + surface2(0x1d1d1a), + + /// The 1px hairline on white. + lineOnLight(0xe5e5e5), + + /// Error signal, sparingly. + error(0xe64239), + + /// Success signal, sparingly. + success(0x80c340), + + /// Site tint: a section background a step above black. + nearBlack(0x0a0a08), + + /// Site tint: a section background a step below white. + paper(0xf7f7f5), + + /// Site tint: a second surface step on white. + paper2(0xf2f2ef), + + /// Site tint: body copy on light surfaces. + bodyOnLight(0x3f3f3c), + + /// Site tint: the stronger hairline on white, for controls. + lineStrongOnLight(0xcfcfcb), + + /// Site tint: the CTA yellow lifted, for the primary button's hover. + ctaYellowLight(0xf5ff4d), + + /// Site tint: the CTA yellow as text on white, which yellow itself never is. + ctaYellowOnLight(0x606a00), + + /// Site tint: the error signal as text on white. + errorOnLight(0xbe2119), + + /// Site tint: the success signal as text on white. + successOnLight(0x527b28); + + const Palette(this.rgb); + + final int rgb; + + Color get color => Color.value(rgb); + + /// `#rrggbb`. + String get hex => color.value; + + Color alpha(double alpha) => + Color.rgba(rgb >> 16 & 0xff, rgb >> 8 & 0xff, rgb & 0xff, alpha); +} + +/// The site's `--af-*` tokens for one theme, as the docs and the landing +/// page paint them. +class AfTheme { + const AfTheme({ + required this.bg, + required this.bg2, + required this.surface, + required this.surface2, + required this.border, + required this.border2, + required this.text, + required this.text2, + required this.muted, + required this.accent, + required this.accentInk, + required this.accentText, + required this.accentHover, + required this.accentSoft, + required this.primary, + required this.primaryInk, + required this.overlay, + required this.danger, + required this.ok, + }); + + /// Page background. + final Color bg; + + /// Section background: footer, code panels. + final Color bg2; + + /// Cards and frames. + final Color surface; + + /// Bars and chips on a surface. + final Color surface2; + + /// The hairline. + final Color border; + + /// The stronger hairline, for controls. + final Color border2; + + /// Headings and primary text. + final Color text; + + /// Body copy. + final Color text2; + + /// Captions and metadata. + final Color muted; + + /// The CTA yellow, as a fill. + final Color accent; + + /// Text on the accent. + final Color accentInk; + + /// The accent as text. + final Color accentText; + + /// The primary button's hover fill. + final Color accentHover; + + /// The accent as a wash behind icons, pills and glows. + final Color accentSoft; + + /// The docs' primary action fill. + final Color primary; + + /// Text on the primary fill. + final Color primaryInk; + + /// The scrim behind dialogs. + final Color overlay; + + /// Error signal. + final Color danger; + + /// Success signal. + final Color ok; +} + +/// The light theme. +final afLight = AfTheme( + bg: Palette.white.color, + bg2: Palette.paper.color, + surface: Palette.white.color, + surface2: Palette.paper2.color, + border: Palette.lineOnLight.color, + border2: Palette.lineStrongOnLight.color, + text: Palette.black.color, + text2: Palette.bodyOnLight.color, + muted: Palette.mutedOnLight.color, + accent: Palette.ctaYellow.color, + accentInk: Palette.black.color, + accentText: Palette.ctaYellowOnLight.color, + accentHover: Palette.ctaYellowLight.color, + accentSoft: Palette.ctaYellow.alpha(0.45), + primary: Palette.black.color, + primaryInk: Palette.ctaYellow.color, + overlay: Palette.black.alpha(0.3), + danger: Palette.errorOnLight.color, + ok: Palette.successOnLight.color, +); + +/// The dark theme. +final afDark = AfTheme( + bg: Palette.black.color, + bg2: Palette.nearBlack.color, + surface: Palette.surface.color, + surface2: Palette.surface2.color, + border: Palette.white.alpha(0.12), + border2: Palette.white.alpha(0.2), + text: Palette.white.color, + text2: Palette.bodyGray.color, + muted: Palette.mutedGray.color, + accent: Palette.ctaYellow.color, + accentInk: Palette.black.color, + accentText: Palette.ctaYellow.color, + accentHover: Palette.ctaYellowLight.color, + accentSoft: Palette.ctaYellow.alpha(0.06), + primary: Palette.ctaYellow.color, + primaryInk: Palette.black.color, + overlay: Palette.black.alpha(0.6), + danger: Palette.error.color, + ok: Palette.success.color, +); + /// The dark theme's page background, for ``. -const themeColor = '#000000'; +final themeColor = afDark.bg.value; From 9c099c97e1c0990fa1ea1f251e257eb7f5d49769 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 16:32:37 +0000 Subject: [PATCH 04/19] Write the landing page's theme from the Dart palette Review: the landing's whole theme should come from these colors, not from a second generated stylesheet. The generated AfTheme gains a cssVariables map and _Stylesheet writes the :root and .dark blocks from afLight and afDark ahead of landing.css, so landing/web/af-tokens.css goes; the docs keep their generated af-tokens.css, since Next consumes CSS. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- docs_app/README.md | 14 ++--- docs_app/scripts/landing.mjs | 3 +- docs_app/scripts/palette.mjs | 22 +++++--- landing/README.md | 5 +- landing/lib/main.server.dart | 28 +++++----- landing/lib/palette.generated.dart | 23 +++++++++ landing/web/af-tokens.css | 82 ------------------------------ landing/web/landing.css | 7 +-- 8 files changed, 67 insertions(+), 117 deletions(-) delete mode 100644 landing/web/af-tokens.css diff --git a/docs_app/README.md b/docs_app/README.md index 2fd962b..1fa2694 100644 --- a/docs_app/README.md +++ b/docs_app/README.md @@ -137,13 +137,13 @@ debug asset server sends no CORS headers, unlike the release one. The colors are the LeanCode design system — black ground, warm surfaces, one CTA yellow `#f0ff00` — and live in one place, `palette.json`: the swatches as the design system names them, and the light and dark themes as `--af-*` tokens -built from them. `npm run palette:generate` writes every copy a consumer needs (`app/af-tokens.css` and the landing -page's twin, `lib/palette.generated.ts` for the social cards and the logo, `flutter/lib/support/palette.generated.dart` -for the live demos, `../landing/lib/palette.generated.dart` for the landing page) and recolors the shapes marked -`data-palette` in the logo and icon SVGs; `npm run palette:check` fails CI when they drift. The stylesheet is -hand-written on top of the Fumadocs preset, with the `--color-fd-*` variables Fumadocs paints with pointed at the -tokens. Space Grotesk and JetBrains Mono. Dark is the design and the default; light is a paper variant where the accent -is a fill with black ink on it, the way leancode.co does it. +built from them. `npm run palette:generate` writes every copy a consumer needs (`app/af-tokens.css` for this stylesheet, +`lib/palette.generated.ts` for the social cards and the logo, `flutter/lib/support/palette.generated.dart` for the live +demos, `../landing/lib/palette.generated.dart` for the landing page, which writes its own `--af-*` variables from it) +and recolors the shapes marked `data-palette` in the logo and icon SVGs; `npm run palette:check` fails CI when they +drift. The stylesheet is hand-written on top of the Fumadocs preset, with the `--color-fd-*` variables Fumadocs paints +with pointed at the tokens. Space Grotesk and JetBrains Mono. Dark is the design and the default; light is a paper +variant where the accent is a fill with black ink on it, the way leancode.co does it. Code blocks and live examples share one window frame: a title bar with three dots (the first one yellow), a mono title, and the content below. The landing page in `../landing/web/landing.css` uses the same tokens and the same frame, so the diff --git a/docs_app/scripts/landing.mjs b/docs_app/scripts/landing.mjs index 511af8f..31bf584 100644 --- a/docs_app/scripts/landing.mjs +++ b/docs_app/scripts/landing.mjs @@ -29,8 +29,7 @@ const publicDir = path.join(docsApp, "public") /** * What ends up in public/. Everything else in build/jaspr is build tooling — - * including af-tokens.css and landing.css, which the page inlines into its - * . + * including landing.css, which the page inlines into its . */ const artifacts = ["index.html", "landing.js", "landing-icon.svg", "fonts"] diff --git a/docs_app/scripts/palette.mjs b/docs_app/scripts/palette.mjs index 14ea95a..b6cf6d6 100644 --- a/docs_app/scripts/palette.mjs +++ b/docs_app/scripts/palette.mjs @@ -16,10 +16,11 @@ * check fail if any of them differs from what palette.json says * * Generated (committed, so every checkout builds without running this): - * app/af-tokens.css, ../landing/web/af-tokens.css the tokens as CSS variables - * lib/palette.generated.ts the swatches and tokens for the OG cards and the logo - * flutter/lib/support/palette.generated.dart the swatches and tokens as Flutter Colors, for the live demos - * ../landing/lib/palette.generated.dart the same as Jaspr Colors, and the landing page's theme-color + * app/af-tokens.css the tokens as CSS variables, for the docs' stylesheet + * lib/palette.generated.ts the swatches and tokens for the OG cards and the logo + * flutter/lib/support/palette.generated.dart the swatches and tokens as Flutter Colors, for the live demos + * ../landing/lib/palette.generated.dart the same as Jaspr Colors; the landing page writes its + * `--af-*` variables and theme-color from it at build time * * Recolored in place: SVG elements carrying `data-palette=""` get that * swatch as their fill. The logos and icons stay single files of artwork; the @@ -138,9 +139,10 @@ function paletteTs() { * The palette as Dart: `enum Palette` names the swatches, `AfTheme` holds one * theme's tokens, and `afLight`/`afDark` build them from the enum — the same * shape ciach's landing page uses. `members` are the enum's color accessors, - * which differ between Flutter's and Jaspr's `Color`. + * which differ between Flutter's and Jaspr's `Color`; `themeMembers` are extra + * members of `AfTheme`. */ -function dart({ header, import: importLine, members, trailer = "" }) { +function dart({ header, import: importLine, members, themeMembers = "", trailer = "" }) { const swatches = Object.entries(palette.swatches) .map(([name, { hex, note }]) => `${doc(note)} ${name}(0x${hex.slice(1)}),\n`) .join("\n") @@ -169,7 +171,7 @@ function dart({ header, import: importLine, members, trailer = "" }) { `/// page paint them.\n` + `class AfTheme {\n` + ` const AfTheme({\n${params} });\n\n` + - `${fields}}\n\n` + + `${fields}${themeMembers}}\n\n` + `/// The light theme.\n` + `final afLight = AfTheme(\n${theme(palette.themes.light)});\n\n` + `/// The dark theme.\n` + @@ -208,6 +210,11 @@ function landingDart() { ` String get hex => color.value;\n\n` + ` Color alpha(double alpha) =>\n` + ` Color.rgba(rgb >> 16 & 0xff, rgb >> 8 & 0xff, rgb & 0xff, alpha);\n`, + themeMembers: + `\n /// The theme as the \`--af-*\` custom properties the stylesheet reads.\n` + + ` Map get cssVariables => {\n` + + tokenNames.map(name => ` '${cssName(name)}': ${name},\n`).join("") + + ` };\n`, trailer: `\n/// The dark theme's page background, for \`\`.\n` + `final themeColor = afDark.bg.value;\n`, @@ -228,7 +235,6 @@ function recolorSvg(file, svg) { const generated = [ { file: join(docsApp, "app", "af-tokens.css"), content: tokensCss }, - { file: join(landing, "web", "af-tokens.css"), content: tokensCss }, { file: join(docsApp, "lib", "palette.generated.ts"), content: paletteTs }, { file: join(docsApp, "flutter", "lib", "support", "palette.generated.dart"), content: flutterDart }, { file: join(landing, "lib", "palette.generated.dart"), content: landingDart }, diff --git a/landing/README.md b/landing/README.md index 09d57ea..6508e7a 100644 --- a/landing/README.md +++ b/landing/README.md @@ -31,9 +31,8 @@ inlined into `index.html`, so it is not copied on its own. | `lib/examples.dart` | Reads the demos from `../docs_app/content/landing/*.mdx` and maps them to the compiled bundle. | | `lib/highlight.dart` | Build-time Dart syntax highlighting (`syntax_highlight_lite`) into `tk-*` spans. | | `lib/site.dart` | URLs, copy, the package version from `../pubspec.yaml`. `SITE_URL` (a `--dart-define`) for previews. | -| `lib/palette.generated.dart` | The palette as Dart — `enum Palette`, `afLight`/`afDark` — generated from `../docs_app/palette.json`. | -| `web/af-tokens.css` | The `--af-*` colors, generated from `../docs_app/palette.json` by `npm run palette:generate` there. Inlined. | -| `web/landing.css` | The stylesheet: everything but the colors, dark by default. Inlined at build, after the tokens. | +| `lib/palette.generated.dart` | The palette — `enum Palette`, `afLight`/`afDark` — generated from `../docs_app/palette.json` by `npm run palette:generate` there. The `` writes the `--af-*` variables from it. | +| `web/landing.css` | The stylesheet: everything but the colors, dark by default. Inlined at build, after the variables. | | `web/fonts/` | Space Grotesk and JetBrains Mono, self-hosted (see its README), so no third-party request blocks paint. | | `web/landing.js` | The client: Flutter islands, copy buttons, theme toggle. No framework, no build step. | diff --git a/landing/lib/main.server.dart b/landing/lib/main.server.dart index 2c05012..9d4436f 100644 --- a/landing/lib/main.server.dart +++ b/landing/lib/main.server.dart @@ -96,23 +96,27 @@ Future main() async { ); } -/// The stylesheet, inlined: `web/af-tokens.css` (the colors, generated from -/// `docs_app/palette.json`) and then `web/landing.css`. The page is one HTML -/// document with nothing render-blocking left but its stylesheet, and that -/// stylesheet is 6 KB over the wire: fetching it separately would cost a round -/// trip before the first paint and save nothing, since there is no second page -/// to reuse it on. Read on every render so `jaspr serve` picks up an edit on -/// reload. +/// The stylesheet, inlined: the `--af-*` colors of both themes, written from +/// the palette (`palette.generated.dart`, from `docs_app/palette.json`), and +/// then `web/landing.css`. The page is one HTML document with nothing +/// render-blocking left but its stylesheet, and that stylesheet is 6 KB over +/// the wire: fetching it separately would cost a round trip before the first +/// paint and save nothing, since there is no second page to reuse it on. Read +/// on every render so `jaspr serve` picks up an edit on reload. class _Stylesheet extends StatelessComponent { const _Stylesheet(); @override Component build(BuildContext context) { - final css = [ - 'web/af-tokens.css', - 'web/landing.css', - ].map((file) => File(file).readAsStringSync()).join('\n'); - return RawText(''); + final tokens = [(':root', afLight), ('.dark', afDark)].map((theme) { + final (selector, palette) = theme; + final declarations = palette.cssVariables.entries + .map((variable) => '${variable.key}:${variable.value.value}') + .join(';'); + return '$selector{$declarations}'; + }).join(); + final css = File('web/landing.css').readAsStringSync(); + return RawText(''); } /// Drops comments and the whitespace the grammar does not need. Strings in diff --git a/landing/lib/palette.generated.dart b/landing/lib/palette.generated.dart index 48dfa35..fa6d292 100644 --- a/landing/lib/palette.generated.dart +++ b/landing/lib/palette.generated.dart @@ -162,6 +162,29 @@ class AfTheme { /// Success signal. final Color ok; + + /// The theme as the `--af-*` custom properties the stylesheet reads. + Map get cssVariables => { + '--af-bg': bg, + '--af-bg-2': bg2, + '--af-surface': surface, + '--af-surface-2': surface2, + '--af-border': border, + '--af-border-2': border2, + '--af-text': text, + '--af-text-2': text2, + '--af-muted': muted, + '--af-accent': accent, + '--af-accent-ink': accentInk, + '--af-accent-text': accentText, + '--af-accent-hover': accentHover, + '--af-accent-soft': accentSoft, + '--af-primary': primary, + '--af-primary-ink': primaryInk, + '--af-overlay': overlay, + '--af-danger': danger, + '--af-ok': ok, + }; } /// The light theme. diff --git a/landing/web/af-tokens.css b/landing/web/af-tokens.css deleted file mode 100644 index 12761db..0000000 --- a/landing/web/af-tokens.css +++ /dev/null @@ -1,82 +0,0 @@ -/* Generated by docs_app/scripts/palette.mjs from docs_app/palette.json — do not edit. Run `npm run palette:generate` in docs_app after changing the palette. */ - -:root { - /* Page background. */ - --af-bg: #ffffff; - - /* Section background: footer, code panels. */ - --af-bg-2: #f7f7f5; - - /* Cards and frames. */ - --af-surface: #ffffff; - - /* Bars and chips on a surface. */ - --af-surface-2: #f2f2ef; - - /* The hairline. */ - --af-border: #e5e5e5; - - /* The stronger hairline, for controls. */ - --af-border-2: #cfcfcb; - - /* Headings and primary text. */ - --af-text: #000000; - - /* Body copy. */ - --af-text-2: #3f3f3c; - - /* Captions and metadata. */ - --af-muted: #6f6f6f; - - /* The CTA yellow, as a fill. */ - --af-accent: #f0ff00; - - /* Text on the accent. */ - --af-accent-ink: #000000; - - /* The accent as text. */ - --af-accent-text: #606a00; - - /* The primary button's hover fill. */ - --af-accent-hover: #f5ff4d; - - /* The accent as a wash behind icons, pills and glows. */ - --af-accent-soft: rgba(240, 255, 0, 0.45); - - /* The docs' primary action fill. */ - --af-primary: #000000; - - /* Text on the primary fill. */ - --af-primary-ink: #f0ff00; - - /* The scrim behind dialogs. */ - --af-overlay: rgba(0, 0, 0, 0.3); - - /* Error signal. */ - --af-danger: #be2119; - - /* Success signal. */ - --af-ok: #527b28; -} - -.dark { - --af-bg: #000000; - --af-bg-2: #0a0a08; - --af-surface: #151513; - --af-surface-2: #1d1d1a; - --af-border: rgba(255, 255, 255, 0.12); - --af-border-2: rgba(255, 255, 255, 0.2); - --af-text: #ffffff; - --af-text-2: #d8d8d4; - --af-muted: #a3a3a0; - --af-accent: #f0ff00; - --af-accent-ink: #000000; - --af-accent-text: #f0ff00; - --af-accent-hover: #f5ff4d; - --af-accent-soft: rgba(240, 255, 0, 0.06); - --af-primary: #f0ff00; - --af-primary-ink: #000000; - --af-overlay: rgba(0, 0, 0, 0.6); - --af-danger: #e64239; - --af-ok: #80c340; -} diff --git a/landing/web/landing.css b/landing/web/landing.css index c7b1ff7..e34eaa0 100644 --- a/landing/web/landing.css +++ b/landing/web/landing.css @@ -6,9 +6,10 @@ * The advanced_forms homepage — the LeanCode design system: black surfaces, * one CTA yellow, Space Grotesk and JetBrains Mono. Dark is the design; light * is a paper variant where the accent is a fill with black ink on it. The - * colors are the `--af-*` variables of af-tokens.css, generated from - * docs_app/palette.json and shared with the docs app under /docs, so the two - * read as one site; this file holds everything else. + * colors are `--af-*` variables that main.server.dart writes ahead of this + * sheet from lib/palette.generated.dart — the palette in docs_app/palette.json, + * shared with the docs app under /docs, so the two read as one site; this file + * holds everything else. */ /* ---------- Fonts ---------- */ From 8d4cd306ee922a89dbea13b421cdac3a4c08df3d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 20:09:17 +0000 Subject: [PATCH 05/19] Landing: derive the token classes from a list of names Review: iterating a map's entries for an ad-hoc collection was more than the job needed, and the variable is always --tk- anyway. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- landing/lib/highlight.dart | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/landing/lib/highlight.dart b/landing/lib/highlight.dart index 1ab09c2..5f4b1e4 100644 --- a/landing/lib/highlight.dart +++ b/landing/lib/highlight.dart @@ -24,21 +24,21 @@ final _theme = sh.HighlighterTheme.fromConfiguration( final _highlighter = sh.Highlighter(language: 'dart', theme: _theme); -/// One colour per token class. The `--tk-*` variables are defined with the -/// other tokens in `styles.dart`. +/// One colour per token class, `.tk-` from `--tk-`. The +/// variables are defined with the other tokens in `styles.dart`. @css List get tokenStyles => [ - for (final MapEntry(key: token, value: variable) in { - 'keyword': '--tk-keyword', - 'type': '--tk-type', - 'string': '--tk-string', - 'number': '--tk-number', - 'comment': '--tk-comment', - 'annotation': '--tk-annotation', - 'function': '--tk-function', - 'operator': '--tk-operator', - }.entries) - css('.tk-$token').styles(color: Color.variable(variable)), + for (final token in const [ + 'keyword', + 'type', + 'string', + 'number', + 'comment', + 'annotation', + 'function', + 'operator', + ]) + css('.tk-$token').styles(color: Color.variable('--tk-$token')), css( '.tk-doc', ).styles(color: const .variable('--tk-comment'), fontStyle: .italic), From cb6d87ebe4eb5f786f4396fc0c155a21dd98f884 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 20:49:56 +0000 Subject: [PATCH 06/19] Build the TS themes from the swatches; drop the ciach mentions Review: the TS palette inlined the theme colors while the Dart one built them from the enum, so now themes reference swatches with withAlpha() emitted alongside, and lib/palette.ts only re-exports. The READMEs and comments stop pointing at another package for the branding. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- docs_app/README.md | 7 ++- docs_app/app/global.css | 2 +- docs_app/lib/palette.generated.ts | 84 ++++++++++++++++-------------- docs_app/lib/palette.ts | 10 +--- docs_app/scripts/palette.mjs | 18 +++++-- landing/README.md | 8 ++- landing/lib/components/button.dart | 6 +-- landing/lib/styles.dart | 2 +- 8 files changed, 72 insertions(+), 65 deletions(-) diff --git a/docs_app/README.md b/docs_app/README.md index 3093939..c63087e 100644 --- a/docs_app/README.md +++ b/docs_app/README.md @@ -7,10 +7,9 @@ AI-Provenance: # advanced-forms-docs The documentation site for `advanced_forms`, under `/docs`: built with [Fumadocs](https://fumadocs.dev) on Next.js, -styled after the LeanCode design system used on [ciach.leancode.co](https://ciach.leancode.co), and running its own code -examples in Flutter, in the browser. The landing page at `/` is a separate static site written in -[Jaspr](https://jaspr.site), in the repo-root `landing/` folder; this app serves its build output from `public/`, so the -two ship as one deployment on one domain. +styled after the LeanCode design system, and running its own code examples in Flutter, in the browser. The landing page +at `/` is a separate static site written in [Jaspr](https://jaspr.site), in the repo-root `landing/` folder; this app +serves its build output from `public/`, so the two ship as one deployment on one domain. MDX for the docs lives in the repo-root `docs/` folder; the landing page's live demos live in `content/landing/`. From this directory: diff --git a/docs_app/app/global.css b/docs_app/app/global.css index 97d679e..63ab357 100644 --- a/docs_app/app/global.css +++ b/docs_app/app/global.css @@ -197,7 +197,7 @@ kbd { content: none; } -/* ---------- Code blocks: the ciach window frame ---------- */ +/* ---------- Code blocks: the window frame ---------- */ .prose figure.shiki, .af-landing figure.shiki { diff --git a/docs_app/lib/palette.generated.ts b/docs_app/lib/palette.generated.ts index c461293..0d11156 100644 --- a/docs_app/lib/palette.generated.ts +++ b/docs_app/lib/palette.generated.ts @@ -44,86 +44,92 @@ export const swatches = { successOnLight: "#527b28", } as const -/** The site's `--af-*` tokens per theme, as CSS colors. */ +/** `#rrggbb` at an alpha, as `rgba()` — the one color form every renderer of ours understands. */ +export function withAlpha(hex: string, alpha: number): string { + const [r, g, b] = [1, 3, 5].map(i => Number.parseInt(hex.slice(i, i + 2), 16)) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} + +/** The site's `--af-*` tokens per theme, built from the swatches. */ export const themes = { light: { /** Page background. */ - bg: "#ffffff", + bg: swatches.white, /** Section background: footer, code panels. */ - bg2: "#f7f7f5", + bg2: swatches.paper, /** Cards and frames. */ - surface: "#ffffff", + surface: swatches.white, /** Bars and chips on a surface. */ - surface2: "#f2f2ef", + surface2: swatches.paper2, /** The hairline. */ - border: "#e5e5e5", + border: swatches.lineOnLight, /** The stronger hairline, for controls. */ - border2: "#cfcfcb", + border2: swatches.lineStrongOnLight, /** Headings and primary text. */ - text: "#000000", + text: swatches.black, /** Body copy. */ - text2: "#3f3f3c", + text2: swatches.bodyOnLight, /** Captions and metadata. */ - muted: "#6f6f6f", + muted: swatches.mutedOnLight, /** The CTA yellow, as a fill. */ - accent: "#f0ff00", + accent: swatches.ctaYellow, /** Text on the accent. */ - accentInk: "#000000", + accentInk: swatches.black, /** The accent as text. */ - accentText: "#606a00", + accentText: swatches.ctaYellowOnLight, /** The primary button's hover fill. */ - accentHover: "#f5ff4d", + accentHover: swatches.ctaYellowLight, /** The accent as a wash behind icons, pills and glows. */ - accentSoft: "rgba(240, 255, 0, 0.45)", + accentSoft: withAlpha(swatches.ctaYellow, 0.45), /** The docs' primary action fill. */ - primary: "#000000", + primary: swatches.black, /** Text on the primary fill. */ - primaryInk: "#f0ff00", + primaryInk: swatches.ctaYellow, /** The scrim behind dialogs. */ - overlay: "rgba(0, 0, 0, 0.3)", + overlay: withAlpha(swatches.black, 0.3), /** Error signal. */ - danger: "#be2119", + danger: swatches.errorOnLight, /** Success signal. */ - ok: "#527b28", + ok: swatches.successOnLight, }, dark: { /** Page background. */ - bg: "#000000", + bg: swatches.black, /** Section background: footer, code panels. */ - bg2: "#0a0a08", + bg2: swatches.nearBlack, /** Cards and frames. */ - surface: "#151513", + surface: swatches.surface, /** Bars and chips on a surface. */ - surface2: "#1d1d1a", + surface2: swatches.surface2, /** The hairline. */ - border: "rgba(255, 255, 255, 0.12)", + border: withAlpha(swatches.white, 0.12), /** The stronger hairline, for controls. */ - border2: "rgba(255, 255, 255, 0.2)", + border2: withAlpha(swatches.white, 0.2), /** Headings and primary text. */ - text: "#ffffff", + text: swatches.white, /** Body copy. */ - text2: "#d8d8d4", + text2: swatches.bodyGray, /** Captions and metadata. */ - muted: "#a3a3a0", + muted: swatches.mutedGray, /** The CTA yellow, as a fill. */ - accent: "#f0ff00", + accent: swatches.ctaYellow, /** Text on the accent. */ - accentInk: "#000000", + accentInk: swatches.black, /** The accent as text. */ - accentText: "#f0ff00", + accentText: swatches.ctaYellow, /** The primary button's hover fill. */ - accentHover: "#f5ff4d", + accentHover: swatches.ctaYellowLight, /** The accent as a wash behind icons, pills and glows. */ - accentSoft: "rgba(240, 255, 0, 0.06)", + accentSoft: withAlpha(swatches.ctaYellow, 0.06), /** The docs' primary action fill. */ - primary: "#f0ff00", + primary: swatches.ctaYellow, /** Text on the primary fill. */ - primaryInk: "#000000", + primaryInk: swatches.black, /** The scrim behind dialogs. */ - overlay: "rgba(0, 0, 0, 0.6)", + overlay: withAlpha(swatches.black, 0.6), /** Error signal. */ - danger: "#e64239", + danger: swatches.error, /** Success signal. */ - ok: "#80c340", + ok: swatches.success, }, } as const diff --git a/docs_app/lib/palette.ts b/docs_app/lib/palette.ts index 4e1baef..3d1c2f6 100644 --- a/docs_app/lib/palette.ts +++ b/docs_app/lib/palette.ts @@ -3,15 +3,9 @@ * model: Claude Fable 5.1 * harness: Claude Code */ -import { swatches, themes } from "@/lib/palette.generated" +import { themes } from "@/lib/palette.generated" -export { swatches, themes } +export { swatches, themes, withAlpha } from "@/lib/palette.generated" /** The dark theme: what the social cards are drawn in, whatever the reader's theme. */ export const dark = themes.dark - -/** `#rrggbb` at an alpha, as `rgba()` — the one color form every renderer of ours understands. */ -export function withAlpha(hex: string, alpha: number): string { - const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16)) - return `rgba(${r}, ${g}, ${b}, ${alpha})` -} diff --git a/docs_app/scripts/palette.mjs b/docs_app/scripts/palette.mjs index b6cf6d6..45bb399 100644 --- a/docs_app/scripts/palette.mjs +++ b/docs_app/scripts/palette.mjs @@ -121,13 +121,23 @@ function paletteTs() { .join("") const theme = tokens => tokenNames - .map(name => ` /** ${palette.tokens[name]} */\n ${name}: "${cssColor(color(tokens[name]))}",\n`) + .map(name => { + const value = tokens[name] + const swatch = + typeof value === "string" ? `swatches.${value}` : `withAlpha(swatches.${value.of}, ${value.alpha})` + return ` /** ${palette.tokens[name]} */\n ${name}: ${swatch},\n` + }) .join("") return ( `// ${doNotEdit("scripts/palette.mjs", "palette.json")}\n\n` + `/** The LeanCode swatches, as \`#rrggbb\`. */\n` + `export const swatches = {\n${swatches}} as const\n\n` + - `/** The site's \`--af-*\` tokens per theme, as CSS colors. */\n` + + `/** \`#rrggbb\` at an alpha, as \`rgba()\` — the one color form every renderer of ours understands. */\n` + + `export function withAlpha(hex: string, alpha: number): string {\n` + + ` const [r, g, b] = [1, 3, 5].map(i => Number.parseInt(hex.slice(i, i + 2), 16))\n` + + ` return \`rgba(\${r}, \${g}, \${b}, \${alpha})\`\n` + + `}\n\n` + + `/** The site's \`--af-*\` tokens per theme, built from the swatches. */\n` + `export const themes = {\n` + ` light: {\n${theme(palette.themes.light)} },\n` + ` dark: {\n${theme(palette.themes.dark)} },\n` + @@ -137,8 +147,8 @@ function paletteTs() { /** * The palette as Dart: `enum Palette` names the swatches, `AfTheme` holds one - * theme's tokens, and `afLight`/`afDark` build them from the enum — the same - * shape ciach's landing page uses. `members` are the enum's color accessors, + * theme's tokens, and `afLight`/`afDark` build them from the enum. `members` + * are the enum's color accessors, * which differ between Flutter's and Jaspr's `Color`; `themeMembers` are extra * members of `AfTheme`. */ diff --git a/landing/README.md b/landing/README.md index 4e1c534..304674a 100644 --- a/landing/README.md +++ b/landing/README.md @@ -39,8 +39,7 @@ stylesheet to copy: the styles are Dart, and the build inlines them into `index. ## Styles There is no stylesheet file. Every component declares the rules for the classes it renders in a -`@css static List get styles` getter next to its `build`, the way [ciach's -website](https://github.com/leancodepl/ciach/tree/main/website/lib) does; `lib/styles.dart` holds what is not any one +`@css static List get styles` getter next to its `build`; `lib/styles.dart` holds what is not any one component's: the `@font-face` rules, the `--af-*` tokens for the light (`:root`) and dark (`.dark`) themes — written from `afLight` and `afDark` in `lib/palette.generated.dart`, so the colors come from `docs_app/palette.json` like the docs' — the reset, the container and skip-link utilities and the reduced-motion rule. `jaspr_builder` collects every `@css` getter into @@ -53,9 +52,8 @@ element, the cascade follows the bundle order, so a rule in `sections.dart` can ## Fonts -`web/fonts/` holds Space Grotesk and JetBrains Mono as variable fonts in their Latin and Latin Extended subsets, the -same files [ciach.leancode.co](https://github.com/leancodepl/ciach/tree/main/website/web/fonts) ships, under the SIL -Open Font License (the `OFL-*.txt` files alongside). Self-hosting them means no third-party stylesheet blocks the first +`web/fonts/` holds Space Grotesk and JetBrains Mono as variable fonts in their Latin and Latin Extended subsets, under +the SIL Open Font License (the `OFL-*.txt` files alongside). Self-hosting them means no third-party stylesheet blocks the first paint; `lib/styles.dart` opens with the `@font-face` rules and `main.server.dart` preloads the two Latin files, which carry every glyph above the fold. To update a font, fetch the Google Fonts CSS with a modern Chrome user agent and copy the `woff2` files it points at. diff --git a/landing/lib/components/button.dart b/landing/lib/components/button.dart index ad8b256..9ede746 100644 --- a/landing/lib/components/button.dart +++ b/landing/lib/components/button.dart @@ -13,9 +13,9 @@ enum ButtonVariant { secondary, } -/// A pill-shaped call to action that links somewhere, the same component -/// Ciach's site uses. [external] links open in a new tab. An icon can go -/// before the label ([leading]) or after it ([trailing]). +/// A pill-shaped call to action that links somewhere. [external] links open in +/// a new tab. An icon can go before the label ([leading]) or after it +/// ([trailing]). class Button extends StatelessComponent { const Button( this.label, { diff --git a/landing/lib/styles.dart b/landing/lib/styles.dart index f9cc093..3bd6639 100644 --- a/landing/lib/styles.dart +++ b/landing/lib/styles.dart @@ -161,7 +161,7 @@ List get _tokens => [ raw: { ..._colors(afDark), '--af-shadow': '0 24px 60px -32px rgba(0, 0, 0, 0.9)', - // Code tokens: Material Palenight, the palette ciach uses. + // Code tokens: Material Palenight. '--tk-keyword': '#c792ea', '--tk-type': '#ffcb6b', '--tk-string': '#c3e88d', From d55102afb4aa596492f89fed62ef6c8dbef6fa38 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 08:51:29 +0000 Subject: [PATCH 07/19] Give every landing class one owner Each component keeps the classes it renders as typed `ClassName` constants and spells its selectors and `classes:` attributes from them, so a name is written once and a rename cannot miss a use. Pieces two places styled or rendered by string are now their own components: `CopyButton` (the install line and the example toolbar), `Logo` (header and footer, with a `large` size instead of the footer's override), `ButtonRow` (the action rows, `af-actions`), the `Card` family, and the `Section` vocabulary (`Eyebrow`, `Lead`, `Checklist`, `MoreLink`, `DemoSlot`; `Section.plain` for the skill band). The `> *` min-width rules move to the grids they belong to, `af-accent` to the hero, `af-landing` and the skip link to `App`; `rich` and `externalLink` get their own file. The class names `landing.js` looks up and the ones the docs' `global.css` shares are unchanged. The rendered stylesheet differs only in the moved and renamed rules; screenshots of both themes match the previous build. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv --- landing/README.md | 43 ++-- landing/lib/app.dart | 31 ++- landing/lib/components/button.dart | 74 ++++++- landing/lib/components/card.dart | 116 ++++++++++ landing/lib/components/copy_button.dart | 132 ++++++++++++ landing/lib/components/example_frame.dart | 212 ++++++++++--------- landing/lib/components/footer.dart | 64 +++--- landing/lib/components/hero.dart | 185 ++++++++-------- landing/lib/components/logo.dart | 63 ++++++ landing/lib/components/nav_bar.dart | 90 +++----- landing/lib/components/pill.dart | 13 +- landing/lib/components/section.dart | 219 +++++++++++++------ landing/lib/components/sections.dart | 246 +++++++++------------- landing/lib/components/text.dart | 27 +++ landing/lib/styles.dart | 65 +++--- 15 files changed, 1007 insertions(+), 573 deletions(-) create mode 100644 landing/lib/components/card.dart create mode 100644 landing/lib/components/copy_button.dart create mode 100644 landing/lib/components/logo.dart create mode 100644 landing/lib/components/text.dart diff --git a/landing/README.md b/landing/README.md index 304674a..90d25f4 100644 --- a/landing/README.md +++ b/landing/README.md @@ -23,18 +23,18 @@ stylesheet to copy: the styles are Dart, and the build inlines them into `index. ## Layout -| Path | What lives there | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `lib/main.server.dart` | The entrypoint: the `` (font preloads, theme bootstrap, Open Graph) and the `App`. | -| `lib/app.dart` | The page: header, hero, three sections with a live demo each, features, agent skill band, footer. | -| `lib/components/` | One file per section, each with its styles in a `@css` getter; `example_frame.dart` is the window around a demo. | -| `lib/examples.dart` | Reads the demos from `../docs_app/content/landing/*.mdx` and maps them to the compiled bundle. | -| `lib/highlight.dart` | Build-time Dart syntax highlighting (`syntax_highlight_lite`) into `tk-*` spans, and their colours. | -| `lib/site.dart` | URLs, copy, the package version from `../pubspec.yaml`. `SITE_URL` (a `--dart-define`) for previews. | +| Path | What lives there | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `lib/main.server.dart` | The entrypoint: the `` (font preloads, theme bootstrap, Open Graph) and the `App`. | +| `lib/app.dart` | The page: header, hero, three sections with a live demo each, features, agent skill band, footer. | +| `lib/components/` | One file per section or shared piece, each with its styles in a `@css` getter; `example_frame.dart` is the window around a demo. | +| `lib/examples.dart` | Reads the demos from `../docs_app/content/landing/*.mdx` and maps them to the compiled bundle. | +| `lib/highlight.dart` | Build-time Dart syntax highlighting (`syntax_highlight_lite`) into `tk-*` spans, and their colours. | +| `lib/site.dart` | URLs, copy, the package version from `../pubspec.yaml`. `SITE_URL` (a `--dart-define`) for previews. | | `lib/palette.generated.dart` | The palette — `enum Palette`, `afLight`/`afDark` — generated from `../docs_app/palette.json` by `npm run palette:generate` there. | -| `lib/styles.dart` | Fonts, the `--af-*` tokens (written from the palette), the reset and utilities. See "Styles" below. | -| `web/fonts/` | Space Grotesk and JetBrains Mono, self-hosted (see its README), so no third-party request blocks paint. | -| `web/landing.js` | The client: Flutter islands, copy buttons, theme toggle. No framework, no build step. | +| `lib/styles.dart` | Fonts, the `--af-*` tokens (written from the palette), the reset, the container and `ClassName`. See "Styles" below. | +| `web/fonts/` | Space Grotesk and JetBrains Mono, self-hosted (see its README), so no third-party request blocks paint. | +| `web/landing.js` | The client: Flutter islands, copy buttons, theme toggle. No framework, no build step. | ## Styles @@ -42,13 +42,26 @@ There is no stylesheet file. Every component declares the rules for the classes `@css static List get styles` getter next to its `build`; `lib/styles.dart` holds what is not any one component's: the `@font-face` rules, the `--af-*` tokens for the light (`:root`) and dark (`.dark`) themes — written from `afLight` and `afDark` in `lib/palette.generated.dart`, so the colors come from `docs_app/palette.json` like the -docs' — the reset, the container and skip-link utilities and the reduced-motion rule. `jaspr_builder` collects every `@css` getter into -`lib/main.server.options.dart` (generated, not committed), and Jaspr renders them as one `