commit 54a24066941ba29ad595692eda9a5fada8668116 Author: Dion Timmer <825343+diontimmer@users.noreply.github.com> Date: Fri Mar 13 01:00:30 2026 -0400 Initial commit: VeryExtraOS ARG site diff --git a/README.md b/README.md new file mode 100644 index 0000000..904fab1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# veryextra ARG site diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml new file mode 100644 index 0000000..35d526b --- /dev/null +++ b/ops/docker-compose.yml @@ -0,0 +1,9 @@ +services: + veryextra-arg: + image: nginx:alpine + container_name: veryextra-arg + restart: unless-stopped + ports: + - "8008:80" + volumes: + - ../site:/usr/share/nginx/html:ro diff --git a/site/assets/audio/violin.mp3 b/site/assets/audio/violin.mp3 new file mode 100644 index 0000000..1553626 Binary files /dev/null and b/site/assets/audio/violin.mp3 differ diff --git a/site/fakeweb/bbs-luna-board.html b/site/fakeweb/bbs-luna-board.html new file mode 100644 index 0000000..74225bb --- /dev/null +++ b/site/fakeweb/bbs-luna-board.html @@ -0,0 +1,5 @@ +

LunaBoard BBS

+

[Thread] Show your desktop screenshots (248 replies)

+

[Thread] Best old internet sounds?? (93 replies)

+

[Thread] Help: my css gradients look too modern

+

post your ASCII signatures here

diff --git a/site/fakeweb/fansite-winamp-skins.html b/site/fakeweb/fansite-winamp-skins.html new file mode 100644 index 0000000..c8e76c3 --- /dev/null +++ b/site/fakeweb/fansite-winamp-skins.html @@ -0,0 +1,9 @@ +

Winamp Skins Archive

+

It really whips the llama's ass.

+ + + + + +
SkinStyleStatus
Neon BubblegumY2KDownload mirror offline
Crystal LavenderFuturisticAvailable
Night KittyDark CuteAvailable
+

need old download badges?

diff --git a/site/fakeweb/geocities-ponzu-home.html b/site/fakeweb/geocities-ponzu-home.html new file mode 100644 index 0000000..4ce336f --- /dev/null +++ b/site/fakeweb/geocities-ponzu-home.html @@ -0,0 +1,10 @@ +

~* Ponzu's Cosmic Corner *~

+

Welcome to my personal homepage!!!

+

sign my guestbook! sign my guestbook! sign my guestbook!

+ +

visit my anime shrine

+

back to pink webring

diff --git a/site/fakeweb/index.json b/site/fakeweb/index.json new file mode 100644 index 0000000..1bce296 --- /dev/null +++ b/site/fakeweb/index.json @@ -0,0 +1,92 @@ +{ + "pages": [ + { + "route": "home://portal", + "title": "Retro Portal", + "file": "/fakeweb/portal.html", + "favorite": true, + "tags": [ + "portal", + "home", + "retro" + ] + }, + { + "route": "site://geocities/ponzu-home", + "title": "Ponzu Home Page", + "file": "/fakeweb/geocities-ponzu-home.html", + "favorite": true, + "tags": [ + "geocities", + "anime", + "home" + ] + }, + { + "route": "site://webring/pink-directory", + "title": "Pink WebRing Directory", + "file": "/fakeweb/webring-pink-directory.html", + "favorite": true, + "tags": [ + "webring", + "directory", + "links" + ] + }, + { + "route": "site://fansite/winamp-skins", + "title": "Winamp Skins Archive", + "file": "/fakeweb/fansite-winamp-skins.html", + "favorite": true, + "tags": [ + "music", + "skins", + "download" + ] + }, + { + "route": "site://bbs/luna-board", + "title": "LunaBoard BBS", + "file": "/fakeweb/bbs-luna-board.html", + "favorite": true, + "tags": [ + "bbs", + "forum", + "community" + ] + }, + { + "route": "site://museum/under-construction", + "title": "Under Construction Museum", + "file": "/fakeweb/museum-under-construction.html", + "favorite": false, + "tags": [ + "museum", + "construction", + "nostalgia" + ] + }, + { + "route": "site://otaku/sailor-shrine", + "title": "Sailor Shrine", + "file": "/fakeweb/otaku-sailor-shrine.html", + "favorite": false, + "tags": [ + "anime", + "shrine", + "otaku" + ] + }, + { + "route": "site://lab/ascii-lounge", + "title": "ASCII Lounge", + "file": "/fakeweb/lab-ascii-lounge.html", + "favorite": false, + "tags": [ + "ascii", + "art", + "lab" + ] + } + ] +} \ No newline at end of file diff --git a/site/fakeweb/lab-ascii-lounge.html b/site/fakeweb/lab-ascii-lounge.html new file mode 100644 index 0000000..33facd1 --- /dev/null +++ b/site/fakeweb/lab-ascii-lounge.html @@ -0,0 +1,8 @@ +

ASCII Lounge

+
+  (^_^)
+ <(  โ˜†  )>
+   /   \
+
+

Share your signatures and keyboard art.

+

go to BBS thread

diff --git a/site/fakeweb/museum-under-construction.html b/site/fakeweb/museum-under-construction.html new file mode 100644 index 0000000..4ead906 --- /dev/null +++ b/site/fakeweb/museum-under-construction.html @@ -0,0 +1,9 @@ +

Under Construction Museum

+

Collection of classic web relics:

+ +

escape the museum

diff --git a/site/fakeweb/otaku-sailor-shrine.html b/site/fakeweb/otaku-sailor-shrine.html new file mode 100644 index 0000000..cbc4d31 --- /dev/null +++ b/site/fakeweb/otaku-sailor-shrine.html @@ -0,0 +1,9 @@ +

Sailor Shrine โ˜†

+

This shrine is dedicated to magical girl aesthetics.

+

Top 3 opening themes:

+
    +
  1. Moonlight Densetsu
  2. +
  3. Sailor Star Song
  4. +
  5. La Soldier
  6. +
+

back to ponzu home

diff --git a/site/fakeweb/portal.html b/site/fakeweb/portal.html new file mode 100644 index 0000000..3203c30 --- /dev/null +++ b/site/fakeweb/portal.html @@ -0,0 +1,13 @@ +

RETRO PORTAL 2000

+

new layout!!! welcome surfer~

+

Pick your vibe:

+ +

ASCII Lounge ยท Sailor Shrine

+
+

This page is hand-coded with love in Notepad.

diff --git a/site/fakeweb/webring-pink-directory.html b/site/fakeweb/webring-pink-directory.html new file mode 100644 index 0000000..f2cb71b --- /dev/null +++ b/site/fakeweb/webring-pink-directory.html @@ -0,0 +1,9 @@ +

Pink WebRing Directory

+

All links approved by glitter council.

+
    +
  1. Ponzu Home Page
  2. +
  3. Winamp Skins Archive
  4. +
  5. LunaBoard BBS
  6. +
  7. ASCII Lounge
  8. +
+

return to portal

diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..3d104b3 --- /dev/null +++ b/site/index.html @@ -0,0 +1,89 @@ + + + + + + veryextra.net + + + +
+ +
+ + + + + + + + + +
+ + + + +
+ + + + + + diff --git a/site/script.js b/site/script.js new file mode 100644 index 0000000..34f249e --- /dev/null +++ b/site/script.js @@ -0,0 +1,2156 @@ +const ICON_APPS = { + mycomputer: { + title: "My Computer", + type: "explorer", + startPath: "/", + }, + notepad: { + title: "Notepad", + type: "notepad", + }, + paint: { + title: "Paint", + type: "paint", + }, + ie: { + title: "Internet Explorer", + type: "ie", + }, + recycle: { + title: "Recycle Bin", + type: "explorer", + startPath: "/Recycle Bin", + }, +}; + +const RETRO_WEB_HOME = 'home://portal'; +let retroWebIndexPromise = null; +let retroWebRoutes = new Map(); + +async function loadRetroWebIndex() { + if (retroWebIndexPromise) return retroWebIndexPromise; + + retroWebIndexPromise = fetch('/fakeweb/index.json', { cache: 'no-store' }) + .then((res) => (res.ok ? res.json() : { pages: [] })) + .catch(() => ({ pages: [] })) + .then((data) => { + const pages = Array.isArray(data.pages) ? data.pages : []; + retroWebRoutes = new Map( + pages + .filter((p) => p && p.route && p.file) + .map((p) => [String(p.route).toLowerCase(), p]), + ); + return pages; + }); + + return retroWebIndexPromise; +} + +function retroRouteKey(url) { + return String(url || '').trim().toLowerCase(); +} + +function normalizeRetroUrl(raw) { + if (!raw) return RETRO_WEB_HOME; + const text = String(raw).trim(); + if (!text) return RETRO_WEB_HOME; + + if (text.startsWith('home://') || text.startsWith('site://') || text.startsWith('search://')) { + return text; + } + + if (/^https?:\/\//i.test(text)) { + return `search://${encodeURIComponent(text)}`; + } + + if (text.includes(' ')) { + return `search://${encodeURIComponent(text)}`; + } + + if (text.includes('.')) { + return `site://${text.toLowerCase()}`; + } + + return `search://${encodeURIComponent(text)}`; +} + +function buildRetroSearchPage(query, pages) { + const q = decodeURIComponent(query || '').trim() || 'empty search'; + const needle = q.toLowerCase(); + + const matches = pages + .filter((p) => { + const hay = `${p.route || ''} ${p.title || ''} ${(p.tags || []).join(' ')}`.toLowerCase(); + return hay.includes(needle); + }) + .slice(0, 12) + .map((p) => `
  • ${p.title || p.route}
    ${p.route}
  • `) + .join(''); + + return { + title: `Search results for "${q}"`, + html: ` +

    RetroSearch 2000

    +

    Results for: ${q}

    +
      ${matches || '
    1. No direct matches. Try home://portal
    2. '}
    + `, + }; +} + +async function getRetroPage(url) { + const pages = await loadRetroWebIndex(); + + if (url.startsWith('search://')) { + return buildRetroSearchPage(url.slice('search://'.length), pages); + } + + const entry = retroWebRoutes.get(retroRouteKey(url)); + if (!entry) return null; + + try { + const res = await fetch(entry.file, { cache: 'no-store' }); + if (!res.ok) return null; + const html = await res.text(); + return { + title: entry.title || entry.route, + html, + route: entry.route, + }; + } catch { + return null; + } +} + +function htmlEscape(text) { + return String(text || '').replace(/[&<>"']/g, (m) => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }[m])); +} + +const FILE_SYSTEM = { + type: 'folder', + name: 'My Computer', + children: [ + { + type: 'folder', + name: 'C:', + children: [ + { type: 'folder', name: 'Program Files', children: [ + { type: 'file', name: 'readme.txt', fileType: 'txt', content: 'Welcome to veryextraOS 1.0\n\nThis is a mock file system.' }, + ]}, + { type: 'folder', name: 'WINDOWS', children: [ + { type: 'file', name: 'win.ini', fileType: 'txt', content: '[fonts]\n[extensions]\n[desktop]' }, + ]}, + { type: 'folder', name: 'Documents and Settings', children: [ + { type: 'folder', name: 'Dion', children: [ + { type: 'folder', name: 'My Documents', children: [ + { type: 'file', name: 'shopping-list.txt', fileType: 'txt', content: 'milk\ncoffee\nstickers' }, + { type: 'file', name: 'desktop-ideas.txt', fileType: 'txt', content: 'Make everything cuter\nAdd sounds\nAdd themes' }, + ]}, + { type: 'folder', name: 'My Pictures', children: [ + { type: 'file', name: 'wallpaper-preview.jpg', fileType: 'image', content: 'https://wallpaperaccess.com/full/4810999.jpg' }, + { type: 'file', name: 'pink-theme.png', fileType: 'image', content: 'https://picsum.photos/seed/pinktheme/720/450' }, + ]}, + ]}, + ]}, + ], + }, + { + type: 'folder', + name: 'D:', + children: [ + { type: 'folder', name: 'Music', children: [ + { type: 'file', name: 'favorites.txt', fileType: 'txt', content: 'Track 01\nTrack 02\nTrack 03' }, + { type: 'file', name: 'violin.mp3', fileType: 'audio', content: '/assets/audio/violin.mp3' }, + ]}, + ], + }, + { + type: 'folder', + name: 'Recycle Bin', + children: [ + { type: 'file', name: 'old-theme.zip', fileType: 'txt', content: 'Archive is empty.' }, + { type: 'file', name: 'notes (1).txt', fileType: 'txt', content: 'Draft notes moved to recycle bin.' }, + ], + }, + ], +}; + +const statusEl = document.getElementById('status'); +const clockEl = document.getElementById('clock'); +const iconsEl = document.getElementById('icons'); +const iconButtons = Array.from(document.querySelectorAll('.desktop-icon')); +const resetLayoutBtn = document.getElementById('reset-layout-btn'); +const startBtn = document.getElementById('start-btn'); +const taskbarWindowsEl = document.getElementById('taskbar-windows'); +const showDesktopBtn = document.getElementById('show-desktop-btn'); +const windowTemplate = document.getElementById('window-template'); + +const ICON_LAYOUT_KEY = 'veryextraOS.iconLayout.v1'; +const selectedIcons = new Set(); +const windowsById = new Map(); +const startMenuEl = document.getElementById('start-menu'); + +let selectionBoxEl = null; +let topWindowZ = 1000; +let windowSeq = 0; + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +function setStatus(text) { + if (statusEl) statusEl.textContent = text; +} + +let contextMenuEl = null; + +function ensureContextMenu() { + if (contextMenuEl) return contextMenuEl; + + contextMenuEl = document.createElement('div'); + contextMenuEl.id = 'context-menu'; + contextMenuEl.className = 'hidden'; + contextMenuEl.setAttribute('role', 'menu'); + contextMenuEl.addEventListener('click', (e) => e.stopPropagation()); + document.body.appendChild(contextMenuEl); + return contextMenuEl; +} + +function hideContextMenu() { + if (!contextMenuEl) return; + contextMenuEl.classList.add('hidden'); + contextMenuEl.innerHTML = ''; +} + +function showContextMenu(x, y, items = []) { + if (!items.length) { + hideContextMenu(); + return; + } + + const menu = ensureContextMenu(); + menu.innerHTML = ''; + + items.forEach((item) => { + if (item === 'separator') { + const sep = document.createElement('div'); + sep.className = 'context-menu-sep'; + menu.appendChild(sep); + return; + } + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'context-menu-item'; + btn.textContent = item.label || 'Action'; + btn.disabled = Boolean(item.disabled); + btn.setAttribute('role', 'menuitem'); + btn.addEventListener('click', () => { + hideContextMenu(); + item.action?.(); + }); + menu.appendChild(btn); + }); + + menu.classList.remove('hidden'); + menu.style.left = '0px'; + menu.style.top = '0px'; + + const pad = 6; + const menuRect = menu.getBoundingClientRect(); + const maxLeft = Math.max(pad, window.innerWidth - menuRect.width - pad); + const maxTop = Math.max(pad, window.innerHeight - menuRect.height - pad); + menu.style.left = `${clamp(x, pad, maxLeft)}px`; + menu.style.top = `${clamp(y, pad, maxTop)}px`; +} + +function openPropertiesWindow(title, rows = []) { + const htmlRows = rows + .map((row) => `
    ${htmlEscape(row.label)}: ${htmlEscape(row.value)}
    `) + .join(''); + + openAppFromDescriptor({ + title: `${title} Properties`, + width: 360, + height: 240, + html: ` +
    +
    ${htmlEscape(title)}
    +
    ${htmlRows || '
    No details available.
    '}
    +
    + `, + }); +} + +function parsePosition(btn) { + return { + x: parseInt(btn.style.left || '0', 10) || 0, + y: parseInt(btn.style.top || '0', 10) || 0, + }; +} + +function setIconPosition(btn, x, y) { + const maxX = Math.max(0, iconsEl.clientWidth - btn.offsetWidth - 6); + const maxY = Math.max(0, iconsEl.clientHeight - btn.offsetHeight - 6); + btn.style.left = `${clamp(x, 0, maxX)}px`; + btn.style.top = `${clamp(y, 0, maxY)}px`; +} + +function saveIconLayout() { + const payload = iconButtons.map((btn) => ({ + app: btn.dataset.app, + ...parsePosition(btn), + })); + localStorage.setItem(ICON_LAYOUT_KEY, JSON.stringify(payload)); +} + +function loadIconLayout() { + try { + return JSON.parse(localStorage.getItem(ICON_LAYOUT_KEY) || 'null'); + } catch { + return null; + } +} + +function resetIconLayout() { + iconButtons.forEach((btn, i) => { + setIconPosition(btn, 8, 8 + i * 106); + }); + saveIconLayout(); + setStatus('Icons reset to vertical stack'); +} + +function applyInitialLayout() { + const saved = loadIconLayout(); + if (Array.isArray(saved) && saved.length) { + const savedMap = new Map(saved.map((item) => [item.app, item])); + iconButtons.forEach((btn, i) => { + const s = savedMap.get(btn.dataset.app); + if (s) { + setIconPosition(btn, Number(s.x) || 0, Number(s.y) || 0); + } else { + setIconPosition(btn, 8, 8 + i * 106); + } + }); + return; + } + resetIconLayout(); +} + +function clearSelection() { + selectedIcons.forEach((btn) => btn.classList.remove('selected')); + selectedIcons.clear(); +} + +function setSelection(buttons) { + clearSelection(); + buttons.forEach((btn) => { + selectedIcons.add(btn); + btn.classList.add('selected'); + }); + setStatus(`${selectedIcons.size} item(s) selected`); +} + +function toggleSelection(btn) { + if (selectedIcons.has(btn)) { + selectedIcons.delete(btn); + btn.classList.remove('selected'); + } else { + selectedIcons.add(btn); + btn.classList.add('selected'); + } + if (selectedIcons.size) { + setStatus(`${selectedIcons.size} item(s) selected`); + } else { + setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); + } +} + +function toIconRect(btn) { + return { + left: btn.offsetLeft, + top: btn.offsetTop, + right: btn.offsetLeft + btn.offsetWidth, + bottom: btn.offsetTop + btn.offsetHeight, + }; +} + +function intersects(a, b) { + return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom); +} + +function ensureSelectionBox() { + if (!selectionBoxEl) { + selectionBoxEl = document.createElement('div'); + selectionBoxEl.id = 'selection-box'; + selectionBoxEl.hidden = true; + iconsEl.appendChild(selectionBoxEl); + } + return selectionBoxEl; +} + +function getWindowRect(winEl) { + return winEl.getBoundingClientRect(); +} + +function splitPath(path) { + return path.split('/').filter(Boolean); +} + +function joinPath(parts) { + return `/${parts.join('/')}` || '/'; +} + +function getNodeByPath(path) { + const parts = splitPath(path); + let node = FILE_SYSTEM; + for (const part of parts) { + if (!node.children) return null; + const next = node.children.find((child) => child.name === part); + if (!next) return null; + node = next; + } + return node; +} + +function updateFileByPath(path, updater) { + const node = getNodeByPath(path); + if (!node || node.type !== 'file') return false; + updater(node); + return true; +} + +function listFolder(path) { + const node = getNodeByPath(path); + if (!node || node.type !== 'folder') return []; + + const rank = (item) => { + if (item.type === 'folder') return 0; + if (item.fileType === 'audio') return 1; + if (item.fileType === 'image') return 2; + if (item.fileType === 'txt') return 3; + return 4; + }; + + return [...(node.children || [])].sort((a, b) => { + const ra = rank(a); + const rb = rank(b); + if (ra !== rb) return ra - rb; + return a.name.localeCompare(b.name); + }); +} + +function fileIconFor(item) { + if (item.type === 'folder') return '๐Ÿ“'; + if (item.fileType === 'image') return '๐Ÿ–ผ๏ธ'; + if (item.fileType === 'audio') return '๐ŸŽต'; + if (item.fileType === 'txt') return '๐Ÿ“„'; + return '๐Ÿ“ฆ'; +} + +function itemTypeLabel(item) { + if (item.type === 'folder') return 'File Folder'; + if (item.fileType === 'image') return 'Image File'; + if (item.fileType === 'audio') return 'Audio File'; + if (item.fileType === 'txt') return 'Text Document'; + return 'File'; +} + +function itemSizeLabel(item) { + if (item.type === 'folder') return `${(item.children || []).length} item(s)`; + const raw = String(item.content || '').length; + const kb = Math.max(1, Math.round(raw / 40)); + return `${kb} KB`; +} + +function itemModifiedLabel(item) { + const seed = item.name.length * 37; + const month = ((seed % 12) + 1).toString().padStart(2, '0'); + const day = ((seed % 27) + 1).toString().padStart(2, '0'); + return `${month}/${day}/2006 3:${(seed % 60).toString().padStart(2, '0')} PM`; +} + +function renderExplorerDetails(path, selectedItem) { + return selectedItem ? ` +
    ${selectedItem.name}
    +
    Type: ${itemTypeLabel(selectedItem)}
    +
    Size: ${itemSizeLabel(selectedItem)}
    +
    Modified: ${itemModifiedLabel(selectedItem)}
    +
    Location: ${path === '/' ? 'My Computer' : path}
    + ` : ` +
    No item selected
    +
    Select a file or folder to view details.
    + `; +} + +function renderExplorer(path, selectedItem, nav, selectedAll = false) { + const items = listFolder(path); + const breadcrumb = path === '/' ? 'My Computer' : `My Computer > ${splitPath(path).join(' > ')}`; + + const list = items.map((item) => { + const encodedName = encodeURIComponent(item.name); + const selected = selectedAll || (selectedItem && selectedItem.name === item.name && selectedItem.type === item.type); + return ` + + `; + }).join(''); + + const detailsHtml = selectedAll + ? `
    ${items.length} item(s) selected
    Press Enter to open the first selected item.
    ` + : renderExplorerDetails(path, selectedItem); + + return ` +
    +
    + + + + +
    ${breadcrumb}
    +
    +
    + +
    +
    + ${list || '
    This folder is empty.
    '} +
    + +
    +
    +
    + `; +} + +function renderNotepadApp(filePath, fileName, textValue = '') { + const escapedName = String(fileName || 'Untitled').replace(/"/g, '"'); + const escapedPath = String(filePath || '').replace(/"/g, '"'); + const escapedText = String(textValue).replace(/[&<>]/g, (m) => ({ '&': '&', '<': '<', '>': '>' }[m])); + + return ` +
    +
    + + + ${escapedName} +
    + +
    + `; +} + +function wireNotepadInteractions(winEl, options = {}) { + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + const editorEl = contentEl.querySelector('.xp-note-editor'); + const filePath = options.filePath || null; + const fileName = options.fileName || 'Untitled.txt'; + + if (!editorEl) return; + + function updateDirtyTitle(isDirty) { + const titleEl = winEl.querySelector('.window-title'); + if (!titleEl) return; + titleEl.textContent = `${isDirty ? '* ' : ''}${fileName}`; + + const entry = windowsById.get(winEl.dataset.windowId || ''); + if (entry) { + entry.title = titleEl.textContent; + updateTaskbarButtons(); + } + } + + let dirty = false; + editorEl.addEventListener('input', () => { + dirty = true; + updateDirtyTitle(true); + }); + + contentEl.querySelectorAll('.xp-note-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-note-action'); + if (action === 'new') { + editorEl.value = ''; + dirty = true; + updateDirtyTitle(true); + return; + } + + if (action === 'save') { + if (filePath) { + updateFileByPath(filePath, (node) => { + node.content = editorEl.value; + }); + dirty = false; + updateDirtyTitle(false); + setStatus(`${fileName} saved`); + } + } + }); + }); + + editorEl.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { + e.preventDefault(); + contentEl.querySelector('[data-note-action="save"]')?.click(); + } + }); + +} + +function openNotepadFile(filePath, fileName, textValue = '') { + const winEl = openAppFromDescriptor({ + title: fileName || 'Notepad', + html: renderNotepadApp(filePath, fileName, textValue), + }); + if (winEl) wireNotepadInteractions(winEl, { filePath, fileName }); +} + +function renderPaintApp() { + return ` +
    +
    + + + + + + + +
    +
    + +
    +
    + `; +} + +function wirePaintInteractions(winEl) { + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + const canvas = contentEl.querySelector('.xp-paint-canvas'); + const ctx = canvas?.getContext('2d'); + const toolEl = contentEl.querySelector('.xp-paint-tool'); + const colorEl = contentEl.querySelector('.xp-paint-color'); + const sizeEl = contentEl.querySelector('.xp-paint-size'); + if (!canvas || !ctx || !toolEl || !colorEl || !sizeEl) return; + + const undoStack = []; + const redoStack = []; + + function snapshot() { + try { + return canvas.toDataURL('image/png'); + } catch { + return null; + } + } + + function pushUndoState() { + const shot = snapshot(); + if (!shot) return; + undoStack.push(shot); + if (undoStack.length > 30) undoStack.shift(); + redoStack.length = 0; + } + + function restoreSnapshot(dataUrl) { + if (!dataUrl) return; + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + }; + img.src = dataUrl; + } + + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + let drawing = false; + let lastX = 0; + let lastY = 0; + + function applyStrokeStyle() { + const tool = toolEl.value; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.lineWidth = Number(sizeEl.value || 4); + if (tool === 'eraser') { + ctx.strokeStyle = '#ffffff'; + } else { + ctx.strokeStyle = colorEl.value || '#d13f8c'; + } + } + + function pointFromEvent(e) { + const rect = canvas.getBoundingClientRect(); + return { + x: (e.clientX - rect.left) * (canvas.width / rect.width), + y: (e.clientY - rect.top) * (canvas.height / rect.height), + }; + } + + canvas.addEventListener('pointerdown', (e) => { + e.preventDefault(); + pushUndoState(); + drawing = true; + const p = pointFromEvent(e); + lastX = p.x; + lastY = p.y; + applyStrokeStyle(); + canvas.setPointerCapture(e.pointerId); + }); + + canvas.addEventListener('pointermove', (e) => { + if (!drawing) return; + const p = pointFromEvent(e); + applyStrokeStyle(); + ctx.beginPath(); + ctx.moveTo(lastX, lastY); + ctx.lineTo(p.x, p.y); + ctx.stroke(); + lastX = p.x; + lastY = p.y; + }); + + function stopDrawing() { + if (!drawing) return; + drawing = false; + } + + canvas.addEventListener('pointerup', stopDrawing); + canvas.addEventListener('pointercancel', stopDrawing); + canvas.addEventListener('pointerleave', stopDrawing); + + contentEl.querySelectorAll('.xp-paint-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-paint-action'); + + if (action === 'undo') { + if (!undoStack.length) return; + const current = snapshot(); + if (current) redoStack.push(current); + const prev = undoStack.pop(); + restoreSnapshot(prev); + return; + } + + if (action === 'redo') { + if (!redoStack.length) return; + const current = snapshot(); + if (current) undoStack.push(current); + const next = redoStack.pop(); + restoreSnapshot(next); + return; + } + + if (action === 'clear') { + pushUndoState(); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + return; + } + + if (action === 'save') { + const a = document.createElement('a'); + a.href = canvas.toDataURL('image/png'); + a.download = `paint-${Date.now()}.png`; + a.click(); + } + }); + }); +} + +function openPaintApp() { + const winEl = openAppFromDescriptor({ + title: 'Paint', + html: renderPaintApp(), + }); + if (winEl) wirePaintInteractions(winEl); +} + +async function renderIeApp(startUrl = RETRO_WEB_HOME) { + await loadRetroWebIndex(); + + return ` +
    +
    + + + + +
    +
    + Address + + +
    +
    Loading...
    +
    + `; +} + +function wrapRetroShell(page, url) { + const stamp = Math.floor(Math.abs(url.split('').reduce((a, c) => a + c.charCodeAt(0), 0)) % 9000) + 1000; + return ` +
    + โ˜… Welcome to ${htmlEscape(page.title || url)} โ˜… +
    Visitors: ${stamp} ยท Best viewed at 800x600 ยท Netscape/IE compatible
    +
    ${page.html}
    + +
    + `; +} + +function wireIeInteractions(winEl, startUrl = RETRO_WEB_HOME) { + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + const root = contentEl.querySelector('.xp-ie'); + const addr = contentEl.querySelector('.xp-ie-address'); + const body = contentEl.querySelector('.xp-ie-content'); + const form = contentEl.querySelector('.xp-ie-address-row'); + if (!root || !addr || !body || !form) return; + + const backStack = []; + const forwardStack = []; + let currentUrl = normalizeRetroUrl(startUrl); + + function setTitle(url, page) { + const titleEl = winEl.querySelector('.window-title'); + if (titleEl) { + titleEl.textContent = `${page?.title || 'Internet Explorer'} - Internet Explorer`; + } + + const entry = windowsById.get(winEl.dataset.windowId || ''); + if (entry) { + entry.title = `IE: ${page?.title || url}`; + updateTaskbarButtons(); + } + } + + function updateNavButtons() { + contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => { + const action = btn.getAttribute('data-ie-action'); + if (action === 'back') btn.disabled = backStack.length === 0; + if (action === 'forward') btn.disabled = forwardStack.length === 0; + }); + } + + async function renderPage(url, pushHistory = false) { + const normalized = normalizeRetroUrl(url); + if (pushHistory && normalized !== currentUrl) { + backStack.push(currentUrl); + forwardStack.length = 0; + } + + currentUrl = normalized; + root.dataset.url = currentUrl; + addr.value = currentUrl; + body.innerHTML = '
    Dialing up... Loading retro page...
    '; + + const page = await getRetroPage(currentUrl); + if (!page) { + body.innerHTML = ` +
    +

    The page cannot be displayed

    +

    URL: ${htmlEscape(currentUrl)}

    +

    Try home://portal or run a search.

    +
    + `; + setTitle(currentUrl, { title: 'Cannot display page' }); + } else { + body.innerHTML = wrapRetroShell(page, currentUrl); + setTitle(currentUrl, page); + } + + updateNavButtons(); + + body.querySelectorAll('a[href]').forEach((a) => { + a.addEventListener('click', (e) => { + e.preventDefault(); + const href = a.getAttribute('href'); + if (href) renderPage(href, true); + }); + }); + } + + contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + const action = btn.getAttribute('data-ie-action'); + if (action === 'home') { + renderPage(RETRO_WEB_HOME, true); + return; + } + if (action === 'refresh') { + renderPage(currentUrl, false); + return; + } + if (action === 'back' && backStack.length) { + forwardStack.push(currentUrl); + const prev = backStack.pop(); + if (prev) renderPage(prev, false); + return; + } + if (action === 'forward' && forwardStack.length) { + backStack.push(currentUrl); + const next = forwardStack.pop(); + if (next) renderPage(next, false); + } + }); + }); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + renderPage(addr.value, true); + }); + + renderPage(currentUrl, false); +} + +async function openIeApp() { + const html = await renderIeApp(RETRO_WEB_HOME); + const winEl = openAppFromDescriptor({ + title: 'Internet Explorer', + html, + }); + if (winEl) wireIeInteractions(winEl, RETRO_WEB_HOME); +} + +function renderImageViewer(fileName, imageUrl) { + const safeName = htmlEscape(fileName || 'Image'); + const safeUrl = htmlEscape(imageUrl || ''); + return ` +
    +
    + + + + + ${safeName} +
    +
    + ${safeName} +
    +
    + `; +} + +function wireImageViewerInteractions(winEl) { + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + const img = contentEl.querySelector('.xp-image-content'); + const stage = contentEl.querySelector('.xp-image-stage'); + if (!img || !stage) return; + + let scale = 1; + let fitMode = true; + + function applyScale() { + if (fitMode) { + img.style.maxWidth = '100%'; + img.style.maxHeight = '100%'; + img.style.width = 'auto'; + img.style.height = 'auto'; + img.style.transform = 'none'; + return; + } + + img.style.maxWidth = 'none'; + img.style.maxHeight = 'none'; + img.style.width = 'auto'; + img.style.height = 'auto'; + img.style.transformOrigin = 'center center'; + img.style.transform = `scale(${scale})`; + } + + contentEl.querySelectorAll('.xp-image-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-img-action'); + + if (action === 'fit') { + fitMode = true; + applyScale(); + return; + } + + if (action === 'actual') { + fitMode = false; + scale = 1; + applyScale(); + return; + } + + if (action === 'zoom-in') { + fitMode = false; + scale = Math.min(6, scale + 0.2); + applyScale(); + return; + } + + if (action === 'zoom-out') { + fitMode = false; + scale = Math.max(0.2, scale - 0.2); + applyScale(); + } + }); + }); + + img.addEventListener('dblclick', () => { + fitMode = !fitMode; + if (!fitMode) scale = 1; + applyScale(); + }); + + applyScale(); +} + +function openImageViewer(fileName, imageUrl) { + const winEl = openAppFromDescriptor({ + title: fileName || 'Image Viewer', + html: renderImageViewer(fileName, imageUrl), + }); + if (winEl) wireImageViewerInteractions(winEl); +} + +function renderAudioPlayer(fileName, audioUrl) { + const safeName = htmlEscape(fileName || 'Track'); + const safeUrl = htmlEscape(audioUrl || ''); + return ` +
    +
    + +
    +
    ${safeName} โ€ข ${safeName} โ€ข ${safeName}
    +
    +
    +
    + + + + +
    +
    00:00 / 00:00
    + +
    + `; +} + +function wireAudioPlayerInteractions(winEl) { + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + const audio = contentEl.querySelector('.xp-audio-element'); + const seek = contentEl.querySelector('.xp-audio-seek'); + const timeEl = contentEl.querySelector('.xp-audio-time'); + if (!audio || !seek || !timeEl) return; + + function fmt(sec) { + if (!Number.isFinite(sec) || sec < 0) return '00:00'; + const m = Math.floor(sec / 60).toString().padStart(2, '0'); + const s = Math.floor(sec % 60).toString().padStart(2, '0'); + return `${m}:${s}`; + } + + function updateTime() { + const dur = Number.isFinite(audio.duration) ? audio.duration : 0; + const cur = Number.isFinite(audio.currentTime) ? audio.currentTime : 0; + const pct = dur > 0 ? Math.round((cur / dur) * 1000) : 0; + seek.value = String(pct); + timeEl.textContent = `${fmt(cur)} / ${fmt(dur)}`; + } + + let seeking = false; + seek.addEventListener('input', () => { + seeking = true; + const dur = Number.isFinite(audio.duration) ? audio.duration : 0; + const pct = Number(seek.value) / 1000; + if (dur > 0) audio.currentTime = pct * dur; + updateTime(); + }); + seek.addEventListener('change', () => { + seeking = false; + }); + + contentEl.querySelectorAll('.xp-audio-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + const action = btn.getAttribute('data-audio-action'); + if (action === 'play') { + try { await audio.play(); } catch {} + return; + } + if (action === 'pause') { + audio.pause(); + return; + } + if (action === 'stop') { + audio.pause(); + audio.currentTime = 0; + updateTime(); + } + }); + }); + + audio.addEventListener('timeupdate', () => { + if (!seeking) updateTime(); + }); + audio.addEventListener('loadedmetadata', updateTime); + audio.addEventListener('ended', () => { + audio.currentTime = 0; + updateTime(); + }); + + updateTime(); +} + +function openAudioPlayer(fileName, audioUrl) { + const winEl = openAppFromDescriptor({ + title: `Winamp - ${fileName || 'Track'}`, + html: renderAudioPlayer(fileName, audioUrl), + width: 420, + height: 180, + }); + if (winEl) wireAudioPlayerInteractions(winEl); +} + +function openVirtualFile(item, fullPath) { + if (item.fileType === 'txt') { + openNotepadFile(fullPath || null, item.name, String(item.content || '')); + return; + } + + if (item.fileType === 'image') { + openImageViewer(item.name, item.content); + return; + } + + if (item.fileType === 'audio') { + openAudioPlayer(item.name, item.content); + return; + } + + openAppFromDescriptor({ + title: item.name, + html: `

    Cannot open this file type.

    `, + }); +} + +function wireExplorerInteractions(winEl, startPath) { + const initialPath = startPath; + let currentPath = startPath; + let selectedKey = null; + let selectedAll = false; + const backStack = []; + const forwardStack = []; + + const contentEl = winEl.querySelector('.window-content'); + if (!contentEl) return; + + function folderItems() { + return listFolder(currentPath); + } + + function selectedItemFromKey() { + if (!selectedKey) return null; + const [type, encoded] = selectedKey.split('|'); + const name = decodeURIComponent(encoded || ''); + const folder = getNodeByPath(currentPath); + if (!folder || !folder.children) return null; + return folder.children.find((child) => child.type === type && child.name === name) || null; + } + + function firstSelectableKey() { + const items = folderItems(); + if (!items.length) return null; + return `${items[0].type}|${encodeURIComponent(items[0].name)}`; + } + + function openItemFromKey(key) { + if (!key) return; + const [type, encoded] = key.split('|'); + const name = decodeURIComponent(encoded || ''); + const folder = getNodeByPath(currentPath); + if (!folder || !folder.children) return; + const item = folder.children.find((child) => child.type === type && child.name === name); + if (!item) return; + + const fullPath = joinPath([...splitPath(currentPath), item.name]); + + if (item.type === 'folder') { + navigateTo(fullPath); + return; + } + + openVirtualFile(item, fullPath); + } + + function selectByIndex(index) { + const items = folderItems(); + if (!items.length) return; + const i = clamp(index, 0, items.length - 1); + selectedAll = false; + selectedKey = `${items[i].type}|${encodeURIComponent(items[i].name)}`; + rerender(); + } + + function moveSelection(delta) { + const items = folderItems(); + if (!items.length) return; + + if (!selectedKey || selectedAll) { + selectByIndex(delta > 0 ? 0 : items.length - 1); + return; + } + + const currentIndex = items.findIndex((it) => `${it.type}|${encodeURIComponent(it.name)}` === selectedKey); + if (currentIndex === -1) { + selectByIndex(0); + return; + } + + selectByIndex(currentIndex + delta); + } + + function navigateTo(path, pushHistory = true) { + const node = getNodeByPath(path); + if (!node || node.type !== 'folder') return; + if (path === currentPath) return; + + if (pushHistory) { + backStack.push(currentPath); + forwardStack.length = 0; + } + + currentPath = path; + selectedKey = null; + selectedAll = false; + rerender(); + } + + function rerender() { + const selectedItem = selectedAll ? null : selectedItemFromKey(); + contentEl.innerHTML = renderExplorer(currentPath, selectedItem, { + canBack: backStack.length > 0, + canForward: forwardStack.length > 0, + }, selectedAll); + + const titleEl = winEl.querySelector('.window-title'); + if (titleEl) { + if (initialPath === '/Recycle Bin') { + titleEl.textContent = 'Recycle Bin'; + } else { + titleEl.textContent = currentPath === '/' ? 'My Computer' : splitPath(currentPath).slice(-1)[0] || 'Explorer'; + } + } + + contentEl.querySelectorAll('.xp-nav-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-action'); + + if (action === 'back' && backStack.length) { + forwardStack.push(currentPath); + currentPath = backStack.pop() || '/'; + selectedKey = null; + selectedAll = false; + rerender(); + return; + } + + if (action === 'forward' && forwardStack.length) { + backStack.push(currentPath); + currentPath = forwardStack.pop() || '/'; + selectedKey = null; + selectedAll = false; + rerender(); + return; + } + + if (action === 'root') { + navigateTo('/'); + return; + } + + if (action === 'up') { + const parts = splitPath(currentPath); + if (!parts.length) return; + parts.pop(); + navigateTo(joinPath(parts)); + } + }); + }); + + contentEl.querySelectorAll('.xp-side-link').forEach((btn) => { + btn.addEventListener('click', () => { + const jump = btn.getAttribute('data-jump') || '/'; + navigateTo(jump); + }); + }); + + contentEl.querySelectorAll('.xp-file-item').forEach((btn) => { + btn.addEventListener('click', () => { + const kind = btn.getAttribute('data-kind'); + const encoded = btn.getAttribute('data-name'); + if (!kind || !encoded) return; + selectedAll = false; + selectedKey = `${kind}|${encoded}`; + + contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected')); + btn.classList.add('selected'); + + const folder = getNodeByPath(currentPath); + const name = decodeURIComponent(encoded); + const item = folder?.children?.find((child) => child.type === kind && child.name === name) || null; + const detailsEl = contentEl.querySelector('.xp-details'); + if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item); + }); + + btn.addEventListener('dblclick', () => { + const encoded = btn.getAttribute('data-name'); + const kind = btn.getAttribute('data-kind'); + const name = decodeURIComponent(encoded || ''); + if (!name || !kind) return; + + const folder = getNodeByPath(currentPath); + if (!folder || !folder.children) return; + const item = folder.children.find((child) => child.name === name && child.type === kind); + if (!item) return; + + const fullPath = joinPath([...splitPath(currentPath), item.name]); + + if (item.type === 'folder') { + navigateTo(fullPath); + return; + } + + openVirtualFile(item, fullPath); + }); + + btn.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const encoded = btn.getAttribute('data-name'); + const kind = btn.getAttribute('data-kind'); + const name = decodeURIComponent(encoded || ''); + if (!name || !kind) return; + + const folder = getNodeByPath(currentPath); + if (!folder || !folder.children) return; + const item = folder.children.find((child) => child.name === name && child.type === kind); + if (!item) return; + + selectedAll = false; + selectedKey = `${kind}|${encodeURIComponent(name)}`; + contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected')); + btn.classList.add('selected'); + const detailsEl = contentEl.querySelector('.xp-details'); + if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item); + + const fullPath = joinPath([...splitPath(currentPath), item.name]); + const menuItems = [ + { + label: 'Open', + action: () => { + if (item.type === 'folder') navigateTo(fullPath); + else openVirtualFile(item, fullPath); + }, + }, + item.type === 'folder' + ? { + label: 'Open in New Window', + action: () => openExplorerAtPath(fullPath, item.name), + } + : { + label: 'Open Parent Folder', + action: () => navigateTo(currentPath), + }, + 'separator', + { + label: 'Properties', + action: () => { + openPropertiesWindow(item.name, [ + { label: 'Type', value: itemTypeLabel(item) }, + { label: 'Path', value: fullPath }, + { label: 'Size', value: itemSizeLabel(item) }, + { label: 'Modified', value: itemModifiedLabel(item) }, + ]); + }, + }, + ]; + + showContextMenu(e.clientX, e.clientY, menuItems); + }); + }); + } + + if (!getNodeByPath(currentPath)) currentPath = '/'; + rerender(); + + winEl.addEventListener('keydown', (e) => { + const activeEl = document.activeElement; + const isTyping = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA'); + if (isTyping) return; + + if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) { + e.preventDefault(); + const items = folderItems(); + if (!items.length) return; + selectedAll = true; + selectedKey = firstSelectableKey(); + rerender(); + return; + } + + if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { + e.preventDefault(); + moveSelection(1); + return; + } + + if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { + e.preventDefault(); + moveSelection(-1); + return; + } + + if (e.key === 'Enter') { + e.preventDefault(); + openItemFromKey(selectedKey || firstSelectableKey()); + return; + } + + if (e.key === 'Backspace') { + e.preventDefault(); + const parts = splitPath(currentPath); + if (!parts.length) return; + parts.pop(); + navigateTo(joinPath(parts)); + } + }); +} + +function setWindowPosition(winEl, left, top) { + const maxLeft = Math.max(0, window.innerWidth - winEl.offsetWidth - 8); + const maxTop = Math.max(36, window.innerHeight - winEl.offsetHeight - 40); + winEl.style.left = `${clamp(left, 0, maxLeft)}px`; + winEl.style.top = `${clamp(top, 32, maxTop)}px`; + winEl.style.transform = 'none'; +} + +function updateTaskbarButtons() { + windowsById.forEach((entry) => { + const { taskBtn, winEl, title } = entry; + taskBtn.textContent = title; + taskBtn.classList.toggle('active', !winEl.classList.contains('hidden') && entry.active); + }); +} + +function activateWindow(id) { + const target = windowsById.get(id); + if (!target) return; + + windowsById.forEach((entry) => { + entry.active = false; + entry.winEl.classList.remove('active'); + }); + + target.active = true; + target.winEl.classList.add('active'); + showDesktopBtn?.classList.remove('active'); + topWindowZ += 1; + target.winEl.style.zIndex = String(topWindowZ); + updateTaskbarButtons(); +} + +function bringToFront(winEl) { + const id = winEl.dataset.windowId; + if (!id) return; + activateWindow(id); + winEl.focus(); +} + +function minimizeWindow(id) { + const entry = windowsById.get(id); + if (!entry) return; + + entry.winEl.classList.add('hidden'); + entry.active = false; + + const nextVisible = Array.from(windowsById.values()).find((w) => !w.winEl.classList.contains('hidden')); + if (nextVisible) { + activateWindow(nextVisible.id); + } + + updateTaskbarButtons(); + setStatus(`${entry.title} minimized`); +} + +function restoreWindow(id) { + const entry = windowsById.get(id); + if (!entry) return; + + entry.winEl.classList.remove('hidden'); + activateWindow(id); + setStatus(`${entry.title} restored`); +} + +function toggleShowDesktop() { + const entries = Array.from(windowsById.values()); + if (!entries.length) return; + + const hasVisible = entries.some((entry) => !entry.winEl.classList.contains('hidden')); + + if (hasVisible) { + entries.forEach((entry) => { + entry.winEl.classList.add('hidden'); + entry.active = false; + }); + showDesktopBtn?.classList.add('active'); + updateTaskbarButtons(); + setStatus('Showing desktop'); + return; + } + + const lastFocused = entries.sort((a, b) => { + const za = parseInt(a.winEl.style.zIndex || '0', 10); + const zb = parseInt(b.winEl.style.zIndex || '0', 10); + return zb - za; + })[0]; + + entries.forEach((entry) => entry.winEl.classList.remove('hidden')); + showDesktopBtn?.classList.remove('active'); + if (lastFocused) activateWindow(lastFocused.id); + else updateTaskbarButtons(); + setStatus('Windows restored'); +} + +function closeWindow(id) { + const entry = windowsById.get(id); + if (!entry) return; + + entry.winEl.remove(); + entry.taskBtn.remove(); + windowsById.delete(id); + + if (windowsById.size) { + const newest = Array.from(windowsById.values()).sort((a, b) => { + const za = parseInt(a.winEl.style.zIndex || '0', 10); + const zb = parseInt(b.winEl.style.zIndex || '0', 10); + return zb - za; + })[0]; + if (newest) activateWindow(newest.id); + } + + updateTaskbarButtons(); + setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); +} + +function bindWindowDrag(winEl) { + const titlebar = winEl.querySelector('.window-titlebar'); + const closeBtn = winEl.querySelector('.window-close'); + const minimizeBtn = winEl.querySelector('.window-minimize'); + const resizeHandle = winEl.querySelector('.window-resize-handle'); + const id = winEl.dataset.windowId; + if (!titlebar || !closeBtn || !minimizeBtn || !id) return; + + winEl.addEventListener('pointerdown', () => bringToFront(winEl)); + + titlebar.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + if (e.target === closeBtn || e.target === minimizeBtn) return; + + bringToFront(winEl); + + const rect = getWindowRect(winEl); + setWindowPosition(winEl, rect.left, rect.top); + + const startLeft = rect.left; + const startTop = rect.top; + const startX = e.clientX; + const startY = e.clientY; + let moved = false; + + winEl.classList.add('dragging'); + titlebar.setPointerCapture(e.pointerId); + + const onMove = (ev) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + if (!moved && Math.hypot(dx, dy) > 2) moved = true; + if (!moved) return; + setWindowPosition(winEl, startLeft + dx, startTop + dy); + }; + + const onUp = () => { + winEl.classList.remove('dragging'); + titlebar.removeEventListener('pointermove', onMove); + titlebar.removeEventListener('pointerup', onUp); + titlebar.removeEventListener('pointercancel', onUp); + }; + + titlebar.addEventListener('pointermove', onMove); + titlebar.addEventListener('pointerup', onUp); + titlebar.addEventListener('pointercancel', onUp); + }); + + if (resizeHandle) { + resizeHandle.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + e.stopPropagation(); + bringToFront(winEl); + + const rect = getWindowRect(winEl); + const startX = e.clientX; + const startY = e.clientY; + const startW = rect.width; + const startH = rect.height; + + winEl.classList.add('resizing'); + resizeHandle.setPointerCapture(e.pointerId); + + const onMove = (ev) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + const minW = 360; + const minH = 240; + const maxW = Math.max(minW, window.innerWidth - rect.left - 8); + const maxH = Math.max(minH, window.innerHeight - rect.top - 40); + const nextW = clamp(startW + dx, minW, maxW); + const nextH = clamp(startH + dy, minH, maxH); + winEl.style.width = `${nextW}px`; + winEl.style.height = `${nextH}px`; + }; + + const onUp = () => { + winEl.classList.remove('resizing'); + resizeHandle.removeEventListener('pointermove', onMove); + resizeHandle.removeEventListener('pointerup', onUp); + resizeHandle.removeEventListener('pointercancel', onUp); + }; + + resizeHandle.addEventListener('pointermove', onMove); + resizeHandle.addEventListener('pointerup', onUp); + resizeHandle.addEventListener('pointercancel', onUp); + }); + } + + minimizeBtn.addEventListener('click', () => minimizeWindow(id)); + closeBtn.addEventListener('click', () => closeWindow(id)); +} + +function createTaskbarButton(id, title) { + const btn = document.createElement('button'); + btn.className = 'taskbar-btn'; + btn.type = 'button'; + btn.textContent = title; + btn.title = title; + btn.addEventListener('click', () => { + const entry = windowsById.get(id); + if (!entry) return; + + if (entry.winEl.classList.contains('hidden')) { + restoreWindow(id); + return; + } + + if (entry.active) { + minimizeWindow(id); + } else { + activateWindow(id); + setStatus(`${entry.title} focused`); + } + }); + taskbarWindowsEl.appendChild(btn); + return btn; +} + +function addWindowControls(winEl) { + const titlebar = winEl.querySelector('.window-titlebar'); + const closeBtn = winEl.querySelector('.window-close'); + if (!titlebar || !closeBtn) return; + + const controls = document.createElement('div'); + controls.className = 'window-controls'; + + const minimizeBtn = document.createElement('button'); + minimizeBtn.className = 'window-minimize'; + minimizeBtn.type = 'button'; + minimizeBtn.textContent = 'โ€”'; + minimizeBtn.setAttribute('aria-label', 'minimize'); + + closeBtn.classList.add('window-close'); + + controls.appendChild(minimizeBtn); + controls.appendChild(closeBtn); + titlebar.appendChild(controls); + + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'window-resize-handle'; + resizeHandle.setAttribute('aria-hidden', 'true'); + winEl.appendChild(resizeHandle); +} + +function openAppFromDescriptor(descriptor) { + if (!descriptor || !windowTemplate) return null; + + hideStartMenu(); + + windowSeq += 1; + const id = `w${windowSeq}`; + + const winEl = windowTemplate.cloneNode(true); + winEl.id = ''; + winEl.dataset.windowId = id; + winEl.classList.remove('hidden'); + + const titleEl = winEl.querySelector('.window-title'); + const contentEl = winEl.querySelector('.window-content'); + + if (titleEl) titleEl.textContent = descriptor.title || 'Window'; + if (contentEl) contentEl.innerHTML = descriptor.html || '

    Empty window

    '; + + addWindowControls(winEl); + document.body.appendChild(winEl); + + const offset = ((windowSeq - 1) % 8) * 22; + const left = (window.innerWidth - winEl.offsetWidth) / 2 + offset; + const top = 68 + offset; + + if (descriptor.width) { + winEl.style.width = typeof descriptor.width === 'number' ? `${descriptor.width}px` : String(descriptor.width); + } + if (descriptor.height) { + winEl.style.height = typeof descriptor.height === 'number' ? `${descriptor.height}px` : String(descriptor.height); + } + + setWindowPosition(winEl, left, top); + + const taskBtn = createTaskbarButton(id, descriptor.title || 'Window'); + + windowsById.set(id, { + id, + title: descriptor.title || 'Window', + winEl, + taskBtn, + active: false, + }); + + bindWindowDrag(winEl); + activateWindow(id); + updateTaskbarButtons(); + + winEl.setAttribute('tabindex', '0'); + + winEl.focus(); + setStatus(`${descriptor.title || 'Window'} opened`); + return winEl; +} + +function openExplorerAtPath(path = '/', title = 'Explorer') { + const winEl = openAppFromDescriptor({ + title, + html: '
    Loading...
    ', + }); + if (winEl) wireExplorerInteractions(winEl, path); +} + +function openSystemPanel(kind) { + const panels = { + 'control-panel': { + title: 'Control Panel', + html: '
    โš™๏ธ Control Panel is under construction.
    Try My Documents, Music, Paint, and Notepad from Start.
    ', + }, + help: { + title: 'Help and Support', + html: '
    โ“ Need help? Double-click folders/files in Explorer, drag desktop icons, and use the tray reset/show-desktop buttons.
    ', + }, + run: { + title: 'Run', + html: '
    ๐Ÿƒ Run shortcut: open Internet, Notepad, Paint, or My Computer from Start.
    ', + }, + }; + + const panel = panels[kind]; + if (!panel) return; + openAppFromDescriptor(panel); +} + +function performSystemAction(action) { + if (action === 'logoff') { + hideStartMenu(); + clearSelection(); + setStatus('Logged off (mock)'); + return; + } + + if (action === 'shutdown') { + hideStartMenu(); + windowsById.forEach((entry) => { + entry.winEl.classList.add('hidden'); + entry.active = false; + }); + showDesktopBtn?.classList.add('active'); + updateTaskbarButtons(); + setStatus('It is now safe to turn off your imagination โœจ'); + return; + } + + openSystemPanel(action); +} + +function openApp(key) { + const app = ICON_APPS[key]; + if (!app) return; + + if (app.type === 'explorer') { + openExplorerAtPath(app.startPath || '/', app.title || 'Explorer'); + return; + } + + if (app.type === 'notepad') { + openNotepadFile(null, 'Untitled.txt', ''); + return; + } + + if (app.type === 'paint') { + openPaintApp(); + return; + } + + if (app.type === 'ie') { + openIeApp(); + return; + } + + openAppFromDescriptor(app); +} + +function bindIconDrag(btn) { + btn.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + e.stopPropagation(); + + if (e.shiftKey) { + toggleSelection(btn); + } else if (!selectedIcons.has(btn)) { + setSelection([btn]); + } + + const dragSet = selectedIcons.size ? Array.from(selectedIcons) : [btn]; + const starts = dragSet.map((icon) => ({ icon, ...parsePosition(icon) })); + const startX = e.clientX; + const startY = e.clientY; + let moved = false; + + dragSet.forEach((icon) => icon.classList.add('dragging')); + btn.setPointerCapture(e.pointerId); + + const onMove = (ev) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + if (!moved && Math.hypot(dx, dy) > 3) moved = true; + if (!moved) return; + + starts.forEach(({ icon, x, y }) => setIconPosition(icon, x + dx, y + dy)); + setStatus(`Moving ${dragSet.length} item(s)`); + }; + + const onUp = () => { + dragSet.forEach((icon) => icon.classList.remove('dragging')); + btn.removeEventListener('pointermove', onMove); + btn.removeEventListener('pointerup', onUp); + btn.removeEventListener('pointercancel', onUp); + + if (moved) { + btn.dataset.wasDragged = '1'; + saveIconLayout(); + setStatus('Desktop layout saved'); + } + }; + + btn.addEventListener('pointermove', onMove); + btn.addEventListener('pointerup', onUp); + btn.addEventListener('pointercancel', onUp); + }); + + btn.addEventListener('click', (e) => { + if (btn.dataset.wasDragged === '1') { + btn.dataset.wasDragged = '0'; + return; + } + + if (e.shiftKey) { + toggleSelection(btn); + return; + } + + if (!selectedIcons.has(btn) || selectedIcons.size !== 1) { + setSelection([btn]); + return; + } + }); + + btn.addEventListener('dblclick', (e) => { + e.stopPropagation(); + setSelection([btn]); + openApp(btn.dataset.app); + }); + + btn.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (!selectedIcons.has(btn)) { + setSelection([btn]); + } + + showContextMenu(e.clientX, e.clientY, [ + { + label: 'Open', + action: () => openApp(btn.dataset.app), + }, + { + label: 'Open All Selected', + disabled: selectedIcons.size <= 1, + action: () => { + Array.from(selectedIcons).forEach((icon) => openApp(icon.dataset.app)); + }, + }, + 'separator', + { + label: 'Reset Icon Layout', + action: () => { + clearSelection(); + localStorage.removeItem(ICON_LAYOUT_KEY); + resetIconLayout(); + }, + }, + { + label: 'Properties', + action: () => { + const appKey = btn.dataset.app || ''; + const appTitle = ICON_APPS[appKey]?.title || btn.innerText || 'Desktop Item'; + const pos = parsePosition(btn); + openPropertiesWindow(appTitle, [ + { label: 'Type', value: 'Desktop Shortcut' }, + { label: 'App Key', value: appKey || 'unknown' }, + { label: 'Position', value: `${pos.x}, ${pos.y}` }, + ]); + }, + }, + ]); + }); +} + +function showStartMenu() { + if (!startMenuEl) return; + startMenuEl.classList.remove('hidden'); + startBtn?.classList.add('active'); +} + +function hideStartMenu() { + if (!startMenuEl) return; + startMenuEl.classList.add('hidden'); + startBtn?.classList.remove('active'); +} + +function toggleStartMenu() { + if (!startMenuEl) return; + if (startMenuEl.classList.contains('hidden')) { + showStartMenu(); + } else { + hideStartMenu(); + } +} + +function bindDesktopSelection() { + const selectionBox = ensureSelectionBox(); + + iconsEl.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + if (e.target.closest('.desktop-icon')) return; + + const rect = iconsEl.getBoundingClientRect(); + const startX = clamp(e.clientX - rect.left, 0, iconsEl.clientWidth); + const startY = clamp(e.clientY - rect.top, 0, iconsEl.clientHeight); + let moved = false; + + if (!e.shiftKey) clearSelection(); + + selectionBox.hidden = false; + selectionBox.style.left = `${startX}px`; + selectionBox.style.top = `${startY}px`; + selectionBox.style.width = '0px'; + selectionBox.style.height = '0px'; + + iconsEl.setPointerCapture(e.pointerId); + + const onMove = (ev) => { + const currentX = clamp(ev.clientX - rect.left, 0, iconsEl.clientWidth); + const currentY = clamp(ev.clientY - rect.top, 0, iconsEl.clientHeight); + const left = Math.min(startX, currentX); + const top = Math.min(startY, currentY); + const width = Math.abs(currentX - startX); + const height = Math.abs(currentY - startY); + + moved = moved || width > 2 || height > 2; + + selectionBox.style.left = `${left}px`; + selectionBox.style.top = `${top}px`; + selectionBox.style.width = `${width}px`; + selectionBox.style.height = `${height}px`; + + if (!moved) return; + + const boxRect = { left, top, right: left + width, bottom: top + height }; + + iconButtons.forEach((btn) => { + const hit = intersects(boxRect, toIconRect(btn)); + if (hit) { + selectedIcons.add(btn); + btn.classList.add('selected'); + } else if (!e.shiftKey) { + selectedIcons.delete(btn); + btn.classList.remove('selected'); + } + }); + + if (selectedIcons.size) { + setStatus(`${selectedIcons.size} item(s) selected`); + } + }; + + const onUp = () => { + selectionBox.hidden = true; + iconsEl.removeEventListener('pointermove', onMove); + iconsEl.removeEventListener('pointerup', onUp); + iconsEl.removeEventListener('pointercancel', onUp); + + if (!moved && !e.shiftKey) { + clearSelection(); + setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); + } + }; + + iconsEl.addEventListener('pointermove', onMove); + iconsEl.addEventListener('pointerup', onUp); + iconsEl.addEventListener('pointercancel', onUp); + }); +} + +iconButtons.forEach(bindIconDrag); +bindDesktopSelection(); + +window.addEventListener('resize', () => { + hideContextMenu(); + + iconButtons.forEach((btn) => { + const { x, y } = parsePosition(btn); + setIconPosition(btn, x, y); + }); + + windowsById.forEach(({ winEl }) => { + if (winEl.classList.contains('hidden')) return; + const rect = getWindowRect(winEl); + setWindowPosition(winEl, rect.left, rect.top); + }); + + saveIconLayout(); +}); + +resetLayoutBtn?.addEventListener('click', () => { + clearSelection(); + localStorage.removeItem(ICON_LAYOUT_KEY); + resetIconLayout(); +}); + +startBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + hideContextMenu(); + toggleStartMenu(); +}); + +showDesktopBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + hideContextMenu(); + toggleShowDesktop(); +}); + +startMenuEl?.querySelectorAll('[data-open-app]').forEach((btn) => { + btn.addEventListener('click', () => { + const appKey = btn.getAttribute('data-open-app'); + if (appKey) openApp(appKey); + }); +}); + +startMenuEl?.querySelectorAll('[data-open-path]').forEach((btn) => { + btn.addEventListener('click', () => { + const path = btn.getAttribute('data-open-path') || '/'; + const label = btn.textContent?.replace(/^[^\w]+\s*/, '').trim() || 'Explorer'; + openExplorerAtPath(path, label); + }); +}); + +startMenuEl?.querySelectorAll('[data-system-action]').forEach((btn) => { + btn.addEventListener('click', () => { + const action = btn.getAttribute('data-system-action'); + if (action) performSystemAction(action); + }); +}); + +document.addEventListener('click', (e) => { + hideContextMenu(); + if (!startMenuEl || startMenuEl.classList.contains('hidden')) return; + if (startMenuEl.contains(e.target)) return; + if (startBtn && startBtn.contains(e.target)) return; + hideStartMenu(); +}); + +document.addEventListener('contextmenu', () => { + hideContextMenu(); +}); + +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + hideContextMenu(); + hideStartMenu(); + } +}); + +function tickClock() { + const d = new Date(); + const timeText = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const dateText = d.toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric' }); + clockEl.innerHTML = `${timeText}${dateText}`; +} + +applyInitialLayout(); +setInterval(tickClock, 1000); +tickClock(); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..b3542af --- /dev/null +++ b/site/styles.css @@ -0,0 +1,1087 @@ +:root { + --pink-sky-1: #ffdff1; + --pink-sky-2: #ffc5e6; + --pink-sky-3: #f6a8d4; + --panel: #fff7fc; + --line: #e7a8ca; + --titlebar-1: #ff9acb; + --titlebar-2: #f07cb6; + --titlebar-border: #c65b93; + --taskbar-1: #ffb3d9; + --taskbar-2: #ef8dc1; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: "Tahoma", "MS Sans Serif", "Segoe UI", Verdana, sans-serif; + font-size: 12px; + background-color: #f6a8d4; + background-image: url('https://wallpaperaccess.com/full/4810999.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + color: #4b1c39; + min-height: 100vh; +} + +#desktop { + min-height: 100vh; + display: flex; + flex-direction: column; + position: relative; +} + +#topbar { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; + padding: 5px 8px; + font-size: 13px; + border-bottom: 1px solid var(--titlebar-border); + display: flex; + align-items: center; + text-shadow: 0 1px 0 rgba(106, 25, 76, 0.45); +} + +#topbar-title { + white-space: nowrap; + letter-spacing: 0.2px; +} + +#reset-layout-btn, +#show-desktop-btn { + margin-left: 0; + width: 22px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + background: linear-gradient(#ffeef8, #e8a8cc); + color: #4c1431; + border-top: 1px solid #fff5fb; + border-left: 1px solid #fff5fb; + border-right: 1px solid #8f3e6b; + border-bottom: 1px solid #8f3e6b; + border-radius: 0; + box-shadow: inset 0 0 0 1px rgba(255, 220, 241, 0.35); + cursor: pointer; +} + +#reset-layout-btn > span { + font-size: 13px; + line-height: 1; + transform: translateY(-1px); +} + +#reset-layout-btn:hover, +#show-desktop-btn:hover { + background: linear-gradient(#fff4fb, #efb7d6); +} + +#reset-layout-btn:active, +#show-desktop-btn:active, +#show-desktop-btn.active { + border-top-color: #8f3e6b; + border-left-color: #8f3e6b; + border-right-color: #fff5fb; + border-bottom-color: #fff5fb; + background: linear-gradient(#dc95be, #f2bbd9); +} + +#reset-layout-btn:focus-visible, +#show-desktop-btn:focus-visible { + outline: 1px dotted #fff; + outline-offset: -4px; +} + +#icons { + position: relative; + padding: 16px; + flex: 1; + overflow: hidden; +} + +.desktop-icon { + position: absolute; + width: 112px; + border: none; + background: transparent; + color: #fff; + text-shadow: 0 1px 2px rgba(70, 25, 52, 0.65); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + cursor: default; + padding: 8px 6px; + border-radius: 4px; + touch-action: none; + user-select: none; +} + +.desktop-icon:hover { + background: rgba(255, 255, 255, 0.16); +} + +.desktop-icon.dragging { + cursor: grabbing; + background: rgba(255, 255, 255, 0.28); +} + +.desktop-icon .emoji { + font-size: 36px; + width: 100%; + text-align: center; + display: block; + line-height: 1; +} + +.desktop-icon span:last-child { + font-size: 12px; + text-align: center; + width: 100%; + line-height: 1.25; +} + +.desktop-icon.selected { + background: rgba(115, 185, 255, 0.34); + outline: 1px dotted rgba(255, 255, 255, 0.95); +} + +#taskbar { + background: linear-gradient(var(--taskbar-1), var(--taskbar-2)); + border-top: 1px solid #f8d3e9; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 8px; + align-items: center; + padding: 4px 6px; + color: #fff; + min-height: 34px; + position: relative; + z-index: 40; +} + +#start-btn { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; + border-top: 1px solid #ffd4ea; + border-left: 1px solid #ffd4ea; + border-right: 1px solid #9f3972; + border-bottom: 1px solid #9f3972; + border-radius: 16px; + font-weight: 700; + font-style: italic; + padding: 5px 16px; + text-transform: lowercase; + cursor: pointer; + text-shadow: 0 1px 0 rgba(94, 20, 64, 0.45); +} + +#start-btn:active, +#start-btn.active { + border-top-color: #9f3972; + border-left-color: #9f3972; + border-right-color: #ffd4ea; + border-bottom-color: #ffd4ea; + background: linear-gradient(var(--titlebar-2), var(--titlebar-1)); +} + +#taskbar-windows { + display: flex; + gap: 4px; + min-width: 0; + overflow-x: auto; + padding: 1px; +} + +.taskbar-btn { + min-width: 120px; + max-width: 180px; + background: linear-gradient(#ffeaf7, #efb5d5); + color: #5a1b3b; + border-top: 1px solid #fff6fd; + border-left: 1px solid #fff6fd; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + padding: 4px 8px; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + +.taskbar-btn.active { + background: linear-gradient(#e7a2c8, #f8d4e9); + border-top-color: #9f4a78; + border-left-color: #9f4a78; + border-right-color: #fff6fd; + border-bottom-color: #fff6fd; +} + +#taskbar-right { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +#taskbar-brand { + font-size: 11px; + font-weight: 700; + color: #ffeef9; + text-shadow: 0 1px 0 rgba(94, 20, 64, 0.45); + white-space: nowrap; +} + +#taskbar-tray-buttons { + display: inline-flex; + align-items: center; + gap: 4px; +} + +#clock { + font-size: 11px; + padding: 2px 8px; + border-top: 1px solid rgba(255, 233, 247, 0.85); + border-left: 1px solid rgba(255, 233, 247, 0.85); + border-right: 1px solid rgba(163, 69, 122, 0.8); + border-bottom: 1px solid rgba(163, 69, 122, 0.8); + background: rgba(255, 207, 232, 0.48); + min-height: 30px; + min-width: 72px; + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + line-height: 1.05; +} + +#clock .clock-time { + font-weight: 700; +} + +#clock .clock-date { + font-size: 10px; + opacity: 0.95; +} + +#start-menu { + position: absolute; + left: 8px; + bottom: 36px; + width: 420px; + border-top: 1px solid #f6bfdc; + border-left: 1px solid #f6bfdc; + border-right: 1px solid #7f2f5a; + border-bottom: 1px solid #7f2f5a; + border-radius: 7px 7px 0 0; + overflow: hidden; + background: #fff6fc; + box-shadow: 0 10px 30px rgba(90, 27, 59, 0.42); + z-index: 1200; +} + +.start-menu-top { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; + padding: 10px 14px; + font-size: 16px; + font-weight: 700; + text-shadow: 0 1px 0 rgba(94, 20, 64, 0.55); +} + +.start-menu-body { + display: grid; + grid-template-columns: 1fr 0.95fr; + min-height: 280px; +} + +.start-col { + padding: 8px 6px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.start-col-left { + background: #fff; + border-right: 1px solid #efbfd9; +} + +.start-col-right { + background: #ffe7f4; +} + +.start-item { + border: none; + background: transparent; + text-align: left; + font-size: 12px; + padding: 7px 8px; + color: #7d2756; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; +} + +.start-item.secondary { + color: #7a3a61; +} + +.start-item:hover { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; +} + +.start-menu-footer { + background: linear-gradient(var(--titlebar-2), #d868a8); + padding: 7px; + display: flex; + justify-content: flex-end; + gap: 6px; +} + +.start-footer-btn { + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + padding: 4px 9px; + font-size: 11px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.start-footer-btn:active { + border-top-color: #9f4a78; + border-left-color: #9f4a78; + border-right-color: #fff; + border-bottom-color: #fff; +} + +.app-window { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + width: min(760px, 92vw); + min-width: 360px; + min-height: 240px; + background: var(--panel); + border-top: 1px solid #ffd9ed; + border-left: 1px solid #ffd9ed; + border-right: 1px solid #a55282; + border-bottom: 1px solid #a55282; + box-shadow: 0 12px 26px rgba(112, 34, 82, 0.32); + display: flex; + flex-direction: column; +} + +.app-window.dragging { + user-select: none; +} + +.app-window.dragging .window-titlebar { + cursor: grabbing; +} + +.hidden { display: none !important; } + +.window-titlebar { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 5px 6px; + border-bottom: 1px solid var(--titlebar-border); + cursor: grab; + text-shadow: 0 1px 0 rgba(106, 25, 76, 0.45); +} + +.window-title { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; +} + +.window-title::before { + content: "โ–ฃ"; + font-size: 11px; + color: #ffe8f6; +} + +.window-controls { + display: inline-flex; + gap: 4px; +} + +.window-minimize, +.window-close { + width: 24px; + height: 20px; + border-top: 1px solid #ffe8f5; + border-left: 1px solid #ffe8f5; + border-right: 1px solid #8f3e6b; + border-bottom: 1px solid #8f3e6b; + background: linear-gradient(#ffd9ee, #e499c2); + color: #4c1431; + font-weight: 700; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.window-minimize:active, +.window-close:active { + border-top-color: #8f3e6b; + border-left-color: #8f3e6b; + border-right-color: #ffe8f5; + border-bottom-color: #ffe8f5; +} + +.window-content { + margin: 4px; + padding: 12px; + flex: 1; + min-height: 0; + overflow: auto; + background: #fff; + border-top: 1px solid #a14f7d; + border-left: 1px solid #a14f7d; + border-right: 1px solid #ffe1f1; + border-bottom: 1px solid #ffe1f1; +} + +.window-resize-handle { + position: absolute; + width: 14px; + height: 14px; + right: 1px; + bottom: 1px; + cursor: nwse-resize; + background: + linear-gradient(135deg, transparent 0 40%, #b56a95 40% 46%, transparent 46% 56%, #b56a95 56% 62%, transparent 62% 72%, #b56a95 72% 78%, transparent 78% 100%); +} + +.app-window.resizing { + user-select: none; +} + +#selection-box { + position: absolute; + border: 1px dashed rgba(255, 255, 255, 0.95); + background: rgba(130, 200, 255, 0.22); + pointer-events: none; + z-index: 20; +} + +.xp-loading { + color: #8c3b67; + padding: 10px; +} + +.xp-explorer { + display: flex; + flex-direction: column; + gap: 6px; + height: 100%; + min-height: 0; +} + +.xp-toolbar { + display: grid; + grid-template-columns: 28px 28px 28px 34px 1fr; + gap: 6px; + align-items: center; +} + +.xp-nav-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-nav-icon { + width: 28px; + height: 24px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +.xp-nav-btn:disabled { + opacity: 0.55; + cursor: default; +} + +.xp-breadcrumb { + background: #fff; + border: 1px solid #e5b4cf; + padding: 5px 8px; + font-size: 11px; + color: #7d2756; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.xp-explorer-body { + display: grid; + grid-template-columns: 170px 1fr; + gap: 8px; + flex: 1; + min-height: 0; +} + +.xp-main { + display: grid; + grid-template-columns: 1fr 210px; + gap: 8px; + min-height: 0; +} + +.xp-sidebar { + background: #fff0f9; + border: 1px solid #e7a8ca; + padding: 6px; + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + overflow: auto; +} + +.xp-side-title { + font-weight: 700; + color: #8c3b67; + margin-bottom: 4px; +} + +.xp-side-link { + border: none; + background: transparent; + text-align: left; + color: #7d2756; + font-size: 11px; + padding: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; +} + +.xp-side-link:hover { + background: #ffdff1; +} + +.xp-files { + min-height: 0; + border: 1px solid #e7a8ca; + background: #fff; + padding: 6px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + grid-auto-rows: 96px; + align-content: start; + gap: 6px; + overflow: auto; +} + +.xp-file-item { + border: 1px solid transparent; + background: transparent; + text-align: center; + color: #5f2244; + padding: 6px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + cursor: default; + width: 92px; + height: 92px; + border-radius: 0; +} + +.xp-file-item:hover { + border-color: #f0c3dc; + background: #fff4fb; +} + +.xp-file-item.selected { + border-color: #d88db8; + background: #ffe8f5; +} + +.xp-file-icon { + font-size: 26px; + line-height: 1; +} + +.xp-file-label { + font-size: 11px; + word-break: break-word; + width: 100%; + text-align: center; +} + +.xp-empty { + color: #8c3b67; + padding: 8px; + font-size: 11px; +} + +.xp-details { + border: 1px solid #e7a8ca; + background: #fff0f9; + padding: 8px; + font-size: 11px; + color: #6e2b52; + display: flex; + flex-direction: column; + gap: 6px; + min-height: 0; + overflow: auto; +} + +.xp-details-title { + font-weight: 700; + color: #8c3b67; + margin-bottom: 4px; +} + +.xp-notepad { + display: flex; + flex-direction: column; + gap: 6px; + height: 100%; +} + +.xp-notepad-toolbar { + display: flex; + align-items: center; + gap: 6px; + border: 1px solid #e5b4cf; + background: #fff0f9; + padding: 5px; +} + +.xp-note-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-note-file { + margin-left: auto; + font-size: 11px; + color: #7a3a61; +} + +.xp-note-editor { + width: 100%; + min-height: 360px; + height: 100%; + resize: none; + border: 1px solid #d58cb8; + background: #fff; + color: #3d1530; + font-family: "Lucida Console", "Courier New", monospace; + font-size: 12px; + line-height: 1.35; + padding: 10px; +} + +.xp-paint { + display: flex; + flex-direction: column; + gap: 6px; + height: 100%; +} + +.xp-paint-toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + border: 1px solid #e5b4cf; + background: #fff0f9; + padding: 5px; + font-size: 11px; + color: #7a3a61; +} + +.xp-paint-toolbar label { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.xp-paint-tool, +.xp-paint-size, +.xp-paint-color { + font-size: 11px; +} + +input[type="range"] { + accent-color: #d24b95; +} + +.xp-paint-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-paint-stage { + border: 1px solid #d58cb8; + background: #fff; + overflow: auto; + flex: 1; + min-height: 160px; +} + +.xp-paint-canvas { + display: block; + width: 100%; + height: 100%; + background: #fff; + cursor: crosshair; +} + +.xp-ie { + display: flex; + flex-direction: column; + gap: 6px; + height: 100%; +} + +.xp-ie-toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + border: 1px solid #e5b4cf; + background: #fff0f9; + padding: 5px; +} + +.xp-ie-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-ie-btn:disabled { + opacity: 0.55; + cursor: default; +} + +.xp-ie-address-row { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 6px; + align-items: center; + border: 1px solid #e5b4cf; + background: #fff0f9; + padding: 5px; + margin: 0; +} + +.xp-ie-address-row span { + color: #7a3a61; + font-size: 11px; +} + +.xp-ie-address { + border: 1px solid #d58cb8; + padding: 5px 7px; + font-size: 12px; + color: #5a1b3b; + background: #fff; +} + +.xp-ie-content { + border: 1px solid #d58cb8; + background: #fff; + min-height: 220px; + padding: 12px; + overflow: auto; + flex: 1; +} + +.ie-loading { + color: #8c3b67; +} + +.ie-error h3 { + margin-top: 0; + color: #8d1f54; +} + +.ie-page h2 { + margin-top: 0; + color: #8c3b67; +} + +.ie-page a { + color: #b32972; +} + +.ie-page a:hover { + color: #7f1f54; +} + +.ie-results li { + margin-bottom: 10px; +} + +.ie-results li div { + color: #8d5780; + font-size: 11px; +} + +.xp-image-viewer { + display: flex; + flex-direction: column; + gap: 6px; + height: 100%; +} + +.xp-image-toolbar { + display: flex; + align-items: center; + gap: 6px; + border: 1px solid #e5b4cf; + background: #fff0f9; + padding: 5px; +} + +.xp-image-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #9f4a78; + border-bottom: 1px solid #9f4a78; + background: linear-gradient(#fff3fb, #f2c0de); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-image-title { + margin-left: auto; + color: #7a3a61; + font-size: 11px; +} + +.xp-image-stage { + border: 1px solid #d58cb8; + background: #fff; + flex: 1; + min-height: 220px; + display: flex; + align-items: center; + justify-content: center; + overflow: auto; +} + +.xp-image-content { + display: block; + max-width: 100%; + max-height: 100%; + image-rendering: auto; +} + +.xp-audio-player { + display: flex; + flex-direction: column; + gap: 8px; + height: 100%; + background: linear-gradient(#f7d9ec, #e9acd0); + border: 1px solid #a25a85; + padding: 8px; +} + +.xp-audio-top { + display: grid; + grid-template-columns: 44px 1fr; + gap: 8px; + align-items: center; +} + +.xp-audio-logo { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #fff; + background: linear-gradient(#bf3f8a, #7e1f54); + border: 1px solid #5f123d; +} + +.xp-audio-track-wrap { + border: 1px solid #8f4a72; + background: #220b17; + color: #8dff9e; + font-family: "Lucida Console", "Courier New", monospace; + font-size: 11px; + overflow: hidden; + white-space: nowrap; +} + +.xp-audio-track-marquee { + display: inline-block; + padding: 6px 8px; + min-width: 100%; + animation: xp-audio-scroll 12s linear infinite; +} + +@keyframes xp-audio-scroll { + 0% { transform: translateX(100%); } + 100% { transform: translateX(-100%); } +} + +.xp-audio-controls { + display: grid; + grid-template-columns: auto auto auto 1fr; + gap: 6px; + align-items: center; +} + +.xp-audio-btn { + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #7b355d; + border-bottom: 1px solid #7b355d; + background: linear-gradient(#fff2fb, #efbfdc); + color: #6f2750; + font-size: 11px; + padding: 3px 8px; + cursor: pointer; +} + +.xp-audio-seek { + width: 100%; +} + +.xp-audio-time { + margin-top: auto; + border: 1px solid #8f4a72; + background: #fff6fc; + color: #6f2750; + font-family: "Lucida Console", "Courier New", monospace; + font-size: 11px; + padding: 4px 6px; + text-align: right; +} + +#context-menu { + position: fixed; + z-index: 3000; + min-width: 170px; + background: #fff5fc; + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid #8e3f6c; + border-bottom: 1px solid #8e3f6c; + box-shadow: 2px 2px 0 rgba(110, 41, 78, 0.35); + padding: 3px; +} + +.context-menu-item { + width: 100%; + text-align: left; + border: none; + background: transparent; + color: #6f2750; + font-size: 11px; + padding: 6px 8px; + cursor: pointer; +} + +.context-menu-item:hover:not(:disabled), +.context-menu-item:focus-visible { + background: linear-gradient(var(--titlebar-1), var(--titlebar-2)); + color: #fff; + outline: none; +} + +.context-menu-item:disabled { + color: #b78aa4; + cursor: default; +} + +.context-menu-sep { + height: 1px; + background: #e2a9cb; + margin: 3px 2px; +} + +.xp-props { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 100%; +} + +.xp-props-title { + font-size: 13px; + font-weight: 700; + color: #7c2e5b; +} + +.xp-props-body { + border: 1px solid #e7a8ca; + background: #fff; + padding: 8px; + font-size: 11px; + color: #5f2244; + display: flex; + flex-direction: column; + gap: 6px; +} +