Align the site with the LeanCode palette, from one source - #80
Align the site with the LeanCode palette, from one source#80claude[bot] wants to merge 17 commits into
Conversation
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
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<StyleRule> 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 `<style>` in the head; nothing render-blocking is fetched, as before. The `_Stylesheet` component and web/landing.css are gone, and the docs_app build script and both READMEs describe where styles live now. Where Jaspr's typed properties cannot say something — color-mix(), :has(), counter(), an infinite animation, a @font-face with a weight range — the rule uses its `raw` map. The generated CSS was diffed rule by rule against the old sheet (only equivalent shorthand expansions differ), and screenshots of the hero, an example frame, the features and the footer are pixel-identical to main in both themes at 1280px and 390px. The Lighthouse fixes from #79 (contrast tokens, underlined footer links, the tab strip layout, selection and focus ring, :has() panel switching) carry over. Requested by Komoszek in review of #79. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U992t9eVSDf5TZXpaxRbju
main now inlines landing.css into the page instead of copying it to public/, so the generated af-tokens.css is inlined the same way, ahead of the rest of the sheet, and the copy-and-ignore entries for it go. main's Lighthouse contrast fixes to the old palette are carried by darkening the light muted gray one step so it reads AA on the paper footer too.
|
Docs preview: https://advanced-forms-foyqx2r03-leancode.vercel.app Built from b537458; the landing page is at |
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
Combines the landing page's move to Jaspr @css getters with the palette. The tokens block in styles.dart is written from afLight and afDark rather than typed out, the hover, glow and pill colors become tokens and colorMix(), --af-accent-2 goes (hover borders use the accent), and the docs CI runs build_runner before analyzing the landing page, since main.server.dart now imports the generated options.
|
Merged 🤖 Generated with Claude Code Generated by Claude Code |
| 'keyword': '--tk-keyword', | ||
| 'type': '--tk-type', | ||
| 'string': '--tk-string', | ||
| 'number': '--tk-number', | ||
| 'comment': '--tk-comment', | ||
| 'annotation': '--tk-annotation', | ||
| 'function': '--tk-function', | ||
| 'operator': '--tk-operator', |
There was a problem hiding this comment.
To iterate over such an ad-hoc collection, I'd use a list of pairs; might be simpler
There was a problem hiding this comment.
Done in 8d4cd30 — and one step simpler than pairs: the variable is always --tk-<token>, so it's now a plain list of the token names, for (final token in const ['keyword', 'type', …]) css('.tk-$token').styles(color: Color.variable('--tk-$token')).
🤖 Generated with Claude Code
Generated by Claude Code
Review: iterating a map's entries for an ad-hoc collection was more than the job needed, and the variable is always --tk-<token> anyway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
Komoszek
left a comment
There was a problem hiding this comment.
We should definitely centralize the branding/colors do we don't end up with 1000 different palettes 🙈 the color generation script could definitely be improved, but it's really important
| ## Styles | ||
|
|
||
| There is no stylesheet file. Every component declares the rules for the classes it renders in a | ||
| `@css static List<StyleRule> get styles` getter next to its `build`, the way [ciach's |
There was a problem hiding this comment.
IMO we shouldn't mention ciach or aby other package that is not the real branding package (maybe we should create the company-wide branding package? 🤔 )
There was a problem hiding this comment.
Agreed — every ciach mention in the READMEs and code comments is gone in cb6d87e. On a company-wide branding package: palette.json is shaped for exactly that (the swatches named as the design system names them, the themes as tokens built from them), so lifting it and the generator out into a shared package is the natural next step — out of scope for this PR, though.
🤖 Generated with Claude Code
Generated by Claude Code
|
|
||
| @css | ||
| static List<StyleRule> get styles => [ | ||
| css('.af-button').styles( |
There was a problem hiding this comment.
WDYT about extracting the af-button to some const and reusing it instead of the raw strings? The same applies to sll the other components
There was a problem hiding this comment.
Fair point, but it's a bigger bite than it looks here: 71 distinct classes across 125 selectors and 83 classes: strings in 10 components, and many selectors are compound (.af-button-primary:hover, .af-hero-grid > *) or shared between files. That code just landed with the @css refactor, so I'd rather not reshuffle it inside this PR — leaving the call to @PiotrRogulski whether to fold it in here or follow up separately.
🤖 Generated with Claude Code
Generated by Claude Code
There was a problem hiding this comment.
Compound selectors arę not a problem, just do string interpolation. If for some reson we had to export the base classnames then IMO something iż wrong with the code. We shouldn't need to target the internal classname
There was a problem hiding this comment.
Measured it, and you're right that it isn't just a rename: of 82 classes, 17 cross a file boundary. 3 are the intended global utilities from styles.dart (af-container, af-landing, af-skip-link); the other 14 are real couplings — footer.dart renders hero.dart's af-hero-actions/af-center, sections.dart renders five of section.dart's classes (af-checklist, af-eyebrow, af-lead, af-section-demo, af-section-more), footer.dart styles nav_bar.dart's af-logo, and af-copy-idle/af-copy-done are styled in both hero.dart and example_frame.dart. Consts would surface each of those as an import, which is the point — but resolving them properly means moving shared rules (an actions row, a copy button, the logo) to shared homes, i.e. a component-boundary pass over the @css refactor rather than string interpolation alone. Happy to do it; whether here or as a follow-up is @PiotrRogulski's call.
🤖 Generated with Claude Code
Generated by Claude Code
There was a problem hiding this comment.
Do whatever you want, make it good, future-proof, and scalable 🥰
Listen to your heart, there's nothing else you can do
~Roxette
There was a problem hiding this comment.
Done in d55102a, and it went further than a rename.
Typed names. ClassName is an extension type over the string (in styles.dart); every component keeps the classes it renders as static const ClassNames and spells both sides from them — css(_hero.selector), '${_grid.selector} > *' for selectors, classes: _hero.name / (container + _grid).name for the attribute. Compound selectors are interpolation, as you said; + combines two classes on one element. Variants carry their class as enum values (ButtonVariant.primary.className, CopyButtonVariant), so 'af-button-${variant.name}' is gone.
Boundaries. The 14 real cross-file couplings I listed are resolved by giving the shared piece an owner rather than exporting base names:
CopyButton— the hero's install line and the example toolbar rendered and styled the sameaf-copy-idle/doneswap in two files, with a global[data-copied="true"] { !important }rule doing the work for both. One component now, two variants (framed,ghost), no!important.Logo— was a function innav_bar.dartstyled there, overridden by the footer (.af-footer-brand .af-logo img). Now its own component withlarge: true, and the theme-switch classes areaf-logo-light/darkinstead of borrowingaf-theme-*from the toggle.ButtonRow—af-hero-actions(+af-center) was used by the footer and the skill band and refined by.af-band .af-hero-actions; nowaf-actionswithcenter/flushoptions.Card,CardGrid,CardIconincard.dart— the mode cards reusedaf-card/af-feature-iconfromFeatures.stylesby string; both grids now compose these and pass their ownclassNamefor their own rules.Eyebrow,Lead,Checklist,MoreLink,DemoSlotinsection.dart— the section vocabularysections.dartrendered by string;Section.plaincovers the skill band's headerless section.- The
> *min-width rules moved fromstyles.dartto the grids they belong to;af-accentto the hero;af-landingand the skip link toApp;rich/externalLinktotext.dart.styles.dartexports exactly one class,container.
The one export in the other direction is CopyButton.labelClassName, which the hero's install line hides under 540px — a parent reaching into a child it composes, which I think is the legitimate case.
Kept as they are: the names landing.js looks up (af-example*, af-tab-input, af-code-panel) and the ones the docs' global.css shares for the frame and the logo. The README's Styles section describes the rule.
Verified: dart analyze --fatal-infos and dart format clean; the rendered <style> differs from the previous build only in the moved/renamed rules (166 of 193 rules byte-identical, the rest are the ones above); full-page screenshots of both themes pixel-match the previous build (light: 0 pixels; dark: the 36 pixels of the pulsing demo dot).
Generated by Claude Code
|
|
||
| /** The LeanCode swatches, as `#rrggbb`. */ | ||
| export const swatches = { | ||
| /** Primary surface. LeanCode is black-first; yellow is the exception. */ |
There was a problem hiding this comment.
Are those swatches used anywhere? Dart counterparts uses them to build the themes, but both CSS and TS variants inlines the colors into the themes
There was a problem hiding this comment.
Yes — LogoMark in components/logo.tsx takes its yellow from swatches.ctaYellow (that is what keeps the docs favicon and the OG cards on the palette), and as of cb6d87e the TS themes are built from the swatches too, like the Dart ones: bg: swatches.white, accentSoft: withAlpha(swatches.ctaYellow, 0.45), with withAlpha emitted alongside and lib/palette.ts reduced to re-exports. The CSS stays resolved on purpose: a --lc-black-style indirection would add a hop nothing else reads, and resolved values are what you want to see in DevTools. If a consumer ever needs the swatches as CSS variables, that's a few lines in the generator.
🤖 Generated with Claude Code
Generated by Claude Code
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
| static const _footer = ClassName('af-footer'); | ||
| static const _cta = ClassName('af-cta'); | ||
| static const _ctaInner = ClassName('af-cta-inner'); | ||
| static const _grid = ClassName('af-footer-grid'); | ||
| static const _brand = ClassName('af-footer-brand'); | ||
| static const _bottom = ClassName('af-footer-bottom'); |
There was a problem hiding this comment.
Question, if some other component used raw af-footer classname then would we have name collision? Is there any way in jaspr to avoid that?
There was a problem hiding this comment.
Yes, it would collide, and Jaspr has nothing built in to stop it. The @css getters are collected into one global <style>; there are no CSS modules, no hashed class names, no scoping — Styles inline on an element is the only scoped form, and it can't express hover, media queries or descendants. So the guarantee has to come from us:
- By construction: a class exists only as a
ClassNameconstant on the component that owns it, and everyone else gets to it through that component (composing it, or a public constant it exports). A raw'af-footer'string in another file would be the one way to break that. - By check, as of fee6bbb:
tool/check_class_names.dartreadslib/and fails when the sameClassName('…')literal is declared in two places, or when a class or selector is written as a raw string ('af-…','.af-…') outside a declaration. It runs in the docs workflow right afterdart analyze. I probed it with a duplicateaf-footer, a rawclasses: 'af-hero af-x'and a rawcss('.af-band h2')— all three are reported with file and line.
It can't catch a string built at runtime that happens to spell an existing class, but nothing in the codebase does that now, and the check would flag the literal pieces of most attempts. The README's Styles section mentions it.
Generated by Claude Code
There was a problem hiding this comment.
Revert this commit, I would prefere something more in line of css modules, idk, built time suffix scoped to the specific component, like some key or smth.
There was a problem hiding this comment.
Reverted in 9ddf572, and d804a71 does the CSS-modules thing instead.
ClassName now takes its owner: ClassName('af-footer', owner: SiteFooter) renders as af-footer-14zoy — the suffix is five base-36 digits of an FNV-1a hash of the owner's type name, computed when the static site is rendered, so it is stable across builds and short enough to read in the inspector. Two components can both call something af-grid and never meet in the stylesheet, and a raw 'af-footer' string anywhere else matches nothing. Nothing changes at the call sites: selectors and classes: are still spelled from the constant (css(_footer.selector), classes: (container + _grid).name), so the suffix is never written by hand. Variants carry it through their enum (af-button-eaqh0 af-button-secondary-eaqh0).
ClassName.shared('af-example') renders as written, for the names that are a contract with another file: what landing.js looks up (af-example*, af-tab-input, af-code-panel), what the docs' global.css uses for the same frame and logo, af-landing, and the af-container utility. styles.dart documents the rule; the README's Styles section too.
Verified: the rendered stylesheet with the suffixes stripped is rule-for-rule the same as the previous build, and full-page screenshots of both themes are pixel-identical (0 differing pixels).
Generated by Claude Code
There was a problem hiding this comment.
Why a custom hash and the built-in hashCode?
There was a problem hiding this comment.
Could we somehow simplifi this? Is it possible to infer the type from the usage in class? Do not commit anything just answer the question
There was a problem hiding this comment.
Not from the enclosing class, no. Dart has no static self-type: inside class Hero, a static const _class = ClassScope() has nothing to infer T from — the enclosing declaration isn't part of the expression's type context, and typing the field (static const ClassScope<Hero> _class = …) just moves the name. There's no this in a static context, the @css getters have to be static for jaspr_builder to collect them, so there's no instance to take runtimeType from, and macros are gone. A StackTrace.current in the initializer would technically show Hero._class, but that's a trick I wouldn't put in a codebase.
The one way to get the owner for free is to make the names instances of something that knows its type — an enhanced enum per component:
enum _Hero with ClassNames {
hero('af-hero'),
grid('af-hero-grid');
const _Hero(this.local);
final String local;
}
css('${_Hero.grid.selector} > *')
div(classes: (container + _Hero.grid).name)ClassNames is a mixin that scopes by runtimeType, so nothing is passed anywhere; the enum is the scope. It also absorbs the variant enums (_Button { button, primary, secondary }), which removes the _variantClass indirection. The cost: every call site changes from _grid to _Hero.grid, and each enum needs a distinct name (_Hero, _Footer, …) because that name is what gets hashed — the same one-name-per-component ClassScope<Hero>() spells today, just in a different place, and the same collision check would guard it.
Beyond that, the only route to zero declarations is a build_runner builder generating the scope per component. I wouldn't add a builder for one line.
My take: ClassScope<Hero>() is one line per component and leaves call sites alone; the enum is the same information with grouping and a bigger diff. Happy to switch if you prefer the grouping — say the word.
Generated by Claude Code
There was a problem hiding this comment.
Do you think it's possible to genericise it and package it for reuse in different Jaspr projects?
There was a problem hiding this comment.
Yes, and cheaply. Nothing in ClassScope/ClassName is specific to this site — or to Jaspr, for that matter: it's ~60 lines of dart:core that produce strings, and Jaspr only ever sees the strings (css(x.selector), classes: x.name). The project-specific parts (container, the af- prefix, which names are shared) stay on the consumer's side.
What a package would add beyond a copy-paste:
- Configurable scope: prefix, suffix length, and a strategy for the suffix. In particular a dev mode that renders
af-hero-grid--Heroinstead ofaf-hero-grid-174ao(what CSS modules do in development), switched byJaspr.isDevelopment/kDebugModeor a builder flag, so the inspector reads like the source whilejaspr buildstays short and stable. - Jaspr sugar:
extension on ClassName { StyleRule styles(Styles) }so a rule is_grid.styles(display: .grid)instead ofcss(_grid.selector).styles(…), pluschild,hover,descendanthelpers for the compound selectors we currently spell as strings. - The collision guard as it is, and a test helper (
expectUniqueScopes()) for projects that want it in a test rather than at render time. - Tests and a README with the "one scope per component,
sharedfor contracts" convention, so the rule travels with the code.
Where: a small package in the leancodepl org — jaspr_scoped_classes or similar — used by ciach's website/ and this landing first (that's already two consumers, and ciach has the same "one component, one file, global stylesheet" shape), published to pub.dev once it stops moving. Since the core has no Jaspr dependency it could even be a plain Dart package with the Jaspr sugar as an optional import.
I'd do it as a follow-up rather than in this PR: extract the current code verbatim, add the dev-mode names and the sugar, then point both sites at it. Happy to open that PR when you want it.
Generated by Claude Code
There was a problem hiding this comment.
Update for this thread: as of e5b1952 the scoping no longer lives in this repo. It moved to jaspr_class_scope in flutter_corelibrary (pulled from a git ref until it's released), with a builder that answers both open questions here:
- Inferring the owner: a component carries
@scopedCssand apart 'footer.scopes.dart'; the builder writes_$siteFooterScopeinto that part file, sostatic const _class = _$siteFooterScope— no type spelled, nothing hashed at runtime. - The hash: computed at build time from the component's package, path and name, and the build fails on two components that would share a suffix.
ClassName.shared, .name, .selector and + are unchanged at the call sites; every suffix changes, so the class names in the earlier screenshots differ, the styles don't. styles.dart is back to fonts, tokens, reset and container. CI is green on e5b1952 and the README's Styles section describes the new shape.
Generated by Claude Code
Jaspr collects the `@css` getters into one global stylesheet and scopes
nothing, so two components declaring the same class would silently style
each other. `tool/check_class_names.dart` reads `lib/` and fails when a
`ClassName('…')` literal is declared more than once or a class is written
as a raw string outside a declaration; the docs workflow runs it next to
`dart analyze`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
Reverts fee6bbb, per review: a source scan is the wrong tool. The next commit scopes class names to their component instead, the way CSS modules do, so a collision cannot happen in the first place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
Jaspr collects the `@css` getters into one global stylesheet and scopes
nothing, so `ClassName` now does what CSS modules do at build time:
`ClassName('af-grid', owner: Hero)` renders as `af-grid-<suffix>`, the
suffix five base-36 digits of an FNV-1a hash of the owner's type name.
Two components can use the same local name without meeting in the
stylesheet, and a raw string elsewhere matches nothing. Selectors and
`classes:` attributes are still spelled from the constant, so the suffix
is never written by hand.
`ClassName.shared` renders a name as written, for the classes that are a
contract with another file: the ones `landing.js` looks up, the ones the
docs' `global.css` uses for the same example frame and logo,
`af-landing`, and the `af-container` utility.
The rendered stylesheet differs from the previous build only in the
suffixes; screenshots of both themes are pixel-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
`ClassScope<T>` replaces the `owner:` argument on every class: a component
declares `static const _class = ClassScope<Hero>()` once and makes its
classes from it, `_class('af-hero-grid')`. The suffix is unchanged, so
the rendered page is byte-identical to the previous build. Two scopes
that would hash alike now fail the build the first time either renders.
The button variants carry their class before scoping, and the component
scopes it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv
The landing carried its own ClassScope/ClassName in lib/styles.dart, which hashed the Type at runtime. That mechanism now lives in jaspr_class_scope, where the suffix is computed at build time from the component's package, path and name, and a build step fails on two components that would share one. Each component that styles itself carries @scopedCss and a part directive; the builder writes its scope into the part file, which is generated on every build and not committed. Every suffix changes, so the rendered class names differ from the ones in this PR's screenshots; nothing about the styles themselves does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XirTyFcvXRo3Hs9PBUnt7r
jaspr_class_scope_builder now checks scopes across every package in the build, not only this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XirTyFcvXRo3Hs9PBUnt7r
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XirTyFcvXRo3Hs9PBUnt7r
jaspr_class_scope now keeps the component's name in the constant it writes, so `_$heroScope` reads `_$HeroScope`. The suffixes, and with them every rendered class name, are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XirTyFcvXRo3Hs9PBUnt7r
Summary
Brings advanced-forms.leancode.co in line with the LeanCode design system (the LNCD handoff from Claude Design) and makes the palette a single source of truth across the whole site — the follow-up to leancodepl/ciach#47.
Why one source. The colors lived in nine places across four languages:
landing/web/landing.css(light + dark),docs_app/app/global.css(the same--af-*set again, plus a third copy as literal hex in the Fumadocs@themeblocks), both OG routes (TS constants),components/logo.tsx,island_frame.dart(the accent inside the live Flutter demos), and four SVGs (app/icon.svg,public/logo-*.svg,landing-icon.svg). They had drifted: three different yellows (#edff2f,#f9ff07,#f6ff6b), a#050505ground, blue-tinted grays.The source.
docs_app/palette.jsonnames the design system's swatches (black,white,ctaYellow #f0ff00,bodyGray,mutedGray,surface/surface2,error,success, plus the site's own tints for the light theme) and the light/dark--af-*themes built from them, each token a swatch or a swatch at an alpha.The generator.
npm run palette:generate(scripts/palette.mjs) writes every consumer's copy, all committed:app/af-tokens.cssandlanding/web/af-tokens.css— the tokens as CSS variables (global.cssimports one; the landing links the other beforelanding.css, andlanding.mjscopies it intopublic/)lib/palette.generated.ts— for the OG cards andLogoMark, via the smalllib/palette.tswrapper (withAlpha)flutter/lib/support/palette.generated.dart—afLight/afDarkforislandThemelanding/lib/palette.generated.dart— the landing'stheme-colordata-palette="ctaYellow"in place, so the logos stay single files of artworknpm run palette:checkfails CI when any of them drifts (wired intodocs.ymlnext toexamples:check).Also in here. The Fumadocs
--color-fd-*variables are now@theme inlinereferences to the tokens (no dark block, no third copy); alpha variants derive viacolor-mix()in CSS andwithAlpha()in TS; the unused--af-accent-2is gone (hover borders use the accent). Light theme values that the design system doesn't specify (body gray, strong hairline, the accent and signals as text on white) are named…OnLighttints inpalette.json, derived from the design-system hues.Also merged in: the landing's move to Jaspr
@cssgetters (claude/busy-bell-xjh9eo, commit9bd3648, combined ind1c5acc).landing.cssis gone; every component styles itself next to itsbuild, andlib/styles.dartholds the fonts, tokens, reset and utilities — ciach's structure. The palette plugs in the same way it does there:_tokensspreadsafLight/afDark'scssVariables(no more typed-out colors), the hero glows, primary-button hover and pill wash derive throughcolorMix()and tokens, and--af-accent-2is gone. Becausemain.server.dartnow imports the gitignoredmain.server.options.dartthatjaspr_builderwrites, the docs CI runsdart run build_runner buildbefore analyzing the landing page.Also in here: every landing class has one owner (
d55102a, from the review thread onbutton.dart). Each component keeps the classes it renders as typedClassNameconstants and spells its selectors andclasses:attributes from them; variants carry their class as enum values. Pieces two files styled or rendered by string are now components of their own —CopyButton,Logo(withlarge),ButtonRow(af-actions), theCardfamily incard.dart, theSectionvocabulary (Eyebrow,Lead,Checklist,MoreLink,DemoSlot,Section.plain) — andstyles.dartkeeps exactly one class of its own,container. The nameslanding.jsand the docs'global.cssdepend on are unchanged; the rendered stylesheet differs only in the moved and renamed rules, and both themes' screenshots pixel-match the previous build. Because one stylesheet covers the whole page, class names are locally scoped byjaspr_class_scope(e5b1952,56a287c, from a git ref until the package is released). The mechanism started here as aClassScope<T>instyles.dartthat hashed theTypeat runtime; it now lives in that package and computes the suffix at build time instead. A component carries@scopedCssand apart 'hero.scopes.dart'; the builder writes its scope into that part file from the component's package, path and name, and a check phase fails the build when two components anywhere in the package graph would share a suffix._class('af-grid')then renders asaf-grid-<suffix>, andClassName.sharedkeeps the nameslanding.jsand the docs'global.cssknow as written. The part files are generated on every build and not committed, so the docs workflow'sbuild_runnerstep covers them next to the Jaspr options.Out of scope on purpose: typography and shape (Space Grotesk / JetBrains Mono, the 14px radii and pill buttons stay).
Test plan
npm run lint,npm run format,npm run types:check,npm run palette:check— cleandart analyze --fatal-infosanddart format --set-exit-if-changedinlanding/— cleandart format --set-exit-if-changed lib/supportindocs_app/flutterunder the package's 3.8 language version — cleannpm run landing:build(Jaspr) andnext build— succeed;/og/landing.pngand/og/[...slug]prerender<style>diffed rule-by-rule against the previous build (only the moved/renamed rules differ); full-page screenshots of/in both themes pixel-match itbuild_runnerwrites the 11 part files,jaspr buildrenders 67 scoped class names, and the names outside a component —af-landing,af-container, theaf-example*andaf-tab-inputcontract withlanding.jsand the docs'global.css— are unchanged. The suffixes moved when the mechanism did, so the class names in the screenshots above are one build behind; the pixels are not./,/docs,/docs/faqin both themes vianext start: computed--af-bg/--af-accentand--color-fd-*resolve to the palette in each theme, no page errors; both OG cards render with the new yellowflutter analyze/flutter testindocs_app/flutter— no Flutter SDK in my environment; CI's build job runs them🤖 Generated with Claude Code
https://claude.ai/code/session_013Tw8hY1dZyp7s18pUTCacv