Skip to content

Add @workflowbuilder/ui + ui-tokens packages (1/3: packages) - #44

Open
librowski wants to merge 42 commits into
mainfrom
ui-package
Open

Add @workflowbuilder/ui + ui-tokens packages (1/3: packages)#44
librowski wants to merge 42 commits into
mainfrom
ui-package

Conversation

@librowski

@librowski librowski commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Part 1 of 3. Stacked series for the @workflowbuilder/ui work:

  1. this PR - add the packages (no consumers touched yet)
  2. Migrate consumers to @workflowbuilder/ui (2/3: consumers) #45 - migrate consumers onto it
  3. UI Library docs: page-per-component + source-generated tables (3/3) #46 - UI Library documentation

Summary

Brings the overflow-ui Base UI rewrite into the monorepo as the publishable @workflowbuilder/ui package (+ private @workflowbuilder/ui-tokens). Nothing consumes it on this branch - sdk / demo / ai-studio still build against the published @synergycodes/overflow-ui@1.0.0-beta.27; the switch-over is #45.

How to review

Most of the file count is the vendored library (packages/ui/src/**) - low review effort. The real surface, in suggested reading order:

  1. packages/ui/combine-css-bundle.mts - post-build CSS steps; stamps the @layer ui.base, ui.component; order statement into every dist stylesheet (duplicates are no-ops), so any import order and any bundler is safe. This closes the inverted-cascade bug where barrel consumers got ui.base beating ui.component (invisible secondary buttons).
  2. packages/ui/scripts/check-built-css.ts - the built-CSS guard, 5 checks: no var(var()), every rule layered (no exceptions), order statement leads every file, only known layer names, exported entrypoints exist + no @import in dist.
  3. packages/ui/src/components/date-picker/ - react-day-picker's stylesheet joins ui.base via @import ... layer(); calendar theming lives in ui.component and wins by layer order (the old unlayered-specificity hack is gone).
  4. packages/tokens/src/ - manifest-driven pipeline: config.ts uses tokens.json keys verbatim, manifest.ts validates them at build start and derives every path once; brokenReferences: 'throw'; dist/tokens.css is fully inlined (survives being copied out alone).
  5. packages/ui/postcss-box-sizing.mts (+ spec) - injects box-sizing only inside an allowlist of styling contexts; @keyframes steps are never touched. Rationale + rejected alternatives: packages/ui/postcss-box-sizing.decision-log.md.
  6. packages/ui/tsconfig.json + tsconfig.node.json - component program vs build-tooling program. Mixing them made TypeDoc (docs generator) and vite-plugin-dts hang; pnpm typecheck runs both.
  7. Release wiring: release-ui.yml, scoped tags (@workflowbuilder/ui@X.Y.Z); plain v* tags no longer trigger releases after this merges.

Review history

  • 2026-07-31: 10-dimension agent review pass + rebase onto main (CSS layer contract enforced, packaging fixes, @phosphor-icons/react externalized).
  • 2026-08-07: 6-finding external review - all confirmed and fixed (inverted cascade CRITICAL, token-build silent failures, guard blind spots, box-sizing in keyframes, non-self-contained tokens.css, config/file name coupling), then a 4-reviewer adversarial pass over the fixes (guard allowlist by path, conditional exports walking, duplicate-config validation, type-safety of build files).
  • 2026-08-10/11: input/text-area wrapper-padding focus fix, symmetric ease modal timing (exit fade read as abrupt), tsconfig split after the TypeDoc hang surfaced in UI Library docs: page-per-component + source-generated tables (3/3) #46.
  • 2026-08-11: OpenCode (GPT-5.6) adversarial round - 10 findings, all verified on code and addressed here or tracked:
    • handle hover/connecting states shrank (per-rule border-box injection overrode the base rule's content-box opt-out) - state rules now repeat the opt-out, plugin spec documents the contract;
    • variable defaults (--ax-public-* and tokens.css) moved into @layer ui.base - a consumer's unlayered :root override now wins regardless of stylesheet load order; check-built-css dropped its :root and token-file exemptions;
    • var() guard widened (empty/undashed first argument), combiner fails on an empty assets dir, manifest rejects file-name collisions after normalization, tokens build cleans dist and the UI build fails fast when tokens dist is missing;
    • react peer widened to ^18.0.0 || ^19.0.0 (Base UI supports 17-19; no React-19-only API in src);
    • duplicate acc7-100 token in the Figma export and manifest-as-source-of-truth for token file names tracked as backlog tasks.

Verification

  • pnpm build (guard runs inside) + tests (ui 60, tokens 8) + typecheck (both programs) + lint + knip - green.
  • Real-consumer check: fresh Vite app installing the package from disk, README import path - secondary/ghost buttons render correctly in light/dark; date-picker calendar verified in both themes.
  • Known interplay: an xyflow stylesheet outside the layer contract can override layered port rules. Solved on Migrate consumers to @workflowbuilder/ui (2/3: consumers) #45 by moving xyflow (and the SDK resets) into ui.base - a single top-level layer, so load order cannot invert the cascade.

Notes

librowski-synergy and others added 18 commits July 30, 2026 18:57
- SegmentPicker: type onChange/onSelect/onClick event as React.MouseEvent
  instead of MouseEventHandler (types vs runtime mismatch in public API)
- Modal: apply className and ...rest to the same root element with ref,
  not split across the modal and content divs
- Button: log a clear error for children that match no variant instead of
  silently rendering null
- tokens: drop the swallowing try/catch in ejectTokens so a failed write
  aborts the build instead of feeding empty/stale JSON to the CSS step
- tokens: lift tsconfig lib to es2021 (String.replaceAll)
- add per-package lint-staged configs so in-package tsc runs on commit
- remove empty output.json and stale TODO in src/index.ts
- rebrand vite lib + combine-css plugin names away from Overflow UI
- Shape: replace '' sentinel with explicit 'default'
- Snackbar/NodePanel JSDoc match actual behavior; check-built-css comment
  no longer references a non-existent stylelint rule
- Switch: drop redundant styles prop (keep className)
- Tooltip: inline the constant open/close delays, drop the speculative
  delay context (placement context kept)
- tokens: scope codeChunks to generateCSSBundle; fix double-slash token dir
Set up Vitest in @workflowbuilder/ui (jsdom) and @workflowbuilder/ui-tokens
(node) and wire both into the root pnpm test.

Extract the pure helpers that were buried in component files into sibling,
non-barrel modules so they can be unit-tested without rendering: date-picker
date-utils (parseDateValue/normalizeInitialValue/dayjsTokenToDateFns), and the
menu/tooltip placement mappers. Public types are re-exported so the package API
is unchanged.

Coverage: button structural guards, rangeBetween, getValidShape, the date
parsing/timezone logic, placement->side/align + offset mapping, and toFileName.
50 ui + 4 tokens tests. Exclude specs from the dts build so they don't ship.
- narrow @base-ui/react peer range to ~1.4.0 (1.5/1.6 regressed transitions)
- resolve the bundle-vs-dependency split: date-fns, react-day-picker, clsx and
  Phosphor are bundled into dist, so move them to devDependencies; only the
  external react-textarea-autosize stays a runtime dependency (peers unchanged)
- rewrite npm description/keywords away from the diagram-library boilerplate
- fix the README dev section (real pnpm --filter commands; no preview-page) and
  drop leftover 'Overflow UI' branding
- correct CLAUDE.md: ui-tokens is private (not in changeset ignore)
Retire the single-package v* tag scheme now that the repo publishes two
packages. Each package releases via its own scoped tag and workflow:
- @workflowbuilder/sdk@X.Y.Z -> release-sdk.yml (trigger + version-parse updated)
- @workflowbuilder/ui@X.Y.Z  -> release-ui.yml (new; OIDC trusted publisher,
  provenance, build:ui, version/idempotency checks, GitHub Release)

Add a UI lint/typecheck/test/build job to pr-check. Update RELEASE.md and
CLAUDE.md for the scoped-tag two-package flow and correct the stale changeset
ignore claim. Each package needs its own npm trusted publisher (manual, noted
in RELEASE.md).
ui is the sole consumer of Base UI and is version-locked to 1.4.x (later
versions regressed transitions), so it should own that pinned version rather
than delegate it to every consumer via a peer range. Move @base-ui/react to
dependencies (catalog: -> 1.4.1 at publish); react / react-dom stay peers as
true singletons. Consistent with react-textarea-autosize, the other external.
react-day-picker v9 ships no styles of its own; the component never imported
its stylesheet, so the calendar grid, month caption and nav rendered with
browser defaults (vertical nav, bordered day buttons). Import
react-day-picker/style.css so libInjectCss bundles it into dist (and the docs
combined index.css), giving the calendar its real layout.
react-day-picker ships a default blue accent and no hover state. Re-theme it
to the design tokens: selected days get a filled --ax selected background
instead of the accent ring, today uses a themed accent color, and day buttons
gain a hover background (react-day-picker has none). New tokens:
--ax-public-date-picker-date-{hover-background,selected,today}-color.
react-day-picker paints the prev/next chevrons with --rdp-accent-color, which
defaulted to a raw blue and clashed with the rest of the themed calendar. Paint
them with a neutral nav token instead, and retheme --rdp-accent-color (focus
rings, range endpoints) to the design-token accent so no raw blue remains.
- IconSwitch picked the thumb icon from the controlled `checked` prop, so in
  uncontrolled mode the icon never swapped. Render both icons and swap them via
  the switch's `data-checked` state in CSS (works controlled + uncontrolled).
- Accordion fired `onToggleOpen` twice when the chevron was clicked: the
  Collapsible button's toggle and the header's onClick both ran. The header is
  the single click target now (Collapsible is display-only, controlled via
  isExpanded); also fixed aria-expanded to reflect isExpanded.
- @base-ui/react is a regular dependency now, not a peer (the entry still
  claimed peer); react / react-dom are the only peers.
- document the review-driven breaking changes (Switch styles prop removed,
  Shape '' -> 'default', SegmentPicker event type, Modal prop target) and add a
  Fixed section (calendar styling, Accordion double-fire, IconSwitch, Button).
First release under the @workflowbuilder/ui name, versioned 2.0.0 to sit on the
Workflow Builder 2.x line. Rewrite CHANGELOG.md from scratch: a short note that
the library moved from @synergycodes/overflow-ui (with a link to its old
changelog) plus a single 2.0.0 entry covering the highlights and the migration
deltas. Update the README technology note to match.
…t regressions

Several component stylesheets (node ports, radio buttons, edge labels,
snackbar, node icon/description, base typography) shipped rule bodies
outside the ui.base/ui.component layers, letting unlayered CSS beat
layered CSS regardless of specificity - the same bug class that shipped
collapsed decision-node ports (WB-190).

Extend check-built-css.ts with a second, independent check that fails
the build if any dist CSS rule sits outside an @layer block, with
documented exceptions for :root custom-property blocks, the deliberate
react-day-picker override, and verbatim ui-tokens output.
…ort barrel gaps

- License: Apache-2.0 (was MIT), LICENSE file copied from root and added
  to the published files array, publishConfig.access set to public -
  matches the packages/sdk precedent.
- @phosphor-icons/react moves from devDependencies to a regular
  dependency and joins the Vite externalPackages list, so it's imported
  rather than bundled - same treatment as @base-ui/react.
- Export CollapsibleProps and IconSwitchProps, which were already
  reachable through the barrel but not exported as types.
- Extract tooltip's open/close delay constants and placement context
  into tooltip-context.ts so the barrel export of tooltip.tsx no longer
  leaks internals.
- Fix a stale "Overflow UI" reference in css-layers.md.
packages/tokens had no eslint config, no lint/typecheck scripts, and no
README, and its checks weren't wired into pr-check.yml despite feeding
packages/ui's build. Add the non-React eslint re-export used by
packages/types and packages/execution-core, document the tokens.json ->
Style Dictionary -> --ax-* CSS pipeline, and fold lint/typecheck/test
into the existing UI CI job.
Stale generated artifact from an earlier TypeDoc setup; unreferenced
anywhere in the repo and not produced by the current docs build.
librowski-synergy and others added 21 commits July 31, 2026 15:56
Replaces the hand-rolled CSS scanner with postcss (already a
devDependency). Also tightens the :root exemption: a top-level :root
block now passes only when every declaration is a custom property.
- config.ts names sets exactly as tokens.json exports them; buildManifest()
  validates the config up front and lists the available keys on mismatch
- Style Dictionary runs with brokenReferences: 'throw' - a dangling token
  reference fails the build instead of shipping literal {token.path} values
- dist/tokens.css inlines the primitives (no relative @import), so the file
  survives being copied out of the package alone
In the built barrel, libInjectCss evaluates per-chunk CSS before the entry
CSS, so the order statement loaded last and the cascade inverted (ui.base
beat ui.component) for every consumer following the README import path -
secondary/ghost buttons rendered white-on-transparent. Duplicate statements
are no-ops, so stamping the statement into each dist stylesheet makes any
load order safe: barrel, subpath imports, and files copied out alone.

combine-css-bundle now also throws when dist/assets is missing instead of
silently skipping the published entrypoints.
The calendar was the one component whose styles lived outside the layer
contract: react-day-picker's stylesheet shipped unlayered and our theming
had to stay unlayered too, winning by specificity. The stylesheet now joins
ui.base at import time (@import ... layer(ui.base)) and the theming moved
into @layer ui.component, winning by layer order alone.
The plugin visited every rule, so @Keyframes steps got box-sizing too and
the declaration became part of the animation (animated values override
normal author styles while running). Injection now allowlists plain styling
contexts (media/supports/container/layer) and skips anything unknown - a
missed redundant declaration is easy to spot, a wrongly injected one is not.

vite-plugin-dts excludes root-level *.spec.mts: a spec escaping the filter
drags vitest's type graph into the ts program and stalls the build.
…ecks

Three new checks close the guard's blind spots:
- every dist stylesheet must lead with the @layer order statement (the
  inverted-cascade class was invisible to the old checks)
- only layer names declared in src/styles/layers.css may appear - a typo
  creates an undeclared layer that silently wins the cascade
- every *.css entry in package.json exports must exist in dist, and no dist
  stylesheet may use @import (self-containment)

The rdp- substring exemption is gone: react-day-picker styles are layered
now, so the guard runs with no exceptions. Shared FailureReport type, all
parsing through postcss.
…ename

Two latent traps from adversarial review: a set listed twice (or in both
groups) made two builds race for one output file, and the theme filter
matched primitive names as substrings of the whole source path - a set
whose kebab name appears in the directory prefix (e.g. 'Tokens' vs
./dist/tokens/) would have silently emptied every theme.
Guard (check-built-css.ts):
- allowlist matches exact dist paths, not basenames - a component entry
  named 'tokens' no longer inherits the token files' exemption
- exports check walks conditional-export objects, not just string targets
- layers.css is parsed for the @layer statement with a clear error instead
  of an unchecked .first cast that crashed on a leading comment
- comments inside :root token blocks are no longer flagged

Plugin (postcss-box-sizing.mts):
- ancestor walk is properly typed (Document in the parent chain surfaced
  TS2322 under strict) and the :root skip covers grouped/qualified selectors
- dropped the inert vendor-prefix stripper; spec grew to 10 cases

tsconfig now includes the root build .mts files and scripts/ so these files
leave the typecheck blind spot; specs and vite.config.mts stay out - with
allowImportingTsExtensions (needed for the config's .mts imports) or a
vitest-importing spec in the program, tsc stalls for minutes.
Decision log for keeping build-time per-rule injection over lint-based and
selector-based alternatives (box-sizing does not inherit; portals escape
subtree selectors; a global reset leaks onto consumer elements). css-layers.md
points at the mechanism; CHANGELOG no longer claims styles.css establishes
the layer order - every stylesheet carries it now.
…dding

The padding sits on the wrapper, not the form element, so clicks in that
area never focused the field. Forward pointerdown to the element;
adornment clicks keep their own behavior.
ease-out on the exit transition drops most opacity in the first frames,
which reads as an abrupt vanish; the pre-migration modal used symmetric
ease. Applied to popup and backdrop in both directions.
Extending tsconfig.json's include with root build files made every tool
that reads the package tsconfig (TypeDoc in the docs generator, and
vite-plugin-dts before it) pull them into its program - TypeDoc hung
indefinitely. The component program is src-only again; build files and
scripts typecheck through tsconfig.node.json, chained in pnpm typecheck.
The build injects border-box per rule, so hover/connecting states of the
opted-out handle silently shrank from 1rem to 0.75rem. State rules now
repeat the opt-out; the plugin spec documents the contract.
Unlayered :root defaults raced consumer overrides by load order: a lazily
loaded component stylesheet silently restored the default. The build now
wraps top-level :root blocks in @layer ui.base (new postcss plugin for
component CSS, copy transform for the generated token files), so any
unlayered consumer override wins by layer rules. check-built-css drops
its :root and token-file exemptions and also rejects var() calls whose
first argument is not a dashed ident (var(), var(no-dashes)).
Base UI itself supports 17-19 and no React-19-only API is used; the ^19
peer was declared from the build target, not an actual requirement.
Dialog.Popup carried a full-screen wrapper, so every click landed inside
the popup and Base UI never saw an outside press - the modal could only
be dismissed with the close button or Esc. The popup is now the dialog
window itself (centered, capped at the viewport with its own scrolling
content area), and the orphan .backdrop-close class is gone.
Its Positioner was the only one without the shared popup class, so a
calendar opened inside a modal rendered behind it - which the SDK papered
over with a global z-index override. The 4px sideOffset goes away with
it: the shared class already carries that gap.
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.

3 participants