mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	update and add tui for accesscontrol
This commit is contained in:
		@@ -3,10 +3,10 @@
 | 
				
			|||||||
    "devenv": {
 | 
					    "devenv": {
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "dir": "src/modules",
 | 
					        "dir": "src/modules",
 | 
				
			||||||
        "lastModified": 1759939975,
 | 
					        "lastModified": 1760162706,
 | 
				
			||||||
        "owner": "cachix",
 | 
					        "owner": "cachix",
 | 
				
			||||||
        "repo": "devenv",
 | 
					        "repo": "devenv",
 | 
				
			||||||
        "rev": "6eda3b7af3010d289e6e8e047435956fc80c1395",
 | 
					        "rev": "0d5ad578728fe4bce66eb4398b8b1e66deceb4e4",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								devenv.nix
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								devenv.nix
									
									
									
									
									
								
							@@ -1,11 +1,13 @@
 | 
				
			|||||||
{ pkgs, lib, config, inputs, ... }:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					  pkgs,
 | 
				
			||||||
 | 
					  lib,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					  inputs,
 | 
				
			||||||
 | 
					  ...
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
  packages = with pkgs; [
 | 
					  packages = with pkgs; [
 | 
				
			||||||
    pnpm
 | 
					    pnpm
 | 
				
			||||||
    craftos-pc
 | 
					    craftos-pc
 | 
				
			||||||
    qwen-code
 | 
					 | 
				
			||||||
    gemini-cli
 | 
					 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # https://devenv.sh/languages/
 | 
					  # https://devenv.sh/languages/
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,8 @@
 | 
				
			|||||||
inputs:
 | 
					inputs:
 | 
				
			||||||
  nixpkgs:
 | 
					  nixpkgs:
 | 
				
			||||||
    url: github:cachix/devenv-nixpkgs/rolling
 | 
					    url: github:cachix/devenv-nixpkgs/rolling
 | 
				
			||||||
 | 
					 | 
				
			||||||
# If you're using non-OSS software, you can set allowUnfree to true.
 | 
					# If you're using non-OSS software, you can set allowUnfree to true.
 | 
				
			||||||
# allowUnfree: true
 | 
					allowUnfree: true
 | 
				
			||||||
 | 
					 | 
				
			||||||
# If you're willing to use a package that's vulnerable
 | 
					# If you're willing to use a package that's vulnerable
 | 
				
			||||||
# permittedInsecurePackages:
 | 
					# permittedInsecurePackages:
 | 
				
			||||||
#  - "openssl-1.1.1w"
 | 
					#  - "openssl-1.1.1w"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										110
									
								
								src/accesscontrol/logviewer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/accesscontrol/logviewer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Access Control Log Viewer
 | 
				
			||||||
 | 
					 * Simple log viewer that allows launching the TUI with 'c' key
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { launchAccessControlTUI } from "./tui";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const args = [...$vararg];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function displayLog(filepath: string, lines = 20) {
 | 
				
			||||||
 | 
					  const [file] = io.open(filepath, "r");
 | 
				
			||||||
 | 
					  if (!file) {
 | 
				
			||||||
 | 
					    print(`Failed to open log file: ${filepath}`);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const content = file.read("*a");
 | 
				
			||||||
 | 
					  file.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (content === null || content === undefined || content === "") {
 | 
				
			||||||
 | 
					    print("Log file is empty");
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const logLines = content.split("\n");
 | 
				
			||||||
 | 
					  const startIndex = Math.max(0, logLines.length - lines);
 | 
				
			||||||
 | 
					  const displayLines = logLines.slice(startIndex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  term.clear();
 | 
				
			||||||
 | 
					  term.setCursorPos(1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  print("=== Access Control Log Viewer ===");
 | 
				
			||||||
 | 
					  print("Press 'c' to open configuration TUI, 'q' to quit, 'r' to refresh");
 | 
				
			||||||
 | 
					  print("==========================================");
 | 
				
			||||||
 | 
					  print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const line of displayLines) {
 | 
				
			||||||
 | 
					    if (line.trim() !== "") {
 | 
				
			||||||
 | 
					      print(line);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  print("");
 | 
				
			||||||
 | 
					  print("==========================================");
 | 
				
			||||||
 | 
					  print(`Showing last ${displayLines.length} lines of ${filepath}`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function main(args: string[]) {
 | 
				
			||||||
 | 
					  const logFilepath = args[0] || `${shell.dir()}/accesscontrol.log`;
 | 
				
			||||||
 | 
					  const lines = args[1] ? parseInt(args[1]) : 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isNaN(lines) || lines <= 0) {
 | 
				
			||||||
 | 
					    print("Usage: logviewer [logfile] [lines]");
 | 
				
			||||||
 | 
					    print("  logfile - Path to log file (default: accesscontrol.log)");
 | 
				
			||||||
 | 
					    print("  lines   - Number of lines to display (default: 20)");
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let running = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initial display
 | 
				
			||||||
 | 
					  displayLog(logFilepath, lines);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  while (running) {
 | 
				
			||||||
 | 
					    const [eventType, key] = os.pullEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (eventType === "key") {
 | 
				
			||||||
 | 
					      if (key === keys.c) {
 | 
				
			||||||
 | 
					        // Launch TUI
 | 
				
			||||||
 | 
					        print("Launching Access Control TUI...");
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          launchAccessControlTUI();
 | 
				
			||||||
 | 
					          // Refresh display after TUI closes
 | 
				
			||||||
 | 
					          displayLog(logFilepath, lines);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          if (error === "TUI_CLOSE" || error === "Terminated") {
 | 
				
			||||||
 | 
					            displayLog(logFilepath, lines);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            print(`TUI error: ${String(error)}`);
 | 
				
			||||||
 | 
					            os.sleep(2);
 | 
				
			||||||
 | 
					            displayLog(logFilepath, lines);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (key === keys.q) {
 | 
				
			||||||
 | 
					        // Quit
 | 
				
			||||||
 | 
					        running = false;
 | 
				
			||||||
 | 
					      } else if (key === keys.r) {
 | 
				
			||||||
 | 
					        // Refresh
 | 
				
			||||||
 | 
					        displayLog(logFilepath, lines);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (eventType === "terminate") {
 | 
				
			||||||
 | 
					      running = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  term.clear();
 | 
				
			||||||
 | 
					  term.setCursorPos(1, 1);
 | 
				
			||||||
 | 
					  print("Log viewer closed.");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					  main(args);
 | 
				
			||||||
 | 
					} catch (error) {
 | 
				
			||||||
 | 
					  if (error === "Terminated") {
 | 
				
			||||||
 | 
					    print("Log viewer terminated by user.");
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    print("Error in log viewer:");
 | 
				
			||||||
 | 
					    printError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,14 @@
 | 
				
			|||||||
import { CCLog, DAY } from "@/lib/ccLog";
 | 
					import { CCLog, DAY } from "@/lib/ccLog";
 | 
				
			||||||
import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config";
 | 
					import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config";
 | 
				
			||||||
import { createAccessControlCLI } from "./cli";
 | 
					import { createAccessControlCLI } from "./cli";
 | 
				
			||||||
 | 
					import { launchAccessControlTUI } from "./tui";
 | 
				
			||||||
import * as peripheralManager from "../lib/PeripheralManager";
 | 
					import * as peripheralManager from "../lib/PeripheralManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DEBUG = false;
 | 
					const DEBUG = false;
 | 
				
			||||||
const args = [...$vararg];
 | 
					const args = [...$vararg];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init Log
 | 
					// Init Log
 | 
				
			||||||
const log = new CCLog("accesscontrol.log", DAY);
 | 
					const log = new CCLog("accesscontrol.log", true, DAY);
 | 
				
			||||||
setLog(log);
 | 
					setLog(log);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Load Config
 | 
					// Load Config
 | 
				
			||||||
@@ -174,6 +175,21 @@ function mainLoop() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function keyboardLoop() {
 | 
				
			||||||
 | 
					  while (true) {
 | 
				
			||||||
 | 
					    const [eventType, key] = os.pullEvent("key");
 | 
				
			||||||
 | 
					    if (eventType === "key" && key === keys.c) {
 | 
				
			||||||
 | 
					      log.info("Launching Access Control TUI...");
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        launchAccessControlTUI();
 | 
				
			||||||
 | 
					        log.info("TUI closed, resuming normal operation");
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        log.error(`TUI error: ${textutils.serialise(error as object)}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function main(args: string[]) {
 | 
					function main(args: string[]) {
 | 
				
			||||||
  log.info("Starting access control system, get args: " + args.join(", "));
 | 
					  log.info("Starting access control system, get args: " + args.join(", "));
 | 
				
			||||||
  if (args.length == 1) {
 | 
					  if (args.length == 1) {
 | 
				
			||||||
@@ -187,6 +203,9 @@ function main(args: string[]) {
 | 
				
			|||||||
        groupNames,
 | 
					        groupNames,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      print(
 | 
				
			||||||
 | 
					        "Access Control System started. Press 'c' to open configuration TUI.",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      parallel.waitForAll(
 | 
					      parallel.waitForAll(
 | 
				
			||||||
        () => {
 | 
					        () => {
 | 
				
			||||||
          mainLoop();
 | 
					          mainLoop();
 | 
				
			||||||
@@ -197,12 +216,25 @@ function main(args: string[]) {
 | 
				
			|||||||
        () => {
 | 
					        () => {
 | 
				
			||||||
          watchLoop();
 | 
					          watchLoop();
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        () => {
 | 
				
			||||||
 | 
					          keyboardLoop();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
 | 
					    } else if (args[0] == "config") {
 | 
				
			||||||
 | 
					      log.info("Launching Access Control TUI...");
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        launchAccessControlTUI();
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        log.error(`TUI error: ${textutils.serialise(error as object)}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  print(`Usage: accesscontrol start`);
 | 
					  print(`Usage: accesscontrol start | config`);
 | 
				
			||||||
 | 
					  print("  start  - Start the access control system with monitoring");
 | 
				
			||||||
 | 
					  print("  config - Open configuration TUI");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,615 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Access Control TUI Implementation
 | 
				
			||||||
 | 
					 * A text-based user interface for configuring access control settings
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  createSignal,
 | 
				
			||||||
 | 
					  createStore,
 | 
				
			||||||
 | 
					  div,
 | 
				
			||||||
 | 
					  label,
 | 
				
			||||||
 | 
					  button,
 | 
				
			||||||
 | 
					  input,
 | 
				
			||||||
 | 
					  h1,
 | 
				
			||||||
 | 
					  render,
 | 
				
			||||||
 | 
					  Show,
 | 
				
			||||||
 | 
					  For,
 | 
				
			||||||
 | 
					} from "../lib/ccTUI";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AccessConfig,
 | 
				
			||||||
 | 
					  UserGroupConfig,
 | 
				
			||||||
 | 
					  loadConfig,
 | 
				
			||||||
 | 
					  saveConfig,
 | 
				
			||||||
 | 
					} from "./config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Tab indices
 | 
				
			||||||
 | 
					const TABS = {
 | 
				
			||||||
 | 
					  BASIC: 0,
 | 
				
			||||||
 | 
					  GROUPS: 1,
 | 
				
			||||||
 | 
					  WELCOME_TOAST: 2,
 | 
				
			||||||
 | 
					  WARN_TOAST: 3,
 | 
				
			||||||
 | 
					  NOTICE_TOAST: 4,
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TabIndex = (typeof TABS)[keyof typeof TABS];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Error dialog state
 | 
				
			||||||
 | 
					interface ErrorState {
 | 
				
			||||||
 | 
					  show: boolean;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Main TUI Application Component
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const AccessControlTUI = () => {
 | 
				
			||||||
 | 
					  // Configuration state
 | 
				
			||||||
 | 
					  const [config, setConfig] = createStore<AccessConfig>({} as AccessConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // UI state
 | 
				
			||||||
 | 
					  const [currentTab, setCurrentTab] = createSignal<TabIndex>(TABS.BASIC);
 | 
				
			||||||
 | 
					  const [selectedGroupIndex, setSelectedGroupIndex] = createSignal(0);
 | 
				
			||||||
 | 
					  const [errorState, setErrorState] = createStore<ErrorState>({
 | 
				
			||||||
 | 
					    show: false,
 | 
				
			||||||
 | 
					    message: "",
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // New user input for group management
 | 
				
			||||||
 | 
					  const [newUserName, setNewUserName] = createSignal("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Load configuration on initialization
 | 
				
			||||||
 | 
					  const configFilepath = `${shell.dir()}/access.config.json`;
 | 
				
			||||||
 | 
					  const loadedConfig = loadConfig(configFilepath);
 | 
				
			||||||
 | 
					  setConfig(() => loadedConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Tab navigation functions
 | 
				
			||||||
 | 
					  const tabNames = [
 | 
				
			||||||
 | 
					    "Basic",
 | 
				
			||||||
 | 
					    "Groups",
 | 
				
			||||||
 | 
					    "Welcome Toast",
 | 
				
			||||||
 | 
					    "Warn Toast",
 | 
				
			||||||
 | 
					    "Notice Toast",
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const showError = (message: string) => {
 | 
				
			||||||
 | 
					    setErrorState("show", true);
 | 
				
			||||||
 | 
					    setErrorState("message", message);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hideError = () => {
 | 
				
			||||||
 | 
					    setErrorState("show", false);
 | 
				
			||||||
 | 
					    setErrorState("message", "");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Validation functions
 | 
				
			||||||
 | 
					  const validateNumber = (value: string): number | null => {
 | 
				
			||||||
 | 
					    const num = parseInt(value);
 | 
				
			||||||
 | 
					    return isNaN(num) ? null : num;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const validateTextComponent = (value: string): boolean => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const parsed = textutils.unserialiseJSON(value);
 | 
				
			||||||
 | 
					      return parsed !== undefined && typeof parsed === "object";
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Save configuration with validation
 | 
				
			||||||
 | 
					  const handleSave = () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const currentConfig = config();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Validate numbers
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        validateNumber(currentConfig.detectInterval?.toString() ?? "") === null
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        showError("Invalid Detect Interval: must be a number");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        validateNumber(currentConfig.watchInterval?.toString() ?? "") === null
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        showError("Invalid Watch Interval: must be a number");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        validateNumber(currentConfig.noticeTimes?.toString() ?? "") === null
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        showError("Invalid Notice Times: must be a number");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        validateNumber(currentConfig.detectRange?.toString() ?? "") === null
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        showError("Invalid Detect Range: must be a number");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Validate text components for toast configs
 | 
				
			||||||
 | 
					      const toastConfigs = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Welcome Toast Title",
 | 
				
			||||||
 | 
					          value: currentConfig.welcomeToastConfig?.title,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Welcome Toast Message",
 | 
				
			||||||
 | 
					          value: currentConfig.welcomeToastConfig?.msg,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Warn Toast Title",
 | 
				
			||||||
 | 
					          value: currentConfig.warnToastConfig?.title,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Warn Toast Message",
 | 
				
			||||||
 | 
					          value: currentConfig.warnToastConfig?.msg,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Notice Toast Title",
 | 
				
			||||||
 | 
					          value: currentConfig.noticeToastConfig?.title,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "Notice Toast Message",
 | 
				
			||||||
 | 
					          value: currentConfig.noticeToastConfig?.msg,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const toastConfig of toastConfigs) {
 | 
				
			||||||
 | 
					        if (toastConfig.value != undefined) {
 | 
				
			||||||
 | 
					          const serialized = textutils.serialiseJSON(toastConfig.value);
 | 
				
			||||||
 | 
					          if (!validateTextComponent(serialized)) {
 | 
				
			||||||
 | 
					            showError(
 | 
				
			||||||
 | 
					              `Invalid ${toastConfig.name}: must be valid MinecraftTextComponent JSON`,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Save configuration
 | 
				
			||||||
 | 
					      saveConfig(currentConfig, configFilepath);
 | 
				
			||||||
 | 
					      showError("Configuration saved successfully!");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showError(`Failed to save configuration: ${String(error)}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Add user to selected group
 | 
				
			||||||
 | 
					  const addUser = () => {
 | 
				
			||||||
 | 
					    const userName = newUserName().trim();
 | 
				
			||||||
 | 
					    if (userName === "") return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const groupIndex = selectedGroupIndex();
 | 
				
			||||||
 | 
					    if (groupIndex === 0) {
 | 
				
			||||||
 | 
					      // Admin group
 | 
				
			||||||
 | 
					      const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
 | 
					      setConfig("adminGroupConfig", {
 | 
				
			||||||
 | 
					        ...currentAdmin,
 | 
				
			||||||
 | 
					        groupUsers: [...currentAdmin.groupUsers, userName],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Regular group
 | 
				
			||||||
 | 
					      const actualIndex = groupIndex - 1;
 | 
				
			||||||
 | 
					      const currentGroups = config().usersGroups;
 | 
				
			||||||
 | 
					      const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
 | 
					      const newGroups = [...currentGroups];
 | 
				
			||||||
 | 
					      newGroups[actualIndex] = {
 | 
				
			||||||
 | 
					        ...currentGroup,
 | 
				
			||||||
 | 
					        groupUsers: [...(currentGroup?.groupUsers ?? []), userName],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      setConfig("usersGroups", newGroups);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setNewUserName("");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove user from selected group
 | 
				
			||||||
 | 
					  const removeUser = (userName: string) => {
 | 
				
			||||||
 | 
					    const groupIndex = selectedGroupIndex();
 | 
				
			||||||
 | 
					    if (groupIndex === 0) {
 | 
				
			||||||
 | 
					      // Admin group
 | 
				
			||||||
 | 
					      const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
 | 
					      setConfig("adminGroupConfig", {
 | 
				
			||||||
 | 
					        ...currentAdmin,
 | 
				
			||||||
 | 
					        groupUsers: currentAdmin.groupUsers.filter((user) => user !== userName),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Regular group
 | 
				
			||||||
 | 
					      const actualIndex = groupIndex - 1;
 | 
				
			||||||
 | 
					      const currentGroups = config().usersGroups;
 | 
				
			||||||
 | 
					      const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
 | 
					      const newGroups = [...currentGroups];
 | 
				
			||||||
 | 
					      newGroups[actualIndex] = {
 | 
				
			||||||
 | 
					        ...currentGroup,
 | 
				
			||||||
 | 
					        groupUsers: (currentGroup?.groupUsers ?? []).filter(
 | 
				
			||||||
 | 
					          (user) => user !== userName,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      setConfig("usersGroups", newGroups);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get all groups for selection
 | 
				
			||||||
 | 
					  const getAllGroups = (): UserGroupConfig[] => {
 | 
				
			||||||
 | 
					    const currentConfig = config();
 | 
				
			||||||
 | 
					    return [currentConfig.adminGroupConfig, ...currentConfig.usersGroups];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get currently selected group
 | 
				
			||||||
 | 
					  const getSelectedGroup = (): UserGroupConfig => {
 | 
				
			||||||
 | 
					    const groups = getAllGroups();
 | 
				
			||||||
 | 
					    return groups[selectedGroupIndex()] ?? config().adminGroupConfig;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Basic Configuration Tab
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const BasicTab = () => {
 | 
				
			||||||
 | 
					    return div(
 | 
				
			||||||
 | 
					      { class: "flex flex-col" },
 | 
				
			||||||
 | 
					      label({}, "Detect Interval (ms):"),
 | 
				
			||||||
 | 
					      input({
 | 
				
			||||||
 | 
					        type: "text",
 | 
				
			||||||
 | 
					        value: () => config().detectInterval?.toString() ?? "",
 | 
				
			||||||
 | 
					        onInput: (value) => {
 | 
				
			||||||
 | 
					          const num = validateNumber(value);
 | 
				
			||||||
 | 
					          if (num !== null) setConfig("detectInterval", num);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      label({}, "Watch Interval (ms):"),
 | 
				
			||||||
 | 
					      input({
 | 
				
			||||||
 | 
					        type: "text",
 | 
				
			||||||
 | 
					        value: () => config().watchInterval?.toString() ?? "",
 | 
				
			||||||
 | 
					        onInput: (value) => {
 | 
				
			||||||
 | 
					          const num = validateNumber(value);
 | 
				
			||||||
 | 
					          if (num !== null) setConfig("watchInterval", num);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      label({}, "Notice Times:"),
 | 
				
			||||||
 | 
					      input({
 | 
				
			||||||
 | 
					        type: "text",
 | 
				
			||||||
 | 
					        value: () => config().noticeTimes?.toString() ?? "",
 | 
				
			||||||
 | 
					        onInput: (value) => {
 | 
				
			||||||
 | 
					          const num = validateNumber(value);
 | 
				
			||||||
 | 
					          if (num !== null) setConfig("noticeTimes", num);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      label({}, "Detect Range:"),
 | 
				
			||||||
 | 
					      input({
 | 
				
			||||||
 | 
					        type: "text",
 | 
				
			||||||
 | 
					        value: () => config().detectRange?.toString() ?? "",
 | 
				
			||||||
 | 
					        onInput: (value) => {
 | 
				
			||||||
 | 
					          const num = validateNumber(value);
 | 
				
			||||||
 | 
					          if (num !== null) setConfig("detectRange", num);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      label({}, "Is Warn:"),
 | 
				
			||||||
 | 
					      input({
 | 
				
			||||||
 | 
					        type: "checkbox",
 | 
				
			||||||
 | 
					        checked: () => config().isWarn ?? false,
 | 
				
			||||||
 | 
					        onChange: (checked) => setConfig("isWarn", checked),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Groups Configuration Tab
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const GroupsTab = () => {
 | 
				
			||||||
 | 
					    const groups = getAllGroups();
 | 
				
			||||||
 | 
					    const selectedGroup = getSelectedGroup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row" },
 | 
				
			||||||
 | 
					      // Left side - Groups list
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
 | 
					        label({}, "Groups:"),
 | 
				
			||||||
 | 
					        For({ each: () => groups }, (group, index) =>
 | 
				
			||||||
 | 
					          button(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              class:
 | 
				
			||||||
 | 
					                selectedGroupIndex() === index() ? "bg-blue text-white" : "",
 | 
				
			||||||
 | 
					              onClick: () => setSelectedGroupIndex(index()),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            group.groupName,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Right side - Group details
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col ml-2" },
 | 
				
			||||||
 | 
					        label({}, () => `Group: ${selectedGroup.groupName}`),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Is Allowed:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "checkbox",
 | 
				
			||||||
 | 
					          checked: () => selectedGroup.isAllowed,
 | 
				
			||||||
 | 
					          onChange: (checked) => {
 | 
				
			||||||
 | 
					            const groupIndex = selectedGroupIndex();
 | 
				
			||||||
 | 
					            if (groupIndex === 0) {
 | 
				
			||||||
 | 
					              const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
 | 
					              setConfig("adminGroupConfig", {
 | 
				
			||||||
 | 
					                ...currentAdmin,
 | 
				
			||||||
 | 
					                isAllowed: checked,
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              const actualIndex = groupIndex - 1;
 | 
				
			||||||
 | 
					              const currentGroups = config().usersGroups;
 | 
				
			||||||
 | 
					              const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
 | 
					              const newGroups = [...currentGroups];
 | 
				
			||||||
 | 
					              newGroups[actualIndex] = {
 | 
				
			||||||
 | 
					                ...currentGroup,
 | 
				
			||||||
 | 
					                isAllowed: checked,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					              setConfig("usersGroups", newGroups);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Is Notice:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "checkbox",
 | 
				
			||||||
 | 
					          checked: () => selectedGroup.isNotice,
 | 
				
			||||||
 | 
					          onChange: (checked) => {
 | 
				
			||||||
 | 
					            const groupIndex = selectedGroupIndex();
 | 
				
			||||||
 | 
					            if (groupIndex === 0) {
 | 
				
			||||||
 | 
					              const currentAdmin = config().adminGroupConfig;
 | 
				
			||||||
 | 
					              setConfig("adminGroupConfig", {
 | 
				
			||||||
 | 
					                ...currentAdmin,
 | 
				
			||||||
 | 
					                isNotice: checked,
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              const actualIndex = groupIndex - 1;
 | 
				
			||||||
 | 
					              const currentGroups = config().usersGroups;
 | 
				
			||||||
 | 
					              const currentGroup = currentGroups[actualIndex];
 | 
				
			||||||
 | 
					              const newGroups = [...currentGroups];
 | 
				
			||||||
 | 
					              newGroups[actualIndex] = {
 | 
				
			||||||
 | 
					                ...currentGroup,
 | 
				
			||||||
 | 
					                isNotice: checked,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					              setConfig("usersGroups", newGroups);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Group Users:"),
 | 
				
			||||||
 | 
					        // User management
 | 
				
			||||||
 | 
					        div(
 | 
				
			||||||
 | 
					          { class: "flex flex-row" },
 | 
				
			||||||
 | 
					          input({
 | 
				
			||||||
 | 
					            type: "text",
 | 
				
			||||||
 | 
					            value: newUserName,
 | 
				
			||||||
 | 
					            onInput: setNewUserName,
 | 
				
			||||||
 | 
					            placeholder: "Enter username",
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          button({ onClick: addUser }, "Add"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Users list
 | 
				
			||||||
 | 
					        For({ each: () => selectedGroup.groupUsers ?? [] }, (user) =>
 | 
				
			||||||
 | 
					          div(
 | 
				
			||||||
 | 
					            { class: "flex flex-row items-center" },
 | 
				
			||||||
 | 
					            label({}, user),
 | 
				
			||||||
 | 
					            button(
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                class: "ml-1 bg-red text-white",
 | 
				
			||||||
 | 
					                onClick: () => removeUser(user),
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              "Remove",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Toast Configuration Tab Factory
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const createToastTab = (
 | 
				
			||||||
 | 
					    toastType: "welcomeToastConfig" | "warnToastConfig" | "noticeToastConfig",
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      const toastConfig = config()[toastType];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return div(
 | 
				
			||||||
 | 
					        { class: "flex flex-col" },
 | 
				
			||||||
 | 
					        label({}, "Title (JSON):"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
 | 
				
			||||||
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const parsed = textutils.unserialiseJSON(value);
 | 
				
			||||||
 | 
					              if (parsed != undefined && typeof parsed === "object") {
 | 
				
			||||||
 | 
					                const currentConfig = config();
 | 
				
			||||||
 | 
					                const currentToast = currentConfig[toastType];
 | 
				
			||||||
 | 
					                setConfig(toastType, {
 | 
				
			||||||
 | 
					                  ...currentToast,
 | 
				
			||||||
 | 
					                  title: parsed as MinecraftTextComponent,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch {
 | 
				
			||||||
 | 
					              // Invalid JSON, ignore
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Message (JSON):"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
 | 
				
			||||||
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const parsed = textutils.unserialiseJSON(value);
 | 
				
			||||||
 | 
					              if (parsed != undefined && typeof parsed === "object") {
 | 
				
			||||||
 | 
					                const currentConfig = config();
 | 
				
			||||||
 | 
					                const currentToast = currentConfig[toastType];
 | 
				
			||||||
 | 
					                setConfig(toastType, {
 | 
				
			||||||
 | 
					                  ...currentToast,
 | 
				
			||||||
 | 
					                  msg: parsed as MinecraftTextComponent,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch {
 | 
				
			||||||
 | 
					              // Invalid JSON, ignore
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Prefix:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => toastConfig?.prefix ?? "",
 | 
				
			||||||
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            const currentConfig = config();
 | 
				
			||||||
 | 
					            const currentToast = currentConfig[toastType];
 | 
				
			||||||
 | 
					            setConfig(toastType, { ...currentToast, prefix: value });
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Brackets:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => toastConfig?.brackets ?? "",
 | 
				
			||||||
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            const currentConfig = config();
 | 
				
			||||||
 | 
					            const currentToast = currentConfig[toastType];
 | 
				
			||||||
 | 
					            setConfig(toastType, { ...currentToast, brackets: value });
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label({}, "Bracket Color:"),
 | 
				
			||||||
 | 
					        input({
 | 
				
			||||||
 | 
					          type: "text",
 | 
				
			||||||
 | 
					          value: () => toastConfig?.bracketColor ?? "",
 | 
				
			||||||
 | 
					          onInput: (value) => {
 | 
				
			||||||
 | 
					            const currentConfig = config();
 | 
				
			||||||
 | 
					            const currentToast = currentConfig[toastType];
 | 
				
			||||||
 | 
					            setConfig(toastType, { ...currentToast, bracketColor: value });
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create toast tab components
 | 
				
			||||||
 | 
					  const WelcomeToastTab = createToastTab("welcomeToastConfig");
 | 
				
			||||||
 | 
					  const WarnToastTab = createToastTab("warnToastConfig");
 | 
				
			||||||
 | 
					  const NoticeToastTab = createToastTab("noticeToastConfig");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Error Dialog
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const ErrorDialog = () => {
 | 
				
			||||||
 | 
					    return Show(
 | 
				
			||||||
 | 
					      { when: () => errorState().show },
 | 
				
			||||||
 | 
					      div(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          class:
 | 
				
			||||||
 | 
					            "fixed top-1/4 left-1/4 right-1/4 bottom-1/4 bg-red text-white border",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        div(
 | 
				
			||||||
 | 
					          { class: "flex flex-col p-2" },
 | 
				
			||||||
 | 
					          label({}, () => errorState().message),
 | 
				
			||||||
 | 
					          button(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              class: "mt-2 bg-white text-black",
 | 
				
			||||||
 | 
					              onClick: hideError,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "OK",
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Tab Content Renderer
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const TabContent = () => {
 | 
				
			||||||
 | 
					    const tab = currentTab();
 | 
				
			||||||
 | 
					    if (tab === TABS.BASIC) return BasicTab();
 | 
				
			||||||
 | 
					    if (tab === TABS.GROUPS) return GroupsTab();
 | 
				
			||||||
 | 
					    if (tab === TABS.WELCOME_TOAST) return WelcomeToastTab();
 | 
				
			||||||
 | 
					    if (tab === TABS.WARN_TOAST) return WarnToastTab();
 | 
				
			||||||
 | 
					    if (tab === TABS.NOTICE_TOAST) return NoticeToastTab();
 | 
				
			||||||
 | 
					    return BasicTab(); // fallback
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Main UI Layout
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  return div(
 | 
				
			||||||
 | 
					    { class: "flex flex-col h-full" },
 | 
				
			||||||
 | 
					    // Header
 | 
				
			||||||
 | 
					    h1("Access Control Configuration"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tab bar
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row" },
 | 
				
			||||||
 | 
					      For({ each: () => tabNames }, (tabName, index) =>
 | 
				
			||||||
 | 
					        button(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            class: currentTab() === index() ? "bg-blue text-white" : "",
 | 
				
			||||||
 | 
					            onClick: () => setCurrentTab(index() as TabIndex),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          tabName,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Content area
 | 
				
			||||||
 | 
					    div({ class: "flex-1 p-2" }, TabContent()),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Action buttons
 | 
				
			||||||
 | 
					    div(
 | 
				
			||||||
 | 
					      { class: "flex flex-row justify-center p-2" },
 | 
				
			||||||
 | 
					      button(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          class: "bg-green text-white mr-2",
 | 
				
			||||||
 | 
					          onClick: handleSave,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "Save",
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      button(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          class: "bg-gray text-white",
 | 
				
			||||||
 | 
					          onClick: () => {
 | 
				
			||||||
 | 
					            // Close TUI - this will be handled by the application framework
 | 
				
			||||||
 | 
					            error("TUI_CLOSE");
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "Close",
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Error dialog overlay
 | 
				
			||||||
 | 
					    ErrorDialog(),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Launch the Access Control TUI
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function launchAccessControlTUI(): void {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    render(AccessControlTUI);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    if (e === "TUI_CLOSE" || e === "Terminated") {
 | 
				
			||||||
 | 
					      // Normal exit
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      print("Error in Access Control TUI:");
 | 
				
			||||||
 | 
					      printError(e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Export the main component for external use
 | 
				
			||||||
 | 
					export { AccessControlTUI };
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user