diff --git a/.agents/docs/component-authoring.md b/.agents/docs/component-authoring.md
new file mode 100644
index 0000000..d08a887
--- /dev/null
+++ b/.agents/docs/component-authoring.md
@@ -0,0 +1,122 @@
+# Component Authoring: SFC vs Render Function
+
+vue-tui's public components (`Box`, `Text`, `Spacer`, `Static`, `Newline`, `Transform`) are
+authored as **Vue `
+
+
+
+ ok
+ x
+ x
+
+
+ x
+
+ x
+
+ x
+
diff --git a/packages/runtime-tests/integration/pty/fixtures/tsconfig.json b/packages/runtime-tests/integration/pty/fixtures/tsconfig.json
index d73bd17..bae913f 100644
--- a/packages/runtime-tests/integration/pty/fixtures/tsconfig.json
+++ b/packages/runtime-tests/integration/pty/fixtures/tsconfig.json
@@ -8,5 +8,6 @@
"strict": true,
"skipLibCheck": true,
"types": ["node"]
- }
+ },
+ "include": ["./*.tsx", "./*.vue"]
}
diff --git a/packages/runtime-tests/package.json b/packages/runtime-tests/package.json
index 98688a8..0a62dca 100644
--- a/packages/runtime-tests/package.json
+++ b/packages/runtime-tests/package.json
@@ -8,7 +8,7 @@
"test:integration": "vp test",
"test:pty": "vp test run --config vitest.pty.config.ts --passWithNoTests",
"check:type": "tsc --noEmit && vp run check:fixtures",
- "check:fixtures": "tsc -p integration/pty/fixtures/tsconfig.json --noEmit"
+ "check:fixtures": "vue-tsc -p integration/pty/fixtures/tsconfig.json --noEmit"
},
"devDependencies": {
"@types/node": "^25.9.1",
@@ -24,6 +24,7 @@
"tsx": "catalog:",
"typescript": "^6.0.3",
"vite-plus": "^0.1.22",
- "vue": "^3.5.34"
+ "vue": "^3.5.34",
+ "vue-tsc": "catalog:"
}
}
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index cbc4c86..1d210bf 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -19,7 +19,7 @@
"build": "vp pack",
"dev": "vp pack --watch",
"test": "vp test",
- "check:type": "tsc --noEmit",
+ "check:type": "vue-tsc --noEmit",
"prepublishOnly": "vp run build"
},
"dependencies": {
@@ -45,8 +45,10 @@
"@types/stack-utils": "^2.0.3",
"@vitejs/plugin-vue-jsx": "catalog:",
"typescript": "^6.0.3",
+ "unplugin-vue": "catalog:",
"vite-plus": "^0.1.20",
- "vue": "^3.4.0"
+ "vue": "^3.4.0",
+ "vue-tsc": "catalog:"
},
"peerDependencies": {
"vue": "^3.4.0"
diff --git a/packages/runtime/src/components/box-props.ts b/packages/runtime/src/components/box-props.ts
new file mode 100644
index 0000000..1cb9b66
--- /dev/null
+++ b/packages/runtime/src/components/box-props.ts
@@ -0,0 +1,208 @@
+import { type ExtractPublicPropTypes, type PropType } from "vue";
+import cliBoxes from "cli-boxes";
+
+type Spacing = number;
+type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse";
+type FlexWrap = "nowrap" | "wrap" | "wrap-reverse";
+type Align = "flex-start" | "center" | "flex-end" | "stretch" | "baseline";
+type AlignSelf = "auto" | "flex-start" | "center" | "flex-end" | "stretch" | "baseline";
+type AlignContent =
+ | "flex-start"
+ | "center"
+ | "flex-end"
+ | "stretch"
+ | "space-between"
+ | "space-around"
+ | "space-evenly";
+type Justify =
+ | "flex-start"
+ | "center"
+ | "flex-end"
+ | "space-between"
+ | "space-around"
+ | "space-evenly";
+type BorderStyle =
+ | "single"
+ | "double"
+ | "round"
+ | "bold"
+ | "singleDouble"
+ | "doubleSingle"
+ | "classic"
+ | "arrow";
+
+// Matches the shape of the cliBoxes value type — the same alias used in paint.ts.
+// Exported so consumers can type their custom border objects (Ink parity, G13).
+export type BoxStyle = (typeof cliBoxes)[keyof cliBoxes.Boxes];
+
+// The layout-only subset of BoxProps — exported so `` (and consumers) can
+// type a `style` object against the same flex/spacing/size keys a `` accepts,
+// without the color/border/aria surface. (Type aliases hoist, so referencing
+// BoxProps here — defined at the bottom of this file — is fine.)
+export type BoxLayoutStyle = Pick<
+ BoxProps,
+ | "flexDirection"
+ | "flexGrow"
+ | "flexShrink"
+ | "flexBasis"
+ | "flexWrap"
+ | "alignItems"
+ | "alignSelf"
+ | "justifyContent"
+ | "gap"
+ | "columnGap"
+ | "rowGap"
+ | "width"
+ | "height"
+ | "minWidth"
+ | "minHeight"
+ | "maxWidth"
+ | "maxHeight"
+ | "aspectRatio"
+ | "alignContent"
+ | "position"
+ | "top"
+ | "right"
+ | "bottom"
+ | "left"
+ | "margin"
+ | "marginX"
+ | "marginY"
+ | "marginTop"
+ | "marginBottom"
+ | "marginLeft"
+ | "marginRight"
+ | "padding"
+ | "paddingX"
+ | "paddingY"
+ | "paddingTop"
+ | "paddingBottom"
+ | "paddingLeft"
+ | "paddingRight"
+ | "overflow"
+ | "overflowX"
+ | "overflowY"
+ | "display"
+>;
+
+export type AriaRole =
+ | "button"
+ | "checkbox"
+ | "combobox"
+ | "list"
+ | "listbox"
+ | "listitem"
+ | "menu"
+ | "menuitem"
+ | "option"
+ | "progressbar"
+ | "radio"
+ | "radiogroup"
+ | "tab"
+ | "tablist"
+ | "table"
+ | "textbox"
+ | "timer"
+ | "toolbar";
+
+export interface AriaState {
+ busy?: boolean;
+ checked?: boolean;
+ disabled?: boolean;
+ expanded?: boolean;
+ multiline?: boolean;
+ multiselectable?: boolean;
+ readonly?: boolean;
+ required?: boolean;
+ selected?: boolean;
+}
+
+export const boxProps = {
+ flexDirection: String as PropType,
+ flexGrow: Number,
+ flexShrink: Number,
+ flexBasis: [Number, String],
+ flexWrap: String as PropType,
+ alignItems: String as PropType,
+ alignSelf: String as PropType,
+ justifyContent: String as PropType,
+ 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,
+ position: String as PropType<"absolute" | "relative" | "static">,
+ top: [Number, String],
+ right: [Number, String],
+ bottom: [Number, String],
+ left: [Number, String],
+
+ margin: Number as PropType,
+ 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,
+ borderColor: String,
+ // `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
+ // `` in templates (coerces `""` → `true`) and passes
+ // TypeScript type-checking for consumers.
+ borderDimColor: { type: Boolean as PropType, default: undefined },
+ borderTopDimColor: { type: Boolean as PropType, default: undefined },
+ borderBottomDimColor: { type: Boolean as PropType, default: undefined },
+ borderLeftDimColor: { type: Boolean as PropType, default: undefined },
+ borderRightDimColor: { type: Boolean as PropType, default: undefined },
+ borderTop: { type: Boolean, default: true },
+ borderBottom: { type: Boolean, default: true },
+ borderLeft: { type: Boolean, default: true },
+ borderRight: { type: Boolean, default: true },
+ borderTopColor: String,
+ borderBottomColor: String,
+ borderLeftColor: String,
+ borderRightColor: String,
+ borderBackgroundColor: String,
+ borderTopBackgroundColor: String,
+ borderBottomBackgroundColor: String,
+ borderLeftBackgroundColor: String,
+ borderRightBackgroundColor: String,
+
+ backgroundColor: String,
+ 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,
+ ariaState: Object as PropType,
+};
+
+/** Props accepted by `` — the vue-tui analogue of Ink's `BoxProps`. */
+export type BoxProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/box-validate.ts b/packages/runtime/src/components/box-validate.ts
new file mode 100644
index 0000000..92b1822
--- /dev/null
+++ b/packages/runtime/src/components/box-validate.ts
@@ -0,0 +1,148 @@
+import cliBoxes from "cli-boxes";
+import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
+import type { BoxProps, BoxStyle } from "./box-props.ts";
+
+/**
+ * Eager render-time validation for ``. Runs every render and throws into the
+ * error boundary on invalid input — exactly as box.ts's render fn did. Returns
+ * `true` so it can gate a `v-if`. Callers must skip it for a screen-reader-hidden
+ * Box (a non-emitted node never colorizes — same ordering as box.ts).
+ */
+export function assertBoxValid(props: BoxProps): true {
+ // --- backgroundColor validation (A12) ---
+ //
+ // Validate the bg-style props during RENDER so a chalk-modifier name (the
+ // exact case Ink's colorize.ts throws on) is caught by vue-tui's error
+ // boundary, not the post-flush paint pass where a throw wedges Vue's
+ // scheduler (cf. the borderStyle fix #124). See assertValidBackgroundColor /
+ // Ink colorize.ts (40b3a75).
+ //
+ // Placed AFTER the screen-reader-hidden early-return to mirror Ink WHERE it
+ // throws: Ink's render-node-to-output never reaches render-background /
+ // render-border for a node it didn't emit, and a screen-reader-hidden Box
+ // emits nothing — so colorize never runs for it. Below, we additionally
+ // gate each value the way Ink's renderers do, so vue throws exactly where
+ // Ink would colorize, and not elsewhere.
+
+ // Box's OWN backgroundColor: Ink's render-background.ts only colorizes when
+ // the resolved content area is > 0 (after subtracting drawn borders); it
+ // bails early otherwise. We can't know the content area at render time
+ // (layout hasn't run), so we validate eagerly. ACCEPTED tiny over-throw: a
+ // degenerate-size Box (content area <= 0, e.g. width/height collapses to
+ // border-only or 0) with a chalk-modifier-name backgroundColor throws here
+ // where Ink would silently skip the fill. This is a niche case on
+ // clearly-invalid input (a modifier name is never a valid bg); matching
+ // Ink's layout-time gate at render time is not worth the complexity.
+ assertValidBackgroundColor(props.backgroundColor);
+
+ // Border backgrounds: Ink's render-border.ts only colorizes a border bg
+ // when (a) borderStyle is truthy (line 28 gate) AND (b) the specific edge
+ // is DRAWN (`border !== false`), and the value it passes per edge is
+ // `borderBackgroundColor ?? borderBackgroundColor` (the per-edge
+ // value with the general value as fallback — lines 44-52, 80/100/112/126).
+ // So we validate exactly the resolved bg of each DRAWN edge — never the
+ // general value on its own, and never an edge whose border isn't drawn.
+ // Consequences (matching Ink, not over-throwing):
+ // - no borderStyle → no border colorize at all → nothing validated.
+ // - borderTop={false} → top edge not drawn → its resolved bg not checked.
+ // - a bad general borderBackgroundColor but every DRAWN edge overrides it
+ // with a valid per-edge value → general value never reaches colorize →
+ // not validated → no throw.
+ // ACCEPTED irreducible over-throw (same class as the content-area note
+ // above): top/bottom edge bg is validated eagerly, but Ink only colorizes a
+ // top/bottom border string when it's non-empty — a degenerate box (e.g.
+ // width=0 with no left/right border) produces an empty string Ink skips.
+ // Matching that needs layout, unavailable at render. Niche + invalid-input-only.
+ if (props.borderStyle) {
+ const stringStyle = (value: unknown) => (typeof value === "string" ? value : undefined);
+ const generalBg = stringStyle(props.borderBackgroundColor);
+ // Per-edge foreground: Ink's render-border colorizes each drawn edge's glyphs
+ // with `borderColor ?? borderColor` (the same per-edge-with-general
+ // fallback as the bg path). A foreground name chalk has but can't call (e.g.
+ // "level") throws there; we validate it eagerly per DRAWN edge.
+ const generalFg = stringStyle(props.borderColor);
+ if (props.borderTop !== false) {
+ assertValidForegroundColor(stringStyle(props.borderTopColor) ?? generalFg, "borderTopColor");
+ assertValidBackgroundColor(
+ stringStyle(props.borderTopBackgroundColor) ?? generalBg,
+ "borderTopBackgroundColor",
+ );
+ }
+ if (props.borderBottom !== false) {
+ assertValidForegroundColor(
+ stringStyle(props.borderBottomColor) ?? generalFg,
+ "borderBottomColor",
+ );
+ assertValidBackgroundColor(
+ stringStyle(props.borderBottomBackgroundColor) ?? generalBg,
+ "borderBottomBackgroundColor",
+ );
+ }
+ if (props.borderLeft !== false) {
+ assertValidForegroundColor(
+ stringStyle(props.borderLeftColor) ?? generalFg,
+ "borderLeftColor",
+ );
+ assertValidBackgroundColor(
+ stringStyle(props.borderLeftBackgroundColor) ?? generalBg,
+ "borderLeftBackgroundColor",
+ );
+ }
+ if (props.borderRight !== false) {
+ assertValidForegroundColor(
+ stringStyle(props.borderRightColor) ?? generalFg,
+ "borderRightColor",
+ );
+ assertValidBackgroundColor(
+ stringStyle(props.borderRightBackgroundColor) ?? generalBg,
+ "borderRightBackgroundColor",
+ );
+ }
+ }
+
+ // NOTE: this component-level validation covers the public ``/``
+ // API only. A raw host-op call (`h("box", { backgroundColor: "bold" })`)
+ // bypasses it; the paint layer keeps its silent degrade-to-bare-text there
+ // rather than throwing (a throw in the post-flush paint pass wedges Vue's
+ // scheduler). Same accepted limitation as the borderStyle fix (#124).
+
+ // Validate borderStyle during RENDER so an unknown name is caught by
+ // vue-tui's error boundary (onErrorCaptured → ErrorOverview), exactly like
+ // any other component render error. Ink crashes on an unknown borderStyle
+ // with a raw TypeError during paint (render-border.ts reads box.topLeft off
+ // cliBoxes[name] === undefined); we align to that "throw on unknown" contract
+ // but do it here rather than in paint, where a throw would unwind through
+ // Vue's post-flush commit and wedge its scheduler. Only a NON-EMPTY unknown
+ // STRING throws: a falsy value (false/undefined/"" = no border) and a custom
+ // BoxStyle OBJECT are both valid and pass through. (audit 2.3)
+ // `borderStyle.length > 0` (not `!== ""`): once `typeof === "string"` narrows
+ // the prop to the BorderStyle keyof union, an `!== ""` literal comparison has
+ // "no overlap" per TS (the union has no `""` member). The empty string is only
+ // reachable via a TS-bypass; `.length > 0` excludes it without tripping that.
+ //
+ // We validate the RESOLVED box has a real BoxStyle SHAPE rather than testing
+ // `borderStyle in cliBoxes`, because `in` has two false-accept holes that let
+ // a non-box value reach paint (which then reads `.top`/`.topLeft` glyph
+ // strings off it):
+ // 1. cli-boxes' default export carries a CJS-interop `default` self-key, so
+ // `"default" in cliBoxes` is true — but cliBoxes.default is the WHOLE
+ // boxes object, not a BoxStyle (it has no string `top`).
+ // 2. `in` walks the prototype chain, so Object.prototype members
+ // ("toString", "constructor", "hasOwnProperty", …) report as "in
+ // cliBoxes" while resolving to a function/undefined — never a BoxStyle.
+ // Resolving `cliBoxes[name]` and requiring an object with a string `top`
+ // rejects unknown names (undefined), "default" (whole object, no string
+ // `top`), and inherited props, while accepting every real preset.
+ const borderStyle = props.borderStyle;
+ if (typeof borderStyle === "string" && borderStyle.length > 0) {
+ // Cast via `unknown` to add a string index signature: cliBoxes is typed as
+ // the `Boxes` keyof object (no index signature), so a direct cast is a TS2352
+ // "insufficient overlap" error. Same cast paint.ts uses to look up by name.
+ const resolved = (cliBoxes as unknown as Record)[borderStyle];
+ if (typeof resolved !== "object" || resolved === null || typeof resolved.top !== "string") {
+ throw new Error(`Unknown borderStyle: ${JSON.stringify(borderStyle)}`);
+ }
+ }
+
+ return true;
+}
diff --git a/packages/runtime/src/components/box.ts b/packages/runtime/src/components/box.ts
deleted file mode 100644
index c6910b5..0000000
--- a/packages/runtime/src/components/box.ts
+++ /dev/null
@@ -1,365 +0,0 @@
-import { defineComponent, h, inject, type ExtractPublicPropTypes, type PropType } from "vue";
-import cliBoxes from "cli-boxes";
-import { AppContextKey } from "../context.ts";
-import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
-import type { WithChildren } from "./with-children.ts";
-
-type Spacing = number;
-type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse";
-type FlexWrap = "nowrap" | "wrap" | "wrap-reverse";
-type Align = "flex-start" | "center" | "flex-end" | "stretch" | "baseline";
-type AlignSelf = "auto" | "flex-start" | "center" | "flex-end" | "stretch" | "baseline";
-type AlignContent =
- | "flex-start"
- | "center"
- | "flex-end"
- | "stretch"
- | "space-between"
- | "space-around"
- | "space-evenly";
-type Justify =
- | "flex-start"
- | "center"
- | "flex-end"
- | "space-between"
- | "space-around"
- | "space-evenly";
-type BorderStyle =
- | "single"
- | "double"
- | "round"
- | "bold"
- | "singleDouble"
- | "doubleSingle"
- | "classic"
- | "arrow";
-
-// Matches the shape of the cliBoxes value type — the same alias used in paint.ts.
-// Exported so consumers can type their custom border objects (Ink parity, G13).
-export type BoxStyle = (typeof cliBoxes)[keyof cliBoxes.Boxes];
-
-export type BoxLayoutStyle = Pick<
- BoxProps,
- | "flexDirection"
- | "flexGrow"
- | "flexShrink"
- | "flexBasis"
- | "flexWrap"
- | "alignItems"
- | "alignSelf"
- | "justifyContent"
- | "gap"
- | "columnGap"
- | "rowGap"
- | "width"
- | "height"
- | "minWidth"
- | "minHeight"
- | "maxWidth"
- | "maxHeight"
- | "aspectRatio"
- | "alignContent"
- | "position"
- | "top"
- | "right"
- | "bottom"
- | "left"
- | "margin"
- | "marginX"
- | "marginY"
- | "marginTop"
- | "marginBottom"
- | "marginLeft"
- | "marginRight"
- | "padding"
- | "paddingX"
- | "paddingY"
- | "paddingTop"
- | "paddingBottom"
- | "paddingLeft"
- | "paddingRight"
- | "overflow"
- | "overflowX"
- | "overflowY"
- | "display"
->;
-
-export type AriaRole =
- | "button"
- | "checkbox"
- | "combobox"
- | "list"
- | "listbox"
- | "listitem"
- | "menu"
- | "menuitem"
- | "option"
- | "progressbar"
- | "radio"
- | "radiogroup"
- | "tab"
- | "tablist"
- | "table"
- | "textbox"
- | "timer"
- | "toolbar";
-
-export interface AriaState {
- busy?: boolean;
- checked?: boolean;
- disabled?: boolean;
- expanded?: boolean;
- multiline?: boolean;
- multiselectable?: boolean;
- readonly?: boolean;
- required?: boolean;
- selected?: boolean;
-}
-
-const boxProps = {
- flexDirection: String as PropType,
- flexGrow: Number,
- flexShrink: Number,
- flexBasis: [Number, String],
- flexWrap: String as PropType,
- alignItems: String as PropType,
- alignSelf: String as PropType,
- justifyContent: String as PropType,
- 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,
- position: String as PropType<"absolute" | "relative" | "static">,
- top: [Number, String],
- right: [Number, String],
- bottom: [Number, String],
- left: [Number, String],
-
- margin: Number as PropType,
- 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,
- borderColor: String,
- // `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
- // `` in templates (coerces `""` → `true`) and passes
- // TypeScript type-checking for consumers.
- borderDimColor: { type: Boolean as PropType, default: undefined },
- borderTopDimColor: { type: Boolean as PropType, default: undefined },
- borderBottomDimColor: { type: Boolean as PropType, default: undefined },
- borderLeftDimColor: { type: Boolean as PropType, default: undefined },
- borderRightDimColor: { type: Boolean as PropType, default: undefined },
- borderTop: { type: Boolean, default: true },
- borderBottom: { type: Boolean, default: true },
- borderLeft: { type: Boolean, default: true },
- borderRight: { type: Boolean, default: true },
- borderTopColor: String,
- borderBottomColor: String,
- borderLeftColor: String,
- borderRightColor: String,
- borderBackgroundColor: String,
- borderTopBackgroundColor: String,
- borderBottomBackgroundColor: String,
- borderLeftBackgroundColor: String,
- borderRightBackgroundColor: String,
-
- backgroundColor: String,
- 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,
- ariaState: Object as PropType,
-};
-
-const BoxImpl = defineComponent({
- name: "Box",
- props: boxProps,
- setup(props, { slots }) {
- const appCtx = inject(AppContextKey, null);
-
- return () => {
- const isScreenReaderEnabled = appCtx?.isScreenReaderEnabled ?? false;
-
- // When screen reader is enabled and aria-hidden is set, render nothing.
- if (isScreenReaderEnabled && props.ariaHidden) {
- return null;
- }
-
- // --- backgroundColor validation (A12) ---
- //
- // Validate the bg-style props during RENDER so a chalk-modifier name (the
- // exact case Ink's colorize.ts throws on) is caught by vue-tui's error
- // boundary, not the post-flush paint pass where a throw wedges Vue's
- // scheduler (cf. the borderStyle fix #124). See assertValidBackgroundColor /
- // Ink colorize.ts (40b3a75).
- //
- // Placed AFTER the screen-reader-hidden early-return to mirror Ink WHERE it
- // throws: Ink's render-node-to-output never reaches render-background /
- // render-border for a node it didn't emit, and a screen-reader-hidden Box
- // emits nothing — so colorize never runs for it. Below, we additionally
- // gate each value the way Ink's renderers do, so vue throws exactly where
- // Ink would colorize, and not elsewhere.
-
- // Box's OWN backgroundColor: Ink's render-background.ts only colorizes when
- // the resolved content area is > 0 (after subtracting drawn borders); it
- // bails early otherwise. We can't know the content area at render time
- // (layout hasn't run), so we validate eagerly. ACCEPTED tiny over-throw: a
- // degenerate-size Box (content area <= 0, e.g. width/height collapses to
- // border-only or 0) with a chalk-modifier-name backgroundColor throws here
- // where Ink would silently skip the fill. This is a niche case on
- // clearly-invalid input (a modifier name is never a valid bg); matching
- // Ink's layout-time gate at render time is not worth the complexity.
- assertValidBackgroundColor(props.backgroundColor);
-
- // Border backgrounds: Ink's render-border.ts only colorizes a border bg
- // when (a) borderStyle is truthy (line 28 gate) AND (b) the specific edge
- // is DRAWN (`border !== false`), and the value it passes per edge is
- // `borderBackgroundColor ?? borderBackgroundColor` (the per-edge
- // value with the general value as fallback — lines 44-52, 80/100/112/126).
- // So we validate exactly the resolved bg of each DRAWN edge — never the
- // general value on its own, and never an edge whose border isn't drawn.
- // Consequences (matching Ink, not over-throwing):
- // - no borderStyle → no border colorize at all → nothing validated.
- // - borderTop={false} → top edge not drawn → its resolved bg not checked.
- // - a bad general borderBackgroundColor but every DRAWN edge overrides it
- // with a valid per-edge value → general value never reaches colorize →
- // not validated → no throw.
- // ACCEPTED irreducible over-throw (same class as the content-area note
- // above): top/bottom edge bg is validated eagerly, but Ink only colorizes a
- // top/bottom border string when it's non-empty — a degenerate box (e.g.
- // width=0 with no left/right border) produces an empty string Ink skips.
- // Matching that needs layout, unavailable at render. Niche + invalid-input-only.
- if (props.borderStyle) {
- const stringStyle = (value: unknown) => (typeof value === "string" ? value : undefined);
- const generalBg = stringStyle(props.borderBackgroundColor);
- const generalFg = stringStyle(props.borderColor);
- if (props.borderTop !== false) {
- assertValidForegroundColor(
- stringStyle(props.borderTopColor) ?? generalFg,
- "borderTopColor",
- );
- assertValidBackgroundColor(
- stringStyle(props.borderTopBackgroundColor) ?? generalBg,
- "borderTopBackgroundColor",
- );
- }
- if (props.borderBottom !== false) {
- assertValidForegroundColor(
- stringStyle(props.borderBottomColor) ?? generalFg,
- "borderBottomColor",
- );
- assertValidBackgroundColor(
- stringStyle(props.borderBottomBackgroundColor) ?? generalBg,
- "borderBottomBackgroundColor",
- );
- }
- if (props.borderLeft !== false) {
- assertValidForegroundColor(
- stringStyle(props.borderLeftColor) ?? generalFg,
- "borderLeftColor",
- );
- assertValidBackgroundColor(
- stringStyle(props.borderLeftBackgroundColor) ?? generalBg,
- "borderLeftBackgroundColor",
- );
- }
- if (props.borderRight !== false) {
- assertValidForegroundColor(
- stringStyle(props.borderRightColor) ?? generalFg,
- "borderRightColor",
- );
- assertValidBackgroundColor(
- stringStyle(props.borderRightBackgroundColor) ?? generalBg,
- "borderRightBackgroundColor",
- );
- }
- }
-
- // NOTE: this component-level validation covers the public ``/``
- // API only. A raw host-op call (`h("box", { backgroundColor: "bold" })`)
- // bypasses it; the paint layer keeps its silent degrade-to-bare-text there
- // rather than throwing (a throw in the post-flush paint pass wedges Vue's
- // scheduler). Same accepted limitation as the borderStyle fix (#124).
-
- // Validate borderStyle during RENDER so an unknown name is caught by
- // vue-tui's error boundary (onErrorCaptured → ErrorOverview), exactly like
- // any other component render error. Ink crashes on an unknown borderStyle
- // with a raw TypeError during paint (render-border.ts reads box.topLeft off
- // cliBoxes[name] === undefined); we align to that "throw on unknown" contract
- // but do it here rather than in paint, where a throw would unwind through
- // Vue's post-flush commit and wedge its scheduler. Only a NON-EMPTY unknown
- // STRING throws: a falsy value (false/undefined/"" = no border) and a custom
- // BoxStyle OBJECT are both valid and pass through. (audit 2.3)
- // `borderStyle.length > 0` (not `!== ""`): once `typeof === "string"` narrows
- // the prop to the BorderStyle keyof union, an `!== ""` literal comparison has
- // "no overlap" per TS (the union has no `""` member). The empty string is only
- // reachable via a TS-bypass; `.length > 0` excludes it without tripping that.
- //
- // We validate the RESOLVED box has a real BoxStyle SHAPE rather than testing
- // `borderStyle in cliBoxes`, because `in` has two false-accept holes that let
- // a non-box value reach paint (which then reads `.top`/`.topLeft` glyph
- // strings off it):
- // 1. cli-boxes' default export carries a CJS-interop `default` self-key, so
- // `"default" in cliBoxes` is true — but cliBoxes.default is the WHOLE
- // boxes object, not a BoxStyle (it has no string `top`).
- // 2. `in` walks the prototype chain, so Object.prototype members
- // ("toString", "constructor", "hasOwnProperty", …) report as "in
- // cliBoxes" while resolving to a function/undefined — never a BoxStyle.
- // Resolving `cliBoxes[name]` and requiring an object with a string `top`
- // rejects unknown names (undefined), "default" (whole object, no string
- // `top`), and inherited props, while accepting every real preset.
- const borderStyle = props.borderStyle;
- if (typeof borderStyle === "string" && borderStyle.length > 0) {
- // Cast via `unknown` to add a string index signature: cliBoxes is typed as
- // the `Boxes` keyof object (no index signature), so a direct cast is a TS2352
- // "insufficient overlap" error. Same cast paint.ts uses to look up by name.
- const resolved = (cliBoxes as unknown as Record)[borderStyle];
- if (typeof resolved !== "object" || resolved === null || typeof resolved.top !== "string") {
- throw new Error(`Unknown borderStyle: ${JSON.stringify(borderStyle)}`);
- }
- }
-
- const ariaLabel = props.ariaLabel;
- const label = ariaLabel ? h("text", null, ariaLabel) : undefined;
-
- return h("box", props as never, isScreenReaderEnabled && label ? [label] : slots.default?.());
- };
- },
-});
-
-export const Box = BoxImpl as WithChildren;
-
-/** Props accepted by `` — the vue-tui analogue of Ink's `BoxProps`. */
-export type BoxProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/box.vue b/packages/runtime/src/components/box.vue
new file mode 100644
index 0000000..0b24e67
--- /dev/null
+++ b/packages/runtime/src/components/box.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+ {{ props.ariaLabel }}
+
+
+
diff --git a/packages/runtime/src/components/error-overview.ts b/packages/runtime/src/components/error-overview.ts
index 637d88d..7c9e7c1 100644
--- a/packages/runtime/src/components/error-overview.ts
+++ b/packages/runtime/src/components/error-overview.ts
@@ -3,8 +3,8 @@ import { cwd } from "node:process";
import { defineComponent, h, type PropType } from "vue";
import StackUtils from "stack-utils";
import codeExcerpt, { type CodeExcerpt } from "code-excerpt";
-import { Box } from "./box.ts";
-import { Text } from "./text.ts";
+import Box from "./box.vue";
+import Text from "./text.vue";
// Ported from Ink's src/components/ErrorOverview.tsx (v7.0.4). We use the
// and wrapper components (not raw host elements) because Ink does, and
diff --git a/packages/runtime/src/components/newline-props.ts b/packages/runtime/src/components/newline-props.ts
new file mode 100644
index 0000000..1825463
--- /dev/null
+++ b/packages/runtime/src/components/newline-props.ts
@@ -0,0 +1,6 @@
+import type { ExtractPublicPropTypes } from "vue";
+
+export const newlineProps = { count: { type: Number, default: 1 } };
+
+/** Props accepted by `` — the vue-tui analogue of Ink's `NewlineProps`. */
+export type NewlineProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/newline.ts b/packages/runtime/src/components/newline.ts
deleted file mode 100644
index 38f4a15..0000000
--- a/packages/runtime/src/components/newline.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { defineComponent, getCurrentInstance, h, type ExtractPublicPropTypes } from "vue";
-
-const newlineProps = { count: { type: Number, default: 1 } };
-
-export const Newline = defineComponent({
- name: "Newline",
- props: newlineProps,
- setup(props) {
- return () => {
- const content = "\n".repeat(props.count);
- // Inside a Text parent, render as inline virtual-text.
- // Outside Text, render as "text" (yoga carrier) so Newline participates
- // in layout standalone, matching Ink's ink-text behavior.
- if (isInsideText()) {
- return h("virtual-text", {}, content);
- }
- return h("text", {}, content);
- };
- },
-});
-
-function isInsideText(): boolean {
- let parent = getCurrentInstance()?.parent;
- while (parent) {
- const name = parent.type && (parent.type as { name?: string }).name;
- // A is also a text context: Ink models it as an ink-text host,
- // so a directly inside a standalone renders inline
- // (an inline line break in the transform's text), not as a standalone yoga
- // "text" node. (G58)
- if (name === "Text" || name === "Transform") return true;
- parent = parent.parent;
- }
- return false;
-}
-
-/** Props accepted by `` — the vue-tui analogue of Ink's `NewlineProps`. */
-export type NewlineProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/newline.vue b/packages/runtime/src/components/newline.vue
new file mode 100644
index 0000000..0ac4a5f
--- /dev/null
+++ b/packages/runtime/src/components/newline.vue
@@ -0,0 +1,17 @@
+
+
+
+ {{ content }}
+ {{ content }}
+
diff --git a/packages/runtime/src/components/spacer-props.ts b/packages/runtime/src/components/spacer-props.ts
new file mode 100644
index 0000000..10761e2
--- /dev/null
+++ b/packages/runtime/src/components/spacer-props.ts
@@ -0,0 +1,9 @@
+import type { ExtractPublicPropTypes } from "vue";
+
+// Spacer takes no props (it is a fixed flex-grow box). The empty object keeps the
+// same `*-props.ts` + `ExtractPublicPropTypes` pattern the other components use, so
+// `keyof SpacerProps` is `never` — matching Ink's empty `SpacerProps`.
+export const spacerProps = {};
+
+/** Props accepted by `` — the vue-tui analogue of Ink's `SpacerProps`. */
+export type SpacerProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/spacer.ts b/packages/runtime/src/components/spacer.ts
deleted file mode 100644
index f8db033..0000000
--- a/packages/runtime/src/components/spacer.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { defineComponent, h, type ExtractPublicPropTypes } from "vue";
-
-const spacerProps = {};
-
-const SpacerImpl = defineComponent({
- name: "Spacer",
- props: spacerProps,
- setup() {
- return () => h("box", { flexGrow: 1, flexShrink: 1 });
- },
-});
-
-/** Props accepted by `` — the vue-tui analogue of Ink's `SpacerProps`. */
-export type SpacerProps = ExtractPublicPropTypes;
-
-export const Spacer = SpacerImpl as typeof SpacerImpl & {
- new (): { $props: SpacerProps & { children?: never } };
-};
diff --git a/packages/runtime/src/components/spacer.vue b/packages/runtime/src/components/spacer.vue
new file mode 100644
index 0000000..0072f13
--- /dev/null
+++ b/packages/runtime/src/components/spacer.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/packages/runtime/src/components/static-props.ts b/packages/runtime/src/components/static-props.ts
new file mode 100644
index 0000000..ce14646
--- /dev/null
+++ b/packages/runtime/src/components/static-props.ts
@@ -0,0 +1,35 @@
+import type { ExtractPublicPropTypes, PropType, VNodeChild } from "vue";
+import type { BoxLayoutStyle } from "./box-props.ts";
+
+/** The `{ item, index }` object a `` scoped slot receives per item. */
+export interface StaticSlotProps {
+ item: T;
+ index: number;
+}
+
+/** A `` default scoped slot: rendered once per item. */
+export type StaticSlot = (props: StaticSlotProps) => VNodeChild;
+
+/** Accepted `` children: a bare scoped slot or a `{ default }` slot object. */
+export type StaticChildren = StaticSlot | { default: StaticSlot };
+
+/** ``'s `style` surface — the same layout style keys a `` accepts. */
+export type StaticStyle = BoxLayoutStyle;
+
+export const staticProps = {
+ // `required: true as const` (not bare `true`): a standalone `const` widens
+ // `true` -> `boolean`, dropping `items` from the required keys. The literal keeps
+ // `items` required, matching Ink's `StaticProps`.
+ items: { type: Array as PropType, required: true as const },
+ style: { type: Object as PropType, default: undefined },
+};
+
+type StaticBaseProps = ExtractPublicPropTypes;
+
+/**
+ * Props accepted by `` — the vue-tui analogue of Ink's `StaticProps`.
+ * Generic over the item type so `items: T[]` flows into the scoped slot's `item`.
+ */
+export type StaticProps = Omit & {
+ items: T[];
+};
diff --git a/packages/runtime/src/components/static.ts b/packages/runtime/src/components/static.ts
deleted file mode 100644
index b941453..0000000
--- a/packages/runtime/src/components/static.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- defineComponent,
- h,
- shallowRef,
- watch,
- type ExtractPublicPropTypes,
- type PropType,
- type SlotsType,
- type VNodeChild,
-} from "vue";
-import type { BoxLayoutStyle } from "./box.ts";
-
-export interface StaticSlotProps {
- item: T;
- index: number;
-}
-
-export type StaticSlot = (props: StaticSlotProps) => VNodeChild;
-
-export type StaticChildren = StaticSlot | { default: StaticSlot };
-
-export type StaticStyle = BoxLayoutStyle;
-
-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, required: true as const },
- style: { type: Object as PropType, default: undefined },
-};
-
-const StaticImpl = defineComponent({
- name: "Static",
- props: staticProps,
- slots: Object as SlotsType<{
- default: StaticSlot;
- }>,
- setup(props, { slots }) {
- const defaultStyle: StaticStyle = {
- position: "absolute",
- flexDirection: "column",
- };
-
- // Mirrors Ink's `const [index, setIndex] = useState(0)`. Only items at or
- // after `cursor` are rendered; once written, the renderer advances the
- // cursor (via the onWritten callback below) so written items unmount.
- // shallowRef is sufficient — we only ever reassign the number.
- const cursor = shallowRef(0);
-
- // Invoked by the renderer AFTER a commit has painted the freshly-written
- // items. Together with the watch below this is the vue-tui analogue of Ink's
- // post-commit `useLayoutEffect(() => setIndex(items.length), [items.length])`.
- //
- // This callback handles the GROW / steady-state direction: advancing the
- // cursor only AFTER paint guarantees freshly-appended items are written
- // before they are sliced out and unmounted. It must run post-paint, never
- // during render, which is why it can't be a plain watcher. We SET the cursor
- // to items.length (not max-with-current); assigning an equal number is a
- // reactivity no-op (Vue triggers on Object.is inequality), so the common
- // resync-to-same-length case can't loop.
- const onWritten = () => {
- cursor.value = (props.items as unknown[]).length;
- };
-
- // Handles the SHRINK direction, mirroring Ink's effect firing on every
- // [items.length] change — including decreases. When items shrink, the
- // already-rendered Static children may already be empty (sliced out), so no
- // host mutation occurs and no commit/onWritten fires; the cursor would stay
- // stranded above the new length and silently drop any later-appended items
- // (e.g. [A,B] cursor→2, shrink to [A], grow to [A,C] → slice(2)=[] drops C).
- // Lowering the cursor on shrink is safe without waiting for a paint: shrinking
- // never needs to write anything, it only re-syncs the slice window down.
- watch(
- () => (props.items as unknown[]).length,
- (len) => {
- if (len < cursor.value) cursor.value = len;
- },
- );
-
- return () => {
- const merged = { ...defaultStyle, ...props.style };
- const items = props.items as unknown[];
- const start = cursor.value;
- const itemsToRender = items.slice(start);
- return h(
- "static",
- { ...merged, internal_onWritten: onWritten },
- itemsToRender.map((item, i) => slots.default?.({ item, index: start + i })),
- );
- };
- },
-});
-
-/** Props accepted by `` — the vue-tui analogue of Ink's `StaticProps`. */
-type StaticBaseProps = ExtractPublicPropTypes;
-
-export type StaticProps = Omit & {
- items: T[];
-};
-
-export const Static = StaticImpl as typeof StaticImpl & {
- new (): {
- $props: StaticProps & { children?: StaticChildren };
- $slots: {
- default?: StaticSlot;
- };
- };
-};
diff --git a/packages/runtime/src/components/static.vue b/packages/runtime/src/components/static.vue
new file mode 100644
index 0000000..5edc59c
--- /dev/null
+++ b/packages/runtime/src/components/static.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/runtime/src/components/text-props.ts b/packages/runtime/src/components/text-props.ts
new file mode 100644
index 0000000..f880056
--- /dev/null
+++ b/packages/runtime/src/components/text-props.ts
@@ -0,0 +1,26 @@
+import { type ExtractPublicPropTypes, type PropType } from "vue";
+
+type WrapMode =
+ | "wrap"
+ | "hard"
+ | "truncate"
+ | "truncate-end"
+ | "truncate-middle"
+ | "truncate-start";
+
+export const textProps = {
+ color: String,
+ backgroundColor: String,
+ dimColor: Boolean,
+ bold: Boolean,
+ italic: Boolean,
+ underline: Boolean,
+ strikethrough: Boolean,
+ inverse: Boolean,
+ wrap: { type: String as PropType, default: "wrap" },
+ ariaLabel: String,
+ ariaHidden: Boolean,
+};
+
+/** Props accepted by `` — the vue-tui analogue of Ink's `TextProps`. */
+export type TextProps = ExtractPublicPropTypes;
diff --git a/packages/runtime/src/components/text.ts b/packages/runtime/src/components/text.ts
deleted file mode 100644
index 6b9e9f3..0000000
--- a/packages/runtime/src/components/text.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import {
- Comment,
- Text as VueText,
- defineComponent,
- getCurrentInstance,
- h,
- inject,
- isVNode,
- type ExtractPublicPropTypes,
- type PropType,
- type VNode,
-} from "vue";
-import { AppContextKey } from "../context.ts";
-import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
-import type { WithChildren } from "./with-children.ts";
-
-type WrapMode =
- | "wrap"
- | "hard"
- | "truncate"
- | "truncate-end"
- | "truncate-middle"
- | "truncate-start";
-
-const textProps = {
- color: String,
- backgroundColor: String,
- dimColor: Boolean,
- bold: Boolean,
- italic: Boolean,
- underline: Boolean,
- strikethrough: Boolean,
- inverse: Boolean,
- wrap: { type: String as PropType, default: "wrap" },
- ariaLabel: String,
- ariaHidden: Boolean,
-};
-
-const TextImpl = defineComponent({
- name: "Text",
- props: textProps,
- setup(props, { slots }) {
- const appCtx = inject(AppContextKey, null);
-
- return () => {
- const isScreenReaderEnabled = appCtx?.isScreenReaderEnabled ?? false;
-
- // When screen reader is enabled and aria-hidden is set, render nothing.
- if (isScreenReaderEnabled && props.ariaHidden) {
- return null;
- }
-
- const ariaLabel = props.ariaLabel;
- const children = isScreenReaderEnabled && ariaLabel ? ariaLabel : slots.default?.();
-
- if (children === undefined || children === null) {
- return null;
- }
-
- // Validate backgroundColor during RENDER so a chalk-modifier name (the
- // exact case Ink's colorize.ts throws on) is caught by vue-tui's error
- // boundary, not the post-flush paint pass where a throw wedges the
- // scheduler. See assertValidBackgroundColor / Ink colorize.ts (40b3a75).
- //
- // Gated to mirror Ink WHERE it colorizes. Ink's attaches a
- // colorizing `transform` to the ink-text node, but that transform only
- // runs on NON-EMPTY text: squash-text-nodes.ts applies it per child only
- // when `nodeText.length > 0`, and render-node-to-output.ts writes (and so
- // colorizes) only when the squashed `text.length > 0`. So Ink does NOT
- // throw for text that squashes to empty — even though it still renders the
- // (empty) node. The `children === null/undefined` early-return above only
- // catches a literal null/undefined child; a `{""}` empty-string child (or
- // a group of only empty/inert children) gets past it, so we additionally
- // skip validation when the content would render empty. (A12)
- if (wouldRenderNonEmptyText(children)) {
- assertValidForegroundColor(props.color);
- assertValidBackgroundColor(props.backgroundColor);
- }
-
- const insideText = isInsideText();
- if (insideText) {
- return h("virtual-text", props as never, children);
- }
- // Match Ink's defaults: flexShrink=1 so text nodes shrink when
- // they overflow their container (e.g. in no-wrap flex rows).
- return h("text", { ...props, flexShrink: 1 } as never, children);
- };
- },
-});
-
-export const Text = TextImpl as WithChildren;
-
-/** Props accepted by `` — the vue-tui analogue of Ink's `TextProps`. */
-export type TextProps = ExtractPublicPropTypes;
-
-/**
- * Heuristic for Ink's "would this colorize?" condition: its transform
- * only runs on non-empty squashed text (squash-text-nodes.ts gates each child on
- * `nodeText.length > 0`; render-node-to-output.ts writes only when the whole
- * `text.length > 0`). We can't squash at render time (nested content isn't
- * known yet), so we conservatively answer true unless the content is provably
- * empty. Provably-empty = an `""`/whitespace-collapsing string, or a children
- * array whose every entry is an empty-string text vnode or an inert Comment
- * (Vue's materialization of a `null`/`false`/`v-if` child). ANY element/component
- * child or any non-empty text makes it true — matching Ink, which would then
- * colorize the squashed result. The only residual over-throw is a contrived
- * element child that itself squashes to empty (e.g. a nested empty ); that
- * is the same irreducible class as Box's content-area gate.
- */
-function wouldRenderNonEmptyText(children: string | VNode[]): boolean {
- if (typeof children === "string") return children.length > 0;
- return !children.every((child) => {
- if (!isVNode(child)) return false;
- if (child.type === Comment) return true;
- // A Vue text vnode carries its string in `children`; empty ⇒ no text.
- if (child.type === VueText) return typeof child.children !== "string" || child.children === "";
- return false;
- });
-}
-
-function isInsideText(): boolean {
- let parent = getCurrentInstance()?.parent;
- while (parent) {
- const name = parent.type && (parent.type as { name?: string }).name;
- // A is also a text context: Ink models it as an ink-text host,
- // so a directly inside a renders inline (as a nested
- // ink-text squashed into the transform's text), matching Ink's
- // …. (G58)
- if (name === "Text" || name === "Transform") return true;
- parent = parent.parent;
- }
- return false;
-}
diff --git a/packages/runtime/src/components/text.vue b/packages/runtime/src/components/text.vue
new file mode 100644
index 0000000..15fb65a
--- /dev/null
+++ b/packages/runtime/src/components/text.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+ {{ srLabel }}
+
+
+
+
+ {{ srLabel }}
+
+
+
+
diff --git a/packages/runtime/src/components/transform.ts b/packages/runtime/src/components/transform.ts
index 1375d48..dbff9ca 100644
--- a/packages/runtime/src/components/transform.ts
+++ b/packages/runtime/src/components/transform.ts
@@ -4,11 +4,12 @@ import {
h,
inject,
isVNode,
+ provide,
type ExtractPublicPropTypes,
type PropType,
type VNode,
} from "vue";
-import { AppContextKey } from "../context.ts";
+import { AppContextKey, TextContextKey } from "../context.ts";
import type { WithChildren } from "./with-children.ts";
type TransformFn = (line: string, lineIndex: number) => string;
@@ -26,6 +27,10 @@ const TransformImpl = defineComponent({
props: transformProps,
setup(props, { slots }) {
const appCtx = inject(AppContextKey, null);
+ // A is a text context: Ink models it as an ink-text host, so
+ // descendant / render inline (squashed into the transform's
+ // text). It only provides — it never injects. (G58)
+ provide(TextContextKey, true);
return () => {
const children = slots.default?.();
diff --git a/packages/runtime/src/composables/useBoxMetrics.ts b/packages/runtime/src/composables/useBoxMetrics.ts
index c8a128b..48e1908 100644
--- a/packages/runtime/src/composables/useBoxMetrics.ts
+++ b/packages/runtime/src/composables/useBoxMetrics.ts
@@ -29,10 +29,47 @@ export interface UseBoxMetricsReturn {
readonly hasMeasured: ShallowRef;
}
+/**
+ * A component whose root is a `v-if`/`v-else` (e.g. the template-authored ``)
+ * renders as a Vue Fragment, so its `$el` resolves to the fragment's BOUNDARY anchor
+ * (an empty `text-leaf`), NOT the real `box` host node. The actual host node lives in
+ * the component's `subTree`. Walk that vnode tree to the first `el` that is a genuine
+ * host node — skipping the comment and empty-`text-leaf` anchors a fragment inserts —
+ * so a ref to a fragment-rooted Box still resolves to its `box` host node.
+ */
+function hostElFromSubTree(instance: unknown): Record | null {
+ const subTree = (instance as { subTree?: unknown })?.subTree;
+ return findHostEl(subTree);
+}
+
+function findHostEl(vnode: unknown): Record | null {
+ if (!vnode || typeof vnode !== "object") return null;
+ const vn = vnode as { el?: unknown; component?: { subTree?: unknown }; children?: unknown };
+ const el = vn.el as Record | undefined;
+ // A real host node carries a string `type` AND is not an empty boundary anchor.
+ if (el && typeof el.type === "string" && el.type !== "comment") {
+ if (!(el.type === "text-leaf" && el.value === "")) return el;
+ }
+ // A nested component (e.g. wrapping another component): descend its subTree.
+ if (vn.component?.subTree) {
+ const nested = findHostEl(vn.component.subTree);
+ if (nested) return nested;
+ }
+ // A fragment carries its real children in an array; the box vnode is among them.
+ if (Array.isArray(vn.children)) {
+ for (const child of vn.children) {
+ const found = findHostEl(child);
+ if (found) return found;
+ }
+ }
+ return null;
+}
+
/**
* Resolve a ref value to the underlying TUI node with a yoga property.
- * Handles both direct TUI node refs and Vue component instance refs (where
- * the TUI node is accessible via `$el`).
+ * Handles direct TUI node refs, component instance refs whose `$el` IS the host
+ * node, and fragment-rooted component refs (whose `$el` is a boundary anchor, so
+ * the host node is found via the component's subTree).
*/
function resolveYogaNode(value: unknown): { yoga: YogaNode } | null {
if (!value) return null;
@@ -43,6 +80,9 @@ function resolveYogaNode(value: unknown): { yoga: YogaNode } | null {
if (obj.$el && (obj.$el as Record).yoga) {
return obj.$el as { yoga: YogaNode };
}
+ // Fragment-rooted component (template with a root v-if): drill the subTree.
+ const host = hostElFromSubTree(obj.$);
+ if (host?.yoga) return host as { yoga: YogaNode };
return null;
}
@@ -51,10 +91,16 @@ function resolveTuiNode(value: unknown): TuiNode | null {
if (!value) return null;
const obj = value as Record;
if (typeof obj.type === "string") return obj as unknown as TuiNode;
- // Vue component instance — root host element is on $el
- if (obj.$el && typeof (obj.$el as Record).type === "string") {
- return obj.$el as unknown as TuiNode;
+ // Vue component instance whose `$el` IS a real host node (non-template/unconditional
+ // root). An empty `text-leaf` `$el` is a fragment boundary anchor, not the element —
+ // fall through to the subTree drill below so we anchor traversal on the real node.
+ const el = obj.$el as Record | undefined;
+ if (el && typeof el.type === "string" && !(el.type === "text-leaf" && el.value === "")) {
+ return el as unknown as TuiNode;
}
+ // Fragment-rooted component (template with a root v-if): drill the subTree.
+ const host = hostElFromSubTree(obj.$);
+ if (host && typeof host.type === "string") return host as unknown as TuiNode;
return null;
}
diff --git a/packages/runtime/src/context.ts b/packages/runtime/src/context.ts
index 10645b8..e0ec062 100644
--- a/packages/runtime/src/context.ts
+++ b/packages/runtime/src/context.ts
@@ -56,3 +56,8 @@ export const AppContextKey: InjectionKey = Symbol("vue-tui:app");
export const FocusContextKey: InjectionKey = Symbol("vue-tui:focus");
export const StdinContextKey: InjectionKey = Symbol("vue-tui:stdin");
export const AnimationSchedulerKey: InjectionKey = Symbol("vue-tui:animation");
+// Provided by and ; injected by and to decide
+// whether they render inline `virtual-text` (inside a text context) or a standalone
+// yoga `text`. Replaces the former getCurrentInstance() parent-walk — see
+// .agents/docs/component-authoring.md.
+export const TextContextKey: InjectionKey = Symbol("vue-tui:text-context");
diff --git a/packages/runtime/src/host/node-ops.ts b/packages/runtime/src/host/node-ops.ts
index d534bfe..fb1f079 100644
--- a/packages/runtime/src/host/node-ops.ts
+++ b/packages/runtime/src/host/node-ops.ts
@@ -418,7 +418,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions` boundary anchor) advances the
+ * index. Verified against real Ink v7.0.4 (`a{''}b` → `ab[1]`, not
+ * `ab[2]`). The inverse of static-channel's `isInertStaticAnchor`.
+ */
+export function advancesLineIndex(child: TuiNode): boolean {
+ return child.type !== "comment" && !(child.type === "text-leaf" && child.value === "");
+}
diff --git a/packages/runtime/src/host/text-measure.test.ts b/packages/runtime/src/host/text-measure.test.ts
index 625524a..182fdc9 100644
--- a/packages/runtime/src/host/text-measure.test.ts
+++ b/packages/runtime/src/host/text-measure.test.ts
@@ -5,8 +5,8 @@ import wrapAnsi from "wrap-ansi";
import { createText, createTextLeaf, createTransform, createVirtualText } from "./nodes.ts";
import { flattenLeaves, measureTextNatural, wrapText } from "./text-measure.ts";
import { renderToString } from "../render-to-string.ts";
-import { Box } from "../components/box.ts";
-import { Text } from "../components/text.ts";
+import Box from "../components/box.vue";
+import Text from "../components/text.vue";
// Minimal ANSI-stripping helper for test assertions (avoids strip-ansi dep).
function stripAnsi(s: string): string {
diff --git a/packages/runtime/src/host/text-measure.ts b/packages/runtime/src/host/text-measure.ts
index cc1c79f..a1d96c9 100644
--- a/packages/runtime/src/host/text-measure.ts
+++ b/packages/runtime/src/host/text-measure.ts
@@ -5,6 +5,7 @@ import wrapAnsi from "wrap-ansi";
import { tokenizeAnsi } from "../paint/ansi-tokenizer.ts";
import { sanitizeAnsi } from "../paint/sanitize-ansi.ts";
import type { TextProps, TuiNode, TuiText, TuiTransform, TuiVirtualText } from "./nodes.ts";
+import { advancesLineIndex } from "./nodes.ts";
export function flattenLeaves(node: TuiText | TuiVirtualText): string {
if (!node.children || node.children.length === 0) return "";
@@ -21,7 +22,7 @@ export function flattenLeaves(node: TuiText | TuiVirtualText): string {
let transformIndex = 0;
for (const child of node.children) {
out += squashTransformChild(child, transformIndex);
- if (child.type !== "comment") transformIndex++;
+ if (advancesLineIndex(child)) transformIndex++;
}
// Sanitize the measured string so MEASURE+WRAP operate on the SAME bytes PAINT
// emits — parity gap #9. Ink's squashTextNodes returns sanitizeAnsi(text)
@@ -82,7 +83,7 @@ function squashTransformChild(child: TuiNode, index: number): string {
let grandIndex = 0;
for (const grandchild of child.children) {
innerText += squashTransformChild(grandchild, grandIndex);
- if (grandchild.type !== "comment") grandIndex++;
+ if (advancesLineIndex(grandchild)) grandIndex++;
}
if (innerText.length > 0 && child.transform) innerText = child.transform(innerText, index);
return innerText;
@@ -103,7 +104,7 @@ export function flattenTransformLeaves(node: TuiTransform): string {
let transformIndex = 0;
for (const child of node.children) {
out += squashTransformChild(child, transformIndex);
- if (child.type !== "comment") transformIndex++;
+ if (advancesLineIndex(child)) transformIndex++;
}
// Sanitize for the same reason as flattenLeaves (parity gap #9; see that comment
// for the two distinct width/wrap mechanisms): the standalone measure
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index a4dd148..e14c609 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -1,25 +1,57 @@
+import BoxSfc from "./components/box.vue";
+import TextSfc from "./components/text.vue";
+import StaticSfc from "./components/static.vue";
+import SpacerSfc from "./components/spacer.vue";
+import type { WithChildren } from "./components/with-children.ts";
+import type { StaticChildren, StaticProps, StaticSlot } from "./components/static-props.ts";
+import type { SpacerProps } from "./components/spacer-props.ts";
+
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 BoxLayoutStyle,
- 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, type SpacerProps } from "./components/spacer.ts";
-export {
- Static,
- type StaticChildren,
- type StaticProps,
- type StaticSlot,
- type StaticSlotProps,
- type StaticStyle,
-} from "./components/static.ts";
+export const Box = BoxSfc as WithChildren;
+export type {
+ AriaRole,
+ AriaState,
+ BoxLayoutStyle,
+ BoxStyle,
+ BoxProps,
+} from "./components/box-props.ts";
+export const Text = TextSfc as WithChildren;
+export type { TextProps } from "./components/text-props.ts";
+export { default as Newline } from "./components/newline.vue";
+export type { NewlineProps } from "./components/newline-props.ts";
+// Spacer takes no props and no children; the `children?: never` cast makes
+// `x` a type error under the automatic JSX runtime (parity with
+// main's spacer.ts typing). `as unknown as` (like Static) REPLACES the SFC type:
+// the empty `.vue`'s DefineComponent carries an `any` that would make a `typeof
+// SpacerSfc & {…}` intersection redundant (lint: no-redundant-type-constituents).
+export const Spacer = SpacerSfc as unknown as {
+ new (): { $props: SpacerProps & { children?: never } };
+};
+export type { SpacerProps } from "./components/spacer-props.ts";
+// Static exposes typed scoped slots: children receive `{ item, index }` with
+// `item` inferred from `items: T[]` (parity with main's static.ts generic cast).
+// `as unknown as` REPLACES the SFC's type rather than intersecting it: a `.vue`
+// with a scoped `` emits an extra `__VLS_WithSlots` construct signature that
+// bakes a NON-generic `default?: (p: { item: unknown }) => …`. Intersecting (or
+// keeping `typeof StaticSfc` in the target) leaves that competing signature in
+// place and blocks `T` inference (item → any). A clean generic construct signature
+// is all JSX/template resolution needs, and it is exactly the public shape main's
+// defineComponent-based cast produced.
+export const Static = StaticSfc as unknown as {
+ new (): {
+ $props: StaticProps & { children?: StaticChildren };
+ $slots: { default?: StaticSlot };
+ };
+};
+export type {
+ StaticChildren,
+ StaticProps,
+ StaticSlot,
+ StaticSlotProps,
+ StaticStyle,
+} from "./components/static-props.ts";
export { Transform, type TransformProps } from "./components/transform.ts";
export { useApp, type UseAppReturn } from "./composables/useApp.ts";
diff --git a/packages/runtime/src/overlay.ts b/packages/runtime/src/overlay.ts
index 02eae8a..9d79c72 100644
--- a/packages/runtime/src/overlay.ts
+++ b/packages/runtime/src/overlay.ts
@@ -1,6 +1,6 @@
import { defineComponent, h, inject, type Component, type PropType } from "@vue/runtime-core";
-import { Box } from "./components/box.ts";
-import { Text } from "./components/text.ts";
+import Box from "./components/box.vue";
+import Text from "./components/text.vue";
import { DevStateKey, type DevState } from "./hmr.ts";
const ErrorDisplay = defineComponent({
diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts
index a7c3802..a2453d6 100644
--- a/packages/runtime/src/paint/paint.ts
+++ b/packages/runtime/src/paint/paint.ts
@@ -22,7 +22,11 @@ import type {
TuiBox,
} from "../host/nodes.ts";
import { transformHasYogaChild } from "../host/yoga.ts";
-import { createRoot as createIsoRoot, createBox as createIsoBox } from "../host/nodes.ts";
+import {
+ createRoot as createIsoRoot,
+ createBox as createIsoBox,
+ advancesLineIndex,
+} from "../host/nodes.ts";
import { calculateLayoutWithContentGuards } from "../host/layout-guards.ts";
import { wrapText, safeSliceEnd } from "../host/text-measure.ts";
import { attachYoga, detachYoga } from "../host/yoga.ts";
@@ -388,9 +392,10 @@ function squashInlineChildren(children: readonly TuiNode[], inheritedBg: unknown
let transformIndex = 0;
for (const child of children) {
out += squashTransformChild(child, transformIndex, inheritedBg);
- // Comments (Vue's null/v-if/false renders) contribute "" and, like React's
- // absent childNodes, must NOT advance the transform index.
- if (child.type !== "comment") transformIndex++;
+ // Comments (Vue's null/v-if/false renders) and EMPTY text-leaves (`{''}` /
+ // template anchors) contribute "" and, like React's absent childNodes,
+ // must NOT advance the transform index.
+ if (advancesLineIndex(child)) transformIndex++;
}
return out;
}
@@ -445,7 +450,7 @@ function squashTransformChild(child: TuiNode, index: number, inheritedBg: unknow
let grandIndex = 0;
for (const grandchild of child.children) {
innerText += squashTransformChild(grandchild, grandIndex, inheritedBg);
- if (grandchild.type !== "comment") grandIndex++;
+ if (advancesLineIndex(grandchild)) grandIndex++;
}
if (innerText.length > 0 && child.transform) {
innerText = child.transform(innerText, index);
diff --git a/packages/runtime/src/paint/screen-reader.ts b/packages/runtime/src/paint/screen-reader.ts
index 3223bde..fbcdf81 100644
--- a/packages/runtime/src/paint/screen-reader.ts
+++ b/packages/runtime/src/paint/screen-reader.ts
@@ -1,5 +1,6 @@
import Yoga from "yoga-layout";
import type { TuiNode, TuiText, TuiVirtualText, TuiBox } from "../host/nodes.ts";
+import { advancesLineIndex } from "../host/nodes.ts";
import { sanitizeAnsi } from "./sanitize-ansi.ts";
/**
@@ -23,13 +24,14 @@ function squashChildSR(child: TuiNode, index: number): string {
if (child.type === "transform") {
let innerText = "";
// Recurse into the transform's children (each may itself be a transform,
- // recursed to any depth), skipping Vue comment nodes for the index basis
- // (G52), then apply THIS child transform's own fn (it is a child). Matches
+ // recursed to any depth), skipping Vue comment nodes and empty text-leaves
+ // for the index basis (G52), then apply THIS child transform's own fn (it is a
+ // child). Matches
// Ink squash-text-nodes.ts:34 (`internal_transform(nodeText, index)`).
let grandIndex = 0;
for (const grandchild of child.children) {
innerText += squashChildSR(grandchild, grandIndex);
- if (grandchild.type !== "comment") grandIndex++;
+ if (advancesLineIndex(grandchild)) grandIndex++;
}
if (innerText.length > 0 && child.transform) {
innerText = child.transform(innerText, index);
@@ -58,9 +60,10 @@ function squashTextContent(node: TuiText | TuiVirtualText): string {
let index = 0;
for (const child of node.children) {
text += squashChildSR(child, index);
- // Comments (Vue's null/v-if/false renders) contribute nothing and, like
- // React's absent childNodes, must NOT advance the transform index.
- if (child.type !== "comment") index++;
+ // Comments (Vue's null/v-if/false renders) and EMPTY text-leaves (`{''}` /
+ // template anchors) contribute nothing and, like React's absent
+ // childNodes, must NOT advance the transform index.
+ if (advancesLineIndex(child)) index++;
}
// Strip cursor/erase control sequences (keep SGR/OSC) from the squashed text,
// exactly as Ink's squashTextNodes returns sanitizeAnsi(text)
@@ -196,7 +199,7 @@ export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOpt
let squashed = "";
for (const childNode of node.children) {
squashed += squashChildSR(childNode, index);
- if (childNode.type !== "comment") index++;
+ if (advancesLineIndex(childNode)) index++;
}
// A standalone is an `ink-text` node in Ink, squashed via
// squashTextNodes which returns sanitizeAnsi(text) (squash-text-nodes.ts:45).
diff --git a/packages/runtime/src/paint/static-channel.ts b/packages/runtime/src/paint/static-channel.ts
index 57c8be9..572e4c4 100644
--- a/packages/runtime/src/paint/static-channel.ts
+++ b/packages/runtime/src/paint/static-channel.ts
@@ -34,6 +34,10 @@ export function findStatics(root: TuiNode, out: TuiStatic[] = []): TuiStatic[] {
return out;
}
+function isInertStaticAnchor(child: TuiNode): boolean {
+ return child.type === "comment" || (child.type === "text-leaf" && child.value === "");
+}
+
/**
* Paint the not-yet-written children of a single node and record them
* as written. Returns the painted frame (without trailing "\n"), or "" when
@@ -54,6 +58,7 @@ export function paintStaticNode(
isScreenReaderEnabled = false,
): string {
const fresh = stat.children.filter((child) => !stat.writtenNodes.has(child));
+ const paintableFresh = fresh.filter((child) => !isInertStaticAnchor(child));
// Paint (and record as written) only when there is something fresh — but the
// prune and onWritten steps below run on EVERY commit, including the empty
// commit that follows a cursor advance (children sliced to []). That empty
@@ -62,7 +67,7 @@ export function paintStaticNode(
// paints, yet Ink's `useLayoutEffect(setIndex(items.length))` still fires and
// lowers the cursor so subsequent grows ([A,C]) render and write the new item.
let frame = "";
- if (fresh.length > 0) {
+ if (paintableFresh.length > 0) {
if (isScreenReaderEnabled) {
// SR mode: linearize the fresh static children to flat plain text instead
// of the 2D grid painter — otherwise bordered static items would emit box
@@ -76,7 +81,7 @@ export function paintStaticNode(
// exactly how screen-reader.ts linearizes a box/root container of these
// children (screen-reader.ts:73-82): the separator and child order derive
// from the container's resolved flexDirection (defaulting to the
- // "column" default set in static.ts).
+ // "column" default set in static.vue's `merged` computed).
const flexDirection = resolvedFlexDirection(stat);
// Match screen-reader.ts:76 exactly — row/row-reverse use a space, all
// other directions (incl. the column default) use a newline.
@@ -84,16 +89,22 @@ export function paintStaticNode(
// Match screen-reader.ts:79-82 — *-reverse directions reverse child order.
const ordered =
flexDirection === "row-reverse" || flexDirection === "column-reverse"
- ? [...fresh].reverse()
- : fresh;
+ ? [...paintableFresh].reverse()
+ : paintableFresh;
frame = ordered
.map((child) => renderScreenReaderOutput(child, { skipStaticElements: false }))
.filter(Boolean)
.join(separator);
} else {
- frame = paintIsolated(fresh, columns, stat);
+ frame = paintIsolated(paintableFresh, columns, stat);
}
- for (const child of fresh) stat.writtenNodes.add(child);
+ for (const child of paintableFresh) stat.writtenNodes.add(child);
+ }
+ // Empty text leaves and comments can be framework anchors around a template
+ // v-for. They render no content, but still need write-once bookkeeping so
+ // the cursor/prune path behaves like a normal painted batch.
+ for (const child of fresh) {
+ if (isInertStaticAnchor(child)) stat.writtenNodes.add(child);
}
// Prune entries that are no longer mounted so the set can't grow unbounded
// over a long-running app (written children get unmounted on the next render).
diff --git a/packages/runtime/vite.config.ts b/packages/runtime/vite.config.ts
index 4f9c477..a8a53fa 100644
--- a/packages/runtime/vite.config.ts
+++ b/packages/runtime/vite.config.ts
@@ -1,11 +1,39 @@
import { defineConfig } from "vite-plus";
import vueJsx from "@vitejs/plugin-vue-jsx";
+import Vue from "unplugin-vue/rolldown";
+import VueVite from "unplugin-vue/vite";
+
+const HOST_TAGS = ["box", "text", "virtual-text", "static", "transform"];
export default defineConfig({
- plugins: [vueJsx()],
+ // `VueVite` parses `.vue` SFCs in the TEST/dev graph (unit tests may import the
+ // .vue components directly, e.g. host/text-measure.test.ts). The `pack` build has
+ // its own `Vue` rolldown plugin below; both need `isCustomElement` so the host
+ // tags (`` / `` / …) inside SFC templates compile to raw element
+ // vnodes instead of being resolved as components.
+ plugins: [
+ vueJsx(),
+ VueVite({
+ template: {
+ compilerOptions: {
+ isCustomElement: (tag: string) => HOST_TAGS.includes(tag),
+ },
+ },
+ }),
+ ],
pack: {
entry: ["src/index.ts", "src/internal.ts"],
- dts: true,
+ plugins: [
+ Vue({
+ isProduction: true,
+ template: {
+ compilerOptions: {
+ isCustomElement: (tag: string) => HOST_TAGS.includes(tag),
+ },
+ },
+ }),
+ ],
+ dts: { vue: true },
exports: true,
},
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d1b547..891680c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -42,9 +42,15 @@ catalogs:
tsx:
specifier: ^4.22.0
version: 4.22.3
+ unplugin-vue:
+ specifier: ^7.2.0
+ version: 7.2.0
vite-plus:
specifier: ^0.1.22
version: 0.1.22
+ vue-tsc:
+ specifier: ^3.3.4
+ version: 3.3.4
overrides:
vite: npm:@voidzero-dev/vite-plus-core@latest
@@ -75,10 +81,10 @@ importers:
version: 24.12.4
'@vitejs/plugin-vue-jsx':
specifier: ^5
- version: 5.1.5(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
+ version: 5.1.5(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
- version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
examples/basic-template:
dependencies:
@@ -97,10 +103,10 @@ importers:
version: 24.12.4
'@vitejs/plugin-vue':
specifier: ^6
- version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
+ version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
- version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
examples/coding-agent:
dependencies:
@@ -122,10 +128,10 @@ importers:
version: 24.12.4
'@vitejs/plugin-vue':
specifier: ^6
- version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
+ version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
- version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
examples/flappy-bird:
dependencies:
@@ -144,16 +150,16 @@ importers:
version: 24.12.4
'@vitejs/plugin-vue':
specifier: ^6
- version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
+ version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
- version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
packages/cli:
dependencies:
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
- version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
devDependencies:
'@types/node':
specifier: 'catalog:'
@@ -163,7 +169,7 @@ importers:
version: 6.0.3
vite-plus:
specifier: 'catalog:'
- version: 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)
+ version: 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)
packages/runtime:
dependencies:
@@ -228,12 +234,18 @@ importers:
typescript:
specifier: ^6.0.3
version: 6.0.3
+ unplugin-vue:
+ specifier: 'catalog:'
+ version: 7.2.0(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
vite-plus:
specifier: ^0.1.20
version: 0.1.22(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3))
vue:
specifier: ^3.4.0
version: 3.5.34(typescript@6.0.3)
+ vue-tsc:
+ specifier: 'catalog:'
+ version: 3.3.4(typescript@6.0.3)
packages/runtime-tests:
devDependencies:
@@ -279,6 +291,9 @@ importers:
vue:
specifier: ^3.5.34
version: 3.5.34(typescript@6.0.3)
+ vue-tsc:
+ specifier: 'catalog:'
+ version: 3.3.4(typescript@6.0.3)
packages/testing:
dependencies:
@@ -1178,13 +1193,13 @@ packages:
yaml:
optional: true
- '@voidzero-dev/vite-plus-core@0.1.23':
- resolution: {integrity: sha512-Twi+95cq1pObzkNR4u6lP7z4gPhtS0/vxeBAdbTvAeA12qlyyFED7mQZnAgaVIN3k1C1ve0997F3/ncUBAwQ8w==}
+ '@voidzero-dev/vite-plus-core@0.1.24':
+ resolution: {integrity: sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@arethetypeswrong/core': ^0.18.1
- '@tsdown/css': 0.22.0
- '@tsdown/exe': 0.22.0
+ '@tsdown/css': 0.22.1
+ '@tsdown/exe': 0.22.1
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.18
esbuild: ^0.27.0 || ^0.28.0
@@ -1407,6 +1422,15 @@ packages:
cpu: [x64]
os: [win32]
+ '@volar/language-core@2.4.28':
+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
+
+ '@volar/source-map@2.4.28':
+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
+
+ '@volar/typescript@2.4.28':
+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
+
'@vue/babel-helper-vue-transform-on@2.0.1':
resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==}
@@ -1435,6 +1459,9 @@ packages:
'@vue/compiler-ssr@3.5.34':
resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==}
+ '@vue/language-core@3.3.4':
+ resolution: {integrity: sha512-IuHqQ5zGGOE7CXP72VX6A42IVeIzYv4WAhO6arej11TRNqtdZfGyH8Yr2FOCaDX0dSQG+JwULLoFHGY1igYVjQ==}
+
'@vue/reactivity@3.5.34':
resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
@@ -1460,6 +1487,9 @@ packages:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
+ alien-signals@3.2.1:
+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
+
ansi-escapes@7.3.0:
resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
engines: {node: '>=18'}
@@ -1781,6 +1811,9 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1848,6 +1881,9 @@ packages:
resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1963,6 +1999,16 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+ unplugin-vue@7.2.0:
+ resolution: {integrity: sha512-6JHWcRbLO3bySOsipQSW9n2VRoPqsx9GLSWBfp7QLPCvpUr/edeAXR7pSl44EEJ+FIp9bG8Zm+xm5dnM/yir4Q==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ vue: ^3.2.25
+
+ unplugin@3.0.0:
+ resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
@@ -2022,6 +2068,15 @@ packages:
yaml:
optional: true
+ vscode-uri@3.1.0:
+ resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+
+ vue-tsc@3.3.4:
+ resolution: {integrity: sha512-XA/JqmQwS2GZmfgpjOEGdrKwaTSEuPwxpHa7/t6f4yiGrJb3gVHTPb9wBfByMNZwQ+xDXs41b8gaS2DKsOozUw==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.0.0'
+
vue@3.5.34:
resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==}
peerDependencies:
@@ -2037,6 +2092,9 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -2602,14 +2660,14 @@ snapshots:
'@types/stack-utils@2.0.3': {}
- '@vitejs/plugin-vue-jsx@5.1.5(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))':
+ '@vitejs/plugin-vue-jsx@5.1.5(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.1
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
- vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
vue: 3.5.34(typescript@6.0.3)
transitivePeerDependencies:
- supports-color
@@ -2638,10 +2696,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))':
+ '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
- vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
vue: 3.5.34(typescript@6.0.3)
'@voidzero-dev/vite-plus-core@0.1.21(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
@@ -2696,7 +2754,7 @@ snapshots:
tsx: 4.22.3
typescript: 6.0.3
- '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
+ '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
dependencies:
'@oxc-project/runtime': 0.133.0
'@oxc-project/types': 0.133.0
@@ -2709,6 +2767,19 @@ snapshots:
tsx: 4.22.3
typescript: 6.0.3
+ '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
+ dependencies:
+ '@oxc-project/runtime': 0.133.0
+ '@oxc-project/types': 0.133.0
+ lightningcss: 1.32.0
+ postcss: 8.5.14
+ optionalDependencies:
+ '@types/node': 25.8.0
+ esbuild: 0.28.0
+ fsevents: 2.3.3
+ tsx: 4.22.3
+ typescript: 6.0.3
+
'@voidzero-dev/vite-plus-darwin-arm64@0.1.21':
optional: true
@@ -2785,7 +2856,7 @@ snapshots:
- utf-8-validate
- yaml
- '@voidzero-dev/vite-plus-test@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
+ '@voidzero-dev/vite-plus-test@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
@@ -2799,7 +2870,7 @@ snapshots:
tinybench: 2.9.0
tinyexec: 1.2.2
tinyglobby: 0.2.16
- vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
ws: 8.21.0
optionalDependencies:
'@types/node': 24.12.4
@@ -2917,6 +2988,18 @@ snapshots:
'@voidzero-dev/vite-plus-win32-x64-msvc@0.1.22':
optional: true
+ '@volar/language-core@2.4.28':
+ dependencies:
+ '@volar/source-map': 2.4.28
+
+ '@volar/source-map@2.4.28': {}
+
+ '@volar/typescript@2.4.28':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ path-browserify: 1.0.1
+ vscode-uri: 3.1.0
+
'@vue/babel-helper-vue-transform-on@2.0.1': {}
'@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.0)':
@@ -2976,6 +3059,16 @@ snapshots:
'@vue/compiler-dom': 3.5.34
'@vue/shared': 3.5.34
+ '@vue/language-core@3.3.4':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ '@vue/compiler-dom': 3.5.34
+ '@vue/shared': 3.5.34
+ alien-signals: 3.2.1
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ picomatch: 4.0.4
+
'@vue/reactivity@3.5.34':
dependencies:
'@vue/shared': 3.5.34
@@ -3008,6 +3101,8 @@ snapshots:
dependencies:
humanize-ms: 1.2.1
+ alien-signals@3.2.1: {}
+
ansi-escapes@7.3.0:
dependencies:
environment: 1.1.0
@@ -3277,6 +3372,8 @@ snapshots:
ms@2.1.3: {}
+ muggle-string@0.4.1: {}
+
nanoid@3.3.12: {}
node-addon-api@7.1.1: {}
@@ -3367,6 +3464,8 @@ snapshots:
patch-console@2.0.0: {}
+ path-browserify@1.0.1: {}
+
picocolors@1.1.1: {}
picomatch@4.0.4: {}
@@ -3478,6 +3577,42 @@ snapshots:
undici-types@7.24.6: {}
+ unplugin-vue@7.2.0(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)):
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ '@vue/reactivity': 3.5.34
+ obug: 2.1.1
+ unplugin: 3.0.0
+ vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'
+ vue: 3.5.34(typescript@6.0.3)
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@tsdown/css'
+ - '@tsdown/exe'
+ - '@types/node'
+ - '@vitejs/devtools'
+ - esbuild
+ - jiti
+ - less
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - yaml
+
+ unplugin@3.0.0:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ picomatch: 4.0.4
+ webpack-virtual-modules: 0.6.2
+
update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies:
browserslist: 4.28.2
@@ -3532,12 +3667,12 @@ snapshots:
- vite
- yaml
- vite-plus@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3):
+ vite-plus@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3):
dependencies:
'@oxc-project/types': 0.129.0
'@oxlint/plugins': 1.61.0
'@voidzero-dev/vite-plus-core': 0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)
- '@voidzero-dev/vite-plus-test': 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)
+ '@voidzero-dev/vite-plus-test': 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)
oxfmt: 0.48.0
oxlint: 1.63.0(oxlint-tsgolint@0.22.1)
oxlint-tsgolint: 0.22.1
@@ -3705,6 +3840,14 @@ snapshots:
fsevents: 2.3.3
tsx: 4.22.3
+ vscode-uri@3.1.0: {}
+
+ vue-tsc@3.3.4(typescript@6.0.3):
+ dependencies:
+ '@volar/typescript': 2.4.28
+ '@vue/language-core': 3.3.4
+ typescript: 6.0.3
+
vue@3.5.34(typescript@6.0.3):
dependencies:
'@vue/compiler-dom': 3.5.34
@@ -3719,6 +3862,8 @@ snapshots:
webidl-conversions@3.0.1: {}
+ webpack-virtual-modules@0.6.2: {}
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5b20ce1..9a4d3e1 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -23,6 +23,8 @@ catalog:
terminal-size: ^4.0.1
tsx: ^4.22.0
string-width: ^8.0.0
+ unplugin-vue: ^7.2.0
+ vue-tsc: ^3.3.4
overrides:
vite: "catalog:"
vitest: "catalog:"