@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +40 -0
- package/README.md +279 -26
- 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 +62 -0
- 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 +62 -0
- 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 +62 -0
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +19 -10
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- 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 +68 -30
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +49 -14
- package/src/Checkbox.tsx +111 -32
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +179 -0
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +72 -8
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +62 -0
- 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 -13
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +26 -12
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +81 -0
- package/src/List.tsx +8 -6
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +51 -17
- package/src/Menu.tsx +117 -2
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +137 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +41 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +108 -0
- package/src/NumberField.tsx +138 -0
- package/src/PopoverArrow.tsx +19 -5
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +108 -0
- package/src/Radio.tsx +62 -0
- 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 +78 -6
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +57 -11
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +54 -40
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +151 -46
- 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 +336 -84
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
- package/src/button-icon.ts +22 -0
- 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 +63 -0
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/hooks/usePrevious.ts +15 -0
- package/src/index.ts +29 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/Slider.recipe.ts
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Slider slot recipe —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Slider/SliderTrack/SliderThumb.
|
|
9
|
+
* Slider slot recipe — a horizontal slider (3.5-token thumb, 1-token gray.200
|
|
10
|
+
* track, blue filled track). Consumed by the shared-ui Slider, which maps the
|
|
11
|
+
* slots onto react-aria-components' Slider/SliderTrack/SliderThumb.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Geometry: the root is thumb-height with NO horizontal padding, the track is
|
|
14
|
+
* absolutely centred inside it at full width, and the thumb overhangs the
|
|
15
|
+
* track ends at 0%/100%. Call sites position overlays (marks, value labels)
|
|
16
|
+
* against that root box. Sizes are tokens, not px, so apps with resized
|
|
17
|
+
* scales (e.g. python-editor's ×0.88) keep proportional geometry.
|
|
18
|
+
*
|
|
19
|
+
* The `mark` slot is hidden until the slider has focus; the reveal lives here
|
|
20
|
+
* so call sites only style the mark's look.
|
|
17
21
|
*
|
|
18
22
|
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
19
23
|
* needs no `staticCss` entry.
|
|
@@ -26,9 +30,9 @@ export const slider = defineSlotRecipe({
|
|
|
26
30
|
position: "relative",
|
|
27
31
|
display: "block",
|
|
28
32
|
width: "100%",
|
|
33
|
+
// Thumb-height; the track centres inside.
|
|
34
|
+
height: "3.5",
|
|
29
35
|
touchAction: "none",
|
|
30
|
-
// Half the thumb so it doesn't overflow the ends (Chakra container px).
|
|
31
|
-
px: "calc(14px / 2)",
|
|
32
36
|
"&:focus-within [data-part='mark']": {
|
|
33
37
|
display: "block",
|
|
34
38
|
},
|
|
@@ -38,8 +42,12 @@ export const slider = defineSlotRecipe({
|
|
|
38
42
|
},
|
|
39
43
|
},
|
|
40
44
|
track: {
|
|
41
|
-
position: "
|
|
42
|
-
|
|
45
|
+
position: "absolute",
|
|
46
|
+
top: "50%",
|
|
47
|
+
transform: "translateY(-50%)",
|
|
48
|
+
left: 0,
|
|
49
|
+
width: "100%",
|
|
50
|
+
height: "1",
|
|
43
51
|
overflow: "hidden",
|
|
44
52
|
borderRadius: "sm",
|
|
45
53
|
bg: "gray.200",
|
|
@@ -52,15 +60,18 @@ export const slider = defineSlotRecipe({
|
|
|
52
60
|
filledTrack: {
|
|
53
61
|
position: "absolute",
|
|
54
62
|
top: 0,
|
|
55
|
-
|
|
63
|
+
// Logical, unlike the thumb: the fill grows from the track's start, so
|
|
64
|
+
// in RTL it fills from the right and the two still meet.
|
|
65
|
+
insetStart: 0,
|
|
56
66
|
height: "100%",
|
|
57
|
-
|
|
67
|
+
// Fill colour is set per call site via `filledTrackCss`.
|
|
68
|
+
bg: "brand.500",
|
|
58
69
|
},
|
|
59
70
|
thumb: {
|
|
60
71
|
position: "absolute",
|
|
61
72
|
top: "50%",
|
|
62
|
-
width: "
|
|
63
|
-
height: "
|
|
73
|
+
width: "3.5",
|
|
74
|
+
height: "3.5",
|
|
64
75
|
display: "flex",
|
|
65
76
|
alignItems: "center",
|
|
66
77
|
justifyContent: "center",
|
|
@@ -76,7 +87,7 @@ export const slider = defineSlotRecipe({
|
|
|
76
87
|
},
|
|
77
88
|
transform: "translate(-50%, -50%)",
|
|
78
89
|
"&[data-focus-visible]": {
|
|
79
|
-
|
|
90
|
+
focusRing: "outline",
|
|
80
91
|
},
|
|
81
92
|
"&[data-disabled]": {
|
|
82
93
|
bg: "gray.300",
|
package/src/Slider.tsx
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
import { ReactNode } from "react";
|
|
7
|
+
import { useLocale } from "react-aria";
|
|
7
8
|
import {
|
|
8
9
|
Slider as RACSlider,
|
|
9
10
|
SliderThumb,
|
|
@@ -33,16 +34,34 @@ export interface SliderProps {
|
|
|
33
34
|
thumbCss?: SystemStyleObject;
|
|
34
35
|
/**
|
|
35
36
|
* Positioned at the current value along the track and shown while the
|
|
36
|
-
* slider has focus
|
|
37
|
+
* slider has focus.
|
|
37
38
|
*/
|
|
38
39
|
mark?: ReactNode;
|
|
39
40
|
markCss?: SystemStyleObject;
|
|
41
|
+
/**
|
|
42
|
+
* Additional positioned overlays rendered inside the slider root
|
|
43
|
+
* (always-visible value labels, threshold markers, ...). The root is
|
|
44
|
+
* position: relative; position children absolutely.
|
|
45
|
+
*
|
|
46
|
+
* Anything sitting at a *value* along the track takes a percentage `left`,
|
|
47
|
+
* physical and mirrored by hand in RTL as the thumb is (see `offset`
|
|
48
|
+
* below), never `insetInlineStart`. Anything pinned to the track's *ends*
|
|
49
|
+
* — min/max labels and the like — is ordinary layout and should be
|
|
50
|
+
* logical, so `insetStart`/`insetEnd`.
|
|
51
|
+
*/
|
|
52
|
+
children?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Tooltip-styled bubble anchored above the thumb. Rendered only while
|
|
55
|
+
* `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
|
|
56
|
+
* `onThumbFocusChange` and mouse handlers on an enclosing element.
|
|
57
|
+
*/
|
|
58
|
+
thumbTooltip?: ReactNode;
|
|
59
|
+
isThumbTooltipOpen?: boolean;
|
|
60
|
+
/** Thumb focus tracking (react-aria focus events). */
|
|
61
|
+
onThumbFocusChange?: (isFocused: boolean) => void;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
/**
|
|
43
|
-
* Slider — react-aria-components <Slider> styled like Chakra's horizontal md
|
|
44
|
-
* slider.
|
|
45
|
-
*/
|
|
64
|
+
/** Slider — a styled horizontal react-aria-components <Slider>. */
|
|
46
65
|
export const Slider = ({
|
|
47
66
|
value,
|
|
48
67
|
onChange,
|
|
@@ -57,9 +76,19 @@ export const Slider = ({
|
|
|
57
76
|
thumbCss,
|
|
58
77
|
mark,
|
|
59
78
|
markCss,
|
|
79
|
+
children,
|
|
80
|
+
thumbTooltip,
|
|
81
|
+
isThumbTooltipOpen,
|
|
82
|
+
onThumbFocusChange,
|
|
60
83
|
}: SliderProps) => {
|
|
61
84
|
const slots = slider();
|
|
85
|
+
const { direction } = useLocale();
|
|
62
86
|
const percent = ((value - minValue) / (maxValue - minValue)) * 100;
|
|
87
|
+
// react-aria positions the thumb with a physical `left` that it mirrors
|
|
88
|
+
// itself in RTL (useSliderThumb), so anything tracking the thumb has to
|
|
89
|
+
// mirror the same way rather than going logical — `insetInlineStart` would
|
|
90
|
+
// put these on the opposite side of the track from the thumb.
|
|
91
|
+
const offset = direction === "rtl" ? 100 - percent : percent;
|
|
63
92
|
return (
|
|
64
93
|
<RACSlider
|
|
65
94
|
value={value}
|
|
@@ -75,7 +104,7 @@ export const Slider = ({
|
|
|
75
104
|
<div
|
|
76
105
|
data-part="mark"
|
|
77
106
|
className={cx(slots.mark, markCss ? css(markCss) : undefined)}
|
|
78
|
-
style={{ left: `${
|
|
107
|
+
style={{ left: `${offset}%` }}
|
|
79
108
|
>
|
|
80
109
|
{mark}
|
|
81
110
|
</div>
|
|
@@ -91,9 +120,52 @@ export const Slider = ({
|
|
|
91
120
|
style={{ width: `${percent}%` }}
|
|
92
121
|
/>
|
|
93
122
|
</SliderTrack>
|
|
123
|
+
{thumbTooltip && isThumbTooltipOpen && (
|
|
124
|
+
// Matches the shared Tooltip's look (tooltipBase) with a bottom
|
|
125
|
+
// arrow, anchored above the thumb.
|
|
126
|
+
<div
|
|
127
|
+
role="presentation"
|
|
128
|
+
className={css({
|
|
129
|
+
position: "absolute",
|
|
130
|
+
bottom: "calc(50% + token(sizes.3.5))",
|
|
131
|
+
transform: "translateX(-50%)",
|
|
132
|
+
bg: "gray.700",
|
|
133
|
+
color: "white",
|
|
134
|
+
px: "2",
|
|
135
|
+
py: "1",
|
|
136
|
+
borderRadius: "md",
|
|
137
|
+
fontSize: "sm",
|
|
138
|
+
fontWeight: "medium",
|
|
139
|
+
boxShadow: "md",
|
|
140
|
+
zIndex: "tooltip",
|
|
141
|
+
whiteSpace: "nowrap",
|
|
142
|
+
_after: {
|
|
143
|
+
content: '""',
|
|
144
|
+
position: "absolute",
|
|
145
|
+
// 1px into the box so subpixel edges can't antialias into a
|
|
146
|
+
// hairline seam (see PopoverArrow).
|
|
147
|
+
top: "calc(100% - 1px)",
|
|
148
|
+
left: "50%",
|
|
149
|
+
transform: "translateX(-50%)",
|
|
150
|
+
borderWidth: "4px",
|
|
151
|
+
borderStyle: "solid",
|
|
152
|
+
// Per-side: tokens don't resolve inside multi-value
|
|
153
|
+
// shorthands (they emit verbatim and the browser drops the
|
|
154
|
+
// invalid declaration).
|
|
155
|
+
borderColor: "transparent",
|
|
156
|
+
borderTopColor: "gray.700",
|
|
157
|
+
},
|
|
158
|
+
})}
|
|
159
|
+
style={{ left: `${offset}%` }}
|
|
160
|
+
>
|
|
161
|
+
{thumbTooltip}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
94
164
|
<SliderThumb
|
|
165
|
+
onFocusChange={onThumbFocusChange}
|
|
95
166
|
className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
|
|
96
167
|
/>
|
|
168
|
+
{children}
|
|
97
169
|
</RACSlider>
|
|
98
170
|
);
|
|
99
171
|
};
|
package/src/Spinner.tsx
CHANGED
|
@@ -5,29 +5,29 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { CSSProperties } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { dataAttrs } from "./data-attrs";
|
|
8
9
|
import { SystemStyleObject } from "styled-system/types";
|
|
9
10
|
|
|
10
11
|
export interface SpinnerProps {
|
|
11
|
-
/**
|
|
12
|
+
/** sm is 1rem, md 1.5rem (default). */
|
|
12
13
|
size?: "sm" | "md";
|
|
13
14
|
/**
|
|
14
|
-
* Revolution time (
|
|
15
|
-
*
|
|
15
|
+
* Revolution time (default 0.45s). Under prefers-reduced-motion the spin is
|
|
16
|
+
* slowed to ~3x this value.
|
|
16
17
|
*/
|
|
17
18
|
speed?: string;
|
|
18
19
|
/** Per-instance style overrides, merged after the base. */
|
|
19
20
|
css?: SystemStyleObject;
|
|
20
21
|
className?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Accessible name. Required: Chakra's Spinner announced a visually hidden
|
|
23
|
-
* "Loading..." by default, so a nameless spinner would regress on it.
|
|
24
|
-
*/
|
|
22
|
+
/** Accessible name. Required: a spinner must announce what is loading. */
|
|
25
23
|
"aria-label": string;
|
|
24
|
+
/** `data-*` attributes land on the spinner, for tests that wait on it. */
|
|
25
|
+
[key: `data-${string}`]: unknown;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Spinner —
|
|
30
|
-
*
|
|
29
|
+
* Spinner — a border-based spinner (currentColor with transparent bottom/left
|
|
30
|
+
* quadrants).
|
|
31
31
|
*/
|
|
32
32
|
export const Spinner = ({
|
|
33
33
|
size = "md",
|
|
@@ -35,8 +35,10 @@ export const Spinner = ({
|
|
|
35
35
|
css: cssProp,
|
|
36
36
|
className,
|
|
37
37
|
"aria-label": ariaLabel,
|
|
38
|
+
...rest
|
|
38
39
|
}: SpinnerProps) => (
|
|
39
40
|
<span
|
|
41
|
+
{...dataAttrs(rest)}
|
|
40
42
|
role="status"
|
|
41
43
|
aria-label={ariaLabel}
|
|
42
44
|
style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
|
package/src/Svg.tsx
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
import { styled } from "styled-system/jsx";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Svg — a Panda-styled svg element with
|
|
9
|
+
* Svg — a Panda-styled svg element with Icon's base sizing (1em,
|
|
10
10
|
* inline-block). For custom-path icons; react-icons glyphs use `Icon`.
|
|
11
|
-
* As a styled-factory component its call-site style props are extracted
|
|
12
|
-
* (unlike style props forwarded through a plain wrapper — gotcha #9).
|
|
11
|
+
* As a styled-factory component its call-site style props are extracted.
|
|
13
12
|
*/
|
|
14
13
|
export const Svg = styled("svg", {
|
|
15
14
|
base: {
|
package/src/Switch.recipe.ts
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Switch slot recipe
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Switch slot recipe with an sm/md/lg size scale. The track has a 2px inset;
|
|
10
|
+
* the thumb matches the track height and slides by the track width/height
|
|
11
|
+
* difference when selected.
|
|
12
12
|
*
|
|
13
13
|
* State styling keys off data attributes stamped by the shared-ui Switch
|
|
14
14
|
* (react-aria provides the state via render props).
|
|
15
15
|
*
|
|
16
|
-
* Registered in the base preset (base-preset.ts)
|
|
17
|
-
*
|
|
16
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
17
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
18
18
|
*/
|
|
19
19
|
export const switchRecipe = defineSlotRecipe({
|
|
20
20
|
className: "switch",
|
|
@@ -35,8 +35,6 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
35
35
|
boxSizing: "content-box",
|
|
36
36
|
borderRadius: "full",
|
|
37
37
|
p: "0.5",
|
|
38
|
-
width: "1.875rem",
|
|
39
|
-
height: "4",
|
|
40
38
|
transitionProperty: "background-color",
|
|
41
39
|
transitionDuration: "fast",
|
|
42
40
|
bg: "gray.300",
|
|
@@ -44,7 +42,7 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
44
42
|
bg: "controlCheckedBg",
|
|
45
43
|
},
|
|
46
44
|
"&[data-focus-visible]": {
|
|
47
|
-
|
|
45
|
+
focusRing: "outline",
|
|
48
46
|
},
|
|
49
47
|
"&[data-disabled]": { opacity: 0.4 },
|
|
50
48
|
// Forced-colors modes strip author backgrounds, flattening track and
|
|
@@ -61,10 +59,18 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
61
59
|
transitionProperty: "transform",
|
|
62
60
|
transitionDuration: "normal",
|
|
63
61
|
borderRadius: "inherit",
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// The thumb rests at the track's start and slides to its end, so the
|
|
63
|
+
// travel has to reverse in RTL — a transform is physical however the
|
|
64
|
+
// rest of the switch is laid out. Distance is per size, below.
|
|
65
|
+
// Fallbacks carry a unit: `calc(-1 * 0)` is a number, which is invalid
|
|
66
|
+
// where a length is wanted, and would drop the RTL transform whole.
|
|
66
67
|
"&[data-selected]": {
|
|
67
|
-
transform: "translateX(
|
|
68
|
+
transform: "translateX(var(--switch-travel, 0px))",
|
|
69
|
+
},
|
|
70
|
+
_rtl: {
|
|
71
|
+
"&[data-selected]": {
|
|
72
|
+
transform: "translateX(calc(-1 * var(--switch-travel, 0px)))",
|
|
73
|
+
},
|
|
68
74
|
},
|
|
69
75
|
_forcedColors: {
|
|
70
76
|
bg: "ButtonText",
|
|
@@ -77,4 +83,44 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
77
83
|
"&[data-disabled]": { opacity: 0.4 },
|
|
78
84
|
},
|
|
79
85
|
},
|
|
86
|
+
variants: {
|
|
87
|
+
// `start` puts the label first and the control at the row's end — the
|
|
88
|
+
// settings-row pattern (a preference name beside its switch), the toggle
|
|
89
|
+
// counterpart of the field recipe's `labelPosition="side"`. Values are
|
|
90
|
+
// start/end rather than top/side because the default label is already
|
|
91
|
+
// beside the control, after it. The label keeps an end margin so a long
|
|
92
|
+
// translation can't butt against the track (the SelectFormControl
|
|
93
|
+
// lesson — see the field-chrome roadmap bullet).
|
|
94
|
+
labelPosition: {
|
|
95
|
+
end: {},
|
|
96
|
+
start: {
|
|
97
|
+
root: {
|
|
98
|
+
flexDirection: "row-reverse",
|
|
99
|
+
justifyContent: "space-between",
|
|
100
|
+
width: "100%",
|
|
101
|
+
},
|
|
102
|
+
label: { marginStart: "0", marginEnd: "3" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
// Track width x height per size; the thumb's travel is the difference
|
|
106
|
+
// between them.
|
|
107
|
+
size: {
|
|
108
|
+
sm: {
|
|
109
|
+
track: { width: "1.375rem", height: "3" },
|
|
110
|
+
thumb: { width: "3", height: "3", "--switch-travel": "0.625rem" },
|
|
111
|
+
},
|
|
112
|
+
md: {
|
|
113
|
+
track: { width: "1.875rem", height: "4" },
|
|
114
|
+
thumb: { width: "4", height: "4", "--switch-travel": "0.875rem" },
|
|
115
|
+
},
|
|
116
|
+
lg: {
|
|
117
|
+
track: { width: "2.875rem", height: "6" },
|
|
118
|
+
thumb: { width: "6", height: "6", "--switch-travel": "1.375rem" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
defaultVariants: {
|
|
123
|
+
size: "md",
|
|
124
|
+
labelPosition: "end",
|
|
125
|
+
},
|
|
80
126
|
});
|
package/src/Switch.tsx
CHANGED
|
@@ -3,38 +3,59 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import { ReactNode } from "react";
|
|
6
|
+
import { ReactNode, useId } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Switch as RACSwitch,
|
|
9
9
|
SwitchProps as RACSwitchProps,
|
|
10
10
|
} from "react-aria-components";
|
|
11
11
|
import { css, cx } from "styled-system/css";
|
|
12
|
-
import { switchRecipe } from "styled-system/recipes";
|
|
12
|
+
import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
import { FieldHelperText } from "./Field";
|
|
14
15
|
|
|
15
16
|
export interface SwitchProps
|
|
16
|
-
extends Omit<RACSwitchProps, "className" | "children" | "style"
|
|
17
|
+
extends Omit<RACSwitchProps, "className" | "children" | "style">,
|
|
18
|
+
SwitchRecipeVariantProps {
|
|
17
19
|
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
18
20
|
css?: SystemStyleObject;
|
|
19
21
|
className?: string;
|
|
20
22
|
children?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Help text below the switch, wired to its `aria-describedby` — the same
|
|
25
|
+
* chrome the labelled fields' `helperText` renders. With it the component
|
|
26
|
+
* gains a wrapping `<div>`, so the switch-plus-text moves as one block.
|
|
27
|
+
*/
|
|
28
|
+
helperText?: ReactNode;
|
|
29
|
+
/** Per-instance style overrides for the helper text. */
|
|
30
|
+
helperTextCss?: SystemStyleObject;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
|
-
* Switch — react-aria-components <Switch
|
|
25
|
-
*
|
|
34
|
+
* Switch — a styled react-aria-components <Switch>. Children render as the
|
|
35
|
+
* label; pass `aria-label` for label-less switches. `labelPosition="start"`
|
|
36
|
+
* is the settings-row layout: label first, switch at the row's end.
|
|
26
37
|
*/
|
|
27
38
|
export const Switch = ({
|
|
39
|
+
size,
|
|
40
|
+
labelPosition,
|
|
28
41
|
css: cssProp,
|
|
29
42
|
className,
|
|
30
43
|
children,
|
|
44
|
+
helperText,
|
|
45
|
+
helperTextCss,
|
|
31
46
|
...rest
|
|
32
47
|
}: SwitchProps) => {
|
|
33
|
-
const slots = switchRecipe();
|
|
34
|
-
|
|
48
|
+
const slots = switchRecipe({ size, labelPosition });
|
|
49
|
+
const helperId = useId();
|
|
50
|
+
const describedBy =
|
|
51
|
+
[rest["aria-describedby"], helperText != null ? helperId : undefined]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" ") || undefined;
|
|
54
|
+
const switchElement = (
|
|
35
55
|
<RACSwitch
|
|
36
56
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
37
57
|
{...rest}
|
|
58
|
+
aria-describedby={describedBy}
|
|
38
59
|
>
|
|
39
60
|
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
40
61
|
const state = {
|
|
@@ -57,4 +78,15 @@ export const Switch = ({
|
|
|
57
78
|
}}
|
|
58
79
|
</RACSwitch>
|
|
59
80
|
);
|
|
81
|
+
if (helperText == null) {
|
|
82
|
+
return switchElement;
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
{switchElement}
|
|
87
|
+
<FieldHelperText id={helperId} css={helperTextCss}>
|
|
88
|
+
{helperText}
|
|
89
|
+
</FieldHelperText>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
60
92
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Text recipe — sizes only, no default: with no `size`, Text inherits, which
|
|
10
|
+
* is what most family apps expect. A config recipe so an app preset can set
|
|
11
|
+
* an app-wide default size by extending `defaultVariants` (python-editor
|
|
12
|
+
* defaults to md, which its shrunken fontSize scale renders at 0.9rem).
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts).
|
|
15
|
+
*/
|
|
16
|
+
export const text = defineRecipe({
|
|
17
|
+
className: "text",
|
|
18
|
+
jsx: ["Text"],
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
sm: { fontSize: "sm" },
|
|
22
|
+
md: { fontSize: "md" },
|
|
23
|
+
lg: { fontSize: "lg" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
package/src/Text.tsx
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
import { styled } from "styled-system/jsx";
|
|
7
|
+
import { text } from "styled-system/recipes";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Text — a paragraph that accepts Panda style props. Use `as` to render a
|
|
10
|
-
* different element (`span`, `div`, `h2`).
|
|
11
|
+
* different element (`span`, `div`, `h2`).
|
|
12
|
+
* Backed by the `text` config recipe: `size` picks a type-scale size, and an
|
|
13
|
+
* app preset can extend the recipe's `defaultVariants` to give all its Text
|
|
14
|
+
* a default size (see Text.recipe.ts).
|
|
11
15
|
*/
|
|
12
|
-
export const Text = styled("p");
|
|
16
|
+
export const Text = styled("p", text);
|
package/src/TextField.tsx
CHANGED
|
@@ -5,67 +5,81 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { FocusEvent, forwardRef, ReactNode } from "react";
|
|
7
7
|
import {
|
|
8
|
-
FieldError,
|
|
9
8
|
Input as RACInput,
|
|
10
|
-
Label as RACLabel,
|
|
11
|
-
Text as RACText,
|
|
12
9
|
TextField as RACTextField,
|
|
13
10
|
TextFieldProps as RACTextFieldProps,
|
|
14
11
|
} from "react-aria-components";
|
|
15
|
-
import {
|
|
16
|
-
import { field, input } from "styled-system/recipes";
|
|
12
|
+
import { field, input, InputVariantProps } from "styled-system/recipes";
|
|
17
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
import {
|
|
15
|
+
FieldLabel,
|
|
16
|
+
FieldLayoutProps,
|
|
17
|
+
FieldSupport,
|
|
18
|
+
FieldSupportProps,
|
|
19
|
+
} from "./Field";
|
|
18
20
|
|
|
19
21
|
export interface TextFieldProps
|
|
20
22
|
extends Omit<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
RACTextFieldProps,
|
|
24
|
+
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
25
|
+
>,
|
|
26
|
+
InputVariantProps,
|
|
27
|
+
FieldSupportProps,
|
|
28
|
+
FieldLayoutProps {
|
|
29
|
+
/** Visible label (asterisk added when `isRequired`). */
|
|
25
30
|
label: ReactNode;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
/** Shown below the input when `isInvalid` (Chakra's FormErrorMessage). */
|
|
29
|
-
errorMessage?: ReactNode;
|
|
30
|
-
/** Per-instance style overrides for the helper text. */
|
|
31
|
-
helperTextCss?: SystemStyleObject;
|
|
31
|
+
/** Label style overrides. */
|
|
32
|
+
labelCss?: SystemStyleObject;
|
|
32
33
|
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
34
|
+
/** Input autocapitalize attribute (react-aria's TextField omits it). */
|
|
35
|
+
autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
* TextField — a labelled single-line text input
|
|
37
|
-
*
|
|
38
|
-
* forwarded to the input element.
|
|
39
|
+
* TextField — a labelled single-line text input with optional help and error
|
|
40
|
+
* text. The ref is forwarded to the input element.
|
|
39
41
|
*/
|
|
40
42
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
41
43
|
function TextField(
|
|
42
|
-
{
|
|
44
|
+
{
|
|
45
|
+
label,
|
|
46
|
+
labelCss,
|
|
47
|
+
helperText,
|
|
48
|
+
errorMessage,
|
|
49
|
+
helperTextCss,
|
|
50
|
+
labelPosition,
|
|
51
|
+
onFocus,
|
|
52
|
+
autoCapitalize,
|
|
53
|
+
...props
|
|
54
|
+
},
|
|
43
55
|
ref,
|
|
44
56
|
) {
|
|
45
|
-
|
|
57
|
+
// As Input: forward every recipe variant group, not just `size`, so a
|
|
58
|
+
// preset that adds one keeps working.
|
|
59
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
60
|
+
const slots = field({ size: variantProps.size, labelPosition });
|
|
46
61
|
return (
|
|
47
62
|
<RACTextField {...rest} className={slots.root}>
|
|
48
|
-
<
|
|
63
|
+
<FieldLabel
|
|
64
|
+
size={variantProps.size}
|
|
65
|
+
labelPosition={labelPosition}
|
|
66
|
+
isRequired={rest.isRequired}
|
|
67
|
+
css={labelCss}
|
|
68
|
+
>
|
|
49
69
|
{label}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)}
|
|
64
|
-
>
|
|
65
|
-
{helperText}
|
|
66
|
-
</RACText>
|
|
67
|
-
)}
|
|
68
|
-
<FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
|
|
70
|
+
</FieldLabel>
|
|
71
|
+
<RACInput
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={input(variantProps)}
|
|
74
|
+
onFocus={onFocus}
|
|
75
|
+
autoCapitalize={autoCapitalize}
|
|
76
|
+
/>
|
|
77
|
+
<FieldSupport
|
|
78
|
+
helperText={helperText}
|
|
79
|
+
errorMessage={errorMessage}
|
|
80
|
+
helperTextCss={helperTextCss}
|
|
81
|
+
labelPosition={labelPosition}
|
|
82
|
+
/>
|
|
69
83
|
</RACTextField>
|
|
70
84
|
);
|
|
71
85
|
},
|