@react-x11/components 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Andrey Sidorov
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 ADDED
@@ -0,0 +1,113 @@
1
+ # @react-x11/components
2
+
3
+ Components for [react-x11](https://github.com/sidorares/react-x11) that do
4
+ not belong in the core package.
5
+
6
+ Everything here is built on react-x11's public API — the built-in host
7
+ elements, or the `registerElement` seam in `react-x11/host`. Nothing here
8
+ needs a change to core to exist, and core does not grow to carry it.
9
+
10
+ > **Not published yet.** This package needs react-x11 2.0.0, which is not on
11
+ > npm — the subpath exports it imports (`react-x11/host`, `/node`, `/style`,
12
+ > `/test`) are on core's `master`, unreleased. Until then, use it from a
13
+ > checkout.
14
+
15
+ ## What is here, and what is in core
16
+
17
+ react-x11 itself carries an element or component when **any** of these hold:
18
+
19
+ - the vast majority of UI apps use it;
20
+ - it depends on renderer internals — implementing it outside would mean
21
+ exposing details that should not be public, or giving up performance;
22
+ - it needs enough standards compliance that the behaviour is hard to agree on
23
+ or implement piecemeal.
24
+
25
+ This package carries it when **all** of these hold:
26
+
27
+ - a smaller fraction of apps need it;
28
+ - it can be built on the public react-x11 API;
29
+ - it is big enough that core would pay for it, in install closure or in
30
+ maintenance.
31
+
32
+ So `<box>`, `<text>`, `<window>`, buttons, menus, dialogs and the rest of the
33
+ widget set are core. Heavier, more specialised things live here.
34
+
35
+ The line can also fall inside a single feature. `<glarea>` is core — it is a
36
+ real X window on a GLX visual, which is renderer internals. A Three.js-shaped
37
+ scene graph drawn into it is not: that is composition over a public element,
38
+ and it belongs here.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install @react-x11/components
44
+ ```
45
+
46
+ `react` and `react-x11` are peer dependencies — deliberately. Registering a
47
+ host element mutates state inside react-x11, so a second copy of the renderer
48
+ would leave you with an element that lays out correctly and never paints.
49
+
50
+ ## Usage
51
+
52
+ ```jsx
53
+ import { Sparkline } from '@react-x11/components';
54
+
55
+ function App() {
56
+ return (
57
+ <window width={360} height={160} title="components">
58
+ <box style={{ flexGrow: 1, padding: 16 }}>
59
+ <Sparkline
60
+ data={[3, 7, 4, 9, 6, 11, 8]}
61
+ color="#c0392b"
62
+ strokeWidth={2}
63
+ style={{ width: 320, height: 80 }}
64
+ />
65
+ </box>
66
+ </window>
67
+ );
68
+ }
69
+ ```
70
+
71
+ Importing a component is what teaches react-x11 its element, so there is no
72
+ setup call to remember and no registration to run at startup.
73
+
74
+ ## Tree-shaking
75
+
76
+ Use one component, pay for one component. Each is its own module with its own
77
+ entry point, the package declares `"sideEffects": false`, and importing the
78
+ barrel for nothing at all bundles to nothing. That last property is a test in
79
+ this repo, not an aspiration.
80
+
81
+ Deep imports work too, for apps without a bundler:
82
+
83
+ ```js
84
+ import { Sparkline } from '@react-x11/components/sparkline';
85
+ ```
86
+
87
+ ## Components
88
+
89
+ | Component | Import | |
90
+ | ----------- | --------------------------------- | ---------------------------------------------- |
91
+ | `Sparkline` | `@react-x11/components/sparkline` | A bare line chart. Needs a width and a height. |
92
+
93
+ ## Roadmap
94
+
95
+ Candidates to move here, none of them moved yet:
96
+
97
+ - `<markdown>` and `<html>`, currently in react-x11 over ntk's document
98
+ widgets. `<svg>` and `<tex>` stay in ntk. Mermaid was dropped rather than
99
+ extracted — 155 MB of install closure for a grammar.
100
+ - The 3D scene graph and a Three.js / react-three-fiber-shaped layer, with
101
+ `<glarea>` itself staying in core.
102
+ - A react-flow-style node/edge graph editor.
103
+ - `<Tabs>`, undecided — it may well stay in core.
104
+
105
+ ## Contributing
106
+
107
+ [AGENTS.md](AGENTS.md) is the contributor guide: the rule for what belongs
108
+ here, the layout, the tree-shaking constraints, and the two ways a registered
109
+ element fails silently.
110
+
111
+ ## License
112
+
113
+ MIT
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@react-x11/components",
3
+ "version": "0.1.0",
4
+ "description": "Components for react-x11 that do not belong in the core package",
5
+ "keywords": [
6
+ "react",
7
+ "react-x11",
8
+ "x11",
9
+ "components",
10
+ "widgets",
11
+ "desktop",
12
+ "ui"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/sidorares/react-x11-components.git"
17
+ },
18
+ "author": "Andrey Sidorov <andrey.sidorov@gmail.com>",
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "engines": {
22
+ "node": ">=20.19"
23
+ },
24
+ "files": [
25
+ "src"
26
+ ],
27
+ "sideEffects": false,
28
+ "types": "./src/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./src/index.d.ts",
32
+ "default": "./src/index.js"
33
+ },
34
+ "./sparkline": {
35
+ "types": "./src/sparkline/index.d.ts",
36
+ "default": "./src/sparkline/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "scripts": {
41
+ "test": "node --test",
42
+ "typecheck": "tsc -p tsconfig.json",
43
+ "lint": "eslint .",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
46
+ "check:package": "node scripts/check-package.mjs",
47
+ "examples:sparkline": "tsx examples/sparkline.jsx"
48
+ },
49
+ "peerDependencies": {
50
+ "react": "^19.0.0",
51
+ "react-x11": "^2.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^9.32.0",
55
+ "@types/react": "^19.2.17",
56
+ "esbuild": "^0.28.1",
57
+ "eslint": "^9.32.0",
58
+ "eslint-plugin-react": "^7.37.5",
59
+ "globals": "^17.8.0",
60
+ "prettier": "^3.6.0",
61
+ "react": "^19.2.8",
62
+ "react-x11": "github:sidorares/react-x11#master",
63
+ "tsx": "^4.23.1",
64
+ "typescript": "^7.0.2"
65
+ }
66
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Sparkline, SPARKLINE_ELEMENT } from './sparkline/index.js';
2
+ export type { SparklineProps } from './sparkline/index.js';
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The convenience barrel. `import { Sparkline } from '@react-x11/components'`
2
+ // and `import { Sparkline } from '@react-x11/components/sparkline'` are the
3
+ // same module either way — with `sideEffects: false` and no side effects at
4
+ // this level, a bundler drops the components an app does not name.
5
+ //
6
+ // This file must never do more than re-export. Anything with a side effect
7
+ // here (a registration, a theme install, a feature probe) runs for every
8
+ // consumer of the barrel and takes the whole package into their bundle.
9
+ export { Sparkline, SPARKLINE_ELEMENT } from './sparkline/index.js';
@@ -0,0 +1,29 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { Style } from 'react-x11/style';
3
+
4
+ export interface SparklineProps {
5
+ /** The series. Fewer than two points draws nothing. */
6
+ data: number[];
7
+ /** Stroke colour. Falls back to `style.color`, then black. */
8
+ color?: string;
9
+ /** Pen width in pixels. Default `1`. */
10
+ strokeWidth?: number;
11
+ /** No intrinsic size — give it a width and a height. */
12
+ style?: Style | Style[];
13
+ }
14
+
15
+ export declare function Sparkline(props: SparklineProps): ReactElement;
16
+
17
+ /** The host element name, for apps that would rather write `<sparkline>`. */
18
+ export declare const SPARKLINE_ELEMENT: 'sparkline';
19
+
20
+ // Importing this module teaches JSX the element too, so `<sparkline>` is a
21
+ // typed tag and not an error. This is the module-augmentation shape
22
+ // react-x11's docs/typescript.md prescribes for a third-party element.
23
+ declare module 'react-x11/jsx-runtime' {
24
+ namespace JSX {
25
+ interface IntrinsicElements {
26
+ sparkline: SparklineProps;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,50 @@
1
+ // <Sparkline> — a bare line chart, and the worked example of the
2
+ // `registerElement` path this whole package is built on. react-x11's own
3
+ // docs/extending.md illustrates the seam with a `<sparkline>`; this is that
4
+ // element, shipped.
5
+ //
6
+ // **Registration happens when this module is evaluated**, and that is the
7
+ // design, not a shortcut. Nothing in the package registers anything until
8
+ // an app imports the component that needs it, so `sideEffects: false` stays
9
+ // honest: an app that never renders a sparkline ships none of this. Do not
10
+ // move registration up into `../index.js` — that is the one edit that would
11
+ // make the barrel pull every component into every bundle.
12
+ import React from 'react';
13
+ import { registerElement, registeredElements } from 'react-x11/host';
14
+
15
+ import { ELEMENT, SparklineNode } from './node.js';
16
+
17
+ // Idempotent on purpose. `registerElement` throws on a second registration
18
+ // without `override`, which is the right default for two *packages* fighting
19
+ // over a name — but an app that ends up with two copies of this one (a
20
+ // version skew in someone's lockfile) should not fail to boot over it.
21
+ if (!registeredElements().includes(ELEMENT)) {
22
+ registerElement(ELEMENT, {
23
+ create: (props, app) => new SparklineNode(props, app),
24
+ // `color` is also a style name. Without declaring it the element throws
25
+ // on its own props in development and works in production — the worst
26
+ // shape a bug can have, so react-x11 asks elements to say so.
27
+ semanticNames: ['data', 'color', 'strokeWidth'],
28
+ childrenAllowed: false,
29
+ });
30
+ }
31
+
32
+ /**
33
+ * A sparkline. Has no intrinsic size — give it one through `style`, the way
34
+ * react-x11's own `<canvas>` wants one:
35
+ *
36
+ * ```jsx
37
+ * <Sparkline data={[1, 4, 2, 8]} color="#c0392b"
38
+ * style={{ width: 120, height: 40 }} />
39
+ * ```
40
+ *
41
+ * The component is a thin handle on the host element rather than a wrapper
42
+ * with behaviour of its own: it exists to carry the types, to be the thing
43
+ * an app imports (and therefore the thing that triggers registration), and
44
+ * to be the seam if this ever grows props that are not the element's.
45
+ */
46
+ export function Sparkline(props) {
47
+ return React.createElement(ELEMENT, props);
48
+ }
49
+
50
+ export { ELEMENT as SPARKLINE_ELEMENT };
@@ -0,0 +1,72 @@
1
+ // The retained node behind <sparkline>. `./index.js` is the only module
2
+ // that imports this one, and it is also the module that registers the
3
+ // element — so the pair is reachable exactly when an app imports
4
+ // `Sparkline`, and droppable as a unit when it does not (see AGENTS.md,
5
+ // "Tree-shaking is a constraint, not a nice-to-have").
6
+ import { Node } from 'react-x11/node';
7
+
8
+ /**
9
+ * The element name. The registration key, the node's `kind` and the JSX tag
10
+ * are all this one string — react-x11 rejects a node whose `kind` is
11
+ * anything but the name it was registered under, because `kind` is what
12
+ * paint order, queries and the DEV style assertion match on.
13
+ */
14
+ export const ELEMENT = 'sparkline';
15
+
16
+ const DEFAULT_COLOR = '#000000';
17
+
18
+ export class SparklineNode extends Node {
19
+ constructor(props, app) {
20
+ super(ELEMENT, props, app);
21
+ }
22
+
23
+ paint(ctx) {
24
+ // background, border and the clip to this node's rect
25
+ super.paint(ctx);
26
+
27
+ const data = this.props.data;
28
+ if (!Array.isArray(data) || data.length < 2) return;
29
+ // The mock backend in `react-x11/test` has no path API. A component
30
+ // that throws there cannot be tested headlessly, and headless is where
31
+ // CI runs — so the drawing is skipped rather than attempted.
32
+ if (typeof ctx.beginPath !== 'function') return;
33
+
34
+ const { x, y, width, height } = this.abs;
35
+ if (width <= 0 || height <= 0) return;
36
+
37
+ const lineWidth = this.props.strokeWidth ?? 1;
38
+ // `super.paint` clipped to `abs`, so a stroke sitting on the top or
39
+ // bottom edge would be cut in half. Inset by half the pen.
40
+ const inset = lineWidth / 2;
41
+ const top = y + inset;
42
+ const usable = Math.max(0, height - lineWidth);
43
+
44
+ let min = Infinity;
45
+ let max = -Infinity;
46
+ for (const value of data) {
47
+ if (value < min) min = value;
48
+ if (value > max) max = value;
49
+ }
50
+ const span = max - min;
51
+
52
+ const stepX = width / (data.length - 1);
53
+
54
+ ctx.save();
55
+ ctx.beginPath();
56
+ for (let i = 0; i < data.length; i++) {
57
+ const px = x + stepX * i;
58
+ // A flat series has no span to scale against; draw it down the middle
59
+ // rather than dividing by zero or pinning it to an edge.
60
+ const py =
61
+ span === 0
62
+ ? y + height / 2
63
+ : top + usable * (1 - (data[i] - min) / span);
64
+ if (i === 0) ctx.moveTo(px, py);
65
+ else ctx.lineTo(px, py);
66
+ }
67
+ ctx.lineWidth = lineWidth;
68
+ ctx.strokeStyle = this.props.color ?? this.style.color ?? DEFAULT_COLOR;
69
+ ctx.stroke();
70
+ ctx.restore();
71
+ }
72
+ }