feat(runtime): re-export Ink-aligned named prop/data types (BoxProps, …) (#70)
vue-tui withheld Ink's named prop types (BoxProps, TextProps, StaticProps,
TransformProps, NewlineProps) and the WindowSize/CursorPosition data shapes
under a blanket "avoid React-shaped type names" rule. That rule over-reached:
a <Box> has props in Vue exactly as in React, so those names carry no
React-vs-Vue content — there's no reason to rename them. Re-export them under
Ink's names so a consumer can name a component's props the same way as in Ink.
- Derive each XProps from the component's runtime `props` object via Vue's
`ExtractPublicPropTypes`, so the public type can never drift from the real
props. Pin `required: true as const` on Static.items / Transform.transform:
a standalone `const` widens `true`→`boolean`, which would otherwise drop them
from the required keys — in both the exported type AND the component's own
`setup(props)` typing.
- Add `WindowSize { columns, rows }` and `CursorPosition { x, y }`, anchored to
their real usage in useTerminalSize / useCursor. (The composables still return
reactive refs of these — the data shape matches Ink; the ref wrapper is the
framework difference.)
- Keep the genuinely-divergent names as-is: DOMElement→TuiNode (real
DOM-emulation vs host-node difference), RenderOptions/Instance→MountOptions/
TuiApp (downstream of createApp()), App/Stdin/Stdout/StderrProps = N/A. Rewrite
the ink-divergences doc to record what's now aligned vs still divergent.
- Add a tsc-checked type-level test (public-types.test-d.ts) asserting the
exported shapes; it is excluded from vitest's runtime run by naming.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,14 +40,38 @@ deliberate. Divergences fall into a few kinds:
|
||||
- **Why:** intentionally minimal, single-purpose composables. `waitUntilRenderFlush` is
|
||||
deliberately **not** exposed.
|
||||
|
||||
### No named type / prop re-exports
|
||||
### Named type / prop re-exports
|
||||
|
||||
- **Ink:** re-exports `BoxProps`, `TextProps`, `StaticProps`, `TransformProps`,
|
||||
`NewlineProps`, `WindowSize`, `CursorPosition`, `DOMElement`, `RenderOptions`, `Instance`,
|
||||
`App/Stdin/Stdout/StderrProps`.
|
||||
- **vue-tui:** does not re-export those names; exposes its own type surface
|
||||
(`TuiApp`, `MountOptions`, …).
|
||||
- **Why:** avoid leaking React-shaped type names; present a Vue-native type surface.
|
||||
- **Ink:** re-exports its component prop types plus a few data/handle types:
|
||||
`BoxProps`, `TextProps`, `StaticProps`, `TransformProps`, `NewlineProps`,
|
||||
`WindowSize`, `CursorPosition`, `DOMElement`, `RenderOptions`, `Instance`,
|
||||
`AppProps`, `StdinProps`, `StdoutProps`, `StderrProps`.
|
||||
- **vue-tui:** re-exports the framework-neutral ones under the **same names** —
|
||||
`BoxProps`, `TextProps`, `StaticProps`, `TransformProps`, `NewlineProps`,
|
||||
`WindowSize` (`{ columns, rows }`) and `CursorPosition` (`{ x, y }`). These are
|
||||
**not** divergences: a `<Box>` has props in Vue exactly as in React, so the names
|
||||
carry over. They are derived from the runtime `props` objects via Vue's
|
||||
`ExtractPublicPropTypes`, so they never drift from the components' real props.
|
||||
Only the remaining few genuinely differ, each for a concrete reason — never merely
|
||||
to "avoid React-shaped names":
|
||||
- `DOMElement` → **`TuiNode`**. The one genuinely DOM-shaped type: Ink's
|
||||
`DOMElement` models a DOM-emulation node (`nodeName` / `attributes` /
|
||||
`childNodes`). vue-tui's host tree is a different representation
|
||||
(`TuiContainer | TuiTextLeaf | TuiComment`), exported as `TuiNode` from
|
||||
`@vue-tui/runtime/internal`.
|
||||
- `RenderOptions` / `Instance` → **`MountOptions`** / **`TuiApp`**. Downstream of
|
||||
the `createApp()` entry above — vue-tui mounts a Vue app, so the options bag and
|
||||
the returned handle are Vue-shaped, not `render()`-shaped.
|
||||
- `AppProps` / `StdinProps` / `StdoutProps` / `StderrProps` → **N/A**. These are the
|
||||
props of Ink's internal React _context-provider components_ (`<AppContext>`,
|
||||
`<StdinContext>`, …). vue-tui has no such components — that state is reached via
|
||||
`createApp` plus the `useStdin` / `useStdout` / `useStderr` composables — so there
|
||||
is nothing to name.
|
||||
- **Why:** the earlier blanket "expose a Vue-native type surface, don't leak
|
||||
React-shaped names" over-reached — it withheld names like `BoxProps` that have no
|
||||
React vs Vue content at all. The rule is narrower: mirror Ink's names wherever the
|
||||
underlying type is framework-neutral; reshape only where Vue genuinely has a
|
||||
different thing (a host node, a mounted app) or no thing at all.
|
||||
|
||||
## Additive features (vue-tui is a strict superset)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Type-level guarantees for the public *named* type surface.
|
||||
//
|
||||
// vue-tui tracks Ink, and Ink re-exports its component prop types and a couple of
|
||||
// framework-neutral data shapes under stable names. These names (BoxProps, TextProps,
|
||||
// …, WindowSize, CursorPosition) have nothing to do with React vs Vue — a <Box> has
|
||||
// props in Vue exactly as in React — so vue-tui re-exports them too, letting consumers
|
||||
// name a component's props the same way they would in Ink. See
|
||||
// `.agents/docs/ink-divergences.md` ("Named type / prop re-exports").
|
||||
//
|
||||
// These assertions are erased at runtime; the real gate is `tsc --noEmit` (the package's
|
||||
// `check:type` script). This file is named `*.test-d.ts` on purpose so vitest does NOT
|
||||
// pick it up as a runtime test (its include is `*.test.ts`), while tsc still checks it.
|
||||
import { expectTypeOf } from "vite-plus/test";
|
||||
import type {
|
||||
BoxProps,
|
||||
TextProps,
|
||||
StaticProps,
|
||||
TransformProps,
|
||||
NewlineProps,
|
||||
WindowSize,
|
||||
CursorPosition,
|
||||
} from "@vue-tui/runtime";
|
||||
|
||||
// Prop types carry their component's real, declared props.
|
||||
expectTypeOf<BoxProps["flexDirection"]>().toEqualTypeOf<
|
||||
"row" | "row-reverse" | "column" | "column-reverse" | undefined
|
||||
>();
|
||||
expectTypeOf<BoxProps["gap"]>().toEqualTypeOf<number | undefined>();
|
||||
expectTypeOf<TextProps["bold"]>().toEqualTypeOf<boolean | undefined>();
|
||||
expectTypeOf<StaticProps["items"]>().toEqualTypeOf<unknown[]>();
|
||||
expectTypeOf<TransformProps["transform"]>().toEqualTypeOf<
|
||||
(line: string, lineIndex: number) => string
|
||||
>();
|
||||
expectTypeOf<NewlineProps["count"]>().toEqualTypeOf<number | undefined>();
|
||||
|
||||
// Framework-neutral data shapes, mirrored from Ink exactly.
|
||||
expectTypeOf<WindowSize>().toEqualTypeOf<{ readonly columns: number; readonly rows: number }>();
|
||||
expectTypeOf<CursorPosition>().toEqualTypeOf<{ x: number; y: number }>();
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineComponent, h, inject, type PropType } from "vue";
|
||||
import { defineComponent, h, inject, type ExtractPublicPropTypes, type PropType } from "vue";
|
||||
import type cliBoxes from "cli-boxes";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
@@ -69,94 +69,96 @@ export interface AriaState {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const boxProps = {
|
||||
flexDirection: String as PropType<FlexDirection>,
|
||||
flexGrow: Number,
|
||||
flexShrink: Number,
|
||||
flexBasis: [Number, String],
|
||||
flexWrap: String as PropType<FlexWrap>,
|
||||
alignItems: String as PropType<Align>,
|
||||
alignSelf: String as PropType<AlignSelf>,
|
||||
justifyContent: String as PropType<Justify>,
|
||||
gap: Number,
|
||||
columnGap: Number,
|
||||
rowGap: Number,
|
||||
|
||||
width: [Number, String],
|
||||
height: [Number, String],
|
||||
minWidth: [Number, String],
|
||||
minHeight: [Number, String],
|
||||
maxWidth: [Number, String],
|
||||
maxHeight: [Number, String],
|
||||
aspectRatio: Number,
|
||||
alignContent: String as PropType<AlignContent>,
|
||||
position: String as PropType<"absolute" | "relative" | "static">,
|
||||
top: [Number, String],
|
||||
right: [Number, String],
|
||||
bottom: [Number, String],
|
||||
left: [Number, String],
|
||||
|
||||
margin: Number as PropType<Spacing>,
|
||||
marginX: Number,
|
||||
marginY: Number,
|
||||
marginTop: Number,
|
||||
marginBottom: Number,
|
||||
marginLeft: Number,
|
||||
marginRight: Number,
|
||||
padding: Number,
|
||||
paddingX: Number,
|
||||
paddingY: Number,
|
||||
paddingTop: Number,
|
||||
paddingBottom: Number,
|
||||
paddingLeft: Number,
|
||||
paddingRight: Number,
|
||||
|
||||
// Accept either a preset name string or a full custom BoxStyle object (Ink parity, G13).
|
||||
// Ink types borderStyle as `keyof Boxes | BoxStyle`; we mirror that here.
|
||||
borderStyle: [String, Object] as PropType<BorderStyle | BoxStyle>,
|
||||
borderColor: [String, Array],
|
||||
// `default: undefined` is intentional and load-bearing: Vue's boolean-casting
|
||||
// rule coerces absent Boolean props to `false` only when there is no explicit
|
||||
// default. Adding `default: undefined` suppresses that coercion so absent
|
||||
// per-edge dim props arrive in the paint pass as `undefined`, not `false`.
|
||||
// This lets `edgeDim = (perEdge ?? generalDim)` correctly fall back to the
|
||||
// general value only when the per-edge prop was truly omitted — mirroring
|
||||
// Ink render-border.ts:54 which uses real-undefined via React's prop model
|
||||
// (G16). The `Boolean` type is kept so Vue still accepts bare-attribute
|
||||
// `<Box borderDimColor>` in templates (coerces `""` → `true`) and passes
|
||||
// TypeScript type-checking for consumers.
|
||||
borderDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderTopDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderBottomDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderLeftDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderRightDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderTop: { type: Boolean, default: true },
|
||||
borderBottom: { type: Boolean, default: true },
|
||||
borderLeft: { type: Boolean, default: true },
|
||||
borderRight: { type: Boolean, default: true },
|
||||
borderTopColor: [String, Array],
|
||||
borderBottomColor: [String, Array],
|
||||
borderLeftColor: [String, Array],
|
||||
borderRightColor: [String, Array],
|
||||
borderBackgroundColor: [String, Array],
|
||||
borderTopBackgroundColor: [String, Array],
|
||||
borderBottomBackgroundColor: [String, Array],
|
||||
borderLeftBackgroundColor: [String, Array],
|
||||
borderRightBackgroundColor: [String, Array],
|
||||
|
||||
backgroundColor: [String, Array],
|
||||
overflow: String as PropType<"visible" | "hidden">,
|
||||
overflowX: String as PropType<"visible" | "hidden">,
|
||||
overflowY: String as PropType<"visible" | "hidden">,
|
||||
display: String as PropType<"flex" | "none">,
|
||||
|
||||
ariaLabel: String,
|
||||
ariaHidden: Boolean,
|
||||
ariaRole: String as PropType<AriaRole>,
|
||||
ariaState: Object as PropType<AriaState>,
|
||||
};
|
||||
|
||||
const BoxImpl = defineComponent({
|
||||
name: "Box",
|
||||
props: {
|
||||
flexDirection: String as PropType<FlexDirection>,
|
||||
flexGrow: Number,
|
||||
flexShrink: Number,
|
||||
flexBasis: [Number, String],
|
||||
flexWrap: String as PropType<FlexWrap>,
|
||||
alignItems: String as PropType<Align>,
|
||||
alignSelf: String as PropType<AlignSelf>,
|
||||
justifyContent: String as PropType<Justify>,
|
||||
gap: Number,
|
||||
columnGap: Number,
|
||||
rowGap: Number,
|
||||
|
||||
width: [Number, String],
|
||||
height: [Number, String],
|
||||
minWidth: [Number, String],
|
||||
minHeight: [Number, String],
|
||||
maxWidth: [Number, String],
|
||||
maxHeight: [Number, String],
|
||||
aspectRatio: Number,
|
||||
alignContent: String as PropType<AlignContent>,
|
||||
position: String as PropType<"absolute" | "relative" | "static">,
|
||||
top: [Number, String],
|
||||
right: [Number, String],
|
||||
bottom: [Number, String],
|
||||
left: [Number, String],
|
||||
|
||||
margin: Number as PropType<Spacing>,
|
||||
marginX: Number,
|
||||
marginY: Number,
|
||||
marginTop: Number,
|
||||
marginBottom: Number,
|
||||
marginLeft: Number,
|
||||
marginRight: Number,
|
||||
padding: Number,
|
||||
paddingX: Number,
|
||||
paddingY: Number,
|
||||
paddingTop: Number,
|
||||
paddingBottom: Number,
|
||||
paddingLeft: Number,
|
||||
paddingRight: Number,
|
||||
|
||||
// Accept either a preset name string or a full custom BoxStyle object (Ink parity, G13).
|
||||
// Ink types borderStyle as `keyof Boxes | BoxStyle`; we mirror that here.
|
||||
borderStyle: [String, Object] as PropType<BorderStyle | BoxStyle>,
|
||||
borderColor: [String, Array],
|
||||
// `default: undefined` is intentional and load-bearing: Vue's boolean-casting
|
||||
// rule coerces absent Boolean props to `false` only when there is no explicit
|
||||
// default. Adding `default: undefined` suppresses that coercion so absent
|
||||
// per-edge dim props arrive in the paint pass as `undefined`, not `false`.
|
||||
// This lets `edgeDim = (perEdge ?? generalDim)` correctly fall back to the
|
||||
// general value only when the per-edge prop was truly omitted — mirroring
|
||||
// Ink render-border.ts:54 which uses real-undefined via React's prop model
|
||||
// (G16). The `Boolean` type is kept so Vue still accepts bare-attribute
|
||||
// `<Box borderDimColor>` in templates (coerces `""` → `true`) and passes
|
||||
// TypeScript type-checking for consumers.
|
||||
borderDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderTopDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderBottomDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderLeftDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderRightDimColor: { type: Boolean as PropType<boolean | undefined>, default: undefined },
|
||||
borderTop: { type: Boolean, default: true },
|
||||
borderBottom: { type: Boolean, default: true },
|
||||
borderLeft: { type: Boolean, default: true },
|
||||
borderRight: { type: Boolean, default: true },
|
||||
borderTopColor: [String, Array],
|
||||
borderBottomColor: [String, Array],
|
||||
borderLeftColor: [String, Array],
|
||||
borderRightColor: [String, Array],
|
||||
borderBackgroundColor: [String, Array],
|
||||
borderTopBackgroundColor: [String, Array],
|
||||
borderBottomBackgroundColor: [String, Array],
|
||||
borderLeftBackgroundColor: [String, Array],
|
||||
borderRightBackgroundColor: [String, Array],
|
||||
|
||||
backgroundColor: [String, Array],
|
||||
overflow: String as PropType<"visible" | "hidden">,
|
||||
overflowX: String as PropType<"visible" | "hidden">,
|
||||
overflowY: String as PropType<"visible" | "hidden">,
|
||||
display: String as PropType<"flex" | "none">,
|
||||
|
||||
ariaLabel: String,
|
||||
ariaHidden: Boolean,
|
||||
ariaRole: String as PropType<AriaRole>,
|
||||
ariaState: Object as PropType<AriaState>,
|
||||
},
|
||||
props: boxProps,
|
||||
setup(props, { slots }) {
|
||||
const appCtx = inject(AppContextKey, null);
|
||||
|
||||
@@ -177,3 +179,6 @@ const BoxImpl = defineComponent({
|
||||
});
|
||||
|
||||
export const Box = BoxImpl as WithChildren<typeof BoxImpl>;
|
||||
|
||||
/** Props accepted by `<Box>` — the vue-tui analogue of Ink's `BoxProps`. */
|
||||
export type BoxProps = ExtractPublicPropTypes<typeof boxProps>;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { defineComponent, getCurrentInstance, h } from "vue";
|
||||
import { defineComponent, getCurrentInstance, h, type ExtractPublicPropTypes } from "vue";
|
||||
|
||||
const newlineProps = { count: { type: Number, default: 1 } };
|
||||
|
||||
export const Newline = defineComponent({
|
||||
name: "Newline",
|
||||
props: { count: { type: Number, default: 1 } },
|
||||
props: newlineProps,
|
||||
setup(props) {
|
||||
return () => {
|
||||
const content = "\n".repeat(props.count);
|
||||
@@ -30,3 +32,6 @@ function isInsideText(): boolean {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Props accepted by `<Newline>` — the vue-tui analogue of Ink's `NewlineProps`. */
|
||||
export type NewlineProps = ExtractPublicPropTypes<typeof newlineProps>;
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { defineComponent, h, shallowRef, watch, type PropType } from "vue";
|
||||
import {
|
||||
defineComponent,
|
||||
h,
|
||||
shallowRef,
|
||||
watch,
|
||||
type ExtractPublicPropTypes,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
const staticProps = {
|
||||
// `required: true as const` (not bare `true`): a standalone `const` widens
|
||||
// `true` → `boolean`, which would drop `items` from ExtractPublicPropTypes'
|
||||
// required keys (and from the component's own `props.items` typing). The
|
||||
// literal keeps `items` required, matching Ink's `StaticProps`.
|
||||
items: { type: Array as PropType<unknown[]>, required: true as const },
|
||||
style: { type: Object as PropType<Record<string, unknown>>, default: undefined },
|
||||
};
|
||||
|
||||
const StaticImpl = defineComponent({
|
||||
name: "Static",
|
||||
props: {
|
||||
items: { type: Array as PropType<unknown[]>, required: true },
|
||||
style: { type: Object as PropType<Record<string, unknown>>, default: undefined },
|
||||
},
|
||||
props: staticProps,
|
||||
setup(props, { slots }) {
|
||||
const defaultStyle: Record<string, unknown> = {
|
||||
position: "absolute",
|
||||
@@ -64,3 +77,6 @@ const StaticImpl = defineComponent({
|
||||
});
|
||||
|
||||
export const Static = StaticImpl as WithChildren<typeof StaticImpl>;
|
||||
|
||||
/** Props accepted by `<Static>` — the vue-tui analogue of Ink's `StaticProps`. */
|
||||
export type StaticProps = ExtractPublicPropTypes<typeof staticProps>;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { defineComponent, getCurrentInstance, h, inject, type PropType } from "vue";
|
||||
import {
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
inject,
|
||||
type ExtractPublicPropTypes,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
@@ -11,21 +18,23 @@ type WrapMode =
|
||||
| "truncate-middle"
|
||||
| "truncate-start";
|
||||
|
||||
const textProps = {
|
||||
color: [String, Array] as PropType<Color>,
|
||||
backgroundColor: [String, Array] as PropType<Color>,
|
||||
dimColor: Boolean,
|
||||
bold: Boolean,
|
||||
italic: Boolean,
|
||||
underline: Boolean,
|
||||
strikethrough: Boolean,
|
||||
inverse: Boolean,
|
||||
wrap: { type: String as PropType<WrapMode>, default: "wrap" },
|
||||
ariaLabel: String,
|
||||
ariaHidden: Boolean,
|
||||
};
|
||||
|
||||
const TextImpl = defineComponent({
|
||||
name: "Text",
|
||||
props: {
|
||||
color: [String, Array] as PropType<Color>,
|
||||
backgroundColor: [String, Array] as PropType<Color>,
|
||||
dimColor: Boolean,
|
||||
bold: Boolean,
|
||||
italic: Boolean,
|
||||
underline: Boolean,
|
||||
strikethrough: Boolean,
|
||||
inverse: Boolean,
|
||||
wrap: { type: String as PropType<WrapMode>, default: "wrap" },
|
||||
ariaLabel: String,
|
||||
ariaHidden: Boolean,
|
||||
},
|
||||
props: textProps,
|
||||
setup(props, { slots }) {
|
||||
const appCtx = inject(AppContextKey, null);
|
||||
|
||||
@@ -57,6 +66,9 @@ const TextImpl = defineComponent({
|
||||
|
||||
export const Text = TextImpl as WithChildren<typeof TextImpl>;
|
||||
|
||||
/** Props accepted by `<Text>` — the vue-tui analogue of Ink's `TextProps`. */
|
||||
export type TextProps = ExtractPublicPropTypes<typeof textProps>;
|
||||
|
||||
function isInsideText(): boolean {
|
||||
let parent = getCurrentInstance()?.parent;
|
||||
while (parent) {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { defineComponent, h, inject, type PropType } from "vue";
|
||||
import { defineComponent, h, inject, type ExtractPublicPropTypes, type PropType } from "vue";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
type TransformFn = (line: string, lineIndex: number) => string;
|
||||
|
||||
const transformProps = {
|
||||
// `required: true as const` keeps `transform` a required key once the props
|
||||
// object lives in a standalone `const` (which would otherwise widen `true` →
|
||||
// `boolean`). Matches Ink's `TransformProps`.
|
||||
transform: { type: Function as PropType<TransformFn>, required: true as const },
|
||||
accessibilityLabel: String,
|
||||
};
|
||||
|
||||
const TransformImpl = defineComponent({
|
||||
name: "Transform",
|
||||
props: {
|
||||
transform: { type: Function as PropType<TransformFn>, required: true },
|
||||
accessibilityLabel: String,
|
||||
},
|
||||
props: transformProps,
|
||||
setup(props, { slots }) {
|
||||
const appCtx = inject(AppContextKey, null);
|
||||
|
||||
@@ -28,3 +33,6 @@ const TransformImpl = defineComponent({
|
||||
});
|
||||
|
||||
export const Transform = TransformImpl as WithChildren<typeof TransformImpl>;
|
||||
|
||||
/** Props accepted by `<Transform>` — the vue-tui analogue of Ink's `TransformProps`. */
|
||||
export type TransformProps = ExtractPublicPropTypes<typeof transformProps>;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { inject, shallowRef, watch, onScopeDispose } from "vue";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
|
||||
/**
|
||||
* A cursor position in output-origin coordinates. Mirrors Ink's `CursorPosition`.
|
||||
*/
|
||||
export interface CursorPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `setCursorPosition` so a component can control the terminal cursor.
|
||||
*
|
||||
@@ -12,9 +20,9 @@ export function useCursor() {
|
||||
const ctx = inject(AppContextKey);
|
||||
if (!ctx) throw new Error("useCursor() must be called inside a vue-tui render tree");
|
||||
|
||||
const positionRef = shallowRef<{ x: number; y: number } | undefined>(undefined);
|
||||
const positionRef = shallowRef<CursorPosition | undefined>(undefined);
|
||||
|
||||
function setCursorPosition(position: { x: number; y: number } | undefined) {
|
||||
function setCursorPosition(position: CursorPosition | undefined) {
|
||||
positionRef.value = position;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,28 @@ import { inject, onScopeDispose, shallowRef, type ShallowRef } from "vue";
|
||||
import terminalSize from "terminal-size";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
|
||||
/**
|
||||
* A terminal's character-cell dimensions. Mirrors Ink's `WindowSize` exactly
|
||||
* (`columns`/`rows`, not `width`/`height`).
|
||||
*
|
||||
* Note the Vue-vs-React shape: Ink's `useWindowSize()` returns a `WindowSize`
|
||||
* snapshot, whereas vue-tui's `useWindowSize()` / `useTerminalSize()` return
|
||||
* reactive **refs** of these dimensions
|
||||
* (`{ columns: ShallowRef<number>; rows: ShallowRef<number> }`). The data shape
|
||||
* is the same; the reactivity wrapper is the framework difference.
|
||||
*/
|
||||
export interface WindowSize {
|
||||
readonly columns: number;
|
||||
readonly rows: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve terminal dimensions with a fallback chain:
|
||||
* 1. stdout.columns / stdout.rows (available in TTY mode)
|
||||
* 2. terminal-size package (works even when stdout is redirected)
|
||||
* 3. Hardcoded defaults (80x24)
|
||||
*/
|
||||
export function resolveSize(stdout: NodeJS.WriteStream): { columns: number; rows: number } {
|
||||
export function resolveSize(stdout: NodeJS.WriteStream): WindowSize {
|
||||
const cols = stdout.columns;
|
||||
const rowsVal = stdout.rows;
|
||||
if (cols && rowsVal) return { columns: cols, rows: rowsVal };
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
export { createApp, type TuiApp, type MountOptions } from "./render.ts";
|
||||
export { renderToString, type RenderToStringOptions } from "./render-to-string.ts";
|
||||
|
||||
export { Box, type AriaRole, type AriaState, type BoxStyle } from "./components/Box.ts";
|
||||
export { Text } from "./components/Text.ts";
|
||||
export { Newline } from "./components/Newline.ts";
|
||||
export {
|
||||
Box,
|
||||
type AriaRole,
|
||||
type AriaState,
|
||||
type BoxStyle,
|
||||
type BoxProps,
|
||||
} from "./components/Box.ts";
|
||||
export { Text, type TextProps } from "./components/Text.ts";
|
||||
export { Newline, type NewlineProps } from "./components/Newline.ts";
|
||||
export { Spacer } from "./components/Spacer.ts";
|
||||
export { Static } from "./components/Static.ts";
|
||||
export { Transform } from "./components/Transform.ts";
|
||||
export { Static, type StaticProps } from "./components/Static.ts";
|
||||
export { Transform, type TransformProps } from "./components/Transform.ts";
|
||||
|
||||
export { useExit } from "./composables/useExit.ts";
|
||||
export { useInput, type Key, type UseInputOptions } from "./composables/useInput.ts";
|
||||
@@ -16,8 +22,8 @@ export { useFocusManager } from "./composables/useFocusManager.ts";
|
||||
export { useStdin } from "./composables/useStdin.ts";
|
||||
export { useStdout } from "./composables/useStdout.ts";
|
||||
export { useStderr } from "./composables/useStderr.ts";
|
||||
export { useTerminalSize, useWindowSize } from "./composables/useTerminalSize.ts";
|
||||
export { useCursor } from "./composables/useCursor.ts";
|
||||
export { useTerminalSize, useWindowSize, type WindowSize } from "./composables/useTerminalSize.ts";
|
||||
export { useCursor, type CursorPosition } from "./composables/useCursor.ts";
|
||||
export { useIsScreenReaderEnabled } from "./composables/useIsScreenReaderEnabled.ts";
|
||||
export {
|
||||
useAnimation,
|
||||
|
||||
Reference in New Issue
Block a user