@microbit/ui 0.1.0-alpha.10

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.
Files changed (86) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +220 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.de.json +22 -0
  5. package/lang/ui.en-us.json +22 -0
  6. package/lang/ui.en.json +22 -0
  7. package/lang/ui.es-es.json +22 -0
  8. package/lang/ui.fr.json +22 -0
  9. package/lang/ui.ga-ie.json +22 -0
  10. package/lang/ui.ja.json +22 -0
  11. package/lang/ui.ko.json +22 -0
  12. package/lang/ui.lol.json +22 -0
  13. package/lang/ui.nl.json +22 -0
  14. package/lang/ui.pl.json +22 -0
  15. package/lang/ui.pt-br.json +22 -0
  16. package/lang/ui.zh-cn.json +22 -0
  17. package/lang/ui.zh-tw.json +22 -0
  18. package/package.json +58 -0
  19. package/postcss-legacy-safari.cjs +96 -0
  20. package/reset.css +35 -0
  21. package/src/Button.recipe.ts +162 -0
  22. package/src/Button.tsx +69 -0
  23. package/src/ButtonGroup.tsx +63 -0
  24. package/src/Card.recipe.ts +49 -0
  25. package/src/Card.tsx +56 -0
  26. package/src/Checkbox.recipe.ts +105 -0
  27. package/src/Checkbox.tsx +76 -0
  28. package/src/CloseButton.tsx +87 -0
  29. package/src/CloseIcon.tsx +40 -0
  30. package/src/Code.tsx +20 -0
  31. package/src/Collapse.tsx +180 -0
  32. package/src/Divider.tsx +52 -0
  33. package/src/Drawer.recipe.ts +98 -0
  34. package/src/Drawer.tsx +138 -0
  35. package/src/Fade.tsx +48 -0
  36. package/src/Heading.recipe.ts +52 -0
  37. package/src/Heading.tsx +14 -0
  38. package/src/Icon.tsx +55 -0
  39. package/src/IconButton.tsx +44 -0
  40. package/src/Image.tsx +11 -0
  41. package/src/Input.recipe.ts +72 -0
  42. package/src/Input.tsx +40 -0
  43. package/src/InputGroup.tsx +62 -0
  44. package/src/Kbd.tsx +26 -0
  45. package/src/Link.tsx +23 -0
  46. package/src/LinkBox.tsx +88 -0
  47. package/src/LinkButton.tsx +80 -0
  48. package/src/List.tsx +31 -0
  49. package/src/Menu.recipe.ts +119 -0
  50. package/src/Menu.tsx +261 -0
  51. package/src/Modal.recipe.ts +163 -0
  52. package/src/Modal.tsx +279 -0
  53. package/src/NativeSelect.tsx +94 -0
  54. package/src/NumberField.recipe.ts +67 -0
  55. package/src/NumberField.tsx +86 -0
  56. package/src/PopoverArrow.tsx +67 -0
  57. package/src/ProgressBar.tsx +61 -0
  58. package/src/Radio.recipe.ts +107 -0
  59. package/src/Radio.tsx +89 -0
  60. package/src/SharedUIProvider.tsx +55 -0
  61. package/src/Slide.tsx +54 -0
  62. package/src/Slider.recipe.ts +102 -0
  63. package/src/Slider.tsx +163 -0
  64. package/src/Spinner.tsx +69 -0
  65. package/src/Svg.tsx +23 -0
  66. package/src/Switch.recipe.ts +107 -0
  67. package/src/Switch.tsx +62 -0
  68. package/src/Text.recipe.ts +29 -0
  69. package/src/Text.tsx +16 -0
  70. package/src/TextField.recipe.ts +54 -0
  71. package/src/TextField.tsx +89 -0
  72. package/src/Toast.recipe.ts +98 -0
  73. package/src/Toast.tsx +171 -0
  74. package/src/Tooltip.tsx +91 -0
  75. package/src/UnmountCallback.tsx +18 -0
  76. package/src/VisuallyHidden.tsx +14 -0
  77. package/src/base-preset.ts +294 -0
  78. package/src/base-tokens.ts +1017 -0
  79. package/src/button-icon.ts +23 -0
  80. package/src/hooks/useBreakpointValue.ts +63 -0
  81. package/src/hooks/useClipboard.ts +64 -0
  82. package/src/hooks/useMediaQuery.ts +28 -0
  83. package/src/hooks/usePrevious.ts +18 -0
  84. package/src/index.ts +55 -0
  85. package/src/messages.ts +17 -0
  86. package/src/system.ts +36 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { forwardRef, ReactNode } from "react";
7
+ import {
8
+ Link as RACLink,
9
+ LinkProps as RACLinkProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { button, ButtonVariantProps } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+ import { buttonIcon } from "./button-icon";
15
+
16
+ export interface LinkButtonProps
17
+ extends Omit<RACLinkProps, "className" | "children">,
18
+ ButtonVariantProps {
19
+ /** Per-instance style overrides, merged after the recipe. */
20
+ css?: SystemStyleObject;
21
+ className?: string;
22
+ /** Icon rendered before the label, matching Chakra's `leftIcon`. */
23
+ leftIcon?: ReactNode;
24
+ /** Icon rendered after the label, matching Chakra's `rightIcon`. */
25
+ rightIcon?: ReactNode;
26
+ children?: ReactNode;
27
+ }
28
+
29
+ // Anchors pick up underline styling that buttons never have.
30
+ const linkReset = css.raw({
31
+ textDecoration: "none",
32
+ _hover: { textDecoration: "none" },
33
+ });
34
+
35
+ /**
36
+ * LinkButton — a navigation link that looks like a Button (Chakra's
37
+ * `Button as="a"`). react-aria-components <Link> renders a real anchor
38
+ * (`href`, `target`, new-tab/middle-click semantics preserved) with the same
39
+ * interaction data attributes as Button, so the `button` recipe's
40
+ * hover/press/focus/disabled states apply unchanged.
41
+ *
42
+ * Use for navigation that should read as a call to action (e.g. an external
43
+ * help page presented as a dialog's primary action); use Button for
44
+ * in-page actions.
45
+ */
46
+ export const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>(
47
+ function LinkButton(
48
+ {
49
+ variant,
50
+ size,
51
+ css: cssProp,
52
+ className,
53
+ leftIcon,
54
+ rightIcon,
55
+ children,
56
+ ...rest
57
+ },
58
+ ref,
59
+ ) {
60
+ return (
61
+ <RACLink
62
+ ref={ref}
63
+ className={cx(
64
+ button({ variant, size }),
65
+ css(linkReset, cssProp),
66
+ className,
67
+ )}
68
+ {...rest}
69
+ >
70
+ {leftIcon ? (
71
+ <span className={buttonIcon({ side: "left" })}>{leftIcon}</span>
72
+ ) : null}
73
+ {children}
74
+ {rightIcon ? (
75
+ <span className={buttonIcon({ side: "right" })}>{rightIcon}</span>
76
+ ) : null}
77
+ </RACLink>
78
+ );
79
+ },
80
+ );
package/src/List.tsx ADDED
@@ -0,0 +1,31 @@
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
+ * Unstyled list, matching Chakra's <List>. The marker removal is explicit —
10
+ * Chakra's List set listStyleType itself, and coexisting apps run without
11
+ * Panda's preflight (margins/padding are zeroed by whichever reset is
12
+ * active).
13
+ */
14
+ export const List = styled("ul", {
15
+ base: { listStyleType: "none" },
16
+ });
17
+
18
+ /** List item, matching Chakra's <ListItem>. */
19
+ export const ListItem = styled("li", {
20
+ base: {},
21
+ });
22
+
23
+ /** Bulleted list matching Chakra's <UnorderedList>. */
24
+ export const UnorderedList = styled("ul", {
25
+ base: { listStyleType: "disc", marginStart: "1em" },
26
+ });
27
+
28
+ /** Numbered list matching Chakra's <OrderedList>. */
29
+ export const OrderedList = styled("ol", {
30
+ base: { listStyleType: "decimal", marginStart: "1em" },
31
+ });
@@ -0,0 +1,119 @@
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
+ * Menu slot recipe — Chakra's default Menu parts (light mode). `content` is the
10
+ * dropdown card (react-aria-components' Popover), `list` the RAC Menu, `item` a
11
+ * MenuItem, `icon` the leading-icon wrapper.
12
+ *
13
+ * A config slot recipe (rather than an atomic `sva`) for consistency with
14
+ * `dialog` and so presets can override it later if brands diverge.
15
+ * No variants, so it needs no `staticCss` entry.
16
+ *
17
+ * Registered in the base preset (base-preset.ts).
18
+ */
19
+ export const menu = defineSlotRecipe({
20
+ className: "menu",
21
+ slots: [
22
+ "content",
23
+ "list",
24
+ "item",
25
+ "icon",
26
+ "label",
27
+ "divider",
28
+ "group",
29
+ "groupTitle",
30
+ "itemIndicator",
31
+ ],
32
+ base: {
33
+ content: {
34
+ bg: "white",
35
+ color: "inherit",
36
+ minWidth: "3xs",
37
+ py: "2",
38
+ zIndex: "dropdown",
39
+ borderRadius: "md",
40
+ borderWidth: "1px",
41
+ borderColor: "gray.200",
42
+ boxShadow: "sm",
43
+ // Approximate Chakra's menu fade/scale. RAC toggles data-entering/
44
+ // data-exiting on the Popover and waits for the transition before unmount.
45
+ transformOrigin: "top",
46
+ opacity: 1,
47
+ transform: "scale(1)",
48
+ transition: "opacity 0.1s ease-out, transform 0.1s ease-out",
49
+ "&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
50
+ "&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
51
+ _motionReduce: { transition: "none" },
52
+ },
53
+ list: {
54
+ outline: "none",
55
+ },
56
+ item: {
57
+ display: "flex",
58
+ alignItems: "center",
59
+ py: "1.5",
60
+ px: "3",
61
+ cursor: "pointer",
62
+ color: "inherit",
63
+ textDecoration: "none",
64
+ outline: "none",
65
+ transitionProperty: "background",
66
+ transitionDuration: "ultra-fast",
67
+ transitionTimingFunction: "ease-in",
68
+ // RAC highlights the active item (keyboard or pointer) with data-focused;
69
+ // data-pressed is the pressed state — mirrors Chakra's _focus/_active.
70
+ "&[data-focused]": { bg: "gray.100" },
71
+ "&[data-pressed]": { bg: "gray.200" },
72
+ "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
73
+ },
74
+ label: {
75
+ // Chakra wraps an icon-item's children in a flex:1 span, so block
76
+ // children (e.g. two stacked <Text>s) lay out vertically rather than as
77
+ // flex-row siblings of the icon.
78
+ flex: "1",
79
+ },
80
+ icon: {
81
+ display: "inline-flex",
82
+ alignItems: "center",
83
+ justifyContent: "center",
84
+ flexShrink: 0,
85
+ marginEnd: "0.75rem",
86
+ // Chakra's MenuIcon shrinks glyphs to 0.8em; items passing an explicitly
87
+ // sized icon (e.g. h/w) override this.
88
+ fontSize: "0.8em",
89
+ },
90
+ divider: {
91
+ border: 0,
92
+ borderBottom: "1px solid",
93
+ borderColor: "gray.200",
94
+ my: "2",
95
+ opacity: 0.6,
96
+ },
97
+ group: {},
98
+ // Chakra's MenuGroup/MenuOptionGroup title.
99
+ groupTitle: {
100
+ display: "block",
101
+ mx: "4",
102
+ my: "2",
103
+ fontWeight: "semibold",
104
+ fontSize: "sm",
105
+ },
106
+ // Check glyph slot for MenuItemOption: space always reserved (as Chakra
107
+ // does), visible only on the selected item (RAC sets data-selected on it).
108
+ itemIndicator: {
109
+ display: "inline-flex",
110
+ alignItems: "center",
111
+ justifyContent: "center",
112
+ flexShrink: 0,
113
+ marginEnd: "0.75rem",
114
+ fontSize: "0.8em",
115
+ opacity: 0,
116
+ "[data-selected] &": { opacity: 1 },
117
+ },
118
+ },
119
+ });
package/src/Menu.tsx ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode, useCallback, useState } from "react";
7
+ import {
8
+ Header as RACHeader,
9
+ Menu as RACMenu,
10
+ MenuItem as RACMenuItem,
11
+ MenuItemProps as RACMenuItemProps,
12
+ MenuSection as RACMenuSection,
13
+ MenuTrigger as RACMenuTrigger,
14
+ Popover,
15
+ PopoverProps,
16
+ Separator,
17
+ } from "react-aria-components";
18
+ import { RiCheckLine } from "react-icons/ri";
19
+ import { css, cx } from "styled-system/css";
20
+ import { menu } from "styled-system/recipes";
21
+ import { SystemStyleObject } from "styled-system/types";
22
+ import { Icon } from "./Icon";
23
+ import { useOverlayCloseRegistrar } from "./SharedUIProvider";
24
+
25
+ export interface MenuTriggerProps {
26
+ /** Called when the menu opens. */
27
+ onOpen?: () => void;
28
+ /** Called when the menu closes. */
29
+ onClose?: () => void;
30
+ /** The trigger element followed by a `MenuList` (react-aria pattern). */
31
+ children: ReactNode;
32
+ }
33
+
34
+ /**
35
+ * MenuTrigger — react-aria-components' <MenuTrigger>. Its first child is the
36
+ * trigger (e.g. a Button); the second is a `MenuList`. RAC returns focus to the
37
+ * trigger when the menu closes.
38
+ *
39
+ * When the app installs an overlay-close registrar (e.g. so the Android back
40
+ * button can close the menu), the trigger runs controlled and registers its
41
+ * close function while open. Otherwise it's an uncontrolled pass-through.
42
+ */
43
+ export const MenuTrigger = ({
44
+ onOpen,
45
+ onClose,
46
+ children,
47
+ }: MenuTriggerProps) => {
48
+ const registerClose = useOverlayCloseRegistrar();
49
+ const [isOpen, setIsOpen] = useState(false);
50
+
51
+ const handleOpenChange = useCallback(
52
+ (open: boolean) => {
53
+ setIsOpen(open);
54
+ if (open) {
55
+ registerClose?.(() => {
56
+ setIsOpen(false);
57
+ registerClose(null);
58
+ });
59
+ onOpen?.();
60
+ } else {
61
+ registerClose?.(null);
62
+ onClose?.();
63
+ }
64
+ },
65
+ [registerClose, onOpen, onClose],
66
+ );
67
+
68
+ if (!registerClose) {
69
+ return (
70
+ <RACMenuTrigger
71
+ onOpenChange={(open) => (open ? onOpen?.() : onClose?.())}
72
+ >
73
+ {children}
74
+ </RACMenuTrigger>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <RACMenuTrigger isOpen={isOpen} onOpenChange={handleOpenChange}>
80
+ {children}
81
+ </RACMenuTrigger>
82
+ );
83
+ };
84
+
85
+ export interface MenuListProps {
86
+ children: ReactNode;
87
+ /** Placement of the dropdown relative to the trigger. */
88
+ placement?: PopoverProps["placement"];
89
+ /** Per-instance style overrides, merged after the recipe. */
90
+ css?: SystemStyleObject;
91
+ className?: string;
92
+ }
93
+
94
+ /**
95
+ * MenuList — the dropdown surface: RAC's <Popover> (positioned card) wrapping a
96
+ * RAC <Menu> (the list). Place `MenuItem`s inside.
97
+ */
98
+ export const MenuList = ({
99
+ children,
100
+ // Matches Chakra Menu's default "bottom-start".
101
+ placement = "bottom start",
102
+ css: cssProp,
103
+ className,
104
+ }: MenuListProps) => {
105
+ const slots = menu();
106
+ return (
107
+ <Popover
108
+ placement={placement}
109
+ className={cx(
110
+ slots.content,
111
+ cssProp ? css(cssProp) : undefined,
112
+ className,
113
+ )}
114
+ >
115
+ <RACMenu className={slots.list}>{children}</RACMenu>
116
+ </Popover>
117
+ );
118
+ };
119
+
120
+ export interface MenuItemProps
121
+ extends Omit<RACMenuItemProps, "className" | "children"> {
122
+ /** Icon rendered before the label, matching Chakra's MenuItem `icon`. */
123
+ icon?: ReactNode;
124
+ /** Per-instance style overrides, merged after the recipe. */
125
+ css?: SystemStyleObject;
126
+ className?: string;
127
+ children?: ReactNode;
128
+ }
129
+
130
+ /**
131
+ * MenuItem — a RAC <MenuItem> styled with the `menu` recipe's `item` slot.
132
+ * Use `onAction` for the click behaviour (RAC closes the menu automatically).
133
+ */
134
+ export const MenuItem = ({
135
+ icon,
136
+ css: cssProp,
137
+ className,
138
+ children,
139
+ ...rest
140
+ }: MenuItemProps) => {
141
+ const slots = menu();
142
+ return (
143
+ <RACMenuItem
144
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
145
+ {...rest}
146
+ >
147
+ {icon ? (
148
+ <>
149
+ <span className={slots.icon}>{icon}</span>
150
+ <span className={slots.label}>{children}</span>
151
+ </>
152
+ ) : (
153
+ children
154
+ )}
155
+ </RACMenuItem>
156
+ );
157
+ };
158
+
159
+ export interface MenuOptionGroupProps {
160
+ /** Group heading shown above the options (Chakra's `title`). */
161
+ title?: ReactNode;
162
+ /** The selected `MenuItemOption`'s value (radio semantics). */
163
+ value?: string;
164
+ /** Called with the newly selected option's value. */
165
+ onChange?: (value: string) => void;
166
+ /** `MenuItemOption` children. */
167
+ children: ReactNode;
168
+ css?: SystemStyleObject;
169
+ className?: string;
170
+ }
171
+
172
+ /**
173
+ * MenuOptionGroup — a single-select (radio) group of `MenuItemOption`s within
174
+ * a menu, replacing Chakra's `MenuOptionGroup type="radio"`. Selection is
175
+ * section-scoped (RAC MenuSection), so a menu can mix action items and option
176
+ * groups.
177
+ */
178
+ export const MenuOptionGroup = ({
179
+ title,
180
+ value,
181
+ onChange,
182
+ children,
183
+ css: cssProp,
184
+ className,
185
+ }: MenuOptionGroupProps) => {
186
+ const slots = menu();
187
+ return (
188
+ <RACMenuSection
189
+ className={cx(slots.group, cssProp ? css(cssProp) : undefined, className)}
190
+ selectionMode="single"
191
+ selectedKeys={value != null ? [value] : []}
192
+ onSelectionChange={(keys) => {
193
+ if (keys !== "all") {
194
+ const key = keys.values().next().value;
195
+ if (key != null) {
196
+ onChange?.(String(key));
197
+ }
198
+ }
199
+ }}
200
+ >
201
+ {title != null && (
202
+ <RACHeader className={slots.groupTitle}>{title}</RACHeader>
203
+ )}
204
+ {children}
205
+ </RACMenuSection>
206
+ );
207
+ };
208
+
209
+ export interface MenuItemOptionProps
210
+ extends Omit<RACMenuItemProps, "className" | "children" | "id" | "value"> {
211
+ /** This option's value within its `MenuOptionGroup`. */
212
+ value: string;
213
+ css?: SystemStyleObject;
214
+ className?: string;
215
+ children?: ReactNode;
216
+ }
217
+
218
+ /**
219
+ * MenuItemOption — a selectable option inside a `MenuOptionGroup`, with a
220
+ * check indicator on the selected item (Chakra's MenuItemOption).
221
+ */
222
+ export const MenuItemOption = ({
223
+ value,
224
+ css: cssProp,
225
+ className,
226
+ children,
227
+ ...rest
228
+ }: MenuItemOptionProps) => {
229
+ const slots = menu();
230
+ return (
231
+ <RACMenuItem
232
+ id={value}
233
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
234
+ {...rest}
235
+ >
236
+ <span className={slots.itemIndicator} aria-hidden>
237
+ <Icon as={RiCheckLine} />
238
+ </span>
239
+ <span className={slots.label}>{children}</span>
240
+ </RACMenuItem>
241
+ );
242
+ };
243
+
244
+ export interface MenuDividerProps {
245
+ css?: SystemStyleObject;
246
+ className?: string;
247
+ }
248
+
249
+ /** Horizontal rule separating groups of menu items. */
250
+ export const MenuDivider = ({ css: cssProp, className }: MenuDividerProps) => {
251
+ const slots = menu();
252
+ return (
253
+ <Separator
254
+ className={cx(
255
+ slots.divider,
256
+ cssProp ? css(cssProp) : undefined,
257
+ className,
258
+ )}
259
+ />
260
+ );
261
+ };
@@ -0,0 +1,163 @@
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
+ * Dialog slot recipe — Chakra's default Modal parts (light mode), plus this
10
+ * app's overrides: a full-viewport overlay (the iOS WKWebView 100% fix) and a
11
+ * `full` size with safe-area insets and the brand status-bar gradient.
12
+ *
13
+ * A config recipe (rather than an atomic `sva`) so the `size` variant accepts
14
+ * responsive values, e.g. `{ base: "full", md: "4xl" }`. Consumed by the
15
+ * shared-ui Modal, which maps the slots onto react-aria-components'
16
+ * ModalOverlay / Modal / Dialog.
17
+ *
18
+ * Registered in the base preset (base-preset.ts).
19
+ */
20
+ // Appearance of a normal (non-full) modal box. Restated by every non-full size
21
+ // variant so that, when `size` is responsive (e.g. { base: "full", md: "4xl" }),
22
+ // the larger breakpoint fully overrides the `full` variant rather than leaking
23
+ // its margin:0 / border-radius:0 / full-height / gradient into desktop. Panda
24
+ // applies the base-breakpoint variant value unconditionally, so symmetric
25
+ // property sets across size values are required.
26
+ const dialogBox = {
27
+ my: "16",
28
+ mx: "2",
29
+ borderRadius: "md",
30
+ background: "white",
31
+ minHeight: "auto",
32
+ padding: "0",
33
+ };
34
+
35
+ // The `full` variant also styles these slots; same symmetry requirement.
36
+ const dialogSlots = {
37
+ header: { pl: "6" },
38
+ body: { overflowY: "visible" },
39
+ closeTrigger: { top: "2" },
40
+ };
41
+
42
+ export const dialog = defineSlotRecipe({
43
+ className: "dialog",
44
+ slots: [
45
+ "overlay",
46
+ "content",
47
+ "inner",
48
+ "header",
49
+ "body",
50
+ "footer",
51
+ "closeTrigger",
52
+ ],
53
+ base: {
54
+ overlay: {
55
+ position: "fixed",
56
+ inset: 0,
57
+ w: "100%",
58
+ h: "100%",
59
+ bg: "blackAlpha.600",
60
+ zIndex: "modal",
61
+ display: "flex",
62
+ justifyContent: "center",
63
+ alignItems: "flex-start",
64
+ overflow: "auto",
65
+ overscrollBehaviorY: "none",
66
+ // Fade the backdrop in/out. RAC toggles data-entering/data-exiting and
67
+ // waits for the transition before unmounting.
68
+ opacity: 1,
69
+ transition: "opacity 0.2s ease-out",
70
+ "&[data-entering]": { opacity: 0 },
71
+ "&[data-exiting]": { opacity: 0 },
72
+ _motionReduce: { transition: "none" },
73
+ },
74
+ content: {
75
+ position: "relative",
76
+ color: "inherit",
77
+ boxShadow: "lg",
78
+ width: "100%",
79
+ display: "flex",
80
+ flexDirection: "column",
81
+ outline: "none",
82
+ // Approximate Chakra's fade + scale enter/exit.
83
+ opacity: 1,
84
+ transform: "scale(1)",
85
+ transition: "opacity 0.2s ease-out, transform 0.2s ease-out",
86
+ "&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
87
+ "&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
88
+ _motionReduce: { transition: "none" },
89
+ },
90
+ inner: {
91
+ outline: "none",
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ width: "100%",
95
+ flex: "1 1 auto",
96
+ },
97
+ header: {
98
+ px: "6",
99
+ py: "4",
100
+ fontSize: "xl",
101
+ fontWeight: "semibold",
102
+ flexShrink: 0,
103
+ },
104
+ body: { px: "6", py: "2", flex: "1" },
105
+ footer: {
106
+ px: "6",
107
+ py: "4",
108
+ display: "flex",
109
+ alignItems: "center",
110
+ justifyContent: "flex-end",
111
+ // Chakra had no footer gap (call sites wrapped buttons in HStacks);
112
+ // baked in as the house style. Override via css for tighter layouts.
113
+ gap: "5",
114
+ flexShrink: 0,
115
+ },
116
+ closeTrigger: { position: "absolute", top: "2", insetEnd: "3" },
117
+ },
118
+ variants: {
119
+ // Chakra's `isCentered`: vertically centre the dialog in the viewport
120
+ // rather than the default top alignment.
121
+ centered: {
122
+ true: {
123
+ overlay: { alignItems: "center" },
124
+ },
125
+ },
126
+ size: {
127
+ xs: { ...dialogSlots, content: { ...dialogBox, maxWidth: "xs" } },
128
+ sm: { ...dialogSlots, content: { ...dialogBox, maxWidth: "sm" } },
129
+ md: { ...dialogSlots, content: { ...dialogBox, maxWidth: "md" } },
130
+ lg: { ...dialogSlots, content: { ...dialogBox, maxWidth: "lg" } },
131
+ xl: { ...dialogSlots, content: { ...dialogBox, maxWidth: "xl" } },
132
+ "2xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "2xl" } },
133
+ "3xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "3xl" } },
134
+ "4xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "4xl" } },
135
+ "5xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "5xl" } },
136
+ "6xl": { ...dialogSlots, content: { ...dialogBox, maxWidth: "6xl" } },
137
+ full: {
138
+ content: {
139
+ maxWidth: "100vw",
140
+ minHeight: "100dvh",
141
+ my: "0",
142
+ mx: "0",
143
+ borderRadius: "0",
144
+ paddingTop: "env(safe-area-inset-top)",
145
+ paddingBottom: "env(safe-area-inset-bottom)",
146
+ paddingLeft: "env(safe-area-inset-left)",
147
+ paddingRight: "env(safe-area-inset-right)",
148
+ // brand colour in the status-bar area, white below (matches ActionBar)
149
+ background:
150
+ "linear-gradient(to bottom, token(colors.statusBarBg) env(safe-area-inset-top), white env(safe-area-inset-top))",
151
+ },
152
+ header: {
153
+ pl: "calc(var(--window-controls-left, 0px) + token(spacing.6))",
154
+ },
155
+ body: { flex: "1", overflowY: "auto" },
156
+ closeTrigger: {
157
+ top: "calc(env(safe-area-inset-top) + token(spacing.2))",
158
+ },
159
+ },
160
+ },
161
+ },
162
+ defaultVariants: { size: "md" },
163
+ });