@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.
- package/LICENSE.md +8 -0
- package/README.md +171 -20
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +44 -4
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +43 -3
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +44 -4
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +13 -11
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +67 -14
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +13 -12
- package/src/Checkbox.tsx +103 -34
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +1 -1
- package/src/Collapse.tsx +13 -14
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +40 -7
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +18 -4
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +8 -18
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +30 -26
- package/src/Input.tsx +16 -7
- package/src/InputGroup.tsx +9 -9
- package/src/Kbd.tsx +1 -1
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +14 -13
- package/src/List.tsx +5 -7
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +22 -19
- package/src/Menu.tsx +54 -28
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +123 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +37 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +56 -15
- package/src/NumberField.tsx +74 -22
- package/src/PopoverArrow.tsx +1 -2
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +13 -12
- package/src/Radio.tsx +2 -29
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +20 -12
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +40 -21
- package/src/Switch.tsx +35 -5
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +35 -38
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +147 -48
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +323 -84
- package/src/base-tokens.ts +0 -3
- package/src/button-icon.ts +4 -5
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +5 -6
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +3 -4
- package/src/hooks/usePrevious.ts +1 -4
- package/src/index.ts +19 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/ComboBox.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
ListBox as RACListBox,
|
|
20
|
+
Popover,
|
|
21
|
+
PopoverProps,
|
|
22
|
+
} from "react-aria-components";
|
|
23
|
+
import { useFocusVisible } from "react-aria";
|
|
24
|
+
import { useIntl } from "react-intl";
|
|
25
|
+
import { RiArrowDownSLine } from "react-icons/ri";
|
|
26
|
+
import { css, cx } from "styled-system/css";
|
|
27
|
+
import { field, select, SelectVariantProps } from "styled-system/recipes";
|
|
28
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
29
|
+
import {
|
|
30
|
+
FieldLabel,
|
|
31
|
+
FieldLayoutProps,
|
|
32
|
+
FieldSupport,
|
|
33
|
+
FieldSupportProps,
|
|
34
|
+
} from "./Field";
|
|
35
|
+
import { Icon } from "./Icon";
|
|
36
|
+
import { uiMessage } from "./messages";
|
|
37
|
+
import { SelectSlotProvider } from "./Select";
|
|
38
|
+
|
|
39
|
+
export interface ComboBoxProps<T extends object>
|
|
40
|
+
extends Omit<RACComboBoxProps<T>, "className" | "children" | "style">,
|
|
41
|
+
SelectVariantProps,
|
|
42
|
+
FieldSupportProps,
|
|
43
|
+
FieldLayoutProps {
|
|
44
|
+
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
45
|
+
label?: ReactNode;
|
|
46
|
+
/** Label style overrides. */
|
|
47
|
+
labelCss?: SystemStyleObject;
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Rendered inside the control, before the input — an icon for the current
|
|
51
|
+
* value, say. A ComboBox's control is a text input, so unlike a Select it
|
|
52
|
+
* cannot show anything but text for what is chosen; this is the way round
|
|
53
|
+
* that (react-select did it with a custom `SingleValue`).
|
|
54
|
+
*/
|
|
55
|
+
startContent?: ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* `SelectOption`s, or a render function over the `items` prop for a
|
|
58
|
+
* dynamic collection — which is how an async lookup works: drive `items`
|
|
59
|
+
* from loaded results (e.g. react-stately's useAsyncList) and filter
|
|
60
|
+
* server-side; react-aria skips its own text filtering when `items` is
|
|
61
|
+
* controlled. Pair with `emptyState` (swap its content while loading) and
|
|
62
|
+
* `isPopoverHidden` for a minimum query length.
|
|
63
|
+
*/
|
|
64
|
+
children: ReactNode | ((item: T) => ReactNode);
|
|
65
|
+
/**
|
|
66
|
+
* Replaces the chevron; pass `null` for none, which is what a plain
|
|
67
|
+
* autocomplete wants (react-select's `dropdownIndicator: display none`).
|
|
68
|
+
*/
|
|
69
|
+
indicator?: ReactNode | null;
|
|
70
|
+
/**
|
|
71
|
+
* Shown in place of the list when nothing matches (react-select's
|
|
72
|
+
* `noOptionsMessage`). Implies `allowsEmptyCollection`, since RAC otherwise
|
|
73
|
+
* closes the popover the moment the collection empties.
|
|
74
|
+
*/
|
|
75
|
+
emptyState?: ReactNode;
|
|
76
|
+
/**
|
|
77
|
+
* Keep the dropdown shut until this prop is true. For gating on a minimum
|
|
78
|
+
* query length — react-aria has no `minLength`, and rendering an empty list
|
|
79
|
+
* still opens an empty card.
|
|
80
|
+
*/
|
|
81
|
+
isPopoverHidden?: boolean;
|
|
82
|
+
placement?: PopoverProps["placement"];
|
|
83
|
+
/**
|
|
84
|
+
* Cap the dropdown's height (react-select's `maxMenuHeight`). A prop rather
|
|
85
|
+
* than a `contentCss` rule because RAC writes its own max-height inline
|
|
86
|
+
* while positioning, which beats any class.
|
|
87
|
+
*/
|
|
88
|
+
maxHeight?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Per-instance overrides for the trigger — the box around the input, its
|
|
91
|
+
* `startContent` and its indicator, the same slot `Select`'s `triggerCss`
|
|
92
|
+
* styles. Reach the input itself through the `select` recipe's `value`
|
|
93
|
+
* slot.
|
|
94
|
+
*/
|
|
95
|
+
triggerCss?: SystemStyleObject;
|
|
96
|
+
/** Per-instance overrides for the dropdown card. */
|
|
97
|
+
contentCss?: SystemStyleObject;
|
|
98
|
+
className?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* ComboBox — a text input that filters a listbox, for choosing one of a known
|
|
103
|
+
* set where typing to narrow it down is the point. Use Select where the list
|
|
104
|
+
* is short enough to just pick from.
|
|
105
|
+
*
|
|
106
|
+
* Note the react-select difference this replaces: react-select filtered on
|
|
107
|
+
* `label` and kept the menu open on selection unless told otherwise, whereas
|
|
108
|
+
* react-aria filters on each item's `textValue` and closes on selection.
|
|
109
|
+
*/
|
|
110
|
+
const ComboBoxInner = <T extends object>(
|
|
111
|
+
{
|
|
112
|
+
label,
|
|
113
|
+
labelCss,
|
|
114
|
+
placeholder,
|
|
115
|
+
startContent,
|
|
116
|
+
children,
|
|
117
|
+
indicator,
|
|
118
|
+
emptyState,
|
|
119
|
+
isPopoverHidden,
|
|
120
|
+
placement = "bottom start",
|
|
121
|
+
maxHeight,
|
|
122
|
+
helperText,
|
|
123
|
+
errorMessage,
|
|
124
|
+
helperTextCss,
|
|
125
|
+
labelPosition,
|
|
126
|
+
triggerCss,
|
|
127
|
+
contentCss,
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
}: ComboBoxProps<T>,
|
|
131
|
+
ref: ForwardedRef<HTMLInputElement>,
|
|
132
|
+
) => {
|
|
133
|
+
// As Select: forward whatever variant groups the merged recipe has.
|
|
134
|
+
const [variantProps, rest] = select.splitVariantProps(props);
|
|
135
|
+
const intl = useIntl();
|
|
136
|
+
const slots = select(variantProps);
|
|
137
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
138
|
+
// Anchor the card to the whole control, not to the bare input inside it —
|
|
139
|
+
// otherwise it hangs off the text baseline and is as narrow as the input.
|
|
140
|
+
// Global modality (text-input key filter): unlike the input's own RAC
|
|
141
|
+
// attributes, it survives react-aria's synthetic blur during virtual
|
|
142
|
+
// focus — the select recipe's ring rule pairs it with :has(input:focus).
|
|
143
|
+
const { isFocusVisible } = useFocusVisible({ isTextInput: true });
|
|
144
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
145
|
+
// RAC's --trigger-width measures the input it anchors a ComboBox to, which
|
|
146
|
+
// is the control's content box — so a card sized from it is narrower than
|
|
147
|
+
// the field by the padding and border. Measure the control instead. State
|
|
148
|
+
// rather than reading the ref at render time: the popover is mounted from
|
|
149
|
+
// the first render, before the ref is set, and nothing would re-render it.
|
|
150
|
+
const [triggerWidth, setTriggerWidth] = useState<number>();
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
const el = triggerRef.current;
|
|
153
|
+
if (!el) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const update = () => setTriggerWidth(el.offsetWidth);
|
|
157
|
+
update();
|
|
158
|
+
if (typeof ResizeObserver === "undefined") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const observer = new ResizeObserver(update);
|
|
162
|
+
observer.observe(el);
|
|
163
|
+
return () => observer.disconnect();
|
|
164
|
+
}, []);
|
|
165
|
+
return (
|
|
166
|
+
<SelectSlotProvider value={slots}>
|
|
167
|
+
<RACComboBox
|
|
168
|
+
allowsEmptyCollection={emptyState != null}
|
|
169
|
+
{...(rest as RACComboBoxProps<T>)}
|
|
170
|
+
className={cx(fieldSlots.root, slots.root, className)}
|
|
171
|
+
>
|
|
172
|
+
{label != null && (
|
|
173
|
+
<FieldLabel
|
|
174
|
+
size={variantProps.size}
|
|
175
|
+
labelPosition={labelPosition}
|
|
176
|
+
isRequired={props.isRequired}
|
|
177
|
+
css={labelCss}
|
|
178
|
+
>
|
|
179
|
+
{label}
|
|
180
|
+
</FieldLabel>
|
|
181
|
+
)}
|
|
182
|
+
<div
|
|
183
|
+
ref={triggerRef}
|
|
184
|
+
data-keyboard-modality={isFocusVisible || undefined}
|
|
185
|
+
className={cx(
|
|
186
|
+
slots.trigger,
|
|
187
|
+
triggerCss ? css(triggerCss) : undefined,
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{startContent}
|
|
191
|
+
<RACInput
|
|
192
|
+
ref={ref}
|
|
193
|
+
placeholder={placeholder}
|
|
194
|
+
className={slots.value}
|
|
195
|
+
/>
|
|
196
|
+
{indicator !== null && (
|
|
197
|
+
<RACButton
|
|
198
|
+
aria-label={intl.formatMessage(uiMessage("ui.combobox-trigger"))}
|
|
199
|
+
className={slots.indicator}
|
|
200
|
+
>
|
|
201
|
+
{indicator ?? <Icon as={RiArrowDownSLine} />}
|
|
202
|
+
</RACButton>
|
|
203
|
+
)}
|
|
204
|
+
</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
|
+
)}
|
|
215
|
+
>
|
|
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
|
+
)}
|
|
229
|
+
<FieldSupport
|
|
230
|
+
helperText={helperText}
|
|
231
|
+
errorMessage={errorMessage}
|
|
232
|
+
helperTextCss={helperTextCss}
|
|
233
|
+
labelPosition={labelPosition}
|
|
234
|
+
/>
|
|
235
|
+
</RACComboBox>
|
|
236
|
+
</SelectSlotProvider>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* forwardRef with generics needs the cast (React's types cannot express it),
|
|
242
|
+
* so the ref lands on the input — call sites focus it for validation.
|
|
243
|
+
*/
|
|
244
|
+
export const ComboBox = forwardRef(ComboBoxInner) as <T extends object>(
|
|
245
|
+
props: ComboBoxProps<T> & { ref?: ForwardedRef<HTMLInputElement> },
|
|
246
|
+
) => ReturnType<typeof ComboBoxInner>;
|
package/src/Divider.tsx
CHANGED
|
@@ -15,33 +15,66 @@ const StyledDivider = styled("hr", {
|
|
|
15
15
|
variants: {
|
|
16
16
|
orientation: {
|
|
17
17
|
horizontal: {
|
|
18
|
-
borderBottomWidth: "1px",
|
|
19
18
|
borderBottomStyle: "solid",
|
|
20
19
|
width: "100%",
|
|
21
20
|
},
|
|
22
21
|
vertical: {
|
|
23
|
-
borderLeftWidth: "1px",
|
|
24
22
|
borderLeftStyle: "solid",
|
|
25
23
|
height: "100%",
|
|
26
24
|
},
|
|
27
25
|
},
|
|
26
|
+
// Rule weight, on the orientation's drawn edge (see compoundVariants).
|
|
27
|
+
// Semantic rather than a raw border width so call sites don't need to
|
|
28
|
+
// know which edge an orientation draws with.
|
|
29
|
+
thickness: {
|
|
30
|
+
thin: {},
|
|
31
|
+
thick: {},
|
|
32
|
+
},
|
|
28
33
|
},
|
|
29
|
-
|
|
34
|
+
compoundVariants: [
|
|
35
|
+
{
|
|
36
|
+
orientation: "horizontal",
|
|
37
|
+
thickness: "thin",
|
|
38
|
+
css: { borderBottomWidth: "1px" },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
orientation: "horizontal",
|
|
42
|
+
thickness: "thick",
|
|
43
|
+
css: { borderBottomWidth: "2px" },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
orientation: "vertical",
|
|
47
|
+
thickness: "thin",
|
|
48
|
+
css: { borderLeftWidth: "1px" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
orientation: "vertical",
|
|
52
|
+
thickness: "thick",
|
|
53
|
+
css: { borderLeftWidth: "2px" },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
defaultVariants: { orientation: "horizontal", thickness: "thin" },
|
|
30
57
|
});
|
|
31
58
|
|
|
32
59
|
export interface DividerProps extends ComponentProps<typeof StyledDivider> {}
|
|
33
60
|
|
|
34
61
|
/**
|
|
35
|
-
* Divider — a hairline rule
|
|
36
|
-
* `
|
|
37
|
-
* layout, e.g. a stretched
|
|
38
|
-
*
|
|
62
|
+
* Divider — a hairline rule (60% opacity; set `borderColor` to tint,
|
|
63
|
+
* `thickness="thick"` for a 2px rule).
|
|
64
|
+
* `orientation="vertical"` needs a height from the layout, e.g. a stretched
|
|
65
|
+
* flex row.
|
|
66
|
+
*
|
|
67
|
+
* Decorative by default: hidden from assistive tech, since a visual rule
|
|
68
|
+
* between sections is noise as an announced separator (and every call site
|
|
69
|
+
* was opting out by hand). Pass `aria-hidden={false}` for a divider that
|
|
70
|
+
* should be exposed as a semantic separator.
|
|
39
71
|
*/
|
|
40
72
|
export const Divider = forwardRef<HTMLHRElement, DividerProps>(
|
|
41
73
|
function Divider(props, ref) {
|
|
42
74
|
return (
|
|
43
75
|
<StyledDivider
|
|
44
76
|
ref={ref}
|
|
77
|
+
aria-hidden
|
|
45
78
|
aria-orientation={
|
|
46
79
|
props.orientation === "vertical" ? "vertical" : "horizontal"
|
|
47
80
|
}
|
package/src/Drawer.recipe.ts
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Drawer slot recipe —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Drawer slot recipe — a 20rem (`xs`) side panel. Consumed by the shared-ui
|
|
10
|
+
* Drawer, which maps the slots onto react-aria-components' ModalOverlay /
|
|
11
|
+
* Modal / Dialog.
|
|
12
12
|
*
|
|
13
|
-
* The enter transition
|
|
14
|
-
* stiffness 180) with a decelerating tween; exit matches its 0.15s ease-in-out
|
|
13
|
+
* The enter transition is a decelerating tween; the exit a 0.15s ease-in-out
|
|
15
14
|
* tween.
|
|
16
15
|
*
|
|
17
16
|
* Registered in the base preset (base-preset.ts); `placement` is
|
|
@@ -77,22 +76,34 @@ export const drawer = defineSlotRecipe({
|
|
|
77
76
|
},
|
|
78
77
|
},
|
|
79
78
|
variants: {
|
|
79
|
+
// Reading-order sides, so a drawer keeps its relationship to the content
|
|
80
|
+
// in an RTL locale rather than its screen position. The slide-out
|
|
81
|
+
// transforms are physical, so each needs mirroring under `dir=rtl` to
|
|
82
|
+
// travel towards its own edge.
|
|
80
83
|
placement: {
|
|
81
|
-
|
|
84
|
+
start: {
|
|
82
85
|
content: {
|
|
83
|
-
|
|
86
|
+
insetStart: 0,
|
|
84
87
|
"&[data-entering]": { transform: "translateX(-100%)" },
|
|
85
88
|
"&[data-exiting]": { transform: "translateX(-100%)" },
|
|
89
|
+
_rtl: {
|
|
90
|
+
"&[data-entering]": { transform: "translateX(100%)" },
|
|
91
|
+
"&[data-exiting]": { transform: "translateX(100%)" },
|
|
92
|
+
},
|
|
86
93
|
},
|
|
87
94
|
},
|
|
88
|
-
|
|
95
|
+
end: {
|
|
89
96
|
content: {
|
|
90
|
-
|
|
97
|
+
insetEnd: 0,
|
|
91
98
|
"&[data-entering]": { transform: "translateX(100%)" },
|
|
92
99
|
"&[data-exiting]": { transform: "translateX(100%)" },
|
|
100
|
+
_rtl: {
|
|
101
|
+
"&[data-entering]": { transform: "translateX(-100%)" },
|
|
102
|
+
"&[data-exiting]": { transform: "translateX(-100%)" },
|
|
103
|
+
},
|
|
93
104
|
},
|
|
94
105
|
},
|
|
95
106
|
},
|
|
96
107
|
},
|
|
97
|
-
defaultVariants: { placement: "
|
|
108
|
+
defaultVariants: { placement: "start" },
|
|
98
109
|
});
|
package/src/Drawer.tsx
CHANGED
|
@@ -24,7 +24,7 @@ export interface DrawerProps extends DrawerVariantProps {
|
|
|
24
24
|
onClose: () => void;
|
|
25
25
|
/**
|
|
26
26
|
* Called after the drawer has fully closed (exit transition done and the
|
|
27
|
-
* drawer removed).
|
|
27
|
+
* drawer removed).
|
|
28
28
|
*/
|
|
29
29
|
onCloseComplete?: () => void;
|
|
30
30
|
/** Allow closing by clicking the backdrop / pressing Escape (default true). */
|
|
@@ -39,9 +39,8 @@ export interface DrawerProps extends DrawerVariantProps {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Drawer — a focus-trapping panel that slides in from the side
|
|
43
|
-
*
|
|
44
|
-
* DrawerHeader and DrawerBody inside.
|
|
42
|
+
* Drawer — a focus-trapping panel that slides in from the side, in a single
|
|
43
|
+
* shell; place DrawerHeader and DrawerBody inside.
|
|
45
44
|
*/
|
|
46
45
|
export const Drawer = ({
|
|
47
46
|
isOpen,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ComponentProps } from "react";
|
|
7
|
+
import { RiExternalLinkLine } from "react-icons/ri";
|
|
8
|
+
import { FormattedMessage } from "react-intl";
|
|
9
|
+
import { Icon } from "./Icon";
|
|
10
|
+
import { Link } from "./Link";
|
|
11
|
+
import { uiMessage } from "./messages";
|
|
12
|
+
import { VisuallyHidden } from "./VisuallyHidden";
|
|
13
|
+
|
|
14
|
+
export type ExternalLinkProps = Omit<
|
|
15
|
+
ComponentProps<typeof Link>,
|
|
16
|
+
"target" | "rel"
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ExternalLink — a Link that opens in a new tab and says so both ways: a
|
|
21
|
+
* trailing external-link glyph for sighted users, and a visually hidden,
|
|
22
|
+
* translated "opens in a new tab" for assistive tech. Use this rather than
|
|
23
|
+
* restating `target="_blank"` + icon so the announcement can't be forgotten.
|
|
24
|
+
*
|
|
25
|
+
* For app chrome, where a new tab protects the user's session-only state.
|
|
26
|
+
* Content surfaces should prefer same-tab links, or a visible
|
|
27
|
+
* "(opens in new tab)" suffix and no icon.
|
|
28
|
+
*
|
|
29
|
+
* Inline-safe: the icon is 1em and follows the text colour, so the link can
|
|
30
|
+
* sit in prose as well as stand alone.
|
|
31
|
+
*/
|
|
32
|
+
export const ExternalLink = ({ children, ...props }: ExternalLinkProps) => (
|
|
33
|
+
<Link {...props} target="_blank" rel="noopener">
|
|
34
|
+
{children}
|
|
35
|
+
<VisuallyHidden>
|
|
36
|
+
{/* The comma separates the suffix in the accessible name: inline nodes
|
|
37
|
+
concatenate without a space, and leading whitespace is trimmed. */}
|
|
38
|
+
{", "}
|
|
39
|
+
<FormattedMessage {...uiMessage("ui.new-tab-notice")} />
|
|
40
|
+
</VisuallyHidden>
|
|
41
|
+
<Icon as={RiExternalLinkLine} aria-hidden css={{ ms: 1 }} />
|
|
42
|
+
</Link>
|
|
43
|
+
);
|
package/src/Fade.tsx
CHANGED
|
@@ -3,30 +3,44 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import { ReactNode } from "react";
|
|
6
|
+
import { CSSProperties, ReactNode } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
8
|
import { SystemStyleObject } from "styled-system/types";
|
|
9
9
|
|
|
10
10
|
export interface FadeProps {
|
|
11
11
|
/** Visible when true; faded out (but mounted) when false. */
|
|
12
12
|
isOpen: boolean;
|
|
13
|
+
/** Fade-in time in seconds. Default 0.2. */
|
|
14
|
+
enterDuration?: number;
|
|
15
|
+
/** Fade-out time in seconds. Default 0.2. */
|
|
16
|
+
exitDuration?: number;
|
|
13
17
|
css?: SystemStyleObject;
|
|
14
18
|
className?: string;
|
|
15
19
|
children: ReactNode;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
|
-
* Fade —
|
|
20
|
-
*
|
|
23
|
+
* Fade — a CSS opacity transition (see Slide for the pattern). Content stays
|
|
24
|
+
* mounted throughout.
|
|
21
25
|
*/
|
|
22
26
|
export const Fade = ({
|
|
23
27
|
isOpen,
|
|
28
|
+
enterDuration = 0.2,
|
|
29
|
+
exitDuration = 0.2,
|
|
24
30
|
css: cssProp,
|
|
25
31
|
className,
|
|
26
32
|
children,
|
|
27
33
|
}: FadeProps) => (
|
|
28
34
|
<div
|
|
29
35
|
data-open={isOpen ? "" : undefined}
|
|
36
|
+
// Runtime values, so an inline custom property rather than the css()
|
|
37
|
+
// object. The var switches in the same commit as the opacity, so the
|
|
38
|
+
// transition picks up the direction's own duration.
|
|
39
|
+
style={
|
|
40
|
+
{
|
|
41
|
+
"--fade-duration": `${isOpen ? enterDuration : exitDuration}s`,
|
|
42
|
+
} as CSSProperties
|
|
43
|
+
}
|
|
30
44
|
className={cx(
|
|
31
45
|
css(
|
|
32
46
|
{
|
|
@@ -34,7 +48,7 @@ export const Fade = ({
|
|
|
34
48
|
pointerEvents: "none",
|
|
35
49
|
"&[data-open]": { opacity: 1, pointerEvents: "auto" },
|
|
36
50
|
transitionProperty: "opacity",
|
|
37
|
-
transitionDuration: "
|
|
51
|
+
transitionDuration: "var(--fade-duration)",
|
|
38
52
|
transitionTimingFunction: "ease-out",
|
|
39
53
|
_motionReduce: { transition: "none" },
|
|
40
54
|
},
|
|
@@ -0,0 +1,114 @@
|
|
|
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 — the label/helper/error chrome, mapped onto
|
|
10
|
+
* react-aria-components' Label/Text/FieldError by `FieldLabel` and
|
|
11
|
+
* `FieldSupport`. Every labelled
|
|
12
|
+
* field in the library draws its chrome from here, including Select and
|
|
13
|
+
* ComboBox, whose own recipe styles only the dropdown pair; the input itself is
|
|
14
|
+
* styled by the `input` recipe (Input.recipe.ts).
|
|
15
|
+
*
|
|
16
|
+
* `root` is also the single owner of field-root *layout*: Select, ComboBox and
|
|
17
|
+
* NumberField wear it alongside their own recipe's root slot, which carries no
|
|
18
|
+
* layout of its own — two recipes fighting over `flexDirection` would leave
|
|
19
|
+
* `labelPosition` at the mercy of emission order. RadioGroup and CheckboxGroup
|
|
20
|
+
* deliberately don't wear it (their roots carry no layout at all).
|
|
21
|
+
*
|
|
22
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
23
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
24
|
+
*/
|
|
25
|
+
export const field = defineSlotRecipe({
|
|
26
|
+
className: "field",
|
|
27
|
+
slots: ["root", "label", "requiredIndicator", "helperText", "errorMessage"],
|
|
28
|
+
base: {
|
|
29
|
+
root: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
alignItems: "stretch",
|
|
33
|
+
width: "100%",
|
|
34
|
+
},
|
|
35
|
+
label: {
|
|
36
|
+
display: "block",
|
|
37
|
+
fontSize: "md",
|
|
38
|
+
// Deliberately `normal`, not `medium`: no font in the family's stack
|
|
39
|
+
// has a 500 face (the two render identically on macOS and Windows),
|
|
40
|
+
// and the call sites that cared (settings rows) want `normal`.
|
|
41
|
+
fontWeight: "normal",
|
|
42
|
+
marginEnd: "3",
|
|
43
|
+
mb: "2",
|
|
44
|
+
transitionProperty: "opacity",
|
|
45
|
+
transitionDuration: "normal",
|
|
46
|
+
// RAC stamps `data-disabled` on the field root and on the control, never
|
|
47
|
+
// on the label, so an `&[data-disabled]` rule here matches nothing — it
|
|
48
|
+
// has to come down from the root. Direct child rather than a
|
|
49
|
+
// descendant selector, as the select recipe's invalid rule: an app's own
|
|
50
|
+
// disabled form wrapper must not be able to dim every label inside it.
|
|
51
|
+
"[data-disabled] > &": { opacity: 0.4 },
|
|
52
|
+
},
|
|
53
|
+
requiredIndicator: {
|
|
54
|
+
marginStart: "1",
|
|
55
|
+
color: "danger.500",
|
|
56
|
+
},
|
|
57
|
+
helperText: {
|
|
58
|
+
// RAC's Text renders a span, and RadioGroup/CheckboxGroup roots are not
|
|
59
|
+
// flex containers to blockify it, where an inline box would drop the
|
|
60
|
+
// margin below.
|
|
61
|
+
display: "block",
|
|
62
|
+
mt: "2",
|
|
63
|
+
fontSize: "sm",
|
|
64
|
+
lineHeight: "normal",
|
|
65
|
+
color: "gray.600",
|
|
66
|
+
},
|
|
67
|
+
errorMessage: {
|
|
68
|
+
display: "flex",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
mt: "2",
|
|
71
|
+
fontSize: "sm",
|
|
72
|
+
lineHeight: "normal",
|
|
73
|
+
color: "danger.500",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
variants: {
|
|
77
|
+
// The label follows its control's size (the control itself is sized by
|
|
78
|
+
// the `input`/`select` recipes), so one `size` prop scales the whole row.
|
|
79
|
+
// Helper and error text stay `sm` at every size.
|
|
80
|
+
size: {
|
|
81
|
+
lg: { label: { fontSize: "lg" } },
|
|
82
|
+
md: { label: { fontSize: "md" } },
|
|
83
|
+
sm: { label: { fontSize: "sm" } },
|
|
84
|
+
},
|
|
85
|
+
// `side` puts the label beside its control — the settings-row pattern,
|
|
86
|
+
// where the label is a preference name and absorbs the free space. The
|
|
87
|
+
// control keeps its own width; give it one at the call site. Named after
|
|
88
|
+
// React Spectrum's `labelPosition`, not `orientation`, which RAC's
|
|
89
|
+
// RadioGroup already uses for the radios' own layout.
|
|
90
|
+
labelPosition: {
|
|
91
|
+
top: {},
|
|
92
|
+
side: {
|
|
93
|
+
root: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
// Helper and error text are full-width items, so they wrap to
|
|
97
|
+
// their own line below the label/control pair.
|
|
98
|
+
flexWrap: "wrap",
|
|
99
|
+
},
|
|
100
|
+
// Basis 0, not auto: the wrap container breaks lines on hypothetical
|
|
101
|
+
// sizes, and an auto basis is the label's max-content — a long
|
|
102
|
+
// translated label would push the control onto its own line instead
|
|
103
|
+
// of wrapping its text beside it.
|
|
104
|
+
label: { mb: "0", flex: "1 1 0" },
|
|
105
|
+
helperText: { width: "100%" },
|
|
106
|
+
errorMessage: { width: "100%" },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
defaultVariants: {
|
|
111
|
+
size: "md",
|
|
112
|
+
labelPosition: "top",
|
|
113
|
+
},
|
|
114
|
+
});
|