fix(runtime): type Static scoped slots

This commit is contained in:
Yunfei He
2026-06-13 23:08:24 +08:00
parent 8f2eabfa8d
commit dab5125c90
7 changed files with 161 additions and 13 deletions
@@ -11,7 +11,7 @@
* - children rejected -> the `accepted` cases error
* - prop validation lost -> the `@ts-expect-error` directives become unused
*/
import { Box, Static, Text, Transform } from "@vue-tui/runtime";
import { Box, Newline, Spacer, Static, Text, Transform } from "@vue-tui/runtime";
// Children are accepted (the shim's whole purpose under the automatic runtime).
export const accepted = [
@@ -20,13 +20,27 @@ export const accepted = [
<Text>nested child</Text>
</Box>,
<Static items={[1, 2, 3]}>
<Text>static child</Text>
{({ item, index }) => {
const renderedItem = item.toFixed(0);
const renderedIndex = index.toFixed(0);
return (
<Text>
{renderedIndex}:{renderedItem}
</Text>
);
}}
</Static>,
<Transform transform={(line) => line}>
<Text>transformed child</Text>
</Transform>,
<Newline />,
<Spacer />,
];
const invalidScopedTransformSlot = {
default: ({ item }: { item: string }) => <Text>{item}</Text>,
};
// Declared props stay validated — children must not widen the prop bag.
export const rejected = [
// @ts-expect-error `display` accepts "flex" | "none", not a number
@@ -39,4 +53,20 @@ export const rejected = [
<Transform>x</Transform>,
// @ts-expect-error `items` is required
<Static>x</Static>,
// @ts-expect-error `<Text>` has an ordinary default slot, not a scoped slot
<Text>{({ item }: { item: string }) => item}</Text>,
// @ts-expect-error `<Box>` has no named `foo` slot
<Box>{{ foo: () => <Text>x</Text> }}</Box>,
// @ts-expect-error `<Transform>` has an ordinary default slot, not a scoped slot
<Transform transform={(line) => line}>{invalidScopedTransformSlot}</Transform>,
// @ts-expect-error `<Newline>` does not accept children
<Newline>x</Newline>,
// @ts-expect-error `<Spacer>` does not accept children
<Spacer>x</Spacer>,
// @ts-expect-error `<Static>` children are Vue scoped slots: one `{ item, index }` object
<Static items={[1]}>{(item: number, index: number) => <Text>{item + index}</Text>}</Static>,
// @ts-expect-error `style` is the supported layout style surface, not an arbitrary object
<Static items={[1]} style={{ flexDirektion: "row" }}>
{({ item }) => <Text>{item}</Text>}
</Static>,
];
@@ -16,10 +16,16 @@ import { shallowRef } from "vue";
import { useApp, useInput, usePaste, useStdin, useStdout, useStderr } from "@vue-tui/runtime";
import type {
BoxProps,
BoxLayoutStyle,
TextProps,
StaticChildren,
StaticProps,
StaticSlot,
StaticSlotProps,
StaticStyle,
TransformProps,
NewlineProps,
SpacerProps,
Key,
WindowSize,
CursorPosition,
@@ -41,10 +47,25 @@ expectTypeOf<BoxProps["backgroundColor"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<BoxProps["borderColor"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<BoxProps["borderBackgroundColor"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<StaticProps["items"]>().toEqualTypeOf<unknown[]>();
expectTypeOf<StaticProps<string>["items"]>().toEqualTypeOf<string[]>();
expectTypeOf<StaticProps["style"]>().toEqualTypeOf<StaticStyle | undefined>();
expectTypeOf<StaticStyle>().toEqualTypeOf<BoxLayoutStyle>();
expectTypeOf<StaticStyle["flexDirection"]>().toEqualTypeOf<BoxProps["flexDirection"]>();
expectTypeOf<StaticSlotProps<string>>().toEqualTypeOf<{ item: string; index: number }>();
expectTypeOf<StaticSlot<string>>().toEqualTypeOf<
(props: StaticSlotProps<string>) => import("vue").VNodeChild
>();
expectTypeOf<StaticChildren<string>>().toEqualTypeOf<
| StaticSlot<string>
| {
default: StaticSlot<string>;
}
>();
expectTypeOf<TransformProps["transform"]>().toEqualTypeOf<
(line: string, lineIndex: number) => string
>();
expectTypeOf<NewlineProps["count"]>().toEqualTypeOf<number | undefined>();
expectTypeOf<keyof SpacerProps>().toEqualTypeOf<never>();
// Framework-neutral data shapes, mirrored from Ink exactly.
expectTypeOf<WindowSize>().toEqualTypeOf<{ readonly columns: number; readonly rows: number }>();
+46
View File
@@ -38,6 +38,52 @@ type BorderStyle =
// 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"
+12 -2
View File
@@ -1,8 +1,18 @@
import { defineComponent, h } from "vue";
import { defineComponent, h, type ExtractPublicPropTypes } from "vue";
export const Spacer = defineComponent({
const spacerProps = {};
const SpacerImpl = defineComponent({
name: "Spacer",
props: spacerProps,
setup() {
return () => h("box", { flexGrow: 1, flexShrink: 1 });
},
});
/** Props accepted by `<Spacer>` — the vue-tui analogue of Ink's `SpacerProps`. */
export type SpacerProps = ExtractPublicPropTypes<typeof spacerProps>;
export const Spacer = SpacerImpl as typeof SpacerImpl & {
new (): { $props: SpacerProps & { children?: never } };
};
+33 -6
View File
@@ -5,8 +5,21 @@ import {
watch,
type ExtractPublicPropTypes,
type PropType,
type SlotsType,
type VNodeChild,
} from "vue";
import type { WithChildren } from "./with-children.ts";
import type { BoxLayoutStyle } from "./box.ts";
export interface StaticSlotProps<T = unknown> {
item: T;
index: number;
}
export type StaticSlot<T = unknown> = (props: StaticSlotProps<T>) => VNodeChild;
export type StaticChildren<T = unknown> = StaticSlot<T> | { default: StaticSlot<T> };
export type StaticStyle = BoxLayoutStyle;
const staticProps = {
// `required: true as const` (not bare `true`): a standalone `const` widens
@@ -14,14 +27,17 @@ const staticProps = {
// required keys (and from the component's own `props.items` typing). The
// literal keeps `items` required, matching Ink's `StaticProps`.
items: { type: Array as PropType<unknown[]>, required: true as const },
style: { type: Object as PropType<Record<string, unknown>>, default: undefined },
style: { type: Object as PropType<StaticStyle>, default: undefined },
};
const StaticImpl = defineComponent({
name: "Static",
props: staticProps,
slots: Object as SlotsType<{
default: StaticSlot;
}>,
setup(props, { slots }) {
const defaultStyle: Record<string, unknown> = {
const defaultStyle: StaticStyle = {
position: "absolute",
flexDirection: "column",
};
@@ -76,7 +92,18 @@ const StaticImpl = defineComponent({
},
});
export const Static = StaticImpl as WithChildren<typeof StaticImpl>;
/** Props accepted by `<Static>` — the vue-tui analogue of Ink's `StaticProps`. */
export type StaticProps = ExtractPublicPropTypes<typeof staticProps>;
type StaticBaseProps = ExtractPublicPropTypes<typeof staticProps>;
export type StaticProps<T = unknown> = Omit<StaticBaseProps, "items"> & {
items: T[];
};
export const Static = StaticImpl as typeof StaticImpl & {
new <T = unknown>(): {
$props: StaticProps<T> & { children?: StaticChildren<T> };
$slots: {
default?: StaticSlot<T>;
};
};
};
@@ -1,3 +1,9 @@
import type { VNodeChild } from "vue";
export type DefaultSlot = () => VNodeChild;
export type DefaultChildren = VNodeChild | DefaultSlot | { default: DefaultSlot };
/**
* Adds an optional `children` prop to a component's JSX `$props` so that TSX
* written under the automatic runtime (`jsx: "react-jsx"` + `jsxImportSource:
@@ -7,6 +13,6 @@
* that prop to the default slot at runtime, so this is a type-only shim with no
* runtime effect.
*/
export type WithChildren<C, T = unknown> = C & {
export type WithChildren<C, T = DefaultChildren> = C & {
new (): { $props: { children?: T } };
};
+10 -2
View File
@@ -5,13 +5,21 @@ 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 } from "./components/spacer.ts";
export { Static, type StaticProps } from "./components/static.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 { Transform, type TransformProps } from "./components/transform.ts";
export { useApp, type UseAppReturn } from "./composables/useApp.ts";