opentui-responsive 0.1.0 → 0.2.4

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
@@ -1,8 +1,17 @@
1
- # opentui-responsive
2
-
3
- Typed responsive breakpoints for OpenTUI, with a framework-neutral core and a Solid adapter.
4
-
5
- ## Install
1
+ <div align="center">
2
+ <h1>opentui-responsive</h1>
3
+ <p>
4
+ <a href="https://www.npmjs.com/package/opentui-responsive"><img src="https://img.shields.io/npm/v/opentui-responsive" alt="npm version"></a>
5
+ <a href="https://github.com/itsmeyaw/opentui-responsive/actions/workflows/ci.yml"><img src="https://github.com/itsmeyaw/opentui-responsive/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
6
+ <a href="https://github.com/itsmeyaw/opentui-responsive/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/opentui-responsive" alt="MIT license"></a>
7
+ </p>
8
+ <p>Typed responsive breakpoints for OpenTUI, with a framework-neutral core and React and Solid adapters.</p>
9
+ <p align="center">
10
+ <img src="https://raw.githubusercontent.com/itsmeyaw/opentui-responsive/main/docs/assets/demo.gif" alt="opentui-responsive terminal demo">
11
+ </p>
12
+ </div>
13
+
14
+ ## Installation
6
15
 
7
16
  For OpenTUI Solid applications:
8
17
 
@@ -12,31 +21,42 @@ bun add opentui-responsive @opentui/solid solid-js
12
21
  npm install opentui-responsive @opentui/solid solid-js
13
22
  ```
14
23
 
24
+ For OpenTUI React applications:
25
+
26
+ ```sh
27
+ bun add opentui-responsive @opentui/react react
28
+ # or
29
+ npm install opentui-responsive @opentui/react react
30
+ ```
31
+
15
32
  Core-only consumers only need `opentui-responsive`.
16
33
 
17
- ## Solid
34
+ ## Usage
18
35
 
19
- Define mobile-first tiers once, then create a provider and hook bound to that definition:
36
+ Define breakpoint tiers, then create a provider and hook bound to that definition:
20
37
 
21
38
  ```tsx
22
39
  import { defineBreakpoints } from "opentui-responsive/core";
23
40
  import { createResponsiveTui } from "opentui-responsive/solid";
24
41
 
25
42
  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
- },
43
+ width: { narrow: 0, medium: 60, wide: 100 },
44
+ height: { short: 0, medium: 16, tall: 28 },
36
45
  });
37
46
 
38
47
  const { ResponsiveTUI, useResponsiveTui } = createResponsiveTui(breakpoints);
39
48
 
49
+ function Content() {
50
+ const breakpoint = useResponsiveTui();
51
+
52
+ return (
53
+ <box flexDirection={breakpoint().width === "wide" ? "row" : "column"}>
54
+ <text>{`${breakpoint().width}/${breakpoint().height}`}</text>
55
+ <text>{breakpoint(["wide", "tall"]) ? "Full layout" : "Compact layout"}</text>
56
+ </box>
57
+ );
58
+ }
59
+
40
60
  function App() {
41
61
  return (
42
62
  <ResponsiveTUI>
@@ -44,94 +64,63 @@ function App() {
44
64
  </ResponsiveTUI>
45
65
  );
46
66
  }
47
-
48
- function Content() {
49
- const breakpoint = useResponsiveTui();
50
- return <text>{`${breakpoint().width}/${breakpoint().height}`}</text>;
51
- }
52
67
  ```
53
68
 
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" }`.
69
+ `useResponsiveTui()` updates when the terminal crosses a configured threshold. Its accessor returns the current width and height names, or accepts an exact `[width, height]` pair and returns whether both axes match.
55
70
 
56
- Pass an exact `[width, height]` pair to check both axes reactively:
71
+ Run `bun run demo` from the package directory to try the Solid example. Resize the terminal to see its layout respond.
57
72
 
58
- ```ts
59
- breakpoint(["narrow", "tall"]); // boolean
60
- ```
73
+ The React adapter has the same factory, provider, hook, and inferred breakpoint types; import `createResponsiveTui` from `opentui-responsive/react` instead.
61
74
 
62
- The pair must contain exactly one configured name for each axis, in width-then-height order.
75
+ ## API Reference
63
76
 
64
- TypeScript rejects names that are not configured for that axis:
77
+ ### `opentui-responsive/core`
65
78
 
66
- ```ts
67
- breakpoint().height === "extra-tall";
68
- // Type error: "extra-tall" is not a configured height breakpoint
69
- ```
70
-
71
- ## Tiers
79
+ | API | Description |
80
+ | ------------------------------------ | ------------------------------------------------------------------------------------ |
81
+ | `defineBreakpoints(scales)` | Validates width and height scales and returns typed `match` and `matches` functions. |
82
+ | `definition.match(viewport)` | Returns the highest inclusive breakpoint reached on each axis. |
83
+ | `definition.matches(viewport, pair)` | Tests an exact `[width, height]` breakpoint pair. |
84
+ | `BreakpointOf<Input, Axis>` | Extracts the inferred breakpoint-name union for one axis. |
85
+ | `BreakpointAxis` | The `"width" \| "height"` axis union. |
86
+ | `BreakpointScale` | A map of breakpoint names to minimum terminal-cell thresholds. |
87
+ | `BreakpointScales` | The width and height scale configuration. |
88
+ | `BreakpointViewport` | Explicit width and height dimensions to match. |
89
+ | `BreakpointMatch` | The matched breakpoint name for each axis. |
90
+ | `BreakpointPair` | An exact breakpoint pair in width-then-height order. |
91
+ | `BreakpointDefinition` | The typed result of `defineBreakpoints`. |
92
+ | `ResponsiveTuiConfigurationError` | Thrown when breakpoint scales are invalid. |
72
93
 
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.
94
+ ### `opentui-responsive/solid` and `opentui-responsive/react`
74
95
 
75
- Breakpoint names must be non-empty strings. Thresholds must be unique finite non-negative integers within their axis.
96
+ | API | Description |
97
+ | --------------------------------- | ---------------------------------------------------------------------------------- |
98
+ | `createResponsiveTui(definition)` | Creates a framework-specific `ResponsiveTUI` provider and `useResponsiveTui` hook. |
99
+ | `ResponsiveTUI` | Tracks terminal dimensions and provides the current breakpoint accessor. |
100
+ | `useResponsiveTui()` | Reads the accessor from the nearest generated provider. |
101
+ | `ResponsiveBreakpointAccessor` | Reads the current match or tests an exact breakpoint pair. |
102
+ | `ResponsiveTuiProviderError` | Thrown when the generated hook is called outside its provider. |
76
103
 
77
- Each axis must include a zero threshold so every terminal dimension has a match:
104
+ ## Breakpoint Behavior
78
105
 
79
- ```ts
80
- width: { narrow: 0, medium: 60, wide: 100 },
81
- height: { short: 0, medium: 12, tall: 20 },
82
- ```
106
+ Width and height use independent sets of inclusive minimum thresholds measured in terminal cells. Each axis must contain a zero threshold so every terminal size has a match. Names must be non-empty, and thresholds must be unique finite non-negative integers within their axis.
83
107
 
84
- Declaration order does not affect matching:
108
+ Declaration order does not affect matching. Each axis selects its highest satisfied threshold:
85
109
 
86
110
  ```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:
111
+ const breakpoints = defineBreakpoints({
112
+ width: { wide: 100, narrow: 0, medium: 60 },
113
+ height: { tall: 28, short: 0, medium: 16 },
114
+ });
97
115
 
98
- ```ts
99
- const current = breakpoints.match({ width: 120, height: 10 });
116
+ breakpoints.match({ width: 120, height: 10 });
100
117
  // { 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
118
  ```
112
119
 
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
- ```
120
+ Use breakpoints for discrete layout modes. Keep continuous measurements such as progress-bar width and available list height on OpenTUI's `useTerminalDimensions()`.
132
121
 
133
- Use Bun 1.4.0 or later on native Windows arm64.
122
+ ## Runtime Support
134
123
 
135
- ## Publishing
124
+ The package is ESM-only and supports Bun 1.3.0 or later and Node.js 26.4.0 or later. CommonJS `require()` is not supported.
136
125
 
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.
126
+ The `/core` entrypoint is pure JavaScript and does not require native FFI. The `/react` and `/solid` entrypoints inherit OpenTUI's native runtime requirements; Node.js applications using an adapter must start with `node --experimental-ffi app.mjs`. Use Bun 1.4.0 or later on native Windows arm64.
@@ -1,27 +1,42 @@
1
+ /** A terminal dimension used to select a breakpoint. */
1
2
  export type BreakpointAxis = "width" | "height";
3
+ /** Named inclusive minimum thresholds for one terminal dimension. */
2
4
  export type BreakpointScale = Readonly<Record<string, number>>;
5
+ /** Independent breakpoint scales for terminal width and height. */
3
6
  export type BreakpointScales = {
4
7
  readonly width: BreakpointScale;
5
8
  readonly height: BreakpointScale;
6
9
  };
10
+ /** Terminal dimensions in cells. */
7
11
  export type BreakpointViewport = {
8
12
  readonly width: number;
9
13
  readonly height: number;
10
14
  };
15
+ /** The breakpoint name matched on each axis. */
11
16
  export type BreakpointMatch<Scales extends BreakpointScales = BreakpointScales> = {
12
17
  readonly [Axis in BreakpointAxis]: Extract<keyof Scales[Axis], string>;
13
18
  };
19
+ /** An exact breakpoint pair in width-then-height order. */
14
20
  export type BreakpointPair<Scales extends BreakpointScales = BreakpointScales> = readonly [
15
21
  width: BreakpointMatch<Scales>["width"],
16
22
  height: BreakpointMatch<Scales>["height"]
17
23
  ];
24
+ /** A validated breakpoint definition that matches explicit terminal dimensions. */
18
25
  export type BreakpointDefinition<Scales extends BreakpointScales = BreakpointScales> = {
26
+ /** Returns the highest inclusive breakpoint reached on each axis. */
19
27
  readonly match: (viewport: BreakpointViewport) => BreakpointMatch<Scales>;
28
+ /** Tests whether a viewport matches an exact width and height pair. */
20
29
  readonly matches: (viewport: BreakpointViewport, pair: BreakpointPair<Scales>) => boolean;
21
30
  };
31
+ /** Extracts the configured breakpoint-name union for one axis. */
22
32
  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;
33
+ /** Thrown when breakpoint scales cannot produce a valid definition. */
23
34
  export declare class ResponsiveTuiConfigurationError extends Error {
24
35
  readonly _tag = "ResponsiveTuiConfigurationError";
25
36
  constructor(message: string);
26
37
  }
38
+ /**
39
+ * Creates a typed breakpoint definition from inclusive width and height thresholds.
40
+ * Each axis must contain a zero threshold.
41
+ */
27
42
  export declare const defineBreakpoints: <const Scales extends BreakpointScales>(scales: Scales & Record<Exclude<keyof Scales, BreakpointAxis>, never>) => BreakpointDefinition<Scales>;
@@ -74,6 +74,6 @@ var matchScale = (value, entries) => {
74
74
  var isRecord = (value) => typeof value === "object" && value !== null;
75
75
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
76
76
  export {
77
- defineBreakpoints,
78
- ResponsiveTuiConfigurationError
77
+ ResponsiveTuiConfigurationError,
78
+ defineBreakpoints
79
79
  };
@@ -0,0 +1,17 @@
1
+ import { type PropsWithChildren } from "react";
2
+ import type { BreakpointDefinition, BreakpointMatch, BreakpointPair, BreakpointScales } from "../core/index.js";
3
+ /** A React accessor that reads or tests the current breakpoint pair. */
4
+ export type ResponsiveBreakpointAccessor<Scales extends BreakpointScales = BreakpointScales> = {
5
+ (): BreakpointMatch<Scales>;
6
+ (pair: BreakpointPair<Scales>): boolean;
7
+ };
8
+ /** Thrown when a responsive hook is used outside its generated React provider. */
9
+ export declare class ResponsiveTuiProviderError extends Error {
10
+ readonly _tag = "ResponsiveTuiProviderError";
11
+ constructor();
12
+ }
13
+ /** Creates a React provider and hook bound to one breakpoint definition. */
14
+ export declare const createResponsiveTui: <const Scales extends BreakpointScales>(breakpoints: BreakpointDefinition<Scales>) => {
15
+ ResponsiveTUI: (props: PropsWithChildren) => import("react").FunctionComponentElement<import("react").ProviderProps<ResponsiveBreakpointAccessor<Scales> | undefined>>;
16
+ useResponsiveTui: () => ResponsiveBreakpointAccessor<Scales>;
17
+ };
@@ -0,0 +1,31 @@
1
+ // src/react/index.ts
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import { createContext, createElement, useContext } from "react";
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(undefined);
14
+ const ResponsiveTUI = (props) => {
15
+ const dimensions = useTerminalDimensions();
16
+ const breakpoint = (pair) => pair ? breakpoints.matches(dimensions, pair) : breakpoints.match(dimensions);
17
+ return createElement(ResponsiveTuiContext.Provider, { value: breakpoint }, props.children);
18
+ };
19
+ const useResponsiveTui = () => {
20
+ const breakpoint = useContext(ResponsiveTuiContext);
21
+ if (!breakpoint) {
22
+ throw new ResponsiveTuiProviderError;
23
+ }
24
+ return breakpoint;
25
+ };
26
+ return { ResponsiveTUI, useResponsiveTui };
27
+ };
28
+ export {
29
+ ResponsiveTuiProviderError,
30
+ createResponsiveTui
31
+ };
@@ -1,13 +1,16 @@
1
1
  import { type ParentProps } from "solid-js";
2
2
  import type { BreakpointDefinition, BreakpointMatch, BreakpointPair, BreakpointScales } from "../core/index.js";
3
+ /** A reactive Solid accessor that reads or tests the current breakpoint pair. */
3
4
  export type ResponsiveBreakpointAccessor<Scales extends BreakpointScales = BreakpointScales> = {
4
5
  (): BreakpointMatch<Scales>;
5
6
  (pair: BreakpointPair<Scales>): boolean;
6
7
  };
8
+ /** Thrown when a responsive hook is used outside its generated Solid provider. */
7
9
  export declare class ResponsiveTuiProviderError extends Error {
8
10
  readonly _tag = "ResponsiveTuiProviderError";
9
11
  constructor();
10
12
  }
13
+ /** Creates a Solid provider and hook bound to one breakpoint definition. */
11
14
  export declare const createResponsiveTui: <const Scales extends BreakpointScales>(breakpoints: BreakpointDefinition<Scales>) => {
12
15
  ResponsiveTUI: (props: ParentProps) => any;
13
16
  useResponsiveTui: () => ResponsiveBreakpointAccessor<Scales>;
@@ -1,4 +1,4 @@
1
- // src/solid/index.tsx
1
+ // src/solid/index.ts
2
2
  import { useTerminalDimensions } from "@opentui/solid";
3
3
  import { createContext, createMemo, useContext } from "solid-js";
4
4
 
@@ -29,12 +29,9 @@ var createResponsiveTui = (breakpoints) => {
29
29
  }
30
30
  return breakpoint;
31
31
  };
32
- return {
33
- ResponsiveTUI,
34
- useResponsiveTui
35
- };
32
+ return { ResponsiveTUI, useResponsiveTui };
36
33
  };
37
34
  export {
38
- createResponsiveTui,
39
- ResponsiveTuiProviderError
35
+ ResponsiveTuiProviderError,
36
+ createResponsiveTui
40
37
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentui-responsive",
3
- "version": "0.1.0",
3
+ "version": "0.2.4",
4
4
  "description": "Typed responsive breakpoints for OpenTUI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,16 +24,23 @@
24
24
  "types": "./dist/solid/index.d.ts",
25
25
  "import": "./dist/solid/index.js",
26
26
  "default": "./dist/solid/index.js"
27
+ },
28
+ "./react": {
29
+ "types": "./dist/react/index.d.ts",
30
+ "import": "./dist/react/index.js",
31
+ "default": "./dist/react/index.js"
27
32
  }
28
33
  },
29
34
  "publishConfig": {
30
35
  "access": "public"
31
36
  },
32
37
  "scripts": {
33
- "accept:node": "node scripts/accept-node-core.mjs && node --experimental-ffi scripts/accept-node-solid.mjs",
38
+ "accept:node": "node scripts/accept-node-core.mjs && node --experimental-ffi scripts/accept-node-solid.mjs && node --experimental-ffi scripts/accept-node-react.mjs",
34
39
  "build": "bun build.ts && tsc -p tsconfig.build.json",
35
40
  "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
41
  "check:package": "bun package-check.ts && publint --strict && attw --pack . --profile esm-only",
42
+ "demo": "bun --preload @opentui/solid/preload examples/solid.tsx",
43
+ "demo:react": "bun examples/react.tsx",
37
44
  "format": "oxfmt .",
38
45
  "format:check": "oxfmt --check .",
39
46
  "lint": "oxlint --deny-warnings .",
@@ -43,22 +50,34 @@
43
50
  },
44
51
  "devDependencies": {
45
52
  "@arethetypeswrong/cli": "0.18.5",
53
+ "@opentui/core": "0.5.11",
54
+ "@opentui/react": "0.5.11",
46
55
  "@opentui/solid": "0.5.11",
47
56
  "@types/bun": "latest",
57
+ "@types/react": "19.2.0",
48
58
  "oxfmt": "latest",
49
59
  "oxlint": "1.82.0",
50
60
  "publint": "0.3.24",
61
+ "react": "19.2.0",
51
62
  "solid-js": "1.9.12",
52
63
  "typescript": "5.9.3"
53
64
  },
54
65
  "peerDependencies": {
66
+ "@opentui/react": "^0.5.11",
55
67
  "@opentui/solid": "^0.5.11",
68
+ "react": ">=19.2.0",
56
69
  "solid-js": "1.9.12"
57
70
  },
58
71
  "peerDependenciesMeta": {
72
+ "@opentui/react": {
73
+ "optional": true
74
+ },
59
75
  "@opentui/solid": {
60
76
  "optional": true
61
77
  },
78
+ "react": {
79
+ "optional": true
80
+ },
62
81
  "solid-js": {
63
82
  "optional": true
64
83
  }