@microbit/ui 0.1.0-alpha.3
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 +21 -0
- package/README.md +118 -0
- package/lang/ui.ca.json +22 -0
- package/lang/ui.en-us.json +22 -0
- package/lang/ui.en.json +22 -0
- package/lang/ui.es-es.json +22 -0
- package/lang/ui.fr.json +22 -0
- package/lang/ui.ja.json +22 -0
- package/lang/ui.ko.json +22 -0
- package/lang/ui.lol.json +22 -0
- package/lang/ui.nl.json +22 -0
- package/lang/ui.pl.json +22 -0
- package/lang/ui.pt-br.json +22 -0
- package/lang/ui.zh-tw.json +22 -0
- package/package.json +54 -0
- package/src/Button.recipe.ts +162 -0
- package/src/Button.tsx +84 -0
- package/src/ButtonGroup.tsx +63 -0
- package/src/Card.recipe.ts +49 -0
- package/src/Card.tsx +56 -0
- package/src/Checkbox.recipe.ts +71 -0
- package/src/Checkbox.tsx +66 -0
- package/src/CloseButton.tsx +87 -0
- package/src/CloseIcon.tsx +40 -0
- package/src/Divider.tsx +21 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +55 -0
- package/src/IconButton.tsx +39 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +63 -0
- package/src/Input.tsx +33 -0
- package/src/InputGroup.tsx +48 -0
- package/src/Link.tsx +23 -0
- package/src/LinkBox.tsx +88 -0
- package/src/List.tsx +27 -0
- package/src/Menu.recipe.ts +88 -0
- package/src/Menu.tsx +172 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +265 -0
- package/src/NativeSelect.tsx +76 -0
- package/src/PopoverArrow.tsx +52 -0
- package/src/ProgressBar.tsx +61 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +91 -0
- package/src/Slider.tsx +99 -0
- package/src/Spinner.tsx +69 -0
- package/src/Svg.tsx +23 -0
- package/src/Switch.recipe.ts +80 -0
- package/src/Switch.tsx +60 -0
- package/src/Text.tsx +12 -0
- package/src/TextField.recipe.ts +54 -0
- package/src/TextField.tsx +72 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +165 -0
- package/src/Tooltip.tsx +91 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +278 -0
- package/src/chakra-tokens.ts +1014 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/index.ts +45 -0
- package/src/messages.ts +17 -0
- package/src/system.ts +36 -0
|
@@ -0,0 +1,54 @@
|
|
|
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 — Chakra's FormControl parts (FormLabel/FormHelperText/
|
|
10
|
+
* FormErrorMessage, light mode). Consumed by the shared-ui TextField, which
|
|
11
|
+
* maps the slots onto react-aria-components' TextField/Label/Text/FieldError;
|
|
12
|
+
* the input itself is styled by the `input` recipe (Input.recipe.ts).
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
15
|
+
* needs no `staticCss` entry.
|
|
16
|
+
*/
|
|
17
|
+
export const field = defineSlotRecipe({
|
|
18
|
+
className: "field",
|
|
19
|
+
slots: ["root", "label", "requiredIndicator", "helperText", "errorMessage"],
|
|
20
|
+
base: {
|
|
21
|
+
root: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
alignItems: "stretch",
|
|
25
|
+
width: "100%",
|
|
26
|
+
},
|
|
27
|
+
label: {
|
|
28
|
+
display: "block",
|
|
29
|
+
fontSize: "md",
|
|
30
|
+
fontWeight: "medium",
|
|
31
|
+
marginEnd: "3",
|
|
32
|
+
mb: "2",
|
|
33
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
34
|
+
},
|
|
35
|
+
requiredIndicator: {
|
|
36
|
+
marginStart: "1",
|
|
37
|
+
color: "danger.500",
|
|
38
|
+
},
|
|
39
|
+
helperText: {
|
|
40
|
+
mt: "2",
|
|
41
|
+
fontSize: "sm",
|
|
42
|
+
lineHeight: "normal",
|
|
43
|
+
color: "gray.600",
|
|
44
|
+
},
|
|
45
|
+
errorMessage: {
|
|
46
|
+
display: "flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
mt: "2",
|
|
49
|
+
fontSize: "sm",
|
|
50
|
+
lineHeight: "normal",
|
|
51
|
+
color: "danger.500",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { FocusEvent, forwardRef, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
FieldError,
|
|
9
|
+
Input as RACInput,
|
|
10
|
+
Label as RACLabel,
|
|
11
|
+
Text as RACText,
|
|
12
|
+
TextField as RACTextField,
|
|
13
|
+
TextFieldProps as RACTextFieldProps,
|
|
14
|
+
} from "react-aria-components";
|
|
15
|
+
import { css, cx } from "styled-system/css";
|
|
16
|
+
import { field, input } from "styled-system/recipes";
|
|
17
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
18
|
+
|
|
19
|
+
export interface TextFieldProps
|
|
20
|
+
extends Omit<
|
|
21
|
+
RACTextFieldProps,
|
|
22
|
+
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
23
|
+
> {
|
|
24
|
+
/** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
|
|
25
|
+
label: ReactNode;
|
|
26
|
+
/** Help text below the input (Chakra's FormHelperText). */
|
|
27
|
+
helperText?: ReactNode;
|
|
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;
|
|
32
|
+
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* TextField — a labelled single-line text input, collapsing Chakra's
|
|
37
|
+
* FormControl/FormLabel/Input/FormHelperText/FormErrorMessage. The ref is
|
|
38
|
+
* forwarded to the input element.
|
|
39
|
+
*/
|
|
40
|
+
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
41
|
+
function TextField(
|
|
42
|
+
{ label, helperText, errorMessage, helperTextCss, onFocus, ...rest },
|
|
43
|
+
ref,
|
|
44
|
+
) {
|
|
45
|
+
const slots = field();
|
|
46
|
+
return (
|
|
47
|
+
<RACTextField {...rest} className={slots.root}>
|
|
48
|
+
<RACLabel className={slots.label}>
|
|
49
|
+
{label}
|
|
50
|
+
{rest.isRequired ? (
|
|
51
|
+
<span aria-hidden className={slots.requiredIndicator}>
|
|
52
|
+
*
|
|
53
|
+
</span>
|
|
54
|
+
) : null}
|
|
55
|
+
</RACLabel>
|
|
56
|
+
<RACInput ref={ref} className={input()} onFocus={onFocus} />
|
|
57
|
+
{helperText && (
|
|
58
|
+
<RACText
|
|
59
|
+
slot="description"
|
|
60
|
+
className={cx(
|
|
61
|
+
slots.helperText,
|
|
62
|
+
helperTextCss ? css(helperTextCss) : undefined,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{helperText}
|
|
66
|
+
</RACText>
|
|
67
|
+
)}
|
|
68
|
+
<FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
|
|
69
|
+
</RACTextField>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
* Toast slot recipe — the Chakra-era solid Alert restyled as a toast (white
|
|
10
|
+
* text, teal for every status except error). Status colours come from the
|
|
11
|
+
* `toast*Bg` semantic tokens so brand presets can retune them without
|
|
12
|
+
* touching the recipe.
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). The status variant
|
|
15
|
+
* is chosen at runtime from the toast content, so it needs the preset's
|
|
16
|
+
* `staticCss` entry.
|
|
17
|
+
*/
|
|
18
|
+
export const toast = defineSlotRecipe({
|
|
19
|
+
className: "toast",
|
|
20
|
+
slots: ["region", "root", "icon", "title", "description", "closeButton"],
|
|
21
|
+
base: {
|
|
22
|
+
region: {
|
|
23
|
+
position: "fixed",
|
|
24
|
+
top: "4",
|
|
25
|
+
left: "50%",
|
|
26
|
+
transform: "translateX(-50%)",
|
|
27
|
+
zIndex: "toast",
|
|
28
|
+
display: "flex",
|
|
29
|
+
flexDirection: "column",
|
|
30
|
+
gap: "2",
|
|
31
|
+
pointerEvents: "none",
|
|
32
|
+
},
|
|
33
|
+
root: {
|
|
34
|
+
position: "relative",
|
|
35
|
+
display: "flex",
|
|
36
|
+
alignItems: "flex-start",
|
|
37
|
+
gap: "3",
|
|
38
|
+
p: "4",
|
|
39
|
+
// Leave room for the absolutely-positioned close button plus breathing
|
|
40
|
+
// space so its hover area never overlaps the title text (24px button at
|
|
41
|
+
// 4px inset spans ~4-28px from the right; content stops at 40px for a
|
|
42
|
+
// ~12px gap).
|
|
43
|
+
paddingRight: "10",
|
|
44
|
+
borderRadius: "md",
|
|
45
|
+
boxShadow: "lg",
|
|
46
|
+
color: "white",
|
|
47
|
+
maxW: "sm",
|
|
48
|
+
pointerEvents: "auto",
|
|
49
|
+
},
|
|
50
|
+
icon: {
|
|
51
|
+
fontSize: "1.25rem",
|
|
52
|
+
flexShrink: 0,
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
fontWeight: "bold",
|
|
56
|
+
},
|
|
57
|
+
description: {
|
|
58
|
+
mt: "1",
|
|
59
|
+
},
|
|
60
|
+
closeButton: {
|
|
61
|
+
// A sized box with the glyph centred (like Chakra's CloseButton size
|
|
62
|
+
// "sm": 24px box, 2xs glyph) gives padding around the X and a hover
|
|
63
|
+
// affordance.
|
|
64
|
+
position: "absolute",
|
|
65
|
+
top: "1",
|
|
66
|
+
insetEnd: "1",
|
|
67
|
+
color: "white",
|
|
68
|
+
display: "inline-flex",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
justifyContent: "center",
|
|
71
|
+
width: "6",
|
|
72
|
+
height: "6",
|
|
73
|
+
padding: "0",
|
|
74
|
+
fontSize: "2xs",
|
|
75
|
+
borderRadius: "md",
|
|
76
|
+
cursor: "pointer",
|
|
77
|
+
bg: "transparent",
|
|
78
|
+
border: "none",
|
|
79
|
+
outline: "none",
|
|
80
|
+
transitionProperty: "background-color, box-shadow",
|
|
81
|
+
transitionDuration: "normal",
|
|
82
|
+
// Chakra's CloseButton hover is a subtle dark overlay (blackAlpha) in
|
|
83
|
+
// light mode, not a bright highlight.
|
|
84
|
+
_hover: { bg: "blackAlpha.100" },
|
|
85
|
+
_active: { bg: "blackAlpha.200" },
|
|
86
|
+
_focusVisible: { focusShadow: "outline" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
variants: {
|
|
90
|
+
status: {
|
|
91
|
+
info: { root: { bg: "toastInfoBg" } },
|
|
92
|
+
success: { root: { bg: "toastSuccessBg" } },
|
|
93
|
+
warning: { root: { bg: "toastWarningBg" } },
|
|
94
|
+
error: { root: { bg: "toastErrorBg" } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
defaultVariants: { status: "info" },
|
|
98
|
+
});
|
package/src/Toast.tsx
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode, useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
UNSTABLE_Toast as RACToast,
|
|
10
|
+
UNSTABLE_ToastContent as RACToastContent,
|
|
11
|
+
UNSTABLE_ToastQueue as RACToastQueue,
|
|
12
|
+
UNSTABLE_ToastRegion as RACToastRegion,
|
|
13
|
+
} from "react-aria-components";
|
|
14
|
+
import { useIntl } from "react-intl";
|
|
15
|
+
import { IconType } from "react-icons/lib";
|
|
16
|
+
import {
|
|
17
|
+
RiAlertFill,
|
|
18
|
+
RiCheckboxCircleFill,
|
|
19
|
+
RiErrorWarningFill,
|
|
20
|
+
RiInformationFill,
|
|
21
|
+
} from "react-icons/ri";
|
|
22
|
+
import { toast as toastRecipe } from "styled-system/recipes";
|
|
23
|
+
import { CloseIcon } from "./CloseIcon";
|
|
24
|
+
import { Icon } from "./Icon";
|
|
25
|
+
import { uiMessage } from "./messages";
|
|
26
|
+
import { VisuallyHidden } from "./VisuallyHidden";
|
|
27
|
+
|
|
28
|
+
export type ToastStatus = "info" | "success" | "warning" | "error";
|
|
29
|
+
|
|
30
|
+
export interface ToastContent {
|
|
31
|
+
/** Dedup key: adding a toast whose id is already visible is a no-op. */
|
|
32
|
+
id?: string;
|
|
33
|
+
title?: ReactNode;
|
|
34
|
+
description?: ReactNode;
|
|
35
|
+
status?: ToastStatus;
|
|
36
|
+
isClosable?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Module-level queue shared by useToast() and the <ToastProvider/> region.
|
|
40
|
+
// (RAC's Toast API is still flagged UNSTABLE_*; the surface is small and behind
|
|
41
|
+
// this module, so a swap to a custom queue later is contained.)
|
|
42
|
+
export const toastQueue = new RACToastQueue<ToastContent>({
|
|
43
|
+
maxVisibleToasts: 5,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Status icon matching Chakra's AlertIcon (filled glyphs, coloured by the
|
|
47
|
+
// toast foreground = white here). Warning is a triangle, error a circle, as
|
|
48
|
+
// in Chakra — the glyph must distinguish them because the colours alone
|
|
49
|
+
// don't reliably. The icon is decorative; the status is announced via the
|
|
50
|
+
// visually hidden status text.
|
|
51
|
+
const statusIcon: Record<ToastStatus, IconType> = {
|
|
52
|
+
info: RiInformationFill,
|
|
53
|
+
success: RiCheckboxCircleFill,
|
|
54
|
+
warning: RiAlertFill,
|
|
55
|
+
error: RiErrorWarningFill,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mount once near the app root, inside the IntlProvider (the close button's
|
|
60
|
+
* label and the status announcements are react-intl messages).
|
|
61
|
+
* Renders the live region that announces and displays queued toasts.
|
|
62
|
+
*/
|
|
63
|
+
export const ToastProvider = () => {
|
|
64
|
+
const intl = useIntl();
|
|
65
|
+
const slots = toastRecipe();
|
|
66
|
+
return (
|
|
67
|
+
<RACToastRegion queue={toastQueue} className={slots.region}>
|
|
68
|
+
{({ toast }) => {
|
|
69
|
+
const status = toast.content.status ?? "info";
|
|
70
|
+
return (
|
|
71
|
+
<RACToast toast={toast} className={toastRecipe({ status }).root}>
|
|
72
|
+
<Icon as={statusIcon[status]} className={slots.icon} aria-hidden />
|
|
73
|
+
<RACToastContent>
|
|
74
|
+
{/* Colour and icon are the only visible status signals; say it
|
|
75
|
+
for assistive tech too. */}
|
|
76
|
+
<VisuallyHidden>
|
|
77
|
+
{intl.formatMessage(uiMessage(`ui.toast-status-${status}`))}{" "}
|
|
78
|
+
</VisuallyHidden>
|
|
79
|
+
{toast.content.title && (
|
|
80
|
+
<p className={slots.title}>{toast.content.title}</p>
|
|
81
|
+
)}
|
|
82
|
+
{toast.content.description && (
|
|
83
|
+
<div className={slots.description}>
|
|
84
|
+
{toast.content.description}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</RACToastContent>
|
|
88
|
+
{toast.content.isClosable && (
|
|
89
|
+
<RACButton
|
|
90
|
+
slot="close"
|
|
91
|
+
aria-label={intl.formatMessage(uiMessage("ui.close-action"))}
|
|
92
|
+
className={slots.closeButton}
|
|
93
|
+
>
|
|
94
|
+
<CloseIcon />
|
|
95
|
+
</RACButton>
|
|
96
|
+
)}
|
|
97
|
+
</RACToast>
|
|
98
|
+
);
|
|
99
|
+
}}
|
|
100
|
+
</RACToastRegion>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface ToastOptions extends ToastContent {
|
|
105
|
+
/**
|
|
106
|
+
* Auto-dismiss after ms; null means no auto-dismiss (Chakra's semantics).
|
|
107
|
+
* RAC enforces a 5000ms minimum for accessibility.
|
|
108
|
+
*/
|
|
109
|
+
duration?: number | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ToastFn {
|
|
113
|
+
(options: ToastOptions): void;
|
|
114
|
+
/** Whether a toast with this id is currently visible. */
|
|
115
|
+
isActive(id: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Replace a visible toast's content (Chakra's toast.update). The toast is
|
|
118
|
+
* re-added, so unlike Chakra it re-animates and restarts any timeout.
|
|
119
|
+
*/
|
|
120
|
+
update(id: string, options: ToastOptions): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* useToast — imperative toast trigger matching the shape of Chakra's
|
|
125
|
+
* `useToast()` call sites: `toast({ title, description, status, duration })`.
|
|
126
|
+
*/
|
|
127
|
+
export const useToast = (): ToastFn =>
|
|
128
|
+
useMemo(() => {
|
|
129
|
+
const isActive = (id: string) =>
|
|
130
|
+
toastQueue.visibleToasts.some((t) => t.content.id === id);
|
|
131
|
+
const add = ({
|
|
132
|
+
id,
|
|
133
|
+
title,
|
|
134
|
+
description,
|
|
135
|
+
status,
|
|
136
|
+
isClosable,
|
|
137
|
+
duration,
|
|
138
|
+
}: ToastOptions) => {
|
|
139
|
+
if (id && isActive(id)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
toastQueue.add(
|
|
143
|
+
{
|
|
144
|
+
id,
|
|
145
|
+
title,
|
|
146
|
+
description,
|
|
147
|
+
status,
|
|
148
|
+
// A toast that never times out must be dismissable or it is
|
|
149
|
+
// permanent and unremovable; force the close button on.
|
|
150
|
+
isClosable: isClosable || duration == null,
|
|
151
|
+
},
|
|
152
|
+
{ timeout: duration ?? undefined },
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
const update = (id: string, options: ToastOptions) => {
|
|
156
|
+
const existing = toastQueue.visibleToasts.find(
|
|
157
|
+
(t) => t.content.id === id,
|
|
158
|
+
);
|
|
159
|
+
if (existing) {
|
|
160
|
+
toastQueue.close(existing.key);
|
|
161
|
+
}
|
|
162
|
+
add({ ...options, id });
|
|
163
|
+
};
|
|
164
|
+
return Object.assign(add, { isActive, update });
|
|
165
|
+
}, []);
|
package/src/Tooltip.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactElement, ReactNode, RefObject } from "react";
|
|
7
|
+
import { Tooltip as RACTooltip, TooltipTrigger } from "react-aria-components";
|
|
8
|
+
import { css } from "styled-system/css";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
import { PopoverArrow } from "./PopoverArrow";
|
|
11
|
+
|
|
12
|
+
// Base as an object (not a precomputed class) so a caller's `css` override is
|
|
13
|
+
// merged into a single css() call — Panda then dedupes conflicting utilities
|
|
14
|
+
// (e.g. px/py) so overrides actually win.
|
|
15
|
+
const tooltipBase: SystemStyleObject = {
|
|
16
|
+
bg: "gray.700",
|
|
17
|
+
color: "white",
|
|
18
|
+
px: "2",
|
|
19
|
+
py: "1",
|
|
20
|
+
borderRadius: "md",
|
|
21
|
+
fontSize: "sm",
|
|
22
|
+
fontWeight: "medium",
|
|
23
|
+
boxShadow: "md",
|
|
24
|
+
maxW: "xs",
|
|
25
|
+
zIndex: "tooltip",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface TooltipProps {
|
|
29
|
+
/**
|
|
30
|
+
* Tooltip body (Chakra's `label`). Not named `content`: Panda extracts
|
|
31
|
+
* utility-named props with literal values from any JSX component, so a
|
|
32
|
+
* `content` prop taking a string would emit a broken CSS `content` rule.
|
|
33
|
+
*/
|
|
34
|
+
label: ReactNode;
|
|
35
|
+
/** A single focusable trigger element (e.g. a Button). */
|
|
36
|
+
children: ReactElement;
|
|
37
|
+
placement?:
|
|
38
|
+
| "top"
|
|
39
|
+
| "bottom"
|
|
40
|
+
| "left"
|
|
41
|
+
| "right"
|
|
42
|
+
| "top start"
|
|
43
|
+
| "top end"
|
|
44
|
+
| "bottom start"
|
|
45
|
+
| "bottom end"
|
|
46
|
+
| "left top"
|
|
47
|
+
| "left bottom"
|
|
48
|
+
| "right top"
|
|
49
|
+
| "right bottom";
|
|
50
|
+
hasArrow?: boolean;
|
|
51
|
+
/** Controlled open state (otherwise hover/focus driven). */
|
|
52
|
+
isOpen?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Anchor element for positioning, when the child cannot register itself as
|
|
55
|
+
* the trigger (i.e. it is not a RAC component or `Focusable`).
|
|
56
|
+
*/
|
|
57
|
+
triggerRef?: RefObject<HTMLElement | null>;
|
|
58
|
+
/** Hover open delay in ms (RAC default ~1500; pass 0 for instant). */
|
|
59
|
+
delay?: number;
|
|
60
|
+
css?: SystemStyleObject;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tooltip — react-aria-components TooltipTrigger + Tooltip, styled to match
|
|
65
|
+
* Chakra's dark tooltip. The child must be a focusable element so the tooltip
|
|
66
|
+
* is reachable by keyboard (RAC requirement), unless `triggerRef` provides
|
|
67
|
+
* the anchor and the caller manages open state and keyboard access itself.
|
|
68
|
+
*/
|
|
69
|
+
export const Tooltip = ({
|
|
70
|
+
label,
|
|
71
|
+
children,
|
|
72
|
+
placement = "top",
|
|
73
|
+
hasArrow,
|
|
74
|
+
isOpen,
|
|
75
|
+
triggerRef,
|
|
76
|
+
delay = 0,
|
|
77
|
+
css: cssProp,
|
|
78
|
+
}: TooltipProps) => (
|
|
79
|
+
<TooltipTrigger isOpen={isOpen} delay={delay} closeDelay={0}>
|
|
80
|
+
{children}
|
|
81
|
+
<RACTooltip
|
|
82
|
+
triggerRef={triggerRef}
|
|
83
|
+
placement={placement}
|
|
84
|
+
offset={hasArrow ? 8 : 4}
|
|
85
|
+
className={css({ ...tooltipBase, ...cssProp })}
|
|
86
|
+
>
|
|
87
|
+
{hasArrow && <PopoverArrow css={{ "& svg": { fill: "gray.700" } }} />}
|
|
88
|
+
{label}
|
|
89
|
+
</RACTooltip>
|
|
90
|
+
</TooltipTrigger>
|
|
91
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs its callback when unmounted. RAC keeps an overlay tree mounted until
|
|
10
|
+
* the exit transition finishes, so mounting this inside a Modal/Drawer gives
|
|
11
|
+
* the equivalent of Chakra's `onCloseComplete`. Internal to shared-ui.
|
|
12
|
+
*/
|
|
13
|
+
export const UnmountCallback = ({ callback }: { callback?: () => void }) => {
|
|
14
|
+
const ref = useRef(callback);
|
|
15
|
+
ref.current = callback;
|
|
16
|
+
useEffect(() => () => ref.current?.(), []);
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { styled } from "styled-system/jsx";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* VisuallyHidden — screen-reader-only content (Panda's `srOnly` utility).
|
|
10
|
+
* Renders a span like Chakra's; use `as="div"` for block children.
|
|
11
|
+
*/
|
|
12
|
+
export const VisuallyHidden = styled("span", {
|
|
13
|
+
base: { srOnly: true },
|
|
14
|
+
});
|