Skip to content

Migrate UI package from Blueprint to Mantine - #627

Draft
noahm wants to merge 8 commits into
partykitfrom
claude/mantine-ui-migration-sju0b6
Draft

Migrate UI package from Blueprint to Mantine#627
noahm wants to merge 8 commits into
partykitfrom
claude/mantine-ui-migration-sju0b6

Conversation

@noahm

@noahm noahm commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Increasingly the slow pace of blueprint is holding this app back, and feels bad to use for ethical reasons as well. Time for something new!

claude added 2 commits June 10, 2026 06:55
Migrates the entire UI from @blueprintjs/* packages to @mantine/core,
@mantine/dates, and @mantine/notifications, with @tabler/icons-react
replacing @blueprintjs/icons.

Highlights:
- MantineProvider drives light/dark mode via the existing theme store
  (forceColorScheme), preserving OS preference sync, the in-app toggle,
  and OBS browser-source detection
- Toaster adapter wraps @mantine/notifications behind the existing
  toaster.show()/clear() API used across the app
- New shared EmptyState and HeaderBar components replace NonIdealState
  and Navbar
- Blueprint's Omnibar song search replaced with a Mantine modal search
  with keyboard navigation
- Gauntlet score editor rebuilt on Mantine Table with inline-editable
  cells and per-column sort menus (drops @blueprintjs/table)
- Release date filter now uses @mantine/dates DateInput
- Removed unused round-label and sortable-columns modules

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
Brings the Mantine migration up to date with partykit, re-expressing the
UI that landed upstream in the meantime with Mantine components:

- config-select: batch export dialog and "From JSON / From scratch"
  creation, using Modal + Checkbox (Button.Group stacks vertically so the
  labels fit the narrow config column)
- player-list-input: new drag-to-reorder player list ported to
  TextInput/ActionIcon, keeping react-movable and the stable player ids
- diagnostics-dialog: new connection diagnostics panel ported to
  Modal/Alert/Badge, with the pending-count badge kept beside its heading
- header: connection diagnostics menu item (IconActivityHeartbeat)
- cab-management: OBS sources menu restructured to the nested
  Players > Single Player layout with the new player/N routes
- dashboard: delete button for OBS text sources
- card-label / acton-menu / drawing-labels: follow the switch from
  player indexes to player ids
- toaster: added dismiss() and the "import" icon that the new sync and
  config-import code calls

Non-UI logic (state slices, party server, sync manager, diagnostics,
models) is taken from partykit unchanged.

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ddr-tools Ready Ready Preview Aug 11, 2026 12:03am

Pill is Mantine's component for a tag with a built-in remove button, so it
replaces the Badge that had a CloseButton stuffed into its rightSection.

- the white outline is now on the pill itself, so it follows the pill's own
  radius. Before, the outline's 0.5em radius disagreed with the badge's `sm`
  radius and the two sets of corners visibly didn't line up
- the remove button gets hover and focus-visible styling via Pill's `remove`
  Styles API slot, so it reads as its own click target. A darkening overlay
  works over every label color without a rule per color
- Pill already stops click/mousedown propagation on remove, so removing a
  label no longer relies on our own stopPropagation to avoid opening the
  card's action menu
- Pill hides its remove button from the tab order and from assistive tech
  (a PillsInput drives it from the surrounding input). Here it's the only
  way to undo an action, so it's restored to the tab order and given an
  aria-label

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
The toaster module was a Blueprint-shaped shim (intent, timeout,
icon-by-string-name, a hand-built action button) sitting on top of
@mantine/notifications, and it lost the old behaviour where an intent
coloured the entire toast — Mantine only tints the icon.

Replaced it with src/notify, which passes Mantine's own notification
props straight through (title, loading, autoClose, allowClose, icon,
priority, position) and adds one thing on top: `intent`, which both picks
the colour and fills the whole surface via the Notification Styles API
slots, restoring the old at-a-glance read. Notifications with no intent
keep Mantine's default neutral treatment.

Adopted along the way, rather than porting the old shapes:

- the update flow is now one notification that transitions in place with
  notifications.update: a non-dismissable spinner while the update
  installs, then green with the Apply action. It previously cleared all
  toasts and opened a second one under a different key — and those two
  keys disagreed ("UpdateManager" vs "UpdateManger"), so the clear was
  doing the work the key should have
- timeout: 0 -> autoClose: false, and allowClose where dismissing early
  would be wrong
- icon-name strings ("warning-sign", "import", …) -> real Tabler icons,
  which drops the lookup table entirely
- config load failure now puts the error text in the notification body
  under a title instead of only in the console
- toaster.dismiss -> notify.hide, toaster.clear -> notify.clean

share and config-persistence render icons now, so they become .tsx.

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
Blueprint set `body { font-size: 14px }`; Mantine sets it to 16px. Nothing
in this repo overrides either, so every percentage-based font-size in our
own stylesheets grew by 14%, and the card text that stacks several of them
grew most visibly.

Scaled those percentages by 14/16 so they compute to the sizes they had
before: song name 19.6px, name translation 10.5px, match labels 28px,
weight percentages 9.8px. They stay percentages rather than becoming rem
so a custom OBS stylesheet can still scale a whole card by setting one
font-size on it.

Rules in rem or pt are untouched — Blueprint never changed the `html`
font-size, so those already computed the same under both.

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
Brings in the user-supplied SMX edit charts feature, re-expressing its UI
with Mantine:

- smx-edit-import: the publish dialog ported to Modal/TextInput/Textarea/
  Alert, keeping the Turnstile widget, debounced lookup and publish gating
- song-card variants/smx: edit author byline and the QR share popover;
  Blueprint's Barcode became IconQrcode, which is what that popover
  actually shows
- song-card: variant-supplied info actions now drive the footer dropdown,
  showing either the action menu or an open action's content
- acton-menu: info action rows use closeMenuOnClick={false}, matching
  Blueprint's shouldDismissPopover={false} so the dropdown can swap to the
  action's content in place
- version-select: Select has no action-row concept, so the custom-data
  group's "create" entry is an option carrying a sentinel value that
  onChange intercepts to open the dialog instead of selecting
- config-page/config-select: route-driven config selection and the
  game-name-in-label change carried onto the Mantine markup
- song-search: chart identity de-duplication for edit charts

Two things fixed rather than carried over:

- turnstile.tsx assigned a ref during render, which fails the repo's own
  oxlint rule on partykit as well; moved into an effect so validate passes
- the new 300%/115% font sizes were written against Blueprint's 14px base,
  so they were scaled by 14/16 like the rest of our styles

Note: song-search/index.tsx contains a literal NUL byte (a `.join()`
separator), which makes git treat the file as binary and refuse to merge
it — this merge applied that file's changes by hand. Writing it as "\0"
here keeps the file mergeable.

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
…-migration-sju0b6

# Conflicts:
#	src/song-search/index.tsx
Select has no concept of an action row, so "Import StepManiaX edits…" was a
normal option carrying a sentinel value, sitting at the bottom of ~40 games:
indistinguishable from a game, and only reachable by scrolling to the end.

Combobox gives control of the dropdown's contents, which buys three things:

- Combobox.Search filters the list. With 40 data sets and growing, typing
  "pump" to get three results beats scrolling.
- Combobox.Footer pins the import action outside the scrolling option list,
  so it stays visible — including while the list is filtered — and reads as
  an action rather than another game.
- Group labels get their folder icon back, which the Blueprint menu had and
  Select's plain group headings dropped.

The action is still a Combobox.Option so it stays in the arrow-key flow, and
is intercepted in onOptionSubmit, so the sentinel can never become a value.

https://claude.ai/code/session_01Duin5Bjnq1D6V3XzXx5qiW
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