opentui-responsive 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 +7 -0
- package/README.md +137 -0
- package/dist/core/index.d.ts +27 -0
- package/dist/core/index.js +79 -0
- package/dist/solid/index.d.ts +14 -0
- package/dist/solid/index.js +40 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Yudhistira Arief Wibowo
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# opentui-responsive
|
|
2
|
+
|
|
3
|
+
Typed responsive breakpoints for OpenTUI, with a framework-neutral core and a Solid adapter.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
For OpenTUI Solid applications:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add opentui-responsive @opentui/solid solid-js
|
|
11
|
+
# or
|
|
12
|
+
npm install opentui-responsive @opentui/solid solid-js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Core-only consumers only need `opentui-responsive`.
|
|
16
|
+
|
|
17
|
+
## Solid
|
|
18
|
+
|
|
19
|
+
Define mobile-first tiers once, then create a provider and hook bound to that definition:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { defineBreakpoints } from "opentui-responsive/core";
|
|
23
|
+
import { createResponsiveTui } from "opentui-responsive/solid";
|
|
24
|
+
|
|
25
|
+
const breakpoints = defineBreakpoints({
|
|
26
|
+
width: {
|
|
27
|
+
narrow: 0,
|
|
28
|
+
medium: 60,
|
|
29
|
+
wide: 100,
|
|
30
|
+
},
|
|
31
|
+
height: {
|
|
32
|
+
short: 0,
|
|
33
|
+
medium: 12,
|
|
34
|
+
tall: 20,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const { ResponsiveTUI, useResponsiveTui } = createResponsiveTui(breakpoints);
|
|
39
|
+
|
|
40
|
+
function App() {
|
|
41
|
+
return (
|
|
42
|
+
<ResponsiveTUI>
|
|
43
|
+
<Content />
|
|
44
|
+
</ResponsiveTUI>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Content() {
|
|
49
|
+
const breakpoint = useResponsiveTui();
|
|
50
|
+
return <text>{`${breakpoint().width}/${breakpoint().height}`}</text>;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`useResponsiveTui()` returns a Solid accessor. Each axis has its own inferred name union and updates when that terminal dimension crosses a configured threshold. In this example, width is `"narrow" | "medium" | "wide"`, height is `"short" | "medium" | "tall"`, and a `120 x 10` terminal returns `{ width: "wide", height: "short" }`.
|
|
55
|
+
|
|
56
|
+
Pass an exact `[width, height]` pair to check both axes reactively:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
breakpoint(["narrow", "tall"]); // boolean
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The pair must contain exactly one configured name for each axis, in width-then-height order.
|
|
63
|
+
|
|
64
|
+
TypeScript rejects names that are not configured for that axis:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
breakpoint().height === "extra-tall";
|
|
68
|
+
// Type error: "extra-tall" is not a configured height breakpoint
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Tiers
|
|
72
|
+
|
|
73
|
+
Width and height define independent sets of inclusive minimum thresholds in terminal cells. Each axis is matched to its highest satisfied threshold, so the number and names of options can differ between axes.
|
|
74
|
+
|
|
75
|
+
Breakpoint names must be non-empty strings. Thresholds must be unique finite non-negative integers within their axis.
|
|
76
|
+
|
|
77
|
+
Each axis must include a zero threshold so every terminal dimension has a match:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
width: { narrow: 0, medium: 60, wide: 100 },
|
|
81
|
+
height: { short: 0, medium: 12, tall: 20 },
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Declaration order does not affect matching:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
width: { wide: 100, narrow: 0, medium: 60 },
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Invalid definitions throw `ResponsiveTuiConfigurationError` during configuration. Calling a generated hook outside its provider throws `ResponsiveTuiProviderError`.
|
|
91
|
+
|
|
92
|
+
Use breakpoints for discrete layout modes. Keep continuous measurements such as progress-bar width and available list height on OpenTUI's `useTerminalDimensions()`.
|
|
93
|
+
|
|
94
|
+
## Core
|
|
95
|
+
|
|
96
|
+
The core definition has no framework dependencies and can match explicit dimensions directly:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const current = breakpoints.match({ width: 120, height: 10 });
|
|
100
|
+
// { width: "wide", height: "short" }
|
|
101
|
+
|
|
102
|
+
const exact = breakpoints.matches({ width: 120, height: 10 }, ["wide", "short"]);
|
|
103
|
+
// true
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Extract an axis's inferred name union with `BreakpointOf`:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
type WidthBreakpoint = BreakpointOf<typeof breakpoints, "width">;
|
|
110
|
+
type HeightBreakpoint = BreakpointOf<typeof breakpoints, "height">;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Packaging
|
|
114
|
+
|
|
115
|
+
The package is ESM-only. `opentui-responsive/core` and `opentui-responsive/solid` are separate package entrypoints, and the package is marked side-effect free so modern bundlers can remove unused exports. Importing `/core` does not load Solid or OpenTUI.
|
|
116
|
+
|
|
117
|
+
## Runtime support
|
|
118
|
+
|
|
119
|
+
The package supports Bun 1.3.0 or later and Node.js 26.4.0 or later. Node.js applications must use ESM. CommonJS `require()` is not supported.
|
|
120
|
+
|
|
121
|
+
The `/core` entrypoint is pure JavaScript and does not require native FFI:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
node core-example.mjs
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The `/solid` entrypoint inherits OpenTUI's native runtime requirements. Start Node.js applications that use it with:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
node --experimental-ffi app.mjs
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use Bun 1.4.0 or later on native Windows arm64.
|
|
134
|
+
|
|
135
|
+
## Publishing
|
|
136
|
+
|
|
137
|
+
Publishing a GitHub release whose tag matches the package version triggers `.github/workflows/publish.yml`. Configure npm trusted publishing for the `itsmeyaw/opentui-responsive` repository, the `publish.yml` workflow, and direct `npm publish` access; no `NPM_TOKEN` secret is used.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type BreakpointAxis = "width" | "height";
|
|
2
|
+
export type BreakpointScale = Readonly<Record<string, number>>;
|
|
3
|
+
export type BreakpointScales = {
|
|
4
|
+
readonly width: BreakpointScale;
|
|
5
|
+
readonly height: BreakpointScale;
|
|
6
|
+
};
|
|
7
|
+
export type BreakpointViewport = {
|
|
8
|
+
readonly width: number;
|
|
9
|
+
readonly height: number;
|
|
10
|
+
};
|
|
11
|
+
export type BreakpointMatch<Scales extends BreakpointScales = BreakpointScales> = {
|
|
12
|
+
readonly [Axis in BreakpointAxis]: Extract<keyof Scales[Axis], string>;
|
|
13
|
+
};
|
|
14
|
+
export type BreakpointPair<Scales extends BreakpointScales = BreakpointScales> = readonly [
|
|
15
|
+
width: BreakpointMatch<Scales>["width"],
|
|
16
|
+
height: BreakpointMatch<Scales>["height"]
|
|
17
|
+
];
|
|
18
|
+
export type BreakpointDefinition<Scales extends BreakpointScales = BreakpointScales> = {
|
|
19
|
+
readonly match: (viewport: BreakpointViewport) => BreakpointMatch<Scales>;
|
|
20
|
+
readonly matches: (viewport: BreakpointViewport, pair: BreakpointPair<Scales>) => boolean;
|
|
21
|
+
};
|
|
22
|
+
export type BreakpointOf<Input, Axis extends BreakpointAxis> = Input extends BreakpointDefinition<infer Scales> ? Extract<keyof Scales[Axis], string> : Input extends BreakpointScales ? Extract<keyof Input[Axis], string> : never;
|
|
23
|
+
export declare class ResponsiveTuiConfigurationError extends Error {
|
|
24
|
+
readonly _tag = "ResponsiveTuiConfigurationError";
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
export declare const defineBreakpoints: <const Scales extends BreakpointScales>(scales: Scales & Record<Exclude<keyof Scales, BreakpointAxis>, never>) => BreakpointDefinition<Scales>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/core/index.ts
|
|
2
|
+
class ResponsiveTuiConfigurationError extends Error {
|
|
3
|
+
_tag = "ResponsiveTuiConfigurationError";
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = this._tag;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
var defineBreakpoints = (scales) => {
|
|
10
|
+
const entries = validateScales(scales);
|
|
11
|
+
const match = (viewport) => ({
|
|
12
|
+
width: matchScale(viewport.width, entries.width),
|
|
13
|
+
height: matchScale(viewport.height, entries.height)
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
match,
|
|
17
|
+
matches: (viewport, pair) => {
|
|
18
|
+
const current = match(viewport);
|
|
19
|
+
return current.width === pair[0] && current.height === pair[1];
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
var breakpointAxes = ["width", "height"];
|
|
24
|
+
var validateScales = (scales) => {
|
|
25
|
+
if (!isRecord(scales) || Array.isArray(scales)) {
|
|
26
|
+
throw new ResponsiveTuiConfigurationError("Breakpoint definitions must be an object.");
|
|
27
|
+
}
|
|
28
|
+
const unknown = Object.keys(scales).find((key) => !breakpointAxes.includes(key));
|
|
29
|
+
if (unknown) {
|
|
30
|
+
throw new ResponsiveTuiConfigurationError(`Unknown breakpoint axis: ${unknown}.`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
width: validateScale("width", scales.width),
|
|
34
|
+
height: validateScale("height", scales.height)
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
var validateScale = (axis, scale) => {
|
|
38
|
+
if (!isRecord(scale) || Array.isArray(scale)) {
|
|
39
|
+
throw new ResponsiveTuiConfigurationError(`Breakpoint ${axis} scale must be an object.`);
|
|
40
|
+
}
|
|
41
|
+
const entries = Object.entries(scale);
|
|
42
|
+
if (entries.length === 0) {
|
|
43
|
+
throw new ResponsiveTuiConfigurationError(`Breakpoint ${axis} scale cannot be empty.`);
|
|
44
|
+
}
|
|
45
|
+
const thresholds = new Set;
|
|
46
|
+
const validated = [];
|
|
47
|
+
for (const [name, threshold] of entries) {
|
|
48
|
+
if (name.length === 0) {
|
|
49
|
+
throw new ResponsiveTuiConfigurationError("Breakpoint names must be non-empty strings.");
|
|
50
|
+
}
|
|
51
|
+
if (!isThreshold(threshold)) {
|
|
52
|
+
throw new ResponsiveTuiConfigurationError("Breakpoint thresholds must be finite non-negative integers.");
|
|
53
|
+
}
|
|
54
|
+
if (thresholds.has(threshold)) {
|
|
55
|
+
throw new ResponsiveTuiConfigurationError(`Breakpoint ${axis} thresholds must be unique.`);
|
|
56
|
+
}
|
|
57
|
+
thresholds.add(threshold);
|
|
58
|
+
validated.push([name, threshold]);
|
|
59
|
+
}
|
|
60
|
+
if (!thresholds.has(0)) {
|
|
61
|
+
throw new ResponsiveTuiConfigurationError(`Breakpoint ${axis} scale must include zero.`);
|
|
62
|
+
}
|
|
63
|
+
return validated.sort((left, right) => left[1] - right[1]);
|
|
64
|
+
};
|
|
65
|
+
var matchScale = (value, entries) => {
|
|
66
|
+
let match = entries[0][0];
|
|
67
|
+
for (const [name, threshold] of entries) {
|
|
68
|
+
if (value < threshold)
|
|
69
|
+
break;
|
|
70
|
+
match = name;
|
|
71
|
+
}
|
|
72
|
+
return match;
|
|
73
|
+
};
|
|
74
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
75
|
+
var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
76
|
+
export {
|
|
77
|
+
defineBreakpoints,
|
|
78
|
+
ResponsiveTuiConfigurationError
|
|
79
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ParentProps } from "solid-js";
|
|
2
|
+
import type { BreakpointDefinition, BreakpointMatch, BreakpointPair, BreakpointScales } from "../core/index.js";
|
|
3
|
+
export type ResponsiveBreakpointAccessor<Scales extends BreakpointScales = BreakpointScales> = {
|
|
4
|
+
(): BreakpointMatch<Scales>;
|
|
5
|
+
(pair: BreakpointPair<Scales>): boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare class ResponsiveTuiProviderError extends Error {
|
|
8
|
+
readonly _tag = "ResponsiveTuiProviderError";
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
export declare const createResponsiveTui: <const Scales extends BreakpointScales>(breakpoints: BreakpointDefinition<Scales>) => {
|
|
12
|
+
ResponsiveTUI: (props: ParentProps) => any;
|
|
13
|
+
useResponsiveTui: () => ResponsiveBreakpointAccessor<Scales>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/solid/index.tsx
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/solid";
|
|
3
|
+
import { createContext, createMemo, useContext } from "solid-js";
|
|
4
|
+
|
|
5
|
+
class ResponsiveTuiProviderError extends Error {
|
|
6
|
+
_tag = "ResponsiveTuiProviderError";
|
|
7
|
+
constructor() {
|
|
8
|
+
super("useResponsiveTui must be used within a ResponsiveTUI");
|
|
9
|
+
this.name = this._tag;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
var createResponsiveTui = (breakpoints) => {
|
|
13
|
+
const ResponsiveTuiContext = createContext();
|
|
14
|
+
const ResponsiveTUI = (props) => {
|
|
15
|
+
const dimensions = useTerminalDimensions();
|
|
16
|
+
const current = createMemo(() => breakpoints.match(dimensions()));
|
|
17
|
+
const breakpoint = (pair) => pair ? breakpoints.matches(dimensions(), pair) : current();
|
|
18
|
+
return ResponsiveTuiContext.Provider({
|
|
19
|
+
get children() {
|
|
20
|
+
return props.children;
|
|
21
|
+
},
|
|
22
|
+
value: breakpoint
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const useResponsiveTui = () => {
|
|
26
|
+
const breakpoint = useContext(ResponsiveTuiContext);
|
|
27
|
+
if (!breakpoint) {
|
|
28
|
+
throw new ResponsiveTuiProviderError;
|
|
29
|
+
}
|
|
30
|
+
return breakpoint;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
ResponsiveTUI,
|
|
34
|
+
useResponsiveTui
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export {
|
|
38
|
+
createResponsiveTui,
|
|
39
|
+
ResponsiveTuiProviderError
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opentui-responsive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed responsive breakpoints for OpenTUI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/itsmeyaw/opentui-responsive.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
"./core": {
|
|
19
|
+
"types": "./dist/core/index.d.ts",
|
|
20
|
+
"import": "./dist/core/index.js",
|
|
21
|
+
"default": "./dist/core/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./solid": {
|
|
24
|
+
"types": "./dist/solid/index.d.ts",
|
|
25
|
+
"import": "./dist/solid/index.js",
|
|
26
|
+
"default": "./dist/solid/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"accept:node": "node scripts/accept-node-core.mjs && node --experimental-ffi scripts/accept-node-solid.mjs",
|
|
34
|
+
"build": "bun build.ts && tsc -p tsconfig.build.json",
|
|
35
|
+
"check": "bun run format:check && bun run lint && bun run typecheck && bun test && bun run build && bun run check:package && bun run accept:node",
|
|
36
|
+
"check:package": "bun package-check.ts && publint --strict && attw --pack . --profile esm-only",
|
|
37
|
+
"format": "oxfmt .",
|
|
38
|
+
"format:check": "oxfmt --check .",
|
|
39
|
+
"lint": "oxlint --deny-warnings .",
|
|
40
|
+
"test": "bun test",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"prepack": "bun run build"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
46
|
+
"@opentui/solid": "0.5.11",
|
|
47
|
+
"@types/bun": "latest",
|
|
48
|
+
"oxfmt": "latest",
|
|
49
|
+
"oxlint": "1.82.0",
|
|
50
|
+
"publint": "0.3.24",
|
|
51
|
+
"solid-js": "1.9.12",
|
|
52
|
+
"typescript": "5.9.3"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@opentui/solid": "^0.5.11",
|
|
56
|
+
"solid-js": "1.9.12"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@opentui/solid": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"solid-js": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"bun": ">=1.3.0",
|
|
68
|
+
"node": ">=26.4.0"
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "bun@1.4.0"
|
|
71
|
+
}
|