@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +32 -0
- package/README.md +104 -15
- package/lang/ui.de.json +22 -0
- package/lang/ui.ga-ie.json +22 -0
- package/lang/ui.zh-cn.json +22 -0
- package/package.json +6 -4
- package/postcss-legacy-safari.cjs +96 -0
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +168 -0
- package/src/Divider.tsx +39 -8
- package/src/Fade.tsx +48 -0
- package/src/IconButton.tsx +6 -1
- package/src/Kbd.tsx +26 -0
- package/src/List.tsx +7 -3
- package/src/Menu.recipe.ts +32 -1
- package/src/Menu.tsx +89 -0
- package/src/Modal.tsx +15 -1
- package/src/NumberField.recipe.ts +67 -0
- package/src/NumberField.tsx +86 -0
- package/src/PopoverArrow.tsx +18 -3
- package/src/Slider.tsx +64 -0
- package/src/TextField.tsx +17 -2
- package/src/Toast.tsx +7 -1
- package/src/base-preset.ts +5 -3
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -4
- package/src/hooks/useClipboard.ts +27 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +8 -0
package/LICENSE.md
CHANGED
|
@@ -19,3 +19,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Third-party notices
|
|
26
|
+
|
|
27
|
+
This package includes design tokens and component styling derived from
|
|
28
|
+
[Chakra UI](https://github.com/chakra-ui/chakra-ui) v2: the token scales in
|
|
29
|
+
`src/base-tokens.ts` were snapshotted from `@chakra-ui/theme`'s default
|
|
30
|
+
values, and the component recipes were ported from Chakra's component styles.
|
|
31
|
+
Chakra UI is used under the MIT License:
|
|
32
|
+
|
|
33
|
+
> MIT License
|
|
34
|
+
>
|
|
35
|
+
> Copyright (c) 2019 Segun Adebayo
|
|
36
|
+
>
|
|
37
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
38
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
39
|
+
> in the Software without restriction, including without limitation the rights
|
|
40
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
41
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
42
|
+
> furnished to do so, subject to the following conditions:
|
|
43
|
+
>
|
|
44
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
45
|
+
> copies or substantial portions of the Software.
|
|
46
|
+
>
|
|
47
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
48
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
49
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
50
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
51
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
52
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
53
|
+
> SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @microbit/ui
|
|
2
2
|
|
|
3
3
|
react-aria-components + Panda CSS primitives with a design language ported from
|
|
4
|
-
the Micro:bit Educational Foundation apps' original
|
|
4
|
+
the Micro:bit Educational Foundation apps' original
|
|
5
|
+
[Chakra UI](https://chakra-ui.com/) v2 themes (see
|
|
6
|
+
[Chakra UI heritage](#chakra-ui-heritage-and-license)).
|
|
5
7
|
|
|
6
8
|
The package **ships as source**: components import `styled-system/*`, which
|
|
7
9
|
each consumer generates with its own Panda preset stack. There is no build
|
|
8
|
-
step and no CSS shipped — the consumer's
|
|
9
|
-
its tree uses
|
|
10
|
+
step and no CSS shipped — the consumer's Panda run produces exactly the styles
|
|
11
|
+
its tree uses (`panda codegen` for the `styled-system/*` helpers, the Panda
|
|
12
|
+
PostCSS plugin for the CSS).
|
|
10
13
|
|
|
11
14
|
## App-side installation
|
|
12
15
|
|
|
@@ -16,17 +19,15 @@ an app must do:
|
|
|
16
19
|
|
|
17
20
|
1. **Panda preset stack** (`panda.config.ts`): `@pandacss/preset-base`, then
|
|
18
21
|
the **base preset** (`@microbit/ui/base-preset` — the complete micro:bit
|
|
19
|
-
design system
|
|
20
|
-
**private brand preset** (
|
|
21
|
-
fonts).
|
|
22
|
+
design system), then optionally the app's own preset, then optionally a
|
|
23
|
+
**private brand preset** (Foundation colours, licensed fonts).
|
|
22
24
|
|
|
23
25
|
Later presets override earlier ones token-by-token — the base recipes and
|
|
24
26
|
semantic tokens reference the brand tokens, which is how a brand swap
|
|
25
27
|
restyles everything without touching recipes. Set `eject: true` (the stack
|
|
26
28
|
supplies the full token system). After changing an _external_ preset
|
|
27
|
-
dependency, regenerate clean: `rm -rf styled-system
|
|
28
|
-
|
|
29
|
-
changes.
|
|
29
|
+
dependency, regenerate clean: `rm -rf styled-system && npm run panda` —
|
|
30
|
+
incremental codegen does not detect external preset changes.
|
|
30
31
|
|
|
31
32
|
2. **Include this package's source** in `panda.config.ts` so Panda extracts
|
|
32
33
|
the styles the components use:
|
|
@@ -40,10 +41,25 @@ npm run panda` — incremental codegen does not detect external preset
|
|
|
40
41
|
importers, this package's source included — a `styled-system` alias in
|
|
41
42
|
both `tsconfig.json` `paths` and the bundler config (see the
|
|
42
43
|
`viteFinal` in `.storybook/main.ts`).
|
|
43
|
-
4. **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
4. **Generate and load the CSS** with Panda's PostCSS plugin. Keep Vite's
|
|
45
|
+
default transformer — do **not** set `css.transformer: "lightningcss"`,
|
|
46
|
+
which disables PostCSS. Add a `postcss.config.cjs`:
|
|
47
|
+
```js
|
|
48
|
+
module.exports = { plugins: { "@pandacss/dev/postcss": {} } };
|
|
49
|
+
```
|
|
50
|
+
Run `panda codegen` as a `prepare`/`predev` step so the `styled-system/*`
|
|
51
|
+
helpers exist before `tsc`; the plugin generates the CSS during the bundle.
|
|
52
|
+
Import **one** entry stylesheet — first, before app styles — that declares
|
|
53
|
+
the cascade-layer order; the plugin injects the generated CSS into it (the
|
|
54
|
+
declaration must list all of Panda's layers, hence ≥5 names):
|
|
55
|
+
```css
|
|
56
|
+
/* e.g. src/layers.css, imported once at the app root */
|
|
57
|
+
@layer reset, vendor, base, tokens, recipes, utilities;
|
|
58
|
+
```
|
|
59
|
+
The `vendor` layer is for third-party stylesheets: import any vendor CSS
|
|
60
|
+
with `@import "..." layer(vendor)` so it beats the preflight reset but
|
|
61
|
+
loses to app styling. See `.storybook/{layers.css,preview.tsx,main.ts}` +
|
|
62
|
+
`postcss.config.cjs` for the worked example.
|
|
47
63
|
5. **react-intl**: an `IntlProvider` above any shared-ui usage. English
|
|
48
64
|
works with no setup (components carry inline `defaultMessage`); for
|
|
49
65
|
other locales compile this package's `lang/ui.<locale>.json` into the
|
|
@@ -58,6 +74,64 @@ node_modules/@microbit/ui/lang/ui.fr.json --ast --out-file ...` (multiple
|
|
|
58
74
|
app can dismiss open menus from outside the tree (e.g. the Android
|
|
59
75
|
hardware back button). Apps without one can omit the provider.
|
|
60
76
|
|
|
77
|
+
## Legacy browser support (Safari < 15) — temporary
|
|
78
|
+
|
|
79
|
+
Panda's output uses two things Safari below 15 mishandles. If an app must
|
|
80
|
+
support that far back (e.g. Safari 14.1 web views), add the app-side wiring
|
|
81
|
+
below. **All of it is meant to be deleted once the app's support floor rises
|
|
82
|
+
past these browsers** — it lives entirely in the consuming app's build config,
|
|
83
|
+
never in shipped component source. This package's own Storybook does _not_ use
|
|
84
|
+
any of it (it targets modern browsers, where these work natively).
|
|
85
|
+
|
|
86
|
+
Two concerns:
|
|
87
|
+
|
|
88
|
+
1. **`@layer`** — Safari < 15.4 drops `@layer` blocks wholesale, leaving the
|
|
89
|
+
app unstyled. Flatten them with `@csstools/postcss-cascade-layers` (which
|
|
90
|
+
rewrites layers into `:not(#\#)` specificity fallbacks; ~+8% gzipped CSS,
|
|
91
|
+
mostly compressible).
|
|
92
|
+
2. **Logical shorthands + `var()`** — Safari 14.x silently drops
|
|
93
|
+
`padding-inline: var(--…)` and friends (a literal value, or the -start/-end
|
|
94
|
+
longhands, both work). Panda emits these shorthands for its px/py/mx/my
|
|
95
|
+
utilities, so most token spacing collapses. Expand them to longhands with
|
|
96
|
+
this package's `postcss-legacy-safari` plugin (kept logical, so RTL flips).
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm i -D @csstools/postcss-cascade-layers
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// postcss.config.cjs — the two legacy plugins run AFTER Panda's (step 4), so
|
|
104
|
+
// switch that config to array form:
|
|
105
|
+
const {
|
|
106
|
+
expandLogicalShorthands,
|
|
107
|
+
} = require("@microbit/ui/postcss-legacy-safari");
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
plugins: [
|
|
111
|
+
require("@pandacss/dev/postcss")(),
|
|
112
|
+
expandLogicalShorthands(),
|
|
113
|
+
require("@csstools/postcss-cascade-layers"),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// vite.config.ts — pin the CSS/JS floor. Otherwise the lightningcss minifier
|
|
120
|
+
// inherits build.target and downlevels logical longhands into fragile
|
|
121
|
+
// :lang()-based physical rules. Keep in sync with package.json "browserslist".
|
|
122
|
+
const BUILD_TARGETS = ["safari14.1", "ios14.5", "chrome90", "edge90", "firefox88"];
|
|
123
|
+
// ...
|
|
124
|
+
build: {
|
|
125
|
+
target: BUILD_TARGETS,
|
|
126
|
+
cssTarget: BUILD_TARGETS,
|
|
127
|
+
cssMinify: "lightningcss", // lightningcss as minifier only, not the transformer
|
|
128
|
+
},
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To drop it all: raise `BUILD_TARGETS`/`browserslist` past the affected
|
|
132
|
+
browsers, then remove the two PostCSS plugins (and this package's
|
|
133
|
+
`postcss-legacy-safari` export).
|
|
134
|
+
|
|
61
135
|
## The CSS-variable contract
|
|
62
136
|
|
|
63
137
|
Panda emits every token as a CSS custom property with its default naming —
|
|
@@ -82,8 +156,9 @@ the token is consumed.
|
|
|
82
156
|
## Runtime token lookups
|
|
83
157
|
|
|
84
158
|
For values that feed _computation_ rather than stylesheets (canvas painting,
|
|
85
|
-
colour math, inline `style` for data-driven values — see gotcha #9 in
|
|
86
|
-
|
|
159
|
+
colour math, inline `style` for data-driven values — see gotcha #9 in the
|
|
160
|
+
repo's [migration playbook](../../docs/migration-playbook.md)), import the
|
|
161
|
+
runtime lookup:
|
|
87
162
|
|
|
88
163
|
```ts
|
|
89
164
|
import { token } from "@microbit/ui"; // re-exports styled-system/tokens
|
|
@@ -116,3 +191,17 @@ Crowdin via the repo-root `npm run update-translations -- <path to extracted
|
|
|
116
191
|
Crowdin ZIP>` (config-driven over packages in
|
|
117
192
|
`bin/update-translations.cjs`), after which you run `npm run i18n:tidy`
|
|
118
193
|
from the root.
|
|
194
|
+
|
|
195
|
+
## Chakra UI heritage and license
|
|
196
|
+
|
|
197
|
+
This package's design language began as a faithful port of
|
|
198
|
+
[Chakra UI](https://chakra-ui.com/) v2's, done so the apps it serves could
|
|
199
|
+
leave Chakra without a redesign: `src/base-tokens.ts` was snapshotted from
|
|
200
|
+
`@chakra-ui/theme`'s default token scales, and the `*.recipe.ts` files were
|
|
201
|
+
ported from Chakra's component styles onto Panda config recipes.
|
|
202
|
+
However far it evolves from that starting point, it owes its foundations to
|
|
203
|
+
Chakra. Thanks to Segun Adebayo and the Chakra UI contributors.
|
|
204
|
+
|
|
205
|
+
The package is [MIT](LICENSE.md) © Micro:bit Educational Foundation and
|
|
206
|
+
contributors; Chakra UI is MIT © Segun Adebayo, and its notice is carried
|
|
207
|
+
in [LICENSE.md](LICENSE.md).
|
package/lang/ui.de.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "Schließen",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "Dún",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "关闭",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a Chakra v2
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
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",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./base-preset": "./src/base-preset.ts",
|
|
10
|
-
"./
|
|
10
|
+
"./base-tokens": "./src/base-tokens.ts",
|
|
11
11
|
"./messages": "./src/messages.ts",
|
|
12
|
+
"./postcss-legacy-safari": "./postcss-legacy-safari.cjs",
|
|
12
13
|
"./lang/*": "./lang/*"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"src",
|
|
16
17
|
"lang",
|
|
18
|
+
"postcss-legacy-safari.cjs",
|
|
17
19
|
"README.md",
|
|
18
20
|
"LICENSE.md"
|
|
19
21
|
],
|
|
20
22
|
"sideEffects": false,
|
|
21
23
|
"scripts": {
|
|
22
|
-
"panda": "panda codegen
|
|
24
|
+
"panda": "panda codegen",
|
|
23
25
|
"typecheck": "npm run panda && tsc --noEmit",
|
|
24
26
|
"prestorybook": "npm run panda",
|
|
25
27
|
"storybook": "storybook dev -p 6006 --no-open",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* TEMPORARY compatibility shim for consuming apps that still support Safari
|
|
7
|
+
* below 15. Delete this file (and the app-side wiring — see the README) once
|
|
8
|
+
* every consuming app has raised its support floor past the affected browsers.
|
|
9
|
+
*
|
|
10
|
+
* The bug: Safari 14.x silently drops logical *shorthands* whose value
|
|
11
|
+
* contains var() — `padding-inline: var(--spacing-2)` applies nothing, even
|
|
12
|
+
* though `padding-inline: 10px` (literal) and `padding-inline-start:
|
|
13
|
+
* var(--spacing-2)` (longhand) both work. Panda emits these shorthands for its
|
|
14
|
+
* px/py/mx/my utilities, so the bug removes most token-based spacing.
|
|
15
|
+
*
|
|
16
|
+
* The fix: rewrite the inline/block logical shorthands into their -start/-end
|
|
17
|
+
* longhands. Kept logical (not physical left/right) so RTL still flips.
|
|
18
|
+
*
|
|
19
|
+
* This is one of TWO legacy concerns for the same era of browsers; the other
|
|
20
|
+
* is @layer, which those browsers drop wholesale. Apps also run
|
|
21
|
+
* @csstools/postcss-cascade-layers (de-layering) and pin build.cssTarget — see
|
|
22
|
+
* the "Legacy browser support" section of the README. All of it is expected to
|
|
23
|
+
* be removed together when the floor rises.
|
|
24
|
+
*
|
|
25
|
+
* Exposed as a PostCSS plugin factory:
|
|
26
|
+
* const { expandLogicalShorthands } = require("@microbit/ui/postcss-legacy-safari");
|
|
27
|
+
* module.exports = { plugins: [expandLogicalShorthands(), ...] };
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Logical shorthands whose value is `<start> <end>` (one value applies to
|
|
31
|
+
// both). NOT included: border-inline / border-block — those are compound
|
|
32
|
+
// (`width style color`) and duplicate the whole value to each side rather than
|
|
33
|
+
// splitting, so they would need different handling. Add them explicitly if a
|
|
34
|
+
// consuming app ever emits them.
|
|
35
|
+
const LOGICAL_SHORTHANDS = {
|
|
36
|
+
"padding-inline": ["padding-inline-start", "padding-inline-end"],
|
|
37
|
+
"padding-block": ["padding-block-start", "padding-block-end"],
|
|
38
|
+
"margin-inline": ["margin-inline-start", "margin-inline-end"],
|
|
39
|
+
"margin-block": ["margin-block-start", "margin-block-end"],
|
|
40
|
+
"inset-inline": ["inset-inline-start", "inset-inline-end"],
|
|
41
|
+
"inset-block": ["inset-block-start", "inset-block-end"],
|
|
42
|
+
"scroll-margin-inline": [
|
|
43
|
+
"scroll-margin-inline-start",
|
|
44
|
+
"scroll-margin-inline-end",
|
|
45
|
+
],
|
|
46
|
+
"scroll-margin-block": [
|
|
47
|
+
"scroll-margin-block-start",
|
|
48
|
+
"scroll-margin-block-end",
|
|
49
|
+
],
|
|
50
|
+
"scroll-padding-inline": [
|
|
51
|
+
"scroll-padding-inline-start",
|
|
52
|
+
"scroll-padding-inline-end",
|
|
53
|
+
],
|
|
54
|
+
"scroll-padding-block": [
|
|
55
|
+
"scroll-padding-block-start",
|
|
56
|
+
"scroll-padding-block-end",
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Split a value on top-level whitespace, ignoring spaces inside parens so
|
|
61
|
+
// var() fallbacks stay intact. One value applies to both sides; two map to
|
|
62
|
+
// start then end (per the CSS shorthand rules).
|
|
63
|
+
const splitTopLevel = (value) => {
|
|
64
|
+
const parts = [];
|
|
65
|
+
let depth = 0;
|
|
66
|
+
let current = "";
|
|
67
|
+
for (const ch of value) {
|
|
68
|
+
if (ch === "(") depth++;
|
|
69
|
+
else if (ch === ")") depth--;
|
|
70
|
+
if (depth === 0 && /\s/.test(ch)) {
|
|
71
|
+
if (current.trim()) parts.push(current.trim());
|
|
72
|
+
current = "";
|
|
73
|
+
} else {
|
|
74
|
+
current += ch;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (current.trim()) parts.push(current.trim());
|
|
78
|
+
return parts;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const expandLogicalShorthands = () => ({
|
|
82
|
+
postcssPlugin: "microbit-ui-expand-logical-shorthands",
|
|
83
|
+
Declaration(decl) {
|
|
84
|
+
const longhands = LOGICAL_SHORTHANDS[decl.prop.toLowerCase()];
|
|
85
|
+
if (!longhands) return;
|
|
86
|
+
const parts = splitTopLevel(decl.value);
|
|
87
|
+
if (parts.length === 0) return;
|
|
88
|
+
const [start, end = start] = parts;
|
|
89
|
+
decl.cloneBefore({ prop: longhands[0], value: start });
|
|
90
|
+
decl.cloneBefore({ prop: longhands[1], value: end });
|
|
91
|
+
decl.remove();
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expandLogicalShorthands.postcss = true;
|
|
95
|
+
|
|
96
|
+
module.exports = { expandLogicalShorthands };
|
package/src/Code.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* Code — inline code chip matching Chakra's <Code> (gray subtle, light mode).
|
|
10
|
+
*/
|
|
11
|
+
export const Code = styled("code", {
|
|
12
|
+
base: {
|
|
13
|
+
fontFamily: "mono",
|
|
14
|
+
fontSize: "sm",
|
|
15
|
+
px: "0.2em",
|
|
16
|
+
borderRadius: "sm",
|
|
17
|
+
bg: "gray.100",
|
|
18
|
+
color: "gray.800",
|
|
19
|
+
},
|
|
20
|
+
});
|
package/src/Collapse.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
CSSProperties,
|
|
8
|
+
ReactNode,
|
|
9
|
+
useEffect,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { css, cx } from "styled-system/css";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface CollapseProps {
|
|
18
|
+
/** Expanded when true (Chakra's `in`). */
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
/** Height when collapsed (Chakra's `startingHeight`). */
|
|
21
|
+
startingHeight?: number | string;
|
|
22
|
+
/**
|
|
23
|
+
* Height when expanded (Chakra's `endingHeight`; defaults to the measured
|
|
24
|
+
* content height, tracked with a ResizeObserver so nested expansion works).
|
|
25
|
+
*/
|
|
26
|
+
endingHeight?: number | string;
|
|
27
|
+
/** Remove the content from the DOM once the exit transition finishes. */
|
|
28
|
+
unmountOnExit?: boolean;
|
|
29
|
+
css?: SystemStyleObject;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toCssSize = (v: number | string) =>
|
|
36
|
+
typeof v === "number" ? `${v}px` : v;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Collapse — Chakra's Collapse transition without framer-motion: the content
|
|
40
|
+
* is measured and the wrapper's height (and opacity, when collapsing to zero)
|
|
41
|
+
* transitions between the collapsed and expanded sizes. Content stays mounted
|
|
42
|
+
* unless `unmountOnExit` is set.
|
|
43
|
+
*
|
|
44
|
+
* As with framer-motion, only `isOpen` changes animate: initial mounts render
|
|
45
|
+
* at rest, and height corrections (late measurement, content growth) snap —
|
|
46
|
+
* otherwise a collapse mounted inside an entering view morphs during the
|
|
47
|
+
* outer animation.
|
|
48
|
+
*/
|
|
49
|
+
export const Collapse = ({
|
|
50
|
+
isOpen,
|
|
51
|
+
startingHeight = 0,
|
|
52
|
+
endingHeight,
|
|
53
|
+
unmountOnExit = false,
|
|
54
|
+
css: cssProp,
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
children,
|
|
58
|
+
}: CollapseProps) => {
|
|
59
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const [measuredHeight, setMeasuredHeight] = useState<number>();
|
|
61
|
+
// For unmountOnExit: stay in the DOM until the exit transition ends.
|
|
62
|
+
const [present, setPresent] = useState(isOpen);
|
|
63
|
+
// The state the DOM shows. It follows isOpen from a layout effect so the
|
|
64
|
+
// transition-enable and the height flip land in the same commit (a
|
|
65
|
+
// render-time flip detector is unreliable: React can discard renders,
|
|
66
|
+
// losing the state update while keeping ref mutations).
|
|
67
|
+
const [displayOpen, setDisplayOpen] = useState(isOpen);
|
|
68
|
+
const [animating, setAnimating] = useState(false);
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (isOpen === displayOpen) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (isOpen) {
|
|
75
|
+
// Opening: mount (collapsed) first, then flip on the next frame so
|
|
76
|
+
// the browser has a computed collapsed state to transition from.
|
|
77
|
+
setPresent(true);
|
|
78
|
+
const raf = requestAnimationFrame(() => {
|
|
79
|
+
setAnimating(true);
|
|
80
|
+
setDisplayOpen(true);
|
|
81
|
+
});
|
|
82
|
+
return () => cancelAnimationFrame(raf);
|
|
83
|
+
}
|
|
84
|
+
// Closing: enable the transition and flip together, pre-paint.
|
|
85
|
+
setAnimating(true);
|
|
86
|
+
setDisplayOpen(false);
|
|
87
|
+
}, [isOpen, displayOpen]);
|
|
88
|
+
|
|
89
|
+
useLayoutEffect(() => {
|
|
90
|
+
const el = contentRef.current;
|
|
91
|
+
if (!el || endingHeight !== undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setMeasuredHeight(el.scrollHeight);
|
|
95
|
+
const observer = new ResizeObserver(() =>
|
|
96
|
+
setMeasuredHeight(el.scrollHeight),
|
|
97
|
+
);
|
|
98
|
+
observer.observe(el);
|
|
99
|
+
return () => observer.disconnect();
|
|
100
|
+
}, [endingHeight, present]);
|
|
101
|
+
|
|
102
|
+
// Fallback for browsers/edge cases where transitionend doesn't fire.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (animating) {
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
setAnimating(false);
|
|
107
|
+
if (!isOpen && unmountOnExit) {
|
|
108
|
+
setPresent(false);
|
|
109
|
+
}
|
|
110
|
+
}, 400);
|
|
111
|
+
return () => clearTimeout(timeout);
|
|
112
|
+
}
|
|
113
|
+
}, [animating, isOpen, unmountOnExit]);
|
|
114
|
+
|
|
115
|
+
if (unmountOnExit && !present) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const expanded =
|
|
120
|
+
endingHeight !== undefined
|
|
121
|
+
? toCssSize(endingHeight)
|
|
122
|
+
: measuredHeight !== undefined
|
|
123
|
+
? `${measuredHeight}px`
|
|
124
|
+
: "auto";
|
|
125
|
+
const collapsed = toCssSize(startingHeight);
|
|
126
|
+
const hideWhenCollapsed = collapsed === "0px" || collapsed === "0";
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
onTransitionEnd={(e) => {
|
|
131
|
+
if (e.propertyName === "height" && e.target === e.currentTarget) {
|
|
132
|
+
setAnimating(false);
|
|
133
|
+
if (!isOpen && unmountOnExit) {
|
|
134
|
+
setPresent(false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
className={cx(
|
|
139
|
+
css(
|
|
140
|
+
{
|
|
141
|
+
overflow: "hidden",
|
|
142
|
+
display: "block",
|
|
143
|
+
transitionDuration: "0.25s",
|
|
144
|
+
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
145
|
+
_motionReduce: { transition: "none" },
|
|
146
|
+
},
|
|
147
|
+
cssProp,
|
|
148
|
+
),
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
// Runtime-measured/caller-supplied heights; transitions enabled only
|
|
152
|
+
// while an isOpen change is in flight.
|
|
153
|
+
style={{
|
|
154
|
+
transitionProperty: animating ? "height, opacity" : "none",
|
|
155
|
+
height: displayOpen ? expanded : collapsed,
|
|
156
|
+
opacity: displayOpen || !hideWhenCollapsed ? 1 : 0,
|
|
157
|
+
// At rest fully-collapsed, remove the content from the a11y tree
|
|
158
|
+
// and tab order, as Chakra did (kept visible while animating so
|
|
159
|
+
// the exit transition shows).
|
|
160
|
+
visibility:
|
|
161
|
+
displayOpen || animating || !hideWhenCollapsed ? undefined : "hidden",
|
|
162
|
+
...style,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<div ref={contentRef}>{children}</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
package/src/Divider.tsx
CHANGED
|
@@ -3,19 +3,50 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
+
import { ComponentProps, forwardRef } from "react";
|
|
6
7
|
import { styled } from "styled-system/jsx";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
* Divider — a horizontal rule matching Chakra's <Divider> (hairline at 60%
|
|
10
|
-
* opacity; set `borderColor` to tint). Horizontal only.
|
|
11
|
-
*/
|
|
12
|
-
export const Divider = styled("hr", {
|
|
9
|
+
const StyledDivider = styled("hr", {
|
|
13
10
|
base: {
|
|
14
11
|
border: 0,
|
|
15
|
-
borderBottomWidth: "1px",
|
|
16
|
-
borderBottomStyle: "solid",
|
|
17
12
|
borderColor: "gray.200",
|
|
18
13
|
opacity: 0.6,
|
|
19
|
-
width: "100%",
|
|
20
14
|
},
|
|
15
|
+
variants: {
|
|
16
|
+
orientation: {
|
|
17
|
+
horizontal: {
|
|
18
|
+
borderBottomWidth: "1px",
|
|
19
|
+
borderBottomStyle: "solid",
|
|
20
|
+
width: "100%",
|
|
21
|
+
},
|
|
22
|
+
vertical: {
|
|
23
|
+
borderLeftWidth: "1px",
|
|
24
|
+
borderLeftStyle: "solid",
|
|
25
|
+
height: "100%",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { orientation: "horizontal" },
|
|
21
30
|
});
|
|
31
|
+
|
|
32
|
+
export interface DividerProps extends ComponentProps<typeof StyledDivider> {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Divider — a hairline rule matching Chakra's <Divider> (60% opacity; set
|
|
36
|
+
* `borderColor` to tint). `orientation="vertical"` needs a height from the
|
|
37
|
+
* layout, e.g. a stretched flex row (Chakra's vertical divider likewise
|
|
38
|
+
* relied on `height: 100%`).
|
|
39
|
+
*/
|
|
40
|
+
export const Divider = forwardRef<HTMLHRElement, DividerProps>(
|
|
41
|
+
function Divider(props, ref) {
|
|
42
|
+
return (
|
|
43
|
+
<StyledDivider
|
|
44
|
+
ref={ref}
|
|
45
|
+
aria-orientation={
|
|
46
|
+
props.orientation === "vertical" ? "vertical" : "horizontal"
|
|
47
|
+
}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|