@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 +21 -0
- package/README.md +113 -0
- package/package.json +66 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +9 -0
- package/src/sparkline/index.d.ts +29 -0
- package/src/sparkline/index.js +50 -0
- package/src/sparkline/node.js +72 -0
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
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
|
+
}
|