๐Ÿ๐Ÿ๐Ÿ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

webui improvements

autumn 5d804856 1cbf1e6a

+770 -198
-1
webui/content/main.html
··· 9 9 <link rel="icon" href="favicon.png"> 10 10 <link id="themeLink" rel="stylesheet" type="text/css" href="style/theme/void.css"> 11 11 <link rel="stylesheet" type="text/css" href="style/main.css"> 12 - <link rel="stylesheet" type="text/css" href="style/load.css"> 13 12 </head> 14 13 <body> 15 14 <script src="main.js"></script>
+37 -31
webui/js/col.js
··· 1 1 2 2 const sheet = new CSSStyleSheet(); 3 3 sheet.replaceSync(` 4 - .hsplitter { 4 + .column { 5 + display: flex; 6 + flex-direction: column; 7 + height: 100%; 8 + width: 100%; 9 + } 10 + 11 + .column > .splitter { 5 12 height: 1px; 6 13 background-color: var(--main-faded); 7 14 cursor: row-resize; ··· 10 17 overflow: visible; 11 18 } 12 19 13 - .hsplitter::before { 20 + .column > .splitter::before { 14 21 content: ''; 15 22 position: relative; 16 23 display: inline-block; 17 - top: -6px; 24 + top: -8px; 18 25 left: 0; 19 26 width: 100%; 20 - height: 13px; 27 + height: 17px; 21 28 /*background-color: rgba(255, 0, 0, 0.1);*/ 22 29 } 23 30 24 - .target.top { 25 - padding-bottom: 1rem; 31 + .column > :first-child { 32 + margin-bottom: 1rem; 33 + height: calc(var(--current-portion) - 0.5px - 1rem); 26 34 } 27 35 28 - .target.bottom { 29 - padding-top: 1rem; 36 + .column > :not(.splitter):not(:first-child):not(:last-child) { 37 + margin-top: 1rem; 38 + margin-bottom: 1rem; 39 + height: calc(var(--current-portion) - 1px - 2rem); 30 40 } 31 41 32 - .target.middle { 33 - padding-top: 1rem; 34 - padding-bottom: 1rem; 42 + .column > :last-child { 43 + margin-top: 1rem; 44 + height: calc(var(--current-portion) - 0.5px - 1rem); 35 45 } 36 46 `); 37 47 document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 38 48 39 - export function main(target, n = 2) { 49 + export async function main(target, n = 2) { 40 50 n = parseInt(n, 10); 41 51 if (!Number.isInteger(n) || n < 2) { 42 52 n = 2; 43 53 } 44 54 45 55 const container = document.createElement('div'); 46 - container.style.cssText = 'display: flex; flex-direction: column; height: 100%; width: 100%;'; 56 + container.className = "column"; 47 57 48 58 const targets = []; 49 59 const splitters = []; 50 60 61 + const minPercent = 3; 62 + 51 63 function createDragHandler(splitter, i) { 52 64 splitter.onmousedown = (e) => { 65 + if (e.button !== 0) return; 53 66 e.preventDefault(); 54 67 55 68 function resizeCallback(e) { 56 69 const containerRect = container.getBoundingClientRect(); 57 - const relativeY = e.clientY - containerRect.top; 70 + const relativeY = e.clientY - containerRect.top - 1; // TODO: figure out *why* this "- 1" is required for perfect pointer alignment 58 71 const percent = (relativeY / containerRect.height) * 100; 59 72 60 73 // Get current heights of the two adjacent panes 61 - const topHeight = parseFloat(targets[i].style.height) || (100/n); 62 - const bottomHeight = parseFloat(targets[i + 1].style.height) || (100/n); 74 + const topHeight = parseFloat(targets[i].style.getPropertyValue('--current-portion')) || (100/n); 75 + const bottomHeight = parseFloat(targets[i + 1].style.getPropertyValue('--current-portion')) || (100/n); 63 76 const totalAdjacent = topHeight + bottomHeight; 64 77 65 78 // Calculate how much of the adjacent space we're at 66 79 let adjacentStart = 0; 67 80 for (let j = 0; j < i; j++) { 68 - adjacentStart += parseFloat(targets[j].style.height) || (100/n); 81 + adjacentStart += parseFloat(targets[j].style.getPropertyValue('--current-portion')) || (100/n); 69 82 } 70 83 71 84 const adjacentPercent = Math.max(0, Math.min(100, percent - adjacentStart)); 72 - const topRatio = Math.max(1, Math.min(totalAdjacent - 1, adjacentPercent)); 85 + const topRatio = Math.max(minPercent, Math.min(totalAdjacent - minPercent, adjacentPercent)); 73 86 const bottomRatio = totalAdjacent - topRatio; 74 87 75 - targets[i].style.height = topRatio + '%'; 76 - targets[i + 1].style.height = bottomRatio + '%'; 88 + targets[i].style.setProperty('--current-portion', topRatio + '%'); 89 + targets[i + 1].style.setProperty('--current-portion', bottomRatio + '%'); 77 90 } 78 91 79 92 function cleanup() { ··· 90 103 91 104 for (let i = 0; i < n; i++) { 92 105 const target = document.createElement('div'); 93 - target.style.height = `${100/n}%`; 106 + target.style.setProperty('--current-portion', `${100/n}%`); 94 107 95 - if (i === 0) { 96 - target.className = "target top"; 97 - } else if (i === n - 1) { 98 - target.className = "target bottom"; 99 - target.style.flex = '1'; // Last one gets flex to handle rounding 100 - target.style.height = 'auto'; 101 - } else { 102 - target.className = "target middle"; 103 - } 108 + await $mod("nothing", target); 104 109 105 110 targets.push(target); 106 111 container.appendChild(target); ··· 108 113 if (i === n - 1) continue; 109 114 110 115 const splitter = document.createElement('div'); 111 - splitter.className = 'hsplitter'; 116 + splitter.className = 'splitter'; 112 117 splitters.push(splitter); 113 118 container.appendChild(splitter); 114 119 ··· 118 123 target.appendChild(container); 119 124 120 125 return { 126 + replace: true, 121 127 targets: targets 122 128 }; 123 129 }
-9
webui/js/exit.js
··· 1 - 2 - export function main(target) { 3 - const dummy = document.createElement("div"); 4 - 5 - return { 6 - targets: [dummy] 7 - }; 8 - } 9 -
+13 -21
webui/js/main.js
··· 1 1 "use strict"; 2 2 3 3 window.userPreferences = { 4 - setTheme: (theme) => { 5 - if (theme === null) 6 - theme = 'void'; 7 - userPreferences.theme = theme; 8 - localStorage.setItem('theme', theme); 9 - const themeLink = document.getElementById('themeLink'); 10 - themeLink.href = `style/theme/${theme}.css`; 11 - }, 12 4 setLoadButton: (show) => { 13 5 if (show === null) 14 6 show = 'show'; 15 7 userPreferences.loadButton = show; 16 8 localStorage.setItem('loadButton', show); 17 9 }, 18 - toggleTheme: () => { 19 - if (userPreferences.theme === 'void') { 20 - userPreferences.setTheme('parchment'); 21 - } 22 - else { 23 - userPreferences.setTheme('void'); 24 - } 25 - }, 26 10 toggleLoadButton: () => { 27 11 if (userPreferences.loadButton === 'show') { 28 12 userPreferences.setLoadButton('hide'); ··· 33 17 } 34 18 }; 35 19 36 - 37 - window.userPreferences.setTheme(localStorage.getItem('theme')); 38 20 window.userPreferences.setLoadButton(localStorage.getItem('loadButton')); 39 21 22 + window.$mod = async function(moduleName, targetElement, args = []) { 23 + try { 24 + const module = await import(`/${moduleName}.js`); 25 + if ("main" in module) { 26 + return await module.main(targetElement, ...args); 27 + } 28 + } catch (error) { 29 + console.error('Failed to load module:', error.message); 30 + } 31 + return null; 32 + }; 40 33 41 - import("/prompt.js").then( 42 - module => module.main(document.body) 43 - ) 34 + $mod("theme", document.head); 35 + $mod("nothing", document.body); 44 36
+153
webui/js/menu.js
··· 1 + const sheet = new CSSStyleSheet(); 2 + sheet.replaceSync(` 3 + .context-menu { 4 + position: fixed; 5 + background-color: var(--main-background); 6 + border: 1px solid var(--main-faded); 7 + border-radius: 2px; 8 + padding: 0.25rem 0; 9 + min-width: 8rem; 10 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 11 + font-size: 0.875rem; 12 + user-select: none; 13 + } 14 + 15 + .context-menu-item { 16 + padding: 0.5rem 1rem; 17 + cursor: pointer; 18 + white-space: nowrap; 19 + color: var(--main-solid); 20 + } 21 + 22 + .context-menu-item:hover { 23 + background-color: var(--main-faded); 24 + } 25 + 26 + .context-menu-item.disabled { 27 + opacity: 0.5; 28 + cursor: default; 29 + } 30 + 31 + .context-menu-item.disabled:hover { 32 + background-color: transparent; 33 + } 34 + 35 + .context-menu-separator { 36 + height: 1px; 37 + background-color: var(--main-faded); 38 + margin: 0.25rem 0; 39 + } 40 + `); 41 + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 42 + 43 + let activeMenu = null; 44 + 45 + function closeMenu() { 46 + if (activeMenu) { 47 + activeMenu.remove(); 48 + activeMenu = null; 49 + } 50 + } 51 + 52 + function showMenu(x, y, items, containerElement = null) { 53 + closeMenu(); 54 + 55 + const menu = document.createElement('div'); 56 + menu.className = 'context-menu'; 57 + 58 + // Handle both old format and new string->action format 59 + const menuItems = Array.isArray(items) ? items : Object.entries(items); 60 + 61 + menuItems.forEach(item => { 62 + if (item === null || item === '-' || item[0] === '-') { 63 + const separator = document.createElement('div'); 64 + separator.className = 'context-menu-separator'; 65 + menu.appendChild(separator); 66 + return; 67 + } 68 + 69 + const menuItem = document.createElement('div'); 70 + menuItem.className = 'context-menu-item'; 71 + 72 + menuItem.textContent = item[0]; 73 + menuItem.onclick = async () => { 74 + closeMenu(); 75 + await item[1](); 76 + }; 77 + menu.appendChild(menuItem); 78 + }); 79 + 80 + // Position menu 81 + menu.style.left = x + 'px'; 82 + menu.style.top = y + 'px'; 83 + 84 + document.body.appendChild(menu); 85 + activeMenu = menu; 86 + 87 + // Adjust position if menu goes off-screen 88 + const rect = menu.getBoundingClientRect(); 89 + const bounds = containerElement ? containerElement.getBoundingClientRect() : { 90 + left: 0, 91 + top: 0, 92 + right: window.innerWidth, 93 + bottom: window.innerHeight 94 + }; 95 + 96 + if (rect.right > bounds.right) { 97 + menu.style.left = (x - rect.width) + 'px'; 98 + } 99 + if (rect.left < bounds.left) { 100 + menu.style.left = bounds.left + 'px'; 101 + } 102 + if (rect.bottom > bounds.bottom) { 103 + menu.style.top = (y - rect.height) + 'px'; 104 + } 105 + if (rect.top < bounds.top) { 106 + menu.style.top = bounds.top + 'px'; 107 + } 108 + 109 + return menu; 110 + } 111 + 112 + // Close menu on outside click or escape 113 + document.addEventListener('click', (e) => { 114 + if (activeMenu && !activeMenu.contains(e.target)) { 115 + closeMenu(); 116 + } 117 + }); 118 + 119 + document.addEventListener('keydown', (e) => { 120 + if (e.key === 'Escape') { 121 + closeMenu(); 122 + } 123 + }); 124 + 125 + export function main(target, ...args) { 126 + // Register context menu on target 127 + target.addEventListener('contextmenu', (e) => { 128 + if (e.target !== e.currentTarget) return; 129 + e.preventDefault(); 130 + 131 + // Default menu items 132 + const defaultItems = { 133 + 'Inspect': () => console.log('Inspect clicked'), 134 + '-': null, 135 + 'Reload': () => location.reload() 136 + }; 137 + 138 + // Use provided items or defaults 139 + let items = defaultItems; 140 + if (args.length > 0 && args[0]) { 141 + items = args[0]; 142 + } 143 + 144 + showMenu(e.clientX, e.clientY, items, target); 145 + }); 146 + 147 + return { 148 + replace: false, 149 + showMenu: (x, y, items) => showMenu(x, y, items, target), 150 + closeMenu 151 + }; 152 + } 153 +
+43
webui/js/nothing.js
··· 1 + 2 + const sheet = new CSSStyleSheet(); 3 + sheet.replaceSync(` 4 + .nothing { 5 + background-color: var(--main-background); 6 + width: 100%; 7 + height: 100%; 8 + } 9 + `); 10 + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 11 + 12 + export async function main(target) { 13 + const backdrop = document.createElement("div"); 14 + 15 + backdrop.className = "nothing"; 16 + 17 + const load = (modName, args=[]) => { 18 + return async () => { 19 + const result = await $mod(modName, target, args); 20 + if (result?.replace) { 21 + backdrop.remove(); 22 + } 23 + } 24 + }; 25 + 26 + await $mod("menu", backdrop, [{ 27 + prompt: load("prompt"), 28 + row2: load("row"), 29 + row3: load("row", [3]), 30 + col2: load("col"), 31 + col3: load("col", [3]), 32 + void: load("theme", ["void"]), 33 + parchment: load("theme", ["parchment"]), 34 + spinner: load("spinner") 35 + }]); 36 + 37 + target.appendChild(backdrop); 38 + 39 + return { 40 + replace: true 41 + }; 42 + } 43 +
+301
webui/js/pane.js
··· 1 + 2 + // UNREVIEWED CLAUDESLOP 3 + 4 + const sheet = new CSSStyleSheet(); 5 + sheet.replaceSync(` 6 + .pane-container { 7 + position: absolute; 8 + min-width: 8em; 9 + min-height: 5em; 10 + background: var(--main-bg, #fff); 11 + border: 1px solid var(--main-border, #ccc); 12 + border-radius: 0.75em; 13 + box-shadow: 0 4px 12px rgba(0,0,0,0.15); 14 + font-size: 1em; 15 + overflow: hidden; 16 + } 17 + 18 + .pane-header { 19 + height: 1.7em; 20 + background: var(--pane-header, #f0f0f0); 21 + border-radius: 0.75em 0.75em 0 0; 22 + display: flex; 23 + align-items: center; 24 + padding: 0 1em; 25 + user-select: none; 26 + cursor: move; 27 + position: relative; 28 + } 29 + 30 + .pane-title { 31 + flex: 1; 32 + font-size: 0.9em; 33 + color: var(--pane-title, #333); 34 + white-space: nowrap; 35 + overflow: hidden; 36 + text-overflow: ellipsis; 37 + } 38 + 39 + .pane-drag-handle { 40 + width: 1.2em; 41 + height: 1.2em; 42 + display: flex; 43 + flex-direction: column; 44 + justify-content: center; 45 + gap: 2px; 46 + opacity: 0.6; 47 + } 48 + 49 + .pane-drag-bar { 50 + height: 2px; 51 + background: var(--drag-handle, #666); 52 + border-radius: 1px; 53 + } 54 + 55 + .pane-body { 56 + height: calc(100% - 1.7em); 57 + overflow: auto; 58 + position: relative; 59 + } 60 + 61 + .pane-resize-handle { 62 + position: absolute; 63 + background: transparent; 64 + z-index: 10; 65 + } 66 + 67 + .pane-resize-corner { 68 + width: 12px; 69 + height: 12px; 70 + right: -6px; 71 + bottom: -6px; 72 + cursor: nw-resize; 73 + border-radius: 50%; 74 + } 75 + 76 + .pane-resize-right { 77 + width: 8px; 78 + height: calc(100% - 20px); 79 + right: -4px; 80 + top: 10px; 81 + cursor: ew-resize; 82 + } 83 + 84 + .pane-resize-bottom { 85 + width: calc(100% - 20px); 86 + height: 8px; 87 + left: 10px; 88 + bottom: -4px; 89 + cursor: ns-resize; 90 + } 91 + 92 + .pane-resize-handle:hover { 93 + background: var(--accent, rgba(0,100,200,0.3)); 94 + } 95 + `); 96 + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 97 + 98 + // Global pane management 99 + const panes = new Map(); 100 + let nextZIndex = 1000; 101 + 102 + function bringToFront(pane) { 103 + pane.style.zIndex = ++nextZIndex; 104 + } 105 + 106 + function constrainPosition(container, x, y) { 107 + const rect = container.getBoundingClientRect(); 108 + 109 + const maxX = rect.width; 110 + const maxY = rect.height; 111 + 112 + return { 113 + x: Math.max(0, Math.min(x, maxX)), 114 + y: Math.max(0, Math.min(y, maxY)) 115 + }; 116 + } 117 + 118 + function makeDraggable(element, handle) { 119 + let isDragging = false; 120 + let startX, startY, startLeft, startTop; 121 + 122 + handle.addEventListener('mousedown', (e) => { 123 + if (e.button !== 0) return; 124 + e.preventDefault(); 125 + 126 + isDragging = true; 127 + startX = e.clientX; 128 + startY = e.clientY; 129 + startLeft = element.offsetLeft; 130 + startTop = element.offsetTop; 131 + 132 + bringToFront(element); 133 + 134 + document.addEventListener('mousemove', onMouseMove); 135 + document.addEventListener('mouseup', onMouseUp); 136 + }); 137 + 138 + function onMouseMove(e) { 139 + if (!isDragging) return; 140 + 141 + const deltaX = e.clientX - startX; 142 + const deltaY = e.clientY - startY; 143 + 144 + const newPos = constrainPosition(element, startLeft + deltaX, startTop + deltaY); 145 + element.style.left = newPos.x + 'px'; 146 + element.style.top = newPos.y + 'px'; 147 + } 148 + 149 + function onMouseUp() { 150 + isDragging = false; 151 + document.removeEventListener('mousemove', onMouseMove); 152 + document.removeEventListener('mouseup', onMouseUp); 153 + } 154 + } 155 + 156 + function makeResizable(element) { 157 + const handles = element.querySelectorAll('.pane-resize-handle'); 158 + 159 + handles.forEach(handle => { 160 + let isResizing = false; 161 + let startX, startY, startWidth, startHeight, startLeft, startTop; 162 + 163 + handle.addEventListener('mousedown', (e) => { 164 + if (e.button !== 0) return; 165 + e.preventDefault(); 166 + 167 + isResizing = true; 168 + startX = e.clientX; 169 + startY = e.clientY; 170 + startWidth = element.offsetWidth; 171 + startHeight = element.offsetHeight; 172 + startLeft = element.offsetLeft; 173 + startTop = element.offsetTop; 174 + 175 + bringToFront(element); 176 + 177 + document.addEventListener('mousemove', onMouseMove); 178 + document.addEventListener('mouseup', onMouseUp); 179 + }); 180 + 181 + function onMouseMove(e) { 182 + if (!isResizing) return; 183 + 184 + const deltaX = e.clientX - startX; 185 + const deltaY = e.clientY - startY; 186 + 187 + if (handle.classList.contains('pane-resize-corner')) { 188 + const newWidth = Math.max(150, startWidth + deltaX); 189 + const newHeight = Math.max(100, startHeight + deltaY); 190 + element.style.width = newWidth + 'px'; 191 + element.style.height = newHeight + 'px'; 192 + } else if (handle.classList.contains('pane-resize-right')) { 193 + const newWidth = Math.max(150, startWidth + deltaX); 194 + element.style.width = newWidth + 'px'; 195 + } else if (handle.classList.contains('pane-resize-bottom')) { 196 + const newHeight = Math.max(100, startHeight + deltaY); 197 + element.style.height = newHeight + 'px'; 198 + } 199 + } 200 + 201 + function onMouseUp() { 202 + isResizing = false; 203 + document.removeEventListener('mousemove', onMouseMove); 204 + document.removeEventListener('mouseup', onMouseUp); 205 + } 206 + }); 207 + } 208 + 209 + export function main(target, title = 'Untitled', width = 300, height = 200) { 210 + const container = document.createElement('div'); 211 + container.className = 'pane-container'; 212 + 213 + // Parse dimensions 214 + width = parseInt(width) || 300; 215 + height = parseInt(height) || 200; 216 + 217 + // Position in center with slight randomness 218 + const centerX = (target.offsetWidth - width) / 2 + (Math.random() - 0.5) * 100; 219 + const centerY = (target.offsetHeight - height) / 2 + (Math.random() - 0.5) * 100; 220 + 221 + const pos = constrainPosition(container, centerX, centerY); 222 + 223 + container.style.cssText = ` 224 + width: ${width}px; 225 + height: ${height}px; 226 + left: ${pos.x}px; 227 + top: ${pos.y}px; 228 + z-index: ${++nextZIndex}; 229 + `; 230 + 231 + // Create header 232 + const header = document.createElement('div'); 233 + header.className = 'pane-header'; 234 + 235 + const titleSpan = document.createElement('span'); 236 + titleSpan.className = 'pane-title'; 237 + titleSpan.textContent = title; 238 + 239 + const dragHandle = document.createElement('div'); 240 + dragHandle.className = 'pane-drag-handle'; 241 + for (let i = 0; i < 3; i++) { 242 + const bar = document.createElement('div'); 243 + bar.className = 'pane-drag-bar'; 244 + dragHandle.appendChild(bar); 245 + } 246 + 247 + header.appendChild(titleSpan); 248 + header.appendChild(dragHandle); 249 + 250 + // Create body 251 + const body = document.createElement('div'); 252 + body.className = 'pane-body'; 253 + 254 + // Create resize handles 255 + const cornerHandle = document.createElement('div'); 256 + cornerHandle.className = 'pane-resize-handle pane-resize-corner'; 257 + 258 + const rightHandle = document.createElement('div'); 259 + rightHandle.className = 'pane-resize-handle pane-resize-right'; 260 + 261 + const bottomHandle = document.createElement('div'); 262 + bottomHandle.className = 'pane-resize-handle pane-resize-bottom'; 263 + 264 + // Assemble 265 + container.appendChild(header); 266 + container.appendChild(body); 267 + container.appendChild(cornerHandle); 268 + container.appendChild(rightHandle); 269 + container.appendChild(bottomHandle); 270 + 271 + // Middle-click to close 272 + header.addEventListener('mousedown', (e) => { 273 + if (e.button === 1) { 274 + e.preventDefault(); 275 + container.remove(); 276 + panes.delete(container); 277 + } 278 + }); 279 + 280 + // Click to bring to front 281 + container.addEventListener('mousedown', (e) => { 282 + if (e.button === 0) { 283 + bringToFront(container); 284 + } 285 + }); 286 + 287 + // Make functional 288 + makeDraggable(container, header); 289 + makeResizable(container); 290 + 291 + // Add to DOM and tracking 292 + target.appendChild(container); 293 + panes.set(container, { title, body }); 294 + 295 + return { 296 + replace: false, 297 + targets: [body], 298 + pane: container 299 + }; 300 + } 301 +
+8 -15
webui/js/prompt.js
··· 1 1 2 - export function main(target) { 2 + export async function main(target) { 3 3 const container = document.createElement('div'); 4 4 5 5 const input = document.createElement('input'); ··· 12 12 const moduleName = inputSplit[0]; 13 13 const args = inputSplit.slice(1); 14 14 15 - try { 16 - const module = await import(`/${moduleName}.js`); 17 - if ("main" in module) { 18 - const result = await module.main(target, ...args); 19 - if (result?.targets?.length) { 20 - for (const targetElement of result.targets) { 21 - main(targetElement); 22 - } 23 - container.remove(); 24 - } 25 - } 26 - 27 - } catch (error) { 28 - console.error('Failed to load module:', error.message); 15 + const result = await $mod(moduleName, target, args); 16 + if (result?.replace) { 17 + container.remove(); 29 18 } 30 19 } 31 20 ··· 38 27 container.appendChild(input); 39 28 target.appendChild(container); 40 29 input.focus(); 30 + 31 + return { 32 + replace: true 33 + }; 41 34 } 42 35
+42 -33
webui/js/row.js
··· 1 1 2 2 const sheet = new CSSStyleSheet(); 3 3 sheet.replaceSync(` 4 - .vsplitter { 4 + .row { 5 + display: flex; 6 + flex-direction: row; 7 + height: 100%; 8 + width: 100%; 9 + } 10 + 11 + .row > .splitter { 5 12 width: 1px; 6 13 background-color: var(--main-faded); 7 14 cursor: col-resize; ··· 10 17 overflow: visible; 11 18 } 12 19 13 - .vsplitter::before { 20 + .row > .splitter::before { 14 21 content: ''; 15 22 position: relative; 16 23 display: inline-block; 17 24 top: 0; 18 - left: -6px; 19 - width: 13px; 25 + left: -8px; 26 + width: 17px; 20 27 height: 100%; 21 28 /*background-color: rgba(255, 0, 0, 0.1);*/ 22 29 } 23 30 24 - .target.left { 25 - padding-right: 1rem; 31 + .row > :first-child { 32 + margin-right: 1rem; 33 + width: calc(var(--current-portion) - 0.5px - 1rem); 26 34 } 27 35 28 - .target.right { 29 - padding-left: 1rem; 36 + .row > :not(.splitter):not(:first-child):not(:last-child) { 37 + margin-left: 1rem; 38 + margin-right: 1rem; 39 + width: calc(var(--current-portion) - 1px - 2rem); 30 40 } 31 41 32 - .target.middle { 33 - padding-left: 1rem; 34 - padding-right: 1rem; 42 + .row > :last-child { 43 + margin-left: 1rem; 44 + width: calc(var(--current-portion) - 0.5px - 1rem); 35 45 } 36 46 `); 37 47 document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 38 48 39 - export function main(target, n = 2) { 49 + export async function main(target, n = 2) { 40 50 n = parseInt(n, 10); 41 51 if (!Number.isInteger(n) || n < 2) { 42 52 n = 2; 43 53 } 44 54 45 55 const container = document.createElement('div'); 46 - container.style.cssText = 'display: flex; height: 100%; width: 100%;'; 56 + container.className = "row"; 47 57 48 58 const targets = []; 49 59 const splitters = []; 60 + 61 + const minPercent = 2; 50 62 51 63 function createDragHandler(splitter, i) { 52 64 splitter.onmousedown = (e) => { 65 + if (e.button !== 0) return; 53 66 e.preventDefault(); 54 67 55 68 function resizeCallback(e) { 56 69 const containerRect = container.getBoundingClientRect(); 57 - const relativeX = e.clientX - containerRect.left; 58 - const percent = (relativeX / containerRect.width) * 100; 70 + 71 + let leftmost = containerRect.left; 72 + let width = containerRect.width; 73 + 74 + const relativeX = e.clientX - leftmost; 75 + const percent = (relativeX / width) * 100; 59 76 60 77 // Get current widths of the two adjacent panes 61 - const leftWidth = parseFloat(targets[i].style.width) || (100/n); 62 - const rightWidth = parseFloat(targets[i + 1].style.width) || (100/n); 78 + const leftWidth = parseFloat(targets[i].style.getPropertyValue('--current-portion')) || (100/n); 79 + const rightWidth = parseFloat(targets[i + 1].style.getPropertyValue('--current-portion')) || (100/n); 63 80 const totalAdjacent = leftWidth + rightWidth; 64 81 65 82 // Calculate how much of the adjacent space we're at 66 83 let adjacentStart = 0; 67 84 for (let j = 0; j < i; j++) { 68 - adjacentStart += parseFloat(targets[j].style.width) || (100/n); 85 + adjacentStart += parseFloat(targets[j].style.getPropertyValue('--current-portion')) || (100/n); 69 86 } 70 87 71 88 const adjacentPercent = Math.max(0, Math.min(100, percent - adjacentStart)); 72 - const leftRatio = Math.max(1, Math.min(totalAdjacent - 1, adjacentPercent)); 89 + const leftRatio = Math.max(minPercent, Math.min(totalAdjacent - minPercent, adjacentPercent)); 73 90 const rightRatio = totalAdjacent - leftRatio; 74 91 75 - targets[i].style.width = leftRatio + '%'; 76 - targets[i + 1].style.width = rightRatio + '%'; 92 + targets[i].style.setProperty('--current-portion', leftRatio + '%'); 93 + targets[i + 1].style.setProperty('--current-portion', rightRatio + '%'); 77 94 } 78 95 79 96 function cleanup() { ··· 90 107 91 108 for (let i = 0; i < n; i++) { 92 109 const target = document.createElement('div'); 93 - target.style.width = `${100/n}%`; 110 + target.style.setProperty('--current-portion', `${100/n}%`); 94 111 95 - if (i === 0) { 96 - target.className = "target left"; 97 - } else if (i === n - 1) { 98 - target.className = "target right"; 99 - target.style.flex = '1'; // Last one gets flex to handle rounding 100 - target.style.width = 'auto'; 101 - } else { 102 - target.className = "target middle"; 103 - } 112 + await $mod("nothing", target); 104 113 105 114 targets.push(target); 106 115 container.appendChild(target); ··· 108 117 if (i === n - 1) continue; 109 118 110 119 const splitter = document.createElement('div'); 111 - splitter.className = 'vsplitter'; 120 + splitter.className = "splitter"; 112 121 splitters.push(splitter); 113 122 container.appendChild(splitter); 114 123 ··· 118 127 target.appendChild(container); 119 128 120 129 return { 121 - targets: targets 130 + replace: true 122 131 }; 123 132 } 124 133
+138
webui/js/spinner.js
··· 1 + 2 + const sheet = new CSSStyleSheet(); 3 + sheet.replaceSync(` 4 + .spinner { 5 + opacity: 1; 6 + width: 0px; 7 + height: 0px; 8 + position: relative; 9 + top: 50%; 10 + left: 50%; 11 + transform: translate(-50%, -50%); 12 + background-color: transparent; 13 + animation: spin 2.7s infinite linear; 14 + transition: opacity 1.5s ease-in; 15 + overflow: initial; 16 + } 17 + 18 + .spinner-button { 19 + position: relative; 20 + top: 50%; 21 + left: 50%; 22 + transform: translate(-50%, -50%); 23 + width: 4em; 24 + height: 4em; 25 + border-radius: 50%; 26 + border: none; 27 + opacity: 0; 28 + transition: opacity 1s ease; 29 + background-color: var(--main-solid); 30 + color: var(--main-solid-content); 31 + cursor: pointer; 32 + display: none; 33 + padding: 0; 34 + } 35 + 36 + .spinner-button:hover { 37 + background-color: var(--main-transparent); 38 + } 39 + 40 + .spinner-orb { 41 + position: absolute; 42 + top: 50%; 43 + left: 50%; 44 + transform-origin: center; 45 + transform: translate(-50%, -50%); 46 + background-color: transparent; 47 + border: 0.3em solid var(--main-solid); 48 + border-radius: 50%; 49 + transition: left 1.3s ease; 50 + overflow: initial; 51 + } 52 + 53 + .spinner-orb[class*=" 0"] { 54 + width: 100px; 55 + height: 100px; 56 + left: 0.5em; 57 + border-width: 0.5em; 58 + animation: spin 3.3s infinite linear; 59 + } 60 + 61 + .spinner-orb[class*=" 1"] { 62 + width: 75px; 63 + height: 75px; 64 + left: 175px; 65 + animation: spin 0.8s reverse infinite linear; 66 + } 67 + 68 + .spinner-orb[class*=" 2"] { 69 + width: 55px; 70 + height: 55px; 71 + left: -200px; 72 + border-width: 0.15em; 73 + transform: translate(-50%, -50%); 74 + } 75 + 76 + @keyframes spin { 77 + from { 78 + transform: translate(-50%, -50%) rotate(0deg); 79 + } 80 + 81 + to { 82 + transform: translate(-50%, -50%) rotate(360deg); 83 + } 84 + } 85 + `); 86 + document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 87 + 88 + export async function main(target, modNext = "nothing") { 89 + let spinner = document.createElement('div'); 90 + let orb0 = document.createElement('div'); 91 + let orb1 = document.createElement('div'); 92 + let orb2 = document.createElement('div'); 93 + let button = document.createElement('button'); 94 + 95 + button.innerText = 'enter'; 96 + button.classList.add('spinner-button'); 97 + 98 + spinner.classList.add('spinner'); 99 + 100 + orb0.classList.add('spinner-orb'); 101 + orb0.classList.add('0'); 102 + 103 + orb1.classList.add('spinner-orb'); 104 + orb1.classList.add('1'); 105 + 106 + orb2.classList.add('spinner-orb'); 107 + orb2.classList.add('2'); 108 + 109 + target.appendChild(spinner); 110 + target.appendChild(button); 111 + spinner.appendChild(orb0); 112 + orb0.appendChild(orb1); 113 + orb1.appendChild(orb2); 114 + 115 + let removeLoader = async (e) => { 116 + spinner.style.opacity = '0'; 117 + button.style.opacity = '0'; 118 + orb0.style.left = '50%'; 119 + orb1.style.left = '50%'; 120 + orb2.style.left = '50%'; 121 + await $mod(modNext, target); 122 + setTimeout(async () => { 123 + spinner.remove(); 124 + button.remove(); 125 + }, 1500); 126 + }; 127 + 128 + setTimeout(() => { 129 + button.style.opacity = 1; 130 + }, 100); 131 + button.addEventListener('click', removeLoader); 132 + button.style.display = 'initial'; 133 + 134 + return { 135 + replace: true 136 + }; 137 + } 138 +
+33
webui/js/theme.js
··· 1 + 2 + export function main(target, initialTheme = null) { 3 + const storedTheme = localStorage.getItem('theme'); 4 + let theme = initialTheme || storedTheme || 'void'; 5 + 6 + if (!target.setTheme) { 7 + const themeLink = document.createElement('link'); 8 + themeLink.rel = 'stylesheet'; 9 + themeLink.type = 'text/css'; 10 + 11 + target.appendChild(themeLink); 12 + 13 + target.setTheme = (newTheme) => { 14 + themeLink.href = `style/theme/${newTheme}.css`; 15 + 16 + theme = newTheme; 17 + if (target === document.head) { 18 + localStorage.setItem('theme', theme); 19 + } 20 + }; 21 + target.toggleTheme = () => { 22 + const newTheme = theme === 'void' ? 'parchment' : 'void'; 23 + target.setTheme(newTheme); 24 + }; 25 + } 26 + 27 + target.setTheme(theme); 28 + 29 + return { 30 + replace: false, 31 + }; 32 + } 33 +
-87
webui/style/load.css
··· 1 - 2 - .loader { 3 - opacity: 1; 4 - width: 0px; 5 - height: 0px; 6 - position: absolute; 7 - top: 50%; 8 - left: 50%; 9 - transform: translate(-50%, -50%); 10 - background-color: transparent; 11 - animation: spin 2.7s infinite linear; 12 - transition: opacity 1.5s ease-in; 13 - overflow: initial; 14 - } 15 - 16 - .loader-button { 17 - position: absolute; 18 - top: 50%; 19 - left: 50%; 20 - transform: translate(-50%, -50%); 21 - width: 4em; 22 - height: 4em; 23 - border-radius: 50%; 24 - border: none; 25 - opacity: 0; 26 - transition: opacity 1s ease; 27 - background-color: var(--main-solid); 28 - color: var(--main-solid-content); 29 - cursor: pointer; 30 - display: none; 31 - padding: 0; 32 - } 33 - 34 - .loader-button:hover { 35 - background-color: var(--main-transparent); 36 - } 37 - 38 - .loader-orb { 39 - position: absolute; 40 - top: 50%; 41 - left: 50%; 42 - transform-origin: center; 43 - transform: translate(-50%, -50%); 44 - background-color: transparent; 45 - border: 0.3em solid var(--main-solid); 46 - border-radius: 50%; 47 - transition: left 1.3s ease; 48 - overflow: initial; 49 - } 50 - 51 - .loader-orb[class*=" 0"] { 52 - width: 100px; 53 - height: 100px; 54 - left: 0.5em; 55 - border-width: 0.5em; 56 - animation: spin 3.3s infinite linear; 57 - } 58 - 59 - .loader-orb[class*=" 1"] { 60 - width: 75px; 61 - height: 75px; 62 - left: 175px; 63 - animation: spin 0.8s reverse infinite linear; 64 - } 65 - 66 - .loader-orb[class*=" 2"] { 67 - width: 55px; 68 - height: 55px; 69 - left: -200px; 70 - border-width: 0.15em; 71 - transform: translate(-50%, -50%); 72 - } 73 - 74 - @keyframes spin { 75 - from { 76 - transform: translate(-50%, -50%) rotate(0deg); 77 - } 78 - 79 - to { 80 - transform: translate(-50%, -50%) rotate(360deg); 81 - } 82 - } 83 - 84 - body { 85 - background-color: var(--main-background); 86 - } 87 -
+2 -1
webui/style/main.css
··· 21 21 height: 100vh; 22 22 width: 100vw; 23 23 padding: 1rem; 24 + background-color: var(--main-background); 24 25 } 25 26 26 27 .target { ··· 61 62 background-color: var(--main-faded); 62 63 } 63 64 64 - .nothing { 65 + .nothing_old { 65 66 position: absolute; 66 67 font-family: var(--code-font); 67 68 color: var(--main-solid);