This commit is contained in:
2026-03-06 11:15:39 +02:00
parent 5994e165c6
commit 7299157ba9
4 changed files with 351 additions and 265 deletions

View File

@@ -17,16 +17,7 @@
<body data-page="room" data-room-id="{{ .RoomID }}" class="prejoin"> <body data-page="room" data-room-id="{{ .RoomID }}" class="prejoin">
<div class="mobile-control-strip"> <div class="mobile-control-strip">
<div class="taskbar-shell"> <div class="taskbar-shell">
<div class="taskbar-program-list"> <div class="taskbar-program-list" data-role="taskbar-program-list"></div>
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="theme-tool-window" aria-label="Open theme picker">
<img class="taskbar-icon" src="/static/img/Windows Icons - PNG/main.cpl_14_109-1.png" alt="">
<span>ThemePicker.exe</span>
</button>
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="mode-tool-window" aria-label="Open display mode settings">
<img class="taskbar-icon" data-role="mode-icon" src="/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png" alt="">
<span>DisplayMode.exe</span>
</button>
</div>
</div> </div>
</div> </div>
@@ -124,7 +115,6 @@
<div id="admin-controls" class="admin-controls hidden"> <div id="admin-controls" class="admin-controls hidden">
<button type="button" id="reveal-btn" class="btn">Reveal</button> <button type="button" id="reveal-btn" class="btn">Reveal</button>
<button type="button" id="reset-btn" class="btn">Reset</button> <button type="button" id="reset-btn" class="btn">Reset</button>
<button type="button" id="terminal-btn" class="btn">Terminal</button>
</div> </div>
<p id="room-message" class="status-line">Waiting for join...</p> <p id="room-message" class="status-line">Waiting for join...</p>
</div> </div>
@@ -165,21 +155,22 @@
</div> </div>
</section> </section>
<div id="terminal-modal-overlay" class="terminal-modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="terminal-title"> <section
<section class="window terminal-window"> id="theme-tool-window"
<div class="title-bar"> class="window ui-tool-window hidden"
<span id="terminal-title">RoomTerminal.exe</span> role="dialog"
<div class="title-bar-controls"> aria-modal="false"
<button type="button" id="terminal-close-btn">×</button> aria-labelledby="theme-tool-title"
</div> data-ui-window
</div> data-window-title="ThemePicker.exe"
<div class="window-content terminal-window-content"> data-window-rights="all"
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div> data-window-order="10"
</div> data-window-default-left="16"
</section> data-window-default-top="88"
</div> data-window-default-width="390"
data-window-default-height="250"
<section id="theme-tool-window" class="window ui-tool-window hidden" role="dialog" aria-modal="false" aria-labelledby="theme-tool-title"> data-window-icons='{"win98":"/static/img/Windows Icons - PNG/main.cpl_14_109-1.png","modern":"/static/img/Windows Icons - PNG/msconfig.exe_14_128-0.png","none":"/static/img/Windows Icons - PNG/taskmgr.exe_14_118-1.png","default":"/static/img/Windows Icons - PNG/main.cpl_14_109-1.png"}'
>
<div class="title-bar ui-tool-title-bar" data-role="drag-handle"> <div class="title-bar ui-tool-title-bar" data-role="drag-handle">
<span id="theme-tool-title">ThemePicker.exe</span> <span id="theme-tool-title">ThemePicker.exe</span>
<div class="title-bar-controls"> <div class="title-bar-controls">
@@ -205,7 +196,22 @@
</div> </div>
</section> </section>
<section id="mode-tool-window" class="window ui-tool-window hidden" role="dialog" aria-modal="false" aria-labelledby="mode-tool-title"> <section
id="mode-tool-window"
class="window ui-tool-window hidden"
role="dialog"
aria-modal="false"
aria-labelledby="mode-tool-title"
data-ui-window
data-window-title="DisplayMode.exe"
data-window-rights="all"
data-window-order="20"
data-window-default-left="424"
data-window-default-top="88"
data-window-default-width="340"
data-window-default-height="190"
data-window-icons='{"win98":"/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png","modern":"/static/img/Windows Icons - PNG/desk.cpl_14_100-0.png","none":"/static/img/Windows Icons - PNG/timedate.cpl_14_200-6.png","default":"/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png"}'
>
<div class="title-bar ui-tool-title-bar" data-role="drag-handle"> <div class="title-bar ui-tool-title-bar" data-role="drag-handle">
<span id="mode-tool-title">DisplayMode.exe</span> <span id="mode-tool-title">DisplayMode.exe</span>
<div class="title-bar-controls"> <div class="title-bar-controls">
@@ -220,20 +226,38 @@
</button> </button>
</div> </div>
</section> </section>
<section
id="terminal-tool-window"
class="window ui-tool-window terminal-window hidden"
role="dialog"
aria-modal="false"
aria-labelledby="terminal-title"
data-ui-window
data-window-title="RoomTerminal.exe"
data-window-rights="admin"
data-window-order="30"
data-window-default-left="780"
data-window-default-top="88"
data-window-default-width="500"
data-window-default-height="350"
data-window-icons='{"win98":"/static/img/Windows Icons - PNG/taskmgr.exe_14_107-1.png","modern":"/static/img/Windows Icons - PNG/taskmgr.exe_14_137.png","none":"/static/img/Windows Icons - PNG/taskmgr.exe_14_118-1.png","default":"/static/img/Windows Icons - PNG/taskmgr.exe_14_107-1.png"}'
>
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
<span id="terminal-title">RoomTerminal.exe</span>
<div class="title-bar-controls">
<button type="button" data-role="close-window" data-target="terminal-tool-window">×</button>
</div>
</div>
<div class="window-content terminal-window-content">
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div>
</div>
</section>
</main> </main>
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar"> <footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
<div class="taskbar-shell"> <div class="taskbar-shell">
<div class="taskbar-program-list"> <div class="taskbar-program-list" data-role="taskbar-program-list"></div>
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="theme-tool-window" aria-label="Open theme picker">
<img class="taskbar-icon" src="/static/img/Windows Icons - PNG/main.cpl_14_109-1.png" alt="">
<span>ThemePicker.exe</span>
</button>
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="mode-tool-window" aria-label="Open display mode settings">
<img class="taskbar-icon" data-role="mode-icon" src="/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png" alt="">
<span>DisplayMode.exe</span>
</button>
</div>
</div> </div>
</footer> </footer>

View File

@@ -499,15 +499,6 @@ body.is-dragging-window .ui-tool-title-bar {
width: min(27rem, 92vw); width: min(27rem, 92vw);
} }
.terminal-modal-overlay {
position: fixed;
inset: 0;
z-index: 72;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-window { .terminal-window {
width: min(46rem, 94vw); width: min(46rem, 94vw);
} }

View File

@@ -16,15 +16,13 @@ const participantList = document.getElementById('participant-list');
const adminControls = document.getElementById('admin-controls'); const adminControls = document.getElementById('admin-controls');
const revealBtn = document.getElementById('reveal-btn'); const revealBtn = document.getElementById('reveal-btn');
const resetBtn = document.getElementById('reset-btn'); const resetBtn = document.getElementById('reset-btn');
const terminalBtn = document.getElementById('terminal-btn');
const shareLinkInput = document.getElementById('share-link'); const shareLinkInput = document.getElementById('share-link');
const shareAdminToggle = document.getElementById('share-admin-toggle'); const shareAdminToggle = document.getElementById('share-admin-toggle');
const votesCounter = document.getElementById('votes-counter'); const votesCounter = document.getElementById('votes-counter');
const roomMessage = document.getElementById('room-message'); const roomMessage = document.getElementById('room-message');
const changeNameBtn = document.getElementById('change-name-btn'); const changeNameBtn = document.getElementById('change-name-btn');
const terminalModalOverlay = document.getElementById('terminal-modal-overlay');
const terminalCloseBtn = document.getElementById('terminal-close-btn');
const terminalLogOutput = document.getElementById('terminal-log-output'); const terminalLogOutput = document.getElementById('terminal-log-output');
const TERMINAL_WINDOW_ID = 'terminal-tool-window';
const joinPanel = document.getElementById('join-panel'); const joinPanel = document.getElementById('join-panel');
const joinForm = document.getElementById('join-form'); const joinForm = document.getElementById('join-form');
@@ -315,15 +313,6 @@ function renderTerminalLogs(logs) {
terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight; terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight;
} }
function openTerminal() {
terminalModalOverlay.classList.remove('hidden');
renderTerminalLogs(latestAdminLogs);
}
function closeTerminal() {
terminalModalOverlay.classList.add('hidden');
}
function renderState(state) { function renderState(state) {
roomTitle.textContent = `${state.roomName} (${state.roomId})`; roomTitle.textContent = `${state.roomName} (${state.roomId})`;
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`; revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
@@ -341,16 +330,18 @@ function renderState(state) {
latestLinks = state.links || { participantLink: '', adminLink: '' }; latestLinks = state.links || { participantLink: '', adminLink: '' };
updateShareLink(); updateShareLink();
if (typeof window.setUIWindowAccess === 'function') {
window.setUIWindowAccess({ admin: state.viewerIsAdmin });
}
if (state.viewerIsAdmin) { if (state.viewerIsAdmin) {
adminControls.classList.remove('hidden'); adminControls.classList.remove('hidden');
terminalBtn.classList.remove('hidden');
} else { } else {
adminControls.classList.add('hidden'); adminControls.classList.add('hidden');
terminalBtn.classList.add('hidden');
closeTerminal();
} }
latestAdminLogs = Array.isArray(state.adminLogs) ? state.adminLogs : []; latestAdminLogs = Array.isArray(state.adminLogs) ? state.adminLogs : [];
if (state.viewerIsAdmin && !terminalModalOverlay.classList.contains('hidden')) { if (state.viewerIsAdmin && typeof window.isUIWindowOpen === 'function' && window.isUIWindowOpen(TERMINAL_WINDOW_ID)) {
renderTerminalLogs(latestAdminLogs); renderTerminalLogs(latestAdminLogs);
} }
@@ -466,21 +457,21 @@ async function changeName() {
revealBtn.addEventListener('click', () => adminAction('reveal')); revealBtn.addEventListener('click', () => adminAction('reveal'));
resetBtn.addEventListener('click', () => adminAction('reset')); resetBtn.addEventListener('click', () => adminAction('reset'));
terminalBtn.addEventListener('click', openTerminal);
terminalCloseBtn.addEventListener('click', closeTerminal);
terminalModalOverlay.addEventListener('click', (event) => {
if (event.target === terminalModalOverlay) {
closeTerminal();
}
});
shareAdminToggle.addEventListener('change', updateShareLink); shareAdminToggle.addEventListener('change', updateShareLink);
changeNameBtn.addEventListener('click', () => { changeNameBtn.addEventListener('click', () => {
void changeName(); void changeName();
}); });
window.addEventListener('keydown', (event) => { document.addEventListener('click', (event) => {
if (event.key === 'Escape') { const openBtn = event.target.closest('[data-role="open-window"]');
closeTerminal(); if (!openBtn || openBtn.dataset.target !== TERMINAL_WINDOW_ID) {
return;
} }
requestAnimationFrame(() => {
if (typeof window.isUIWindowOpen === 'function' && window.isUIWindowOpen(TERMINAL_WINDOW_ID)) {
renderTerminalLogs(latestAdminLogs);
}
});
}); });
joinForm.addEventListener('submit', async (event) => { joinForm.addEventListener('submit', async (event) => {

View File

@@ -5,12 +5,11 @@
const DEFAULT_THEME = 'win98'; const DEFAULT_THEME = 'win98';
const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png'; const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png';
const MODE_ICON_DARK = '/static/img/Windows Icons - PNG/desk.cpl_14_40-6.png'; const MODE_ICON_DARK = '/static/img/Windows Icons - PNG/desk.cpl_14_40-6.png';
const DEFAULT_WINDOW_LAYOUTS = {
'theme-tool-window': { left: 16, top: 88, width: 390, height: 250 },
'mode-tool-window': { left: 424, top: 88, width: 340, height: 190 },
};
let floatingWindowZ = 80; let floatingWindowZ = 80;
let windowLayouts = {}; let windowLayouts = {};
let windowDefs = [];
let accessState = { admin: false };
function isMobileViewport() { function isMobileViewport() {
return window.matchMedia('(max-width: 899px)').matches; return window.matchMedia('(max-width: 899px)').matches;
@@ -29,58 +28,37 @@
document.documentElement.removeAttribute('data-theme'); document.documentElement.removeAttribute('data-theme');
} }
function getCurrentTheme() {
return document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
}
function getCurrentMode() { function getCurrentMode() {
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
} }
function isWindowOpen(id) {
const win = document.getElementById(id);
return Boolean(win && !win.classList.contains('hidden'));
}
function syncTaskButtons() {
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
const target = button.dataset.target;
button.classList.toggle('is-active', isWindowOpen(target));
});
}
function syncControls() {
const theme = document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
const mode = getCurrentMode();
const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
const selected = button.dataset.theme === theme;
button.classList.toggle('is-selected', selected);
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
});
document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
el.textContent = modeLabel;
});
document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
el.src = modeIcon;
});
document.querySelectorAll('#mode-status-text').forEach((el) => {
el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
});
syncTaskButtons();
}
function bringWindowToFront(windowEl) {
floatingWindowZ += 1;
windowEl.style.zIndex = String(floatingWindowZ);
}
function clamp(value, min, max) { function clamp(value, min, max) {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }
function parseJSON(value, fallback) {
if (!value) {
return fallback;
}
try {
return JSON.parse(value);
} catch (_err) {
return fallback;
}
}
function hasWindowAccess(def) {
const rights = def.rights || 'all';
if (rights === 'admin') {
return Boolean(accessState.admin);
}
return true;
}
function loadWindowLayouts() { function loadWindowLayouts() {
try { try {
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY); const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
@@ -88,10 +66,7 @@
return {}; return {};
} }
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') { return parsed && typeof parsed === 'object' ? parsed : {};
return {};
}
return parsed;
} catch (_err) { } catch (_err) {
return {}; return {};
} }
@@ -130,6 +105,7 @@
const height = clamp(layout.height, minHeight, maxHeight); const height = clamp(layout.height, minHeight, maxHeight);
const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin)); const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin));
const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin)); const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin));
return { left, top, width, height }; return { left, top, width, height };
} }
@@ -153,94 +129,217 @@
}; };
} }
function bringWindowToFront(windowEl) {
floatingWindowZ += 1;
windowEl.style.zIndex = String(floatingWindowZ);
}
function getDefaultLayout(el, index) {
const step = 28 * index;
return normalizeLayout({
left: Number(el.dataset.windowDefaultLeft) || 16 + step,
top: Number(el.dataset.windowDefaultTop) || 88 + step,
width: Number(el.dataset.windowDefaultWidth) || 360,
height: Number(el.dataset.windowDefaultHeight) || 220,
});
}
function persistWindowLayout(windowEl) { function persistWindowLayout(windowEl) {
const id = windowEl.id; const id = windowEl.id;
if (!id) { if (!id || isMobileViewport()) {
return; return;
} }
const next = clampLayoutToViewport(readLayoutFromDOM(windowEl)); windowLayouts[id] = clampLayoutToViewport(readLayoutFromDOM(windowEl));
windowLayouts[id] = next;
saveWindowLayouts(); saveWindowLayouts();
} }
function ensureWindowLayout(windowEl) { function ensureWindowLayout(def) {
const id = windowEl.id;
if (!id) {
return;
}
if (isMobileViewport()) { if (isMobileViewport()) {
windowEl.style.right = 'auto'; def.el.style.right = 'auto';
windowEl.style.bottom = 'auto'; def.el.style.bottom = 'auto';
windowEl.style.transform = 'none'; def.el.style.transform = 'none';
return; return;
} }
const defaults = DEFAULT_WINDOW_LAYOUTS[id];
const saved = windowLayouts[id]; const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
const normalized = normalizeLayout(saved, defaults);
const clamped = clampLayoutToViewport(normalized); const clamped = clampLayoutToViewport(normalized);
applyLayout(windowEl, clamped); applyLayout(def.el, clamped);
windowLayouts[id] = clamped; windowLayouts[def.id] = clamped;
} }
function openToolWindow(id) { function isWindowOpen(id) {
const windowEl = document.getElementById(id); const def = windowDefs.find((item) => item.id === id);
if (!windowEl) { return Boolean(def && !def.el.classList.contains('hidden'));
}
function closeWindowById(id) {
const def = windowDefs.find((item) => item.id === id);
if (!def) {
return; return;
} }
ensureWindowLayout(windowEl); def.el.classList.add('hidden');
windowEl.classList.remove('hidden');
bringWindowToFront(windowEl);
syncTaskButtons(); syncTaskButtons();
} }
function closeToolWindow(id) { function openWindowById(id) {
const windowEl = document.getElementById(id); const def = windowDefs.find((item) => item.id === id);
if (!windowEl) { if (!def || !hasWindowAccess(def)) {
return; return;
} }
windowEl.classList.add('hidden'); ensureWindowLayout(def);
def.el.classList.remove('hidden');
bringWindowToFront(def.el);
syncTaskButtons(); syncTaskButtons();
} }
function resolveWindowIcon(def, theme) {
return def.icons[theme] || def.icons.default || '/static/img/Windows Icons - PNG/main.cpl_14_109-1.png';
}
function renderTaskbarButtons() {
const theme = getCurrentTheme();
const visibleDefs = windowDefs
.filter(hasWindowAccess)
.sort((a, b) => a.order - b.order);
const html = visibleDefs.map((def) => {
const icon = resolveWindowIcon(def, theme);
const safeTitle = def.title;
return [
`<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="${def.id}" aria-label="Open ${safeTitle}">`,
`<img class="taskbar-icon" src="${icon}" alt="">`,
`<span>${safeTitle}</span>`,
'</button>',
].join('');
}).join('');
document.querySelectorAll('[data-role="taskbar-program-list"]').forEach((container) => {
container.innerHTML = html;
});
syncTaskButtons();
}
function syncTaskButtons() {
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
const target = button.dataset.target;
button.classList.toggle('is-active', isWindowOpen(target));
});
}
function syncControls() {
const theme = getCurrentTheme();
const mode = getCurrentMode();
const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
const selected = button.dataset.theme === theme;
button.classList.toggle('is-selected', selected);
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
});
document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
el.textContent = modeLabel;
});
document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
el.src = modeIcon;
});
document.querySelectorAll('#mode-status-text').forEach((el) => {
el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
});
renderTaskbarButtons();
}
function collectWindowDefinitions() {
const windows = Array.from(document.querySelectorAll('[data-ui-window]'));
windowDefs = windows.map((el, index) => ({
id: el.id,
el,
title: el.dataset.windowTitle || el.id,
rights: el.dataset.windowRights || 'all',
order: Number(el.dataset.windowOrder) || (index + 1) * 10,
icons: parseJSON(el.dataset.windowIcons, {}),
defaultLayout: getDefaultLayout(el, index),
})).filter((def) => Boolean(def.id));
}
function setWindowAccess(nextAccess) {
accessState = {
...accessState,
...(nextAccess || {}),
};
windowDefs.forEach((def) => {
if (!hasWindowAccess(def)) {
def.el.classList.add('hidden');
}
});
syncControls();
}
function initWindowLayouts() {
windowLayouts = loadWindowLayouts();
windowDefs.forEach((def) => {
ensureWindowLayout(def);
});
window.addEventListener('resize', () => {
if (isMobileViewport()) {
return;
}
windowDefs.forEach((def) => {
const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
const clamped = clampLayoutToViewport(normalized);
applyLayout(def.el, clamped);
windowLayouts[def.id] = clamped;
});
saveWindowLayouts();
});
}
function initDraggableWindows() { function initDraggableWindows() {
let dragState = null; let dragState = null;
document.querySelectorAll('[data-role="drag-handle"]').forEach((handle) => { document.addEventListener('pointerdown', (event) => {
handle.addEventListener('pointerdown', (event) => { const handle = event.target.closest('[data-role="drag-handle"]');
if (event.button !== 0) { if (!handle || event.button !== 0 || event.target.closest('button')) {
return; return;
} }
if (event.target.closest('button')) { if (isMobileViewport()) {
return; return;
} }
const windowEl = handle.closest('.ui-tool-window'); const windowEl = handle.closest('.ui-tool-window[data-ui-window]');
if (!windowEl || windowEl.classList.contains('hidden')) { if (!windowEl || windowEl.classList.contains('hidden')) {
return; return;
} }
if (isMobileViewport()) {
return;
}
bringWindowToFront(windowEl); bringWindowToFront(windowEl);
const rect = windowEl.getBoundingClientRect(); const rect = windowEl.getBoundingClientRect();
windowEl.style.left = `${rect.left}px`; windowEl.style.left = `${rect.left}px`;
windowEl.style.top = `${rect.top}px`; windowEl.style.top = `${rect.top}px`;
windowEl.style.right = 'auto'; windowEl.style.right = 'auto';
windowEl.style.bottom = 'auto'; windowEl.style.bottom = 'auto';
windowEl.style.transform = 'none'; windowEl.style.transform = 'none';
dragState = { dragState = {
pointerId: event.pointerId, pointerId: event.pointerId,
windowEl, windowEl,
offsetX: event.clientX - rect.left, offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top, offsetY: event.clientY - rect.top,
}; };
document.body.classList.add('is-dragging-window'); document.body.classList.add('is-dragging-window');
handle.setPointerCapture(event.pointerId); handle.setPointerCapture(event.pointerId);
event.preventDefault(); event.preventDefault();
});
}); });
window.addEventListener('pointermove', (event) => { window.addEventListener('pointermove', (event) => {
@@ -279,59 +378,64 @@
const observer = new ResizeObserver((entries) => { const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
const windowEl = entry.target; const windowEl = entry.target;
if (windowEl.classList.contains('hidden')) { if (windowEl.classList.contains('hidden') || isMobileViewport()) {
return;
}
if (isMobileViewport()) {
return; return;
} }
persistWindowLayout(windowEl); persistWindowLayout(windowEl);
}); });
}); });
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => { windowDefs.forEach((def) => observer.observe(def.el));
observer.observe(windowEl);
});
} }
function initWindowLayouts() { function initTaskbarAndWindowEvents() {
windowLayouts = loadWindowLayouts(); document.addEventListener('click', (event) => {
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => { const openBtn = event.target.closest('[data-role="open-window"]');
ensureWindowLayout(windowEl); if (openBtn) {
}); const target = openBtn.dataset.target;
if (!target) {
window.addEventListener('resize', () => {
if (isMobileViewport()) {
return;
}
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
const id = windowEl.id;
if (!id) {
return; return;
} }
const normalized = normalizeLayout(windowLayouts[id], DEFAULT_WINDOW_LAYOUTS[id]); if (isWindowOpen(target)) {
const clamped = clampLayoutToViewport(normalized); closeWindowById(target);
applyLayout(windowEl, clamped); } else {
windowLayouts[id] = clamped; openWindowById(target);
}
return;
}
const closeBtn = event.target.closest('[data-role="close-window"]');
if (!closeBtn) {
return;
}
const target = closeBtn.dataset.target || closeBtn.closest('.ui-tool-window')?.id;
if (target) {
closeWindowById(target);
}
});
windowDefs.forEach((def) => {
def.el.addEventListener('pointerdown', () => {
if (def.el.classList.contains('hidden')) {
return;
}
bringWindowToFront(def.el);
}); });
saveWindowLayouts(); });
window.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
windowDefs.forEach((def) => {
def.el.classList.add('hidden');
});
syncTaskButtons();
}); });
} }
function initUIControls() { function initThemeAndModeHandlers() {
if (window.__uiControlsInitialized) {
syncControls();
return;
}
window.__uiControlsInitialized = true;
const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
const savedMode = localStorage.getItem(MODE_KEY) || 'light';
applyTheme(savedTheme);
applyMode(savedMode);
initWindowLayouts();
syncControls();
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => { document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const value = button.dataset.theme || DEFAULT_THEME; const value = button.dataset.theme || DEFAULT_THEME;
@@ -349,58 +453,34 @@
syncControls(); syncControls();
}); });
}); });
}
document.querySelectorAll('[data-role="open-window"]').forEach((button) => { function initUIControls() {
button.addEventListener('click', () => { if (window.__uiControlsInitialized) {
const target = button.dataset.target; syncControls();
if (!target) { return;
return; }
} window.__uiControlsInitialized = true;
if (isWindowOpen(target)) {
closeToolWindow(target);
return;
}
openToolWindow(target);
});
});
document.querySelectorAll('[data-role="close-window"]').forEach((button) => { const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
button.addEventListener('click', () => { const savedMode = localStorage.getItem(MODE_KEY) || 'light';
const target = button.dataset.target; applyTheme(savedTheme);
if (!target) { applyMode(savedMode);
return;
}
closeToolWindow(target);
});
});
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
windowEl.addEventListener('pointerdown', () => {
if (windowEl.classList.contains('hidden')) {
return;
}
bringWindowToFront(windowEl);
});
});
window.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
if (windowEl.classList.contains('hidden')) {
return;
}
windowEl.classList.add('hidden');
});
syncTaskButtons();
});
collectWindowDefinitions();
initWindowLayouts();
initTaskbarAndWindowEvents();
initThemeAndModeHandlers();
initDraggableWindows(); initDraggableWindows();
initResizableWindows(); initResizableWindows();
syncControls();
} }
window.initUIControls = initUIControls; window.initUIControls = initUIControls;
window.isUIWindowOpen = isWindowOpen;
window.openUIWindow = openWindowById;
window.closeUIWindow = closeWindowById;
window.setUIWindowAccess = setWindowAccess;
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUIControls, { once: true }); document.addEventListener('DOMContentLoaded', initUIControls, { once: true });