Compare commits

...

3 Commits

Author SHA1 Message Date
SikongJueluo
b9ce947b9b fix: conflict with log and tui, position parse nothing
fix:
- fix ui conflict with log and tui
- fix player position parse failed
2025-10-14 23:26:53 +08:00
SikongJueluo
da2c6c1ebb feature: auto-gen configuration, notice settings, player position parse
feature:
- auto generate configuration file
- adopt notice toast settings
- toast config add new parse parameter: player position x, y, z
reconstruct:
- justfile: remove copy config file command from project
2025-10-14 23:04:00 +08:00
SikongJueluo
c85c072376 feature: tui onFocusChanged event
feature: add focus event for tui
reconstruct: tui props and accesscontrol parse logic
2025-10-14 22:29:17 +08:00
11 changed files with 287 additions and 215 deletions

View File

@@ -9,7 +9,6 @@ build-autocraft:
build-accesscontrol:
pnpm tstl -p ./tsconfig.accesscontrol.json
cp ./src/accesscontrol/access.config.json ./build/
build-test:
pnpm tstl -p ./tsconfig.test.json

View File

@@ -1,72 +0,0 @@
{
"detectRange": 256,
"detectInterval": 1,
"watchInterval": 10,
"noticeTimes": 2,
"isWarn": false,
"adminGroupConfig": {
"groupName": "Admin",
"groupUsers": ["Selcon"],
"isAllowed": true,
"isNotice": true
},
"usersGroups": [
{
"groupName": "user",
"groupUsers": [],
"isAllowed": true,
"isNotice": true
},
{
"groupName": "VIP",
"groupUsers": [],
"isAllowed": true,
"isNotice": false
},
{
"groupName": "enemies",
"groupUsers": [],
"isAllowed": false,
"isNotice": false
}
],
"welcomeToastConfig": {
"title": {
"text": "Welcome",
"color": "green"
},
"msg": {
"text": "Hello User %playerName%",
"color": "green"
},
"prefix": "Taohuayuan",
"brackets": "[]",
"bracketColor": ""
},
"noticeToastConfig": {
"title": {
"text": "Welcome",
"color": "green"
},
"msg": {
"text": "Hello User %playerName%",
"color": "green"
},
"prefix": "Taohuayuan",
"brackets": "[]",
"bracketColor": ""
},
"warnToastConfig": {
"title": {
"text": "Attention!!!",
"color": "red"
},
"msg": {
"text": "%playerName% you are not allowed to be here",
"color": "red"
},
"prefix": "Taohuayuan",
"brackets": "[]",
"bracketColor": ""
}
}

View File

@@ -75,12 +75,12 @@ const defaultConfig: AccessConfig = {
},
noticeToastConfig: {
title: {
text: "Welcome",
color: "green",
text: "Notice",
color: "red",
},
msg: {
text: "Hello User %playerName%",
color: "green",
text: "Unfamiliar player %playerName% appeared at Position %playerPosX%, %playerPosY%, %playerPosZ%",
color: "red",
},
prefix: "Taohuayuan",
brackets: "[]",
@@ -105,12 +105,16 @@ function loadConfig(filepath: string): AccessConfig {
const [fp] = io.open(filepath, "r");
if (fp == undefined) {
print("Failed to open config file " + filepath);
print("Use default config");
saveConfig(defaultConfig, filepath);
return defaultConfig;
}
const configJson = fp.read("*a");
if (configJson == undefined) {
print("Failed to read config file");
print("Use default config");
saveConfig(defaultConfig, filepath);
return defaultConfig;
}

View File

@@ -8,13 +8,14 @@ const DEBUG = false;
const args = [...$vararg];
// Init Log
const log = new CCLog("accesscontrol.log", true, DAY);
const logger = new CCLog("accesscontrol.log", true, DAY);
// Load Config
const configFilepath = `${shell.dir()}/access.config.json`;
const config = loadConfig(configFilepath);
log.info("Load config successfully!");
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true }));
logger.info("Load config successfully!");
if (DEBUG)
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
const groupNames = config.usersGroups.map((value) => value.groupName);
let noticeTargetPlayers: string[];
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
@@ -23,26 +24,56 @@ const chatBox = peripheralManager.findByNameRequired("chatBox");
let inRangePlayers: string[] = [];
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
interface ParseParams {
name?: string;
group?: string;
info?: PlayerInfo;
}
function safeParseTextComponent(
component: MinecraftTextComponent,
playerName: string,
groupName?: string,
params?: ParseParams,
): string {
if (component.text == undefined) {
component.text = "Wrong text, please contanct with admin";
} else if (component.text.includes("%")) {
component.text = component.text.replace("%playerName%", playerName);
if (groupName != undefined)
component.text = component.text.replace("%groupName%", groupName);
component.text = component.text.replace(
"%playerName%",
params?.name ?? "UnknowPlayer",
);
component.text = component.text.replace(
"%groupName%",
params?.group ?? "UnknowGroup",
);
component.text = component.text.replace(
"%playerPosX%",
params?.info?.x.toString() ?? "UnknowPosX",
);
component.text = component.text.replace(
"%playerPosY%",
params?.info?.y.toString() ?? "UnknowPosY",
);
component.text = component.text.replace(
"%playerPosZ%",
params?.info?.z.toString() ?? "UnknowPosZ",
);
}
return textutils.serialiseJSON(component);
}
function sendToast(toastConfig: ToastConfig, targetPlayer: string) {
function sendToast(
toastConfig: ToastConfig,
targetPlayer: string,
params: ParseParams,
) {
return chatBox.sendFormattedToastToPlayer(
textutils.serialiseJSON(toastConfig.msg ?? config.welcomeToastConfig.msg),
textutils.serialiseJSON(
safeParseTextComponent(
toastConfig.msg ?? config.welcomeToastConfig.msg,
params,
),
safeParseTextComponent(
toastConfig.title ?? config.welcomeToastConfig.title,
params,
),
targetPlayer,
toastConfig.prefix ?? config.welcomeToastConfig.prefix,
@@ -62,29 +93,22 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
.flat(),
);
const toastConfig: ToastConfig = {
title: {
text: "Notice",
color: "red",
},
msg: {
text: `Unfamiliar Player ${player} appeared at\n Position ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
color: "red",
},
};
for (const targetPlayer of noticeTargetPlayers) {
if (!onlinePlayers.includes(targetPlayer)) continue;
sendToast(toastConfig, targetPlayer);
sendToast(config.noticeToastConfig, targetPlayer, {
name: player,
info: playerInfo,
});
}
}
function sendWarn(player: string) {
const warnMsg = `Not Allowed Player ${player} Break in Home `;
log.warn(warnMsg);
logger.warn(warnMsg);
sendToast(config.warnToastConfig, player);
sendToast(config.warnToastConfig, player, { name: player });
chatBox.sendFormattedMessageToPlayer(
safeParseTextComponent(config.warnToastConfig.msg, player),
safeParseTextComponent(config.warnToastConfig.msg, { name: player }),
player,
"AccessControl",
"[]",
@@ -110,7 +134,7 @@ function watchLoop() {
if (config.isWarn) sendWarn(player.name);
// Record
log.warn(
logger.warn(
`${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
} else {
@@ -118,6 +142,7 @@ function watchLoop() {
watchPlayersInfo = watchPlayersInfo.filter(
(value) => value.name != player.name,
);
logger.info(`${player.name} has left the range`);
}
}
@@ -130,14 +155,14 @@ function mainLoop() {
const players = playerDetector.getPlayersInRange(config.detectRange);
if (DEBUG) {
const playersList = "[ " + players.join(",") + " ]";
log.debug(`Detected ${players.length} players: ${playersList}`);
logger.debug(`Detected ${players.length} players: ${playersList}`);
}
for (const player of players) {
if (inRangePlayers.includes(player)) continue;
if (config.adminGroupConfig.groupUsers.includes(player)) {
log.info(`Admin ${player} appear`);
logger.info(`Admin ${player} appear`);
continue;
}
@@ -154,7 +179,7 @@ function mainLoop() {
if (!userGroupConfig.groupUsers.includes(player)) continue;
groupConfig = userGroupConfig;
log.info(
logger.info(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
@@ -162,7 +187,7 @@ function mainLoop() {
}
if (groupConfig.isAllowed) continue;
log.warn(
logger.warn(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
if (config.isWarn) sendWarn(player);
@@ -178,26 +203,29 @@ function keyboardLoop() {
while (true) {
const [eventType, key] = os.pullEvent("key");
if (eventType === "key" && key === keys.c) {
log.info("Launching Access Control TUI...");
logger.info("Launching Access Control TUI...");
try {
logger.setInTerminal(false);
launchAccessControlTUI();
log.info("TUI closed, resuming normal operation");
logger.info("TUI closed, resuming normal operation");
} catch (error) {
log.error(`TUI error: ${textutils.serialise(error as object)}`);
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
} finally {
logger.setInTerminal(true);
}
}
}
}
function main(args: string[]) {
log.info("Starting access control system, get args: " + args.join(", "));
logger.info("Starting access control system, get args: " + args.join(", "));
if (args.length == 1) {
if (args[0] == "start") {
// 创建CLI处理器
const cli = createAccessControlCLI(
config,
configFilepath,
log,
logger,
chatBox,
groupNames,
);
@@ -221,12 +249,12 @@ function main(args: string[]) {
);
return;
} else if (args[0] == "config") {
log.info("Launching Access Control TUI...");
log.setInTerminal(false);
logger.info("Launching Access Control TUI...");
logger.setInTerminal(false);
try {
launchAccessControlTUI();
} catch (error) {
log.error(`TUI error: ${textutils.serialise(error as object)}`);
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
}
return;
}
@@ -240,7 +268,7 @@ function main(args: string[]) {
try {
main(args);
} catch (error: unknown) {
log.error(textutils.serialise(error as object));
logger.error(textutils.serialise(error as object));
} finally {
log.close();
logger.close();
}

View File

@@ -243,6 +243,18 @@ const AccessControlTUI = () => {
/**
* Basic Configuration Tab
*/
const [getDetectInterval, setDetectInterval] = createSignal(
config().detectInterval.toString(),
);
const [getWatchInterval, setWatchInterval] = createSignal(
config().watchInterval.toString(),
);
const [getNoticeTimes, setNoticeTimes] = createSignal(
config().noticeTimes.toString(),
);
const [getDetectRange, setDetectRange] = createSignal(
config().detectRange.toString(),
);
const BasicTab = () => {
return div(
{ class: "flex flex-col" },
@@ -251,10 +263,12 @@ const AccessControlTUI = () => {
label({}, "Detect Interval (ms):"),
input({
type: "text",
value: () => config().detectInterval?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
value: () => getDetectInterval(),
onInput: (value) => setDetectInterval(value),
onFocusChanged: () => {
const num = validateNumber(getDetectInterval());
if (num !== null) setConfig("detectInterval", num);
else setDetectInterval(config().detectInterval.toString());
},
}),
),
@@ -263,10 +277,12 @@ const AccessControlTUI = () => {
label({}, "Watch Interval (ms):"),
input({
type: "text",
value: () => config().watchInterval?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
value: () => getWatchInterval(),
onInput: (value) => setWatchInterval(value),
onFocusChanged: () => {
const num = validateNumber(getWatchInterval());
if (num !== null) setConfig("watchInterval", num);
else setWatchInterval(config().watchInterval.toString());
},
}),
),
@@ -275,10 +291,12 @@ const AccessControlTUI = () => {
label({}, "Notice Times:"),
input({
type: "text",
value: () => config().noticeTimes?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
value: () => getNoticeTimes(),
onInput: (value) => setNoticeTimes(value),
onFocusChanged: () => {
const num = validateNumber(getNoticeTimes());
if (num !== null) setConfig("noticeTimes", num);
else setNoticeTimes(config().noticeTimes.toString());
},
}),
),
@@ -287,10 +305,12 @@ const AccessControlTUI = () => {
label({}, "Detect Range:"),
input({
type: "text",
value: () => config().detectRange?.toString() ?? "",
onInput: (value) => {
const num = validateNumber(value);
value: () => getDetectRange(),
onInput: (value) => setDetectRange(value),
onFocusChanged: () => {
const num = validateNumber(getDetectRange());
if (num !== null) setConfig("detectRange", num);
else setDetectRange(config().detectRange.toString());
},
}),
),
@@ -436,6 +456,13 @@ const AccessControlTUI = () => {
) => {
return () => {
const toastConfig = config()[toastType];
const [getTempToastConfig, setTempToastConfig] = createSignal({
title: textutils.serialiseJSON(toastConfig.title),
msg: textutils.serialiseJSON(toastConfig.msg),
prefix: toastConfig.prefix ?? "",
brackets: toastConfig.brackets ?? "",
bracketColor: toastConfig.bracketColor ?? "",
});
return div(
{ class: "flex flex-col w-full" },
@@ -443,20 +470,34 @@ const AccessControlTUI = () => {
input({
class: "w-full",
type: "text",
value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
onInput: (value) => {
value: () => getTempToastConfig().title,
onInput: (value) =>
setTempToastConfig({
...getTempToastConfig(),
title: value,
}),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
try {
const parsed = textutils.unserialiseJSON(value);
if (parsed != undefined && typeof parsed === "object") {
const currentConfig = config();
const currentToast = currentConfig[toastType];
const parsed = textutils.unserialiseJSON(
getTempToastConfig().title,
) as MinecraftTextComponent;
if (
typeof parsed === "object" &&
parsed.text !== undefined &&
parsed.color !== undefined
) {
setConfig(toastType, {
...currentToast,
title: parsed as MinecraftTextComponent,
...currentToastConfig,
title: parsed,
});
}
} else throw new Error("Invalid JSON");
} catch {
// Invalid JSON, ignore
setTempToastConfig({
...getTempToastConfig(),
title: textutils.serialiseJSON(currentToastConfig.title),
});
}
},
}),
@@ -465,19 +506,31 @@ const AccessControlTUI = () => {
input({
class: "w-full",
type: "text",
value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
onInput: (value) => {
value: () => getTempToastConfig().msg,
onInput: (value) =>
setTempToastConfig({ ...getTempToastConfig(), msg: value }),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
try {
const parsed = textutils.unserialiseJSON(value);
if (parsed != undefined && typeof parsed === "object") {
const currentConfig = config();
const currentToast = currentConfig[toastType];
const parsed = textutils.unserialiseJSON(
getTempToastConfig().msg,
) as MinecraftTextComponent;
if (
typeof parsed === "object" &&
parsed.text !== undefined &&
parsed.color !== undefined
) {
setConfig(toastType, {
...currentToast,
msg: parsed as MinecraftTextComponent,
...currentToastConfig,
msg: parsed,
});
}
} else throw new Error("Invalid JSON");
} catch {
setTempToastConfig({
...getTempToastConfig(),
msg: textutils.serialiseJSON(currentToastConfig.msg),
});
// Invalid JSON, ignore
}
},
@@ -488,11 +541,15 @@ const AccessControlTUI = () => {
label({}, "Prefix:"),
input({
type: "text",
value: () => toastConfig?.prefix ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, prefix: value });
value: () => getTempToastConfig().prefix,
onInput: (value) =>
setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
prefix: getTempToastConfig().prefix,
});
},
}),
),
@@ -502,11 +559,15 @@ const AccessControlTUI = () => {
label({}, "Brackets:"),
input({
type: "text",
value: () => toastConfig?.brackets ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, brackets: value });
value: () => getTempToastConfig().brackets,
onInput: (value) =>
setTempToastConfig({ ...getTempToastConfig(), brackets: value }),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
brackets: getTempToastConfig().brackets,
});
},
}),
),
@@ -516,11 +577,18 @@ const AccessControlTUI = () => {
label({}, "Bracket Color:"),
input({
type: "text",
value: () => toastConfig?.bracketColor ?? "",
onInput: (value) => {
const currentConfig = config();
const currentToast = currentConfig[toastType];
setConfig(toastType, { ...currentToast, bracketColor: value });
value: () => getTempToastConfig().bracketColor,
onInput: (value) =>
setTempToastConfig({
...getTempToastConfig(),
bracketColor: value,
}),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
bracketColor: getTempToastConfig().bracketColor,
});
},
}),
),

View File

@@ -3,7 +3,9 @@
* Represents a node in the UI tree
*/
import { Accessor } from "./reactivity";
import { ButtonProps, DivProps, InputProps, LabelProps } from "./components";
import { Accessor, Setter } from "./reactivity";
import { ScrollContainerProps } from "./scrollContainer";
/**
* Layout properties for flexbox layout
@@ -34,7 +36,7 @@ export interface StyleProps {
/**
* Scroll properties for scroll containers
*/
export interface ScrollProps {
export interface ScrollProps extends BaseProps {
/** Current horizontal scroll position */
scrollX: number;
/** Current vertical scroll position */
@@ -69,6 +71,9 @@ export interface ComputedLayout {
export interface BaseProps {
/** CSS-like class names for layout (e.g., "flex flex-col") */
class?: string;
width?: number;
height?: number;
onFocusChanged?: Setter<boolean> | ((value: boolean) => void);
}
/**
@@ -90,6 +95,14 @@ export type UIObjectType =
| "fragment"
| "scroll-container";
export type UIObjectProps =
| DivProps
| LabelProps
| InputProps
| ButtonProps
| ScrollProps
| ScrollContainerProps;
/**
* UIObject represents a node in the UI tree
* It can be a component, text, or a control flow element
@@ -99,7 +112,7 @@ export class UIObject {
type: UIObjectType;
/** Props passed to the component */
props: Record<string, unknown>;
props: UIObjectProps;
/** Children UI objects */
children: UIObject[];
@@ -136,7 +149,7 @@ export class UIObject {
constructor(
type: UIObjectType,
props: Record<string, unknown> = {},
props: UIObjectProps = {},
children: UIObject[] = [],
) {
this.type = type;
@@ -155,7 +168,7 @@ export class UIObject {
this.extractHandlers();
// Initialize cursor position for text inputs
if (type === "input" && props.type !== "checkbox") {
if (type === "input" && (props as InputProps).type !== "checkbox") {
this.cursorPos = 0;
}
@@ -168,9 +181,9 @@ export class UIObject {
maxScrollY: 0,
contentWidth: 0,
contentHeight: 0,
showScrollbar: props.showScrollbar !== false,
viewportWidth: (props.width as number) ?? 10,
viewportHeight: (props.height as number) ?? 10,
showScrollbar: (props as ScrollProps).showScrollbar !== false,
viewportWidth: props.width ?? 10,
viewportHeight: props.height ?? 10,
};
}
}
@@ -208,7 +221,7 @@ export class UIObject {
* Parse CSS-like class string into layout and style properties
*/
private parseClassNames(): void {
const className = this.props.class as string | undefined;
const className = this.props.class;
if (className === undefined) return;
const classes = className.split(" ").filter((c) => c.length > 0);

View File

@@ -7,6 +7,8 @@ import { calculateLayout } from "./layout";
import { render as renderTree, clearScreen } from "./renderer";
import { CCLog, HOUR } from "../ccLog";
import { setLogger } from "./context";
import { InputProps } from "./components";
import { Setter } from "./reactivity";
/**
* Main application class
@@ -145,7 +147,7 @@ export class Application {
if (
this.focusedNode !== undefined &&
this.focusedNode.type === "input" &&
this.focusedNode.props.type !== "checkbox"
(this.focusedNode.props as InputProps).type !== "checkbox"
) {
this.needsRender = true;
}
@@ -213,11 +215,13 @@ export class Application {
this.needsRender = true;
}
} else if (this.focusedNode.type === "input") {
const type = this.focusedNode.props.type as string | undefined;
const type = (this.focusedNode.props as InputProps).type as
| string
| undefined;
if (type === "checkbox") {
// Toggle checkbox
const onChangeProp = this.focusedNode.props.onChange;
const checkedProp = this.focusedNode.props.checked;
const onChangeProp = (this.focusedNode.props as InputProps).onChange;
const checkedProp = (this.focusedNode.props as InputProps).checked;
if (
typeof onChangeProp === "function" &&
@@ -234,7 +238,9 @@ export class Application {
this.focusedNode.type === "input"
) {
// Handle text input key events
const type = this.focusedNode.props.type as string | undefined;
const type = (this.focusedNode.props as InputProps).type as
| string
| undefined;
if (type !== "checkbox") {
this.handleTextInputKey(key);
}
@@ -247,8 +253,8 @@ export class Application {
private handleTextInputKey(key: number): void {
if (this.focusedNode === undefined) return;
const valueProp = this.focusedNode.props.value;
const onInputProp = this.focusedNode.props.onInput;
const valueProp = (this.focusedNode.props as InputProps).value;
const onInputProp = (this.focusedNode.props as InputProps).onInput;
if (typeof valueProp !== "function" || typeof onInputProp !== "function") {
return;
@@ -292,11 +298,11 @@ export class Application {
*/
private handleCharEvent(char: string): void {
if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
const type = this.focusedNode.props.type as string | undefined;
const type = (this.focusedNode.props as InputProps).type;
if (type !== "checkbox") {
// Insert character at cursor position
const onInputProp = this.focusedNode.props.onInput;
const valueProp = this.focusedNode.props.value;
const onInputProp = (this.focusedNode.props as InputProps).onInput;
const valueProp = (this.focusedNode.props as InputProps).value;
if (
typeof onInputProp === "function" &&
@@ -331,11 +337,26 @@ export class Application {
string.format("handleMouseClick: Found node of type %s.", clicked.type),
);
// Set focus
if (
this.focusedNode !== undefined &&
typeof this.focusedNode.props.onFocusChanged === "function"
) {
const onFocusChanged = this.focusedNode.props
.onFocusChanged as Setter<boolean>;
onFocusChanged(false);
}
this.focusedNode = clicked;
if (typeof clicked.props.onFocusChanged === "function") {
const onFocusChanged = clicked.props.onFocusChanged as Setter<boolean>;
onFocusChanged(true);
}
// Initialize cursor position for text inputs on focus
if (clicked.type === "input" && clicked.props.type !== "checkbox") {
const valueProp = clicked.props.value;
if (
clicked.type === "input" &&
(clicked.props as InputProps).type !== "checkbox"
) {
const valueProp = (clicked.props as InputProps).value;
if (typeof valueProp === "function") {
const currentValue = (valueProp as () => string)();
clicked.cursorPos = currentValue.length;
@@ -354,10 +375,10 @@ export class Application {
this.needsRender = true;
}
} else if (clicked.type === "input") {
const type = clicked.props.type as string | undefined;
const type = (clicked.props as InputProps).type as string | undefined;
if (type === "checkbox") {
const onChangeProp = clicked.props.onChange;
const checkedProp = clicked.props.checked;
const onChangeProp = (clicked.props as InputProps).onChange;
const checkedProp = (clicked.props as InputProps).checked;
if (
typeof onChangeProp === "function" &&
@@ -424,6 +445,14 @@ export class Application {
const interactive = this.collectInteractive(this.root);
if (
this.focusedNode !== undefined &&
typeof this.focusedNode.props.onFocusChanged === "function"
) {
const onFocusChanged = this.focusedNode.props
.onFocusChanged as Setter<boolean>;
onFocusChanged(false);
}
if (interactive.length === 0) {
this.focusedNode = undefined;
return;

View File

@@ -12,7 +12,7 @@ import { concatSentence } from "../common";
/**
* Props for div component
*/
export type DivProps = BaseProps & Record<string, unknown>;
export type DivProps = BaseProps;
/**
* Props for label component
@@ -20,7 +20,7 @@ export type DivProps = BaseProps & Record<string, unknown>;
export type LabelProps = BaseProps & {
/** Whether to automatically wrap long text. Defaults to false. */
wordWrap?: boolean;
} & Record<string, unknown>;
};
/**
* Props for button component
@@ -28,7 +28,7 @@ export type LabelProps = BaseProps & {
export type ButtonProps = BaseProps & {
/** Click handler */
onClick?: () => void;
} & Record<string, unknown>;
};
/**
* Props for input component
@@ -46,7 +46,7 @@ export type InputProps = BaseProps & {
onChange?: Setter<boolean> | ((checked: boolean) => void);
/** Placeholder text */
placeholder?: string;
} & Record<string, unknown>;
};
/**
* Props for form component
@@ -54,7 +54,7 @@ export type InputProps = BaseProps & {
export type FormProps = BaseProps & {
/** Submit handler */
onSubmit?: () => void;
} & Record<string, unknown>;
};
/**
* Generic container component for layout

View File

@@ -3,6 +3,7 @@
* Calculates positions and sizes for UI elements based on flexbox rules
*/
import { InputProps } from "./components";
import { UIObject } from "./UIObject";
/**
@@ -109,7 +110,7 @@ function measureNode(
}
case "input": {
const type = node.props.type as string | undefined;
const type = (node.props as InputProps).type as string | undefined;
if (type === "checkbox") {
const naturalWidth = 3; // [X] or [ ]
const naturalHeight = 1;
@@ -119,7 +120,7 @@ function measureNode(
};
}
// Text input - use a default width or from props
const defaultWidth = (node.props.width as number | undefined) ?? 20;
const defaultWidth = node.props.width ?? 20;
const naturalHeight = 1;
return {
width: measuredWidth ?? defaultWidth,

View File

@@ -3,7 +3,7 @@
*/
import { UIObject } from "./UIObject";
import { Accessor } from "./reactivity";
import { InputProps } from "./components";
import { isScrollContainer } from "./scrollContainer";
/**
@@ -189,14 +189,14 @@ function drawNode(
}
case "input": {
const type = node.props.type as string | undefined;
const type = (node.props as InputProps).type as string | undefined;
if (type === "checkbox") {
// Draw checkbox
let isChecked = false;
const checkedProp = node.props.checked;
const checkedProp = (node.props as InputProps).checked;
if (typeof checkedProp === "function") {
isChecked = (checkedProp as Accessor<boolean>)();
isChecked = checkedProp();
}
if (focused) {
@@ -212,12 +212,11 @@ function drawNode(
} else {
// Draw text input
let displayText = "";
const valueProp = node.props.value;
const valueProp = (node.props as InputProps).value;
if (typeof valueProp === "function") {
displayText = (valueProp as Accessor<string>)();
displayText = valueProp();
}
const placeholder = node.props.placeholder as string | undefined;
const placeholder = (node.props as InputProps).placeholder;
const cursorPos = node.cursorPos ?? 0;
let currentTextColor = textColor;
let showPlaceholder = false;

View File

@@ -31,7 +31,10 @@ const Counter = () => {
div(
{ class: "flex flex-row" },
button({ onClick: () => setCount(count() - 1), class: "text-red" }, "-"),
button({ onClick: () => setCount(count() + 1), class: "text-green" }, "+"),
button(
{ onClick: () => setCount(count() + 1), class: "text-green" },
"+",
),
),
);
};
@@ -82,9 +85,9 @@ const TodosApp = () => {
}),
label(
{
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white"
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white",
},
() => todo.title
() => todo.title,
),
button(
{
@@ -318,13 +321,13 @@ const App = () => {
when: () => tabIndex() === 3,
fallback: MultiScrollExample(),
},
StaticScrollExample()
)
StaticScrollExample(),
),
},
SimpleScrollExample()
)
SimpleScrollExample(),
),
},
TodosApp()
TodosApp(),
),
},
Counter(),