UI improvements

master
Talon 2025-07-18 19:41:49 +01:00
parent 45b52fbcf3
commit fc5c17752a
17 changed files with 698 additions and 260 deletions

View File

@ -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;
}
}

View File

@ -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];
}
}

View File

@ -9,15 +9,15 @@ import VolumeCommand from "./commands/volume";
import InventoryCommand from "./commands/inventory"; import InventoryCommand from "./commands/inventory";
const defaultCommands = [ const defaultCommands = [
[["look", "l"], LookCommand], LookCommand,
[["use", "interact"], UseCommand], UseCommand,
[["take", "get"], TakeCommand], TakeCommand,
[["drop", "put"], DropCommand], DropCommand,
["echo", EchoCommand], EchoCommand,
["save", SaveCommand], SaveCommand,
["load", LoadCommand], LoadCommand,
["volume", VolumeCommand], VolumeCommand,
[["i", "inv", "inventory"], InventoryCommand] InventoryCommand
]; ];
const directionMap = [ const directionMap = [
@ -49,8 +49,15 @@ export default class Commands {
} }
const room = this.context.getRoom(this.context.player.currentRoom); const room = this.context.getRoom(this.context.player.currentRoom);
const split = str.split(" "); const split = str.split(" ");
if (this.commands.get(split[0])) { const command = this.commands.get(split[0]);
this.commands.get(split[0])(split, this.context); if (command) {
if (command.execute) {
// New Command object
command.execute(split, this.context);
} else {
// Old function-based command
command(split, this.context);
}
return; 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) { addCommands(commands) {
commands.forEach((command) => { commands.forEach((command) => {
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]); this.addCommand(command[0], command[1]);
} else {
// Old single command
this.addCommand(command.name, command);
}
}); });
} }

View File

@ -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 room = context.getRoom(context.player.currentRoom);
const items = context.player.getInventory(); const items = context.player.getInventory();
let item = null; let item = null;
@ -21,17 +23,11 @@ function DropCommand(args, context) {
} }
} }
DropCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("drop")
description: "Drop an object", .withAliases(["put"])
arguments: [ .withCategory("Actions")
{ .withDescription("Drop an object")
type: "item", .withArgument("item", "target", false, "Object to drop")
name: "target", .withHandler(dropHandler)
optional: false, .create();
description: "Object to drop"
}
]
};
export default DropCommand;

View File

@ -1,4 +1,6 @@
function EchoCommand(args, context) { import CommandBuilder from "../builders/command";
function echoHandler(args, context) {
if (args[1] != "on" && args[1] != "off") { if (args[1] != "on" && args[1] != "off") {
context.print(`Usage: echo <on/off>`); context.print(`Usage: echo <on/off>`);
} else { } else {
@ -7,18 +9,10 @@ function EchoCommand(args, context) {
} }
} }
EchoCommand.metadata = { export default new CommandBuilder()
category: "System", .withName("echo")
description: "Toggle command echo", .withCategory("System")
arguments: [ .withDescription("Toggle command echo")
{ .withArgument("select", "state", false, "Echo state")
type: "select", .withHandler(echoHandler)
name: "state", .create();
optional: false,
description: "Echo state",
options: ["on", "off"]
}
]
};
export default EchoCommand;

View File

@ -1,4 +1,6 @@
function InventoryCommand(args, context) { import CommandBuilder from "../builders/command";
function inventoryHandler(args, context) {
const items = context.player.getInventory(); const items = context.player.getInventory();
if (items.length < 1) return context.print(`You're not carrying anything.`); if (items.length < 1) return context.print(`You're not carrying anything.`);
let itemDescription = `You are carrying `; let itemDescription = `You are carrying `;
@ -14,10 +16,10 @@ function InventoryCommand(args, context) {
context.print(itemDescription + "."); context.print(itemDescription + ".");
} }
InventoryCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("inventory")
description: "View inventory", .withAliases(["i", "inv"])
arguments: [] .withCategory("Actions")
}; .withDescription("View inventory")
.withHandler(inventoryHandler)
export default InventoryCommand; .create();

View File

@ -1,12 +1,13 @@
function LoadCommand(args, context) { import CommandBuilder from "../builders/command";
function loadHandler(args, context) {
context.print(`Loading game...`); context.print(`Loading game...`);
context.load(); context.load();
} }
LoadCommand.metadata = { export default new CommandBuilder()
category: "System", .withName("load")
description: "Load game", .withCategory("System")
arguments: [] .withDescription("Load game")
}; .withHandler(loadHandler)
.create();
export default LoadCommand;

View File

@ -1,4 +1,6 @@
function LookCommand(args, context) { import CommandBuilder from "../builders/command";
function lookHandler(args, context) {
if (args.length == 1) { if (args.length == 1) {
context.examineRoom(); context.examineRoom();
} else { } else {
@ -29,17 +31,11 @@ function LookCommand(args, context) {
} }
} }
LookCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("look")
description: "Examine room or object", .withAliases(["l"])
arguments: [ .withCategory("Actions")
{ .withDescription("Examine room or object")
type: "item", .withArgument("item", "target", true, "Object to examine")
name: "target", .withHandler(lookHandler)
optional: true, .create();
description: "Object to examine"
}
]
};
export default LookCommand;

View File

@ -1,12 +1,13 @@
function SaveCommand(args, context) { import CommandBuilder from "../builders/command";
function saveHandler(args, context) {
context.print(`Saving game...`); context.print(`Saving game...`);
context.save(); context.save();
} }
SaveCommand.metadata = { export default new CommandBuilder()
category: "System", .withName("save")
description: "Save game", .withCategory("System")
arguments: [] .withDescription("Save game")
}; .withHandler(saveHandler)
.create();
export default SaveCommand;

View File

@ -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 room = context.getRoom(context.player.currentRoom);
const items = room.getItems(); const items = room.getItems();
let item = null; let item = null;
@ -25,17 +27,11 @@ function TakeCommand(args, context) {
} }
} }
TakeCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("take")
description: "Take an object", .withAliases(["get"])
arguments: [ .withCategory("Actions")
{ .withDescription("Take an object")
type: "item", .withArgument("item", "target", false, "Object to take")
name: "target", .withHandler(takeHandler)
optional: false, .create();
description: "Object to take"
}
]
};
export default TakeCommand;

View File

@ -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 room = context.getRoom(context.player.currentRoom);
const items = room.getItems(); const items = room.getItems();
let item = null; let item = null;
@ -24,17 +26,12 @@ async function UseCommand(args, context) {
} }
} }
UseCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("use")
description: "Use an object", .withAliases(["interact"])
arguments: [ .withCategory("Actions")
{ .withDescription("Use an object")
type: "item", .withArgument("item", "target", false, "Object to use")
name: "target", .withHandler(useHandler)
optional: false, .isAsync(true)
description: "Object to use" .create();
}
]
};
export default UseCommand;

View File

@ -1,4 +1,6 @@
function VolumeCommand(args, context) { import CommandBuilder from "../builders/command";
function volumeHandler(args, context) {
if (args.length < 3) { if (args.length < 3) {
return context.print(`Usage: volume <music/sfx/ambience> <0-100>`); return context.print(`Usage: volume <music/sfx/ambience> <0-100>`);
} }
@ -18,24 +20,11 @@ function VolumeCommand(args, context) {
context.print(`${args[1]} volume set to ${value}%`) context.print(`${args[1]} volume set to ${value}%`)
} }
VolumeCommand.metadata = { export default new CommandBuilder()
category: "System", .withName("volume")
description: "Adjust volume", .withCategory("System")
arguments: [ .withDescription("Adjust volume")
{ .withArgument("select", "channel", false, "Audio channel")
type: "select", .withArgument("text", "level", false, "Volume level (1-100)")
name: "channel", .withHandler(volumeHandler)
optional: false, .create();
description: "Audio channel",
options: ["music", "sfx", "ambience"]
},
{
type: "text",
name: "level",
optional: false,
description: "Volume level (1-100)"
}
]
};
export default VolumeCommand;

View File

@ -30,6 +30,90 @@ export default class MobileCommands {
this.modeToggleButton.addEventListener('click', () => this.toggleInputMode()); this.modeToggleButton.addEventListener('click', () => this.toggleInputMode());
this.executeButton.addEventListener('click', () => this.executeCommand()); this.executeButton.addEventListener('click', () => this.executeCommand());
this.cancelButton.addEventListener('click', () => this.closeDialog()); 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() { populateCommands() {
@ -44,7 +128,7 @@ export default class MobileCommands {
// Populate each category // Populate each category
Object.entries(categories).forEach(([category, commandList]) => { Object.entries(categories).forEach(([category, commandList]) => {
const container = document.querySelector(`[data-category="${category}"]`); const container = document.querySelector(`[data-category="${category}"].command-list`);
if (container) { if (container) {
commandList.forEach(command => { commandList.forEach(command => {
const button = this.createCommandButton(command); const button = this.createCommandButton(command);
@ -66,10 +150,21 @@ export default class MobileCommands {
continue; 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({ commands.push({
name: name, name: name,
...func.metadata ...metadata
}); });
seenFunctions.add(func); seenFunctions.add(func);
} }
@ -115,6 +210,29 @@ export default class MobileCommands {
<div class="command-description">${command.description}</div> <div class="command-description">${command.description}</div>
`; `;
// 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', () => { button.addEventListener('click', () => {
this.selectCommand(command); this.selectCommand(command);
}); });
@ -362,6 +480,8 @@ export default class MobileCommands {
refreshCommands() { refreshCommands() {
if (this.isMobileMode) { if (this.isMobileMode) {
this.populateCommands(); this.populateCommands();
// Re-setup tab handlers after commands are populated
this.setupTabHandlers();
} }
} }
} }

View File

@ -20,6 +20,20 @@ export default class Output {
}); });
this.history.appendChild(node); this.history.appendChild(node);
// this.tts.speak(string); // 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) { play(file) {

View File

@ -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'); document.body.classList.toggle('dark-theme');
} }
DarkCommand.metadata = { export default new CommandBuilder()
category: "System", .withName("dark")
description: "Toggle dark theme", .withCategory("System")
arguments: [] .withDescription("Toggle dark theme")
}; .withHandler(darkHandler)
.isAsync(true)
export default DarkCommand; .create();

View File

@ -1,11 +1,13 @@
async function MeowCommand(args, context) { import CommandBuilder from "../../engine/builders/command";
async function meowHandler(args, context) {
context.print(`You meow.`); context.print(`You meow.`);
} }
MeowCommand.metadata = { export default new CommandBuilder()
category: "Actions", .withName("meow")
description: "Make a cat sound", .withCategory("Actions")
arguments: [] .withDescription("Make a cat sound")
}; .withHandler(meowHandler)
.isAsync(true)
export default MeowCommand; .create();

View File

@ -88,11 +88,13 @@
font-family: var(--font-family); font-family: var(--font-family);
font-size: var(--font-size-base); font-size: var(--font-size-base);
line-height: var(--line-height); line-height: var(--line-height);
padding: var(--spacing-xl); padding: 0;
min-height: 100vh; margin: 0;
height: 100vh;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow: hidden;
} }
/* Typography */ /* Typography */
@ -134,16 +136,102 @@
outline-offset: 2px; 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 ===== */ /* ===== LAYOUT COMPONENTS ===== */
.game-container { .game-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: var(--max-width); max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
padding: var(--spacing-xl);
background: var(--surface-color); background: var(--surface-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(10px); 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 ===== */ /* ===== FORM CONTROLS ===== */
@ -258,94 +346,112 @@
} }
.input-mode { .input-mode {
margin-top: var(--spacing-md); padding: var(--spacing-md) var(--spacing-xl);
} }
.mobile-mode { .mobile-mode {
position: sticky;
bottom: 0;
background: var(--background-color); background: var(--background-color);
border-top: 2px solid var(--primary-color); border-top: 2px solid var(--primary-color);
padding: var(--spacing-md); max-height: 40vh;
margin: var(--spacing-md) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl));
max-height: 50vh;
overflow-y: auto; overflow-y: auto;
border-radius: var(--border-radius) var(--border-radius) 0 0; padding: var(--spacing-md) var(--spacing-xl);
} }
.command-interface h3 { .command-interface h3 {
margin-top: 0; margin-top: 0;
margin-bottom: var(--spacing-md); margin-bottom: var(--spacing-sm);
color: var(--primary-color); color: var(--primary-color);
text-align: center; text-align: center;
font-size: var(--font-size-base);
} }
.command-categories-permanent { .command-categories-permanent {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); flex-direction: column;
gap: var(--spacing-md); height: 100%;
} }
.command-category { .command-tabs {
display: flex;
flex-shrink: 0;
border-bottom: var(--border-width) solid var(--border-color);
background: var(--surface-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); 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; 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); background: var(--primary-color);
color: white; 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; 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 { .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 { .command-button {
display: block; display: block;
width: 100%; width: 100%;
margin-bottom: var(--spacing-sm); margin-bottom: var(--spacing-xs);
padding: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm);
text-align: left; text-align: left;
border: var(--border-width) solid var(--border-color); border: var(--border-width) solid var(--border-color);
background: var(--surface-color); background: var(--surface-color);
color: var(--text-color); color: var(--text-color);
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
font-size: var(--font-size-small); font-size: 0.75rem;
transition: all var(--transition-fast); transition: all var(--transition-fast);
min-height: 36px;
} }
.command-button:hover { .command-button:hover {
@ -471,20 +577,41 @@
/* Tablet and Mobile */ /* Tablet and Mobile */
@media (max-width: 768px) { @media (max-width: 768px) {
body { .game-header {
padding: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
} }
.game-container { .log-area {
padding: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
} }
.command-categories-permanent { .input-mode {
grid-template-columns: 1fr; padding: var(--spacing-sm) var(--spacing-md);
} }
.mobile-mode { .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 { dialog {
@ -515,30 +642,39 @@
} }
/* Typography adjustments for mobile */ /* Typography adjustments for mobile */
h1 { font-size: 2rem; } .game-header h1 { font-size: 1.25rem; }
h2 { font-size: 1.75rem; } h2 { font-size: 1.75rem; }
h3 { font-size: 1.5rem; } h3 { font-size: 1.5rem; }
} }
/* Small Mobile */ /* Small Mobile */
@media (max-width: 480px) { @media (max-width: 480px) {
body { .game-header {
padding: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm);
} }
.game-container { .log-area {
padding: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm);
}
.input-mode {
padding: var(--spacing-xs) var(--spacing-sm);
} }
.mobile-mode { .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 { .command-tabs {
gap: var(--spacing-sm); 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; } h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; } h3 { font-size: 1.25rem; }
} }
@ -582,16 +718,25 @@
</head> </head>
<body> <body>
<h1>Assassin bug</h1>
<div class="game-container" id="play-area" hidden=true> <div class="game-container" id="play-area" hidden=true>
<!-- Game Header -->
<div class="game-header">
<h1>Assassin bug</h1>
<!-- Mode Toggle --> <!-- Mode Toggle -->
<div class="mode-toggle"> <div class="mode-toggle">
<button id="toggle-input-mode" class="mode-button">📱 Mobile Mode</button> <button id="toggle-input-mode" class="mode-button">📱 Mobile Mode</button>
</div> </div>
</div>
<!-- Output Area --> <!-- Game Content -->
<div class="game-content">
<!-- Log Area -->
<div class="log-area">
<div aria-live="polite" id="output-area"></div> <div aria-live="polite" id="output-area"></div>
</div>
<!-- Input Area -->
<div class="input-area">
<!-- Text Input Mode --> <!-- Text Input Mode -->
<div id="text-input-mode" class="input-mode"> <div id="text-input-mode" class="input-mode">
<input type="text" id="input-area" placeholder="Type command" /> <input type="text" id="input-area" placeholder="Type command" />
@ -600,20 +745,67 @@
<!-- Mobile Command Mode --> <!-- Mobile Command Mode -->
<div id="mobile-command-mode" class="input-mode mobile-mode" hidden> <div id="mobile-command-mode" class="input-mode mobile-mode" hidden>
<div class="command-interface"> <div class="command-interface">
<h3>Commands</h3> <h3 id="command-interface-title">Commands</h3>
<div class="command-categories-permanent"> <div class="command-categories-permanent">
<details class="command-category" open> <div class="command-tabs" role="tablist" aria-labelledby="command-interface-title">
<summary>Movement</summary> <button class="command-tab active"
<div class="command-list" data-category="Movement"></div> role="tab"
</details> aria-selected="true"
<details class="command-category" open> aria-controls="movement-panel"
<summary>Actions</summary> id="movement-tab"
<div class="command-list" data-category="Actions"></div> data-category="Movement"
</details> tabindex="0">Movement</button>
<details class="command-category" open> <button class="command-tab"
<summary>System</summary> role="tab"
<div class="command-list" data-category="System"></div> aria-selected="false"
</details> aria-controls="actions-panel"
id="actions-tab"
data-category="Actions"
tabindex="-1">Actions</button>
<button class="command-tab"
role="tab"
aria-selected="false"
aria-controls="system-panel"
id="system-tab"
data-category="System"
tabindex="-1">System</button>
</div>
<div class="command-content">
<div class="command-panel active"
role="tabpanel"
aria-labelledby="movement-tab"
id="movement-panel"
data-category="Movement">
<div class="command-list"
data-category="Movement"
role="group"
aria-label="Movement commands"></div>
</div>
<div class="command-panel"
role="tabpanel"
aria-labelledby="actions-tab"
id="actions-panel"
data-category="Actions"
aria-hidden="true">
<div class="command-list"
data-category="Actions"
role="group"
aria-label="Action commands"></div>
</div>
<div class="command-panel"
role="tabpanel"
aria-labelledby="system-tab"
id="system-panel"
data-category="System"
aria-hidden="true">
<div class="command-list"
data-category="System"
role="group"
aria-label="System commands"></div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -631,15 +823,31 @@
</dialog> </dialog>
</div> </div>
<div id="save-game-found" hidden=true> <div class="welcome-screen" id="save-game-found" hidden=true>
<div class="game-header">
<h1>Found a save game</h1> <h1>Found a save game</h1>
</div>
<div class="game-content">
<div class="log-area">
<div style="text-align: center; margin-top: 2rem;">
<button id="load-save-game">Load</button> <button id="load-save-game">Load</button>
<button id="start-new-game">New</button> <button id="start-new-game">New</button>
</div> </div>
<div id="before-play"> </div>
</div>
</div>
<div class="welcome-screen" id="before-play">
<div class="game-header">
<h1>Welcome</h1> <h1>Welcome</h1>
</div>
<div class="game-content">
<div class="log-area">
<div style="text-align: center; margin-top: 2rem;">
<button id="begin">Begin the adventure</button> <button id="begin">Begin the adventure</button>
</div> </div>
</div>
</div>
</div>
</body> </body>
</html> </html>