Skip to content

refactor(web): migrate styled-components to Tailwind#1873

Merged
tyler-dane merged 38 commits into
mainfrom
refactor/styled-components-to-tw
Jun 23, 2026
Merged

refactor(web): migrate styled-components to Tailwind#1873
tyler-dane merged 38 commits into
mainfrom
refactor/styled-components-to-tw

Conversation

@tyler-dane

@tyler-dane tyler-dane commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What

Closes #1629

Completes the migration of packages/web off styled-components onto a hybrid Tailwind system, and removes the dependency entirely.

  • Styling system: inline Tailwind utilities for one-off layout/state; c-* recipes (Tailwind v4 @utility in packages/web/src/index.css) for reusable component recipes and complex third-party selectors.
  • Theming-ready: all theme-dependent colors are semantic CSS-variable tokens under @theme/:root, so a future [data-theme="light"] (light/dark) rollout needs no component changes. Inline CSS custom properties are used only for runtime values (event colors, positions, dynamic grid counts).
  • Dependency removed: no styled-components imports, type augmentation, Babel plugin, package dependency, or lockfile entry remain. A guard test (packages/web/src/common/styles/no-styled-components.test.ts) enforces this going forward.
  • Docs: .cursorrules/ updated to describe Tailwind as the sole styling system and document the c-* convention.

Why

styled-components added a runtime CSS-in-JS layer and a ThemeProvider dependency that made a semantic-token-based theming rollout awkward. Tailwind + semantic CSS variables keeps reusable recipes named while moving one-off styling local, and sets up light/dark theming without re-touching components.

Notable fix for reviewers

The migration had dropped role="form" when replacing <StyledEventForm role="form"> with a bare <form>. A <form> without an accessible name exposes no ARIA form role, so getByRole("form") stopped matching and silently broke 19 event-form e2e specs (unit tests query by placeholder, so they stayed green and hid it). Restored role="form" on both the timed and someday event forms — see 59091ce46. Takeaway: semantic attributes (role, aria-*, title) are load-bearing, not presentation; preserve them when restyling.

Verification

  • test:web ✅ 1085 pass (incl. styling guard + characterization tests)
  • test:core ✅ 145 pass
  • test:e2e50 pass (was 19 failing before the role="form" fix)
  • type-check ✅ · lint ✅ (27 pre-existing, non-blocking a11y/noSvgWithoutTitle warnings) · build:web ✅ · build:backend

Not covered

  • test:backend / test:scripts were not run — they require local Compass config/env (MONGO_URI, SUPERTOKENS_KEY, etc.) absent from this environment, and are unrelated to the web styling change. They run in CI.

🤖 Generated with Claude Code

tyler-dane and others added 30 commits June 18, 2026 16:49
The styled-to-tailwind migration replaced <StyledEventForm role="form">
with a bare <form>, dropping the explicit ARIA role. A <form> without an
accessible name exposes no "form" role, so getByRole("form") stopped
matching and broke the event-form e2e flows (19 specs). Restore role="form"
on both the timed and someday event forms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Describe Tailwind as the sole styling system and document the hybrid
inline-utility + c-* recipe convention. Note that semantic attributes
(role, aria-*) must be preserved when restyling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the internal design spec so it is not carried into the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reminder feature was deleted (#1875); its c-reminder-* utilities and
keyframes in index.css had no remaining consumers. Remove 7 utilities and
4 keyframes (~138 lines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace single-use c-* layout recipes with inline Tailwind utilities,
removing the global-CSS indirection for styling used in exactly one place:
c-floating-form-container, c-calendar-main-grid, c-calendar-grid-rows,
c-calendar-timed-columns, c-week-columns, c-week-grid-track.

No visual change; theme tokens and semantic attributes unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert single-use c-someday-event and c-someday-recurrence-value recipes
to inline Tailwind utilities using data-[...]/hover: variants. Keep
c-week-edge-zone (its dual data-position gradient variants are a coherent,
complex treatment better expressed as a named recipe).

No visual change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Push descendant-selector recipes onto their child elements and inline the
remaining single-use calendar grid layout:
- c-week-day-labels: clamp font sizes moved onto the Text children
- c-calendar-grid-row: inlined (its '& > span' rule was dead)
- c-calendar-day-times: child height/display moved onto mapped children
- c-calendar-now-line: inlined (trivial)
- c-calendar-all-day-columns: inlined; pseudo grid-line via before: variant

Keep c-calendar-date-column (2 consumers) and c-week-edge-zone (gradient
variant set). No visual change; verified with web unit + 50/50 e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document that inline is the default and a c-* recipe is justified only when
inline can't express it (third-party descendant selectors, pseudo-elements,
keyframe bundles, or genuine reuse). Refresh stale examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move single-use (or feature-local) recipes out of index.css to their call
sites as inline Tailwind, trading minor duplication for a leaner global
stylesheet: c-actions-menu(-item), c-recurrence-row/weekday/interval/caret,
c-week-grid-scroller, c-week-edge-zone, c-calendar-date-column. Pseudo-element
and data-state styling use arbitrary/data- variants.

Kept c-time-picker and c-planner-month-picker as global recipes: they style
third-party DOM (react-select/react-datepicker) that can't take Tailwind
classes, and the test harness already special-cases them.

Verified: web unit (1041) + 50/50 e2e + type-check + lint + build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-use button recipe with no descendant selectors; inline at its call
site in NotFound.tsx and drop from index.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The styled-components migration moved the timepicker overrides into an
@Utility (c-time-picker), which Tailwind places in @layer utilities.
react-select injects its default styles unlayered at runtime, and unlayered
rules beat any cascade layer regardless of specificity — so the dropdown
rendered with react-select's defaults.

Make .c-time-picker a plain (unlayered) rule so it competes with react-select
on specificity again, as the original styled-component did. Verified the menu
background now resolves to the themed --time-picker-bg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tyler-dane and others added 8 commits June 20, 2026 12:18
Same cascade-layer bug as the timepicker: as an @Utility it landed in
@layer utilities and lost to react-datepicker's unlayered defaults. The
recipe had worked around this with !important on nearly every rule.

Make it a plain unlayered rule (defined after c-date-picker, so it wins on
source order) and drop the now-redundant !important hacks. Verified the
sidebar selected-day pill renders: ::before background resolves to the accent
color and the day background is transparent as intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit 565e166 inlined c-event-form and dropped role="form" from both
the timed and someday event forms (the same regression fixed earlier in
59091ce). A bare <form> exposes no ARIA form role without an accessible
name, so getByRole("form") stopped matching and 16 event-form e2e specs
failed in CI (create/update/delete for timed, allday, someday).

Restore role="form" on both forms. Full e2e back to green apart from known
timing flakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/superpowers/ is already gitignored; these two spec files were
force-added previously. Remove them from tracking so the directory
stays local-only. Files remain on disk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tyler-dane tyler-dane marked this pull request as ready for review June 23, 2026 00:19
@tyler-dane tyler-dane merged commit 076579b into main Jun 23, 2026
9 checks passed
@tyler-dane tyler-dane deleted the refactor/styled-components-to-tw branch June 23, 2026 00:19
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.

1 participant