@microbit/ui 0.1.0-alpha.29 → 0.1.0-alpha.30
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/package.json +1 -1
- package/src/Button.recipe.ts +4 -4
- package/src/ButtonGroup.tsx +33 -19
- package/src/IconButton.tsx +7 -17
- package/src/MoreMenuButton.tsx +63 -0
- package/src/base-preset.ts +13 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.30",
|
|
4
4
|
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a design language ported from Chakra UI v2. Ships as source; see README for the consumption setup.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/Button.recipe.ts
CHANGED
|
@@ -176,10 +176,10 @@ export const button = defineRecipe({
|
|
|
176
176
|
},
|
|
177
177
|
/**
|
|
178
178
|
* The palette behind `solid`/`outline`. An allowlist rather than Panda's
|
|
179
|
-
* open `colorPalette` prop
|
|
180
|
-
*
|
|
181
|
-
* (docs/hints.md)
|
|
182
|
-
* alias a whole ramp
|
|
179
|
+
* open `colorPalette` prop: if a shape reads a stop the palette doesn't
|
|
180
|
+
* define, the button renders as nothing at all, with no error anywhere
|
|
181
|
+
* (docs/hints.md). This is where a palette is vetted, and a tone should
|
|
182
|
+
* alias a whole ramp so it has nowhere to fall through. Apps may add
|
|
183
183
|
* their own.
|
|
184
184
|
*/
|
|
185
185
|
tone: {
|
package/src/ButtonGroup.tsx
CHANGED
|
@@ -21,43 +21,57 @@ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
21
21
|
/**
|
|
22
22
|
* ButtonGroup — lays out related buttons in a row. Works with any button
|
|
23
23
|
* elements (shared-ui or native).
|
|
24
|
+
*
|
|
25
|
+
* Attached, the buttons divide by a hairline and the group keeps whatever
|
|
26
|
+
* outline the variant draws round the outside, so a 2px `secondary` reads as
|
|
27
|
+
* one bordered control with cells inside it rather than three boxes. A
|
|
28
|
+
* variant with no border of its own (`primary`, `solid`, `neutral`) divides
|
|
29
|
+
* by a gap in the same place, the surface behind the group showing through —
|
|
30
|
+
* see the `[data-attached]` rule in base-preset.ts.
|
|
24
31
|
*/
|
|
25
32
|
export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
26
33
|
function ButtonGroup({ isAttached, css: cssProp, className, ...rest }, ref) {
|
|
27
34
|
return (
|
|
28
35
|
<div
|
|
29
36
|
ref={ref}
|
|
37
|
+
data-attached={isAttached ? "" : undefined}
|
|
30
38
|
className={cx(
|
|
31
39
|
css({
|
|
32
40
|
display: "inline-flex",
|
|
33
41
|
alignItems: "center",
|
|
34
|
-
// Buttons are position: relative, so
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// a seam its neighbour draws (e.g. a solid split button's white
|
|
39
|
-
// borderLeft).
|
|
42
|
+
// Buttons are position: relative, so a sibling would paint over
|
|
43
|
+
// the focused button's ring. Only while it shows: a permanently
|
|
44
|
+
// raised child would own the seam it shares, so a hover on its
|
|
45
|
+
// neighbour would stop short of it.
|
|
40
46
|
"& > *": { _focusVisible: { zIndex: 1 } },
|
|
41
47
|
}),
|
|
42
48
|
isAttached
|
|
43
49
|
? css({
|
|
44
50
|
gap: 0,
|
|
45
|
-
// :first-child/:last-child, not
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
51
|
+
// :first-child/:last-child, not -of-type: attached groups can
|
|
52
|
+
// mix element types (e.g. select + button).
|
|
53
|
+
//
|
|
54
|
+
// A hairline whatever the variant's border, which stays as the
|
|
55
|
+
// group's outline — matching it made a 2px seam read as a gap
|
|
56
|
+
// (microbit-foundation/ui#22). Both sides of a seam keep a
|
|
57
|
+
// border and overlap by its width so the two paint as one
|
|
58
|
+
// line; dropping one instead would move that button's border
|
|
59
|
+
// box off the cell you see, and `focusRing` is an outline, so
|
|
60
|
+
// the ring would land on the seam rather than outside it.
|
|
61
|
+
//
|
|
62
|
+
// padding-box so a fill doesn't paint under the transparent
|
|
63
|
+
// seam a borderless variant gets from the preset. Here rather
|
|
64
|
+
// than with that rule because `bg` is the `background`
|
|
65
|
+
// shorthand, which resets it from `recipes`.
|
|
66
|
+
"& > *": { backgroundClip: "padding-box" },
|
|
67
|
+
"& > *:not(:last-child)": {
|
|
52
68
|
borderEndRadius: 0,
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
"& > *:not(:first-child):not(:last-child)": {
|
|
56
|
-
borderRadius: 0,
|
|
57
|
-
marginEnd: "-1px",
|
|
69
|
+
borderInlineEndWidth: "1px",
|
|
58
70
|
},
|
|
59
|
-
"& > *:not(:first-child)
|
|
71
|
+
"& > *:not(:first-child)": {
|
|
60
72
|
borderStartRadius: 0,
|
|
73
|
+
borderInlineStartWidth: "1px",
|
|
74
|
+
marginInlineStart: "-1px",
|
|
61
75
|
},
|
|
62
76
|
})
|
|
63
77
|
: css({ gap: 2 }),
|
package/src/IconButton.tsx
CHANGED
|
@@ -10,13 +10,6 @@ export interface IconButtonProps
|
|
|
10
10
|
extends Omit<ButtonProps, "leftIcon" | "rightIcon"> {
|
|
11
11
|
/** Icon-only buttons have no visible label, so this is required. */
|
|
12
12
|
"aria-label": string;
|
|
13
|
-
/**
|
|
14
|
-
* Circular rather than the recipe's border-radius.
|
|
15
|
-
* Usually a visual no-op here: the 2rem `button` radius already renders
|
|
16
|
-
* square icon buttons as circles. Kept for ported call sites and for
|
|
17
|
-
* variants/overrides with a smaller radius (e.g. `unstyled`).
|
|
18
|
-
*/
|
|
19
|
-
isRound?: boolean;
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
/**
|
|
@@ -24,19 +17,16 @@ export interface IconButtonProps
|
|
|
24
17
|
* padding (its size variants add `px`, which would squeeze a single glyph in a
|
|
25
18
|
* fixed-width button) and keeps the `minW` from the size so the box stays
|
|
26
19
|
* square. Pass the icon as children.
|
|
20
|
+
*
|
|
21
|
+
* Circular at every size without asking: the 2rem `button` radius exceeds half
|
|
22
|
+
* the tallest size (3rem), so CSS clamps it to a full round. Override
|
|
23
|
+
* `borderRadius` per instance for anything else — an attached group or a
|
|
24
|
+
* button that sits in a card corner needs per-corner control, not a shape flag.
|
|
27
25
|
*/
|
|
28
26
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
29
|
-
function IconButton({
|
|
27
|
+
function IconButton({ css: cssProp, children, ...rest }, ref) {
|
|
30
28
|
return (
|
|
31
|
-
<Button
|
|
32
|
-
ref={ref}
|
|
33
|
-
css={{
|
|
34
|
-
px: 0,
|
|
35
|
-
...(isRound ? { borderRadius: "full" } : {}),
|
|
36
|
-
...cssProp,
|
|
37
|
-
}}
|
|
38
|
-
{...rest}
|
|
39
|
-
>
|
|
29
|
+
<Button ref={ref} css={{ px: 0, ...cssProp }} {...rest}>
|
|
40
30
|
{children}
|
|
41
31
|
</Button>
|
|
42
32
|
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
import { MdMoreVert } from "react-icons/md";
|
|
8
|
+
import { Icon } from "./Icon";
|
|
9
|
+
import { IconButton, IconButtonProps } from "./IconButton";
|
|
10
|
+
|
|
11
|
+
export interface MoreMenuButtonProps
|
|
12
|
+
extends Omit<IconButtonProps, "children"> {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MoreMenuButton — the "more options" half of a split button. Use as the
|
|
16
|
+
* trigger inside a MenuTrigger, alongside the main action, both in an
|
|
17
|
+
* attached ButtonGroup:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <ButtonGroup isAttached>
|
|
21
|
+
* <Button variant="primary" onPress={send}>Send</Button>
|
|
22
|
+
* <MenuTrigger>
|
|
23
|
+
* <MoreMenuButton variant="primary" aria-label="More send options" />
|
|
24
|
+
* <MenuList>…</MenuList>
|
|
25
|
+
* </MenuTrigger>
|
|
26
|
+
* </ButtonGroup>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Give it the main action's `variant`, `tone` and `size`: the two halves are
|
|
30
|
+
* one control. The seam between them is ButtonGroup's job.
|
|
31
|
+
*/
|
|
32
|
+
export const MoreMenuButton = forwardRef<
|
|
33
|
+
HTMLButtonElement,
|
|
34
|
+
MoreMenuButtonProps
|
|
35
|
+
>(function MoreMenuButton({ css: cssProp, ...props }, ref) {
|
|
36
|
+
return (
|
|
37
|
+
<IconButton ref={ref} css={cssProp} {...props}>
|
|
38
|
+
<Icon
|
|
39
|
+
as={MdMoreVert}
|
|
40
|
+
css={{
|
|
41
|
+
// Optical centring: an attached end button is a rectangle plus a
|
|
42
|
+
// semicircle, whose area sits 4.7% of the width towards the flat
|
|
43
|
+
// edge. A share of the width rather than a length because that is a
|
|
44
|
+
// property of the shape, not the size — the button stays square and
|
|
45
|
+
// the radius always clamps to half its height (see IconButton), so
|
|
46
|
+
// one percentage holds everywhere. Leading is the mirror image; a
|
|
47
|
+
// middle or lone button is symmetric and needs nothing.
|
|
48
|
+
//
|
|
49
|
+
// Position rather than margin so the nudge can't feed back into the
|
|
50
|
+
// width the percentage resolves against.
|
|
51
|
+
"[data-attached] > *:not(:first-child):last-child &": {
|
|
52
|
+
position: "relative",
|
|
53
|
+
insetInlineStart: "-4.7%",
|
|
54
|
+
},
|
|
55
|
+
"[data-attached] > *:first-child:not(:last-child) &": {
|
|
56
|
+
position: "relative",
|
|
57
|
+
insetInlineStart: "4.7%",
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</IconButton>
|
|
62
|
+
);
|
|
63
|
+
});
|
package/src/base-preset.ts
CHANGED
|
@@ -363,6 +363,19 @@ export const basePreset = definePreset({
|
|
|
363
363
|
"h1, h2, h3, h4, h5, h6": {
|
|
364
364
|
textWrap: "wrap",
|
|
365
365
|
},
|
|
366
|
+
// The colour of the seams an attached ButtonGroup draws (ButtonGroup.tsx).
|
|
367
|
+
// Transparent, so a variant with no border of its own divides by letting
|
|
368
|
+
// the surface show through rather than by a line in the text colour.
|
|
369
|
+
//
|
|
370
|
+
// From `base`, so a variant that does bring a border colours its seams
|
|
371
|
+
// from `recipes` (as the cursor rule above is overridden). Longhands
|
|
372
|
+
// because Panda resolves `transparent` to a token, and the logical
|
|
373
|
+
// *shorthand* with a var() value is the one Safari 14.x drops — the
|
|
374
|
+
// postcss-legacy-safari shim does not cover the border ones.
|
|
375
|
+
"[data-attached] > *": {
|
|
376
|
+
borderInlineStartColor: "transparent",
|
|
377
|
+
borderInlineEndColor: "transparent",
|
|
378
|
+
},
|
|
366
379
|
// While a full-size dialog is open (the Modal stamps data-fullsize on
|
|
367
380
|
// its overlay), release the scrollbar gutter that react-aria's scroll
|
|
368
381
|
// lock reserves on the root. The reserved strip is scrollbar chrome to
|
package/src/index.ts
CHANGED