@microbit/ui 0.5.1 → 0.6.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/README.md +16 -5
- package/lang/ui.es-ES.json +3 -3
- package/package.json +10 -9
- package/src/ComboBox.tsx +73 -25
- package/src/ConfirmDialog.tsx +1 -1
- package/src/Modal.tsx +1 -1
- package/src/Select.recipe.ts +3 -0
- package/src/base-preset.ts +14 -0
- package/src/hooks/useClipboard.ts +3 -1
- package/src/hooks/usePrevious.ts +1 -1
- package/src/system.ts +13 -10
package/README.md
CHANGED
|
@@ -25,10 +25,12 @@ setup below, minus the app/brand presets. Everything an app must do:
|
|
|
25
25
|
|
|
26
26
|
Later presets override earlier ones token-by-token — the base recipes and
|
|
27
27
|
semantic tokens reference the brand tokens, which is how a brand swap
|
|
28
|
-
restyles everything without touching recipes.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
restyles everything without touching recipes. Install
|
|
29
|
+
`@pandacss/preset-base` as a direct dependency: `@pandacss/dev` no longer
|
|
30
|
+
pulls it in, and nothing is added to the stack implicitly. After changing
|
|
31
|
+
an _external_ preset dependency, regenerate clean:
|
|
32
|
+
`rm -rf styled-system && npm run panda` — incremental codegen does not
|
|
33
|
+
detect external preset changes.
|
|
32
34
|
|
|
33
35
|
2. **Include this package's source** in `panda.config.ts` so Panda extracts
|
|
34
36
|
the styles the components use:
|
|
@@ -42,6 +44,14 @@ setup below, minus the app/brand presets. Everything an app must do:
|
|
|
42
44
|
importers, this package's source included — a `styled-system` alias in
|
|
43
45
|
both `tsconfig.json` `paths` and the bundler config (see the
|
|
44
46
|
`viteFinal` in `apps/storybook`'s `.storybook/main.ts`).
|
|
47
|
+
|
|
48
|
+
App code imports `css`, `cx`, `cva` and `sva` from `styled-system/css`
|
|
49
|
+
and `styled` from `styled-system/jsx`, not from `@microbit/ui`: Panda
|
|
50
|
+
extracts those only when they are imported from the generated module, and
|
|
51
|
+
a call through a re-export emits no CSS and no diagnostic. What
|
|
52
|
+
`@microbit/ui` does export — the components, the layout patterns,
|
|
53
|
+
`token` — is safe wherever it is used.
|
|
54
|
+
|
|
45
55
|
4. **Generate and load the CSS** with Panda's PostCSS plugin. Keep Vite's
|
|
46
56
|
default transformer — do **not** set `css.transformer: "lightningcss"`,
|
|
47
57
|
which disables PostCSS. Add a `postcss.config.cjs`:
|
|
@@ -209,7 +219,8 @@ Panda emits every token as a CSS custom property with its default naming —
|
|
|
209
219
|
- `fonts.display` → `var(--fonts-display)`
|
|
210
220
|
|
|
211
221
|
These names are **API** for styling that lives outside React/Panda — e.g.
|
|
212
|
-
CodeMirror highlight styles
|
|
222
|
+
CodeMirror highlight styles, xterm themes, or `@microbit/ui-code-view`'s
|
|
223
|
+
stylesheet, which reads the `code.*` syntax-highlighting colours. Two rules
|
|
213
224
|
keep them stable:
|
|
214
225
|
|
|
215
226
|
- Never set `hashing` or `prefix` in `panda.config.ts`.
|
package/lang/ui.es-ES.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ui.breadcrumb": {
|
|
3
|
-
"defaultMessage": "
|
|
3
|
+
"defaultMessage": "Breadcrumb"
|
|
4
4
|
},
|
|
5
5
|
"ui.cancel-action": {
|
|
6
6
|
"defaultMessage": "Cancelar"
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"defaultMessage": "Cargando"
|
|
19
19
|
},
|
|
20
20
|
"ui.numberfield-decrease": {
|
|
21
|
-
"defaultMessage": "
|
|
21
|
+
"defaultMessage": "Disminuir {fieldLabel}"
|
|
22
22
|
},
|
|
23
23
|
"ui.numberfield-increase": {
|
|
24
24
|
"defaultMessage": "Aumentar {fieldLabel}"
|
|
25
25
|
},
|
|
26
26
|
"ui.select-placeholder": {
|
|
27
|
-
"defaultMessage": "
|
|
27
|
+
"defaultMessage": "Selecciona un elemento"
|
|
28
28
|
},
|
|
29
29
|
"ui.select-row-action": {
|
|
30
30
|
"defaultMessage": "Seleccionar"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a design language ported from Chakra UI v2. Ships as source; see README for the consumption setup.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,31 +35,32 @@
|
|
|
35
35
|
"test": "vitest run"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@pandacss/dev": "^
|
|
39
|
-
"react": "^18.3.1",
|
|
38
|
+
"@pandacss/dev": "^2.4.0",
|
|
39
|
+
"react": "^18.3.1 || ^19.0.0",
|
|
40
40
|
"react-aria": "^3.50.0",
|
|
41
41
|
"react-aria-components": "^1.19.0",
|
|
42
42
|
"react-icons": "^4.12.0 || ^5.0.0",
|
|
43
|
-
"react-intl": "^6.6.8 || ^7.0.0"
|
|
43
|
+
"react-intl": "^6.6.8 || ^7.0.0 || ^10.1.14 || ^12.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@pandacss/dev": "^
|
|
46
|
+
"@pandacss/dev": "^2.4.0",
|
|
47
|
+
"@pandacss/preset-base": "^2.4.0",
|
|
47
48
|
"@storybook/react-vite": "^10.5.2",
|
|
48
49
|
"@testing-library/react": "^16.3.2",
|
|
49
50
|
"@testing-library/user-event": "^14.6.1",
|
|
50
|
-
"@types/node": "^
|
|
51
|
+
"@types/node": "^24.13.3",
|
|
51
52
|
"@types/react": "^18.3.3",
|
|
52
53
|
"@types/react-dom": "^18.3.0",
|
|
53
54
|
"@vitejs/plugin-react": "^5.2.0",
|
|
54
|
-
"jsdom": "^
|
|
55
|
+
"jsdom": "^30.1.3",
|
|
55
56
|
"react": "^18.3.1",
|
|
56
57
|
"react-aria": "^3.50.0",
|
|
57
58
|
"react-aria-components": "^1.19.0",
|
|
58
59
|
"react-dom": "^18.3.1",
|
|
59
60
|
"react-icons": "^4.12.0",
|
|
60
|
-
"react-intl": "^
|
|
61
|
+
"react-intl": "^12.1.5",
|
|
61
62
|
"typescript": "^5.4.2",
|
|
62
63
|
"vite": "^7.3.6",
|
|
63
|
-
"vitest": "^
|
|
64
|
+
"vitest": "^5.0.5"
|
|
64
65
|
}
|
|
65
66
|
}
|
package/src/ComboBox.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ForwardedRef,
|
|
8
8
|
forwardRef,
|
|
9
9
|
ReactNode,
|
|
10
|
+
useContext,
|
|
10
11
|
useLayoutEffect,
|
|
11
12
|
useRef,
|
|
12
13
|
useState,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
Button as RACButton,
|
|
16
17
|
ComboBox as RACComboBox,
|
|
17
18
|
ComboBoxProps as RACComboBoxProps,
|
|
19
|
+
ComboBoxStateContext,
|
|
18
20
|
Input as RACInput,
|
|
19
21
|
ListBox as RACListBox,
|
|
20
22
|
Popover,
|
|
@@ -74,9 +76,14 @@ export interface ComboBoxProps<T extends object>
|
|
|
74
76
|
*/
|
|
75
77
|
emptyState?: ReactNode;
|
|
76
78
|
/**
|
|
77
|
-
* Keep the dropdown shut
|
|
79
|
+
* Keep the dropdown shut while this prop is true. For gating on a minimum
|
|
78
80
|
* query length — react-aria has no `minLength`, and rendering an empty list
|
|
79
|
-
* still opens an empty card.
|
|
81
|
+
* still opens an empty card. Derive it from a controlled `inputValue`, so
|
|
82
|
+
* the keystroke that clears the gate is the one that opens the list.
|
|
83
|
+
*
|
|
84
|
+
* While true, nothing can open the list: the chevron is disabled, and
|
|
85
|
+
* ArrowUp/ArrowDown are stopped before they reach the input (so an
|
|
86
|
+
* `onKeyDown` you pass won't see them either).
|
|
80
87
|
*/
|
|
81
88
|
isPopoverHidden?: boolean;
|
|
82
89
|
placement?: PopoverProps["placement"];
|
|
@@ -98,6 +105,24 @@ export interface ComboBoxProps<T extends object>
|
|
|
98
105
|
className?: string;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Closes a list left open when `isPopoverHidden` turns on (deleting back
|
|
110
|
+
* below the minimum query length). Opening is blocked at source — see the
|
|
111
|
+
* `menuTrigger` override and arrow-key guard below — and this also catches
|
|
112
|
+
* RAC's rarer open paths, such as the iPad VoiceOver tap on the input.
|
|
113
|
+
*
|
|
114
|
+
* Layout effect, not a passive one, so the stale list never reaches a paint.
|
|
115
|
+
*/
|
|
116
|
+
const ClosePopoverWhenHidden = ({ isHidden }: { isHidden?: boolean }) => {
|
|
117
|
+
const state = useContext(ComboBoxStateContext);
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
if (isHidden && state?.isOpen) {
|
|
120
|
+
state.setOpen(false);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
|
|
101
126
|
/**
|
|
102
127
|
* ComboBox — a text input that filters a listbox, for choosing one of a known
|
|
103
128
|
* set where typing to narrow it down is the point. Use Select where the list
|
|
@@ -117,6 +142,7 @@ const ComboBoxInner = <T extends object>(
|
|
|
117
142
|
indicator,
|
|
118
143
|
emptyState,
|
|
119
144
|
isPopoverHidden,
|
|
145
|
+
menuTrigger,
|
|
120
146
|
placement = "bottom start",
|
|
121
147
|
maxHeight,
|
|
122
148
|
helperText,
|
|
@@ -167,6 +193,8 @@ const ComboBoxInner = <T extends object>(
|
|
|
167
193
|
<RACComboBox
|
|
168
194
|
allowsEmptyCollection={emptyState != null}
|
|
169
195
|
{...rest}
|
|
196
|
+
// "manual" stops typing and focus from opening the list at all.
|
|
197
|
+
menuTrigger={isPopoverHidden ? "manual" : menuTrigger}
|
|
170
198
|
className={cx(fieldSlots.root, slots.root, className)}
|
|
171
199
|
>
|
|
172
200
|
{label != null && (
|
|
@@ -182,6 +210,17 @@ const ComboBoxInner = <T extends object>(
|
|
|
182
210
|
<div
|
|
183
211
|
ref={triggerRef}
|
|
184
212
|
data-keyboard-modality={isFocusVisible || undefined}
|
|
213
|
+
// "manual" still lets the arrow keys open the list, and RAC's
|
|
214
|
+
// handler is on the input, so stop them on the way down.
|
|
215
|
+
onKeyDownCapture={
|
|
216
|
+
isPopoverHidden
|
|
217
|
+
? (e) => {
|
|
218
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
: undefined
|
|
223
|
+
}
|
|
185
224
|
className={cx(
|
|
186
225
|
slots.trigger,
|
|
187
226
|
triggerCss ? css(triggerCss) : undefined,
|
|
@@ -196,36 +235,45 @@ const ComboBoxInner = <T extends object>(
|
|
|
196
235
|
{indicator !== null && (
|
|
197
236
|
<RACButton
|
|
198
237
|
aria-label={intl.formatMessage(uiMessage("ui.combobox-trigger"))}
|
|
238
|
+
isDisabled={isPopoverHidden}
|
|
199
239
|
className={slots.indicator}
|
|
200
240
|
>
|
|
201
241
|
{indicator ?? <Icon as={RiArrowDownSLine} />}
|
|
202
242
|
</RACButton>
|
|
203
243
|
)}
|
|
204
244
|
</div>
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
245
|
+
<ClosePopoverWhenHidden isHidden={isPopoverHidden} />
|
|
246
|
+
{/*
|
|
247
|
+
Always mounted, even while isPopoverHidden: RAC builds the
|
|
248
|
+
collection from the ListBox children inside the popover, so
|
|
249
|
+
unmounting it also empties the collection — and an empty
|
|
250
|
+
collection can't open (without allowsEmptyCollection), so the next
|
|
251
|
+
keystroke's open lands before the remounted children have
|
|
252
|
+
repopulated it and the list never appears. React 18's effect
|
|
253
|
+
ordering happened to win that race; React 19's does not.
|
|
254
|
+
*/}
|
|
255
|
+
<Popover
|
|
256
|
+
triggerRef={triggerRef}
|
|
257
|
+
placement={placement}
|
|
258
|
+
maxHeight={maxHeight}
|
|
259
|
+
style={triggerWidth ? { width: triggerWidth } : undefined}
|
|
260
|
+
className={cx(
|
|
261
|
+
slots.content,
|
|
262
|
+
contentCss ? css(contentCss) : undefined,
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
<RACListBox
|
|
266
|
+
aria-label={intl.formatMessage(uiMessage("ui.combobox-listbox"))}
|
|
267
|
+
className={slots.list}
|
|
268
|
+
renderEmptyState={
|
|
269
|
+
emptyState
|
|
270
|
+
? () => <div className={slots.empty}>{emptyState}</div>
|
|
271
|
+
: undefined
|
|
272
|
+
}
|
|
215
273
|
>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
renderEmptyState={
|
|
220
|
-
emptyState
|
|
221
|
-
? () => <div className={slots.empty}>{emptyState}</div>
|
|
222
|
-
: undefined
|
|
223
|
-
}
|
|
224
|
-
>
|
|
225
|
-
{children}
|
|
226
|
-
</RACListBox>
|
|
227
|
-
</Popover>
|
|
228
|
-
)}
|
|
274
|
+
{children}
|
|
275
|
+
</RACListBox>
|
|
276
|
+
</Popover>
|
|
229
277
|
<FieldSupport
|
|
230
278
|
helperText={helperText}
|
|
231
279
|
errorMessage={errorMessage}
|
package/src/ConfirmDialog.tsx
CHANGED
package/src/Modal.tsx
CHANGED
|
@@ -173,7 +173,7 @@ export interface ModalOwnProps {
|
|
|
173
173
|
* Element to focus when the dialog closes, instead of the element that was
|
|
174
174
|
* focused when it opened.
|
|
175
175
|
*/
|
|
176
|
-
finalFocusRef?: RefObject<HTMLElement>;
|
|
176
|
+
finalFocusRef?: RefObject<HTMLElement | null>;
|
|
177
177
|
/**
|
|
178
178
|
* Accessible name for dialogs without a ModalHeader (which otherwise
|
|
179
179
|
* provides the label). One of the two is required — react-aria warns in
|
package/src/Select.recipe.ts
CHANGED
|
@@ -127,6 +127,9 @@ export const select = defineSlotRecipe({
|
|
|
127
127
|
background: "transparent",
|
|
128
128
|
border: "none",
|
|
129
129
|
cursor: "pointer",
|
|
130
|
+
// Disabled only while ComboBox's isPopoverHidden withholds the list:
|
|
131
|
+
// it should stop looking clickable, not look like a disabled field.
|
|
132
|
+
"&[data-disabled]": { cursor: "default" },
|
|
130
133
|
// No focus styling, deliberately: react-aria keeps a ComboBox's toggle
|
|
131
134
|
// button out of the tab order (the input owns the keyboard), so a ring
|
|
132
135
|
// here would only ever be reachable programmatically, and would suggest
|
package/src/base-preset.ts
CHANGED
|
@@ -170,6 +170,20 @@ export const basePreset = definePreset({
|
|
|
170
170
|
// Removing the slot is a follow-up needing an ml-trainer lockstep.
|
|
171
171
|
brand: colors.blue,
|
|
172
172
|
brand2: colors.gray,
|
|
173
|
+
// Syntax-highlighting palette, read as CSS variables by the Python
|
|
174
|
+
// Editor's CodeMirror theme and by @microbit/ui-code-view's stylesheet
|
|
175
|
+
// (whose var() fallbacks repeat these values for apps without Panda).
|
|
176
|
+
// Generic OSS defaults; brand presets set the micro:bit palette.
|
|
177
|
+
code: {
|
|
178
|
+
default: { value: "black" },
|
|
179
|
+
comment: { value: "gray" },
|
|
180
|
+
keyword: { value: "darkblue" },
|
|
181
|
+
literal: { value: "darkgreen" },
|
|
182
|
+
string: { value: "green" },
|
|
183
|
+
// Below the 4.5:1 text floor (gray.500 meets it); matches the
|
|
184
|
+
// editor's gutter pending a decision on line-number contrast.
|
|
185
|
+
lineNumber: { value: "{colors.gray.350}" },
|
|
186
|
+
},
|
|
173
187
|
},
|
|
174
188
|
spacing: {
|
|
175
189
|
...spacing,
|
|
@@ -14,7 +14,9 @@ export function useClipboard(
|
|
|
14
14
|
timeoutMs = 1500,
|
|
15
15
|
): { onCopy: () => void; hasCopied: boolean } {
|
|
16
16
|
const [hasCopied, setHasCopied] = useState(false);
|
|
17
|
-
const timeoutRef = useRef<ReturnType<typeof setTimeout
|
|
17
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
18
|
+
undefined,
|
|
19
|
+
);
|
|
18
20
|
const markCopied = useCallback(() => {
|
|
19
21
|
setHasCopied(true);
|
|
20
22
|
clearTimeout(timeoutRef.current);
|
package/src/hooks/usePrevious.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { useEffect, useRef } from "react";
|
|
|
7
7
|
|
|
8
8
|
/** The value from the previous render (undefined on the first render). */
|
|
9
9
|
export function usePrevious<T>(value: T): T | undefined {
|
|
10
|
-
const ref = useRef<T>();
|
|
10
|
+
const ref = useRef<T | undefined>(undefined);
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
ref.current = value;
|
|
13
13
|
});
|
package/src/system.ts
CHANGED
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Stable re-export of the Panda
|
|
7
|
+
* Stable re-export of the Panda layout patterns and runtime helpers, so
|
|
8
8
|
* shared-ui consumers (and a future extracted library) import from one place
|
|
9
9
|
* rather than reaching into the generated `styled-system` directly.
|
|
10
|
+
*
|
|
11
|
+
* `css`, `cva`, `sva`, `cx` and `styled` are deliberately NOT re-exported:
|
|
12
|
+
* Panda extracts those only when they are imported from `styled-system`, so
|
|
13
|
+
* calls reaching one through here would produce no CSS and no diagnostic.
|
|
14
|
+
* (`cx` only joins strings, but it shares the import line with `css`.)
|
|
15
|
+
* Everything this module does export works wherever it is used: the pattern
|
|
16
|
+
* components extract by name, and `token` is a runtime variable lookup with
|
|
17
|
+
* nothing to extract.
|
|
10
18
|
*/
|
|
11
|
-
export { css, cva, sva, cx } from "styled-system/css";
|
|
12
19
|
export { token } from "styled-system/tokens";
|
|
13
20
|
export type { SystemStyleObject } from "styled-system/types";
|
|
14
21
|
// react-aria collection types call sites need for selection handlers.
|
|
@@ -22,13 +29,10 @@ export type {
|
|
|
22
29
|
GridProps,
|
|
23
30
|
} from "styled-system/jsx";
|
|
24
31
|
|
|
25
|
-
// Layout patterns — Box/Flex/Stack/etc.
|
|
26
|
-
//
|
|
27
|
-
// `styled`
|
|
28
|
-
//
|
|
29
|
-
// the module it was imported from, so `<styled.table css={…}>` on a `styled`
|
|
30
|
-
// imported from here silently produces no CSS. Import it from
|
|
31
|
-
// "styled-system/jsx" for that.
|
|
32
|
+
// Layout patterns — Box/Flex/Stack/etc. These extract by component and prop
|
|
33
|
+
// name, so their style props work through a re-export. `styled` does not:
|
|
34
|
+
// `styled(Component)` would be safe but `<styled.table css={…}>` silently
|
|
35
|
+
// produces no CSS, so it stays in "styled-system/jsx" with the helpers.
|
|
32
36
|
export {
|
|
33
37
|
AspectRatio,
|
|
34
38
|
Box,
|
|
@@ -41,7 +45,6 @@ export {
|
|
|
41
45
|
GridItem,
|
|
42
46
|
Center,
|
|
43
47
|
Wrap,
|
|
44
|
-
styled,
|
|
45
48
|
} from "styled-system/jsx";
|
|
46
49
|
|
|
47
50
|
/**
|