diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 8dc1b93ca..c7d6c7a60 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -35,7 +35,7 @@ module.exports = {
rules: {
// More trouble than it's worth
"react/no-unescaped-entities": "off",
- // False positives from library imports from Chakra UI
+ // False positives from library imports
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/no-misused-promises": [
"error",
diff --git a/AGENTS.md b/AGENTS.md
index 769d8b540..8ce9bd8b6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,11 +2,9 @@
## Styling/theming
-This app uses react-aria-components + Panda CSS via `@microbit/ui`
-(migrated from Chakra UI, July 2026). **Read the gotcha catalog in
-`../ui/docs/migration-playbook.md` before styling/theming/UI work** —
-Panda's static extraction rules and RAC behaviours catalogued there still
-apply to new code. The private theme package is the sibling repo
+This app uses react-aria-components + Panda CSS via `@microbit/ui`.
+Read `../ui/docs/hints.md` before styling/theming/UI work.
+The private theme package is the sibling repo
`../python-editor-v3-microbit` (consumed via a manual `node_modules`
symlink locally — re-create it after `npm install`).
@@ -15,8 +13,7 @@ symlink locally — re-create it after `npm install`).
want whenever a change spans the library and this app — symlink
`node_modules/@microbit/ui` to `../ui/packages/ui`, the same arrangement as the
theme package above. Re-create it after `npm install` (which restores the pinned
-version), and regenerate clean afterwards (`rm -rf styled-system && npm run
-panda`) — incremental codegen does not detect external preset changes.
+version), and regenerate clean afterwards (`rm -rf styled-system && npm run panda`) — incremental codegen does not detect external preset changes.
`resolve.dedupe` in `vite.config.ts` is what stops the symlinked package loading
its own copies of React and friends, so leave it in place either way.
diff --git a/docs/tech-overview.md b/docs/tech-overview.md
index 2e04077ce..054c2cd38 100644
--- a/docs/tech-overview.md
+++ b/docs/tech-overview.md
@@ -8,7 +8,7 @@ The document assumes some familiarity with the app as a user. [Try it out](http:
The editor is written in [TypeScript](https://www.typescriptlang.org/) using [React](https://reactjs.org/).
-We use the [Chakra UI component library](https://chakra-ui.com/docs/getting-started) which provides a base set of accessible components. We're currently using Chakra UI 2.x.
+The user interface is built with the Foundation's shared `@microbit/ui` component library, which combines [react-aria-components](https://react-spectrum.adobe.com/react-aria/) for accessible component behaviour with [Panda CSS](https://panda-css.com/) for styling.
The project is bundled using [Vite](https://vitejs.dev/). The test runner is [Vitest](https://vitest.dev/) and we're using [eslint](https://eslint.org/).
diff --git a/panda.config.ts b/panda.config.ts
index 4bc105b66..99f9cd4ae 100644
--- a/panda.config.ts
+++ b/panda.config.ts
@@ -12,7 +12,7 @@ import { appPreset } from "./src/deployment/default/panda-preset";
// alias swap in vite.config.ts. When the private package is installed it
// overrides brand tokens (colour ramps, code palette, semantic tokens);
// otherwise the OSS default preset stands alone. Panda merges them at codegen
-// time — the build-time equivalent of Chakra's runtime theme swap.
+// time.
//
// Panda loads this config as CommonJS, so `require` is the real (sync) require;
// Node 24 resolves the ESM private package through it.
@@ -29,12 +29,12 @@ export default defineConfig({
preflight: true,
jsxFramework: "react",
// Drop Panda's default theme preset; the preset stack below supplies the full
- // token system (ported from Chakra). preset-base still provides the utilities.
+ // token system. preset-base still provides the utilities.
eject: true,
// Later presets override earlier ones: @microbit/ui's base preset (the
// complete design system + recipes, OSS default brand values), the shared
// dense preset (this app's × 0.88 spacing / × 0.9 font-size density, shared
- // with classroom — see playbook gotcha #25), this app's own preset, then the
+ // with classroom), this app's own preset, then the
// optional private brand preset which overrides the OSS brand values.
// staticCss lives in the base preset.
presets: [
diff --git a/src/common/ConfirmDialog.tsx b/src/common/ConfirmDialog.tsx
index 807c3c292..df0aa6695 100644
--- a/src/common/ConfirmDialog.tsx
+++ b/src/common/ConfirmDialog.tsx
@@ -37,7 +37,7 @@ export const ConfirmDialog = ({
{body}
{/* Least-destructive initial focus: RAC honours autoFocus within the
- dialog's FocusScope, replacing Chakra's leastDestructiveRef. */}
+ dialog's FocusScope. */}
diff --git a/src/common/ExpandCollapseIcon.tsx b/src/common/ExpandCollapseIcon.tsx
index b0159f47b..f98f61984 100644
--- a/src/common/ExpandCollapseIcon.tsx
+++ b/src/common/ExpandCollapseIcon.tsx
@@ -10,8 +10,6 @@ interface ExpandCollapseIconProps extends Omit {
open: boolean;
}
-// The Material arrow glyphs are the same paths as the Chakra chevron icons
-// this replaced.
const ExpandCollapseIcon = ({ open, ...props }: ExpandCollapseIconProps) =>
open ? (
diff --git a/src/common/SplitView/HideSplitViewButton.tsx b/src/common/SplitView/HideSplitViewButton.tsx
index c2e3c3719..7b67c6590 100644
--- a/src/common/SplitView/HideSplitViewButton.tsx
+++ b/src/common/SplitView/HideSplitViewButton.tsx
@@ -78,8 +78,7 @@ const HideSplitViewButton = React.forwardRef(
width: mode === "icon" ? "20px" : "auto",
background: "#eaecf1",
// The flat background is a utility, which beats the ghost
- // variant's recipe-layer hover/active — restate them (Chakra's
- // ghost-over-gray values).
+ // variant's recipe-layer hover/active — restate them.
_hover: { background: "gray.100" },
_active: { background: "gray.200" },
// Likewise a flat boxShadow from a call site (e.g. Simulator's
diff --git a/src/deployment/default/panda-preset.ts b/src/deployment/default/panda-preset.ts
index c220f1aa9..21bfdbd9f 100644
--- a/src/deployment/default/panda-preset.ts
+++ b/src/deployment/default/panda-preset.ts
@@ -10,11 +10,8 @@ import { definePreset } from "@pandacss/dev";
* after the shared-ui base preset (@microbit/ui) and before the optional
* private brand preset (see panda.config.ts). Kept minimal — the base preset
* already supplies the token scales, recipes, `radii.button`, focus shadows,
- * Helvetica fonts and most semantic tokens.
- *
- * Values mirror the OSS Chakra theme (src/deployment/default) as converged
- * during the semantic-token pre-work; the private brand preset overrides the
- * brand-divergent token values.
+ * Helvetica fonts and most semantic tokens. The private brand preset overrides
+ * the brand-divergent token values.
*/
export const appPreset = definePreset({
name: "python-editor-v3",
@@ -22,8 +19,8 @@ export const appPreset = definePreset({
extend: {
tokens: {
// This app's stacking contexts, calibrated against third-party
- // layers (xterm.js ~10, the library's Chakra-derived overlay scale
- // from 1000). Token references are required in Panda styles: an
+ // layers (xterm.js ~10, the library's overlay scale from 1000).
+ // Token references are required in Panda styles: an
// imported constant in a zIndex prop is not statically extractable
// (silently unstyled).
zIndex: {
@@ -45,8 +42,7 @@ export const appPreset = definePreset({
},
colors: {
// This app's very light grays differ slightly from the family base
- // preset's (gray.75 is #f5f6f8 here vs #f5f5f5): mirror the OSS
- // Chakra theme's values (src/deployment/default/colors.ts).
+ // preset's (gray.75 is #f5f6f8 here vs #f5f5f5).
gray: {
10: { value: "#fcfcfc" },
75: { value: "#f5f6f8" },
@@ -54,7 +50,7 @@ export const appPreset = definePreset({
// Syntax-highlight / code-block palette. Consumed outside React as
// CSS vars (CodeMirror highlight styles, structure highlighting), so
// it's an app-preset token category; the private preset overrides the
- // values. OSS values are the Chakra defaults the old theme resolved.
+ // values.
code: {
blockBorder: { value: "#A0AEC0" }, // gray.400
blockBackground: { value: "rgba(185, 185, 185, 0.1)" },
@@ -94,35 +90,29 @@ export const appPreset = definePreset({
},
},
recipes: {
- // The Chakra theme defaulted Text to `md` — 0.9rem on this app's
- // shrunken scale, vs the 16px an unsized would inherit.
- // App-level because other apps rely on inheritance (Text.recipe.ts).
+ // Default Text to `md` — 0.9rem on this app's shrunken scale, vs the
+ // 16px an unsized would inherit. App-level because other apps
+ // rely on inheritance (Text.recipe.ts).
text: {
defaultVariants: { size: "md" },
},
- // python-editor's default button variant is `outline` (census: the
- // OSS theme applies withDefaultVariant("outline"); the family base
- // recipe defaults to `secondary`). Ported from the Chakra `outline`
- // variant, resolved at the brand colorScheme (the app applies
- // withDefaultColorScheme("brand")): brand text over a transparent,
- // brand-bordered button. Non-brand outline cases use `warning`.
+ // python-editor's default button variant is `outline` (the family
+ // base recipe defaults to `secondary`): brand text over a
+ // transparent, brand-bordered button. Non-brand outline cases use
+ // `warning`.
button: {
variants: {
variant: {
// Icon buttons on the app's dark chrome (sidebar header,
- // serial bar): white glyph, white pill on hover. The Chakra
- // variant was ghost-based with callers passing color="white";
- // the base colour is folded in here instead (both call sites
- // used white).
+ // serial bar): white glyph, white pill on hover.
sidebar: {
color: "white",
bg: "transparent",
_hover: { bg: "white", color: "gray.700" },
_active: { bg: "white", color: "gray.800" },
},
- // The editor zoom / undo-redo pill pairs: Chakra solid+gray
- // with darker hover/active. (The Chakra theme comment said
- // "ideally we'd drop this variant" — carried as-is.)
+ // The editor zoom / undo-redo pill pairs: gray fill with
+ // darker hover/active.
zoom: {
color: "gray.800",
bg: "gray.100",
diff --git a/src/documentation/api/ApiNode.tsx b/src/documentation/api/ApiNode.tsx
index 439ce6fea..88cb1b222 100644
--- a/src/documentation/api/ApiNode.tsx
+++ b/src/documentation/api/ApiNode.tsx
@@ -271,8 +271,6 @@ const ApiNodeChildren = ({ docs, anchor }: ApiNodeChildrenProps) => {
(childKind) =>
groupedChildren?.get(childKind as any) && (
- {/* The Chakra original had fontWeight="lg", an invalid
- token that never resolved, so this renders normal. */}
{groupHeading(intl, kind, childKind)}
{groupedChildren?.get(childKind as any)?.map((c) => (
diff --git a/src/documentation/common/ImageWithFallback.tsx b/src/documentation/common/ImageWithFallback.tsx
index cbcbc5dec..d3e5b9ba5 100644
--- a/src/documentation/common/ImageWithFallback.tsx
+++ b/src/documentation/common/ImageWithFallback.tsx
@@ -7,15 +7,15 @@ import { Image } from "@microbit/ui";
import { ComponentProps, ReactElement, useState } from "react";
interface ImageWithFallbackProps extends ComponentProps {
- /** Shown while loading and on error (Chakra Image's `fallback`). */
+ /** Shown while loading and on error. */
fallback?: ReactElement;
- /** Skip the fallback entirely (Chakra Image's `ignoreFallback`). */
+ /** Skip the fallback entirely. */
ignoreFallback?: boolean;
}
/**
- * The library Image plus Chakra Image's fallback behaviour, for the offline
- * placeholders used by the documentation areas.
+ * The library Image plus fallback behaviour, for the offline placeholders
+ * used by the documentation areas.
*/
const ImageWithFallback = ({
fallback,
diff --git a/src/documentation/common/ShowMoreButton.tsx b/src/documentation/common/ShowMoreButton.tsx
index 8c04e4697..fc86988b4 100644
--- a/src/documentation/common/ShowMoreButton.tsx
+++ b/src/documentation/common/ShowMoreButton.tsx
@@ -22,10 +22,10 @@ const ShowMoreButton = ({ isOpen, isBrief, onClick }: ShowMoreButtonProps) => {
onPress={onClick}
css={{
color: "brand.600",
- // The Chakra version was a Link as="button" — content-sized and
- // left-aligned, inheriting the surrounding text size. The button
- // recipe's base/size styles (semibold, centred, md height, md font)
- // must all be undone for the link form.
+ // Link-styled button: content-sized, left-aligned, inheriting the
+ // surrounding text size. The button recipe's base/size styles
+ // (semibold, centred, md height, md font) must all be undone for
+ // the link form.
fontWeight: "normal",
textAlign: "left",
justifyContent: "flex-start",
diff --git a/src/documentation/common/V2Tag.tsx b/src/documentation/common/V2Tag.tsx
index c5b9f6c36..9821b3a96 100644
--- a/src/documentation/common/V2Tag.tsx
+++ b/src/documentation/common/V2Tag.tsx
@@ -7,8 +7,6 @@ import { ComponentProps } from "react";
import { styled } from "styled-system/jsx";
// App-side tag (decision: no library Tag while this is the only consumer).
-// Chakra Tag md base (inline-flex, centred, fontSize md, lineHeight 1.2)
-// plus this file's overrides.
const TagSpan = styled("span", {
base: {
display: "inline-flex",
diff --git a/src/documentation/reference/Highlight.tsx b/src/documentation/reference/Highlight.tsx
index b0f51f38f..6a751d214 100644
--- a/src/documentation/reference/Highlight.tsx
+++ b/src/documentation/reference/Highlight.tsx
@@ -11,8 +11,7 @@ import { useLogging } from "../../logging/logging-hooks";
import { useScrollablePanelAncestor } from "../../common/ScrollablePanel";
/**
- * The subset of Chakra's useDisclosure return value we use, implemented
- * with useState by callers.
+ * Disclosure state for the highlight, implemented with useState by callers.
*/
export interface HighlightDisclosure {
isOpen: boolean;
diff --git a/src/documentation/search/SearchDialog.tsx b/src/documentation/search/SearchDialog.tsx
index c374a2f6c..7788ce4a4 100644
--- a/src/documentation/search/SearchDialog.tsx
+++ b/src/documentation/search/SearchDialog.tsx
@@ -55,8 +55,8 @@ const SearchDialog = ({
- {/* The Chakra original's color="gray.400" didn't reach the border
- (dividers inherited the global gray.200); keep what shipped. */}
);
diff --git a/src/layers.css b/src/layers.css
index a94afb542..bc94e39fb 100644
--- a/src/layers.css
+++ b/src/layers.css
@@ -7,13 +7,13 @@
* cascade-layer order (Panda's PostCSS plugin injects the generated CSS into
* these layers) and imports third-party stylesheets into the `vendor` layer,
* which beats Panda's preflight reset but loses to app styling (see the
- * @microbit/ui README and playbook gotcha #1). Plain app .css files stay
- * unlayered and beat everything, as they did under Chakra.
+ * @microbit/ui README). Plain app .css files stay unlayered and beat
+ * everything.
*/
@layer reset, vendor, base, tokens, recipes, utilities;
-/* Required: Chakra-parity border-color/word-wrap defaults, kept in the
- * bottom layer so the production cascade-layer flattening can't boost them
- * above CodeMirror's runtime-injected styles (playbook gotcha #28). */
+/* Required: border-color/word-wrap defaults, kept in the bottom layer so the
+ * production cascade-layer flattening can't boost them above CodeMirror's
+ * runtime-injected styles. */
@import "@microbit/ui/reset.css" layer(reset);
@import "xterm/css/xterm.css" layer(vendor);
diff --git a/src/project/MoreMenuButton.tsx b/src/project/MoreMenuButton.tsx
index 68aa0e38f..627b21be5 100644
--- a/src/project/MoreMenuButton.tsx
+++ b/src/project/MoreMenuButton.tsx
@@ -31,8 +31,7 @@ const MoreMenuButton = React.forwardRef(
css={{
borderLeft: "1px solid",
borderRadius: "button",
- // Nudge the glyph towards the attached (square) edge, as the old
- // inline style did with the Chakra radii var.
+ // Nudge the glyph towards the attached (square) edge.
"& svg": { marginLeft: "calc(-0.15 * token(radii.button))" },
}}
{...props}
diff --git a/src/project/NewFileNameQuestion.tsx b/src/project/NewFileNameQuestion.tsx
index 0966c2ccf..5a4524b7e 100644
--- a/src/project/NewFileNameQuestion.tsx
+++ b/src/project/NewFileNameQuestion.tsx
@@ -21,8 +21,8 @@ const NewFileNameQuestion = ({
<>
{
- {/* warning = the red outline that Chakra's outline + red
- colorScheme resolved to. */}
diff --git a/src/project/ProjectNameQuestion.tsx b/src/project/ProjectNameQuestion.tsx
index bfa95a3c2..8ce280777 100644
--- a/src/project/ProjectNameQuestion.tsx
+++ b/src/project/ProjectNameQuestion.tsx
@@ -27,8 +27,8 @@ const ProjectNameQuestion = ({
(null);
const activeElementRef = useRef(null);
const handleSendToMicrobitShortcut = useCallback(() => {
diff --git a/src/serial/SerialBar.tsx b/src/serial/SerialBar.tsx
index ced186265..1e5d49088 100644
--- a/src/serial/SerialBar.tsx
+++ b/src/serial/SerialBar.tsx
@@ -72,8 +72,8 @@ const SerialBar = ({
p="1"
// Runtime token lookup: a three-way conditional value isn't reliably
// statically extractable.
- // whiteAlpha over the inherited terminal background lands close to
- // the Chakra-era branded gray.700 (#4c4c4c) without pinning a colour.
+ // whiteAlpha over the inherited terminal background gives the
+ // out-of-sync gray (≈#4c4c4c) without pinning a colour.
style={{
backgroundColor:
traceback && syncStatus === SyncStatus.IN_SYNC
diff --git a/src/simulator/CompassModule.tsx b/src/simulator/CompassModule.tsx
index 06b79e75b..bff006cd5 100644
--- a/src/simulator/CompassModule.tsx
+++ b/src/simulator/CompassModule.tsx
@@ -67,8 +67,7 @@ const CompassModule = ({
onSensorChange={onValueChange}
minimised={minimised}
/>
- {/* Ref on a wrapper: the svgr component doesn't forward refs
- (the old Chakra wrapper did). */}
+ {/* Ref on a wrapper: the svgr component doesn't forward refs. */}
@@ -136,7 +135,7 @@ const DataLoggingModule = ({
className={cellClass}
fontSize={row.isHeading ? "sm" : undefined}
fontWeight={row.isHeading ? "semibold" : undefined}
- // Chakra's isNumeric.
+ // Right-align numeric data cells.
textAlign={row.isHeading ? "start" : "end"}
fontFamily={row.isHeading ? undefined : "code"}
>
diff --git a/src/simulator/RangeSensor.tsx b/src/simulator/RangeSensor.tsx
index f44675080..23786d0e4 100644
--- a/src/simulator/RangeSensor.tsx
+++ b/src/simulator/RangeSensor.tsx
@@ -83,7 +83,6 @@ const RangeSensor = ({
maxValue={max}
onChange={handleChange}
trackCss={{ height: "2" }}
- // Chakra colorScheme="blackAlpha" filled track.
filledTrackCss={{ bg: "blackAlpha.500" }}
thumbTooltip={valueText}
isThumbTooltipOpen={minimised ? showTooltip : false}
@@ -131,8 +130,8 @@ const RangeSensor = ({
};
/**
- * An always-visible equivalent of Chakra's SliderMark: absolutely
- * positioned at a percentage along the track, below it by default.
+ * An always-visible slider mark: absolutely positioned at a percentage
+ * along the track, below it by default.
*/
const SensorMark = ({
percent,
diff --git a/src/simulator/SensorInput.tsx b/src/simulator/SensorInput.tsx
index 7806aaf29..cf2b27fcd 100644
--- a/src/simulator/SensorInput.tsx
+++ b/src/simulator/SensorInput.tsx
@@ -127,7 +127,7 @@ const SensorInput = ({
)}
css={{
transition: "none",
- // Chakra's isActive + _active override: held look while pressed.
+ // Held look while pressed.
background: pressed ? "brand.100" : undefined,
}}
isDisabled={disabled}
diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx
index fbdb6c9b0..36677d462 100644
--- a/src/simulator/Simulator.tsx
+++ b/src/simulator/Simulator.tsx
@@ -123,7 +123,6 @@ const Simulator = ({
style={{ minWidth: minWidth }}
>
`${x.name} ${x.value}`)
.join("\n");
-// Minimal replacement for Chakra's useClipboard (a library-gap hook).
+// Minimal clipboard hook (the component library has no equivalent).
const useClipboard = (text: string) => {
const [hasCopied, setHasCopied] = useState(false);
const onCopy = useCallback(() => {
diff --git a/src/workbench/SideBar.tsx b/src/workbench/SideBar.tsx
index e47ebbf8e..d2a73bd41 100644
--- a/src/workbench/SideBar.tsx
+++ b/src/workbench/SideBar.tsx
@@ -254,9 +254,9 @@ const SideBar = ({
Chakra rendered), and
- // react-aria's data-focus-visible does so in Firefox.
- // Keyboard focus sets both in all modern browsers.
+ // in Chromium, and react-aria's data-focus-visible does so in
+ // Firefox. Keyboard focus sets both in all modern browsers.
".sidebar-tab[data-focus-visible]:focus-visible &": {
borderBottom: "3px solid",
// To match the active/inactive colour.