@microbit/ui-code-view 0.0.0 → 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Micro:bit Educational Foundation and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,145 @@
1
1
  # @microbit/ui-code-view
2
2
 
3
- This version is a placeholder used to set up publishing and contains no
4
- code. Use a later version.
3
+ Read-only, syntax-highlighted code views for micro:bit web apps, styled to
4
+ match the editors the code comes from. Successor to
5
+ `@microbit-foundation/react-code-view`.
5
6
 
6
- See https://github.com/microbit-foundation/ui/tree/main/packages/ui-code-view
7
+ ## What's here
8
+
9
+ - **`PythonEditorCodeView`** — Python, styled like the micro:bit Python
10
+ Editor.
11
+ - **`MakeCodeJavaScriptCodeView`** — MakeCode JavaScript, styled like
12
+ MakeCode's JavaScript view.
13
+
14
+ Both take a `code` prop and show line numbers. An optional `className` goes on
15
+ the outermost element, for example to make the background transparent. The
16
+ stylesheet's rules for that element (background, colour, font, padding) have
17
+ zero specificity, so any class wins over them whatever the stylesheet order.
18
+ Highlighting is done by [highlight.js](https://highlightjs.org/), which is a
19
+ regular dependency.
20
+
21
+ The view is always left to right (`dir="ltr"`), whatever the page direction,
22
+ and marked `translate="no"` so browser translation leaves the code alone.
23
+ While the code is too wide for the view, it is a focusable group named
24
+ "Python code" or "JavaScript code", so keyboard users can scroll it; code
25
+ that fits adds no tab stop. Line numbers are hidden from screen readers.
26
+
27
+ ## Consumption
28
+
29
+ Unlike the other packages in this repo, this one **ships built** (ES modules
30
+ plus type declarations) and is styled by a **plain stylesheet**, so it works
31
+ in apps with or without Panda CSS. It supports React 17, 18 and 19, and
32
+ needs `react-intl` (6 or 7), a peer dependency, for its accessible names.
33
+
34
+ 1. Install the package, and `react-intl` if the app doesn't already have it.
35
+ 2. Import the stylesheet once.
36
+
37
+ In an app set up as the [`@microbit/ui` README](../ui/README.md)
38
+ describes, import it into the `vendor` cascade layer of your entry
39
+ stylesheet, so it beats Panda's preflight but loses to your own styling:
40
+
41
+ ```css
42
+ @layer reset, vendor, base, tokens, recipes, utilities;
43
+
44
+ @import "@microbit/ui/reset.css" layer(reset);
45
+ @import "@microbit/ui-code-view/style.css" layer(vendor);
46
+ ```
47
+
48
+ Elsewhere, import it as you would any other stylesheet:
49
+
50
+ ```ts
51
+ import "@microbit/ui-code-view/style.css";
52
+ ```
53
+
54
+ 3. Render the components:
55
+
56
+ ```tsx
57
+ import { PythonEditorCodeView } from "@microbit/ui-code-view";
58
+
59
+ <PythonEditorCodeView code={"from microbit import *\n"} />;
60
+ ```
61
+
62
+ They must be inside an `IntlProvider`.
63
+
64
+ 4. **Strings**: the components carry no inline English, so compile this
65
+ package's catalogs into your per-locale catalogs alongside your own,
66
+ **English included** (message ids are namespaced `ui-code-view.`). With
67
+ [`@microbit/i18n-tools`](../i18n-tools), list the package in your catalog
68
+ config and `microbit-i18n compile` merges it, backfilling English for
69
+ locales without a translation:
70
+
71
+ ```js
72
+ catalogs: [
73
+ {
74
+ source: "lang/ui.en.json",
75
+ out: "app/messages/ui.{lang}.json",
76
+ packages: ["@microbit/ui-code-view"],
77
+ },
78
+ ],
79
+ ```
80
+
81
+ Without the catalog, the accessible names render as message ids.
82
+
83
+ The stylesheet loads Source Code Pro from `fonts.microbit.org` for the Python
84
+ view. Its class names (`mb-code-view…` and highlight.js's `hljs-…`) are part
85
+ of this package's API.
86
+
87
+ ## Colours
88
+
89
+ The Python view's syntax colours are CSS variables shared with the Python
90
+ Editor's CodeMirror theme:
91
+
92
+ | Variable | Used for | OSS default |
93
+ | --------------------------- | -------------------------------- | ---------------------- |
94
+ | `--colors-code-default` | Plain text | `black` |
95
+ | `--colors-code-keyword` | Keywords | `darkblue` |
96
+ | `--colors-code-string` | Strings | `green` |
97
+ | `--colors-code-comment` | Comments | `gray` |
98
+ | `--colors-code-literal` | `True`, `False`, `None`, numbers | `darkgreen` |
99
+ | `--colors-code-line-number` | Line numbers | `#b4b4b4` (`gray.350`) |
100
+
101
+ In a Panda app they come from the `code.*` tokens in `@microbit/ui`'s base
102
+ preset, so a brand preset that sets those tokens restyles this view and the
103
+ editor together. Without Panda, the stylesheet falls back to the OSS defaults;
104
+ set the variables in your own CSS (for example on `:root`) to change them.
105
+
106
+ The MakeCode view's colours are fixed: they copy MakeCode's own look.
107
+
108
+ ## Migrating from `@microbit-foundation/react-code-view`
109
+
110
+ - `PythonCodeRendering` is now `PythonEditorCodeView`, and
111
+ `MakeCodeJavaScriptRendering` is now `MakeCodeJavaScriptCodeView`.
112
+ - The stylesheet is `@microbit/ui-code-view/style.css`.
113
+ - The components now need an `IntlProvider` and this package's compiled
114
+ catalogs (see [Consumption](#consumption)).
115
+ - Style the view through `className` rather than by targeting its inner
116
+ elements. The old package rendered a single `<pre>`; the new markup is
117
+ different, so selectors such as `& pre` no longer reach the background.
118
+ - The Python view's colours are now the OSS defaults unless the app sets
119
+ them (see [Colours](#colours)). To keep the micro:bit palette, set the
120
+ `code.*` tokens in your brand preset or the variables in your CSS.
121
+ - Line numbers stay in place when long lines scroll sideways.
122
+ - The view has no outer margin. The old one rendered a bare `<pre>`, so it
123
+ picked up the browser's default margin unless the app reset it.
124
+
125
+ ## Development
126
+
127
+ Stories live in `stories/` here but are rendered by the repo's Storybook
128
+ harness, [`apps/storybook`](../../apps/storybook). Run it from the repo root:
129
+
130
+ ```bash
131
+ npm run storybook
132
+ ```
133
+
134
+ `npm run build -w @microbit/ui-code-view` compiles to `dist/`; it also runs on
135
+ `npm install` and before packing.
136
+
137
+ ## Releases
138
+
139
+ Publish a GitHub release with a tag of the form `ui-code-view-vX.Y.Z`. The
140
+ build workflow routes the release to this package by the tag prefix and
141
+ publishes it. This package is versioned independently of `@microbit/ui`.
142
+
143
+ ## License
144
+
145
+ [MIT](LICENSE.md) © Micro:bit Educational Foundation and contributors.
@@ -0,0 +1,13 @@
1
+ export interface CodeViewProps {
2
+ code: string;
3
+ /**
4
+ * Added to the outermost element, e.g. to change the background. Any class
5
+ * wins over the stylesheet's rules for that element, which are
6
+ * zero-specificity, so stylesheet order and cascade layers don't matter.
7
+ */
8
+ className?: string;
9
+ }
10
+ /** Python, styled like the micro:bit Python Editor. */
11
+ export declare const PythonEditorCodeView: (props: CodeViewProps) => import("react").JSX.Element;
12
+ /** MakeCode's JavaScript (a TypeScript subset), styled like its code view. */
13
+ export declare const MakeCodeJavaScriptCodeView: (props: CodeViewProps) => import("react").JSX.Element;
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * (c) 2026, Micro:bit Educational Foundation and contributors
4
+ *
5
+ * SPDX-License-Identifier: MIT
6
+ */
7
+ /**
8
+ * ui-code-view — read-only, syntax-highlighted code in the style of the
9
+ * micro:bit editors. Styled by the package's plain stylesheet
10
+ * (`@microbit/ui-code-view/style.css`), not Panda.
11
+ */
12
+ import hljs from "highlight.js/lib/core";
13
+ import python from "highlight.js/lib/languages/python";
14
+ import typescript from "highlight.js/lib/languages/typescript";
15
+ import { useEffect, useMemo, useRef, useState } from "react";
16
+ import { useIntl } from "react-intl";
17
+ // A private instance so an app's own highlight.js configuration (e.g. a
18
+ // classPrefix, which would break style.css) can't reach these components.
19
+ const highlighter = hljs.newInstance();
20
+ highlighter.registerLanguage("python", python);
21
+ highlighter.registerLanguage("typescript", typescript);
22
+ /**
23
+ * Whether the element currently scrolls sideways. Safari doesn't make scroll
24
+ * containers keyboard-focusable, so a scrolling view needs a tabIndex to be
25
+ * reachable without a mouse; one that fits stays out of the tab order.
26
+ */
27
+ const useScrollsHorizontally = (ref, content) => {
28
+ const [scrolls, setScrolls] = useState(false);
29
+ useEffect(() => {
30
+ const element = ref.current;
31
+ if (!element) {
32
+ return;
33
+ }
34
+ const update = () => setScrolls(element.scrollWidth > element.clientWidth);
35
+ update();
36
+ const observer = new ResizeObserver(update);
37
+ observer.observe(element);
38
+ // A web font arriving changes the content's width but not the element's.
39
+ let active = true;
40
+ void document.fonts?.ready.then(() => active && update());
41
+ return () => {
42
+ active = false;
43
+ observer.disconnect();
44
+ };
45
+ }, [ref, content]);
46
+ return scrolls;
47
+ };
48
+ const CodeView = ({ code: source, className, language, variant, label, }) => {
49
+ const intl = useIntl();
50
+ // The HTML parser turns a lone \r into a line break, so counting only \n
51
+ // would number fewer lines than the code shows.
52
+ const code = source.replace(/\r\n?/g, "\n");
53
+ // A trailing newline ends the last line rather than starting another.
54
+ const lineNumbers = code
55
+ .replace(/\n$/, "")
56
+ .split("\n")
57
+ .map((_, i) => i + 1)
58
+ .join("\n");
59
+ // highlight.js escapes the source; its output contains only its own spans.
60
+ const html = useMemo(() => highlighter.highlight(code, { language, ignoreIllegals: true }).value, [code, language]);
61
+ const codeRef = useRef(null);
62
+ const scrolls = useScrollsHorizontally(codeRef, html);
63
+ // Browsers drop focus from an element that stops being focusable, so a
64
+ // resize or zoom that ends the scrolling mustn't take the tabIndex away
65
+ // until focus moves on.
66
+ const [focused, setFocused] = useState(false);
67
+ const focusable = scrolls || focused;
68
+ // Line numbers are a separate column rather than a prefix on each line so
69
+ // highlight.js's output never needs splitting: its spans for multi-line
70
+ // strings and comments cross line breaks.
71
+ return (
72
+ // Code reads left to right whatever the page direction, and browser
73
+ // translation would rewrite identifiers and comments.
74
+ _jsxs("div", { className: `mb-code-view mb-code-view--${variant}${className ? ` ${className}` : ""}`, dir: "ltr", translate: "no", children: [_jsx("pre", { className: "mb-code-view__gutter", "aria-hidden": "true", children: lineNumbers }), _jsx("pre", { ref: codeRef, className: "mb-code-view__code", onFocus: () => setFocused(true), onBlur: () => setFocused(false), ...(focusable && {
75
+ tabIndex: 0,
76
+ role: "group",
77
+ "aria-label": intl.formatMessage({ id: label }),
78
+ }), children: _jsx("code", { dangerouslySetInnerHTML: { __html: html } }) })] }));
79
+ };
80
+ /** Python, styled like the micro:bit Python Editor. */
81
+ export const PythonEditorCodeView = (props) => (_jsx(CodeView, { ...props, language: "python", variant: "python-editor", label: "ui-code-view.python-label" }));
82
+ /** MakeCode's JavaScript (a TypeScript subset), styled like its code view. */
83
+ export const MakeCodeJavaScriptCodeView = (props) => (_jsx(CodeView, { ...props, language: "typescript", variant: "makecode-javascript", label: "ui-code-view.makecode-javascript-label" }));
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,10 @@
1
+ {
2
+ "ui-code-view.makecode-javascript-label": {
3
+ "defaultMessage": "JavaScript code",
4
+ "description": "Accessible name for a scrollable, read-only view of a MakeCode project's JavaScript"
5
+ },
6
+ "ui-code-view.python-label": {
7
+ "defaultMessage": "Python code",
8
+ "description": "Accessible name for a scrollable, read-only view of Python code"
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,15 +1,59 @@
1
1
  {
2
2
  "name": "@microbit/ui-code-view",
3
- "version": "0.0.0",
4
- "description": "Placeholder used to set up publishing. Not a release.",
3
+ "version": "0.1.1",
4
+ "description": "Read-only, syntax-highlighted code views styled like the micro:bit Python Editor and MakeCode's JavaScript view. Plain CSS, no Panda setup needed; see README.",
5
5
  "license": "MIT",
6
- "author": "Micro:bit Educational Foundation",
6
+ "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/microbit-foundation/ui.git",
10
10
  "directory": "packages/ui-code-view"
11
11
  },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./style.css": "./style.css",
18
+ "./lang/*": "./lang/*"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "lang",
23
+ "style.css",
24
+ "README.md",
25
+ "LICENSE.md"
26
+ ],
27
+ "sideEffects": [
28
+ "**/*.css"
29
+ ],
12
30
  "publishConfig": {
13
31
  "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
35
+ "prepare": "npm run build",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run"
38
+ },
39
+ "dependencies": {
40
+ "highlight.js": "^11.12.0"
41
+ },
42
+ "peerDependencies": {
43
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
44
+ "react-intl": "^6.6.8 || ^7.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@storybook/react-vite": "^10.5.2",
48
+ "@testing-library/react": "^16.3.2",
49
+ "@types/react": "^18.3.3",
50
+ "@types/react-dom": "^18.3.0",
51
+ "@vitejs/plugin-react": "^5.2.0",
52
+ "jsdom": "^29.1.1",
53
+ "react": "^18.3.1",
54
+ "react-dom": "^18.3.1",
55
+ "react-intl": "^6.6.8",
56
+ "typescript": "^5.4.2",
57
+ "vitest": "^4.1.10"
14
58
  }
15
59
  }
package/style.css ADDED
@@ -0,0 +1,124 @@
1
+ /*
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Plain CSS so apps without Panda can use the package. Panda apps import it
7
+ * into their `vendor` cascade layer, where it beats the preflight's pre/code
8
+ * rules but loses to app styling.
9
+ */
10
+ @font-face {
11
+ font-family: "Source Code Pro";
12
+ font-style: normal;
13
+ font-weight: 500;
14
+ src: url("https://fonts.microbit.org/source-code-pro/v14/source-code-pro-v14-latin-500.woff2")
15
+ format("woff2");
16
+ }
17
+
18
+ /* Rules on the outer element are zero-specificity so a consumer's className
19
+ beats them in apps without cascade layers too, whatever the stylesheet
20
+ order. The resets below keep their specificity: they must still beat
21
+ app-wide pre/code rules. */
22
+ :where(.mb-code-view) {
23
+ display: grid;
24
+ grid-template-columns: auto minmax(0, 1fr);
25
+ margin: 0;
26
+ padding: 20px 10px;
27
+ }
28
+
29
+ /* Resets so app-wide pre/code styling can't throw the two columns out of
30
+ line with each other. */
31
+ .mb-code-view__gutter,
32
+ .mb-code-view__code,
33
+ .mb-code-view__code code {
34
+ margin: 0;
35
+ padding: 0;
36
+ border: none;
37
+ background: none;
38
+ color: inherit;
39
+ font: inherit;
40
+ }
41
+
42
+ .mb-code-view__gutter {
43
+ text-align: right;
44
+ -webkit-user-select: none;
45
+ user-select: none;
46
+ }
47
+
48
+ .mb-code-view__code {
49
+ overflow-x: auto;
50
+ }
51
+
52
+ /* The @microbit/ui focus ring. The view's padding leaves room for it. */
53
+ .mb-code-view__code:focus-visible {
54
+ outline: 2px solid var(--colors-focus-ring, #1a1a1a);
55
+ outline-offset: 2px;
56
+ }
57
+
58
+ /* Colours come from the `code.*` tokens in @microbit/ui's base preset, so a
59
+ brand preset restyles this view and the Python Editor together. The
60
+ fallbacks repeat the base preset's OSS defaults for apps without Panda. */
61
+ :where(.mb-code-view--python-editor) {
62
+ background: #fcfcfc;
63
+ color: var(--colors-code-default, black);
64
+ font-family: "Source Code Pro", monospace;
65
+ font-size: 1.2rem;
66
+ line-height: 1.4;
67
+ }
68
+
69
+ .mb-code-view--python-editor .mb-code-view__gutter {
70
+ padding-right: 1em;
71
+ color: var(--colors-code-line-number, #b4b4b4);
72
+ }
73
+
74
+ .mb-code-view--python-editor .hljs-keyword {
75
+ color: var(--colors-code-keyword, darkblue);
76
+ }
77
+
78
+ .mb-code-view--python-editor .hljs-string {
79
+ color: var(--colors-code-string, green);
80
+ }
81
+
82
+ .mb-code-view--python-editor .hljs-comment {
83
+ color: var(--colors-code-comment, gray);
84
+ }
85
+
86
+ /* The editor has no number colour: CodeMirror's Python grammar treats numbers
87
+ as literals. */
88
+ .mb-code-view--python-editor .hljs-literal,
89
+ .mb-code-view--python-editor .hljs-number {
90
+ color: var(--colors-code-literal, darkgreen);
91
+ }
92
+
93
+ :where(.mb-code-view--makecode-javascript) {
94
+ background: #ecf0f1;
95
+ color: #000000;
96
+ font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro,
97
+ monospace;
98
+ /* 19px/26px at the default root size, scaling with the user's setting. */
99
+ font-size: 1.1875rem;
100
+ line-height: calc(26 / 19);
101
+ }
102
+
103
+ .mb-code-view--makecode-javascript .mb-code-view__gutter {
104
+ padding-right: 1.6em;
105
+ color: #2c3e50;
106
+ }
107
+
108
+ .mb-code-view--makecode-javascript .hljs-keyword,
109
+ .mb-code-view--makecode-javascript .hljs-built_in,
110
+ .mb-code-view--makecode-javascript .hljs-literal {
111
+ color: #0000ff;
112
+ }
113
+
114
+ .mb-code-view--makecode-javascript .hljs-string {
115
+ color: #a31515;
116
+ }
117
+
118
+ .mb-code-view--makecode-javascript .hljs-comment {
119
+ color: #008000;
120
+ }
121
+
122
+ .mb-code-view--makecode-javascript .hljs-number {
123
+ color: #008080;
124
+ }