Conversation
Replace Mantine entirely with shadcn/ui (Radix + Tailwind v4) across services/web, and add a Chrome-based visual + conformance + layout-audit CI gate. ~100 screen/component files converted; Mantine, postcss-preset- mantine and the custom theme are removed and Tailwind preflight re-enabled. - Foundation: Tailwind v4 + @tailwindcss/vite, components.json, cn(), globals.css design tokens (literal shadcn-neutral default with a documented brand-primary knob), ThemeProvider (dark mode) + sonner. - ~28 shadcn/ui primitives under src/components/ui. - All chrome (AppShell -> Sidebar), auth flows, CRUD list/detail/form screens, settings, the shared Search framework, modals, form-fields, and the API Docs app migrated. - Removed @mantine/*, postcss-preset-mantine, postcss-simple-vars, theme.js/theme.less, postcss.config.js; MantineProvider/Notifications dropped from Wrapper; preflight re-enabled (single @import "tailwindcss"). - Visual gate (visual/, .github/workflows/visual.yml): Playwright + Chromium screenshots, §5 conformance checks, and a layout audit (font consistency, breadcrumb alignment, list-marker, broken-image, dark-mode text-colour). CSS bundle ~237kB -> ~49kB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Ship Geist (Geist Sans + Mono) via @fontsource-variable/geist so the UI font is stable across platforms; point --font-sans/--font-mono at it. - Fix nested sidebar active state: a parent (e.g. "Users") no longer highlights when a child item (e.g. "Invites") is the actual match. - Add a Theming section to the web README (brand --primary knob, font swap, dark mode, adding components) and remove stale Mantine references. - Remove the Chrome visual-regression harness and workflow that were only used to verify this migration (services/web/visual, .github/workflows/visual.yml, @playwright/test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| import { cn } from '@/lib/utils'; | ||
|
|
||
| // Semantic variants only — colour carries meaning, never decoration (§5.6). |
There was a problem hiding this comment.
what are these rules are they documented?
There was a problem hiding this comment.
Good catch — §5.6 was a stale pointer to the migration plan doc (which isn't in the repo). Reworded it to be self-documenting: each variant maps to a semantic token (destructive / success / info / warning) so colour conveys state rather than decoration. (ea0b23c)
| * Notification shim backed by sonner, with a Mantine-compatible signature so | ||
| * migrated call sites can swap `notifications.show(...)` / `showNotification(...)` | ||
| * for `notify(...)` with minimal churn. `<Notifications/>` (Mantine) stays | ||
| * mounted until every call site is migrated (plan §6.1, teardown in §6). |
There was a problem hiding this comment.
maybe the comments needs some cleanup
There was a problem hiding this comment.
Cleaned up in ea0b23c — dropped the migration-plan references and the (now-wrong) line about Mantine <Notifications/> staying mounted; Mantine is fully gone. The comment now just describes the sonner wrapper and its { title, message, color } shape.
| /** | ||
| * Local replacement for the Mantine useDisclosure. | ||
| * Returns [opened, { open, close, toggle }]. | ||
| */ | ||
| export function useDisclosure(initial = false) { | ||
| const [opened, setOpened] = useState(initial); | ||
| const open = useCallback(() => setOpened(true), []); | ||
| const close = useCallback(() => setOpened(false), []); | ||
| const toggle = useCallback(() => setOpened((o) => !o), []); | ||
| return [opened, { open, close, toggle }]; | ||
| } | ||
|
|
There was a problem hiding this comment.
possible bs
i suspect we could kill it, but no biggie.
There was a problem hiding this comment.
It's actually still used in Dashboard.js and Portal.js (sidebar/drawer open state), so I kept the hook but removed the Mantine-migration framing from the doc comment (ea0b23c). Happy to inline the useState in both call sites instead if you'd rather not keep the helper.
| <form onSubmit={onSubmit} className="flex flex-col gap-4"> | ||
| <ErrorMessage error={error} /> | ||
|
|
||
| <Textarea | ||
| rows="5" | ||
| label="Emails" | ||
| value={input} | ||
| onChange={(evt) => { | ||
| setInput(evt.target.value); | ||
| }} | ||
| onBlur={onEmailsBlur} | ||
| placeholder="Enter email addresses separated by comma or new line." | ||
| /> | ||
| <div className="flex flex-col gap-2"> | ||
| <Label htmlFor="emails">Emails</Label> | ||
| <Textarea | ||
| id="emails" | ||
| rows="5" |
There was a problem hiding this comment.
maybe we should expand zod to here too ?
There was a problem hiding this comment.
Done — migrated this form to react-hook-form + zod in ea0b23c. The emails textarea is parsed (comma/newline separated) and each address is validated, plus a role enum. This also tightens the old parsing, which only split on ,\s+ and did no validation.
| const schema = z.object({ | ||
| name: z.string().min(1, 'Name is required'), | ||
| description: z.string().optional(), | ||
| isFeatured: z.boolean().optional(), | ||
| priceUsd: z.union([z.number(), z.string()]).nullable().optional(), | ||
| expiresAt: z.any().nullable().optional(), | ||
| sellingPoints: z.array(z.string()).optional(), | ||
| images: z.array(z.any()).optional(), | ||
| shop: z.any().nullable().optional(), | ||
| }); |
There was a problem hiding this comment.
Good good, so now we are doing client-side validation. Wanted this for a long time.
Maybe in the future we can share a sdk for the api. So we ensure that things are aligned and typed.
There was a problem hiding this comment.
Agreed — a shared, typed SDK generated from the API would be the natural next step to keep client and server validation aligned. Out of scope for this PR but a good follow-up.
There was a problem hiding this comment.
Client side validation is something I thought we had agreed against, and for good reason. I've been avoiding it all this time :)
Maybe AI makes this feasible now though. I don't find it to be such a good addition in any case.
- Invites form: migrate from useFields/manual state to react-hook-form + zod with client-side validation (per-address email validation + role enum); drop the unused `name` prop at the call site. - alert.jsx / notify.js / useDisclosure.js: replace migration-plan references and Mantine-era framing with self-documenting comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Replaces Mantine entirely with shadcn/ui (Radix UI + Tailwind CSS v4) across
services/web. The UI now uses the neutral shadcn default palette in light and dark, with the brand colour exposed as a single token, and ships the Geist font (bundled, so it's stable across platforms).What changed
Foundation
@tailwindcss/vite,components.json,cn()helper, and design tokens insrc/styles/globals.css.ThemeProvider(light/dark via a.darkclass) +sonnertoasts replacing MantineNotifications.@fontsource-variable/geist.src/components/ui/(button, input, form, select, dialog, sheet, dropdown-menu, table, card, tabs, command, popover, etc.).Screens & components (Mantine → shadcn)
AppShell→ tailored shadcn sidebar (+ mobile drawer, breadcrumb/title region).InputOTP, federated buttons).Searchframework → shadcnTable/filters/pagination), detail (Card+ definition lists), and forms (react-hook-form + zod + shadcnForm).Button+ size-matchedDropdownMenu), the 3 modals, the form-fields, Settings, and the entire API Docs app.Teardown
@mantine/*,postcss-preset-mantine,postcss-simple-vars; deletedtheme.js,theme.less,postcss.config.js.MantineProvider/<Notifications/>fromWrapper.js; re-enabled Tailwind preflight + shadcn base layer.Theming
Documented in
services/web/README.md:--primary/--primary-foregroundin:rootand.dark(one knob drives primary buttons, the active tab indicator, focus rings).--font-sans/--font-mono+ the@fontsourceimports inWrapper.js.Notes for reviewers
react-icons(Phosphor) icons are retained alongsidelucide-react.Verification
yarn build✓ ·yarn lint srcclean across the tree.🤖 Generated with Claude Code