opentui-responsive 0.3.1 → 0.4.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/README.md CHANGED
@@ -61,12 +61,17 @@ export function Content() {
61
61
  const breakpoint = useBreakpoint();
62
62
  // You can use it to directly check [width, height] breakpoint
63
63
  const isWideAndTall = breakpoint(["wide", "tall"]);
64
+ // Compare one axis, or compare both axes with AND semantics
65
+ const isMediumOrWider = breakpoint.atLeast("width", "medium");
66
+ const hasMediumViewport = breakpoint.atLeast(["medium", "medium"]);
64
67
 
65
68
  return (
66
69
  // Or you can get the current width and height breakpoint
67
70
  <box flexDirection={breakpoint().width === "wide" ? "row" : "column"}>
68
71
  <text>{`${breakpoint().width}/${breakpoint().height}`}</text>
69
- <text>{isWideAndTall ? "Full layout" : "Compact layout"}</text>
72
+ <text>
73
+ {isWideAndTall || (isMediumOrWider && hasMediumViewport) ? "Full layout" : "Compact layout"}
74
+ </text>
70
75
  </box>
71
76
  );
72
77
  }
@@ -87,7 +92,15 @@ export function App() {
87
92
  }
88
93
  ```
89
94
 
90
- `useBreakpoint()` 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.
95
+ `useBreakpoint()` updates when the terminal crosses a configured threshold. Its accessor returns the current width and height names, accepts an exact `[width, height]` pair, and provides named relation methods. Relation methods accept either an axis and name or a pair; pair comparisons return true only when both axes satisfy the relation.
96
+
97
+ ```tsx
98
+ breakpoint.below("width", "medium");
99
+ breakpoint.atMost(["medium", "tall"]);
100
+ breakpoint.only("height", "short");
101
+ breakpoint.atLeast(["medium", "medium"]);
102
+ breakpoint.above("width", "medium");
103
+ ```
91
104
 
92
105
  Run `bun run demo` from the package directory to try the Solid example. Resize the terminal to see its layout respond.
93
106
 
@@ -97,14 +110,15 @@ The React adapter has the same factory, provider, hook, and inferred breakpoint
97
110
 
98
111
  ### `opentui-responsive/solid` and `opentui-responsive/react`
99
112
 
100
- | API | Description |
101
- | --------------------------------- | ------------------------------------------------------------------------------------- |
102
- | `createResponsiveTui(scales)` | Validates the scales and creates a `ResponsiveTUI` provider and `useBreakpoint` hook. |
103
- | `ResponsiveTUI` | Tracks terminal dimensions and provides the current breakpoint accessor. |
104
- | `useBreakpoint()` | Reads the accessor from the nearest generated provider. |
105
- | `ResponsiveBreakpointAccessor` | Reads the current match or tests an exact breakpoint pair. |
106
- | `ResponsiveTuiConfigurationError` | Thrown when the supplied breakpoint scales are invalid. |
107
- | `ResponsiveTuiProviderError` | Thrown when the generated hook is called outside its provider. |
113
+ | API | Description |
114
+ | ------------------------------------- | ------------------------------------------------------------------------------------- |
115
+ | `createResponsiveTui(scales)` | Validates the scales and creates a `ResponsiveTUI` provider and `useBreakpoint` hook. |
116
+ | `ResponsiveTUI` | Tracks terminal dimensions and provides the current breakpoint accessor. |
117
+ | `useBreakpoint()` | Reads the accessor from the nearest generated provider. |
118
+ | `ResponsiveBreakpointAccessor` | Reads the current match or tests exact and relative breakpoint positions. |
119
+ | `ResponsiveBreakpointRelationMatcher` | Tests one axis or an AND-combined width and height pair. |
120
+ | `ResponsiveTuiConfigurationError` | Thrown when the supplied breakpoint scales are invalid. |
121
+ | `ResponsiveTuiProviderError` | Thrown when the generated hook is called outside its provider. |
108
122
 
109
123
  ### `opentui-responsive/core`
110
124
 
@@ -117,6 +131,7 @@ For implementing another framework adapter, not application setup:
117
131
  | `BreakpointScales` | The width and height scale configuration. |
118
132
  | `BreakpointMatch` | The matched breakpoint name for each axis. |
119
133
  | `BreakpointPair` | An exact breakpoint pair in width-then-height order. |
134
+ | `BreakpointRelationMatcher` | Tests one axis or an AND-combined pair against a named relation. |
120
135
  | `ResponsiveTuiConfigurationError` | Thrown when supplied breakpoint scales are invalid. |
121
136
 
122
137
  ## Breakpoint Behavior
@@ -125,6 +140,18 @@ Width and height use independent sets of inclusive minimum thresholds measured i
125
140
 
126
141
  Declaration order does not affect matching. Each axis selects its highest satisfied threshold.
127
142
 
143
+ Relation methods compare complete tiers rather than their raw threshold values. Given `width: { narrow: 0, medium: 60, wide: 100 }`, `atMost("width", "medium")` matches widths from `0` through `99`.
144
+
145
+ | Method | Tier relation |
146
+ | --------- | ------------- |
147
+ | `below` | `<` |
148
+ | `atMost` | `<=` |
149
+ | `only` | `===` |
150
+ | `atLeast` | `>=` |
151
+ | `above` | `>` |
152
+
153
+ Each method accepts `(axis, name)` for a single-axis comparison or `[width, height]` for a two-axis comparison. Two-axis comparisons use AND semantics. Calling `breakpoint.only(["wide", "tall"])` is equivalent to calling `breakpoint(["wide", "tall"])`.
154
+
128
155
  Use breakpoints for discrete layout modes. Keep continuous measurements such as progress-bar width and available list height on OpenTUI's `useTerminalDimensions()`.
129
156
 
130
157
  ## Runtime Support
@@ -24,10 +24,20 @@ export type BreakpointPair<Scales extends BreakpointScales = BreakpointScales> =
24
24
  width: BreakpointMatch<Scales>["width"],
25
25
  height: BreakpointMatch<Scales>["height"]
26
26
  ];
27
+ /** Tests one axis or both axes against a named breakpoint relation. */
28
+ export type BreakpointRelationMatcher<Scales extends BreakpointScales = BreakpointScales> = {
29
+ <Axis extends BreakpointAxis>(viewport: BreakpointViewport, axis: Axis, name: BreakpointMatch<Scales>[Axis]): boolean;
30
+ (viewport: BreakpointViewport, pair: BreakpointPair<Scales>): boolean;
31
+ };
27
32
  /** A validated breakpoint matcher for framework adapter implementations. */
28
33
  export type BreakpointDefinition<Scales extends BreakpointScales = BreakpointScales> = {
29
34
  readonly match: (viewport: BreakpointViewport) => BreakpointMatch<Scales>;
30
35
  readonly matches: (viewport: BreakpointViewport, pair: BreakpointPair<Scales>) => boolean;
36
+ readonly below: BreakpointRelationMatcher<Scales>;
37
+ readonly atMost: BreakpointRelationMatcher<Scales>;
38
+ readonly only: BreakpointRelationMatcher<Scales>;
39
+ readonly atLeast: BreakpointRelationMatcher<Scales>;
40
+ readonly above: BreakpointRelationMatcher<Scales>;
31
41
  };
32
42
  /** Thrown when breakpoint scales cannot produce a valid definition. */
33
43
  export declare class ResponsiveTuiConfigurationError extends Error {
@@ -12,12 +12,19 @@ var createBreakpointDefinition = (scales) => {
12
12
  width: matchScale(viewport.width, entries.width),
13
13
  height: matchScale(viewport.height, entries.height)
14
14
  });
15
+ const below = createRelationMatcher(entries, "below");
16
+ const atMost = createRelationMatcher(entries, "atMost");
17
+ const only = createRelationMatcher(entries, "only");
18
+ const atLeast = createRelationMatcher(entries, "atLeast");
19
+ const above = createRelationMatcher(entries, "above");
15
20
  return {
16
21
  match,
17
- matches: (viewport, pair) => {
18
- const current = match(viewport);
19
- return current.width === pair[0] && current.height === pair[1];
20
- }
22
+ matches: (viewport, pair) => only(viewport, pair),
23
+ below,
24
+ atMost,
25
+ only,
26
+ atLeast,
27
+ above
21
28
  };
22
29
  };
23
30
  var breakpointAxes = ["width", "height"];
@@ -63,14 +70,39 @@ var validateScale = (axis, scale) => {
63
70
  return validated.sort((left, right) => left[1] - right[1]);
64
71
  };
65
72
  var matchScale = (value, entries) => {
66
- let match = entries[0][0];
67
- for (const [name, threshold] of entries) {
68
- if (value < threshold)
73
+ return entries[matchScaleIndex(value, entries)][0];
74
+ };
75
+ var matchScaleIndex = (value, entries) => {
76
+ let match = 0;
77
+ for (let index = 1;index < entries.length; index += 1) {
78
+ if (value < entries[index][1])
69
79
  break;
70
- match = name;
80
+ match = index;
71
81
  }
72
82
  return match;
73
83
  };
84
+ var createRelationMatcher = (entries, relation) => (viewport, axisOrPair, name) => {
85
+ if (Array.isArray(axisOrPair)) {
86
+ return matchesRelation(viewport.width, entries.width, axisOrPair[0], relation) && matchesRelation(viewport.height, entries.height, axisOrPair[1], relation);
87
+ }
88
+ const axis = axisOrPair;
89
+ return matchesRelation(viewport[axis], entries[axis], name, relation);
90
+ };
91
+ var matchesRelation = (value, entries, name, relation) => {
92
+ const target = entries.findIndex(([entryName]) => entryName === name);
93
+ if (target === -1)
94
+ return false;
95
+ const current = matchScaleIndex(value, entries);
96
+ if (relation === "below")
97
+ return current < target;
98
+ if (relation === "atMost")
99
+ return current <= target;
100
+ if (relation === "only")
101
+ return current === target;
102
+ if (relation === "atLeast")
103
+ return current >= target;
104
+ return current > target;
105
+ };
74
106
  var isRecord = (value) => typeof value === "object" && value !== null;
75
107
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
76
108
  export {
@@ -1,10 +1,20 @@
1
1
  import { type PropsWithChildren } from "react";
2
- import { createBreakpointDefinition, type BreakpointMatch, type BreakpointPair, type BreakpointScales } from "../core/index.js";
2
+ import { createBreakpointDefinition, type BreakpointAxis, type BreakpointMatch, type BreakpointPair, type BreakpointScales } from "../core/index.js";
3
3
  export { ResponsiveTuiConfigurationError } from "../core/index.js";
4
+ /** A React breakpoint relation test for one axis or both axes. */
5
+ export type ResponsiveBreakpointRelationMatcher<Scales extends BreakpointScales = BreakpointScales> = {
6
+ <Axis extends BreakpointAxis>(axis: Axis, name: BreakpointMatch<Scales>[Axis]): boolean;
7
+ (pair: BreakpointPair<Scales>): boolean;
8
+ };
4
9
  /** A React accessor that reads or tests the current breakpoint pair. */
5
10
  export type ResponsiveBreakpointAccessor<Scales extends BreakpointScales = BreakpointScales> = {
6
11
  (): BreakpointMatch<Scales>;
7
12
  (pair: BreakpointPair<Scales>): boolean;
13
+ readonly below: ResponsiveBreakpointRelationMatcher<Scales>;
14
+ readonly atMost: ResponsiveBreakpointRelationMatcher<Scales>;
15
+ readonly only: ResponsiveBreakpointRelationMatcher<Scales>;
16
+ readonly atLeast: ResponsiveBreakpointRelationMatcher<Scales>;
17
+ readonly above: ResponsiveBreakpointRelationMatcher<Scales>;
8
18
  };
9
19
  /** Thrown when a responsive hook is used outside its generated React provider. */
10
20
  export declare class ResponsiveTuiProviderError extends Error {
@@ -16,12 +16,19 @@ var createBreakpointDefinition = (scales) => {
16
16
  width: matchScale(viewport.width, entries.width),
17
17
  height: matchScale(viewport.height, entries.height)
18
18
  });
19
+ const below = createRelationMatcher(entries, "below");
20
+ const atMost = createRelationMatcher(entries, "atMost");
21
+ const only = createRelationMatcher(entries, "only");
22
+ const atLeast = createRelationMatcher(entries, "atLeast");
23
+ const above = createRelationMatcher(entries, "above");
19
24
  return {
20
25
  match,
21
- matches: (viewport, pair) => {
22
- const current = match(viewport);
23
- return current.width === pair[0] && current.height === pair[1];
24
- }
26
+ matches: (viewport, pair) => only(viewport, pair),
27
+ below,
28
+ atMost,
29
+ only,
30
+ atLeast,
31
+ above
25
32
  };
26
33
  };
27
34
  var breakpointAxes = ["width", "height"];
@@ -67,14 +74,39 @@ var validateScale = (axis, scale) => {
67
74
  return validated.sort((left, right) => left[1] - right[1]);
68
75
  };
69
76
  var matchScale = (value, entries) => {
70
- let match = entries[0][0];
71
- for (const [name, threshold] of entries) {
72
- if (value < threshold)
77
+ return entries[matchScaleIndex(value, entries)][0];
78
+ };
79
+ var matchScaleIndex = (value, entries) => {
80
+ let match = 0;
81
+ for (let index = 1;index < entries.length; index += 1) {
82
+ if (value < entries[index][1])
73
83
  break;
74
- match = name;
84
+ match = index;
75
85
  }
76
86
  return match;
77
87
  };
88
+ var createRelationMatcher = (entries, relation) => (viewport, axisOrPair, name) => {
89
+ if (Array.isArray(axisOrPair)) {
90
+ return matchesRelation(viewport.width, entries.width, axisOrPair[0], relation) && matchesRelation(viewport.height, entries.height, axisOrPair[1], relation);
91
+ }
92
+ const axis = axisOrPair;
93
+ return matchesRelation(viewport[axis], entries[axis], name, relation);
94
+ };
95
+ var matchesRelation = (value, entries, name, relation) => {
96
+ const target = entries.findIndex(([entryName]) => entryName === name);
97
+ if (target === -1)
98
+ return false;
99
+ const current = matchScaleIndex(value, entries);
100
+ if (relation === "below")
101
+ return current < target;
102
+ if (relation === "atMost")
103
+ return current <= target;
104
+ if (relation === "only")
105
+ return current === target;
106
+ if (relation === "atLeast")
107
+ return current >= target;
108
+ return current > target;
109
+ };
78
110
  var isRecord = (value) => typeof value === "object" && value !== null;
79
111
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
80
112
 
@@ -91,7 +123,14 @@ var createResponsiveTui = (scales) => {
91
123
  const ResponsiveTuiContext = createContext(undefined);
92
124
  const ResponsiveTUI = (props) => {
93
125
  const dimensions = useTerminalDimensions();
94
- const breakpoint = (pair) => pair ? breakpoints.matches(dimensions, pair) : breakpoints.match(dimensions);
126
+ const bind = (relation) => bindRelation(() => dimensions, relation);
127
+ const breakpoint = Object.assign((pair) => pair ? breakpoints.matches(dimensions, pair) : breakpoints.match(dimensions), {
128
+ below: bind(breakpoints.below),
129
+ atMost: bind(breakpoints.atMost),
130
+ only: bind(breakpoints.only),
131
+ atLeast: bind(breakpoints.atLeast),
132
+ above: bind(breakpoints.above)
133
+ });
95
134
  return createElement(ResponsiveTuiContext.Provider, { value: breakpoint }, props.children);
96
135
  };
97
136
  const useBreakpoint = () => {
@@ -103,6 +142,12 @@ var createResponsiveTui = (scales) => {
103
142
  };
104
143
  return { ResponsiveTUI, useBreakpoint };
105
144
  };
145
+ var bindRelation = (viewport, relation) => (axisOrPair, name) => {
146
+ if (typeof axisOrPair !== "string") {
147
+ return relation(viewport(), axisOrPair);
148
+ }
149
+ return relation(viewport(), axisOrPair, name);
150
+ };
106
151
  export {
107
152
  ResponsiveTuiConfigurationError,
108
153
  ResponsiveTuiProviderError,
@@ -1,10 +1,20 @@
1
1
  import { type ParentProps } from "solid-js";
2
- import { createBreakpointDefinition, type BreakpointMatch, type BreakpointPair, type BreakpointScales } from "../core/index.js";
2
+ import { createBreakpointDefinition, type BreakpointAxis, type BreakpointMatch, type BreakpointPair, type BreakpointScales } from "../core/index.js";
3
3
  export { ResponsiveTuiConfigurationError } from "../core/index.js";
4
+ /** A reactive Solid breakpoint relation test for one axis or both axes. */
5
+ export type ResponsiveBreakpointRelationMatcher<Scales extends BreakpointScales = BreakpointScales> = {
6
+ <Axis extends BreakpointAxis>(axis: Axis, name: BreakpointMatch<Scales>[Axis]): boolean;
7
+ (pair: BreakpointPair<Scales>): boolean;
8
+ };
4
9
  /** A reactive Solid accessor that reads or tests the current breakpoint pair. */
5
10
  export type ResponsiveBreakpointAccessor<Scales extends BreakpointScales = BreakpointScales> = {
6
11
  (): BreakpointMatch<Scales>;
7
12
  (pair: BreakpointPair<Scales>): boolean;
13
+ readonly below: ResponsiveBreakpointRelationMatcher<Scales>;
14
+ readonly atMost: ResponsiveBreakpointRelationMatcher<Scales>;
15
+ readonly only: ResponsiveBreakpointRelationMatcher<Scales>;
16
+ readonly atLeast: ResponsiveBreakpointRelationMatcher<Scales>;
17
+ readonly above: ResponsiveBreakpointRelationMatcher<Scales>;
8
18
  };
9
19
  /** Thrown when a responsive hook is used outside its generated Solid provider. */
10
20
  export declare class ResponsiveTuiProviderError extends Error {
@@ -16,12 +16,19 @@ var createBreakpointDefinition = (scales) => {
16
16
  width: matchScale(viewport.width, entries.width),
17
17
  height: matchScale(viewport.height, entries.height)
18
18
  });
19
+ const below = createRelationMatcher(entries, "below");
20
+ const atMost = createRelationMatcher(entries, "atMost");
21
+ const only = createRelationMatcher(entries, "only");
22
+ const atLeast = createRelationMatcher(entries, "atLeast");
23
+ const above = createRelationMatcher(entries, "above");
19
24
  return {
20
25
  match,
21
- matches: (viewport, pair) => {
22
- const current = match(viewport);
23
- return current.width === pair[0] && current.height === pair[1];
24
- }
26
+ matches: (viewport, pair) => only(viewport, pair),
27
+ below,
28
+ atMost,
29
+ only,
30
+ atLeast,
31
+ above
25
32
  };
26
33
  };
27
34
  var breakpointAxes = ["width", "height"];
@@ -67,14 +74,39 @@ var validateScale = (axis, scale) => {
67
74
  return validated.sort((left, right) => left[1] - right[1]);
68
75
  };
69
76
  var matchScale = (value, entries) => {
70
- let match = entries[0][0];
71
- for (const [name, threshold] of entries) {
72
- if (value < threshold)
77
+ return entries[matchScaleIndex(value, entries)][0];
78
+ };
79
+ var matchScaleIndex = (value, entries) => {
80
+ let match = 0;
81
+ for (let index = 1;index < entries.length; index += 1) {
82
+ if (value < entries[index][1])
73
83
  break;
74
- match = name;
84
+ match = index;
75
85
  }
76
86
  return match;
77
87
  };
88
+ var createRelationMatcher = (entries, relation) => (viewport, axisOrPair, name) => {
89
+ if (Array.isArray(axisOrPair)) {
90
+ return matchesRelation(viewport.width, entries.width, axisOrPair[0], relation) && matchesRelation(viewport.height, entries.height, axisOrPair[1], relation);
91
+ }
92
+ const axis = axisOrPair;
93
+ return matchesRelation(viewport[axis], entries[axis], name, relation);
94
+ };
95
+ var matchesRelation = (value, entries, name, relation) => {
96
+ const target = entries.findIndex(([entryName]) => entryName === name);
97
+ if (target === -1)
98
+ return false;
99
+ const current = matchScaleIndex(value, entries);
100
+ if (relation === "below")
101
+ return current < target;
102
+ if (relation === "atMost")
103
+ return current <= target;
104
+ if (relation === "only")
105
+ return current === target;
106
+ if (relation === "atLeast")
107
+ return current >= target;
108
+ return current > target;
109
+ };
78
110
  var isRecord = (value) => typeof value === "object" && value !== null;
79
111
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
80
112
 
@@ -92,7 +124,14 @@ var createResponsiveTui = (scales) => {
92
124
  const ResponsiveTUI = (props) => {
93
125
  const dimensions = useTerminalDimensions();
94
126
  const current = createMemo(() => breakpoints.match(dimensions()));
95
- const breakpoint = (pair) => pair ? breakpoints.matches(dimensions(), pair) : current();
127
+ const bind = (relation) => bindRelation(dimensions, relation);
128
+ const breakpoint = Object.assign((pair) => pair ? breakpoints.matches(dimensions(), pair) : current(), {
129
+ below: bind(breakpoints.below),
130
+ atMost: bind(breakpoints.atMost),
131
+ only: bind(breakpoints.only),
132
+ atLeast: bind(breakpoints.atLeast),
133
+ above: bind(breakpoints.above)
134
+ });
96
135
  return ResponsiveTuiContext.Provider({
97
136
  get children() {
98
137
  return props.children;
@@ -109,6 +148,12 @@ var createResponsiveTui = (scales) => {
109
148
  };
110
149
  return { ResponsiveTUI, useBreakpoint };
111
150
  };
151
+ var bindRelation = (viewport, relation) => (axisOrPair, name) => {
152
+ if (typeof axisOrPair !== "string") {
153
+ return relation(viewport(), axisOrPair);
154
+ }
155
+ return relation(viewport(), axisOrPair, name);
156
+ };
112
157
  export {
113
158
  ResponsiveTuiConfigurationError,
114
159
  ResponsiveTuiProviderError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentui-responsive",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Typed responsive breakpoints for OpenTUI.",
5
5
  "keywords": [
6
6
  "breakpoints",