Skip to content

#15 Add components-react and components-web packages with HTML/React toggle - #40

Draft
trackleft wants to merge 22 commits into
mainfrom
issue/15-react-components
Draft

#15 Add components-react and components-web packages with HTML/React toggle#40
trackleft wants to merge 22 commits into
mainfrom
issue/15-react-components

Conversation

@trackleft

@trackleft trackleft commented Aug 26, 2026

Copy link
Copy Markdown
Member

Adds @az-digital/components-react (React) and @az-digital/components-html
(framework-agnostic HTML/CSS) as parallel implementations of the design
system's components, starting with Button. Both packages share the same
prop shape and are consumed live from source in Storybook via a Vite
alias, no build step required during development.

The Button story wires up a toolbar "Implementation" switcher (HTML |
React) plus a synced docs code panel, so a single set of controls can
demo both implementations side by side. Button styling loads Arizona
Bootstrap's real CSS from the CDN rather than a local stub, so classes
like .btn-red and .btn-outline-blue match production exactly.

How to test

Go to : https://review.digital.arizona.edu/design/pr-40/?path=/story/components-button--outline&globals=implementation:react

  • In the left sidebar, expand Components → Button, then click "Default." Near the top of the page there's a toolbar with an "Implementation" dropdown — flip it between HTML and React. The button preview in the middle of the page and the "Code" tab at the bottom of the page should both update to match every time you switch.
image
  • Still on that same left sidebar, click into the "Controls" tab at the bottom of the page (next to "Code") and change the Style/Color options there. Then click through the other Button sub-pages in the sidebar — Outline, Blue, and Disabled — checking the same Implementation toggle on each.
  • In the left sidebar under Components → Button, click "Success." With the Implementation dropdown set to HTML you should see a green button; switch it to React and instead of a button you should see a message box saying "Button doesn't have a react implementation yet."
image image
  • In the left sidebar under Components → Button, click "Tokens." This shows a searchable table in the middle of the page listing 15 values — colors, padding, font size, border, etc. Every row should show a real value, not blank or undefined.
  • Near the top of the left sidebar (above Components), expand Tokens and click "Docs." Scroll down the page until you reach the "Component tokens" heading — the same 15 Button values from the previous step should be listed there too.
image image

…toggle

Adds @az-digital/components-react (React) and @az-digital/components-web
(framework-agnostic HTML/CSS) as parallel implementations of the design
system's components, starting with Button. Both packages share the same
prop shape and are consumed live from source in Storybook via a Vite
alias, no build step required during development.

The Button story wires up a toolbar "Implementation" switcher (HTML |
React) plus a synced docs code panel, so a single set of controls can
demo both implementations side by side. Button styling loads Arizona
Bootstrap's real CSS from the CDN rather than a local stub, so classes
like .btn-red and .btn-outline-blue match production exactly.
…ision

@az-digital/components-web read as "Web Components," but this package is
plain framework-agnostic HTML/CSS markup, not the Lit-based custom
elements from az-web-components (a separate, actual Web Components
project under consideration for issue #16). Renamed to components-html
to keep the two concepts unambiguous.
Not every component needs both a React and HTML implementation. Extract
the render/source branching into a shared implementations.tsx helper:
stories now declare a per-story `implementations` map keyed by whichever
kinds they actually have, and the toolbar switcher falls back to a
"not implemented yet" placeholder (canvas and docs code panel both) for
any kind a story doesn't provide, instead of assuming every component
ships both.
Chosen over reactstrap after checking maintenance signal: react-bootstrap
has commits within the last few months vs. reactstrap's ~2-year-stale
repo, ~3x the weekly npm downloads, and 2x the GitHub stars. It also
doesn't ship its own CSS (we already load Arizona Bootstrap's), and
builds class names as `${prefix}-${variant}` — the same pattern our
hand-rolled components already use — so custom Arizona Bootstrap
variants like `red`/`blue`/`sky` work via its `variant` prop without any
type augmentation, verified live with a throwaway Accordion probe
(collapse/expand, focus ring, ARIA all worked correctly, then removed).

Simple presentational components (Button) stay hand-rolled since there's
little to gain from wrapping them; react-bootstrap is for components
with real interactive behavior (Modal, Dropdown, Accordion, Offcanvas,
Tooltip/Popover, Nav/Tabs) where hand-rolling focus-trap/keyboard/ARIA
behavior correctly is expensive and easy to get subtly wrong.
Documents the React/HTML dual-implementation pattern, the Storybook
toggle and its docs-panel-sync gotcha, partial implementation coverage
with placeholders, and when to hand-roll vs. wrap react-bootstrap.

Symlinked into .github/instructions/ (with an added `applyTo` glob in
the frontmatter) so VS Code's GitHub Copilot picks up the same guidance
when editing matching paths, mirroring the CLAUDE.md -> AGENTS.md
symlink from issue #14 — one canonical file instead of two to keep in
sync.
@github-actions

Copy link
Copy Markdown

Design tokens had no component layer yet — Button's color prop reached
straight down to the brand primitives (az.color.brand.red/blue) with no
named indirection point specific to the component. Add
az.component.button.color.{red,blue}, aliased to those primitives, so
Button's actual token dependency is explicit and can be repointed later
(e.g. once a semantic layer exists) without touching the component.

Add a Tokens story rendering swatchbook's <TokenTable filter="az.component.button.**" />
so the component's real, resolved token values are visible and
interactive right next to its other stories, instead of only living in
the global token catalog.
Component-tier tokens (az.component.*) were only visible mixed into the
unfiltered "All tokens" table, with nothing calling out that they're a
distinct tier from the base/brand tokens. Give them their own filtered
section, same pattern as the existing Colors section, and link out to
Button's own filtered Tokens story as an example of the per-component view.
Extends az.component.button.* beyond color to cover the rest of
Button's real CSS surface: padding, font size/weight, border
width/radius, disabled opacity, and sm/lg size overrides — 15 tokens
total, values pulled directly from Arizona Bootstrap's compiled
.btn/.btn-sm/.btn-lg CSS (verified against the CDN build), not
estimated. Colors keep aliasing to az.color.brand.*; everything else is
hard-coded since there's no base/semantic tier for spacing or
typography yet to alias to.

Also downgrades core/valid-dimension to warn and switches dimension
values to the DTCG { value, unit } object format. This isn't cosmetic:
the plain legacy-string format (e.g. "1.25rem") passes lint but
@terrazzo/plugin-css's dimension serializer can't read it, silently
compiling every dimension token to `undefinedundefined` in tokens.css.
Verified directly by building both ways. Colors don't have this
problem — legacy hex strings compile correctly — so this is
dimension-specific.
Covers what the Button work just established: every component needs
its own az.component.<name>.* tokens covering its real structural
properties, not just color; colors alias to existing base tokens while
everything else without a backing base/semantic tier gets hard-coded to
the real production value (sourced from Arizona Bootstrap's actual CSS,
not guessed); and dimension tokens specifically need the DTCG
{ value, unit } object format since the legacy string format compiles
to broken CSS even though lint only warns on it.
Adds `success` to components-html's ButtonColor (Bootstrap's stock
semantic color, not an Arizona brand color — components-react doesn't
support it). The Success story demonstrates the partial-implementation
placeholder at the variant level rather than the whole-component level.

Along the way, found and fixed a real bug in the mechanism itself:
overriding parameters.implementations at the story level doesn't work
because Storybook deep-merges parameters objects, so a story's narrower
map gets merged into the meta-level one instead of replacing it — the
key being omitted quietly survives. Fixed by reading a separate
parameters.implementationsOverride key instead, which nothing at the
meta level defines, so there's nothing to merge against. Verified live:
the canvas and docs code panel both now correctly show the "no react
implementation" placeholder instead of silently falling back to a
default-colored React render.
Storybook deep-merges parameters objects, so a story can't override
parameters.implementations by setting a narrower map at the story level
— it merges into the meta-level default instead of replacing it,
silently keeping keys the story meant to omit. Document the working
fix (a separate implementationsOverride parameter, established while
adding Button's HTML-only Success variant) as its own subsection under
Partial implementation coverage, with the broken version shown first
since it looks correct until the toggle is actually tested.
Three comments still said things that were true a few commits ago but
aren't now: the Tokens story and tokens.mdx both said component tokens
are "aliased to base tokens" (only 2 of 15 are — the rest are
deliberately hard-coded), and components-react's Button.tsx still
claimed 1:1 prop parity with components-html despite components-html's
`success` color having no React counterpart. Caught by re-reading the
docs against the actual code rather than assuming they were still
accurate.
Combines the Storybook MCP addon (main) with real React/HTML Button
components (this branch).

Conflict resolutions:
- tokens.json: kept both branches' new brand colors plus the
  az.component.button.* tree; the colliding `white` token keeps main's
  definition (#FFFFFF, no $extensions) per user decision.
- tokens/dist/* and .swatchbook/tokens.d.ts: regenerated rather than
  hand-merged (npm run build:tokens).
- package-lock.json: regenerated via npm install.
- packages/storybook/.storybook/main.ts: kept both addons
  (addon-designs, addon-mcp) and this branch's source-alias viteFinal.
- debug-storybook.log: deleted (also removed from tracking on this
  branch; see .gitignore change).
const label = escapeHtml(text);

if (htmlTag === 'button') {
return `<button type="button" class="${classes}"${disabled ? ' disabled' : ''}>${label}</button>`;
return `<button type="button" class="${classes}"${disabled ? ' disabled' : ''}>${label}</button>`;
}

return `<a href="${escapeHtml(href)}" role="button" class="${classes}"${disabled ? ' aria-disabled="true" tabindex="-1"' : ''}>${label}</a>`;
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