mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	finish basic tui for accesscontrol, add scroll for tui
This commit is contained in:
		@@ -1,8 +1,5 @@
 | 
				
			|||||||
import { CCLog } from "@/lib/ccLog";
 | 
					 | 
				
			||||||
import * as dkjson from "@sikongjueluo/dkjson-types";
 | 
					import * as dkjson from "@sikongjueluo/dkjson-types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let log: CCLog | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ToastConfig {
 | 
					interface ToastConfig {
 | 
				
			||||||
  title: MinecraftTextComponent;
 | 
					  title: MinecraftTextComponent;
 | 
				
			||||||
  msg: MinecraftTextComponent;
 | 
					  msg: MinecraftTextComponent;
 | 
				
			||||||
@@ -104,10 +101,6 @@ const defaultConfig: AccessConfig = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setLog(newLog: CCLog) {
 | 
					 | 
				
			||||||
  log = newLog;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadConfig(filepath: string): AccessConfig {
 | 
					function loadConfig(filepath: string): AccessConfig {
 | 
				
			||||||
  const [fp] = io.open(filepath, "r");
 | 
					  const [fp] = io.open(filepath, "r");
 | 
				
			||||||
  if (fp == undefined) {
 | 
					  if (fp == undefined) {
 | 
				
			||||||
@@ -121,18 +114,18 @@ function loadConfig(filepath: string): AccessConfig {
 | 
				
			|||||||
    return defaultConfig;
 | 
					    return defaultConfig;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [config, pos, err] = dkjson.decode(configJson);
 | 
					  // const [config, pos, err] = dkjson.decode(configJson);
 | 
				
			||||||
  if (config == undefined) {
 | 
					  // if (config == undefined) {
 | 
				
			||||||
    log?.warn(
 | 
					  //   log?.warn(
 | 
				
			||||||
      `Config decode failed at ${pos}, use default instead. Error :${err}`,
 | 
					  //     `Config decode failed at ${pos}, use default instead. Error :${err}`,
 | 
				
			||||||
    );
 | 
					  //   );
 | 
				
			||||||
    return defaultConfig;
 | 
					  //   return defaultConfig;
 | 
				
			||||||
  }
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Not use external lib
 | 
					  // Not use external lib
 | 
				
			||||||
  // const config = textutils.unserialiseJSON(configJson, {
 | 
					  const config = textutils.unserialiseJSON(configJson, {
 | 
				
			||||||
  //   parse_empty_array: true,
 | 
					    parse_empty_array: true,
 | 
				
			||||||
  // });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return config as AccessConfig;
 | 
					  return config as AccessConfig;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -155,11 +148,4 @@ function saveConfig(config: AccessConfig, filepath: string) {
 | 
				
			|||||||
  fp.close();
 | 
					  fp.close();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export { ToastConfig, UserGroupConfig, AccessConfig, loadConfig, saveConfig };
 | 
				
			||||||
  ToastConfig,
 | 
					 | 
				
			||||||
  UserGroupConfig,
 | 
					 | 
				
			||||||
  AccessConfig,
 | 
					 | 
				
			||||||
  loadConfig,
 | 
					 | 
				
			||||||
  saveConfig,
 | 
					 | 
				
			||||||
  setLog,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { CCLog, DAY } from "@/lib/ccLog";
 | 
					import { CCLog, DAY } from "@/lib/ccLog";
 | 
				
			||||||
import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config";
 | 
					import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
 | 
				
			||||||
import { createAccessControlCLI } from "./cli";
 | 
					import { createAccessControlCLI } from "./cli";
 | 
				
			||||||
import { launchAccessControlTUI } from "./tui";
 | 
					import { launchAccessControlTUI } from "./tui";
 | 
				
			||||||
import * as peripheralManager from "../lib/PeripheralManager";
 | 
					import * as peripheralManager from "../lib/PeripheralManager";
 | 
				
			||||||
@@ -9,7 +9,6 @@ const args = [...$vararg];
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Init Log
 | 
					// Init Log
 | 
				
			||||||
const log = new CCLog("accesscontrol.log", true, DAY);
 | 
					const log = new CCLog("accesscontrol.log", true, DAY);
 | 
				
			||||||
setLog(log);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Load Config
 | 
					// Load Config
 | 
				
			||||||
const configFilepath = `${shell.dir()}/access.config.json`;
 | 
					const configFilepath = `${shell.dir()}/access.config.json`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,8 @@ import {
 | 
				
			|||||||
  render,
 | 
					  render,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
  For,
 | 
					  For,
 | 
				
			||||||
 | 
					  Switch,
 | 
				
			||||||
 | 
					  Match,
 | 
				
			||||||
} from "../lib/ccTUI";
 | 
					} from "../lib/ccTUI";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AccessConfig,
 | 
					  AccessConfig,
 | 
				
			||||||
@@ -63,13 +65,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
  setConfig(() => loadedConfig);
 | 
					  setConfig(() => loadedConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Tab navigation functions
 | 
					  // Tab navigation functions
 | 
				
			||||||
  const tabNames = [
 | 
					  const tabNames = ["Basic", "Groups", "Welcome", "Warn", "Notice Toast"];
 | 
				
			||||||
    "Basic",
 | 
					 | 
				
			||||||
    "Groups",
 | 
					 | 
				
			||||||
    "Welcome Toast",
 | 
					 | 
				
			||||||
    "Warn Toast",
 | 
					 | 
				
			||||||
    "Notice Toast",
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const showError = (message: string) => {
 | 
					  const showError = (message: string) => {
 | 
				
			||||||
    setErrorState("show", true);
 | 
					    setErrorState("show", true);
 | 
				
			||||||
@@ -247,52 +243,63 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
  const BasicTab = () => {
 | 
					  const BasicTab = () => {
 | 
				
			||||||
    return div(
 | 
					    return div(
 | 
				
			||||||
      { class: "flex flex-col" },
 | 
					      { class: "flex flex-col" },
 | 
				
			||||||
      label({}, "Detect Interval (ms):"),
 | 
					      div(
 | 
				
			||||||
      input({
 | 
					        { class: "flex flex-row" },
 | 
				
			||||||
        type: "text",
 | 
					        label({}, "Detect Interval (ms):"),
 | 
				
			||||||
        value: () => config().detectInterval?.toString() ?? "",
 | 
					        input({
 | 
				
			||||||
        onInput: (value) => {
 | 
					          type: "text",
 | 
				
			||||||
          const num = validateNumber(value);
 | 
					          value: () => config().detectInterval?.toString() ?? "",
 | 
				
			||||||
          if (num !== null) setConfig("detectInterval", num);
 | 
					          onInput: (value) => {
 | 
				
			||||||
        },
 | 
					            const num = validateNumber(value);
 | 
				
			||||||
      }),
 | 
					            if (num !== null) setConfig("detectInterval", num);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
      label({}, "Watch Interval (ms):"),
 | 
					        }),
 | 
				
			||||||
      input({
 | 
					      ),
 | 
				
			||||||
        type: "text",
 | 
					      div(
 | 
				
			||||||
        value: () => config().watchInterval?.toString() ?? "",
 | 
					        { class: "flex flex-row" },
 | 
				
			||||||
        onInput: (value) => {
 | 
					        label({}, "Watch Interval (ms):"),
 | 
				
			||||||
          const num = validateNumber(value);
 | 
					        input({
 | 
				
			||||||
          if (num !== null) setConfig("watchInterval", num);
 | 
					          type: "text",
 | 
				
			||||||
        },
 | 
					          value: () => config().watchInterval?.toString() ?? "",
 | 
				
			||||||
      }),
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            const num = validateNumber(value);
 | 
				
			||||||
      label({}, "Notice Times:"),
 | 
					            if (num !== null) setConfig("watchInterval", num);
 | 
				
			||||||
      input({
 | 
					          },
 | 
				
			||||||
        type: "text",
 | 
					        }),
 | 
				
			||||||
        value: () => config().noticeTimes?.toString() ?? "",
 | 
					      ),
 | 
				
			||||||
        onInput: (value) => {
 | 
					      div(
 | 
				
			||||||
          const num = validateNumber(value);
 | 
					        { class: "flex flex-row" },
 | 
				
			||||||
          if (num !== null) setConfig("noticeTimes", num);
 | 
					        label({}, "Notice Times:"),
 | 
				
			||||||
        },
 | 
					        input({
 | 
				
			||||||
      }),
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => config().noticeTimes?.toString() ?? "",
 | 
				
			||||||
      label({}, "Detect Range:"),
 | 
					          onInput: (value) => {
 | 
				
			||||||
      input({
 | 
					            const num = validateNumber(value);
 | 
				
			||||||
        type: "text",
 | 
					            if (num !== null) setConfig("noticeTimes", num);
 | 
				
			||||||
        value: () => config().detectRange?.toString() ?? "",
 | 
					          },
 | 
				
			||||||
        onInput: (value) => {
 | 
					        }),
 | 
				
			||||||
          const num = validateNumber(value);
 | 
					      ),
 | 
				
			||||||
          if (num !== null) setConfig("detectRange", num);
 | 
					      div(
 | 
				
			||||||
        },
 | 
					        { class: "flex flex-row" },
 | 
				
			||||||
      }),
 | 
					        label({}, "Detect Range:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
      label({}, "Is Warn:"),
 | 
					          type: "text",
 | 
				
			||||||
      input({
 | 
					          value: () => config().detectRange?.toString() ?? "",
 | 
				
			||||||
        type: "checkbox",
 | 
					          onInput: (value) => {
 | 
				
			||||||
        checked: () => config().isWarn ?? false,
 | 
					            const num = validateNumber(value);
 | 
				
			||||||
        onChange: (checked) => setConfig("isWarn", checked),
 | 
					            if (num !== null) setConfig("detectRange", num);
 | 
				
			||||||
      }),
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-row" },
 | 
				
			||||||
 | 
					        label({}, "Is Warn:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "checkbox",
 | 
				
			||||||
 | 
					          checked: () => config().isWarn ?? false,
 | 
				
			||||||
 | 
					          onChange: (checked) => setConfig("isWarn", checked),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -301,7 +308,6 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  const GroupsTab = () => {
 | 
					  const GroupsTab = () => {
 | 
				
			||||||
    const groups = getAllGroups();
 | 
					    const groups = getAllGroups();
 | 
				
			||||||
    const selectedGroup = getSelectedGroup();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return div(
 | 
					    return div(
 | 
				
			||||||
      { class: "flex flex-row" },
 | 
					      { class: "flex flex-row" },
 | 
				
			||||||
@@ -309,7 +315,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
      div(
 | 
					      div(
 | 
				
			||||||
        { class: "flex flex-col" },
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
        label({}, "Groups:"),
 | 
					        label({}, "Groups:"),
 | 
				
			||||||
        For({ each: () => groups }, (group, index) =>
 | 
					        For({ each: () => groups, class: "flex flex-col" }, (group, index) =>
 | 
				
			||||||
          button(
 | 
					          button(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              class:
 | 
					              class:
 | 
				
			||||||
@@ -324,59 +330,64 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
      // Right side - Group details
 | 
					      // Right side - Group details
 | 
				
			||||||
      div(
 | 
					      div(
 | 
				
			||||||
        { class: "flex flex-col ml-2" },
 | 
					        { class: "flex flex-col ml-2" },
 | 
				
			||||||
        label({}, () => `Group: ${selectedGroup.groupName}`),
 | 
					        label({}, () => `Group: ${getSelectedGroup().groupName}`),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label({}, "Is Allowed:"),
 | 
					        div(
 | 
				
			||||||
        input({
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
          type: "checkbox",
 | 
					          label({}, "Is Allowed:"),
 | 
				
			||||||
          checked: () => selectedGroup.isAllowed,
 | 
					          input({
 | 
				
			||||||
          onChange: (checked) => {
 | 
					            type: "checkbox",
 | 
				
			||||||
            const groupIndex = selectedGroupIndex();
 | 
					            checked: () => getSelectedGroup().isAllowed,
 | 
				
			||||||
            if (groupIndex === 0) {
 | 
					            onChange: (checked) => {
 | 
				
			||||||
              const currentAdmin = config().adminGroupConfig;
 | 
					              const groupIndex = selectedGroupIndex();
 | 
				
			||||||
              setConfig("adminGroupConfig", {
 | 
					              if (groupIndex === 0) {
 | 
				
			||||||
                ...currentAdmin,
 | 
					                const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
                isAllowed: checked,
 | 
					                setConfig("adminGroupConfig", {
 | 
				
			||||||
              });
 | 
					                  ...currentAdmin,
 | 
				
			||||||
            } else {
 | 
					                  isAllowed: checked,
 | 
				
			||||||
              const actualIndex = groupIndex - 1;
 | 
					                });
 | 
				
			||||||
              const currentGroups = config().usersGroups;
 | 
					              } else {
 | 
				
			||||||
              const currentGroup = currentGroups[actualIndex];
 | 
					                const actualIndex = groupIndex - 1;
 | 
				
			||||||
              const newGroups = [...currentGroups];
 | 
					                const currentGroups = config().usersGroups;
 | 
				
			||||||
              newGroups[actualIndex] = {
 | 
					                const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
                ...currentGroup,
 | 
					                const newGroups = [...currentGroups];
 | 
				
			||||||
                isAllowed: checked,
 | 
					                newGroups[actualIndex] = {
 | 
				
			||||||
              };
 | 
					                  ...currentGroup,
 | 
				
			||||||
              setConfig("usersGroups", newGroups);
 | 
					                  isAllowed: checked,
 | 
				
			||||||
            }
 | 
					                };
 | 
				
			||||||
          },
 | 
					                setConfig("usersGroups", newGroups);
 | 
				
			||||||
        }),
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        label({}, "Is Notice:"),
 | 
					          }),
 | 
				
			||||||
        input({
 | 
					        ),
 | 
				
			||||||
          type: "checkbox",
 | 
					        div(
 | 
				
			||||||
          checked: () => selectedGroup.isNotice,
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
          onChange: (checked) => {
 | 
					          label({}, "Is Notice:"),
 | 
				
			||||||
            const groupIndex = selectedGroupIndex();
 | 
					          input({
 | 
				
			||||||
            if (groupIndex === 0) {
 | 
					            type: "checkbox",
 | 
				
			||||||
              const currentAdmin = config().adminGroupConfig;
 | 
					            checked: () => getSelectedGroup().isNotice,
 | 
				
			||||||
              setConfig("adminGroupConfig", {
 | 
					            onChange: (checked) => {
 | 
				
			||||||
                ...currentAdmin,
 | 
					              const groupIndex = selectedGroupIndex();
 | 
				
			||||||
                isNotice: checked,
 | 
					              if (groupIndex === 0) {
 | 
				
			||||||
              });
 | 
					                const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
            } else {
 | 
					                setConfig("adminGroupConfig", {
 | 
				
			||||||
              const actualIndex = groupIndex - 1;
 | 
					                  ...currentAdmin,
 | 
				
			||||||
              const currentGroups = config().usersGroups;
 | 
					                  isNotice: checked,
 | 
				
			||||||
              const currentGroup = currentGroups[actualIndex];
 | 
					                });
 | 
				
			||||||
              const newGroups = [...currentGroups];
 | 
					              } else {
 | 
				
			||||||
              newGroups[actualIndex] = {
 | 
					                const actualIndex = groupIndex - 1;
 | 
				
			||||||
                ...currentGroup,
 | 
					                const currentGroups = config().usersGroups;
 | 
				
			||||||
                isNotice: checked,
 | 
					                const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
              };
 | 
					                const newGroups = [...currentGroups];
 | 
				
			||||||
              setConfig("usersGroups", newGroups);
 | 
					                newGroups[actualIndex] = {
 | 
				
			||||||
            }
 | 
					                  ...currentGroup,
 | 
				
			||||||
          },
 | 
					                  isNotice: checked,
 | 
				
			||||||
        }),
 | 
					                };
 | 
				
			||||||
 | 
					                setConfig("usersGroups", newGroups);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label({}, "Group Users:"),
 | 
					        label({}, "Group Users:"),
 | 
				
			||||||
        // User management
 | 
					        // User management
 | 
				
			||||||
@@ -392,7 +403,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Users list
 | 
					        // Users list
 | 
				
			||||||
        For({ each: () => selectedGroup.groupUsers ?? [] }, (user) =>
 | 
					        For({ each: () => getSelectedGroup().groupUsers ?? [] }, (user) =>
 | 
				
			||||||
          div(
 | 
					          div(
 | 
				
			||||||
            { class: "flex flex-row items-center" },
 | 
					            { class: "flex flex-row items-center" },
 | 
				
			||||||
            label({}, user),
 | 
					            label({}, user),
 | 
				
			||||||
@@ -401,7 +412,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
                class: "ml-1 bg-red text-white",
 | 
					                class: "ml-1 bg-red text-white",
 | 
				
			||||||
                onClick: () => removeUser(user),
 | 
					                onClick: () => removeUser(user),
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              "Remove",
 | 
					              "X",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -419,9 +430,10 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
      const toastConfig = config()[toastType];
 | 
					      const toastConfig = config()[toastType];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return div(
 | 
					      return div(
 | 
				
			||||||
        { class: "flex flex-col" },
 | 
					        { class: "flex flex-col w-full" },
 | 
				
			||||||
        label({}, "Title (JSON):"),
 | 
					        label({}, "Title (JSON):"),
 | 
				
			||||||
        input({
 | 
					        input({
 | 
				
			||||||
 | 
					          class: "w-full",
 | 
				
			||||||
          type: "text",
 | 
					          type: "text",
 | 
				
			||||||
          value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
 | 
					          value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
 | 
				
			||||||
          onInput: (value) => {
 | 
					          onInput: (value) => {
 | 
				
			||||||
@@ -443,6 +455,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        label({}, "Message (JSON):"),
 | 
					        label({}, "Message (JSON):"),
 | 
				
			||||||
        input({
 | 
					        input({
 | 
				
			||||||
 | 
					          class: "w-full",
 | 
				
			||||||
          type: "text",
 | 
					          type: "text",
 | 
				
			||||||
          value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
 | 
					          value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
 | 
				
			||||||
          onInput: (value) => {
 | 
					          onInput: (value) => {
 | 
				
			||||||
@@ -462,38 +475,47 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label({}, "Prefix:"),
 | 
					        div(
 | 
				
			||||||
        input({
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
          type: "text",
 | 
					          label({}, "Prefix:"),
 | 
				
			||||||
          value: () => toastConfig?.prefix ?? "",
 | 
					          input({
 | 
				
			||||||
          onInput: (value) => {
 | 
					            type: "text",
 | 
				
			||||||
            const currentConfig = config();
 | 
					            value: () => toastConfig?.prefix ?? "",
 | 
				
			||||||
            const currentToast = currentConfig[toastType];
 | 
					            onInput: (value) => {
 | 
				
			||||||
            setConfig(toastType, { ...currentToast, prefix: value });
 | 
					              const currentConfig = config();
 | 
				
			||||||
          },
 | 
					              const currentToast = currentConfig[toastType];
 | 
				
			||||||
        }),
 | 
					              setConfig(toastType, { ...currentToast, prefix: value });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label({}, "Brackets:"),
 | 
					        div(
 | 
				
			||||||
        input({
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
          type: "text",
 | 
					          label({}, "Brackets:"),
 | 
				
			||||||
          value: () => toastConfig?.brackets ?? "",
 | 
					          input({
 | 
				
			||||||
          onInput: (value) => {
 | 
					            type: "text",
 | 
				
			||||||
            const currentConfig = config();
 | 
					            value: () => toastConfig?.brackets ?? "",
 | 
				
			||||||
            const currentToast = currentConfig[toastType];
 | 
					            onInput: (value) => {
 | 
				
			||||||
            setConfig(toastType, { ...currentToast, brackets: value });
 | 
					              const currentConfig = config();
 | 
				
			||||||
          },
 | 
					              const currentToast = currentConfig[toastType];
 | 
				
			||||||
        }),
 | 
					              setConfig(toastType, { ...currentToast, brackets: value });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label({}, "Bracket Color:"),
 | 
					        div(
 | 
				
			||||||
        input({
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
          type: "text",
 | 
					          label({}, "Bracket Color:"),
 | 
				
			||||||
          value: () => toastConfig?.bracketColor ?? "",
 | 
					          input({
 | 
				
			||||||
          onInput: (value) => {
 | 
					            type: "text",
 | 
				
			||||||
            const currentConfig = config();
 | 
					            value: () => toastConfig?.bracketColor ?? "",
 | 
				
			||||||
            const currentToast = currentConfig[toastType];
 | 
					            onInput: (value) => {
 | 
				
			||||||
            setConfig(toastType, { ...currentToast, bracketColor: value });
 | 
					              const currentConfig = config();
 | 
				
			||||||
          },
 | 
					              const currentToast = currentConfig[toastType];
 | 
				
			||||||
        }),
 | 
					              setConfig(toastType, { ...currentToast, bracketColor: value });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -533,13 +555,20 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
   * Tab Content Renderer
 | 
					   * Tab Content Renderer
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  const TabContent = () => {
 | 
					  const TabContent = () => {
 | 
				
			||||||
    const tab = currentTab();
 | 
					    return Switch(
 | 
				
			||||||
    if (tab === TABS.BASIC) return BasicTab();
 | 
					      { fallback: BasicTab() },
 | 
				
			||||||
    if (tab === TABS.GROUPS) return GroupsTab();
 | 
					      Match({ when: () => currentTab() === TABS.BASIC }, BasicTab()),
 | 
				
			||||||
    if (tab === TABS.WELCOME_TOAST) return WelcomeToastTab();
 | 
					      Match({ when: () => currentTab() === TABS.GROUPS }, GroupsTab()),
 | 
				
			||||||
    if (tab === TABS.WARN_TOAST) return WarnToastTab();
 | 
					      Match(
 | 
				
			||||||
    if (tab === TABS.NOTICE_TOAST) return NoticeToastTab();
 | 
					        { when: () => currentTab() === TABS.WELCOME_TOAST },
 | 
				
			||||||
    return BasicTab(); // fallback
 | 
					        WelcomeToastTab(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      Match({ when: () => currentTab() === TABS.WARN_TOAST }, WarnToastTab()),
 | 
				
			||||||
 | 
					      Match(
 | 
				
			||||||
 | 
					        { when: () => currentTab() === TABS.NOTICE_TOAST },
 | 
				
			||||||
 | 
					        NoticeToastTab(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -548,7 +577,10 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
  return div(
 | 
					  return div(
 | 
				
			||||||
    { class: "flex flex-col h-full" },
 | 
					    { class: "flex flex-col h-full" },
 | 
				
			||||||
    // Header
 | 
					    // Header
 | 
				
			||||||
    h1("Access Control Configuration"),
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row justify-center" },
 | 
				
			||||||
 | 
					      h1("Access Control Configuration"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Tab bar
 | 
					    // Tab bar
 | 
				
			||||||
    div(
 | 
					    div(
 | 
				
			||||||
@@ -565,7 +597,7 @@ const AccessControlTUI = () => {
 | 
				
			|||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Content area
 | 
					    // Content area
 | 
				
			||||||
    div({ class: "flex-1 p-2" }, TabContent()),
 | 
					    div({ class: "flex-1 p-2 w-screen" }, TabContent()),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Action buttons
 | 
					    // Action buttons
 | 
				
			||||||
    div(
 | 
					    div(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,32 @@ export interface StyleProps {
 | 
				
			|||||||
  textColor?: number;
 | 
					  textColor?: number;
 | 
				
			||||||
  /** Background color */
 | 
					  /** Background color */
 | 
				
			||||||
  backgroundColor?: number;
 | 
					  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
 | 
					 * UIObject node type
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type UIObjectType = 
 | 
					export type UIObjectType =
 | 
				
			||||||
  | "div" 
 | 
					  | "div"
 | 
				
			||||||
  | "label" 
 | 
					  | "label"
 | 
				
			||||||
  | "button" 
 | 
					  | "button"
 | 
				
			||||||
  | "input" 
 | 
					  | "input"
 | 
				
			||||||
  | "form"
 | 
					  | "form"
 | 
				
			||||||
  | "h1"
 | 
					  | "h1"
 | 
				
			||||||
  | "h2"
 | 
					  | "h2"
 | 
				
			||||||
  | "h3"
 | 
					  | "h3"
 | 
				
			||||||
  | "for"
 | 
					  | "for"
 | 
				
			||||||
  | "show"
 | 
					  | "show"
 | 
				
			||||||
  | "fragment";
 | 
					  | "switch"
 | 
				
			||||||
 | 
					  | "match"
 | 
				
			||||||
 | 
					  | "fragment"
 | 
				
			||||||
 | 
					  | "scroll-container";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * UIObject represents a node in the UI tree
 | 
					 * UIObject represents a node in the UI tree
 | 
				
			||||||
@@ -68,44 +97,47 @@ export type UIObjectType =
 | 
				
			|||||||
export class UIObject {
 | 
					export class UIObject {
 | 
				
			||||||
  /** Type of the UI object */
 | 
					  /** Type of the UI object */
 | 
				
			||||||
  type: UIObjectType;
 | 
					  type: UIObjectType;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Props passed to the component */
 | 
					  /** Props passed to the component */
 | 
				
			||||||
  props: Record<string, unknown>;
 | 
					  props: Record<string, unknown>;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Children UI objects */
 | 
					  /** Children UI objects */
 | 
				
			||||||
  children: UIObject[];
 | 
					  children: UIObject[];
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Parent UI object */
 | 
					  /** Parent UI object */
 | 
				
			||||||
  parent?: UIObject;
 | 
					  parent?: UIObject;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Computed layout after flexbox calculation */
 | 
					  /** Computed layout after flexbox calculation */
 | 
				
			||||||
  layout?: ComputedLayout;
 | 
					  layout?: ComputedLayout;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Layout properties parsed from class string */
 | 
					  /** Layout properties parsed from class string */
 | 
				
			||||||
  layoutProps: LayoutProps;
 | 
					  layoutProps: LayoutProps;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Style properties parsed from class string */
 | 
					  /** Style properties parsed from class string */
 | 
				
			||||||
  styleProps: StyleProps;
 | 
					  styleProps: StyleProps;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Whether this component is currently mounted */
 | 
					  /** Whether this component is currently mounted */
 | 
				
			||||||
  mounted: boolean;
 | 
					  mounted: boolean;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Cleanup functions to call when unmounting */
 | 
					  /** Cleanup functions to call when unmounting */
 | 
				
			||||||
  cleanupFns: (() => void)[];
 | 
					  cleanupFns: (() => void)[];
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** For text nodes - the text content (can be reactive) */
 | 
					  /** For text nodes - the text content (can be reactive) */
 | 
				
			||||||
  textContent?: string | Accessor<string>;
 | 
					  textContent?: string | Accessor<string>;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** Event handlers */
 | 
					  /** Event handlers */
 | 
				
			||||||
  handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
 | 
					  handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** For input text components - cursor position */
 | 
					  /** For input text components - cursor position */
 | 
				
			||||||
  cursorPos?: number;
 | 
					  cursorPos?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** For scroll containers - scroll state */
 | 
				
			||||||
 | 
					  scrollProps?: ScrollProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    type: UIObjectType,
 | 
					    type: UIObjectType,
 | 
				
			||||||
    props: Record<string, unknown> = {},
 | 
					    props: Record<string, unknown> = {},
 | 
				
			||||||
    children: UIObject[] = []
 | 
					    children: UIObject[] = [],
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.type = type;
 | 
					    this.type = type;
 | 
				
			||||||
    this.props = props;
 | 
					    this.props = props;
 | 
				
			||||||
@@ -115,45 +147,60 @@ export class UIObject {
 | 
				
			|||||||
    this.mounted = false;
 | 
					    this.mounted = false;
 | 
				
			||||||
    this.cleanupFns = [];
 | 
					    this.cleanupFns = [];
 | 
				
			||||||
    this.handlers = {};
 | 
					    this.handlers = {};
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Parse layout and styles from class prop
 | 
					    // Parse layout and styles from class prop
 | 
				
			||||||
    this.parseClassNames();
 | 
					    this.parseClassNames();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Extract event handlers
 | 
					    // Extract event handlers
 | 
				
			||||||
    this.extractHandlers();
 | 
					    this.extractHandlers();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Initialize cursor position for text inputs
 | 
					    // Initialize cursor position for text inputs
 | 
				
			||||||
    if (type === "input" && props.type !== "checkbox") {
 | 
					    if (type === "input" && props.type !== "checkbox") {
 | 
				
			||||||
      this.cursorPos = 0;
 | 
					      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
 | 
					   * Map color name to ComputerCraft colors API value
 | 
				
			||||||
   * 
 | 
					   *
 | 
				
			||||||
   * @param colorName - The color name from class (e.g., "white", "red")
 | 
					   * @param colorName - The color name from class (e.g., "white", "red")
 | 
				
			||||||
   * @returns The color value from colors API, or undefined if invalid
 | 
					   * @returns The color value from colors API, or undefined if invalid
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private parseColor(colorName: string): number | undefined {
 | 
					  private parseColor(colorName: string): number | undefined {
 | 
				
			||||||
    const colorMap: Record<string, number> = {
 | 
					    const colorMap: Record<string, number> = {
 | 
				
			||||||
      "white": colors.white,
 | 
					      white: colors.white,
 | 
				
			||||||
      "orange": colors.orange,
 | 
					      orange: colors.orange,
 | 
				
			||||||
      "magenta": colors.magenta,
 | 
					      magenta: colors.magenta,
 | 
				
			||||||
      "lightBlue": colors.lightBlue,
 | 
					      lightBlue: colors.lightBlue,
 | 
				
			||||||
      "yellow": colors.yellow,
 | 
					      yellow: colors.yellow,
 | 
				
			||||||
      "lime": colors.lime,
 | 
					      lime: colors.lime,
 | 
				
			||||||
      "pink": colors.pink,
 | 
					      pink: colors.pink,
 | 
				
			||||||
      "gray": colors.gray,
 | 
					      gray: colors.gray,
 | 
				
			||||||
      "lightGray": colors.lightGray,
 | 
					      lightGray: colors.lightGray,
 | 
				
			||||||
      "cyan": colors.cyan,
 | 
					      cyan: colors.cyan,
 | 
				
			||||||
      "purple": colors.purple,
 | 
					      purple: colors.purple,
 | 
				
			||||||
      "blue": colors.blue,
 | 
					      blue: colors.blue,
 | 
				
			||||||
      "brown": colors.brown,
 | 
					      brown: colors.brown,
 | 
				
			||||||
      "green": colors.green,
 | 
					      green: colors.green,
 | 
				
			||||||
      "red": colors.red,
 | 
					      red: colors.red,
 | 
				
			||||||
      "black": colors.black,
 | 
					      black: colors.black,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    return colorMap[colorName];
 | 
					    return colorMap[colorName];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -164,8 +211,8 @@ export class UIObject {
 | 
				
			|||||||
    const className = this.props.class as string | undefined;
 | 
					    const className = this.props.class as string | undefined;
 | 
				
			||||||
    if (className === undefined) return;
 | 
					    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) {
 | 
					    for (const cls of classes) {
 | 
				
			||||||
      // Flex direction
 | 
					      // Flex direction
 | 
				
			||||||
      if (cls === "flex-row") {
 | 
					      if (cls === "flex-row") {
 | 
				
			||||||
@@ -173,7 +220,7 @@ export class UIObject {
 | 
				
			|||||||
      } else if (cls === "flex-col") {
 | 
					      } else if (cls === "flex-col") {
 | 
				
			||||||
        this.layoutProps.flexDirection = "column";
 | 
					        this.layoutProps.flexDirection = "column";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Justify content
 | 
					      // Justify content
 | 
				
			||||||
      else if (cls === "justify-start") {
 | 
					      else if (cls === "justify-start") {
 | 
				
			||||||
        this.layoutProps.justifyContent = "start";
 | 
					        this.layoutProps.justifyContent = "start";
 | 
				
			||||||
@@ -184,7 +231,7 @@ export class UIObject {
 | 
				
			|||||||
      } else if (cls === "justify-between") {
 | 
					      } else if (cls === "justify-between") {
 | 
				
			||||||
        this.layoutProps.justifyContent = "between";
 | 
					        this.layoutProps.justifyContent = "between";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Align items
 | 
					      // Align items
 | 
				
			||||||
      else if (cls === "items-start") {
 | 
					      else if (cls === "items-start") {
 | 
				
			||||||
        this.layoutProps.alignItems = "start";
 | 
					        this.layoutProps.alignItems = "start";
 | 
				
			||||||
@@ -193,7 +240,7 @@ export class UIObject {
 | 
				
			|||||||
      } else if (cls === "items-end") {
 | 
					      } else if (cls === "items-end") {
 | 
				
			||||||
        this.layoutProps.alignItems = "end";
 | 
					        this.layoutProps.alignItems = "end";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Text color (text-<color>)
 | 
					      // Text color (text-<color>)
 | 
				
			||||||
      else if (cls.startsWith("text-")) {
 | 
					      else if (cls.startsWith("text-")) {
 | 
				
			||||||
        const colorName = cls.substring(5); // Remove "text-" prefix
 | 
					        const colorName = cls.substring(5); // Remove "text-" prefix
 | 
				
			||||||
@@ -202,7 +249,7 @@ export class UIObject {
 | 
				
			|||||||
          this.styleProps.textColor = color;
 | 
					          this.styleProps.textColor = color;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Background color (bg-<color>)
 | 
					      // Background color (bg-<color>)
 | 
				
			||||||
      else if (cls.startsWith("bg-")) {
 | 
					      else if (cls.startsWith("bg-")) {
 | 
				
			||||||
        const colorName = cls.substring(3); // Remove "bg-" prefix
 | 
					        const colorName = cls.substring(3); // Remove "bg-" prefix
 | 
				
			||||||
@@ -211,8 +258,38 @@ export class UIObject {
 | 
				
			|||||||
          this.styleProps.backgroundColor = color;
 | 
					          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
 | 
					    // Set defaults
 | 
				
			||||||
    if (this.type === "div") {
 | 
					    if (this.type === "div") {
 | 
				
			||||||
      this.layoutProps.flexDirection ??= "row";
 | 
					      this.layoutProps.flexDirection ??= "row";
 | 
				
			||||||
@@ -226,7 +303,11 @@ export class UIObject {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  private extractHandlers(): void {
 | 
					  private extractHandlers(): void {
 | 
				
			||||||
    for (const [key, value] of pairs(this.props)) {
 | 
					    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;
 | 
					        this.handlers[key] = value as (...args: unknown[]) => void;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -257,7 +338,7 @@ export class UIObject {
 | 
				
			|||||||
  mount(): void {
 | 
					  mount(): void {
 | 
				
			||||||
    if (this.mounted) return;
 | 
					    if (this.mounted) return;
 | 
				
			||||||
    this.mounted = true;
 | 
					    this.mounted = true;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Mount all children
 | 
					    // Mount all children
 | 
				
			||||||
    for (const child of this.children) {
 | 
					    for (const child of this.children) {
 | 
				
			||||||
      child.mount();
 | 
					      child.mount();
 | 
				
			||||||
@@ -270,12 +351,12 @@ export class UIObject {
 | 
				
			|||||||
  unmount(): void {
 | 
					  unmount(): void {
 | 
				
			||||||
    if (!this.mounted) return;
 | 
					    if (!this.mounted) return;
 | 
				
			||||||
    this.mounted = false;
 | 
					    this.mounted = false;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Unmount all children first
 | 
					    // Unmount all children first
 | 
				
			||||||
    for (const child of this.children) {
 | 
					    for (const child of this.children) {
 | 
				
			||||||
      child.unmount();
 | 
					      child.unmount();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Run cleanup functions
 | 
					    // Run cleanup functions
 | 
				
			||||||
    for (const cleanup of this.cleanupFns) {
 | 
					    for (const cleanup of this.cleanupFns) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
@@ -293,6 +374,75 @@ export class UIObject {
 | 
				
			|||||||
  onCleanup(fn: () => void): void {
 | 
					  onCleanup(fn: () => void): void {
 | 
				
			||||||
    this.cleanupFns.push(fn);
 | 
					    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),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { UIObject } from "./UIObject";
 | 
				
			|||||||
import { calculateLayout } from "./layout";
 | 
					import { calculateLayout } from "./layout";
 | 
				
			||||||
import { render as renderTree, clearScreen } from "./renderer";
 | 
					import { render as renderTree, clearScreen } from "./renderer";
 | 
				
			||||||
import { CCLog } from "../ccLog";
 | 
					import { CCLog } from "../ccLog";
 | 
				
			||||||
 | 
					import { findScrollContainer } from "./scrollContainer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Main application class
 | 
					 * Main application class
 | 
				
			||||||
@@ -138,7 +139,7 @@ export class Application {
 | 
				
			|||||||
      if (currentTime - this.lastBlinkTime >= this.BLINK_INTERVAL) {
 | 
					      if (currentTime - this.lastBlinkTime >= this.BLINK_INTERVAL) {
 | 
				
			||||||
        this.lastBlinkTime = currentTime;
 | 
					        this.lastBlinkTime = currentTime;
 | 
				
			||||||
        this.cursorBlinkState = !this.cursorBlinkState;
 | 
					        this.cursorBlinkState = !this.cursorBlinkState;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Only trigger render if we have a focused text input
 | 
					        // Only trigger render if we have a focused text input
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
          this.focusedNode !== undefined &&
 | 
					          this.focusedNode !== undefined &&
 | 
				
			||||||
@@ -176,6 +177,20 @@ export class Application {
 | 
				
			|||||||
          eventData[1] as number,
 | 
					          eventData[1] as number,
 | 
				
			||||||
          eventData[2] as number,
 | 
					          eventData[2] as number,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					      } else if (eventType === "mouse_scroll") {
 | 
				
			||||||
 | 
					        this.logger.debug(
 | 
				
			||||||
 | 
					          string.format(
 | 
				
			||||||
 | 
					            "eventLoop: Mouse scroll detected at (%d, %d) direction %d",
 | 
				
			||||||
 | 
					            eventData[1],
 | 
				
			||||||
 | 
					            eventData[2],
 | 
				
			||||||
 | 
					            eventData[0],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        this.handleMouseScroll(
 | 
				
			||||||
 | 
					          eventData[0] as number,
 | 
				
			||||||
 | 
					          eventData[1] as number,
 | 
				
			||||||
 | 
					          eventData[2] as number,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -213,7 +228,10 @@ export class Application {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
 | 
					    } else if (
 | 
				
			||||||
 | 
					      this.focusedNode !== undefined &&
 | 
				
			||||||
 | 
					      this.focusedNode.type === "input"
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      // Handle text input key events
 | 
					      // Handle text input key events
 | 
				
			||||||
      const type = this.focusedNode.props.type as string | undefined;
 | 
					      const type = this.focusedNode.props.type as string | undefined;
 | 
				
			||||||
      if (type !== "checkbox") {
 | 
					      if (type !== "checkbox") {
 | 
				
			||||||
@@ -231,10 +249,7 @@ export class Application {
 | 
				
			|||||||
    const valueProp = this.focusedNode.props.value;
 | 
					    const valueProp = this.focusedNode.props.value;
 | 
				
			||||||
    const onInputProp = this.focusedNode.props.onInput;
 | 
					    const onInputProp = this.focusedNode.props.onInput;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (typeof valueProp !== "function" || typeof onInputProp !== "function") {
 | 
				
			||||||
      typeof valueProp !== "function" ||
 | 
					 | 
				
			||||||
      typeof onInputProp !== "function"
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -422,6 +437,72 @@ export class Application {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Find the scrollable UI node at a specific screen position
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private findScrollableNodeAt(
 | 
				
			||||||
 | 
					    node: UIObject,
 | 
				
			||||||
 | 
					    x: number,
 | 
				
			||||||
 | 
					    y: number,
 | 
				
			||||||
 | 
					  ): UIObject | undefined {
 | 
				
			||||||
 | 
					    // Check children first (depth-first)
 | 
				
			||||||
 | 
					    for (const child of node.children) {
 | 
				
			||||||
 | 
					      const found = this.findScrollableNodeAt(child, x, y);
 | 
				
			||||||
 | 
					      if (found !== undefined) {
 | 
				
			||||||
 | 
					        return found;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check this node
 | 
				
			||||||
 | 
					    if (node.layout !== undefined) {
 | 
				
			||||||
 | 
					      const { x: nx, y: ny, width, height } = node.layout;
 | 
				
			||||||
 | 
					      const hit = x >= nx && x < nx + width && y >= ny && y < ny + height;
 | 
				
			||||||
 | 
					      if (hit) {
 | 
				
			||||||
 | 
					        this.logger.debug(
 | 
				
			||||||
 | 
					          string.format(
 | 
				
			||||||
 | 
					            "findNodeAt: Hit test TRUE for %s at (%d, %d)",
 | 
				
			||||||
 | 
					            node.type,
 | 
				
			||||||
 | 
					            nx,
 | 
				
			||||||
 | 
					            ny,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        // Only return scrollable elements
 | 
				
			||||||
 | 
					        if (node.type === "scroll-container") {
 | 
				
			||||||
 | 
					          this.logger.debug("findNodeAt: Node is scrollable, returning.");
 | 
				
			||||||
 | 
					          return node;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Handle mouse scroll events
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private handleMouseScroll(direction: number, x: number, y: number): void {
 | 
				
			||||||
 | 
					    if (this.root === undefined) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find which element was scrolled over
 | 
				
			||||||
 | 
					    const scrollContainer = this.findScrollableNodeAt(this.root, x, y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (scrollContainer?.scrollProps) {
 | 
				
			||||||
 | 
					      // Scroll by 1 line per wheel step
 | 
				
			||||||
 | 
					      const scrollAmount = direction * 1;
 | 
				
			||||||
 | 
					      scrollContainer.scrollBy(0, scrollAmount);
 | 
				
			||||||
 | 
					      this.needsRender = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.logger.debug(
 | 
				
			||||||
 | 
					        string.format(
 | 
				
			||||||
 | 
					          "handleMouseScroll: Scrolled container by %d, new position: (%d, %d)",
 | 
				
			||||||
 | 
					          scrollAmount,
 | 
				
			||||||
 | 
					          scrollContainer.scrollProps.scrollX,
 | 
				
			||||||
 | 
					          scrollContainer.scrollProps.scrollY,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Collect all interactive elements in the tree
 | 
					   * Collect all interactive elements in the tree
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -176,11 +176,11 @@ export function input(props: InputProps): UIObject {
 | 
				
			|||||||
  const normalizedProps = { ...props };
 | 
					  const normalizedProps = { ...props };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (Array.isArray(normalizedProps.value)) {
 | 
					  if (Array.isArray(normalizedProps.value)) {
 | 
				
			||||||
    normalizedProps.value = (normalizedProps.value)[0];
 | 
					    normalizedProps.value = normalizedProps.value[0];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (Array.isArray(normalizedProps.checked)) {
 | 
					  if (Array.isArray(normalizedProps.checked)) {
 | 
				
			||||||
    normalizedProps.checked = (normalizedProps.checked)[0];
 | 
					    normalizedProps.checked = normalizedProps.checked[0];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return new UIObject("input", normalizedProps, []);
 | 
					  return new UIObject("input", normalizedProps, []);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,19 +23,35 @@ export type ShowProps = {
 | 
				
			|||||||
  fallback?: UIObject;
 | 
					  fallback?: UIObject;
 | 
				
			||||||
} & Record<string, unknown>;
 | 
					} & Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Props for Switch component
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type SwitchProps = {
 | 
				
			||||||
 | 
					  /** Optional fallback to show when no Match condition is met */
 | 
				
			||||||
 | 
					  fallback?: UIObject;
 | 
				
			||||||
 | 
					} & Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Props for Match component
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type MatchProps = {
 | 
				
			||||||
 | 
					  /** Condition accessor - when truthy, this Match will be selected */
 | 
				
			||||||
 | 
					  when: Accessor<boolean>;
 | 
				
			||||||
 | 
					} & Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * For component - renders a list of items
 | 
					 * For component - renders a list of items
 | 
				
			||||||
 * Efficiently updates when the array changes
 | 
					 * Efficiently updates when the array changes
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @template T - The type of items in the array
 | 
					 * @template T - The type of items in the array
 | 
				
			||||||
 * @param props - Props containing the array accessor
 | 
					 * @param props - Props containing the array accessor
 | 
				
			||||||
 * @param renderFn - Function to render each item
 | 
					 * @param renderFn - Function to render each item
 | 
				
			||||||
 * @returns UIObject representing the list
 | 
					 * @returns UIObject representing the list
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * ```typescript
 | 
					 * ```typescript
 | 
				
			||||||
 * const [todos, setTodos] = createStore<Todo[]>([]);
 | 
					 * const [todos, setTodos] = createStore<Todo[]>([]);
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * For({ each: () => todos },
 | 
					 * For({ each: () => todos },
 | 
				
			||||||
 *   (todo, i) => div({ class: "flex flex-row" },
 | 
					 *   (todo, i) => div({ class: "flex flex-row" },
 | 
				
			||||||
 *     label({}, () => todo.title),
 | 
					 *     label({}, () => todo.title),
 | 
				
			||||||
@@ -46,24 +62,24 @@ export type ShowProps = {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export function For<T>(
 | 
					export function For<T>(
 | 
				
			||||||
  props: ForProps<T>,
 | 
					  props: ForProps<T>,
 | 
				
			||||||
  renderFn: (item: T, index: Accessor<number>) => UIObject
 | 
					  renderFn: (item: T, index: Accessor<number>) => UIObject,
 | 
				
			||||||
): UIObject {
 | 
					): UIObject {
 | 
				
			||||||
  const container = new UIObject("for", props, []);
 | 
					  const container = new UIObject("for", props, []);
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // Track rendered items
 | 
					  // Track rendered items
 | 
				
			||||||
  let renderedItems: UIObject[] = [];
 | 
					  let renderedItems: UIObject[] = [];
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Update the list when the array changes
 | 
					   * Update the list when the array changes
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  const updateList = () => {
 | 
					  const updateList = () => {
 | 
				
			||||||
    const items = props.each();
 | 
					    const items = props.each();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Clear old items
 | 
					    // Clear old items
 | 
				
			||||||
    renderedItems.forEach(item => item.unmount());
 | 
					    renderedItems.forEach((item) => item.unmount());
 | 
				
			||||||
    container.children = [];
 | 
					    container.children = [];
 | 
				
			||||||
    renderedItems = [];
 | 
					    renderedItems = [];
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Render new items
 | 
					    // Render new items
 | 
				
			||||||
    items.forEach((item, index) => {
 | 
					    items.forEach((item, index) => {
 | 
				
			||||||
      const indexAccessor = () => index;
 | 
					      const indexAccessor = () => index;
 | 
				
			||||||
@@ -74,26 +90,26 @@ export function For<T>(
 | 
				
			|||||||
      rendered.mount();
 | 
					      rendered.mount();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // Create effect to watch for changes
 | 
					  // Create effect to watch for changes
 | 
				
			||||||
  createEffect(() => {
 | 
					  createEffect(() => {
 | 
				
			||||||
    updateList();
 | 
					    updateList();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  return container;
 | 
					  return container;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Show component - conditionally renders content
 | 
					 * Show component - conditionally renders content
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @param props - Props containing condition and optional fallback
 | 
					 * @param props - Props containing condition and optional fallback
 | 
				
			||||||
 * @param child - Content to show when condition is true
 | 
					 * @param child - Content to show when condition is true
 | 
				
			||||||
 * @returns UIObject representing the conditional content
 | 
					 * @returns UIObject representing the conditional content
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * ```typescript
 | 
					 * ```typescript
 | 
				
			||||||
 * const [loggedIn, setLoggedIn] = createSignal(false);
 | 
					 * const [loggedIn, setLoggedIn] = createSignal(false);
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * Show(
 | 
					 * Show(
 | 
				
			||||||
 *   {
 | 
					 *   {
 | 
				
			||||||
 *     when: loggedIn,
 | 
					 *     when: loggedIn,
 | 
				
			||||||
@@ -105,21 +121,21 @@ export function For<T>(
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export function Show(props: ShowProps, child: UIObject): UIObject {
 | 
					export function Show(props: ShowProps, child: UIObject): UIObject {
 | 
				
			||||||
  const container = new UIObject("show", props, []);
 | 
					  const container = new UIObject("show", props, []);
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  let currentChild: UIObject | undefined = undefined;
 | 
					  let currentChild: UIObject | undefined = undefined;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Update which child is shown based on condition
 | 
					   * Update which child is shown based on condition
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  const updateChild = () => {
 | 
					  const updateChild = () => {
 | 
				
			||||||
    const condition = props.when();
 | 
					    const condition = props.when();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Unmount current child
 | 
					    // Unmount current child
 | 
				
			||||||
    if (currentChild !== undefined) {
 | 
					    if (currentChild !== undefined) {
 | 
				
			||||||
      currentChild.unmount();
 | 
					      currentChild.unmount();
 | 
				
			||||||
      container.removeChild(currentChild);
 | 
					      container.removeChild(currentChild);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Mount appropriate child
 | 
					    // Mount appropriate child
 | 
				
			||||||
    if (condition) {
 | 
					    if (condition) {
 | 
				
			||||||
      currentChild = child;
 | 
					      currentChild = child;
 | 
				
			||||||
@@ -129,17 +145,115 @@ export function Show(props: ShowProps, child: UIObject): UIObject {
 | 
				
			|||||||
      currentChild = undefined;
 | 
					      currentChild = undefined;
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (currentChild !== undefined) {
 | 
					    if (currentChild !== undefined) {
 | 
				
			||||||
      container.appendChild(currentChild);
 | 
					      container.appendChild(currentChild);
 | 
				
			||||||
      currentChild.mount();
 | 
					      currentChild.mount();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // Create effect to watch for condition changes
 | 
					  // Create effect to watch for condition changes
 | 
				
			||||||
  createEffect(() => {
 | 
					  createEffect(() => {
 | 
				
			||||||
    updateChild();
 | 
					    updateChild();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
 | 
					  return container;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Switch component - renders the first Match whose condition is truthy
 | 
				
			||||||
 | 
					 * Similar to a switch statement or if/else if/else chain
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param props - Props containing optional fallback
 | 
				
			||||||
 | 
					 * @param matches - Array of Match components to evaluate
 | 
				
			||||||
 | 
					 * @returns UIObject representing the switch statement
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @example
 | 
				
			||||||
 | 
					 * ```typescript
 | 
				
			||||||
 | 
					 * const [status, setStatus] = createSignal("loading");
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Switch(
 | 
				
			||||||
 | 
					 *   { fallback: div({}, "Unknown status") },
 | 
				
			||||||
 | 
					 *   Match({ when: () => status() === "loading" }, div({}, "Loading...")),
 | 
				
			||||||
 | 
					 *   Match({ when: () => status() === "success" }, div({}, "Success!")),
 | 
				
			||||||
 | 
					 *   Match({ when: () => status() === "error" }, div({}, "Error occurred"))
 | 
				
			||||||
 | 
					 * )
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function Switch(props: SwitchProps, ...matches: UIObject[]): UIObject {
 | 
				
			||||||
 | 
					  const container = new UIObject("switch", props, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let currentChild: UIObject | undefined = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Evaluate all Match conditions and show the first truthy one
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const updateChild = () => {
 | 
				
			||||||
 | 
					    // Unmount current child
 | 
				
			||||||
 | 
					    if (currentChild !== undefined) {
 | 
				
			||||||
 | 
					      currentChild.unmount();
 | 
				
			||||||
 | 
					      container.removeChild(currentChild);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find the first Match with a truthy condition
 | 
				
			||||||
 | 
					    for (const match of matches) {
 | 
				
			||||||
 | 
					      if (match.type === "match") {
 | 
				
			||||||
 | 
					        const matchProps = match.props as MatchProps;
 | 
				
			||||||
 | 
					        const condition = matchProps.when();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          condition !== undefined &&
 | 
				
			||||||
 | 
					          condition !== null &&
 | 
				
			||||||
 | 
					          condition !== false
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          // This Match's condition is truthy, use it
 | 
				
			||||||
 | 
					          if (match.children.length > 0) {
 | 
				
			||||||
 | 
					            currentChild = match.children[0];
 | 
				
			||||||
 | 
					            container.appendChild(currentChild);
 | 
				
			||||||
 | 
					            currentChild.mount();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // No Match condition was truthy, use fallback if available
 | 
				
			||||||
 | 
					    if (props.fallback !== undefined) {
 | 
				
			||||||
 | 
					      currentChild = props.fallback;
 | 
				
			||||||
 | 
					      container.appendChild(currentChild);
 | 
				
			||||||
 | 
					      currentChild.mount();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      currentChild = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create effect to watch for condition changes
 | 
				
			||||||
 | 
					  createEffect(() => {
 | 
				
			||||||
 | 
					    updateChild();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return container;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Match component - represents a single case in a Switch
 | 
				
			||||||
 | 
					 * Should only be used as a child of Switch
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param props - Props containing the condition
 | 
				
			||||||
 | 
					 * @param child - Content to render when condition is truthy
 | 
				
			||||||
 | 
					 * @returns UIObject representing this match case
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @example
 | 
				
			||||||
 | 
					 * ```typescript
 | 
				
			||||||
 | 
					 * const [color, setColor] = createSignal("red");
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Match({ when: () => color() === "red" },
 | 
				
			||||||
 | 
					 *   div({ class: "text-red" }, "Stop")
 | 
				
			||||||
 | 
					 * )
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function Match(props: MatchProps, child: UIObject): UIObject {
 | 
				
			||||||
 | 
					  const container = new UIObject("match", props, [child]);
 | 
				
			||||||
 | 
					  child.parent = container;
 | 
				
			||||||
  return container;
 | 
					  return container;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,6 +136,37 @@ render(App);
 | 
				
			|||||||
    - `fallback?: UIObject`: 当 `when` 返回 `false` 时要渲染的组件。
 | 
					    - `fallback?: UIObject`: 当 `when` 返回 `false` 时要渲染的组件。
 | 
				
			||||||
  - `child`: 当 `when` 返回 `true` 时要渲染的组件。
 | 
					  - `child`: 当 `when` 返回 `true` 时要渲染的组件。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `<Switch>` and `<Match>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more complex conditional logic involving multiple branches (like an `if/else if/else` chain), you can use the `<Switch>` and `<Match>` components. `<Switch>` evaluates its `<Match>` children in order and renders the first one whose `when` prop evaluates to a truthy value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An optional `fallback` prop on the `<Switch>` component will be rendered if none of the `<Match>` conditions are met.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Example:**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { createSignal } from 'cc-tui';
 | 
				
			||||||
 | 
					import { Switch, Match } from 'cc-tui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TrafficLight() {
 | 
				
			||||||
 | 
					  const [color, setColor] = createSignal('red');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Switch fallback={<span>Signal is broken</span>}>
 | 
				
			||||||
 | 
					      <Match when={color() === 'red'}>
 | 
				
			||||||
 | 
					        <p style={{ color: 'red' }}>Stop</p>
 | 
				
			||||||
 | 
					      </Match>
 | 
				
			||||||
 | 
					      <Match when={color() === 'yellow'}>
 | 
				
			||||||
 | 
					        <p style={{ color: 'yellow' }}>Slow Down</p>
 | 
				
			||||||
 | 
					      </Match>
 | 
				
			||||||
 | 
					      <Match when={color() === 'green'}>
 | 
				
			||||||
 | 
					        <p style={{ color: 'green' }}>Go</p>
 | 
				
			||||||
 | 
					      </Match>
 | 
				
			||||||
 | 
					    </Switch>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 3. 布局系统 (Flexbox)
 | 
					## 3. 布局系统 (Flexbox)
 | 
				
			||||||
@@ -170,6 +201,50 @@ render(App);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
颜色名称直接映射自 `tweaked.cc` 的 `colors` API: `white`, `orange`, `magenta`, `lightBlue`, `yellow`, `lime`, `pink`, `gray`, `lightGray`, `cyan`, `purple`, `blue`, `brown`, `green`, `red`, `black`.
 | 
					颜色名称直接映射自 `tweaked.cc` 的 `colors` API: `white`, `orange`, `magenta`, `lightBlue`, `yellow`, `lime`, `pink`, `gray`, `lightGray`, `cyan`, `purple`, `blue`, `brown`, `green`, `red`, `black`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Sizing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Control the width of an element using the `w` property in the `style` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   `w: <number>`: Sets a fixed width in characters.
 | 
				
			||||||
 | 
					-   `w: "full"`: Sets the width to 100% of its parent's content area.
 | 
				
			||||||
 | 
					-   `w: "screen"`: Sets the width to the full width of the terminal screen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Examples:**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					// A box with a fixed width of 20 characters
 | 
				
			||||||
 | 
					<Box style={{ w: 20, border: 'single' }}>Fixed Width</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A box that fills its parent's width
 | 
				
			||||||
 | 
					<Box style={{ w: 'full', border: 'single' }}>Full Width</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A box that spans the entire screen width
 | 
				
			||||||
 | 
					<Box style={{ w: 'screen', border: 'single' }}>Screen Width</Box>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Height
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Control the height of an element using the `h` property in the `style` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   `h: <number>`: Sets a fixed height in characters.
 | 
				
			||||||
 | 
					-   `h: "full"`: Sets the height to 100% of its parent's content area.
 | 
				
			||||||
 | 
					-   `h: "screen"`: Sets the height to the full height of the terminal screen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Examples:**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					// A box with a fixed height of 5 characters
 | 
				
			||||||
 | 
					<Box style={{ h: 5, border: 'single' }}>Fixed Height</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A box that fills its parent's height
 | 
				
			||||||
 | 
					<Box style={{ h: 'full', border: 'single' }}>Full Height</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A box that spans the entire screen height
 | 
				
			||||||
 | 
					<Box style={{ h: 'screen', border: 'single' }}>Screen Height</Box>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 4. 响应式系统 (Reactivity System)
 | 
					## 4. 响应式系统 (Reactivity System)
 | 
				
			||||||
@@ -367,4 +442,6 @@ pnpm dlx eslint src/**/*.ts
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# OR
 | 
					# OR
 | 
				
			||||||
just lint
 | 
					just lint
 | 
				
			||||||
```
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					为ccTUI添加滚动支持,当内容放不下的时候可以使鼠标滚轮滚动查看更多内容,最好能够实现滚动条。
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,26 @@ export {
 | 
				
			|||||||
} from "./components";
 | 
					} from "./components";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Control flow
 | 
					// Control flow
 | 
				
			||||||
export { For, Show, type ForProps, type ShowProps } from "./controlFlow";
 | 
					export {
 | 
				
			||||||
 | 
					  For,
 | 
				
			||||||
 | 
					  Show,
 | 
				
			||||||
 | 
					  Switch,
 | 
				
			||||||
 | 
					  Match,
 | 
				
			||||||
 | 
					  type ForProps,
 | 
				
			||||||
 | 
					  type ShowProps,
 | 
				
			||||||
 | 
					  type SwitchProps,
 | 
				
			||||||
 | 
					  type MatchProps,
 | 
				
			||||||
 | 
					} from "./controlFlow";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Scroll container
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  ScrollContainer,
 | 
				
			||||||
 | 
					  isScrollContainer,
 | 
				
			||||||
 | 
					  findScrollContainer,
 | 
				
			||||||
 | 
					  isPointVisible,
 | 
				
			||||||
 | 
					  screenToContent,
 | 
				
			||||||
 | 
					  type ScrollContainerProps,
 | 
				
			||||||
 | 
					} from "./scrollContainer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Application
 | 
					// Application
 | 
				
			||||||
export { Application, render } from "./application";
 | 
					export { Application, render } from "./application";
 | 
				
			||||||
@@ -51,6 +70,7 @@ export {
 | 
				
			|||||||
  UIObject,
 | 
					  UIObject,
 | 
				
			||||||
  type LayoutProps,
 | 
					  type LayoutProps,
 | 
				
			||||||
  type StyleProps,
 | 
					  type StyleProps,
 | 
				
			||||||
 | 
					  type ScrollProps,
 | 
				
			||||||
  type ComputedLayout,
 | 
					  type ComputedLayout,
 | 
				
			||||||
  type BaseProps,
 | 
					  type BaseProps,
 | 
				
			||||||
} from "./UIObject";
 | 
					} from "./UIObject";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,81 +5,209 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { UIObject } from "./UIObject";
 | 
					import { UIObject } from "./UIObject";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get the terminal dimensions
 | 
				
			||||||
 | 
					 * @returns Terminal width and height
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function getTerminalSize(): { width: number; height: number } {
 | 
				
			||||||
 | 
					  const [w, h] = term.getSize();
 | 
				
			||||||
 | 
					  return { width: w, height: h };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Measure the natural size of a UI element
 | 
					 * Measure the natural size of a UI element
 | 
				
			||||||
 * This determines how much space an element wants to take up
 | 
					 * This determines how much space an element wants to take up
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @param node - The UI node to measure
 | 
					 * @param node - The UI node to measure
 | 
				
			||||||
 | 
					 * @param parentWidth - Available width from parent (for percentage calculations)
 | 
				
			||||||
 | 
					 * @param parentHeight - Available height from parent (for percentage calculations)
 | 
				
			||||||
 * @returns Width and height of the element
 | 
					 * @returns Width and height of the element
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function measureNode(node: UIObject): { width: number; height: number } {
 | 
					function measureNode(
 | 
				
			||||||
 | 
					  node: UIObject,
 | 
				
			||||||
 | 
					  parentWidth?: number,
 | 
				
			||||||
 | 
					  parentHeight?: number,
 | 
				
			||||||
 | 
					): { width: number; height: number } {
 | 
				
			||||||
  // Get text content if it exists
 | 
					  // Get text content if it exists
 | 
				
			||||||
  const getTextContent = (): string => {
 | 
					  const getTextContent = (): string => {
 | 
				
			||||||
    if (node.textContent !== undefined) {
 | 
					    if (node.textContent !== undefined) {
 | 
				
			||||||
      if (typeof node.textContent === "function") {
 | 
					      if (typeof node.textContent === "function") {
 | 
				
			||||||
        return (node.textContent)();
 | 
					        return node.textContent();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return node.textContent;
 | 
					      return node.textContent;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // For nodes with text children, get their content
 | 
					    // For nodes with text children, get their content
 | 
				
			||||||
    if (node.children.length > 0 && node.children[0].textContent !== undefined) {
 | 
					    if (
 | 
				
			||||||
 | 
					      node.children.length > 0 &&
 | 
				
			||||||
 | 
					      node.children[0].textContent !== undefined
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      const child = node.children[0];
 | 
					      const child = node.children[0];
 | 
				
			||||||
      if (typeof child.textContent === "function") {
 | 
					      if (typeof child.textContent === "function") {
 | 
				
			||||||
        return (child.textContent)();
 | 
					        return child.textContent();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return child.textContent!;
 | 
					      return child.textContent!;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    return "";
 | 
					    return "";
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check for explicit size styling first
 | 
				
			||||||
 | 
					  let measuredWidth: number | undefined;
 | 
				
			||||||
 | 
					  let measuredHeight: number | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle width styling
 | 
				
			||||||
 | 
					  if (node.styleProps.width !== undefined) {
 | 
				
			||||||
 | 
					    if (node.styleProps.width === "screen") {
 | 
				
			||||||
 | 
					      const termSize = getTerminalSize();
 | 
				
			||||||
 | 
					      measuredWidth = termSize.width;
 | 
				
			||||||
 | 
					    } else if (node.styleProps.width === "full" && parentWidth !== undefined) {
 | 
				
			||||||
 | 
					      measuredWidth = parentWidth;
 | 
				
			||||||
 | 
					    } else if (typeof node.styleProps.width === "number") {
 | 
				
			||||||
 | 
					      measuredWidth = node.styleProps.width;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle height styling
 | 
				
			||||||
 | 
					  if (node.styleProps.height !== undefined) {
 | 
				
			||||||
 | 
					    if (node.styleProps.height === "screen") {
 | 
				
			||||||
 | 
					      const termSize = getTerminalSize();
 | 
				
			||||||
 | 
					      measuredHeight = termSize.height;
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      node.styleProps.height === "full" &&
 | 
				
			||||||
 | 
					      parentHeight !== undefined
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      measuredHeight = parentHeight;
 | 
				
			||||||
 | 
					    } else if (typeof node.styleProps.height === "number") {
 | 
				
			||||||
 | 
					      measuredHeight = node.styleProps.height;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  switch (node.type) {
 | 
					  switch (node.type) {
 | 
				
			||||||
    case "label":
 | 
					    case "label":
 | 
				
			||||||
    case "h1":
 | 
					    case "h1":
 | 
				
			||||||
    case "h2":
 | 
					    case "h2":
 | 
				
			||||||
    case "h3": {
 | 
					    case "h3": {
 | 
				
			||||||
      const text = getTextContent();
 | 
					      const text = getTextContent();
 | 
				
			||||||
      return { width: text.length, height: 1 };
 | 
					      const naturalWidth = text.length;
 | 
				
			||||||
 | 
					      const naturalHeight = 1;
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        width: measuredWidth ?? naturalWidth,
 | 
				
			||||||
 | 
					        height: measuredHeight ?? naturalHeight,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    case "button": {
 | 
					    case "button": {
 | 
				
			||||||
      const text = getTextContent();
 | 
					      const text = getTextContent();
 | 
				
			||||||
      // Buttons have brackets around them: [text]
 | 
					      // Buttons have brackets around them: [text]
 | 
				
			||||||
      return { width: text.length + 2, height: 1 };
 | 
					      const naturalWidth = text.length + 2;
 | 
				
			||||||
 | 
					      const naturalHeight = 1;
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        width: measuredWidth ?? naturalWidth,
 | 
				
			||||||
 | 
					        height: measuredHeight ?? naturalHeight,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    case "input": {
 | 
					    case "input": {
 | 
				
			||||||
      const type = node.props.type as string | undefined;
 | 
					      const type = node.props.type as string | undefined;
 | 
				
			||||||
      if (type === "checkbox") {
 | 
					      if (type === "checkbox") {
 | 
				
			||||||
        return { width: 3, height: 1 }; // [X] or [ ]
 | 
					        const naturalWidth = 3; // [X] or [ ]
 | 
				
			||||||
 | 
					        const naturalHeight = 1;
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          width: measuredWidth ?? naturalWidth,
 | 
				
			||||||
 | 
					          height: measuredHeight ?? naturalHeight,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Text input - use a default width or from props
 | 
					      // Text input - use a default width or from props
 | 
				
			||||||
      const width = (node.props.width as number | undefined) ?? 20;
 | 
					      const defaultWidth = (node.props.width as number | undefined) ?? 20;
 | 
				
			||||||
      return { width, height: 1 };
 | 
					      const naturalHeight = 1;
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        width: measuredWidth ?? defaultWidth,
 | 
				
			||||||
 | 
					        height: measuredHeight ?? naturalHeight,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    case "div":
 | 
					    case "div":
 | 
				
			||||||
    case "form":
 | 
					    case "form":
 | 
				
			||||||
    case "for":
 | 
					    case "for":
 | 
				
			||||||
    case "show":
 | 
					    case "show":
 | 
				
			||||||
    case "fragment": {
 | 
					    case "switch":
 | 
				
			||||||
 | 
					    case "match":
 | 
				
			||||||
 | 
					    case "fragment":
 | 
				
			||||||
 | 
					    case "scroll-container": {
 | 
				
			||||||
      // Container elements size based on their children
 | 
					      // Container elements size based on their children
 | 
				
			||||||
      let totalWidth = 0;
 | 
					      let totalWidth = 0;
 | 
				
			||||||
      let totalHeight = 0;
 | 
					      let totalHeight = 0;
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (node.children.length === 0) {
 | 
					      if (node.children.length === 0) {
 | 
				
			||||||
        return { width: 0, height: 0 };
 | 
					        const naturalWidth = 0;
 | 
				
			||||||
 | 
					        const naturalHeight = 0;
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          width: measuredWidth ?? naturalWidth,
 | 
				
			||||||
 | 
					          height: measuredHeight ?? naturalHeight,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      const direction = node.layoutProps.flexDirection ?? "row";
 | 
					      const direction = node.layoutProps.flexDirection ?? "row";
 | 
				
			||||||
      const isFlex = node.type === "div" || node.type === "form";
 | 
					      const isFlex = node.type === "div" || node.type === "form";
 | 
				
			||||||
      const gap = isFlex ? 1 : 0;
 | 
					      const gap = isFlex ? 1 : 0;
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
 | 
					      // For scroll containers, calculate content size and update scroll bounds
 | 
				
			||||||
 | 
					      if (node.type === "scroll-container" && node.scrollProps) {
 | 
				
			||||||
 | 
					        // Calculate actual content size without viewport constraints
 | 
				
			||||||
 | 
					        const childParentWidth = undefined; // No width constraint for content measurement
 | 
				
			||||||
 | 
					        const childParentHeight = undefined; // No height constraint for content measurement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (direction === "row") {
 | 
				
			||||||
 | 
					          for (const child of node.children) {
 | 
				
			||||||
 | 
					            const childSize = measureNode(
 | 
				
			||||||
 | 
					              child,
 | 
				
			||||||
 | 
					              childParentWidth,
 | 
				
			||||||
 | 
					              childParentHeight,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            totalWidth += childSize.width;
 | 
				
			||||||
 | 
					            totalHeight = math.max(totalHeight, childSize.height);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (node.children.length > 1) {
 | 
				
			||||||
 | 
					            totalWidth += gap * (node.children.length - 1);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          for (const child of node.children) {
 | 
				
			||||||
 | 
					            const childSize = measureNode(
 | 
				
			||||||
 | 
					              child,
 | 
				
			||||||
 | 
					              childParentWidth,
 | 
				
			||||||
 | 
					              childParentHeight,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            totalWidth = math.max(totalWidth, childSize.width);
 | 
				
			||||||
 | 
					            totalHeight += childSize.height;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (node.children.length > 1) {
 | 
				
			||||||
 | 
					            totalHeight += gap * (node.children.length - 1);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update scroll bounds with actual content size
 | 
				
			||||||
 | 
					        node.updateScrollBounds(totalWidth, totalHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Return viewport size as the container size
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          width: measuredWidth ?? node.scrollProps.viewportWidth,
 | 
				
			||||||
 | 
					          height: measuredHeight ?? node.scrollProps.viewportHeight,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Calculate available space for children (non-scroll containers)
 | 
				
			||||||
 | 
					      const childParentWidth = measuredWidth ?? parentWidth;
 | 
				
			||||||
 | 
					      const childParentHeight = measuredHeight ?? parentHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (direction === "row") {
 | 
					      if (direction === "row") {
 | 
				
			||||||
        // In row direction, width is sum of children, height is max
 | 
					        // In row direction, width is sum of children, height is max
 | 
				
			||||||
        for (const child of node.children) {
 | 
					        for (const child of node.children) {
 | 
				
			||||||
          const childSize = measureNode(child);
 | 
					          const childSize = measureNode(
 | 
				
			||||||
 | 
					            child,
 | 
				
			||||||
 | 
					            childParentWidth,
 | 
				
			||||||
 | 
					            childParentHeight,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          totalWidth += childSize.width;
 | 
					          totalWidth += childSize.width;
 | 
				
			||||||
          totalHeight = math.max(totalHeight, childSize.height);
 | 
					          totalHeight = math.max(totalHeight, childSize.height);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -89,7 +217,11 @@ function measureNode(node: UIObject): { width: number; height: number } {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        // In column direction, height is sum of children, width is max
 | 
					        // In column direction, height is sum of children, width is max
 | 
				
			||||||
        for (const child of node.children) {
 | 
					        for (const child of node.children) {
 | 
				
			||||||
          const childSize = measureNode(child);
 | 
					          const childSize = measureNode(
 | 
				
			||||||
 | 
					            child,
 | 
				
			||||||
 | 
					            childParentWidth,
 | 
				
			||||||
 | 
					            childParentHeight,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          totalWidth = math.max(totalWidth, childSize.width);
 | 
					          totalWidth = math.max(totalWidth, childSize.width);
 | 
				
			||||||
          totalHeight += childSize.height;
 | 
					          totalHeight += childSize.height;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -97,18 +229,24 @@ function measureNode(node: UIObject): { width: number; height: number } {
 | 
				
			|||||||
          totalHeight += gap * (node.children.length - 1);
 | 
					          totalHeight += gap * (node.children.length - 1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      return { width: totalWidth, height: totalHeight };
 | 
					      return {
 | 
				
			||||||
 | 
					        width: measuredWidth ?? totalWidth,
 | 
				
			||||||
 | 
					        height: measuredHeight ?? totalHeight,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return { width: 0, height: 0 };
 | 
					      return {
 | 
				
			||||||
 | 
					        width: measuredWidth ?? 0,
 | 
				
			||||||
 | 
					        height: measuredHeight ?? 0,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Apply flexbox layout algorithm to a container and its children
 | 
					 * Apply flexbox layout algorithm to a container and its children
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @param node - The container node
 | 
					 * @param node - The container node
 | 
				
			||||||
 * @param availableWidth - Available width for layout
 | 
					 * @param availableWidth - Available width for layout
 | 
				
			||||||
 * @param availableHeight - Available height for layout
 | 
					 * @param availableHeight - Available height for layout
 | 
				
			||||||
@@ -120,7 +258,7 @@ export function calculateLayout(
 | 
				
			|||||||
  availableWidth: number,
 | 
					  availableWidth: number,
 | 
				
			||||||
  availableHeight: number,
 | 
					  availableHeight: number,
 | 
				
			||||||
  startX = 1,
 | 
					  startX = 1,
 | 
				
			||||||
  startY = 1
 | 
					  startY = 1,
 | 
				
			||||||
): void {
 | 
					): void {
 | 
				
			||||||
  // Set this node's layout
 | 
					  // Set this node's layout
 | 
				
			||||||
  node.layout = {
 | 
					  node.layout = {
 | 
				
			||||||
@@ -141,13 +279,37 @@ export function calculateLayout(
 | 
				
			|||||||
  const isFlex = node.type === "div" || node.type === "form";
 | 
					  const isFlex = node.type === "div" || node.type === "form";
 | 
				
			||||||
  const gap = isFlex ? 1 : 0;
 | 
					  const gap = isFlex ? 1 : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle scroll container layout
 | 
				
			||||||
 | 
					  if (node.type === "scroll-container" && node.scrollProps) {
 | 
				
			||||||
 | 
					    // For scroll containers, position children based on scroll offset
 | 
				
			||||||
 | 
					    const scrollOffsetX = -node.scrollProps.scrollX;
 | 
				
			||||||
 | 
					    const scrollOffsetY = -node.scrollProps.scrollY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const child of node.children) {
 | 
				
			||||||
 | 
					      // Calculate child's natural size and position it with scroll offset
 | 
				
			||||||
 | 
					      const childSize = measureNode(
 | 
				
			||||||
 | 
					        child,
 | 
				
			||||||
 | 
					        node.scrollProps.contentWidth,
 | 
				
			||||||
 | 
					        node.scrollProps.contentHeight,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      const childX = startX + scrollOffsetX;
 | 
				
			||||||
 | 
					      const childY = startY + scrollOffsetY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Recursively calculate layout for child with its natural size
 | 
				
			||||||
 | 
					      calculateLayout(child, childSize.width, childSize.height, childX, childY);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Measure all children
 | 
					  // Measure all children
 | 
				
			||||||
  const childMeasurements = node.children.map((child: UIObject) => measureNode(child));
 | 
					  const childMeasurements = node.children.map((child: UIObject) =>
 | 
				
			||||||
  
 | 
					    measureNode(child, availableWidth, availableHeight),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Calculate total size needed
 | 
					  // Calculate total size needed
 | 
				
			||||||
  let totalMainAxisSize = 0;
 | 
					  let totalMainAxisSize = 0;
 | 
				
			||||||
  let maxCrossAxisSize = 0;
 | 
					  let maxCrossAxisSize = 0;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  if (direction === "row") {
 | 
					  if (direction === "row") {
 | 
				
			||||||
    for (const measure of childMeasurements) {
 | 
					    for (const measure of childMeasurements) {
 | 
				
			||||||
      totalMainAxisSize += measure.width;
 | 
					      totalMainAxisSize += measure.width;
 | 
				
			||||||
@@ -168,10 +330,10 @@ export function calculateLayout(
 | 
				
			|||||||
  // Calculate starting position based on justify-content
 | 
					  // Calculate starting position based on justify-content
 | 
				
			||||||
  let mainAxisPos = 0;
 | 
					  let mainAxisPos = 0;
 | 
				
			||||||
  let spacing = 0;
 | 
					  let spacing = 0;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  if (direction === "row") {
 | 
					  if (direction === "row") {
 | 
				
			||||||
    const remainingSpace = availableWidth - totalMainAxisSize;
 | 
					    const remainingSpace = availableWidth - totalMainAxisSize;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (justify === "center") {
 | 
					    if (justify === "center") {
 | 
				
			||||||
      mainAxisPos = remainingSpace / 2;
 | 
					      mainAxisPos = remainingSpace / 2;
 | 
				
			||||||
    } else if (justify === "end") {
 | 
					    } else if (justify === "end") {
 | 
				
			||||||
@@ -181,7 +343,7 @@ export function calculateLayout(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    const remainingSpace = availableHeight - totalMainAxisSize;
 | 
					    const remainingSpace = availableHeight - totalMainAxisSize;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (justify === "center") {
 | 
					    if (justify === "center") {
 | 
				
			||||||
      mainAxisPos = remainingSpace / 2;
 | 
					      mainAxisPos = remainingSpace / 2;
 | 
				
			||||||
    } else if (justify === "end") {
 | 
					    } else if (justify === "end") {
 | 
				
			||||||
@@ -195,14 +357,14 @@ export function calculateLayout(
 | 
				
			|||||||
  for (let i = 0; i < node.children.length; i++) {
 | 
					  for (let i = 0; i < node.children.length; i++) {
 | 
				
			||||||
    const child = node.children[i];
 | 
					    const child = node.children[i];
 | 
				
			||||||
    const measure = childMeasurements[i];
 | 
					    const measure = childMeasurements[i];
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    let childX = startX;
 | 
					    let childX = startX;
 | 
				
			||||||
    let childY = startY;
 | 
					    let childY = startY;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (direction === "row") {
 | 
					    if (direction === "row") {
 | 
				
			||||||
      // Main axis is horizontal
 | 
					      // Main axis is horizontal
 | 
				
			||||||
      childX = startX + math.floor(mainAxisPos);
 | 
					      childX = startX + math.floor(mainAxisPos);
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Cross axis (vertical) alignment
 | 
					      // Cross axis (vertical) alignment
 | 
				
			||||||
      if (align === "center") {
 | 
					      if (align === "center") {
 | 
				
			||||||
        childY = startY + math.floor((availableHeight - measure.height) / 2);
 | 
					        childY = startY + math.floor((availableHeight - measure.height) / 2);
 | 
				
			||||||
@@ -211,7 +373,7 @@ export function calculateLayout(
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        childY = startY; // start
 | 
					        childY = startY; // start
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      mainAxisPos += measure.width + spacing;
 | 
					      mainAxisPos += measure.width + spacing;
 | 
				
			||||||
      if (i < node.children.length - 1) {
 | 
					      if (i < node.children.length - 1) {
 | 
				
			||||||
        mainAxisPos += gap;
 | 
					        mainAxisPos += gap;
 | 
				
			||||||
@@ -219,7 +381,7 @@ export function calculateLayout(
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Main axis is vertical
 | 
					      // Main axis is vertical
 | 
				
			||||||
      childY = startY + math.floor(mainAxisPos);
 | 
					      childY = startY + math.floor(mainAxisPos);
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // Cross axis (horizontal) alignment
 | 
					      // Cross axis (horizontal) alignment
 | 
				
			||||||
      if (align === "center") {
 | 
					      if (align === "center") {
 | 
				
			||||||
        childX = startX + math.floor((availableWidth - measure.width) / 2);
 | 
					        childX = startX + math.floor((availableWidth - measure.width) / 2);
 | 
				
			||||||
@@ -228,13 +390,13 @@ export function calculateLayout(
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        childX = startX; // start
 | 
					        childX = startX; // start
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      mainAxisPos += measure.height + spacing;
 | 
					      mainAxisPos += measure.height + spacing;
 | 
				
			||||||
      if (i < node.children.length - 1) {
 | 
					      if (i < node.children.length - 1) {
 | 
				
			||||||
        mainAxisPos += gap;
 | 
					        mainAxisPos += gap;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Recursively calculate layout for child
 | 
					    // Recursively calculate layout for child
 | 
				
			||||||
    calculateLayout(child, measure.width, measure.height, childX, childY);
 | 
					    calculateLayout(child, measure.width, measure.height, childX, childY);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { UIObject } from "./UIObject";
 | 
					import { UIObject } from "./UIObject";
 | 
				
			||||||
import { Accessor } from "./reactivity";
 | 
					import { Accessor } from "./reactivity";
 | 
				
			||||||
 | 
					import { isScrollContainer } from "./scrollContainer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Get text content from a node (resolving signals if needed)
 | 
					 * Get text content from a node (resolving signals if needed)
 | 
				
			||||||
@@ -11,50 +12,144 @@ import { Accessor } from "./reactivity";
 | 
				
			|||||||
function getTextContent(node: UIObject): string {
 | 
					function getTextContent(node: UIObject): string {
 | 
				
			||||||
  if (node.textContent !== undefined) {
 | 
					  if (node.textContent !== undefined) {
 | 
				
			||||||
    if (typeof node.textContent === "function") {
 | 
					    if (typeof node.textContent === "function") {
 | 
				
			||||||
      return (node.textContent)();
 | 
					      return node.textContent();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return node.textContent;
 | 
					    return node.textContent;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // For nodes with text children, get their content
 | 
					  // For nodes with text children, get their content
 | 
				
			||||||
  if (node.children.length > 0 && node.children[0].textContent !== undefined) {
 | 
					  if (node.children.length > 0 && node.children[0].textContent !== undefined) {
 | 
				
			||||||
    const child = node.children[0];
 | 
					    const child = node.children[0];
 | 
				
			||||||
    if (typeof child.textContent === "function") {
 | 
					    if (typeof child.textContent === "function") {
 | 
				
			||||||
      return (child.textContent)();
 | 
					      return child.textContent();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return child.textContent!;
 | 
					    return child.textContent!;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  return "";
 | 
					  return "";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if a position is within the visible area of all scroll container ancestors
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isPositionVisible(
 | 
				
			||||||
 | 
					  node: UIObject,
 | 
				
			||||||
 | 
					  screenX: number,
 | 
				
			||||||
 | 
					  screenY: number,
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
					  let current = node.parent;
 | 
				
			||||||
 | 
					  while (current) {
 | 
				
			||||||
 | 
					    if (isScrollContainer(current) && current.layout && current.scrollProps) {
 | 
				
			||||||
 | 
					      const { x: containerX, y: containerY } = current.layout;
 | 
				
			||||||
 | 
					      const { viewportWidth, viewportHeight } = current.scrollProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check if position is within the scroll container's viewport
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        screenX < containerX ||
 | 
				
			||||||
 | 
					        screenX >= containerX + viewportWidth ||
 | 
				
			||||||
 | 
					        screenY < containerY ||
 | 
				
			||||||
 | 
					        screenY >= containerY + viewportHeight
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    current = current.parent;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Draw a scrollbar for a scroll container
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function drawScrollbar(container: UIObject): void {
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    !container.layout ||
 | 
				
			||||||
 | 
					    !container.scrollProps ||
 | 
				
			||||||
 | 
					    container.scrollProps.showScrollbar === false
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { x, y, width, height } = container.layout;
 | 
				
			||||||
 | 
					  const { scrollY, maxScrollY, viewportHeight, contentHeight } =
 | 
				
			||||||
 | 
					    container.scrollProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Only draw vertical scrollbar if content is scrollable
 | 
				
			||||||
 | 
					  if (maxScrollY <= 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const scrollbarX = x + width - 1; // Position scrollbar at the right edge
 | 
				
			||||||
 | 
					  const scrollbarHeight = height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Calculate scrollbar thumb position and size
 | 
				
			||||||
 | 
					  const thumbHeight = Math.max(
 | 
				
			||||||
 | 
					    1,
 | 
				
			||||||
 | 
					    Math.floor((viewportHeight / contentHeight) * scrollbarHeight),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const thumbPosition = Math.floor(
 | 
				
			||||||
 | 
					    (scrollY / maxScrollY) * (scrollbarHeight - thumbHeight),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Save current colors
 | 
				
			||||||
 | 
					  const [origX, origY] = term.getCursorPos();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Draw scrollbar track
 | 
				
			||||||
 | 
					    term.setTextColor(colors.gray);
 | 
				
			||||||
 | 
					    term.setBackgroundColor(colors.lightGray);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < scrollbarHeight; i++) {
 | 
				
			||||||
 | 
					      term.setCursorPos(scrollbarX, y + i);
 | 
				
			||||||
 | 
					      if (i >= thumbPosition && i < thumbPosition + thumbHeight) {
 | 
				
			||||||
 | 
					        // Draw scrollbar thumb
 | 
				
			||||||
 | 
					        term.setBackgroundColor(colors.gray);
 | 
				
			||||||
 | 
					        term.write(" ");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Draw scrollbar track
 | 
				
			||||||
 | 
					        term.setBackgroundColor(colors.lightGray);
 | 
				
			||||||
 | 
					        term.write(" ");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    term.setCursorPos(origX, origY);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Draw a single UI node to the terminal
 | 
					 * Draw a single UI node to the terminal
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @param node - The node to draw
 | 
					 * @param node - The node to draw
 | 
				
			||||||
 * @param focused - Whether this node has focus
 | 
					 * @param focused - Whether this node has focus
 | 
				
			||||||
 * @param cursorBlinkState - Whether the cursor should be visible (for blinking)
 | 
					 * @param cursorBlinkState - Whether the cursor should be visible (for blinking)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean): void {
 | 
					function drawNode(
 | 
				
			||||||
 | 
					  node: UIObject,
 | 
				
			||||||
 | 
					  focused: boolean,
 | 
				
			||||||
 | 
					  cursorBlinkState: boolean,
 | 
				
			||||||
 | 
					): void {
 | 
				
			||||||
  if (!node.layout) return;
 | 
					  if (!node.layout) return;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  const { x, y, width } = node.layout;
 | 
					  const { x, y, width, height } = node.layout;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
 | 
					  // Check if this node is visible within scroll container viewports
 | 
				
			||||||
 | 
					  if (!isPositionVisible(node, x, y)) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Save cursor position
 | 
					  // Save cursor position
 | 
				
			||||||
  const [origX, origY] = term.getCursorPos();
 | 
					  const [origX, origY] = term.getCursorPos();
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Default colors that can be overridden by styleProps
 | 
					    // Default colors that can be overridden by styleProps
 | 
				
			||||||
    let textColor = node.styleProps.textColor;
 | 
					    let textColor = node.styleProps.textColor;
 | 
				
			||||||
    const bgColor = node.styleProps.backgroundColor;
 | 
					    const bgColor = node.styleProps.backgroundColor;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    switch (node.type) {
 | 
					    switch (node.type) {
 | 
				
			||||||
      case "label":
 | 
					      case "label":
 | 
				
			||||||
      case "h1":
 | 
					      case "h1":
 | 
				
			||||||
      case "h2":
 | 
					      case "h2":
 | 
				
			||||||
      case "h3": {
 | 
					      case "h3": {
 | 
				
			||||||
        const text = getTextContent(node);
 | 
					        const text = getTextContent(node);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Set colors based on heading level (if not overridden by styleProps)
 | 
					        // Set colors based on heading level (if not overridden by styleProps)
 | 
				
			||||||
        if (textColor === undefined) {
 | 
					        if (textColor === undefined) {
 | 
				
			||||||
          if (node.type === "h1") {
 | 
					          if (node.type === "h1") {
 | 
				
			||||||
@@ -67,18 +162,18 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
            textColor = colors.white;
 | 
					            textColor = colors.white;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        term.setTextColor(textColor);
 | 
					        term.setTextColor(textColor);
 | 
				
			||||||
        term.setBackgroundColor(bgColor ?? colors.black);
 | 
					        term.setBackgroundColor(bgColor ?? colors.black);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        term.setCursorPos(x, y);
 | 
					        term.setCursorPos(x, y);
 | 
				
			||||||
        term.write(text.substring(0, width));
 | 
					        term.write(text.substring(0, width));
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      case "button": {
 | 
					      case "button": {
 | 
				
			||||||
        const text = getTextContent(node);
 | 
					        const text = getTextContent(node);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Set colors based on focus (if not overridden by styleProps)
 | 
					        // Set colors based on focus (if not overridden by styleProps)
 | 
				
			||||||
        if (focused) {
 | 
					        if (focused) {
 | 
				
			||||||
          term.setTextColor(textColor ?? colors.black);
 | 
					          term.setTextColor(textColor ?? colors.black);
 | 
				
			||||||
@@ -87,15 +182,15 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
          term.setTextColor(textColor ?? colors.white);
 | 
					          term.setTextColor(textColor ?? colors.white);
 | 
				
			||||||
          term.setBackgroundColor(bgColor ?? colors.gray);
 | 
					          term.setBackgroundColor(bgColor ?? colors.gray);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        term.setCursorPos(x, y);
 | 
					        term.setCursorPos(x, y);
 | 
				
			||||||
        term.write(`[${text}]`);
 | 
					        term.write(`[${text}]`);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      case "input": {
 | 
					      case "input": {
 | 
				
			||||||
        const type = node.props.type as string | undefined;
 | 
					        const type = node.props.type as string | undefined;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        if (type === "checkbox") {
 | 
					        if (type === "checkbox") {
 | 
				
			||||||
          // Draw checkbox
 | 
					          // Draw checkbox
 | 
				
			||||||
          let isChecked = false;
 | 
					          let isChecked = false;
 | 
				
			||||||
@@ -103,7 +198,7 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
          if (typeof checkedProp === "function") {
 | 
					          if (typeof checkedProp === "function") {
 | 
				
			||||||
            isChecked = (checkedProp as Accessor<boolean>)();
 | 
					            isChecked = (checkedProp as Accessor<boolean>)();
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          if (focused) {
 | 
					          if (focused) {
 | 
				
			||||||
            term.setTextColor(textColor ?? colors.black);
 | 
					            term.setTextColor(textColor ?? colors.black);
 | 
				
			||||||
            term.setBackgroundColor(bgColor ?? colors.white);
 | 
					            term.setBackgroundColor(bgColor ?? colors.white);
 | 
				
			||||||
@@ -111,7 +206,7 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
            term.setTextColor(textColor ?? colors.white);
 | 
					            term.setTextColor(textColor ?? colors.white);
 | 
				
			||||||
            term.setBackgroundColor(bgColor ?? colors.black);
 | 
					            term.setBackgroundColor(bgColor ?? colors.black);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          term.setCursorPos(x, y);
 | 
					          term.setCursorPos(x, y);
 | 
				
			||||||
          term.write(isChecked ? "[X]" : "[ ]");
 | 
					          term.write(isChecked ? "[X]" : "[ ]");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -189,14 +284,21 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      case "div":
 | 
					      case "div":
 | 
				
			||||||
      case "form":
 | 
					      case "form":
 | 
				
			||||||
      case "for":
 | 
					      case "for":
 | 
				
			||||||
      case "show": {
 | 
					      case "show":
 | 
				
			||||||
 | 
					      case "switch":
 | 
				
			||||||
 | 
					      case "match": {
 | 
				
			||||||
        // Container elements may have background colors
 | 
					        // Container elements may have background colors
 | 
				
			||||||
        if (bgColor !== undefined && node.layout !== undefined) {
 | 
					        if (bgColor !== undefined && node.layout !== undefined) {
 | 
				
			||||||
          const { x: divX, y: divY, width: divWidth, height: divHeight } = node.layout;
 | 
					          const {
 | 
				
			||||||
 | 
					            x: divX,
 | 
				
			||||||
 | 
					            y: divY,
 | 
				
			||||||
 | 
					            width: divWidth,
 | 
				
			||||||
 | 
					            height: divHeight,
 | 
				
			||||||
 | 
					          } = node.layout;
 | 
				
			||||||
          term.setBackgroundColor(bgColor);
 | 
					          term.setBackgroundColor(bgColor);
 | 
				
			||||||
          // Fill the background area
 | 
					          // Fill the background area
 | 
				
			||||||
          for (let row = 0; row < divHeight; row++) {
 | 
					          for (let row = 0; row < divHeight; row++) {
 | 
				
			||||||
@@ -206,14 +308,30 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
 | 
					      case "scroll-container": {
 | 
				
			||||||
 | 
					        // Draw the scroll container background
 | 
				
			||||||
 | 
					        if (bgColor !== undefined) {
 | 
				
			||||||
 | 
					          term.setBackgroundColor(bgColor);
 | 
				
			||||||
 | 
					          for (let row = 0; row < height; row++) {
 | 
				
			||||||
 | 
					            term.setCursorPos(x, y + row);
 | 
				
			||||||
 | 
					            term.write(string.rep(" ", width));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Draw scrollbar after rendering children
 | 
				
			||||||
 | 
					        // (This will be called after children are rendered)
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case "fragment": {
 | 
					      case "fragment": {
 | 
				
			||||||
        // Fragment with text content
 | 
					        // Fragment with text content
 | 
				
			||||||
        if (node.textContent !== undefined) {
 | 
					        if (node.textContent !== undefined) {
 | 
				
			||||||
          const text = typeof node.textContent === "function" 
 | 
					          const text =
 | 
				
			||||||
            ? (node.textContent)() 
 | 
					            typeof node.textContent === "function"
 | 
				
			||||||
            : node.textContent;
 | 
					              ? node.textContent()
 | 
				
			||||||
          
 | 
					              : node.textContent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          term.setTextColor(textColor ?? colors.white);
 | 
					          term.setTextColor(textColor ?? colors.white);
 | 
				
			||||||
          term.setBackgroundColor(bgColor ?? colors.black);
 | 
					          term.setBackgroundColor(bgColor ?? colors.black);
 | 
				
			||||||
          term.setCursorPos(x, y);
 | 
					          term.setCursorPos(x, y);
 | 
				
			||||||
@@ -230,19 +348,34 @@ function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Recursively render a UI tree
 | 
					 * Recursively render a UI tree
 | 
				
			||||||
 * 
 | 
					 *
 | 
				
			||||||
 * @param node - The root node to render
 | 
					 * @param node - The root node to render
 | 
				
			||||||
 * @param focusedNode - The currently focused node (if any)
 | 
					 * @param focusedNode - The currently focused node (if any)
 | 
				
			||||||
 * @param cursorBlinkState - Whether the cursor should be visible (for blinking)
 | 
					 * @param cursorBlinkState - Whether the cursor should be visible (for blinking)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function render(node: UIObject, focusedNode?: UIObject, cursorBlinkState = false): void {
 | 
					export function render(
 | 
				
			||||||
 | 
					  node: UIObject,
 | 
				
			||||||
 | 
					  focusedNode?: UIObject,
 | 
				
			||||||
 | 
					  cursorBlinkState = false,
 | 
				
			||||||
 | 
					): void {
 | 
				
			||||||
  // Draw this node
 | 
					  // Draw this node
 | 
				
			||||||
  const isFocused = node === focusedNode;
 | 
					  const isFocused = node === focusedNode;
 | 
				
			||||||
  drawNode(node, isFocused, cursorBlinkState);
 | 
					  drawNode(node, isFocused, cursorBlinkState);
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // Recursively draw children
 | 
					  // For scroll containers, set up clipping region before rendering children
 | 
				
			||||||
  for (const child of node.children) {
 | 
					  if (isScrollContainer(node) && node.layout && node.scrollProps) {
 | 
				
			||||||
    render(child, focusedNode, cursorBlinkState);
 | 
					    // Recursively draw children (they will be clipped by visibility checks)
 | 
				
			||||||
 | 
					    for (const child of node.children) {
 | 
				
			||||||
 | 
					      render(child, focusedNode, cursorBlinkState);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Draw scrollbar after children
 | 
				
			||||||
 | 
					    drawScrollbar(node);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    // Recursively draw children normally
 | 
				
			||||||
 | 
					    for (const child of node.children) {
 | 
				
			||||||
 | 
					      render(child, focusedNode, cursorBlinkState);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										204
									
								
								src/lib/ccTUI/scrollContainer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								src/lib/ccTUI/scrollContainer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Scroll container component for handling scrollable content
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { UIObject } from "./UIObject";
 | 
				
			||||||
 | 
					import { createSignal, createEffect } from "./reactivity";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Props for ScrollContainer component
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type ScrollContainerProps = {
 | 
				
			||||||
 | 
					  /** Maximum width of the scroll container viewport */
 | 
				
			||||||
 | 
					  width?: number;
 | 
				
			||||||
 | 
					  /** Maximum height of the scroll container viewport */
 | 
				
			||||||
 | 
					  height?: number;
 | 
				
			||||||
 | 
					  /** Whether to show scrollbars (default: true) */
 | 
				
			||||||
 | 
					  showScrollbar?: boolean;
 | 
				
			||||||
 | 
					  /** CSS-like class names for styling */
 | 
				
			||||||
 | 
					  class?: string;
 | 
				
			||||||
 | 
					  /** Callback when scroll position changes */
 | 
				
			||||||
 | 
					  onScroll?: (scrollX: number, scrollY: number) => void;
 | 
				
			||||||
 | 
					} & Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * ScrollContainer component - provides scrollable viewport for content
 | 
				
			||||||
 | 
					 * When content exceeds the container size, scrollbars appear and mouse wheel scrolling is enabled
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param props - Props containing dimensions and scroll options
 | 
				
			||||||
 | 
					 * @param content - Content to be scrolled
 | 
				
			||||||
 | 
					 * @returns UIObject representing the scroll container
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @example
 | 
				
			||||||
 | 
					 * ```typescript
 | 
				
			||||||
 | 
					 * const [items, setItems] = createStore<string[]>([]);
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * ScrollContainer(
 | 
				
			||||||
 | 
					 *   { width: 20, height: 10, showScrollbar: true },
 | 
				
			||||||
 | 
					 *   div({ class: "flex flex-col" },
 | 
				
			||||||
 | 
					 *     For({ each: () => items },
 | 
				
			||||||
 | 
					 *       (item, i) => div({}, item)
 | 
				
			||||||
 | 
					 *     )
 | 
				
			||||||
 | 
					 *   )
 | 
				
			||||||
 | 
					 * )
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function ScrollContainer(
 | 
				
			||||||
 | 
					  props: ScrollContainerProps,
 | 
				
			||||||
 | 
					  content: UIObject,
 | 
				
			||||||
 | 
					): UIObject {
 | 
				
			||||||
 | 
					  const container = new UIObject("scroll-container", props, [content]);
 | 
				
			||||||
 | 
					  content.parent = container;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Set up scroll properties from props
 | 
				
			||||||
 | 
					  if (container.scrollProps) {
 | 
				
			||||||
 | 
					    container.scrollProps.viewportWidth = props.width ?? 10;
 | 
				
			||||||
 | 
					    container.scrollProps.viewportHeight = props.height ?? 10;
 | 
				
			||||||
 | 
					    container.scrollProps.showScrollbar = props.showScrollbar !== false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create reactive signals for scroll position
 | 
				
			||||||
 | 
					  const [scrollX, setScrollX] = createSignal(0);
 | 
				
			||||||
 | 
					  const [scrollY, setScrollY] = createSignal(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Update scroll position when signals change
 | 
				
			||||||
 | 
					  createEffect(() => {
 | 
				
			||||||
 | 
					    const x = scrollX();
 | 
				
			||||||
 | 
					    const y = scrollY();
 | 
				
			||||||
 | 
					    container.scrollTo(x, y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Call onScroll callback if provided
 | 
				
			||||||
 | 
					    if (props.onScroll && typeof props.onScroll === "function") {
 | 
				
			||||||
 | 
					      props.onScroll(x, y);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Override scroll methods to update signals
 | 
				
			||||||
 | 
					  const originalScrollBy = container.scrollBy.bind(container);
 | 
				
			||||||
 | 
					  const originalScrollTo = container.scrollTo.bind(container);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  container.scrollBy = (deltaX: number, deltaY: number): void => {
 | 
				
			||||||
 | 
					    originalScrollBy(deltaX, deltaY);
 | 
				
			||||||
 | 
					    if (container.scrollProps) {
 | 
				
			||||||
 | 
					      setScrollX(container.scrollProps.scrollX);
 | 
				
			||||||
 | 
					      setScrollY(container.scrollProps.scrollY);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  container.scrollTo = (x: number, y: number): void => {
 | 
				
			||||||
 | 
					    originalScrollTo(x, y);
 | 
				
			||||||
 | 
					    if (container.scrollProps) {
 | 
				
			||||||
 | 
					      setScrollX(container.scrollProps.scrollX);
 | 
				
			||||||
 | 
					      setScrollY(container.scrollProps.scrollY);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Expose scroll control methods on the container
 | 
				
			||||||
 | 
					  const containerWithMethods = container as UIObject & {
 | 
				
			||||||
 | 
					    getScrollX: () => number;
 | 
				
			||||||
 | 
					    getScrollY: () => number;
 | 
				
			||||||
 | 
					    setScrollX: (value: number) => void;
 | 
				
			||||||
 | 
					    setScrollY: (value: number) => void;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  containerWithMethods.getScrollX = () => scrollX();
 | 
				
			||||||
 | 
					  containerWithMethods.getScrollY = () => scrollY();
 | 
				
			||||||
 | 
					  containerWithMethods.setScrollX = (value: number) => setScrollX(value);
 | 
				
			||||||
 | 
					  containerWithMethods.setScrollY = (value: number) => setScrollY(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return container;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if a UI node is a scroll container
 | 
				
			||||||
 | 
					 * @param node - The UI node to check
 | 
				
			||||||
 | 
					 * @returns True if the node is a scroll container
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isScrollContainer(node: UIObject): boolean {
 | 
				
			||||||
 | 
					  return node.type === "scroll-container";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Find the nearest scroll container ancestor of a node
 | 
				
			||||||
 | 
					 * @param node - The node to start searching from
 | 
				
			||||||
 | 
					 * @returns The nearest scroll container, or undefined if none found
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function findScrollContainer(node: UIObject): UIObject | undefined {
 | 
				
			||||||
 | 
					  let current = node.parent;
 | 
				
			||||||
 | 
					  while (current) {
 | 
				
			||||||
 | 
					    if (isScrollContainer(current)) {
 | 
				
			||||||
 | 
					      return current;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    current = current.parent;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if a point is within the visible area of a scroll container
 | 
				
			||||||
 | 
					 * @param container - The scroll container
 | 
				
			||||||
 | 
					 * @param x - X coordinate relative to container
 | 
				
			||||||
 | 
					 * @param y - Y coordinate relative to container
 | 
				
			||||||
 | 
					 * @returns True if the point is visible
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isPointVisible(
 | 
				
			||||||
 | 
					  container: UIObject,
 | 
				
			||||||
 | 
					  x: number,
 | 
				
			||||||
 | 
					  y: number,
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
					  if (!isScrollContainer(container) || !container.scrollProps) {
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { scrollX, scrollY, viewportWidth, viewportHeight } =
 | 
				
			||||||
 | 
					    container.scrollProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    x >= scrollX &&
 | 
				
			||||||
 | 
					    x < scrollX + viewportWidth &&
 | 
				
			||||||
 | 
					    y >= scrollY &&
 | 
				
			||||||
 | 
					    y < scrollY + viewportHeight
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Convert screen coordinates to scroll container content coordinates
 | 
				
			||||||
 | 
					 * @param container - The scroll container
 | 
				
			||||||
 | 
					 * @param screenX - Screen X coordinate
 | 
				
			||||||
 | 
					 * @param screenY - Screen Y coordinate
 | 
				
			||||||
 | 
					 * @returns Content coordinates, or undefined if not within container
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function screenToContent(
 | 
				
			||||||
 | 
					  container: UIObject,
 | 
				
			||||||
 | 
					  screenX: number,
 | 
				
			||||||
 | 
					  screenY: number,
 | 
				
			||||||
 | 
					): { x: number; y: number } | undefined {
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    !isScrollContainer(container) ||
 | 
				
			||||||
 | 
					    !container.layout ||
 | 
				
			||||||
 | 
					    !container.scrollProps
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { x: containerX, y: containerY } = container.layout;
 | 
				
			||||||
 | 
					  const { scrollX, scrollY } = container.scrollProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if point is within container bounds
 | 
				
			||||||
 | 
					  const relativeX = screenX - containerX;
 | 
				
			||||||
 | 
					  const relativeY = screenY - containerY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    relativeX < 0 ||
 | 
				
			||||||
 | 
					    relativeY < 0 ||
 | 
				
			||||||
 | 
					    relativeX >= container.scrollProps.viewportWidth ||
 | 
				
			||||||
 | 
					    relativeY >= container.scrollProps.viewportHeight
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    x: relativeX + scrollX,
 | 
				
			||||||
 | 
					    y: relativeY + scrollY,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,6 +15,7 @@ import {
 | 
				
			|||||||
  For,
 | 
					  For,
 | 
				
			||||||
  createStore,
 | 
					  createStore,
 | 
				
			||||||
  removeIndex,
 | 
					  removeIndex,
 | 
				
			||||||
 | 
					  ScrollContainer,
 | 
				
			||||||
} from "../lib/ccTUI";
 | 
					} from "../lib/ccTUI";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -99,6 +100,194 @@ const TodosApp = () => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Example data type
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					interface ListItem {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple scroll example with a list of items
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function SimpleScrollExample() {
 | 
				
			||||||
 | 
					  // Create a large list of items to demonstrate scrolling
 | 
				
			||||||
 | 
					  const [items, setItems] = createStore<ListItem[]>([]);
 | 
				
			||||||
 | 
					  const [itemCount, setItemCount] = createSignal(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Generate initial items
 | 
				
			||||||
 | 
					  const generateItems = (count: number) => {
 | 
				
			||||||
 | 
					    const newItems: ListItem[] = [];
 | 
				
			||||||
 | 
					    for (let i = 1; i <= count; i++) {
 | 
				
			||||||
 | 
					      newItems.push({
 | 
				
			||||||
 | 
					        id: i,
 | 
				
			||||||
 | 
					        title: `Item ${i}`,
 | 
				
			||||||
 | 
					        description: `Description for item ${i}`,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setItems(() => newItems);
 | 
				
			||||||
 | 
					    setItemCount(count);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initialize with some items
 | 
				
			||||||
 | 
					  generateItems(20);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return div(
 | 
				
			||||||
 | 
					    { class: "flex flex-col h-screen bg-black text-white" },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Header
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row justify-center bg-blue text-white" },
 | 
				
			||||||
 | 
					      label({}, "Scroll Container Demo"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Control buttons
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row justify-center bg-gray" },
 | 
				
			||||||
 | 
					      button(
 | 
				
			||||||
 | 
					        { onClick: () => generateItems(itemCount() + 10) },
 | 
				
			||||||
 | 
					        "Add 10 Items",
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      button(
 | 
				
			||||||
 | 
					        { onClick: () => generateItems(Math.max(0, itemCount() - 10)) },
 | 
				
			||||||
 | 
					        "Remove 10 Items",
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      button({ onClick: () => generateItems(50) }, "Generate 50 Items"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Main scrollable content
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-col" },
 | 
				
			||||||
 | 
					      label({}, "Scrollable List:"),
 | 
				
			||||||
 | 
					      ScrollContainer(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          width: 40,
 | 
				
			||||||
 | 
					          height: 15,
 | 
				
			||||||
 | 
					          showScrollbar: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        div(
 | 
				
			||||||
 | 
					          { class: "flex flex-col" },
 | 
				
			||||||
 | 
					          For({ each: items }, (item: ListItem, index) =>
 | 
				
			||||||
 | 
					            div(
 | 
				
			||||||
 | 
					              { class: "flex flex-col" },
 | 
				
			||||||
 | 
					              label({}, () => `${index() + 1}. ${item.title}`),
 | 
				
			||||||
 | 
					              label({}, item.description),
 | 
				
			||||||
 | 
					              label({}, ""), // Empty line for spacing
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Instructions
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-col bg-brown text-white" },
 | 
				
			||||||
 | 
					      label({}, "Instructions:"),
 | 
				
			||||||
 | 
					      label({}, "• Use mouse wheel to scroll within the container"),
 | 
				
			||||||
 | 
					      label({}, "• Notice the scrollbar on the right side"),
 | 
				
			||||||
 | 
					      label({}, "• Try adding/removing items to see scroll behavior"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Example with static long content
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function StaticScrollExample() {
 | 
				
			||||||
 | 
					  const longText = [
 | 
				
			||||||
 | 
					    "Line 1: This is a demonstration of vertical scrolling.",
 | 
				
			||||||
 | 
					    "Line 2: The content extends beyond the visible area.",
 | 
				
			||||||
 | 
					    "Line 3: Use your mouse wheel to scroll up and down.",
 | 
				
			||||||
 | 
					    "Line 4: Notice how the scrollbar appears on the right.",
 | 
				
			||||||
 | 
					    "Line 5: The scrollbar thumb shows your current position.",
 | 
				
			||||||
 | 
					    "Line 6: This content is much longer than the container.",
 | 
				
			||||||
 | 
					    "Line 7: Keep scrolling to see more lines.",
 | 
				
			||||||
 | 
					    "Line 8: The scroll container handles overflow automatically.",
 | 
				
			||||||
 | 
					    "Line 9: You can also scroll horizontally if content is wide.",
 | 
				
			||||||
 | 
					    "Line 10: This demonstrates the scroll functionality.",
 | 
				
			||||||
 | 
					    "Line 11: More content here to fill the scrollable area.",
 | 
				
			||||||
 | 
					    "Line 12: The framework handles all the complex scroll logic.",
 | 
				
			||||||
 | 
					    "Line 13: Just wrap your content in a ScrollContainer.",
 | 
				
			||||||
 | 
					    "Line 14: Set width and height to define the viewport.",
 | 
				
			||||||
 | 
					    "Line 15: The end! Try scrolling back to the top.",
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return div(
 | 
				
			||||||
 | 
					    { class: "flex flex-col justify-center items-center h-screen bg-black" },
 | 
				
			||||||
 | 
					    label({}, "Static Scroll Example"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ScrollContainer(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        width: 50,
 | 
				
			||||||
 | 
					        height: 10,
 | 
				
			||||||
 | 
					        showScrollbar: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
 | 
					        ...longText.map((line) => label({}, line)),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    label({}, "Use mouse wheel to scroll"),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Example with multiple independent scroll containers
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function MultiScrollExample() {
 | 
				
			||||||
 | 
					  return div(
 | 
				
			||||||
 | 
					    { class: "flex flex-col h-screen bg-black" },
 | 
				
			||||||
 | 
					    label({}, "Multiple Scroll Containers"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row justify-between" },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Left container - numbers
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
 | 
					        label({}, "Numbers"),
 | 
				
			||||||
 | 
					        ScrollContainer(
 | 
				
			||||||
 | 
					          { width: 15, height: 10 },
 | 
				
			||||||
 | 
					          div(
 | 
				
			||||||
 | 
					            { class: "flex flex-col" },
 | 
				
			||||||
 | 
					            For(
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                each: () =>
 | 
				
			||||||
 | 
					                  Array.from({ length: 30 }, (_, i) => i + 1) as number[],
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              (num: number) => label({}, () => `Number: ${num}`),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Right container - letters
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
 | 
					        label({}, "Letters"),
 | 
				
			||||||
 | 
					        ScrollContainer(
 | 
				
			||||||
 | 
					          { width: 15, height: 10 },
 | 
				
			||||||
 | 
					          div(
 | 
				
			||||||
 | 
					            { class: "flex flex-col" },
 | 
				
			||||||
 | 
					            For(
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                each: () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("") as string[],
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              (letter: string, index) =>
 | 
				
			||||||
 | 
					                label({}, () => `${index() + 1}. Letter ${letter}`),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    label({}, "Each container scrolls independently"),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Main application component with tabs
 | 
					 * Main application component with tabs
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@@ -111,11 +300,32 @@ const App = () => {
 | 
				
			|||||||
      { class: "flex flex-row" },
 | 
					      { class: "flex flex-row" },
 | 
				
			||||||
      button({ onClick: () => setTabIndex(0) }, "CountDemo"),
 | 
					      button({ onClick: () => setTabIndex(0) }, "CountDemo"),
 | 
				
			||||||
      button({ onClick: () => setTabIndex(1) }, "TodosDemo"),
 | 
					      button({ onClick: () => setTabIndex(1) }, "TodosDemo"),
 | 
				
			||||||
 | 
					      button({ onClick: () => setTabIndex(2) }, "SimpleScroll"),
 | 
				
			||||||
 | 
					      button({ onClick: () => setTabIndex(3) }, "StaticScroll"),
 | 
				
			||||||
 | 
					      button({ onClick: () => setTabIndex(4) }, "MultiScroll"),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    Show(
 | 
					    Show(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        when: () => tabIndex() === 0,
 | 
					        when: () => tabIndex() === 0,
 | 
				
			||||||
        fallback: Show({ when: () => tabIndex() === 1 }, TodosApp()),
 | 
					        fallback: Show(
 | 
				
			||||||
 | 
					          { 
 | 
				
			||||||
 | 
					            when: () => tabIndex() === 1,
 | 
				
			||||||
 | 
					            fallback: Show(
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                when: () => tabIndex() === 2,
 | 
				
			||||||
 | 
					                fallback: Show(
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    when: () => tabIndex() === 3,
 | 
				
			||||||
 | 
					                    fallback: MultiScrollExample(),
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  StaticScrollExample()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              SimpleScrollExample()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }, 
 | 
				
			||||||
 | 
					          TodosApp()
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      Counter(),
 | 
					      Counter(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
@@ -137,4 +347,4 @@ try {
 | 
				
			|||||||
    print("Error running application:");
 | 
					    print("Error running application:");
 | 
				
			||||||
    printError(e);
 | 
					    printError(e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user