Skip to content

feat(ui): migrate to HeroUI v3 and rebuild the marketing landing - #7

Draft
sardorml wants to merge 14 commits into
feat/extension-completefrom
feat/heroui-migration
Draft

feat(ui): migrate to HeroUI v3 and rebuild the marketing landing#7
sardorml wants to merge 14 commits into
feat/extension-completefrom
feat/heroui-migration

Conversation

@sardorml

@sardorml sardorml commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Replaces two overlapping UI stacks — shadcn/Radix in the dashboard and antd v6 on the marketing pages — with HeroUI v3 across apps/web and the extension popup, then rebuilds the landing on top of it.

Based on feat/extension-complete (#6), so the diff here is the UI work only.

What's in it

The migration. antd, @ant-design/*, all @radix-ui/*, @base-ui/react, class-variance-authority, shadcn and tw-animate-css are gone from apps/web. @captureflow/ui shrinks to what HeroUI has no equivalent for — theme tokens, ThemeToggle + cookie helpers, cn(), AvatarGroup, GridLoader, VisibilityPicker — and drops its per-primitive subpath exports. The marketing tree kept an antd-shaped API in a lot of call sites, so components/marketing gains local Row/Col/Flex, typography and small-UI shims instead of rewriting every section's layout at once.

Landing. Pricing is two subgrid-aligned cards with a gradient panel; the managed card's storage tiers became a segmented switch that drives the headline price, the CTA target and the storage highlight. Hero CTAs are platform install buttons plus a signup line, icon strip and download link. Footer wordmark is a dashed SVG outline. Nav loses the theme switch — marketing is dark-only via MarketingShell. The collaboration block splits into one titled section per capability.

Theme bugs that were masking content app-wide. Each of these hid real UI rather than just looking off:

  • HeroUI's dark --field-background is the same value as --surface with a 0px transparent border, so any Input inside a Card was invisible. Fields now recess to the canvas with a real border.
  • The light theme inverts --color-neutral-* for ~900 legacy utilities, and nothing restored the upright scale for a dark subtree inside a light root — text-neutral-900 painted white on white inside MarketingShell.
  • Nothing sets a text color on <body>, so anything that inherits rather than picking a token (a HeroUI Accordion trigger) fell back to the UA default and rendered near-black. MarketingShell now sets the baseline.
  • Col folded every breakpoint's order into one value, so the lg order applied on mobile too and flipped sections put the feature list above the mockup.

Dashboard. Dead Upgrade and Invite triggers — React Aria's usePress calls stopPropagation() on click, so an ancestor onClick never fires; the trigger is cloned with a handler instead of wrapped (it has to stay an element, not a render prop, because server components render it). The upgrade modal is rebuilt around the same storage switch as the plan card. Share-dialog selection is visible again via Radio.Content — plain children skip it and render no selected state. Plus card padding, alignment and type-scale corrections.

Testing

  • pnpm typecheck — clean across all 7 projects
  • pnpm --filter @captureflow/web test — 26/26

Not run: the production build. A next build while next dev is serving apps/web corrupts .next, and the dev server was up throughout. Needs pnpm --filter @captureflow/web build on a quiet tree before this leaves draft.

Also unverified by machine: the visual result. Every fix above was checked against the running dev server's rendered HTML and compiled CSS, not screenshotted.

sardorml added 14 commits August 2, 2026 07:43
Replaces two overlapping UI stacks — shadcn/Radix in the dashboard and
antd v6 on the marketing pages — with HeroUI v3 everywhere. Both are gone
from apps/web, along with @base-ui/react, class-variance-authority and
tw-animate-css.

@captureflow/ui shrinks to what HeroUI has no equivalent for: the theme
tokens, ThemeToggle and cookie helpers, cn(), AvatarGroup, GridLoader and
VisibilityPicker. Its per-primitive subpath exports are dropped.

The marketing tree kept an antd-shaped API in a lot of places, so
components/marketing gains local Row/Col/Flex, typography and small-UI
shims rather than rewriting every section's layout call sites at once.

Landing work on top of the migration:
- pricing rebuilt as two subgrid-aligned cards with a gradient panel; the
  managed card's storage tiers are a segmented switch that drives the
  headline price, the CTA target and the storage highlight
- hero CTAs are platform install buttons plus a signup line, icon strip
  and a link to the download page
- footer wordmark is a dashed SVG outline; nav loses the theme switch and
  marketing is dark-only via MarketingShell
- the collaboration block splits into one titled section per capability

Theme fixes that were masking content app-wide:
- HeroUI's dark --field-background equals --surface with a 0px transparent
  border, so any Input inside a Card was invisible; fields now recess to
  the canvas with a real border
- the light theme inverts --color-neutral-*, and nothing restored it for a
  dark subtree inside a light root, so text-neutral-900 painted white on
  white in MarketingShell
- MarketingShell now sets the baseline text color; nothing sets one on
  <body>, so anything inheriting (a HeroUI Accordion trigger) fell back to
  the UA default and rendered near-black
- Col resolved one order for every breakpoint, so flipped sections put the
  feature list above the mockup on mobile

Dashboard fixes: dead Upgrade and Invite triggers (React Aria's usePress
stops click propagation, so the trigger is cloned with a handler instead
of wrapped), the upgrade modal rebuilt around the same storage switch,
share-dialog selection made visible via Radio.Content, plus card padding,
alignment and type-scale corrections.
Brings the popup onto the same component vocabulary as the web app —
Button, Select, Switch, Meter, Alert and Card from @heroui/react in place
of the hand-rolled Tailwind elements — and adds the Tailwind v4 Vite
plugin so wxt compiles the stylesheet.

Net removal: the panels lose ~360 lines of bespoke markup.
Section 7 described antd v6 and the shadcn kit. Rewrites it around
HeroUI: the React Aria prop vocabulary (onPress, isDisabled, isSelected),
compound parts as statics, buttonVariants for anchors, and which
semantic color names belong to HeroUI's theme rather than ours.
The modal portals to <body>, so it lands outside MarketingShell and
resolved its theme off <html> — a white dialog with dark text over the
dark landing whenever the app cookie said light. Re-declare the theme on
the backdrop, which is the outermost portalled node.
Windows, Chrome and Firefox were disabled lg buttons with a Soon chip
inside — three filled pills reading as CTAs competing with the one real
download, for things nobody can click. They're status, so they render as
a muted icon + label line under the requirements note instead.
HeroUI's Typography emits text-align:start by default, which beats the
centered ancestor — the heading and subtitle sat left of the logo. Pass
align explicitly, as the marketing sections already do.

Chrome moves out of the "coming soon" line into a live secondary button
beside the macOS download. It points at "#" until CHROME_WEBSTORE_URL is
set; setting that constant swaps in the store link and the new tab.
"Download for Apple Silicon Macs (Beta)" dwarfed the Chrome button beside
it. The requirements line under the pair already says "Requires macOS 14
or later on Apple Silicon", so the qualifiers were duplicated anyway.
HeroUI's secondary variant paints its label with --accent-soft-foreground
over the same grey fill tertiary uses, and blue on grey reads as a
disabled control rather than an alternative action — the hero's "Add to
Chrome" looked dead beside the primary download.

Switches the hero install button, the download page's Chrome CTA, the
mobile nav's Log in, and the marketing Button shim's `default` type (which
covers "Suggest a feature") to tertiary, which leaves the label at
currentColor.
"Download for macOS" and "Add to Chrome" are different lengths, so
shrink-to-fit buttons came out visibly lopsided. Lays them out as equal
grid columns capped at max-w-md, stacking full width below sm.
Grey on a dark page recedes next to the blue primary. Both Chrome CTAs —
hero and download page — now render as a white pill with dark text, the
same treatment the plan cards' CTA uses.

Overrides .button's own --button-* colour vars rather than passing bg-*
utilities: HeroUI's component styles land after Tailwind's, so its
background-color rule wins over a bg-* class at equal specificity.
Drops the split layout — the desktop-only carousel panel took half the
viewport and forced the form into the right pane. The shell is now one
centered column with the wordmark at the top, and AuthCarousel goes with
it since nothing else rendered it.

Reorders the form email-first to match: an email field with a leading
mail icon and a Continue button, an OR rule, then Google and GitHub. The
password (and name, on signup) moves to a second step that shows the
captured address with a way back to change it. Buttons are pills.

No terms/privacy line — those pages don't exist yet.
The input and the buttons were three different heights: InputGroup is a
fixed min-h-9 (36px) with no size variant, and HeroUI's .button--lg drops
from 44px to 40px at md and up. Pins every control to h-11 so the column
reads as one stack, and opens the vertical rhythm to 20px gaps with a
32px divider.

Also corrects the comment added with the white Chrome pill: it claimed a
Tailwind bg-* utility loses to .button's background-color rule. The
bundle has it the other way round — utilities land after HeroUI's
component styles and win at equal specificity. Overriding the --button-*
vars is still the right call, but because hover and pressed follow from
the same place, not because bg-* wouldn't apply.
Column 440 -> 360px, controls 44 -> 38px, heading 30 -> 22px, body text
13px, and the gaps tighten to match.

Sizes are px rather than HeroUI's rem-based ones so the column keeps its
proportions regardless of the browser's root font size, and so
.button--lg's 4px drop at md doesn't desync the buttons from the input.
Header becomes an identity block — avatar beside the name and address,
not the name alone above it. Nav items gain an optional trailing slot,
used for the Billing row's Free/Pro chip. A theme row with an inline
segmented control sits above Sign out, which retires the standalone
toggle from the top bar.

The dashboard's list fills out to match: Billing, Profile, Connected
devices, Settings, Feedback, Contact support, Sign out. Billing has no
page of its own — it opens the upgrade modal via ?upgrade=1.

The segmented control is two-way (light/dark), not the reference's three:
Theme is typed "light" | "dark" and resolved from a cookie server-side,
so a "system" option needs OS-preference detection before first paint to
avoid a flash. Out of scope here.
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