Skip to content

feat: shadcn/ui design system + research page sections + Google Sans#103

Merged
ahnafnafee merged 8 commits intomainfrom
migrate/shadcn
Apr 26, 2026
Merged

feat: shadcn/ui design system + research page sections + Google Sans#103
ahnafnafee merged 8 commits intomainfrom
migrate/shadcn

Conversation

@ahnafnafee
Copy link
Copy Markdown
Owner

@ahnafnafee ahnafnafee commented Apr 26, 2026

Summary

  • Adopt shadcn/ui (radix base, nova style) as the single primitive layer for design-system management and extensibility.
  • Theme parity: shadcn semantic tokens (--background, --primary, --foreground, --muted, --border, --ring) are mapped in OKLCH onto the existing blue-primary / neutral-theme palette so legacy classes (bg-primary-500, text-theme-700) and shadcn classes (bg-primary, text-foreground) render identically. Light/dark are unchanged visually.
  • One UI system, no competition: @headlessui/react, react-hot-toast, framer-motion, and vite-tsconfig-paths were replaced and dropped. radix-ui stays — it's shadcn's primitive layer, not a competing system.
  • Research page expansion: Overview, News, and Research Areas sections render between the (now hidden) hero and the listings. Hero title/description removed visually; an sr-only <h1>Research</h1> is kept for accessibility/SEO.
  • Default font swapped from Inter to Google Sans sitewide, with a system-font fallback chain (system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, …). Inter remains via the local @font-face as a deeper fallback.

What's new under src/components/ui/

19 shadcn primitives via the CLI: button, input, field, input-group, dialog, alert-dialog, sheet, dropdown-menu, sonner, tooltip, card, badge, separator, skeleton, empty, spinner, toggle-group, label, textarea, toggle. Use npx shadcn@latest add <name> for more — or ... add <name> --diff to merge upstream updates without losing local edits.

Component migrations (parity-preserving)

Old New
DialogResume (framer-motion + plain divs) shadcn Dialog (Radix focus trap, Esc/click-outside, a11y title)
MobileNav (@headlessui/react Menu) DropdownMenu
ThemeMenu (inline SVG) shadcn Button + lucide Sun/Moon
Searchbar (raw <input> + absolute icon) InputGroup + InputGroupAddon
EmptyResult (styled div) Empty + EmptyMedia + EmptyDescription
BackToTop (framer-motion AnimatePresence) shadcn Button + CSS opacity/translate transition
UnstyledButton (3 call sites) shadcn Button with appropriate variant/size
react-hot-toast Toaster sonner via @/components/ui/sonner

AlertResume + the dead alert state in ResumePageClient were removed — never rendered, isMatch was hardcoded true so the effect was a no-op.

Theme bridge (src/styles/globals.css)

  • @theme inline block maps --color-*var(--*) for shadcn-aware Tailwind classes.
  • :root pins shadcn vars to light palette: --primary = blue-500, --background = neutral-50, --foreground = neutral-800, --muted = neutral-100, --border = neutral-200, --ring = blue-500 (matches legacy focus ring).
  • .dark pins shadcn vars to dark palette: --primary = blue-400, --background = custom #111 (legacy gray-900), --border = neutral-700, --ring = blue-400.
  • --font-sans resolves to the new Google Sans stack (see Font section).

Research page additions

Three new sections rendered above the listings (src/components/content/research/):

  • ResearchOverview — intro paragraph framing the AI ↔ 3D-graphics focus.

  • ResearchNews — date-column timeline. Seeded with Dec 2025 — 🎉 Joining the DCXR Group at George Mason University, starting Spring 2026. Future updates just append to the NEWS array.

  • ResearchAreas — colorful rectangular chips (matching the existing BlogItem topic-chip shape) — each area gets its own tint:

    Area Tint
    AI for 3D Graphics blue
    Mesh Simplification rose
    Geometric Processing cyan
    ML for Graphics emerald
    3D Content Generation amber

    Light mode: bg-{c}-100 + text-{c}-800. Dark mode: bg-{c}-500/15 + text-{c}-200 so colors stay vibrant on the dark page without competing with title accents.

  • The visual Hero (title + description) was removed per design feedback. An sr-only <h1>Research</h1> keeps the landmark for screen readers and SEO.

  • Bonus: NEW badge on listing cards now sits in a small orange-tinted rectangular chip instead of bare orange text.

  • Bonus: fixed the mesh-decimation-benchmark MDX so the <msub/> markup error stops firing in the console (v_{\min}v_{\text{min}}).

Default font: Google Sans

  • Loaded sitewide via Google Fonts CSS (preconnect + stylesheet in layout.tsx). The Google Sans family is publicly served from fonts.gstatic.com (verified v67 woff2 URLs work without referrer restriction).
  • Stack matches the GitHub-style fallback chain: 'Google Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'. Even if Google Fonts is slow or blocked, the page renders in a polished system font.
  • Local Inter @font-face stays in globals.css as a deeper fallback so we don't FOUC.
  • tailwind.config.js fontFamily.primary mirrors the same stack so the legacy font-primary utility picks it up.
  • next.config.js CSP font-src extended to include fonts.gstatic.com.
  • next/font Inter import dropped — it was redundant with the local @font-face.

Directory layout

  • src/components/ui/ — shadcn primitives (don't hand-edit; use the CLI's --diff to merge upstream).
  • src/components/legacy-ui/ — bespoke site composition components (Header, Footer, Nav, AppLayoutPage, Hero, links, image wrappers, BackToTop, EmptyResult, Searchbar, MobileNav, ThemeMenu). They now compose shadcn primitives internally. The folder kept its name to keep the diff focused; rename to site/ is a separate PR.

Why the rename: Windows is case-insensitive — shadcn's lowercase ui/ would have collided with the existing UI/ directory and broken on Vercel's Linux build. The legacy tree was renamed via git mv UI → UI_TEMP → legacy-ui (two-step to avoid case-only-rename pitfalls). The @/UI tsconfig alias was repointed at legacy-ui/ so import sites are untouched.

Build robustness fixes

  • badge.tsx / button.tsx — cast Comp to React.ElementType so the prop spread isn't constrained by radix-ui's SlotProps (which doesn't yet accept React 19.2's popover="hint"). Robust under any @types/react version going forward.
  • vercel.json installCommand — added corepack prepare yarn@4.14.1 --activate so Vercel actually runs Yarn 4 instead of falling back to Yarn 1 (which silently rewrote the lockfile and resolved different transitive types). Lockfile is now authoritative on Vercel.
  • package.json resolutions bumped to @types/react@19.2.7 / @types/react-dom@19.2.3 to match devDependencies.

Tooling modernization

  • @trivago/prettier-plugin-sort-imports (last published Mar 2024) → @ianvs/prettier-plugin-sort-imports (actively maintained, better Prettier-3 / TS-5 support). Same import ordering; empty-string separators in importOrder produce blank lines between groups.
  • vite-tsconfig-paths plugin dropped in favor of Vite's native resolve.tsconfigPaths: true (the plugin was emitting a deprecation notice on every test run).
  • ESLint flat config (eslint.config.mjs) was already current — next/core-web-vitals already pulls in eslint-plugin-react-hooks. No changes.
  • shadcn (CLI) moved from runtime dependencies to devDependencies.
  • shadcn pulls in: radix-ui, lucide-react, sonner, tw-animate-css, class-variance-authority, tailwind-merge.

Test plan

  • yarn lint — clean
  • yarn type-check — clean (verified under both @types/react@19.1.11 and 19.2.7)
  • yarn test — 48/48 pass
  • yarn build — 50 routes generated
  • yarn validate:json-ld — 123 blocks across 39 files, all valid
  • yarn audit:alt-text — only pre-existing content issues in postscript-preview.mdx
  • Visual smoke test on yarn dev — light + dark mode, mobile nav, theme toggle, dialog open/close, blog/portfolio search, BackToTop scroll, copy-bibtex / copy-code buttons, research page sections (Overview / News / Research Areas), Google Sans rendering across both modes

Commits in this PR

  • feat(ui): migrate to shadcn/ui design system
  • fix(ui): make Slot.Root callsites typecheck under @types/react 19.2
  • chore(test): use Vite's native tsconfig paths resolution
  • fix(research): replace \min with \text{min} in mesh-decimation subscript
  • feat(research): add rectangular tinted background to NEW badge
  • feat(research): add Overview / News / Research Areas + swap to Google Sans
  • refactor(research): swap to Google Sans + match existing chip design
  • style(research): give each research-area chip its own color

Known follow-ups (out of scope for this PR)

  1. Rename legacy-ui/site/ (or split into layout/, templates/, etc.) once we're confident no external links/aliases depend on it.
  2. Consolidate icons (react-iconslucide-react). react-icons is still used in the legacy components; the migration isn't trivial because each icon needs a visual equivalent picked.
  3. react-image-lightbox is still in deps — it's a feature component (used by LightboxLazy.tsx), not a competing UI system.
  4. Optionally add a Card-based card style to listing pages (blog/portfolio/research) to lean further into shadcn. Today they use bespoke styled divs; that's parity-preserving but doesn't take advantage of Card composition.

Adopt shadcn/ui (radix base, nova style) as the single primitive layer so
future UI work can compose from a known design system instead of bespoke
components. Visual parity is preserved by mapping shadcn semantic tokens
(--background, --primary, --foreground, --muted, --border, --ring) onto
the existing blue-primary / neutral-theme palette in OKLCH — same colors,
new tokens — so legacy classes (bg-primary-500, text-theme-700) and shadcn
classes (bg-primary, text-foreground) render identically during the
transition.

UI primitives added under src/components/ui/ via the shadcn CLI: button,
input, field, input-group, dialog, alert-dialog, sheet, dropdown-menu,
sonner, tooltip, card, badge, separator, skeleton, empty, spinner,
toggle-group, label, textarea, toggle.

Component swaps (call sites updated):
- DialogResume → shadcn Dialog (drops framer-motion entrance/exit; gains
  Radix focus trap, Escape/click-outside, a11y title)
- MobileNav (HeadlessUI Menu) → DropdownMenu (drops @headlessui/react)
- ThemeMenu → shadcn Button + lucide Sun/Moon
- Searchbar → InputGroup + InputGroupAddon (was raw input + absolute icon)
- EmptyResult → Empty + EmptyMedia + EmptyDescription
- BackToTop → shadcn Button with CSS transition (drops framer-motion)
- UnstyledButton call sites in BlogPostClient, ResearchBibTeX, Pre, Resume
  → shadcn Button with appropriate variant/size
- Toaster (react-hot-toast) → sonner via @/components/ui/sonner
- AlertResume + dead alert state in ResumePageClient removed (was unused)

Directory layout:
- src/components/ui/ now holds shadcn primitives
- src/components/UI/ renamed to src/components/legacy-ui/ (Windows
  case-insensitive FS would have collided with the lowercase ui/ shadcn
  default). The @/UI tsconfig alias remaps to legacy-ui/ so the rename is
  invisible to import sites. Bespoke composition components (Header,
  Footer, Nav, AppLayoutPage, Hero, links, image wrappers) live there
  and now compose shadcn primitives internally.

Theme bridge in src/styles/globals.css:
- @theme inline maps --color-* tokens onto :root / .dark CSS variables
- :root pins --primary to blue-500, --background to neutral-50, etc.
- .dark pins --primary to blue-400, --background to custom #111 (legacy
  gray-900), --border to neutral-700 — matches the legacy dark surface
- --font-sans resolves to var(--font-inter) so font-sans = font-primary

Dependency cleanup (drops competing systems):
- @headlessui/react: removed (MobileNav was the only consumer)
- react-hot-toast: removed (replaced with sonner)
- framer-motion: removed (only used in deleted drawer + dialog files)
- @trivago/prettier-plugin-sort-imports: replaced with the actively
  maintained @ianvs/prettier-plugin-sort-imports; equivalent ordering,
  empty-string separators between groups
- shadcn (CLI): moved from runtime deps to devDependencies
- shadcn pulls in: radix-ui, lucide-react, sonner, tw-animate-css,
  class-variance-authority, tailwind-merge

Dead code deleted:
- src/components/legacy-ui/drawer/ (DrawerButton, DrawerMenu — replaced
  by MobileNav DropdownMenu)
- src/components/legacy-ui/buttons/UnstyledButton.tsx,
  src/components/legacy-ui/buttons/SkipToContent.tsx (no callers)
- src/components/legacy-ui/common/Spinner.tsx (replaced by shadcn)
- src/libs/animation/variants.ts (only consumer was BackToTop)
- src/hooks/UI/useDrawer.tsx (only consumer was deleted drawer)

Verification: yarn lint, yarn type-check, yarn test (48/48), yarn build
(50 routes), yarn validate:json-ld (123 blocks across 39 files) all
pass. Documented the new layout, theme bridge, and shadcn rules in
CLAUDE.md.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 26, 2026

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

Project Deployment Actions Updated (UTC)
ahnafnafee-dev Ready Ready Preview, Comment Apr 26, 2026 9:07pm

Vercel's build failed because its install path fell back to yarn 1 (which
silently rewrote the yarn 4 berry lockfile and resolved @types/react to
19.2.7 instead of the 19.1.11 pinned in resolutions). Two issues to fix.

1) Type robustness: @types/react 19.2.x adds "hint" to the popover HTML
attribute, which radix-ui's SlotProps doesn't include. Spreading
React.ComponentProps<'span'> (or 'button') onto Slot.Root therefore fails
type-check because span/button now allow popover="hint" but Slot.Root
doesn't. Cast Comp to React.ElementType in badge.tsx + button.tsx so the
spread isn't constrained by Slot.Root's stricter prop type. Aligns
resolutions with the devDeps version (both 19.2.7) so the build is
consistent regardless of which yarn picks them up.

2) Vercel install command: `corepack enable && yarn install --immutable`
wasn't reliably switching the active yarn binary to 4.14.1 — corepack's
shim wasn't taking effect inside Vercel's PATH, so yarn 1.22.19 ran
instead. Add `corepack prepare yarn@4.14.1 --activate` to explicitly
download and activate yarn 4 before `yarn install`. This makes the lockfile
authoritative on Vercel and stops the silent rewrite that produced the
type-version drift.

Verified locally: yarn lint, yarn type-check, yarn test (48/48), yarn build
(50 routes) all pass with @types/react 19.2.7 + @types/react-dom 19.2.3.
Vite added `resolve.tsconfigPaths: true` natively, so the
`vite-tsconfig-paths` plugin is no longer needed (and was emitting a
deprecation notice on every test run). Drop the plugin, set the option
directly in vitest.config.ts.

Verified: yarn test still passes 48/48 with @/ path aliases resolving
through the native option.
`v_{\min}` produced an invalid <msub/> in MathML because `\min` renders as
the math operator (multiple MathML children with operator spacing), and
KaTeX didn't wrap them into a single <mrow> for the subscript slot.
Browsers reported "Incorrect number of children for <msub/> tag" on the
research detail page.

Use `v_{\text{min}}` since "min" here is a label (the minimum vertex
coordinate of the bounding box), not the binary math min operator.
Renders identically; produces a single <mtext>min</mtext> child.
The NEW badge on research listing cards was orange text only, which got
lost next to the purple title at small sizes. Wrap it in a subtle
orange-tinted chip (bg-orange-100 / dark:bg-orange-500/15) with rounded-sm
corners — keeps the existing 10px tracked-out type, just gains a
rectangular background that reads as a label.
… Sans

Three new sections render between the Research hero and the listings:

- ResearchOverview — short intro paragraph framing the research focus
  (AI ↔ 3D graphics, mesh simplification, geometric processing,
  generative 3D content).
- ResearchNews — date-column timeline of milestones. Seeded with a
  single Dec 2025 entry: "Joining the DCXR Group at George Mason
  University, starting Spring 2026." Future updates just append to the
  NEWS array in the component.
- ResearchAreas — chip row with colored dots tagging the active
  research areas; matches the visual language of the topic chips on
  the existing listing cards.

Each section header uses the same uppercase / bordered-bottom style as
ResearchSections so the page reads as one continuous document.

Font: site default switched from Inter to Google Sans Text — same modern
geometric feel as Google's product surfaces. Loaded via Google Fonts CSS
(preconnect + stylesheet); local Inter @font-face stays as the fallback
in globals.css. Wiring:

- layout.tsx: drop next/font Inter import, add Google Fonts <link> tags.
- globals.css @theme inline: --font-sans now reads
  'Google Sans Text', 'Inter', ui-sans-serif, system-ui, sans-serif.
- tailwind.config.js: fontFamily.primary leads with "Google Sans Text"
  so the legacy `font-primary` utility picks it up too.
- next.config.js CSP: font-src now includes fonts.gstatic.com.

Verified: yarn lint, yarn type-check, yarn test (48/48), yarn build
(50 routes) all pass.
Font:
- Use 'Google Sans' (no Text suffix) directly. Verified the v67 woff/ttf
  files load publicly from fonts.gstatic.com — no domain restrictions.
- Adopt the GitHub-style fallback stack so even if Google Fonts is slow
  or blocked, the site still renders in a polished system font:
  'Google Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
  Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, plus the four
  emoji families at the tail.
- tailwind.config.js fontFamily.primary mirrors the same stack.

Research page:
- Remove the visual Hero (title + description). Replace with an sr-only
  <h1>Research</h1> so accessibility / SEO still get the landmark.
- Rewrite ResearchAreas to use the existing topic-chip style from
  BlogItem.tsx: rounded-sm bg-gray-200 dark:bg-gray-800 px-2 py-1
  text-[10px] font-bold tracking-wider uppercase. Drop the colored dots
  + pill shape — they didn't match the rest of the site's design system.

Verified locally: yarn type-check + yarn build (50 routes) clean.
Same rectangular shape, padding, and uppercase typography as the rest of
the site's chip system — but each area now reads with its own tint:

  AI for 3D Graphics    → blue
  Mesh Simplification   → rose
  Geometric Processing  → cyan
  ML for Graphics       → emerald
  3D Content Generation → amber

Light mode uses the -100 shade for the chip background and -800 for text
(strong contrast). Dark mode uses /15 opacity on -500 for the bg and -200
for text so the colors stay vibrant on the gray-900 page surface without
clashing.
@ahnafnafee ahnafnafee changed the title feat(ui): migrate to shadcn/ui design system feat: shadcn/ui design system + research page sections + Google Sans Apr 26, 2026
@ahnafnafee ahnafnafee merged commit 616a13d into main Apr 26, 2026
6 checks passed
@ahnafnafee ahnafnafee deleted the migrate/shadcn branch April 26, 2026 21:08
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