Compare commits

..

4 Commits

Author SHA1 Message Date
09e0499ba3 Full initial 2.0 build
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 31s
Signed-off-by: Alexander Lyall <alex@adcm.uk>
2026-03-01 17:30:41 +00:00
7ecd065305 Updates to canvas of logic gate simulator:
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 29s
- Addition of zoom feature

Signed-off-by: Alexander Lyall <alex@adcm.uk>
2026-03-01 16:37:02 +00:00
e0e72c17e8 Fully functional logic gates page
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 28s
Signed-off-by: Alexander Lyall <alex@adcm.uk>
2026-03-01 16:32:27 +00:00
ffab71cfcc Completed Wave 3 features:
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 27s
- [X] New User Interface (Responsive)
- [X] Two's Compliment Simulator
- [X] Extended Binary Simulator (Custom bit sizes)
- [X] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [X] Extended Hexadecimal Simulator
- [X] Unified Hexadecimal Simulator (For GCSE & A Level Specification)
- [X] Enhanced Gate Simulator (Truth Table Creator)
- [X] Compound Gate Simulator
- [ ] Computer Components Simulator

Signed-off-by: Alexander Lyall <alex@adcm.uk>
2026-03-01 16:22:58 +00:00
69 changed files with 3900 additions and 4500 deletions

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -95,14 +95,14 @@ jobs:
shell: bash
run: |
set -e
if [ ! -d "export" ]; then
echo "❌ export/ folder not found in repo root"
if [ ! -d "dist" ]; then
echo "❌ dist/ folder not found in repo root"
ls -la
exit 1
fi
rm -f "Computing:Box Website.zip"
(cd export && zip -r "../Computing:Box Website.zip" .)
(cd dist && zip -r "../Computing:Box Website.zip" .)
test -s "Computing:Box Website.zip"
ls -lh "Computing:Box Website.zip"

View File

@@ -1,235 +0,0 @@
name: Changelog + Release on main
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
changelog_and_release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Stop if this is the bot changelog commit
shell: bash
run: |
set -e
msg="$(git log -1 --pretty=%B)"
echo "$msg" | tr -d '\r' | grep -qi "\[skip ci\]" && {
echo "Skipping (bot commit with [skip ci])"
exit 0
} || true
- name: Install git-cliff
shell: bash
run: |
set -e
GIT_CLIFF_VERSION="2.11.0"
URL="https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -L "$URL" -o /tmp/git-cliff.tar.gz
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
git-cliff --version
- name: Generate CHANGELOG.md (Keep a Changelog)
shell: bash
run: |
set -e
git-cliff --config cliff.toml --output CHANGELOG.md
test -s CHANGELOG.md
- name: Commit and push CHANGELOG.md if changed (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
if git diff --quiet -- CHANGELOG.md; then
echo "No changelog changes."
else
git config user.name "changelog-bot"
git config user.email "changelog-bot@users.noreply.local"
git add CHANGELOG.md
git commit -m "docs(changelog): update changelog [skip ci]"
origin_url="$(git remote get-url origin)"
# Convert SSH origin to HTTPS if needed
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
git push "$authed_url" HEAD:main
fi
- name: Extract newest changelog section for release body
shell: bash
run: |
set -e
# Extract the first "## ..." section (newest section) from CHANGELOG.md
# Includes the "## ..." heading and everything until the next "## ..." heading.
awk '
/^## / { if (seen) exit; seen=1 }
seen { print }
' CHANGELOG.md > RELEASE_NOTES.md
# Clean trailing whitespace/newlines a bit
sed -i 's/[[:space:]]*$//' RELEASE_NOTES.md
test -s RELEASE_NOTES.md
echo "---- RELEASE_NOTES.md ----"
head -n 60 RELEASE_NOTES.md
echo "--------------------------"
- name: Create export zip (Computing:Box Website.zip)
shell: bash
run: |
set -e
if [ ! -d "export" ]; then
echo "❌ export/ folder not found in repo root"
ls -la
exit 1
fi
rm -f "Computing:Box Website.zip"
(cd export && zip -r "../Computing:Box Website.zip" .)
test -s "Computing:Box Website.zip"
ls -lh "Computing:Box Website.zip"
- name: Prepare YY.MM.DD letter-suffix tag + release name
shell: bash
run: |
set -e
# Version: YY.MM.DD (UTC). Swap to `date +...` if you prefer UK-local runner time.
VERSION="$(date -u +'%y.%m.%d')"
PREFIX="v${VERSION}."
last_letter="$(
git tag --list "${PREFIX}[a-z]" \
| sed -E "s/^${PREFIX}([a-z])$/\1/" \
| sort \
| tail -n 1
)"
if [ -z "$last_letter" ]; then
next_letter="a"
else
if [ "$last_letter" = "z" ]; then
echo "❌ Already have v${VERSION}.z today. Refusing to create more than 26 releases/day."
exit 1
fi
next_letter="$(printf "%b" "$(printf '\\%03o' "$(( $(printf '%d' "'$last_letter") + 1 ))")")"
fi
TAG="${PREFIX}${next_letter}"
RELEASE_NAME="Computing:Box v${VERSION}.${next_letter}"
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV"
echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV"
echo "Using tag: $TAG"
echo "Release name: $RELEASE_NAME"
- name: Create and push tag (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
git tag -f "$TAG"
origin_url="$(git remote get-url origin)"
# Convert SSH origin to HTTPS if needed
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
git push "$authed_url" "refs/tags/$TAG" --force
- name: Create Gitea release + upload asset (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
origin_url="$(git remote get-url origin)"
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
base="$(echo "$origin_url" | sed -E 's#(https?://[^/]+)/.*#\1#')"
repo_path="$(echo "$origin_url" | sed -E 's#https?://[^/]+/##')"
repo_path="$(echo "$repo_path" | sed -E 's/\.git$//')"
owner="$(echo "$repo_path" | cut -d/ -f1)"
repo="$(echo "$repo_path" | cut -d/ -f2-)"
api="$base/api/v1"
python3 - <<'PY'
import json, os
tag = os.environ["TAG"]
name = os.environ["RELEASE_NAME"]
with open("RELEASE_NOTES.md", "r", encoding="utf-8") as f:
body = f.read()
payload = {
"tag_name": tag,
"target_commitish": "main",
"name": name,
"body": body, # newest section only
"draft": False,
"prerelease": False,
}
with open("release.json", "w", encoding="utf-8") as f:
json.dump(payload, f)
PY
curl -sS -X POST \
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
-H "Content-Type: application/json" \
"${api}/repos/${owner}/${repo}/releases" \
--data-binary @release.json \
-o release_response.json
release_id="$(python3 - <<'PY'
import json
with open("release_response.json","r",encoding="utf-8") as f:
data=json.load(f)
rid=data.get("id")
if not rid:
raise SystemExit("No release id returned. Response:\n" + json.dumps(data, indent=2))
print(rid)
PY
)"
echo "Created release id: $release_id"
curl -sS -X POST \
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
"${api}/repos/${owner}/${repo}/releases/${release_id}/assets?name=Computing%3ABox%20Website.zip" \
-F "attachment=@${ZIP_PATH}" \
>/dev/null
echo "✅ Release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded"

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# build output
# dist/
dist/
# generated types
.astro/

View File

@@ -28,15 +28,18 @@ An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Com
- [X] XNOR Gate Simulator
### Wave 3 CS:Box Features (Spring 2026)
- [ ] New User Interface (Responsive)
- [X] Two's Compliment Simulator
- [ ] Extended Binary Simulator (Custom bit sizes)
- [ ] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [X] Extended Binary Simulator (Custom bit sizes)
- [X] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [ ] Extended Hexadecimal Simulator
- [ ] Unified Hexadecimal Simulator (For GCSE & A Level Specification)
- [ ] Enhanced Gate Simulator (Truth Table Creator)
- [ ] Compound Gate Simulator
- [ ] Computer Components Simulator
## Version 1.0 Release Date: 1<sup>st</sup> September 2025
## Version 2.0 Release Date (Goal): 1<sup>st</sup> April 2026
## Version 2.0 Release Date (Goal): 1<sup>st</sup> May 2026
Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
(()=>{const d=document.getElementById("bitsGrid"),h=document.getElementById("denaryNumber"),M=document.getElementById("binaryNumber"),f=document.getElementById("bitsInput"),m=document.getElementById("modeToggle"),E=document.getElementById("modeHint"),T=document.getElementById("lblUnsigned"),k=document.getElementById("lblTwos"),H=document.getElementById("btnCustomBinary"),G=document.getElementById("btnCustomDenary"),P=document.getElementById("btnShiftLeft"),V=document.getElementById("btnShiftRight"),q=document.getElementById("btnDec"),Z=document.getElementById("btnInc"),z=document.getElementById("btnClear"),I=document.getElementById("btnRandom"),O=document.getElementById("btnBitsUp"),W=document.getElementById("btnBitsDown"),R=document.getElementById("toolboxToggle"),w=document.getElementById("binaryPage");let i=b(Number(f?.value??8),1,64),s=new Array(i).fill(!1),u=null;function b(t,n,e){return Number.isFinite(t)?Math.max(n,Math.min(e,Math.trunc(t))):n}function l(){return!!m?.checked}function a(t){return 1n<<BigInt(t)}function y(t){return a(t)}function $(t){return a(t)-1n}function B(t){return-a(t-1)}function p(t){return a(t-1)-1n}function v(){let t=0n;for(let n=0;n<i;n++)s[n]&&(t+=a(n));return t}function g(t){const n=y(i),e=(t%n+n)%n;for(let o=0;o<i;o++)s[o]=(e>>BigInt(o)&1n)===1n}function L(){const t=v();return s[i-1]===!0?t-a(i):t}function x(t){const n=a(i);let e=t;e=(e%n+n)%n,g(e)}function j(){let t="";for(let n=i-1;n>=0;n--){t+=s[n]?"1":"0";const e=i-n;n!==0&&e%4===0&&(t+=" ")}return t.trimEnd()}function D(){E&&(l()?E.textContent="Tip: In two's complement, the left-most bit (MSB) represents a negative value.":E.textContent="Tip: In unsigned binary, all bits represent positive values.")}function A(){if(!d)return;const t=d.parentElement;if(!t)return;const n=t.getBoundingClientRect().width,o=b(Math.floor(n/100),1,12);d.style.setProperty("--cols",String(Math.min(o,i)))}function C(t){i=b(t,1,64),f&&(f.value=String(i));const n=s.slice();s=new Array(i).fill(!1);for(let e=0;e<Math.min(n.length,i);e++)s[e]=n[e];d.innerHTML="",d.classList.toggle("bitsFew",i<8);for(let e=i-1;e>=0;e--){const o=document.createElement("div");o.className="bit",o.innerHTML=`
<div class="bulb" id="bulb-${e}" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
</svg>
</div>
<div class="bitVal" id="bitLabel-${e}"></div>
<label class="switch" aria-label="Toggle bit ${e}">
<input type="checkbox" data-index="${e}">
<span class="slider"></span>
</label>
`,d.appendChild(o)}d.querySelectorAll('input[type="checkbox"]').forEach(e=>{e.addEventListener("change",()=>{const o=Number(e.dataset.index);s[o]=e.checked,r()})}),A(),r()}function J(){for(let t=0;t<i;t++){const n=document.getElementById(`bitLabel-${t}`);if(!n)continue;let e;l()&&t===i-1?e=`-${a(i-1).toString()}`:e=a(t).toString(),n.textContent=e,n.style.setProperty("--len",e.length)}}function K(){d.querySelectorAll('input[type="checkbox"]').forEach(t=>{const n=Number(t.dataset.index);t.checked=!!s[n]})}function Q(){for(let t=0;t<i;t++){const n=document.getElementById(`bulb-${t}`);n&&n.classList.toggle("on",s[t]===!0)}}function X(){!h||!M||(l()?h.textContent=L().toString():h.textContent=v().toString(),M.textContent=j())}function r(){D(),T&&k&&(T.classList.toggle("activeMode",!l()),k.classList.toggle("activeMode",l())),J(),K(),Q(),X()}function Y(t){const n=String(t??"").replace(/\s+/g,"");if(!/^[01]+$/.test(n))return!1;const e=n.slice(-i).padStart(i,"0");for(let o=0;o<i;o++){const c=e[e.length-1-o];s[o]=c==="1"}return r(),!0}function _(t){const n=String(t??"").trim();if(!n)return!1;let e;try{if(!/^-?\d+$/.test(n))return!1;e=BigInt(n)}catch{return!1}if(l()){const o=B(i),c=p(i);if(e<o||e>c)return!1;x(e)}else{if(e<0n||e>$(i))return!1;g(e)}return r(),!0}function tt(){for(let t=i-1;t>=1;t--)s[t]=s[t-1];s[0]=!1,r()}function nt(){const t=s[i-1];for(let n=0;n<i-1;n++)s[n]=s[n+1];s[i-1]=l()?t:!1,r()}function et(){s=[],m&&(m.checked=!1),C(8)}function it(){if(l()){const t=B(i),n=p(i);let e=L()+1n;e>n&&(e=t),x(e)}else{const t=y(i);g((v()+1n)%t)}r()}function ot(){if(l()){const t=B(i),n=p(i);let e=L()-1n;e<t&&(e=n),x(e)}else{const t=y(i);g((v()-1n+t)%t)}r()}function st(t){if(t<=0n)return 0n;const n=t.toString(2).length,e=Math.ceil(n/8);for(;;){const o=new Uint8Array(e);crypto.getRandomValues(o);let c=0n;for(const ct of o)c=c<<8n|BigInt(ct);const U=BigInt(e*8-n);if(U>0n&&(c=c>>U),c<t)return c}}function lt(){const t=y(i),n=st(t);g(n),r()}function N(t){I&&I.classList.toggle("btnRandomRunning",!!t)}function rt(){u&&(clearInterval(u),u=null),N(!0);const t=Date.now(),n=1125;u=setInterval(()=>{lt(),Date.now()-t>=n&&(clearInterval(u),u=null,N(!1))},80)}function S(t){const n=b(t,1,64);C(n)}function F(t){if(!w)return;w.classList.toggle("toolboxCollapsed",!!t);const n=!t;R?.setAttribute("aria-expanded",n?"true":"false")}m?.addEventListener("change",r),H?.addEventListener("click",()=>{const t=prompt(`Enter binary (spaces allowed). Current width: ${i} bits`);t!==null&&(Y(t)||alert("Invalid binary"))}),G?.addEventListener("click",()=>{const t=prompt(l()?`Enter denary (${B(i).toString()} to ${p(i).toString()}):`:`Enter denary (0 to ${$(i).toString()}):`);t!==null&&(_(t)||alert("Invalid denary for current mode/bit width"))}),P?.addEventListener("click",tt),V?.addEventListener("click",nt),Z?.addEventListener("click",it),q?.addEventListener("click",ot),z?.addEventListener("click",et),I?.addEventListener("click",rt),O?.addEventListener("click",()=>S(i+1)),W?.addEventListener("click",()=>S(i-1)),f?.addEventListener("change",()=>S(Number(f.value))),R?.addEventListener("click",()=>{const t=w?.classList.contains("toolboxCollapsed");F(!t)}),window.addEventListener("resize",()=>{A()}),D(),C(i),F(!1)})();

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,20 @@
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
:root{--panel-w: 360px;--gap: 22px}.wrap{max-width:1400px;margin:0 auto;padding:26px 20px 48px;position:relative}.topGrid{display:grid;grid-template-columns:1fr var(--panel-w);gap:var(--gap);align-items:start}body.toolboxClosed .topGrid{grid-template-columns:1fr}body.toolboxClosed #toolboxPanel{display:none}.mainCol{min-width:0}.readout{text-align:center;margin-top:8px}.label{opacity:.8;letter-spacing:.12em;text-transform:uppercase;font-size:12px}.num{display:inline-block;width:fit-content;max-width:100%;white-space:pre-line;letter-spacing:2px}.denaryValue{font-size:54px;margin:6px 0 10px}.binaryValue{font-size:56px;margin:4px 0 18px}.divider{height:1px;background:#ffffff1a;margin:14px auto 24px;max-width:900px}.bitsWrap{padding-top:6px}.bitsGrid{display:grid;gap:24px;justify-content:center}.bitsGrid{grid-template-columns:repeat(auto-fit,minmax(110px,1fr));max-width:1200px;margin:0 auto}.bitsGrid.bitsFew{justify-content:center}.bit{display:grid;justify-items:center;gap:8px}.bulb{font-size:32px;line-height:1;opacity:.45}.bitVal{font-size:22px;line-height:1.05;text-align:center;white-space:nowrap}.switch{position:relative;display:inline-block;width:52px;height:28px}.switch input{display:none}.slider{position:absolute;inset:0;border-radius:999px;background:#ffffff2e;border:1px solid rgba(255,255,255,.14)}.slider:before{content:"";position:absolute;height:22px;width:22px;left:3px;top:2.5px;border-radius:999px;background:#fff;transition:transform .18s ease}.switch input:checked+.slider:before{transform:translate(22px)}.toolboxToggle{position:absolute;right:20px;top:18px;z-index:20;display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid rgba(255,255,255,.14);background:#ffffff0f;color:#ffffffeb;cursor:pointer}.toolboxText{letter-spacing:.12em;font-weight:900}.panelCol{position:sticky;top:calc(var(--nav-h, 72px) + 18px);align-self:start;display:grid;gap:16px}.card{border:1px solid rgba(255,255,255,.12);border-radius:16px;background:#ffffff0d;padding:14px}.cardTitle{opacity:.8;letter-spacing:.14em;text-transform:uppercase;font-size:12px;margin-bottom:10px}.hint{opacity:.7;font-size:11px;margin-top:10px;line-height:1.35}.toggleRow{display:grid;grid-template-columns:1fr auto 1fr;gap:10px;align-items:center}.toggleLabel{font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap}.subCard{margin-top:12px;border:1px solid rgba(255,255,255,.1);border-radius:14px;background:#0000001f;padding:12px}.subTitle{opacity:.8;letter-spacing:.14em;text-transform:uppercase;font-size:11px;margin-bottom:10px}.bitWidthRow{display:grid;grid-template-columns:44px 1fr 44px;gap:10px;align-items:center}.bitInputWrap{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;padding:10px 12px;border:1px solid rgba(255,255,255,.1);border-radius:12px;background:#ffffff0a}.bitInputLabel{opacity:.75;letter-spacing:.14em;text-transform:uppercase;font-size:11px;white-space:nowrap}.bitInput{width:100%;min-width:0;background:transparent;border:none;outline:none;color:inherit;font-size:20px;text-align:right}.miniBtn{height:44px;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffe6;font-size:18px;cursor:pointer}.controlsRow{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}.btn{border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffeb;padding:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.btnWide{width:100%}.btnAccent{background:#00ff8c1f;border-color:#00ff8c38}.toolRowCentered{display:flex;justify-content:center;gap:12px;margin:10px 0 12px}.toolBtn{width:56px;height:56px;border-radius:14px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffeb;font-size:18px;cursor:pointer}.toolDec{background:#ff000024;border-color:#f003}.toolInc{background:#00ff8c24;border-color:#00ff8c33}.toolRow2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.btnReset{color:#ffffffeb}
</style></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxLabel">TOOLBOX</span> </button> <main class="wrap"> <section class="topGrid"> <!-- LEFT --> <div> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">0000 0000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Bit switches"> <div class="bitsGrid" id="bitsGrid"></div> </section> </div> <!-- RIGHT TOOLBOX --> <aside id="toolbox" class="panelCol" aria-label="Toolbox"> <!-- SETTINGS --> <div class="card"> <div class="cardTitle">Settings</div> <div class="toggleRow"> <div class="toggleLabel" id="lblUnsigned">Unsigned</div> <label class="switch" aria-label="Toggle mode"> <input id="modeToggle" type="checkbox"> <span class="slider"></span> </label> <div class="toggleLabel" id="lblTwos">Two&rsquo;s complement</div> </div> <div class="hint" id="modeHint">
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator | Computing:Box</title><script>
var _paq = window._paq = window._paq || [];
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
_paq.push(["enableCrossDomainLinking"]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.adcmnetworks.co.uk/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_astro/about.CswAWODG.css">
<link rel="stylesheet" href="/_astro/binary.9peKc0z2.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div class="binaryPage" id="binaryPage"> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxText">TOOLBOX</span> </button> <section class="topGrid"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">00000000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Bit switches"> <div class="bitsGrid" id="bitsGrid"></div> </section> </div> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <div class="card"> <div class="cardTitle">Settings</div> <div class="toggleRow"> <div class="toggleLabel" id="lblUnsigned">Unsigned</div> <label class="switch" aria-label="Toggle mode"> <input id="modeToggle" type="checkbox"> <span class="slider"></span> </label> <div class="toggleLabel" id="lblTwos">Two's complement</div> </div> <div class="hint" id="modeHint">
Tip: In unsigned binary, all bits represent positive values.
</div> <div class="subCard"> <div class="subTitle">Bit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button> <div class="bitInputWrap"> <div class="bitInputLabel">Bits</div> <input id="bitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="64" step="1" value="8" aria-label="Number of bits"> </div> <button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button> </div> </div> </div> <!-- CUSTOM --> <div class="card"> <div class="cardTitle">Custom</div> <div class="twoBtnRow"> <button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button> </div> <button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
Random
</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <!-- TOOLS --> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolsTopRow"> <button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement"></button> <button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment"></button> </div> <div class="twoBtnRow"> <button class="btn" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn" id="btnShiftRight" type="button">Right Shift</button> </div> <button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button> </div> </aside> </section> </main> <script type="module" src="/src/scripts/binary.js"></script> </main> </body></html>
</div> <div class="subCard"> <div class="subTitle">Bit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button> <div class="bitInputWrap"> <div class="bitInputLabel">Bits</div> <input id="bitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="64" step="1" value="8" aria-label="Number of bits"> </div> <button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button> </div> </div> </div> <div class="card"> <div class="cardTitle">Custom Number</div> <div class="controlsRow"> <button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button> </div> <button class="btn btnWide" id="btnRandom" type="button">Random</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolRowCentered"> <button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement"></button> <button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment"></button> </div> <div class="toolRow2"> <button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button> </div> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button> </div> </aside> </section> </div> <script type="module" src="/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div>Computer Science Concept Simulators</div> <div>© 2026 Computing:Box • Created with ♥ by Alexander Lyall</div> <div style="margin-top: 5px;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a>
<a href="/legal-code" style="color: var(--muted); text-decoration: underline;">Legal Code</a> </div> </div> </footer> </body></html>

9
dist/favicon.svg vendored Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -1,5 +1,20 @@
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal | Computing:Box</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
</style>
<link rel="stylesheet" href="/_astro/hexadecimal.C_Opoo6d.css"></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <section class="hex-sim" data-hex-sim> <div class="hex-main"> <div class="hex-readout"> <div class="hex-label">DENARY</div> <div class="hex-number" data-out="denary">0</div> <div class="hex-label hex-mt">HEXADECIMAL</div> <div class="hex-number hex-number--small" data-out="hex">00</div> <div class="hex-label hex-mt">BINARY</div> <div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div> </div> <div class="hex-divider"></div> <div class="hex-digits" data-out="digitsRow"></div> </div> <!-- Toolbox button --> <button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true"> <span class="hex-toolbox-icon" aria-hidden="true"> <!-- toolbox icon --> <svg viewBox="0 0 24 24" width="18" height="18" fill="none"> <path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path> <path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path> <path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path> </svg> </span>
TOOLBOX
</button> <!-- Toolbox panel --> <aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox"> <div class="hex-panel"> <div class="hex-panel-title">SETTINGS</div> <div class="hex-setting-title">HEX DIGIT WIDTH</div> <div class="hex-width"> <button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus"></button> <div class="hex-width-readout"> <div class="hex-width-label">DIGITS</div> <div class="hex-width-number" data-out="digitsCount">2</div> </div> <button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button> </div> <div class="hex-hint" data-out="bitsHint">= 8 bits</div> </div> <div class="hex-panel"> <div class="hex-panel-title">CUSTOM NUMBER</div> <div class="hex-grid-2"> <button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button> <button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button> </div> <!-- Custom Binary + Random on SAME row, same size --> <div class="hex-grid-2 hex-mt-sm"> <button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button> <button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button> </div> <div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div> </div> <div class="hex-panel"> <div class="hex-panel-title">TOOLS</div> <div class="hex-tools-top"> <button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement"></button> <button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment"></button> </div> <button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button> </div> </aside> <!-- Custom number dialog --> <dialog class="hex-dialog" data-out="dialog"> <div class="hex-dialog-card"> <div class="hex-dialog-title" data-out="dialogTitle">Custom</div> <input class="hex-dialog-input hex-font-mono" data-out="dialogInput"> <div class="hex-dialog-hint" data-out="dialogHint"></div> <div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div> <div class="hex-dialog-actions"> <button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button> <button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button> </div> </div> </dialog> <script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script> </section> </main> </body></html>
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal Simulator | Computing:Box</title><script>
var _paq = window._paq = window._paq || [];
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
_paq.push(["enableCrossDomainLinking"]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.adcmnetworks.co.uk/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_astro/about.CswAWODG.css">
<link rel="stylesheet" href="/_astro/binary.9peKc0z2.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div class="binaryPage" id="hexPage"> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxText">TOOLBOX</span> </button> <section class="topGrid"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Hexadecimal</div> <div id="hexNumber" class="num hexValue">00</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">00000000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Hexadecimal sliders"> <div class="hexGrid" id="hexGrid"></div> </section> </div> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <div class="card"> <div class="cardTitle">Settings</div> <div class="hint" style="margin-top: 0; margin-bottom: 14px;">
Hexadecimal represents numbers using base 16 (0-9, A-F).
</div> <div class="subCard"> <div class="subTitle">Digit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits"></button> <div class="bitInputWrap"> <div class="bitInputLabel">Digits</div> <input id="digitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="16" step="1" value="2" aria-label="Number of hex digits"> </div> <button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button> </div> </div> </div> <div class="card"> <div class="cardTitle">Custom Number</div> <div class="controlsRow"> <button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button> </div> <div class="controlsRow"> <button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button> </div> <button class="btn btnWide" id="btnRandom" type="button">Random</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolRowCentered"> <button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement"></button> <button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment"></button> </div> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button> </div> </aside> </section> </div> <script type="module" src="/_astro/hexadecimal.astro_astro_type_script_index_0_lang.C4Wx7oaX.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div>Computer Science Concept Simulators</div> <div>© 2026 Computing:Box • Created with ♥ by Alexander Lyall</div> <div style="margin-top: 5px;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a>
<a href="/legal-code" style="color: var(--muted); text-decoration: underline;">Legal Code</a> </div> </div> </footer> </body></html>

26
dist/index.html vendored
View File

@@ -1,7 +1,19 @@
<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v5.16.6"><title>Astro Basics</title><style>#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}#container[data-astro-cid-mmc7otgs]{font-family:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;height:100%}main[data-astro-cid-mmc7otgs]{height:100%;display:flex;justify-content:center}#hero[data-astro-cid-mmc7otgs]{display:flex;align-items:start;flex-direction:column;justify-content:center;padding:16px}h1[data-astro-cid-mmc7otgs]{font-size:22px;margin-top:.25em}#links[data-astro-cid-mmc7otgs]{display:flex;gap:16px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]{display:flex;align-items:center;padding:10px 12px;color:#111827;text-decoration:none;transition:color .2s}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]:hover{color:#4e5056}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs] svg[data-astro-cid-mmc7otgs]{height:1em;margin-left:8px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{color:#fff;background:linear-gradient(83.21deg,#3245ff,#bc52ee);box-shadow:inset 0 0 0 1px #ffffff1f,inset 0 -2px #0000003d;border-radius:10px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button:hover{color:#e6e6e6;box-shadow:none}pre[data-astro-cid-mmc7otgs]{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-weight:400;background:linear-gradient(14deg,#d83333,#f041ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:0}h2[data-astro-cid-mmc7otgs]{margin:0 0 1em;font-weight:400;color:#111827;font-size:20px}p[data-astro-cid-mmc7otgs]{color:#4b5563;font-size:16px;line-height:24px;letter-spacing:-.006em;margin:0}code[data-astro-cid-mmc7otgs]{display:inline-block;background:linear-gradient(66.77deg,#f3cddd,#f5cee7) padding-box,linear-gradient(155deg,#d83333,#f041ff 18%,#f5cee7 45%) border-box;border-radius:8px;border:1px solid transparent;padding:6px 8px}.box[data-astro-cid-mmc7otgs]{padding:16px;background:#fff;border-radius:16px;border:1px solid white}#news[data-astro-cid-mmc7otgs]{position:absolute;bottom:16px;right:16px;max-width:300px;text-decoration:none;transition:background .2s;backdrop-filter:blur(50px)}#news[data-astro-cid-mmc7otgs]:hover{background:#ffffff8c}@media screen and (max-height:368px){#news[data-astro-cid-mmc7otgs]{display:none}}@media screen and (max-width:768px){#container[data-astro-cid-mmc7otgs]{display:flex;flex-direction:column}#hero[data-astro-cid-mmc7otgs]{display:block;padding-top:10%}#links[data-astro-cid-mmc7otgs]{flex-wrap:wrap}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{padding:14px 18px}#news[data-astro-cid-mmc7otgs]{right:16px;left:16px;bottom:2.5rem;max-width:100%}h1[data-astro-cid-mmc7otgs]{line-height:1.5}}html,body{margin:0;width:100%;height:100%}
</style></head> <body data-astro-cid-sckkx6r4> <div id="container" data-astro-cid-mmc7otgs> <img id="background" src="/_astro/background.Mahwsfbs.svg" alt="" fetchpriority="high" data-astro-cid-mmc7otgs> <main data-astro-cid-mmc7otgs> <section id="hero" data-astro-cid-mmc7otgs> <a href="https://astro.build" data-astro-cid-mmc7otgs><img src="/_astro/astro.CXuftnGC.svg" width="115" height="48" alt="Astro Homepage" data-astro-cid-mmc7otgs></a> <h1 data-astro-cid-mmc7otgs>
To get started, open the <code data-astro-cid-mmc7otgs><pre data-astro-cid-mmc7otgs>src/pages</pre></code> directory in your project.
</h1> <section id="links" data-astro-cid-mmc7otgs> <a class="button" href="https://docs.astro.build" data-astro-cid-mmc7otgs>Read our docs</a> <a href="https://astro.build/chat" data-astro-cid-mmc7otgs>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" data-astro-cid-mmc7otgs><path fill="currentColor" d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" data-astro-cid-mmc7otgs></path></svg> </a> </section> </section> </main> <a href="https://astro.build/blog/astro-5/" id="news" class="box" data-astro-cid-mmc7otgs> <svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg" data-astro-cid-mmc7otgs><path d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z" fill="#111827" data-astro-cid-mmc7otgs></path></svg> <h2 data-astro-cid-mmc7otgs>What's New in Astro 5.0?</h2> <p data-astro-cid-mmc7otgs>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p> </a> </div> </body></html>
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome | Computing:Box</title><script>
var _paq = window._paq = window._paq || [];
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
_paq.push(["enableCrossDomainLinking"]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.adcmnetworks.co.uk/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_astro/about.CswAWODG.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div style="display: flex; align-items: center; justify-content: space-between; gap: 40px; min-height: 60vh; padding: 40px 0;"> <div style="flex: 1;"> <p style="color: var(--accent); font-weight: 800; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px;">Version 2.0 Now Live</p> <h1 class="brandName" style="font-size: 48px; line-height: 1.1; margin-bottom: 24px;">Understand Computing concepts better.</h1> <p style="font-size: 18px; color: var(--muted);">
Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum.
</p> <div style="display: flex; gap: 16px; margin-top: 32px;"> <a href="/about" class="btn btnAccent" style="text-decoration: none; padding: 14px 28px;">Learn More</a> <a href="/binary" class="btn" style="text-decoration: none; padding: 14px 28px;">Get Started</a> </div> </div> <div style="flex: 1; text-align: right;"> <img src="/images/computing-box-logo.svg" alt="Computing Box Logo" style="width: 100%; max-width: 450px; filter: drop-shadow(0 0 50px rgba(40, 240, 122, 0.15));"> </div> </div> </main> <footer class="siteFooter"> <div class="footerInner"> <div>Computer Science Concept Simulators</div> <div>© 2026 Computing:Box • Created with ♥ by Alexander Lyall</div> <div style="margin-top: 5px;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a>
<a href="/legal-code" style="color: var(--muted); text-decoration: underline;">Legal Code</a> </div> </div> </footer> </body></html>

115
dist/js/binary/unsigned-binary.js vendored Normal file
View File

@@ -0,0 +1,115 @@
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
const BIT_COUNT = 8; // unsigned page = 8 bits
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
const elDenary = document.getElementById("denaryNumber");
const elBinary = document.getElementById("binaryNumber");
const elSwitches = document.getElementById("bitSwitches");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnReset = document.getElementById("btnReset");
let bits = new Array(BIT_COUNT).fill(false);
function renderSwitches() {
elSwitches.innerHTML = "";
bitValues.forEach((value, index) => {
const id = `bit-${value}`;
const wrapper = document.createElement("div");
wrapper.className = "switch-col";
const labelTop = document.createElement("div");
labelTop.className = "bit-label";
labelTop.textContent = value;
const label = document.createElement("label");
label.className = "rocker";
label.setAttribute("for", id);
const input = document.createElement("input");
input.type = "checkbox";
input.id = id;
input.checked = bits[index];
input.addEventListener("change", () => {
bits[index] = input.checked;
updateNumbers();
});
const span = document.createElement("span");
span.className = "rocker-body";
span.setAttribute("aria-hidden", "true");
label.appendChild(input);
label.appendChild(span);
wrapper.appendChild(labelTop);
wrapper.appendChild(label);
elSwitches.appendChild(wrapper);
});
}
function updateNumbers() {
const binary = bits.map(b => (b ? "1" : "0")).join("");
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
elBinary.textContent = binary;
elDenary.textContent = denary.toString();
}
function resetAll() {
bits = new Array(BIT_COUNT).fill(false);
renderSwitches();
updateNumbers();
}
function requestCustomDenary() {
let input = prompt(`Enter a denary number (0 to 255):`);
if (input === null) return;
const n = Number.parseInt(input, 10);
if (Number.isNaN(n) || n < 0 || n > 255) {
alert("Invalid input. Enter a number from 0 to 255.");
return;
}
let remaining = n;
bits = bitValues.map(v => {
if (remaining >= v) {
remaining -= v;
return true;
}
return false;
});
renderSwitches();
updateNumbers();
}
function requestCustomBinary() {
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
if (input === null) return;
input = input.trim();
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
if (!re.test(input)) {
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
return;
}
bits = input.split("").map(c => c === "1");
renderSwitches();
updateNumbers();
}
btnCustomDenary?.addEventListener("click", requestCustomDenary);
btnCustomBinary?.addEventListener("click", requestCustomBinary);
btnReset?.addEventListener("click", resetAll);
renderSwitches();
updateNumbers();

72
dist/js/tools/unsigned-binary.js vendored Normal file
View File

@@ -0,0 +1,72 @@
// public/js/tools/unsigned-binary.js
// Lightweight: no frameworks. Works on weak devices.
const BIT_COUNT = 8;
const MAX_DENARY = 255;
let bits = new Array(BIT_COUNT).fill(false);
function bitsToBinaryString(){
return bits.map(b => (b ? "1" : "0")).join("");
}
function bitsToDenary(){
// MSB on the left: 128..1
const weights = [128,64,32,16,8,4,2,1];
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
}
function render(){
const grid = document.getElementById("bitGrid");
grid.innerHTML = "";
const weights = [128,64,32,16,8,4,2,1];
bits.forEach((on, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn";
btn.style.width = "100%";
btn.style.justifyContent = "space-between";
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
btn.addEventListener("click", () => {
bits[i] = !bits[i];
update();
});
grid.appendChild(btn);
});
}
function update(){
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
document.getElementById("denaryNumber").innerText = bitsToDenary();
render();
}
function requestBinary(){
let v;
do{
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
if(v === null) return;
v = v.trim();
}while(!/^[01]{8}$/.test(v));
bits = v.split("").map(ch => ch === "1");
update();
}
function requestDenary(){
let v;
do{
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
if(v === null) return;
v = Number(v);
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
// set bits from MSB to LSB
const weights = [128,64,32,16,8,4,2,1];
bits = weights.map(w => {
if(v >= w){ v -= w; return true; }
return false;
});
update();
}
function reset(){
bits = new Array(BIT_COUNT).fill(false);
update();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
document.getElementById("btnReset")?.addEventListener("click", reset);
update();
});

96
dist/styles.css vendored Normal file
View File

@@ -0,0 +1,96 @@
/* src/styles/md3-tokens.css */
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
:root{
/* Typography */
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Spacing + shape */
--radius-1: 10px;
--radius-2: 16px;
--radius-3: 22px;
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
/* Color roles (keep simple) */
--md-surface: #ffffff;
--md-surface-2: #f6f7fb;
--md-on-surface: #111318;
--md-primary: #2f6fed; /* calm blue */
--md-on-primary: #ffffff;
--md-secondary: #5a5f72; /* muted */
--md-on-secondary: #ffffff;
--md-tertiary: #0f766e; /* teal for "practical" tools */
--md-on-tertiary: #ffffff;
--md-outline: #d7dbe7;
--md-success: #1a7f37;
--md-warning: #b54708;
--md-danger: #b42318;
/* Focus ring for accessibility */
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
}
@media (prefers-color-scheme: dark){
:root{
--md-surface: #0b0e14;
--md-surface-2: #121725;
--md-on-surface: #e8eaf2;
--md-primary: #9bb6ff;
--md-on-primary: #0b0e14;
--md-secondary: #b8bccd;
--md-on-secondary: #0b0e14;
--md-tertiary: #4fd1c5;
--md-on-tertiary: #0b0e14;
--md-outline: #2b3244;
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
}
}
/* src/styles/base.css */
@import "./md3-tokens.css";
html, body{ height:100%; }
body{
margin:0;
font-family: var(--font-sans);
background: var(--md-surface-2);
color: var(--md-on-surface);
}
a{ color: var(--md-primary); text-decoration: none; }
a:hover{ text-decoration: underline; }
.container{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.card{
background: var(--md-surface);
border: 1px solid var(--md-outline);
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
padding: 16px;
}
.btn{
display:inline-flex;
gap:8px;
align-items:center;
justify-content:center;
border-radius: 999px;
border: 1px solid var(--md-outline);
background: var(--md-surface);
color: var(--md-on-surface);
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn:hover{ filter: brightness(0.98); }
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
.btn-primary{
background: var(--md-primary);
color: var(--md-on-primary);
border-color: transparent;
}
.badge{
display:inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--md-outline);
background: var(--md-surface-2);
}
code, pre{ font-family: var(--font-mono); }

1037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "Computing:Box",
"name": "computing-box",
"type": "module",
"version": "2.0.0 Alpha",
"version": "2.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
@@ -9,6 +9,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.16.6"
"astro": "^5.18.0"
}
}

9
public/favicon.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,115 @@
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
const BIT_COUNT = 8; // unsigned page = 8 bits
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
const elDenary = document.getElementById("denaryNumber");
const elBinary = document.getElementById("binaryNumber");
const elSwitches = document.getElementById("bitSwitches");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnReset = document.getElementById("btnReset");
let bits = new Array(BIT_COUNT).fill(false);
function renderSwitches() {
elSwitches.innerHTML = "";
bitValues.forEach((value, index) => {
const id = `bit-${value}`;
const wrapper = document.createElement("div");
wrapper.className = "switch-col";
const labelTop = document.createElement("div");
labelTop.className = "bit-label";
labelTop.textContent = value;
const label = document.createElement("label");
label.className = "rocker";
label.setAttribute("for", id);
const input = document.createElement("input");
input.type = "checkbox";
input.id = id;
input.checked = bits[index];
input.addEventListener("change", () => {
bits[index] = input.checked;
updateNumbers();
});
const span = document.createElement("span");
span.className = "rocker-body";
span.setAttribute("aria-hidden", "true");
label.appendChild(input);
label.appendChild(span);
wrapper.appendChild(labelTop);
wrapper.appendChild(label);
elSwitches.appendChild(wrapper);
});
}
function updateNumbers() {
const binary = bits.map(b => (b ? "1" : "0")).join("");
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
elBinary.textContent = binary;
elDenary.textContent = denary.toString();
}
function resetAll() {
bits = new Array(BIT_COUNT).fill(false);
renderSwitches();
updateNumbers();
}
function requestCustomDenary() {
let input = prompt(`Enter a denary number (0 to 255):`);
if (input === null) return;
const n = Number.parseInt(input, 10);
if (Number.isNaN(n) || n < 0 || n > 255) {
alert("Invalid input. Enter a number from 0 to 255.");
return;
}
let remaining = n;
bits = bitValues.map(v => {
if (remaining >= v) {
remaining -= v;
return true;
}
return false;
});
renderSwitches();
updateNumbers();
}
function requestCustomBinary() {
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
if (input === null) return;
input = input.trim();
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
if (!re.test(input)) {
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
return;
}
bits = input.split("").map(c => c === "1");
renderSwitches();
updateNumbers();
}
btnCustomDenary?.addEventListener("click", requestCustomDenary);
btnCustomBinary?.addEventListener("click", requestCustomBinary);
btnReset?.addEventListener("click", resetAll);
renderSwitches();
updateNumbers();

View File

@@ -0,0 +1,72 @@
// public/js/tools/unsigned-binary.js
// Lightweight: no frameworks. Works on weak devices.
const BIT_COUNT = 8;
const MAX_DENARY = 255;
let bits = new Array(BIT_COUNT).fill(false);
function bitsToBinaryString(){
return bits.map(b => (b ? "1" : "0")).join("");
}
function bitsToDenary(){
// MSB on the left: 128..1
const weights = [128,64,32,16,8,4,2,1];
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
}
function render(){
const grid = document.getElementById("bitGrid");
grid.innerHTML = "";
const weights = [128,64,32,16,8,4,2,1];
bits.forEach((on, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn";
btn.style.width = "100%";
btn.style.justifyContent = "space-between";
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
btn.addEventListener("click", () => {
bits[i] = !bits[i];
update();
});
grid.appendChild(btn);
});
}
function update(){
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
document.getElementById("denaryNumber").innerText = bitsToDenary();
render();
}
function requestBinary(){
let v;
do{
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
if(v === null) return;
v = v.trim();
}while(!/^[01]{8}$/.test(v));
bits = v.split("").map(ch => ch === "1");
update();
}
function requestDenary(){
let v;
do{
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
if(v === null) return;
v = Number(v);
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
// set bits from MSB to LSB
const weights = [128,64,32,16,8,4,2,1];
bits = weights.map(w => {
if(v >= w){ v -= w; return true; }
return false;
});
update();
}
function reset(){
bits = new Array(BIT_COUNT).fill(false);
update();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
document.getElementById("btnReset")?.addEventListener("click", reset);
update();
});

96
public/styles.css Normal file
View File

@@ -0,0 +1,96 @@
/* src/styles/md3-tokens.css */
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
:root{
/* Typography */
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Spacing + shape */
--radius-1: 10px;
--radius-2: 16px;
--radius-3: 22px;
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
/* Color roles (keep simple) */
--md-surface: #ffffff;
--md-surface-2: #f6f7fb;
--md-on-surface: #111318;
--md-primary: #2f6fed; /* calm blue */
--md-on-primary: #ffffff;
--md-secondary: #5a5f72; /* muted */
--md-on-secondary: #ffffff;
--md-tertiary: #0f766e; /* teal for "practical" tools */
--md-on-tertiary: #ffffff;
--md-outline: #d7dbe7;
--md-success: #1a7f37;
--md-warning: #b54708;
--md-danger: #b42318;
/* Focus ring for accessibility */
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
}
@media (prefers-color-scheme: dark){
:root{
--md-surface: #0b0e14;
--md-surface-2: #121725;
--md-on-surface: #e8eaf2;
--md-primary: #9bb6ff;
--md-on-primary: #0b0e14;
--md-secondary: #b8bccd;
--md-on-secondary: #0b0e14;
--md-tertiary: #4fd1c5;
--md-on-tertiary: #0b0e14;
--md-outline: #2b3244;
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
}
}
/* src/styles/base.css */
@import "./md3-tokens.css";
html, body{ height:100%; }
body{
margin:0;
font-family: var(--font-sans);
background: var(--md-surface-2);
color: var(--md-on-surface);
}
a{ color: var(--md-primary); text-decoration: none; }
a:hover{ text-decoration: underline; }
.container{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.card{
background: var(--md-surface);
border: 1px solid var(--md-outline);
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
padding: 16px;
}
.btn{
display:inline-flex;
gap:8px;
align-items:center;
justify-content:center;
border-radius: 999px;
border: 1px solid var(--md-outline);
background: var(--md-surface);
color: var(--md-on-surface);
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn:hover{ filter: brightness(0.98); }
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
.btn-primary{
background: var(--md-primary);
color: var(--md-on-primary);
border-color: transparent;
}
.badge{
display:inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--md-outline);
background: var(--md-surface-2);
}
code, pre{ font-family: var(--font-mono); }

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,210 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

View File

@@ -1,104 +0,0 @@
---
import "./hex/hex-simulator.css";
---
<section class="hex-sim" data-hex-sim>
<div class="hex-main">
<div class="hex-readout">
<div class="hex-label">DENARY</div>
<div class="hex-number" data-out="denary">0</div>
<div class="hex-label hex-mt">HEXADECIMAL</div>
<div class="hex-number hex-number--small" data-out="hex">00</div>
<div class="hex-label hex-mt">BINARY</div>
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
</div>
<div class="hex-divider"></div>
<div class="hex-digits" data-out="digitsRow"></div>
</div>
<!-- Toolbox button -->
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
<span class="hex-toolbox-icon" aria-hidden="true">
<!-- toolbox icon -->
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
TOOLBOX
</button>
<!-- Toolbox panel -->
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
<div class="hex-panel">
<div class="hex-panel-title">SETTINGS</div>
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
<div class="hex-width">
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus"></button>
<div class="hex-width-readout">
<div class="hex-width-label">DIGITS</div>
<div class="hex-width-number" data-out="digitsCount">2</div>
</div>
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
</div>
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">CUSTOM NUMBER</div>
<div class="hex-grid-2">
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
</div>
<!-- Custom Binary + Random on SAME row, same size -->
<div class="hex-grid-2 hex-mt-sm">
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
</div>
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">TOOLS</div>
<div class="hex-tools-top">
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
</div>
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
</div>
</aside>
<!-- Custom number dialog -->
<dialog class="hex-dialog" data-out="dialog">
<div class="hex-dialog-card">
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
<div class="hex-dialog-hint" data-out="dialogHint"></div>
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
<div class="hex-dialog-actions">
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
</div>
</div>
</dialog>
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
</section>

View File

@@ -1,346 +0,0 @@
/* ================= Fonts to match Binary ================= */
/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
@font-face {
font-family: "DSEG7Classic";
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "SevenSegment";
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
url("/fonts/Seven-Segment.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
.hex-sim {
min-height: 100vh;
background: #14151c;
color: #e7e8ee;
padding: 28px;
}
.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
.hex-readout { text-align: center; }
.hex-label {
font-family: "SevenSegment", ui-sans-serif, system-ui;
font-size: 12px;
letter-spacing: 2px;
opacity: 0.7;
}
.hex-mt { margin-top: 12px; }
.hex-number {
font-family: "DSEG7Classic", ui-monospace, monospace;
font-size: 76px;
line-height: 1;
font-weight: 400;
color: #46ff8a;
text-shadow: 0 0 18px rgba(70,255,138,0.18);
}
.hex-number--small { font-size: 64px; }
.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
.hex-divider {
margin: 26px auto 18px;
height: 1px;
width: min(760px, 90%);
background: rgba(255,255,255,0.10);
}
/* ================= Main digit columns ================= */
.hex-digits {
margin-top: 18px;
display: flex;
justify-content: center;
gap: 18px;
flex-wrap: wrap;
}
.hex-digit-col {
width: 160px;
border-radius: 18px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.10);
padding: 12px;
display: grid;
gap: 10px;
justify-items: center;
}
.hex-digit-controls {
width: 100%;
display: flex;
justify-content: center;
gap: 10px;
}
.hex-digit-char {
font-size: 64px;
line-height: 1;
color: #46ff8a;
text-shadow: 0 0 18px rgba(70,255,138,0.18);
}
.hex-digit-place {
font-family: "SevenSegment", ui-monospace, monospace;
opacity: 0.65;
font-size: 14px;
letter-spacing: 1px;
}
/* ================= Bulbs (brightness changes) ================= */
.hex-bulbs {
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: end;
}
.hex-bulb {
display: grid;
justify-items: center;
gap: 6px;
opacity: 0.35;
filter: grayscale(30%);
transition: opacity 160ms ease, filter 160ms ease;
}
.hex-bulb .hex-bulb-cap {
width: 18px;
height: 18px;
border-radius: 999px;
background: rgba(255,255,255,0.22);
border: 1px solid rgba(255,255,255,0.14);
}
.hex-bulb .hex-bulb-glow {
width: 18px;
height: 10px;
border-radius: 999px;
background: rgba(70,255,138,0.0);
box-shadow: 0 0 0 rgba(70,255,138,0.0);
transition: background 160ms ease, box-shadow 160ms ease;
}
.hex-bulb .hex-bulb-label {
font-family: "SevenSegment", ui-monospace, monospace;
font-size: 12px;
opacity: 0.8;
}
.hex-bulb.is-on {
opacity: 1;
filter: none;
}
.hex-bulb.is-on .hex-bulb-cap {
background: rgba(255,255,255,0.35);
}
.hex-bulb.is-on .hex-bulb-glow {
background: rgba(70,255,138,0.25);
box-shadow: 0 0 18px rgba(70,255,138,0.35);
}
/* ================= Buttons (toolbox style reused everywhere) ================= */
.hex-btn {
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #e7e8ee;
font-weight: 800;
cursor: pointer;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.hex-btn:hover { background: rgba(255,255,255,0.10); }
.hex-btn--square {
width: 48px;
height: 48px;
padding: 0;
display: grid;
place-items: center;
font-size: 18px;
}
.hex-btn--wide { width: 100%; }
.hex-btn--green {
background: rgba(46, 200, 120, 0.18);
border-color: rgba(46,200,120,0.35);
}
.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
.hex-btn--green2 {
background: rgba(46, 200, 120, 0.18);
border-color: rgba(46,200,120,0.35);
}
.hex-btn--red {
background: rgba(220, 60, 70, 0.18);
border-color: rgba(220,60,70,0.35);
}
/* Random = green pulse while running */
.hex-btn--random.is-running {
border-color: rgba(80, 255, 160, 0.55);
background: rgba(46, 200, 120, 0.22);
box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
animation: hexPulseGreen 900ms ease-in-out infinite;
}
@keyframes hexPulseGreen {
0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
}
/* Reset = red background + pulse on hover */
.hex-btn--reset:hover {
background: rgba(220, 60, 70, 0.28);
border-color: rgba(255, 80, 90, 0.55);
animation: hexPulseRed 900ms ease-in-out infinite;
}
@keyframes hexPulseRed {
0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
}
/* ================= Toolbox button + panel (slide) ================= */
.hex-toolbox-btn {
position: fixed;
top: 88px;
right: 28px;
z-index: 30;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #e7e8ee;
font-weight: 800;
letter-spacing: 1px;
cursor: pointer;
}
.hex-toolbox-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: #ff4fa6;
filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
}
.hex-toolbox {
position: fixed;
top: 140px;
right: 28px;
width: 340px;
display: grid;
gap: 14px;
z-index: 25;
transform: translateX(0);
opacity: 1;
transition: transform 220ms ease, opacity 220ms ease;
}
.hex-toolbox:not(.is-open) {
transform: translateX(380px);
opacity: 0;
pointer-events: none;
}
.hex-panel {
border-radius: 16px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
padding: 14px;
}
.hex-panel-title {
font-family: "SevenSegment", ui-sans-serif, system-ui;
font-size: 12px;
letter-spacing: 2px;
opacity: 0.7;
margin-bottom: 10px;
}
.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
.hex-width {
display: grid;
grid-template-columns: 48px 1fr 48px;
gap: 10px;
align-items: center;
}
.hex-width-readout {
border-radius: 14px;
background: rgba(0,0,0,0.22);
border: 1px solid rgba(255,255,255,0.10);
padding: 10px 12px;
display: flex;
justify-content: space-between;
align-items: baseline;
}
.hex-width-label {
font-family: "SevenSegment", ui-sans-serif, system-ui;
opacity: 0.7;
font-weight: 800;
letter-spacing: 1px;
font-size: 12px;
}
.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.hex-mt-sm { margin-top: 10px; }
.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
/* ================= Dialog ================= */
.hex-dialog { border: none; padding: 0; background: transparent; }
.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
.hex-dialog-card {
width: min(560px, 92vw);
border-radius: 18px;
background: #1a1b24;
border: 1px solid rgba(255,255,255,0.12);
padding: 16px;
color: #e7e8ee;
}
.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-input {
width: 100%;
padding: 12px 12px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(0,0,0,0.25);
color: #e7e8ee;
font-size: 18px;
}
.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
@media (max-width: 900px) {
.hex-toolbox { width: min(360px, 92vw); right: 16px; }
.hex-toolbox-btn { right: 16px; }
.hex-number { font-size: 60px; }
.hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
}

View File

@@ -1,232 +0,0 @@
type DialogMode = "hex" | "den" | "bin";
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
if (!root) throw new Error("Hex simulator root not found");
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
const dialogError = root.querySelector<HTMLElement>('[data-out="dialogError"]')!;
let digits = 2; // 1..8
let value = 0; // unsigned denary
let randomTimer: number | null = null;
let dialogMode: DialogMode | null = null;
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
const maxForDigits = (d: number) => (16 ** d) - 1;
const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
function stopRandom(): void {
if (randomTimer !== null) window.clearInterval(randomTimer);
randomTimer = null;
randomBtn.classList.remove("is-running");
}
function startRandom(): void {
stopRandom();
const max = maxForDigits(digits);
const start = Date.now();
randomBtn.classList.add("is-running");
randomTimer = window.setInterval(() => {
value = Math.floor(Math.random() * (max + 1));
render();
if (Date.now() - start > 1600) stopRandom();
}, 90);
}
function render(): void {
const bits = digits * 4;
digitsCount.textContent = String(digits);
bitsHint.textContent = `= ${bits} bits`;
outDen.textContent = String(value);
outHex.textContent = padHex(value, digits);
outBin.textContent = groupBin(padBin(value, bits));
renderDigitsRow();
}
function renderDigitsRow(): void {
const hex = padHex(value, digits);
outDigitsRow.innerHTML = "";
for (let i = 0; i < digits; i++) {
const pow = digits - 1 - i;
const placeValue = 16 ** pow;
const digitChar = hex[i];
const digitVal = parseInt(digitChar, 16);
const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
const col = document.createElement("div");
col.className = "hex-digit-col";
col.innerHTML = `
<div class="hex-digit-controls">
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
</div>
<div class="hex-digit-char hex-font-number">${digitChar}</div>
<!-- bulbs: brightness changes based on nibble bits -->
<div class="hex-bulbs" aria-label="Nibble bits">
${[8,4,2,1].map((w, idx) => {
const on = nibbleBits[idx] === 1;
return `
<div class="hex-bulb ${on ? "is-on" : ""}">
<div class="hex-bulb-cap"></div>
<div class="hex-bulb-glow"></div>
<div class="hex-bulb-label">${w}</div>
</div>
`;
}).join("")}
</div>
<div class="hex-digit-place">${placeValue}</div>
`;
outDigitsRow.appendChild(col);
}
}
function openDialog(mode: DialogMode): void {
stopRandom();
dialogMode = mode;
dialogError.textContent = "";
dialogInput.value = "";
if (mode === "hex") {
dialogTitle.textContent = "Custom Hexadecimal";
dialogHint.textContent = `Enter 1${digits} hex digit(s) (09, AF).`;
dialogInput.placeholder = "A1";
dialogInput.inputMode = "text";
} else if (mode === "den") {
dialogTitle.textContent = "Custom Denary";
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
dialogInput.placeholder = "42";
dialogInput.inputMode = "numeric";
} else {
dialogTitle.textContent = "Custom Binary";
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
dialogInput.placeholder = "00101010";
dialogInput.inputMode = "text";
}
dialog.showModal();
window.setTimeout(() => dialogInput.focus(), 0);
}
function closeDialog(): void {
dialogMode = null;
dialogError.textContent = "";
if (dialog.open) dialog.close();
}
function applyDialog(): void {
const raw = (dialogInput.value || "").trim();
if (!dialogMode) return closeDialog();
if (raw.length === 0) return closeDialog();
const max = maxForDigits(digits);
const bits = digits * 4;
if (dialogMode === "hex") {
const v = raw.toUpperCase();
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 09 and AF only."; return; }
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
value = clamp(parseInt(v, 16), 0, max);
render();
return closeDialog();
}
if (dialogMode === "den") {
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
const n = Number(raw);
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
value = clamp(n, 0, max);
render();
return closeDialog();
}
// bin
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
value = clamp(parseInt(raw, 2), 0, max);
render();
return closeDialog();
}
function applyDigitDelta(i: number, delta: number): void {
stopRandom();
const hexArr = padHex(value, digits).split("");
let v = parseInt(hexArr[i], 16);
v = (v + delta) % 16;
if (v < 0) v += 16;
hexArr[i] = v.toString(16).toUpperCase();
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
render();
}
// dialog cancel / backdrop
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
dialog.addEventListener("click", (e) => {
const card = dialog.querySelector(".hex-dialog-card");
if (card && !card.contains(e.target as Node)) closeDialog();
});
dialogInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") applyDialog();
if (e.key === "Escape") closeDialog();
});
// main click handler
root.addEventListener("click", (e) => {
const btn = (e.target as HTMLElement).closest<HTMLElement>("[data-action]");
if (!btn) return;
const action = btn.getAttribute("data-action")!;
if (action === "toggleToolbox") {
toolbox.classList.toggle("is-open");
toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
return;
}
if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
if (action === "reset") { stopRandom(); value = 0; return render(); }
if (action === "random") { return startRandom(); }
if (action === "customHex") return openDialog("hex");
if (action === "customDenary") return openDialog("den");
if (action === "customBinary") return openDialog("bin");
if (action === "dialogCancel") return closeDialog();
if (action === "dialogApply") return applyDialog();
if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
});
render();

View File

@@ -1,8 +0,0 @@
<footer class="site-footer">
<div class="site-footer__inner">
<div class="site-footer__text">
COMPUTER SCIENCE CONCEPT SIMULATORS<br />
© 2025 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
</div>
</div>
</footer>

View File

@@ -1,25 +0,0 @@
---
const nav = [
{ href: "/about", label: "About" },
{ href: "/binary", label: "Binary" },
{ href: "/hexadecimal", label: "Hexadecimal" },
{ href: "/hex-colours", label: "Hex Colours" },
{ href: "/logic-gates", label: "Logic Gates" },
];
---
<header class="site-header">
<div class="site-header__inner">
<a class="site-header__brand" href="/" aria-label="Computing:Box home">
<img class="site-header__logo" src="/img/logo.png" alt="" width="26" height="26" />
<span class="site-header__name">COMPUTING:BOX</span>
</a>
<nav class="site-header__nav" aria-label="Primary">
{nav.map((i) => (
<a class="site-header__link" href={i.href}>
{i.label.toUpperCase()}
</a>
))}
</nav>
</div>
</header>

View File

@@ -1,4 +1,5 @@
---
import "../styles/global.css";
const { title = "Computing:Box" } = Astro.props;
---
@@ -8,105 +9,42 @@ const { title = "Computing:Box" } = Astro.props;
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{title}</title>
<style>
:root{
--nav-h: 108px; /* 3x-ish height */
--bg: #1f2027;
--text: #e8e8ee;
--muted: #a9acb8;
--line: rgba(255,255,255,.10);
}
body{
margin:0;
background:var(--bg);
color:var(--text);
}
.siteNav{
position: sticky;
top: 0;
z-index: 50;
height: var(--nav-h);
background: rgba(0,0,0,.10);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(8px);
}
.navInner{
height: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand{
display:flex;
align-items:center;
gap:12px;
text-decoration:none;
color:var(--text);
}
.brandLogo{
width: 2em;
height: 2em;
image-rendering: pixelated;
}
.brandName{
letter-spacing: .12em;
font-weight: 900;
text-transform: uppercase;
font-size: 18px;
}
.navLinks{
display:flex;
align-items:center;
gap:18px;
flex-wrap:wrap;
}
.navLinks a{
color: var(--muted);
text-decoration: none;
font-weight: 800;
letter-spacing: .12em;
font-size: 16px;
text-transform: uppercase;
}
.navLinks a:hover{
color: var(--text);
}
.pageWrap{
max-width: 1400px;
margin: 0 auto;
}
</style>
<script is:inline>
var _paq = window._paq = window._paq || [];
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
_paq.push(["enableCrossDomainLinking"]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.adcmnetworks.co.uk/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet">
</head>
<body>
<header class="siteNav">
<div class="navInner">
<a class="brand" href="/">
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
<span class="brandName">COMPUTING:BOX</span>
<span class="brandName">Computing:Box</span>
</a>
<nav class="navLinks" aria-label="Site navigation">
<a href="/about">ABOUT</a>
<a href="/binary">BINARY</a>
<a href="/hexadecimal">HEXADECIMAL</a>
<a href="/hex-colours">HEX COLOURS</a>
<a href="/logic-gates">LOGIC GATES</a>
<a href="/about">About</a>
<a href="/binary">Binary</a>
<a href="/hexadecimal">Hexadecimal</a>
<a href="/hex-colours">Hex Colours</a>
<a href="/logic-gates">Logic Gates</a>
<a href="/pc-builder">PC Components</a>
</nav>
</div>
</header>
@@ -114,5 +52,16 @@ const { title = "Computing:Box" } = Astro.props;
<main class="pageWrap">
<slot />
</main>
<footer class="siteFooter">
<div class="footerInner">
<div>Computer Science Concept Simulators</div>
<div>© {new Date().getFullYear()} Computing:Box • Created with ♥ by Alexander Lyall</div>
<div style="margin-top: 5px;">
<a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a> •
<a href="/legal-code" style="color: var(--muted); text-decoration: underline;">Legal Code</a>
</div>
</div>
</footer>
</body>
</html>
</html>

View File

@@ -1,22 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

37
src/pages/about.astro Normal file
View File

@@ -0,0 +1,37 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="About | Computing:Box">
<article style="max-width: 900px; margin: 0 auto;">
<div class="card" style="margin-bottom: 40px;">
<h1 class="brandName" style="font-size: 32px; color: var(--text); margin-bottom: 16px;">The New Computing:Box Experience</h1>
<p style="color: var(--muted); font-size: 18px; line-height: 1.6;">
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:
</p>
<div class="divider"></div>
<ul style="list-style: none; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> New User Interface (Responsive)</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Two's Complement Simulator</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Extended Binary Simulator (Custom bit sizes)</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Unified Binary Simulator (Unsigned & Two's Complement)</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Extended Hexadecimal Simulator</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Unified Hexadecimal Simulator (GCSE & A Level)</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Enhanced Gate Simulator (Truth Table Creator)</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Compound Gate Simulator</li>
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Computer Components Simulator</li>
</ul>
</div>
<div class="card">
<h2 class="brandName" style="font-size: 24px; margin-bottom: 12px;">Educational Impact</h2>
<p style="color: var(--muted);">
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.
</p>
</div>
</article>
</BaseLayout>

View File

@@ -1,24 +1,28 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/binary.css";
import "../styles/number-simulators.css";
---
<BaseLayout title="Binary Simulator">
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxLabel">TOOLBOX</span>
</button>
<BaseLayout title="Binary Simulator | Computing:Box">
<div class="binaryPage" id="binaryPage">
<button
id="toolboxToggle"
class="toolboxToggle"
type="button"
aria-expanded="true"
>
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<main class="wrap">
<section class="topGrid">
<!-- LEFT -->
<div>
<div class="leftCol">
<div class="readout">
<div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">0000 0000</div>
<div id="binaryNumber" class="num binaryValue">00000000</div>
</div>
<div class="divider"></div>
@@ -28,9 +32,7 @@ import "../styles/binary.css";
</section>
</div>
<!-- RIGHT TOOLBOX -->
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
<!-- SETTINGS -->
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Settings</div>
@@ -40,7 +42,7 @@ import "../styles/binary.css";
<input id="modeToggle" type="checkbox" />
<span class="slider"></span>
</label>
<div class="toggleLabel" id="lblTwos">Two&rsquo;s complement</div>
<div class="toggleLabel" id="lblTwos">Two's complement</div>
</div>
<div class="hint" id="modeHint">
@@ -51,7 +53,6 @@ import "../styles/binary.css";
<div class="subTitle">Bit width</div>
<div class="bitWidthRow">
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button>
<div class="bitInputWrap">
<div class="bitInputLabel">Bits</div>
<input
@@ -66,47 +67,36 @@ import "../styles/binary.css";
aria-label="Number of bits"
/>
</div>
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
</div>
</div>
</div>
<!-- CUSTOM -->
<div class="card">
<div class="cardTitle">Custom</div>
<div class="twoBtnRow">
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
<div class="cardTitle">Custom Number</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
</div>
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
Random
</button>
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<!-- TOOLS -->
<div class="card">
<div class="cardTitle">Tools</div>
<div class="toolsTopRow">
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement"></button>
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
<div class="toolRowCentered">
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment"></button>
</div>
<div class="twoBtnRow">
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
<div class="toolRow2">
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
</div>
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</main>
</div>
<script type="module" src="/src/scripts/binary.js"></script>
</BaseLayout>
<script src="../scripts/binary.js"></script>
</BaseLayout>

30
src/pages/copyright.astro Normal file
View File

@@ -0,0 +1,30 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Copyright Notice | Computing:Box">
<div class="card" style="max-width: 800px; margin: 0 auto;">
<h1 class="brandName" style="color: var(--accent); margin-bottom: 20px;">Copyright Notice</h1>
<p style="color: var(--text);">
<a href="https://www.computingbox.co.uk" style="color: var(--accent); text-decoration: none;">Computing:Box</a>
© 2024 by <a href="https://git.adcmnetworks.co.uk/alexander.lyall" style="color: var(--accent); text-decoration: none;">Alexander Lyall</a> is licensed under
<strong>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</strong>.
</p>
<div class="divider"></div>
<h2 class="brandName" style="font-size: 20px;">What Does This Mean For You?</h2>
<p style="color: var(--muted); font-size: 1.2em;">You are free to:</p>
<ul style="color: var(--muted); line-height: 1.6; margin-bottom: 20px;">
<li><strong>Share</strong> — copy and redistribute the material in any medium or format.</li>
<li><strong>Adapt</strong> — remix, transform, and build upon the material.</li>
</ul>
<h2 class="brandName" style="font-size: 20px;">Under the following terms:</h2>
<ul style="color: var(--muted); line-height: 1.6;">
<li><strong>Attribution</strong> — You must give appropriate credit, provide a link to the license, and indicate if changes were made.</li>
<li><strong>NonCommercial</strong> — You may not use the material for commercial purposes.</li>
<li><strong>ShareAlike</strong> — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.</li>
</ul>
</div>
</BaseLayout>

100
src/pages/hex-colours.astro Normal file
View File

@@ -0,0 +1,100 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/number-simulators.css";
---
<BaseLayout title="Hex Colours Simulator | Computing:Box">
<div class="binaryPage" id="colorPage">
<button
id="toolboxToggle"
class="toolboxToggle"
type="button"
aria-expanded="true"
>
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<section class="topGrid">
<div class="leftCol">
<div class="readoutContainer">
<div class="readout">
<div class="readoutBlock">
<div class="label">Denary (R, G, B)</div>
<div id="denaryNumber" class="num denaryValue">
<span class="text-red">0</span>
<span class="text-green">0</span>
<span class="text-blue">0</span>
</div>
</div>
<div class="readoutBlock">
<div class="label">Hexadecimal</div>
<div id="hexNumber" class="num hexValue">
<span class="text-red"><span style="color:var(--muted)">#</span>00</span>
<span class="text-green">00</span>
<span class="text-blue">00</span>
</div>
</div>
<div class="readoutBlock">
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">
<span class="text-red">00000000</span>
<span class="text-green">00000000</span>
<span class="text-blue">00000000</span>
</div>
</div>
</div>
<div class="colorPreviewSide">
<div class="colorBoxWrap">
<div id="previewColor" class="colorBox"></div>
<div class="colorBoxLabel">Colour</div>
</div>
<div class="colorBoxWrap">
<div id="previewInverted" class="colorBox"></div>
<div class="colorBoxLabel">Inverted</div>
</div>
</div>
</div>
<div class="divider"></div>
<section class="bitsWrap" aria-label="Hex Colour Cards">
<div class="colorGroupWrap" id="colorGrid"></div>
</section>
</div>
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Info</div>
<div class="hint" style="margin-top: 0;">
Hexadecimal colours are made of 3 channels (Red, Green, Blue). Each channel is an 8-bit value ranging from 00 to FF (0 to 255).
</div>
</div>
<div class="card">
<div class="cardTitle">Colour Tools</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
<button class="btn btnAccent btnHalf" id="btnCustomRGB" type="button">Custom RGB</button>
</div>
<div class="controlsRow">
<button class="btn btnAccent btnWide" id="btnInvert" type="button">Invert Colour</button>
</div>
<button class="btn btnWide" id="btnRandom" type="button">Random Colour</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</div>
<script src="../scripts/hexColours.js"></script>
</BaseLayout>

View File

@@ -1,8 +1,95 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import HexSimulator from "../components/simulators/HexSimulator.astro";
import "../styles/number-simulators.css";
---
<BaseLayout title="Hexadecimal | Computing:Box">
<HexSimulator />
</BaseLayout>
<BaseLayout title="Hexadecimal Simulator | Computing:Box">
<div class="binaryPage" id="hexPage">
<button
id="toolboxToggle"
class="toolboxToggle"
type="button"
aria-expanded="true"
>
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<section class="topGrid">
<div class="leftCol">
<div class="readout">
<div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Hexadecimal</div>
<div id="hexNumber" class="num hexValue">00</div>
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">00000000</div>
</div>
<div class="divider"></div>
<section class="bitsWrap" aria-label="Hexadecimal sliders">
<div class="hexGrid" id="hexGrid"></div>
</section>
</div>
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Settings</div>
<div class="hint" style="margin-top: 0; margin-bottom: 14px;">
Hexadecimal represents numbers using base 16 (0-9, A-F).
</div>
<div class="subCard">
<div class="subTitle">Digit width</div>
<div class="bitWidthRow">
<button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits"></button>
<div class="bitInputWrap">
<div class="bitInputLabel">Digits</div>
<input
id="digitsInput"
class="bitInput"
type="number"
inputmode="numeric"
min="1"
max="16"
step="1"
value="2"
aria-label="Number of hex digits"
/>
</div>
<button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button>
</div>
</div>
</div>
<div class="card">
<div class="cardTitle">Custom Number</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
</div>
<div class="controlsRow">
<button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button>
</div>
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<div class="toolRowCentered">
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
</div>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</div>
<script src="../scripts/hexadecimal.js"></script>
</BaseLayout>

View File

@@ -1,11 +1,23 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
import BaseLayout from "../layouts/BaseLayout.astro";
---
<Layout>
<Welcome />
</Layout>
<BaseLayout title="Welcome | Computing:Box">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 40px; min-height: 60vh; padding: 40px 0;">
<div style="flex: 1;">
<p style="color: var(--accent); font-weight: 800; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px;">Version 2.0 Now Live</p>
<h1 class="brandName" style="font-size: 48px; line-height: 1.1; margin-bottom: 24px;">Understand Computing concepts better.</h1>
<p style="font-size: 18px; color: var(--muted);">
Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum.
</p>
<div style="display: flex; gap: 16px; margin-top: 32px;">
<a href="/about" class="btn btnAccent" style="text-decoration: none; padding: 14px 28px;">Learn More</a>
<a href="/binary" class="btn" style="text-decoration: none; padding: 14px 28px;">Get Started</a>
</div>
</div>
<div style="flex: 1; text-align: right;">
<img src="/images/computing-box-logo.svg" alt="Computing Box Logo" style="width: 100%; max-width: 450px; filter: drop-shadow(0 0 50px rgba(40, 240, 122, 0.15));" />
</div>
</div>
</BaseLayout>

View File

@@ -0,0 +1,20 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Legal Code | Computing:Box">
<div class="card" style="max-width: 900px; margin: 0 auto;">
<h1 class="brandName" style="color: var(--accent); margin-bottom: 20px;">Legal Code</h1>
<div style="background: rgba(255, 193, 7, 0.1); border: 1px solid #ffc107; padding: 20px; border-radius: 12px; margin-bottom: 24px;">
<h3 style="color: #ffc107; margin-top: 0; font-family: var(--ui-font);">About the license and Creative Commons</h3>
<p style="color: #eee; margin-bottom: 0; font-size: 14px;">
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.
</p>
</div>
<p style="color: var(--muted); font-size: 14px; line-height: 1.6;">
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.
</p>
</div>
</BaseLayout>

View File

@@ -0,0 +1,56 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/logic-gates.css";
---
<BaseLayout title="Logic Gate Builder | Computing:Box">
<div id="logicPage" class="lg-container">
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<div class="lg-top-header">
<div class="lg-title">Interactive Logic Circuit Builder</div>
<div class="lg-subtitle">
Drag items from the toolbox. Drag from output ports to input ports to wire. Click a wire/node and press <kbd>Delete</kbd>. Scroll to Zoom.
</div>
</div>
<div class="lg-workspace" id="workspace">
<div class="lg-zoom-controls">
<button class="lg-zoom-btn" id="btnZoomIn" title="Zoom In">+</button>
<button class="lg-zoom-btn" id="btnZoomOut" title="Zoom Out"></button>
<button class="lg-zoom-btn" id="btnZoomReset" title="Reset View" style="font-size: 16px;">⌂</button>
</div>
<div class="lg-viewport" id="viewport">
<svg class="lg-svg-layer" id="wireLayer"></svg>
</div>
</div>
<aside id="toolboxPanel" class="lg-toolbox" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Components</div>
<div class="tb-icon-grid" id="toolboxGrid"></div>
</div>
<div class="card">
<div class="cardTitle">Live Truth Table</div>
<details open>
<summary class="tt-summary">Show / Hide Table</summary>
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Auto-generates based on current wiring. (Max 6 inputs)</div>
<div class="tt-table-wrap" id="truthTableContainer"></div>
</details>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Clear Board</button>
</div>
</aside>
</div>
<script src="../scripts/logicGates.js"></script>
</BaseLayout>

View File

@@ -0,0 +1,65 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/pc-builder.css";
---
<BaseLayout title="PC Part Simulator | Computing:Box">
<div id="pcPage" class="pb-container">
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<div class="pb-top-header">
<div class="pb-title">PC Part Simulator</div>
<div class="pb-subtitle">
Build inside the Case! Snap the Motherboard into the chassis, then populate the slots. Add the side panel when done. <strong>Double-Click</strong> parts to inspect in 3D.
</div>
</div>
<div class="pb-workspace" id="workspace">
<div class="pb-zoom-controls">
<button class="pb-zoom-btn" id="btnZoomIn" title="Zoom In">+</button>
<button class="pb-zoom-btn" id="btnZoomOut" title="Zoom Out"></button>
<button class="pb-zoom-btn" id="btnZoomReset" title="Reset View" style="font-size: 16px;">⌂</button>
</div>
<div class="pb-viewport" id="viewport">
<svg class="pb-svg-layer pb-wire-internal" id="wireLayerInternal"></svg>
<svg class="pb-svg-layer pb-wire-external" id="wireLayerExternal"></svg>
</div>
</div>
<aside id="toolboxPanel" class="pb-toolbox" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Inventory</div>
<div class="tb-icon-grid" id="toolboxGrid"></div>
</div>
<div class="card">
<div class="cardTitle">System Diagnostics</div>
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Live pre-flight boot analysis.</div>
<div class="specs-panel" id="buildSpecsContainer"></div>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Disassemble PC</button>
</div>
</aside>
<div id="inspectModal" class="inspect-modal">
<div class="inspect-close" id="inspectClose">&times;</div>
<div id="inspectName" style="color: white; font-family: var(--ui-font); font-size: 28px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase;"></div>
<div class="inspect-stage" id="inspectStage">
<div class="inspect-object" id="inspectObject"></div>
</div>
<div style="color: var(--muted); font-family: var(--ui-font); margin-top: 20px; font-size: 14px;">Move mouse to rotate component. Scroll to zoom.</div>
</div>
</div>
<script src="../scripts/pcBuilder.js"></script>
</BaseLayout>

View File

@@ -1,6 +1,3 @@
// src/scripts/binary.js
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
(() => {
/* -----------------------------
DOM
@@ -12,6 +9,10 @@
const modeToggle = document.getElementById("modeToggle");
const modeHint = document.getElementById("modeHint");
// Connect the text labels to the JS
const lblUnsigned = document.getElementById("lblUnsigned");
const lblTwos = document.getElementById("lblTwos");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary");
@@ -27,14 +28,13 @@
const btnBitsDown = document.getElementById("btnBitsDown");
const toolboxToggle = document.getElementById("toolboxToggle");
const toolboxPanel = document.getElementById("toolboxPanel");
const binaryPage = document.getElementById("binaryPage");
/* -----------------------------
STATE
----------------------------- */
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
let bits = new Array(bitCount).fill(false);
let randomTimer = null;
/* -----------------------------
@@ -54,7 +54,7 @@
}
function unsignedMaxExclusive(nBits) {
return pow2Big(nBits);
return pow2Big(nBits);
}
function unsignedMaxValue(nBits) {
@@ -94,7 +94,8 @@
function signedBigIntToBitsTwos(vSigned) {
const span = pow2Big(bitCount);
let v = ((vSigned % span) + span) % span;
let v = vSigned;
v = ((v % span) + span) % span;
unsignedBigIntToBits(v);
}
@@ -102,17 +103,33 @@
let s = "";
for (let i = bitCount - 1; i >= 0; i--) {
s += bits[i] ? "1" : "0";
const posFromRight = (bitCount - i);
if (i !== 0 && posFromRight % 4 === 0) s += " ";
const posFromLeft = (bitCount - i);
if (i !== 0 && posFromLeft % 4 === 0) s += " ";
}
return s;
return s.trimEnd();
}
function updateModeHint() {
if (!modeHint) return;
modeHint.textContent = isTwosMode()
? "Tip: In twos complement, the left-most bit (MSB) represents a negative value."
: "Tip: In unsigned binary, all bits represent positive values.";
if (isTwosMode()) {
modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value.";
} else {
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
}
}
/* -----------------------------
RESPONSIVE GRID COLS
----------------------------- */
function computeColsForBitsGrid() {
if (!bitsGrid) return;
const wrap = bitsGrid.parentElement;
if (!wrap) return;
const width = wrap.getBoundingClientRect().width;
const minCell = 100;
const cols = clampInt(Math.floor(width / minCell), 1, 12);
bitsGrid.style.setProperty("--cols", String(Math.min(cols, bitCount)));
}
/* -----------------------------
@@ -127,18 +144,25 @@
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
bitsGrid.innerHTML = "";
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
for (let i = bitCount - 1; i >= 0; i--) {
const bitEl = document.createElement("div");
bitEl.className = "bit";
bitEl.innerHTML = `
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
<div class="bulb" id="bulb-${i}" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
</svg>
</div>
<div class="bitVal" id="bitLabel-${i}"></div>
<label class="switch" aria-label="Toggle bit ${i}">
<input type="checkbox" data-index="${i}">
<span class="slider"></span>
</label>
`;
bitsGrid.appendChild(bitEl);
}
@@ -150,6 +174,7 @@
});
});
computeColsForBitsGrid();
updateUI();
}
@@ -161,14 +186,14 @@
const label = document.getElementById(`bitLabel-${i}`);
if (!label) continue;
// Keep label on ONE LINE (no wrapping)
label.style.whiteSpace = "nowrap";
let valStr;
if (isTwosMode() && i === bitCount - 1) {
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
valStr = `-${pow2Big(bitCount - 1).toString()}`;
} else {
label.textContent = pow2Big(i).toString();
valStr = pow2Big(i).toString();
}
label.textContent = valStr;
label.style.setProperty('--len', valStr.length);
}
}
@@ -189,12 +214,23 @@
function updateReadout() {
if (!denaryEl || !binaryEl) return;
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
if (isTwosMode()) {
denaryEl.textContent = bitsToSignedBigIntTwos().toString();
} else {
denaryEl.textContent = bitsToUnsignedBigInt().toString();
}
binaryEl.textContent = formatBinaryGrouped();
}
function updateUI() {
updateModeHint();
// Toggle the glowing CSS class on the active mode text
if (lblUnsigned && lblTwos) {
lblUnsigned.classList.toggle("activeMode", !isTwosMode());
lblTwos.classList.toggle("activeMode", isTwosMode());
}
updateBitLabels();
syncSwitchesToBits();
updateBulbs();
@@ -202,20 +238,25 @@
}
/* -----------------------------
INPUT SETTERS
SET FROM BINARY STRING
----------------------------- */
function setFromBinaryString(binStr) {
const clean = String(binStr ?? "").replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return false;
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
for (let i = 0; i < bitCount; i++) {
const charFromRight = padded[padded.length - 1 - i];
bits[i] = charFromRight === "1";
}
updateUI();
return true;
}
/* -----------------------------
SET FROM DENARY INPUT
----------------------------- */
function setFromDenaryInput(vStr) {
const raw = String(vStr ?? "").trim();
if (!raw) return false;
@@ -224,9 +265,7 @@
try {
if (!/^-?\d+$/.test(raw)) return false;
v = BigInt(raw);
} catch {
return false;
}
} catch { return false; }
if (isTwosMode()) {
const min = twosMin(bitCount);
@@ -234,8 +273,7 @@
if (v < min || v > max) return false;
signedBigIntToBitsTwos(v);
} else {
if (v < 0n) return false;
if (v > unsignedMaxValue(bitCount)) return false;
if (v < 0n || v > unsignedMaxValue(bitCount)) return false;
unsignedBigIntToBits(v);
}
@@ -247,18 +285,14 @@
SHIFTS
----------------------------- */
function shiftLeft() {
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
for (let i = bitCount - 1; i >= 1; i--) { bits[i] = bits[i - 1]; }
bits[0] = false;
updateUI();
}
function shiftRight() {
// Unsigned: logical right shift (MSB becomes 0)
// Two's complement: arithmetic right shift (MSB preserved)
const msb = bits[bitCount - 1];
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
for (let i = 0; i < bitCount - 1; i++) { bits[i] = bits[i + 1]; }
bits[bitCount - 1] = isTwosMode() ? msb : false;
updateUI();
}
@@ -267,8 +301,9 @@
CLEAR / INC / DEC
----------------------------- */
function clearAll() {
bits.fill(false);
updateUI();
bits = [];
if (modeToggle) modeToggle.checked = false;
buildBits(8);
}
function increment() {
@@ -280,8 +315,7 @@
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
const v = (bitsToUnsignedBigInt() + 1n) % span;
unsignedBigIntToBits(v);
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
}
updateUI();
}
@@ -295,25 +329,22 @@
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
unsignedBigIntToBits(v);
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
}
updateUI();
}
/* -----------------------------
RANDOM (with running pulse + longer run)
RANDOM
----------------------------- */
function cryptoRandomBigInt(maxExclusive) {
if (maxExclusive <= 0n) return 0n;
const bitLen = maxExclusive.toString(2).length;
const byteLen = Math.ceil(bitLen / 8);
while (true) {
const bytes = new Uint8Array(byteLen);
crypto.getRandomValues(bytes);
let x = 0n;
for (const b of bytes) x = (x << 8n) | BigInt(b);
@@ -325,23 +356,26 @@
}
function setRandomOnce() {
const span = unsignedMaxExclusive(bitCount); // 2^n
const span = unsignedMaxExclusive(bitCount);
const u = cryptoRandomBigInt(span);
unsignedBigIntToBits(u);
updateUI();
}
function setRandomRunning(isRunning) {
if (!btnRandom) return;
btnRandom.classList.toggle("btnRandomRunning", !!isRunning);
}
function runRandomBriefly() {
if (randomTimer) {
clearInterval(randomTimer);
randomTimer = null;
}
// pulse while running
btnRandom?.classList.add("is-running");
setRandomRunning(true);
const start = Date.now();
const durationMs = 1125; // 25% longer than 900ms
const durationMs = 1125;
const tickMs = 80;
randomTimer = setInterval(() => {
@@ -349,25 +383,27 @@
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
btnRandom?.classList.remove("is-running");
setRandomRunning(false);
}
}, tickMs);
}
/* -----------------------------
BIT WIDTH
BIT WIDTH CONTROLS
----------------------------- */
function setBitWidth(n) {
buildBits(clampInt(n, 1, 64));
const v = clampInt(n, 1, 64);
buildBits(v);
}
/* -----------------------------
TOOLBOX VISIBILITY
TOOLBOX TOGGLE
----------------------------- */
function setToolboxVisible(isVisible) {
if (!toolboxPanel) return;
toolboxPanel.style.display = isVisible ? "flex" : "none";
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
function setToolboxCollapsed(collapsed) {
if (!binaryPage) return;
binaryPage.classList.toggle("toolboxCollapsed", !!collapsed);
const expanded = !collapsed;
toolboxToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
}
/* -----------------------------
@@ -384,8 +420,8 @@
btnCustomDenary?.addEventListener("click", () => {
const v = prompt(
isTwosMode()
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
);
if (v === null) return;
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
@@ -406,8 +442,12 @@
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
toolboxToggle?.addEventListener("click", () => {
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
setToolboxVisible(!isOpen);
const isCollapsed = binaryPage?.classList.contains("toolboxCollapsed");
setToolboxCollapsed(!isCollapsed);
});
window.addEventListener("resize", () => {
computeColsForBitsGrid();
});
/* -----------------------------
@@ -415,5 +455,5 @@
----------------------------- */
updateModeHint();
buildBits(bitCount);
setToolboxVisible(true);
})();
setToolboxCollapsed(false);
})();

241
src/scripts/hexColours.js Normal file
View File

@@ -0,0 +1,241 @@
// src/scripts/hexColours.js
// Computing:Box — Hex Colours logic
(() => {
/* -----------------------------
DOM
----------------------------- */
const colorGrid = document.getElementById("colorGrid");
const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber");
const hexEl = document.getElementById("hexNumber");
const previewColor = document.getElementById("previewColor");
const previewInverted = document.getElementById("previewInverted");
const btnCustomHex = document.getElementById("btnCustomHex");
const btnCustomRGB = document.getElementById("btnCustomRGB");
const btnInvert = document.getElementById("btnInvert");
const btnRandom = document.getElementById("btnRandom");
const btnClear = document.getElementById("btnClear");
const toolboxToggle = document.getElementById("toolboxToggle");
const colorPage = document.getElementById("colorPage");
/* -----------------------------
STATE
----------------------------- */
// rgb[0]=Red, rgb[1]=Green, rgb[2]=Blue (Values 0-255)
let rgb = [0, 0, 0];
let randomTimer = null;
/* -----------------------------
BUILD UI
----------------------------- */
function buildGrid() {
if (!colorGrid) return;
colorGrid.innerHTML = "";
const colorClasses = ['text-red', 'text-green', 'text-blue'];
for (let c = 0; c < 3; c++) {
const group = document.createElement("div");
group.className = "colorGroup";
for (let i = 1; i >= 0; i--) {
const col = document.createElement("div");
col.className = "hexCol";
let cardHTML = `
<div class="hexCard">
<div class="hexCardButtons">
<button class="hexCardBtn inc" id="colorInc-${c}-${i}">▲</button>
<button class="hexCardBtn dec" id="colorDec-${c}-${i}">▼</button>
</div>
<div class="hexDigitDisplay num ${colorClasses[c]}" id="colorDisplay-${c}-${i}">0</div>
<div class="hexNibbleRow">
`;
for (let j = 3; j >= 0; j--) {
cardHTML += `
<div class="hexNibbleBit">
<div class="bulb hexNibbleBulb" id="colorBulb-${c}-${i}-${j}" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
</svg>
</div>
<div class="hexNibbleLabel">${1 << j}</div>
</div>
`;
}
cardHTML += `
</div>
</div>
<div class="hexColWeight ${colorClasses[c]}">${16 ** i}</div>
`;
col.innerHTML = cardHTML;
const incBtn = col.querySelector(`#colorInc-${c}-${i}`);
const decBtn = col.querySelector(`#colorDec-${c}-${i}`);
incBtn.addEventListener("click", () => {
const weight = 16 ** i;
rgb[c] = (rgb[c] + weight) % 256;
updateUI();
});
decBtn.addEventListener("click", () => {
const weight = 16 ** i;
rgb[c] = (rgb[c] - weight + 256) % 256;
updateUI();
});
group.appendChild(col);
}
colorGrid.appendChild(group);
}
}
/* -----------------------------
UI UPDATE
----------------------------- */
function updateUI() {
if (denaryEl) {
denaryEl.innerHTML = `
<span class="text-red">${rgb[0]}</span>
<span class="text-green">${rgb[1]}</span>
<span class="text-blue">${rgb[2]}</span>
`;
}
const hexVals = rgb.map(v => v.toString(16).padStart(2, '0').toUpperCase());
const fullHexString = `#${hexVals.join('')}`;
if (hexEl) {
hexEl.innerHTML = `
<span class="text-red"><span style="color:var(--muted)">#</span>${hexVals[0]}</span>
<span class="text-green">${hexVals[1]}</span>
<span class="text-blue">${hexVals[2]}</span>
`;
}
if (binaryEl) {
binaryEl.innerHTML = `
<span class="text-red">${rgb[0].toString(2).padStart(8, '0')}</span>
<span class="text-green">${rgb[1].toString(2).padStart(8, '0')}</span>
<span class="text-blue">${rgb[2].toString(2).padStart(8, '0')}</span>
`;
}
if (previewColor) previewColor.style.backgroundColor = fullHexString;
const invertedHexString = "#" + rgb.map(v => (255 - v).toString(16).padStart(2, '0').toUpperCase()).join('');
if (previewInverted) previewInverted.style.backgroundColor = invertedHexString;
for (let c = 0; c < 3; c++) {
const val = rgb[c];
const nibbles = [val % 16, Math.floor(val / 16)];
for (let i = 0; i < 2; i++) {
const display = document.getElementById(`colorDisplay-${c}-${i}`);
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
for (let j = 0; j < 4; j++) {
const bulb = document.getElementById(`colorBulb-${c}-${i}-${j}`);
if (bulb) {
const isOn = (nibbles[i] & (1 << j)) !== 0;
bulb.classList.toggle("on", isOn);
}
}
}
}
}
/* -----------------------------
ACTIONS
----------------------------- */
function clearAll() {
rgb = [0, 0, 0];
updateUI();
}
function setRandomOnce() {
const arr = new Uint8Array(3);
crypto.getRandomValues(arr);
rgb = [arr[0], arr[1], arr[2]];
updateUI();
}
function runRandomBriefly() {
if (randomTimer) {
clearInterval(randomTimer);
randomTimer = null;
}
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
const start = Date.now();
const durationMs = 1125;
const tickMs = 80;
randomTimer = setInterval(() => {
setRandomOnce();
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
}
}, tickMs);
}
/* -----------------------------
EVENTS
----------------------------- */
btnCustomHex?.addEventListener("click", () => {
let v = prompt("Enter a 6-character hex code (e.g. FF0055):");
if (v === null) return;
v = v.replace(/\s+/g, "").replace(/^#/i, "").toUpperCase();
if (!/^[0-9A-F]{6}$/.test(v)) return alert("Invalid hex code. Please enter exactly 6 hexadecimal characters.");
rgb = [
parseInt(v.substring(0, 2), 16),
parseInt(v.substring(2, 4), 16),
parseInt(v.substring(4, 6), 16)
];
updateUI();
});
btnCustomRGB?.addEventListener("click", () => {
const v = prompt("Enter R, G, B values (0-255) separated by commas (e.g. 255, 128, 0):");
if (v === null) return;
const parts = v.split(',').map(s => parseInt(s.trim(), 10));
if (parts.length !== 3 || parts.some(isNaN) || parts.some(n => n < 0 || n > 255)) {
return alert("Invalid input. Please provide three numbers between 0 and 255.");
}
rgb = parts;
updateUI();
});
btnInvert?.addEventListener("click", () => {
rgb = rgb.map(v => 255 - v);
updateUI();
});
btnClear?.addEventListener("click", clearAll);
btnRandom?.addEventListener("click", runRandomBriefly);
toolboxToggle?.addEventListener("click", () => {
const isCollapsed = colorPage?.classList.contains("toolboxCollapsed");
colorPage.classList.toggle("toolboxCollapsed", !isCollapsed);
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
});
/* -----------------------------
INIT
----------------------------- */
buildGrid();
updateUI();
})();

312
src/scripts/hexadecimal.js Normal file
View File

@@ -0,0 +1,312 @@
// src/scripts/hexadecimal.js
// Computing:Box — Hexadecimal page logic
(() => {
/* -----------------------------
DOM
----------------------------- */
const hexGrid = document.getElementById("hexGrid");
const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber");
const hexEl = document.getElementById("hexNumber");
const digitsInput = document.getElementById("digitsInput");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomHex = document.getElementById("btnCustomHex");
const btnDec = document.getElementById("btnDec");
const btnInc = document.getElementById("btnInc");
const btnClear = document.getElementById("btnClear");
const btnRandom = document.getElementById("btnRandom");
const btnDigitsUp = document.getElementById("btnDigitsUp");
const btnDigitsDown = document.getElementById("btnDigitsDown");
const toolboxToggle = document.getElementById("toolboxToggle");
const hexPage = document.getElementById("hexPage");
/* -----------------------------
STATE
----------------------------- */
let hexCount = clampInt(Number(digitsInput?.value ?? 2), 1, 16);
let nibbles = new Array(hexCount).fill(0);
let randomTimer = null;
/* -----------------------------
HELPERS
----------------------------- */
function clampInt(n, min, max) {
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, Math.trunc(n)));
}
function maxExclusive() {
return 1n << BigInt(hexCount * 4);
}
function maxValue() {
return maxExclusive() - 1n;
}
function getValue() {
let v = 0n;
for (let i = 0; i < hexCount; i++) {
v += BigInt(nibbles[i]) << BigInt(i * 4);
}
return v;
}
function setValue(v) {
if (v < 0n) return false;
if (v > maxValue()) return false;
for (let i = 0; i < hexCount; i++) {
nibbles[i] = Number((v >> BigInt(i * 4)) & 0xFn);
}
return true;
}
/* -----------------------------
RESPONSIVE GRID
----------------------------- */
function computeColsForHexGrid() {
if (!hexGrid) return;
hexGrid.style.setProperty("--cols", String(Math.min(hexCount, 8)));
hexGrid.classList.toggle("bitsFew", hexCount < 4);
}
/* -----------------------------
BUILD UI (CARDS + BULBS)
----------------------------- */
function buildGrid(count) {
hexCount = clampInt(count, 1, 16);
if (digitsInput) digitsInput.value = String(hexCount);
const oldNibbles = nibbles.slice();
nibbles = new Array(hexCount).fill(0);
for (let i = 0; i < Math.min(oldNibbles.length, hexCount); i++) {
nibbles[i] = oldNibbles[i];
}
hexGrid.innerHTML = "";
for (let i = hexCount - 1; i >= 0; i--) {
const col = document.createElement("div");
col.className = "hexCol";
let cardHTML = `
<div class="hexCard">
<div class="hexCardButtons">
<button class="hexCardBtn inc" id="hexInc-${i}">▲</button>
<button class="hexCardBtn dec" id="hexDec-${i}">▼</button>
</div>
<div class="hexDigitDisplay num" id="hexDisplay-${i}">0</div>
<div class="hexNibbleRow">
`;
for(let j = 3; j >= 0; j--) {
cardHTML += `
<div class="hexNibbleBit">
<div class="bulb hexNibbleBulb" id="hexBulb-${i}-${j}" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
</svg>
</div>
<div class="hexNibbleLabel">${1 << j}</div>
</div>
`;
}
cardHTML += `
</div>
</div>
<div class="hexColWeight">${(1n << BigInt(i * 4)).toString()}</div>
`;
col.innerHTML = cardHTML;
const incBtn = col.querySelector(`#hexInc-${i}`);
const decBtn = col.querySelector(`#hexDec-${i}`);
incBtn.addEventListener("click", () => {
const span = maxExclusive();
const weight = 1n << BigInt(i * 4);
setValue((getValue() + weight) % span);
updateUI();
});
decBtn.addEventListener("click", () => {
const span = maxExclusive();
const weight = 1n << BigInt(i * 4);
setValue((getValue() - weight + span) % span);
updateUI();
});
hexGrid.appendChild(col);
}
computeColsForHexGrid();
updateUI();
}
/* -----------------------------
UI UPDATE
----------------------------- */
function updateUI() {
const val = getValue();
if (denaryEl) denaryEl.textContent = val.toString();
if (hexEl) hexEl.textContent = val.toString(16).toUpperCase().padStart(hexCount, '0');
if (binaryEl) {
let binStr = "";
for (let i = hexCount - 1; i >= 0; i--) {
binStr += nibbles[i].toString(2).padStart(4, '0') + " ";
}
binaryEl.textContent = binStr.trimEnd();
}
for (let i = 0; i < hexCount; i++) {
const display = document.getElementById(`hexDisplay-${i}`);
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
for (let j = 0; j < 4; j++) {
const bulb = document.getElementById(`hexBulb-${i}-${j}`);
if (bulb) {
const isOn = (nibbles[i] & (1 << j)) !== 0;
bulb.classList.toggle("on", isOn);
}
}
}
}
/* -----------------------------
CLEAR / INC / DEC
----------------------------- */
function clearAll() {
nibbles.fill(0);
buildGrid(2);
}
function increment() {
const span = maxExclusive();
setValue((getValue() + 1n) % span);
updateUI();
}
function decrement() {
const span = maxExclusive();
setValue((getValue() - 1n + span) % span);
updateUI();
}
/* -----------------------------
RANDOM
----------------------------- */
function cryptoRandomBigInt(maxExcl) {
if (maxExcl <= 0n) return 0n;
const bitLen = maxExcl.toString(2).length;
const byteLen = Math.ceil(bitLen / 8);
while (true) {
const bytes = new Uint8Array(byteLen);
crypto.getRandomValues(bytes);
let x = 0n;
for (const b of bytes) x = (x << 8n) | BigInt(b);
const extraBits = BigInt(byteLen * 8 - bitLen);
if (extraBits > 0n) x = x >> extraBits;
if (x < maxExcl) return x;
}
}
function setRandomOnce() {
const u = cryptoRandomBigInt(maxExclusive());
setValue(u);
updateUI();
}
function runRandomBriefly() {
if (randomTimer) {
clearInterval(randomTimer);
randomTimer = null;
}
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
const start = Date.now();
const durationMs = 1125;
const tickMs = 80;
randomTimer = setInterval(() => {
setRandomOnce();
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
}
}, tickMs);
}
/* -----------------------------
EVENTS
----------------------------- */
btnCustomHex?.addEventListener("click", () => {
const v = prompt(`Enter hexadecimal (0-9, A-F). Current width: ${hexCount} digits`);
if (v === null) return;
const clean = v.replace(/\s+/g, "").toUpperCase();
if (!/^[0-9A-F]+$/.test(clean)) return alert("Invalid hexadecimal.");
if (clean.length > hexCount) return alert("Value too large for current digit width.");
if (!setValue(BigInt("0x" + clean))) alert("Value out of range.");
else updateUI();
});
btnCustomBinary?.addEventListener("click", () => {
const v = prompt(`Enter binary (0, 1). Current width: ${hexCount * 4} bits`);
if (v === null) return;
const clean = v.replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return alert("Invalid binary.");
if (clean.length > hexCount * 4) return alert("Value too large for current digit width.");
if (!setValue(BigInt("0b" + clean))) alert("Value out of range.");
else updateUI();
});
btnCustomDenary?.addEventListener("click", () => {
const v = prompt(`Enter denary (0 to ${maxValue().toString()}):`);
if (v === null) return;
const clean = v.trim();
if (!/^\d+$/.test(clean)) return alert("Invalid denary. Digits only.");
if (!setValue(BigInt(clean))) alert(`Value out of range. Enter a number between 0 and ${maxValue().toString()}.`);
else updateUI();
});
btnInc?.addEventListener("click", increment);
btnDec?.addEventListener("click", decrement);
btnClear?.addEventListener("click", clearAll);
btnRandom?.addEventListener("click", runRandomBriefly);
btnDigitsUp?.addEventListener("click", () => buildGrid(hexCount + 1));
btnDigitsDown?.addEventListener("click", () => buildGrid(hexCount - 1));
digitsInput?.addEventListener("change", () => buildGrid(Number(digitsInput.value)));
toolboxToggle?.addEventListener("click", () => {
const isCollapsed = hexPage?.classList.contains("toolboxCollapsed");
hexPage.classList.toggle("toolboxCollapsed", !isCollapsed);
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
});
window.addEventListener("resize", computeColsForHexGrid);
/* -----------------------------
INIT
----------------------------- */
buildGrid(hexCount);
})();

488
src/scripts/logicGates.js Normal file
View File

@@ -0,0 +1,488 @@
// src/scripts/logicGates.js
// Computing:Box — Drag & Drop Logic Builder
(() => {
/* --- DOM Elements --- */
const workspace = document.getElementById("workspace");
const viewport = document.getElementById("viewport");
const wireLayer = document.getElementById("wireLayer");
const ttContainer = document.getElementById("truthTableContainer");
const toolboxGrid = document.getElementById("toolboxGrid");
const btnClearBoard = document.getElementById("btnClearBoard");
const toolboxToggle = document.getElementById("toolboxToggle");
const logicPage = document.getElementById("logicPage");
/* --- ANSI Gate SVGs --- */
const GATE_SVGS = {
'AND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M70,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/></g>`,
'OR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M70,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/></g>`,
'NOT': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,25 L30,25 M71,25 L100,25"/><path d="M30,10 L60,25 L30,40 Z" fill="var(--bg)"/><circle cx="65.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
'NAND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M80,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
'NOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M80,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
'XOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M75,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/></g>`,
'XNOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M85,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/><circle cx="79.5" cy="25" r="4.5" fill="var(--bg)"/></g>`
};
const INPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
const OUTPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
/* --- State --- */
let nodes = {};
let connections = [];
let nextNodeId = 1;
let nextWireId = 1;
// Interaction State
let isDraggingNode = null;
let dragOffset = { x: 0, y: 0 };
let clickStartX = 0, clickStartY = 0;
let wiringStart = null;
let tempWirePath = null;
let selectedWireId = null;
let selectedNodeId = null;
// Camera State (Pan & Zoom)
let panX = 0, panY = 0, zoom = 1;
let isPanning = false;
let panStart = { x: 0, y: 0 };
/* --- Setup Toolbox --- */
function initToolbox() {
if(!toolboxGrid) return;
let html = `
<div draggable="true" data-spawn="INPUT" class="drag-item tb-icon-box" title="Input Toggle">
<div class="switch" style="pointer-events:none;"><span class="slider"></span></div>
<div class="tb-icon-label">Input</div>
</div>
<div draggable="true" data-spawn="OUTPUT" class="drag-item tb-icon-box" title="Output Bulb">
<div class="bulb on" style="pointer-events:none; width:28px; height:28px;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
<div class="tb-icon-label">Output</div>
</div>
`;
Object.keys(GATE_SVGS).forEach(gate => {
html += `
<div draggable="true" data-spawn="GATE" data-gate="${gate}" class="drag-item tb-icon-box" title="${gate} Gate">
<svg viewBox="0 0 100 50" style="width:50px; height:25px; pointer-events:none;">${GATE_SVGS[gate]}</svg>
<div class="tb-icon-label">${gate}</div>
</div>
`;
});
toolboxGrid.innerHTML = html;
document.querySelectorAll('.drag-item').forEach(item => {
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('spawnType', item.dataset.spawn);
if(item.dataset.spawn === 'GATE') e.dataTransfer.setData('gateType', item.dataset.gate);
});
});
}
/* --- Camera Math --- */
function updateViewport() {
viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
workspace.style.backgroundSize = `${24 * zoom}px ${24 * zoom}px`;
workspace.style.backgroundPosition = `${panX}px ${panY}px`;
}
function zoomWorkspace(factor, mouseX, mouseY) {
const newZoom = Math.min(Math.max(0.2, zoom * factor), 3);
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();
// Calculate backwards through camera scale/pan to find true local coordinates
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, 'out');
const to = getPortCoords(conn.toNode, `in${conn.toPort}`);
const sourceNode = nodes[conn.fromNode];
const isActive = sourceNode && sourceNode.value === true;
const isSelected = conn.id === selectedWireId;
svgHTML += `<path class="lg-wire ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
});
if (wiringStart && tempWirePath) {
svgHTML += `<path class="lg-wire lg-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
}
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('.lg-node.selected').forEach(el => el.classList.remove('selected'));
renderWires();
}
/* --- Logic Evaluation --- */
function evaluateGraph(overrideInputs = null) {
let context = {};
Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => {
context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value;
});
let changed = true; let loops = 0;
while (changed && loops < 10) {
changed = false; loops++;
Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => {
let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1');
let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2');
let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false;
let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false;
let res = false;
switch(gate.gateType) {
case 'AND': res = val1 && val2; break;
case 'OR': res = val1 || val2; break;
case 'NOT': res = !val1; break;
case 'NAND': res = !(val1 && val2); break;
case 'NOR': res = !(val1 || val2); break;
case 'XOR': res = val1 !== val2; break;
case 'XNOR': res = val1 === val2; break;
}
if (context[gate.id] !== res) { context[gate.id] = res; changed = true; }
});
}
let outStates = {};
Object.values(nodes).filter(n => n.type === 'OUTPUT').forEach(out => {
let conn = connections.find(c => c.toNode === out.id);
let res = conn ? (context[conn.fromNode] || false) : false;
outStates[out.id] = res;
});
if (!overrideInputs) {
Object.values(nodes).forEach(n => {
if (n.type === 'GATE') n.value = context[n.id] || false;
if (n.type === 'OUTPUT') {
n.value = outStates[n.id] || false;
const bulb = n.el.querySelector('.bulb');
if (bulb) bulb.classList.toggle('on', n.value);
}
});
}
return outStates;
}
/* --- Truth Table Generation --- */
function generateTruthTable() {
if (!ttContainer) return;
const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label));
const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').sort((a,b) => a.label.localeCompare(b.label));
if (inNodes.length === 0 || outNodes.length === 0) {
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Add inputs and outputs to generate table.</div>'; return;
}
if (inNodes.length > 6) {
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Maximum 6 inputs supported.</div>'; return;
}
let html = '<table class="tt-table"><thead><tr>';
inNodes.forEach(n => html += `<th>${n.label}</th>`);
outNodes.forEach(n => html += `<th style="color:var(--text);">${n.label}</th>`);
html += '</tr></thead><tbody>';
const numRows = Math.pow(2, inNodes.length);
for (let i = 0; i < numRows; i++) {
let override = {};
inNodes.forEach((n, idx) => { override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; });
let outStates = evaluateGraph(override);
html += '<tr>';
inNodes.forEach(n => { let val = override[n.id]; html += `<td class="${val ? 'tt-on' : ''}">${val ? 1 : 0}</td>`; });
outNodes.forEach(n => { let val = outStates[n.id]; html += `<td class="${val ? 'tt-on' : ''}" style="font-weight:bold;">${val ? 1 : 0}</td>`; });
html += '</tr>';
}
html += '</tbody></table>';
ttContainer.innerHTML = html;
}
function runSimulation() {
evaluateGraph();
renderWires();
generateTruthTable();
}
/* --- Smart Label Generation --- */
function getNextInputLabel() {
let charCode = 65;
while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { charCode++; }
return String.fromCharCode(charCode);
}
function getNextOutputLabel() {
let idx = 1;
while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { idx++; }
return 'Q' + idx;
}
/* --- Node Creation --- */
function createNodeElement(node) {
const el = document.createElement('div');
el.className = `lg-node`; el.dataset.id = node.id;
el.style.left = `${node.x}px`; el.style.top = `${node.y}px`;
let innerHTML = `<div class="lg-header">${node.label}</div><div class="lg-gate-container">`;
if (node.type === 'INPUT') {
innerHTML += `
<div class="switch" style="margin:0;"><span class="slider"></span></div>
${INPUT_SVG}
<div class="lg-port" data-port="out" style="top: 25px; left: 86px;"></div>
`;
}
else if (node.type === 'OUTPUT') {
innerHTML += `
<div class="lg-port" data-port="in1" style="top: 25px; left: 0;"></div>
${OUTPUT_SVG}
<div class="bulb" style="margin:0;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
`;
}
else if (node.type === 'GATE') {
const isNot = node.gateType === 'NOT';
innerHTML += `
<div class="lg-port" data-port="in1" style="top: ${isNot ? '25px' : '15px'}; left: 0;"></div>
${!isNot ? `<div class="lg-port" data-port="in2" style="top: 35px; left: 0;"></div>` : ''}
<svg class="lg-gate-svg" viewBox="0 0 100 50">${GATE_SVGS[node.gateType]}</svg>
<div class="lg-port" data-port="out" style="top: 25px; left: 100px;"></div>
`;
}
innerHTML += `</div>`;
el.innerHTML = innerHTML;
viewport.appendChild(el);
node.el = el;
if (node.type === 'INPUT') {
el.querySelector('.switch').addEventListener('click', (e) => {
const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY);
if (dist > 3) {
e.preventDefault(); // Prevents toggle if it was a drag motion
} else {
node.value = !node.value;
el.querySelector('.switch').classList.toggle('active-sim', node.value);
el.querySelector('.slider').style.background = node.value ? 'rgba(40,240,122,.25)' : '';
el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : '';
el.querySelector('.slider').innerHTML = node.value ? `<style>#logicPage [data-id="${node.id}"] .slider::before { transform: translateX(28px); }</style>` : '';
runSimulation();
}
});
}
return el;
}
function spawnNode(type, gateType = null, dropX = null, dropY = null) {
let label = '';
if (type === 'INPUT') label = getNextInputLabel();
if (type === 'OUTPUT') label = getNextOutputLabel();
if (type === 'GATE') label = gateType;
const id = `node_${nextNodeId++}`;
const offset = Math.floor(Math.random() * 40);
const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset);
const y = dropY !== null ? dropY : 150 + offset;
const node = { id, type, gateType, label, x, y, value: false, el: null };
nodes[id] = node;
createNodeElement(node);
runSimulation();
}
/* --- Global Interaction Handlers --- */
// Camera Zoom Controls
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();
const factor = e.deltaY < 0 ? 1.1 : (1/1.1);
zoomWorkspace(factor, e.clientX - wsRect.left, e.clientY - wsRect.top);
});
workspace.addEventListener('mousedown', (e) => {
clickStartX = e.clientX; clickStartY = e.clientY;
const port = e.target.closest('.lg-port');
if (port) {
const nodeEl = port.closest('.lg-node');
const portId = port.dataset.port;
if (portId.startsWith('in')) {
const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', ''));
if (existingIdx !== -1) { connections.splice(existingIdx, 1); runSimulation(); return; }
}
if (portId === 'out') {
const coords = getPortCoords(nodeEl.dataset.id, 'out');
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('.lg-wire');
if (wire && wire.dataset.connId) {
clearSelection();
selectedWireId = wire.dataset.connId;
renderWires();
e.stopPropagation();
return;
}
const nodeEl = e.target.closest('.lg-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 };
return;
}
// Clicked empty space -> Pan Camera
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;
node.x = newX; node.y = newY;
updateNodePositions();
}
if (wiringStart) {
tempWirePath = {
x: (e.clientX - wsRect.left - panX) / zoom,
y: (e.clientY - wsRect.top - panY) / zoom
};
renderWires();
}
});
window.addEventListener('mouseup', (e) => {
isDraggingNode = null;
isPanning = false;
if (wiringStart) {
const port = e.target.closest('.lg-port');
if (port && port.dataset.port.startsWith('in')) {
const targetNodeId = port.closest('.lg-node').dataset.id;
const targetPortId = port.dataset.port.replace('in', '');
if (targetNodeId !== wiringStart.node) {
connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId));
connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: 'out', toNode: targetNodeId, toPort: targetPortId });
}
}
wiringStart = null; tempWirePath = null;
runSimulation();
}
});
/* --- Deletion --- */
window.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (selectedWireId) {
connections = connections.filter(c => c.id !== selectedWireId);
clearSelection(); runSimulation();
}
else if (selectedNodeId) {
connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId);
if (nodes[selectedNodeId] && nodes[selectedNodeId].el) {
viewport.removeChild(nodes[selectedNodeId].el);
}
delete nodes[selectedNodeId];
clearSelection(); runSimulation();
}
}
});
/* --- Drag and Drop --- */
workspace.addEventListener('dragover', (e) => { e.preventDefault(); });
workspace.addEventListener('drop', (e) => {
e.preventDefault();
const spawnType = e.dataTransfer.getData('spawnType');
if (spawnType) {
const gateType = e.dataTransfer.getData('gateType');
const wsRect = workspace.getBoundingClientRect();
const x = (e.clientX - wsRect.left - panX) / zoom - 40;
const y = (e.clientY - wsRect.top - panY) / zoom - 30;
spawnNode(spawnType, gateType || null, x, y);
}
});
/* --- Init --- */
btnClearBoard?.addEventListener('click', () => {
viewport.querySelectorAll('.lg-node').forEach(el => el.remove());
nodes = {}; connections = [];
runSimulation();
});
toolboxToggle?.addEventListener("click", () => {
const isCollapsed = logicPage?.classList.contains("toolboxCollapsed");
logicPage.classList.toggle("toolboxCollapsed", !isCollapsed);
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
setTimeout(renderWires, 450);
});
initToolbox();
})();

423
src/scripts/pcBuilder.js Normal file
View File

@@ -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: `<rect width="600" height="550" fill="#15171c" rx="10" stroke="#333" stroke-width="4"/><rect x="20" y="20" width="380" height="400" fill="none" stroke="#222" stroke-width="2"/><rect x="20" y="440" width="180" height="90" fill="none" stroke="#222" stroke-width="2"/><rect x="440" y="20" width="140" height="510" fill="none" stroke="#222" stroke-width="2"/>`
},
'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: `<rect width="360" height="400" fill="#2C303A" rx="8" stroke="#4b5060" stroke-width="3"/><rect x="120" y="40" width="80" height="80" fill="#1f2229" stroke="#4b5060"/><rect x="230" y="30" width="15" height="100" fill="#1f2229"/><rect x="250" y="30" width="15" height="100" fill="#1f2229"/><rect x="270" y="30" width="15" height="100" fill="#1f2229"/><rect x="290" y="30" width="15" height="100" fill="#1f2229"/><rect x="40" y="200" width="280" height="15" fill="#15171c"/><rect x="40" y="300" width="280" height="15" fill="#15171c"/><rect x="120" y="170" width="80" height="15" fill="#1f2229"/><rect x="120" y="250" width="80" height="15" fill="#1f2229"/>`
},
'CPU': { name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {}, svg: `<rect width="80" height="80" fill="#0b381a"/><rect x="10" y="10" width="60" height="60" rx="4" fill="#d4d4d4"/><polygon points="5,75 15,75 5,65" fill="#ffd700"/><text x="40" y="45" fill="#555" font-family="sans-serif" font-size="14" font-weight="bold" text-anchor="middle">CPU</text>` },
'COOLER': { name: 'CPU Fan', w: 120, h: 120, z: 30, ports: [], slots: {}, svg: `<rect width="120" height="120" rx="60" fill="#1a1c23" stroke="#aaa" stroke-width="3"/><circle cx="60" cy="60" r="50" fill="#111"/><path d="M60,15 A45,45 0 0,1 105,60 L60,60 Z" fill="#444"/><path d="M105,60 A45,45 0 0,1 60,105 L60,60 Z" fill="#555"/><path d="M60,105 A45,45 0 0,1 15,60 L60,60 Z" fill="#444"/><path d="M15,60 A45,45 0 0,1 60,15 L60,60 Z" fill="#555"/><circle cx="60" cy="60" r="20" fill="#222"/>` },
'RAM': { name: 'DDR4 Memory', w: 15, h: 100, z: 20, ports: [], slots: {}, svg: `<rect width="15" height="100" fill="#111"/><rect x="2" y="5" width="11" height="80" fill="#2a2a2a"/><rect x="0" y="90" width="15" height="10" fill="#ffd700"/>` },
'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: `<rect width="280" height="60" rx="5" fill="#1a1a1a"/><circle cx="70" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><circle cx="140" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><circle cx="210" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><rect x="20" y="55" width="80" height="5" fill="#ffd700"/>` },
'M2_SSD': { name: 'M.2 NVMe SSD', w: 80, h: 15, z: 20, ports: [], slots: {}, svg: `<rect width="80" height="15" rx="1" fill="#000"/><rect x="10" y="2" width="20" height="11" fill="#1a1a1a"/><rect x="35" y="2" width="20" height="11" fill="#1a1a1a"/><rect x="60" y="2" width="10" height="11" fill="#ccc"/><rect x="0" y="0" width="4" height="15" fill="#ffd700"/>` },
'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: `<rect width="100" height="70" fill="#111" rx="4" stroke="#444"/><rect x="10" y="10" width="80" height="50" fill="#1a1a1a" rx="2" stroke="#222"/><text x="50" y="40" fill="#888" font-family="sans-serif" font-size="14" font-weight="bold" text-anchor="middle">SSD</text>` },
'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: `<rect width="120" height="140" fill="#d0d0d0" rx="4" stroke="#888"/><rect x="10" y="10" width="100" height="100" fill="#e0e0e0" rx="50"/><circle cx="60" cy="60" r="35" fill="#ddd" stroke="#aaa"/><circle cx="60" cy="60" r="10" fill="#999"/><rect x="30" y="120" width="60" height="10" fill="#111"/>` },
'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: `<rect width="160" height="90" rx="4" fill="#1a1a1a" stroke="#333" stroke-width="2"/><circle cx="80" cy="45" r="35" fill="#0a0a0a" stroke="#222" stroke-width="2"/><line x1="80" y1="10" x2="80" y2="80" stroke="#333" stroke-width="2"/><line x1="45" y1="45" x2="115" y2="45" stroke="#333" stroke-width="2"/><circle cx="80" cy="45" r="10" fill="#222"/>` },
'MONITOR': { name: 'Monitor', w: 240, h: 160, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}], svg: `<rect width="240" height="160" fill="#111" rx="5"/><rect x="10" y="10" width="220" height="120" fill="#000"/><rect x="100" y="140" width="40" height="20" fill="#222"/><rect x="60" y="150" width="120" height="10" fill="#222"/>` },
'KEYBOARD': { name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}], svg: `<rect width="180" height="60" fill="#111" rx="3"/><rect x="5" y="5" width="170" height="50" fill="#222" rx="2" stroke="#333" stroke-dasharray="8 8"/>` },
'MOUSE': { name: 'Mouse', w: 30, h: 50, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}], svg: `<rect width="30" height="50" fill="#111" rx="15"/><line x1="15" y1="0" x2="15" y2="20" stroke="#333" stroke-width="2"/><circle cx="15" cy="15" r="4" fill="#333"/>` },
'SPEAKER': { name: 'Speakers', w: 40, h: 80, z: 30, slots: {}, ports: [{id:'audio', x:20, y:10}], svg: `<rect width="40" height="80" fill="#111" rx="4"/><circle cx="20" cy="25" r="12" fill="#222"/><circle cx="20" cy="60" r="16" fill="#222"/>` }
};
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 += `
<div draggable="true" data-spawn="${partKey}" class="drag-item tb-icon-box" title="${PC_PARTS[partKey].name}">
<svg viewBox="0 0 ${PC_PARTS[partKey].w} ${PC_PARTS[partKey].h}" style="max-width:80%; max-height:40px; pointer-events:none;">${PC_PARTS[partKey].svg}</svg>
<div class="tb-icon-label">${partKey}</div>
</div>
`;
});
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 += `<path class="pb-wire active ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
});
if (wiringStart && tempWirePath) {
svgHTML += `<path class="pb-wire pb-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
}
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 = `
<div class="diag-cat">Core System</div>
<div class="diag-row"><span>CHASSIS</span><span style="color: ${hasCase ? '#28f07a' : '#ff5555'}">${hasCase ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>MOTHERBOARD</span><span style="color: ${hasMB ? '#28f07a' : '#ff5555'}">${hasMB ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>CPU</span><span style="color: ${hasCPU ? '#28f07a' : '#ff5555'}">${hasCPU ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>COOLING</span><span style="color: ${hasCooler ? '#28f07a' : '#ff5555'}">${hasCooler ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>MEMORY</span><span style="color: ${hasRAM ? '#28f07a' : '#ff5555'}">${hasRAM ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>POWER SPLY</span><span style="color: ${hasPSU ? '#28f07a' : '#ff5555'}">${hasPSU ? 'OK' : 'ERR'}</span></div>
<div class="diag-cat">Connections</div>
<div class="diag-row"><span>MB POWER</span><span style="color: ${mbPwr ? '#28f07a' : '#ff5555'}">${mbPwr ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>STORAGE</span><span style="color: ${hasStorage ? '#28f07a' : '#ff5555'}">${hasStorage ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>GPU POWER</span><span style="color: ${!hasGPU ? '#888' : (gpuPwr ? '#28f07a' : '#ff5555')}">${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}</span></div>
<div class="diag-row"><span>DISPLAY</span><span style="color: ${dispConn ? '#28f07a' : '#ff5555'}">${dispConn ? 'OK' : 'ERR'}</span></div>
<div class="diag-row"><span>USB DEVS</span><span style="color: #55aaff">${usbCount}</span></div>
<hr style="border-color: rgba(255,255,255,0.1); margin: 12px 0 8px 0;">
<div style="text-align:center; font-size: 28px; color: ${isBootable ? '#28f07a' : '#ff5555'}; font-family: var(--bit-font); letter-spacing: 2px;">
${isBootable ? 'BOOTING...' : 'HALTED'}
</div>
`;
}
/* --- 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 = `<svg class="pb-part-svg" viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}">${PC_PARTS[node.type].svg}</svg>`;
PC_PARTS[node.type].ports.forEach(p => {
innerHTML += `<div class="pb-port" data-port="${p.id}" style="left: ${p.x}px; top: ${p.y}px;"></div>`;
});
// Debug Labels for bare parts
if(node.type !== 'CASE' && node.type !== 'MB') {
innerHTML += `<div style="position:absolute; top:-20px; font-family:var(--ui-font); font-size:12px; color:var(--muted);">${node.type}</div>`;
}
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 = `<svg viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}" style="width:100%; height:100%;">${PC_PARTS[node.type].svg}</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();
})();

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,29 +0,0 @@
<footer class="siteFooter">
<div class="inner">
<div class="title">Computer Science Concept Simulators</div>
<div class="meta">
© 2025 Computing:Box · Created with 💗 by Mr Lyall<br />
Powered by ADCM Networks
</div>
</div>
</footer>
<style>
.siteFooter{
border-top: 1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.10);
}
.inner{
max-width:1200px;
margin:0 auto;
padding: 18px 20px;
color: rgba(255,255,255,.65);
font-size: 12px;
line-height: 1.6;
}
.title{
color: rgba(255,255,255,.80);
font-weight: 800;
margin-bottom: 6px;
}
</style>

View File

@@ -1,57 +0,0 @@
<header class="siteHeader">
<div class="inner">
<div class="brand">
<a href="/">Computing:Box</a>
</div>
<nav class="nav">
<a href="/binary">Binary</a>
<a href="/hexadecimal">Hexadecimal</a>
<a href="/hex-colours">Hex Colours</a>
<a href="/logic-gates">Logic Gates</a>
<a href="/about">About</a>
</nav>
</div>
</header>
<style>
.siteHeader{
height: 64px;
display:flex;
align-items:center;
border-bottom: 1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.10);
backdrop-filter: blur(8px);
}
.inner{
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0 20px;
display:flex;
align-items:center;
justify-content: space-between;
gap: 18px;
}
.brand a{
color:#fff;
text-decoration:none;
font-weight: 800;
letter-spacing: .02em;
}
.nav{
display:flex;
gap: 16px;
flex-wrap: wrap;
}
.nav a{
color: rgba(255,255,255,.78);
text-decoration:none;
font-weight: 700;
font-size: 13px;
letter-spacing:.02em;
}
.nav a:hover{
color:#fff;
}
</style>

View File

@@ -1,210 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

View File

@@ -1,104 +0,0 @@
---
import "./hex/hex-simulator.css";
---
<section class="hex-sim" data-hex-sim>
<div class="hex-main">
<div class="hex-readout">
<div class="hex-label">DENARY</div>
<div class="hex-number" data-out="denary">0</div>
<div class="hex-label hex-mt">HEXADECIMAL</div>
<div class="hex-number hex-number--small" data-out="hex">00</div>
<div class="hex-label hex-mt">BINARY</div>
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
</div>
<div class="hex-divider"></div>
<div class="hex-digits" data-out="digitsRow"></div>
</div>
<!-- Toolbox button -->
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
<span class="hex-toolbox-icon" aria-hidden="true">
<!-- toolbox icon -->
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
TOOLBOX
</button>
<!-- Toolbox panel -->
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
<div class="hex-panel">
<div class="hex-panel-title">SETTINGS</div>
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
<div class="hex-width">
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus"></button>
<div class="hex-width-readout">
<div class="hex-width-label">DIGITS</div>
<div class="hex-width-number" data-out="digitsCount">2</div>
</div>
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
</div>
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">CUSTOM NUMBER</div>
<div class="hex-grid-2">
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
</div>
<!-- Custom Binary + Random on SAME row, same size -->
<div class="hex-grid-2 hex-mt-sm">
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
</div>
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">TOOLS</div>
<div class="hex-tools-top">
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
</div>
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
</div>
</aside>
<!-- Custom number dialog -->
<dialog class="hex-dialog" data-out="dialog">
<div class="hex-dialog-card">
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
<div class="hex-dialog-hint" data-out="dialogHint"></div>
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
<div class="hex-dialog-actions">
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
</div>
</div>
</dialog>
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
</section>

View File

@@ -1,346 +0,0 @@
/* ================= Fonts to match Binary ================= */
/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
@font-face {
font-family: "DSEG7Classic";
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "SevenSegment";
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
url("/fonts/Seven-Segment.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
.hex-sim {
min-height: 100vh;
background: #14151c;
color: #e7e8ee;
padding: 28px;
}
.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
.hex-readout { text-align: center; }
.hex-label {
font-family: "SevenSegment", ui-sans-serif, system-ui;
font-size: 12px;
letter-spacing: 2px;
opacity: 0.7;
}
.hex-mt { margin-top: 12px; }
.hex-number {
font-family: "DSEG7Classic", ui-monospace, monospace;
font-size: 76px;
line-height: 1;
font-weight: 400;
color: #46ff8a;
text-shadow: 0 0 18px rgba(70,255,138,0.18);
}
.hex-number--small { font-size: 64px; }
.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
.hex-divider {
margin: 26px auto 18px;
height: 1px;
width: min(760px, 90%);
background: rgba(255,255,255,0.10);
}
/* ================= Main digit columns ================= */
.hex-digits {
margin-top: 18px;
display: flex;
justify-content: center;
gap: 18px;
flex-wrap: wrap;
}
.hex-digit-col {
width: 160px;
border-radius: 18px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.10);
padding: 12px;
display: grid;
gap: 10px;
justify-items: center;
}
.hex-digit-controls {
width: 100%;
display: flex;
justify-content: center;
gap: 10px;
}
.hex-digit-char {
font-size: 64px;
line-height: 1;
color: #46ff8a;
text-shadow: 0 0 18px rgba(70,255,138,0.18);
}
.hex-digit-place {
font-family: "SevenSegment", ui-monospace, monospace;
opacity: 0.65;
font-size: 14px;
letter-spacing: 1px;
}
/* ================= Bulbs (brightness changes) ================= */
.hex-bulbs {
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: end;
}
.hex-bulb {
display: grid;
justify-items: center;
gap: 6px;
opacity: 0.35;
filter: grayscale(30%);
transition: opacity 160ms ease, filter 160ms ease;
}
.hex-bulb .hex-bulb-cap {
width: 18px;
height: 18px;
border-radius: 999px;
background: rgba(255,255,255,0.22);
border: 1px solid rgba(255,255,255,0.14);
}
.hex-bulb .hex-bulb-glow {
width: 18px;
height: 10px;
border-radius: 999px;
background: rgba(70,255,138,0.0);
box-shadow: 0 0 0 rgba(70,255,138,0.0);
transition: background 160ms ease, box-shadow 160ms ease;
}
.hex-bulb .hex-bulb-label {
font-family: "SevenSegment", ui-monospace, monospace;
font-size: 12px;
opacity: 0.8;
}
.hex-bulb.is-on {
opacity: 1;
filter: none;
}
.hex-bulb.is-on .hex-bulb-cap {
background: rgba(255,255,255,0.35);
}
.hex-bulb.is-on .hex-bulb-glow {
background: rgba(70,255,138,0.25);
box-shadow: 0 0 18px rgba(70,255,138,0.35);
}
/* ================= Buttons (toolbox style reused everywhere) ================= */
.hex-btn {
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #e7e8ee;
font-weight: 800;
cursor: pointer;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.hex-btn:hover { background: rgba(255,255,255,0.10); }
.hex-btn--square {
width: 48px;
height: 48px;
padding: 0;
display: grid;
place-items: center;
font-size: 18px;
}
.hex-btn--wide { width: 100%; }
.hex-btn--green {
background: rgba(46, 200, 120, 0.18);
border-color: rgba(46,200,120,0.35);
}
.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
.hex-btn--green2 {
background: rgba(46, 200, 120, 0.18);
border-color: rgba(46,200,120,0.35);
}
.hex-btn--red {
background: rgba(220, 60, 70, 0.18);
border-color: rgba(220,60,70,0.35);
}
/* Random = green pulse while running */
.hex-btn--random.is-running {
border-color: rgba(80, 255, 160, 0.55);
background: rgba(46, 200, 120, 0.22);
box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
animation: hexPulseGreen 900ms ease-in-out infinite;
}
@keyframes hexPulseGreen {
0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
}
/* Reset = red background + pulse on hover */
.hex-btn--reset:hover {
background: rgba(220, 60, 70, 0.28);
border-color: rgba(255, 80, 90, 0.55);
animation: hexPulseRed 900ms ease-in-out infinite;
}
@keyframes hexPulseRed {
0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
}
/* ================= Toolbox button + panel (slide) ================= */
.hex-toolbox-btn {
position: fixed;
top: 88px;
right: 28px;
z-index: 30;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #e7e8ee;
font-weight: 800;
letter-spacing: 1px;
cursor: pointer;
}
.hex-toolbox-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: #ff4fa6;
filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
}
.hex-toolbox {
position: fixed;
top: 140px;
right: 28px;
width: 340px;
display: grid;
gap: 14px;
z-index: 25;
transform: translateX(0);
opacity: 1;
transition: transform 220ms ease, opacity 220ms ease;
}
.hex-toolbox:not(.is-open) {
transform: translateX(380px);
opacity: 0;
pointer-events: none;
}
.hex-panel {
border-radius: 16px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
padding: 14px;
}
.hex-panel-title {
font-family: "SevenSegment", ui-sans-serif, system-ui;
font-size: 12px;
letter-spacing: 2px;
opacity: 0.7;
margin-bottom: 10px;
}
.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
.hex-width {
display: grid;
grid-template-columns: 48px 1fr 48px;
gap: 10px;
align-items: center;
}
.hex-width-readout {
border-radius: 14px;
background: rgba(0,0,0,0.22);
border: 1px solid rgba(255,255,255,0.10);
padding: 10px 12px;
display: flex;
justify-content: space-between;
align-items: baseline;
}
.hex-width-label {
font-family: "SevenSegment", ui-sans-serif, system-ui;
opacity: 0.7;
font-weight: 800;
letter-spacing: 1px;
font-size: 12px;
}
.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.hex-mt-sm { margin-top: 10px; }
.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
/* ================= Dialog ================= */
.hex-dialog { border: none; padding: 0; background: transparent; }
.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
.hex-dialog-card {
width: min(560px, 92vw);
border-radius: 18px;
background: #1a1b24;
border: 1px solid rgba(255,255,255,0.12);
padding: 16px;
color: #e7e8ee;
}
.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-input {
width: 100%;
padding: 12px 12px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(0,0,0,0.25);
color: #e7e8ee;
font-size: 18px;
}
.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
@media (max-width: 900px) {
.hex-toolbox { width: min(360px, 92vw); right: 16px; }
.hex-toolbox-btn { right: 16px; }
.hex-number { font-size: 60px; }
.hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
}

View File

@@ -1,232 +0,0 @@
type DialogMode = "hex" | "den" | "bin";
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
if (!root) throw new Error("Hex simulator root not found");
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
const dialogError = root.querySelector<HTMLElement>('[data-out="dialogError"]')!;
let digits = 2; // 1..8
let value = 0; // unsigned denary
let randomTimer: number | null = null;
let dialogMode: DialogMode | null = null;
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
const maxForDigits = (d: number) => (16 ** d) - 1;
const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
function stopRandom(): void {
if (randomTimer !== null) window.clearInterval(randomTimer);
randomTimer = null;
randomBtn.classList.remove("is-running");
}
function startRandom(): void {
stopRandom();
const max = maxForDigits(digits);
const start = Date.now();
randomBtn.classList.add("is-running");
randomTimer = window.setInterval(() => {
value = Math.floor(Math.random() * (max + 1));
render();
if (Date.now() - start > 1600) stopRandom();
}, 90);
}
function render(): void {
const bits = digits * 4;
digitsCount.textContent = String(digits);
bitsHint.textContent = `= ${bits} bits`;
outDen.textContent = String(value);
outHex.textContent = padHex(value, digits);
outBin.textContent = groupBin(padBin(value, bits));
renderDigitsRow();
}
function renderDigitsRow(): void {
const hex = padHex(value, digits);
outDigitsRow.innerHTML = "";
for (let i = 0; i < digits; i++) {
const pow = digits - 1 - i;
const placeValue = 16 ** pow;
const digitChar = hex[i];
const digitVal = parseInt(digitChar, 16);
const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
const col = document.createElement("div");
col.className = "hex-digit-col";
col.innerHTML = `
<div class="hex-digit-controls">
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
</div>
<div class="hex-digit-char hex-font-number">${digitChar}</div>
<!-- bulbs: brightness changes based on nibble bits -->
<div class="hex-bulbs" aria-label="Nibble bits">
${[8,4,2,1].map((w, idx) => {
const on = nibbleBits[idx] === 1;
return `
<div class="hex-bulb ${on ? "is-on" : ""}">
<div class="hex-bulb-cap"></div>
<div class="hex-bulb-glow"></div>
<div class="hex-bulb-label">${w}</div>
</div>
`;
}).join("")}
</div>
<div class="hex-digit-place">${placeValue}</div>
`;
outDigitsRow.appendChild(col);
}
}
function openDialog(mode: DialogMode): void {
stopRandom();
dialogMode = mode;
dialogError.textContent = "";
dialogInput.value = "";
if (mode === "hex") {
dialogTitle.textContent = "Custom Hexadecimal";
dialogHint.textContent = `Enter 1${digits} hex digit(s) (09, AF).`;
dialogInput.placeholder = "A1";
dialogInput.inputMode = "text";
} else if (mode === "den") {
dialogTitle.textContent = "Custom Denary";
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
dialogInput.placeholder = "42";
dialogInput.inputMode = "numeric";
} else {
dialogTitle.textContent = "Custom Binary";
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
dialogInput.placeholder = "00101010";
dialogInput.inputMode = "text";
}
dialog.showModal();
window.setTimeout(() => dialogInput.focus(), 0);
}
function closeDialog(): void {
dialogMode = null;
dialogError.textContent = "";
if (dialog.open) dialog.close();
}
function applyDialog(): void {
const raw = (dialogInput.value || "").trim();
if (!dialogMode) return closeDialog();
if (raw.length === 0) return closeDialog();
const max = maxForDigits(digits);
const bits = digits * 4;
if (dialogMode === "hex") {
const v = raw.toUpperCase();
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 09 and AF only."; return; }
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
value = clamp(parseInt(v, 16), 0, max);
render();
return closeDialog();
}
if (dialogMode === "den") {
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
const n = Number(raw);
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
value = clamp(n, 0, max);
render();
return closeDialog();
}
// bin
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
value = clamp(parseInt(raw, 2), 0, max);
render();
return closeDialog();
}
function applyDigitDelta(i: number, delta: number): void {
stopRandom();
const hexArr = padHex(value, digits).split("");
let v = parseInt(hexArr[i], 16);
v = (v + delta) % 16;
if (v < 0) v += 16;
hexArr[i] = v.toString(16).toUpperCase();
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
render();
}
// dialog cancel / backdrop
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
dialog.addEventListener("click", (e) => {
const card = dialog.querySelector(".hex-dialog-card");
if (card && !card.contains(e.target as Node)) closeDialog();
});
dialogInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") applyDialog();
if (e.key === "Escape") closeDialog();
});
// main click handler
root.addEventListener("click", (e) => {
const btn = (e.target as HTMLElement).closest<HTMLElement>("[data-action]");
if (!btn) return;
const action = btn.getAttribute("data-action")!;
if (action === "toggleToolbox") {
toolbox.classList.toggle("is-open");
toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
return;
}
if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
if (action === "reset") { stopRandom(); value = 0; return render(); }
if (action === "random") { return startRandom(); }
if (action === "customHex") return openDialog("hex");
if (action === "customDenary") return openDialog("den");
if (action === "customBinary") return openDialog("bin");
if (action === "dialogCancel") return closeDialog();
if (action === "dialogApply") return applyDialog();
if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
});
render();

View File

@@ -1,118 +0,0 @@
---
const { title = "Computing:Box" } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{title}</title>
<style>
:root{
--nav-h: 108px; /* 3x-ish height */
--bg: #1f2027;
--text: #e8e8ee;
--muted: #a9acb8;
--line: rgba(255,255,255,.10);
}
body{
margin:0;
background:var(--bg);
color:var(--text);
}
.siteNav{
position: sticky;
top: 0;
z-index: 50;
height: var(--nav-h);
background: rgba(0,0,0,.10);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(8px);
}
.navInner{
height: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand{
display:flex;
align-items:center;
gap:12px;
text-decoration:none;
color:var(--text);
}
.brandLogo{
width: 2em;
height: 2em;
image-rendering: pixelated;
}
.brandName{
letter-spacing: .12em;
font-weight: 900;
text-transform: uppercase;
font-size: 18px;
}
.navLinks{
display:flex;
align-items:center;
gap:18px;
flex-wrap:wrap;
}
.navLinks a{
color: var(--muted);
text-decoration: none;
font-weight: 800;
letter-spacing: .12em;
font-size: 16px;
text-transform: uppercase;
}
.navLinks a:hover{
color: var(--text);
}
.pageWrap{
max-width: 1400px;
margin: 0 auto;
}
</style>
</head>
<body>
<header class="siteNav">
<div class="navInner">
<a class="brand" href="/">
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
<span class="brandName">COMPUTING:BOX</span>
</a>
<nav class="navLinks" aria-label="Site navigation">
<a href="/about">ABOUT</a>
<a href="/binary">BINARY</a>
<a href="/hexadecimal">HEXADECIMAL</a>
<a href="/hex-colours">HEX COLOURS</a>
<a href="/logic-gates">LOGIC GATES</a>
</nav>
</div>
</header>
<main class="pageWrap">
<slot />
</main>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,115 +0,0 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/binary.css";
---
<BaseLayout title="Binary Simulator">
<main class="wrap">
<!-- Toolbox toggle sits below navbar (navbar is in BaseLayout) -->
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<section class="topGrid">
<!-- LEFT -->
<div class="mainCol">
<div class="readout">
<div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Binary</div>
<!-- NOTE: JS writes exact bit-width here, so initial value doesn't matter much -->
<div id="binaryNumber" class="num binaryValue">00000000</div>
</div>
<div class="divider"></div>
<section class="bitsWrap" aria-label="Bit switches">
<div class="bitsGrid" id="bitsGrid"></div>
</section>
</div>
<!-- RIGHT (Toolbox panel) -->
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<!-- Settings -->
<div class="card">
<div class="cardTitle">Settings</div>
<div class="toggleRow">
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
<label class="switch" aria-label="Toggle mode">
<input id="modeToggle" type="checkbox" />
<span class="slider"></span>
</label>
<!-- keep this on ONE line -->
<div class="toggleLabel" id="lblTwos">Two&apos;s&nbsp;complement</div>
</div>
<div class="hint" id="modeHint">
Tip: In unsigned binary, all bits represent positive values.
</div>
<div class="subCard">
<div class="subTitle">Bit width</div>
<div class="bitWidthRow">
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button>
<div class="bitInputWrap">
<div class="bitInputLabel">Bits</div>
<input
id="bitsInput"
class="bitInput"
type="number"
inputmode="numeric"
min="1"
max="64"
step="1"
value="8"
aria-label="Number of bits"
/>
</div>
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
</div>
</div>
</div>
<!-- Custom Number -->
<div class="card">
<div class="cardTitle">Custom Number</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
</div>
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<!-- Tools -->
<div class="card">
<div class="cardTitle">Tools</div>
<div class="toolRowCentered">
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
</div>
<div class="toolRow2">
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
</div>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</main>
<script type="module" src="/src/scripts/binary.js"></script>
</BaseLayout>

View File

@@ -1,8 +0,0 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import HexSimulator from "../components/simulators/HexSimulator.astro";
---
<BaseLayout title="Hexadecimal | Computing:Box">
<HexSimulator />
</BaseLayout>

View File

@@ -1,11 +0,0 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
---
<Layout>
<Welcome />
</Layout>

View File

@@ -1,522 +0,0 @@
// src/scripts/binary.js
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
(() => {
/* -----------------------------
DOM
----------------------------- */
const bitsGrid = document.getElementById("bitsGrid");
const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber");
const bitsInput = document.getElementById("bitsInput");
const modeToggle = document.getElementById("modeToggle");
const modeHint = document.getElementById("modeHint");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnShiftLeft = document.getElementById("btnShiftLeft");
const btnShiftRight = document.getElementById("btnShiftRight");
const btnDec = document.getElementById("btnDec");
const btnInc = document.getElementById("btnInc");
const btnClear = document.getElementById("btnClear");
const btnRandom = document.getElementById("btnRandom");
const btnBitsUp = document.getElementById("btnBitsUp");
const btnBitsDown = document.getElementById("btnBitsDown");
const toolboxToggle = document.getElementById("toolboxToggle");
const toolboxPanel = document.getElementById("toolboxPanel");
/* -----------------------------
STATE
----------------------------- */
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
let bits = new Array(bitCount).fill(false);
let randomTimer = null;
// For responsive wrapping of the top binary display
let nibblesPerLine = null;
let wrapMeasureSpan = null;
/* -----------------------------
HELPERS
----------------------------- */
function clampInt(n, min, max) {
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, Math.trunc(n)));
}
function isTwosMode() {
return !!modeToggle?.checked;
}
function pow2Big(n) {
return 1n << BigInt(n);
}
function unsignedMaxExclusive(nBits) {
return pow2Big(nBits);
}
function unsignedMaxValue(nBits) {
return pow2Big(nBits) - 1n;
}
function twosMin(nBits) {
return -pow2Big(nBits - 1);
}
function twosMax(nBits) {
return pow2Big(nBits - 1) - 1n;
}
function bitsToUnsignedBigInt() {
let v = 0n;
for (let i = 0; i < bitCount; i++) {
if (bits[i]) v += pow2Big(i);
}
return v;
}
function unsignedBigIntToBits(vUnsigned) {
const span = unsignedMaxExclusive(bitCount);
const v = ((vUnsigned % span) + span) % span;
for (let i = 0; i < bitCount; i++) {
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
}
}
function bitsToSignedBigIntTwos() {
const u = bitsToUnsignedBigInt();
const signBit = bits[bitCount - 1] === true;
if (!signBit) return u;
return u - pow2Big(bitCount);
}
function signedBigIntToBitsTwos(vSigned) {
const span = pow2Big(bitCount);
let v = ((vSigned % span) + span) % span;
unsignedBigIntToBits(v);
}
function updateModeHint() {
if (!modeHint) return;
modeHint.textContent = isTwosMode()
? "Tip: In twos complement, the left-most bit (MSB) represents a negative value."
: "Tip: In unsigned binary, all bits represent positive values.";
}
/* -----------------------------
TOP BINARY DISPLAY: responsive wrap by nibble count
----------------------------- */
function ensureWrapMeasurer() {
if (wrapMeasureSpan || !binaryEl) return;
wrapMeasureSpan = document.createElement("span");
wrapMeasureSpan.style.position = "absolute";
wrapMeasureSpan.style.visibility = "hidden";
wrapMeasureSpan.style.whiteSpace = "pre";
wrapMeasureSpan.style.pointerEvents = "none";
// Inherit font/letterspacing from binaryEl
wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font;
wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing;
document.body.appendChild(wrapMeasureSpan);
}
function computeNibblesPerLine() {
if (!binaryEl) return null;
ensureWrapMeasurer();
// Available width = width of the readout area (binaryEl parent)
const host = binaryEl.parentElement;
if (!host) return null;
const hostW = host.getBoundingClientRect().width;
if (!Number.isFinite(hostW) || hostW <= 0) return null;
// Measure one nibble including trailing space ("0000 ")
wrapMeasureSpan.textContent = "0000 ";
const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1;
// Safety: keep at least 1 nibble per line
const max = Math.max(1, Math.floor(hostW / nibbleW));
return max;
}
function formatBinaryWrapped() {
// EXACT bitCount digits (no padding to 4)
let raw = "";
for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0";
// If <= 4 bits, do NOT insert spaces/newlines at all
if (bitCount <= 4) return raw;
const groups = [];
for (let i = 0; i < raw.length; i += 4) {
groups.push(raw.slice(i, i + 4));
}
const perLine = nibblesPerLine ?? groups.length;
if (perLine >= groups.length) return groups.join(" ");
const lines = [];
for (let i = 0; i < groups.length; i += perLine) {
lines.push(groups.slice(i, i + perLine).join(" "));
}
return lines.join("\n");
}
function refreshBinaryWrap() {
const next = computeNibblesPerLine();
// Only update if it actually changes (prevents jitter)
if (next !== nibblesPerLine) nibblesPerLine = next;
updateReadout(); // re-render with new wrap
}
/* -----------------------------
BUILD UI (BITS)
----------------------------- */
function buildBits(count) {
bitCount = clampInt(count, 1, 64);
if (bitsInput) bitsInput.value = String(bitCount);
const oldBits = bits.slice();
bits = new Array(bitCount).fill(false);
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
bitsGrid.innerHTML = "";
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
if (bitCount < 8) {
bitsGrid.style.setProperty("--cols", String(bitCount));
} else {
bitsGrid.style.removeProperty("--cols");
}
for (let i = bitCount - 1; i >= 0; i--) {
const bitEl = document.createElement("div");
bitEl.className = "bit";
bitEl.innerHTML = `
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
<div class="bitVal" id="bitLabel-${i}"></div>
<label class="switch" aria-label="Toggle bit ${i}">
<input type="checkbox" data-index="${i}">
<span class="slider"></span>
</label>
`;
bitsGrid.appendChild(bitEl);
}
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
input.addEventListener("change", () => {
const i = Number(input.dataset.index);
bits[i] = input.checked;
updateUI();
});
});
// bulb styling + 25% bigger (vs 26px previously)
for (let i = 0; i < bitCount; i++) {
const bulb = document.getElementById(`bulb-${i}`);
if (!bulb) continue;
bulb.style.width = "auto";
bulb.style.height = "auto";
bulb.style.border = "none";
bulb.style.background = "transparent";
bulb.style.borderRadius = "0";
bulb.style.boxShadow = "none";
bulb.style.opacity = "0.45";
bulb.style.fontSize = "32px";
bulb.style.lineHeight = "1";
bulb.style.display = "flex";
bulb.style.alignItems = "center";
bulb.style.justifyContent = "center";
bulb.style.filter = "grayscale(1)";
bulb.textContent = "💡";
}
// wrapping may change when bit width changes
refreshBinaryWrap();
updateUI();
}
/* -----------------------------
UI UPDATE
----------------------------- */
function updateBitLabels() {
for (let i = 0; i < bitCount; i++) {
const label = document.getElementById(`bitLabel-${i}`);
if (!label) continue;
if (isTwosMode() && i === bitCount - 1) {
// Keep on one line (CSS: white-space:nowrap)
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
} else {
label.textContent = pow2Big(i).toString();
}
}
}
function syncSwitchesToBits() {
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
const i = Number(input.dataset.index);
input.checked = !!bits[i];
});
}
function updateBulbs() {
for (let i = 0; i < bitCount; i++) {
const bulb = document.getElementById(`bulb-${i}`);
if (!bulb) continue;
const on = bits[i] === true;
if (on) {
bulb.style.opacity = "1";
bulb.style.filter = "grayscale(0)";
bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)";
} else {
bulb.style.opacity = "0.45";
bulb.style.filter = "grayscale(1)";
bulb.style.textShadow = "none";
}
}
}
function updateReadout() {
if (!denaryEl || !binaryEl) return;
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
binaryEl.textContent = formatBinaryWrapped();
}
function updateUI() {
updateModeHint();
updateBitLabels();
syncSwitchesToBits();
updateBulbs();
updateReadout();
}
/* -----------------------------
SET FROM INPUT
----------------------------- */
function setFromBinaryString(binStr) {
const clean = String(binStr ?? "").replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return false;
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
for (let i = 0; i < bitCount; i++) {
const charFromRight = padded[padded.length - 1 - i];
bits[i] = charFromRight === "1";
}
updateUI();
return true;
}
function setFromDenaryInput(vStr) {
const raw = String(vStr ?? "").trim();
if (!raw) return false;
let v;
try {
if (!/^-?\d+$/.test(raw)) return false;
v = BigInt(raw);
} catch {
return false;
}
if (isTwosMode()) {
const min = twosMin(bitCount);
const max = twosMax(bitCount);
if (v < min || v > max) return false;
signedBigIntToBitsTwos(v);
} else {
if (v < 0n) return false;
if (v > unsignedMaxValue(bitCount)) return false;
unsignedBigIntToBits(v);
}
updateUI();
return true;
}
/* -----------------------------
SHIFTS
----------------------------- */
function shiftLeft() {
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
bits[0] = false;
updateUI();
}
function shiftRight() {
if (isTwosMode()) {
// arithmetic right shift: keep MSB
const msb = bits[bitCount - 1];
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
bits[bitCount - 1] = msb;
} else {
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
bits[bitCount - 1] = false;
}
updateUI();
}
/* -----------------------------
CLEAR / INC / DEC
----------------------------- */
function clearAll() {
bits.fill(false);
updateUI();
}
function increment() {
if (isTwosMode()) {
const min = twosMin(bitCount);
const max = twosMax(bitCount);
let v = bitsToSignedBigIntTwos() + 1n;
if (v > max) v = min;
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
}
updateUI();
}
function decrement() {
if (isTwosMode()) {
const min = twosMin(bitCount);
const max = twosMax(bitCount);
let v = bitsToSignedBigIntTwos() - 1n;
if (v < min) v = max;
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
}
updateUI();
}
/* -----------------------------
RANDOM
----------------------------- */
function cryptoRandomBigInt(maxExclusive) {
if (maxExclusive <= 0n) return 0n;
const bitLen = maxExclusive.toString(2).length;
const byteLen = Math.ceil(bitLen / 8);
while (true) {
const bytes = new Uint8Array(byteLen);
crypto.getRandomValues(bytes);
let x = 0n;
for (const b of bytes) x = (x << 8n) | BigInt(b);
const extraBits = BigInt(byteLen * 8 - bitLen);
if (extraBits > 0n) x = x >> extraBits;
if (x < maxExclusive) return x;
}
}
function setRandomOnce() {
const span = unsignedMaxExclusive(bitCount);
const u = cryptoRandomBigInt(span);
unsignedBigIntToBits(u);
updateUI();
}
function runRandomBriefly() {
if (randomTimer) {
clearInterval(randomTimer);
randomTimer = null;
}
const start = Date.now();
const durationMs = 1125; // (your “~25% longer” vs 900ms)
const tickMs = 80;
randomTimer = setInterval(() => {
setRandomOnce();
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
}
}, tickMs);
}
/* -----------------------------
BIT WIDTH CONTROLS
----------------------------- */
function setBitWidth(n) {
buildBits(clampInt(n, 1, 64));
}
/* -----------------------------
TOOLBOX TOGGLE (simple open/close state)
----------------------------- */
function setToolboxOpen(open) {
document.body.classList.toggle("toolboxClosed", !open);
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
refreshBinaryWrap(); // width changes when toolbox closes/opens
}
/* -----------------------------
EVENTS
----------------------------- */
modeToggle?.addEventListener("change", () => updateUI());
btnCustomBinary?.addEventListener("click", () => {
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
if (v === null) return;
if (!setFromBinaryString(v)) alert("Invalid binary");
});
btnCustomDenary?.addEventListener("click", () => {
const v = prompt(
isTwosMode()
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
);
if (v === null) return;
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
});
btnShiftLeft?.addEventListener("click", shiftLeft);
btnShiftRight?.addEventListener("click", shiftRight);
btnInc?.addEventListener("click", increment);
btnDec?.addEventListener("click", decrement);
btnClear?.addEventListener("click", clearAll);
btnRandom?.addEventListener("click", runRandomBriefly);
btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1));
btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1));
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
toolboxToggle?.addEventListener("click", () => {
const isOpen = !document.body.classList.contains("toolboxClosed");
setToolboxOpen(!isOpen);
});
// Recompute wrapping live when the window size changes
let resizeT = null;
window.addEventListener("resize", () => {
if (resizeT) clearTimeout(resizeT);
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
});
/* -----------------------------
INIT
----------------------------- */
updateModeHint();
buildBits(bitCount);
setToolboxOpen(true);
})();

View File

@@ -1,342 +0,0 @@
/*
Binary page styles (keeps the last-working simulator markup + binary.js).
Goals:
- Do NOT change any IDs/classes expected by src/scripts/binary.js
- Toolbox button toggles the ENTIRE right-hand column via body.toolboxClosed
- Fix toolbox button positioning (no overlap, consistent with header container)
- Fix spacing/consistency of cards + buttons
- Keep binary readout wrapping/bit-width behaviour from JS (\n in output)
*/
:root{
--panel-w: 360px;
--gap: 22px;
}
/* Page wrapper (inside BaseLayout .pageWrap) */
.wrap{
max-width: 1400px;
margin: 0 auto;
padding: 22px 20px 48px;
display: flex;
flex-direction: column;
gap: 14px;
}
/* Toolbox toggle button (sits below navbar, aligned right, never overlaps) */
.toolboxToggle{
align-self: flex-end;
position: sticky;
top: calc(var(--nav-h, 108px) + 14px);
z-index: 30;
display: inline-flex;
align-items: center;
gap: 10px;
height: 40px;
padding: 0 14px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
cursor: pointer;
}
.toolboxToggle:hover{ background: rgba(255,255,255,.08); }
.toolboxText{
letter-spacing: .12em;
font-weight: 900;
}
/* Main layout grid */
.topGrid{
display: grid;
grid-template-columns: 1fr var(--panel-w);
gap: var(--gap);
align-items: start;
}
/* Hide ENTIRE toolbox column when toggled closed */
body.toolboxClosed .topGrid{ grid-template-columns: 1fr; }
body.toolboxClosed #toolboxPanel{ display: none; }
.mainCol{ min-width: 0; }
/* Readout */
.readout{
text-align: center;
margin-top: 8px;
}
.label{
opacity: .8;
letter-spacing: .12em;
text-transform: uppercase;
font-size: 12px;
}
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
.num{
display: inline-block;
width: fit-content;
max-width: 100%;
white-space: pre-line; /* allows JS \n wraps */
letter-spacing: 2px;
}
.denaryValue{
font-size: 54px;
margin: 6px 0 10px;
}
.binaryValue{
font-size: 56px;
margin: 4px 0 18px;
}
.divider{
height: 1px;
background: rgba(255,255,255,.10);
margin: 14px auto 24px;
max-width: 900px;
}
/* Bits area */
.bitsWrap{ padding-top: 6px; }
.bitsGrid{
display: grid;
gap: 24px;
justify-content: center;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
max-width: 1200px;
margin: 0 auto;
}
.bitsGrid.bitsFew{ justify-content: center; }
.bit{
display: grid;
justify-items: center;
gap: 8px;
}
.bulb{
font-size: 32px; /* JS also bumps this */
line-height: 1;
opacity: .45;
}
.bitVal{
font-size: 22px;
line-height: 1.05;
text-align: center;
white-space: nowrap; /* keep -128 on one line */
}
/* Switch (existing classes assumed) */
.switch{
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.switch input{ display:none; }
.slider{
position:absolute;
inset:0;
border-radius:999px;
background: rgba(255,255,255,.18);
border: 1px solid rgba(255,255,255,.14);
}
.slider:before{
content:"";
position:absolute;
height: 22px;
width: 22px;
left: 3px;
top: 2.5px;
border-radius: 999px;
background: #fff;
transition: transform .18s ease;
}
.switch input:checked + .slider:before{ transform: translateX(22px); }
/* Toolbox column */
.panelCol{
position: sticky;
top: calc(var(--nav-h, 108px) + 72px); /* leaves space for sticky toolbox button */
align-self: start;
display: grid;
gap: 16px;
}
/* Cards */
.card{
border: 1px solid rgba(255,255,255,.12);
border-radius: 16px;
background: rgba(255,255,255,.05);
padding: 14px;
}
.cardTitle{
opacity: .8;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 12px;
margin-bottom: 10px;
}
.hint{
opacity: .7;
font-size: 11px;
margin-top: 10px;
line-height: 1.35;
}
/* Keep mode labels on one line */
.toggleRow{
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 10px;
align-items: center;
}
.toggleLabel{
font-size: 12px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
white-space: nowrap;
}
.subCard{
margin-top: 12px;
border: 1px solid rgba(255,255,255,.10);
border-radius: 14px;
background: rgba(0,0,0,.12);
padding: 12px;
}
.subTitle{
opacity: .8;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 11px;
margin-bottom: 10px;
}
.bitWidthRow{
display: grid;
grid-template-columns: 44px 1fr 44px;
gap: 10px;
align-items: center;
}
.bitInputWrap{
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
align-items: center;
padding: 10px 12px;
border: 1px solid rgba(255,255,255,.10);
border-radius: 12px;
background: rgba(255,255,255,.04);
}
.bitInputLabel{
opacity: .75;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 11px;
white-space: nowrap;
}
.bitInput{
width: 100%;
min-width: 0;
background: transparent;
border: none;
outline: none;
color: inherit;
font-size: 20px;
text-align: right;
}
.miniBtn{
height: 44px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.9);
font-size: 18px;
cursor: pointer;
}
.miniBtn:hover{ background: rgba(255,255,255,.08); }
/* Buttons */
.controlsRow{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.btn{
border-radius: 12px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
padding: 12px 12px;
font-weight: 800;
letter-spacing: .10em;
text-transform: uppercase;
cursor: pointer;
}
.btn:hover{ background: rgba(255,255,255,.08); }
.btnWide{ width: 100%; }
.btnAccent{
background: rgba(0,255,140,.12);
border-color: rgba(0,255,140,.22);
}
.btnAccent:hover{ background: rgba(0,255,140,.16); }
.toolRowCentered{
display: flex;
justify-content: center;
gap: 12px;
margin: 10px 0 12px;
}
.toolBtn{
width: 56px;
height: 56px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
font-size: 18px;
cursor: pointer;
}
.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
.toolRow2{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
/* Reset stays white text */
.btnReset{ color: rgba(255,255,255,.92); }
/* Responsive */
@media (max-width: 980px){
.topGrid{ grid-template-columns: 1fr; }
.panelCol{ position: static; }
.toolboxToggle{ position: static; align-self: flex-start; }
}

View File

@@ -1,85 +0,0 @@
:root{
--bg: #1f2027;
--panel: #22242d;
--panel2: rgba(255,255,255,.04);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
*{ box-sizing:border-box; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.siteHeader{
position: sticky;
top: 0;
z-index: 10;
background: rgba(0,0,0,.15);
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(255,255,255,.06);
}
.siteHeaderInner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 16px;
}
.brand{
color: var(--text);
text-decoration:none;
font-weight: 900;
letter-spacing:.02em;
}
.nav{
display:flex;
gap: 14px;
flex-wrap:wrap;
justify-content:flex-end;
}
.nav a{
color: var(--muted);
text-decoration:none;
font-weight: 700;
font-size: 14px;
}
.nav a:hover{ color: var(--text); }
.siteMain{
min-height: calc(100vh - 140px);
}
.siteFooter{
border-top: 1px solid rgba(255,255,255,.08);
margin-top: 32px;
background: rgba(0,0,0,.10);
}
.siteFooterInner{
max-width: 1200px;
margin: 0 auto;
padding: 18px 20px 26px;
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.footerTitle{
color: var(--text);
opacity:.9;
font-weight: 800;
margin-bottom: 6px;
}

View File

@@ -1,75 +0,0 @@
:root{
--bg: #1f2027;
--panel: rgba(255,255,255,.04);
--panel-border: rgba(255,255,255,.10);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
*{ box-sizing: border-box; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.site-header{
border-bottom: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.12);
}
.site-header__inner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 18px;
}
.brand{
font-weight: 800;
letter-spacing: .02em;
}
.nav{
display:flex;
gap: 14px;
flex-wrap: wrap;
justify-content:flex-end;
}
.nav__link{
color: var(--muted);
text-decoration:none;
font-weight: 700;
font-size: 13px;
}
.nav__link:hover{ color: var(--text); }
.site-main{
max-width: 1200px;
margin: 0 auto;
padding: 28px 20px 40px;
min-height: calc(100vh - 140px);
}
.site-footer{
border-top: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.10);
}
.site-footer__inner{
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}

View File

@@ -1,326 +0,0 @@
/* Binary page styles (moved OUT of binary.astro) */
:root{
--panel-w: 360px;
--gap: 22px;
}
.wrap{
max-width: 1400px;
margin: 0 auto;
padding: 26px 20px 48px;
position: relative;
}
.topGrid{
display: grid;
grid-template-columns: 1fr var(--panel-w);
gap: var(--gap);
align-items: start;
}
/* When toolbox is hidden, reclaim space + centre content */
body.toolboxClosed .topGrid{
grid-template-columns: 1fr;
}
body.toolboxClosed #toolboxPanel{
display: none;
}
.mainCol{
min-width: 0;
}
.readout{
text-align: center;
margin-top: 8px;
}
.label{
opacity: .8;
letter-spacing: .12em;
text-transform: uppercase;
font-size: 12px;
}
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
.num{
display: inline-block;
width: fit-content;
max-width: 100%;
white-space: pre-line; /* allows JS \n wraps */
letter-spacing: 2px;
}
.denaryValue{
font-size: 54px;
margin: 6px 0 10px;
}
.binaryValue{
font-size: 56px;
margin: 4px 0 18px;
}
.divider{
height: 1px;
background: rgba(255,255,255,.10);
margin: 14px auto 24px;
max-width: 900px;
}
.bitsWrap{
padding-top: 6px;
}
.bitsGrid{
display: grid;
gap: 24px;
justify-content: center;
}
/* Default: a single row of bits (will wrap automatically as bit count grows) */
.bitsGrid{
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
max-width: 1200px;
margin: 0 auto;
}
.bitsGrid.bitsFew{
justify-content: center;
}
/* Bit tile */
.bit{
display: grid;
justify-items: center;
gap: 8px;
}
.bulb{
font-size: 32px; /* JS also bumps this */
line-height: 1;
opacity: .45;
}
.bitVal{
font-size: 22px;
line-height: 1.05;
text-align: center;
white-space: nowrap; /* keep -128 on one line */
}
/* Switch (existing classes assumed) */
.switch{
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.switch input{ display:none; }
.slider{
position:absolute;
inset:0;
border-radius:999px;
background: rgba(255,255,255,.18);
border: 1px solid rgba(255,255,255,.14);
}
.slider:before{
content:"";
position:absolute;
height: 22px;
width: 22px;
left: 3px;
top: 2.5px;
border-radius: 999px;
background: #fff;
transition: transform .18s ease;
}
.switch input:checked + .slider:before{
transform: translateX(22px);
}
/* Toolbox toggle button */
.toolboxToggle{
position: absolute;
right: 20px;
top: 18px;
z-index: 20;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
cursor: pointer;
}
.toolboxText{
letter-spacing: .12em;
font-weight: 900;
}
/* Toolbox panel */
.panelCol{
position: sticky;
top: calc(var(--nav-h, 72px) + 18px);
align-self: start;
display: grid;
gap: 16px;
}
/* Cards */
.card{
border: 1px solid rgba(255,255,255,.12);
border-radius: 16px;
background: rgba(255,255,255,.05);
padding: 14px;
}
.cardTitle{
opacity: .8;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 12px;
margin-bottom: 10px;
}
.hint{
opacity: .7;
font-size: 11px;
margin-top: 10px;
line-height: 1.35;
}
/* Keep mode labels on one line */
.toggleRow{
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 10px;
align-items: center;
}
.toggleLabel{
font-size: 12px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
white-space: nowrap;
}
.subCard{
margin-top: 12px;
border: 1px solid rgba(255,255,255,.10);
border-radius: 14px;
background: rgba(0,0,0,.12);
padding: 12px;
}
.subTitle{
opacity: .8;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 11px;
margin-bottom: 10px;
}
.bitWidthRow{
display: grid;
grid-template-columns: 44px 1fr 44px;
gap: 10px;
align-items: center;
}
.bitInputWrap{
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
align-items: center;
padding: 10px 12px;
border: 1px solid rgba(255,255,255,.10);
border-radius: 12px;
background: rgba(255,255,255,.04);
}
.bitInputLabel{
opacity: .75;
letter-spacing: .14em;
text-transform: uppercase;
font-size: 11px;
white-space: nowrap;
}
.bitInput{
width: 100%;
min-width: 0;
background: transparent;
border: none;
outline: none;
color: inherit;
font-size: 20px;
text-align: right;
}
.miniBtn{
height: 44px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.9);
font-size: 18px;
cursor: pointer;
}
/* Buttons */
.controlsRow{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.btn{
border-radius: 12px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
padding: 12px 12px;
font-weight: 800;
letter-spacing: .10em;
text-transform: uppercase;
cursor: pointer;
}
.btnWide{ width: 100%; }
.btnAccent{
background: rgba(0,255,140,.12);
border-color: rgba(0,255,140,.22);
}
.toolRowCentered{
display: flex;
justify-content: center;
gap: 12px;
margin: 10px 0 12px;
}
.toolBtn{
width: 56px;
height: 56px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color: rgba(255,255,255,.92);
font-size: 18px;
cursor: pointer;
}
.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
.toolRow2{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
/* Reset stays white text */
.btnReset{
color: rgba(255,255,255,.92);
}

View File

@@ -1,85 +1,80 @@
:root{
/* Global fonts */
@font-face {
font-family: "SevenSegment";
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
url("/fonts/Seven-Segment.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "DSEG7Classic";
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
--nav-h: 92px;
--bg: #1f2027;
--panel: #22242d;
--panel2: rgba(255,255,255,.04);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
--line: rgba(255,255,255,.10);
--ui-font: "Inter", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--num-font: "DSEG7Classic", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--bit-font: "SevenSegment", monospace;
}
*{ box-sizing:border-box; }
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--ui-font); display: flex; flex-direction: column; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
/* --- BASE LAYOUT --- */
.siteNav { position: sticky; top: 0; z-index: 50; height: var(--nav-h); background: rgba(0,0,0,.10); border-bottom: 1px solid var(--line); backdrop-filter: blur(8px); }
.navInner { height: 100%; max-width: 1400px; margin: 0 auto; padding: 0 20px; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); }
.brandLogo { width: 2.5em; height: 2.5em; image-rendering: pixelated; }
.brandName { letter-spacing: .12em; font-weight: 900; font-size: 18px; }
.navLinks { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.navLinks a { color: var(--muted); text-decoration: none; font-weight: 800; letter-spacing: .12em; font-size: 16px; }
.navLinks a:hover, .navLinks a.active { color: #e8e8ee; }
.siteHeader{
position: sticky;
top: 0;
z-index: 10;
background: rgba(0,0,0,.15);
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(255,255,255,.06);
}
.pageWrap { flex: 1; max-width: 1400px; margin: 0 auto; padding: 0 20px 40px; width: 100%; display: flex; flex-direction: column; }
.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; }
.siteHeaderInner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 16px;
}
/* --- SHARED UI COMPONENTS (Used by ALL Simulators) --- */
.divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; }
.brand{
color: var(--text);
text-decoration:none;
font-weight: 900;
letter-spacing:.02em;
}
.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; }
.bulb.on svg { fill: #ffd86b !important; }
.nav{
display:flex;
gap: 14px;
flex-wrap:wrap;
justify-content:flex-end;
}
.nav a{
color: var(--muted);
text-decoration:none;
font-weight: 700;
font-size: 14px;
}
.nav a:hover{ color: var(--text); }
.switch { position: relative; width: 56px; height: 28px; display: inline-block; }
.switch input { display: none; }
.slider { position: absolute; inset: 0; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14); border-radius: 999px; transition: .2s ease; }
.slider::before { content: ""; position: absolute; width: 22px; height: 22px; left: 3px; top: 2px; background: rgba(255,255,255,.92); border-radius: 999px; transition: .2s ease; pointer-events: none; }
.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); }
.siteMain{
min-height: calc(100vh - 140px);
}
.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; }
.siteFooter{
border-top: 1px solid rgba(255,255,255,.08);
margin-top: 32px;
background: rgba(0,0,0,.10);
}
.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%; }
.btnReset { color: rgba(232,232,238,.95); }
.btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); }
.siteFooterInner{
max-width: 1200px;
margin: 0 auto;
padding: 18px 20px 26px;
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.footerTitle{
color: var(--text);
opacity:.9;
font-weight: 800;
margin-bottom: 6px;
}
.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); }

165
src/styles/logic-gates.css Normal file
View File

@@ -0,0 +1,165 @@
/* === FULL PAGE OVERRIDES FOR LOGIC GATES === */
body:has(#logicPage) { overflow: hidden; }
body:has(#logicPage) .pageWrap {
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
height: calc(100vh - var(--nav-h));
display: flex;
flex-direction: column;
}
#logicPage { padding: 0 !important; margin: 0 !important; }
/* === MAIN CONTAINER === */
.lg-container {
flex: 1; display: flex; flex-direction: column; position: relative;
width: 100%; height: 100%; overflow: hidden;
}
/* === FIXED HEADER === */
.lg-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);
}
.lg-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;
}
.lg-subtitle {
color: var(--muted); font-size: 14px; font-family: var(--ui-font);
font-weight: 500; margin: 0; line-height: 1.2;
}
.lg-subtitle kbd {
background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;
font-family: var(--ui-font); color: #e8e8ee;
}
/* === DYNAMIC CANVAS & CAMERA VIEWPORT === */
.lg-workspace {
flex: 1; position: relative; width: 100%;
background-color: transparent;
background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px);
background-size: 24px 24px; overflow: hidden;
cursor: grab; /* Indicates pannable area */
}
.lg-workspace:active { cursor: grabbing; }
.lg-viewport {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform-origin: 0 0;
pointer-events: none; /* Let events reach workspace, nodes will re-enable it */
}
/* Zoom UI Controls */
.lg-zoom-controls {
position: absolute; bottom: 20px; left: 20px; z-index: 100;
display: flex; gap: 8px;
}
.lg-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;
}
.lg-zoom-btn:hover { background: rgba(255,255,255,0.1); border-color: #28f07a; color: #28f07a; }
.lg-svg-layer {
position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
}
/* Wires */
.lg-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;
}
.lg-wire:hover { stroke: rgba(255,255,255,0.6); stroke-width: 10; }
.lg-wire.active { stroke: #28f07a; filter: drop-shadow(0 0 6px rgba(40,240,122,0.6)); }
.lg-wire.active:hover { stroke: #5dff9e; }
.lg-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; } }
.lg-wire-temp { stroke: rgba(255,255,255,0.4); stroke-dasharray: 8 8; pointer-events: none; }
/* Nodes */
.lg-node {
position: absolute; background: transparent; border: none; border-radius: 0; padding: 4px;
display: flex; flex-direction: column; align-items: center; cursor: grab;
z-index: 10; user-select: none; transition: filter 0.2s;
pointer-events: auto; /* Re-enables interaction inside the viewport */
}
.lg-node:active { cursor: grabbing; z-index: 20; }
.lg-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); }
.lg-header {
font-size: 24px; color: var(--muted); font-family: var(--bit-font);
letter-spacing: 2px; pointer-events: none; margin-bottom: 6px;
}
.lg-gate-container { position: relative; display: inline-flex; align-items: center; }
.lg-gate-svg { width: 100px; height: 50px; display: block; }
.lg-line-svg { width: 30px; height: 50px; display: block; }
/* Connection Ports */
.lg-port {
width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair;
border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s;
position: absolute; z-index: 5; transform: translate(-50%, -50%);
}
.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; }
.lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; }
/* === FLOATING TOOLBOX === */
.toolboxToggle {
position: absolute; top: 10px; right: 20px; z-index: 90;
display: flex; align-items: center; gap: 10px; padding: 8px 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); }
.lg-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;
}
.lg-toolbox::-webkit-scrollbar { width: 6px; }
.lg-toolbox::-webkit-scrollbar-track { background: transparent; }
.lg-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; }
.lg-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); }
.lg-container.toolboxCollapsed .lg-toolbox { transform: translateX(calc(100% + 40px)); opacity: 0; pointer-events: none; }
.tb-icon-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tb-icon-box {
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; width: 100%; padding: 12px 0;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
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: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; }
.tt-summary {
font-family: var(--ui-font); font-size: 14px; font-weight: 800; color: var(--accent, #28f07a);
cursor: pointer; user-select: none; outline: none; margin-bottom: 10px; text-transform: uppercase;
}
.tt-table-wrap {
width: 100%; max-height: 250px; overflow-y: auto; overflow-x: auto;
border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2);
}
.tt-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
.tt-table-wrap::-webkit-scrollbar-track { background: transparent; }
.tt-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
.tt-table { width: 100%; border-collapse: collapse; text-align: center; font-family: var(--num-font); font-size: 14px; color: #e8e8ee; }
.tt-table th { position: sticky; top: 0; background: rgba(31,32,39,0.95); padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15); color: var(--muted); font-family: var(--bit-font); font-weight: normal; }
.tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.tt-table .tt-on { color: #28f07a; text-shadow: 0 0 8px rgba(40,240,122,0.5); }

View File

@@ -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)); } }

120
src/styles/pc-builder.css Normal file
View File

@@ -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)); }

View File

@@ -1,75 +0,0 @@
:root{
--bg: #1f2027;
--panel: rgba(255,255,255,.04);
--panel-border: rgba(255,255,255,.10);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
*{ box-sizing: border-box; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.site-header{
border-bottom: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.12);
}
.site-header__inner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 18px;
}
.brand{
font-weight: 800;
letter-spacing: .02em;
}
.nav{
display:flex;
gap: 14px;
flex-wrap: wrap;
justify-content:flex-end;
}
.nav__link{
color: var(--muted);
text-decoration:none;
font-weight: 700;
font-size: 13px;
}
.nav__link:hover{ color: var(--text); }
.site-main{
max-width: 1200px;
margin: 0 auto;
padding: 28px 20px 40px;
min-height: calc(100vh - 140px);
}
.site-footer{
border-top: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.10);
}
.site-footer__inner{
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}