@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/Select.tsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, ReactNode, useContext } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
ListBox as RACListBox,
|
|
10
|
+
ListBoxItem as RACListBoxItem,
|
|
11
|
+
ListBoxItemProps as RACListBoxItemProps,
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverProps,
|
|
14
|
+
Select as RACSelect,
|
|
15
|
+
SelectProps as RACSelectProps,
|
|
16
|
+
SelectValue,
|
|
17
|
+
} from "react-aria-components";
|
|
18
|
+
import { useIntl } from "react-intl";
|
|
19
|
+
import { RiArrowDownSLine } from "react-icons/ri";
|
|
20
|
+
import { css, cx } from "styled-system/css";
|
|
21
|
+
import { field, select, SelectVariantProps } from "styled-system/recipes";
|
|
22
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
23
|
+
import {
|
|
24
|
+
FieldLabel,
|
|
25
|
+
FieldLayoutProps,
|
|
26
|
+
FieldSupport,
|
|
27
|
+
FieldSupportProps,
|
|
28
|
+
} from "./Field";
|
|
29
|
+
import { Icon } from "./Icon";
|
|
30
|
+
import { uiMessage } from "./messages";
|
|
31
|
+
|
|
32
|
+
export type SelectSlots = ReturnType<typeof select>;
|
|
33
|
+
|
|
34
|
+
// Options are children, so they can't see the variant their Select was given.
|
|
35
|
+
// The parent hands its resolved slots down, as Modal does for its own slots.
|
|
36
|
+
const SlotContext = createContext<SelectSlots>(select({}));
|
|
37
|
+
|
|
38
|
+
export const useSelectSlots = () => useContext(SlotContext);
|
|
39
|
+
|
|
40
|
+
export const SelectSlotProvider = SlotContext.Provider;
|
|
41
|
+
|
|
42
|
+
export interface SelectProps<T extends object>
|
|
43
|
+
extends Omit<
|
|
44
|
+
RACSelectProps<T>,
|
|
45
|
+
"className" | "children" | "style" | "placeholder"
|
|
46
|
+
>,
|
|
47
|
+
SelectVariantProps,
|
|
48
|
+
FieldSupportProps,
|
|
49
|
+
FieldLayoutProps {
|
|
50
|
+
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
51
|
+
label?: ReactNode;
|
|
52
|
+
/** Label style overrides. */
|
|
53
|
+
labelCss?: SystemStyleObject;
|
|
54
|
+
/**
|
|
55
|
+
* Shown in the trigger while nothing is chosen. Defaults to a translated
|
|
56
|
+
* "Select an item".
|
|
57
|
+
*/
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
/** `SelectOption`s. */
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Replaces the chevron; `null` removes it. Rarely right on a Select — the
|
|
63
|
+
* chevron is the only thing marking its trigger as a dropdown rather than
|
|
64
|
+
* a label, where a ComboBox's text input speaks for itself (which is why
|
|
65
|
+
* classroom's chevron-less autocomplete is a ComboBox).
|
|
66
|
+
*/
|
|
67
|
+
indicator?: ReactNode | null;
|
|
68
|
+
/** Placement of the dropdown relative to the trigger. */
|
|
69
|
+
placement?: PopoverProps["placement"];
|
|
70
|
+
/**
|
|
71
|
+
* Cap the dropdown's height. A prop rather than a `contentCss` rule because
|
|
72
|
+
* RAC writes its own max-height inline while positioning, which beats any
|
|
73
|
+
* class.
|
|
74
|
+
*/
|
|
75
|
+
maxHeight?: number;
|
|
76
|
+
/** Per-instance overrides for the trigger (the button the value sits in). */
|
|
77
|
+
triggerCss?: SystemStyleObject;
|
|
78
|
+
/** Per-instance overrides for the dropdown card. */
|
|
79
|
+
contentCss?: SystemStyleObject;
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Select — a listbox behind a button, for choosing one of a known set. Use
|
|
85
|
+
* ComboBox where the user should be able to type to filter.
|
|
86
|
+
*/
|
|
87
|
+
export const Select = <T extends object>({
|
|
88
|
+
label,
|
|
89
|
+
labelCss,
|
|
90
|
+
placeholder,
|
|
91
|
+
children,
|
|
92
|
+
indicator,
|
|
93
|
+
placement = "bottom start",
|
|
94
|
+
maxHeight,
|
|
95
|
+
helperText,
|
|
96
|
+
errorMessage,
|
|
97
|
+
helperTextCss,
|
|
98
|
+
labelPosition,
|
|
99
|
+
triggerCss,
|
|
100
|
+
contentCss,
|
|
101
|
+
className,
|
|
102
|
+
...props
|
|
103
|
+
}: SelectProps<T>) => {
|
|
104
|
+
// splitVariantProps, not a hand-picked list: an app preset can add variant
|
|
105
|
+
// groups to the recipe and they have to reach it.
|
|
106
|
+
const [variantProps, rest] = select.splitVariantProps(props);
|
|
107
|
+
const intl = useIntl();
|
|
108
|
+
const slots = select(variantProps);
|
|
109
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
110
|
+
return (
|
|
111
|
+
<SelectSlotProvider value={slots}>
|
|
112
|
+
<RACSelect
|
|
113
|
+
{...(rest as RACSelectProps<T>)}
|
|
114
|
+
className={cx(fieldSlots.root, slots.root, className)}
|
|
115
|
+
>
|
|
116
|
+
{label != null && (
|
|
117
|
+
<FieldLabel
|
|
118
|
+
size={variantProps.size}
|
|
119
|
+
labelPosition={labelPosition}
|
|
120
|
+
isRequired={props.isRequired}
|
|
121
|
+
css={labelCss}
|
|
122
|
+
>
|
|
123
|
+
{label}
|
|
124
|
+
</FieldLabel>
|
|
125
|
+
)}
|
|
126
|
+
<RACButton
|
|
127
|
+
className={cx(
|
|
128
|
+
slots.trigger,
|
|
129
|
+
triggerCss ? css(triggerCss) : undefined,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<SelectValue className={slots.value}>
|
|
133
|
+
{({ isPlaceholder, defaultChildren }) =>
|
|
134
|
+
isPlaceholder
|
|
135
|
+
? placeholder ??
|
|
136
|
+
intl.formatMessage(uiMessage("ui.select-placeholder"))
|
|
137
|
+
: defaultChildren
|
|
138
|
+
}
|
|
139
|
+
</SelectValue>
|
|
140
|
+
{indicator !== null && (
|
|
141
|
+
<span className={slots.indicator} aria-hidden>
|
|
142
|
+
{indicator ?? <Icon as={RiArrowDownSLine} />}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</RACButton>
|
|
146
|
+
<Popover
|
|
147
|
+
placement={placement}
|
|
148
|
+
maxHeight={maxHeight}
|
|
149
|
+
className={cx(
|
|
150
|
+
slots.content,
|
|
151
|
+
contentCss ? css(contentCss) : undefined,
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<RACListBox className={slots.list}>{children}</RACListBox>
|
|
155
|
+
</Popover>
|
|
156
|
+
<FieldSupport
|
|
157
|
+
helperText={helperText}
|
|
158
|
+
errorMessage={errorMessage}
|
|
159
|
+
helperTextCss={helperTextCss}
|
|
160
|
+
labelPosition={labelPosition}
|
|
161
|
+
/>
|
|
162
|
+
</RACSelect>
|
|
163
|
+
</SelectSlotProvider>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export interface SelectOptionProps
|
|
168
|
+
extends Omit<RACListBoxItemProps, "className" | "children" | "style"> {
|
|
169
|
+
children?: ReactNode;
|
|
170
|
+
css?: SystemStyleObject;
|
|
171
|
+
className?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A row in a `Select` or `ComboBox` list. */
|
|
175
|
+
export const SelectOption = ({
|
|
176
|
+
children,
|
|
177
|
+
css: cssProp,
|
|
178
|
+
className,
|
|
179
|
+
...rest
|
|
180
|
+
}: SelectOptionProps) => {
|
|
181
|
+
const slots = useSelectSlots();
|
|
182
|
+
return (
|
|
183
|
+
<RACListBoxItem
|
|
184
|
+
{...rest}
|
|
185
|
+
className={cx(
|
|
186
|
+
slots.option,
|
|
187
|
+
cssProp ? css(cssProp) : undefined,
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</RACListBoxItem>
|
|
193
|
+
);
|
|
194
|
+
};
|
package/src/SharedUIProvider.tsx
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
ReactNode,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { isRTL } from "react-aria";
|
|
14
|
+
import { I18nProvider } from "react-aria-components";
|
|
15
|
+
import { IntlContext } from "react-intl";
|
|
16
|
+
import { racLocale } from "./rac-locale";
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Registers the close function of the currently open dismissable overlay, or
|
|
@@ -21,27 +31,73 @@ const SharedUIContext = createContext<SharedUIContextValue | null>(null);
|
|
|
21
31
|
|
|
22
32
|
export interface SharedUIProviderProps {
|
|
23
33
|
overlayCloseRegistrar?: OverlayCloseRegistrar;
|
|
34
|
+
/**
|
|
35
|
+
* Locale for react-aria's own built-in strings. Defaults to the surrounding
|
|
36
|
+
* IntlProvider's locale, which is what apps want: pass this only where the
|
|
37
|
+
* two must differ.
|
|
38
|
+
*/
|
|
39
|
+
locale?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Keeps `<html lang>` and `<html dir>` in step with the locale, so assistive
|
|
42
|
+
* tech announces the page in the app's language and the layout runs the
|
|
43
|
+
* right way round. On by default; set false where the app doesn't own the
|
|
44
|
+
* document it's mounted in (an embedded widget), or when mounting more than
|
|
45
|
+
* one provider per page. The static `lang="en"` in index.html remains the
|
|
46
|
+
* pre-hydration default.
|
|
47
|
+
*
|
|
48
|
+
* Turning it off in an RTL locale means the app must set `dir` itself above
|
|
49
|
+
* the components, or the UI half-mirrors: react-aria mirrors from the locale
|
|
50
|
+
* in JS whatever the DOM says, while logical properties and `_rtl` rules
|
|
51
|
+
* read `dir` (see the README's RTL note).
|
|
52
|
+
*
|
|
53
|
+
* Correct only when the IntlProvider locale is the language the app
|
|
54
|
+
* actually renders in — an app falling back wholesale to its English
|
|
55
|
+
* catalog should claim `en` there, not the user's language setting (see
|
|
56
|
+
* the README's react-intl step).
|
|
57
|
+
*/
|
|
58
|
+
setDocumentLang?: boolean;
|
|
24
59
|
children: ReactNode;
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
/**
|
|
28
|
-
* SharedUIProvider — the app-side installation point for
|
|
29
|
-
* integrations
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
63
|
+
* SharedUIProvider — the app-side installation point for shared-ui
|
|
64
|
+
* integrations: the optional overlay-close registrar, and the locale for
|
|
65
|
+
* react-aria's built-in strings.
|
|
66
|
+
*
|
|
67
|
+
* This package's own strings come from react-intl, so an IntlProvider must be
|
|
68
|
+
* mounted above shared-ui components (see the package README). react-aria
|
|
69
|
+
* translates its built-in strings itself, from its own bundled catalogs, and
|
|
70
|
+
* without this provider it picks the locale from the browser rather than from
|
|
71
|
+
* the app's language setting — mount it inside the IntlProvider so the two
|
|
72
|
+
* agree.
|
|
33
73
|
*/
|
|
34
74
|
export const SharedUIProvider = ({
|
|
35
75
|
overlayCloseRegistrar,
|
|
76
|
+
locale,
|
|
77
|
+
setDocumentLang = true,
|
|
36
78
|
children,
|
|
37
79
|
}: SharedUIProviderProps) => {
|
|
80
|
+
// Read the context rather than calling useIntl(), which throws when there is
|
|
81
|
+
// no IntlProvider: react-aria falls back to the browser locale, as before.
|
|
82
|
+
const intlLocale = useContext(IntlContext)?.locale;
|
|
83
|
+
const appLocale = locale ?? intlLocale;
|
|
84
|
+
// The tag react-aria gets, so the document's direction and the components'
|
|
85
|
+
// cannot disagree; also the only form isRTL can be handed safely, as it
|
|
86
|
+
// throws on a malformed tag.
|
|
87
|
+
const sanitizedLocale = racLocale(appLocale);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (setDocumentLang && appLocale && sanitizedLocale) {
|
|
90
|
+
document.documentElement.lang = appLocale;
|
|
91
|
+
document.documentElement.dir = isRTL(sanitizedLocale) ? "rtl" : "ltr";
|
|
92
|
+
}
|
|
93
|
+
}, [setDocumentLang, appLocale, sanitizedLocale]);
|
|
38
94
|
const value = useMemo(
|
|
39
95
|
() => ({ overlayCloseRegistrar }),
|
|
40
96
|
[overlayCloseRegistrar],
|
|
41
97
|
);
|
|
42
98
|
return (
|
|
43
99
|
<SharedUIContext.Provider value={value}>
|
|
44
|
-
{children}
|
|
100
|
+
<I18nProvider locale={sanitizedLocale}>{children}</I18nProvider>
|
|
45
101
|
</SharedUIContext.Provider>
|
|
46
102
|
);
|
|
47
103
|
};
|
package/src/Skeleton.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The placeholder block, as an object rather than a precomputed class so a
|
|
12
|
+
* caller's `css` is merged into one `css()` call and its overrides win.
|
|
13
|
+
*
|
|
14
|
+
* The colours flow through a pair of custom properties, so a call site can
|
|
15
|
+
* retint one skeleton without knowing how the animation works.
|
|
16
|
+
*/
|
|
17
|
+
const skeletonBase: SystemStyleObject = {
|
|
18
|
+
"--skeleton-start-color": "token(colors.gray.100)",
|
|
19
|
+
// gray.350, the decorative-fill stop, as the Avatar default.
|
|
20
|
+
"--skeleton-end-color": "token(colors.gray.350)",
|
|
21
|
+
background: "var(--skeleton-start-color)",
|
|
22
|
+
borderColor: "var(--skeleton-end-color)",
|
|
23
|
+
opacity: 0.7,
|
|
24
|
+
borderRadius: "sm",
|
|
25
|
+
boxShadow: "none",
|
|
26
|
+
backgroundClip: "padding-box",
|
|
27
|
+
cursor: "default",
|
|
28
|
+
color: "transparent",
|
|
29
|
+
pointerEvents: "none",
|
|
30
|
+
userSelect: "none",
|
|
31
|
+
// The content is hidden rather than unmounted, so a skeleton sized from
|
|
32
|
+
// real children keeps their dimensions.
|
|
33
|
+
"&::before, &::after, *": { visibility: "hidden" },
|
|
34
|
+
animation:
|
|
35
|
+
"skeletonFade var(--skeleton-speed, 0.8s) linear infinite alternate",
|
|
36
|
+
_motionReduce: { animation: "none" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface SkeletonProps
|
|
40
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
|
|
41
|
+
/** Show the children instead of the placeholder. */
|
|
42
|
+
isLoaded?: boolean;
|
|
43
|
+
/** Seconds per pulse (default 0.8). */
|
|
44
|
+
speed?: number;
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
/** Per-instance style overrides, merged after the base. */
|
|
47
|
+
css?: SystemStyleObject;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Skeleton — a loading placeholder: a block pulsing between two greys until
|
|
53
|
+
* its content is ready.
|
|
54
|
+
*
|
|
55
|
+
* Content simply appears when `isLoaded` turns true; wrap in `Fade` where a
|
|
56
|
+
* transition matters.
|
|
57
|
+
*/
|
|
58
|
+
export const Skeleton = ({
|
|
59
|
+
isLoaded,
|
|
60
|
+
speed,
|
|
61
|
+
children,
|
|
62
|
+
css: cssProp,
|
|
63
|
+
className,
|
|
64
|
+
style,
|
|
65
|
+
...rest
|
|
66
|
+
}: SkeletonProps) => {
|
|
67
|
+
if (isLoaded) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
{...rest}
|
|
71
|
+
style={style}
|
|
72
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
{...rest}
|
|
81
|
+
style={
|
|
82
|
+
speed === undefined
|
|
83
|
+
? style
|
|
84
|
+
: ({ ...style, "--skeleton-speed": `${speed}s` } as typeof style)
|
|
85
|
+
}
|
|
86
|
+
className={cx(css({ ...skeletonBase, ...cssProp }), className)}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface SkeletonTextProps extends SkeletonProps {
|
|
94
|
+
/** How many lines to draw (default 3). */
|
|
95
|
+
noOfLines?: number;
|
|
96
|
+
/** Gap between the lines. Any CSS length. */
|
|
97
|
+
spacing?: string;
|
|
98
|
+
/** Height of each line. Any CSS length. */
|
|
99
|
+
skeletonHeight?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* SkeletonText — a paragraph-shaped `Skeleton`: evenly spaced lines, the last
|
|
104
|
+
* one short.
|
|
105
|
+
*/
|
|
106
|
+
export const SkeletonText = ({
|
|
107
|
+
noOfLines = 3,
|
|
108
|
+
spacing = "0.5rem",
|
|
109
|
+
skeletonHeight = "0.5rem",
|
|
110
|
+
isLoaded,
|
|
111
|
+
speed,
|
|
112
|
+
children,
|
|
113
|
+
css: cssProp,
|
|
114
|
+
className,
|
|
115
|
+
...rest
|
|
116
|
+
}: SkeletonTextProps) => {
|
|
117
|
+
if (isLoaded) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
{...rest}
|
|
121
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
{...rest}
|
|
130
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
131
|
+
>
|
|
132
|
+
{Array.from({ length: noOfLines }, (_, index) => (
|
|
133
|
+
<Skeleton
|
|
134
|
+
key={index}
|
|
135
|
+
speed={speed}
|
|
136
|
+
style={{
|
|
137
|
+
height: skeletonHeight,
|
|
138
|
+
// The last line is drawn at 80%, with no gap after it.
|
|
139
|
+
width: noOfLines > 1 && index === noOfLines - 1 ? "80%" : "100%",
|
|
140
|
+
marginBottom: index === noOfLines - 1 ? "0" : spacing,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
package/src/Slide.tsx
CHANGED
|
@@ -17,8 +17,8 @@ export interface SlideProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Slide —
|
|
21
|
-
*
|
|
20
|
+
* Slide — a slide-in transition, bottom edge only (add other directions when
|
|
21
|
+
* a use appears): a full-width panel pinned to the viewport bottom,
|
|
22
22
|
* translated offscreen when closed. Content stays mounted throughout, so
|
|
23
23
|
* closing doesn't drop in-flight state and the exit animates.
|
|
24
24
|
*/
|
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,20 +34,24 @@ 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;
|
|
40
41
|
/**
|
|
41
42
|
* Additional positioned overlays rendered inside the slider root
|
|
42
43
|
* (always-visible value labels, threshold markers, ...). The root is
|
|
43
|
-
* position: relative; position children absolutely
|
|
44
|
-
*
|
|
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`.
|
|
45
51
|
*/
|
|
46
52
|
children?: ReactNode;
|
|
47
53
|
/**
|
|
48
|
-
* Tooltip-styled bubble anchored above the thumb
|
|
49
|
-
* Tooltip-around-SliderThumb pattern). Rendered only while
|
|
54
|
+
* Tooltip-styled bubble anchored above the thumb. Rendered only while
|
|
50
55
|
* `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
|
|
51
56
|
* `onThumbFocusChange` and mouse handlers on an enclosing element.
|
|
52
57
|
*/
|
|
@@ -56,10 +61,7 @@ export interface SliderProps {
|
|
|
56
61
|
onThumbFocusChange?: (isFocused: boolean) => void;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/**
|
|
60
|
-
* Slider — react-aria-components <Slider> styled like Chakra's horizontal md
|
|
61
|
-
* slider.
|
|
62
|
-
*/
|
|
64
|
+
/** Slider — a styled horizontal react-aria-components <Slider>. */
|
|
63
65
|
export const Slider = ({
|
|
64
66
|
value,
|
|
65
67
|
onChange,
|
|
@@ -80,7 +82,13 @@ export const Slider = ({
|
|
|
80
82
|
onThumbFocusChange,
|
|
81
83
|
}: SliderProps) => {
|
|
82
84
|
const slots = slider();
|
|
85
|
+
const { direction } = useLocale();
|
|
83
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;
|
|
84
92
|
return (
|
|
85
93
|
<RACSlider
|
|
86
94
|
value={value}
|
|
@@ -96,7 +104,7 @@ export const Slider = ({
|
|
|
96
104
|
<div
|
|
97
105
|
data-part="mark"
|
|
98
106
|
className={cx(slots.mark, markCss ? css(markCss) : undefined)}
|
|
99
|
-
style={{ left: `${
|
|
107
|
+
style={{ left: `${offset}%` }}
|
|
100
108
|
>
|
|
101
109
|
{mark}
|
|
102
110
|
</div>
|
|
@@ -119,7 +127,7 @@ export const Slider = ({
|
|
|
119
127
|
role="presentation"
|
|
120
128
|
className={css({
|
|
121
129
|
position: "absolute",
|
|
122
|
-
bottom: "calc(50% +
|
|
130
|
+
bottom: "calc(50% + token(sizes.3.5))",
|
|
123
131
|
transform: "translateX(-50%)",
|
|
124
132
|
bg: "gray.700",
|
|
125
133
|
color: "white",
|
|
@@ -148,7 +156,7 @@ export const Slider = ({
|
|
|
148
156
|
borderTopColor: "gray.700",
|
|
149
157
|
},
|
|
150
158
|
})}
|
|
151
|
-
style={{ left: `${
|
|
159
|
+
style={{ left: `${offset}%` }}
|
|
152
160
|
>
|
|
153
161
|
{thumbTooltip}
|
|
154
162
|
</div>
|
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: {
|