@microbit/ui 0.1.0-alpha.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +8 -0
- package/README.md +171 -20
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +44 -4
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +43 -3
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +44 -4
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +12 -10
- package/reset.css +12 -0
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +67 -14
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +49 -14
- package/src/Checkbox.tsx +111 -32
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +1 -1
- package/src/Collapse.tsx +13 -14
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +40 -7
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +18 -4
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +8 -18
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +26 -12
- package/src/Kbd.tsx +1 -1
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +14 -13
- package/src/List.tsx +5 -7
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +22 -19
- package/src/Menu.tsx +54 -28
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +123 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +41 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +56 -15
- package/src/NumberField.tsx +74 -22
- package/src/PopoverArrow.tsx +1 -2
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +108 -0
- package/src/Radio.tsx +62 -0
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +20 -12
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +57 -11
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +39 -40
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +147 -48
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +329 -84
- package/src/base-tokens.ts +0 -3
- package/src/button-icon.ts +4 -5
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +43 -7
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +3 -4
- package/src/hooks/usePrevious.ts +1 -4
- package/src/index.ts +20 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/base-preset.ts
CHANGED
|
@@ -23,37 +23,103 @@ import {
|
|
|
23
23
|
} from "./base-tokens";
|
|
24
24
|
// Config recipes are colocated with the shared-ui components they style; this
|
|
25
25
|
// preset registers them so Panda merges them at codegen time.
|
|
26
|
+
import { avatar } from "./Avatar.recipe";
|
|
27
|
+
import { breadcrumb } from "./Breadcrumb.recipe";
|
|
26
28
|
import { button } from "./Button.recipe";
|
|
27
29
|
import { card } from "./Card.recipe";
|
|
28
30
|
import { checkbox } from "./Checkbox.recipe";
|
|
31
|
+
import { radio } from "./Radio.recipe";
|
|
29
32
|
import { drawer } from "./Drawer.recipe";
|
|
33
|
+
import { gridList } from "./GridList.recipe";
|
|
30
34
|
import { heading } from "./Heading.recipe";
|
|
31
35
|
import { input } from "./Input.recipe";
|
|
36
|
+
import { listBox } from "./ListBox.recipe";
|
|
32
37
|
import { numberField } from "./NumberField.recipe";
|
|
33
38
|
import { menu } from "./Menu.recipe";
|
|
39
|
+
import { select } from "./Select.recipe";
|
|
34
40
|
import { slider } from "./Slider.recipe";
|
|
35
41
|
import { switchRecipe } from "./Switch.recipe";
|
|
36
42
|
import { dialog } from "./Modal.recipe";
|
|
37
|
-
import {
|
|
43
|
+
import { text } from "./Text.recipe";
|
|
44
|
+
import { tooltip } from "./Tooltip.recipe";
|
|
45
|
+
import { field } from "./Field.recipe";
|
|
38
46
|
import { toast } from "./Toast.recipe";
|
|
39
47
|
|
|
48
|
+
// The family gray ramp: pure neutrals as the library default. The two
|
|
49
|
+
// halves have different jobs and different override rules:
|
|
50
|
+
//
|
|
51
|
+
// - 10–300 are surface stops (panels, page backdrops, hairlines, subtle
|
|
52
|
+
// fills). Presets may freely override these values — pinning an app's
|
|
53
|
+
// existing surfaces or applying a brand tint — because a few bits of
|
|
54
|
+
// lightness here reads across a whole viewport.
|
|
55
|
+
// - 350 is the decorative/state fill stop (~2.1:1): avatar discs, skeleton
|
|
56
|
+
// pulse, pressed fills. Never text or boundaries.
|
|
57
|
+
// - 400–900 are ink stops (outlines, placeholders, text) with a contrast
|
|
58
|
+
// contract on white: 400 ≥ 3:1, the floor for boundaries that identify
|
|
59
|
+
// a control (checkbox-family boxes; fields rest lighter — see the input
|
|
60
|
+
// recipe); 500 ≥ 4.5:1, text-safe secondary. Presets may re-tint these
|
|
61
|
+
// only luminance-matched — the contrast figures are the contract, hue
|
|
62
|
+
// is free.
|
|
63
|
+
//
|
|
64
|
+
// Override values, never names: raw var(--colors-gray-*) references and
|
|
65
|
+
// paired private presets depend on the names, so a rename is a breaking
|
|
66
|
+
// change to both and needs every app and paired preset moved in lockstep
|
|
67
|
+
// (as was done when the misnamed darker-than-50 stop `25` became
|
|
68
|
+
// `75`). And never override partially in a way that lets a stop fall
|
|
69
|
+
// through to a different grey system.
|
|
70
|
+
const gray = {
|
|
71
|
+
10: { value: "#fcfcfc" },
|
|
72
|
+
50: { value: "#f9f9f9" },
|
|
73
|
+
75: { value: "#f5f5f5" },
|
|
74
|
+
100: { value: "#f1f1f1" },
|
|
75
|
+
200: { value: "#e7e7e7" },
|
|
76
|
+
300: { value: "#d4d4d4" },
|
|
77
|
+
350: { value: "#b4b4b4" }, // ~2.1:1 — decorative fills only
|
|
78
|
+
400: { value: "#949494" }, // 3.05:1 — accessible outline stop
|
|
79
|
+
500: { value: "#767676" }, // 4.54:1 — text-safe secondary
|
|
80
|
+
600: { value: "#575757" },
|
|
81
|
+
700: { value: "#404040" },
|
|
82
|
+
800: { value: "#262626" },
|
|
83
|
+
900: { value: "#1a1a1a" },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// The family red, on the gray ramp's ladder — it is the conventional colour
|
|
87
|
+
// for errors and recording, not a brand colour, so it can be graded rather
|
|
88
|
+
// than negotiated. 400 was already exactly gray's 3:1 and stays verbatim,
|
|
89
|
+
// anchoring the hue and saturation the darker stops hold while their
|
|
90
|
+
// lightness solves for gray's ratio. The washes have no contract and keep
|
|
91
|
+
// their values.
|
|
92
|
+
const red = {
|
|
93
|
+
50: { value: "#FFF5F5" },
|
|
94
|
+
100: { value: "#FED7D7" },
|
|
95
|
+
200: { value: "#FEB2B2" },
|
|
96
|
+
300: { value: "#FC8181" },
|
|
97
|
+
400: { value: "#F56565" }, // 3.03:1
|
|
98
|
+
500: { value: "#e22b2b" }, // 4.55:1 — the white-text fill stop
|
|
99
|
+
600: { value: "#ac1818" },
|
|
100
|
+
700: { value: "#811212" },
|
|
101
|
+
800: { value: "#4f0b0b" },
|
|
102
|
+
900: { value: "#380808" },
|
|
103
|
+
};
|
|
104
|
+
|
|
40
105
|
/**
|
|
41
106
|
* The base preset: the complete, working micro:bit design system. The base
|
|
42
107
|
* token scales (base-tokens.ts), the micro:bit house style
|
|
43
|
-
* (pill `radii.button`, `
|
|
44
|
-
*
|
|
108
|
+
* (pill `radii.button`, the `focusRing` utility/token pair, Helvetica
|
|
109
|
+
* fonts, the
|
|
110
|
+
* `toolbar` button variant in Button.recipe.ts, the
|
|
45
111
|
* `languageText`/`toast*Bg`/`statusBarBg` semantic tokens), the shared-ui
|
|
46
|
-
* component recipes, the react-aria condition widening, the
|
|
47
|
-
*
|
|
112
|
+
* component recipes, the react-aria condition widening, the `globalCss`
|
|
113
|
+
* defaults, and the `staticCss` that keeps runtime-prop recipe
|
|
48
114
|
* variants generated. Used alone it renders in the OSS default look.
|
|
49
115
|
*
|
|
50
116
|
* ── Brand contract ──────────────────────────────────────────────────────
|
|
51
117
|
* A private brand preset (a sibling repo, e.g. CreateAI) is merged AFTER this
|
|
52
118
|
* one to restyle everything by overriding just these token *values* (never
|
|
53
119
|
* their names — see the CSS-var contract in the README):
|
|
54
|
-
* - colours: the `brand` and `brand2` ramps (OSS defaults:
|
|
55
|
-
*
|
|
56
|
-
* (teal/purple/pink/…) already exist in the
|
|
120
|
+
* - colours: the `brand` and `brand2` ramps (OSS defaults: the blue
|
|
121
|
+
* ramp / a legacy slate gray). Other ramps a brand tweaks
|
|
122
|
+
* (teal/purple/pink/…) already exist in the base scales below.
|
|
57
123
|
* - font: `display` (OSS default: Helvetica; e.g. GT Walsheim privately).
|
|
58
124
|
* The recipes and semantic tokens here reference those, so a brand swap needs
|
|
59
125
|
* no recipe changes. With no private preset, these OSS defaults stand.
|
|
@@ -63,25 +129,44 @@ export const basePreset = definePreset({
|
|
|
63
129
|
theme: {
|
|
64
130
|
breakpoints,
|
|
65
131
|
keyframes: {
|
|
66
|
-
// Spinner's revolution
|
|
132
|
+
// Spinner's revolution.
|
|
67
133
|
spin: {
|
|
68
134
|
"0%": { transform: "rotate(0deg)" },
|
|
69
135
|
"100%": { transform: "rotate(360deg)" },
|
|
70
136
|
},
|
|
137
|
+
// Skeleton's pulse, over the pair of custom properties the component
|
|
138
|
+
// sets, so a retinted skeleton animates between its own colours.
|
|
139
|
+
skeletonFade: {
|
|
140
|
+
from: {
|
|
141
|
+
borderColor: "var(--skeleton-start-color)",
|
|
142
|
+
background: "var(--skeleton-start-color)",
|
|
143
|
+
},
|
|
144
|
+
to: {
|
|
145
|
+
borderColor: "var(--skeleton-end-color)",
|
|
146
|
+
background: "var(--skeleton-end-color)",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
// Toast enter/exit, played on the view-transition snapshots (see the
|
|
150
|
+
// ::view-transition rules in globalCss): fade + short slide in,
|
|
151
|
+
// quicker fade + shrink out.
|
|
152
|
+
toastSlideIn: {
|
|
153
|
+
from: { opacity: 0, transform: "translateY(-24px)" },
|
|
154
|
+
},
|
|
155
|
+
toastSlideOut: {
|
|
156
|
+
to: { opacity: 0, transform: "scale(0.85)" },
|
|
157
|
+
},
|
|
71
158
|
},
|
|
72
159
|
tokens: {
|
|
73
160
|
colors: {
|
|
74
161
|
...colors,
|
|
75
|
-
gray
|
|
76
|
-
|
|
77
|
-
// Very light grays the family's designs use below Chakra's 50.
|
|
78
|
-
10: { value: "#fcfcfc" },
|
|
79
|
-
25: { value: "#f5f5f5" },
|
|
80
|
-
},
|
|
162
|
+
gray,
|
|
163
|
+
red,
|
|
81
164
|
// OSS default brand ramps (see the brand contract above). `brand`
|
|
82
|
-
// aliases
|
|
83
|
-
//
|
|
84
|
-
//
|
|
165
|
+
// aliases the blue ramp; `brand2` is a frozen legacy alias of the
|
|
166
|
+
// slate gray in base-tokens, deliberately decoupled from the neutral
|
|
167
|
+
// `gray` above so ml-trainer's OSS look and `statusBarBg`'s default
|
|
168
|
+
// don't move.
|
|
169
|
+
// Removing the slot is a follow-up needing an ml-trainer lockstep.
|
|
85
170
|
brand: colors.blue,
|
|
86
171
|
brand2: colors.gray,
|
|
87
172
|
},
|
|
@@ -103,16 +188,11 @@ export const basePreset = definePreset({
|
|
|
103
188
|
},
|
|
104
189
|
shadows: {
|
|
105
190
|
...shadows,
|
|
106
|
-
// Chakra's outline shadow widened to 4px, plus dark/light-surface
|
|
107
|
-
// companions. Consumed via the `focusShadow` utility.
|
|
108
|
-
outline: { value: "0 0 0 4px rgba(66, 153, 225, 0.6)" },
|
|
109
|
-
outlineDark: { value: "0 0 0 4px rgba(0, 0, 0, 0.5)" },
|
|
110
|
-
outlineLight: { value: "0 0 0 4px rgba(255, 255, 255, 0.8)" },
|
|
111
191
|
},
|
|
112
192
|
fonts: {
|
|
113
193
|
// Helvetica heading/body (4/4 apps); a brand preset leaves these and
|
|
114
194
|
// overrides only `display` (the marketing font — see the brand
|
|
115
|
-
// contract above).
|
|
195
|
+
// contract above).
|
|
116
196
|
heading: { value: "Helvetica, Arial, sans-serif" },
|
|
117
197
|
body: { value: "Helvetica, Arial, sans-serif" },
|
|
118
198
|
mono: {
|
|
@@ -126,42 +206,86 @@ export const basePreset = definePreset({
|
|
|
126
206
|
},
|
|
127
207
|
semanticTokens: {
|
|
128
208
|
colors: {
|
|
129
|
-
// Checked
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
209
|
+
// Checked states of form controls: Checkbox/Switch/Radio checked
|
|
210
|
+
// backgrounds. Semantic so a brand can diverge them from its ramp.
|
|
211
|
+
controlCheckedBg: { value: "{colors.brand.500}" },
|
|
212
|
+
controlCheckedHoverBg: { value: "{colors.brand.600}" },
|
|
213
|
+
// Focused form-control border, any modality: the dark brand stop
|
|
214
|
+
// (all-ink read flat next to the ink ring). Flips white under the
|
|
215
|
+
// dark-surface tag, like `focusRing`.
|
|
216
|
+
// Both flips are condition objects, and a merge replaces a token
|
|
217
|
+
// value wholesale: an override must keep the `{ base, _onDark }`
|
|
218
|
+
// shape or silently lose the flip.
|
|
219
|
+
focusBorder: {
|
|
220
|
+
value: { base: "{colors.brand.600}", _onDark: "{colors.white}" },
|
|
221
|
+
},
|
|
222
|
+
// The focus ring colour: ink, or white inside `data-surface="dark"`
|
|
223
|
+
// (the `onDark` condition). The var inherits — tag the bar, cover
|
|
224
|
+
// its controls; portalled overlays escape with the DOM. Dark
|
|
225
|
+
// surfaces MUST tag (ink is near-invisible there). Opaque
|
|
226
|
+
// deliberately: translucent rings washed out (classroom #780).
|
|
227
|
+
// Both tag states: the Button "Variants" story.
|
|
228
|
+
focusRing: {
|
|
229
|
+
value: { base: "{colors.gray.900}", _onDark: "{colors.white}" },
|
|
230
|
+
},
|
|
231
|
+
// Error/destructive ramp: field error states, the error toast, and
|
|
232
|
+
// the `danger` button tone. Aliased whole, not just at the stops in
|
|
233
|
+
// use, so a tone has nowhere to fall through (Button.recipe.ts).
|
|
142
234
|
danger: {
|
|
143
235
|
50: { value: "{colors.red.50}" },
|
|
144
236
|
100: { value: "{colors.red.100}" },
|
|
237
|
+
200: { value: "{colors.red.200}" },
|
|
238
|
+
300: { value: "{colors.red.300}" },
|
|
239
|
+
400: { value: "{colors.red.400}" },
|
|
145
240
|
500: { value: "{colors.red.500}" },
|
|
146
241
|
600: { value: "{colors.red.600}" },
|
|
147
242
|
700: { value: "{colors.red.700}" },
|
|
243
|
+
800: { value: "{colors.red.800}" },
|
|
244
|
+
900: { value: "{colors.red.900}" },
|
|
148
245
|
},
|
|
149
|
-
// The
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// python-editor to brand.500/600 —
|
|
153
|
-
// the
|
|
154
|
-
//
|
|
155
|
-
// apps' final values sit on their primary brand, so the default
|
|
156
|
-
// follows; OSS language buttons are brand blue.)
|
|
246
|
+
// The language-dialog cards' text colour (@microbit/ui-patterns'
|
|
247
|
+
// LanguageDialog) follows the primary interactive brand: every
|
|
248
|
+
// consumer resolves it to its `brand` ramp (CreateAI privately to
|
|
249
|
+
// brand.600 with no hover change, python-editor to brand.500/600 —
|
|
250
|
+
// the default; data-microbit-org to black). Semantic tokens so the
|
|
251
|
+
// pattern stays shared and a brand preset overrides only values.
|
|
157
252
|
languageText: { value: "{colors.brand.500}" },
|
|
158
253
|
languageTextHover: { value: "{colors.brand.600}" },
|
|
159
|
-
//
|
|
160
|
-
//
|
|
254
|
+
// The `label`/`subtitle` heading variants' colour (page-title chrome).
|
|
255
|
+
// classroom and data-microbit-org carried byte-identical variants with
|
|
256
|
+
// a hardcoded #cd0365 — the brand deep pink, which is data's
|
|
257
|
+
// `pink.500`; both override this to it. The OSS default follows the
|
|
258
|
+
// languageText precedent: the primary interactive brand.
|
|
259
|
+
headingAccent: { value: "{colors.brand.500}" },
|
|
260
|
+
// The `primary`/`secondary` button variants' colours. Two brand
|
|
261
|
+
// idioms exist in the family: brand-coloured buttons (ml-trainer,
|
|
262
|
+
// python-editor — the defaults below) and a black-on-white system
|
|
263
|
+
// (classroom, data-microbit-org: black solid, black outline, no
|
|
264
|
+
// border colour change on hover but a blackAlpha wash instead).
|
|
265
|
+
// Tokens rather than per-app recipe overrides so both idioms share
|
|
266
|
+
// one recipe — a `variant` fork would be duplicated by every app on
|
|
267
|
+
// the far side of it. `primary`'s text colour stays a literal
|
|
268
|
+
// `white`: every app in the family puts white on a dark solid.
|
|
269
|
+
// `ghost` needs no tokens (black + blackAlpha in all four apps).
|
|
270
|
+
button: {
|
|
271
|
+
primaryBg: { value: "{colors.brand.500}" },
|
|
272
|
+
primaryHoverBg: { value: "{colors.brand.600}" },
|
|
273
|
+
primaryActiveBg: { value: "{colors.brand.700}" },
|
|
274
|
+
secondaryText: { value: "{colors.brand.700}" },
|
|
275
|
+
secondaryBorder: { value: "{colors.brand.500}" },
|
|
276
|
+
secondaryHoverBorder: { value: "{colors.brand.600}" },
|
|
277
|
+
secondaryHoverBg: { value: "transparent" },
|
|
278
|
+
secondaryActiveBorder: { value: "{colors.brand.700}" },
|
|
279
|
+
secondaryActiveBg: { value: "{colors.brand.50}" },
|
|
280
|
+
},
|
|
281
|
+
// Toast status colours (teal for every status except error), shared
|
|
282
|
+
// across the app family.
|
|
161
283
|
toastInfoBg: { value: "{colors.teal.800}" },
|
|
162
284
|
toastSuccessBg: { value: "{colors.teal.800}" },
|
|
163
285
|
toastWarningBg: { value: "{colors.teal.800}" },
|
|
164
|
-
|
|
286
|
+
// 500, the text-safe stop, rather than following the teal toasts'
|
|
287
|
+
// 800: white on it is 4.55:1 and an error toast should read as red.
|
|
288
|
+
toastErrorBg: { value: "{colors.danger.500}" },
|
|
165
289
|
// The native app's status-bar area colour, shared by the ActionBar
|
|
166
290
|
// and the full-size dialog's safe-area gradient.
|
|
167
291
|
statusBarBg: { value: "{colors.brand2.500}" },
|
|
@@ -171,34 +295,45 @@ export const basePreset = definePreset({
|
|
|
171
295
|
button,
|
|
172
296
|
heading,
|
|
173
297
|
input,
|
|
298
|
+
text,
|
|
299
|
+
tooltip,
|
|
174
300
|
},
|
|
175
301
|
slotRecipes: {
|
|
302
|
+
avatar,
|
|
303
|
+
breadcrumb,
|
|
176
304
|
card,
|
|
177
305
|
checkbox,
|
|
178
306
|
dialog,
|
|
179
307
|
drawer,
|
|
180
308
|
field,
|
|
309
|
+
gridList,
|
|
310
|
+
listBox,
|
|
181
311
|
menu,
|
|
182
312
|
numberField,
|
|
313
|
+
radio,
|
|
314
|
+
select,
|
|
183
315
|
slider,
|
|
184
316
|
switchRecipe,
|
|
185
317
|
toast,
|
|
186
318
|
},
|
|
187
319
|
},
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// shifts glyphs page-wide), word-wrap and touch-action. Token references
|
|
193
|
-
// resolve against the merged preset stack, so the values track any brand
|
|
194
|
-
// overrides exactly as they did under Chakra's runtime theme.
|
|
320
|
+
// Global defaults Panda's preflight doesn't cover: body text/background,
|
|
321
|
+
// placeholder colour, kerning/text-rendering (their absence shifts
|
|
322
|
+
// glyphs page-wide) and touch-action. Token references resolve against
|
|
323
|
+
// the merged preset stack, so the values track any brand overrides.
|
|
195
324
|
globalCss: {
|
|
196
325
|
html: {
|
|
197
326
|
textRendering: "optimizeLegibility",
|
|
198
327
|
touchAction: "manipulation",
|
|
328
|
+
// Alias preset-base's ring plumbing to our colour, so a stray use
|
|
329
|
+
// of its focusVisibleRing/focusRing* utilities renders in our
|
|
330
|
+
// ink rather than #005FCC. Still don't use them: un-gated focus
|
|
331
|
+
// selector, and this alias resolves on <html> (no tag awareness).
|
|
332
|
+
"--global-color-focus-ring": "var(--colors-focus-ring)",
|
|
199
333
|
},
|
|
200
334
|
body: {
|
|
201
|
-
position:
|
|
335
|
+
// No `position: relative` (Chakra had it): it breaks react-aria's
|
|
336
|
+
// overlay positioning — see Tooltip's "In a scrolling page" story.
|
|
202
337
|
minHeight: "100%",
|
|
203
338
|
fontFeatureSettings: '"kern"',
|
|
204
339
|
fontFamily: "body",
|
|
@@ -211,23 +346,70 @@ export const basePreset = definePreset({
|
|
|
211
346
|
"*::placeholder": {
|
|
212
347
|
color: "gray.500",
|
|
213
348
|
},
|
|
214
|
-
// The `* { border-color; word-wrap }`
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// Panda's preflight
|
|
221
|
-
//
|
|
222
|
-
//
|
|
349
|
+
// The `* { border-color; word-wrap }` defaults live in ../reset.css,
|
|
350
|
+
// imported into the `reset` layer by consumers' layers.css — NOT here:
|
|
351
|
+
// globalCss emits into the `base` layer, which the legacy-Safari
|
|
352
|
+
// cascade-layer flattening specificity-boosts above runtime-injected
|
|
353
|
+
// CSS (CodeMirror themes) and other app CSS files. Resets must stay in
|
|
354
|
+
// the bottom layer.
|
|
355
|
+
// Panda's preflight doesn't set the pointer cursor on buttons.
|
|
356
|
+
// Recipes' disabled states (cursor: not-allowed) override this from
|
|
357
|
+
// the higher recipes layer.
|
|
223
358
|
"button, [role='button']": {
|
|
224
359
|
cursor: "pointer",
|
|
225
360
|
},
|
|
226
|
-
// Panda's preflight balance-wraps headings;
|
|
227
|
-
//
|
|
361
|
+
// Panda's preflight balance-wraps headings; balanced multi-line
|
|
362
|
+
// headings break at different points (mobile/translations), so undo it.
|
|
228
363
|
"h1, h2, h3, h4, h5, h6": {
|
|
229
364
|
textWrap: "wrap",
|
|
230
365
|
},
|
|
366
|
+
// The colour of the seams an attached ButtonGroup draws (ButtonGroup.tsx).
|
|
367
|
+
// Transparent, so a variant with no border of its own divides by letting
|
|
368
|
+
// the surface show through rather than by a line in the text colour.
|
|
369
|
+
//
|
|
370
|
+
// From `base`, so a variant that does bring a border colours its seams
|
|
371
|
+
// from `recipes` (as the cursor rule above is overridden). Longhands
|
|
372
|
+
// because Panda resolves `transparent` to a token, and the logical
|
|
373
|
+
// *shorthand* with a var() value is the one Safari 14.x drops — the
|
|
374
|
+
// postcss-legacy-safari shim does not cover the border ones.
|
|
375
|
+
"[data-attached] > *": {
|
|
376
|
+
borderInlineStartColor: "transparent",
|
|
377
|
+
borderInlineEndColor: "transparent",
|
|
378
|
+
},
|
|
379
|
+
// While a full-size dialog is open (the Modal stamps data-fullsize on
|
|
380
|
+
// its overlay), release the scrollbar gutter that react-aria's scroll
|
|
381
|
+
// lock reserves on the root. The reserved strip is scrollbar chrome to
|
|
382
|
+
// hit-testing — elementFromPoint returns null there, so clicks fall
|
|
383
|
+
// through to the root and dismiss the dialog, and controls near the
|
|
384
|
+
// right edge lose part of their target. With the page fully covered,
|
|
385
|
+
// the reflow this causes is invisible. !important: react-aria sets the
|
|
386
|
+
// reservation as a non-important inline style.
|
|
387
|
+
"html:has([data-fullsize])": {
|
|
388
|
+
scrollbarGutter: "auto !important",
|
|
389
|
+
},
|
|
390
|
+
// Toast enter/exit (the ToastQueue wraps updates in
|
|
391
|
+
// document.startViewTransition — see Toast.tsx, which also stamps the
|
|
392
|
+
// scoping class on <html> while its transitions run). Timings: 0.4s
|
|
393
|
+
// fade+slide in, 0.2s fade+shrink out; the
|
|
394
|
+
// stack reflow comes from the default group animation. `(*)` +
|
|
395
|
+
// `:only-child` matches exactly the entering/exiting toast groups: the
|
|
396
|
+
// root snapshot always has both old and new children, and toasts are
|
|
397
|
+
// the only named groups during a toast transition. The snapshot
|
|
398
|
+
// overlay must not eat clicks while a toast animates, hence
|
|
399
|
+
// pointer-events, scoped likewise.
|
|
400
|
+
"html.microbit-ui-toast-transition::view-transition": {
|
|
401
|
+
pointerEvents: "none",
|
|
402
|
+
},
|
|
403
|
+
// `both` fill: the snapshots must hold the keyframes' end states for
|
|
404
|
+
// however long the rest of the transition (e.g. the 0.25s default group
|
|
405
|
+
// animation) outlives them, or they snap back to full size/opacity for
|
|
406
|
+
// the remainder.
|
|
407
|
+
"html.microbit-ui-toast-transition::view-transition-new(*):only-child": {
|
|
408
|
+
animation: "toastSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) both",
|
|
409
|
+
},
|
|
410
|
+
"html.microbit-ui-toast-transition::view-transition-old(*):only-child": {
|
|
411
|
+
animation: "toastSlideOut 0.2s cubic-bezier(0.4, 0, 1, 1) both",
|
|
412
|
+
},
|
|
231
413
|
},
|
|
232
414
|
// shared-ui components forward `variant`/`size`/etc. as runtime props to
|
|
233
415
|
// the recipe functions, so Panda's static analysis can't see which variants
|
|
@@ -236,45 +418,108 @@ export const basePreset = definePreset({
|
|
|
236
418
|
// can silently lose runtime-prop variants.
|
|
237
419
|
staticCss: {
|
|
238
420
|
recipes: {
|
|
239
|
-
|
|
421
|
+
// Size is passed responsively at call sites
|
|
422
|
+
// (`size={["md", "lg"]}`), so generate the breakpoint-prefixed variants
|
|
423
|
+
// too — otherwise the class lands on the element with no rule behind it
|
|
424
|
+
// and the button silently falls back to the base size.
|
|
425
|
+
avatar: ["*"],
|
|
426
|
+
// `tone` generates as its own rule, not crossed with `variant`: it
|
|
427
|
+
// only assigns the palette custom properties a shape reads.
|
|
428
|
+
button: [
|
|
429
|
+
{ size: ["*"], responsive: true },
|
|
430
|
+
{ variant: ["*"] },
|
|
431
|
+
{ tone: ["*"] },
|
|
432
|
+
],
|
|
433
|
+
checkbox: ["*"],
|
|
240
434
|
heading: ["*"],
|
|
241
435
|
card: ["*"],
|
|
242
436
|
// Dialog size is chosen with responsive objects ({ base, md }) passed
|
|
243
437
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
244
438
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
245
439
|
drawer: ["*"],
|
|
440
|
+
field: ["*"],
|
|
441
|
+
gridList: ["*"],
|
|
442
|
+
listBox: ["*"],
|
|
443
|
+
input: ["*"],
|
|
444
|
+
numberField: ["*"],
|
|
445
|
+
radio: ["*"],
|
|
446
|
+
select: ["*"],
|
|
447
|
+
switchRecipe: ["*"],
|
|
448
|
+
text: ["*"],
|
|
449
|
+
tooltip: ["*"],
|
|
246
450
|
// Toast status is chosen at runtime from the toast content.
|
|
247
451
|
toast: ["*"],
|
|
248
452
|
},
|
|
249
453
|
},
|
|
250
454
|
utilities: {
|
|
251
455
|
extend: {
|
|
252
|
-
// The app-wide focus indicator, usually inside `_focusVisible
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
456
|
+
// The app-wide focus indicator, usually inside `_focusVisible`: a
|
|
457
|
+
// 2px `focusRing`-coloured outline at 2px offset — the surface shows
|
|
458
|
+
// through the gap, and call sites never pick a ring per background.
|
|
459
|
+
// A real outline, so forced-colors modes keep a ring; longhands
|
|
460
|
+
// because Panda resolves tokens per-property. Thickness/contrast
|
|
461
|
+
// rationale: ui-private docs/a11y-positions.md. Shadows
|
|
462
|
+
// preset-base's `focusRing`: our transform replaces theirs, but the
|
|
463
|
+
// values arrays union, so its outside/inside/mixed/none typecheck
|
|
464
|
+
// here. `none` is honoured — the alternative is a permanent un-gated
|
|
465
|
+
// ring; the other three fall through to the standard one. Don't use
|
|
466
|
+
// them.
|
|
467
|
+
focusRing: {
|
|
468
|
+
className: "focus-ring",
|
|
469
|
+
// `outlineInset` draws the ring just inside — for full-bleed rows
|
|
470
|
+
// whose outward ring would overhang their popover.
|
|
471
|
+
values: ["outline", "outlineInset"],
|
|
472
|
+
transform: (value: string, { token }) =>
|
|
473
|
+
value === "none"
|
|
474
|
+
? { outlineStyle: "none" }
|
|
475
|
+
: {
|
|
476
|
+
outlineStyle: "solid",
|
|
477
|
+
outlineWidth: "2px",
|
|
478
|
+
outlineColor: token("colors.focusRing"),
|
|
479
|
+
outlineOffset: value === "outlineInset" ? "-2px" : "2px",
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
// preset-base's remaining ring plumbing sets --focus-ring-* custom
|
|
483
|
+
// properties only its own utilities read. Repointed at the outline
|
|
484
|
+
// longhands ours draws with, rather than left as no-ops that read
|
|
485
|
+
// like working knobs.
|
|
486
|
+
focusRingWidth: {
|
|
487
|
+
className: "focus-ring-w",
|
|
488
|
+
values: "borderWidths",
|
|
489
|
+
transform: (value: string) => ({ outlineWidth: value }),
|
|
490
|
+
},
|
|
491
|
+
focusRingOffset: {
|
|
492
|
+
className: "focus-ring-o",
|
|
493
|
+
values: "spacing",
|
|
494
|
+
transform: (value: string) => ({ outlineOffset: value }),
|
|
495
|
+
},
|
|
496
|
+
focusRingStyle: {
|
|
497
|
+
className: "focus-ring-s",
|
|
498
|
+
values: "borderStyles",
|
|
499
|
+
// `outlineStyle` is a keyword union, hence the cast.
|
|
500
|
+
transform: (value: string) => ({ outlineStyle: value as "solid" }),
|
|
265
501
|
},
|
|
266
502
|
},
|
|
267
503
|
},
|
|
268
|
-
// Widen the interaction conditions so
|
|
269
|
-
//
|
|
504
|
+
// Widen the interaction conditions so recipe/style objects written with
|
|
505
|
+
// `_hover`/`_active`/`_focusVisible`/`_disabled` also respond to
|
|
270
506
|
// react-aria-components' data attributes, not just native pseudo-classes.
|
|
271
507
|
conditions: {
|
|
272
508
|
extend: {
|
|
273
509
|
hover: "&:is(:hover, [data-hovered])",
|
|
274
510
|
active: "&:is(:active, [data-pressed])",
|
|
275
|
-
|
|
511
|
+
// Native :focus-visible counts only on elements RAC doesn't manage:
|
|
512
|
+
// react-aria's modality tracking is stricter than the browser's
|
|
513
|
+
// (e.g. focus restored from a menu after mouse-only use).
|
|
514
|
+
focusVisible:
|
|
515
|
+
"&:is(:focus-visible:not([data-rac]), [data-focus-visible])",
|
|
276
516
|
disabled:
|
|
277
517
|
"&:is(:disabled, [disabled], [data-disabled], [aria-disabled=true])",
|
|
518
|
+
// A dark-by-design surface (spread the exported `darkSurface` onto
|
|
519
|
+
// the bar element); scopes the focusRing/focusBorder flips. Never
|
|
520
|
+
// theme-relative: a future dark mode flips untagged defaults via
|
|
521
|
+
// token conditions, not markup.
|
|
522
|
+
onDark: '[data-surface="dark"] &',
|
|
278
523
|
// High-contrast/forced-palette modes (e.g. Windows High Contrast), which
|
|
279
524
|
// strip author backgrounds and box-shadows.
|
|
280
525
|
forcedColors: "@media (forced-colors: active)",
|
package/src/base-tokens.ts
CHANGED
package/src/button-icon.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { cva } from "styled-system/css";
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// exported from the package index.
|
|
8
|
+
// Keeps the glyph centred and spaced 0.5rem from the label. Shared by Button
|
|
9
|
+
// and LinkButton; deliberately not exported from the package index.
|
|
11
10
|
export const buttonIcon = cva({
|
|
12
11
|
base: {
|
|
13
12
|
display: "inline-flex",
|
|
@@ -16,8 +15,8 @@ export const buttonIcon = cva({
|
|
|
16
15
|
},
|
|
17
16
|
variants: {
|
|
18
17
|
side: {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
start: { marginEnd: "2" },
|
|
19
|
+
end: { marginStart: "2" },
|
|
21
20
|
},
|
|
22
21
|
},
|
|
23
22
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `data-*` entries of a props object, for components that let a caller
|
|
9
|
+
* put test hooks on an inner element rather than the one their props land on.
|
|
10
|
+
*
|
|
11
|
+
* Internal: not exported from the package.
|
|
12
|
+
*/
|
|
13
|
+
export const dataAttrs = (props: object): Record<string, unknown> =>
|
|
14
|
+
Object.fromEntries(
|
|
15
|
+
Object.entries(props).filter(([key]) => key.startsWith("data-")),
|
|
16
|
+
);
|