Adopt @microbit/ui - #937
Draft
microbit-matt-hillsdon wants to merge 97 commits into
Draft
Conversation
Stand up Panda CSS + react-aria-components alongside Chakra as the basis for migrating off Chakra UI, with reusable primitives in src/shared-ui/ (to be extracted into a shared library later). Visuals match the Chakra theme; the OSS-vs-private brand split is preserved as a Panda preset swap. - Panda via CLI codegen/cssgen (no PostCSS; Vite uses LightningCSS). Generated styled-system/ and src/styled-system.css are git-ignored. - OSS preset ports the exact Chakra v2 token set (snapshotted by bin/gen-chakra-tokens.mjs) plus this app's overrides; config recipes for button, heading, dialog. RAC interaction conditions widened to match RAC data-attributes so Chakra-shaped style objects work unchanged. - Coexistence: Chakra/Emotion inject unlayered CSS that beats Panda's @layer output, so bin/unlayer-panda.mjs strips Panda's layers during the transition (bin/panda-dev.mjs keeps it unlayered in watch). staticCss generates recipe variants used via runtime props. Emotion files excluded from Panda extraction. - shared-ui: Button, Text, Heading, Link, Icon, CloseIcon, List, Modal, Tooltip, Toast (+useToast), useBreakpointValue, layout re-exports. - LanguageDialog + ModalFooterContent migrated and verified against the live branded deployment; ToastProvider mounted in App (ChakraProvider retained). See RAC-MIGRATION.md for architecture, gotchas, and next steps.
The dev watcher spawned `panda --watch --outfile …`, but the bare
`panda` command runs codegen, which rejects `--outfile` ("Unknown
option"). Use `panda cssgen --watch --outfile …` instead — it
re-extracts src/styled-system.css on source changes and watches the
config too.
Build a shared-ui Menu primitive (MenuTrigger/MenuList/MenuItem) on react-aria-components, preserving the native Android back-button integration, and migrate SettingsMenu, LanguageMenuItem and SettingsMenuItem onto it. Styling comes from a new `menu` slot recipe and a `plain` button variant in panda-recipes.ts, mirroring Chakra's menu theme (3xs width, sm shadow, gray.200 border, gray.100/200 focus/press, 0.75rem icon spacing) so the dropdown matches visually. The square icon-button trigger zeroes horizontal padding so the recipe's default md px:4 doesn't clamp the icon. RAC restores focus to the trigger on close, so the LanguageDialog finalFocusRef focus-restoration hack is removed.
Extend the shared-ui Menu with a MenuDivider (RAC Separator + a `divider` slot) and add a shared-ui IconButton (square, px:0, isRound) for the action-bar icon-button triggers. Migrate HelpMenu and HelpMenuItems onto shared-ui: external links use RAC MenuItem href/target/rel (rendered as anchors), actions use onAction. SettingsMenu's trigger now uses the same IconButton, with the shared white/round/focus styling in ActionBar/action-bar-menu-button.ts. The menu icon slot gains fontSize 0.8em to match Chakra's MenuIcon; items passing an explicitly sized icon still override it.
The shared `actionBarMenuButtonCss()` helper returned the trigger's style object from a plain function, but Panda only statically extracts `css` prop literals at the JSX site — not objects returned from helpers. So h/w/minW utilities were never generated and the button fell back to the recipe's size:md (40px), shrinking the circular focus ring's radius. Replace the helper with an ActionBarMenuButton component whose inline `css` literal Panda can extract, and take the 48px dimensions from the recipe's size="lg" variant (always generated via staticCss) rather than utility overrides the size variant would otherwise beat.
Replace the Chakra Box/HStack/VStack in ActionBar with the shared-ui (Panda) patterns, and the stray Chakra HStack in ActionBarItemsRight. Chakra->Panda prop translations: spacing->gap, bgColor->bg, sx->css / direct style props. itemsLeftProps/itemsCenterProps are typed HstackProps (spread into HStack, whose row-only direction is narrower than StackProps). Export the Panda pattern prop types from shared-ui/system.ts. Surfaced (not caused) a branding bug: the Panda brand preset override wasn't in the emitted tokens, so the now-Panda header rendered OSS grey instead of CreateAI green. Root cause was a stale incremental codegen — 'panda codegen' didn't pick up the external preset change; a clean regen fixed it. All Panda brand colours (e.g. brand.500 #3182ce -> #007dbc) are now correct.
Split src/deployment/default/panda-recipes.ts into per-component <Component>.recipe.ts files in src/shared-ui/, still registered in the OSS preset. The private brand preset only overrides tokens/semantic tokens in practice, so the recipes aren't deployment-specific; deployment/default now holds just the genuinely brandable surface. Also correct stale recipe comments claiming the private preset overrides variants (divergence is token-driven via semantic tokens), and document the .recipe.ts convention in RAC-MIGRATION.md. Generated CSS is unchanged.
Migrate DataSamplesMenu, ProjectCardActions, ActionDataSamplesCard (record options) and TestingModelPage (MakeCode split button) onto shared-ui MenuTrigger/MenuList/MenuItem, and delete the old components/Menu.tsx back-button wrapper (shared-ui MenuTrigger already handles Android back), the now-unused ToolbarMenu and LoadProjectMenuItem. Details: - shared-ui MenuItem now wraps an icon-item's children in a flex:1 "label" slot span, matching Chakra's MenuItem so block children (the two-line record-options items) stack vertically. - MoreMenuButton is rebuilt on shared-ui IconButton. Chakra's borderLeft="1px" resolved via the borders scale to "1px solid" currentColor, so the split-button divider is white on filled variants and red on recordOutline; ported as-is. - LoadProjectInput moves outside the menu in DataSamplesMenu: RAC popovers unmount on close (Chakra kept the list mounted), which would drop the hidden file input's change event mid-pick. The menu item now triggers it via ref. - The Android save toast uses shared-ui useToast; Chakra's id-based dedup and per-call position aren't supported, so rapid repeat saves can stack duplicate toasts (region is top-centre). - The split buttons keep Chakra ButtonGroup isAttached during coexistence; its child selectors style the RAC trigger too. Verified against the live branded deployment (data-actions, record options, project card and settings menus match in placement, sizing, icons and dividers; Chakra's focus-first-item-on-open is the only interaction difference). Typecheck, lint, unit tests green; e2e radio flakes reproduce identically on the base commit under parallel load. ActionBar.tsx is a Prettier-only diff (the committed file was not format-clean).
With the brand package installed as a symlink to a sibling checkout, its font/image assets resolve to real paths outside the project root, which Vite's server.fs allow list rejects (403) — the GT Walsheim @font-face loaded but every font file request failed, so marketing headings fell back to the default font. Dev-only; builds bundle the assets. Add the theme package's realpath to server.fs.allow alongside the workspace root when the external package is present.
Add a shared-ui Drawer built on react-aria-components ModalOverlay/ Modal/Dialog with a Drawer.recipe.ts slot recipe matching Chakra's default xs drawer; placement (left/right) is a runtime prop generated via staticCss. The drawer has no title slot so it requires an aria-label. onCloseComplete (used to defer navigation until the exit animation ends) is an unmount-callback sentinel inside the overlay — RAC keeps the tree mounted until the exit transition finishes. NavigationDrawer and DefaultPageLayout are fully ported. Nav items are now li > button / li > a rather than Chakra's ul > button. The ProjectToolbarItems native share menu was a raw Chakra Menu with no Android back-button integration; as shared-ui MenuTrigger it now gets that behaviour too. shared-ui additions/changes: - Divider (horizontal hr matching Chakra's). - Button wraps leftIcon/rightIcon in Chakra-style spaced spans (iconSpacing 0.5rem), removing the need for call-site gap tweaks. - BackArrow becomes a plain Panda-styled svg (only used by the layout). - useNativeTabletBreakpoint switches to shared-ui useBreakpointValue. Verified: unit + full e2e green (66/66); pixel-compared against the live branded deployment at desktop and tablet widths, both drawer placements.
ConfirmDialog uses shared-ui Modal with role=alertdialog, isCentered
and autoFocus on the least-destructive (cancel) button, replacing
Chakra's AlertDialog/leastDestructiveRef. The confirm button uses a new
warningSolid variant (Chakra's solid + red colorScheme; same values as
record today, kept separate so recording UI and destructive actions can
diverge).
NameProjectDialog is the first form port, via a new shared-ui TextField
(react-aria TextField/Label/Input/Text/FieldError) with a `field` slot
recipe collapsing Chakra's FormControl/FormLabel/Input/FormHelperText/
FormErrorMessage. Focus keys off data-focused (react-aria treats
text-input focus as keyboard-visible, matching Chakra's _focusVisible
on inputs) and is declared after invalid so a focused invalid field
shows the focus ring, as in Chakra. Helper/error text use
lineHeight normal to match Chakra's dialog height exactly.
Modal grows role, isCentered (a `centered` recipe variant),
onCloseComplete and finalFocusRef (unmount sentinel shared with Drawer;
final focus applied on rAF after RAC's own restore) and
ModalCloseButton (closeTrigger slot, localised via close-action where
Chakra's CloseButton was hardcoded "Close").
react-aria popovers (menus included) render role="dialog" and linger
with data-exiting while animating out, so a bare Playwright
getByRole("dialog") hits strict-mode ambiguity when a dialog opens from
a menu. e2e page objects now use the modalDialog() helper
(src/e2e/app/shared.ts), which scopes to <section> — both Chakra and
shared-ui modals render the dialog on a section; popovers are divs.
Accepted interaction diff: the auto-focused cancel button shows its
focus ring even after mouse interaction (Chakra focused it invisibly).
Verified: unit + full e2e green (66/66); rename dialog (incl. invalid
state) and delete confirm pixel-compared against the live branded
deployment.
Bottom-up port of the home page: carousel stack (CarouselRow, SwiperCarousel, SwiperCarouselButtons, CarouselButton), ResourceCard, ClickableTooltip (+InfoToolTip), HomepageBanner, ProjectCard and the page itself. New shared-ui primitives: Card/CardBody (Card.recipe.ts, elevated + outline variants per Chakra md), LinkBox/LinkOverlay, Image, an AspectRatio pattern re-export, and left/right top/bottom Tooltip placements. ClickableTooltip is now a controlled shared-ui Tooltip with a react-aria Focusable trigger span and a document-level Escape listener (Chakra's closeOnEsc equivalent; the span-level handler missed the key). A new _shortHeight preset condition (@media max-height: 800px) replaces src/responsive.ts's shortScreenHeightBreakpoint for style usage — Panda's extractor cannot resolve constants imported across files. HomepageBanner keeps its own tighter 700px same-file query, matching the Chakra original (it shadowed the shared constant). Chakra's LinkOverlay-over-Button pattern needs two adjustments in RAC-land, both applied to the project/action card overlay buttons: - react-aria's usePress cancels presses landing outside the button's bounding rect, so an inset ::before overlay never fires on a RAC <Button>; the overlays are plain <button>s styled with the button recipe (the widened conditions match native :focus-visible). - The button recipe base sets position: relative, which anchors the overlay to the button itself; the call sites set position: static, which is what Chakra's LinkOverlay did over its Button. RecordingFingerprint's tooltip placement maps from Chakra's "end-end" to RAC's "right bottom". Deferred (noted in RAC-MIGRATION.md): rebuilding card collections on RAC GridList — native whole-item presses, arrow-key navigation and built-in multi-selection — as a deliberate UX change, best tried on the projects page grid first. Verified: unit + full e2e green (66/66); home page pixel-compared against the live branded deployment at desktop and mobile widths incl. the open storage tooltip, and whole-card click checked end-to-end on the built app.
Port ProjectsToolbar, Search, SortInput, the remainder of ProjectCardActions (checkbox + skip-to-toolbar button, which uses RAC excludeFromTabOrder rather than tabIndex=-1) and ProjectsPage itself. SimpleGrid becomes a Panda Grid; Chakra's Slide becomes a fixed bottom-sheet div with a transform transition. New shared-ui primitives (extracted after review feedback that the first pass leaned on call-site css): - input config recipe: Chakra's outline field, shared by new Input and NativeSelect components and TextField's input (removing the duplicate styles from the field recipe). State selectors match both native pseudo-classes and react-aria data attributes. - InputGroup/InputLeftElement/InputRightElement, so Search reads like the Chakra original. - ButtonGroup with isAttached; also replaces the two remaining Chakra ButtonGroups (record and MakeCode split buttons). Its child selectors use :first-child/:last-child, not -of-type: attached groups can mix element types (select + button) and -of-type matches per type, which silently skipped the corner squaring in SortInput. - Checkbox (Chakra md/blue; borderColor: inherit on the control so call sites tint via the root, as in Chakra). - LinkOverlayButton, encapsulating the plain-button + position:static + ::before overlay pattern from the home page; both card call sites now use it. SortInput details: the arrow button squares the recipe's pill radius to md on its outer corners, and the select gets flex:1/minW:0 so it shrinks below its longest option at narrow widths, clipping like Chakra's Select. Verified: unit + full e2e green (66/66); projects page compared against the live branded deployment for grid, single-selection with desktop attached toolbar, tablet labelled bottom sheet, mobile icon-only bottom sheet and the search state; sort-control seam radii checked via computed styles.
Page, DataSamplesTable/Row, HeadingGrid (className API; the TestingModelTable call site passes a css() literal), ActionNameCard, ActionDataSamplesCard, DataSamplesTableHints, ShowGraphsCheckbox, LedIcon/LedIconSvg/LedIconPicker, LiveGraphPanel/LiveGraph/ LiveGraphLabels/PredictedAction, and the Emoji/EmojiArrow/UpCurveArrow/ AlertIcon svgs. The still-Chakra dialog flows (Recording, TrainModelFlow, Welcome, ConnectFirst, ...) are a later pass. New shared-ui: CloseButton (a plain button rather than react-aria so call sites can extend the hit area with pseudo-elements, which the usePress bounding-rect check would defeat) and Toast id dedup (Chakra-parity suppression of repeat toasts, used by the name-too-long toast). New useElementSize hook replaces Chakra's useSize; it measures synchronously in a layout effect because LiveGraph paints its only frame while the chart is stopped and an async first measure resizes (= clears) the canvas after that paint. Emotion keyframes move to preset keyframes (tada, spin3d, microbitWobble, ledTurnOn/Off, recordingFlash) and the ported files come out of panda.config.ts's exclude list — Panda skips extraction for excluded files entirely, which left applied class names with no generated rules (visible as a 0x0 emoji svg in the hints). usePrefersReducedMotion becomes prefers-reduced-motion media queries. Chakra Portal containerRef usages become createPortal with state refs. LedIconPicker is a react-aria DialogTrigger/Popover; its decorative dropdown arrow reuses the button recipe's ghost/sm classes on a span (Chakra had IconButton as="div") to keep the hover effect. Border width/style/color are set as longhands where a colour is also set: border shorthands imply currentColor and the class order between the shorthand and borderColor utilities is not guaranteed. Accepted diff: the action-name-too-long toast renders our solid error style rather than Chakra's subtle variant. Verified: unit tests green; full e2e 65/66 with the documented flaky radio reconnection spec passing in isolation; data samples page compared against the live branded deployment (single/two actions with hints, record split button, LED icon picker, live graph panel) with computed-style, element-size and canvas painted-pixel probes. The comparison workflow is now documented in RAC-MIGRATION.md.
Page remainder, TestingModelTable, ActionCertaintyCard, PercentageMeter/PercentageDisplay (token() for the runtime prediction colour updates), CodeViewCard/CodeViewDefaultBlockCard/ CodeViewDefaultBlock. This completes the pages phase; the remaining Chakra surfaces are the dialog flows, EditableName, ProjectPreview and the tour/animation components. New shared-ui: - Slider (Slider.recipe.ts): react-aria Slider styled like Chakra's horizontal md slider, with an optional `mark` positioned at the value and revealed on focus-within (how the certainty percentage appears). - Spinner: Chakra's border-based spinner, sm/md. ButtonWithLoading is rebuilt on shared-ui Button + Spinner (label kept in layout and hidden while loading so the width is stable) and keeps the onClick prop name so its four still-Chakra dialog consumers are untouched. New usePrevious hook replaces Chakra's. The MakeCode blocks loader is an opacity-pulse skeleton (skeletonPulse keyframe) standing in for Chakra's SkeletonText shimmer. Verified: unit + full e2e green (66/66). This page is unreachable on the live deployment without a device, so it was compared Chakra-build vs Panda-build instead: a temporary spec on the e2e mock fixtures screenshots the trained-model page on the working tree and again with the port stashed — near-identical output; approach noted in RAC-MIGRATION.md as a preview of the fidelity harness. Accepted diffs: slider thumb shows its focus ring where Chakra's was invisible, ~3px internal shift in the certainty card, pulse-vs-shimmer skeleton.
Modal grows `motionless` (Chakra's motionPreset="none" — the connect
flow steps between dialogs without animation) and ModalHeader a `level`
prop. Chakra's closeOnOverlayClick={false} maps to
isDismissable={false}; react-aria handles Escape separately so it still
closes, matching Chakra.
ConnectContainerDialog (the shared shell) ported first, making its 12
step dialogs largely mechanical: WhatYouWillNeed (dynamic grid template
via inline style), ConnectCable, SelectMicrobitUsb/Bluetooth (list
spacing → flex gap, VisuallyHidden → srOnly), EnterBluetoothPattern,
ConnectBattery, ManualFlashing, UnplugRadioLink,
ConnectRadioDataCollection, ResetToBluetoothMode,
NativeBluetoothConnectBattery, and DownloadChooseMicrobit, whose radio
cards are now react-aria RadioGroup/Radio with render-prop state
(arrow-key navigation between cards comes free).
The ten error/troubleshoot dialogs (TryAgain, ConnectError,
BluetoothPermissionError, UnsupportedMicrobit, BrokenFirmware,
NativeBluetoothPairingLost, NativeBluetoothError,
ResetToBluetoothModeTroubleshoot, WebUsbBluetoothUnsupported,
BluetoothConnecting) were batch-converted — unwrap
ModalOverlay/ModalContent, map dialog props, onClick → onPress — with
the remainder fixed from typecheck errors (footer justifyContent → css,
Icon boxSize → css, isLoading → ButtonWithLoading).
Helpers: ExternalLink (Chakra's ExternalLinkIcon glyph inlined as svg;
built on shared-ui Link so jsx-no-target-blank sees the rel) and
DialogFooterLink. New shared-ui UnorderedList/OrderedList.
Deferred within the flow: BluetoothPatternInput — the pairing pattern
grid was recently reworked for screen reader accessibility (#926) and
its radio machinery gets its own pass.
Verified: unit tests green; full e2e 66/66 (the mocked bluetooth,
radio, reconnection and download specs exercise nearly every dialog
here); Web Bluetooth connect flow compared against the live branded
deployment — the what-you-need and WebUSB select-device steps are
pixel-identical.
The testing model page's edited-project code view spans rows with
rowSpan={actions.length + 1} — a computed value, so Panda generated no
CSS for it and the grid re-flowed, scattering the row cells (visible
with a real trained-and-edited project; the e2e mocks only exercise the
default-code path). Now an inline gridRow span style.
An audit for other computed style props in ported files found two more,
fixed the same way: PercentageMeter's prop-driven width (rendering
correctly only because the certainty slider call site happens to emit
an identical w_240px class) and TestingModelTable's
scrollbar-compensated calc() width.
Recording, the progress dialogs (DownloadProgress/TrainingModelProgress/ Save/Loading/LoadingOverlay), Welcome/About/Settings/Feedback, the three help dialogs, TrainModelInsufficientData, TrainingError, ImportError, NotCreateAiHex, MakeCodeLoadError, IncompatibleEditorDevice, ConnectFirst, EditCodeDialog and SelectFormControl (label + NativeSelect + chevron). New shared-ui: ProgressBar (RAC, Chakra md Progress; fill via barCss) and Switch (Switch.recipe.ts, Chakra md/blue). Modal grew isKeyboardDismissDisabled, contentCss and aria-label (for dialogs without a ModalHeader). Chakra's useClipboard/useDisclosure replaced with navigator.clipboard/useState at the call sites. Spinner and ProgressBar merge base + caller css into a single css() call: same-property overrides across separate css() calls race on stylesheet order and were silently losing (LoadingOverlay's 166px spinner rendered 24px; progress fills stayed blue.500). SettingsDialog drops the Chakra-era initial-focus hack: react-aria focuses the dialog element itself on open, so the first <select> never steals focus. Its graph preview uses the native aspect-ratio property because the AspectRatio pattern's &>* child selector loses to the still-Chakra RecordingGraph's own position style. Deferred to their own passes: Tour/TourOverlay (usePopper positioning + spotlight overlay), EditableName, ProjectPreview, animation components. Verified against live: Welcome, Settings, About, Feedback, InsufficientData and SaveHelp match (Feedback pixel-identical); 66 e2e and 406 unit tests pass.
Drop the per-chunk changelog (git history covers it); promote the still- operative learnings buried in those entries to first-class gotchas (Emotion exclude list, AspectRatio vs still-Chakra child, popover unmount/file inputs, modalDialog() e2e helper, react-aria focus defaults) and collect the deliberate Chakra behaviour diffs into one section.
…gences bin/diff-chakra-themes.mjs diffs the resolved OSS and private Chakra themes (esbuild-bundled so both share this repo's hoisted Chakra, as vite's theme-package alias does at runtime) and cross-checks that the Panda preset pair encodes the same delta for every divergent token. Result: 70 token deltas, all within the seven brand colour ramps the private preset overrides, all mechanically reproduced by the Panda presets; 3 structural diffs (language button colour + hover, marketing heading font), all already driven by the languageText/languageTextHover/ display tokens. Closes the gotcha #6 TODO.
RecordingGraph and RecordingFingerprint (data-driven grid columns and
cell colours via inline style), EditableName (Chakra Editable rebuilt as
a preview button/input swap: Enter/blur commits, Escape reverts,
select-all on edit, focus returns to the button), ProjectPreview and
OpenSharedProjectPage (array responsive props to object syntax;
BlocksLoadingSkeleton extracted from CodeViewCard for the MakeCode
loader), ChooseDeviceOverlay, NativeConsentDialog, CodePage, and the two
remaining Chakra useToast call sites (App.tsx, project-hooks).
New shared-ui: VisuallyHidden (Panda srOnly span; all raw
css({srOnly: true}) call sites swept onto it) and Toast isActive/update
plus duration: null persistence for the storage-error toast (update
re-adds, so it re-animates, unlike Chakra's in-place update).
Verified against live: toolbar and drawer EditableName (editing state
pixel-identical, behaviour probes match), the shared-project preview
page (pixel-identical including RecordingGraph previews; blocks-frame
computed styles match exactly). 66 e2e and 406 unit tests pass.
…ation Link wrapper is now styled(RouterLinkAdapted) - Panda extracts style props on styled-factory components cross-file, so call sites are unchanged. AppLogo instead takes a css prop: forwarding style props through a plain wrapper component is not extracted (its call-site transforms were silently missing from the generated CSS); call sites updated and the vertical divider is a one-off (shared-ui Divider is horizontal-only). Also PreReleaseNotice, FileDropTarget, ProjectDropTarget (drops an unused BoxProps spread), LoadProjectInput (plain hidden input), icons/PauseIcon (plain svg), PauseResumeAnimationLink, ErrorPage and ErrorHandlerErrorView. NewPageChoice and StepByStepIllustration had no importers (NewPage was removed with the multi-project work) and are deleted rather than ported. Verified: 404 page link computed styles identical to live; welcome pause link renders; drawer header strip byte-identical before/after the AppLogo change. 406 unit tests pass; e2e green on rerun (one transient open-shared-project failure under parallel load - it fetches the real MakeCode API - passed in isolation and in a full rerun).
HowItWorksAnimation (18 files), PairingModeAnimation (5),
PlugMicrobitAnimation, LoadingAnimation, ArrowOne/ArrowTwo and
AnimationProvider are now shared-ui/Panda. utils/animations.ts is
deleted and panda.config.ts's Emotion exclude list is empty.
All animation keyframes move to the preset (Panda emits preset
keyframes unconditionally, so inline-style-only references are safe).
Runtime-parameterised Emotion keyframes become static keyframes over
CSS custom properties set inline per instance: gauge segment colours
(--gauge-*), signal travel offset and per-dot opacities (--signal-*),
and the wave-scroll window width (--wave-window). withPlayState
animation shorthands are inline styles throughout; CodeBlocks' only
per-breakpoint keyframe uses an extracted animationName {base, sm}
plus uniform inline longhands.
New shared-ui Svg (styled svg with Chakra Icon base sizing) hosts the
custom-path icons; icon wrappers take css/style props rather than
forwarding style props, which the extractor cannot see.
Verified by Chakra-build vs Panda-build stash-compare (live runs an
older release with different animations): the welcome animation at a
paused checkpoint, the connect-cable plug animation and the
native-flow reset-press animation match frame-for-frame. One deliberate
fix, noted in RAC-MIGRATION.md: the A/B-hold pairing label fill-up
interpolated a raw token name into a linear-gradient, which Chakra
never resolved, so it likely never painted; the port uses the resolved
colour. 66 e2e and 406 unit tests pass.
Spotlighted steps are a RAC Popover anchored to the spotlit element via
an external triggerRef (assigned during render so the keyed,
remounted-per-step popover measures its new anchor on mount), with an
OverlayArrow (white, shadowless, rotated via the placement data
attribute) and shouldCloseOnInteractOutside disabled to match Chakra's
closeOnOverlayClick={false}. Default placement is "bottom" - RAC
defaults to "top" but Chakra's popper defaulted bottom. Selector-less
steps render as a shared-ui Modal; Modal grows overlayCss so
TourOverlay can own the dimming in multi-step tours. TourOverlay's
spotlight svg is unchanged apart from createPortal/token swaps.
Chakra's returnFocusOnClose={false} (Tab restarts from the page top,
and on the MakeCode page focus restoration would land in the editor
iframe) is reproduced by blurring the active element before each tour
action: react-aria only restores focus on unmount while focus remains
inside the dialog.
model.ts and tours.tsx no longer import Chakra (TourStep's placement
and modalSize are now local/shared-ui types), removing two kill-switch
type unpicks early.
Verified by Chakra-build vs Panda-build comparison over the mock
connect flow's Connect tour: step content, arrows, spotlight cutouts
and end-of-tour focus (body) all match; accepted diff is RAC's 12px
viewport-edge clamp where popper sat flush. 66 e2e and 406 unit tests
pass.
Each pattern column remains a radio group whose options are the LEDs, with the checked option the topmost lit LED. The ARIA tree is verified structurally identical to the Chakra build via Playwright ariaSnapshot (radiogroup names, per-LED labels, checked semantics), as are keyboard behaviour (arrow keys change the LED count, one tabstop per column), the e2e test-id contract and the rendered pixels. The subtle part is the reactivate affordance: clicking the checked topmost lit LED turns it off, and radios fire no change event for a click on the checked option. react-aria's press handling swallows the click before React's synthetic handlers see it, and re-selects the pressed value against current state late in the dispatch, which silently reverts any state written by an earlier handler. A native capture listener on the option wrapper defers the reactivate write by a tick so it lands after react-aria's press processing; the listener is only wired on the currently-checked option so selection clicks are unaffected, and keyboard activation of a checked radio also fires a click, matching Chakra. 66 e2e (which drive this input in the bluetooth flows) and 406 unit tests pass.
@microbit/ui merged its core + micro:bit-foundation presets into a single base-preset (with OSS default brand values) and dropped the separate foundation/oss preset. Update the import and stack accordingly: base-preset, then the app preset, then the optional private brand preset. Verified output-identical: the generated styled-system.css for the branded build is byte-equivalent (ordering-only) before and after.
i18n:compile now runs bin/compile-lang.mjs, which compiles each locale from lang/ plus the source catalogs @microbit/ui ships, so package strings ride the existing lazy per-locale chunks instead of an eagerly bundled catalog-of-all-locales in the main chunk (verified: the lol marker string moved from index-*.js to ui.lol-*.js). Tracks the package-side change (ui repo 2329f67) that dropped the @microbit/ui/messages catalogs export, and removes the withSharedUiMessages runtime merge it fed.
Exact-pinned to 0.1.0-alpha.3, published from github.com/microbit-foundation/ui (byte-identical to the sibling checkout the symlink pointed at; verified with a clean Panda regen, unit tests and the catalog chunk-placement check). Fresh installs and CI now get the package with no extra wiring; symlinking back to ../ui/packages/ui remains the local package-development workflow. The lockfile churn is dropped "dev": true flags: @microbit/ui peers on @pandacss/dev and friends, which were previously dev-only.
Panda wraps all its CSS in @layer, which older browsers (Safari <15.4) drop wholesale, leaving the app unstyled. Switch Vite off the lightningcss transformer (which disables PostCSS and does not downlevel @layer) to the default PostCSS transformer and run @csstools/postcss-cascade-layers to flatten @layer into :not(#\#) specificity fallbacks. lightningcss is kept as the minifier. Cost is ~+8% gzipped CSS. Also degrade the cross-tab-sync BroadcastChannel to a no-op EventTarget where it is unavailable, so the module no longer throws at load on Safari <15.4.
Safari 14.x silently drops logical *shorthands* whose value contains var() (padding-inline: var(--spacing-2) applies nothing, though a literal value or the -start/-end longhands both work). Panda emits these for its px/py/mx/my utilities, so most spacing collapsed on Safari 14.1. - Add an expand-logical-shorthands PostCSS plugin that rewrites the inline/block shorthands into their -start/-end longhands (kept logical, so RTL still flips; lightningcss won't downlevel logical props itself). - Pin build.cssTarget to safari14.1. Without it the lightningcss minifier inherits build.target (es2017 -> ~Safari 11) and downlevels logical longhands into fragile :lang()-based physical rules; safari14.1 ships clean logical output. var()-valued longhands can't be recombined into the buggy shorthand, so the fix is stable.
Replace the scattered, browser-unaware build inputs with a single pinned
floor:
- BUILD_TARGETS in vite.config.ts drives both build.target (JS syntax via
esbuild) and build.cssTarget (CSS via the lightningcss minifier).
- Document the same floor in package.json's browserslist for humans and
browserslist-aware tooling.
- Remove the dead css.lightningcss.targets block: since the transformer is
PostCSS, css.lightningcss was ignored and the minifier read build.target
(es2017 -> ~Safari 11), which is what forced logical props into fragile
:lang() rules. Also drops the now-unused browserslist/lightningcss imports.
Previously build.target ('es2017') was the only real target input and CSS
inherited it; now JS and CSS share one honest floor.
Now that the CSS pipeline is on PostCSS (not the lightningcss transformer), use Panda's PostCSS plugin to generate the CSS instead of the CLI cssgen + a static styled-system.css import: - postcss.config.cjs runs @pandacss/dev/postcss first, then the two legacy Safari plugins (logical-shorthand expansion, cascade-layers). - panda script drops cssgen (codegen only, still needed for the TS helpers); panda:watch removed (the plugin gives live CSS in dev). - main.tsx drops the styled-system.css import; layers.css is now the plugin's injection entry. - Update panda.config.ts and RAC-MIGRATION.md notes accordingly. Verified: production build emits Panda CSS with @layer flattened and logical shorthands expanded, unchanged output size.
Replace ml-trainer's inline copy of the logical-shorthand plugin with the shared @microbit/ui/postcss-legacy-safari export now that alpha.4 ships it. Behaviour and build output are unchanged (verified: @layer flattened, no var()-valued logical shorthands, same gzip size).
Safari 14.1 support
Swiper ships modules/autoplay.css as a 0-byte file (the autoplay module has no styles), and postcss-import warns about empty imports on every dev server run. The autoplay behaviour comes from the JS module, which is unaffected.
Comment on lines
+21
to
+22
| and **Panda CSS** for styling, building reusable primitives in `src/shared-ui/` | ||
| (intended for later extraction into a library shared across sibling apps). The |
There was a problem hiding this comment.
I guess src/shared-ui doesn't exist because it is already extracted as https://github.com/microbit-foundation/ui ?
There was a problem hiding this comment.
Yeah this doc is a bit of a mess, I want to delete it but first extract some kind of playbook for updating other apps.
There was a problem hiding this comment.
I killed the doc. There's now a temporary one in the @microbit/ui as context for new conversions.
The reusable method (sequence, gotcha catalog, fidelity/verification recipes, family roadmap) moved to ../ui's docs/migration-playbook.md and the censuses moved out (python-editor-v3's to that repo's own doc). What remains here: open items and current how-to-run/verify up top, the frozen migration/extraction record below. Also drop bin/gen-chakra-tokens.mjs: its output path (src/shared-ui/) is gone since the extraction; the script now lives in the ui repo's migration kit.
Picks up the languageText default change (brand.500/600): OSS language buttons/labels are now brand blue rather than brand2 grey, matching the family primary-brand vocabulary; branded is unaffected (private preset overrides to brand.600/600). Also brings LinkButton (unused here).
The reusable method already lives in ../ui/docs/migration-playbook.md (this doc seeded it); the last transferable bits move there too (gotcha 27: clean Panda regen for sibling-preset changes; the expected behavioural deltas list) and the run/verify operational notes move to AGENTS.md. Remaining open items (landing the experiment-rai branches, the @microbit/ui catalog translation pipeline) are tracked elsewhere.
alpha.7 moves the Chakra-parity * { border-color; word-wrap } defaults
out of the preset's globalCss (which the production cascade-layer
flattening specificity-boosts above CSS it doesn't process) into
@microbit/ui/reset.css, imported here into the reset layer. Verified:
the built CSS carries the reset rule unboosted; 426 tests and build
green.
Brings the input/checkbox/switch size scales and the new Radio (unused here yet), plus fixes that apply directly: visible unselected checkbox/radio borders (--global-color-border), Chakra's disabled checkbox greys, and useClipboard's execCommand fallback for unpermissioned frames and insecure origins.
- Slider regains Chakra root geometry: the track recovers the 14px the root's px inset consumed, thumbs sit on the track ends at 0%/100%, and the certainty card's mark bubble returns to its Chakra-era spot. - NativeSelect is full-width by default (Chakra Select parity); the settings dialog selects keep their 28ch via the new wrapperCss, which is where Chakra put layout props. SortInput is chevronless and flex-constrained, so unaffected. - Text gains the config recipe (no default size here — unsized Text still inherits, as before).
- ButtonGroup attached children overlap borders by 1px (Chakra parity): SortInput's select/sort-order joint tightens by 1px. - Dividers are decorative by default (aria-hidden) — the NavigationDrawer separators stop announcing to screen readers — and gain a thickness variant (unused here).
Adapt to the toast API change: drop duration: 5000 now it's the default and use persistent rather than duration: null.
This makes the outline cancel/stop recording button one step darker as a minor alignment with Python.
Carries everything classroom's migration added to the library, plus three renderings that change here: Tooltip takes Chakra's colour, padding and radius (fba05fb), Icon regains Chakra's vertical-align rather than relying on Panda's preflight (2f7e18a), and Avatar shows its fallback until a photo loads and keeps it if the load fails (c80530d). All three are deliberate corrections in the library. The one breaking change is in the types: Modal's isOpen/onClose are now a union — both or neither, so a DialogTrigger can own the state — and `Omit` over that union collapses to just the keys both halves share, widening isOpen to `boolean | undefined` and then matching neither half. Every dialog here that forwards modal props hit it. They now say `Omit<ControlledModalProps, "children">`, which is what a forwarding shell means anyway; the library exports that half for exactly this. Eight files, all of them shells. Verified: typecheck clean, 427 unit tests pass, production build fine. The one lint error (ml4f-output/autogenerated.ts outside the tsconfig) is pre-existing and unrelated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR extracted the initial seed version of @microbit/ui (initially as src/shared-ui) and has since adopted the separately packaged version in https://github.com/microbit-foundation/ui.