@microbit/ui 0.1.0-alpha.23 → 0.1.0-alpha.25

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/README.md CHANGED
@@ -13,9 +13,8 @@ PostCSS plugin for the CSS).
13
13
 
14
14
  ## App-side installation
15
15
 
16
- This package's own Storybook (`.storybook/` + `panda.config.ts`) is a
17
- worked example of the setup below, minus the app/brand presets. Everything
18
- an app must do:
16
+ The repo's Storybook harness (`apps/storybook`) is a worked example of the
17
+ setup below, minus the app/brand presets. Everything an app must do:
19
18
 
20
19
  1. **Panda preset stack** (`panda.config.ts`): `@pandacss/preset-base`, then
21
20
  the **base preset** (`@microbit/ui/base-preset` — the complete micro:bit
@@ -42,7 +41,7 @@ an app must do:
42
41
  3. **Resolve `styled-system/*` onto the generated output** for all
43
42
  importers, this package's source included — a `styled-system` alias in
44
43
  both `tsconfig.json` `paths` and the bundler config (see the
45
- `viteFinal` in `.storybook/main.ts`).
44
+ `viteFinal` in `apps/storybook`'s `.storybook/main.ts`).
46
45
  4. **Generate and load the CSS** with Panda's PostCSS plugin. Keep Vite's
47
46
  default transformer — do **not** set `css.transformer: "lightningcss"`,
48
47
  which disables PostCSS. Add a `postcss.config.cjs`:
@@ -72,8 +71,9 @@ an app must do:
72
71
  width but no colour render `currentColor` borders.
73
72
  The `vendor` layer is for third-party stylesheets: import any vendor CSS
74
73
  with `@import "..." layer(vendor)` so it beats the preflight reset but
75
- loses to app styling. See `.storybook/{layers.css,preview.tsx,main.ts}` +
76
- `postcss.config.cjs` for the worked example.
74
+ loses to app styling. See `apps/storybook`'s
75
+ `.storybook/{layers.css,preview.tsx,main.ts}` + `postcss.config.cjs` for
76
+ the worked example.
77
77
 
78
78
  5. **react-intl**: an `IntlProvider` above any shared-ui usage. English
79
79
  works with no setup (components carry inline `defaultMessage`); for
@@ -87,11 +87,25 @@ node_modules/@microbit/ui/lang/ui.fr.json --ast --out-file ...` (multiple
87
87
  6. **`SharedUIProvider`** inside the `IntlProvider`, wrapping the app. It
88
88
  passes the locale on to react-aria, which translates its own built-in
89
89
  strings (see [Strings](#strings)); without it those follow the browser
90
- rather than the app's language setting. Also takes an optional
90
+ rather than the app's language setting. It also keeps `<html lang>` in
91
+ step with the locale so assistive tech announces the page in the app's
92
+ language — pass `setDocumentLang={false}` where the app doesn't own the
93
+ document it's mounted in (an embedded widget). Also takes an optional
91
94
  overlay-close registrar, so the app can dismiss open menus from outside
92
95
  the tree (e.g. the Android hardware back button).
93
96
  7. **`ToastProvider`** once near the root, inside the two providers above.
94
97
 
98
+ ## Upgrading in an app
99
+
100
+ After bumping the `@microbit/ui` version:
101
+
102
+ 1. `npm install` — the apps' postinstall runs `panda codegen`, but incremental
103
+ codegen does not detect external preset changes, so regenerate clean:
104
+ `rm -rf styled-system && npm run panda`.
105
+ 2. `npm run i18n:compile` — recompiles the app's per-locale catalogs so new
106
+ and retranslated `ui.*` strings ship (otherwise they fall back to English,
107
+ or stay missing).
108
+
95
109
  ## Legacy browser support (Safari < 15) — temporary
96
110
 
97
111
  Panda's output uses two things Safari below 15 mishandles. If an app must
package/lang/ui.ca.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "S'està carregant",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrementa {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.cy.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Wrthi'n Llwytho",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.de.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Laden",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "{fieldLabel} verringern",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Loading",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.en.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Loading",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Cargando",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Reducir {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.fr.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Chargement",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Diminuer {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Ag lódáil",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.it.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Caricamento",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Riduci {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.ja.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "読み込み中",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "{fieldLabel}を縮小",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.ko.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "불러오는 중...",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "{fieldLabel} 감소",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.lo.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "ກຳລັງໂຫຼດ",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.lol.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Loading",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Decrease {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.nl.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Laden",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Verlaag {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.pl.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Ładowanie",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Zmniejsz {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Carregando",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Diminuir {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/lang/ui.vi.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "Đang tải",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "Giảm {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "正在加载",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "降低 {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
@@ -19,6 +19,10 @@
19
19
  "defaultMessage": "正在載入",
20
20
  "description": "Announced by screen readers for a button showing a loading spinner"
21
21
  },
22
+ "ui.new-tab-notice": {
23
+ "defaultMessage": "opens in a new tab",
24
+ "description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
25
+ },
22
26
  "ui.numberfield-decrease": {
23
27
  "defaultMessage": "縮小 {fieldLabel}",
24
28
  "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microbit/ui",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.25",
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",
@@ -27,11 +27,7 @@
27
27
  "panda": "panda codegen",
28
28
  "typecheck": "npm run panda && tsc --noEmit",
29
29
  "pretest": "npm run panda",
30
- "test": "vitest run",
31
- "prestorybook": "npm run panda",
32
- "storybook": "storybook dev -p 6006 --no-open",
33
- "prebuild-storybook": "npm run panda",
34
- "build-storybook": "storybook build"
30
+ "test": "vitest run"
35
31
  },
36
32
  "peerDependencies": {
37
33
  "@pandacss/dev": "^1.11.4",
@@ -55,7 +51,6 @@
55
51
  "react-dom": "^18.3.1",
56
52
  "react-icons": "^4.12.0",
57
53
  "react-intl": "^6.6.8",
58
- "storybook": "^10.5.2",
59
54
  "typescript": "^5.4.2",
60
55
  "vite": "^6.3.5",
61
56
  "vitest": "^4.1.10"
@@ -154,8 +154,7 @@ export const button = defineRecipe({
154
154
  _hover: { bg: "danger.600", _disabled: { bg: "danger.500" } },
155
155
  _active: { bg: "danger.700" },
156
156
  },
157
- // Family-wide variants (every censused app has language- and
158
- // toolbar-class buttons).
157
+ // Family-wide variant (every censused app has toolbar-class buttons).
159
158
  toolbar: {
160
159
  color: "black",
161
160
  bg: "white",
@@ -163,12 +162,6 @@ export const button = defineRecipe({
163
162
  _active: { bg: "whiteAlpha.800" },
164
163
  _focusVisible: { focusShadow: "outlineDark" },
165
164
  },
166
- language: {
167
- borderWidth: "2px",
168
- borderColor: "gray.200",
169
- color: "languageText",
170
- _hover: { color: "languageTextHover", bg: "gray.100" },
171
- },
172
165
  },
173
166
  },
174
167
  defaultVariants: {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ComponentProps } from "react";
7
+ import { RiExternalLinkLine } from "react-icons/ri";
8
+ import { FormattedMessage } from "react-intl";
9
+ import { Icon } from "./Icon";
10
+ import { Link } from "./Link";
11
+ import { uiMessage } from "./messages";
12
+ import { VisuallyHidden } from "./VisuallyHidden";
13
+
14
+ export type ExternalLinkProps = Omit<
15
+ ComponentProps<typeof Link>,
16
+ "target" | "rel"
17
+ >;
18
+
19
+ /**
20
+ * ExternalLink — a Link that opens in a new tab and says so both ways: a
21
+ * trailing external-link glyph for sighted users, and a visually hidden,
22
+ * translated "opens in a new tab" for assistive tech. Use this rather than
23
+ * restating `target="_blank"` + icon so the announcement can't be forgotten.
24
+ *
25
+ * For app chrome, where a new tab protects the user's session-only state.
26
+ * Content surfaces should prefer same-tab links, or a visible
27
+ * "(opens in new tab)" suffix and no icon.
28
+ *
29
+ * Inline-safe: the icon is 1em and follows the text colour, so the link can
30
+ * sit in prose as well as stand alone.
31
+ */
32
+ export const ExternalLink = ({ children, ...props }: ExternalLinkProps) => (
33
+ <Link {...props} target="_blank" rel="noopener">
34
+ {children}
35
+ <VisuallyHidden>
36
+ {/* The comma separates the suffix in the accessible name: inline nodes
37
+ concatenate without a space, and leading whitespace is trimmed. */}
38
+ {", "}
39
+ <FormattedMessage {...uiMessage("ui.new-tab-notice")} />
40
+ </VisuallyHidden>
41
+ <Icon
42
+ as={RiExternalLinkLine}
43
+ aria-hidden
44
+ css={{ verticalAlign: "text-bottom", ml: 1 }}
45
+ />
46
+ </Link>
47
+ );
@@ -3,7 +3,13 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { createContext, ReactNode, useContext, useMemo } from "react";
6
+ import {
7
+ createContext,
8
+ ReactNode,
9
+ useContext,
10
+ useEffect,
11
+ useMemo,
12
+ } from "react";
7
13
  import { I18nProvider } from "react-aria-components";
8
14
  import { IntlContext } from "react-intl";
9
15
  import { racLocale } from "./rac-locale";
@@ -30,6 +36,14 @@ export interface SharedUIProviderProps {
30
36
  * two must differ.
31
37
  */
32
38
  locale?: string;
39
+ /**
40
+ * Keeps `<html lang>` in step with the locale, so assistive tech announces
41
+ * the page in the app's language. On by default; set false where the app
42
+ * doesn't own the document it's mounted in (an embedded widget), or when
43
+ * mounting more than one provider per page. The static `lang="en"` in
44
+ * index.html remains the pre-hydration default.
45
+ */
46
+ setDocumentLang?: boolean;
33
47
  children: ReactNode;
34
48
  }
35
49
 
@@ -48,20 +62,25 @@ export interface SharedUIProviderProps {
48
62
  export const SharedUIProvider = ({
49
63
  overlayCloseRegistrar,
50
64
  locale,
65
+ setDocumentLang = true,
51
66
  children,
52
67
  }: SharedUIProviderProps) => {
53
68
  // Read the context rather than calling useIntl(), which throws when there is
54
69
  // no IntlProvider: react-aria falls back to the browser locale, as before.
55
70
  const intlLocale = useContext(IntlContext)?.locale;
71
+ const appLocale = locale ?? intlLocale;
72
+ useEffect(() => {
73
+ if (setDocumentLang && appLocale) {
74
+ document.documentElement.lang = appLocale;
75
+ }
76
+ }, [setDocumentLang, appLocale]);
56
77
  const value = useMemo(
57
78
  () => ({ overlayCloseRegistrar }),
58
79
  [overlayCloseRegistrar],
59
80
  );
60
81
  return (
61
82
  <SharedUIContext.Provider value={value}>
62
- <I18nProvider locale={racLocale(locale ?? intlLocale)}>
63
- {children}
64
- </I18nProvider>
83
+ <I18nProvider locale={racLocale(appLocale)}>{children}</I18nProvider>
65
84
  </SharedUIContext.Provider>
66
85
  );
67
86
  };
package/src/Toast.tsx CHANGED
@@ -56,11 +56,20 @@ const wrapUpdate = (fn: () => void) => {
56
56
  activeTransitions++;
57
57
  document.documentElement.classList.add(TRANSITION_CLASS);
58
58
  const transition = document.startViewTransition(fn);
59
- transition.finished.finally(() => {
59
+ // A transition is routinely skipped — another update starts one while
60
+ // this is still animating, or the tab is hidden — and the browser then
61
+ // rejects its promises with "AbortError: Transition was skipped". The
62
+ // DOM update has still run; only the animation is dropped. Settle on
63
+ // rejection too (`.finally()` would re-throw, surfacing routine skips
64
+ // as unhandled rejections in error reporting) and swallow `ready`,
65
+ // which some engines reject without marking handled.
66
+ transition.ready.catch(() => {});
67
+ const settle = () => {
60
68
  if (--activeTransitions === 0) {
61
69
  document.documentElement.classList.remove(TRANSITION_CLASS);
62
70
  }
63
- });
71
+ };
72
+ transition.finished.then(settle, settle);
64
73
  } else {
65
74
  fn();
66
75
  }
@@ -86,7 +86,7 @@ const gray = {
86
86
  * The base preset: the complete, working micro:bit design system. The base
87
87
  * token scales (base-tokens.ts), the micro:bit house style
88
88
  * (pill `radii.button`, `outline*` focus shadows, Helvetica fonts, the
89
- * `language`/`toolbar` button variants in Button.recipe.ts, the
89
+ * `toolbar` button variant in Button.recipe.ts, the
90
90
  * `languageText`/`toast*Bg`/`statusBarBg` semantic tokens), the shared-ui
91
91
  * component recipes, the react-aria condition widening, the `globalCss`
92
92
  * defaults, and the `staticCss` that keeps runtime-prop recipe
@@ -207,11 +207,12 @@ export const basePreset = definePreset({
207
207
  600: { value: "{colors.red.600}" },
208
208
  700: { value: "{colors.red.700}" },
209
209
  },
210
- // The `language` button variant's text colour follows the primary
211
- // interactive brand: every consumer resolves it to its `brand` ramp
212
- // (CreateAI privately to brand.600 with no hover change,
213
- // python-editor to brand.500/600 — the default). Semantic tokens so
214
- // the recipe stays shared and a brand preset overrides only values.
210
+ // The language-dialog cards' text colour (@microbit/ui-patterns'
211
+ // LanguageDialog) follows the primary interactive brand: every
212
+ // consumer resolves it to its `brand` ramp (CreateAI privately to
213
+ // brand.600 with no hover change, python-editor to brand.500/600 —
214
+ // the default; data-microbit-org to black). Semantic tokens so the
215
+ // pattern stays shared and a brand preset overrides only values.
215
216
  languageText: { value: "{colors.brand.500}" },
216
217
  languageTextHover: { value: "{colors.brand.600}" },
217
218
  // The `label`/`subtitle` heading variants' colour (page-title chrome).
@@ -288,7 +289,8 @@ export const basePreset = definePreset({
288
289
  touchAction: "manipulation",
289
290
  },
290
291
  body: {
291
- position: "relative",
292
+ // No `position: relative` (Chakra had it): it breaks react-aria's
293
+ // overlay positioning — see Tooltip's "In a scrolling page" story.
292
294
  minHeight: "100%",
293
295
  fontFeatureSettings: '"kern"',
294
296
  fontFamily: "body",
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export * from "./Text";
40
40
  export * from "./TextField";
41
41
  export * from "./Heading";
42
42
  export * from "./Link";
43
+ export * from "./ExternalLink";
43
44
  export * from "./Icon";
44
45
  export * from "./CloseButton";
45
46
  export * from "./CloseIcon";