110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
import {
|
|
defineComponent,
|
|
h,
|
|
shallowRef,
|
|
watch,
|
|
type ExtractPublicPropTypes,
|
|
type PropType,
|
|
type SlotsType,
|
|
type VNodeChild,
|
|
} from "vue";
|
|
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
|
|
// `true` → `boolean`, which would drop `items` from ExtractPublicPropTypes'
|
|
// required keys (and from the component's own `props.items` typing). The
|
|
// literal keeps `items` required, matching Ink's `StaticProps`.
|
|
items: { type: Array as PropType<unknown[]>, required: true as const },
|
|
style: { type: Object as PropType<StaticStyle>, 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 `<Static>` — the vue-tui analogue of Ink's `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>;
|
|
};
|
|
};
|
|
};
|