@microbit/ui 0.1.0-alpha.9 → 0.1.1

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.
Files changed (113) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +171 -20
  3. package/lang/ui.ar.json +62 -0
  4. package/lang/ui.ca.json +43 -3
  5. package/lang/ui.cy.json +62 -0
  6. package/lang/ui.de.json +44 -4
  7. package/lang/ui.en-us.json +40 -0
  8. package/lang/ui.en.json +40 -0
  9. package/lang/ui.es-es.json +43 -3
  10. package/lang/ui.fr.json +44 -4
  11. package/lang/ui.ga-ie.json +43 -3
  12. package/lang/ui.it.json +62 -0
  13. package/lang/ui.ja.json +44 -4
  14. package/lang/ui.ko.json +44 -4
  15. package/lang/ui.lo.json +62 -0
  16. package/lang/ui.lol.json +40 -0
  17. package/lang/ui.nl.json +44 -4
  18. package/lang/ui.pl.json +44 -4
  19. package/lang/ui.pt-br.json +44 -4
  20. package/lang/ui.vi.json +62 -0
  21. package/lang/ui.zh-cn.json +44 -4
  22. package/lang/ui.zh-tw.json +44 -4
  23. package/package.json +13 -11
  24. package/src/Avatar.recipe.ts +191 -0
  25. package/src/Avatar.tsx +278 -0
  26. package/src/Breadcrumb.recipe.ts +45 -0
  27. package/src/Breadcrumb.tsx +114 -0
  28. package/src/Button.recipe.ts +88 -50
  29. package/src/Button.tsx +67 -14
  30. package/src/ButtonGroup.tsx +37 -15
  31. package/src/Card.recipe.ts +1 -2
  32. package/src/Card.tsx +2 -1
  33. package/src/Checkbox.recipe.ts +13 -12
  34. package/src/Checkbox.tsx +103 -34
  35. package/src/CheckboxGroup.tsx +70 -0
  36. package/src/CloseButton.tsx +3 -3
  37. package/src/CloseIcon.tsx +6 -4
  38. package/src/Code.tsx +1 -1
  39. package/src/Collapse.tsx +13 -14
  40. package/src/ComboBox.tsx +246 -0
  41. package/src/Divider.tsx +40 -7
  42. package/src/Drawer.recipe.ts +21 -10
  43. package/src/Drawer.tsx +3 -4
  44. package/src/ExternalLink.tsx +43 -0
  45. package/src/Fade.tsx +18 -4
  46. package/src/Field.recipe.ts +114 -0
  47. package/src/Field.tsx +187 -0
  48. package/src/GridList.recipe.ts +57 -0
  49. package/src/GridList.tsx +81 -0
  50. package/src/Heading.recipe.ts +20 -1
  51. package/src/Heading.tsx +3 -3
  52. package/src/Icon.tsx +23 -7
  53. package/src/IconButton.tsx +8 -18
  54. package/src/Image.tsx +1 -1
  55. package/src/Input.recipe.ts +30 -26
  56. package/src/Input.tsx +16 -7
  57. package/src/InputGroup.tsx +9 -9
  58. package/src/Kbd.tsx +1 -1
  59. package/src/Link.tsx +3 -3
  60. package/src/LinkBox.tsx +2 -3
  61. package/src/LinkButton.tsx +14 -13
  62. package/src/List.tsx +5 -7
  63. package/src/ListBox.recipe.ts +43 -0
  64. package/src/ListBox.tsx +88 -0
  65. package/src/Menu.recipe.ts +22 -19
  66. package/src/Menu.tsx +54 -28
  67. package/src/Modal.recipe.ts +17 -9
  68. package/src/Modal.tsx +123 -24
  69. package/src/MoreMenuButton.tsx +63 -0
  70. package/src/NativeSelect.tsx +37 -14
  71. package/src/NativeSelectField.tsx +84 -0
  72. package/src/NumberField.recipe.ts +56 -15
  73. package/src/NumberField.tsx +74 -22
  74. package/src/PopoverArrow.tsx +1 -2
  75. package/src/ProgressBar.tsx +3 -5
  76. package/src/Radio.recipe.ts +13 -12
  77. package/src/Radio.tsx +2 -29
  78. package/src/RadioGroup.tsx +68 -0
  79. package/src/Select.recipe.ts +211 -0
  80. package/src/Select.tsx +194 -0
  81. package/src/SharedUIProvider.tsx +63 -7
  82. package/src/Skeleton.tsx +146 -0
  83. package/src/Slide.tsx +2 -2
  84. package/src/Slider.recipe.ts +27 -16
  85. package/src/Slider.tsx +20 -12
  86. package/src/Spinner.tsx +11 -9
  87. package/src/Svg.tsx +2 -3
  88. package/src/Switch.recipe.ts +40 -21
  89. package/src/Switch.tsx +35 -5
  90. package/src/Text.recipe.ts +26 -0
  91. package/src/Text.tsx +6 -2
  92. package/src/TextField.tsx +35 -38
  93. package/src/Toast.recipe.ts +52 -15
  94. package/src/Toast.tsx +147 -48
  95. package/src/Tooltip.recipe.ts +32 -0
  96. package/src/Tooltip.tsx +68 -28
  97. package/src/TooltipButton.tsx +229 -0
  98. package/src/UnmountCallback.tsx +2 -2
  99. package/src/VisuallyHidden.tsx +1 -1
  100. package/src/base-preset.ts +323 -84
  101. package/src/base-tokens.ts +0 -3
  102. package/src/button-icon.ts +4 -5
  103. package/src/data-attrs.ts +16 -0
  104. package/src/dense-preset.ts +107 -0
  105. package/src/hooks/useBreakpointValue.ts +4 -4
  106. package/src/hooks/useClipboard.ts +5 -6
  107. package/src/hooks/useDisclosure.ts +31 -0
  108. package/src/hooks/useMediaQuery.ts +3 -4
  109. package/src/hooks/usePrevious.ts +1 -4
  110. package/src/index.ts +19 -3
  111. package/src/rac-locale.ts +33 -0
  112. package/src/system.ts +22 -1
  113. package/src/TextField.recipe.ts +0 -54
@@ -0,0 +1,107 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { definePreset } from "@pandacss/dev";
7
+
8
+ const toTokens = (values: Record<string, string>) =>
9
+ Object.fromEntries(
10
+ Object.entries(values).map(([k, value]) => [k, { value }]),
11
+ ) as Record<string, { value: string }>;
12
+
13
+ /**
14
+ * The numeric spacing/size grid, the base 0.25rem step × 0.88.
15
+ *
16
+ * Only the numeric scale is touched: the named `sizes` (`xs`…`8xl`, `max`,
17
+ * `full`, `container.*`) stay at their base-preset values.
18
+ */
19
+ const scale = toTokens({
20
+ px: "1px",
21
+ 0.5: "0.11rem",
22
+ 1: "0.22rem",
23
+ 1.5: "0.33rem",
24
+ 2: "0.44rem",
25
+ 2.5: "0.55rem",
26
+ 3: "0.66rem",
27
+ 3.5: "0.77rem",
28
+ 4: "0.88rem",
29
+ 5: "1.1rem",
30
+ 6: "1.32rem",
31
+ 7: "1.54rem",
32
+ 8: "1.76rem",
33
+ 9: "1.98rem",
34
+ 10: "2.2rem",
35
+ 12: "2.64rem",
36
+ 14: "3.08rem",
37
+ 16: "3.52rem",
38
+ 20: "4.4rem",
39
+ 24: "5.28rem",
40
+ 28: "6.16rem",
41
+ 32: "7.04rem",
42
+ 36: "7.92rem",
43
+ 40: "8.8rem",
44
+ 44: "9.68rem",
45
+ 48: "10.56rem",
46
+ 52: "11.44rem",
47
+ 56: "12.32rem",
48
+ 60: "13.2rem",
49
+ 64: "14.08rem",
50
+ 72: "15.84rem",
51
+ 80: "17.6rem",
52
+ 96: "21.12rem",
53
+ });
54
+
55
+ /**
56
+ * Font sizes from `md` up, × 0.9. `xs`/`sm` keep their full size so small
57
+ * text never gets too small, and `3xs`/`2xs` (which neither app's theme
58
+ * listed) stay at their base-preset values.
59
+ */
60
+ const denseFontSizes = toTokens({
61
+ md: "0.9rem",
62
+ lg: "1.012rem",
63
+ xl: "1.125rem",
64
+ "2xl": "1.35rem",
65
+ "3xl": "1.687rem",
66
+ "4xl": "2.025rem",
67
+ "5xl": "2.7rem",
68
+ "6xl": "3.375rem",
69
+ "7xl": "4.05rem",
70
+ "8xl": "5.4rem",
71
+ "9xl": "7.2rem",
72
+ });
73
+
74
+ /**
75
+ * The dense preset — an optional density override for the information-dense
76
+ * apps in the family. Stacks between the base preset and the app preset:
77
+ *
78
+ * ```ts
79
+ * presets: ["@pandacss/preset-base", basePreset, densePreset, appPreset]
80
+ * ```
81
+ *
82
+ * Both python-editor and classroom shipped the same "make everything
83
+ * smaller" theme change (2022): the numeric spacing/sizes grid at
84
+ * × 0.88 and `fontSizes` from `md` up at × 0.9. The two apps' values were
85
+ * byte-identical, so the scale lives here rather than being replicated in
86
+ * each app preset — a global scale override hides from every safeguard,
87
+ * so it needs to be explicit and shared.
88
+ *
89
+ * Whether this density stays or the family aligns on one scale is an open
90
+ * design question; when it is answered, this preset is the single place the
91
+ * answer lands (deleting it from an app's stack un-shrinks that app).
92
+ * ml-trainer and data-microbit-org do not use it.
93
+ */
94
+ export const densePreset = definePreset({
95
+ name: "microbit-ui-dense",
96
+ theme: {
97
+ extend: {
98
+ tokens: {
99
+ spacing: scale,
100
+ sizes: scale,
101
+ fontSizes: denseFontSizes,
102
+ },
103
+ },
104
+ },
105
+ });
106
+
107
+ export default densePreset;
@@ -31,10 +31,10 @@ const activeBreakpoint = (): Breakpoint => {
31
31
  };
32
32
 
33
33
  /**
34
- * JS-side responsive value resolver, replacing Chakra's `useBreakpointValue`.
35
- * Panda handles responsive *styles* in CSS; this is for responsive *logic*
36
- * (choosing a prop value, branching behaviour). Resolves to the value at the
37
- * active breakpoint, falling back to the nearest smaller one that is defined.
34
+ * JS-side responsive value resolver. Panda handles responsive *styles* in
35
+ * CSS; this is for responsive *logic* (choosing a prop value, branching
36
+ * behaviour). Resolves to the value at the active breakpoint, falling back to
37
+ * the nearest smaller one that is defined.
38
38
  */
39
39
  export function useBreakpointValue<T>(
40
40
  values: Partial<Record<Breakpoint, T>>,
@@ -7,7 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
7
7
 
8
8
  /**
9
9
  * Copy a value to the clipboard with a transient `hasCopied` flag for
10
- * "Copied!" feedback. Replaces Chakra's `useClipboard`.
10
+ * "Copied!" feedback.
11
11
  */
12
12
  export function useClipboard(
13
13
  value: string,
@@ -34,11 +34,10 @@ export function useClipboard(
34
34
  }
35
35
 
36
36
  /**
37
- * Chakra's useClipboard wrapped the execCommand-based copy-to-clipboard
38
- * package, which works on a user gesture in contexts the async API refuses:
39
- * insecure (non-localhost http) origins, where `navigator.clipboard` is
40
- * undefined, and frames without a clipboard-write permissions policy, where
41
- * `writeText` rejects.
37
+ * execCommand-based fallback copy. Works on a user gesture in contexts the
38
+ * async API refuses: insecure (non-localhost http) origins, where
39
+ * `navigator.clipboard` is undefined, and frames without a clipboard-write
40
+ * permissions policy, where `writeText` rejects.
42
41
  */
43
42
  function execCommandCopy(value: string): boolean {
44
43
  const active = document.activeElement;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { useCallback, useMemo, useState } from "react";
7
+
8
+ export interface Disclosure {
9
+ isOpen: boolean;
10
+ onOpen: () => void;
11
+ onClose: () => void;
12
+ onToggle: () => void;
13
+ }
14
+
15
+ /**
16
+ * useDisclosure — the open/closed state of a dialog, menu or drawer, and the
17
+ * three functions that change it.
18
+ *
19
+ * A thin `useState` wrapper; the stable object means a disclosure can be
20
+ * passed to a memoised child without re-rendering it.
21
+ */
22
+ export const useDisclosure = (defaultIsOpen = false): Disclosure => {
23
+ const [isOpen, setIsOpen] = useState(defaultIsOpen);
24
+ const onOpen = useCallback(() => setIsOpen(true), []);
25
+ const onClose = useCallback(() => setIsOpen(false), []);
26
+ const onToggle = useCallback(() => setIsOpen((open) => !open), []);
27
+ return useMemo(
28
+ () => ({ isOpen, onOpen, onClose, onToggle }),
29
+ [isOpen, onOpen, onClose, onToggle],
30
+ );
31
+ };
@@ -6,10 +6,9 @@
6
6
  import { useCallback, useSyncExternalStore } from "react";
7
7
 
8
8
  /**
9
- * Tracks a raw CSS media query, replacing Chakra's `useMediaQuery` for
10
- * queries that aren't breakpoint-based (custom widths, height-based
11
- * queries). For the preset's breakpoint scale prefer `useBreakpointValue`.
12
- * Returns false during SSR.
9
+ * Tracks a raw CSS media query, for queries that aren't breakpoint-based
10
+ * (custom widths, height-based queries). For the preset's breakpoint scale
11
+ * prefer `useBreakpointValue`. Returns false during SSR.
13
12
  */
14
13
  export function useMediaQuery(query: string): boolean {
15
14
  const subscribe = useCallback(
@@ -5,10 +5,7 @@
5
5
  */
6
6
  import { useEffect, useRef } from "react";
7
7
 
8
- /**
9
- * The value from the previous render (undefined on the first render).
10
- * Replaces Chakra's `usePrevious`.
11
- */
8
+ /** The value from the previous render (undefined on the first render). */
12
9
  export function usePrevious<T>(value: T): T | undefined {
13
10
  const ref = useRef<T>();
14
11
  useEffect(() => {
package/src/index.ts CHANGED
@@ -4,27 +4,36 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  /**
7
- * shared-ui — react-aria-components + Panda CSS primitives replacing Chakra,
8
- * designed to be extracted into a library shared across sibling apps. Visuals
9
- * match the Chakra theme; behaviour follows react-aria patterns.
7
+ * shared-ui — react-aria-components + Panda CSS primitives shared across the
8
+ * micro:bit app family. Behaviour follows react-aria patterns; styling comes
9
+ * from the recipe/token system in the base preset.
10
10
  */
11
11
  export * from "./system";
12
+ export * from "./Avatar";
13
+ export * from "./Breadcrumb";
12
14
  export * from "./Button";
13
15
  export * from "./LinkButton";
14
16
  export * from "./ButtonGroup";
15
17
  export * from "./Card";
16
18
  export * from "./Checkbox";
19
+ export * from "./CheckboxGroup";
20
+ export * from "./Field";
17
21
  export * from "./IconButton";
22
+ export * from "./MoreMenuButton";
18
23
  export * from "./Image";
19
24
  export * from "./Input";
20
25
  export * from "./InputGroup";
21
26
  export * from "./LinkBox";
22
27
  export * from "./NativeSelect";
28
+ export * from "./NativeSelectField";
23
29
  export * from "./NumberField";
24
30
  export * from "./ProgressBar";
25
31
  export * from "./Radio";
32
+ export * from "./RadioGroup";
33
+ export * from "./Skeleton";
26
34
  export * from "./Slide";
27
35
  export * from "./Slider";
36
+ export * from "./Select";
28
37
  export * from "./Spinner";
29
38
  export * from "./Svg";
30
39
  export * from "./Switch";
@@ -32,6 +41,7 @@ export * from "./Text";
32
41
  export * from "./TextField";
33
42
  export * from "./Heading";
34
43
  export * from "./Link";
44
+ export * from "./ExternalLink";
35
45
  export * from "./Icon";
36
46
  export * from "./CloseButton";
37
47
  export * from "./CloseIcon";
@@ -40,16 +50,22 @@ export * from "./Collapse";
40
50
  export * from "./Fade";
41
51
  export * from "./Kbd";
42
52
  export * from "./Divider";
53
+ export * from "./GridList";
43
54
  export * from "./Drawer";
44
55
  export * from "./List";
56
+ export * from "./ListBox";
57
+ export * from "./ComboBox";
45
58
  export * from "./Menu";
46
59
  export * from "./Modal";
47
60
  export * from "./PopoverArrow";
48
61
  export * from "./SharedUIProvider";
49
62
  export * from "./Tooltip";
63
+ export * from "./TooltipButton";
50
64
  export * from "./Toast";
51
65
  export * from "./VisuallyHidden";
52
66
  export { useBreakpointValue } from "./hooks/useBreakpointValue";
53
67
  export { useClipboard } from "./hooks/useClipboard";
68
+ export { useDisclosure } from "./hooks/useDisclosure";
69
+ export type { Disclosure } from "./hooks/useDisclosure";
54
70
  export { useMediaQuery } from "./hooks/useMediaQuery";
55
71
  export { usePrevious } from "./hooks/usePrevious";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+
7
+ /**
8
+ * Sanitizes one of our locale ids for react-aria's I18nProvider.
9
+ *
10
+ * No mapping is needed to reach react-aria's own strings: its fallback chain
11
+ * runs exact tag, then the bare language, then any variant of that language,
12
+ * then en-US — so `fr` finds fr-FR, `ja` finds ja-JP, and the locales it has no
13
+ * strings for at all (ca, cy, ga-IE, and our `lol` pseudo-locale) land on
14
+ * English.
15
+ *
16
+ * What does need handling is a malformed tag: I18nProvider's explicit-locale
17
+ * path feeds it straight to `new Intl.Locale` with no guard of its own (unlike
18
+ * its browser-locale path), so a bad tag throws during render and takes out the
19
+ * tree. Only reachable through SharedUIProvider's `locale` prop in practice —
20
+ * react-intl rejects a malformed locale before we could read it from context.
21
+ */
22
+ export const racLocale = (locale: string | undefined): string | undefined => {
23
+ if (locale === undefined) {
24
+ // Leave react-aria on its browser-locale default.
25
+ return undefined;
26
+ }
27
+ try {
28
+ Intl.DateTimeFormat.supportedLocalesOf([locale]);
29
+ } catch {
30
+ return "en-GB";
31
+ }
32
+ return locale;
33
+ };
package/src/system.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  export { css, cva, sva, cx } from "styled-system/css";
12
12
  export { token } from "styled-system/tokens";
13
13
  export type { SystemStyleObject } from "styled-system/types";
14
+ // react-aria collection types call sites need for selection handlers.
15
+ export type { Key, Selection } from "react-aria-components";
14
16
  export type {
15
17
  BoxProps,
16
18
  FlexProps,
@@ -20,10 +22,17 @@ export type {
20
22
  GridProps,
21
23
  } from "styled-system/jsx";
22
24
 
23
- // Layout patterns — the Panda-native equivalents of Chakra's Box/Flex/Stack/etc.
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.
24
32
  export {
25
33
  AspectRatio,
26
34
  Box,
35
+ Container,
27
36
  Flex,
28
37
  Stack,
29
38
  HStack,
@@ -34,3 +43,15 @@ export {
34
43
  Wrap,
35
44
  styled,
36
45
  } from "styled-system/jsx";
46
+
47
+ /**
48
+ * Spread onto a surface element that is dark by design — a black toolbar,
49
+ * a coloured sidebar header — so focus indicators inside it flip to their
50
+ * on-dark colours: `<header {...darkSurface}>`.
51
+ *
52
+ * Tag a surface's own designed luminance, never anything theme-relative
53
+ * (a future dark mode flips untagged defaults via token conditions, not
54
+ * markup). One inherited data attribute (the `onDark` condition): tag the
55
+ * bar, cover its controls; portalled overlays escape with the DOM.
56
+ */
57
+ export const darkSurface = { "data-surface": "dark" } as const;
@@ -1,54 +0,0 @@
1
- /**
2
- * (c) 2026, Micro:bit Educational Foundation and contributors
3
- *
4
- * SPDX-License-Identifier: MIT
5
- */
6
- import { defineSlotRecipe } from "@pandacss/dev";
7
-
8
- /**
9
- * Field slot recipe — Chakra's FormControl parts (FormLabel/FormHelperText/
10
- * FormErrorMessage, light mode). Consumed by the shared-ui TextField, which
11
- * maps the slots onto react-aria-components' TextField/Label/Text/FieldError;
12
- * the input itself is styled by the `input` recipe (Input.recipe.ts).
13
- *
14
- * Registered in the base preset (base-preset.ts). No variants, so it
15
- * needs no `staticCss` entry.
16
- */
17
- export const field = defineSlotRecipe({
18
- className: "field",
19
- slots: ["root", "label", "requiredIndicator", "helperText", "errorMessage"],
20
- base: {
21
- root: {
22
- display: "flex",
23
- flexDirection: "column",
24
- alignItems: "stretch",
25
- width: "100%",
26
- },
27
- label: {
28
- display: "block",
29
- fontSize: "md",
30
- fontWeight: "medium",
31
- marginEnd: "3",
32
- mb: "2",
33
- "&[data-disabled]": { opacity: 0.4 },
34
- },
35
- requiredIndicator: {
36
- marginStart: "1",
37
- color: "danger.500",
38
- },
39
- helperText: {
40
- mt: "2",
41
- fontSize: "sm",
42
- lineHeight: "normal",
43
- color: "gray.600",
44
- },
45
- errorMessage: {
46
- display: "flex",
47
- alignItems: "center",
48
- mt: "2",
49
- fontSize: "sm",
50
- lineHeight: "normal",
51
- color: "danger.500",
52
- },
53
- },
54
- });