mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			d41117cecc
			...
			b9ce947b9b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b9ce947b9b | ||
| 
						 | 
					da2c6c1ebb | ||
| 
						 | 
					c85c072376 | 
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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": ""
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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" },
 | 
			
		||||
        "+",
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -81,10 +84,10 @@ const TodosApp = () => {
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
        label(
 | 
			
		||||
          { 
 | 
			
		||||
            class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white"
 | 
			
		||||
          }, 
 | 
			
		||||
          () => todo.title
 | 
			
		||||
          {
 | 
			
		||||
            class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white",
 | 
			
		||||
          },
 | 
			
		||||
          () => todo.title,
 | 
			
		||||
        ),
 | 
			
		||||
        button(
 | 
			
		||||
          {
 | 
			
		||||
@@ -308,7 +311,7 @@ const App = () => {
 | 
			
		||||
      {
 | 
			
		||||
        when: () => tabIndex() === 0,
 | 
			
		||||
        fallback: Show(
 | 
			
		||||
          { 
 | 
			
		||||
          {
 | 
			
		||||
            when: () => tabIndex() === 1,
 | 
			
		||||
            fallback: Show(
 | 
			
		||||
              {
 | 
			
		||||
@@ -318,13 +321,13 @@ const App = () => {
 | 
			
		||||
                    when: () => tabIndex() === 3,
 | 
			
		||||
                    fallback: MultiScrollExample(),
 | 
			
		||||
                  },
 | 
			
		||||
                  StaticScrollExample()
 | 
			
		||||
                )
 | 
			
		||||
                  StaticScrollExample(),
 | 
			
		||||
                ),
 | 
			
		||||
              },
 | 
			
		||||
              SimpleScrollExample()
 | 
			
		||||
            )
 | 
			
		||||
          }, 
 | 
			
		||||
          TodosApp()
 | 
			
		||||
              SimpleScrollExample(),
 | 
			
		||||
            ),
 | 
			
		||||
          },
 | 
			
		||||
          TodosApp(),
 | 
			
		||||
        ),
 | 
			
		||||
      },
 | 
			
		||||
      Counter(),
 | 
			
		||||
@@ -347,4 +350,4 @@ try {
 | 
			
		||||
    print("Error running application:");
 | 
			
		||||
    printError(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user