diff --git a/dist/binary/index.html b/dist/binary/index.html index be4f129..e72913a 100644 --- a/dist/binary/index.html +++ b/dist/binary/index.html @@ -1,3 +1,20 @@ - Binary Simulator | Computing:Box
Denary
0
Binary
00000000
\ No newline at end of file diff --git a/dist/hexadecimal/index.html b/dist/hexadecimal/index.html index f88bac9..71344d9 100644 --- a/dist/hexadecimal/index.html +++ b/dist/hexadecimal/index.html @@ -1,3 +1,20 @@ - Hexadecimal Simulator | Computing:Box
Denary
0
Hexadecimal
00
Binary
00000000
\ No newline at end of file diff --git a/dist/index.html b/dist/index.html index f669b69..2065b3f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,7 +1,19 @@ - Astro Basics
Astro Homepage

-To get started, open the
src/pages
directory in your project. -

What's New in Astro 5.0?

-From content layers to server islands, click to learn more about the new features and - improvements in Astro 5.0 -

\ No newline at end of file + Welcome | Computing:Box

Version 2.0 Now Live

Understand Computing concepts better.

+Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum. +

Learn More Get Started
Computing Box Logo
\ No newline at end of file diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 6fa4a20..cf04c9d 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,6 +1,5 @@ --- -import '../styles/global.css'; - +import "../styles/global.css"; const { title = "Computing:Box" } = Astro.props; --- @@ -11,6 +10,22 @@ const { title = "Computing:Box" } = Astro.props; {title} + @@ -29,6 +44,7 @@ const { title = "Computing:Box" } = Astro.props; Hexadecimal Hex Colours Logic Gates + PC Components @@ -40,7 +56,11 @@ const { title = "Computing:Box" } = Astro.props; diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..80122a7 --- /dev/null +++ b/src/pages/about.astro @@ -0,0 +1,37 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

The New Computing:Box Experience

+

+ The platform has been rebuilt from the ground up to provide a deeper, more professional simulation environment. + We've introduced several key simulators and interface upgrades to support the modern Computing classroom: +

+ +
+ +
    +
  • ✔ New User Interface (Responsive)
  • +
  • ✔ Two's Complement Simulator
  • +
  • ✔ Extended Binary Simulator (Custom bit sizes)
  • +
  • ✔ Unified Binary Simulator (Unsigned & Two's Complement)
  • +
  • ✔ Extended Hexadecimal Simulator
  • +
  • ✔ Unified Hexadecimal Simulator (GCSE & A Level)
  • +
  • ✔ Enhanced Gate Simulator (Truth Table Creator)
  • +
  • ✔ Compound Gate Simulator
  • +
  • ✔ Computer Components Simulator
  • +
+
+ +
+

Educational Impact

+

+ Computing:Box is designed to help students learn computing concepts step by step, using clear visuals and simple interactions. + By changing values and seeing results straight away, students can build understanding at their own pace. +

+
+
+
\ No newline at end of file diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 0caf8dc..5233717 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/copyright.astro b/src/pages/copyright.astro new file mode 100644 index 0000000..2b6f65f --- /dev/null +++ b/src/pages/copyright.astro @@ -0,0 +1,30 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+

Copyright Notice

+

+ Computing:Box + © 2024 by Alexander Lyall is licensed under + Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. +

+ +
+ +

What Does This Mean For You?

+

You are free to:

+
    +
  • Share — copy and redistribute the material in any medium or format.
  • +
  • Adapt — remix, transform, and build upon the material.
  • +
+ +

Under the following terms:

+
    +
  • Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
  • +
  • NonCommercial — You may not use the material for commercial purposes.
  • +
  • ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
  • +
+
+
\ No newline at end of file diff --git a/src/pages/hex-colours.astro b/src/pages/hex-colours.astro index d1e7e40..21a5624 100644 --- a/src/pages/hex-colours.astro +++ b/src/pages/hex-colours.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/hexadecimal.astro b/src/pages/hexadecimal.astro index e74ca17..a981dc5 100644 --- a/src/pages/hexadecimal.astro +++ b/src/pages/hexadecimal.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..7490d18 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,23 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

Version 2.0 Now Live

+

Understand Computing concepts better.

+

+ Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum. +

+ +
+ +
+ Computing Box Logo +
+
+
\ No newline at end of file diff --git a/src/pages/legal-code.astro b/src/pages/legal-code.astro new file mode 100644 index 0000000..51c7256 --- /dev/null +++ b/src/pages/legal-code.astro @@ -0,0 +1,20 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+

Legal Code

+ +
+

About the license and Creative Commons

+

+ Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. +

+
+ +

+ By using this licensed material, you accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License. +

+
+
\ No newline at end of file diff --git a/src/pages/pc-builder.astro b/src/pages/pc-builder.astro new file mode 100644 index 0000000..5f68a10 --- /dev/null +++ b/src/pages/pc-builder.astro @@ -0,0 +1,65 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/pc-builder.css"; +--- + + +
+ + + +
+
PC Part Simulator
+
+ Build inside the Case! Snap the Motherboard into the chassis, then populate the slots. Add the side panel when done. Double-Click parts to inspect in 3D. +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
×
+
+
+
+
+
Move mouse to rotate component. Scroll to zoom.
+
+ +
+ + +
\ No newline at end of file diff --git a/src/scripts/pcBuilder.js b/src/scripts/pcBuilder.js new file mode 100644 index 0000000..b23e2b2 --- /dev/null +++ b/src/scripts/pcBuilder.js @@ -0,0 +1,423 @@ +// src/scripts/pcBuilder.js +// Computing:Box — Advanced PC Sandbox + +(() => { + const workspace = document.getElementById("workspace"); + const viewport = document.getElementById("viewport"); + const wireLayer = document.getElementById("wireLayer"); + const specsContainer = document.getElementById("buildSpecsContainer"); + const toolboxGrid = document.getElementById("toolboxGrid"); + const btnClearBoard = document.getElementById("btnClearBoard"); + const toolboxToggle = document.getElementById("toolboxToggle"); + const pcPage = document.getElementById("pcPage"); + + /* --- Extensive PC Component Library --- */ + const PC_PARTS = { + 'CASE': { + name: 'ATX PC Case', w: 600, h: 550, z: 5, ports: [], + slots: { + 'MB1': { x: 20, y: 20, accepts: 'MB' }, + 'PSU1': { x: 20, y: 440, accepts: 'PSU' }, + 'HDD1': { x: 440, y: 20, accepts: 'HDD' }, + 'HDD2': { x: 440, y: 170, accepts: 'HDD' }, + 'SATA_SSD1': { x: 440, y: 320, accepts: 'SATA_SSD' }, + 'SATA_SSD2': { x: 440, y: 400, accepts: 'SATA_SSD' } + }, + svg: `` + }, + 'MB': { + name: 'Motherboard', w: 360, h: 400, z: 10, + ports: [ + { id: 'atx_pwr', x: 340, y: 150 }, { id: 'sata1', x: 340, y: 300 }, { id: 'sata2', x: 340, y: 330 }, + { id: 'usb1', x: 10, y: 40 }, { id: 'usb2', x: 10, y: 70 }, { id: 'usb3', x: 10, y: 100 }, { id: 'usb4', x: 10, y: 130 }, + { id: 'audio', x: 10, y: 170 }, { id: 'disp', x: 10, y: 210 } + ], + slots: { + 'CPU1': { x: 120, y: 40, accepts: 'CPU' }, + 'COOLER1': { x: 100, y: 20, accepts: 'COOLER' }, + 'RAM1': { x: 230, y: 30, accepts: 'RAM' }, 'RAM2': { x: 250, y: 30, accepts: 'RAM' }, + 'RAM3': { x: 270, y: 30, accepts: 'RAM' }, 'RAM4': { x: 290, y: 30, accepts: 'RAM' }, + 'M2_1': { x: 120, y: 170, accepts: 'M2_SSD' }, 'M2_2': { x: 120, y: 250, accepts: 'M2_SSD' }, + 'PCIE1': { x: 40, y: 200, accepts: 'GPU' }, 'PCIE2': { x: 40, y: 300, accepts: 'GPU' } + }, + // Uses a lighter slate grey #2C303A to stand out from the case + svg: `` + }, + 'CPU': { name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {}, svg: `CPU` }, + 'COOLER': { name: 'CPU Fan', w: 120, h: 120, z: 30, ports: [], slots: {}, svg: `` }, + 'RAM': { name: 'DDR4 Memory', w: 15, h: 100, z: 20, ports: [], slots: {}, svg: `` }, + 'GPU': { name: 'Graphics Card', w: 280, h: 60, z: 40, slots: {}, ports: [{ id: 'pwr_in', x: 270, y: 10 }, { id: 'disp_out', x: 10, y: 30 }], svg: `` }, + 'M2_SSD': { name: 'M.2 NVMe SSD', w: 80, h: 15, z: 20, ports: [], slots: {}, svg: `` }, + 'SATA_SSD': { name: '2.5" SATA SSD', w: 100, h: 70, z: 20, slots: {}, ports: [{id:'data', x:90, y:20}, {id:'pwr', x:90, y:50}], svg: `SSD` }, + 'HDD': { name: '3.5" Mech HDD', w: 120, h: 140, z: 20, slots: {}, ports: [{id:'data', x:110, y:20}, {id:'pwr', x:110, y:120}], svg: `` }, + 'PSU': { name: 'Power Supply', w: 160, h: 90, z: 20, slots: {}, ports: [{id:'out1',x:150,y:20}, {id:'out2',x:150,y:40}, {id:'out3',x:150,y:60}, {id:'out4',x:150,y:80}], svg: `` }, + 'MONITOR': { name: 'Monitor', w: 240, h: 160, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}], svg: `` }, + 'KEYBOARD': { name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}], svg: `` }, + 'MOUSE': { name: 'Mouse', w: 30, h: 50, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}], svg: `` }, + 'SPEAKER': { name: 'Speakers', w: 40, h: 80, z: 30, slots: {}, ports: [{id:'audio', x:20, y:10}], svg: `` } + }; + + let nodes = {}; + let connections = []; + let nextNodeId = 1, nextWireId = 1; + + let isDraggingNode = null, dragOffset = { x: 0, y: 0 }; + let wiringStart = null, tempWirePath = null; + let selectedWireId = null, selectedNodeId = null; + + let panX = 0, panY = 0, zoom = 1; + let isPanning = false, panStart = { x: 0, y: 0 }; + + /* --- Setup Toolbox --- */ + function initToolbox() { + if(!toolboxGrid) return; + let html = ''; + Object.keys(PC_PARTS).forEach(partKey => { + html += ` +
+ ${PC_PARTS[partKey].svg} +
${partKey}
+
+ `; + }); + toolboxGrid.innerHTML = html; + document.querySelectorAll('.drag-item').forEach(item => { + item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); }); + }); + } + + /* --- Camera Math --- */ + function updateViewport() { + viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; + workspace.style.backgroundSize = `${32 * zoom}px ${32 * zoom}px`; + workspace.style.backgroundPosition = `${panX}px ${panY}px`; + } + function zoomWorkspace(factor, mouseX, mouseY) { + const newZoom = Math.min(Math.max(0.1, zoom * factor), 2); + panX = mouseX - (mouseX - panX) * (newZoom / zoom); + panY = mouseY - (mouseY - panY) * (newZoom / zoom); + zoom = newZoom; updateViewport(); + } + function getPortCoords(nodeId, portDataAttr) { + const node = nodes[nodeId]; + if (!node || !node.el) return {x:0, y:0}; + const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`); + if (!portEl) return {x:0, y:0}; + const wsRect = workspace.getBoundingClientRect(); + const portRect = portEl.getBoundingClientRect(); + return { + x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom, + y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom + }; + } + function drawBezier(x1, y1, x2, y2) { + const cpDist = Math.abs(x2 - x1) * 0.6 + 20; + return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`; + } + + /* --- Rendering --- */ + function renderWires() { + let svgHTML = ''; + connections.forEach(conn => { + const from = getPortCoords(conn.fromNode, conn.fromPort); + const to = getPortCoords(conn.toNode, conn.toPort); + const isSelected = conn.id === selectedWireId; + svgHTML += ``; + }); + if (wiringStart && tempWirePath) { + svgHTML += ``; + } + wireLayer.innerHTML = svgHTML; + } + + function updateNodePositions() { + Object.values(nodes).forEach(n => { + if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; } + }); + renderWires(); + } + + function clearSelection() { + selectedWireId = null; selectedNodeId = null; + document.querySelectorAll('.pb-node.selected').forEach(el => el.classList.remove('selected')); + renderWires(); + } + + /* --- Seven-Segment Diagnostics Engine --- */ + function evaluateBuild() { + if(!specsContainer) return; + + let hasCase = false, hasMB = false, hasCPU = false, hasCooler = false, hasRAM = false, hasPSU = false; + let hasStorage = false, hasGPU = false; + let mbPwr = false, gpuPwr = false; + let usbCount = 0, dispConn = false, audConn = false; + + let caseNode = Object.values(nodes).find(n => n.type === 'CASE'); + let mbNode = Object.values(nodes).find(n => n.type === 'MB'); + + if (caseNode) { + hasCase = true; + if (caseNode.slots['MB1']) hasMB = true; + if (caseNode.slots['PSU1']) hasPSU = true; + if (caseNode.slots['HDD1'] || caseNode.slots['HDD2'] || caseNode.slots['SATA_SSD1'] || caseNode.slots['SATA_SSD2']) hasStorage = true; + } else if (mbNode) { + hasMB = true; // Motherboard exists outside case + } + + if (mbNode) { + if (mbNode.slots['CPU1']) hasCPU = true; + if (mbNode.slots['COOLER1']) hasCooler = true; + if (mbNode.slots['RAM1'] || mbNode.slots['RAM2'] || mbNode.slots['RAM3'] || mbNode.slots['RAM4']) hasRAM = true; + if (mbNode.slots['PCIE1'] || mbNode.slots['PCIE2']) hasGPU = true; + if (mbNode.slots['M2_1'] || mbNode.slots['M2_2']) hasStorage = true; + } + + // Check Cables + connections.forEach(c => { + let n1 = nodes[c.fromNode], n2 = nodes[c.toNode]; + if(!n1 || !n2) return; + let types = [n1.type, n2.type]; + + if(types.includes('MB') && types.includes('PSU')) mbPwr = true; + if(types.includes('GPU') && types.includes('PSU')) gpuPwr = true; + if(types.includes('MB') && ['KEYBOARD','MOUSE','WEBCAM','MIC','PRINTER'].some(t => types.includes(t))) usbCount++; + if(types.includes('MB') && types.includes('SPEAKER')) audConn = true; + if((types.includes('MB') || types.includes('GPU')) && types.includes('MONITOR')) dispConn = true; + }); + + const isBootable = (hasMB && hasCPU && hasCooler && hasRAM && hasPSU && hasStorage && mbPwr && (hasGPU ? gpuPwr : true) && dispConn); + + specsContainer.innerHTML = ` +
Core System
+
CHASSIS${hasCase ? 'OK' : 'ERR'}
+
MOTHERBOARD${hasMB ? 'OK' : 'ERR'}
+
CPU${hasCPU ? 'OK' : 'ERR'}
+
COOLING${hasCooler ? 'OK' : 'ERR'}
+
MEMORY${hasRAM ? 'OK' : 'ERR'}
+
POWER SPLY${hasPSU ? 'OK' : 'ERR'}
+
Connections
+
MB POWER${mbPwr ? 'OK' : 'ERR'}
+
STORAGE${hasStorage ? 'OK' : 'ERR'}
+
GPU POWER${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}
+
DISPLAY${dispConn ? 'OK' : 'ERR'}
+
USB DEVS${usbCount}
+
+
+ ${isBootable ? 'BOOTING...' : 'HALTED'} +
+ `; + } + + /* --- Node Creation & Snapping --- */ + function createNodeElement(node) { + const el = document.createElement('div'); + el.className = `pb-node`; el.dataset.id = node.id; + el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; + el.style.width = `${PC_PARTS[node.type].w}px`; el.style.height = `${PC_PARTS[node.type].h}px`; + el.style.zIndex = PC_PARTS[node.type].z; + + let innerHTML = `${PC_PARTS[node.type].svg}`; + PC_PARTS[node.type].ports.forEach(p => { + innerHTML += `
`; + }); + + // Debug Labels for bare parts + if(node.type !== 'CASE' && node.type !== 'MB') { + innerHTML += `
${node.type}
`; + } + + el.innerHTML = innerHTML; + viewport.appendChild(el); + node.el = el; + return el; + } + + function spawnNode(type, dropX = null, dropY = null) { + const id = `node_${nextNodeId++}`; + const x = dropX !== null ? dropX : 300 + Math.random()*40; + const y = dropY !== null ? dropY : 150 + Math.random()*40; + + const node = { id, type, x, y, snappedTo: null, el: null }; + if (PC_PARTS[type].slots) node.slots = { ...PC_PARTS[type].slots }; // Copy slots schema, values will be filled with IDs + + // Reset slot values to null + if(node.slots) { + for(let k in node.slots) { node.slots[k] = null; } + } + + nodes[id] = node; + createNodeElement(node); + evaluateBuild(); + } + + // Recursive movement to handle nested snaps (MB inside CASE inside ...) + function moveNodeRecursive(nodeId, dx, dy) { + const n = nodes[nodeId]; + if(!n) return; + n.x += dx; n.y += dy; + if(n.slots) { + Object.keys(n.slots).forEach(k => { + if(typeof n.slots[k] === 'string') moveNodeRecursive(n.slots[k], dx, dy); + }); + } + } + + /* --- Inspect Mode --- */ + let inspectZoom = 1, inspectRotX = 0, inspectRotY = 0; + workspace.addEventListener('dblclick', (e) => { + const nodeEl = e.target.closest('.pb-node'); + if (nodeEl) { + const node = nodes[nodeEl.dataset.id]; + document.getElementById('inspectModal').classList.add('active'); + document.getElementById('inspectObject').innerHTML = `${PC_PARTS[node.type].svg}`; + document.getElementById('inspectName').innerText = PC_PARTS[node.type].name; + inspectZoom = 1.5; inspectRotX = 0; inspectRotY = 0; updateInspectTransform(); clearSelection(); + } + }); + document.getElementById('inspectStage')?.addEventListener('mousemove', (e) => { + const rect = e.currentTarget.getBoundingClientRect(); + inspectRotY = (e.clientX - rect.left - rect.width/2) / 5; + inspectRotX = -(e.clientY - rect.top - rect.height/2) / 5; + updateInspectTransform(); + }); + document.getElementById('inspectStage')?.addEventListener('wheel', (e) => { + e.preventDefault(); inspectZoom += e.deltaY < 0 ? 0.1 : -0.1; + inspectZoom = Math.max(0.5, Math.min(inspectZoom, 4)); updateInspectTransform(); + }); + function updateInspectTransform() { const obj = document.getElementById('inspectObject'); if(obj) obj.style.transform = `scale(${inspectZoom}) rotateX(${inspectRotX}deg) rotateY(${inspectRotY}deg)`; } + document.getElementById('inspectClose')?.addEventListener('click', () => { document.getElementById('inspectModal').classList.remove('active'); }); + + /* --- Interaction --- */ + document.getElementById("btnZoomIn")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1.2, r.width/2, r.height/2); }); + document.getElementById("btnZoomOut")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1/1.2, r.width/2, r.height/2); }); + document.getElementById("btnZoomReset")?.addEventListener('click', () => { panX = 0; panY = 0; zoom = 1; updateViewport(); }); + + workspace.addEventListener('wheel', (e) => { e.preventDefault(); const wsRect = workspace.getBoundingClientRect(); zoomWorkspace(e.deltaY < 0 ? 1.1 : (1/1.1), e.clientX - wsRect.left, e.clientY - wsRect.top); }); + + workspace.addEventListener('mousedown', (e) => { + const port = e.target.closest('.pb-port'); + if (port) { + const nodeEl = port.closest('.pb-node'); + const portId = port.dataset.port; + const existingIdx = connections.findIndex(c => (c.toNode === nodeEl.dataset.id && c.toPort === portId) || (c.fromNode === nodeEl.dataset.id && c.fromPort === portId)); + if (existingIdx !== -1) { connections.splice(existingIdx, 1); evaluateBuild(); renderWires(); return; } + const coords = getPortCoords(nodeEl.dataset.id, portId); + wiringStart = { node: nodeEl.dataset.id, port: portId, x: coords.x, y: coords.y }; + tempWirePath = { x: coords.x, y: coords.y }; return; + } + + const wire = e.target.closest('.pb-wire'); + if (wire && wire.dataset.connId) { clearSelection(); selectedWireId = wire.dataset.connId; renderWires(); e.stopPropagation(); return; } + + const nodeEl = e.target.closest('.pb-node'); + if (nodeEl) { + clearSelection(); selectedNodeId = nodeEl.dataset.id; nodeEl.classList.add('selected'); isDraggingNode = nodeEl.dataset.id; + const rect = nodeEl.getBoundingClientRect(); dragOffset = { x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom }; + + // Unsnap from parent when picked up + const node = nodes[isDraggingNode]; + if (node.snappedTo) { + const parent = nodes[node.snappedTo.id]; + if (parent && parent.slots[node.snappedTo.key] === node.id) parent.slots[node.snappedTo.key] = null; + node.snappedTo = null; + node.el.style.zIndex = PC_PARTS[node.type].z; // Reset Z + evaluateBuild(); + } + return; + } + + clearSelection(); isPanning = true; panStart = { x: e.clientX - panX, y: e.clientY - panY }; + }); + + window.addEventListener('mousemove', (e) => { + const wsRect = workspace.getBoundingClientRect(); + if (isPanning) { panX = e.clientX - panStart.x; panY = e.clientY - panStart.y; updateViewport(); return; } + if (isDraggingNode) { + const node = nodes[isDraggingNode]; + let newX = (e.clientX - wsRect.left - panX) / zoom - dragOffset.x; + let newY = (e.clientY - wsRect.top - panY) / zoom - dragOffset.y; + moveNodeRecursive(node.id, newX - node.x, newY - node.y); + updateNodePositions(); + } + if (wiringStart) { tempWirePath = { x: (e.clientX - wsRect.left - panX) / zoom, y: (e.clientY - wsRect.top - panY) / zoom }; renderWires(); } + }); + + window.addEventListener('mouseup', (e) => { + if (isDraggingNode) { + const node = nodes[isDraggingNode]; + let snapped = false; + + // Check all other nodes for compatible slots + Object.values(nodes).forEach(target => { + if (target.slots && !snapped && target.id !== node.id) { + for(let slotKey in target.slots) { + let slotDef = PC_PARTS[target.type].slots[slotKey]; + if(slotDef.accepts === node.type && target.slots[slotKey] === null) { + let tX = target.x + slotDef.x; let tY = target.y + slotDef.y; + if (Math.hypot(node.x - tX, node.y - tY) < 80) { + moveNodeRecursive(node.id, tX - node.x, tY - node.y); + node.snappedTo = { id: target.id, key: slotKey }; + target.slots[slotKey] = node.id; + node.el.style.zIndex = PC_PARTS[target.type].z + 5; // Layer above parent + snapped = true; break; + } + } + } + } + }); + isDraggingNode = null; updateNodePositions(); evaluateBuild(); + } + + if (wiringStart) { + const port = e.target.closest('.pb-port'); + if (port) { + const targetNodeId = port.closest('.pb-node').dataset.id; + const targetPortId = port.dataset.port; + if (targetNodeId !== wiringStart.node) { connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: wiringStart.port, toNode: targetNodeId, toPort: targetPortId }); } + } + wiringStart = null; tempWirePath = null; evaluateBuild(); renderWires(); + } + isPanning = false; + }); + + /* --- Deletion (Recursive) --- */ + function deleteNodeRecursive(id) { + const n = nodes[id]; if(!n) return; + if(n.slots) { Object.keys(n.slots).forEach(k => { if(typeof n.slots[k] === 'string') deleteNodeRecursive(n.slots[k]); }); } + if(n.snappedTo) { const p = nodes[n.snappedTo.id]; if(p) p.slots[n.snappedTo.key] = null; } + connections = connections.filter(c => c.fromNode !== id && c.toNode !== id); + viewport.removeChild(n.el); delete nodes[id]; + } + + window.addEventListener('keydown', (e) => { + if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeId) { + deleteNodeRecursive(selectedNodeId); clearSelection(); evaluateBuild(); renderWires(); + } + if ((e.key === 'Delete' || e.key === 'Backspace') && selectedWireId) { + connections = connections.filter(c => c.id !== selectedWireId); clearSelection(); evaluateBuild(); renderWires(); + } + }); + + workspace.addEventListener('dragover', (e) => { e.preventDefault(); }); + workspace.addEventListener('drop', (e) => { + e.preventDefault(); + const type = e.dataTransfer.getData('spawnType'); + if (type) { + const r = workspace.getBoundingClientRect(); + spawnNode(type, (e.clientX - r.left - panX) / zoom - (PC_PARTS[type].w / 2), (e.clientY - r.top - panY) / zoom - (PC_PARTS[type].h / 2)); + } + }); + + btnClearBoard?.addEventListener('click', () => { + viewport.querySelectorAll('.pb-node').forEach(el => el.remove()); + nodes = {}; connections = []; evaluateBuild(); renderWires(); + }); + + toolboxToggle?.addEventListener("click", () => { + const c = pcPage?.classList.contains("toolboxCollapsed"); + pcPage.classList.toggle("toolboxCollapsed", !c); + toolboxToggle?.setAttribute("aria-expanded", c ? "true" : "false"); + }); + + initToolbox(); evaluateBuild(); +})(); \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css index 0a6b418..ec816f0 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -47,39 +47,9 @@ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(-- .siteFooter { border-top: 1px solid var(--line); background: rgba(0,0,0,.08); } .footerInner { max-width: 1400px; margin: 0 auto; padding: 18px 20px; color: var(--muted); font-size: 12px; letter-spacing: .08em; display: flex; flex-direction: column; gap: 6px; } -/* --- APP LAYOUT --- */ -.binaryPage { - --toolbox-w: 360px; - --toolbox-gap: 22px; - --toolbox-toggle-top: calc(var(--nav-h) + 16px); - --toolbox-top: calc(var(--toolbox-toggle-top) + 60px); - position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column; -} -.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); } -.binaryPage.toolboxCollapsed { padding-right: 0; } -.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; } -.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; } - -/* --- READOUT FORMATTING --- */ -.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; } -.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; } -.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; } -.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; } -.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; } - -.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; } -.denaryValue { font-size: 56px; } -.hexValue { font-size: 48px; } -.binaryValue { font-size: 40px; } +/* --- SHARED UI COMPONENTS (Used by ALL Simulators) --- */ .divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; } -/* --- GRIDS & BITS --- */ -.bitsWrap { width: 100%; } -.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; } -.bitsGrid.bitsFew { justify-content: center; } -.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; } -.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; } - .bulb { width: 44px; height: 44px; color: rgba(255,255,255,.15); margin-bottom: 8px; flex-shrink: 0; transition: 0.2s ease; background: transparent; display: flex; align-items: center; justify-content: center; } .bulb svg { width: 100%; height: 100%; display: block; } .bulb.on { color: #ffd86b !important; filter: drop-shadow(0 0 14px rgba(255, 216, 107, 1)) !important; } @@ -92,99 +62,19 @@ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(-- .switch input:checked + .slider { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.30); } .switch input:checked + .slider::before { transform: translateX(28px); } -/* --- HEXADECIMAL --- */ -.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; } -.hexGrid.bitsFew { justify-content: center; } -.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; } - -/* --- HEX COLOURS SPECIFIC --- */ -.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; } -.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; } -.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; } -.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; } -.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; } -.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); } - -.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; } -.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; } -.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; } - -/* HEX CARD */ -.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); } -.hexCardButtons { display: flex; gap: 10px; } -.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; } -.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); } -.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); } -.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); } -.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); } -.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; } -.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; } -.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; } -.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; } -.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); } -.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; } - - -/* --- TOOLBOX --- */ -.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; } -.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); } -.toolboxToggle:hover { border-color: rgba(255,255,255,.22); } -.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; } -.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; } .card { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 16px; padding: 16px; backdrop-filter: blur(10px); } .cardTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 18px; color: rgba(232,232,238,.9); margin-bottom: 12px; } .hint { font-family: var(--bit-font); font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: rgba(232,232,238,.55); margin-top: 10px; line-height: 1.35; } -.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; } -.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); } -.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); } -.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; } -.bitWidthRow { display: flex; align-items: center; gap: 10px; } -.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; } -.miniBtn:hover { border-color: rgba(255,255,255,.22); } -.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; } -.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; } -.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; } + .btn { border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); border-radius: 12px; padding: 10px 12px; font-family: var(--bit-font); font-size: 14px; letter-spacing: .12em; text-transform: uppercase; font-weight: 900; cursor: pointer; } .btn:hover { border-color: rgba(255,255,255,.22); } .btnAccent { background: rgba(40,240,122,.12); border-color: rgba(40,240,122,.22); } .btnAccent:hover { border-color: rgba(40,240,122,.35); } .btnHalf { width: calc(50% - 6px); } .btnWide { width: 100%; } -.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; } -.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } -.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; } -.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); } -.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); } .btnReset { color: rgba(232,232,238,.95); } .btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); } -/* === CONTAINER QUERIES === */ -@container (max-width: 1050px) { - .readoutContainer { gap: 40px; } - .colorGroupWrap { gap: 10px; } - .colorGroup { padding: 10px; gap: 8px; border-radius: 16px; } - .hexCard { padding: 12px 8px; width: 140px; gap: 12px; } - .hexDigitDisplay { font-size: 40px; } - .hexNibbleBulb { width: 24px !important; height: 24px !important; } - .hexNibbleLabel { font-size: 20px; } - .hexColWeight { font-size: 26px; margin-top: 10px; } - .hexCardBtn { width: 34px; height: 34px; font-size: 14px; } -} - -@container (max-width: 800px) { - .readoutContainer { flex-direction: column; gap: 24px; } - .colorPreviewSide { padding-top: 0; } - .colorGroupWrap { gap: 6px; } - .colorGroup { padding: 6px; gap: 6px; border-radius: 12px; } - .hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; } - .hexDigitDisplay { font-size: 32px; } - .hexNibbleBulb { width: 16px !important; height: 16px !important; } - .hexNibbleLabel { font-size: 16px; } - .hexColWeight { font-size: 20px; margin-top: 6px; } - .hexCardBtn { width: 28px; height: 28px; font-size: 12px; } - .denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; } -} - -@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } } -@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } } \ No newline at end of file +.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top, calc(var(--nav-h) + 16px)); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; } +.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); } +.toolboxToggle:hover { border-color: rgba(255,255,255,.22); } \ No newline at end of file diff --git a/src/styles/number-simulators.css b/src/styles/number-simulators.css new file mode 100644 index 0000000..68dc7d5 --- /dev/null +++ b/src/styles/number-simulators.css @@ -0,0 +1,114 @@ +/* --- APP LAYOUT --- */ +.binaryPage { + --toolbox-w: 360px; + --toolbox-gap: 22px; + --toolbox-toggle-top: calc(var(--nav-h) + 16px); + --toolbox-top: calc(var(--toolbox-toggle-top) + 60px); + position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column; +} +.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); } +.binaryPage.toolboxCollapsed { padding-right: 0; } +.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; } +.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; } + +/* --- READOUT FORMATTING --- */ +.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; } +.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; } +.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; } +.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; } + +.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; } +.denaryValue { font-size: 56px; } +.hexValue { font-size: 48px; } +.binaryValue { font-size: 40px; } + +/* --- GRIDS & BITS --- */ +.bitsWrap { width: 100%; } +.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; } +.bitsGrid.bitsFew { justify-content: center; } +.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; } +.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; } + +/* --- HEXADECIMAL --- */ +.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; } +.hexGrid.bitsFew { justify-content: center; } +.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; } + +/* --- HEX COLOURS SPECIFIC --- */ +.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; } +.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; } +.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; } +.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; } +.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); } + +.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; } +.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; } +.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; } + +/* HEX CARD */ +.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); } +.hexCardButtons { display: flex; gap: 10px; } +.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; } +.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); } +.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); } +.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); } +.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); } +.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; } +.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; } +.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; } +.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; } +.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); } +.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; } + +/* --- TOOLBOX COMPONENTS FOR NUMBERS --- */ +.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; } +.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; } + +.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; } +.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); } +.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); } +.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; } +.bitWidthRow { display: flex; align-items: center; gap: 10px; } +.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; } +.miniBtn:hover { border-color: rgba(255,255,255,.22); } +.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; } +.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; } +.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; } +.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; } +.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } +.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; } +.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); } +.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); } + +/* === CONTAINER QUERIES === */ +@container (max-width: 1050px) { + .readoutContainer { gap: 40px; } + .colorGroupWrap { gap: 10px; } + .colorGroup { padding: 10px; gap: 8px; border-radius: 16px; } + .hexCard { padding: 12px 8px; width: 140px; gap: 12px; } + .hexDigitDisplay { font-size: 40px; } + .hexNibbleBulb { width: 24px !important; height: 24px !important; } + .hexNibbleLabel { font-size: 20px; } + .hexColWeight { font-size: 26px; margin-top: 10px; } + .hexCardBtn { width: 34px; height: 34px; font-size: 14px; } +} + +@container (max-width: 800px) { + .readoutContainer { flex-direction: column; gap: 24px; } + .colorPreviewSide { padding-top: 0; } + .colorGroupWrap { gap: 6px; } + .colorGroup { padding: 6px; gap: 6px; border-radius: 12px; } + .hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; } + .hexDigitDisplay { font-size: 32px; } + .hexNibbleBulb { width: 16px !important; height: 16px !important; } + .hexNibbleLabel { font-size: 16px; } + .hexColWeight { font-size: 20px; margin-top: 6px; } + .hexCardBtn { width: 28px; height: 28px; font-size: 12px; } + .denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; } +} + +@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } } +@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } } \ No newline at end of file diff --git a/src/styles/pc-builder.css b/src/styles/pc-builder.css new file mode 100644 index 0000000..df4e5ff --- /dev/null +++ b/src/styles/pc-builder.css @@ -0,0 +1,120 @@ +/* === FULL PAGE OVERRIDES FOR PC BUILDER === */ +body:has(#pcPage) { overflow: hidden; } +body:has(#pcPage) .pageWrap { + max-width: 100% !important; padding: 0 !important; margin: 0 !important; + height: calc(100vh - var(--nav-h)); display: flex; flex-direction: column; +} +#pcPage { padding: 0 !important; margin: 0 !important; } + +/* === MAIN CONTAINER === */ +.pb-container { flex: 1; display: flex; flex-direction: column; position: relative; width: 100%; height: 100%; overflow: hidden; } + +/* === FIXED HEADER === */ +.pb-top-header { + width: 100%; text-align: center; padding: 8px 20px 8px; + background: var(--bg); z-index: 10; flex-shrink: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; + border-bottom: 1px solid rgba(255,255,255,0.08); +} +.pb-title { font-family: var(--bit-font); font-size: 32px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text); margin: 0 0 2px 0; line-height: 1; } +.pb-subtitle { color: var(--muted); font-size: 14px; font-family: var(--ui-font); font-weight: 500; margin: 0; line-height: 1.2; } +.pb-subtitle kbd { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; font-family: var(--ui-font); color: #e8e8ee; } + +/* === DYNAMIC CANVAS === */ +.pb-workspace { + flex: 1; position: relative; width: 100%; background-color: transparent; + background-image: radial-gradient(rgba(255,255,255,0.08) 2px, transparent 2px); + background-size: 32px 32px; overflow: hidden; cursor: grab; +} +.pb-workspace:active { cursor: grabbing; } +.pb-viewport { position: absolute; inset: 0; width: 100%; height: 100%; transform-origin: 0 0; pointer-events: none; } + +/* Zoom UI Controls */ +.pb-zoom-controls { position: absolute; bottom: 20px; left: 20px; z-index: 100; display: flex; gap: 8px; } +.pb-zoom-btn { + width: 40px; height: 40px; border-radius: 8px; font-family: var(--ui-font); font-size: 22px; font-weight: 800; + display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); + border: 1px solid rgba(255,255,255,0.15); color: #e8e8ee; cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s; +} +.pb-zoom-btn:hover { background: rgba(255,255,255,0.1); border-color: #55aaff; color: #55aaff; } + +/* Wires sit at the VERY FRONT so they are never hidden in the case */ +.pb-svg-layer { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 100; pointer-events: none; } + +/* Cables */ +.pb-wire { + stroke: rgba(255,255,255,0.25); stroke-width: 6; fill: none; stroke-linecap: round; + transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease; pointer-events: stroke; cursor: pointer; +} +.pb-wire:hover { stroke: rgba(255,255,255,0.6); stroke-width: 10; } +.pb-wire.active { stroke: #55aaff; filter: drop-shadow(0 0 6px rgba(85,170,255,0.6)); } +.pb-wire.active:hover { stroke: #88ccff; } +.pb-wire.selected { stroke: #ff5555 !important; stroke-width: 8 !important; stroke-dasharray: 8 8; filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; animation: wireDash 1s linear infinite; } +@keyframes wireDash { to { stroke-dashoffset: -16; } } +.pb-wire-temp { stroke: rgba(255,255,255,0.4); stroke-dasharray: 8 8; pointer-events: none; } + +/* PC Parts */ +.pb-node { + position: absolute; background: transparent; border: none; border-radius: 0; padding: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: grab; + user-select: none; transition: filter 0.2s; pointer-events: auto; +} +.pb-node:active { cursor: grabbing; } +.pb-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); } +.pb-part-svg { width: 100%; height: 100%; display: block; pointer-events: none; filter: drop-shadow(0 10px 15px rgba(0,0,0,0.5)); } + +/* Connection Ports */ +.pb-port { + width: 14px; height: 14px; background: #222; border-radius: 50%; cursor: crosshair; + border: 2px solid #55aaff; box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s; + position: absolute; z-index: 200; transform: translate(-50%, -50%); pointer-events: auto; +} +.pb-port:hover { transform: translate(-50%, -50%) scale(1.4); background: #fff; } +.pb-port.active { background: #55aaff; box-shadow: 0 0 12px rgba(85,170,255,0.8); } + +/* === ANIMATIONS (Triggered on Boot) === */ +@keyframes spin { 100% { transform: rotate(360deg); } } +.system-running .fan-blades { animation: spin 0.4s linear infinite; } +.system-running .monitor-screen { opacity: 1 !important; } + +/* === FLOATING TOOLBOX === */ +.pb-toolbox { + position: absolute; top: 60px; right: 20px; bottom: 20px; width: var(--toolbox-w, 360px); + z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); + transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; overflow-y: auto; pointer-events: auto; padding-right: 6px; +} +.pb-toolbox::-webkit-scrollbar { width: 6px; } +.pb-toolbox::-webkit-scrollbar-track { background: transparent; } +.pb-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; } +.pb-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); } +.pb-container.toolboxCollapsed .pb-toolbox { transform: translateX(calc(100% + 40px)); opacity: 0; pointer-events: none; } + +.tb-icon-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.tb-icon-box { + background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; width: 100%; padding: 8px 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; cursor: grab; transition: background 0.2s, border-color 0.2s; +} +.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); } +.tb-icon-label { font-family: var(--ui-font); font-size: 10px; font-weight: 800; color: var(--text); letter-spacing: 0px; text-transform: uppercase; text-align: center;} + +/* Diagnostics Panel */ +.specs-panel { width: 100%; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.4); padding: 12px; } +.diag-cat { font-family: var(--ui-font); font-size: 12px; font-weight: 800; color: #55aaff; letter-spacing: 1px; text-transform: uppercase; margin: 12px 0 4px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 2px;} +.diag-cat:first-child { margin-top: 0; } +.diag-row { + display: flex; justify-content: space-between; font-family: var(--bit-font); font-size: 16px; + letter-spacing: 1px; text-transform: uppercase; margin-bottom: 4px; +} + +/* === 3D INSPECT MODAL === */ +.inspect-modal { + position: fixed; inset: 0; background: rgba(10,11,15,0.95); z-index: 1000; + display: flex; align-items: center; justify-content: center; flex-direction: column; + opacity: 0; pointer-events: none; transition: opacity 0.3s ease; backdrop-filter: blur(10px); +} +.inspect-modal.active { opacity: 1; pointer-events: auto; } +.inspect-close { position: absolute; top: 30px; right: 40px; font-size: 40px; color: var(--muted); cursor: pointer; transition: color 0.2s; } +.inspect-close:hover { color: #ff5555; } +.inspect-stage { width: 600px; height: 600px; perspective: 1200px; display: flex; align-items: center; justify-content: center; margin-top: 20px;} +.inspect-object { width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.1s cubic-bezier(0.2, 0.8, 0.2, 1); display: flex; align-items: center; justify-content: center; } +.inspect-object svg { width: 100%; height: 100%; filter: drop-shadow(0 30px 40px rgba(0,0,0,0.8)); } \ No newline at end of file