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.