diff --git a/src/engine/builders/command.js b/src/engine/builders/command.js new file mode 100644 index 0000000..6bbc62a --- /dev/null +++ b/src/engine/builders/command.js @@ -0,0 +1,64 @@ +import Command from '../command'; + +export default class CommandBuilder { + constructor() { + this.command = new Command(); + } + + withName(name) { + this.command.name = name; + return this; + } + + withAliases(aliases) { + this.command.aliases = Array.isArray(aliases) ? aliases : [aliases]; + return this; + } + + withCategory(category) { + this.command.category = category; + return this; + } + + withDescription(description) { + this.command.description = description; + return this; + } + + withArguments(args) { + this.command.arguments = args; + return this; + } + + withArgument(type, name, optional = false, description = '') { + if (!this.command.arguments) { + this.command.arguments = []; + } + this.command.arguments.push({ + type, + name, + optional, + description + }); + return this; + } + + withHandler(handler) { + this.command.handler = handler; + return this; + } + + isAsync(value = true) { + this.command.async = value; + return this; + } + + withMetadata(metadata) { + Object.assign(this.command, metadata); + return this; + } + + create() { + return this.command; + } +} \ No newline at end of file diff --git a/src/engine/command.js b/src/engine/command.js new file mode 100644 index 0000000..3383818 --- /dev/null +++ b/src/engine/command.js @@ -0,0 +1,31 @@ +export default class Command { + constructor() { + this.name = ''; + this.aliases = []; + this.category = ''; + this.description = ''; + this.arguments = []; + this.handler = null; + this.async = false; + } + + execute(args, context) { + if (this.handler) { + return this.handler(args, context); + } + throw new Error(`Command ${this.name} has no handler defined`); + } + + getMetadata() { + return { + category: this.category, + description: this.description, + arguments: this.arguments, + async: this.async + }; + } + + getAllNames() { + return [this.name, ...this.aliases]; + } +} \ No newline at end of file diff --git a/src/engine/commands.js b/src/engine/commands.js index f5a0ae9..b85ac8a 100644 --- a/src/engine/commands.js +++ b/src/engine/commands.js @@ -9,15 +9,15 @@ import VolumeCommand from "./commands/volume"; import InventoryCommand from "./commands/inventory"; const defaultCommands = [ - [["look", "l"], LookCommand], - [["use", "interact"], UseCommand], - [["take", "get"], TakeCommand], - [["drop", "put"], DropCommand], - ["echo", EchoCommand], - ["save", SaveCommand], - ["load", LoadCommand], - ["volume", VolumeCommand], - [["i", "inv", "inventory"], InventoryCommand] + LookCommand, + UseCommand, + TakeCommand, + DropCommand, + EchoCommand, + SaveCommand, + LoadCommand, + VolumeCommand, + InventoryCommand ]; const directionMap = [ @@ -49,8 +49,15 @@ export default class Commands { } const room = this.context.getRoom(this.context.player.currentRoom); const split = str.split(" "); - if (this.commands.get(split[0])) { - this.commands.get(split[0])(split, this.context); + const command = this.commands.get(split[0]); + if (command) { + if (command.execute) { + // New Command object + command.execute(split, this.context); + } else { + // Old function-based command + command(split, this.context); + } return; } @@ -71,9 +78,27 @@ export default class Commands { } } + addCommandObject(commandObj) { + // Add the main command name + this.commands.set(commandObj.name, commandObj); + // Add all aliases + commandObj.aliases.forEach(alias => { + this.commands.set(alias, commandObj); + }); + } + addCommands(commands) { commands.forEach((command) => { - this.addCommand(command[0], command[1]); + if (command.execute && command.getAllNames) { + // New Command object + this.addCommandObject(command); + } else if (Array.isArray(command)) { + // Old array format + this.addCommand(command[0], command[1]); + } else { + // Old single command + this.addCommand(command.name, command); + } }); } diff --git a/src/engine/commands/drop.js b/src/engine/commands/drop.js index 1d4233d..d18b367 100644 --- a/src/engine/commands/drop.js +++ b/src/engine/commands/drop.js @@ -1,4 +1,6 @@ -function DropCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function dropHandler(args, context) { const room = context.getRoom(context.player.currentRoom); const items = context.player.getInventory(); let item = null; @@ -21,17 +23,11 @@ function DropCommand(args, context) { } } -DropCommand.metadata = { - category: "Actions", - description: "Drop an object", - arguments: [ - { - type: "item", - name: "target", - optional: false, - description: "Object to drop" - } - ] -}; - -export default DropCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("drop") + .withAliases(["put"]) + .withCategory("Actions") + .withDescription("Drop an object") + .withArgument("item", "target", false, "Object to drop") + .withHandler(dropHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/echo.js b/src/engine/commands/echo.js index a1777a5..d44dfa8 100644 --- a/src/engine/commands/echo.js +++ b/src/engine/commands/echo.js @@ -1,4 +1,6 @@ -function EchoCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function echoHandler(args, context) { if (args[1] != "on" && args[1] != "off") { context.print(`Usage: echo `); } else { @@ -7,18 +9,10 @@ function EchoCommand(args, context) { } } -EchoCommand.metadata = { - category: "System", - description: "Toggle command echo", - arguments: [ - { - type: "select", - name: "state", - optional: false, - description: "Echo state", - options: ["on", "off"] - } - ] -}; - -export default EchoCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("echo") + .withCategory("System") + .withDescription("Toggle command echo") + .withArgument("select", "state", false, "Echo state") + .withHandler(echoHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/inventory.js b/src/engine/commands/inventory.js index ca4ced0..b805c56 100644 --- a/src/engine/commands/inventory.js +++ b/src/engine/commands/inventory.js @@ -1,4 +1,6 @@ -function InventoryCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function inventoryHandler(args, context) { const items = context.player.getInventory(); if (items.length < 1) return context.print(`You're not carrying anything.`); let itemDescription = `You are carrying `; @@ -14,10 +16,10 @@ function InventoryCommand(args, context) { context.print(itemDescription + "."); } -InventoryCommand.metadata = { - category: "Actions", - description: "View inventory", - arguments: [] -}; - -export default InventoryCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("inventory") + .withAliases(["i", "inv"]) + .withCategory("Actions") + .withDescription("View inventory") + .withHandler(inventoryHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/load.js b/src/engine/commands/load.js index f7ed924..d550ba9 100644 --- a/src/engine/commands/load.js +++ b/src/engine/commands/load.js @@ -1,12 +1,13 @@ -function LoadCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function loadHandler(args, context) { context.print(`Loading game...`); context.load(); } -LoadCommand.metadata = { - category: "System", - description: "Load game", - arguments: [] -}; - -export default LoadCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("load") + .withCategory("System") + .withDescription("Load game") + .withHandler(loadHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/look.js b/src/engine/commands/look.js index abb2e54..d8d6d6c 100644 --- a/src/engine/commands/look.js +++ b/src/engine/commands/look.js @@ -1,4 +1,6 @@ -function LookCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function lookHandler(args, context) { if (args.length == 1) { context.examineRoom(); } else { @@ -29,17 +31,11 @@ function LookCommand(args, context) { } } -LookCommand.metadata = { - category: "Actions", - description: "Examine room or object", - arguments: [ - { - type: "item", - name: "target", - optional: true, - description: "Object to examine" - } - ] -}; - -export default LookCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("look") + .withAliases(["l"]) + .withCategory("Actions") + .withDescription("Examine room or object") + .withArgument("item", "target", true, "Object to examine") + .withHandler(lookHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/save.js b/src/engine/commands/save.js index 1160fde..2694c95 100644 --- a/src/engine/commands/save.js +++ b/src/engine/commands/save.js @@ -1,12 +1,13 @@ -function SaveCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function saveHandler(args, context) { context.print(`Saving game...`); context.save(); } -SaveCommand.metadata = { - category: "System", - description: "Save game", - arguments: [] -}; - -export default SaveCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("save") + .withCategory("System") + .withDescription("Save game") + .withHandler(saveHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/take.js b/src/engine/commands/take.js index 4fab30b..1c24a7b 100644 --- a/src/engine/commands/take.js +++ b/src/engine/commands/take.js @@ -1,4 +1,6 @@ -function TakeCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function takeHandler(args, context) { const room = context.getRoom(context.player.currentRoom); const items = room.getItems(); let item = null; @@ -25,17 +27,11 @@ function TakeCommand(args, context) { } } -TakeCommand.metadata = { - category: "Actions", - description: "Take an object", - arguments: [ - { - type: "item", - name: "target", - optional: false, - description: "Object to take" - } - ] -}; - -export default TakeCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("take") + .withAliases(["get"]) + .withCategory("Actions") + .withDescription("Take an object") + .withArgument("item", "target", false, "Object to take") + .withHandler(takeHandler) + .create(); \ No newline at end of file diff --git a/src/engine/commands/use.js b/src/engine/commands/use.js index 831d00a..dfa4c7e 100644 --- a/src/engine/commands/use.js +++ b/src/engine/commands/use.js @@ -1,4 +1,6 @@ -async function UseCommand(args, context) { +import CommandBuilder from "../builders/command"; + +async function useHandler(args, context) { const room = context.getRoom(context.player.currentRoom); const items = room.getItems(); let item = null; @@ -24,17 +26,12 @@ async function UseCommand(args, context) { } } -UseCommand.metadata = { - category: "Actions", - description: "Use an object", - arguments: [ - { - type: "item", - name: "target", - optional: false, - description: "Object to use" - } - ] -}; - -export default UseCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("use") + .withAliases(["interact"]) + .withCategory("Actions") + .withDescription("Use an object") + .withArgument("item", "target", false, "Object to use") + .withHandler(useHandler) + .isAsync(true) + .create(); \ No newline at end of file diff --git a/src/engine/commands/volume.js b/src/engine/commands/volume.js index cf384c9..39bae5f 100644 --- a/src/engine/commands/volume.js +++ b/src/engine/commands/volume.js @@ -1,4 +1,6 @@ -function VolumeCommand(args, context) { +import CommandBuilder from "../builders/command"; + +function volumeHandler(args, context) { if (args.length < 3) { return context.print(`Usage: volume <0-100>`); } @@ -18,24 +20,11 @@ function VolumeCommand(args, context) { context.print(`${args[1]} volume set to ${value}%`) } -VolumeCommand.metadata = { - category: "System", - description: "Adjust volume", - arguments: [ - { - type: "select", - name: "channel", - optional: false, - description: "Audio channel", - options: ["music", "sfx", "ambience"] - }, - { - type: "text", - name: "level", - optional: false, - description: "Volume level (1-100)" - } - ] -}; - -export default VolumeCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("volume") + .withCategory("System") + .withDescription("Adjust volume") + .withArgument("select", "channel", false, "Audio channel") + .withArgument("text", "level", false, "Volume level (1-100)") + .withHandler(volumeHandler) + .create(); \ No newline at end of file diff --git a/src/engine/mobile-commands.js b/src/engine/mobile-commands.js index bb7d83e..eefb9e3 100644 --- a/src/engine/mobile-commands.js +++ b/src/engine/mobile-commands.js @@ -30,6 +30,90 @@ export default class MobileCommands { this.modeToggleButton.addEventListener('click', () => this.toggleInputMode()); this.executeButton.addEventListener('click', () => this.executeCommand()); this.cancelButton.addEventListener('click', () => this.closeDialog()); + + // Setup tab click handlers + this.setupTabHandlers(); + } + + setupTabHandlers() { + const tabs = document.querySelectorAll('.command-tab'); + tabs.forEach(tab => { + // Handle click events + tab.addEventListener('click', (e) => { + const category = e.target.dataset.category; + this.switchTab(category); + }); + + // Handle keyboard navigation + tab.addEventListener('keydown', (e) => { + this.handleTabKeydown(e); + }); + }); + } + + handleTabKeydown(e) { + const tabs = Array.from(document.querySelectorAll('.command-tab')); + const currentIndex = tabs.findIndex(tab => tab === e.target); + let newIndex = currentIndex; + + switch (e.key) { + case 'ArrowLeft': + case 'ArrowUp': + e.preventDefault(); + newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1; + break; + case 'ArrowRight': + case 'ArrowDown': + e.preventDefault(); + newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0; + break; + case 'Home': + e.preventDefault(); + newIndex = 0; + break; + case 'End': + e.preventDefault(); + newIndex = tabs.length - 1; + break; + default: + return; + } + + if (newIndex !== currentIndex) { + const newTab = tabs[newIndex]; + const category = newTab.dataset.category; + this.switchTab(category); + } + } + + switchTab(category) { + // Update all tabs and panels + document.querySelectorAll('.command-tab').forEach(tab => { + const isActive = tab.dataset.category === category; + + // Update CSS classes + tab.classList.toggle('active', isActive); + + // Update ARIA attributes + tab.setAttribute('aria-selected', isActive.toString()); + tab.setAttribute('tabindex', isActive ? '0' : '-1'); + }); + + document.querySelectorAll('.command-panel').forEach(panel => { + const isActive = panel.dataset.category === category; + + // Update CSS classes + panel.classList.toggle('active', isActive); + + // Update ARIA attributes + panel.setAttribute('aria-hidden', (!isActive).toString()); + }); + + // Focus the active tab for keyboard navigation + const activeTab = document.querySelector(`[data-category="${category}"].command-tab`); + if (activeTab) { + activeTab.focus(); + } } populateCommands() { @@ -44,7 +128,7 @@ export default class MobileCommands { // Populate each category Object.entries(categories).forEach(([category, commandList]) => { - const container = document.querySelector(`[data-category="${category}"]`); + const container = document.querySelector(`[data-category="${category}"].command-list`); if (container) { commandList.forEach(command => { const button = this.createCommandButton(command); @@ -66,10 +150,21 @@ export default class MobileCommands { continue; } - if (func.metadata) { + let metadata = null; + + // Check for new Command objects with getMetadata() method + if (func.getMetadata) { + metadata = func.getMetadata(); + } + // Check for old-style metadata property + else if (func.metadata) { + metadata = func.metadata; + } + + if (metadata) { commands.push({ name: name, - ...func.metadata + ...metadata }); seenFunctions.add(func); } @@ -115,6 +210,29 @@ export default class MobileCommands {
${command.description}
`; + // Add ARIA attributes + button.setAttribute('aria-label', `${command.name}: ${command.description}`); + button.setAttribute('type', 'button'); + + // Add arguments info for screen readers if present + if (command.arguments && command.arguments.length > 0) { + const argCount = command.arguments.length; + const requiredArgs = command.arguments.filter(arg => !arg.optional).length; + let ariaDescription = `Requires ${requiredArgs} argument${requiredArgs !== 1 ? 's' : ''}`; + if (argCount > requiredArgs) { + const optionalArgs = argCount - requiredArgs; + ariaDescription += ` with ${optionalArgs} optional argument${optionalArgs !== 1 ? 's' : ''}`; + } + button.setAttribute('aria-describedby', `${command.name}-args-info`); + + // Create hidden description for screen readers + const ariaDescElement = document.createElement('span'); + ariaDescElement.id = `${command.name}-args-info`; + ariaDescElement.className = 'sr-only'; + ariaDescElement.textContent = ariaDescription; + button.appendChild(ariaDescElement); + } + button.addEventListener('click', () => { this.selectCommand(command); }); @@ -362,6 +480,8 @@ export default class MobileCommands { refreshCommands() { if (this.isMobileMode) { this.populateCommands(); + // Re-setup tab handlers after commands are populated + this.setupTabHandlers(); } } } \ No newline at end of file diff --git a/src/engine/output.js b/src/engine/output.js index 9e8efae..284816f 100644 --- a/src/engine/output.js +++ b/src/engine/output.js @@ -20,6 +20,20 @@ export default class Output { }); this.history.appendChild(node); // this.tts.speak(string); + + // Auto-scroll to bottom + this.scrollToBottom(); + } + + scrollToBottom() { + // Find the log area container + const logArea = document.querySelector('.log-area'); + if (logArea) { + // Use requestAnimationFrame for smooth scrolling + requestAnimationFrame(() => { + logArea.scrollTop = logArea.scrollHeight; + }); + } } play(file) { diff --git a/src/game/commands/dark.js b/src/game/commands/dark.js index 5e727e5..a3d8e90 100644 --- a/src/game/commands/dark.js +++ b/src/game/commands/dark.js @@ -1,11 +1,13 @@ -async function DarkCommand(args, context) { +import CommandBuilder from "../../engine/builders/command"; + +async function darkHandler(args, context) { document.body.classList.toggle('dark-theme'); } -DarkCommand.metadata = { - category: "System", - description: "Toggle dark theme", - arguments: [] -}; - -export default DarkCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("dark") + .withCategory("System") + .withDescription("Toggle dark theme") + .withHandler(darkHandler) + .isAsync(true) + .create(); \ No newline at end of file diff --git a/src/game/commands/meow.js b/src/game/commands/meow.js index d066df4..c2d3176 100644 --- a/src/game/commands/meow.js +++ b/src/game/commands/meow.js @@ -1,11 +1,13 @@ -async function MeowCommand(args, context) { +import CommandBuilder from "../../engine/builders/command"; + +async function meowHandler(args, context) { context.print(`You meow.`); } -MeowCommand.metadata = { - category: "Actions", - description: "Make a cat sound", - arguments: [] -}; - -export default MeowCommand; \ No newline at end of file +export default new CommandBuilder() + .withName("meow") + .withCategory("Actions") + .withDescription("Make a cat sound") + .withHandler(meowHandler) + .isAsync(true) + .create(); \ No newline at end of file diff --git a/src/game/index.html b/src/game/index.html index a1ec100..90330d4 100644 --- a/src/game/index.html +++ b/src/game/index.html @@ -88,11 +88,13 @@ font-family: var(--font-family); font-size: var(--font-size-base); line-height: var(--line-height); - padding: var(--spacing-xl); - min-height: 100vh; + padding: 0; + margin: 0; + height: 100vh; font-weight: var(--font-weight-normal); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + overflow: hidden; } /* Typography */ @@ -134,16 +136,102 @@ outline-offset: 2px; } + /* Screen reader only content */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + /* ===== LAYOUT COMPONENTS ===== */ .game-container { + display: flex; + flex-direction: column; + height: 100vh; max-width: var(--max-width); margin: 0 auto; - padding: var(--spacing-xl); background: var(--surface-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-lg); backdrop-filter: blur(10px); - border: var(--border-width) solid var(--border-color); + border-left: var(--border-width) solid var(--border-color); + border-right: var(--border-width) solid var(--border-color); + } + + .game-container[hidden] { + display: none !important; + } + + .welcome-screen { + display: flex; + flex-direction: column; + height: 100vh; + max-width: var(--max-width); + margin: 0 auto; + background: var(--surface-color); + backdrop-filter: blur(10px); + border-left: var(--border-width) solid var(--border-color); + border-right: var(--border-width) solid var(--border-color); + } + + .welcome-screen[hidden] { + display: none !important; + } + + .game-header { + flex-shrink: 0; + padding: var(--spacing-md) var(--spacing-xl); + background: var(--background-color); + border-bottom: var(--border-width) solid var(--border-color); + } + + .game-header h1 { + margin: 0; + font-size: 1.5rem; + text-align: center; + } + + .game-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .log-area { + flex: 1; + overflow-y: auto; + padding: var(--spacing-md) var(--spacing-xl); + background: var(--background-color); + scroll-behavior: smooth; + } + + .log-area::-webkit-scrollbar { + width: 8px; + } + + .log-area::-webkit-scrollbar-track { + background: var(--surface-color); + border-radius: var(--border-radius-sm); + } + + .log-area::-webkit-scrollbar-thumb { + background: var(--primary-color); + border-radius: var(--border-radius-sm); + } + + .log-area::-webkit-scrollbar-thumb:hover { + background: var(--secondary-color); + } + + .input-area { + flex-shrink: 0; + background: var(--surface-color); + border-top: var(--border-width) solid var(--border-color); } /* ===== FORM CONTROLS ===== */ @@ -258,94 +346,112 @@ } .input-mode { - margin-top: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-xl); } .mobile-mode { - position: sticky; - bottom: 0; background: var(--background-color); border-top: 2px solid var(--primary-color); - padding: var(--spacing-md); - margin: var(--spacing-md) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl)); - max-height: 50vh; + max-height: 40vh; overflow-y: auto; - border-radius: var(--border-radius) var(--border-radius) 0 0; + padding: var(--spacing-md) var(--spacing-xl); } .command-interface h3 { margin-top: 0; - margin-bottom: var(--spacing-md); + margin-bottom: var(--spacing-sm); color: var(--primary-color); text-align: center; + font-size: var(--font-size-base); } .command-categories-permanent { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-md); + display: flex; + flex-direction: column; + height: 100%; } - .command-category { + .command-tabs { + display: flex; + flex-shrink: 0; + border-bottom: var(--border-width) solid var(--border-color); background: var(--surface-color); - border: var(--border-width) solid var(--border-color); - border-radius: var(--border-radius); + } + + .command-tab { + flex: 1; + background: var(--surface-color); + border: none; + border-right: var(--border-width) solid var(--border-color); padding: var(--spacing-sm); - transition: all var(--transition-fast); - } - - .command-category:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow-md); - } - - .command-category summary { cursor: pointer; - padding: var(--spacing-sm); + transition: all var(--transition-fast); + font-size: var(--font-size-small); + font-weight: var(--font-weight-bold); + color: var(--text-muted); + } + + .command-tab:last-child { + border-right: none; + } + + .command-tab:hover { background: var(--primary-color); color: white; - border-radius: var(--border-radius-sm); - font-weight: var(--font-weight-bold); - margin-bottom: var(--spacing-sm); - transition: all var(--transition-fast); - list-style: none; } - .command-category summary::-webkit-details-marker { + .command-tab.active, + .command-tab[aria-selected="true"] { + background: var(--primary-color); + color: white; + border-bottom: 2px solid var(--secondary-color); + } + + .command-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .command-panel { + display: none; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + .command-panel.active, + .command-panel[aria-hidden="false"] { + display: flex; + } + + .command-panel[aria-hidden="true"] { display: none; } - .command-category summary::before { - content: "▶"; - margin-right: var(--spacing-xs); - transition: transform var(--transition-fast); - } - - .command-category[open] summary::before { - transform: rotate(90deg); - } - - .command-category summary:hover { - background: var(--secondary-color); - transform: translateY(-1px); - } - .command-list { - padding: var(--spacing-sm) 0; + flex: 1; + overflow-y: auto; + padding: var(--spacing-sm); + display: flex; + flex-direction: column; + gap: var(--spacing-xs); } .command-button { display: block; width: 100%; - margin-bottom: var(--spacing-sm); - padding: var(--spacing-sm); + margin-bottom: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); text-align: left; border: var(--border-width) solid var(--border-color); background: var(--surface-color); color: var(--text-color); border-radius: var(--border-radius-sm); - font-size: var(--font-size-small); + font-size: 0.75rem; transition: all var(--transition-fast); + min-height: 36px; } .command-button:hover { @@ -471,20 +577,41 @@ /* Tablet and Mobile */ @media (max-width: 768px) { - body { - padding: var(--spacing-md); + .game-header { + padding: var(--spacing-sm) var(--spacing-md); } - .game-container { - padding: var(--spacing-md); + .log-area { + padding: var(--spacing-sm) var(--spacing-md); } - .command-categories-permanent { - grid-template-columns: 1fr; + .input-mode { + padding: var(--spacing-sm) var(--spacing-md); } .mobile-mode { - margin: var(--spacing-md) calc(-1 * var(--spacing-md)) calc(-1 * var(--spacing-md)) calc(-1 * var(--spacing-md)); + padding: var(--spacing-sm) var(--spacing-md); + } + + .command-tabs { + flex-direction: column; + border-bottom: none; + border-right: var(--border-width) solid var(--border-color); + } + + .command-tab { + border-right: none; + border-bottom: var(--border-width) solid var(--border-color); + } + + .command-tab:last-child { + border-bottom: none; + } + + .command-tab.active, + .command-tab[aria-selected="true"] { + border-bottom: none; + border-right: 2px solid var(--secondary-color); } dialog { @@ -515,30 +642,39 @@ } /* Typography adjustments for mobile */ - h1 { font-size: 2rem; } + .game-header h1 { font-size: 1.25rem; } h2 { font-size: 1.75rem; } h3 { font-size: 1.5rem; } } /* Small Mobile */ @media (max-width: 480px) { - body { - padding: var(--spacing-sm); + .game-header { + padding: var(--spacing-xs) var(--spacing-sm); } - .game-container { - padding: var(--spacing-sm); + .log-area { + padding: var(--spacing-xs) var(--spacing-sm); + } + + .input-mode { + padding: var(--spacing-xs) var(--spacing-sm); } .mobile-mode { - margin: var(--spacing-sm) calc(-1 * var(--spacing-sm)) calc(-1 * var(--spacing-sm)) calc(-1 * var(--spacing-sm)); + padding: var(--spacing-xs) var(--spacing-sm); } - .command-categories-permanent { - gap: var(--spacing-sm); + .command-tabs { + flex-direction: row; } - h1 { font-size: 1.75rem; } + .command-tab { + font-size: 0.75rem; + padding: var(--spacing-xs); + } + + .game-header h1 { font-size: 1.125rem; } h2 { font-size: 1.5rem; } h3 { font-size: 1.25rem; } } @@ -582,38 +718,94 @@ -

Assassin bug