@microbit/ui 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +7 -5
- package/lang/ui.cy.json +22 -0
- package/lang/ui.it.json +22 -0
- package/package.json +2 -1
- package/src/Avatar.recipe.ts +168 -0
- package/src/Avatar.tsx +276 -0
- package/src/Button.recipe.ts +31 -8
- package/src/Checkbox.tsx +59 -29
- package/src/ComboBox.tsx +192 -0
- package/src/GridList.recipe.ts +58 -0
- package/src/GridList.tsx +81 -0
- package/src/Icon.tsx +23 -3
- package/src/Input.tsx +6 -2
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +6 -1
- package/src/Menu.tsx +54 -24
- package/src/Modal.tsx +105 -7
- package/src/Select.recipe.ts +207 -0
- package/src/Select.tsx +153 -0
- package/src/Skeleton.tsx +146 -0
- package/src/Spinner.tsx +5 -0
- package/src/TextField.tsx +5 -3
- package/src/Tooltip.recipe.ts +37 -0
- package/src/Tooltip.tsx +3 -18
- package/src/base-preset.ts +54 -2
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +108 -0
- package/src/hooks/useDisclosure.ts +32 -0
- package/src/index.ts +8 -0
- package/src/system.ts +9 -0
package/src/Checkbox.tsx
CHANGED
|
@@ -12,13 +12,33 @@ import { css, cx } from "styled-system/css";
|
|
|
12
12
|
import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
14
|
|
|
15
|
+
/** What a render-prop child is told about the checkbox. */
|
|
16
|
+
export interface CheckboxState {
|
|
17
|
+
isSelected: boolean;
|
|
18
|
+
isFocusVisible: boolean;
|
|
19
|
+
isDisabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export interface CheckboxProps
|
|
16
23
|
extends Omit<RACCheckboxProps, "className" | "children" | "style">,
|
|
17
24
|
CheckboxVariantProps {
|
|
18
25
|
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
19
26
|
css?: SystemStyleObject;
|
|
20
27
|
className?: string;
|
|
21
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The label. A function receives the checkbox's state, for a label that
|
|
30
|
+
* changes with it.
|
|
31
|
+
*/
|
|
32
|
+
children?: ReactNode | ((state: CheckboxState) => ReactNode);
|
|
33
|
+
/**
|
|
34
|
+
* Whether to draw the box. `false` is for a checkbox whose children draw
|
|
35
|
+
* the selected state themselves — a selectable tile, or an avatar that
|
|
36
|
+
* grows a tick. The label wrapper goes with it, so the children own the
|
|
37
|
+
* whole row, including the focus ring the box would otherwise carry.
|
|
38
|
+
*
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
control?: boolean;
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
/**
|
|
@@ -31,6 +51,7 @@ export const Checkbox = ({
|
|
|
31
51
|
css: cssProp,
|
|
32
52
|
className,
|
|
33
53
|
children,
|
|
54
|
+
control,
|
|
34
55
|
...rest
|
|
35
56
|
}: CheckboxProps) => {
|
|
36
57
|
const slots = checkbox({ size });
|
|
@@ -39,38 +60,47 @@ export const Checkbox = ({
|
|
|
39
60
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
40
61
|
{...rest}
|
|
41
62
|
>
|
|
42
|
-
{({ isSelected, isFocusVisible, isDisabled }) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
|
|
53
|
-
<polyline
|
|
54
|
-
points="1.5 6 4.5 9 10.5 1"
|
|
55
|
-
fill="none"
|
|
56
|
-
stroke="currentColor"
|
|
57
|
-
strokeWidth="2"
|
|
58
|
-
strokeLinecap="round"
|
|
59
|
-
strokeLinejoin="round"
|
|
60
|
-
/>
|
|
61
|
-
</svg>
|
|
62
|
-
)}
|
|
63
|
-
</span>
|
|
64
|
-
{children != null && (
|
|
63
|
+
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
64
|
+
const content =
|
|
65
|
+
typeof children === "function"
|
|
66
|
+
? children({ isSelected, isFocusVisible, isDisabled })
|
|
67
|
+
: children;
|
|
68
|
+
if (control === false) {
|
|
69
|
+
return content;
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
65
73
|
<span
|
|
66
|
-
className={slots.
|
|
74
|
+
className={slots.control}
|
|
75
|
+
data-selected={isSelected || undefined}
|
|
76
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
67
77
|
data-disabled={isDisabled || undefined}
|
|
78
|
+
aria-hidden
|
|
68
79
|
>
|
|
69
|
-
{
|
|
80
|
+
{isSelected && (
|
|
81
|
+
<svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
|
|
82
|
+
<polyline
|
|
83
|
+
points="1.5 6 4.5 9 10.5 1"
|
|
84
|
+
fill="none"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
strokeWidth="2"
|
|
87
|
+
strokeLinecap="round"
|
|
88
|
+
strokeLinejoin="round"
|
|
89
|
+
/>
|
|
90
|
+
</svg>
|
|
91
|
+
)}
|
|
70
92
|
</span>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
{content != null && (
|
|
94
|
+
<span
|
|
95
|
+
className={slots.label}
|
|
96
|
+
data-disabled={isDisabled || undefined}
|
|
97
|
+
>
|
|
98
|
+
{content}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
}}
|
|
74
104
|
</RACCheckbox>
|
|
75
105
|
);
|
|
76
106
|
};
|
package/src/ComboBox.tsx
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
ForwardedRef,
|
|
8
|
+
forwardRef,
|
|
9
|
+
ReactNode,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import {
|
|
15
|
+
Button as RACButton,
|
|
16
|
+
ComboBox as RACComboBox,
|
|
17
|
+
ComboBoxProps as RACComboBoxProps,
|
|
18
|
+
Input as RACInput,
|
|
19
|
+
Label as RACLabel,
|
|
20
|
+
ListBox as RACListBox,
|
|
21
|
+
Popover,
|
|
22
|
+
PopoverProps,
|
|
23
|
+
} from "react-aria-components";
|
|
24
|
+
import { RiArrowDownSLine } from "react-icons/ri";
|
|
25
|
+
import { css, cx } from "styled-system/css";
|
|
26
|
+
import { select, SelectVariantProps } from "styled-system/recipes";
|
|
27
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
28
|
+
import { Icon } from "./Icon";
|
|
29
|
+
import { SelectSlotProvider } from "./Select";
|
|
30
|
+
|
|
31
|
+
export interface ComboBoxProps<T extends object>
|
|
32
|
+
extends Omit<RACComboBoxProps<T>, "className" | "children" | "style">,
|
|
33
|
+
SelectVariantProps {
|
|
34
|
+
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
35
|
+
label?: ReactNode;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Rendered inside the control, before the input — an icon for the current
|
|
39
|
+
* value, say. A ComboBox's control is a text input, so unlike a Select it
|
|
40
|
+
* cannot show anything but text for what is chosen; this is the way round
|
|
41
|
+
* that (react-select did it with a custom `SingleValue`).
|
|
42
|
+
*/
|
|
43
|
+
startContent?: ReactNode;
|
|
44
|
+
/** `SelectOption`s. */
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Replaces the chevron; pass `null` for none, which is what a plain
|
|
48
|
+
* autocomplete wants (react-select's `dropdownIndicator: display none`).
|
|
49
|
+
*/
|
|
50
|
+
indicator?: ReactNode | null;
|
|
51
|
+
/**
|
|
52
|
+
* Shown in place of the list when nothing matches (react-select's
|
|
53
|
+
* `noOptionsMessage`). Implies `allowsEmptyCollection`, since RAC otherwise
|
|
54
|
+
* closes the popover the moment the collection empties.
|
|
55
|
+
*/
|
|
56
|
+
emptyState?: ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* Keep the dropdown shut until this prop is true. For gating on a minimum
|
|
59
|
+
* query length — react-aria has no `minLength`, and rendering an empty list
|
|
60
|
+
* still opens an empty card.
|
|
61
|
+
*/
|
|
62
|
+
isPopoverHidden?: boolean;
|
|
63
|
+
placement?: PopoverProps["placement"];
|
|
64
|
+
/**
|
|
65
|
+
* Cap the dropdown's height (react-select's `maxMenuHeight`). A prop rather
|
|
66
|
+
* than a `contentCss` rule because RAC writes its own max-height inline
|
|
67
|
+
* while positioning, which beats any class.
|
|
68
|
+
*/
|
|
69
|
+
maxHeight?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Per-instance overrides for the control — the box around the input, its
|
|
72
|
+
* `startContent` and its indicator, which is what `Select`'s `css` styles
|
|
73
|
+
* too. Reach the input itself through the `select` recipe's `value` slot.
|
|
74
|
+
*/
|
|
75
|
+
css?: SystemStyleObject;
|
|
76
|
+
/** Per-instance overrides for the dropdown card. */
|
|
77
|
+
contentCss?: SystemStyleObject;
|
|
78
|
+
className?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ComboBox — a text input that filters a listbox, for choosing one of a known
|
|
83
|
+
* set where typing to narrow it down is the point. Use Select where the list
|
|
84
|
+
* is short enough to just pick from.
|
|
85
|
+
*
|
|
86
|
+
* Note the react-select difference this replaces: react-select filtered on
|
|
87
|
+
* `label` and kept the menu open on selection unless told otherwise, whereas
|
|
88
|
+
* react-aria filters on each item's `textValue` and closes on selection.
|
|
89
|
+
*/
|
|
90
|
+
const ComboBoxInner = <T extends object>(
|
|
91
|
+
{
|
|
92
|
+
label,
|
|
93
|
+
placeholder,
|
|
94
|
+
startContent,
|
|
95
|
+
children,
|
|
96
|
+
indicator,
|
|
97
|
+
emptyState,
|
|
98
|
+
isPopoverHidden,
|
|
99
|
+
placement = "bottom start",
|
|
100
|
+
maxHeight,
|
|
101
|
+
css: cssProp,
|
|
102
|
+
contentCss,
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: ComboBoxProps<T>,
|
|
106
|
+
ref: ForwardedRef<HTMLInputElement>,
|
|
107
|
+
) => {
|
|
108
|
+
// As Select: forward whatever variant groups the merged recipe has.
|
|
109
|
+
const [variantProps, rest] = select.splitVariantProps(props);
|
|
110
|
+
const slots = select(variantProps);
|
|
111
|
+
// Anchor the card to the whole control, not to the bare input inside it —
|
|
112
|
+
// otherwise it hangs off the text baseline and is as narrow as the input.
|
|
113
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
114
|
+
// RAC's --trigger-width measures the input it anchors a ComboBox to, which
|
|
115
|
+
// is the control's content box — so a card sized from it is narrower than
|
|
116
|
+
// the field by the padding and border. Measure the control instead. State
|
|
117
|
+
// rather than reading the ref at render time: the popover is mounted from
|
|
118
|
+
// the first render, before the ref is set, and nothing would re-render it.
|
|
119
|
+
const [triggerWidth, setTriggerWidth] = useState<number>();
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
const el = triggerRef.current;
|
|
122
|
+
if (!el) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const update = () => setTriggerWidth(el.offsetWidth);
|
|
126
|
+
update();
|
|
127
|
+
if (typeof ResizeObserver === "undefined") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const observer = new ResizeObserver(update);
|
|
131
|
+
observer.observe(el);
|
|
132
|
+
return () => observer.disconnect();
|
|
133
|
+
}, []);
|
|
134
|
+
return (
|
|
135
|
+
<SelectSlotProvider value={slots}>
|
|
136
|
+
<RACComboBox
|
|
137
|
+
allowsEmptyCollection={emptyState != null}
|
|
138
|
+
{...(rest as RACComboBoxProps<T>)}
|
|
139
|
+
className={cx(slots.root, className)}
|
|
140
|
+
>
|
|
141
|
+
{label != null && <RACLabel className={slots.label}>{label}</RACLabel>}
|
|
142
|
+
<div
|
|
143
|
+
ref={triggerRef}
|
|
144
|
+
className={cx(slots.trigger, cssProp ? css(cssProp) : undefined)}
|
|
145
|
+
>
|
|
146
|
+
{startContent}
|
|
147
|
+
<RACInput
|
|
148
|
+
ref={ref}
|
|
149
|
+
placeholder={placeholder}
|
|
150
|
+
className={slots.value}
|
|
151
|
+
/>
|
|
152
|
+
{indicator !== null && (
|
|
153
|
+
<RACButton className={slots.indicator}>
|
|
154
|
+
{indicator ?? <Icon as={RiArrowDownSLine} />}
|
|
155
|
+
</RACButton>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
{!isPopoverHidden && (
|
|
159
|
+
<Popover
|
|
160
|
+
triggerRef={triggerRef}
|
|
161
|
+
placement={placement}
|
|
162
|
+
maxHeight={maxHeight}
|
|
163
|
+
style={triggerWidth ? { width: triggerWidth } : undefined}
|
|
164
|
+
className={cx(
|
|
165
|
+
slots.content,
|
|
166
|
+
contentCss ? css(contentCss) : undefined,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
<RACListBox
|
|
170
|
+
className={slots.list}
|
|
171
|
+
renderEmptyState={
|
|
172
|
+
emptyState
|
|
173
|
+
? () => <div className={slots.empty}>{emptyState}</div>
|
|
174
|
+
: undefined
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</RACListBox>
|
|
179
|
+
</Popover>
|
|
180
|
+
)}
|
|
181
|
+
</RACComboBox>
|
|
182
|
+
</SelectSlotProvider>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* forwardRef with generics needs the cast (React's types cannot express it),
|
|
188
|
+
* so the ref lands on the input — call sites focus it for validation.
|
|
189
|
+
*/
|
|
190
|
+
export const ComboBox = forwardRef(ComboBoxInner) as <T extends object>(
|
|
191
|
+
props: ComboBoxProps<T> & { ref?: ForwardedRef<HTMLInputElement> },
|
|
192
|
+
) => ReturnType<typeof ComboBoxInner>;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
* GridList slot recipe — a vertical list of selectable rows, each of which may
|
|
10
|
+
* hold its own interactive controls (which is what makes it a grid rather than
|
|
11
|
+
* a listbox: the roving tab index moves through rows, and the controls inside
|
|
12
|
+
* a row are reachable without leaving it).
|
|
13
|
+
*
|
|
14
|
+
* Chakra had no equivalent, so there is no Chakra look to match: the greys
|
|
15
|
+
* here are the family's neutral list styling, and an app with a strong
|
|
16
|
+
* selection colour restates them (classroom's roster does).
|
|
17
|
+
*
|
|
18
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
19
|
+
* `staticCss` entry that keeps the runtime-prop variants generated.
|
|
20
|
+
*/
|
|
21
|
+
export const gridList = defineSlotRecipe({
|
|
22
|
+
className: "grid-list",
|
|
23
|
+
slots: ["root", "item"],
|
|
24
|
+
base: {
|
|
25
|
+
root: {
|
|
26
|
+
// The list takes the roving tab index, so it is focusable itself and
|
|
27
|
+
// would otherwise draw the platform ring around the whole list.
|
|
28
|
+
outline: "none",
|
|
29
|
+
},
|
|
30
|
+
item: {
|
|
31
|
+
display: "flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
position: "relative",
|
|
34
|
+
// A row is interactive by definition — it selects, or it acts.
|
|
35
|
+
cursor: "pointer",
|
|
36
|
+
outline: "none",
|
|
37
|
+
transitionProperty: "background",
|
|
38
|
+
transitionDuration: "ultra-fast",
|
|
39
|
+
transitionTimingFunction: "ease-in",
|
|
40
|
+
_hover: { bg: "gray.50" },
|
|
41
|
+
// A row holding an open menu (or any other popover) keeps the hover
|
|
42
|
+
// grey, so the row an open menu belongs to stays visible. Hover state
|
|
43
|
+
// cannot do this on its own: a Popover lays a fixed full-viewport
|
|
44
|
+
// underlay over the page while open, which takes the pointer off the
|
|
45
|
+
// row — `:hover` and RAC's own `data-hovered` both drop the moment the
|
|
46
|
+
// menu appears. A trigger carries `aria-expanded` (useOverlayTrigger),
|
|
47
|
+
// so the row can see its own open overlay.
|
|
48
|
+
"&:has([aria-expanded=true])": { bg: "gray.50" },
|
|
49
|
+
"&[data-selected]": {
|
|
50
|
+
bg: "gray.100",
|
|
51
|
+
_hover: { bg: "gray.100" },
|
|
52
|
+
"&:has([aria-expanded=true])": { bg: "gray.100" },
|
|
53
|
+
},
|
|
54
|
+
"&[data-focus-visible]": { focusShadow: "outline" },
|
|
55
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
package/src/GridList.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
GridList as RACGridList,
|
|
9
|
+
GridListItem as RACGridListItem,
|
|
10
|
+
GridListItemProps as RACGridListItemProps,
|
|
11
|
+
GridListProps as RACGridListProps,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { gridList } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface GridListProps<T extends object>
|
|
18
|
+
extends Omit<RACGridListProps<T>, "className" | "style" | "children"> {
|
|
19
|
+
/** `GridListItem`s, or a render function when `items` is given. */
|
|
20
|
+
children: RACGridListProps<T>["children"];
|
|
21
|
+
/** Per-instance style overrides for the list, merged after the recipe. */
|
|
22
|
+
css?: SystemStyleObject;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* GridList — react-aria-components' <GridList>: a list of selectable rows,
|
|
28
|
+
* each of which may contain its own buttons and menus.
|
|
29
|
+
*
|
|
30
|
+
* Reach for it over a `ListBox` when the rows carry controls: a listbox option
|
|
31
|
+
* is a leaf, so a button inside one is unreachable by keyboard, where a grid
|
|
32
|
+
* row's contents are part of the grid's navigation.
|
|
33
|
+
*/
|
|
34
|
+
export const GridList = <T extends object>({
|
|
35
|
+
css: cssProp,
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
...rest
|
|
39
|
+
}: GridListProps<T>) => {
|
|
40
|
+
const slots = gridList();
|
|
41
|
+
return (
|
|
42
|
+
<RACGridList
|
|
43
|
+
{...rest}
|
|
44
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</RACGridList>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface GridListItemProps<T extends object = object>
|
|
52
|
+
extends Omit<RACGridListItemProps<T>, "className" | "style" | "children"> {
|
|
53
|
+
children?: ReactNode;
|
|
54
|
+
/** Per-instance style overrides for the row, merged after the recipe. */
|
|
55
|
+
css?: SystemStyleObject;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A row in a `GridList`. Its children are laid out by the row itself — the
|
|
61
|
+
* gridcell react-aria puts between them is `display: contents`.
|
|
62
|
+
*
|
|
63
|
+
* Give every row a `textValue`: react-aria derives typeahead text from string
|
|
64
|
+
* children only, and a row is usually a composition rather than a string.
|
|
65
|
+
*/
|
|
66
|
+
export const GridListItem = <T extends object = object>({
|
|
67
|
+
css: cssProp,
|
|
68
|
+
className,
|
|
69
|
+
children,
|
|
70
|
+
...rest
|
|
71
|
+
}: GridListItemProps<T>) => {
|
|
72
|
+
const slots = gridList();
|
|
73
|
+
return (
|
|
74
|
+
<RACGridListItem
|
|
75
|
+
{...rest}
|
|
76
|
+
className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</RACGridListItem>
|
|
80
|
+
);
|
|
81
|
+
};
|
package/src/Icon.tsx
CHANGED
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { ComponentType, SVGProps } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
8
|
import { SystemStyleObject } from "styled-system/types";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Any component that renders an `<svg>` from svg props. Deliberately no
|
|
12
|
+
* narrower than the props `Icon` actually passes, so it accepts both
|
|
13
|
+
* react-icons' `IconType` and svgr components (`import X from "./x.svg?react"`,
|
|
14
|
+
* which the apps use for their custom-path icons — Chakra's `<Icon as={…}>`
|
|
15
|
+
* took either).
|
|
16
|
+
*/
|
|
17
|
+
export type IconComponent = ComponentType<
|
|
18
|
+
Pick<
|
|
19
|
+
SVGProps<SVGSVGElement>,
|
|
20
|
+
"className" | "focusable" | "role" | "aria-label" | "aria-hidden"
|
|
21
|
+
>
|
|
22
|
+
>;
|
|
23
|
+
|
|
10
24
|
export interface IconProps {
|
|
11
|
-
/** The react-icons
|
|
12
|
-
as:
|
|
25
|
+
/** The icon component to render: a react-icons glyph or an svgr import. */
|
|
26
|
+
as: IconComponent;
|
|
13
27
|
/** Panda style overrides (size via fontSize/boxSize, colour, etc.). */
|
|
14
28
|
css?: SystemStyleObject;
|
|
15
29
|
className?: string;
|
|
@@ -43,6 +57,12 @@ export const Icon = ({
|
|
|
43
57
|
lineHeight: "1em",
|
|
44
58
|
flexShrink: 0,
|
|
45
59
|
fill: "currentColor",
|
|
60
|
+
// Chakra's Icon set this on the element itself, and an inline-block
|
|
61
|
+
// icon sits ~3px off without it. Panda's preflight happens to set it
|
|
62
|
+
// on every svg, which hid the omission in apps that had already
|
|
63
|
+
// flipped — classroom measured the difference at its kill-switch,
|
|
64
|
+
// where the preflight arrived and moved every icon back.
|
|
65
|
+
verticalAlign: "middle",
|
|
46
66
|
...cssProp,
|
|
47
67
|
}),
|
|
48
68
|
className,
|
package/src/Input.tsx
CHANGED
|
@@ -23,14 +23,18 @@ export interface InputProps
|
|
|
23
23
|
* labelled field with help/error text use TextField instead.
|
|
24
24
|
*/
|
|
25
25
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
26
|
-
{
|
|
26
|
+
{ css: cssProp, className, ...props },
|
|
27
27
|
ref,
|
|
28
28
|
) {
|
|
29
|
+
// splitVariantProps, not a hand-picked `size`: an app preset can add variant
|
|
30
|
+
// groups to the recipe (classroom adds `variant`), and cherry-picking would
|
|
31
|
+
// silently drop them onto the DOM as unknown attributes instead.
|
|
32
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
29
33
|
return (
|
|
30
34
|
<input
|
|
31
35
|
ref={ref}
|
|
32
36
|
className={cx(
|
|
33
|
-
input(
|
|
37
|
+
input(variantProps),
|
|
34
38
|
cssProp ? css(cssProp) : undefined,
|
|
35
39
|
className,
|
|
36
40
|
)}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
* ListBox slot recipe — a standalone list of choosable options, single or
|
|
10
|
+
* multiple. Distinct from the `select` recipe's `list`/`option` slots, which
|
|
11
|
+
* style the same react-aria primitive inside a dropdown card: this one sits
|
|
12
|
+
* inline on the page, so it carries no surface of its own.
|
|
13
|
+
*
|
|
14
|
+
* An option is a leaf — if the rows need their own buttons or menus, they
|
|
15
|
+
* want `GridList` instead.
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop variants generated.
|
|
19
|
+
*/
|
|
20
|
+
export const listBox = defineSlotRecipe({
|
|
21
|
+
className: "list-box",
|
|
22
|
+
slots: ["root", "option"],
|
|
23
|
+
base: {
|
|
24
|
+
root: {
|
|
25
|
+
// The listbox holds the roving tab index, so it is focusable itself and
|
|
26
|
+
// would otherwise draw the platform ring around the whole list.
|
|
27
|
+
outline: "none",
|
|
28
|
+
},
|
|
29
|
+
option: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
cursor: "pointer",
|
|
33
|
+
outline: "none",
|
|
34
|
+
transitionProperty: "background",
|
|
35
|
+
transitionDuration: "ultra-fast",
|
|
36
|
+
transitionTimingFunction: "ease-in",
|
|
37
|
+
_hover: { bg: "gray.50" },
|
|
38
|
+
"&[data-selected]": { bg: "gray.100", _hover: { bg: "gray.100" } },
|
|
39
|
+
"&[data-focus-visible]": { focusShadow: "outline" },
|
|
40
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
package/src/ListBox.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
ListBox as RACListBox,
|
|
8
|
+
ListBoxItem as RACListBoxItem,
|
|
9
|
+
ListBoxItemProps as RACListBoxItemProps,
|
|
10
|
+
ListBoxProps as RACListBoxProps,
|
|
11
|
+
} from "react-aria-components";
|
|
12
|
+
import { css, cx } from "styled-system/css";
|
|
13
|
+
import { listBox } from "styled-system/recipes";
|
|
14
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
15
|
+
|
|
16
|
+
export interface ListBoxProps<T extends object>
|
|
17
|
+
extends Omit<RACListBoxProps<T>, "className" | "style" | "children"> {
|
|
18
|
+
/** `ListBoxOption`s, or a render function when `items` is given. */
|
|
19
|
+
children: RACListBoxProps<T>["children"];
|
|
20
|
+
/** Per-instance style overrides for the list, merged after the recipe. */
|
|
21
|
+
css?: SystemStyleObject;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ListBox — react-aria-components' <ListBox>, standing on the page rather
|
|
27
|
+
* than inside a dropdown (that is `Select`/`ComboBox`, which share their own
|
|
28
|
+
* recipe).
|
|
29
|
+
*
|
|
30
|
+
* Options are leaves: a button inside one is unreachable by keyboard, so rows
|
|
31
|
+
* carrying their own controls want `GridList`.
|
|
32
|
+
*/
|
|
33
|
+
export const ListBox = <T extends object>({
|
|
34
|
+
css: cssProp,
|
|
35
|
+
className,
|
|
36
|
+
children,
|
|
37
|
+
...rest
|
|
38
|
+
}: ListBoxProps<T>) => {
|
|
39
|
+
const slots = listBox();
|
|
40
|
+
return (
|
|
41
|
+
<RACListBox
|
|
42
|
+
{...rest}
|
|
43
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</RACListBox>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface ListBoxOptionProps<T extends object = object>
|
|
51
|
+
extends Omit<RACListBoxItemProps<T>, "className" | "style" | "children"> {
|
|
52
|
+
/**
|
|
53
|
+
* The option's content. A function receives the option's state, for a row
|
|
54
|
+
* that draws its own selected marker rather than taking the recipe's
|
|
55
|
+
* background.
|
|
56
|
+
*/
|
|
57
|
+
children?: RACListBoxItemProps<T>["children"];
|
|
58
|
+
/** Per-instance style overrides for the option, merged after the recipe. */
|
|
59
|
+
css?: SystemStyleObject;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An option in a `ListBox`.
|
|
65
|
+
*
|
|
66
|
+
* Give it a `textValue` where its children aren't a plain string: react-aria
|
|
67
|
+
* derives typeahead text from string children only.
|
|
68
|
+
*/
|
|
69
|
+
export const ListBoxOption = <T extends object = object>({
|
|
70
|
+
css: cssProp,
|
|
71
|
+
className,
|
|
72
|
+
children,
|
|
73
|
+
...rest
|
|
74
|
+
}: ListBoxOptionProps<T>) => {
|
|
75
|
+
const slots = listBox();
|
|
76
|
+
return (
|
|
77
|
+
<RACListBoxItem
|
|
78
|
+
{...rest}
|
|
79
|
+
className={cx(
|
|
80
|
+
slots.option,
|
|
81
|
+
cssProp ? css(cssProp) : undefined,
|
|
82
|
+
className,
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</RACListBoxItem>
|
|
87
|
+
);
|
|
88
|
+
};
|
package/src/Menu.recipe.ts
CHANGED
|
@@ -35,7 +35,12 @@ export const menu = defineSlotRecipe({
|
|
|
35
35
|
color: "inherit",
|
|
36
36
|
minWidth: "3xs",
|
|
37
37
|
py: "2",
|
|
38
|
-
|
|
38
|
+
// `popover` (1500), not `dropdown` (1000): a RAC Popover always portals
|
|
39
|
+
// to the body, so a menu opened from inside a Modal (zIndex `modal`,
|
|
40
|
+
// 1400) escapes the modal's stacking context and would paint behind it.
|
|
41
|
+
// Chakra never hit this — its MenuList rendered inline unless explicitly
|
|
42
|
+
// portalled. Nothing else lives between 1400 and the toast/tooltip layer.
|
|
43
|
+
zIndex: "popover",
|
|
39
44
|
borderRadius: "md",
|
|
40
45
|
borderWidth: "1px",
|
|
41
46
|
borderColor: "gray.200",
|