(() => { const THEME_KEY = 'scrumPoker.ui.theme'; const MODE_KEY = 'scrumPoker.ui.mode'; const WINDOW_LAYOUTS_KEY = 'scrumPoker.ui.windowLayouts.v1'; const DEFAULT_THEME = 'win98'; 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'; let floatingWindowZ = 80; let windowLayouts = {}; let windowDefs = []; let accessState = { admin: false }; function isMobileViewport() { return window.matchMedia('(max-width: 899px)').matches; } function applyTheme(theme) { const normalized = theme || DEFAULT_THEME; document.documentElement.setAttribute('data-ui-theme', normalized); } function applyMode(mode) { if (mode === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); return; } document.documentElement.removeAttribute('data-theme'); } function getCurrentTheme() { return document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME; } function getCurrentMode() { return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; } function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } 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() { try { const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY); if (!raw) { return {}; } const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? parsed : {}; } catch (_err) { return {}; } } function saveWindowLayouts() { localStorage.setItem(WINDOW_LAYOUTS_KEY, JSON.stringify(windowLayouts)); } function normalizeLayout(raw, defaults) { const fallback = defaults || { left: 16, top: 88, width: 360, height: 220 }; const minWidth = 260; const minHeight = 160; const width = Number(raw?.width); const height = Number(raw?.height); const left = Number(raw?.left); const top = Number(raw?.top); return { width: Number.isFinite(width) ? Math.max(minWidth, width) : fallback.width, height: Number.isFinite(height) ? Math.max(minHeight, height) : fallback.height, left: Number.isFinite(left) ? left : fallback.left, top: Number.isFinite(top) ? top : fallback.top, }; } function clampLayoutToViewport(layout) { const margin = 8; const minWidth = 260; const minHeight = 160; const maxWidth = Math.max(minWidth, window.innerWidth - margin * 2); const maxHeight = Math.max(minHeight, window.innerHeight - margin * 2); const width = clamp(layout.width, minWidth, maxWidth); const height = clamp(layout.height, minHeight, maxHeight); 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)); return { left, top, width, height }; } function applyLayout(windowEl, layout) { windowEl.style.left = `${layout.left}px`; windowEl.style.top = `${layout.top}px`; windowEl.style.width = `${layout.width}px`; windowEl.style.height = `${layout.height}px`; windowEl.style.right = 'auto'; windowEl.style.bottom = 'auto'; windowEl.style.transform = 'none'; } function readLayoutFromDOM(windowEl) { const rect = windowEl.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: rect.width, height: rect.height, }; } 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) { const id = windowEl.id; if (!id || isMobileViewport()) { return; } windowLayouts[id] = clampLayoutToViewport(readLayoutFromDOM(windowEl)); saveWindowLayouts(); } function measureLayoutFromContent(def) { const windowEl = def.el; const fallback = def.defaultLayout; const wasHidden = windowEl.classList.contains('hidden'); const previousStyles = { visibility: windowEl.style.visibility, left: windowEl.style.left, top: windowEl.style.top, width: windowEl.style.width, height: windowEl.style.height, right: windowEl.style.right, bottom: windowEl.style.bottom, transform: windowEl.style.transform, }; if (wasHidden) { windowEl.classList.remove('hidden'); } windowEl.style.visibility = 'hidden'; windowEl.style.left = '-10000px'; windowEl.style.top = '-10000px'; windowEl.style.width = 'max-content'; windowEl.style.height = 'auto'; windowEl.style.right = 'auto'; windowEl.style.bottom = 'auto'; windowEl.style.transform = 'none'; const rect = windowEl.getBoundingClientRect(); windowEl.style.visibility = previousStyles.visibility; windowEl.style.left = previousStyles.left; windowEl.style.top = previousStyles.top; windowEl.style.width = previousStyles.width; windowEl.style.height = previousStyles.height; windowEl.style.right = previousStyles.right; windowEl.style.bottom = previousStyles.bottom; windowEl.style.transform = previousStyles.transform; if (wasHidden) { windowEl.classList.add('hidden'); } return normalizeLayout({ left: fallback.left, top: fallback.top, width: Number.isFinite(rect.width) && rect.width > 0 ? Math.ceil(rect.width) : fallback.width, height: Number.isFinite(rect.height) && rect.height > 0 ? Math.ceil(rect.height) : fallback.height, }, fallback); } function ensureWindowLayout(def, options = {}) { if (isMobileViewport()) { def.el.style.right = 'auto'; def.el.style.bottom = 'auto'; def.el.style.transform = 'none'; return; } const hasSavedLayout = hasOwn(options, 'hasSavedLayout') ? options.hasSavedLayout : hasOwn(windowLayouts, def.id); const normalized = hasSavedLayout ? normalizeLayout(windowLayouts[def.id], def.defaultLayout) : measureLayoutFromContent(def); const clamped = clampLayoutToViewport(normalized); applyLayout(def.el, clamped); windowLayouts[def.id] = clamped; } function isWindowOpen(id) { const def = windowDefs.find((item) => item.id === id); return Boolean(def && !def.el.classList.contains('hidden')); } function closeWindowById(id) { const def = windowDefs.find((item) => item.id === id); if (!def) { return; } def.el.classList.add('hidden'); syncTaskButtons(); } function openWindowById(id) { const def = windowDefs.find((item) => item.id === id); if (!def || !hasWindowAccess(def)) { return; } ensureWindowLayout(def); def.el.classList.remove('hidden'); bringWindowToFront(def.el); 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 [ `', ].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(); let shouldPersistSeededLayouts = false; windowDefs.forEach((def) => { const hasSavedLayout = hasOwn(windowLayouts, def.id); ensureWindowLayout(def, { hasSavedLayout }); if (!hasSavedLayout) { shouldPersistSeededLayouts = true; } }); if (shouldPersistSeededLayouts) { saveWindowLayouts(); } 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() { let dragState = null; document.addEventListener('pointerdown', (event) => { const handle = event.target.closest('[data-role="drag-handle"]'); if (!handle || event.button !== 0 || event.target.closest('button')) { return; } if (isMobileViewport()) { return; } const windowEl = handle.closest('.ui-tool-window[data-ui-window]'); if (!windowEl || windowEl.classList.contains('hidden')) { return; } bringWindowToFront(windowEl); const rect = windowEl.getBoundingClientRect(); windowEl.style.left = `${rect.left}px`; windowEl.style.top = `${rect.top}px`; windowEl.style.right = 'auto'; windowEl.style.bottom = 'auto'; windowEl.style.transform = 'none'; dragState = { pointerId: event.pointerId, windowEl, offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top, }; document.body.classList.add('is-dragging-window'); handle.setPointerCapture(event.pointerId); event.preventDefault(); }); window.addEventListener('pointermove', (event) => { if (!dragState || dragState.pointerId !== event.pointerId) { return; } const windowEl = dragState.windowEl; const maxLeft = Math.max(0, window.innerWidth - windowEl.offsetWidth); const maxTop = Math.max(0, window.innerHeight - windowEl.offsetHeight); const nextLeft = clamp(event.clientX - dragState.offsetX, 0, maxLeft); const nextTop = clamp(event.clientY - dragState.offsetY, 0, maxTop); windowEl.style.left = `${nextLeft}px`; windowEl.style.top = `${nextTop}px`; }); function finishDrag(event) { if (!dragState || dragState.pointerId !== event.pointerId) { return; } persistWindowLayout(dragState.windowEl); dragState = null; document.body.classList.remove('is-dragging-window'); } window.addEventListener('pointerup', finishDrag); window.addEventListener('pointercancel', finishDrag); } function initResizableWindows() { if (typeof ResizeObserver !== 'function') { return; } const observer = new ResizeObserver((entries) => { entries.forEach((entry) => { const windowEl = entry.target; if (windowEl.classList.contains('hidden') || isMobileViewport()) { return; } persistWindowLayout(windowEl); }); }); windowDefs.forEach((def) => observer.observe(def.el)); } function initTaskbarAndWindowEvents() { document.addEventListener('click', (event) => { const openBtn = event.target.closest('[data-role="open-window"]'); if (openBtn) { const target = openBtn.dataset.target; if (!target) { return; } if (isWindowOpen(target)) { closeWindowById(target); } else { 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); }); }); window.addEventListener('keydown', (event) => { if (event.key !== 'Escape') { return; } windowDefs.forEach((def) => { def.el.classList.add('hidden'); }); syncTaskButtons(); }); } function initThemeAndModeHandlers() { document.querySelectorAll('[data-role="theme-option"]').forEach((button) => { button.addEventListener('click', () => { const value = button.dataset.theme || DEFAULT_THEME; localStorage.setItem(THEME_KEY, value); applyTheme(value); syncControls(); }); }); document.querySelectorAll('[data-role="mode-toggle-action"]').forEach((button) => { button.addEventListener('click', () => { const next = getCurrentMode() === 'dark' ? 'light' : 'dark'; localStorage.setItem(MODE_KEY, next); applyMode(next); syncControls(); }); }); } function initUIControls() { 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); collectWindowDefinitions(); initWindowLayouts(); initTaskbarAndWindowEvents(); initThemeAndModeHandlers(); initDraggableWindows(); initResizableWindows(); syncControls(); } window.initUIControls = initUIControls; window.isUIWindowOpen = isWindowOpen; window.openUIWindow = openWindowById; window.closeUIWindow = closeWindowById; window.setUIWindowAccess = setWindowAccess; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initUIControls, { once: true }); } else { initUIControls(); } })();