diff --git a/src/index.ts b/src/index.ts index e2e6b97..9ceae64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,15 @@ import { AuroraClient } from "@/lib/BotClient"; import { env } from "@lib/env"; -import { WebServer } from "@/web/server"; +import { webServer } from "./web/src"; // Load commands & events await AuroraClient.loadCommands(); await AuroraClient.loadEvents(); await AuroraClient.deployCommands(); -WebServer.start(); +webServer.start(); +console.log("Web server is running on http://localhost:3000") // login with the token from .env if (!env.DISCORD_BOT_TOKEN) { @@ -18,7 +19,7 @@ AuroraClient.login(env.DISCORD_BOT_TOKEN); // Handle graceful shutdown const shutdownHandler = () => { - WebServer.stop(); + webServer.stop(); AuroraClient.shutdown(); }; diff --git a/src/lib/BotClient.ts b/src/lib/BotClient.ts index d3a0a9a..08f5664 100644 --- a/src/lib/BotClient.ts +++ b/src/lib/BotClient.ts @@ -4,7 +4,6 @@ import type { Command } from "@lib/types"; import { env } from "@lib/env"; import { CommandLoader } from "@lib/loaders/CommandLoader"; import { EventLoader } from "@lib/loaders/EventLoader"; -import { logger } from "@lib/logger"; export class Client extends DiscordClient { @@ -23,25 +22,25 @@ export class Client extends DiscordClient { async loadCommands(reload: boolean = false) { if (reload) { this.commands.clear(); - logger.info("ā™»ļø Reloading commands..."); + console.log("ā™»ļø Reloading commands..."); } const commandsPath = join(import.meta.dir, '../commands'); const result = await this.commandLoader.loadFromDirectory(commandsPath, reload); - logger.info(`šŸ“¦ Command loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); + console.log(`šŸ“¦ Command loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } async loadEvents(reload: boolean = false) { if (reload) { this.removeAllListeners(); - logger.info("ā™»ļø Reloading events..."); + console.log("ā™»ļø Reloading events..."); } const eventsPath = join(import.meta.dir, '../events'); const result = await this.eventLoader.loadFromDirectory(eventsPath, reload); - logger.info(`šŸ“¦ Event loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); + console.log(`šŸ“¦ Event loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } @@ -50,7 +49,7 @@ export class Client extends DiscordClient { // We use env.DISCORD_BOT_TOKEN directly so this can run without client.login() const token = env.DISCORD_BOT_TOKEN; if (!token) { - logger.error("DISCORD_BOT_TOKEN is not set."); + console.error("DISCORD_BOT_TOKEN is not set."); return; } @@ -60,16 +59,16 @@ export class Client extends DiscordClient { const clientId = env.DISCORD_CLIENT_ID; if (!clientId) { - logger.error("DISCORD_CLIENT_ID is not set."); + console.error("DISCORD_CLIENT_ID is not set."); return; } try { - logger.info(`Started refreshing ${commandsData.length} application (/) commands.`); + console.log(`Started refreshing ${commandsData.length} application (/) commands.`); let data; if (guildId) { - logger.info(`Registering commands to guild: ${guildId}`); + console.log(`Registering commands to guild: ${guildId}`); data = await rest.put( Routes.applicationGuildCommands(clientId, guildId), { body: commandsData }, @@ -77,20 +76,20 @@ export class Client extends DiscordClient { // Clear global commands to avoid duplicates await rest.put(Routes.applicationCommands(clientId), { body: [] }); } else { - logger.info('Registering commands globally'); + console.log('Registering commands globally'); data = await rest.put( Routes.applicationCommands(clientId), { body: commandsData }, ); } - logger.success(`Successfully reloaded ${(data as any).length} application (/) commands.`); + console.log(`Successfully reloaded ${(data as any).length} application (/) commands.`); } catch (error: any) { if (error.code === 50001) { - logger.warn("Missing Access: The bot is not in the guild or lacks 'applications.commands' scope."); - logger.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'."); + console.warn("Missing Access: The bot is not in the guild or lacks 'applications.commands' scope."); + console.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'."); } else { - logger.error(error); + console.error(error); } } } @@ -99,22 +98,22 @@ export class Client extends DiscordClient { const { setShuttingDown, waitForTransactions } = await import("./shutdown"); const { closeDatabase } = await import("./DrizzleClient"); - logger.info("šŸ›‘ Shutdown signal received. Starting graceful shutdown..."); + console.log("šŸ›‘ Shutdown signal received. Starting graceful shutdown..."); setShuttingDown(true); // Wait for transactions to complete - logger.info("ā³ Waiting for active transactions to complete..."); + console.log("ā³ Waiting for active transactions to complete..."); await waitForTransactions(10000); // Destroy Discord client - logger.info("šŸ”Œ Disconnecting from Discord..."); + console.log("šŸ”Œ Disconnecting from Discord..."); this.destroy(); // Close database - logger.info("šŸ—„ļø Closing database connection..."); + console.log("šŸ—„ļø Closing database connection..."); await closeDatabase(); - logger.success("šŸ‘‹ Graceful shutdown complete. Exiting."); + console.log("šŸ‘‹ Graceful shutdown complete. Exiting."); process.exit(0); } } diff --git a/src/lib/handlers/AutocompleteHandler.ts b/src/lib/handlers/AutocompleteHandler.ts index 8993e68..61b5132 100644 --- a/src/lib/handlers/AutocompleteHandler.ts +++ b/src/lib/handlers/AutocompleteHandler.ts @@ -1,6 +1,6 @@ import { AutocompleteInteraction } from "discord.js"; import { AuroraClient } from "@/lib/BotClient"; -import { logger } from "@lib/logger"; + /** * Handles autocomplete interactions for slash commands @@ -16,7 +16,7 @@ export class AutocompleteHandler { try { await command.autocomplete(interaction); } catch (error) { - logger.error(`Error handling autocomplete for ${interaction.commandName}:`, error); + console.error(`Error handling autocomplete for ${interaction.commandName}:`, error); } } } diff --git a/src/lib/handlers/CommandHandler.ts b/src/lib/handlers/CommandHandler.ts index 61f7210..9deee7f 100644 --- a/src/lib/handlers/CommandHandler.ts +++ b/src/lib/handlers/CommandHandler.ts @@ -2,7 +2,7 @@ import { ChatInputCommandInteraction, MessageFlags } from "discord.js"; import { AuroraClient } from "@/lib/BotClient"; import { userService } from "@/modules/user/user.service"; import { createErrorEmbed } from "@lib/embeds"; -import { logger } from "@lib/logger"; + /** * Handles slash command execution @@ -13,7 +13,7 @@ export class CommandHandler { const command = AuroraClient.commands.get(interaction.commandName); if (!command) { - logger.error(`No command matching ${interaction.commandName} was found.`); + console.error(`No command matching ${interaction.commandName} was found.`); return; } @@ -21,14 +21,14 @@ export class CommandHandler { try { await userService.getOrCreateUser(interaction.user.id, interaction.user.username); } catch (error) { - logger.error("Failed to ensure user exists:", error); + console.error("Failed to ensure user exists:", error); } try { await command.execute(interaction); AuroraClient.lastCommandTimestamp = Date.now(); } catch (error) { - logger.error(String(error)); + console.error(String(error)); const errorEmbed = createErrorEmbed('There was an error while executing this command!'); if (interaction.replied || interaction.deferred) { diff --git a/src/lib/handlers/ComponentInteractionHandler.ts b/src/lib/handlers/ComponentInteractionHandler.ts index b48501e..4773bee 100644 --- a/src/lib/handlers/ComponentInteractionHandler.ts +++ b/src/lib/handlers/ComponentInteractionHandler.ts @@ -1,5 +1,5 @@ import { ButtonInteraction, StringSelectMenuInteraction, ModalSubmitInteraction, MessageFlags } from "discord.js"; -import { logger } from "@lib/logger"; + import { UserError } from "@lib/errors"; import { createErrorEmbed } from "@lib/embeds"; @@ -28,7 +28,7 @@ export class ComponentInteractionHandler { return; } } else { - logger.error(`Handler method ${route.method} not found in module`); + console.error(`Handler method ${route.method} not found in module`); } } } @@ -52,7 +52,7 @@ export class ComponentInteractionHandler { // Log system errors (non-user errors) for debugging if (!isUserError) { - logger.error(`Error in ${handlerName}:`, error); + console.error(`Error in ${handlerName}:`, error); } const errorEmbed = createErrorEmbed(errorMessage); @@ -72,7 +72,7 @@ export class ComponentInteractionHandler { } } catch (replyError) { // If we can't send a reply, log it - logger.error(`Failed to send error response in ${handlerName}:`, replyError); + console.error(`Failed to send error response in ${handlerName}:`, replyError); } } } diff --git a/src/lib/loaders/CommandLoader.ts b/src/lib/loaders/CommandLoader.ts index aba877a..d5c5323 100644 --- a/src/lib/loaders/CommandLoader.ts +++ b/src/lib/loaders/CommandLoader.ts @@ -4,7 +4,7 @@ import type { Command } from "@lib/types"; import { config } from "@lib/config"; import type { LoadResult, LoadError } from "./types"; import type { Client } from "../BotClient"; -import { logger } from "@lib/logger"; + /** * Handles loading commands from the file system @@ -45,7 +45,7 @@ export class CommandLoader { await this.loadCommandFile(filePath, reload, result); } } catch (error) { - logger.error(`Error reading directory ${dir}:`, error); + console.error(`Error reading directory ${dir}:`, error); result.errors.push({ file: dir, error }); } } @@ -60,7 +60,7 @@ export class CommandLoader { const commands = Object.values(commandModule); if (commands.length === 0) { - logger.warn(`No commands found in ${filePath}`); + console.warn(`No commands found in ${filePath}`); result.skipped++; return; } @@ -74,21 +74,21 @@ export class CommandLoader { const isEnabled = config.commands[command.data.name] !== false; if (!isEnabled) { - logger.info(`🚫 Skipping disabled command: ${command.data.name}`); + console.log(`🚫 Skipping disabled command: ${command.data.name}`); result.skipped++; continue; } this.client.commands.set(command.data.name, command); - logger.success(`Loaded command: ${command.data.name}`); + console.log(`Loaded command: ${command.data.name}`); result.loaded++; } else { - logger.warn(`Skipping invalid command in ${filePath}`); + console.warn(`Skipping invalid command in ${filePath}`); result.skipped++; } } } catch (error) { - logger.error(`Failed to load command from ${filePath}:`, error); + console.error(`Failed to load command from ${filePath}:`, error); result.errors.push({ file: filePath, error }); } } diff --git a/src/lib/loaders/EventLoader.ts b/src/lib/loaders/EventLoader.ts index c81c912..c2faad1 100644 --- a/src/lib/loaders/EventLoader.ts +++ b/src/lib/loaders/EventLoader.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import type { Event } from "@lib/types"; import type { LoadResult } from "./types"; import type { Client } from "../BotClient"; -import { logger } from "@lib/logger"; + /** * Handles loading events from the file system @@ -44,7 +44,7 @@ export class EventLoader { await this.loadEventFile(filePath, reload, result); } } catch (error) { - logger.error(`Error reading directory ${dir}:`, error); + console.error(`Error reading directory ${dir}:`, error); result.errors.push({ file: dir, error }); } } @@ -64,14 +64,14 @@ export class EventLoader { } else { this.client.on(event.name, (...args) => event.execute(...args)); } - logger.success(`Loaded event: ${event.name}`); + console.log(`Loaded event: ${event.name}`); result.loaded++; } else { - logger.warn(`Skipping invalid event in ${filePath}`); + console.warn(`Skipping invalid event in ${filePath}`); result.skipped++; } } catch (error) { - logger.error(`Failed to load event from ${filePath}:`, error); + console.error(`Failed to load event from ${filePath}:`, error); result.errors.push({ file: filePath, error }); } } diff --git a/src/lib/logger.test.ts b/src/lib/logger.test.ts deleted file mode 100644 index 6183666..0000000 --- a/src/lib/logger.test.ts +++ /dev/null @@ -1,38 +0,0 @@ - -import { describe, it, expect, beforeEach } from "bun:test"; -import { logger, getRecentLogs } from "./logger"; - -describe("Logger Buffer", () => { - // Note: Since the buffer is a module-level variable, it persists across tests. - // In a real scenario we might want a reset function, but for now we'll just check relative additions. - - it("should add logs to the buffer", () => { - const initialLength = getRecentLogs().length; - logger.info("Test Info Log"); - const newLogs = getRecentLogs(); - - expect(newLogs.length).toBe(initialLength + 1); - expect(newLogs[0]?.message).toBe("Test Info Log"); - expect(newLogs[0]?.type).toBe("info"); - }); - - it("should cap the buffer size at 50", () => { - // Fill the buffer - for (let i = 0; i < 60; i++) { - logger.debug(`Log overflow test ${i}`); - } - - const logs = getRecentLogs(); - expect(logs.length).toBeLessThanOrEqual(50); - expect(logs[0]?.message).toBe("Log overflow test 59"); - }); - - it("should handle different log levels", () => { - logger.error("Critical Error"); - logger.success("Operation Successful"); - - const logs = getRecentLogs(); - expect(logs[0]?.type).toBe("success"); - expect(logs[1]?.type).toBe("error"); - }); -}); diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index 998abb3..0000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { WebServer } from "@/web/server"; - -/** - * Centralized logging utility with consistent formatting - */ - -const LOG_BUFFER_SIZE = 50; -const logBuffer: Array<{ time: string; type: string; message: string }> = []; - -function addToBuffer(type: string, message: string) { - const time = new Date().toLocaleTimeString(); - logBuffer.unshift({ time, type, message }); - if (logBuffer.length > LOG_BUFFER_SIZE) { - logBuffer.pop(); - } -} - -export function getRecentLogs() { - return logBuffer; -} - -export const logger = { - /** - * General information message - */ - info: (message: string, ...args: any[]) => { - console.log(`ā„¹ļø ${message}`, ...args); - addToBuffer("info", message); - try { WebServer.broadcastLog("info", message); } catch { } - }, - - /** - * Success message - */ - success: (message: string, ...args: any[]) => { - console.log(`āœ… ${message}`, ...args); - addToBuffer("success", message); - try { WebServer.broadcastLog("success", message); } catch { } - }, - - /** - * Warning message - */ - warn: (message: string, ...args: any[]) => { - console.warn(`āš ļø ${message}`, ...args); - addToBuffer("warning", message); - try { WebServer.broadcastLog("warning", message); } catch { } - }, - - /** - * Error message - */ - error: (message: string, ...args: any[]) => { - console.error(`āŒ ${message}`, ...args); - addToBuffer("error", message); - try { WebServer.broadcastLog("error", message); } catch { } - }, - - /** - * Debug message - */ - debug: (message: string, ...args: any[]) => { - console.log(`šŸ” ${message}`, ...args); - addToBuffer("debug", message); - try { WebServer.broadcastLog("debug", message); } catch { } - }, -}; diff --git a/src/lib/shutdown.ts b/src/lib/shutdown.ts index eddc1cf..731607f 100644 --- a/src/lib/shutdown.ts +++ b/src/lib/shutdown.ts @@ -1,4 +1,4 @@ -import { logger } from "@lib/logger"; + let shuttingDown = false; let activeTransactions = 0; @@ -22,7 +22,7 @@ export const waitForTransactions = async (timeoutMs: number = 10000) => { const start = Date.now(); while (activeTransactions > 0) { if (Date.now() - start > timeoutMs) { - logger.warn(`Shutdown timed out waiting for ${activeTransactions} transactions after ${timeoutMs}ms`); + console.warn(`Shutdown timed out waiting for ${activeTransactions} transactions after ${timeoutMs}ms`); break; } await new Promise(resolve => setTimeout(resolve, 100)); diff --git a/src/web/.gitignore b/src/web/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/src/web/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 0000000..5a22787 --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,21 @@ +# bun-react-tailwind-shadcn-template + +To install dependencies: + +```bash +bun install +``` + +To start a development server: + +```bash +bun dev +``` + +To run for production: + +```bash +bun start +``` + +This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/src/web/build.ts b/src/web/build.ts new file mode 100644 index 0000000..f3c5cd4 --- /dev/null +++ b/src/web/build.ts @@ -0,0 +1,149 @@ +#!/usr/bin/env bun +import plugin from "bun-plugin-tailwind"; +import { existsSync } from "fs"; +import { rm } from "fs/promises"; +import path from "path"; + +if (process.argv.includes("--help") || process.argv.includes("-h")) { + console.log(` +šŸ—ļø Bun Build Script + +Usage: bun run build.ts [options] + +Common Options: + --outdir Output directory (default: "dist") + --minify Enable minification (or --minify.whitespace, --minify.syntax, etc) + --sourcemap Sourcemap type: none|linked|inline|external + --target Build target: browser|bun|node + --format Output format: esm|cjs|iife + --splitting Enable code splitting + --packages Package handling: bundle|external + --public-path Public path for assets + --env Environment handling: inline|disable|prefix* + --conditions Package.json export conditions (comma separated) + --external External packages (comma separated) + --banner Add banner text to output + --footer Add footer text to output + --define Define global constants (e.g. --define.VERSION=1.0.0) + --help, -h Show this help message + +Example: + bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom +`); + process.exit(0); +} + +const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase()); + +const parseValue = (value: string): any => { + if (value === "true") return true; + if (value === "false") return false; + + if (/^\d+$/.test(value)) return parseInt(value, 10); + if (/^\d*\.\d+$/.test(value)) return parseFloat(value); + + if (value.includes(",")) return value.split(",").map(v => v.trim()); + + return value; +}; + +function parseArgs(): Partial { + const config: Partial = {}; + const args = process.argv.slice(2); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === undefined) continue; + if (!arg.startsWith("--")) continue; + + if (arg.startsWith("--no-")) { + const key = toCamelCase(arg.slice(5)); + config[key] = false; + continue; + } + + if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) { + const key = toCamelCase(arg.slice(2)); + config[key] = true; + continue; + } + + let key: string; + let value: string; + + if (arg.includes("=")) { + [key, value] = arg.slice(2).split("=", 2) as [string, string]; + } else { + key = arg.slice(2); + value = args[++i] ?? ""; + } + + key = toCamelCase(key); + + if (key.includes(".")) { + const [parentKey, childKey] = key.split("."); + config[parentKey] = config[parentKey] || {}; + config[parentKey][childKey] = parseValue(value); + } else { + config[key] = parseValue(value); + } + } + + return config; +} + +const formatFileSize = (bytes: number): string => { + const units = ["B", "KB", "MB", "GB"]; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(2)} ${units[unitIndex]}`; +}; + +console.log("\nšŸš€ Starting build process...\n"); + +const cliConfig = parseArgs(); +const outdir = cliConfig.outdir || path.join(process.cwd(), "dist"); + +if (existsSync(outdir)) { + console.log(`šŸ—‘ļø Cleaning previous build at ${outdir}`); + await rm(outdir, { recursive: true, force: true }); +} + +const start = performance.now(); + +const entrypoints = [...new Bun.Glob("**.html").scanSync("src")] + .map(a => path.resolve("src", a)) + .filter(dir => !dir.includes("node_modules")); +console.log(`šŸ“„ Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`); + +const result = await Bun.build({ + entrypoints, + outdir, + plugins: [plugin], + minify: true, + target: "browser", + sourcemap: "linked", + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, + ...cliConfig, +}); + +const end = performance.now(); + +const outputTable = result.outputs.map(output => ({ + File: path.relative(process.cwd(), output.path), + Type: output.kind, + Size: formatFileSize(output.size), +})); + +console.table(outputTable); +const buildTime = (end - start).toFixed(2); + +console.log(`\nāœ… Build completed in ${buildTime}ms\n`); diff --git a/src/web/bun-env.d.ts b/src/web/bun-env.d.ts new file mode 100644 index 0000000..72f1c26 --- /dev/null +++ b/src/web/bun-env.d.ts @@ -0,0 +1,17 @@ +// Generated by `bun init` + +declare module "*.svg" { + /** + * A path to the SVG file + */ + const path: `${string}.svg`; + export = path; +} + +declare module "*.module.css" { + /** + * A record of class names to their corresponding CSS module classes + */ + const classes: { readonly [key: string]: string }; + export = classes; +} diff --git a/src/web/bun.lock b/src/web/bun.lock new file mode 100644 index 0000000..236c916 --- /dev/null +++ b/src/web/bun.lock @@ -0,0 +1,199 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "bun-react-template", + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "bun-plugin-tailwind": "^0.1.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.545.0", + "react": "^19", + "react-dom": "^19", + "tailwind-merge": "^3.3.1", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.4.0", + }, + }, + }, + "packages": { + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8GvNtMo0NINM7Emk9cNAviCG3teEgr3BUX9be0+GD029zIagx2Sf54jMui1Eu1IpFm7nWHODuLEefGOQNaJ0gQ=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-r33eHQOHAwkuiBJIwmkXIyqONQOQMnd1GMTpDzaxx9vf9+svby80LZO9Hcm1ns6KT/TBRFyODC/0loA7FAaffg=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-p5q3rJk48qhLuLBOFehVc+kqCE03YrswTc6NCxbwsxiwfySXwcAvpF2KWKF/ZZObvvR8hCCvqe1F81b2p5r2dg=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-zkcHPI23QxJ1TdqafhgkXt1NOEN8o5C460sVeNnrhfJ43LwZgtfcvcQE39x/pBedu67fatY8CU0iY00nOh46ZQ=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-HKBeUlJdNduRkzJKZ5DXM+pPqntfC50/Hu2X65jVX0Y7hu/6IC8RaUTqpr8FtCZqqmc9wDK0OTL+Mbi9UQIKYQ=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-n7zhKTSDZS0yOYg5Rq8easZu5Y/o47sv0c7yGr2ciFdcie9uYV55fZ7QMqhWMGK33ezCSikh5EDkUMCIvfWpjA=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FeCQyBU62DMuB0nn01vPnf3McXrKOsrK9p7sHaBFYycw0mmoU8kCq/WkBkGMnLuvQljJSyen8QBTx+fXdNupWg=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XkCCHkByYn8BIDvoxnny898znju4xnW2kvFE8FT5+0Y62cWdcBGMZ9RdsEUTeRz16k8hHtJpaSfLcEmNTFIwRQ=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-TJiYC7KCr0XxFTsxgwQOeE7dncrEL/RSyL0EzSL3xRkrxJMWBCvCSjQn7LV1i6T7hFst0+3KoN3VWvD5BinqHA=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-T3xkODItb/0ftQPFsZDc7EAX2D6A4TEazQ2YZyofZToO8Q7y8YT8ooWdhd0BQiTCd66uEvgE1DCZetynwg2IoA=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-rtVQB9/1XK8FWJgFtsOthbPifRMYypgJwxu+pK3NHx8WvFKmq7HcPDqNr8xLzGULjQEO7eAo2aOZfONOwYz+5g=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "bun": ["bun@1.3.5", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.5", "@oven/bun-darwin-x64": "1.3.5", "@oven/bun-darwin-x64-baseline": "1.3.5", "@oven/bun-linux-aarch64": "1.3.5", "@oven/bun-linux-aarch64-musl": "1.3.5", "@oven/bun-linux-x64": "1.3.5", "@oven/bun-linux-x64-baseline": "1.3.5", "@oven/bun-linux-x64-musl": "1.3.5", "@oven/bun-linux-x64-musl-baseline": "1.3.5", "@oven/bun-windows-x64": "1.3.5", "@oven/bun-windows-x64-baseline": "1.3.5" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw=="], + + "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], + + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + } +} diff --git a/src/web/bunfig.toml b/src/web/bunfig.toml new file mode 100644 index 0000000..0175d44 --- /dev/null +++ b/src/web/bunfig.toml @@ -0,0 +1,3 @@ +[serve.static] +plugins = ["bun-plugin-tailwind"] +env = "BUN_PUBLIC_*" diff --git a/src/web/components.json b/src/web/components.json new file mode 100644 index 0000000..a084701 --- /dev/null +++ b/src/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/src/web/package.json b/src/web/package.json new file mode 100644 index 0000000..1082313 --- /dev/null +++ b/src/web/package.json @@ -0,0 +1,30 @@ +{ + "name": "bun-react-template", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun --hot src/index.ts", + "start": "NODE_ENV=production bun src/index.ts", + "build": "bun run build.ts" + }, + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "bun-plugin-tailwind": "^0.1.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.545.0", + "react": "^19", + "react-dom": "^19", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/bun": "latest", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.4.0" + } +} diff --git a/src/web/public/script.js b/src/web/public/script.js deleted file mode 100644 index 51d88fa..0000000 --- a/src/web/public/script.js +++ /dev/null @@ -1,216 +0,0 @@ -function formatUptime(seconds) { - if (seconds < 0) return "0s"; - - const days = Math.floor(seconds / (3600 * 24)); - const hours = Math.floor((seconds % (3600 * 24)) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); - - const parts = []; - if (days > 0) parts.push(`${days}d`); - if (hours > 0) parts.push(`${hours}h`); - if (minutes > 0) parts.push(`${minutes}m`); - parts.push(`${secs}s`); - - return parts.join(" "); -} - -function updateUptime() { - const elements = document.querySelectorAll(".uptime-display, #uptime-display"); - elements.forEach(el => { - const startTimestamp = parseInt(el.getAttribute("data-start-timestamp"), 10); - if (isNaN(startTimestamp)) return; - - const now = Date.now(); - const elapsedSeconds = (now - startTimestamp) / 1000; - - el.textContent = formatUptime(elapsedSeconds); - }); -} - -document.addEventListener("DOMContentLoaded", () => { - // Initialize Lucide Icons - if (window.lucide) { - window.lucide.createIcons(); - } - - // Update immediately to prevent stale content flash if possible - updateUptime(); - // Update every second - setInterval(updateUptime, 1000); - - // WebSocket Connection - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - function connectWs() { - const ws = new WebSocket(wsUrl); - const statusIndicator = document.querySelector(".status-indicator"); - - ws.onopen = () => { - console.log("WS Connected"); - if (statusIndicator) statusIndicator.classList.add("online"); - }; - - ws.onmessage = (event) => { - try { - const msg = JSON.parse(event.data); - if (msg.type === "HEARTBEAT") { - updateVitals(msg.data); - } else if (msg.type === "WELCOME") { - console.log(msg.message); - } else if (msg.type === "LOG") { - appendToActivityFeed(msg.data); - } - } catch (e) { - console.error("WS Parse Error", e); - } - }; - - function updateVitals(data) { - // Update Stats - if (data.guildCount !== undefined) { - const el = document.getElementById("stat-servers"); - if (el) el.textContent = data.guildCount; - } - if (data.userCount !== undefined) { - const el = document.getElementById("stat-users"); - if (el) el.textContent = data.userCount; - } - if (data.commandCount !== undefined) { - const el = document.getElementById("stat-commands"); - if (el) el.textContent = data.commandCount; - } - if (data.ping !== undefined) { - const el = document.getElementById("stat-ping"); - if (el) el.textContent = `${data.ping < 0 ? "?" : data.ping}ms`; - - const trend = document.getElementById("stat-ping-trend"); - if (trend) { - trend.className = `stat-trend ${data.ping < 100 ? "up" : "down"}`; - const icon = trend.querySelector('i'); - if (icon) { - icon.setAttribute('data-lucide', data.ping < 100 ? "check-circle" : "alert-circle"); - if (window.lucide) window.lucide.createIcons(); - } - const text = trend.querySelector('span'); - if (text) text.textContent = data.ping < 100 ? "Excellent" : "Decent"; - } - } - - // Update System Health - if (data.memory !== undefined) { - const el = document.getElementById("stat-memory"); - if (el && data.memoryTotal) { - el.textContent = `${data.memory} / ${data.memoryTotal} MB`; - } else if (el) { - el.textContent = `${data.memory} MB`; - } - - const bar = document.getElementById("stat-memory-bar"); - if (bar && data.memoryTotal) { - const percent = Math.min(100, (data.memory / data.memoryTotal) * 100); - bar.style.width = `${percent}%`; - } - } - - if (data.uptime !== undefined) { - // We handle uptime fluidly in updateUptime() using data-start-timestamp. - // We just ensure the attribute is kept in sync if needed (though startTimestamp shouldn't change). - const elements = document.querySelectorAll(".uptime-display, #uptime-display"); - elements.forEach(el => { - const currentStart = parseInt(el.getAttribute("data-start-timestamp"), 10); - const newStart = Math.floor(Date.now() - (data.uptime * 1000)); - // Only update if there's a significant drift (> 5s) - if (isNaN(currentStart) || Math.abs(currentStart - newStart) > 5000) { - el.setAttribute("data-start-timestamp", newStart); - } - }); - } - } - - function appendToActivityFeed(log) { - const list = document.querySelector(".activity-feed"); - if (!list) return; - - const item = document.createElement("li"); - item.className = "activity-item"; - - const timeSpan = document.createElement("span"); - timeSpan.className = "time"; - timeSpan.textContent = log.timestamp; - - const messageSpan = document.createElement("span"); - messageSpan.className = "message"; - messageSpan.textContent = log.message; - - item.appendChild(timeSpan); - item.appendChild(messageSpan); - - // Prepend - list.insertBefore(item, list.firstChild); - if (list.children.length > 50) list.lastChild.remove(); - } - - ws.onclose = () => { - console.log("WS Disconnected"); - if (statusIndicator) statusIndicator.classList.remove("online"); - // Retry in 5s - setTimeout(connectWs, 5000); - }; - - ws.onerror = (err) => { - console.error("WS Error", err); - ws.close(); - }; - } - - // Action Buttons - document.querySelectorAll(".btn[data-action]").forEach(btn => { - btn.addEventListener("click", async () => { - const action = btn.getAttribute("data-action"); - const actionName = btn.textContent.trim(); - - if (action === "restart_bot") { - if (!confirm("Are you sure you want to restart the bot? This will cause a brief downtime.")) { - return; - } - } - - btn.disabled = true; - const originalText = btn.textContent; - btn.textContent = "Processing..."; - - try { - const response = await fetch("/api/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action }) - }); - - const result = await response.json(); - if (result.success) { - alert(`${actionName} successful: ${result.message}`); - if (action === "restart_bot") { - btn.textContent = "Restarting..."; - setTimeout(() => window.location.reload(), 5000); - } else { - btn.disabled = false; - btn.textContent = originalText; - } - } else { - alert(`Error: ${result.error}`); - btn.disabled = false; - btn.textContent = originalText; - } - } catch (err) { - console.error("Action error:", err); - alert("Failed to execute action. Check console."); - btn.disabled = false; - btn.textContent = originalText; - } - }); - }); - - connectWs(); -}); diff --git a/src/web/public/style.css b/src/web/public/style.css deleted file mode 100644 index e8ca21f..0000000 --- a/src/web/public/style.css +++ /dev/null @@ -1,385 +0,0 @@ -:root { - /* Geist Inspired Minimal Palette */ - --background: #000; - --foreground: #fff; - --accents-1: #111; - --accents-2: #333; - --accents-3: #444; - --accents-4: #666; - --accents-5: #888; - --accents-6: #999; - --accents-7: #eaeaea; - --accents-8: #fafafa; - - --success: #0070f3; - --success-light: #3291ff; - --error: #ee0000; - --error-light: #ff1a1a; - --warning: #f5a623; - --warning-light: #f7b955; - - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - --font-mono: 'Menlo', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; - - --radius: 5px; - --header-height: 64px; - --sidebar-width: 240px; -} - -* { - box-sizing: border-box; -} - -body { - background: var(--background); - color: var(--foreground); - font-family: var(--font-sans); - margin: 0; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-size: 14px; -} - -/* Typography */ -h1, -h2, -h3, -h4 { - font-weight: 600; - margin: 0; - color: var(--foreground); -} - -h1 { - font-size: 2rem; - letter-spacing: -0.05rem; -} - -h2 { - font-size: 1.5rem; - letter-spacing: -0.02rem; -} - -h3 { - font-size: 1rem; -} - -a { - color: var(--accents-5); - text-decoration: none; - transition: color 0.2s ease; -} - -a:hover { - color: var(--foreground); -} - -/* Header */ -header { - height: var(--header-height); - border-bottom: 1px solid var(--accents-2); - display: flex; - align-items: center; - padding: 0 24px; - position: sticky; - top: 0; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: saturate(180%) blur(5px); - z-index: 1000; -} - -header h1 { - font-size: 1.25rem; - font-weight: 700; -} - -header nav { - margin-left: 24px; - display: flex; - gap: 16px; -} - -header nav a { - font-size: 0.875rem; -} - -header nav a.active { - color: var(--foreground); -} - -/* Layout */ -main { - max-width: 1000px; - margin: 0 auto; - padding: 48px 24px; -} - -/* Dashboard Grid */ -.dashboard-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; -} - -/* Cards */ -.card, -.panel, -.stat-card { - background: var(--background); - border: 1px solid var(--accents-2); - border-radius: var(--radius); - padding: 24px; - transition: border-color 0.2s ease; -} - -.card:hover, -.panel:hover, -.stat-card:hover { - border-color: var(--accents-4); -} - -.stat-card { - display: flex; - flex-direction: column; - justify-content: space-between; - min-height: 120px; -} - -.stat-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.stat-header h3 { - font-size: 0.75rem; - text-transform: uppercase; - color: var(--accents-5); - letter-spacing: 1px; -} - -.stat-value { - font-size: 2rem; - font-weight: 700; - letter-spacing: -1px; -} - -.stat-trend { - font-size: 0.75rem; - margin-top: 8px; - color: var(--accents-5); - display: flex; - align-items: center; - gap: 4px; -} - -.stat-trend.up { - color: var(--success); -} - -.stat-trend.down { - color: var(--error); -} - -/* Panels */ -.dashboard-main { - grid-column: 1 / -1; - display: grid; - grid-template-columns: 1.5fr 1fr; - gap: 24px; - margin-top: 24px; -} - -.panel-header { - margin-bottom: 20px; -} - -.panel-header h2 { - font-size: 1.1rem; - font-weight: 600; -} - -/* Activity Feed */ -.activity-feed { - list-style: none; - padding: 0; - margin: 0; - max-height: 400px; - overflow-y: auto; -} - -.activity-item { - padding: 12px 0; - border-bottom: 1px solid var(--accents-2); - font-size: 0.875rem; -} - -.activity-item:last-child { - border-bottom: none; -} - -.activity-item .time { - color: var(--accents-4); - font-family: var(--font-mono); - font-size: 0.75rem; - display: block; - margin-bottom: 4px; -} - -/* Badges */ -.badge { - padding: 2px 8px; - border-radius: 20px; - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; -} - -.badge.live { - background: rgba(0, 112, 243, 0.1); - color: var(--success); - border: 1px solid rgba(0, 112, 243, 0.2); -} - -/* System Metrics */ -.metrics-grid { - display: flex; - flex-direction: column; - gap: 16px; -} - -.metric-card { - padding: 12px; - border: 1px solid var(--accents-2); - border-radius: var(--radius); -} - -.metric-header { - display: flex; - justify-content: space-between; - font-size: 0.8rem; - margin-bottom: 8px; -} - -.metric-label { - color: var(--accents-5); -} - -.metric-value { - font-weight: 500; - font-family: var(--font-mono); -} - -.progress-bar-bg { - height: 4px; - background: var(--accents-2); - border-radius: 2px; - overflow: hidden; -} - -.progress-bar-fill { - height: 100%; - background: var(--foreground); - transition: width 0.3s ease; -} - -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 16px; - height: 32px; - border-radius: var(--radius); - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid var(--accents-2); - background: var(--background); - color: var(--foreground); -} - -.btn:hover:not(:disabled) { - border-color: var(--foreground); -} - -.btn-primary { - background: var(--foreground); - color: var(--background); - border: 1px solid var(--foreground); -} - -.btn-primary:hover:not(:disabled) { - background: var(--background); - color: var(--foreground); -} - -.btn-danger { - color: var(--error); - border-color: var(--error); -} - -.btn-danger:hover:not(:disabled) { - background: var(--error); - color: white; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Control Panel */ -.control-panel { - grid-column: 1 / -1; - margin-top: 24px; -} - -.action-buttons { - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -/* Footer */ -footer { - padding: 48px 24px; - border-top: 1px solid var(--accents-2); - color: var(--accents-5); - font-size: 0.8rem; -} - -.footer-content { - max-width: 1000px; - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; -} - -.status-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; - background: var(--accents-4); - margin-right: 8px; -} - -.status-indicator.online { - background: var(--success); - box-shadow: 0 0 8px var(--success); -} - -/* Responsive */ -@media (max-width: 768px) { - .dashboard-grid { - grid-template-columns: 1fr; - } - - .dashboard-main { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/src/web/router.test.ts b/src/web/router.test.ts deleted file mode 100644 index 9f46776..0000000 --- a/src/web/router.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { router } from "./router"; - -describe("Web Router", () => { - it("should return home page on /", async () => { - const req = new Request("http://localhost/"); - const res = await router(req); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); - const text = await res.text(); - expect(text).toContain("Aurora Web"); - expect(text).toContain("Uptime:"); - expect(text).toContain('id="uptime-display"'); - }); - - it("should return dashboard page on /dashboard", async () => { - const req = new Request("http://localhost/dashboard"); - const res = await router(req); - expect(res.status).toBe(200); - expect(await res.text()).toContain("Live Activity"); - }); - - it("should return health check on /health", async () => { - const req = new Request("http://localhost/health"); - const res = await router(req); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("application/json"); - const data = await res.json(); - expect(data).toHaveProperty("status", "ok"); - }); - - it("should block path traversal", async () => { - // Attempts to go up two directories to reach the project root or src - const req = new Request("http://localhost/public/../../package.json"); - const res = await router(req); - // Should be 403 Forbidden or 404 Not Found (our logical change makes it 403) - expect([403, 404]).toContain(res.status); - }); - - it("should serve existing static file", async () => { - // We know style.css exists in src/web/public - const req = new Request("http://localhost/public/style.css"); - const res = await router(req); - expect(res.status).toBe(200); - if (res.status === 200) { - const text = await res.text(); - expect(text).toContain("body"); - } - }); - - it("should not serve static files on non-GET methods", async () => { - const req = new Request("http://localhost/public/style.css", { method: "POST" }); - const res = await router(req); - expect(res.status).toBe(404); - }); - - it("should return 404 for unknown routes", async () => { - const req = new Request("http://localhost/unknown"); - const res = await router(req); - expect(res.status).toBe(404); - }); -}); diff --git a/src/web/router.ts b/src/web/router.ts deleted file mode 100644 index 7ba3e28..0000000 --- a/src/web/router.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { homeRoute } from "./routes/home"; -import { healthRoute } from "./routes/health"; -import { dashboardRoute } from "./routes/dashboard"; -import { file } from "bun"; -import { join, resolve } from "path"; - -export async function router(request: Request): Promise { - const url = new URL(request.url); - const method = request.method; - - // Resolve the absolute path to the public directory - const publicDir = resolve(import.meta.dir, "public"); - - if (method === "GET") { - // Handle Static Files - // We handle requests starting with /public/ OR containing an extension (like /style.css) - if (url.pathname.startsWith("/public/") || url.pathname.includes(".")) { - // Normalize path: remove /public prefix if present so that - // /public/style.css and /style.css both map to .../public/style.css - const relativePath = url.pathname.replace(/^\/public/, ""); - - // Resolve full path - const normalizedRelative = relativePath.startsWith("/") ? "." + relativePath : relativePath; - const requestedPath = resolve(publicDir, normalizedRelative); - - // Security Check: Block Path Traversal - if (requestedPath.startsWith(publicDir)) { - const staticFile = file(requestedPath); - if (await staticFile.exists()) { - return new Response(staticFile); - } - } else { - return new Response("Forbidden", { status: 403 }); - } - } - - if (url.pathname === "/" || url.pathname === "/index.html") { - return homeRoute(); - } - if (url.pathname === "/health") { - return healthRoute(); - } - if (url.pathname === "/dashboard") { - return dashboardRoute(); - } - } - - if (method === "POST") { - if (url.pathname === "/api/actions") { - const { actionsRoute } = await import("./routes/actions"); - return actionsRoute(request); - } - } - - return new Response("Not Found", { status: 404 }); -} diff --git a/src/web/routes/actions.ts b/src/web/routes/actions.ts deleted file mode 100644 index d529c8b..0000000 --- a/src/web/routes/actions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { AuroraClient } from "@/lib/BotClient"; -import { logger } from "@/lib/logger"; - -export async function actionsRoute(request: Request): Promise { - const url = new URL(request.url); - const body = await request.json().catch(() => ({})) as any; - const action = body.action; - - if (!action) { - return new Response(JSON.stringify({ success: false, error: "No action provided" }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); - } - - try { - switch (action) { - case "reload_commands": - logger.info("Web Dashboard: Triggering command reload..."); - await AuroraClient.loadCommands(true); - await AuroraClient.deployCommands(); - return new Response(JSON.stringify({ success: true, message: "Commands reloaded successfully" }), { - headers: { "Content-Type": "application/json" } - }); - - case "clear_cache": - logger.info("Web Dashboard: Triggering cache clear..."); - // For now, we'll reload events and commands as a "clear cache" action - await AuroraClient.loadEvents(true); - await AuroraClient.loadCommands(true); - return new Response(JSON.stringify({ success: true, message: "Cache cleared and systems reloaded" }), { - headers: { "Content-Type": "application/json" } - }); - - case "restart_bot": - logger.info("Web Dashboard: Triggering bot restart..."); - // We don't await this because it will exit the process - setTimeout(() => AuroraClient.shutdown(), 1000); - return new Response(JSON.stringify({ success: true, message: "Bot shutdown initiated. If managed by a process manager, it will restart." }), { - headers: { "Content-Type": "application/json" } - }); - - default: - return new Response(JSON.stringify({ success: false, error: `Unknown action: ${action}` }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); - } - } catch (error: any) { - logger.error(`Error executing action ${action}:`, error); - return new Response(JSON.stringify({ success: false, error: error.message }), { - status: 500, - headers: { "Content-Type": "application/json" } - }); - } -} diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts deleted file mode 100644 index b167bc3..0000000 --- a/src/web/routes/dashboard.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { BaseLayout } from "../views/layout"; - -import { AuroraClient } from "@/lib/BotClient"; -import { getRecentLogs } from "@/lib/logger"; - -export function dashboardRoute(): Response { - - // Gather real data - const guildCount = AuroraClient.guilds.cache.size; - const userCount = AuroraClient.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0); - const commandCount = AuroraClient.commands.size; - const ping = AuroraClient.ws.ping; - - // Real system metrics - const memUsage = process.memoryUsage(); - const memoryUsage = (memUsage.heapUsed / 1024 / 1024).toFixed(2); - const memoryTotal = (memUsage.rss / 1024 / 1024).toFixed(1); - const uptimeSeconds = process.uptime(); - const uptime = new Date(uptimeSeconds * 1000).toISOString().substr(11, 8); // HH:MM:SS - const startTimestamp = Date.now() - (uptimeSeconds * 1000); - - // Real activity logs - const activityLogs = getRecentLogs(); - - const memPercent = Math.min(100, (memUsage.heapUsed / memUsage.rss) * 100).toFixed(1); - - // Get top guilds - const topGuilds = AuroraClient.guilds.cache - .sort((a, b) => b.memberCount - a.memberCount) - .first(5); - - const content = ` -
- -
-
-

Members

- -
-
${userCount.toLocaleString()}
-
- Total user reach -
-
-
-
-

Guilds

- -
-
${guildCount}
-
- Active connections -
-
-
-
-

Latency

- -
-
${ping < 0 ? "?" : ping}ms
-
- - ${ping < 100 ? "Stable" : "High"} -
-
- - -
-
-
-

Activity Flow

- Live -
-
    - ${activityLogs.length > 0 ? activityLogs.map(log => ` -
  • - ${log.time} - ${log.message} -
  • - `).join('') : ` -
  • - Listening for activity... -
  • - `} -
-
- -
-
-
-

Health

-
-
-
-
- Memory - ${memoryUsage} MB -
-
-
-
-
- -
-
- Uptime - ${uptime} -
-
-
-
- -
-
-

Top Guilds

-
-
- ${topGuilds.map(g => ` -
- ${g.name} - ${g.memberCount} members -
- `).join('')} -
-
-
-
- - -
-
-

Quick Actions

-
-
- - - -
-
-
- `; - - const html = BaseLayout({ title: "Dashboard", content }); - - return new Response(html, { - headers: { "Content-Type": "text/html" }, - }); -} diff --git a/src/web/routes/health.ts b/src/web/routes/health.ts deleted file mode 100644 index 75341a4..0000000 --- a/src/web/routes/health.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function healthRoute(): Response { - return new Response(JSON.stringify({ - status: "ok", - uptime: process.uptime(), - timestamp: new Date().toISOString() - }), { - headers: { "Content-Type": "application/json" }, - }); -} diff --git a/src/web/routes/home.ts b/src/web/routes/home.ts deleted file mode 100644 index 060b8a1..0000000 --- a/src/web/routes/home.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseLayout } from "../views/layout"; - -export function homeRoute(): Response { - const content = ` -
-

Welcome

-

The Aurora web server is up and running!

-
- `; - - const html = BaseLayout({ title: "Home", content }); - - return new Response(html, { - headers: { "Content-Type": "text/html" }, - }); -} diff --git a/src/web/server.ts b/src/web/server.ts deleted file mode 100644 index 1b8cde6..0000000 --- a/src/web/server.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { env } from "@/lib/env"; -import { router } from "./router"; -import type { Server } from "bun"; -import { AuroraClient } from "@/lib/BotClient"; - -export class WebServer { - private static server: Server | null = null; - private static heartbeatInterval: ReturnType | null = null; - - public static start(port?: number) { - this.server = Bun.serve({ - port: port ?? env.PORT, - hostname: env.HOST, - fetch: (req, server) => { - const url = new URL(req.url); - if (url.pathname === "/ws") { - // Upgrade the request to a WebSocket - // We pass dummy data for now - if (server.upgrade(req, { data: undefined })) { - return undefined; - } - return new Response("WebSocket upgrade failed", { status: 500 }); - } - return router(req); - }, - websocket: { - open(ws) { - // console.log("ws: client connected"); - ws.subscribe("status-updates"); - ws.send(JSON.stringify({ type: "WELCOME", message: "Connected to Aurora WebSocket" })); - }, - message(ws, message) { - // Handle incoming messages if needed - }, - close(ws) { - // console.log("ws: client disconnected"); - ws.unsubscribe("status-updates"); - }, - }, - }); - - console.log(`🌐 Web server listening on http://${this.server.hostname}:${this.server.port} (Restricted to Local Interface)`); - - // Start a heartbeat loop - this.heartbeatInterval = setInterval(() => { - if (this.server) { - const memoryUsage = process.memoryUsage(); - this.server.publish("status-updates", JSON.stringify({ - type: "HEARTBEAT", - data: { - guildCount: AuroraClient.guilds.cache.size, - userCount: AuroraClient.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), - commandCount: AuroraClient.commands.size, - ping: AuroraClient.ws.ping, - memory: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2), - memoryTotal: (memoryUsage.rss / 1024 / 1024).toFixed(2), - uptime: process.uptime(), - timestamp: Date.now() - } - })); - } - }, 3000); // Update every 3 seconds for better responsiveness - } - - public static stop() { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - if (this.server) { - this.server.stop(); - console.log("šŸ›‘ Web server stopped"); - this.server = null; - } - } - - public static get port(): number | undefined { - return this.server?.port; - } - - public static broadcastLog(type: string, message: string) { - if (this.server) { - this.server.publish("status-updates", JSON.stringify({ - type: "LOG", - data: { - timestamp: new Date().toLocaleTimeString(), - type, - message - } - })); - } - } -} diff --git a/src/web/src/APITester.tsx b/src/web/src/APITester.tsx new file mode 100644 index 0000000..a1f8fff --- /dev/null +++ b/src/web/src/APITester.tsx @@ -0,0 +1,64 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { useRef, type FormEvent } from "react"; + +export function APITester() { + const responseInputRef = useRef(null); + + const testEndpoint = async (e: FormEvent) => { + e.preventDefault(); + + try { + const form = e.currentTarget; + const formData = new FormData(form); + const endpoint = formData.get("endpoint") as string; + const url = new URL(endpoint, location.href); + const method = formData.get("method") as string; + const res = await fetch(url, { method }); + + const data = await res.json(); + responseInputRef.current!.value = JSON.stringify(data, null, 2); + } catch (error) { + responseInputRef.current!.value = String(error); + } + }; + + return ( +
+
+ + + + + +
+ +