Skip to content

Add design token extraction for button components#351

Open
bartveneman wants to merge 9 commits into
mainfrom
claude/design-token-extraction-hfk16p
Open

Add design token extraction for button components#351
bartveneman wants to merge 9 commits into
mainfrom
claude/design-token-extraction-hfk16p

Conversation

@bartveneman

Copy link
Copy Markdown
Member

Summary

This PR adds a new feature to extract and analyze design tokens from button-like components on any website. It includes a complete pipeline for parsing CSS, matching selectors to DOM elements, resolving the CSS cascade, and suggesting common token combinations.

Key Changes

  • New API endpoint (/api/design-tokens) that accepts a URL and optional component type, returning extracted design tokens and suggested combinations
  • Token extraction engine (extract-tokens.ts) that:
    • Parses CSS and extracts all style rules (including those in @media/@supports)
    • Matches selectors to DOM elements using structural matching (excluding pseudo-elements)
    • Strips interaction pseudo-classes (:hover, :focus, :disabled, etc.) and groups tokens by state
    • Resolves the CSS cascade correctly (!important > specificity > source order)
    • Filters declarations to only design-token properties (colors, fonts, borders, etc.), excluding layout/spacing
  • Component presets system for defining which selectors to target (currently supports button-like elements: button, .button, .btn, [role="button"])
  • Suggestion grouping that deduplicates elements with identical base token sets and ranks by frequency
  • New UI page (/component-tokens) for interactively analyzing any website's button design tokens
  • Comprehensive test suite covering cascade resolution, specificity, source order, state handling, and pseudo-element exclusion

Implementation Details

  • Uses @projectwallace/css-parser for CSS parsing and @projectwallace/css-analyzer for specificity calculation
  • Leverages linkedom for server-side DOM parsing and selector matching
  • State pseudo-classes are handled separately since a static DOM can't have real :hover/:focus states—they're stripped before matching and tracked as separate token buckets
  • Design token properties are prefix-matched against a curated list (background, color, border, font, etc.) to exclude spacing/layout properties
  • Results are cached at 10 minutes (s-maxage=600) for performance

https://claude.ai/code/session_01WbLSKV7rJBokuRvBRNeWRf

claude added 2 commits July 9, 2026 11:35
New, additive /api/design-tokens endpoint: scrapes a URL's HTML+CSS,
finds button/.button/.btn/[role=button] elements via linkedom, matches
CSS rules against them with element.matches(), and resolves the CSS
cascade (specificity, !important, source order) to surface only
design-token-relevant declarations (background/color/border/shadow/
font, not spacing) per element and per interaction state
(hover/focus/active/disabled). Results are grouped into suggested
token combinations. No existing files were modified.
New (public)/component-tokens page reuses the existing CSS-form URL
submit flow (same pattern as lint-css), fetches the design-tokens API
for the submitted URL, and renders the raw JSON in a <pre> so the
extracted token combinations can be eyeballed. File/raw-CSS tabs of
the form are present but unused. No existing files were modified.
@netlify

netlify Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploy Preview for projectwallace ready!

Name Link
🔨 Latest commit 76b1aec
🔍 Latest deploy log https://app.netlify.com/projects/projectwallace/deploys/6a4fe12597674100083138a7
😎 Deploy Preview https://deploy-preview-351--projectwallace.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

claude added 7 commits July 9, 2026 12:51
/api/design-tokens no longer takes a ?component= selector; it now runs
the extraction pipeline once per known preset (button, heading) and
returns a report keyed by component name. Also adds the heading
(h1/.h1/.heading-1) preset, and switches to the non-deprecated
compareSpecificity export from @projectwallace/css-analyzer/selectors
(the main-index one is documented as its inverse, which is why the
cascade sort previously needed flipped arguments).
get_design_tokens fetched the raw, unresolved url string directly for
the page's HTML, unlike get_css which normalizes it through
resolve_url() first. Any protocol-less input (e.g. "example.com" -
literally the form's own placeholder text) made the native fetch()
throw "Invalid URL" inside the Promise.all, which propagated past the
try/catch in +server.ts as a generic 500 that the page always reported
as "Something went wrong."

Now the url is validated/normalized via resolve_url() up front (same
as get_css), and the HTML fetch failure is caught and turned into a
proper structured error instead of an uncaught rejection.
padding is part of a component's visual shape, not just layout noise
like margin/width/gap - add it to the design-token property allowlist
alongside the already-present outline.
Drop the bare "button" tag selector in favor of more specific matches:
button[type="submit"], input[type="button"], .button/.btn classes,
[role="button"], and suffix classes like [class*="-button"] and
[class*="-btn"].
Wrap the button preset's selectors in :is(...):not([class*="-link"])
so fuzzy matchers like [class*="-button"] don't pick up elements whose
class also identifies them as a link (e.g. "nav-link"). Component
presets are now single compound selector strings instead of arrays
joined with a comma.
[class*="-link"] only caught hyphen-delimited class names (e.g.
"nav-link"). Add a second :not([class*=":link"]) clause so
colon-delimited variants (e.g. "pd:link") are excluded too.
New independent module (src/lib/resolve-css-variables.ts) that:
- extracts the winning value of every custom property declared
  unconditionally on :root or html (same cascade rules as elsewhere -
  specificity, !important, source order - explicitly skipping anything
  nested inside a conditional at-rule like @media, since that can't be
  known without evaluating the condition)
- substitutes var(--name[, fallback]) references in an arbitrary value
  string against that map, recursively resolving chained references
  and falling back gracefully on unknown/circular references

Framework-agnostic and reusable outside this feature. Wired into
/api/design-tokens (now returns root_custom_properties alongside the
existing per-component report) and into the component-tokens debug
page as a checkbox that resolves variables client-side, instantly,
using data already in the initial response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants