Skip to content

Adopt @microbit/ui - #1248

Draft
microbit-matt-hillsdon wants to merge 104 commits into
mainfrom
experiment-rai
Draft

Adopt @microbit/ui#1248
microbit-matt-hillsdon wants to merge 104 commits into
mainfrom
experiment-rai

Conversation

@microbit-matt-hillsdon

@microbit-matt-hillsdon microbit-matt-hillsdon commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Replaces Chakra UI v2 with microbit UI library based on react-aria-components and Panda CSS.

Accepted differences

  • The button spacing betwen icon and text uses the theme token rather than hardcoded 0.5rem so is affected by the scaling done in the Python whereas it was the only length not affected under Chakra. So ~7px spacing rather than ~8px.
  • Sidebar tabs are now a single tab stop (up/down within) which is an accessibility improvement. It's maybe a bit odd how far away "Project" is despite being in the same tab group.
  • Very minor slider track height change
  • Very minor select text position change
  • Toast duration forced to >= 5s for accessibility
  • ... ?

TODO

  • Toasts with no duration passed don't get default they're persistent instead (bug in UI lib)
  • Reset project button: danger.500 red vs Chakra's red.600 outline
    — slight shade change, accepted provisionally.
    • This looks worse alongside the other buttons IMO, will explore further.

Census (July 2026, moved from ml-trainer's RAC-MIGRATION.md), the
semantic-token pre-work plan (startable while still on Chakra), and the
app-specific migration items. Method lives in the @microbit/ui repo's
docs/migration-playbook.md.
Plus two status-log caveats: the revived kit scripts are unexercised
(first differ run doubles as validation), and the private theme package
pulled changes postdating the census.
Semantic-token pre-work for the RAC/Panda migration (lockstep with the
private theme package). The OSS theme now carries the same component
structure as the private theme, with brand divergence expressed through a
new semanticTokens block rather than forked component configs:

- Button: adopt 2rem radius, add baseStyle + unstyled/outline/language
  variants (language via languageText* tokens)
- Alert: toast variant sets per-status bg via toast*Bg tokens
- Container/Tabs: sidebar chrome via sidebar* tokens (incl. gradient)
- Tooltip: new OSS config (fontSize md), registered in the theme

The resolved-theme differ now shows zero structural divergence between the
OSS and private themes. See RAC-MIGRATION.md.
Status-log the semantic-token pre-work: differ run + kit fix, census
cross-check, divergence classification, the executed 7-step convergence,
and toCSSVar verification of the semantic-token CSS-var emission. Adds
parked notes: the brand/brand2-vs-purple/teal colour-scheme finding (teal
does not map to brand2), brand-ramp data nits, and the languageText
base-preset default question for the shared-ui repo.
Record the headless side-by-side vs python.microbit.org/v/3: the sidebar
Tabs gradient renders byte-identical (brand.500 -> blimpTeal.400) to live,
confirming the inline-string -> semantic-token refactor preserves the branded
appearance. Notes the sandbox setup needed (node_modules reinstall for
platform-native binaries, proxy-authed Chromium, CI=true vite).
RAC/Panda migration step 2: install Panda alongside Chakra without porting any
components. The app renders unchanged on Chakra; the generated CSS is inert
until components port.

- Deps: @pandacss/dev, @csstools/postcss-cascade-layers; @microbit/ui linked
  via file:../ui/packages/ui (ships as source); react-aria-components (peer).
- panda.config.ts: preset stack (preset-base -> @microbit/ui/base-preset ->
  app preset -> optional private brand preset), eject:true, preflight:false,
  include app + @microbit/ui source.
- App preset src/deployment/default/panda-preset.ts: code.* palette + sidebar/
  language semantic tokens the base preset lacks.
- Coexistence CSS: panda script does codegen + cssgen + unlayer; styled-system
  alias in tsconfig + vite; styled-system.css imported first in index.tsx;
  generated output ignored by git/eslint/prettier.
- Legacy Safari (<15.4, app targets Safari/iOS 14): postcss.config.cjs runs
  expandLogicalShorthands + cascade-layers in production only.

Providers + i18n are deferred to step 3 (first shared component port). See
RAC-MIGRATION.md.
Status-log step 2: preset stack + coexistence CSS wiring, legacy-Safari
postcss, verification (panda/typecheck/build/lint clean, app unchanged on
Chakra), the section-D differ improvement (base-preset merge; 55->13, residual
= known brand-ramp divergence), and the deferral of providers/i18n to step 3.
RAC/Panda migration step 3, pilot area. Replace Chakra AlertDialog/Button/Text
with @microbit/ui Modal(role=alertdialog)+Button; onClick->onPress; cancel =
default (outline), destructive = warningSolid. Drop leastDestructiveRef
(gotcha #15) for autoFocus on cancel.

- App preset: add python-editor's outline button variant + defaultVariants
  (outline), the census-flagged per-app default (base recipe defaults to
  secondary). staticCss [*] covers it.
- vite resolve.dedupe for react/react-dom/react-aria-* — @microbit/ui is
  dev-linked as source, so its react imports otherwise resolve to the ui
  monorepo copy (two Reacts -> invalid-hook crash; gotcha #19).

Verified byte-identical to live python.microbit.org/v/3: cancel brand.500
outline, replace white-on-red.500, both pill radius.
Log the first coexistence port (ConfirmDialog -> @microbit/ui Modal), the
per-app outline button default, gotcha #19 (dual React from dev-linking), and
the byte-identical-to-live verification.
RAC/Panda migration step 3, dialog infrastructure. The shared GenericDialog
(shell for 8 dialogs) and GenericDialogFooter move onto @microbit/ui Modal +
Button + Panda HStack. Props API preserved, so the consumer dialogs are
untouched and render their (still-Chakra) content inside the RAC Modal during
coexistence.

- Modal/ModalOverlay/ModalContent -> Modal + ModalCloseButton/Header/Body/
  Footer; minWidth via contentCss + isCentered.
- returnFocusOnClose is now a no-op (RAC restores focus; gotcha #15), kept for
  API compat; finalFocusRef still wired.
- Footer: HStack from styled-system/jsx; don't-show-again Link->Button
  variant=link; close solid->primary.

Verified typecheck/build clean and the PostSaveDialog (exercises shell +
footer) visually identical to live python.microbit.org/v/3.
RAC/Panda migration step 3. Finish the shared common/ dialogs:
- PostSaveDialog/MultipleFilesDialog bodies: Chakra VStack/Text/Link ->
  Panda styled-system/jsx VStack + @microbit/ui Text/Link.
- InputDialog: own Modal -> @microbit/ui Modal + Panda styled.form; shared
  submit() (form onSubmit for Enter, footer Button onPress); primary action,
  isDisabled when invalid.
- ProgressDialog: own Modal -> @microbit/ui Modal + ProgressBar (aria-label
  from 'loading'); non-dismissable during the operation.

Typecheck/build/lint clean; smoke-tested Save -> InputDialog -> PostSaveDialog
on the local branded build (renders, no errors, body matches live).
RAC/Panda migration step 3. All 9 workbench/connect-dialogs/ files
(ConnectCableDialog, Overlay, WebUSBErrorDialog, TransferHexDialog,
ConnectDialog, WebUSBDialog, FirmwareDialog, NotFoundDialog,
ConnectHelpDialog) move off Chakra: Text/Link/Image -> @microbit/ui, layout ->
styled-system/jsx, Button solid->primary / default->outline, onClick->onPress,
useMediaQuery->useBreakpointValue, useDisclosure->useState.

FirmwareDialog uses an <a> with button() recipe classes (no LinkButton
primitive yet - flagged for the library). No @chakra-ui imports remain in the
folder; tsc/build/lint clean; smoke-tested 'Send to micro:bit' -> ConnectDialog
renders correctly.
RAC/Panda migration step 3. WelcomeDialog, FeedbackForm, AboutDialog move off
Chakra; common/ModalCloseButton deleted (all dialogs use @microbit/ui's).

- AboutDialog <Table> -> Panda styled HTML table (app-side, owner decision).
- AspectRatio -> native aspectRatio CSS (gotcha #11).
- Kept Chakra Collapse (AboutDialog read-more; library gap), inlined
  useClipboard, SimpleGrid->Grid, useDisclosure->useState.
- Modals without a header got a (localized) aria-label for RAC.

tsc/build/lint clean; smoke-tested Help->About renders correctly (table,
images, buttons, collapse), no console errors.
- ToastProvider mounted inside TranslationProvider; bin/compile-lang.mjs
  merges @microbit/ui's lang catalogs into the per-locale message chunks
  (i18n:compile).
- use-action-feedback, language-server toast plumbing and
  TranslationProvider move to the library's context-free useToast/ToastFn.
  Toast id dedupe replaces the manual isActive guard; the XTerm paste
  toast's bottom-right position is dropped (single top region).
- common/ leaves ported: MaybeLink, AreaHeading, ErrorBoundary,
  Placeholder, Spinner, YoutubeVideoEmbed, ScrollablePanel,
  HeadedScrollablePanel, FileDropTarget, ExpandCollapseIcon; BoxProps
  forwarding dropped where consumers never used it.
- OSS panda preset gains this app's gray.10/25 values.
…okens

- CollapsibleButton: icon/button modes on library IconButton/Button;
  _collapsed becomes a Panda SystemStyleObject; dead buttonWidth dropped.
- FileInputButton: library Tooltip + plain hidden file input.
- Consumer prop translation across project buttons, SerialBar,
  SideBarHeader search, HideSplitViewButton (+ its three call sites).
  ProjectAreaNav Reset colorScheme=red becomes variant=warning.
- App preset gains a zIndex token scale mirroring zIndex.ts: imported
  constants in Panda styles are not statically extractable (the previous
  breadcrumb z-index only worked via a coincidental literal elsewhere).
- Menus (FileRow, SendButton, SaveMenuButton, ChooseMainScriptQuestion,
  MoreMenuButton) onto MenuTrigger/MenuList/MenuItem; split buttons keep
  the attached look via ButtonGroup isAttached. Portal + explicit menu
  z-index constants dropped (RAC popovers portal natively at the
  dropdown z-index).
- ChooseMainScriptQuestion uses the new library MenuOptionGroup/
  MenuItemOption (menuitemradio semantics).
- ProjectNameQuestion/NewFileNameQuestion onto the library TextField.
- ProjectArea/ProjectAreaNav/ProjectNameEditable/ProjectActionBar onto
  Panda layout + library Text/List/Tooltip; ProjectActionBar renders a
  styled.section (no as-prop polymorphism in Panda).
- project-actions dialog/toast JSX onto library Text/Link/List.
- SendButton's tooltip onFocus suppression hack removed: RAC tooltips
  only open on keyboard focus-visible.
- SettingsDialog/LanguageDialog onto the library Modal; language cards
  use the base preset's language variant; SimpleGrid becomes Panda Grid.
- SettingsMenu onto MenuTrigger with a new app-preset sidebar button
  variant (the Chakra ghost-based variant with the callers' white folded
  into the variant base).
- SettingsArea onto the new library NumberField (react-aria NumberField,
  Chakra NumberInput look), library Checkbox and NativeSelect.
- SelectFormControl renders a styled label row + NativeSelect.
- SerialArea renders a styled.section; XTerm/SerialBar use inline style +
  runtime token() for dynamic values (terminal bg, three-way status bg,
  compactSize-derived height).
- XTerm's useToken(fonts, code) becomes Panda token(fonts.code), backed
  by a new fonts.code token in the OSS app preset.
- SerialMenu/SerialBar onto MenuTrigger + the sidebar variant; SerialHelp
  onto the library Modal with the new library Kbd/Code components.
- TracebackLink/MaybeTracebackLink onto library Link/Text.
Owner decision: SplitView and the sidebar Tabs stay app components —
RAC provides tab behaviour and the split-view value is app layout
machinery; a generic library Tabs is parked until a second family
consumer exists.

Runtime-computed pane dimensions move to inline styles; static
conditionals stay extractable ternaries; zIndex constant becomes the
splitViewDivider token; BoxProps forwarding dropped.
GenericDialog's returnFocusOnClose has been a no-op since the RAC port
(react-aria restores focus to the trigger by default); drop the prop
and the returnFocus state its two remaining callers threaded into it.
MoreButton.tsx had no importers (everything uses ShowMoreButton).
Chakra passed aria-valuetext="21 °C" etc.; react-aria's Slider has no
valuetext passthrough, so SRs heard bare numbers since the port. Use
formatOptions for the two sanctioned units: temperature (celsius, long)
and compass heading (degree). Gated on Intl unit-style support (Safari
14.1/iOS 14.5) because react-aria's fallback throws for anything except
degree/narrow, which is what compass heading uses below the gate. The
accelerometer/compass axes' mg/nT have no Intl equivalent and stay
bare; light/sound/pins never had units. Also corrects RangeSensor.unit
to string|undefined per the simulator's own types.
Replaces the formatOptions approach (ead613b): the unit is constant
per slider, so it belongs in the name - announced once on focus -
rather than repeated on every value change. This also covers mg/nT
(which Intl's sanctioned unit list cannot express), reads uniformly
across all sliders and devices, and drops the Safari 14.1/iOS 14.5
feature gate entirely. Unit names are intl messages ("milli-g",
"nanotesla", ...) so screen readers aren't left guessing at "mg",
and they'll pick up translations via Crowdin (English fallback
meanwhile, per convention).
AGENTS.md now points at the playbook's gotcha catalog (which is where
the retired doc's transferable lessons moved); the fidelity harness
comments cite the playbook; the density preset comment carries its
revisit context inline.
Transferable lessons moved to ../ui/docs/migration-playbook.md (gotchas
23-26, fidelity worked method and harness notes, roadmap/vocabulary
updates); the parked brand-ramp material to the private repo's
docs/brand-ramp-review.md; the pre-merge manual review checklist to
python-manual-review.md (untracked); drop historical content.
The Safari <15.4 cascade-layer flattening (production-only PostCSS)
emulates layers with specificity boosts, and can only see the CSS it
processes per file. The base preset's Chakra-parity global reset
(* { border-color: gray.200 }) therefore ends up beating both
CodeMirror's runtime-injected styles and other app .css files in
production, while dev (real @layer) renders correctly:

- editor caret and drop cursor went gray.200 (the report)
- autocomplete/hover tooltip borders lost gray.400
- structure-highlighting l-shapes lost their code.blockBorder teal
- lint severity bars/marker triangles would lose red/orange
- docs-tooltip code chips lost blimpTeal.100

!important is the one lever that still wins from unprocessed CSS;
applied to exactly these border colours with pointers to the
explanation in CodeMirror.css. Verified in a local production preview:
caret black, l-shapes teal, autocomplete border gray.400. CM base-theme
gutter/panel #ddd borders also lose to gray.200 - imperceptible,
accepted.
alpha.7 moves the Chakra-parity * { border-color; word-wrap } defaults
from globalCss (base layer, specificity-boosted by the production
cascade-layer flattening) to @microbit/ui/reset.css, imported here into
the reset layer via layers.css. That systemic fix supersedes the
interim per-site !importants (7e02fff), reverted here: verified in a
production preview - caret black, structure-highlight l-shapes teal,
autocomplete tooltip border gray.400 - with the built CSS's global
reset rule confirmed unboosted.
The Chakra original was Link as="button" - content-sized,
left-aligned, inheriting the docs context font size. The port's
Button variant="unstyled" kept the recipe's centring, md height
(2.2rem - the taller focus box) and md font size (14.4px vs 14px).
Undo all three in the css override; probed identical to the live
Chakra app (h=21px, flex-start, 14px). Slipped past the fidelity
dual-run because the docs port predates the pre-flip baseline - only
the live site shows the Chakra original.
'content' is a Panda utility name (the CSS property), and Panda
extracts utility-named props from any capitalised JSX component
(playbook gotcha #17) - the unresolvable PortableText values emitted
two broken empty rules, the long-standing 'sheet:process' errors in
every codegen run. 'blocks' matches the BlockContent API it feeds.
panda cssgen now runs error-free.
Brings the Collapse flow-root measurement fix: expanded API docs no
longer clip their last line (gc.mem_alloc repro verified against the
published package - 74px measured, nothing clipped).
Chakra's Tabs hid all panels at index -1; react-aria retains the
previous selection, so the selected panel's content (e.g. the
pre-release notice) bled out of the 86px compact pane over the editor.
Drop the Chakra-era gray.100/gray.200 hover/active restatements and the
padding that widened the button: the plain ghost recipe (as used by
SearchDialog's clear button) gives the lighter blackAlpha hover, and the
square button's hover circle nests into the pill's rounded end.
…size

The alternatives select in reference documentation was size="sm" under
Chakra; the port dropped it while the input recipe was single-size.
alpha.9 also fixes the invisible unselected checkbox border and copy
buttons in unpermissioned/insecure contexts (useClipboard fallback).
These two were Chakra Button variant="link" (semibold via the Button
base style); the shared recipe's link variant is normal-weight (ported
from ml-trainer and matching this app's other link buttons, which were
Chakra <Link as="button">). Site-local bold to match the adjacent
"Beta release" label.
Chakra's <Divider borderWidth="1px"> set all four borders on the
zero-height <hr>, so the top and bottom 1px borders stacked into a
visually 2px rule — heavier than the plain 1px dividers in the inner
lists. The port (bfe5f30) dropped the prop as apparently redundant.
Ask for the 2px explicitly.
The Chakra theme's components.Text defaulted Text to md — 0.9rem on
this app's shrunken fontSize scale — and the port to @microbit/ui's
defaultless Text left unsized <Text> inheriting the 16px document size
(seen on the minimised simulator data-log row count and traceback
summary). Set via the new text recipe's defaultVariants.

Requires @microbit/ui > 0.1.0-alpha.9 (the text recipe).
With @microbit/ui's NativeSelect wrapper now width:100% (restoring the
Chakra Select fill that the simulator gesture select lost), width
constraints belong on the wrapper — where Chakra's layout-prop split
put them — not the select, which fills it.

Requires @microbit/ui > 0.1.0-alpha.9 (wrapperCss).
Text recipe (app preset defaults Text to md), Chakra slider root
geometry, and NativeSelect full-width + wrapperCss — the releases the
last three commits depend on.
Divider thickness variant + decorative-by-default, and the ButtonGroup
attached-children border overlap (restores the Save/Send split
buttons' 1px seam).
Chakra-era <Divider borderWidth="1px"> drew both edges of the
zero-height <hr> — a visually 2px rule — and the port flattened these
to 1px (same class as the top-level item dividers, found by pickaxing
history for Divider borderWidth). Expressed via the library's new
thickness="thick"; DocumentationTopLevelItem's hand-rolled 2px moves
to it too.

The search-result divider's Chakra color="gray.400" never reached the
border (dividers inherited the global gray.200); keep what shipped
rather than the unrendered intent.
These were Chakra <Link as="button">: no size scale, so they inherited
the dialog's 16px document size. The port to Button variant="link"
brought the Button size scale with it (md = 14.4px on this app's
shrunken fontSizes), silently shrinking them. size="lg" (16.2px) is
the nearest scale size; the link variant still zeroes lg's
height/padding. Verified against production: box centres within
0.25px.

ModuleOverlay's inline link is the other Link-conversion but sits in
md Text where inherited and variant sizes coincide — unchanged.
The connection library signals completion by calling the progress
callback with an undefined value. Since the connection library
v1.0.0-beta.0 upgrade (e6b4a7d) we rendered that as 0%, so the
progress bar visibly jumped backwards while the dialog stayed open
during post-flash reinit/reset (most noticeable after partial flash).
Hold the bar at its last value until the dialog closes instead.
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