@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microbit/ui",
3
- "version": "0.1.0-alpha.29",
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",
@@ -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, because a palette missing a stop a shape
180
- * reads renders it as nothing at all, with no error anywhere
181
- * (docs/hints.md): this is where a palette is vetted. A tone should
182
- * alias a whole ramp, so it has nowhere to fall through. Apps may add
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: {
@@ -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 without this an attached
35
- // sibling paints over the focused button's focus-ring shadow.
36
- // Raise only on :focus-visible, when the ring is actually shown:
37
- // with the attached -1px overlap a raised button also paints over
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 :first-of-type: attached
46
- // groups can mix element types (e.g. select + button), and
47
- // -of-type matches per element type.
48
- // marginEnd -1px overlaps adjacent borders so two 1px inner
49
- // edges read as a single 1px seam. That assumes 1px borders;
50
- // the bordered variants are 2px (microbit-foundation/ui#22).
51
- "& > *:first-child:not(:last-child)": {
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
- marginEnd: "-1px",
54
- },
55
- "& > *:not(:first-child):not(:last-child)": {
56
- borderRadius: 0,
57
- marginEnd: "-1px",
69
+ borderInlineEndWidth: "1px",
58
70
  },
59
- "& > *:not(:first-child):last-child": {
71
+ "& > *:not(:first-child)": {
60
72
  borderStartRadius: 0,
73
+ borderInlineStartWidth: "1px",
74
+ marginInlineStart: "-1px",
61
75
  },
62
76
  })
63
77
  : css({ gap: 2 }),
@@ -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({ isRound, css: cssProp, children, ...rest }, ref) {
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
+ });
@@ -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
@@ -19,6 +19,7 @@ export * from "./Checkbox";
19
19
  export * from "./CheckboxGroup";
20
20
  export * from "./Field";
21
21
  export * from "./IconButton";
22
+ export * from "./MoreMenuButton";
22
23
  export * from "./Image";
23
24
  export * from "./Input";
24
25
  export * from "./InputGroup";