mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-05 03:37:50 +08:00
finish basic tui for accesscontrol, add scroll for tui
This commit is contained in:
@@ -25,6 +25,32 @@ export interface StyleProps {
|
||||
textColor?: number;
|
||||
/** Background color */
|
||||
backgroundColor?: number;
|
||||
/** Width - can be a number (fixed), "full" (100% of parent), or "screen" (terminal width) */
|
||||
width?: number | "full" | "screen";
|
||||
/** Height - can be a number (fixed), "full" (100% of parent), or "screen" (terminal height) */
|
||||
height?: number | "full" | "screen";
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll properties for scroll containers
|
||||
*/
|
||||
export interface ScrollProps {
|
||||
/** Current horizontal scroll position */
|
||||
scrollX: number;
|
||||
/** Current vertical scroll position */
|
||||
scrollY: number;
|
||||
/** Maximum horizontal scroll (content width - viewport width) */
|
||||
maxScrollX: number;
|
||||
/** Maximum vertical scroll (content height - viewport height) */
|
||||
maxScrollY: number;
|
||||
/** Content dimensions */
|
||||
contentWidth: number;
|
||||
contentHeight: number;
|
||||
/** Whether to show scrollbars */
|
||||
showScrollbar?: boolean;
|
||||
/** Viewport dimensions (visible area) */
|
||||
viewportWidth: number;
|
||||
viewportHeight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,18 +74,21 @@ export interface BaseProps {
|
||||
/**
|
||||
* UIObject node type
|
||||
*/
|
||||
export type UIObjectType =
|
||||
| "div"
|
||||
| "label"
|
||||
| "button"
|
||||
| "input"
|
||||
export type UIObjectType =
|
||||
| "div"
|
||||
| "label"
|
||||
| "button"
|
||||
| "input"
|
||||
| "form"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "for"
|
||||
| "show"
|
||||
| "fragment";
|
||||
| "switch"
|
||||
| "match"
|
||||
| "fragment"
|
||||
| "scroll-container";
|
||||
|
||||
/**
|
||||
* UIObject represents a node in the UI tree
|
||||
@@ -68,44 +97,47 @@ export type UIObjectType =
|
||||
export class UIObject {
|
||||
/** Type of the UI object */
|
||||
type: UIObjectType;
|
||||
|
||||
|
||||
/** Props passed to the component */
|
||||
props: Record<string, unknown>;
|
||||
|
||||
|
||||
/** Children UI objects */
|
||||
children: UIObject[];
|
||||
|
||||
|
||||
/** Parent UI object */
|
||||
parent?: UIObject;
|
||||
|
||||
|
||||
/** Computed layout after flexbox calculation */
|
||||
layout?: ComputedLayout;
|
||||
|
||||
|
||||
/** Layout properties parsed from class string */
|
||||
layoutProps: LayoutProps;
|
||||
|
||||
|
||||
/** Style properties parsed from class string */
|
||||
styleProps: StyleProps;
|
||||
|
||||
|
||||
/** Whether this component is currently mounted */
|
||||
mounted: boolean;
|
||||
|
||||
|
||||
/** Cleanup functions to call when unmounting */
|
||||
cleanupFns: (() => void)[];
|
||||
|
||||
|
||||
/** For text nodes - the text content (can be reactive) */
|
||||
textContent?: string | Accessor<string>;
|
||||
|
||||
|
||||
/** Event handlers */
|
||||
handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
|
||||
|
||||
|
||||
/** For input text components - cursor position */
|
||||
cursorPos?: number;
|
||||
|
||||
/** For scroll containers - scroll state */
|
||||
scrollProps?: ScrollProps;
|
||||
|
||||
constructor(
|
||||
type: UIObjectType,
|
||||
props: Record<string, unknown> = {},
|
||||
children: UIObject[] = []
|
||||
children: UIObject[] = [],
|
||||
) {
|
||||
this.type = type;
|
||||
this.props = props;
|
||||
@@ -115,45 +147,60 @@ export class UIObject {
|
||||
this.mounted = false;
|
||||
this.cleanupFns = [];
|
||||
this.handlers = {};
|
||||
|
||||
|
||||
// Parse layout and styles from class prop
|
||||
this.parseClassNames();
|
||||
|
||||
|
||||
// Extract event handlers
|
||||
this.extractHandlers();
|
||||
|
||||
|
||||
// Initialize cursor position for text inputs
|
||||
if (type === "input" && props.type !== "checkbox") {
|
||||
this.cursorPos = 0;
|
||||
}
|
||||
|
||||
// Initialize scroll properties for scroll containers
|
||||
if (type === "scroll-container") {
|
||||
this.scrollProps = {
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
maxScrollX: 0,
|
||||
maxScrollY: 0,
|
||||
contentWidth: 0,
|
||||
contentHeight: 0,
|
||||
showScrollbar: props.showScrollbar !== false,
|
||||
viewportWidth: (props.width as number) ?? 10,
|
||||
viewportHeight: (props.height as number) ?? 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map color name to ComputerCraft colors API value
|
||||
*
|
||||
*
|
||||
* @param colorName - The color name from class (e.g., "white", "red")
|
||||
* @returns The color value from colors API, or undefined if invalid
|
||||
*/
|
||||
private parseColor(colorName: string): number | undefined {
|
||||
const colorMap: Record<string, number> = {
|
||||
"white": colors.white,
|
||||
"orange": colors.orange,
|
||||
"magenta": colors.magenta,
|
||||
"lightBlue": colors.lightBlue,
|
||||
"yellow": colors.yellow,
|
||||
"lime": colors.lime,
|
||||
"pink": colors.pink,
|
||||
"gray": colors.gray,
|
||||
"lightGray": colors.lightGray,
|
||||
"cyan": colors.cyan,
|
||||
"purple": colors.purple,
|
||||
"blue": colors.blue,
|
||||
"brown": colors.brown,
|
||||
"green": colors.green,
|
||||
"red": colors.red,
|
||||
"black": colors.black,
|
||||
white: colors.white,
|
||||
orange: colors.orange,
|
||||
magenta: colors.magenta,
|
||||
lightBlue: colors.lightBlue,
|
||||
yellow: colors.yellow,
|
||||
lime: colors.lime,
|
||||
pink: colors.pink,
|
||||
gray: colors.gray,
|
||||
lightGray: colors.lightGray,
|
||||
cyan: colors.cyan,
|
||||
purple: colors.purple,
|
||||
blue: colors.blue,
|
||||
brown: colors.brown,
|
||||
green: colors.green,
|
||||
red: colors.red,
|
||||
black: colors.black,
|
||||
};
|
||||
|
||||
|
||||
return colorMap[colorName];
|
||||
}
|
||||
|
||||
@@ -164,8 +211,8 @@ export class UIObject {
|
||||
const className = this.props.class as string | undefined;
|
||||
if (className === undefined) return;
|
||||
|
||||
const classes = className.split(" ").filter(c => c.length > 0);
|
||||
|
||||
const classes = className.split(" ").filter((c) => c.length > 0);
|
||||
|
||||
for (const cls of classes) {
|
||||
// Flex direction
|
||||
if (cls === "flex-row") {
|
||||
@@ -173,7 +220,7 @@ export class UIObject {
|
||||
} else if (cls === "flex-col") {
|
||||
this.layoutProps.flexDirection = "column";
|
||||
}
|
||||
|
||||
|
||||
// Justify content
|
||||
else if (cls === "justify-start") {
|
||||
this.layoutProps.justifyContent = "start";
|
||||
@@ -184,7 +231,7 @@ export class UIObject {
|
||||
} else if (cls === "justify-between") {
|
||||
this.layoutProps.justifyContent = "between";
|
||||
}
|
||||
|
||||
|
||||
// Align items
|
||||
else if (cls === "items-start") {
|
||||
this.layoutProps.alignItems = "start";
|
||||
@@ -193,7 +240,7 @@ export class UIObject {
|
||||
} else if (cls === "items-end") {
|
||||
this.layoutProps.alignItems = "end";
|
||||
}
|
||||
|
||||
|
||||
// Text color (text-<color>)
|
||||
else if (cls.startsWith("text-")) {
|
||||
const colorName = cls.substring(5); // Remove "text-" prefix
|
||||
@@ -202,7 +249,7 @@ export class UIObject {
|
||||
this.styleProps.textColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Background color (bg-<color>)
|
||||
else if (cls.startsWith("bg-")) {
|
||||
const colorName = cls.substring(3); // Remove "bg-" prefix
|
||||
@@ -211,8 +258,38 @@ export class UIObject {
|
||||
this.styleProps.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Width sizing (w-<size>)
|
||||
else if (cls.startsWith("w-")) {
|
||||
const sizeValue = cls.substring(2); // Remove "w-" prefix
|
||||
if (sizeValue === "full") {
|
||||
this.styleProps.width = "full";
|
||||
} else if (sizeValue === "screen") {
|
||||
this.styleProps.width = "screen";
|
||||
} else {
|
||||
const numValue = tonumber(sizeValue);
|
||||
if (numValue !== undefined) {
|
||||
this.styleProps.width = numValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Height sizing (h-<size>)
|
||||
else if (cls.startsWith("h-")) {
|
||||
const sizeValue = cls.substring(2); // Remove "h-" prefix
|
||||
if (sizeValue === "full") {
|
||||
this.styleProps.height = "full";
|
||||
} else if (sizeValue === "screen") {
|
||||
this.styleProps.height = "screen";
|
||||
} else {
|
||||
const numValue = tonumber(sizeValue);
|
||||
if (numValue !== undefined) {
|
||||
this.styleProps.height = numValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set defaults
|
||||
if (this.type === "div") {
|
||||
this.layoutProps.flexDirection ??= "row";
|
||||
@@ -226,7 +303,11 @@ export class UIObject {
|
||||
*/
|
||||
private extractHandlers(): void {
|
||||
for (const [key, value] of pairs(this.props)) {
|
||||
if (typeof key === "string" && key.startsWith("on") && typeof value === "function") {
|
||||
if (
|
||||
typeof key === "string" &&
|
||||
key.startsWith("on") &&
|
||||
typeof value === "function"
|
||||
) {
|
||||
this.handlers[key] = value as (...args: unknown[]) => void;
|
||||
}
|
||||
}
|
||||
@@ -257,7 +338,7 @@ export class UIObject {
|
||||
mount(): void {
|
||||
if (this.mounted) return;
|
||||
this.mounted = true;
|
||||
|
||||
|
||||
// Mount all children
|
||||
for (const child of this.children) {
|
||||
child.mount();
|
||||
@@ -270,12 +351,12 @@ export class UIObject {
|
||||
unmount(): void {
|
||||
if (!this.mounted) return;
|
||||
this.mounted = false;
|
||||
|
||||
|
||||
// Unmount all children first
|
||||
for (const child of this.children) {
|
||||
child.unmount();
|
||||
}
|
||||
|
||||
|
||||
// Run cleanup functions
|
||||
for (const cleanup of this.cleanupFns) {
|
||||
try {
|
||||
@@ -293,6 +374,75 @@ export class UIObject {
|
||||
onCleanup(fn: () => void): void {
|
||||
this.cleanupFns.push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the container by the given amount
|
||||
* @param deltaX - Horizontal scroll delta
|
||||
* @param deltaY - Vertical scroll delta
|
||||
*/
|
||||
scrollBy(deltaX: number, deltaY: number): void {
|
||||
if (this.type !== "scroll-container" || !this.scrollProps) return;
|
||||
|
||||
const newScrollX = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollX, this.scrollProps.scrollX + deltaX),
|
||||
);
|
||||
const newScrollY = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollY, this.scrollProps.scrollY + deltaY),
|
||||
);
|
||||
|
||||
this.scrollProps.scrollX = newScrollX;
|
||||
this.scrollProps.scrollY = newScrollY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a specific position
|
||||
* @param x - Horizontal scroll position
|
||||
* @param y - Vertical scroll position
|
||||
*/
|
||||
scrollTo(x: number, y: number): void {
|
||||
if (this.type !== "scroll-container" || !this.scrollProps) return;
|
||||
|
||||
this.scrollProps.scrollX = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollX, x),
|
||||
);
|
||||
this.scrollProps.scrollY = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollY, y),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scroll bounds based on content size
|
||||
* @param contentWidth - Total content width
|
||||
* @param contentHeight - Total content height
|
||||
*/
|
||||
updateScrollBounds(contentWidth: number, contentHeight: number): void {
|
||||
if (this.type !== "scroll-container" || !this.scrollProps) return;
|
||||
|
||||
this.scrollProps.contentWidth = contentWidth;
|
||||
this.scrollProps.contentHeight = contentHeight;
|
||||
this.scrollProps.maxScrollX = Math.max(
|
||||
0,
|
||||
contentWidth - this.scrollProps.viewportWidth,
|
||||
);
|
||||
this.scrollProps.maxScrollY = Math.max(
|
||||
0,
|
||||
contentHeight - this.scrollProps.viewportHeight,
|
||||
);
|
||||
|
||||
// Clamp current scroll position to new bounds
|
||||
this.scrollProps.scrollX = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollX, this.scrollProps.scrollX),
|
||||
);
|
||||
this.scrollProps.scrollY = Math.max(
|
||||
0,
|
||||
Math.min(this.scrollProps.maxScrollY, this.scrollProps.scrollY),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user