refactor(runtime)!: prefix host primitive tags with tui- (align to Ink, drop *Impl)
The renderer's intrinsic elements were named with bare words (box/text/static/
transform/virtual-text), which collide with the same-named public components: a
template `<box>` PascalCase-resolves to `<Box>` under vue-tsc (no isCustomElement
at the type layer), forcing the BoxImpl/TextImpl/StaticImpl workaround.
Prefix the 5 host elements to `tui-*` (mirroring Ink's `ink-box`/`ink-text`):
the prefix + hyphen keeps them in their own namespace, so the components keep
their real names (Box/Text/Static) with no self-recursion — the *Impl rename is
removed. root/text-leaf/comment stay unprefixed (not template tags, not elements).
Mechanics: renamed the TuiNode discriminant literals + factories first, then let
vue-tsc enumerate all 145 stale `node.type === "box"` comparisons (the type-
driven finder also kept `position: "static"` and the ansi-tokenizer's separate
`type: "text"` union untouched). Updated createElement cases, HOST_TAGS,
the .vue templates, transform.ts h(), and raw `h("box")` host-op tests.
Two non-type-checked contaminations the sed caused were caught by tests and fixed:
- patchProp's `key === "transform"` (the PROP name, not the node type) must stay
"transform" — the sed wrongly prefixed it, dropping the transform fn (identity).
- text-measure's `token.type === "text"` is an AnsiToken, not a TuiNode — reverted.
BREAKING CHANGE: the internal host element names are now tui-box/tui-text/
tui-virtual-text/tui-static/tui-transform. Public components (Box/Text/Static/
Spacer/Newline/Transform) and their props/types are unchanged; only raw host-op
callers (h("box") -> h("tui-box")) are affected.
vp run ready green: fmt, lint 0/0, vue-tsc, tests (runtime 350, integration 1161,
PTY 129).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,7 @@ and fixed at the root:
|
|||||||
`fresh.length > 0` gate and `paintIsolated` painted the container's **padding** as stray
|
`fresh.length > 0` gate and `paintIsolated` painted the container's **padding** as stray
|
||||||
blank lines — while `findStatics` in the same file already skipped `text-leaf`/`comment`.
|
blank lines — while `findStatics` in the same file already skipped `text-leaf`/`comment`.
|
||||||
Fix: `paintStaticNode` skips inert anchors too (safe: `node-ops.ts` forbids non-empty bare
|
Fix: `paintStaticNode` skips inert anchors too (safe: `node-ops.ts` forbids non-empty bare
|
||||||
text under `<static>`, so the only `text-leaf` there is an empty anchor).
|
text under `<tui-static>`, so the only `text-leaf` there is an empty anchor).
|
||||||
- **Text — `<slot/>` fragment anchors shifted the transform line-index, exposing an Ink-parity
|
- **Text — `<slot/>` fragment anchors shifted the transform line-index, exposing an Ink-parity
|
||||||
bug.** A `<slot/>` mounts as a Fragment whose boundary anchors are empty `text-leaf`s; the
|
bug.** A `<slot/>` mounts as a Fragment whose boundary anchors are empty `text-leaf`s; the
|
||||||
squash loops that give a nested `<Transform>` its positional line index counted every
|
squash loops that give a nested `<Transform>` its positional line index counted every
|
||||||
@@ -98,18 +98,23 @@ and fixed at the root:
|
|||||||
verbatim — so `:flex-grow="1"` reaches the renderer as `flex-grow` and is rejected. Use
|
verbatim — so `:flex-grow="1"` reaches the renderer as `flex-grow` and is rejected. Use
|
||||||
`:flexGrow="1"`, or `v-bind="someObject"` (object keys are preserved). `Box`/`Text`/`Static`
|
`:flexGrow="1"`, or `v-bind="someObject"` (object keys are preserved). `Box`/`Text`/`Static`
|
||||||
bind a whole props/style object with `v-bind`; `Spacer` uses explicit camelCase.
|
bind a whole props/style object with `v-bind`; `Spacer` uses explicit camelCase.
|
||||||
- **Name a component differently from any host tag it renders** (`BoxImpl`/`TextImpl`/
|
- **Host primitive tags are `tui-`-prefixed** (`tui-box`/`tui-text`/`tui-virtual-text`/
|
||||||
`StaticImpl` via `defineOptions`). vue-tsc 3.3.4 self-recurses a `<box>` tag to a component
|
`tui-static`/`tui-transform`), mirroring Ink's `ink-box`/`ink-text`. The prefix keeps the
|
||||||
named "Box" (it has no `isCustomElement` at the type layer). Public names (`Box`/`Text`/
|
renderer's intrinsic elements in their own namespace, so a template `<tui-box>` never
|
||||||
`Static`) come from `index.ts`.
|
resolves to the public `<Box>` component — the components keep their real `name`
|
||||||
|
(`Box`/`Text`/`Static`) with no vue-tsc self-recursion. (Earlier the tags were bare
|
||||||
|
`box`/`text`/…, which collided with the same-named components and forced an `*Impl` internal
|
||||||
|
rename to dodge it; the prefix removed that workaround. vue-tsc has no `isCustomElement` at
|
||||||
|
the type layer, so a bare lowercase tag would PascalCase-resolve to the component — the
|
||||||
|
hyphenated `tui-` name sidesteps that entirely.)
|
||||||
- **Don't reintroduce parent-walking or `parent.type.name` matching for context** — use
|
- **Don't reintroduce parent-walking or `parent.type.name` matching for context** — use
|
||||||
provide/inject (`.name` is also fragile under minification).
|
provide/inject (`.name` is also fragile under minification).
|
||||||
- **Don't force child-vnode inspection into a template** (the double-materialization wart). If
|
- **Don't force child-vnode inspection into a template** (the double-materialization wart). If
|
||||||
a new component needs it, make it a render function — where the whole ecosystem draws the line.
|
a new component needs it, make it a render function — where the whole ecosystem draws the line.
|
||||||
- The host elements (`box`, `text`, `virtual-text`, `static`, `transform`) compile to raw
|
- The host elements (`tui-box`, `tui-text`, `tui-virtual-text`, `tui-static`, `tui-transform`)
|
||||||
element vnodes via the build's `isCustomElement` option and are an **internal** detail.
|
compile to raw element vnodes via the build's `isCustomElement` option and are an **internal**
|
||||||
Consumers use `<Box>` / `<Text>`, never `<box>`. SFC templates may reference the host tags
|
detail. Consumers use `<Box>` / `<Text>`, never `<tui-box>`. SFC templates may reference the
|
||||||
directly; their loose typing under `vue-tsc` (no `strictTemplates`) is intentional.
|
host tags directly; their loose typing under `vue-tsc` (no `strictTemplates`) is intentional.
|
||||||
- Components export typed props (`ExtractPublicPropTypes` over the runtime props object) and
|
- Components export typed props (`ExtractPublicPropTypes` over the runtime props object) and
|
||||||
keep the `WithChildren` shim (`with-children.ts`): Vue's automatic JSX runtime routes
|
keep the `WithChildren` shim (`with-children.ts`): Vue's automatic JSX runtime routes
|
||||||
children to a `children` prop that declared slots do NOT provide, so the shim is required for
|
children to a `children` prop that declared slots do NOT provide, so the shim is required for
|
||||||
|
|||||||
@@ -809,8 +809,10 @@ test("non-string host Box backgroundColor does not override inherited background
|
|||||||
const { lastFrame } = await render(
|
const { lastFrame } = await render(
|
||||||
defineComponent(
|
defineComponent(
|
||||||
() => () =>
|
() => () =>
|
||||||
h("box", { backgroundColor: "red", width: 5, height: 2 }, [
|
h("tui-box", { backgroundColor: "red", width: 5, height: 2 }, [
|
||||||
h("box", { backgroundColor: [0, 0, 255], width: 5, height: 2 }, [h("text", null, "Hi")]),
|
h("tui-box", { backgroundColor: [0, 0, 255], width: 5, height: 2 }, [
|
||||||
|
h("tui-text", null, "Hi"),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
{ columns: 100 },
|
{ columns: 100 },
|
||||||
@@ -828,8 +830,8 @@ test("non-string host Text backgroundColor does not override inherited backgroun
|
|||||||
const { lastFrame } = await render(
|
const { lastFrame } = await render(
|
||||||
defineComponent(
|
defineComponent(
|
||||||
() => () =>
|
() => () =>
|
||||||
h("box", { backgroundColor: "red", alignSelf: "flex-start" }, [
|
h("tui-box", { backgroundColor: "red", alignSelf: "flex-start" }, [
|
||||||
h("text", { backgroundColor: [0, 0, 255] }, "Hi"),
|
h("tui-text", { backgroundColor: [0, 0, 255] }, "Hi"),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
{ columns: 100 },
|
{ columns: 100 },
|
||||||
|
|||||||
@@ -884,7 +884,7 @@ test("non-string host borderTopColor falls back to general borderColor", async (
|
|||||||
const { lastFrame } = await render(
|
const { lastFrame } = await render(
|
||||||
defineComponent(
|
defineComponent(
|
||||||
() => () =>
|
() => () =>
|
||||||
h("box", {
|
h("tui-box", {
|
||||||
borderStyle: "single",
|
borderStyle: "single",
|
||||||
borderColor: "red",
|
borderColor: "red",
|
||||||
borderTopColor: [0, 0, 255],
|
borderTopColor: [0, 0, 255],
|
||||||
@@ -1189,7 +1189,7 @@ test("non-string host borderTopBackgroundColor falls back to general borderBackg
|
|||||||
const { lastFrame } = await render(
|
const { lastFrame } = await render(
|
||||||
defineComponent(
|
defineComponent(
|
||||||
() => () =>
|
() => () =>
|
||||||
h("box", {
|
h("tui-box", {
|
||||||
borderStyle: "single",
|
borderStyle: "single",
|
||||||
borderBackgroundColor: "red",
|
borderBackgroundColor: "red",
|
||||||
borderTopBackgroundColor: [0, 0, 255],
|
borderTopBackgroundColor: [0, 0, 255],
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ function mountWithInput(kittyKeyboard: { mode: "auto" | "enabled" }) {
|
|||||||
useInput((input) => {
|
useInput((input) => {
|
||||||
inputs.push(input);
|
inputs.push(input);
|
||||||
});
|
});
|
||||||
return () => h("text", null, "x");
|
return () => h("tui-text", null, "x");
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function assertBoxValid(props: BoxProps): true {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: this component-level validation covers the public `<Box>`/`<Text>`
|
// NOTE: this component-level validation covers the public `<Box>`/`<Text>`
|
||||||
// API only. A raw host-op call (`h("box", { backgroundColor: "bold" })`)
|
// API only. A raw host-op call (`h("tui-box", { backgroundColor: "bold" })`)
|
||||||
// bypasses it; the paint layer keeps its silent degrade-to-bare-text there
|
// 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
|
// rather than throwing (a throw in the post-flush paint pass wedges Vue's
|
||||||
// scheduler). Same accepted limitation as the borderStyle fix (#124).
|
// scheduler). Same accepted limitation as the borderStyle fix (#124).
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { AppContextKey } from "../context.ts";
|
|||||||
import { boxProps } from "./box-props.ts";
|
import { boxProps } from "./box-props.ts";
|
||||||
import { assertBoxValid } from "./box-validate.ts";
|
import { assertBoxValid } from "./box-validate.ts";
|
||||||
|
|
||||||
// Internal name != "Box" to avoid vue-tsc self-recursion on the `<box>` host tag.
|
// Renders the `<tui-box>` host primitive. The host tag's `tui-` prefix keeps it out
|
||||||
// The public export name "Box" comes from index.ts.
|
// of the component namespace, so the component can take its real name "Box" with no
|
||||||
defineOptions({ name: "BoxImpl" });
|
// vue-tsc self-recursion on the tag. Public export wired in index.ts.
|
||||||
|
defineOptions({ name: "Box" });
|
||||||
const props = defineProps(boxProps);
|
const props = defineProps(boxProps);
|
||||||
defineSlots<{ default?: () => unknown }>();
|
defineSlots<{ default?: () => unknown }>();
|
||||||
const appCtx = inject(AppContextKey, null);
|
const appCtx = inject(AppContextKey, null);
|
||||||
@@ -21,8 +22,8 @@ const srHidden = computed(() => srEnabled.value && props.ariaHidden);
|
|||||||
colorizes). Under a screen reader with an ariaLabel, render the label text
|
colorizes). Under a screen reader with an ariaLabel, render the label text
|
||||||
instead of the slot. The root `v-if` makes this a fragment, but $el still
|
instead of the slot. The root `v-if` makes this a fragment, but $el still
|
||||||
resolves to the real `box` host node, so measureElement/useBoxMetrics work. -->
|
resolves to the real `box` host node, so measureElement/useBoxMetrics work. -->
|
||||||
<box v-if="!srHidden && assertBoxValid(props)" v-bind="props">
|
<tui-box v-if="!srHidden && assertBoxValid(props)" v-bind="props">
|
||||||
<text v-if="srEnabled && props.ariaLabel">{{ props.ariaLabel }}</text>
|
<tui-text v-if="srEnabled && props.ariaLabel">{{ props.ariaLabel }}</tui-text>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</box>
|
</tui-box>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ const content = computed(() => "\n".repeat(props.count));
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<virtual-text v-if="insideText">{{ content }}</virtual-text>
|
<tui-virtual-text v-if="insideText">{{ content }}</tui-virtual-text>
|
||||||
<text v-else>{{ content }}</text>
|
<tui-text v-else>{{ content }}</tui-text>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ defineOptions({ name: "Spacer" });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<box :flexGrow="1" :flexShrink="1" />
|
<tui-box :flexGrow="1" :flexShrink="1" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { computed, shallowRef, watch } from "vue";
|
import { computed, shallowRef, watch } from "vue";
|
||||||
import { staticProps } from "./static-props.ts";
|
import { staticProps } from "./static-props.ts";
|
||||||
|
|
||||||
// Internal name deliberately != "Static": vue-tsc 3.3.4 would bind the `<static>`
|
// Renders the `<tui-static>` host primitive. The host tag's `tui-` prefix keeps it out
|
||||||
// host tag below to this component (self-recursion) if they matched. Public export
|
// of the component namespace, so the component can take its real name "Static" with no
|
||||||
// name is "Static" (index.ts).
|
// vue-tsc self-recursion on the tag. Public export wired in index.ts.
|
||||||
defineOptions({ name: "StaticImpl" });
|
defineOptions({ name: "Static" });
|
||||||
const props = defineProps(staticProps);
|
const props = defineProps(staticProps);
|
||||||
defineSlots<{ default?: (slotProps: { item: unknown; index: number }) => unknown }>();
|
defineSlots<{ default?: (slotProps: { item: unknown; index: number }) => unknown }>();
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ const itemsToRender = computed(() => (props.items as unknown[]).slice(cursor.val
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<static v-bind="merged">
|
<tui-static v-bind="merged">
|
||||||
<template v-for="(item, i) in itemsToRender" :key="cursor + i">
|
<template v-for="(item, i) in itemsToRender" :key="cursor + i">
|
||||||
<slot :item="item" :index="cursor + i" />
|
<slot :item="item" :index="cursor + i" />
|
||||||
</template>
|
</template>
|
||||||
</static>
|
</tui-static>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { AppContextKey, TextContextKey } from "../context.ts";
|
|||||||
import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
|
import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
|
||||||
import { textProps } from "./text-props.ts";
|
import { textProps } from "./text-props.ts";
|
||||||
|
|
||||||
// Internal name != "Text" to avoid vue-tsc self-recursion on the `<text>` host tag.
|
// Renders the `<tui-text>` / `<tui-virtual-text>` host primitives. The `tui-` prefix
|
||||||
// The public export name "Text" comes from index.ts.
|
// keeps the host tags out of the component namespace, so the component can take its
|
||||||
defineOptions({ name: "TextImpl" });
|
// real name "Text" with no vue-tsc self-recursion. Public export wired in index.ts.
|
||||||
|
defineOptions({ name: "Text" });
|
||||||
const props = defineProps(textProps);
|
const props = defineProps(textProps);
|
||||||
const slots = defineSlots<{ default?: () => unknown }>();
|
const slots = defineSlots<{ default?: () => unknown }>();
|
||||||
|
|
||||||
@@ -39,15 +40,15 @@ function validate(): true {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="!srHidden && validate() && hasContent">
|
<template v-if="!srHidden && validate() && hasContent">
|
||||||
<virtual-text v-if="insideText" v-bind="props">
|
<tui-virtual-text v-if="insideText" v-bind="props">
|
||||||
<template v-if="srLabel">{{ srLabel }}</template>
|
<template v-if="srLabel">{{ srLabel }}</template>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</virtual-text>
|
</tui-virtual-text>
|
||||||
<!-- Match Ink's <Text> defaults: flexShrink=1 so text nodes shrink when they
|
<!-- Match Ink's <Text> defaults: flexShrink=1 so text nodes shrink when they
|
||||||
overflow their container (e.g. in no-wrap flex rows). -->
|
overflow their container (e.g. in no-wrap flex rows). -->
|
||||||
<text v-else v-bind="{ ...props, flexShrink: 1 }">
|
<tui-text v-else v-bind="{ ...props, flexShrink: 1 }">
|
||||||
<template v-if="srLabel">{{ srLabel }}</template>
|
<template v-if="srLabel">{{ srLabel }}</template>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</text>
|
</tui-text>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ const TransformImpl = defineComponent({
|
|||||||
// When screen reader is enabled and accessibilityLabel is set,
|
// When screen reader is enabled and accessibilityLabel is set,
|
||||||
// render the label text instead of children.
|
// render the label text instead of children.
|
||||||
if (isScreenReaderEnabled && props.accessibilityLabel) {
|
if (isScreenReaderEnabled && props.accessibilityLabel) {
|
||||||
return h("transform", { transform: props.transform }, props.accessibilityLabel);
|
return h("tui-transform", { transform: props.transform }, props.accessibilityLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return h("transform", { transform: props.transform }, children);
|
return h("tui-transform", { transform: props.transform }, children);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ type ContainerWithChildren = TuiRoot | TuiBox | TuiText | TuiStatic | TuiTransfo
|
|||||||
function hasYoga(node: TuiNode): node is YogaCarrier {
|
function hasYoga(node: TuiNode): node is YogaCarrier {
|
||||||
return (
|
return (
|
||||||
node.type === "root" ||
|
node.type === "root" ||
|
||||||
node.type === "box" ||
|
node.type === "tui-box" ||
|
||||||
node.type === "text" ||
|
node.type === "tui-text" ||
|
||||||
node.type === "static" ||
|
node.type === "tui-static" ||
|
||||||
node.type === "transform"
|
node.type === "tui-transform"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasChildren(node: TuiNode): node is ContainerWithChildren {
|
function hasChildren(node: TuiNode): node is ContainerWithChildren {
|
||||||
return (
|
return (
|
||||||
node.type === "root" ||
|
node.type === "root" ||
|
||||||
node.type === "box" ||
|
node.type === "tui-box" ||
|
||||||
node.type === "text" ||
|
node.type === "tui-text" ||
|
||||||
node.type === "static" ||
|
node.type === "tui-static" ||
|
||||||
node.type === "transform"
|
node.type === "tui-transform"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ function applyZeroContentGuards(node: TuiNode, guarded: Map<YogaNode, number>):
|
|||||||
if (hasYoga(node) && node.yoga.getDisplay() === Yoga.DISPLAY_NONE) return false;
|
if (hasYoga(node) && node.yoga.getDisplay() === Yoga.DISPLAY_NONE) return false;
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
if (node.type === "box") {
|
if (node.type === "tui-box") {
|
||||||
const inner = getBoxInnerSize(node);
|
const inner = getBoxInnerSize(node);
|
||||||
if (inner.width === 0 || inner.height === 0) {
|
if (inner.width === 0 || inner.height === 0) {
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ function findRoot(node: TuiNode): TuiRoot | null {
|
|||||||
function isInsideTextOrTransformContext(node: TuiContainer): boolean {
|
function isInsideTextOrTransformContext(node: TuiContainer): boolean {
|
||||||
let current: TuiContainer | null = node;
|
let current: TuiContainer | null = node;
|
||||||
while (current) {
|
while (current) {
|
||||||
if (current.type === "text" || current.type === "virtual-text" || current.type === "transform")
|
if (
|
||||||
|
current.type === "tui-text" ||
|
||||||
|
current.type === "tui-virtual-text" ||
|
||||||
|
current.type === "tui-transform"
|
||||||
|
)
|
||||||
return true;
|
return true;
|
||||||
current = current.parent;
|
current = current.parent;
|
||||||
}
|
}
|
||||||
@@ -121,8 +125,8 @@ function isInsideTextOrTransformContext(node: TuiContainer): boolean {
|
|||||||
function findMeasureOwner(start: TuiNode | null): TuiNode | null {
|
function findMeasureOwner(start: TuiNode | null): TuiNode | null {
|
||||||
let p: TuiNode | null = start;
|
let p: TuiNode | null = start;
|
||||||
while (p) {
|
while (p) {
|
||||||
if (p.type === "text") return p;
|
if (p.type === "tui-text") return p;
|
||||||
if (p.type === "transform") {
|
if (p.type === "tui-transform") {
|
||||||
const inlineInTextContext =
|
const inlineInTextContext =
|
||||||
p.parent != null &&
|
p.parent != null &&
|
||||||
isContainer(p.parent) &&
|
isContainer(p.parent) &&
|
||||||
@@ -144,12 +148,16 @@ function findMeasureOwner(start: TuiNode | null): TuiNode | null {
|
|||||||
* directly, no measure func to invalidate).
|
* directly, no measure func to invalidate).
|
||||||
*/
|
*/
|
||||||
function dirtyTextMeasureOwner(parent: TuiNode): void {
|
function dirtyTextMeasureOwner(parent: TuiNode): void {
|
||||||
if (parent.type !== "text" && parent.type !== "virtual-text" && parent.type !== "transform") {
|
if (
|
||||||
|
parent.type !== "tui-text" &&
|
||||||
|
parent.type !== "tui-virtual-text" &&
|
||||||
|
parent.type !== "tui-transform"
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const owner = findMeasureOwner(parent);
|
const owner = findMeasureOwner(parent);
|
||||||
if (owner?.type === "transform") markTransformDirty(owner);
|
if (owner?.type === "tui-transform") markTransformDirty(owner);
|
||||||
else if (owner?.type === "text") markTextDirty(owner);
|
else if (owner?.type === "tui-text") markTextDirty(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNode, TuiNode> {
|
export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNode, TuiNode> {
|
||||||
@@ -157,25 +165,25 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
|
|
||||||
function createElement(type: string): TuiNode {
|
function createElement(type: string): TuiNode {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "box": {
|
case "tui-box": {
|
||||||
const n = createBox();
|
const n = createBox();
|
||||||
attachYoga(n);
|
attachYoga(n);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "tui-text": {
|
||||||
const n = createText();
|
const n = createText();
|
||||||
attachYoga(n);
|
attachYoga(n);
|
||||||
bindTextMeasure(n);
|
bindTextMeasure(n);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
case "virtual-text":
|
case "tui-virtual-text":
|
||||||
return createVirtualText();
|
return createVirtualText();
|
||||||
case "static": {
|
case "tui-static": {
|
||||||
const n = createStatic();
|
const n = createStatic();
|
||||||
attachYoga(n);
|
attachYoga(n);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
case "transform": {
|
case "tui-transform": {
|
||||||
const n = createTransform((line) => line); // overwritten by patchProp
|
const n = createTransform((line) => line); // overwritten by patchProp
|
||||||
attachYoga(n);
|
attachYoga(n);
|
||||||
return n;
|
return n;
|
||||||
@@ -208,9 +216,9 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
// transform (inside a <Text> or another <Transform>) does NOT, so we must keep
|
// transform (inside a <Text> or another <Transform>) does NOT, so we must keep
|
||||||
// climbing to the enclosing <Text>/standalone-transform measure owner. (G58)
|
// climbing to the enclosing <Text>/standalone-transform measure owner. (G58)
|
||||||
const owner = findMeasureOwner(node.parent as TuiNode | null);
|
const owner = findMeasureOwner(node.parent as TuiNode | null);
|
||||||
if (owner?.type === "text") {
|
if (owner?.type === "tui-text") {
|
||||||
markTextDirty(owner);
|
markTextDirty(owner);
|
||||||
} else if (owner?.type === "transform") {
|
} else if (owner?.type === "tui-transform") {
|
||||||
markTransformDirty(owner);
|
markTransformDirty(owner);
|
||||||
}
|
}
|
||||||
onCommit();
|
onCommit();
|
||||||
@@ -221,7 +229,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
// Remove existing children first (copy since remove mutates the array).
|
// Remove existing children first (copy since remove mutates the array).
|
||||||
for (const child of Array.from(el.children)) remove(child);
|
for (const child of Array.from(el.children)) remove(child);
|
||||||
insert(createTextLeaf(text), el, null);
|
insert(createTextLeaf(text), el, null);
|
||||||
if (el.type === "text") {
|
if (el.type === "tui-text") {
|
||||||
markTextDirty(el);
|
markTextDirty(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +247,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
// `hostContext.isInsideText && originalType === 'ink-box'`). A standalone
|
// `hostContext.isInsideText && originalType === 'ink-box'`). A standalone
|
||||||
// <Transform> is a text context here (G58), so we use the transform-aware
|
// <Transform> is a text context here (G58), so we use the transform-aware
|
||||||
// context check to mirror Ink exactly. (G58 should-fix)
|
// context check to mirror Ink exactly. (G58 should-fix)
|
||||||
if (child.type === "box" && isInsideTextOrTransformContext(parentC)) {
|
if (child.type === "tui-box" && isInsideTextOrTransformContext(parentC)) {
|
||||||
throw new Error("<Box> can’t be nested inside <Text> component");
|
throw new Error("<Box> can’t be nested inside <Text> component");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +256,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
if (
|
if (
|
||||||
child.type === "text-leaf" &&
|
child.type === "text-leaf" &&
|
||||||
child.value !== "" &&
|
child.value !== "" &&
|
||||||
(parentC.type === "box" || parentC.type === "root" || parentC.type === "static") &&
|
(parentC.type === "tui-box" || parentC.type === "root" || parentC.type === "tui-static") &&
|
||||||
!isInsideTextOrTransformContext(parentC)
|
!isInsideTextOrTransformContext(parentC)
|
||||||
) {
|
) {
|
||||||
throw new Error(`Text string "${child.value}" must be rendered inside <Text> component`);
|
throw new Error(`Text string "${child.value}" must be rendered inside <Text> component`);
|
||||||
@@ -288,7 +296,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
dirtyTextMeasureOwner(parentC);
|
dirtyTextMeasureOwner(parentC);
|
||||||
|
|
||||||
// Track static node identity on the root (mirrors Ink's reconciler).
|
// Track static node identity on the root (mirrors Ink's reconciler).
|
||||||
if (child.type === "static") {
|
if (child.type === "tui-static") {
|
||||||
const root = findRoot(child);
|
const root = findRoot(child);
|
||||||
if (root) root.staticNode = child;
|
if (root) root.staticNode = child;
|
||||||
}
|
}
|
||||||
@@ -303,7 +311,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
// Track static node removal: clear root.staticNode only if it still
|
// Track static node removal: clear root.staticNode only if it still
|
||||||
// points at this node. On key-driven remounts, insert() already
|
// points at this node. On key-driven remounts, insert() already
|
||||||
// registered the new instance before the old one is removed.
|
// registered the new instance before the old one is removed.
|
||||||
if (child.type === "static") {
|
if (child.type === "tui-static") {
|
||||||
const root = findRoot(child);
|
const root = findRoot(child);
|
||||||
if (root && root.staticNode === child) {
|
if (root && root.staticNode === child) {
|
||||||
root.staticNode = undefined;
|
root.staticNode = undefined;
|
||||||
@@ -336,10 +344,10 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
node.type === "box" ||
|
node.type === "tui-box" ||
|
||||||
node.type === "text" ||
|
node.type === "tui-text" ||
|
||||||
node.type === "static" ||
|
node.type === "tui-static" ||
|
||||||
node.type === "transform"
|
node.type === "tui-transform"
|
||||||
) {
|
) {
|
||||||
detachYoga(node);
|
detachYoga(node);
|
||||||
}
|
}
|
||||||
@@ -358,21 +366,26 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function patchProp(el: TuiNode, key: string, prev: unknown, next: unknown): void {
|
function patchProp(el: TuiNode, key: string, prev: unknown, next: unknown): void {
|
||||||
if (el.type === "transform") {
|
if (el.type === "tui-transform") {
|
||||||
if (key === "transform" && typeof next === "function") {
|
if (key === "transform" && typeof next === "function") {
|
||||||
el.transform = next as (line: string, idx: number) => string;
|
el.transform = next as (line: string, idx: number) => string;
|
||||||
}
|
}
|
||||||
onCommit();
|
onCommit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (el.type === "static" && key === "internal_onWritten") {
|
if (el.type === "tui-static" && key === "internal_onWritten") {
|
||||||
// Callback the renderer invokes post-commit to advance the <Static>
|
// Callback the renderer invokes post-commit to advance the <Static>
|
||||||
// component's cursor so written items unmount. Not styling/layout.
|
// component's cursor so written items unmount. Not styling/layout.
|
||||||
el.onWritten = typeof next === "function" ? (next as () => void) : undefined;
|
el.onWritten = typeof next === "function" ? (next as () => void) : undefined;
|
||||||
onCommit();
|
onCommit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (el.type === "box" || el.type === "text" || el.type === "static" || el.type === "root") {
|
if (
|
||||||
|
el.type === "tui-box" ||
|
||||||
|
el.type === "tui-text" ||
|
||||||
|
el.type === "tui-static" ||
|
||||||
|
el.type === "root"
|
||||||
|
) {
|
||||||
if (isYogaProp(key)) {
|
if (isYogaProp(key)) {
|
||||||
applyYogaProp(el, key, next, prev);
|
applyYogaProp(el, key, next, prev);
|
||||||
// Some yoga props also need to be stored in el.props for the paint pass.
|
// Some yoga props also need to be stored in el.props for the paint pass.
|
||||||
@@ -402,12 +415,12 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
} else if (STYLE_PROPS.has(key)) {
|
} else if (STYLE_PROPS.has(key)) {
|
||||||
(el as { props: Record<string, unknown> }).props[key] = next;
|
(el as { props: Record<string, unknown> }).props[key] = next;
|
||||||
} else if (key === "aria-role" || key === "ariaRole") {
|
} else if (key === "aria-role" || key === "ariaRole") {
|
||||||
if (el.type === "box") {
|
if (el.type === "tui-box") {
|
||||||
el.internal_accessibility ??= {};
|
el.internal_accessibility ??= {};
|
||||||
el.internal_accessibility.role = next as string;
|
el.internal_accessibility.role = next as string;
|
||||||
}
|
}
|
||||||
} else if (key === "aria-state" || key === "ariaState") {
|
} else if (key === "aria-state" || key === "ariaState") {
|
||||||
if (el.type === "box") {
|
if (el.type === "tui-box") {
|
||||||
el.internal_accessibility ??= {};
|
el.internal_accessibility ??= {};
|
||||||
el.internal_accessibility.state = next as Record<string, boolean>;
|
el.internal_accessibility.state = next as Record<string, boolean>;
|
||||||
}
|
}
|
||||||
@@ -429,7 +442,7 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
|||||||
onCommit();
|
onCommit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (el.type === "virtual-text" && STYLE_PROPS.has(key)) {
|
if (el.type === "tui-virtual-text" && STYLE_PROPS.has(key)) {
|
||||||
(el.props as Record<string, unknown>)[key] = next;
|
(el.props as Record<string, unknown>)[key] = next;
|
||||||
onCommit();
|
onCommit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { buildNodeOps } from "./node-ops.ts";
|
|||||||
|
|
||||||
test("createBox returns shape with empty children + paintDirty true", () => {
|
test("createBox returns shape with empty children + paintDirty true", () => {
|
||||||
const box = createBox();
|
const box = createBox();
|
||||||
expect(box.type).toBe("box");
|
expect(box.type).toBe("tui-box");
|
||||||
expect(box.children).toEqual([]);
|
expect(box.children).toEqual([]);
|
||||||
expect(box.paintDirty).toBe(true);
|
expect(box.paintDirty).toBe(true);
|
||||||
expect(box.parent).toBe(null);
|
expect(box.parent).toBe(null);
|
||||||
@@ -36,7 +36,7 @@ test("setText coerces a non-string value to a string (Ink setTextNodeValue)", ()
|
|||||||
test("createTransform stores its transform function", () => {
|
test("createTransform stores its transform function", () => {
|
||||||
const fn = (line: string) => line.toUpperCase();
|
const fn = (line: string) => line.toUpperCase();
|
||||||
const node = createTransform(fn);
|
const node = createTransform(fn);
|
||||||
expect(node.type).toBe("transform");
|
expect(node.type).toBe("tui-transform");
|
||||||
expect(node.transform).toBe(fn);
|
expect(node.transform).toBe(fn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export interface TuiRoot extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TuiBox extends NodeBase {
|
export interface TuiBox extends NodeBase {
|
||||||
type: "box";
|
type: "tui-box";
|
||||||
children: TuiNode[];
|
children: TuiNode[];
|
||||||
yoga: YogaNodeRef;
|
yoga: YogaNodeRef;
|
||||||
props: BoxProps;
|
props: BoxProps;
|
||||||
@@ -52,7 +52,7 @@ export interface TuiBox extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TuiText extends NodeBase {
|
export interface TuiText extends NodeBase {
|
||||||
type: "text";
|
type: "tui-text";
|
||||||
children: TuiInlineNode[];
|
children: TuiInlineNode[];
|
||||||
yoga: YogaNodeRef;
|
yoga: YogaNodeRef;
|
||||||
props: TextProps;
|
props: TextProps;
|
||||||
@@ -60,7 +60,7 @@ export interface TuiText extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TuiVirtualText extends NodeBase {
|
export interface TuiVirtualText extends NodeBase {
|
||||||
type: "virtual-text";
|
type: "tui-virtual-text";
|
||||||
// A <Newline>/<Text> directly inside a standalone <Transform> renders inline,
|
// A <Newline>/<Text> directly inside a standalone <Transform> renders inline,
|
||||||
// so a virtual-text can also be parented by a transform (G58).
|
// so a virtual-text can also be parented by a transform (G58).
|
||||||
parent: TuiText | TuiVirtualText | TuiTransform | null;
|
parent: TuiText | TuiVirtualText | TuiTransform | null;
|
||||||
@@ -83,7 +83,7 @@ export interface TuiComment extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TuiStatic extends NodeBase {
|
export interface TuiStatic extends NodeBase {
|
||||||
type: "static";
|
type: "tui-static";
|
||||||
children: TuiNode[];
|
children: TuiNode[];
|
||||||
yoga: YogaNodeRef;
|
yoga: YogaNodeRef;
|
||||||
props: BoxProps;
|
props: BoxProps;
|
||||||
@@ -109,7 +109,7 @@ export interface TuiStatic extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TuiTransform extends NodeBase {
|
export interface TuiTransform extends NodeBase {
|
||||||
type: "transform";
|
type: "tui-transform";
|
||||||
children: TuiNode[];
|
children: TuiNode[];
|
||||||
yoga: YogaNodeRef;
|
yoga: YogaNodeRef;
|
||||||
transform: (line: string, lineIndex: number) => string;
|
transform: (line: string, lineIndex: number) => string;
|
||||||
@@ -154,7 +154,7 @@ export function emitLayoutListeners(root: TuiRoot): void {
|
|||||||
|
|
||||||
export function createBox(): TuiBox {
|
export function createBox(): TuiBox {
|
||||||
return {
|
return {
|
||||||
type: "box",
|
type: "tui-box",
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
yoga: UNATTACHED_YOGA,
|
yoga: UNATTACHED_YOGA,
|
||||||
@@ -165,7 +165,7 @@ export function createBox(): TuiBox {
|
|||||||
|
|
||||||
export function createText(): TuiText {
|
export function createText(): TuiText {
|
||||||
return {
|
return {
|
||||||
type: "text",
|
type: "tui-text",
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
yoga: UNATTACHED_YOGA,
|
yoga: UNATTACHED_YOGA,
|
||||||
@@ -175,7 +175,7 @@ export function createText(): TuiText {
|
|||||||
|
|
||||||
export function createVirtualText(): TuiVirtualText {
|
export function createVirtualText(): TuiVirtualText {
|
||||||
return {
|
return {
|
||||||
type: "virtual-text",
|
type: "tui-virtual-text",
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
props: {},
|
props: {},
|
||||||
@@ -197,7 +197,7 @@ export function createTextLeaf(value: string): TuiTextLeaf {
|
|||||||
|
|
||||||
export function createStatic(): TuiStatic {
|
export function createStatic(): TuiStatic {
|
||||||
return {
|
return {
|
||||||
type: "static",
|
type: "tui-static",
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
yoga: UNATTACHED_YOGA,
|
yoga: UNATTACHED_YOGA,
|
||||||
@@ -208,7 +208,7 @@ export function createStatic(): TuiStatic {
|
|||||||
|
|
||||||
export function createTransform(fn: (line: string, lineIndex: number) => string): TuiTransform {
|
export function createTransform(fn: (line: string, lineIndex: number) => string): TuiTransform {
|
||||||
return {
|
return {
|
||||||
type: "transform",
|
type: "tui-transform",
|
||||||
parent: null,
|
parent: null,
|
||||||
children: [],
|
children: [],
|
||||||
yoga: UNATTACHED_YOGA,
|
yoga: UNATTACHED_YOGA,
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ function squashTransformChild(child: TuiNode, index: number): string {
|
|||||||
if (child.type === "text-leaf") {
|
if (child.type === "text-leaf") {
|
||||||
return child.value;
|
return child.value;
|
||||||
}
|
}
|
||||||
if (child.type === "virtual-text" || child.type === "text") {
|
if (child.type === "tui-virtual-text" || child.type === "tui-text") {
|
||||||
return flattenLeaves(child);
|
return flattenLeaves(child);
|
||||||
}
|
}
|
||||||
if (child.type === "transform") {
|
if (child.type === "tui-transform") {
|
||||||
let innerText = "";
|
let innerText = "";
|
||||||
// Recursive twin of the G52 fix in flattenLeaves: a grandchild's positional
|
// Recursive twin of the G52 fix in flattenLeaves: a grandchild's positional
|
||||||
// index must skip Vue comment nodes (null/v-if/false renders) so a `{null}`
|
// index must skip Vue comment nodes (null/v-if/false renders) so a `{null}`
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ export const yogaNodeTracker = {
|
|||||||
function hasYoga(node: TuiNode): node is YogaCarrier {
|
function hasYoga(node: TuiNode): node is YogaCarrier {
|
||||||
return (
|
return (
|
||||||
node.type === "root" ||
|
node.type === "root" ||
|
||||||
node.type === "box" ||
|
node.type === "tui-box" ||
|
||||||
node.type === "text" ||
|
node.type === "tui-text" ||
|
||||||
node.type === "static" ||
|
node.type === "tui-static" ||
|
||||||
node.type === "transform"
|
node.type === "tui-transform"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function attachYoga(node: YogaCarrier): void {
|
|||||||
node.yoga = createYogaNode();
|
node.yoga = createYogaNode();
|
||||||
// Static nodes are painted via a separate channel (paintIsolated), so they
|
// Static nodes are painted via a separate channel (paintIsolated), so they
|
||||||
// must not occupy space in the dynamic frame's yoga layout.
|
// must not occupy space in the dynamic frame's yoga layout.
|
||||||
if (node.type === "static") {
|
if (node.type === "tui-static") {
|
||||||
(node.yoga as YogaNode).setDisplay(Yoga.DISPLAY_NONE);
|
(node.yoga as YogaNode).setDisplay(Yoga.DISPLAY_NONE);
|
||||||
}
|
}
|
||||||
// Box nodes match Ink's defaults: row direction, shrinkable, no wrap.
|
// Box nodes match Ink's defaults: row direction, shrinkable, no wrap.
|
||||||
@@ -71,7 +71,7 @@ export function attachYoga(node: YogaCarrier): void {
|
|||||||
// are passed through Vue's reactive system (which may include undefined
|
// are passed through Vue's reactive system (which may include undefined
|
||||||
// overrides or border defaults). User-provided props override these via
|
// overrides or border defaults). User-provided props override these via
|
||||||
// patchProp which runs after attachYoga.
|
// patchProp which runs after attachYoga.
|
||||||
if (node.type === "box") {
|
if (node.type === "tui-box") {
|
||||||
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
||||||
(node.yoga as YogaNode).setFlexShrink(1);
|
(node.yoga as YogaNode).setFlexShrink(1);
|
||||||
(node.yoga as YogaNode).setFlexWrap(Yoga.WRAP_NO_WRAP);
|
(node.yoga as YogaNode).setFlexWrap(Yoga.WRAP_NO_WRAP);
|
||||||
@@ -80,7 +80,7 @@ export function attachYoga(node: YogaCarrier): void {
|
|||||||
// Text nodes match Ink's <ink-text> defaults: row direction, shrinkable.
|
// Text nodes match Ink's <ink-text> defaults: row direction, shrinkable.
|
||||||
// Although text nodes rarely have yoga-carrying children, this ensures
|
// Although text nodes rarely have yoga-carrying children, this ensures
|
||||||
// consistent layout behavior matching Ink.
|
// consistent layout behavior matching Ink.
|
||||||
if (node.type === "text") {
|
if (node.type === "tui-text") {
|
||||||
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
||||||
(node.yoga as YogaNode).setFlexShrink(1);
|
(node.yoga as YogaNode).setFlexShrink(1);
|
||||||
(node.yoga as YogaNode).setFlexGrow(0);
|
(node.yoga as YogaNode).setFlexGrow(0);
|
||||||
@@ -88,7 +88,7 @@ export function attachYoga(node: YogaCarrier): void {
|
|||||||
// Transform nodes match Ink's Transform which renders as ink-text:
|
// Transform nodes match Ink's Transform which renders as ink-text:
|
||||||
// flexShrink=1, flexDirection='row'. This makes transform a yoga carrier
|
// flexShrink=1, flexDirection='row'. This makes transform a yoga carrier
|
||||||
// so it participates in layout (multi-line text gets proper height).
|
// so it participates in layout (multi-line text gets proper height).
|
||||||
if (node.type === "transform") {
|
if (node.type === "tui-transform") {
|
||||||
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
(node.yoga as YogaNode).setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
||||||
(node.yoga as YogaNode).setFlexShrink(1);
|
(node.yoga as YogaNode).setFlexShrink(1);
|
||||||
(node.yoga as YogaNode).setFlexGrow(0);
|
(node.yoga as YogaNode).setFlexGrow(0);
|
||||||
@@ -120,13 +120,15 @@ function yogaIndexFor(parent: TuiContainer, child: TuiNode): number {
|
|||||||
// ink-text), so a transform child of it is inline and excluded from yoga —
|
// ink-text), so a transform child of it is inline and excluded from yoga —
|
||||||
// same as for a text/virtual-text parent. (G58 MF2)
|
// same as for a text/virtual-text parent. (G58 MF2)
|
||||||
const isTextParent =
|
const isTextParent =
|
||||||
parent.type === "text" || parent.type === "virtual-text" || parent.type === "transform";
|
parent.type === "tui-text" ||
|
||||||
|
parent.type === "tui-virtual-text" ||
|
||||||
|
parent.type === "tui-transform";
|
||||||
let yIdx = 0;
|
let yIdx = 0;
|
||||||
for (const sibling of parent.children) {
|
for (const sibling of parent.children) {
|
||||||
if (sibling === child) return yIdx;
|
if (sibling === child) return yIdx;
|
||||||
if (hasYoga(sibling)) {
|
if (hasYoga(sibling)) {
|
||||||
// Transform nodes inside text/transform parents are not in the yoga tree.
|
// Transform nodes inside text/transform parents are not in the yoga tree.
|
||||||
if (isTextParent && sibling.type === "transform") continue;
|
if (isTextParent && sibling.type === "tui-transform") continue;
|
||||||
yIdx++;
|
yIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,14 +147,17 @@ export function insertYogaChild(parent: TuiContainer, child: TuiNode, _domIndex:
|
|||||||
// because a transform-in-transform inside a <Text> was already excluded as a
|
// because a transform-in-transform inside a <Text> was already excluded as a
|
||||||
// child of an inline transform here.)
|
// child of an inline transform here.)
|
||||||
// (VirtualText parents are already excluded by the hasYoga check above.)
|
// (VirtualText parents are already excluded by the hasYoga check above.)
|
||||||
if (child.type === "transform" && (parent.type === "text" || parent.type === "transform")) {
|
if (
|
||||||
|
child.type === "tui-transform" &&
|
||||||
|
(parent.type === "tui-text" || parent.type === "tui-transform")
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A transform with a yoga-carrying child (e.g. <Transform><Text>…) lays out
|
// A transform with a yoga-carrying child (e.g. <Transform><Text>…) lays out
|
||||||
// from that child, not from a measure func. Yoga forbids a node having both a
|
// from that child, not from a measure func. Yoga forbids a node having both a
|
||||||
// measure func and children, so clear the standalone-transform measure func
|
// measure func and children, so clear the standalone-transform measure func
|
||||||
// before inserting the child. (G58)
|
// before inserting the child. (G58)
|
||||||
if (parent.type === "transform") {
|
if (parent.type === "tui-transform") {
|
||||||
(parent.yoga as YogaNode).unsetMeasureFunc();
|
(parent.yoga as YogaNode).unsetMeasureFunc();
|
||||||
}
|
}
|
||||||
const yIdx = yogaIndexFor(parent, child);
|
const yIdx = yogaIndexFor(parent, child);
|
||||||
@@ -163,14 +168,17 @@ export function removeYogaChild(parent: TuiContainer, child: TuiNode): void {
|
|||||||
if (!hasYoga(parent) || !hasYoga(child)) return;
|
if (!hasYoga(parent) || !hasYoga(child)) return;
|
||||||
// Transform nodes inside a text/transform parent were never inserted into yoga
|
// Transform nodes inside a text/transform parent were never inserted into yoga
|
||||||
// (mirror of insertYogaChild's inline-transform skip). (G58 MF2)
|
// (mirror of insertYogaChild's inline-transform skip). (G58 MF2)
|
||||||
if (child.type === "transform" && (parent.type === "text" || parent.type === "transform")) {
|
if (
|
||||||
|
child.type === "tui-transform" &&
|
||||||
|
(parent.type === "tui-text" || parent.type === "tui-transform")
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(parent.yoga as YogaNode).removeChild(child.yoga as YogaNode);
|
(parent.yoga as YogaNode).removeChild(child.yoga as YogaNode);
|
||||||
// If removing the last yoga child from a transform, restore the inline-text
|
// If removing the last yoga child from a transform, restore the inline-text
|
||||||
// measure func so the transform can still size its direct text-leaf children
|
// measure func so the transform can still size its direct text-leaf children
|
||||||
// (it has become a standalone inline-text transform again). (G58)
|
// (it has become a standalone inline-text transform again). (G58)
|
||||||
if (parent.type === "transform" && (parent.yoga as YogaNode).getChildCount() === 0) {
|
if (parent.type === "tui-transform" && (parent.yoga as YogaNode).getChildCount() === 0) {
|
||||||
bindTransformMeasure(parent as TuiTransform);
|
bindTransformMeasure(parent as TuiTransform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ function squashInlineChildren(children: readonly TuiNode[], inheritedBg: unknown
|
|||||||
// with no yoga-carrying children) as if it were an inline text node — its
|
// with no yoga-carrying children) as if it were an inline text node — its
|
||||||
// direct text-leaf / virtual-text / <Newline> children are squashed into a
|
// direct text-leaf / virtual-text / <Newline> children are squashed into a
|
||||||
// string. The transform's OWN fn is intentionally NOT applied here: it is pushed
|
// string. The transform's OWN fn is intentionally NOT applied here: it is pushed
|
||||||
// as a line-transformer onto the Output write (paintNode "transform" case), so
|
// as a line-transformer onto the Output write (paintNode "tui-transform" case), so
|
||||||
// it applies per LINE at paint time, matching Ink where internal_transform runs
|
// it applies per LINE at paint time, matching Ink where internal_transform runs
|
||||||
// in the Output, never in squashTextNodes for the node it lives on. (G58)
|
// in the Output, never in squashTextNodes for the node it lives on. (G58)
|
||||||
function renderTransformAsText(node: TuiTransform, inheritedBg?: unknown): string {
|
function renderTransformAsText(node: TuiTransform, inheritedBg?: unknown): string {
|
||||||
@@ -435,10 +435,10 @@ function squashTransformChild(child: TuiNode, index: number, inheritedBg: unknow
|
|||||||
if (child.type === "text-leaf") {
|
if (child.type === "text-leaf") {
|
||||||
return child.value;
|
return child.value;
|
||||||
}
|
}
|
||||||
if (child.type === "virtual-text" || child.type === "text") {
|
if (child.type === "tui-virtual-text" || child.type === "tui-text") {
|
||||||
return renderTextWithInlineStyles(child, inheritedBg);
|
return renderTextWithInlineStyles(child, inheritedBg);
|
||||||
}
|
}
|
||||||
if (child.type === "transform") {
|
if (child.type === "tui-transform") {
|
||||||
let innerText = "";
|
let innerText = "";
|
||||||
// Recursive twin of the G52 fix in renderTextWithInlineStyles: a grandchild's
|
// Recursive twin of the G52 fix in renderTextWithInlineStyles: a grandchild's
|
||||||
// positional index must skip Vue comment nodes (null/v-if/false renders),
|
// positional index must skip Vue comment nodes (null/v-if/false renders),
|
||||||
@@ -631,7 +631,7 @@ function paintNode(
|
|||||||
for (const child of node.children) paintNode(child, output, x0, y0, transformers);
|
for (const child of node.children) paintNode(child, output, x0, y0, transformers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "box": {
|
case "tui-box": {
|
||||||
const layout = node.yoga.getComputedLayout();
|
const layout = node.yoga.getComputedLayout();
|
||||||
const x = x0 + layout.left;
|
const x = x0 + layout.left;
|
||||||
const y = y0 + layout.top;
|
const y = y0 + layout.top;
|
||||||
@@ -709,7 +709,7 @@ function paintNode(
|
|||||||
if (clipped) output.unclip();
|
if (clipped) output.unclip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "tui-text": {
|
||||||
const layout = node.yoga.getComputedLayout();
|
const layout = node.yoga.getComputedLayout();
|
||||||
// Thread the INHERITED Box bg (NOT a pre-computed effective bg) into the
|
// Thread the INHERITED Box bg (NOT a pre-computed effective bg) into the
|
||||||
// squash. The Text's own backgroundColor — including an explicit "" opt-out —
|
// squash. The Text's own backgroundColor — including an explicit "" opt-out —
|
||||||
@@ -753,12 +753,12 @@ function paintNode(
|
|||||||
output.write(x0 + layout.left, y0 + layout.top, wrapped, transformers);
|
output.write(x0 + layout.left, y0 + layout.top, wrapped, transformers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "static": {
|
case "tui-static": {
|
||||||
// Static is rendered through the static channel (written before frame), so
|
// Static is rendered through the static channel (written before frame), so
|
||||||
// it does not contribute to the dynamic frame paint.
|
// it does not contribute to the dynamic frame paint.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "transform": {
|
case "tui-transform": {
|
||||||
const layout = node.yoga.getComputedLayout();
|
const layout = node.yoga.getComputedLayout();
|
||||||
const x = x0 + layout.left;
|
const x = x0 + layout.left;
|
||||||
const y = y0 + layout.top;
|
const y = y0 + layout.top;
|
||||||
@@ -787,7 +787,7 @@ function paintNode(
|
|||||||
for (const child of node.children) paintNode(child, output, x, y, next, inheritedBg);
|
for (const child of node.children) paintNode(child, output, x, y, next, inheritedBg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "virtual-text":
|
case "tui-virtual-text":
|
||||||
case "text-leaf":
|
case "text-leaf":
|
||||||
case "comment":
|
case "comment":
|
||||||
// virtual-text and text-leaf are handled inside renderTextWithInlineStyles.
|
// virtual-text and text-leaf are handled inside renderTextWithInlineStyles.
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ function squashChildSR(child: TuiNode, index: number): string {
|
|||||||
if (child.type === "text-leaf") {
|
if (child.type === "text-leaf") {
|
||||||
return child.value;
|
return child.value;
|
||||||
}
|
}
|
||||||
if (child.type === "virtual-text" || child.type === "text") {
|
if (child.type === "tui-virtual-text" || child.type === "tui-text") {
|
||||||
return squashTextContent(child);
|
return squashTextContent(child);
|
||||||
}
|
}
|
||||||
if (child.type === "transform") {
|
if (child.type === "tui-transform") {
|
||||||
let innerText = "";
|
let innerText = "";
|
||||||
// Recurse into the transform's children (each may itself be a transform,
|
// Recurse into the transform's children (each may itself be a transform,
|
||||||
// recursed to any depth), skipping Vue comment nodes and empty text-leaves
|
// recursed to any depth), skipping Vue comment nodes and empty text-leaves
|
||||||
@@ -119,16 +119,16 @@ export interface ScreenReaderOptions {
|
|||||||
*/
|
*/
|
||||||
export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOptions = {}): string {
|
export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOptions = {}): string {
|
||||||
// Skip static elements if requested
|
// Skip static elements if requested
|
||||||
if (options.skipStaticElements && node.type === "static") {
|
if (options.skipStaticElements && node.type === "tui-static") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// If display: none, return empty
|
// If display: none, return empty
|
||||||
if (
|
if (
|
||||||
(node.type === "box" ||
|
(node.type === "tui-box" ||
|
||||||
node.type === "text" ||
|
node.type === "tui-text" ||
|
||||||
node.type === "root" ||
|
node.type === "root" ||
|
||||||
node.type === "transform") &&
|
node.type === "tui-transform") &&
|
||||||
node.yoga.getDisplay() === Yoga.DISPLAY_NONE
|
node.yoga.getDisplay() === Yoga.DISPLAY_NONE
|
||||||
) {
|
) {
|
||||||
return "";
|
return "";
|
||||||
@@ -136,14 +136,15 @@ export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOpt
|
|||||||
|
|
||||||
let output = "";
|
let output = "";
|
||||||
|
|
||||||
if (node.type === "text") {
|
if (node.type === "tui-text") {
|
||||||
output = squashTextContent(node);
|
output = squashTextContent(node);
|
||||||
} else if (node.type === "box" || node.type === "root") {
|
} else if (node.type === "tui-box" || node.type === "root") {
|
||||||
// Determine separator based on flex direction (resolved from yoga so the
|
// Determine separator based on flex direction (resolved from yoga so the
|
||||||
// Box default of row yields a space separator, matching Ink — see
|
// Box default of row yields a space separator, matching Ink — see
|
||||||
// resolveBoxFlexDirection / G39). Root keeps undefined → "\n" (Ink's column
|
// resolveBoxFlexDirection / G39). Root keeps undefined → "\n" (Ink's column
|
||||||
// default root).
|
// default root).
|
||||||
const flexDirection = node.type === "box" ? resolveBoxFlexDirection(node as TuiBox) : undefined;
|
const flexDirection =
|
||||||
|
node.type === "tui-box" ? resolveBoxFlexDirection(node as TuiBox) : undefined;
|
||||||
|
|
||||||
const separator = flexDirection === "row" || flexDirection === "row-reverse" ? " " : "\n";
|
const separator = flexDirection === "row" || flexDirection === "row-reverse" ? " " : "\n";
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOpt
|
|||||||
)
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(separator);
|
.join(separator);
|
||||||
} else if (node.type === "transform") {
|
} else if (node.type === "tui-transform") {
|
||||||
// Transform nodes: CONCATENATE children with "" (not newline-join), matching
|
// Transform nodes: CONCATENATE children with "" (not newline-join), matching
|
||||||
// Ink's squashTextNodes (squash-text-nodes.ts:42, `text += nodeText`). In Ink
|
// Ink's squashTextNodes (squash-text-nodes.ts:42, `text += nodeText`). In Ink
|
||||||
// a <Transform> is an `ink-text` node, so the SR path squashes it via
|
// a <Transform> is an `ink-text` node, so the SR path squashes it via
|
||||||
@@ -209,7 +210,7 @@ export function renderScreenReaderOutput(node: TuiNode, options: ScreenReaderOpt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add accessibility annotations
|
// Add accessibility annotations
|
||||||
if (node.type === "box") {
|
if (node.type === "tui-box") {
|
||||||
const accessibility = node.internal_accessibility;
|
const accessibility = node.internal_accessibility;
|
||||||
if (accessibility) {
|
if (accessibility) {
|
||||||
const { role, state } = accessibility;
|
const { role, state } = accessibility;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function resolvedFlexDirection(stat: TuiStatic): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findStatics(root: TuiNode, out: TuiStatic[] = []): TuiStatic[] {
|
export function findStatics(root: TuiNode, out: TuiStatic[] = []): TuiStatic[] {
|
||||||
if (root.type === "static") out.push(root);
|
if (root.type === "tui-static") out.push(root);
|
||||||
if (root.type !== "text-leaf" && root.type !== "comment") {
|
if (root.type !== "text-leaf" && root.type !== "comment") {
|
||||||
const containerChildren = (root as { children: TuiNode[] }).children;
|
const containerChildren = (root as { children: TuiNode[] }).children;
|
||||||
for (const child of containerChildren) findStatics(child, out);
|
for (const child of containerChildren) findStatics(child, out);
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import vueJsx from "@vitejs/plugin-vue-jsx";
|
|||||||
import Vue from "unplugin-vue/rolldown";
|
import Vue from "unplugin-vue/rolldown";
|
||||||
import VueVite from "unplugin-vue/vite";
|
import VueVite from "unplugin-vue/vite";
|
||||||
|
|
||||||
const HOST_TAGS = ["box", "text", "virtual-text", "static", "transform"];
|
// Host primitive tags carry a `tui-` prefix (mirroring Ink's `ink-box`/`ink-text`):
|
||||||
|
// it keeps the renderer's intrinsic elements out of the component namespace so a
|
||||||
|
// template `<tui-box>` never collides with the public `<Box>` component (no vue-tsc
|
||||||
|
// self-recursion). The hyphen also makes them valid custom-element names.
|
||||||
|
const HOST_TAGS = ["tui-box", "tui-text", "tui-virtual-text", "tui-static", "tui-transform"];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// `VueVite` parses `.vue` SFCs in the TEST/dev graph (unit tests may import the
|
// `VueVite` parses `.vue` SFCs in the TEST/dev graph (unit tests may import the
|
||||||
|
|||||||
Reference in New Issue
Block a user