Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
48a42b8
Add a shared dense-preset for the × 0.88 density scale
microbit-matt-hillsdon Jul 31, 2026
a185a29
Button: primary/secondary colours via button.* semantic tokens
microbit-matt-hillsdon Jul 31, 2026
286258c
Add cy and it catalogs, backfilled from classroom's translations
microbit-matt-hillsdon Jul 31, 2026
ddcd55a
Playbook: hr box-model change at the kill-switch (#30); refine #9/#17
microbit-matt-hillsdon Jul 31, 2026
5a40244
Icon: accept svgr components; playbook gotcha #31
microbit-matt-hillsdon Aug 1, 2026
0e4a1e9
Playbook: Chakra CSS variables in app values (#32)
microbit-matt-hillsdon Aug 2, 2026
2ff9d1d
Re-export Container from the layout patterns
microbit-matt-hillsdon Aug 2, 2026
8be7bca
Playbook: correct #11 — native aspect-ratio is below the family floor
microbit-matt-hillsdon Aug 2, 2026
e541aca
Playbook: track the aspect-ratio audit as open work, not just a gotch…
microbit-matt-hillsdon Aug 2, 2026
a00d619
Button: generate responsive size variants in staticCss
microbit-matt-hillsdon Aug 2, 2026
f69cf02
Button recipe: note the two outline shapes with no variant
microbit-matt-hillsdon Aug 2, 2026
eacf4c4
Menu: checkbox option groups, and lift the popover above modals
microbit-matt-hillsdon Aug 2, 2026
c62b21a
Playbook: the silent collection truncation, and popovers vs stacking …
microbit-matt-hillsdon Aug 2, 2026
75c0af7
Modal: forward data attributes to the dialog box
microbit-matt-hillsdon Aug 2, 2026
39858f6
Modal: data attributes on the header, body, footer and close button too
microbit-matt-hillsdon Aug 2, 2026
13ab101
Spinner: forward data attributes
microbit-matt-hillsdon Aug 2, 2026
9e1094d
Playbook: dialog gotchas from classroom's area 4
microbit-matt-hillsdon Aug 2, 2026
9c42a47
Input/TextField: forward every recipe variant, not just size
microbit-matt-hillsdon Aug 2, 2026
d6d66d7
Playbook: the recipe-variant trap, and form/toast deltas
microbit-matt-hillsdon Aug 2, 2026
0f31ca4
Select and ComboBox: the pair that retires react-select
microbit-matt-hillsdon Aug 2, 2026
19a3924
ComboBox: startContent, and a staticCss entry for the select recipe
microbit-matt-hillsdon Aug 2, 2026
ec2d266
ComboBox: forward a ref to the input
microbit-matt-hillsdon Aug 2, 2026
9765951
ComboBox: anchor the card to the control, and size it to the trigger
microbit-matt-hillsdon Aug 2, 2026
22f5460
Select/ComboBox: maxHeight passthrough (react-select's maxMenuHeight)
microbit-matt-hillsdon Aug 2, 2026
f42b672
Select recipe: minWidth from the trigger, not width
microbit-matt-hillsdon Aug 2, 2026
486c98c
Select recipe: one 'value' slot for both controls
microbit-matt-hillsdon Aug 2, 2026
f36e811
Playbook: what react-select gives you that a ComboBox does not
microbit-matt-hillsdon Aug 2, 2026
f991382
ComboBox: size the card from the control, not from RAC's trigger var
microbit-matt-hillsdon Aug 2, 2026
0c8cf10
Playbook: #39 is fixed in the library, and note why the obvious fix f…
microbit-matt-hillsdon Aug 2, 2026
1cc9ef2
Add Avatar
microbit-matt-hillsdon Aug 2, 2026
502d06a
Add GridList
microbit-matt-hillsdon Aug 2, 2026
9658857
Add ListBox, and a Checkbox that can drop its box
microbit-matt-hillsdon Aug 2, 2026
c037fa7
Playbook: gotcha #40, and the area 7 components
microbit-matt-hillsdon Aug 2, 2026
df2aa9d
Add Skeleton, useDisclosure, and a Tooltip recipe
microbit-matt-hillsdon Aug 2, 2026
298315b
Playbook: gotcha #41, the styled re-export
microbit-matt-hillsdon Aug 2, 2026
3647a45
Tooltip: Chakra's colour, padding and radius
microbit-matt-hillsdon Aug 2, 2026
cc399a4
Icon: restore Chakra's vertical-align
microbit-matt-hillsdon Aug 3, 2026
f039b71
Playbook: gotcha #42, globalCss selectors don't merge across presets
microbit-matt-hillsdon Aug 3, 2026
d8ac831
Modal: an uncontrolled mode, via DialogTrigger
microbit-matt-hillsdon Aug 3, 2026
3abfae8
Review nits: naming, symmetry and a shared helper
microbit-matt-hillsdon Aug 3, 2026
a89efaa
Avatar: show the fallback until a photo loads, and keep it if it fails
microbit-matt-hillsdon Aug 3, 2026
5132964
Modal: enforce the isOpen/onClose pairing in the types
microbit-matt-hillsdon Aug 3, 2026
ce2433d
Missing stories from recent work
microbit-matt-hillsdon Aug 3, 2026
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: 2 additions & 0 deletions bin/i18n-packages.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ module.exports = [
crowdinDir: "new/packages/microbit-ui",
languages: [
"ca",
"cy",
"es-ES",
"fr",
"it",
"ja",
"ko",
"nl",
Expand Down
504 changes: 419 additions & 85 deletions docs/migration-playbook.md

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ an app must do:

1. **Panda preset stack** (`panda.config.ts`): `@pandacss/preset-base`, then
the **base preset** (`@microbit/ui/base-preset` — the complete micro:bit
design system), then optionally the app's own preset, then optionally a
**private brand preset** (Foundation colours, licensed fonts).
design system), then optionally `@microbit/ui/dense-preset` (the × 0.88
spacing / × 0.9 font-size density the information-dense apps use), then
optionally the app's own preset, then optionally a **private brand
preset** (Foundation colours, licensed fonts).

Later presets override earlier ones token-by-token — the base recipes and
semantic tokens reference the brand tokens, which is how a brand swap
Expand Down Expand Up @@ -162,9 +164,9 @@ keep them stable:
- Brand/app presets may change token _values_, never token _names_.

Semantic tokens (`languageText`, `statusBarBg`, `danger.*`, `toast*Bg`,
`controlCheckedBg`, `focusBorder`, …) are the extension points brand presets
override; they resolve through var indirection, so overrides apply wherever
the token is consumed.
`button.*`, `controlCheckedBg`, `focusBorder`, …) are the extension points
brand presets override; they resolve through var indirection, so overrides
apply wherever the token is consumed.

## Runtime token lookups

Expand Down
22 changes: 22 additions & 0 deletions packages/ui/lang/ui.cy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"ui.close-action": {
"defaultMessage": "Cau",
"description": "Close button text or label"
},
"ui.toast-status-error": {
"defaultMessage": "Gwall",
"description": "Announced by screen readers before an error notification"
},
"ui.toast-status-info": {
"defaultMessage": "Information",
"description": "Announced by screen readers before an informational notification"
},
"ui.toast-status-success": {
"defaultMessage": "Success",
"description": "Announced by screen readers before a success notification"
},
"ui.toast-status-warning": {
"defaultMessage": "Rhybudd",
"description": "Announced by screen readers before a warning notification"
}
}
22 changes: 22 additions & 0 deletions packages/ui/lang/ui.it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"ui.close-action": {
"defaultMessage": "Chiudi",
"description": "Close button text or label"
},
"ui.toast-status-error": {
"defaultMessage": "Errore",
"description": "Announced by screen readers before an error notification"
},
"ui.toast-status-info": {
"defaultMessage": "Information",
"description": "Announced by screen readers before an informational notification"
},
"ui.toast-status-success": {
"defaultMessage": "Success",
"description": "Announced by screen readers before a success notification"
},
"ui.toast-status-warning": {
"defaultMessage": "Attenzione",
"description": "Announced by screen readers before a warning notification"
}
}
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
".": "./src/index.ts",
"./base-preset": "./src/base-preset.ts",
"./base-tokens": "./src/base-tokens.ts",
"./dense-preset": "./src/dense-preset.ts",
"./messages": "./src/messages.ts",
"./postcss-legacy-safari": "./postcss-legacy-safari.cjs",
"./reset.css": "./reset.css",
Expand Down
168 changes: 168 additions & 0 deletions packages/ui/src/Avatar.recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* (c) 2026, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
import { defineSlotRecipe } from "@pandacss/dev";

/**
* Avatar slot recipe — Chakra's avatar: a circle showing an image, the
* initials of a name, or a generic person glyph, optionally with a badge
* pinned to one corner.
*
* The background and text colour come from `var(--avatar-bg)` and
* `var(--avatar-color)` rather than being flat values, because the component
* derives them from the name (see Avatar.tsx) and writes them as inline custom
* properties — exactly as Chakra did. Two reasons, both about letting a call
* site win with a plain `css={{ bg: …, color: … }}`: an inline *property*
* would beat any class, where an inline *variable* only feeds this
* declaration; and both must stay single-class selectors, since a state
* selector like `&[data-light-bg]` outranks the call site's utility class on
* specificity wherever cascade layers aren't in play — which is every app
* still coexisting with Chakra (playbook gotcha #40).
*
* Sizes are Chakra's, with its `calc(size / 2.5)` font size resolved per size
* so an app preset can restate either independently (classroom's avatars are
* a grade larger than Chakra's).
*
* Registered in the base preset (base-preset.ts), which also has the
* `staticCss` entry that keeps the runtime-prop variants generated.
*/
export const avatar = defineSlotRecipe({
className: "avatar",
slots: ["root", "label", "image", "badge"],
base: {
root: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
position: "relative",
verticalAlign: "top",
textAlign: "center",
textTransform: "uppercase",
fontWeight: "medium",
borderRadius: "full",
// Chakra's no-name defaults; the name-derived pair arrives inline.
background: "var(--avatar-bg, token(colors.gray.400))",
color: "var(--avatar-color, token(colors.white))",
borderColor: "white",
},
label: {
lineHeight: "1",
},
image: {
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "inherit",
},
badge: {
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "full",
// em-relative, so a badge keeps its proportions at every avatar size.
borderWidth: "0.2em",
borderStyle: "solid",
borderColor: "white",
},
},
variants: {
// Chakra's scale: the container size, and Chakra's `calc(size / 2.5)`
// font size kept as a calc over the same token so both track a preset
// that rescales `sizes` (the dense preset does, by 0.88).
//
// The font size lands on the root *and* the label, as Chakra's did
// (through one variable). They are separate declarations so an app can
// move one without the other: the root's is the em basis for a badge,
// the label's is how big the initials are, and the two are not always
// the same wish.
size: {
"2xs": {
root: {
width: "4",
height: "4",
fontSize: "calc(token(sizes.4) / 2.5)",
},
label: { fontSize: "calc(token(sizes.4) / 2.5)" },
},
xs: {
root: {
width: "6",
height: "6",
fontSize: "calc(token(sizes.6) / 2.5)",
},
label: { fontSize: "calc(token(sizes.6) / 2.5)" },
},
sm: {
root: {
width: "8",
height: "8",
fontSize: "calc(token(sizes.8) / 2.5)",
},
label: { fontSize: "calc(token(sizes.8) / 2.5)" },
},
md: {
root: {
width: "12",
height: "12",
fontSize: "calc(token(sizes.12) / 2.5)",
},
label: { fontSize: "calc(token(sizes.12) / 2.5)" },
},
lg: {
root: {
width: "16",
height: "16",
fontSize: "calc(token(sizes.16) / 2.5)",
},
label: { fontSize: "calc(token(sizes.16) / 2.5)" },
},
xl: {
root: {
width: "24",
height: "24",
fontSize: "calc(token(sizes.24) / 2.5)",
},
label: { fontSize: "calc(token(sizes.24) / 2.5)" },
},
"2xl": {
root: {
width: "32",
height: "32",
fontSize: "calc(token(sizes.32) / 2.5)",
},
label: { fontSize: "calc(token(sizes.32) / 2.5)" },
},
},
/** Which corner the badge sits in. Chakra's placements, same offsets. */
placement: {
"top-start": {
badge: {
top: "0",
insetStart: "0",
transform: "translate(-25%, -25%)",
},
},
"top-end": {
badge: { top: "0", insetEnd: "0", transform: "translate(25%, -25%)" },
},
"bottom-start": {
badge: {
bottom: "0",
insetStart: "0",
transform: "translate(-25%, 25%)",
},
},
"bottom-end": {
badge: { bottom: "0", insetEnd: "0", transform: "translate(25%, 25%)" },
},
},
},
defaultVariants: {
size: "md",
placement: "bottom-end",
},
});
Loading