@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 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. Set `eject: true` (the stack
29
- supplies the full token system). After changing an _external_ preset
30
- dependency, regenerate clean: `rm -rf styled-system && npm run panda` —
31
- incremental codegen does not detect external preset changes.
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 or xterm themes written as raw CSS. Two rules
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`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "ui.breadcrumb": {
3
- "defaultMessage": "Sendero de navegación"
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": "Reducir {fieldLabel}"
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": "Seleccionar un artículo"
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.5.1",
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": "^1.11.4",
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": "^1.11.4",
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": "^26.1.1",
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": "^29.1.1",
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": "^6.6.8",
61
+ "react-intl": "^12.1.5",
61
62
  "typescript": "^5.4.2",
62
63
  "vite": "^7.3.6",
63
- "vitest": "^4.1.10"
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 until this prop is true. For gating on a minimum
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
- {!isPopoverHidden && (
206
- <Popover
207
- triggerRef={triggerRef}
208
- placement={placement}
209
- maxHeight={maxHeight}
210
- style={triggerWidth ? { width: triggerWidth } : undefined}
211
- className={cx(
212
- slots.content,
213
- contentCss ? css(contentCss) : undefined,
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
- <RACListBox
217
- aria-label={intl.formatMessage(uiMessage("ui.combobox-listbox"))}
218
- className={slots.list}
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}
@@ -22,7 +22,7 @@ export interface ConfirmDialogProps {
22
22
  confirmText: ReactNode;
23
23
  /** Defaults to "Cancel". */
24
24
  cancelText?: ReactNode;
25
- finalFocusRef?: RefObject<HTMLElement>;
25
+ finalFocusRef?: RefObject<HTMLElement | null>;
26
26
  onCloseComplete?: () => void;
27
27
  }
28
28
 
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
@@ -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
@@ -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);
@@ -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 styling primitives and layout patterns, so
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` is re-exported for the `styled(Component)` form, which works from
28
- // anywhere. The `styled.tag` JSX form does NOT: Panda recognises the factory by
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
  /**