diff --git a/.gitea/workflows/pre-release.yaml b/.gitea/workflows/pre-release.yaml new file mode 100644 index 0000000..f965a20 --- /dev/null +++ b/.gitea/workflows/pre-release.yaml @@ -0,0 +1,194 @@ +name: Pre-release on non-main branches + +on: + push: + branches-ignore: [ main ] + workflow_dispatch: + +jobs: + prerelease: + runs-on: ubuntu-latest + + steps: + - name: Checkout (full history + tags) + uses: actions/checkout@v6 + 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 (in runner only) + shell: bash + run: | + set -e + git-cliff --config cliff.toml --output CHANGELOG.md + test -s CHANGELOG.md + + - name: Extract newest changelog section for pre-release body + shell: bash + run: | + set -e + awk ' + /^## / { if (seen) exit; seen=1 } + seen { print } + ' CHANGELOG.md > RELEASE_NOTES.md + + sed -i 's/[[:space:]]*$//' RELEASE_NOTES.md + test -s RELEASE_NOTES.md + + - name: Create export zip (Computing:Box Website.zip) + shell: bash + run: | + set -e + if [ ! -d "dist" ]; then + echo "❌ dist/ folder not found in repo root" + ls -la + exit 1 + fi + + rm -f "Computing:Box Website.zip" + (cd dist && zip -r "../Computing:Box Website.zip" .) + test -s "Computing:Box Website.zip" + ls -lh "Computing:Box Website.zip" + + - name: Prepare pre-release tag + name + shell: bash + run: | + set -e + + # Get branch name from ref: refs/heads/feature/x -> feature/x + ref="${GITHUB_REF#refs/heads/}" + # Make it tag-safe: lowercase, / -> -, remove invalid chars, collapse repeats + safe_branch="$(echo "$ref" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9._-]+#-#g; s#-+#-#g; s#(^-|-$)##g')" + + VERSION="$(date -u +'%y.%m.%d')" + SHORT_SHA="$(git rev-parse --short HEAD)" + + # Pre-release tag format: + # vYY.MM.DD-pre.. + TAG="v${VERSION}-pre.${safe_branch}.${SHORT_SHA}" + + # Release name shown in UI + RELEASE_NAME="Computing:Box pre-release (${ref}) v${VERSION}" + + echo "TAG=$TAG" >> "$GITHUB_ENV" + echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV" + echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV" + echo "BRANCH_NAME=$ref" >> "$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 pre-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"] + branch = os.environ.get("BRANCH_NAME", "") + + with open("RELEASE_NOTES.md", "r", encoding="utf-8") as f: + body = f.read() + + # Add a small pre-release banner at the top + banner = f"⚠️ Pre-release build from branch `{branch}`\n\n" + payload = { + "tag_name": tag, + "target_commitish": branch if branch else "main", + "name": name, + "body": banner + body, + "draft": False, + "prerelease": True, + } + + 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 pre-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 "✅ Pre-release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yaml similarity index 100% rename from .gitea/workflows/release.yml rename to .gitea/workflows/release.yaml diff --git a/README.md b/README.md index fbc07a0..b8242a9 100644 --- a/README.md +++ b/README.md @@ -28,18 +28,19 @@ 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] 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) -- [ ] Extended Hexadecimal Simulator -- [ ] Unified Hexadecimal Simulator (For GCSE & A Level Specification) -- [ ] Enhanced Gate Simulator (Truth Table Creator) -- [ ] Compound Gate Simulator -- [ ] Computer Components Simulator +- [X] Extended Hexadecimal Simulator +- [X] Unified Hexadecimal Simulator (For GCSE & A Level Specification) +- [X] Enhanced Gate Simulator (Truth Table Creator) +- [X] Compound Gate Simulator +- [X] Computer Components Simulator - Beta Available ## Version 1.0 Release Date: 1st September 2025 -## Version 2.0 Release Date (Goal): 1st May 2026 +## Version 2.0 Release Date (Beta): 1st March 2026 +## Version 2.0 Release Date (Full Release): 1st May 2026 Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] diff --git a/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js b/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js new file mode 100644 index 0000000..a23a7d5 --- /dev/null +++ b/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js @@ -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(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=0;e--){const o=document.createElement("div");o.className="bit",o.innerHTML=` + +
+ + `,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{const n=Number(t.dataset.index);t.checked=!!s[n]})}function Q(){for(let t=0;tc)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;nn&&(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;e0n&&(c=c>>U),c{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)})(); diff --git a/dist/binary/index.html b/dist/binary/index.html new file mode 100644 index 0000000..8b1520c --- /dev/null +++ b/dist/binary/index.html @@ -0,0 +1,19 @@ + Binary Simulator | Computing:Box +
Denary
0
Binary
00000000
\ No newline at end of file diff --git a/dist/favicon.svg b/dist/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/dist/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dist/fonts/DSEG7Classic-Regular.ttf b/dist/fonts/DSEG7Classic-Regular.ttf new file mode 100644 index 0000000..b940496 Binary files /dev/null and b/dist/fonts/DSEG7Classic-Regular.ttf differ diff --git a/dist/fonts/DSEG7Classic-Regular.woff b/dist/fonts/DSEG7Classic-Regular.woff new file mode 100644 index 0000000..af83455 Binary files /dev/null and b/dist/fonts/DSEG7Classic-Regular.woff differ diff --git a/dist/fonts/Seven-Segment.woff b/dist/fonts/Seven-Segment.woff new file mode 100644 index 0000000..5a388a9 Binary files /dev/null and b/dist/fonts/Seven-Segment.woff differ diff --git a/dist/fonts/Seven-Segment.woff2 b/dist/fonts/Seven-Segment.woff2 new file mode 100644 index 0000000..7c7e36f Binary files /dev/null and b/dist/fonts/Seven-Segment.woff2 differ diff --git a/dist/hexadecimal/index.html b/dist/hexadecimal/index.html new file mode 100644 index 0000000..dfb3c34 --- /dev/null +++ b/dist/hexadecimal/index.html @@ -0,0 +1,19 @@ + Hexadecimal Simulator | Computing:Box +
Denary
0
Hexadecimal
00
Binary
00000000
\ No newline at end of file diff --git a/dist/images/computing-box-logo.svg b/dist/images/computing-box-logo.svg new file mode 100644 index 0000000..7bf11ef --- /dev/null +++ b/dist/images/computing-box-logo.svg @@ -0,0 +1,1017 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/images/favicon.svg b/dist/images/favicon.svg new file mode 100644 index 0000000..7bf11ef --- /dev/null +++ b/dist/images/favicon.svg @@ -0,0 +1,1017 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..9674850 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,18 @@ + Welcome | Computing:Box

Version 2.0 Now Live

Understand Computing concepts better.

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

Computing Box Logo
\ No newline at end of file diff --git a/dist/js/binary/unsigned-binary.js b/dist/js/binary/unsigned-binary.js new file mode 100644 index 0000000..e28ab9f --- /dev/null +++ b/dist/js/binary/unsigned-binary.js @@ -0,0 +1,115 @@ +// Browser-only script. Safe because it's loaded via - - diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index edce928..d0e14ec 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,6 +1,5 @@ --- -import '../styles/global.css'; - +import "../styles/global.css"; const { title = "Computing:Box" } = Astro.props; --- @@ -11,6 +10,22 @@ const { title = "Computing:Box" } = Astro.props; {title} + @@ -20,15 +35,16 @@ const { title = "Computing:Box" } = Astro.props; @@ -38,9 +54,13 @@ const { title = "Computing:Box" } = Astro.props;
-
-
COMPUTER SCIENCE CONCEPT SIMULATORS
-
© {new Date().getFullYear()} COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
+
+ +
Computer Science Concept Simulators
+
© {new Date().getFullYear()} Computing:Box • Created with ♥ by Mr A Lyall
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro deleted file mode 100644 index e455c61..0000000 --- a/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..0496060 --- /dev/null +++ b/src/pages/about.astro @@ -0,0 +1,55 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+ +
+ Computing:Box Logo +

The New Computing:Box Experience

+
+ +
+

+ Rebuilt from the ground up to provide a deeper, more professional simulation environment for the UK Computing Curriculum. +

+
+
    +
  • New User Interface (Responsive)
  • +
  • Two's Complement Simulator
  • +
  • Extended Binary Simulator (Custom sizes)
  • +
  • Unified Binary Simulator
  • +
  • Extended Hexadecimal Simulator
  • +
  • Unified Hexadecimal Simulator
  • +
  • Enhanced Gate Simulator (Truth Tables)
  • +
  • Compound Gate Simulator
  • +
  • Computer Components Simulator
  • +
+
+ +
+
+

From Bit:Box to Computing:Box

+

Computing:Box began as Bit:Box, a digital version of a physical Binary Light Box created by Mr Davis. The original device used lightbulbs to show how binary numbers are built.

+
+ Bit:Box Logo +
+ +
+ Simulator Illustration +
+

Learning by Doing

+

Students can interact with simulations, change values, and see results instantly. This helps them understand how concepts work rather than memorising rules.

+
+
+ +
+
+

Educational Impact

+

Designed for use in lessons, independent study, and revision, helping students build confidence as they explore how computers work.

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

Copyright Notice

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

What Does This Mean For You?

+

You are free to:

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

Under the following terms:

+
    +
  • Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
  • +
  • NonCommercial — You may not use the material for commercial purposes.
  • +
  • ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
  • + +
  • No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
  • +
+
+
\ No newline at end of file diff --git a/src/pages/hex-colours.astro b/src/pages/hex-colours.astro new file mode 100644 index 0000000..21a5624 --- /dev/null +++ b/src/pages/hex-colours.astro @@ -0,0 +1,100 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; +--- + + +
+ + +
+
+ +
+
+
+
Denary (R, G, B)
+
+ 0 + 0 + 0 +
+
+ +
+
Hexadecimal
+
+ #00 + 00 + 00 +
+
+ +
+
Binary
+
+ 00000000 + 00000000 + 00000000 +
+
+
+ +
+
+
+
Colour
+
+
+
+
Inverted
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/hexadecimal.astro b/src/pages/hexadecimal.astro new file mode 100644 index 0000000..a981dc5 --- /dev/null +++ b/src/pages/hexadecimal.astro @@ -0,0 +1,95 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; +--- + + +
+ + +
+
+
+
Denary
+
0
+ +
Hexadecimal
+
00
+ +
Binary
+
00000000
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index c04f360..7490d18 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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"; --- - - - + +
+
+

Version 2.0 Now Live

+

Understand Computing concepts better.

+

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

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

Legal Code

+ +
+

About the license and Creative Commons

+

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

+
+ +

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

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