@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +40 -0
- package/README.md +256 -26
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +62 -0
- 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 +62 -0
- 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 +62 -0
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +19 -10
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Avatar.recipe.ts +166 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +39 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +64 -26
- 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 +20 -0
- package/src/Collapse.tsx +179 -0
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +72 -8
- package/src/Drawer.recipe.ts +4 -5
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +62 -0
- 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 +7 -12
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +21 -8
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +81 -0
- package/src/List.tsx +8 -6
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +51 -17
- package/src/Menu.tsx +117 -2
- package/src/Modal.recipe.ts +14 -9
- package/src/Modal.tsx +137 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +39 -12
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +108 -0
- package/src/NumberField.tsx +138 -0
- package/src/PopoverArrow.tsx +19 -5
- 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 +51 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +24 -15
- package/src/Slider.tsx +65 -5
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +58 -13
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +54 -40
- package/src/Toast.recipe.ts +49 -12
- package/src/Toast.tsx +151 -46
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +51 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +336 -84
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
- package/src/button-icon.ts +22 -0
- 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 +63 -0
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/hooks/usePrevious.ts +15 -0
- package/src/index.ts +29 -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
|
@@ -20,39 +20,106 @@ import {
|
|
|
20
20
|
sizes,
|
|
21
21
|
spacing,
|
|
22
22
|
zIndex,
|
|
23
|
-
} from "./
|
|
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";
|
|
37
|
+
import { numberField } from "./NumberField.recipe";
|
|
32
38
|
import { menu } from "./Menu.recipe";
|
|
39
|
+
import { select } from "./Select.recipe";
|
|
33
40
|
import { slider } from "./Slider.recipe";
|
|
34
41
|
import { switchRecipe } from "./Switch.recipe";
|
|
35
42
|
import { dialog } from "./Modal.recipe";
|
|
36
|
-
import {
|
|
43
|
+
import { text } from "./Text.recipe";
|
|
44
|
+
import { tooltip } from "./Tooltip.recipe";
|
|
45
|
+
import { field } from "./Field.recipe";
|
|
37
46
|
import { toast } from "./Toast.recipe";
|
|
38
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
|
+
|
|
39
105
|
/**
|
|
40
|
-
* The base preset: the complete, working micro:bit design system.
|
|
41
|
-
*
|
|
42
|
-
* (pill `radii.button`, `
|
|
43
|
-
*
|
|
106
|
+
* The base preset: the complete, working micro:bit design system. The base
|
|
107
|
+
* token scales (base-tokens.ts), the micro:bit house style
|
|
108
|
+
* (pill `radii.button`, the `focusRing` utility/token pair, Helvetica
|
|
109
|
+
* fonts, the
|
|
110
|
+
* `toolbar` button variant in Button.recipe.ts, the
|
|
44
111
|
* `languageText`/`toast*Bg`/`statusBarBg` semantic tokens), the shared-ui
|
|
45
|
-
* component recipes, the react-aria condition widening, the
|
|
46
|
-
*
|
|
112
|
+
* component recipes, the react-aria condition widening, the `globalCss`
|
|
113
|
+
* defaults, and the `staticCss` that keeps runtime-prop recipe
|
|
47
114
|
* variants generated. Used alone it renders in the OSS default look.
|
|
48
115
|
*
|
|
49
116
|
* ── Brand contract ──────────────────────────────────────────────────────
|
|
50
117
|
* A private brand preset (a sibling repo, e.g. CreateAI) is merged AFTER this
|
|
51
118
|
* one to restyle everything by overriding just these token *values* (never
|
|
52
119
|
* their names — see the CSS-var contract in the README):
|
|
53
|
-
* - colours: the `brand` and `brand2` ramps (OSS defaults:
|
|
54
|
-
*
|
|
55
|
-
* (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.
|
|
56
123
|
* - font: `display` (OSS default: Helvetica; e.g. GT Walsheim privately).
|
|
57
124
|
* The recipes and semantic tokens here reference those, so a brand swap needs
|
|
58
125
|
* no recipe changes. With no private preset, these OSS defaults stand.
|
|
@@ -62,25 +129,44 @@ export const basePreset = definePreset({
|
|
|
62
129
|
theme: {
|
|
63
130
|
breakpoints,
|
|
64
131
|
keyframes: {
|
|
65
|
-
// Spinner's revolution
|
|
132
|
+
// Spinner's revolution.
|
|
66
133
|
spin: {
|
|
67
134
|
"0%": { transform: "rotate(0deg)" },
|
|
68
135
|
"100%": { transform: "rotate(360deg)" },
|
|
69
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
|
+
},
|
|
70
158
|
},
|
|
71
159
|
tokens: {
|
|
72
160
|
colors: {
|
|
73
161
|
...colors,
|
|
74
|
-
gray
|
|
75
|
-
|
|
76
|
-
// Very light grays the family's designs use below Chakra's 50.
|
|
77
|
-
10: { value: "#fcfcfc" },
|
|
78
|
-
25: { value: "#f5f5f5" },
|
|
79
|
-
},
|
|
162
|
+
gray,
|
|
163
|
+
red,
|
|
80
164
|
// OSS default brand ramps (see the brand contract above). `brand`
|
|
81
|
-
// aliases
|
|
82
|
-
//
|
|
83
|
-
//
|
|
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.
|
|
84
170
|
brand: colors.blue,
|
|
85
171
|
brand2: colors.gray,
|
|
86
172
|
},
|
|
@@ -102,16 +188,11 @@ export const basePreset = definePreset({
|
|
|
102
188
|
},
|
|
103
189
|
shadows: {
|
|
104
190
|
...shadows,
|
|
105
|
-
// Chakra's outline shadow widened to 4px, plus dark/light-surface
|
|
106
|
-
// companions. Consumed via the `focusShadow` utility.
|
|
107
|
-
outline: { value: "0 0 0 4px rgba(66, 153, 225, 0.6)" },
|
|
108
|
-
outlineDark: { value: "0 0 0 4px rgba(0, 0, 0, 0.5)" },
|
|
109
|
-
outlineLight: { value: "0 0 0 4px rgba(255, 255, 255, 0.8)" },
|
|
110
191
|
},
|
|
111
192
|
fonts: {
|
|
112
193
|
// Helvetica heading/body (4/4 apps); a brand preset leaves these and
|
|
113
194
|
// overrides only `display` (the marketing font — see the brand
|
|
114
|
-
// contract above).
|
|
195
|
+
// contract above).
|
|
115
196
|
heading: { value: "Helvetica, Arial, sans-serif" },
|
|
116
197
|
body: { value: "Helvetica, Arial, sans-serif" },
|
|
117
198
|
mono: {
|
|
@@ -125,39 +206,86 @@ export const basePreset = definePreset({
|
|
|
125
206
|
},
|
|
126
207
|
semanticTokens: {
|
|
127
208
|
colors: {
|
|
128
|
-
// Checked
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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).
|
|
141
234
|
danger: {
|
|
142
235
|
50: { value: "{colors.red.50}" },
|
|
143
236
|
100: { value: "{colors.red.100}" },
|
|
237
|
+
200: { value: "{colors.red.200}" },
|
|
238
|
+
300: { value: "{colors.red.300}" },
|
|
239
|
+
400: { value: "{colors.red.400}" },
|
|
144
240
|
500: { value: "{colors.red.500}" },
|
|
145
241
|
600: { value: "{colors.red.600}" },
|
|
146
242
|
700: { value: "{colors.red.700}" },
|
|
243
|
+
800: { value: "{colors.red.800}" },
|
|
244
|
+
900: { value: "{colors.red.900}" },
|
|
147
245
|
},
|
|
148
|
-
// The
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
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.
|
|
252
|
+
languageText: { value: "{colors.brand.500}" },
|
|
253
|
+
languageTextHover: { value: "{colors.brand.600}" },
|
|
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.
|
|
157
283
|
toastInfoBg: { value: "{colors.teal.800}" },
|
|
158
284
|
toastSuccessBg: { value: "{colors.teal.800}" },
|
|
159
285
|
toastWarningBg: { value: "{colors.teal.800}" },
|
|
160
|
-
|
|
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}" },
|
|
161
289
|
// The native app's status-bar area colour, shared by the ActionBar
|
|
162
290
|
// and the full-size dialog's safe-area gradient.
|
|
163
291
|
statusBarBg: { value: "{colors.brand2.500}" },
|
|
@@ -167,33 +295,45 @@ export const basePreset = definePreset({
|
|
|
167
295
|
button,
|
|
168
296
|
heading,
|
|
169
297
|
input,
|
|
298
|
+
text,
|
|
299
|
+
tooltip,
|
|
170
300
|
},
|
|
171
301
|
slotRecipes: {
|
|
302
|
+
avatar,
|
|
303
|
+
breadcrumb,
|
|
172
304
|
card,
|
|
173
305
|
checkbox,
|
|
174
306
|
dialog,
|
|
175
307
|
drawer,
|
|
176
308
|
field,
|
|
309
|
+
gridList,
|
|
310
|
+
listBox,
|
|
177
311
|
menu,
|
|
312
|
+
numberField,
|
|
313
|
+
radio,
|
|
314
|
+
select,
|
|
178
315
|
slider,
|
|
179
316
|
switchRecipe,
|
|
180
317
|
toast,
|
|
181
318
|
},
|
|
182
319
|
},
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// shifts glyphs page-wide), word-wrap and touch-action. Token references
|
|
188
|
-
// resolve against the merged preset stack, so the values track any brand
|
|
189
|
-
// 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.
|
|
190
324
|
globalCss: {
|
|
191
325
|
html: {
|
|
192
326
|
textRendering: "optimizeLegibility",
|
|
193
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)",
|
|
194
333
|
},
|
|
195
334
|
body: {
|
|
196
|
-
position:
|
|
335
|
+
// No `position: relative` (Chakra had it): it breaks react-aria's
|
|
336
|
+
// overlay positioning — see Tooltip's "In a scrolling page" story.
|
|
197
337
|
minHeight: "100%",
|
|
198
338
|
fontFeatureSettings: '"kern"',
|
|
199
339
|
fontFamily: "body",
|
|
@@ -206,21 +346,70 @@ export const basePreset = definePreset({
|
|
|
206
346
|
"*::placeholder": {
|
|
207
347
|
color: "gray.500",
|
|
208
348
|
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
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.
|
|
216
358
|
"button, [role='button']": {
|
|
217
359
|
cursor: "pointer",
|
|
218
360
|
},
|
|
219
|
-
// Panda's preflight balance-wraps headings;
|
|
220
|
-
//
|
|
361
|
+
// Panda's preflight balance-wraps headings; balanced multi-line
|
|
362
|
+
// headings break at different points (mobile/translations), so undo it.
|
|
221
363
|
"h1, h2, h3, h4, h5, h6": {
|
|
222
364
|
textWrap: "wrap",
|
|
223
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
|
+
},
|
|
224
413
|
},
|
|
225
414
|
// shared-ui components forward `variant`/`size`/etc. as runtime props to
|
|
226
415
|
// the recipe functions, so Panda's static analysis can't see which variants
|
|
@@ -229,45 +418,108 @@ export const basePreset = definePreset({
|
|
|
229
418
|
// can silently lose runtime-prop variants.
|
|
230
419
|
staticCss: {
|
|
231
420
|
recipes: {
|
|
232
|
-
|
|
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: ["*"],
|
|
233
434
|
heading: ["*"],
|
|
234
435
|
card: ["*"],
|
|
235
436
|
// Dialog size is chosen with responsive objects ({ base, md }) passed
|
|
236
437
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
237
438
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
238
439
|
drawer: ["*"],
|
|
440
|
+
field: ["*"],
|
|
441
|
+
gridList: ["*"],
|
|
442
|
+
listBox: ["*"],
|
|
443
|
+
input: ["*"],
|
|
444
|
+
numberField: ["*"],
|
|
445
|
+
radio: ["*"],
|
|
446
|
+
select: ["*"],
|
|
447
|
+
switchRecipe: ["*"],
|
|
448
|
+
text: ["*"],
|
|
449
|
+
tooltip: ["*"],
|
|
239
450
|
// Toast status is chosen at runtime from the toast content.
|
|
240
451
|
toast: ["*"],
|
|
241
452
|
},
|
|
242
453
|
},
|
|
243
454
|
utilities: {
|
|
244
455
|
extend: {
|
|
245
|
-
// The app-wide focus indicator, usually inside `_focusVisible
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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" }),
|
|
258
501
|
},
|
|
259
502
|
},
|
|
260
503
|
},
|
|
261
|
-
// Widen the interaction conditions so
|
|
262
|
-
//
|
|
504
|
+
// Widen the interaction conditions so recipe/style objects written with
|
|
505
|
+
// `_hover`/`_active`/`_focusVisible`/`_disabled` also respond to
|
|
263
506
|
// react-aria-components' data attributes, not just native pseudo-classes.
|
|
264
507
|
conditions: {
|
|
265
508
|
extend: {
|
|
266
509
|
hover: "&:is(:hover, [data-hovered])",
|
|
267
510
|
active: "&:is(:active, [data-pressed])",
|
|
268
|
-
|
|
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])",
|
|
269
516
|
disabled:
|
|
270
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"] &',
|
|
271
523
|
// High-contrast/forced-palette modes (e.g. Windows High Contrast), which
|
|
272
524
|
// strip author backgrounds and box-shadows.
|
|
273
525
|
forcedColors: "@media (forced-colors: active)",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
3
|
*
|
|
4
|
+
* Token values (c) 2019, Segun Adebayo — Chakra UI's @chakra-ui/theme
|
|
5
|
+
* defaults, used under the MIT License (see LICENSE.md third-party notices).
|
|
6
|
+
*
|
|
4
7
|
* SPDX-License-Identifier: MIT
|
|
5
8
|
*/
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// The base token scales, in Panda token format. Began as a mechanical
|
|
10
|
+
// snapshot of Chakra UI v2's @chakra-ui/theme defaults; hand-maintained.
|
|
11
|
+
// Deliberately absent: `fonts` (foundation-owned, defined in base-preset.ts)
|
|
12
|
+
// and `transition.property` (no Panda token category; inlined at use sites).
|
|
10
13
|
|
|
11
14
|
export const colors = {
|
|
12
15
|
transparent: {
|
|
@@ -957,9 +960,6 @@ export const shadows = {
|
|
|
957
960
|
"2xl": {
|
|
958
961
|
value: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
|
959
962
|
},
|
|
960
|
-
outline: {
|
|
961
|
-
value: "0 0 0 3px rgba(66, 153, 225, 0.6)",
|
|
962
|
-
},
|
|
963
963
|
inner: {
|
|
964
964
|
value: "inset 0 2px 4px 0 rgba(0,0,0,0.06)",
|
|
965
965
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { cva } from "styled-system/css";
|
|
7
|
+
|
|
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.
|
|
10
|
+
export const buttonIcon = cva({
|
|
11
|
+
base: {
|
|
12
|
+
display: "inline-flex",
|
|
13
|
+
alignSelf: "center",
|
|
14
|
+
flexShrink: 0,
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
side: {
|
|
18
|
+
left: { marginEnd: "2" },
|
|
19
|
+
right: { marginStart: "2" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
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
|
+
);
|