diff --git a/apps/codebattle/assets/MANTINE_MIGRATION.md b/apps/codebattle/assets/MANTINE_MIGRATION.md new file mode 100644 index 000000000..004ebc5b4 --- /dev/null +++ b/apps/codebattle/assets/MANTINE_MIGRATION.md @@ -0,0 +1,634 @@ +# Bootstrap → Mantine migration + +Living spec for moving the React frontend off Bootstrap/react-bootstrap onto +Mantine. Phases 0–1 are **done**; Phase 2 is **in progress** (shared leaf +components converted — see the progress log under Phase 2); Phase 3 is +**planned**. Described here in enough detail to resume without re-discovery. + +## Scope & standing decisions + +- **React only.** Global Bootstrap CSS (`bootstrap@4.6.2`, imported in + `assets/css/style.scss` and `assets/js/app.ts`) **stays** — 53 `.heex` server + templates still use Bootstrap utility classes. Do **not** remove the + `bootstrap` dependency or its CSS until the heex templates are migrated + (Phase 3, out of current scope). +- **Idiomatic Mantine.** Prefer Mantine layout primitives (`Group`/`Stack`/ + `Flex`/`Box`) + style props (`mb`, `p`, `c`) over porting utility classes 1:1. +- **Incremental, per-widget.** One widget/page per PR, leaf → container, verify + each in the browser before merging. +- **Mantine version:** `@mantine/core` + `@mantine/hooks` v9. + +## Current status + +| Phase | What | Status | +|------|------|--------| +| 0 | Infra: Mantine deps, PostCSS, CSS-layer coexistence, theme, `MantineProvider` on all roots | ✅ done | +| 1 | Replace `react-bootstrap` **components** with Mantine; remove `react-bootstrap` dep | ✅ done | +| 2 | Convert Bootstrap **utility classes** in the ~244 React files to idiomatic Mantine | 🔄 in progress — shared leaf components done; pages: `settings`, `profile`, `lobby` (React markup) done | +| 3 | Migrate `.heex` templates; fully remove Bootstrap CSS + `bootstrap` dep | ⬜ planned (out of current scope) | + +## Coexistence model (how Bootstrap + Mantine live together) + +- Mantine core styles are imported as **`@mantine/core/styles.layer.css`** (in + `style.scss`), which wraps everything in `@layer mantine`. Unlayered Bootstrap + always wins on conflicts, so existing Bootstrap-styled UI is unaffected. +- Because Bootstrap wins, Mantine components that also carry Bootstrap/custom + classes (e.g. Alerts keeping `.alert-dark-theme`, Tables keeping `.table`) get + their look from those classes. Phase 2 removes the classes and lets Mantine + style them. + +## Key files (Phase 0/1 infra) + +- `assets/js/widgets/ui/theme.ts` — Mantine theme. Custom colors `cbSecondary` + (#3a3f50), `cbSuccess` (#46a077), `brand` (orange #ee3737). **Spacing scale is + matched to Bootstrap-4 spacers**: `xs=.25rem, sm=.5rem, md=1rem, lg=1.5rem, + xl=3rem` — so `mb-2 → mb="sm"`, `p-3 → p="md"`, etc. keep the same rhythm. + Also exports **`cssVariablesResolver`** (Phase 2) that sets + `--mantine-color-default-border` = `#4c4c5a` (`$cb-border-color`) in the dark + scheme, so `` matches the legacy `.cb-border-color` + panels. It is passed to `MantineProvider` in **both** `withMantine.tsx` and the + test provider `helpers/mantine.tsx`. `cb-rounded` (`$cb-border-radius` 0.5rem) + → Mantine `radius="md"` (md = 0.5rem). +- `assets/js/widgets/ui/withMantine.tsx` — provider wrapper; applied to every + React root in `widgets/index.tsx` and the Inertia root in `inertia.tsx`. + `forceColorScheme="dark"` (single baked-in dark look, no runtime toggle). +- `assets/js/widgets/ui/alert.ts` — `bootstrapAlertColor(variant)` maps BS alert + variants → Mantine colors. +- `assets/js/widgets/components/CbModal.tsx` — app modal wrapper: keeps a compound + API (`Modal` + `Modal.Header/Title/Body/Footer`) but renders a Mantine `Modal` + underneath, so ~21 modal call sites are untouched. Default `size="lg"` + (Bootstrap's `.modal-dialog { min-width: 700px }` no longer applies to Mantine's + DOM). +- `assets/js/widgets/components/PopoverStickOnHover.tsx` — Mantine `HoverCard` + behind the original API; exports `Placement` (= `FloatingPosition`). + +## Gotchas / learnings (read before Phase 2) + +1. **Mantine v9 components throw without `MantineProvider`.** Any test that + mounts a Mantine component must wrap in `MantineTestProvider` + (`assets/js/__tests__/helpers/mantine.tsx`). The app itself is fine — all + roots use `withMantine`. +2. **jsdom polyfills** for Mantine are in `vitest.setup.ts`: `window.matchMedia` + and `ResizeObserver`. Keep them. +3. **Portals in tests.** Mantine `Modal`/`Menu`/`HoverCard`/`Popover` render in a + portal. Query with `screen.*` and use async `findBy*` for open-on-interaction + content (modals/menus have mount transitions — sync `getByRole` races). +4. **`@/ui/*` path alias is broken in Vite's resolver** (only specific `@/` + subpaths are aliased; `ui` isn't). Import theme/helpers via **relative paths** + (`../ui/...`), not `@/ui/...`. tsc accepts `@/*` but Vite/vitest will fail to + resolve. +5. **Custom-event button theming.** `cb-custom-event-btn-*` classes (in + `external.scss`/`custom.scss`) are event-branded color overrides used across + ~8 files, applied conditionally via `className` on Mantine `Button`s. They are + pure color overrides and still win over Mantine's layer — handle them as a set + in Phase 2, don't drop them piecemeal. +6. **Menu items only render when open.** react-bootstrap `Dropdown.Menu` + rendered items always; Mantine `Menu.Dropdown` mounts on open. Use + `keepMounted` if a test/behavior depends on items being in the DOM while + closed (see `UserSettingsForm` language menu). +7. **Unlayered Bootstrap beats Mantine — watch `border-radius` on buttons.** + Bootstrap is imported unlayered (`style.scss` `@import 'bootstrap/scss/bootstrap'`) + while Mantine is in `@layer mantine`, so **any** unlayered rule wins over + Mantine regardless of specificity. Concretely, `style.scss` has + `.btn { border-radius: unset }` and utilities like `rounded-top`. A Mantine + `