Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 4 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/tech-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down
6 changes: 3 additions & 3 deletions panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion src/common/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const ConfirmDialog = ({
<ModalBody>{body}</ModalBody>
<ModalFooter>
{/* Least-destructive initial focus: RAC honours autoFocus within the
dialog's FocusScope, replacing Chakra's leastDestructiveRef. */}
dialog's FocusScope. */}
<Button autoFocus onPress={() => callback(false)}>
<FormattedMessage id="cancel-action" />
</Button>
Expand Down
2 changes: 0 additions & 2 deletions src/common/ExpandCollapseIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ interface ExpandCollapseIconProps extends Omit<IconProps, "as"> {
open: boolean;
}

// The Material arrow glyphs are the same paths as the Chakra chevron icons
// this replaced.
const ExpandCollapseIcon = ({ open, ...props }: ExpandCollapseIconProps) =>
open ? (
<Icon as={MdKeyboardArrowUp} {...props} />
Expand Down
3 changes: 1 addition & 2 deletions src/common/SplitView/HideSplitViewButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 16 additions & 26 deletions src/deployment/default/panda-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ 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",
theme: {
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: {
Expand All @@ -45,16 +42,15 @@ 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" },
},
// 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)" },
Expand Down Expand Up @@ -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 <Text> 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 <Text> 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",
Expand Down
2 changes: 0 additions & 2 deletions src/documentation/api/ApiNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,6 @@ const ApiNodeChildren = ({ docs, anchor }: ApiNodeChildrenProps) => {
(childKind) =>
groupedChildren?.get(childKind as any) && (
<Box mb="5" key={childKind}>
{/* The Chakra original had fontWeight="lg", an invalid
token that never resolved, so this renders normal. */}
<Text mb="2">{groupHeading(intl, kind, childKind)}</Text>
{groupedChildren?.get(childKind as any)?.map((c) => (
<ApiNode
Expand Down
4 changes: 2 additions & 2 deletions src/documentation/common/DocumentationTopLevelItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const DocumentationTopLevelItem = ({
size="sm"
css={{ color: "brand.200", fontSize: "2xl" }}
variant="ghost"
// The Chakra button relied on its click bubbling to the list
// item's onClick; react-aria's press handling suppresses that.
// react-aria's press handling doesn't produce a click that
// bubbles to the list item's onClick, so forward explicitly.
onPress={onForward}
>
<RiArrowRightLine />
Expand Down
8 changes: 4 additions & 4 deletions src/documentation/common/ImageWithFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { Image } from "@microbit/ui";
import { ComponentProps, ReactElement, useState } from "react";

interface ImageWithFallbackProps extends ComponentProps<typeof Image> {
/** 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,
Expand Down
8 changes: 4 additions & 4 deletions src/documentation/common/ShowMoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions src/documentation/common/V2Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions src/documentation/reference/Highlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/documentation/search/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ const SearchDialog = ({
</InputLeftElement>
<Input
aria-label={intl.formatMessage({ id: "search" })}
// Chakra's Modal focused the first focusable element on open;
// react-aria focuses the dialog itself unless told otherwise.
// react-aria focuses the dialog itself on open unless told
// otherwise.
autoFocus
ref={ref}
value={query}
Expand Down
2 changes: 0 additions & 2 deletions src/documentation/search/SearchResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ const SearchResultItem = ({
<ExtractText extract={extract.content} />
</Stack>
</Link>
{/* The Chakra original's color="gray.400" didn't reach the border
(dividers inherited the global gray.200); keep what shipped. */}
<Divider thickness="thick" />
</Stack>
);
Expand Down
10 changes: 5 additions & 5 deletions src/layers.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
3 changes: 1 addition & 2 deletions src/project/MoreMenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions src/project/NewFileNameQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const NewFileNameQuestion = ({
<>
<TextField
// Focus the field on open: react-aria focuses the dialog
// itself by default, unlike Chakra (dialog name is still
// announced via aria-labelledby).
// itself by default (dialog name is still announced via
// aria-labelledby).
autoFocus
isRequired
isInvalid={!validationResult.ok}
Expand Down
2 changes: 0 additions & 2 deletions src/project/ProjectAreaNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ const ProjectAreaNav = ({ css: cssProp }: ProjectAreaNavProps) => {
<NewButton mode="button" />
<OpenButton mode="button" />
<Box>
{/* warning = the red outline that Chakra's outline + red
colorScheme resolved to. */}
<ResetButton mode="button" variant="warning" css={{ mt: "5" }} />
</Box>
</VStack>
Expand Down
4 changes: 2 additions & 2 deletions src/project/ProjectNameQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const ProjectNameQuestion = ({
<TextField
ref={ref}
// Focus the field on open: react-aria focuses the dialog
// itself by default, unlike Chakra (dialog name is still
// announced via aria-labelledby).
// itself by default (dialog name is still announced via
// aria-labelledby).
autoFocus
isRequired
isInvalid={!validationResult.ok}
Expand Down
4 changes: 0 additions & 4 deletions src/project/SendButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ const SendButton = React.forwardRef(
},
[flashing, actions]
);
// The Chakra version prevented the tooltip's focus handler after a flash
// so it didn't obscure the "micro:bit flashed" text. RAC tooltips only
// show on keyboard focus-visible (not programmatic/pointer focus), so no
// equivalent hack is needed.
const menuButtonRef = useRef<HTMLButtonElement>(null);
const activeElementRef = useRef<HTMLElement | null>(null);
const handleSendToMicrobitShortcut = useCallback(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/serial/SerialBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/simulator/CompassModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ const CompassModule = ({
onSensorChange={onValueChange}
minimised={minimised}
/>
{/* Ref on a wrapper: the svgr component doesn't forward refs
(the old Chakra <Icon> wrapper did). */}
{/* Ref on a wrapper: the svgr component doesn't forward refs. */}
<styled.span ref={ref} display="inline-flex" flexShrink={0}>
<CompassHeadingIcon
className={css({
Expand Down
5 changes: 2 additions & 3 deletions src/simulator/DataLoggingModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ interface TruncatedDataLog extends DataLog {
truncated: boolean;
}

// Chakra's striped table (blackAlpha) approximated on a plain table.
const cellClass = css({
p: "1.5",
whiteSpace: "nowrap",
Expand Down Expand Up @@ -111,7 +110,7 @@ const DataLoggingModule = ({
</styled.thead>
<styled.tbody
className={css({
// Chakra Table variant="striped" colorScheme="blackAlpha".
// Striped rows.
"& tr:nth-of-type(odd) td": { background: "blackAlpha.100" },
})}
>
Expand All @@ -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"}
>
Expand Down
Loading
Loading