@microbit/ui 0.0.0-gray.ramp.90

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 (102) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +222 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.cy.json +22 -0
  5. package/lang/ui.de.json +22 -0
  6. package/lang/ui.en-us.json +22 -0
  7. package/lang/ui.en.json +22 -0
  8. package/lang/ui.es-es.json +22 -0
  9. package/lang/ui.fr.json +22 -0
  10. package/lang/ui.ga-ie.json +22 -0
  11. package/lang/ui.it.json +22 -0
  12. package/lang/ui.ja.json +22 -0
  13. package/lang/ui.ko.json +22 -0
  14. package/lang/ui.lol.json +22 -0
  15. package/lang/ui.nl.json +22 -0
  16. package/lang/ui.pl.json +22 -0
  17. package/lang/ui.pt-br.json +22 -0
  18. package/lang/ui.zh-cn.json +22 -0
  19. package/lang/ui.zh-tw.json +22 -0
  20. package/package.json +66 -0
  21. package/postcss-legacy-safari.cjs +96 -0
  22. package/reset.css +35 -0
  23. package/src/Avatar.recipe.ts +168 -0
  24. package/src/Avatar.tsx +276 -0
  25. package/src/Button.recipe.ts +192 -0
  26. package/src/Button.tsx +69 -0
  27. package/src/ButtonGroup.tsx +70 -0
  28. package/src/Card.recipe.ts +49 -0
  29. package/src/Card.tsx +56 -0
  30. package/src/Checkbox.recipe.ts +111 -0
  31. package/src/Checkbox.tsx +106 -0
  32. package/src/CloseButton.tsx +87 -0
  33. package/src/CloseIcon.tsx +40 -0
  34. package/src/Code.tsx +20 -0
  35. package/src/Collapse.tsx +180 -0
  36. package/src/ComboBox.tsx +192 -0
  37. package/src/Divider.tsx +85 -0
  38. package/src/Drawer.recipe.ts +98 -0
  39. package/src/Drawer.tsx +138 -0
  40. package/src/Fade.tsx +48 -0
  41. package/src/GridList.recipe.ts +58 -0
  42. package/src/GridList.tsx +81 -0
  43. package/src/Heading.recipe.ts +52 -0
  44. package/src/Heading.tsx +14 -0
  45. package/src/Icon.tsx +75 -0
  46. package/src/IconButton.tsx +44 -0
  47. package/src/Image.tsx +11 -0
  48. package/src/Input.recipe.ts +75 -0
  49. package/src/Input.tsx +44 -0
  50. package/src/InputGroup.tsx +62 -0
  51. package/src/Kbd.tsx +26 -0
  52. package/src/Link.tsx +23 -0
  53. package/src/LinkBox.tsx +88 -0
  54. package/src/LinkButton.tsx +80 -0
  55. package/src/List.tsx +31 -0
  56. package/src/ListBox.recipe.ts +43 -0
  57. package/src/ListBox.tsx +88 -0
  58. package/src/Menu.recipe.ts +124 -0
  59. package/src/Menu.tsx +291 -0
  60. package/src/Modal.recipe.ts +163 -0
  61. package/src/Modal.tsx +377 -0
  62. package/src/NativeSelect.tsx +94 -0
  63. package/src/NumberField.recipe.ts +67 -0
  64. package/src/NumberField.tsx +86 -0
  65. package/src/PopoverArrow.tsx +67 -0
  66. package/src/ProgressBar.tsx +61 -0
  67. package/src/Radio.recipe.ts +112 -0
  68. package/src/Radio.tsx +89 -0
  69. package/src/Select.recipe.ts +210 -0
  70. package/src/Select.tsx +153 -0
  71. package/src/SharedUIProvider.tsx +55 -0
  72. package/src/Skeleton.tsx +146 -0
  73. package/src/Slide.tsx +54 -0
  74. package/src/Slider.recipe.ts +102 -0
  75. package/src/Slider.tsx +163 -0
  76. package/src/Spinner.tsx +74 -0
  77. package/src/Svg.tsx +23 -0
  78. package/src/Switch.recipe.ts +107 -0
  79. package/src/Switch.tsx +62 -0
  80. package/src/Text.recipe.ts +29 -0
  81. package/src/Text.tsx +16 -0
  82. package/src/TextField.recipe.ts +54 -0
  83. package/src/TextField.tsx +91 -0
  84. package/src/Toast.recipe.ts +98 -0
  85. package/src/Toast.tsx +173 -0
  86. package/src/Tooltip.recipe.ts +37 -0
  87. package/src/Tooltip.tsx +76 -0
  88. package/src/UnmountCallback.tsx +18 -0
  89. package/src/VisuallyHidden.tsx +14 -0
  90. package/src/base-preset.ts +371 -0
  91. package/src/base-tokens.ts +1017 -0
  92. package/src/button-icon.ts +23 -0
  93. package/src/data-attrs.ts +16 -0
  94. package/src/dense-preset.ts +108 -0
  95. package/src/hooks/useBreakpointValue.ts +63 -0
  96. package/src/hooks/useClipboard.ts +64 -0
  97. package/src/hooks/useDisclosure.ts +32 -0
  98. package/src/hooks/useMediaQuery.ts +28 -0
  99. package/src/hooks/usePrevious.ts +18 -0
  100. package/src/index.ts +63 -0
  101. package/src/messages.ts +17 -0
  102. package/src/system.ts +45 -0
package/src/Menu.tsx ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode, useCallback, useState } from "react";
7
+ import {
8
+ Header as RACHeader,
9
+ Menu as RACMenu,
10
+ MenuItem as RACMenuItem,
11
+ MenuItemProps as RACMenuItemProps,
12
+ MenuSection as RACMenuSection,
13
+ MenuTrigger as RACMenuTrigger,
14
+ Popover,
15
+ PopoverProps,
16
+ Separator,
17
+ } from "react-aria-components";
18
+ import { RiCheckLine } from "react-icons/ri";
19
+ import { css, cx } from "styled-system/css";
20
+ import { menu } from "styled-system/recipes";
21
+ import { SystemStyleObject } from "styled-system/types";
22
+ import { Icon } from "./Icon";
23
+ import { useOverlayCloseRegistrar } from "./SharedUIProvider";
24
+
25
+ export interface MenuTriggerProps {
26
+ /** Called when the menu opens. */
27
+ onOpen?: () => void;
28
+ /** Called when the menu closes. */
29
+ onClose?: () => void;
30
+ /** The trigger element followed by a `MenuList` (react-aria pattern). */
31
+ children: ReactNode;
32
+ }
33
+
34
+ /**
35
+ * MenuTrigger — react-aria-components' <MenuTrigger>. Its first child is the
36
+ * trigger (e.g. a Button); the second is a `MenuList`. RAC returns focus to the
37
+ * trigger when the menu closes.
38
+ *
39
+ * When the app installs an overlay-close registrar (e.g. so the Android back
40
+ * button can close the menu), the trigger runs controlled and registers its
41
+ * close function while open. Otherwise it's an uncontrolled pass-through.
42
+ */
43
+ export const MenuTrigger = ({
44
+ onOpen,
45
+ onClose,
46
+ children,
47
+ }: MenuTriggerProps) => {
48
+ const registerClose = useOverlayCloseRegistrar();
49
+ const [isOpen, setIsOpen] = useState(false);
50
+
51
+ const handleOpenChange = useCallback(
52
+ (open: boolean) => {
53
+ setIsOpen(open);
54
+ if (open) {
55
+ registerClose?.(() => {
56
+ setIsOpen(false);
57
+ registerClose(null);
58
+ });
59
+ onOpen?.();
60
+ } else {
61
+ registerClose?.(null);
62
+ onClose?.();
63
+ }
64
+ },
65
+ [registerClose, onOpen, onClose],
66
+ );
67
+
68
+ if (!registerClose) {
69
+ return (
70
+ <RACMenuTrigger
71
+ onOpenChange={(open) => (open ? onOpen?.() : onClose?.())}
72
+ >
73
+ {children}
74
+ </RACMenuTrigger>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <RACMenuTrigger isOpen={isOpen} onOpenChange={handleOpenChange}>
80
+ {children}
81
+ </RACMenuTrigger>
82
+ );
83
+ };
84
+
85
+ export interface MenuListProps {
86
+ children: ReactNode;
87
+ /** Placement of the dropdown relative to the trigger. */
88
+ placement?: PopoverProps["placement"];
89
+ /** Per-instance style overrides, merged after the recipe. */
90
+ css?: SystemStyleObject;
91
+ className?: string;
92
+ }
93
+
94
+ /**
95
+ * MenuList — the dropdown surface: RAC's <Popover> (positioned card) wrapping a
96
+ * RAC <Menu> (the list). Place `MenuItem`s inside.
97
+ */
98
+ export const MenuList = ({
99
+ children,
100
+ // Matches Chakra Menu's default "bottom-start".
101
+ placement = "bottom start",
102
+ css: cssProp,
103
+ className,
104
+ }: MenuListProps) => {
105
+ const slots = menu();
106
+ return (
107
+ <Popover
108
+ placement={placement}
109
+ className={cx(
110
+ slots.content,
111
+ cssProp ? css(cssProp) : undefined,
112
+ className,
113
+ )}
114
+ >
115
+ <RACMenu className={slots.list}>{children}</RACMenu>
116
+ </Popover>
117
+ );
118
+ };
119
+
120
+ export interface MenuItemProps
121
+ extends Omit<RACMenuItemProps, "className" | "children"> {
122
+ /** Icon rendered before the label, matching Chakra's MenuItem `icon`. */
123
+ icon?: ReactNode;
124
+ /** Per-instance style overrides, merged after the recipe. */
125
+ css?: SystemStyleObject;
126
+ className?: string;
127
+ children?: ReactNode;
128
+ }
129
+
130
+ /**
131
+ * MenuItem — a RAC <MenuItem> styled with the `menu` recipe's `item` slot.
132
+ * Use `onAction` for the click behaviour (RAC closes the menu automatically).
133
+ */
134
+ export const MenuItem = ({
135
+ icon,
136
+ css: cssProp,
137
+ className,
138
+ children,
139
+ ...rest
140
+ }: MenuItemProps) => {
141
+ const slots = menu();
142
+ return (
143
+ <RACMenuItem
144
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
145
+ {...rest}
146
+ >
147
+ {icon ? (
148
+ <>
149
+ <span className={slots.icon}>{icon}</span>
150
+ <span className={slots.label}>{children}</span>
151
+ </>
152
+ ) : (
153
+ children
154
+ )}
155
+ </RACMenuItem>
156
+ );
157
+ };
158
+
159
+ interface MenuOptionGroupBaseProps {
160
+ /** Group heading shown above the options (Chakra's `title`). */
161
+ title?: ReactNode;
162
+ /** `MenuItemOption` children. */
163
+ children: ReactNode;
164
+ css?: SystemStyleObject;
165
+ className?: string;
166
+ }
167
+
168
+ export interface MenuOptionGroupRadioProps extends MenuOptionGroupBaseProps {
169
+ type?: "radio";
170
+ /** The selected `MenuItemOption`'s value. */
171
+ value?: string;
172
+ /** Called with the newly selected option's value. */
173
+ onChange?: (value: string) => void;
174
+ }
175
+
176
+ export interface MenuOptionGroupCheckboxProps extends MenuOptionGroupBaseProps {
177
+ type: "checkbox";
178
+ /** The checked `MenuItemOption`s' values. */
179
+ value?: string[];
180
+ /** Called with the full set of checked values after a toggle. */
181
+ onChange?: (value: string[]) => void;
182
+ }
183
+
184
+ export type MenuOptionGroupProps =
185
+ | MenuOptionGroupRadioProps
186
+ | MenuOptionGroupCheckboxProps;
187
+
188
+ /**
189
+ * MenuOptionGroup — a group of checkable `MenuItemOption`s within a menu,
190
+ * replacing Chakra's `MenuOptionGroup`. `type="radio"` (the default) is
191
+ * single-select and renders `menuitemradio`; `type="checkbox"` is multi-select
192
+ * and renders `menuitemcheckbox`, each option toggling independently.
193
+ *
194
+ * Selection is section-scoped (RAC MenuSection), so a menu can mix action items
195
+ * and option groups. An option's own `onAction` still fires on every press,
196
+ * including the press that deselects it — so a lone toggle can be driven either
197
+ * by the group's `onChange` or by the item's `onAction`.
198
+ *
199
+ * Choosing an option leaves the menu open (a plain `MenuItem` closes it), which
200
+ * is what Chakra's checkbox groups did. Chakra's *radio* groups closed on
201
+ * select, so a ported radio group is a deliberate behaviour change.
202
+ */
203
+ export const MenuOptionGroup = (props: MenuOptionGroupProps) => {
204
+ const { title, children, css: cssProp, className } = props;
205
+ const slots = menu();
206
+ const selectedKeys =
207
+ props.type === "checkbox"
208
+ ? props.value ?? []
209
+ : props.value != null
210
+ ? [props.value]
211
+ : [];
212
+ return (
213
+ <RACMenuSection
214
+ className={cx(slots.group, cssProp ? css(cssProp) : undefined, className)}
215
+ selectionMode={props.type === "checkbox" ? "multiple" : "single"}
216
+ selectedKeys={selectedKeys}
217
+ onSelectionChange={(keys) => {
218
+ if (keys === "all") {
219
+ return;
220
+ }
221
+ const values = [...keys].map(String);
222
+ if (props.type === "checkbox") {
223
+ props.onChange?.(values);
224
+ } else if (values.length > 0) {
225
+ // Radio: a press that clears the selection reports nothing, matching
226
+ // Chakra, which had no way to express an empty radio group.
227
+ props.onChange?.(values[0]);
228
+ }
229
+ }}
230
+ >
231
+ {title != null && (
232
+ <RACHeader className={slots.groupTitle}>{title}</RACHeader>
233
+ )}
234
+ {children}
235
+ </RACMenuSection>
236
+ );
237
+ };
238
+
239
+ export interface MenuItemOptionProps
240
+ extends Omit<RACMenuItemProps, "className" | "children" | "id" | "value"> {
241
+ /** This option's value within its `MenuOptionGroup`. */
242
+ value: string;
243
+ css?: SystemStyleObject;
244
+ className?: string;
245
+ children?: ReactNode;
246
+ }
247
+
248
+ /**
249
+ * MenuItemOption — a selectable option inside a `MenuOptionGroup`, with a
250
+ * check indicator on the selected item (Chakra's MenuItemOption).
251
+ */
252
+ export const MenuItemOption = ({
253
+ value,
254
+ css: cssProp,
255
+ className,
256
+ children,
257
+ ...rest
258
+ }: MenuItemOptionProps) => {
259
+ const slots = menu();
260
+ return (
261
+ <RACMenuItem
262
+ id={value}
263
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
264
+ {...rest}
265
+ >
266
+ <span className={slots.itemIndicator} aria-hidden>
267
+ <Icon as={RiCheckLine} />
268
+ </span>
269
+ <span className={slots.label}>{children}</span>
270
+ </RACMenuItem>
271
+ );
272
+ };
273
+
274
+ export interface MenuDividerProps {
275
+ css?: SystemStyleObject;
276
+ className?: string;
277
+ }
278
+
279
+ /** Horizontal rule separating groups of menu items. */
280
+ export const MenuDivider = ({ css: cssProp, className }: MenuDividerProps) => {
281
+ const slots = menu();
282
+ return (
283
+ <Separator
284
+ className={cx(
285
+ slots.divider,
286
+ cssProp ? css(cssProp) : undefined,
287
+ className,
288
+ )}
289
+ />
290
+ );
291
+ };
@@ -0,0 +1,163 @@
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
+ * Dialog slot recipe — Chakra's default Modal parts (light mode), plus this
10
+ * app's overrides: a full-viewport overlay (the iOS WKWebView 100% fix) and a
11
+ * `full` size with safe-area insets and the brand status-bar gradient.
12
+ *
13
+ * A config recipe (rather than an atomic `sva`) so the `size` variant accepts
14
+ * responsive values, e.g. `{ base: "full", md: "4xl" }`. Consumed by the
15
+ * shared-ui Modal, which maps the slots onto react-aria-components'
16
+ * ModalOverlay / Modal / Dialog.
17
+ *
18
+ * Registered in the base preset (base-preset.ts).
19
+ */
20
+ // Appearance of a normal (non-full) modal box. Restated by every non-full size
21
+ // variant so that, when `size` is responsive (e.g. { base: "full", md: "4xl" }),
22
+ // the larger breakpoint fully overrides the `full` variant rather than leaking
23
+ // its margin:0 / border-radius:0 / full-height / gradient into desktop. Panda
24
+ // applies the base-breakpoint variant value unconditionally, so symmetric
25
+ // property sets across size values are required.
26
+ const dialogBox = {
27
+ my: "16",
28
+ mx: "2",
29
+ borderRadius: "md",
30
+ background: "white",
31
+ minHeight: "auto",
32
+ padding: "0",
33
+ };
34
+
35
+ // The `full` variant also styles these slots; same symmetry requirement.
36
+ const dialogSlots = {
37
+ header: { pl: "6" },
38
+ body: { overflowY: "visible" },
39
+ closeTrigger: { top: "2" },
40
+ };
41
+
42
+ export const dialog = defineSlotRecipe({
43
+ className: "dialog",
44
+ slots: [
45
+ "overlay",
46
+ "content",
47
+ "inner",
48
+ "header",
49
+ "body",
50
+ "footer",
51
+ "closeTrigger",
52
+ ],
53
+ base: {
54
+ overlay: {
55
+ position: "fixed",
56
+ inset: 0,
57
+ w: "100%",
58
+ h: "100%",
59
+ bg: "blackAlpha.600",
60
+ zIndex: "modal",
61
+ display: "flex",
62
+ justifyContent: "center",
63
+ alignItems: "flex-start",
64
+ overflow: "auto",
65
+ overscrollBehaviorY: "none",
66
+ // Fade the backdrop in/out. RAC toggles data-entering/data-exiting and
67
+ // waits for the transition before unmounting.
68
+ opacity: 1,
69
+ transition: "opacity 0.2s ease-out",
70
+ "&[data-entering]": { opacity: 0 },
71
+ "&[data-exiting]": { opacity: 0 },
72
+ _motionReduce: { transition: "none" },
73
+ },
74
+ content: {
75
+ position: "relative",
76
+ color: "inherit",
77
+ boxShadow: "lg",
78
+ width: "100%",
79
+ display: "flex",
80
+ flexDirection: "column",
81
+ outline: "none",
82
+ // Approximate Chakra's fade + scale enter/exit.
83
+ opacity: 1,
84
+ transform: "scale(1)",
85
+ transition: "opacity 0.2s ease-out, transform 0.2s ease-out",
86
+ "&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
87
+ "&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
88
+ _motionReduce: { transition: "none" },
89
+ },
90
+ inner: {
91
+ outline: "none",
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ width: "100%",
95
+ flex: "1 1 auto",
96
+ },
97
+ header: {
98
+ px: "6",
99
+ py: "4",
100
+ fontSize: "xl",
101
+ fontWeight: "semibold",
102
+ flexShrink: 0,
103
+ },
104
+ body: { px: "6", py: "2", flex: "1" },
105
+ footer: {
106
+ px: "6",
107
+ py: "4",
108
+ display: "flex",
109
+ alignItems: "center",
110
+ justifyContent: "flex-end",
111
+ // Chakra had no footer gap (call sites wrapped buttons in HStacks);
112
+ // baked in as the house style. Override via css for tighter layouts.
113
+ gap: "5",
114
+ flexShrink: 0,
115
+ },
116
+ closeTrigger: { position: "absolute", top: "2", insetEnd: "3" },
117
+ },
118
+ variants: {
119
+ // Chakra's `isCentered`: vertically centre the dialog in the viewport
120
+ // rather than the default top alignment.
121
+ centered: {
122
+ true: {
123
+ overlay: { alignItems: "center" },
124
+ },
125
+ },
126
+ size: {
127
+ xs: { ...dialogSlots, content: { ...dialogBox, maxWidth: "xs" } },
128
+ sm: { ...dialogSlots, content: { ...dialogBox, maxWidth: "sm" } },
129
+ md: { ...dialogSlots, content: { ...dialogBox, maxWidth: "md" } },
130
+ lg: { ...dialogSlots, content: { ...dialogBox, maxWidth: "lg" } },
131
+ xl: { ...dialogSlots, content: { ...dialogBox, maxWidth: "xl" } },
132
+ "2xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "2xl" } },
133
+ "3xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "3xl" } },
134
+ "4xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "4xl" } },
135
+ "5xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "5xl" } },
136
+ "6xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "6xl" } },
137
+ full: {
138
+ content: {
139
+ maxWidth: "100vw",
140
+ minHeight: "100dvh",
141
+ my: "0",
142
+ mx: "0",
143
+ borderRadius: "0",
144
+ paddingTop: "env(safe-area-inset-top)",
145
+ paddingBottom: "env(safe-area-inset-bottom)",
146
+ paddingLeft: "env(safe-area-inset-left)",
147
+ paddingRight: "env(safe-area-inset-right)",
148
+ // brand colour in the status-bar area, white below (matches ActionBar)
149
+ background:
150
+ "linear-gradient(to bottom, token(colors.statusBarBg) env(safe-area-inset-top), white env(safe-area-inset-top))",
151
+ },
152
+ header: {
153
+ pl: "calc(var(--window-controls-left, 0px) + token(spacing.6))",
154
+ },
155
+ body: { flex: "1", overflowY: "auto" },
156
+ closeTrigger: {
157
+ top: "calc(env(safe-area-inset-top) + token(spacing.2))",
158
+ },
159
+ },
160
+ },
161
+ },
162
+ defaultVariants: { size: "md" },
163
+ });