Skip to content

feat: v2 button system (ButtonV2, CardAction) + /dev/buttons review surface#5940

Merged
tsahimatsliah merged 6 commits intomainfrom
feat/button-system-overhaul
May 3, 2026
Merged

feat: v2 button system (ButtonV2, CardAction) + /dev/buttons review surface#5940
tsahimatsliah merged 6 commits intomainfrom
feat/button-system-overhaul

Conversation

@tsahimatsliah
Copy link
Copy Markdown
Member

@tsahimatsliah tsahimatsliah commented Apr 30, 2026

Summary

Introduces a refreshed button system designed to address the user feedback that current buttons "feel disabled". Built ground-up from research into Apple HIG, Material 3, Linear, Notion, Vercel, GitHub Primer, ChatGPT, Claude, Cursor, and Tailwind UI.

The v2 system ships alongside v1 — no production call sites are migrated in this PR. v1 Button / QuaternaryButton are JSDoc-deprecated to nudge new code. Migration of real surfaces (post detail, feed cards, comment rows, profile actions, …) will follow as per-surface PRs once this foundation lands.

Key changes

ButtonV2 (new)

  • Tightened icon-to-button ratio (~50%) — was 67–83% on v1.
  • Equal horizontal padding + flex gap-X per size — drops v1's hardcoded negative-margin trick that made Medium/Large/XLarge visibly asymmetric.
  • Variant-driven font weight (Primary font-semibold, others font-medium) — hierarchy carried by fill/border, not weight uniformity.
  • Per-variant focus ring tinted to brand color so it never clashes with the button fill.
  • New inactive (Primer "looks active, behaves disabled") prop — aria-disabled + not-allowed cursor while staying focusable + clickable so callers can show a tooltip/toast explaining why.
  • Refined hover/pressed/disabled contrast in both light and dark themes (sequential 60 → 70 → 80 ladder light / 40 → 30 → 20 dark, vs v1's two-shade dark jump).

CardAction / CardActionBar (new)

  • Purpose-built primitive for engagement bars (post detail, feed cards, comment rows).
  • Built-in counter slot inside the click target (Reddit / X pattern — one affordance, one tap).
  • density prop (comfortable 40px / compact 32px) sized to fit the production card width contract (272 px MIN feed card → 340 px MAX desktop card).
  • :has()-driven responsive label collapse: when usePostActionsLabelVisibility hides the content wrapper, the button collapses to a true icon-only square (no rectangular ghost padding).
  • Engagement-bar icon ratio bumped to 60–62% per density (matches Reddit, X, Instagram, Material 3 small) — defaults read at a glance from a feed scroll.
  • Engagement-bar tightening for count-only buttons (!gap-1 !px-1.5) so a 5-action row fits inside the 272 px card MIN with the math documented inline.

buttons-v2.css + tailwind/buttons-v2.ts (new)

  • Token system with theme-aware hover/press shadows (light tints with pepper.90 at 8%/6%; dark uses black at 30%/20%).
  • Inset press-feedback shadow on :active (replaces v1's box-shadow: none that made active read as weaker than hover).
  • Polish layer: font-smoothing: antialiased (the single biggest fix for "fat label" on Apple), tabular-nums, touch-action: manipulation, appearance: none, background-clip: padding-box, isolation: isolate.
  • forced-colors: active block for Windows High Contrast Mode (WCAG 1.4.1).
  • prefers-reduced-motion: reduce block.
  • Scoped under .btn-v2 — does not touch v1's .btn cascade.

/dev/buttons (new, internal-only)

  • Side-by-side OLD Button vs NEW ButtonV2 across every variant × color × size × state.
  • Sticky controls bar to flip theme, background, and state simultaneously across the page.
  • Real-card simulation at 272 px / 320 px / 480 px / fluid widths so we can verify CardAction never overflows the production card width contract.
  • PostActionsV2 vignette mirrors the production post detail action bar 1:1 with the same ResizeObserver-driven label collapse — provable visual parity before any migration.
  • Reference snapshots from Linear, Notion, Vercel, Cursor, ChatGPT, Claude for direct comparison.
  • Production-gated (isDevelopment runtime check + <NextSeo nofollow noindex />).
  • _app.tsx carves out /dev/* from the full provider tree (BootDataProvider, Serwist, auth) so /dev/buttons runs on a minimal QueryClient-only tree.

Buttons.mdx (new, Storybook docs)

  • Migration guide explaining every v1→v2 difference, per-variant changes, the card width contract, and the CardAction density rules.

no-raw-button-class ESLint rule (new, not enabled)

  • Flags raw btn-* / btn-v2-* class strings to prevent regressions back to hand-stamped variant classes.
  • Defined but intentionally not opted into any project ESLint config — packages opt in once their own call sites are migrated.

What does NOT change

  • No production app code consumes v2 yet — every existing Button / QuaternaryButton call site renders unchanged.
  • v1 Button.tsx / QuaternaryButton.tsx are unchanged in behaviour — only @deprecated JSDoc was added.
  • v1 buttons.css / tailwind/buttons.ts are untouched. v2 styles load alongside v1 with no override.
  • All 16 existing buttons unit tests still pass.

Test plan

  • pnpm storybook — smoke test ButtonV2 and CardAction stories in both light/dark themes
  • /dev/buttons on localhost — flip theme/state/size/color combinations, confirm OLD/NEW parity expectations match documented intent
  • /dev/buttons mock cards — confirm CardAction never overflows at 272 px / 320 px / 480 px widths
  • /dev/buttons post-detail vignette — resize the viewport, confirm labels collapse to true icon-only squares (no rectangular ghosts)
  • Manual spot-check on prod surfaces (feed, post page, profile, settings) — no regressions on existing v1 buttons
  • pnpm --filter @dailydotdev/shared test — buttons specs green
  • pnpm typecheck:strict:changed — green

Known follow-ups (for separate PRs)

These were caught during self-review and intentionally deferred so this PR stays scoped to "introduce v2":

  1. Tighten the no-raw-button-class regex. The current pattern would flag the implementation files themselves (btn-v2, btn-label, btn-loader, btn-icon-left) when enabled. Either narrow it to variant tokens only, or add an allowlist for utility classes. Add a self-test that lints ButtonV2.tsx and expects zero reports.
  2. Production-gate /dev/buttons at the routing level. The page is noindex + runtime-gated, but the JS bundle still ships. Add getStaticProps returning { notFound: true } in production and a Disallow: /dev line in public/robots.txt.
  3. Unit tests for ButtonV2, CardAction. At least cover pressed, inactive, loading, density, and the :has() collapse branch.
  4. MedalBadgeIcon convention bug. Every other icon uses IconPrimary={Outlined}, IconSecondary={Filled}; only MedalBadgeIcon is reversed. Surfaces ~20 call sites that need the secondary prop stripped or flipped. Should land before any real surface migrates to v2 so the toggle pattern doesn't need re-fixing per surface.

Migration plan (TL;DR for after this lands)

Per-surface PRs in this order, each independently reviewable:

  1. MedalBadgeIcon flip (clean-up)
  2. PostActions.tsx — post detail action bar (dev page already mirrors it 1:1 → visual parity provable)
  3. Feed card engagement bar (ActionButtons.tsx and friends)
  4. Comment row actions
  5. Profile actions, squad analytics tiles
  6. Enable no-raw-button-class per package as it's fully migrated
  7. Delete v1 Button / QuaternaryButton

Made with Cursor

Preview domain

https://feat-button-system-overhaul.preview.app.daily.dev

…ctionBar)

Adds a refreshed button system designed to address user feedback that
existing buttons "look disabled". Built ground-up from research into
Material 3, Apple HIG, Linear, Notion, Vercel, GitHub Primer, ChatGPT,
Claude, Cursor, and Tailwind UI guidelines.

Highlights:
- ButtonV2: tightened icon ladder (~50% icon-to-button ratio vs v1's
  67-83%), equal horizontal padding + flex gap (drops v1's hardcoded
  negative-margin trick), refined hover/pressed/disabled contrast in
  both light and dark modes.
- CardAction / CardActionBar: a11y-correct primitive for engagement
  bars (feed cards, comment rows, post detail) with built-in counter
  slot, pressed-icon swap, and density variants (40px / 32px) sized to
  fit the production card width contract.
- buttons-v2.css + tailwind/buttons-v2.ts: scoped utilities + base
  styles that load alongside v1 without overriding it.
- Marks v1 Button and QuaternaryButton @deprecated to nudge new
  call sites toward v2; v1 continues to render unchanged for back-
  compat with existing consumers.

No production call sites are migrated in this commit. The v2 system is
inert in the running app until a surface opts in.

Made-with: Cursor
- ButtonV2.stories.tsx: full variant/size/color matrix with light/dark
  preview, hover/pressed/disabled/loading state coverage, and
  iconPosition (Left/Right/Top) + icon-only variants.
- CardAction.stories.tsx: layout=between, layout=compact, and
  density=compact stories mirroring the live engagement bar contexts
  (post detail, mobile feed card, comment row).
- Buttons.mdx: migration guide explaining v1->v2 differences (icon
  ladder, padding model, pressed-state contract, card width contract,
  per-variant changes). Lives in the buttons folder so it ships
  alongside the components in Storybook docs.

Made-with: Cursor
Internal-only review surface that renders v1 and v2 buttons side by
side across every variant, size, color, density, and state. Includes:

- Full button matrix (Primary/Secondary/Tertiary/Float/Subtle/Option/
  Quiz x XSmall->XLarge x BlueCheese/Bun/Cabbage/Cheese/Avocado/etc.)
- Real-card simulation (mock feed card and list card at 272px / 320px
  / 480px / fluid widths) so we can verify CardAction never overflows
  the production card width contract.
- PostActionsV2 vignette mirroring the production post detail action
  bar 1:1, with the same ResizeObserver-driven label collapse so the
  responsive behavior is provable before any migration.
- InspirationButtons helper renders reference snapshots from Linear,
  Notion, Vercel, Cursor, ChatGPT, Claude for direct comparison.

App shell change in _app.tsx carves out /dev/* from the full provider
tree (BootDataProvider, Serwist, auth) so the dev page can render
without the production cookie/auth chain. /dev/* hits a minimal
QueryClient-only tree.

Made-with: Cursor
Lint rule that flags raw .btn-tertiary-* / .btn-primary-* / etc.
class names in JSX className strings to prevent regressions back to
the v1 styling pattern once surfaces migrate to ButtonV2 / CardAction.

The rule is wired into packages/eslint-rules but intentionally not
opted into any project ESLint config yet. Enable it per-package once
that package's call sites are fully migrated, so the rule can land
without flooding the existing codebase with errors.

Made-with: Cursor
Resolves react/destructuring-assignment ESLint error introduced in
the /dev/buttons commit. Picks up `pageProps` and `Component` from
the top-level destructure instead of reaching back through `props`.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview May 3, 2026 9:56am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored May 3, 2026 9:56am

Request Review

Copy link
Copy Markdown
Contributor

@rebelchris rebelchris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raw button change is ok, jsu feels overkill with all the tests/mock stuff :)

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