Skip to content

Adopt @microbit/ui - #1248

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

Adopt @microbit/ui#1248
microbit-matt-hillsdon wants to merge 127 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
  • ... ?

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.
microbit-matt-hillsdon and others added 30 commits July 31, 2026 10:44
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.
classroom's Chakra theme carries the identical × 0.88 spacing/sizes grid and
× 0.9 fontSizes (md+) — same 2022 change, byte-identical values — so the
scale moved into the shared library as an optional preset rather than being
replicated in a second app preset. Drops common-sizes.ts/font-sizes.ts and
the local toTokens plumbing.

Verified a no-op: regenerated CSS has an identical set of custom properties
with identical values and byte-identical rules; only the emission order of
:root declarations shifts (the categories now come from an earlier preset in
the stack, and no token var is declared twice).

The keep-vs-align-with-the-family-scale question is unchanged, just now
answerable in one place.

Needs the next @microbit/ui publish (the dense-preset export); the pin here
still says 0.1.0-alpha.14 and wants bumping with it.
classroom's migration is extending the library, and this app tracks those
changes, so @microbit/ui is source-linked here the same way the private theme
package is. Both need re-creating after npm install.
The pin catches up with the library work classroom's migration added, so the
`../ui` symlink this app has been carrying since `dense-preset` landed is no
longer needed. AGENTS.md now describes linking as a local-development
arrangement rather than as the current state, since which mode is in force will
change often.

Three library renderings change here, all deliberate corrections: Tooltip takes
Chakra's colour, padding and radius (fba05fb), Icon sets Chakra's
vertical-align itself rather than relying on Panda's preflight (2f7e18a), and
Avatar shows its fallback until a photo loads (c80530d). The Icon change is
what moves `va_middle` earlier in the documentation-content snapshot: the same
three atomic classes in a different order, since the component now supplies the
property the call site used to be alone in setting.

Modal's isOpen/onClose became a union in the library and broke prop-forwarding
shells, but this app has none — the eight affected files were all in ml-trainer.

Verified: typecheck, lint, 217 unit tests, production build.
Its migration is done, and the harness was a migration instrument: the baseline
that gave it meaning was the pre-flip Chakra build, on both sides of the
OSS/private split. Against an ordinary previous commit it is a general
visual-regression suite instead, and it cannot see a dependency bump at all,
since it symlinks one node_modules into the baseline worktree. Reasoning and the
recoverable refs are in the playbook's "Fidelity harness" section; this tree's
copy — the newer generation, with the --baseline-only/--compare-only halves for
paired-package runs — is at f322b68.

Out with it: the Playwright fidelity project and its snapshot path, the
FIDELITY/FIDELITY_NO_WEBSERVER switches, the npm script and the .fidelity
ignore. The HTTPS_PROXY passthrough stays — it serves the ordinary suite in
sandboxed environments, not just the harness. `playwright test --list` still
enumerates 64 tests across 12 files.
Raises Safari/iOS from 14/14 to 14.1/14.5, which is a correctness fix rather
than a tidy-up: flexbox `gap` landed in exactly those two versions, and this
app's layout is built on the Panda stack patterns, which are gap-based. The
built CSS has 18 gap declarations against 24 display:flex, so on Safari 14.0 or
iOS 14.0–14.4 that spacing collapses to zero — nothing can polyfill it. The
floor claimed support the layout could not honour, and no device is stranded
there anyway: Safari 14.1 shipped for the same macOS range as 14.0, and every
iOS 14.0 device can reach 14.5.

Chrome/Edge come down from 98 to 90 to match the other two apps. The 98 was not
load-bearing: it arrived wholesale in the CRA→Vite switch (4425d16), replacing
CRA's usage-share default, with no stated reason, and Chrome 98 predates that
commit by two years. Nothing in src uses a Chrome 91–98 era API — no
structuredClone, .at(), findLast, Object.hasOwn or error.cause; the only modern
method is replaceAll, which is Chrome 85. The device-connection code lives in a
dependency and feature-detects, so it was never governed by this floor.

The two lists also disagreed with each other, which is what let them drift:
browserslist entries are OR'd, so with no explicit Firefox entry `defaults`
supplied Firefox 140 while the build targeted firefox104, and the Vite switch
had dropped CRA's `not op_mini all` / `not ie >0`, leaving Opera Mini and KaiOS
in the resolved set. There is now one BUILD_TARGETS const mirrored into
build.target and build.cssTarget, with browserslist as documentation pointing at
it — the same shape as the other two apps.

Deliberately not aligned: cssMinify stays esbuild rather than lightningcss.
That is a rendering change, not a support declaration — lightningcss rewrites
logical border-radius longhands into fragile :lang()-based physical rules — and
this app's PostCSS chain is documented around the current choice.

Verified: typecheck, lint, production build, and the production CSS still has
its logical shorthands expanded (421 longhands, no var()-bearing shorthands),
its layers flattened, and its 18 gap rules intact.
The data-microbit-org migration's library changes, released: Button
isLoading keeps the label in the layout (Chakra parity), Field helper
text regains its block box, NativeSelect chevron/cursor/padding parity,
Heading label/subtitle hold their font size, and SharedUIProvider now
passes the app locale to react-aria's I18nProvider. Mounted here inside
TranslationProvider so react-aria's built-in strings follow the app
language rather than the browser.
- Font size NumberField: labelPosition="side" replaces the four css
  props. The whole dialog now sits at md, so the number input grows to
  match the selects beside it (the sm was a Chakra-era accident; the
  labels are unchanged).
- SelectFormControl deleted for NativeSelectField; createOptions
  becomes a local helper returning <option>s.
- The two checkbox info texts become helperText: same look bar
  gray.700 -> gray.600, and they now reach aria-describedby.
PAIRS with a gray-ramp-v2 @microbit/ui build and the matching
python-editor-v3-microbit (its gray override shrinks to the designed
light stops, 10-300). Muted text written against the branded #a9aaa9
'600' moves to gray.500 (#767676 - AA, visibly darker: search headings,
module overlay, radio composer - eyeball each); the zoom pills' hover/
press move 400/500 -> 300/400, the nearest stops to the branded values
they were written against. The panel surfaces (gray.25 sidebar and
simulator, gray.50 project pane) deliberately do not move - no renames,
no light-stop changes. Left on gray.600 for the visual pass: the editor
gutter numbers and xterm scrollbar (darker with the ink grade, arguably
better; re-point to a light stop if not).
… press

Inactive line numbers move down to gray.500 (#767676) - the lightest
text-safe stop. Lighter than that is an audit-level 1.4.3 fail: gutter
numbers are functional text (tracebacks point at them). The active
line's number keeps gray.800. Note 4.5:1 is measured against white;
on the gutter's #fcfcfc it is ~4.44:1 - gray.600 is the retreat if an
audit quibbles.

The zoom/undo pills' press state takes the new gray.350 press-weight
stop (#b4b4b4, within a bit of the branded #b0b0b0 press) instead of
the re-graded 400. Pairs with @microbit/ui gray-ramp-v2 (which adds
the stop).
…value

Restores the light inactive line numbers (#b4b4b4 vs the branded
#a9aaa9). A known 1.4.3 exception - line numbers are functional text
and the AA floor is gray.500 - tracked in an issue rather than here.
On the terminal's #333 track the family gray.600 ink (#575757) was the
low-contrast option; 350 (#b4b4b4, = the parked gutter grey and near
the branded #a9aaa9) reads ~6:1 against it.
scrollbar-color is <thumb> <track>; the values were swapped since the
Chakra era, giving Firefox a #333 thumb on a light track - the inverse
of the WebKit rules above. Now light thumb on #333, matching WebKit.
Includes the two raw var(--colors-gray-25) inline-SVG fills in
SideBarTab (the sites the v1 spike's rename missed). PAIRS with
@microbit/ui and python-editor-v3-microbit builds that carry the
rename; against older builds the 75 sites fail black/transparent.
Recompiled the message catalogs: alpha.19 adds seven ui.* ids for the
react-aria strings a prop can replace (NumberField steppers, toast region,
ComboBox trigger/listbox, Select placeholder and row action). Without the
recompile they would fall back to English in every locale.
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