Compare commits
70 Commits
v25.12.26-
...
v26.04.15.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77b21dd42c | ||
| da3c17456c | |||
|
|
fd16b4fc53 | ||
|
|
3c8f0894b0 | ||
| 7d1c318840 | |||
|
|
846f207a4d | ||
|
|
b95c2c4bc3 | ||
| 0373262a4a | |||
|
|
93c8ffec22 | ||
|
|
501349b326 | ||
| 67a5eafe64 | |||
| 5a4c0a0988 | |||
|
|
9d49e3afe3 | ||
|
|
cb2c1c01ae | ||
| 09c6fb18b2 | |||
|
|
f1fdcc6f11 | ||
|
|
7390f0bdf8 | ||
|
7d9d436b25
|
|||
| 32a64b8411 | |||
|
|
bde2752777 | ||
|
|
8b75beb607 | ||
|
21f97b7f78
|
|||
|
cc5473f648
|
|||
| b4fc72c46a | |||
| 338e8665ec | |||
|
8ce81afdaf
|
|||
|
f70120c2a0
|
|||
|
|
7fa57cb782 | ||
| 6dc6eea401 | |||
|
|
4302f6bbba | ||
|
|
2deba8ba2f | ||
| 5aa972ab7f | |||
|
|
1b9cf4b388 | ||
|
|
50d97b4e55 | ||
|
59c0b50396
|
|||
|
f83331ed35
|
|||
|
|
7a5d423dcb | ||
|
c4296137b3
|
|||
|
d980671266
|
|||
|
|
52d129f50a | ||
|
c8b43a3f8f
|
|||
| 63e2c267fb | |||
|
|
14c2dfdb20 | ||
|
68f1ed5d81
|
|||
|
|
875ab670d5 | ||
| 43cef42c3b | |||
|
|
29dd867bcb | ||
|
dba93b67fd
|
|||
|
5d23d0639e
|
|||
|
535c62b838
|
|||
| bcac9f3310 | |||
|
|
3a624cb5cd | ||
| 12f605e987 | |||
|
|
cc3d6f0e48 | ||
|
61b24dc309
|
|||
|
1ec16aecb3
|
|||
|
bb8e0c1969
|
|||
|
09e0499ba3
|
|||
|
7ecd065305
|
|||
|
e0e72c17e8
|
|||
|
ffab71cfcc
|
|||
|
d4ffe30f9b
|
|||
|
93542748e6
|
|||
|
98671cdeee
|
|||
| e74a20ca81 | |||
|
|
4b21391232 | ||
| 5708a184d5 | |||
|
af131fc58a
|
|||
|
|
4a0e4d306a | ||
|
aa9e071d40
|
@@ -1,194 +0,0 @@
|
|||||||
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@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Stop if this is the bot changelog commit
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
msg="$(git log -1 --pretty=%B)"
|
|
||||||
echo "$msg" | tr -d '\r' | grep -qi "\[skip ci\]" && {
|
|
||||||
echo "Skipping (bot commit with [skip ci])"
|
|
||||||
exit 0
|
|
||||||
} || true
|
|
||||||
|
|
||||||
- name: Install git-cliff
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
GIT_CLIFF_VERSION="2.11.0"
|
|
||||||
URL="https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
|
||||||
curl -L "$URL" -o /tmp/git-cliff.tar.gz
|
|
||||||
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
|
|
||||||
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
|
|
||||||
git-cliff --version
|
|
||||||
|
|
||||||
- name: Generate CHANGELOG.md (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.<branch>.<sha>
|
|
||||||
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"
|
|
||||||
@@ -11,11 +11,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout (full history + tags)
|
- name: Checkout (full history + tags)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Stop if this is the bot changelog commit
|
- name: Stop if this is the bot changelog/version commit
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
@@ -36,82 +36,11 @@ jobs:
|
|||||||
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
|
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
|
||||||
git-cliff --version
|
git-cliff --version
|
||||||
|
|
||||||
- name: Generate CHANGELOG.md (Keep a Changelog)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
git-cliff --config cliff.toml --output CHANGELOG.md
|
|
||||||
test -s CHANGELOG.md
|
|
||||||
|
|
||||||
- name: Commit and push CHANGELOG.md if changed (CHANGELOG_PAT)
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if git diff --quiet -- CHANGELOG.md; then
|
|
||||||
echo "No changelog changes."
|
|
||||||
else
|
|
||||||
git config user.name "changelog-bot"
|
|
||||||
git config user.email "changelog-bot@users.noreply.local"
|
|
||||||
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "docs(changelog): update changelog [skip ci]"
|
|
||||||
|
|
||||||
origin_url="$(git remote get-url origin)"
|
|
||||||
|
|
||||||
# Convert SSH origin to HTTPS if needed
|
|
||||||
if echo "$origin_url" | grep -q "^git@"; then
|
|
||||||
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
|
||||||
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
|
||||||
origin_url="https://$host/$path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
|
|
||||||
git push "$authed_url" HEAD:main
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Extract newest changelog section for release body
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
# Extract the first "## ..." section (newest section) from CHANGELOG.md
|
|
||||||
# Includes the "## ..." heading and everything until the next "## ..." heading.
|
|
||||||
awk '
|
|
||||||
/^## / { if (seen) exit; seen=1 }
|
|
||||||
seen { print }
|
|
||||||
' CHANGELOG.md > RELEASE_NOTES.md
|
|
||||||
|
|
||||||
# Clean trailing whitespace/newlines a bit
|
|
||||||
sed -i 's/[[:space:]]*$//' RELEASE_NOTES.md
|
|
||||||
|
|
||||||
test -s RELEASE_NOTES.md
|
|
||||||
echo "---- RELEASE_NOTES.md ----"
|
|
||||||
head -n 60 RELEASE_NOTES.md
|
|
||||||
echo "--------------------------"
|
|
||||||
|
|
||||||
- name: Create export zip (Computing:Box Website.zip)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d "export" ]; then
|
|
||||||
echo "❌ export/ folder not found in repo root"
|
|
||||||
ls -la
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "Computing:Box Website.zip"
|
|
||||||
(cd export && zip -r "../Computing:Box Website.zip" .)
|
|
||||||
test -s "Computing:Box Website.zip"
|
|
||||||
ls -lh "Computing:Box Website.zip"
|
|
||||||
|
|
||||||
- name: Prepare YY.MM.DD letter-suffix tag + release name
|
- name: Prepare YY.MM.DD letter-suffix tag + release name
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Version: YY.MM.DD (UTC). Swap to `date +...` if you prefer UK-local runner time.
|
|
||||||
VERSION="$(date -u +'%y.%m.%d')"
|
VERSION="$(date -u +'%y.%m.%d')"
|
||||||
PREFIX="v${VERSION}."
|
PREFIX="v${VERSION}."
|
||||||
|
|
||||||
@@ -135,12 +64,217 @@ jobs:
|
|||||||
TAG="${PREFIX}${next_letter}"
|
TAG="${PREFIX}${next_letter}"
|
||||||
RELEASE_NAME="Computing:Box v${VERSION}.${next_letter}"
|
RELEASE_NAME="Computing:Box v${VERSION}.${next_letter}"
|
||||||
|
|
||||||
|
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$//')"
|
||||||
|
|
||||||
|
RELEASE_URL="${base}/${repo_path}/releases/tag/${TAG}"
|
||||||
|
|
||||||
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
||||||
echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV"
|
echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV"
|
||||||
echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV"
|
echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV"
|
||||||
|
echo "RELEASE_URL=$RELEASE_URL" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
echo "Using tag: $TAG"
|
echo "Using tag: $TAG"
|
||||||
echo "Release name: $RELEASE_NAME"
|
echo "Release name: $RELEASE_NAME"
|
||||||
|
echo "Release URL: $RELEASE_URL"
|
||||||
|
|
||||||
|
- name: Find previous release tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PREV_TAG="$(
|
||||||
|
git for-each-ref \
|
||||||
|
--sort=-creatordate \
|
||||||
|
--format='%(refname:short)' \
|
||||||
|
refs/tags \
|
||||||
|
| grep -Fxv "$TAG" \
|
||||||
|
| head -n 1 \
|
||||||
|
|| true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "PREV_TAG=$PREV_TAG" >> "$GITHUB_ENV"
|
||||||
|
echo "Previous release tag: $PREV_TAG"
|
||||||
|
else
|
||||||
|
echo "PREV_TAG=" >> "$GITHUB_ENV"
|
||||||
|
echo "No previous release tag found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate CHANGELOG.md from previous release to HEAD
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "${PREV_TAG}" ]; then
|
||||||
|
echo "Generating changelog from ${PREV_TAG}..HEAD"
|
||||||
|
git-cliff --config cliff.toml "${PREV_TAG}..HEAD" --output CHANGELOG.md
|
||||||
|
else
|
||||||
|
echo "Generating changelog from full history"
|
||||||
|
git-cliff --config cliff.toml --output CHANGELOG.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
test -s CHANGELOG.md
|
||||||
|
echo "---- CHANGELOG.md ----"
|
||||||
|
head -n 120 CHANGELOG.md
|
||||||
|
echo "----------------------"
|
||||||
|
|
||||||
|
- name: Commit and push CHANGELOG.md if changed (CHANGELOG_PAT)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if git diff --quiet -- CHANGELOG.md; then
|
||||||
|
echo "No changelog changes."
|
||||||
|
else
|
||||||
|
git config user.name "changelog-bot"
|
||||||
|
git config user.email "changelog-bot@users.noreply.local"
|
||||||
|
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "docs(changelog): update changelog [skip ci]"
|
||||||
|
|
||||||
|
origin_url="$(git remote get-url origin)"
|
||||||
|
|
||||||
|
if echo "$origin_url" | grep -q "^git@"; then
|
||||||
|
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
||||||
|
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
||||||
|
origin_url="https://$host/$path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
|
||||||
|
git push "$authed_url" HEAD:main
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare release notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cp CHANGELOG.md RELEASE_NOTES.md
|
||||||
|
test -s RELEASE_NOTES.md
|
||||||
|
echo "---- RELEASE_NOTES.md ----"
|
||||||
|
head -n 120 RELEASE_NOTES.md
|
||||||
|
echo "--------------------------"
|
||||||
|
|
||||||
|
- name: Derive semver package version from tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PACKAGE_VERSION="$(echo "$TAG" | sed -E 's/^v([0-9]{2})\.0?([0-9]{1,2})\.0?([0-9]{1,2})([a-z])$/\1.\2.\3-\4/')"
|
||||||
|
|
||||||
|
if [ -z "$PACKAGE_VERSION" ]; then
|
||||||
|
echo "❌ Failed to derive PACKAGE_VERSION from TAG=$TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "Using package version: $PACKAGE_VERSION"
|
||||||
|
|
||||||
|
- name: Generate version file for Astro footer
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
mkdir -p src/generated
|
||||||
|
cat > src/generated/version.json <<EOF
|
||||||
|
{
|
||||||
|
"version": "${TAG}",
|
||||||
|
"url": "${RELEASE_URL}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Generated src/generated/version.json"
|
||||||
|
cat src/generated/version.json
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Check Node version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Update package.json and package-lock.json version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
npm version "$PACKAGE_VERSION" --no-git-tag-version
|
||||||
|
|
||||||
|
echo "package.json version:"
|
||||||
|
node -p "require('./package.json').version"
|
||||||
|
|
||||||
|
echo "package-lock.json version:"
|
||||||
|
node -p "require('./package-lock.json').version"
|
||||||
|
|
||||||
|
- name: Commit and push version bump (CHANGELOG_PAT)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if git diff --quiet -- package.json package-lock.json; then
|
||||||
|
echo "No version changes to commit."
|
||||||
|
else
|
||||||
|
git config user.name "release-bot"
|
||||||
|
git config user.email "release-bot@users.noreply.local"
|
||||||
|
|
||||||
|
git add package.json package-lock.json
|
||||||
|
git commit -m "chore(release): bump version to ${PACKAGE_VERSION} [skip ci]"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
|
||||||
|
git push "$authed_url" HEAD:main
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Astro site
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
npm run build
|
||||||
|
test -d dist
|
||||||
|
|
||||||
|
- 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: Create and push tag (CHANGELOG_PAT)
|
- name: Create and push tag (CHANGELOG_PAT)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -153,7 +287,6 @@ jobs:
|
|||||||
|
|
||||||
origin_url="$(git remote get-url origin)"
|
origin_url="$(git remote get-url origin)"
|
||||||
|
|
||||||
# Convert SSH origin to HTTPS if needed
|
|
||||||
if echo "$origin_url" | grep -q "^git@"; then
|
if echo "$origin_url" | grep -q "^git@"; then
|
||||||
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
||||||
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
||||||
@@ -198,7 +331,7 @@ jobs:
|
|||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
"target_commitish": "main",
|
"target_commitish": "main",
|
||||||
"name": name,
|
"name": name,
|
||||||
"body": body, # newest section only
|
"body": body,
|
||||||
"draft": False,
|
"draft": False,
|
||||||
"prerelease": False,
|
"prerelease": False,
|
||||||
}
|
}
|
||||||
@@ -232,4 +365,4 @@ jobs:
|
|||||||
-F "attachment=@${ZIP_PATH}" \
|
-F "attachment=@${ZIP_PATH}" \
|
||||||
>/dev/null
|
>/dev/null
|
||||||
|
|
||||||
echo "✅ Release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded"
|
echo "✅ Release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded"
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
name: Changelog + Release on main
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changelog_and_release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout (full history + tags)
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Stop if this is the bot changelog commit
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
msg="$(git log -1 --pretty=%B)"
|
|
||||||
echo "$msg" | tr -d '\r' | grep -qi "\[skip ci\]" && {
|
|
||||||
echo "Skipping (bot commit with [skip ci])"
|
|
||||||
exit 0
|
|
||||||
} || true
|
|
||||||
|
|
||||||
- name: Install git-cliff
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
GIT_CLIFF_VERSION="2.11.0"
|
|
||||||
URL="https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
|
||||||
curl -L "$URL" -o /tmp/git-cliff.tar.gz
|
|
||||||
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
|
|
||||||
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
|
|
||||||
git-cliff --version
|
|
||||||
|
|
||||||
- name: Generate CHANGELOG.md (Keep a Changelog)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
git-cliff --config cliff.toml --output CHANGELOG.md
|
|
||||||
test -s CHANGELOG.md
|
|
||||||
|
|
||||||
- name: Commit and push CHANGELOG.md if changed (CHANGELOG_PAT)
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if git diff --quiet -- CHANGELOG.md; then
|
|
||||||
echo "No changelog changes."
|
|
||||||
else
|
|
||||||
git config user.name "changelog-bot"
|
|
||||||
git config user.email "changelog-bot@users.noreply.local"
|
|
||||||
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "docs(changelog): update changelog [skip ci]"
|
|
||||||
|
|
||||||
origin_url="$(git remote get-url origin)"
|
|
||||||
|
|
||||||
# Convert SSH origin to HTTPS if needed
|
|
||||||
if echo "$origin_url" | grep -q "^git@"; then
|
|
||||||
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
|
||||||
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
|
||||||
origin_url="https://$host/$path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
|
|
||||||
git push "$authed_url" HEAD:main
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Extract newest changelog section for release body
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
# Extract the first "## ..." section (newest section) from CHANGELOG.md
|
|
||||||
# Includes the "## ..." heading and everything until the next "## ..." heading.
|
|
||||||
awk '
|
|
||||||
/^## / { if (seen) exit; seen=1 }
|
|
||||||
seen { print }
|
|
||||||
' CHANGELOG.md > RELEASE_NOTES.md
|
|
||||||
|
|
||||||
# Clean trailing whitespace/newlines a bit
|
|
||||||
sed -i 's/[[:space:]]*$//' RELEASE_NOTES.md
|
|
||||||
|
|
||||||
test -s RELEASE_NOTES.md
|
|
||||||
echo "---- RELEASE_NOTES.md ----"
|
|
||||||
head -n 60 RELEASE_NOTES.md
|
|
||||||
echo "--------------------------"
|
|
||||||
|
|
||||||
- name: Create export zip (Computing:Box Website.zip)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d "export" ]; then
|
|
||||||
echo "❌ export/ folder not found in repo root"
|
|
||||||
ls -la
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "Computing:Box Website.zip"
|
|
||||||
(cd export && zip -r "../Computing:Box Website.zip" .)
|
|
||||||
test -s "Computing:Box Website.zip"
|
|
||||||
ls -lh "Computing:Box Website.zip"
|
|
||||||
|
|
||||||
- name: Prepare YY.MM.DD letter-suffix tag + release name
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Version: YY.MM.DD (UTC). Swap to `date +...` if you prefer UK-local runner time.
|
|
||||||
VERSION="$(date -u +'%y.%m.%d')"
|
|
||||||
PREFIX="v${VERSION}."
|
|
||||||
|
|
||||||
last_letter="$(
|
|
||||||
git tag --list "${PREFIX}[a-z]" \
|
|
||||||
| sed -E "s/^${PREFIX}([a-z])$/\1/" \
|
|
||||||
| sort \
|
|
||||||
| tail -n 1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [ -z "$last_letter" ]; then
|
|
||||||
next_letter="a"
|
|
||||||
else
|
|
||||||
if [ "$last_letter" = "z" ]; then
|
|
||||||
echo "❌ Already have v${VERSION}.z today. Refusing to create more than 26 releases/day."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
next_letter="$(printf "%b" "$(printf '\\%03o' "$(( $(printf '%d' "'$last_letter") + 1 ))")")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TAG="${PREFIX}${next_letter}"
|
|
||||||
RELEASE_NAME="Computing:Box v${VERSION}.${next_letter}"
|
|
||||||
|
|
||||||
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
|
||||||
echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV"
|
|
||||||
echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
echo "Using tag: $TAG"
|
|
||||||
echo "Release name: $RELEASE_NAME"
|
|
||||||
|
|
||||||
- name: Create and push tag (CHANGELOG_PAT)
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
git tag -f "$TAG"
|
|
||||||
|
|
||||||
origin_url="$(git remote get-url origin)"
|
|
||||||
|
|
||||||
# Convert SSH origin to HTTPS if needed
|
|
||||||
if echo "$origin_url" | grep -q "^git@"; then
|
|
||||||
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
|
||||||
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
|
||||||
origin_url="https://$host/$path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
|
|
||||||
git push "$authed_url" "refs/tags/$TAG" --force
|
|
||||||
|
|
||||||
- name: Create Gitea release + upload asset (CHANGELOG_PAT)
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
origin_url="$(git remote get-url origin)"
|
|
||||||
if echo "$origin_url" | grep -q "^git@"; then
|
|
||||||
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
|
|
||||||
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
|
|
||||||
origin_url="https://$host/$path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
base="$(echo "$origin_url" | sed -E 's#(https?://[^/]+)/.*#\1#')"
|
|
||||||
repo_path="$(echo "$origin_url" | sed -E 's#https?://[^/]+/##')"
|
|
||||||
repo_path="$(echo "$repo_path" | sed -E 's/\.git$//')"
|
|
||||||
|
|
||||||
owner="$(echo "$repo_path" | cut -d/ -f1)"
|
|
||||||
repo="$(echo "$repo_path" | cut -d/ -f2-)"
|
|
||||||
|
|
||||||
api="$base/api/v1"
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json, os
|
|
||||||
tag = os.environ["TAG"]
|
|
||||||
name = os.environ["RELEASE_NAME"]
|
|
||||||
|
|
||||||
with open("RELEASE_NOTES.md", "r", encoding="utf-8") as f:
|
|
||||||
body = f.read()
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"tag_name": tag,
|
|
||||||
"target_commitish": "main",
|
|
||||||
"name": name,
|
|
||||||
"body": body, # newest section only
|
|
||||||
"draft": False,
|
|
||||||
"prerelease": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
with open("release.json", "w", encoding="utf-8") as f:
|
|
||||||
json.dump(payload, f)
|
|
||||||
PY
|
|
||||||
|
|
||||||
curl -sS -X POST \
|
|
||||||
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${api}/repos/${owner}/${repo}/releases" \
|
|
||||||
--data-binary @release.json \
|
|
||||||
-o release_response.json
|
|
||||||
|
|
||||||
release_id="$(python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
with open("release_response.json","r",encoding="utf-8") as f:
|
|
||||||
data=json.load(f)
|
|
||||||
rid=data.get("id")
|
|
||||||
if not rid:
|
|
||||||
raise SystemExit("No release id returned. Response:\n" + json.dumps(data, indent=2))
|
|
||||||
print(rid)
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
echo "Created release id: $release_id"
|
|
||||||
|
|
||||||
curl -sS -X POST \
|
|
||||||
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
|
|
||||||
"${api}/repos/${owner}/${repo}/releases/${release_id}/assets?name=Computing%3ABox%20Website.zip" \
|
|
||||||
-F "attachment=@${ZIP_PATH}" \
|
|
||||||
>/dev/null
|
|
||||||
|
|
||||||
echo "✅ Release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded"
|
|
||||||
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# build output
|
# build output
|
||||||
# dist/
|
dist/
|
||||||
|
|
||||||
# generated types
|
# generated types
|
||||||
.astro/
|
.astro/
|
||||||
|
|||||||
16
README.md
@@ -28,15 +28,19 @@ An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Com
|
|||||||
- [X] XNOR Gate Simulator
|
- [X] XNOR Gate Simulator
|
||||||
|
|
||||||
### Wave 3 CS:Box Features (Spring 2026)
|
### Wave 3 CS:Box Features (Spring 2026)
|
||||||
|
- [X] New User Interface (Responsive)
|
||||||
- [X] Two's Compliment Simulator
|
- [X] Two's Compliment Simulator
|
||||||
- [ ] Extended Binary Simulator (Custom bit sizes)
|
- [X] Extended Binary Simulator (Custom bit sizes)
|
||||||
- [ ] Unified Binary Simulator (Unsigned & Two's Completment combined)
|
- [X] Unified Binary Simulator (Unsigned & Two's Completment combined)
|
||||||
- [ ] Enhanced Gate Simulator (Truth Table Creator)
|
- [X] Extended Hexadecimal Simulator
|
||||||
- [ ] Compound Gate Simulator
|
- [X] Unified Hexadecimal Simulator (For GCSE & A Level Specification)
|
||||||
- [ ] Computer Components Simulator
|
- [X] Enhanced Gate Simulator (Truth Table Creator)
|
||||||
|
- [X] Compound Gate Simulator
|
||||||
|
- [X] Computer Components Simulator - Beta Available
|
||||||
|
|
||||||
## Version 1.0 Release Date: 1<sup>st</sup> September 2025
|
## Version 1.0 Release Date: 1<sup>st</sup> September 2025
|
||||||
## Version 2.0 Release Date (Goal): 1<sup>st</sup> April 2026
|
## Version 2.0 Release Date (Beta): 1<sup>st</sup> March 2026
|
||||||
|
## Version 2.0 Release Date (Full Release): 1<sup>st</sup> May 2026
|
||||||
|
|
||||||
Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]
|
Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 2.0 MiB |
BIN
assets/img/ComputingBoxV1-Logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
35
cliff.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[changelog]
|
||||||
|
header = """
|
||||||
|
# Changelog
|
||||||
|
"""
|
||||||
|
|
||||||
|
body = """
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
## {{ group }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {{ commit.message | upper_first }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
footer = ""
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = false
|
||||||
|
filter_unconventional = false
|
||||||
|
split_commits = false
|
||||||
|
topo_order = true
|
||||||
|
|
||||||
|
# IMPORTANT: match your tag format
|
||||||
|
tag_pattern = "^v[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}[a-z]$"
|
||||||
|
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "🚀 Features" },
|
||||||
|
{ message = "^fix", group = "🐛 Fixes" },
|
||||||
|
{ message = "^refactor", group = "♻️ Refactoring" },
|
||||||
|
{ message = "^docs", group = "📚 Documentation" },
|
||||||
|
{ message = "^chore", group = "💼 Other" },
|
||||||
|
|
||||||
|
# catch-all so NOTHING is dropped
|
||||||
|
{ message = ".*", group = "💼 Other" },
|
||||||
|
]
|
||||||
1
dist/_astro/astro.CXuftnGC.svg
vendored
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
1
dist/_astro/background.Mahwsfbs.svg
vendored
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
1
dist/_astro/hexadecimal.C_Opoo6d.css
vendored
98
dist/binary/index.html
vendored
@@ -1,7 +1,93 @@
|
|||||||
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
|
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator | Computing:Box</title><script>
|
||||||
:root{--panel-w: 360px;--gap: 22px}.wrap{max-width:1400px;margin:0 auto;padding:26px 20px 48px;position:relative}.topGrid{display:grid;grid-template-columns:1fr var(--panel-w);gap:var(--gap);align-items:start}body.toolboxClosed .topGrid{grid-template-columns:1fr}body.toolboxClosed #toolboxPanel{display:none}.mainCol{min-width:0}.readout{text-align:center;margin-top:8px}.label{opacity:.8;letter-spacing:.12em;text-transform:uppercase;font-size:12px}.num{display:inline-block;width:fit-content;max-width:100%;white-space:pre-line;letter-spacing:2px}.denaryValue{font-size:54px;margin:6px 0 10px}.binaryValue{font-size:56px;margin:4px 0 18px}.divider{height:1px;background:#ffffff1a;margin:14px auto 24px;max-width:900px}.bitsWrap{padding-top:6px}.bitsGrid{display:grid;gap:24px;justify-content:center}.bitsGrid{grid-template-columns:repeat(auto-fit,minmax(110px,1fr));max-width:1200px;margin:0 auto}.bitsGrid.bitsFew{justify-content:center}.bit{display:grid;justify-items:center;gap:8px}.bulb{font-size:32px;line-height:1;opacity:.45}.bitVal{font-size:22px;line-height:1.05;text-align:center;white-space:nowrap}.switch{position:relative;display:inline-block;width:52px;height:28px}.switch input{display:none}.slider{position:absolute;inset:0;border-radius:999px;background:#ffffff2e;border:1px solid rgba(255,255,255,.14)}.slider:before{content:"";position:absolute;height:22px;width:22px;left:3px;top:2.5px;border-radius:999px;background:#fff;transition:transform .18s ease}.switch input:checked+.slider:before{transform:translate(22px)}.toolboxToggle{position:absolute;right:20px;top:18px;z-index:20;display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid rgba(255,255,255,.14);background:#ffffff0f;color:#ffffffeb;cursor:pointer}.toolboxText{letter-spacing:.12em;font-weight:900}.panelCol{position:sticky;top:calc(var(--nav-h, 72px) + 18px);align-self:start;display:grid;gap:16px}.card{border:1px solid rgba(255,255,255,.12);border-radius:16px;background:#ffffff0d;padding:14px}.cardTitle{opacity:.8;letter-spacing:.14em;text-transform:uppercase;font-size:12px;margin-bottom:10px}.hint{opacity:.7;font-size:11px;margin-top:10px;line-height:1.35}.toggleRow{display:grid;grid-template-columns:1fr auto 1fr;gap:10px;align-items:center}.toggleLabel{font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap}.subCard{margin-top:12px;border:1px solid rgba(255,255,255,.1);border-radius:14px;background:#0000001f;padding:12px}.subTitle{opacity:.8;letter-spacing:.14em;text-transform:uppercase;font-size:11px;margin-bottom:10px}.bitWidthRow{display:grid;grid-template-columns:44px 1fr 44px;gap:10px;align-items:center}.bitInputWrap{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;padding:10px 12px;border:1px solid rgba(255,255,255,.1);border-radius:12px;background:#ffffff0a}.bitInputLabel{opacity:.75;letter-spacing:.14em;text-transform:uppercase;font-size:11px;white-space:nowrap}.bitInput{width:100%;min-width:0;background:transparent;border:none;outline:none;color:inherit;font-size:20px;text-align:right}.miniBtn{height:44px;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffe6;font-size:18px;cursor:pointer}.controlsRow{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}.btn{border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffeb;padding:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.btnWide{width:100%}.btnAccent{background:#00ff8c1f;border-color:#00ff8c38}.toolRowCentered{display:flex;justify-content:center;gap:12px;margin:10px 0 12px}.toolBtn{width:56px;height:56px;border-radius:14px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffeb;font-size:18px;cursor:pointer}.toolDec{background:#ff000024;border-color:#f003}.toolInc{background:#00ff8c24;border-color:#00ff8c33}.toolRow2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.btnReset{color:#ffffffeb}
|
var _paq = window._paq = window._paq || [];
|
||||||
</style></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxLabel">TOOLBOX</span> </button> <main class="wrap"> <section class="topGrid"> <!-- LEFT --> <div> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">0000 0000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Bit switches"> <div class="bitsGrid" id="bitsGrid"></div> </section> </div> <!-- RIGHT TOOLBOX --> <aside id="toolbox" class="panelCol" aria-label="Toolbox"> <!-- SETTINGS --> <div class="card"> <div class="cardTitle">Settings</div> <div class="toggleRow"> <div class="toggleLabel" id="lblUnsigned">Unsigned</div> <label class="switch" aria-label="Toggle mode"> <input id="modeToggle" type="checkbox"> <span class="slider"></span> </label> <div class="toggleLabel" id="lblTwos">Two’s complement</div> </div> <div class="hint" id="modeHint">
|
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
|
||||||
|
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
|
||||||
|
_paq.push(["enableCrossDomainLinking"]);
|
||||||
|
_paq.push(["disableCookies"]);
|
||||||
|
_paq.push(['trackPageView']);
|
||||||
|
_paq.push(['enableLinkTracking']);
|
||||||
|
(function() {
|
||||||
|
var u="//analytics.adcmnetworks.co.uk/";
|
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
|
_paq.push(['setSiteId', '2']);
|
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
|
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="icon" type="image/x-icon" href="/images/favicon.ico"><link rel="stylesheet" href="/_astro/BaseLayout.B8W3SO34.css">
|
||||||
|
<link rel="stylesheet" href="/_astro/number-simulators.6IzVRJBu.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo-small.webp" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/">Home</a> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div class="binaryPage" id="binaryPage"> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxText">TOOLBOX</span> </button> <section class="topGrid"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">00000000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Bit switches"> <div class="bitsGrid" id="bitsGrid"></div> </section> </div> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <div class="card"> <div class="cardTitle">Settings</div> <div class="toggleRow"> <div class="toggleLabel" id="lblUnsigned">Unsigned</div> <label class="switch" aria-label="Toggle mode"> <input id="modeToggle" type="checkbox"> <span class="slider"></span> </label> <div class="toggleLabel" id="lblTwos">Two's complement</div> </div> <div class="hint" id="modeHint">
|
||||||
Tip: In unsigned binary, all bits represent positive values.
|
Tip: In unsigned binary, all bits represent positive values.
|
||||||
</div> <div class="subCard"> <div class="subTitle">Bit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button> <div class="bitInputWrap"> <div class="bitInputLabel">Bits</div> <input id="bitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="64" step="1" value="8" aria-label="Number of bits"> </div> <button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button> </div> </div> </div> <!-- CUSTOM --> <div class="card"> <div class="cardTitle">Custom</div> <div class="twoBtnRow"> <button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button> </div> <button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
</div> <div class="subCard"> <div class="subTitle">Bit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button> <div class="bitInputWrap"> <div class="bitInputLabel">Bits</div> <input id="bitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="64" step="1" value="8" aria-label="Number of bits"> </div> <button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button> </div> </div> </div> <div class="card"> <div class="cardTitle">Custom Number</div> <div class="controlsRow"> <button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button> </div> <button class="btn btnWide" id="btnRandom" type="button">Random</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolRowCentered"> <button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button> <button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button> </div> <div class="controlsRow"> <button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button> </div> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button> </div> </aside> </section> </div> <script type="module" src="/_astro/binary.astro_astro_type_script_index_0_lang.CNqn-vvz.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div style="margin-top: 5px; display: flex; justify-content: center;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a> <a href="/legal-code" style="margin-left: 32px; color: var(--muted); text-decoration: underline;">Legal Code</a> </div> <div>Computer Science Concept Simulators</div> <div> Version:
|
||||||
Random
|
<a href="#" target="_blank" rel="noopener noreferrer">dev</a> • © 2026 Computing:Box • Created with ♥ by Mr A Lyall</div> </div> </footer> <script>
|
||||||
</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <!-- TOOLS --> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolsTopRow"> <button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button> <button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button> </div> <div class="twoBtnRow"> <button class="btn" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn" id="btnShiftRight" type="button">Right Shift</button> </div> <button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button> </div> </aside> </section> </main> <script type="module" src="/src/scripts/binary.js"></script> </main> </body></html>
|
function setupToolboxAccordions() {
|
||||||
|
// Look for cards inside ANY of the three toolboxes!
|
||||||
|
const cards = document.querySelectorAll('.panelCol .card, .pb-toolbox .card, .lg-toolbox .card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
|
||||||
|
// Your primary cards for each page
|
||||||
|
const primaryCardNames = ['settings', 'info', 'components', 'system diagnostics'];
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const titleEl = card.querySelector('.cardTitle');
|
||||||
|
if (!titleEl) return;
|
||||||
|
|
||||||
|
const titleText = titleEl.textContent.trim().toLowerCase();
|
||||||
|
const isPrimary = primaryCardNames.includes(titleText);
|
||||||
|
|
||||||
|
// 1. DYNAMICALLY WRAP THE CONTENT
|
||||||
|
if (!card.querySelector('.cardBody')) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'cardBody';
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className = 'cardBodyInner';
|
||||||
|
body.appendChild(inner);
|
||||||
|
|
||||||
|
Array.from(card.childNodes).forEach(node => {
|
||||||
|
if (node !== titleEl) {
|
||||||
|
inner.appendChild(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. APPLY DEFAULT STATE
|
||||||
|
if (isPrimary) {
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CLICK LISTENERS
|
||||||
|
if (card.dataset.accordionInit) return;
|
||||||
|
card.dataset.accordionInit = "true";
|
||||||
|
|
||||||
|
titleEl.addEventListener('click', () => {
|
||||||
|
const isCollapsing = !card.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsing) {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
if (isPrimary) {
|
||||||
|
cards.forEach(c => {
|
||||||
|
// Only close cards that share the same parent toolbox
|
||||||
|
if (c !== card && c.closest(card.parentElement.tagName) === card.closest(card.parentElement.tagName)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cards.forEach(c => {
|
||||||
|
if (c.closest(card.parentElement.tagName) !== card.closest(card.parentElement.tagName)) return;
|
||||||
|
const cTitle = c.querySelector('.cardTitle')?.textContent.trim().toLowerCase() || '';
|
||||||
|
if (primaryCardNames.includes(cTitle)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupToolboxAccordions();
|
||||||
|
document.addEventListener('astro:page-load', setupToolboxAccordions);
|
||||||
|
</script> </body> </html>
|
||||||
98
dist/hexadecimal/index.html
vendored
@@ -1,5 +1,93 @@
|
|||||||
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal | Computing:Box</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
|
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal Simulator | Computing:Box</title><script>
|
||||||
</style>
|
var _paq = window._paq = window._paq || [];
|
||||||
<link rel="stylesheet" href="/_astro/hexadecimal.C_Opoo6d.css"></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <section class="hex-sim" data-hex-sim> <div class="hex-main"> <div class="hex-readout"> <div class="hex-label">DENARY</div> <div class="hex-number" data-out="denary">0</div> <div class="hex-label hex-mt">HEXADECIMAL</div> <div class="hex-number hex-number--small" data-out="hex">00</div> <div class="hex-label hex-mt">BINARY</div> <div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div> </div> <div class="hex-divider"></div> <div class="hex-digits" data-out="digitsRow"></div> </div> <!-- Toolbox button --> <button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true"> <span class="hex-toolbox-icon" aria-hidden="true"> <!-- toolbox icon --> <svg viewBox="0 0 24 24" width="18" height="18" fill="none"> <path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path> <path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path> <path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path> </svg> </span>
|
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
|
||||||
TOOLBOX
|
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
|
||||||
</button> <!-- Toolbox panel --> <aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox"> <div class="hex-panel"> <div class="hex-panel-title">SETTINGS</div> <div class="hex-setting-title">HEX DIGIT WIDTH</div> <div class="hex-width"> <button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus">−</button> <div class="hex-width-readout"> <div class="hex-width-label">DIGITS</div> <div class="hex-width-number" data-out="digitsCount">2</div> </div> <button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button> </div> <div class="hex-hint" data-out="bitsHint">= 8 bits</div> </div> <div class="hex-panel"> <div class="hex-panel-title">CUSTOM NUMBER</div> <div class="hex-grid-2"> <button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button> <button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button> </div> <!-- Custom Binary + Random on SAME row, same size --> <div class="hex-grid-2 hex-mt-sm"> <button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button> <button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button> </div> <div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div> </div> <div class="hex-panel"> <div class="hex-panel-title">TOOLS</div> <div class="hex-tools-top"> <button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button> <button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button> </div> <button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button> </div> </aside> <!-- Custom number dialog --> <dialog class="hex-dialog" data-out="dialog"> <div class="hex-dialog-card"> <div class="hex-dialog-title" data-out="dialogTitle">Custom</div> <input class="hex-dialog-input hex-font-mono" data-out="dialogInput"> <div class="hex-dialog-hint" data-out="dialogHint"></div> <div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div> <div class="hex-dialog-actions"> <button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button> <button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button> </div> </div> </dialog> <script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script> </section> </main> </body></html>
|
_paq.push(["enableCrossDomainLinking"]);
|
||||||
|
_paq.push(["disableCookies"]);
|
||||||
|
_paq.push(['trackPageView']);
|
||||||
|
_paq.push(['enableLinkTracking']);
|
||||||
|
(function() {
|
||||||
|
var u="//analytics.adcmnetworks.co.uk/";
|
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
|
_paq.push(['setSiteId', '2']);
|
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
|
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="icon" type="image/x-icon" href="/images/favicon.ico"><link rel="stylesheet" href="/_astro/BaseLayout.B8W3SO34.css">
|
||||||
|
<link rel="stylesheet" href="/_astro/number-simulators.6IzVRJBu.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo-small.webp" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/">Home</a> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div class="binaryPage" id="hexPage"> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxText">TOOLBOX</span> </button> <section class="topGrid"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Hexadecimal</div> <div id="hexNumber" class="num hexValue">00</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">00000000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Hexadecimal sliders"> <div class="hexGrid" id="hexGrid"></div> </section> </div> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <div class="card"> <div class="cardTitle">Settings</div> <div class="hint" style="margin-top: 0; margin-bottom: 14px;">
|
||||||
|
Hexadecimal represents numbers using base 16 (0-9, A-F).
|
||||||
|
</div> <div class="subCard"> <div class="subTitle">Digit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits">−</button> <div class="bitInputWrap"> <div class="bitInputLabel">Digits</div> <input id="digitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="16" step="1" value="2" aria-label="Number of hex digits"> </div> <button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button> </div> </div> </div> <div class="card"> <div class="cardTitle">Custom Number</div> <div class="controlsRow"> <button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button> </div> <div class="controlsRow"> <button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button> </div> <button class="btn btnWide" id="btnRandom" type="button">Random</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <div class="card"> <div class="cardTitle">Tools</div> <div class="toolRowCentered"> <button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button> <button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button> </div> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button> </div> </aside> </section> </div> <script type="module" src="/_astro/hexadecimal.astro_astro_type_script_index_0_lang.C4Wx7oaX.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div style="margin-top: 5px; display: flex; justify-content: center;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a> <a href="/legal-code" style="margin-left: 32px; color: var(--muted); text-decoration: underline;">Legal Code</a> </div> <div>Computer Science Concept Simulators</div> <div> Version:
|
||||||
|
<a href="#" target="_blank" rel="noopener noreferrer">dev</a> • © 2026 Computing:Box • Created with ♥ by Mr A Lyall</div> </div> </footer> <script>
|
||||||
|
function setupToolboxAccordions() {
|
||||||
|
// Look for cards inside ANY of the three toolboxes!
|
||||||
|
const cards = document.querySelectorAll('.panelCol .card, .pb-toolbox .card, .lg-toolbox .card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
|
||||||
|
// Your primary cards for each page
|
||||||
|
const primaryCardNames = ['settings', 'info', 'components', 'system diagnostics'];
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const titleEl = card.querySelector('.cardTitle');
|
||||||
|
if (!titleEl) return;
|
||||||
|
|
||||||
|
const titleText = titleEl.textContent.trim().toLowerCase();
|
||||||
|
const isPrimary = primaryCardNames.includes(titleText);
|
||||||
|
|
||||||
|
// 1. DYNAMICALLY WRAP THE CONTENT
|
||||||
|
if (!card.querySelector('.cardBody')) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'cardBody';
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className = 'cardBodyInner';
|
||||||
|
body.appendChild(inner);
|
||||||
|
|
||||||
|
Array.from(card.childNodes).forEach(node => {
|
||||||
|
if (node !== titleEl) {
|
||||||
|
inner.appendChild(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. APPLY DEFAULT STATE
|
||||||
|
if (isPrimary) {
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CLICK LISTENERS
|
||||||
|
if (card.dataset.accordionInit) return;
|
||||||
|
card.dataset.accordionInit = "true";
|
||||||
|
|
||||||
|
titleEl.addEventListener('click', () => {
|
||||||
|
const isCollapsing = !card.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsing) {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
if (isPrimary) {
|
||||||
|
cards.forEach(c => {
|
||||||
|
// Only close cards that share the same parent toolbox
|
||||||
|
if (c !== card && c.closest(card.parentElement.tagName) === card.closest(card.parentElement.tagName)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cards.forEach(c => {
|
||||||
|
if (c.closest(card.parentElement.tagName) !== card.closest(card.parentElement.tagName)) return;
|
||||||
|
const cTitle = c.querySelector('.cardTitle')?.textContent.trim().toLowerCase() || '';
|
||||||
|
if (primaryCardNames.includes(cTitle)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupToolboxAccordions();
|
||||||
|
document.addEventListener('astro:page-load', setupToolboxAccordions);
|
||||||
|
</script> </body> </html>
|
||||||
1017
dist/images/computing-box-logo.svg
vendored
|
Before Width: | Height: | Size: 732 KiB |
1017
dist/images/favicon.svg
vendored
|
Before Width: | Height: | Size: 732 KiB |
99
dist/index.html
vendored
@@ -1,7 +1,92 @@
|
|||||||
<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v5.16.6"><title>Astro Basics</title><style>#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}#container[data-astro-cid-mmc7otgs]{font-family:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;height:100%}main[data-astro-cid-mmc7otgs]{height:100%;display:flex;justify-content:center}#hero[data-astro-cid-mmc7otgs]{display:flex;align-items:start;flex-direction:column;justify-content:center;padding:16px}h1[data-astro-cid-mmc7otgs]{font-size:22px;margin-top:.25em}#links[data-astro-cid-mmc7otgs]{display:flex;gap:16px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]{display:flex;align-items:center;padding:10px 12px;color:#111827;text-decoration:none;transition:color .2s}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]:hover{color:#4e5056}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs] svg[data-astro-cid-mmc7otgs]{height:1em;margin-left:8px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{color:#fff;background:linear-gradient(83.21deg,#3245ff,#bc52ee);box-shadow:inset 0 0 0 1px #ffffff1f,inset 0 -2px #0000003d;border-radius:10px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button:hover{color:#e6e6e6;box-shadow:none}pre[data-astro-cid-mmc7otgs]{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-weight:400;background:linear-gradient(14deg,#d83333,#f041ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:0}h2[data-astro-cid-mmc7otgs]{margin:0 0 1em;font-weight:400;color:#111827;font-size:20px}p[data-astro-cid-mmc7otgs]{color:#4b5563;font-size:16px;line-height:24px;letter-spacing:-.006em;margin:0}code[data-astro-cid-mmc7otgs]{display:inline-block;background:linear-gradient(66.77deg,#f3cddd,#f5cee7) padding-box,linear-gradient(155deg,#d83333,#f041ff 18%,#f5cee7 45%) border-box;border-radius:8px;border:1px solid transparent;padding:6px 8px}.box[data-astro-cid-mmc7otgs]{padding:16px;background:#fff;border-radius:16px;border:1px solid white}#news[data-astro-cid-mmc7otgs]{position:absolute;bottom:16px;right:16px;max-width:300px;text-decoration:none;transition:background .2s;backdrop-filter:blur(50px)}#news[data-astro-cid-mmc7otgs]:hover{background:#ffffff8c}@media screen and (max-height:368px){#news[data-astro-cid-mmc7otgs]{display:none}}@media screen and (max-width:768px){#container[data-astro-cid-mmc7otgs]{display:flex;flex-direction:column}#hero[data-astro-cid-mmc7otgs]{display:block;padding-top:10%}#links[data-astro-cid-mmc7otgs]{flex-wrap:wrap}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{padding:14px 18px}#news[data-astro-cid-mmc7otgs]{right:16px;left:16px;bottom:2.5rem;max-width:100%}h1[data-astro-cid-mmc7otgs]{line-height:1.5}}html,body{margin:0;width:100%;height:100%}
|
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome | Computing:Box</title><script>
|
||||||
</style></head> <body data-astro-cid-sckkx6r4> <div id="container" data-astro-cid-mmc7otgs> <img id="background" src="/_astro/background.Mahwsfbs.svg" alt="" fetchpriority="high" data-astro-cid-mmc7otgs> <main data-astro-cid-mmc7otgs> <section id="hero" data-astro-cid-mmc7otgs> <a href="https://astro.build" data-astro-cid-mmc7otgs><img src="/_astro/astro.CXuftnGC.svg" width="115" height="48" alt="Astro Homepage" data-astro-cid-mmc7otgs></a> <h1 data-astro-cid-mmc7otgs>
|
var _paq = window._paq = window._paq || [];
|
||||||
To get started, open the <code data-astro-cid-mmc7otgs><pre data-astro-cid-mmc7otgs>src/pages</pre></code> directory in your project.
|
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
|
||||||
</h1> <section id="links" data-astro-cid-mmc7otgs> <a class="button" href="https://docs.astro.build" data-astro-cid-mmc7otgs>Read our docs</a> <a href="https://astro.build/chat" data-astro-cid-mmc7otgs>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" data-astro-cid-mmc7otgs><path fill="currentColor" d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" data-astro-cid-mmc7otgs></path></svg> </a> </section> </section> </main> <a href="https://astro.build/blog/astro-5/" id="news" class="box" data-astro-cid-mmc7otgs> <svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg" data-astro-cid-mmc7otgs><path d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z" fill="#111827" data-astro-cid-mmc7otgs></path></svg> <h2 data-astro-cid-mmc7otgs>What's New in Astro 5.0?</h2> <p data-astro-cid-mmc7otgs>
|
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
|
||||||
From content layers to server islands, click to learn more about the new features and
|
_paq.push(["enableCrossDomainLinking"]);
|
||||||
improvements in Astro 5.0
|
_paq.push(["disableCookies"]);
|
||||||
</p> </a> </div> </body></html>
|
_paq.push(['trackPageView']);
|
||||||
|
_paq.push(['enableLinkTracking']);
|
||||||
|
(function() {
|
||||||
|
var u="//analytics.adcmnetworks.co.uk/";
|
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
|
_paq.push(['setSiteId', '2']);
|
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
|
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="icon" type="image/x-icon" href="/images/favicon.ico"><link rel="stylesheet" href="/_astro/BaseLayout.B8W3SO34.css"></head> <body> <header class="siteNav"> <div class="navInner"> <a class="brand" href="/"> <img class="brandLogo" src="/images/computing-box-logo-small.webp" alt="Computing:Box logo"> <span class="brandName">Computing:Box</span> </a> <nav class="navLinks" aria-label="Site navigation"> <a href="/">Home</a> <a href="/about">About</a> <a href="/binary">Binary</a> <a href="/hexadecimal">Hexadecimal</a> <a href="/hex-colours">Hex Colours</a> <a href="/logic-gates">Logic Gates</a> <a href="/pc-builder">PC Components</a> </nav> </div> </header> <main class="pageWrap"> <div style="display: flex; align-items: center; justify-content: space-between; gap: 40px; min-height: 60vh; padding: 40px 0;"> <div style="flex: 1;"> <p style="color: var(--accent); font-weight: 800; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px;">Version 2.0 Now Live</p> <h1 class="brandName" style="font-size: 48px; line-height: 1.1; margin-bottom: 24px;">Understand Computing concepts better.</h1> <p style="font-size: 18px; color: var(--muted);">
|
||||||
|
Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum.
|
||||||
|
</p> <div style="display: flex; gap: 16px; margin-top: 32px;"> <a href="/about" class="btn btnAccent" style="text-decoration: none; padding: 14px 28px;">Learn More</a> <a href="/binary" class="btn" style="text-decoration: none; padding: 14px 28px;">Get Started</a> </div> </div> <div style="flex: 1; text-align: right;"> <img src="/images/computing-box-logo.webp" alt="Computing Box Logo" style="width: 100%; max-width: 450px; filter: drop-shadow(0 0 50px rgba(40, 240, 122, 0.15));"> </div> </div> </main> <footer class="siteFooter"> <div class="footerInner"> <div style="margin-top: 5px; display: flex; justify-content: center;"> <a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a> <a href="/legal-code" style="margin-left: 32px; color: var(--muted); text-decoration: underline;">Legal Code</a> </div> <div>Computer Science Concept Simulators</div> <div> Version:
|
||||||
|
<a href="#" target="_blank" rel="noopener noreferrer">dev</a> • © 2026 Computing:Box • Created with ♥ by Mr A Lyall</div> </div> </footer> <script>
|
||||||
|
function setupToolboxAccordions() {
|
||||||
|
// Look for cards inside ANY of the three toolboxes!
|
||||||
|
const cards = document.querySelectorAll('.panelCol .card, .pb-toolbox .card, .lg-toolbox .card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
|
||||||
|
// Your primary cards for each page
|
||||||
|
const primaryCardNames = ['settings', 'info', 'components', 'system diagnostics'];
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const titleEl = card.querySelector('.cardTitle');
|
||||||
|
if (!titleEl) return;
|
||||||
|
|
||||||
|
const titleText = titleEl.textContent.trim().toLowerCase();
|
||||||
|
const isPrimary = primaryCardNames.includes(titleText);
|
||||||
|
|
||||||
|
// 1. DYNAMICALLY WRAP THE CONTENT
|
||||||
|
if (!card.querySelector('.cardBody')) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'cardBody';
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className = 'cardBodyInner';
|
||||||
|
body.appendChild(inner);
|
||||||
|
|
||||||
|
Array.from(card.childNodes).forEach(node => {
|
||||||
|
if (node !== titleEl) {
|
||||||
|
inner.appendChild(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. APPLY DEFAULT STATE
|
||||||
|
if (isPrimary) {
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CLICK LISTENERS
|
||||||
|
if (card.dataset.accordionInit) return;
|
||||||
|
card.dataset.accordionInit = "true";
|
||||||
|
|
||||||
|
titleEl.addEventListener('click', () => {
|
||||||
|
const isCollapsing = !card.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsing) {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
if (isPrimary) {
|
||||||
|
cards.forEach(c => {
|
||||||
|
// Only close cards that share the same parent toolbox
|
||||||
|
if (c !== card && c.closest(card.parentElement.tagName) === card.closest(card.parentElement.tagName)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cards.forEach(c => {
|
||||||
|
if (c.closest(card.parentElement.tagName) !== card.closest(card.parentElement.tagName)) return;
|
||||||
|
const cTitle = c.querySelector('.cardTitle')?.textContent.trim().toLowerCase() || '';
|
||||||
|
if (primaryCardNames.includes(cTitle)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupToolboxAccordions();
|
||||||
|
document.addEventListener('astro:page-load', setupToolboxAccordions);
|
||||||
|
</script> </body> </html>
|
||||||
115
dist/js/binary/unsigned-binary.js
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
|
||||||
|
|
||||||
|
const BIT_COUNT = 8; // unsigned page = 8 bits
|
||||||
|
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
|
||||||
|
|
||||||
|
const elDenary = document.getElementById("denaryNumber");
|
||||||
|
const elBinary = document.getElementById("binaryNumber");
|
||||||
|
const elSwitches = document.getElementById("bitSwitches");
|
||||||
|
|
||||||
|
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||||
|
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||||
|
const btnReset = document.getElementById("btnReset");
|
||||||
|
|
||||||
|
let bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
|
||||||
|
function renderSwitches() {
|
||||||
|
elSwitches.innerHTML = "";
|
||||||
|
|
||||||
|
bitValues.forEach((value, index) => {
|
||||||
|
const id = `bit-${value}`;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "switch-col";
|
||||||
|
|
||||||
|
const labelTop = document.createElement("div");
|
||||||
|
labelTop.className = "bit-label";
|
||||||
|
labelTop.textContent = value;
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "rocker";
|
||||||
|
label.setAttribute("for", id);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.id = id;
|
||||||
|
input.checked = bits[index];
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
bits[index] = input.checked;
|
||||||
|
updateNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "rocker-body";
|
||||||
|
span.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(span);
|
||||||
|
|
||||||
|
wrapper.appendChild(labelTop);
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
|
||||||
|
elSwitches.appendChild(wrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNumbers() {
|
||||||
|
const binary = bits.map(b => (b ? "1" : "0")).join("");
|
||||||
|
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
|
||||||
|
|
||||||
|
elBinary.textContent = binary;
|
||||||
|
elDenary.textContent = denary.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCustomDenary() {
|
||||||
|
let input = prompt(`Enter a denary number (0 to 255):`);
|
||||||
|
if (input === null) return;
|
||||||
|
|
||||||
|
const n = Number.parseInt(input, 10);
|
||||||
|
if (Number.isNaN(n) || n < 0 || n > 255) {
|
||||||
|
alert("Invalid input. Enter a number from 0 to 255.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = n;
|
||||||
|
bits = bitValues.map(v => {
|
||||||
|
if (remaining >= v) {
|
||||||
|
remaining -= v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCustomBinary() {
|
||||||
|
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
|
||||||
|
if (input === null) return;
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
|
||||||
|
if (!re.test(input)) {
|
||||||
|
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bits = input.split("").map(c => c === "1");
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCustomDenary?.addEventListener("click", requestCustomDenary);
|
||||||
|
btnCustomBinary?.addEventListener("click", requestCustomBinary);
|
||||||
|
btnReset?.addEventListener("click", resetAll);
|
||||||
|
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
72
dist/js/tools/unsigned-binary.js
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// public/js/tools/unsigned-binary.js
|
||||||
|
// Lightweight: no frameworks. Works on weak devices.
|
||||||
|
const BIT_COUNT = 8;
|
||||||
|
const MAX_DENARY = 255;
|
||||||
|
let bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
function bitsToBinaryString(){
|
||||||
|
return bits.map(b => (b ? "1" : "0")).join("");
|
||||||
|
}
|
||||||
|
function bitsToDenary(){
|
||||||
|
// MSB on the left: 128..1
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
|
||||||
|
}
|
||||||
|
function render(){
|
||||||
|
const grid = document.getElementById("bitGrid");
|
||||||
|
grid.innerHTML = "";
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
bits.forEach((on, i) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "btn";
|
||||||
|
btn.style.width = "100%";
|
||||||
|
btn.style.justifyContent = "space-between";
|
||||||
|
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
bits[i] = !bits[i];
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
grid.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function update(){
|
||||||
|
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
|
||||||
|
document.getElementById("denaryNumber").innerText = bitsToDenary();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function requestBinary(){
|
||||||
|
let v;
|
||||||
|
do{
|
||||||
|
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
|
||||||
|
if(v === null) return;
|
||||||
|
v = v.trim();
|
||||||
|
}while(!/^[01]{8}$/.test(v));
|
||||||
|
bits = v.split("").map(ch => ch === "1");
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
function requestDenary(){
|
||||||
|
let v;
|
||||||
|
do{
|
||||||
|
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
|
||||||
|
if(v === null) return;
|
||||||
|
v = Number(v);
|
||||||
|
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
|
||||||
|
// set bits from MSB to LSB
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
bits = weights.map(w => {
|
||||||
|
if(v >= w){ v -= w; return true; }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
function reset(){
|
||||||
|
bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
|
||||||
|
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
|
||||||
|
document.getElementById("btnReset")?.addEventListener("click", reset);
|
||||||
|
update();
|
||||||
|
});
|
||||||
96
dist/styles.css
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* src/styles/md3-tokens.css */
|
||||||
|
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
|
||||||
|
:root{
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
/* Spacing + shape */
|
||||||
|
--radius-1: 10px;
|
||||||
|
--radius-2: 16px;
|
||||||
|
--radius-3: 22px;
|
||||||
|
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
|
||||||
|
/* Color roles (keep simple) */
|
||||||
|
--md-surface: #ffffff;
|
||||||
|
--md-surface-2: #f6f7fb;
|
||||||
|
--md-on-surface: #111318;
|
||||||
|
--md-primary: #2f6fed; /* calm blue */
|
||||||
|
--md-on-primary: #ffffff;
|
||||||
|
--md-secondary: #5a5f72; /* muted */
|
||||||
|
--md-on-secondary: #ffffff;
|
||||||
|
--md-tertiary: #0f766e; /* teal for "practical" tools */
|
||||||
|
--md-on-tertiary: #ffffff;
|
||||||
|
--md-outline: #d7dbe7;
|
||||||
|
--md-success: #1a7f37;
|
||||||
|
--md-warning: #b54708;
|
||||||
|
--md-danger: #b42318;
|
||||||
|
/* Focus ring for accessibility */
|
||||||
|
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
:root{
|
||||||
|
--md-surface: #0b0e14;
|
||||||
|
--md-surface-2: #121725;
|
||||||
|
--md-on-surface: #e8eaf2;
|
||||||
|
--md-primary: #9bb6ff;
|
||||||
|
--md-on-primary: #0b0e14;
|
||||||
|
--md-secondary: #b8bccd;
|
||||||
|
--md-on-secondary: #0b0e14;
|
||||||
|
--md-tertiary: #4fd1c5;
|
||||||
|
--md-on-tertiary: #0b0e14;
|
||||||
|
--md-outline: #2b3244;
|
||||||
|
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/styles/base.css */
|
||||||
|
@import "./md3-tokens.css";
|
||||||
|
html, body{ height:100%; }
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--md-surface-2);
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
}
|
||||||
|
a{ color: var(--md-primary); text-decoration: none; }
|
||||||
|
a:hover{ text-decoration: underline; }
|
||||||
|
.container{
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.card{
|
||||||
|
background: var(--md-surface);
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
box-shadow: var(--shadow-1);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.btn{
|
||||||
|
display:inline-flex;
|
||||||
|
gap:8px;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
background: var(--md-surface);
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover{ filter: brightness(0.98); }
|
||||||
|
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
|
||||||
|
.btn-primary{
|
||||||
|
background: var(--md-primary);
|
||||||
|
color: var(--md-on-primary);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.badge{
|
||||||
|
display:inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
background: var(--md-surface-2);
|
||||||
|
}
|
||||||
|
code, pre{ font-family: var(--font-mono); }
|
||||||
1672
package-lock.json
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Computing:Box",
|
"name": "computing-box",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.0.0 Alpha",
|
"version": "26.4.1-5.a",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^5.16.6"
|
"astro": "^6.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/Microsoft_Nostalgic_Windows_Wallpaper_4k.jpg
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/images/BitBoxLogo.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/images/Educational_Impact.webp
Normal file
|
After Width: | Height: | Size: 425 KiB |
BIN
public/images/computing-box-logo-small.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 732 KiB |
BIN
public/images/computing-box-logo.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/images/computingbox-concept-illustration.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 732 KiB |
BIN
public/images/favicon.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
115
public/js/binary/unsigned-binary.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
|
||||||
|
|
||||||
|
const BIT_COUNT = 8; // unsigned page = 8 bits
|
||||||
|
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
|
||||||
|
|
||||||
|
const elDenary = document.getElementById("denaryNumber");
|
||||||
|
const elBinary = document.getElementById("binaryNumber");
|
||||||
|
const elSwitches = document.getElementById("bitSwitches");
|
||||||
|
|
||||||
|
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||||
|
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||||
|
const btnReset = document.getElementById("btnReset");
|
||||||
|
|
||||||
|
let bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
|
||||||
|
function renderSwitches() {
|
||||||
|
elSwitches.innerHTML = "";
|
||||||
|
|
||||||
|
bitValues.forEach((value, index) => {
|
||||||
|
const id = `bit-${value}`;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "switch-col";
|
||||||
|
|
||||||
|
const labelTop = document.createElement("div");
|
||||||
|
labelTop.className = "bit-label";
|
||||||
|
labelTop.textContent = value;
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "rocker";
|
||||||
|
label.setAttribute("for", id);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.id = id;
|
||||||
|
input.checked = bits[index];
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
bits[index] = input.checked;
|
||||||
|
updateNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "rocker-body";
|
||||||
|
span.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(span);
|
||||||
|
|
||||||
|
wrapper.appendChild(labelTop);
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
|
||||||
|
elSwitches.appendChild(wrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNumbers() {
|
||||||
|
const binary = bits.map(b => (b ? "1" : "0")).join("");
|
||||||
|
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
|
||||||
|
|
||||||
|
elBinary.textContent = binary;
|
||||||
|
elDenary.textContent = denary.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCustomDenary() {
|
||||||
|
let input = prompt(`Enter a denary number (0 to 255):`);
|
||||||
|
if (input === null) return;
|
||||||
|
|
||||||
|
const n = Number.parseInt(input, 10);
|
||||||
|
if (Number.isNaN(n) || n < 0 || n > 255) {
|
||||||
|
alert("Invalid input. Enter a number from 0 to 255.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = n;
|
||||||
|
bits = bitValues.map(v => {
|
||||||
|
if (remaining >= v) {
|
||||||
|
remaining -= v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCustomBinary() {
|
||||||
|
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
|
||||||
|
if (input === null) return;
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
|
||||||
|
if (!re.test(input)) {
|
||||||
|
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bits = input.split("").map(c => c === "1");
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCustomDenary?.addEventListener("click", requestCustomDenary);
|
||||||
|
btnCustomBinary?.addEventListener("click", requestCustomBinary);
|
||||||
|
btnReset?.addEventListener("click", resetAll);
|
||||||
|
|
||||||
|
renderSwitches();
|
||||||
|
updateNumbers();
|
||||||
72
public/js/tools/unsigned-binary.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// public/js/tools/unsigned-binary.js
|
||||||
|
// Lightweight: no frameworks. Works on weak devices.
|
||||||
|
const BIT_COUNT = 8;
|
||||||
|
const MAX_DENARY = 255;
|
||||||
|
let bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
function bitsToBinaryString(){
|
||||||
|
return bits.map(b => (b ? "1" : "0")).join("");
|
||||||
|
}
|
||||||
|
function bitsToDenary(){
|
||||||
|
// MSB on the left: 128..1
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
|
||||||
|
}
|
||||||
|
function render(){
|
||||||
|
const grid = document.getElementById("bitGrid");
|
||||||
|
grid.innerHTML = "";
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
bits.forEach((on, i) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "btn";
|
||||||
|
btn.style.width = "100%";
|
||||||
|
btn.style.justifyContent = "space-between";
|
||||||
|
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
bits[i] = !bits[i];
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
grid.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function update(){
|
||||||
|
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
|
||||||
|
document.getElementById("denaryNumber").innerText = bitsToDenary();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function requestBinary(){
|
||||||
|
let v;
|
||||||
|
do{
|
||||||
|
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
|
||||||
|
if(v === null) return;
|
||||||
|
v = v.trim();
|
||||||
|
}while(!/^[01]{8}$/.test(v));
|
||||||
|
bits = v.split("").map(ch => ch === "1");
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
function requestDenary(){
|
||||||
|
let v;
|
||||||
|
do{
|
||||||
|
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
|
||||||
|
if(v === null) return;
|
||||||
|
v = Number(v);
|
||||||
|
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
|
||||||
|
// set bits from MSB to LSB
|
||||||
|
const weights = [128,64,32,16,8,4,2,1];
|
||||||
|
bits = weights.map(w => {
|
||||||
|
if(v >= w){ v -= w; return true; }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
function reset(){
|
||||||
|
bits = new Array(BIT_COUNT).fill(false);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
|
||||||
|
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
|
||||||
|
document.getElementById("btnReset")?.addEventListener("click", reset);
|
||||||
|
update();
|
||||||
|
});
|
||||||
96
public/styles.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* src/styles/md3-tokens.css */
|
||||||
|
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
|
||||||
|
:root{
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
/* Spacing + shape */
|
||||||
|
--radius-1: 10px;
|
||||||
|
--radius-2: 16px;
|
||||||
|
--radius-3: 22px;
|
||||||
|
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
|
||||||
|
/* Color roles (keep simple) */
|
||||||
|
--md-surface: #ffffff;
|
||||||
|
--md-surface-2: #f6f7fb;
|
||||||
|
--md-on-surface: #111318;
|
||||||
|
--md-primary: #2f6fed; /* calm blue */
|
||||||
|
--md-on-primary: #ffffff;
|
||||||
|
--md-secondary: #5a5f72; /* muted */
|
||||||
|
--md-on-secondary: #ffffff;
|
||||||
|
--md-tertiary: #0f766e; /* teal for "practical" tools */
|
||||||
|
--md-on-tertiary: #ffffff;
|
||||||
|
--md-outline: #d7dbe7;
|
||||||
|
--md-success: #1a7f37;
|
||||||
|
--md-warning: #b54708;
|
||||||
|
--md-danger: #b42318;
|
||||||
|
/* Focus ring for accessibility */
|
||||||
|
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
:root{
|
||||||
|
--md-surface: #0b0e14;
|
||||||
|
--md-surface-2: #121725;
|
||||||
|
--md-on-surface: #e8eaf2;
|
||||||
|
--md-primary: #9bb6ff;
|
||||||
|
--md-on-primary: #0b0e14;
|
||||||
|
--md-secondary: #b8bccd;
|
||||||
|
--md-on-secondary: #0b0e14;
|
||||||
|
--md-tertiary: #4fd1c5;
|
||||||
|
--md-on-tertiary: #0b0e14;
|
||||||
|
--md-outline: #2b3244;
|
||||||
|
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/styles/base.css */
|
||||||
|
@import "./md3-tokens.css";
|
||||||
|
html, body{ height:100%; }
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--md-surface-2);
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
}
|
||||||
|
a{ color: var(--md-primary); text-decoration: none; }
|
||||||
|
a:hover{ text-decoration: underline; }
|
||||||
|
.container{
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.card{
|
||||||
|
background: var(--md-surface);
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
box-shadow: var(--shadow-1);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.btn{
|
||||||
|
display:inline-flex;
|
||||||
|
gap:8px;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
background: var(--md-surface);
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover{ filter: brightness(0.98); }
|
||||||
|
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
|
||||||
|
.btn-primary{
|
||||||
|
background: var(--md-primary);
|
||||||
|
color: var(--md-on-primary);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.badge{
|
||||||
|
display:inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--md-outline);
|
||||||
|
background: var(--md-surface-2);
|
||||||
|
}
|
||||||
|
code, pre{ font-family: var(--font-mono); }
|
||||||
30
renovate.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"assignAutomerge": true,
|
||||||
|
"assigneesFromCodeOwners": true,
|
||||||
|
"dependencyDashboardAutoclose": true,
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"ignorePaths": ["**/.archive/**"],
|
||||||
|
"labels": ["type/dependencies"],
|
||||||
|
"platformCommit": "enabled",
|
||||||
|
"rebaseWhen": "behind-base-branch",
|
||||||
|
"rollbackPrs": true,
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"commitMessagePrefix": "[SECURITY] ",
|
||||||
|
"enabled": true,
|
||||||
|
"labels": ["security"],
|
||||||
|
"prCreation": "immediate"
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"automerge": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src/assets/Microsoft_Nostalgic_Windows_Wallpaper_4k.jpg
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,210 +0,0 @@
|
|||||||
---
|
|
||||||
import astroLogo from '../assets/astro.svg';
|
|
||||||
import background from '../assets/background.svg';
|
|
||||||
---
|
|
||||||
|
|
||||||
<div id="container">
|
|
||||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
|
||||||
<main>
|
|
||||||
<section id="hero">
|
|
||||||
<a href="https://astro.build"
|
|
||||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
|
||||||
</h1>
|
|
||||||
<section id="links">
|
|
||||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
|
||||||
<a href="https://astro.build/chat"
|
|
||||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
|
|
||||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
|
||||||
fill="#111827"></path></svg
|
|
||||||
>
|
|
||||||
<h2>What's New in Astro 5.0?</h2>
|
|
||||||
<p>
|
|
||||||
From content layers to server islands, click to learn more about the new features and
|
|
||||||
improvements in Astro 5.0
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#background {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
filter: blur(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#container {
|
|
||||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hero {
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
color: #111827;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a:hover {
|
|
||||||
color: rgb(78, 80, 86);
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a svg {
|
|
||||||
height: 1em;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button {
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
|
||||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button:hover {
|
|
||||||
color: rgb(230, 230, 230);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
|
||||||
monospace;
|
|
||||||
font-weight: normal;
|
|
||||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0 0 1em;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #4b5563;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
letter-spacing: -0.006em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
display: inline-block;
|
|
||||||
background:
|
|
||||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
|
||||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
padding: 16px;
|
|
||||||
background: rgba(255, 255, 255, 1);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#news {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
max-width: 300px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 0.2s;
|
|
||||||
backdrop-filter: blur(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#news:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-height: 368px) {
|
|
||||||
#news {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
#container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hero {
|
|
||||||
display: block;
|
|
||||||
padding-top: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button {
|
|
||||||
padding: 14px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#news {
|
|
||||||
right: 16px;
|
|
||||||
left: 16px;
|
|
||||||
bottom: 2.5rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
import "./hex/hex-simulator.css";
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="hex-sim" data-hex-sim>
|
|
||||||
<div class="hex-main">
|
|
||||||
<div class="hex-readout">
|
|
||||||
<div class="hex-label">DENARY</div>
|
|
||||||
<div class="hex-number" data-out="denary">0</div>
|
|
||||||
|
|
||||||
<div class="hex-label hex-mt">HEXADECIMAL</div>
|
|
||||||
<div class="hex-number hex-number--small" data-out="hex">00</div>
|
|
||||||
|
|
||||||
<div class="hex-label hex-mt">BINARY</div>
|
|
||||||
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-divider"></div>
|
|
||||||
|
|
||||||
<div class="hex-digits" data-out="digitsRow"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toolbox button -->
|
|
||||||
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
|
|
||||||
<span class="hex-toolbox-icon" aria-hidden="true">
|
|
||||||
<!-- toolbox icon -->
|
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
|
|
||||||
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
||||||
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
TOOLBOX
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Toolbox panel -->
|
|
||||||
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">SETTINGS</div>
|
|
||||||
|
|
||||||
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
|
|
||||||
|
|
||||||
<div class="hex-width">
|
|
||||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus">−</button>
|
|
||||||
|
|
||||||
<div class="hex-width-readout">
|
|
||||||
<div class="hex-width-label">DIGITS</div>
|
|
||||||
<div class="hex-width-number" data-out="digitsCount">2</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">CUSTOM NUMBER</div>
|
|
||||||
|
|
||||||
<div class="hex-grid-2">
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Binary + Random on SAME row, same size -->
|
|
||||||
<div class="hex-grid-2 hex-mt-sm">
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
|
|
||||||
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">TOOLS</div>
|
|
||||||
|
|
||||||
<div class="hex-tools-top">
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Custom number dialog -->
|
|
||||||
<dialog class="hex-dialog" data-out="dialog">
|
|
||||||
<div class="hex-dialog-card">
|
|
||||||
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
|
|
||||||
|
|
||||||
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
|
|
||||||
|
|
||||||
<div class="hex-dialog-hint" data-out="dialogHint"></div>
|
|
||||||
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
|
|
||||||
|
|
||||||
<div class="hex-dialog-actions">
|
|
||||||
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
|
|
||||||
</section>
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
/* ================= Fonts to match Binary ================= */
|
|
||||||
/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
|
|
||||||
@font-face {
|
|
||||||
font-family: "DSEG7Classic";
|
|
||||||
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
|
||||||
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SevenSegment";
|
|
||||||
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
|
|
||||||
url("/fonts/Seven-Segment.woff") format("woff");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-sim {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #14151c;
|
|
||||||
color: #e7e8ee;
|
|
||||||
padding: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
|
|
||||||
.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
|
|
||||||
|
|
||||||
.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
|
|
||||||
|
|
||||||
.hex-readout { text-align: center; }
|
|
||||||
.hex-label {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.hex-mt { margin-top: 12px; }
|
|
||||||
|
|
||||||
.hex-number {
|
|
||||||
font-family: "DSEG7Classic", ui-monospace, monospace;
|
|
||||||
font-size: 76px;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #46ff8a;
|
|
||||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
|
||||||
}
|
|
||||||
.hex-number--small { font-size: 64px; }
|
|
||||||
.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
|
|
||||||
|
|
||||||
.hex-divider {
|
|
||||||
margin: 26px auto 18px;
|
|
||||||
height: 1px;
|
|
||||||
width: min(760px, 90%);
|
|
||||||
background: rgba(255,255,255,0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Main digit columns ================= */
|
|
||||||
.hex-digits {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 18px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-col {
|
|
||||||
width: 160px;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 12px;
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-controls {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-char {
|
|
||||||
font-size: 64px;
|
|
||||||
line-height: 1;
|
|
||||||
color: #46ff8a;
|
|
||||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-place {
|
|
||||||
font-family: "SevenSegment", ui-monospace, monospace;
|
|
||||||
opacity: 0.65;
|
|
||||||
font-size: 14px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Bulbs (brightness changes) ================= */
|
|
||||||
.hex-bulbs {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb {
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
opacity: 0.35;
|
|
||||||
filter: grayscale(30%);
|
|
||||||
transition: opacity 160ms ease, filter 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-cap {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255,255,255,0.22);
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-glow {
|
|
||||||
width: 18px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(70,255,138,0.0);
|
|
||||||
box-shadow: 0 0 0 rgba(70,255,138,0.0);
|
|
||||||
transition: background 160ms ease, box-shadow 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-label {
|
|
||||||
font-family: "SevenSegment", ui-monospace, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb.is-on {
|
|
||||||
opacity: 1;
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
.hex-bulb.is-on .hex-bulb-cap {
|
|
||||||
background: rgba(255,255,255,0.35);
|
|
||||||
}
|
|
||||||
.hex-bulb.is-on .hex-bulb-glow {
|
|
||||||
background: rgba(70,255,138,0.25);
|
|
||||||
box-shadow: 0 0 18px rgba(70,255,138,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Buttons (toolbox style reused everywhere) ================= */
|
|
||||||
.hex-btn {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
.hex-btn:hover { background: rgba(255,255,255,0.10); }
|
|
||||||
|
|
||||||
.hex-btn--square {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-btn--wide { width: 100%; }
|
|
||||||
|
|
||||||
.hex-btn--green {
|
|
||||||
background: rgba(46, 200, 120, 0.18);
|
|
||||||
border-color: rgba(46,200,120,0.35);
|
|
||||||
}
|
|
||||||
.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
|
|
||||||
|
|
||||||
.hex-btn--green2 {
|
|
||||||
background: rgba(46, 200, 120, 0.18);
|
|
||||||
border-color: rgba(46,200,120,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-btn--red {
|
|
||||||
background: rgba(220, 60, 70, 0.18);
|
|
||||||
border-color: rgba(220,60,70,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Random = green pulse while running */
|
|
||||||
.hex-btn--random.is-running {
|
|
||||||
border-color: rgba(80, 255, 160, 0.55);
|
|
||||||
background: rgba(46, 200, 120, 0.22);
|
|
||||||
box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
|
|
||||||
animation: hexPulseGreen 900ms ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes hexPulseGreen {
|
|
||||||
0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
|
|
||||||
50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset = red background + pulse on hover */
|
|
||||||
.hex-btn--reset:hover {
|
|
||||||
background: rgba(220, 60, 70, 0.28);
|
|
||||||
border-color: rgba(255, 80, 90, 0.55);
|
|
||||||
animation: hexPulseRed 900ms ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes hexPulseRed {
|
|
||||||
0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
|
|
||||||
50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Toolbox button + panel (slide) ================= */
|
|
||||||
.hex-toolbox-btn {
|
|
||||||
position: fixed;
|
|
||||||
top: 88px;
|
|
||||||
right: 28px;
|
|
||||||
z-index: 30;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-toolbox-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: #ff4fa6;
|
|
||||||
filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-toolbox {
|
|
||||||
position: fixed;
|
|
||||||
top: 140px;
|
|
||||||
right: 28px;
|
|
||||||
width: 340px;
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
z-index: 25;
|
|
||||||
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
transition: transform 220ms ease, opacity 220ms ease;
|
|
||||||
}
|
|
||||||
.hex-toolbox:not(.is-open) {
|
|
||||||
transform: translateX(380px);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-panel {
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255,255,255,0.04);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
.hex-panel-title {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
|
|
||||||
|
|
||||||
.hex-width {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 48px 1fr 48px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.hex-width-readout {
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(0,0,0,0.22);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 10px 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
.hex-width-label {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
opacity: 0.7;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
|
|
||||||
|
|
||||||
.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
||||||
.hex-mt-sm { margin-top: 10px; }
|
|
||||||
|
|
||||||
.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
|
|
||||||
.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
/* ================= Dialog ================= */
|
|
||||||
.hex-dialog { border: none; padding: 0; background: transparent; }
|
|
||||||
.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
|
|
||||||
|
|
||||||
.hex-dialog-card {
|
|
||||||
width: min(560px, 92vw);
|
|
||||||
border-radius: 18px;
|
|
||||||
background: #1a1b24;
|
|
||||||
border: 1px solid rgba(255,255,255,0.12);
|
|
||||||
padding: 16px;
|
|
||||||
color: #e7e8ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
.hex-dialog-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(0,0,0,0.25);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.hex-toolbox { width: min(360px, 92vw); right: 16px; }
|
|
||||||
.hex-toolbox-btn { right: 16px; }
|
|
||||||
.hex-number { font-size: 60px; }
|
|
||||||
.hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
type DialogMode = "hex" | "den" | "bin";
|
|
||||||
|
|
||||||
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
|
|
||||||
if (!root) throw new Error("Hex simulator root not found");
|
|
||||||
|
|
||||||
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
|
|
||||||
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
|
|
||||||
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
|
|
||||||
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
|
|
||||||
|
|
||||||
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
|
|
||||||
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
|
|
||||||
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
|
|
||||||
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
|
|
||||||
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
|
|
||||||
|
|
||||||
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
|
|
||||||
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
|
|
||||||
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
|
|
||||||
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
|
|
||||||
const dialogError = root.querySelector<HTMLElement>('[data-out="dialogError"]')!;
|
|
||||||
|
|
||||||
let digits = 2; // 1..8
|
|
||||||
let value = 0; // unsigned denary
|
|
||||||
let randomTimer: number | null = null;
|
|
||||||
let dialogMode: DialogMode | null = null;
|
|
||||||
|
|
||||||
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
|
|
||||||
const maxForDigits = (d: number) => (16 ** d) - 1;
|
|
||||||
|
|
||||||
const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
|
|
||||||
const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
|
|
||||||
const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
|
|
||||||
|
|
||||||
function stopRandom(): void {
|
|
||||||
if (randomTimer !== null) window.clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
randomBtn.classList.remove("is-running");
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRandom(): void {
|
|
||||||
stopRandom();
|
|
||||||
const max = maxForDigits(digits);
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
randomBtn.classList.add("is-running");
|
|
||||||
|
|
||||||
randomTimer = window.setInterval(() => {
|
|
||||||
value = Math.floor(Math.random() * (max + 1));
|
|
||||||
render();
|
|
||||||
if (Date.now() - start > 1600) stopRandom();
|
|
||||||
}, 90);
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(): void {
|
|
||||||
const bits = digits * 4;
|
|
||||||
|
|
||||||
digitsCount.textContent = String(digits);
|
|
||||||
bitsHint.textContent = `= ${bits} bits`;
|
|
||||||
|
|
||||||
outDen.textContent = String(value);
|
|
||||||
outHex.textContent = padHex(value, digits);
|
|
||||||
outBin.textContent = groupBin(padBin(value, bits));
|
|
||||||
|
|
||||||
renderDigitsRow();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDigitsRow(): void {
|
|
||||||
const hex = padHex(value, digits);
|
|
||||||
outDigitsRow.innerHTML = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < digits; i++) {
|
|
||||||
const pow = digits - 1 - i;
|
|
||||||
const placeValue = 16 ** pow;
|
|
||||||
|
|
||||||
const digitChar = hex[i];
|
|
||||||
const digitVal = parseInt(digitChar, 16);
|
|
||||||
const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
|
|
||||||
|
|
||||||
const col = document.createElement("div");
|
|
||||||
col.className = "hex-digit-col";
|
|
||||||
col.innerHTML = `
|
|
||||||
<div class="hex-digit-controls">
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-digit-char hex-font-number">${digitChar}</div>
|
|
||||||
|
|
||||||
<!-- bulbs: brightness changes based on nibble bits -->
|
|
||||||
<div class="hex-bulbs" aria-label="Nibble bits">
|
|
||||||
${[8,4,2,1].map((w, idx) => {
|
|
||||||
const on = nibbleBits[idx] === 1;
|
|
||||||
return `
|
|
||||||
<div class="hex-bulb ${on ? "is-on" : ""}">
|
|
||||||
<div class="hex-bulb-cap"></div>
|
|
||||||
<div class="hex-bulb-glow"></div>
|
|
||||||
<div class="hex-bulb-label">${w}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-digit-place">${placeValue}</div>
|
|
||||||
`;
|
|
||||||
outDigitsRow.appendChild(col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDialog(mode: DialogMode): void {
|
|
||||||
stopRandom();
|
|
||||||
dialogMode = mode;
|
|
||||||
|
|
||||||
dialogError.textContent = "";
|
|
||||||
dialogInput.value = "";
|
|
||||||
|
|
||||||
if (mode === "hex") {
|
|
||||||
dialogTitle.textContent = "Custom Hexadecimal";
|
|
||||||
dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`;
|
|
||||||
dialogInput.placeholder = "A1";
|
|
||||||
dialogInput.inputMode = "text";
|
|
||||||
} else if (mode === "den") {
|
|
||||||
dialogTitle.textContent = "Custom Denary";
|
|
||||||
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
|
|
||||||
dialogInput.placeholder = "42";
|
|
||||||
dialogInput.inputMode = "numeric";
|
|
||||||
} else {
|
|
||||||
dialogTitle.textContent = "Custom Binary";
|
|
||||||
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
|
|
||||||
dialogInput.placeholder = "00101010";
|
|
||||||
dialogInput.inputMode = "text";
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showModal();
|
|
||||||
window.setTimeout(() => dialogInput.focus(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDialog(): void {
|
|
||||||
dialogMode = null;
|
|
||||||
dialogError.textContent = "";
|
|
||||||
if (dialog.open) dialog.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDialog(): void {
|
|
||||||
const raw = (dialogInput.value || "").trim();
|
|
||||||
if (!dialogMode) return closeDialog();
|
|
||||||
if (raw.length === 0) return closeDialog();
|
|
||||||
|
|
||||||
const max = maxForDigits(digits);
|
|
||||||
const bits = digits * 4;
|
|
||||||
|
|
||||||
if (dialogMode === "hex") {
|
|
||||||
const v = raw.toUpperCase();
|
|
||||||
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; }
|
|
||||||
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
|
|
||||||
value = clamp(parseInt(v, 16), 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialogMode === "den") {
|
|
||||||
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
|
|
||||||
const n = Number(raw);
|
|
||||||
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
|
|
||||||
value = clamp(n, 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
// bin
|
|
||||||
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
|
|
||||||
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
|
|
||||||
value = clamp(parseInt(raw, 2), 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDigitDelta(i: number, delta: number): void {
|
|
||||||
stopRandom();
|
|
||||||
const hexArr = padHex(value, digits).split("");
|
|
||||||
let v = parseInt(hexArr[i], 16);
|
|
||||||
v = (v + delta) % 16;
|
|
||||||
if (v < 0) v += 16;
|
|
||||||
hexArr[i] = v.toString(16).toUpperCase();
|
|
||||||
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialog cancel / backdrop
|
|
||||||
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
|
|
||||||
dialog.addEventListener("click", (e) => {
|
|
||||||
const card = dialog.querySelector(".hex-dialog-card");
|
|
||||||
if (card && !card.contains(e.target as Node)) closeDialog();
|
|
||||||
});
|
|
||||||
dialogInput.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter") applyDialog();
|
|
||||||
if (e.key === "Escape") closeDialog();
|
|
||||||
});
|
|
||||||
|
|
||||||
// main click handler
|
|
||||||
root.addEventListener("click", (e) => {
|
|
||||||
const btn = (e.target as HTMLElement).closest<HTMLElement>("[data-action]");
|
|
||||||
if (!btn) return;
|
|
||||||
const action = btn.getAttribute("data-action")!;
|
|
||||||
|
|
||||||
if (action === "toggleToolbox") {
|
|
||||||
toolbox.classList.toggle("is-open");
|
|
||||||
toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
|
||||||
if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
|
||||||
|
|
||||||
if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
|
|
||||||
if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
|
|
||||||
|
|
||||||
if (action === "reset") { stopRandom(); value = 0; return render(); }
|
|
||||||
if (action === "random") { return startRandom(); }
|
|
||||||
|
|
||||||
if (action === "customHex") return openDialog("hex");
|
|
||||||
if (action === "customDenary") return openDialog("den");
|
|
||||||
if (action === "customBinary") return openDialog("bin");
|
|
||||||
|
|
||||||
if (action === "dialogCancel") return closeDialog();
|
|
||||||
if (action === "dialogApply") return applyDialog();
|
|
||||||
|
|
||||||
if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
|
|
||||||
if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
|
|
||||||
});
|
|
||||||
|
|
||||||
render();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<footer class="site-footer">
|
|
||||||
<div class="site-footer__inner">
|
|
||||||
<div class="site-footer__text">
|
|
||||||
COMPUTER SCIENCE CONCEPT SIMULATORS<br />
|
|
||||||
© 2025 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
const nav = [
|
|
||||||
{ href: "/about", label: "About" },
|
|
||||||
{ href: "/binary", label: "Binary" },
|
|
||||||
{ href: "/hexadecimal", label: "Hexadecimal" },
|
|
||||||
{ href: "/hex-colours", label: "Hex Colours" },
|
|
||||||
{ href: "/logic-gates", label: "Logic Gates" },
|
|
||||||
];
|
|
||||||
---
|
|
||||||
<header class="site-header">
|
|
||||||
<div class="site-header__inner">
|
|
||||||
<a class="site-header__brand" href="/" aria-label="Computing:Box home">
|
|
||||||
<img class="site-header__logo" src="/img/logo.png" alt="" width="26" height="26" />
|
|
||||||
<span class="site-header__name">COMPUTING:BOX</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="site-header__nav" aria-label="Primary">
|
|
||||||
{nav.map((i) => (
|
|
||||||
<a class="site-header__link" href={i.href}>
|
|
||||||
{i.label.toUpperCase()}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
4
src/generated/version.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "dev",
|
||||||
|
"url": "#"
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
---
|
---
|
||||||
|
import "../styles/global.css";
|
||||||
|
import versionInfo from "../generated/version.json";
|
||||||
|
|
||||||
|
const version = versionInfo.version;
|
||||||
|
const releaseUrl = versionInfo.url;
|
||||||
const { title = "Computing:Box" } = Astro.props;
|
const { title = "Computing:Box" } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,105 +13,44 @@ const { title = "Computing:Box" } = Astro.props;
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
||||||
<style>
|
<script is:inline>
|
||||||
:root{
|
var _paq = window._paq = window._paq || [];
|
||||||
--nav-h: 108px; /* 3x-ish height */
|
_paq.push(["setCookieDomain", "*.www.computingbox.co.uk"]);
|
||||||
--bg: #1f2027;
|
_paq.push(["setDomains", ["*.www.computingbox.co.uk","*.csbox.mrdaviscsit.uk","*.csbox.mrlyall.co.uk","*.csbox.mrlyall.uk"]]);
|
||||||
--text: #e8e8ee;
|
_paq.push(["enableCrossDomainLinking"]);
|
||||||
--muted: #a9acb8;
|
_paq.push(["disableCookies"]);
|
||||||
--line: rgba(255,255,255,.10);
|
_paq.push(['trackPageView']);
|
||||||
}
|
_paq.push(['enableLinkTracking']);
|
||||||
|
(function() {
|
||||||
body{
|
var u="//analytics.adcmnetworks.co.uk/";
|
||||||
margin:0;
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
background:var(--bg);
|
_paq.push(['setSiteId', '2']);
|
||||||
color:var(--text);
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
}
|
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
.siteNav{
|
</script>
|
||||||
position: sticky;
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
top: 0;
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
z-index: 50;
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet">
|
||||||
height: var(--nav-h);
|
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navInner{
|
|
||||||
height: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand{
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
gap:12px;
|
|
||||||
text-decoration:none;
|
|
||||||
color:var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandLogo{
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandName{
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks{
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
gap:18px;
|
|
||||||
flex-wrap:wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks a{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-size: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks a:hover{
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageWrap{
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header class="siteNav">
|
<header class="siteNav">
|
||||||
<div class="navInner">
|
<div class="navInner">
|
||||||
<a class="brand" href="/">
|
<a class="brand" href="/">
|
||||||
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
|
<img class="brandLogo" src="/images/computing-box-logo-small.webp" alt="Computing:Box logo" />
|
||||||
<span class="brandName">COMPUTING:BOX</span>
|
<span class="brandName">Computing:Box</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="navLinks" aria-label="Site navigation">
|
<nav class="navLinks" aria-label="Site navigation">
|
||||||
<a href="/about">ABOUT</a>
|
<a href="/">Home</a>
|
||||||
<a href="/binary">BINARY</a>
|
<a href="/about">About</a>
|
||||||
<a href="/hexadecimal">HEXADECIMAL</a>
|
<a href="/binary">Binary</a>
|
||||||
<a href="/hex-colours">HEX COLOURS</a>
|
<a href="/hexadecimal">Hexadecimal</a>
|
||||||
<a href="/logic-gates">LOGIC GATES</a>
|
<a href="/hex-colours">Hex Colours</a>
|
||||||
|
<a href="/logic-gates">Logic Gates</a>
|
||||||
|
<a href="/pc-builder">PC Components</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -114,5 +58,93 @@ const { title = "Computing:Box" } = Astro.props;
|
|||||||
<main class="pageWrap">
|
<main class="pageWrap">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="siteFooter">
|
||||||
|
<div class="footerInner">
|
||||||
|
<div style="margin-top: 5px; display: flex; justify-content: center;">
|
||||||
|
<a href="/copyright" style="color: var(--muted); text-decoration: underline;">Copyright Notice</a>
|
||||||
|
<a href="/legal-code" style="margin-left: 32px; color: var(--muted); text-decoration: underline;">Legal Code</a>
|
||||||
|
</div>
|
||||||
|
<div>Computer Science Concept Simulators</div>
|
||||||
|
<div> Version:
|
||||||
|
<a href={releaseUrl} target="_blank" rel="noopener noreferrer">{version}</a> • © {new Date().getFullYear()} Computing:Box • Created with ♥ by Mr A Lyall</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
function setupToolboxAccordions() {
|
||||||
|
// Look for cards inside ANY of the three toolboxes!
|
||||||
|
const cards = document.querySelectorAll('.panelCol .card, .pb-toolbox .card, .lg-toolbox .card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
|
||||||
|
// Your primary cards for each page
|
||||||
|
const primaryCardNames = ['settings', 'info', 'components', 'system diagnostics'];
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const titleEl = card.querySelector('.cardTitle');
|
||||||
|
if (!titleEl) return;
|
||||||
|
|
||||||
|
const titleText = titleEl.textContent.trim().toLowerCase();
|
||||||
|
const isPrimary = primaryCardNames.includes(titleText);
|
||||||
|
|
||||||
|
// 1. DYNAMICALLY WRAP THE CONTENT
|
||||||
|
if (!card.querySelector('.cardBody')) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'cardBody';
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className = 'cardBodyInner';
|
||||||
|
body.appendChild(inner);
|
||||||
|
|
||||||
|
Array.from(card.childNodes).forEach(node => {
|
||||||
|
if (node !== titleEl) {
|
||||||
|
inner.appendChild(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. APPLY DEFAULT STATE
|
||||||
|
if (isPrimary) {
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CLICK LISTENERS
|
||||||
|
if (card.dataset.accordionInit) return;
|
||||||
|
card.dataset.accordionInit = "true";
|
||||||
|
|
||||||
|
titleEl.addEventListener('click', () => {
|
||||||
|
const isCollapsing = !card.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsing) {
|
||||||
|
card.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
if (isPrimary) {
|
||||||
|
cards.forEach(c => {
|
||||||
|
// Only close cards that share the same parent toolbox
|
||||||
|
if (c !== card && c.closest(card.parentElement.tagName) === card.closest(card.parentElement.tagName)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cards.forEach(c => {
|
||||||
|
if (c.closest(card.parentElement.tagName) !== card.closest(card.parentElement.tagName)) return;
|
||||||
|
const cTitle = c.querySelector('.cardTitle')?.textContent.trim().toLowerCase() || '';
|
||||||
|
if (primaryCardNames.includes(cTitle)) {
|
||||||
|
c.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupToolboxAccordions();
|
||||||
|
document.addEventListener('astro:page-load', setupToolboxAccordions);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<title>Astro Basics</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<slot />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
55
src/pages/about.astro
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="About - Computing:Box">
|
||||||
|
<article style="max-width: 1100px; margin: 0 auto; width: 100%;">
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-bottom: 60px;">
|
||||||
|
<img src="/images/computing-box-logo.webp" alt="Computing:Box Logo" style="width: 250px; border-radius: 20px; box-shadow: 0 12px 40px rgba(0,0,0,0.5);" />
|
||||||
|
<h1 class="brandName" style="font-size: 42px; margin-top: 20px; color: var(--text);">The New Computing:Box Experience</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-bottom: 40px;">
|
||||||
|
<p style="color: var(--muted); font-size: 18px; line-height: 1.6; text-align: center;">
|
||||||
|
Rebuilt from the ground up to provide a deeper, more professional simulation environment for the UK Computing Curriculum.
|
||||||
|
</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<ul style="list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> New User Interface (Responsive)</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Two's Complement Simulator</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Extended Binary Simulator (Custom sizes)</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Unified Binary Simulator</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Extended Hexadecimal Simulator</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Unified Hexadecimal Simulator</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Enhanced Gate Simulator (Truth Tables)</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Compound Gate Simulator</li>
|
||||||
|
<li style="color: var(--text);"><span style="color: var(--accent);">✔</span> Computer Components Simulator</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-bottom: 40px; align-items: center;">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="brandName" style="font-size: 24px; color: var(--accent);">From Bit:Box to Computing:Box</h2>
|
||||||
|
<p style="color: var(--muted); line-height: 1.6;">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.</p>
|
||||||
|
</div>
|
||||||
|
<img src="/images/BitBoxLogo.png" alt="Bit:Box Logo" style="width: 100%; border-radius: 16px; border: 1px solid var(--line);" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-bottom: 40px; align-items: center;">
|
||||||
|
<img src="/images/computingbox-concept-illustration.webp" alt="Simulator Illustration" style="width: 100%; border-radius: 16px; border: 1px solid var(--line);" />
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="brandName" style="font-size: 24px; color: var(--accent);">Learning by Doing</h2>
|
||||||
|
<p style="color: var(--muted); line-height: 1.6;">Students can interact with simulations, change values, and see results instantly. This helps them understand how concepts work rather than memorising rules.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center;">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="brandName" style="font-size: 24px; color: var(--accent);">Educational Impact</h2>
|
||||||
|
<p style="color: var(--muted); line-height: 1.6;">Designed for use in lessons, independent study, and revision, helping students build confidence as they explore how computers work.</p>
|
||||||
|
</div>
|
||||||
|
<img src="/images/Educational_Impact.webp" alt="Classroom Impact" style="width: 100%; border-radius: 16px; border: 1px solid var(--line);" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
import "../styles/binary.css";
|
import "../styles/number-simulators.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Binary Simulator">
|
<BaseLayout title="Binary Simulator | Computing:Box">
|
||||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
<div class="binaryPage" id="binaryPage">
|
||||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
<button
|
||||||
<span class="toolboxLabel">TOOLBOX</span>
|
id="toolboxToggle"
|
||||||
</button>
|
class="toolboxToggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||||
|
<span class="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<main class="wrap">
|
|
||||||
<section class="topGrid">
|
<section class="topGrid">
|
||||||
<!-- LEFT -->
|
<div class="leftCol">
|
||||||
<div>
|
|
||||||
<div class="readout">
|
<div class="readout">
|
||||||
<div class="label">Denary</div>
|
<div class="label">Denary</div>
|
||||||
<div id="denaryNumber" class="num denaryValue">0</div>
|
<div id="denaryNumber" class="num denaryValue">0</div>
|
||||||
|
|
||||||
<div class="label">Binary</div>
|
<div class="label">Binary</div>
|
||||||
<div id="binaryNumber" class="num binaryValue">0000 0000</div>
|
<div id="binaryNumber" class="num binaryValue">00000000</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -28,9 +32,7 @@ import "../styles/binary.css";
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT TOOLBOX -->
|
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||||
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
|
|
||||||
<!-- SETTINGS -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Settings</div>
|
<div class="cardTitle">Settings</div>
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ import "../styles/binary.css";
|
|||||||
<input id="modeToggle" type="checkbox" />
|
<input id="modeToggle" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="toggleLabel" id="lblTwos">Two’s complement</div>
|
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint" id="modeHint">
|
<div class="hint" id="modeHint">
|
||||||
@@ -51,7 +53,6 @@ import "../styles/binary.css";
|
|||||||
<div class="subTitle">Bit width</div>
|
<div class="subTitle">Bit width</div>
|
||||||
<div class="bitWidthRow">
|
<div class="bitWidthRow">
|
||||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||||
|
|
||||||
<div class="bitInputWrap">
|
<div class="bitInputWrap">
|
||||||
<div class="bitInputLabel">Bits</div>
|
<div class="bitInputLabel">Bits</div>
|
||||||
<input
|
<input
|
||||||
@@ -66,47 +67,36 @@ import "../styles/binary.css";
|
|||||||
aria-label="Number of bits"
|
aria-label="Number of bits"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CUSTOM -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Custom</div>
|
<div class="cardTitle">Custom Number</div>
|
||||||
|
<div class="controlsRow">
|
||||||
<div class="twoBtnRow">
|
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||||
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
|
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||||
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||||
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
|
||||||
Random
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TOOLS -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Tools</div>
|
<div class="cardTitle">Tools</div>
|
||||||
|
<div class="toolRowCentered">
|
||||||
<div class="toolsTopRow">
|
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||||
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||||
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="controlsRow">
|
||||||
<div class="twoBtnRow">
|
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
||||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
||||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||||
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/binary.js"></script>
|
<script src="../scripts/binary.js"></script>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
28
src/pages/copyright.astro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Copyright Notice | Computing:Box">
|
||||||
|
<div class="card" style="max-width: 800px; margin: 0 auto;">
|
||||||
|
<h1 class="brandName" style="color: var(--accent); margin-bottom: 20px;">Copyright Notice</h1>
|
||||||
|
<a href="https://www.computingbox.co.uk" style="color: var(--accent); text-decoration: none;">Computing:Box</a> © 2024 by <a href="https://www.mrlyall.uk/" style="color: var(--accent); text-decoration: none;">Mr A Lyall</a> is licensed under <strong><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" style="color: var(--accent); text-decoration: none;">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</a></strong><img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;">
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<h2 class="brandName" style="font-size: 20px;">What Does This Mean For You?</h2>
|
||||||
|
<p style="color: var(--muted); font-size: 1.2em;">You are free to:</p>
|
||||||
|
<ul style="color: var(--muted); line-height: 1.6; margin-bottom: 20px;">
|
||||||
|
<li><strong>Share</strong> — copy and redistribute the material in any medium or format.</li>
|
||||||
|
<li><strong>Adapt</strong> — remix, transform, and build upon the material.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="brandName" style="font-size: 20px;">Under the following terms:</h2>
|
||||||
|
<ul style="color: var(--muted); line-height: 1.6;">
|
||||||
|
<li class="cc-by"><strong>Attribution </strong>— You must give <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-appropriate-credit" style="color: var(--accent); margin-bottom: 20px;">appropriate credit</a>, provide a link to the license, and <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-indicate-changes" style="color: var(--accent); margin-bottom: 20px;">indicate if changes were made</a>. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.</li>
|
||||||
|
<li class="cc-nc"><strong>NonCommercial</strong> — You may not use the material for <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-commercial-purposes" style="color: var(--accent); margin-bottom: 20px;">commercial purposes</a>.</li>
|
||||||
|
<li class="cc-sa"><strong>ShareAlike</strong> — If you remix, transform, or build upon the material, you must distribute your contributions under the <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-same-license" style="color: var(--accent); margin-bottom: 20px;">same license</a> as the original.</li>
|
||||||
|
|
||||||
|
<li><strong>No additional restrictions</strong> — You may not apply legal terms or <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-technological-measures" style="color: var(--accent); margin-bottom: 20px;">technological measures</a> that legally restrict others from doing anything the license permits.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
100
src/pages/hex-colours.astro
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
import "../styles/number-simulators.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Hex Colours Simulator | Computing:Box">
|
||||||
|
<div class="binaryPage" id="colorPage">
|
||||||
|
<button
|
||||||
|
id="toolboxToggle"
|
||||||
|
class="toolboxToggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||||
|
<span class="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<section class="topGrid">
|
||||||
|
<div class="leftCol">
|
||||||
|
|
||||||
|
<div class="readoutContainer">
|
||||||
|
<div class="readout">
|
||||||
|
<div class="readoutBlock">
|
||||||
|
<div class="label">Denary (R, G, B)</div>
|
||||||
|
<div id="denaryNumber" class="num denaryValue">
|
||||||
|
<span class="text-red">0</span>
|
||||||
|
<span class="text-green">0</span>
|
||||||
|
<span class="text-blue">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="readoutBlock">
|
||||||
|
<div class="label">Hexadecimal</div>
|
||||||
|
<div id="hexNumber" class="num hexValue">
|
||||||
|
<span class="text-red"><span style="color:var(--muted)">#</span>00</span>
|
||||||
|
<span class="text-green">00</span>
|
||||||
|
<span class="text-blue">00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="readoutBlock">
|
||||||
|
<div class="label">Binary</div>
|
||||||
|
<div id="binaryNumber" class="num binaryValue">
|
||||||
|
<span class="text-red">00000000</span>
|
||||||
|
<span class="text-green">00000000</span>
|
||||||
|
<span class="text-blue">00000000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colorPreviewSide">
|
||||||
|
<div class="colorBoxWrap">
|
||||||
|
<div id="previewColor" class="colorBox"></div>
|
||||||
|
<div class="colorBoxLabel">Colour</div>
|
||||||
|
</div>
|
||||||
|
<div class="colorBoxWrap">
|
||||||
|
<div id="previewInverted" class="colorBox"></div>
|
||||||
|
<div class="colorBoxLabel">Inverted</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section class="bitsWrap" aria-label="Hex Colour Cards">
|
||||||
|
<div class="colorGroupWrap" id="colorGrid"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Info</div>
|
||||||
|
<div class="hint" style="margin-top: 0;">
|
||||||
|
Hexadecimal colours are made of 3 channels (Red, Green, Blue). Each channel is an 8-bit value ranging from 00 to FF (0 to 255).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Colour Tools</div>
|
||||||
|
<div class="controlsRow">
|
||||||
|
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
|
||||||
|
<button class="btn btnAccent btnHalf" id="btnCustomRGB" type="button">Custom RGB</button>
|
||||||
|
</div>
|
||||||
|
<div class="controlsRow">
|
||||||
|
<button class="btn btnAccent btnWide" id="btnInvert" type="button">Invert Colour</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btnWide" id="btnRandom" type="button">Random Colour</button>
|
||||||
|
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Tools</div>
|
||||||
|
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../scripts/hexColours.js"></script>
|
||||||
|
</BaseLayout>
|
||||||
@@ -1,8 +1,95 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
import HexSimulator from "../components/simulators/HexSimulator.astro";
|
import "../styles/number-simulators.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Hexadecimal | Computing:Box">
|
<BaseLayout title="Hexadecimal Simulator | Computing:Box">
|
||||||
<HexSimulator />
|
<div class="binaryPage" id="hexPage">
|
||||||
</BaseLayout>
|
<button
|
||||||
|
id="toolboxToggle"
|
||||||
|
class="toolboxToggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||||
|
<span class="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<section class="topGrid">
|
||||||
|
<div class="leftCol">
|
||||||
|
<div class="readout">
|
||||||
|
<div class="label">Denary</div>
|
||||||
|
<div id="denaryNumber" class="num denaryValue">0</div>
|
||||||
|
|
||||||
|
<div class="label">Hexadecimal</div>
|
||||||
|
<div id="hexNumber" class="num hexValue">00</div>
|
||||||
|
|
||||||
|
<div class="label">Binary</div>
|
||||||
|
<div id="binaryNumber" class="num binaryValue">00000000</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section class="bitsWrap" aria-label="Hexadecimal sliders">
|
||||||
|
<div class="hexGrid" id="hexGrid"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Settings</div>
|
||||||
|
|
||||||
|
<div class="hint" style="margin-top: 0; margin-bottom: 14px;">
|
||||||
|
Hexadecimal represents numbers using base 16 (0-9, A-F).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subCard">
|
||||||
|
<div class="subTitle">Digit width</div>
|
||||||
|
<div class="bitWidthRow">
|
||||||
|
<button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits">−</button>
|
||||||
|
<div class="bitInputWrap">
|
||||||
|
<div class="bitInputLabel">Digits</div>
|
||||||
|
<input
|
||||||
|
id="digitsInput"
|
||||||
|
class="bitInput"
|
||||||
|
type="number"
|
||||||
|
inputmode="numeric"
|
||||||
|
min="1"
|
||||||
|
max="16"
|
||||||
|
step="1"
|
||||||
|
value="2"
|
||||||
|
aria-label="Number of hex digits"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Custom Number</div>
|
||||||
|
<div class="controlsRow">
|
||||||
|
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
|
||||||
|
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||||
|
</div>
|
||||||
|
<div class="controlsRow">
|
||||||
|
<button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||||
|
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Tools</div>
|
||||||
|
<div class="toolRowCentered">
|
||||||
|
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||||
|
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../scripts/hexadecimal.js"></script>
|
||||||
|
</BaseLayout>
|
||||||
@@ -1,11 +1,23 @@
|
|||||||
---
|
---
|
||||||
import Welcome from '../components/Welcome.astro';
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
import Layout from '../layouts/Layout.astro';
|
|
||||||
|
|
||||||
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
|
|
||||||
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<BaseLayout title="Welcome | Computing:Box">
|
||||||
<Welcome />
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 40px; min-height: 60vh; padding: 40px 0;">
|
||||||
</Layout>
|
<div style="flex: 1;">
|
||||||
|
<p style="color: var(--accent); font-weight: 800; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px;">Version 2.0 Now Live</p>
|
||||||
|
<h1 class="brandName" style="font-size: 48px; line-height: 1.1; margin-bottom: 24px;">Understand Computing concepts better.</h1>
|
||||||
|
<p style="font-size: 18px; color: var(--muted);">
|
||||||
|
Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 16px; margin-top: 32px;">
|
||||||
|
<a href="/about" class="btn btnAccent" style="text-decoration: none; padding: 14px 28px;">Learn More</a>
|
||||||
|
<a href="/binary" class="btn" style="text-decoration: none; padding: 14px 28px;">Get Started</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1; text-align: right;">
|
||||||
|
<img src="/images/computing-box-logo.webp" alt="Computing Box Logo" style="width: 100%; max-width: 450px; filter: drop-shadow(0 0 50px rgba(40, 240, 122, 0.15));" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
20
src/pages/legal-code.astro
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Legal Code | Computing:Box">
|
||||||
|
<div class="card" style="max-width: 900px; margin: 0 auto;">
|
||||||
|
<h1 class="brandName" style="color: var(--accent); margin-bottom: 20px;">Legal Code</h1>
|
||||||
|
|
||||||
|
<div style="background: rgba(255, 193, 7, 0.1); border: 1px solid #ffc107; padding: 20px; border-radius: 12px; margin-bottom: 24px;">
|
||||||
|
<h3 style="color: #ffc107; margin-top: 0; font-family: var(--ui-font);">About the license and Creative Commons</h3>
|
||||||
|
<p style="color: #eee; margin-bottom: 0; font-size: 14px;">
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color: var(--muted); font-size: 14px; line-height: 1.6;">
|
||||||
|
By using this licensed material, you accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
55
src/pages/logic-gates.astro
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
import "../styles/logic-gates.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Logic Gate Builder | Computing:Box">
|
||||||
|
<div id="logicPage" class="lg-container">
|
||||||
|
|
||||||
|
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
||||||
|
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||||
|
<span class="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="lg-top-header">
|
||||||
|
<div class="lg-title">Interactive Logic Circuit Builder</div>
|
||||||
|
<div class="lg-subtitle">
|
||||||
|
Drag items from the toolbox. Drag from output ports to input ports to wire. Click a wire/node and press <kbd>Delete</kbd>. Scroll to Zoom.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg-workspace" id="workspace">
|
||||||
|
|
||||||
|
<div class="lg-zoom-controls">
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomIn" title="Zoom In">+</button>
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomOut" title="Zoom Out">−</button>
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomReset" title="Reset View" style="font-size: 16px;">⌂</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg-viewport" id="viewport">
|
||||||
|
<svg class="lg-svg-layer" id="wireLayer"></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="toolboxPanel" class="lg-toolbox" aria-label="Toolbox">
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Components</div>
|
||||||
|
<div class="tb-icon-grid" id="toolboxGrid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Live Truth Table</div>
|
||||||
|
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Auto-generates based on current wiring.</div>
|
||||||
|
<div id="truthTableContainer"> <div class="tt-table-wrap">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Tools</div>
|
||||||
|
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Clear Board</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../scripts/logicGates.js"></script>
|
||||||
|
</BaseLayout>
|
||||||
68
src/pages/pc-builder.astro
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
|
import "../styles/pc-builder.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="PC Part Simulator | Computing:Box">
|
||||||
|
<div id="pcPage" class="pb-container">
|
||||||
|
|
||||||
|
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
||||||
|
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||||
|
<span class="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="pb-top-header">
|
||||||
|
<div class="pb-title">PC Part Simulator</div>
|
||||||
|
<div class="pb-subtitle">
|
||||||
|
Build inside the Case! Snap the Motherboard into the chassis, then populate the slots. Add the side panel when done. <strong>Double-Click</strong> parts to inspect in 3D.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-workspace" id="workspace">
|
||||||
|
<div class="pb-zoom-controls">
|
||||||
|
<button class="pb-zoom-btn" id="btnZoomIn" title="Zoom In">+</button>
|
||||||
|
<button class="pb-zoom-btn" id="btnZoomOut" title="Zoom Out">−</button>
|
||||||
|
<button class="pb-zoom-btn" id="btnZoomReset" title="Reset View" style="font-size: 16px;">⌂</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-viewport" id="viewport">
|
||||||
|
<svg class="pb-svg-layer" id="wireLayer"></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="toolboxPanel" class="pb-toolbox" aria-label="Toolbox">
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">System Diagnostics</div>
|
||||||
|
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Live pre-flight boot analysis.</div>
|
||||||
|
<div class="specs-panel" id="buildSpecsContainer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Inventory</div>
|
||||||
|
<div class="tb-icon-grid" id="toolboxGrid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="cardTitle">Tools</div>
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr; gap: 8px; margin-bottom: 12px;">
|
||||||
|
<button class="btn btnWide" id="btnAssembleHDD" type="button">Assemble (HDD Build)</button>
|
||||||
|
<button class="btn btnWide" id="btnAssembleSATA" type="button">Assemble (SSD Build)</button>
|
||||||
|
<button class="btn btnWide" id="btnAssembleM2" type="button">Assemble (NVMe Build)</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btnReset btnWide" id="btnClearBoard" type="button">Disassemble PC</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="inspectModal" class="inspect-modal">
|
||||||
|
<div class="inspect-close" id="inspectClose">×</div>
|
||||||
|
<div id="inspectName" style="color: white; font-family: var(--ui-font); font-size: 28px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase;"></div>
|
||||||
|
<div class="inspect-stage" id="inspectStage">
|
||||||
|
<div class="inspect-object" id="inspectObject"></div>
|
||||||
|
</div>
|
||||||
|
<div style="color: var(--muted); font-family: var(--ui-font); margin-top: 20px; font-size: 14px;">Move mouse to rotate component. Scroll to zoom.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../scripts/pcBuilder.js"></script>
|
||||||
|
</BaseLayout>
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
// src/scripts/binary.js
|
|
||||||
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
|
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
DOM
|
DOM
|
||||||
@@ -12,6 +9,10 @@
|
|||||||
|
|
||||||
const modeToggle = document.getElementById("modeToggle");
|
const modeToggle = document.getElementById("modeToggle");
|
||||||
const modeHint = document.getElementById("modeHint");
|
const modeHint = document.getElementById("modeHint");
|
||||||
|
|
||||||
|
// Connect the text labels to the JS
|
||||||
|
const lblUnsigned = document.getElementById("lblUnsigned");
|
||||||
|
const lblTwos = document.getElementById("lblTwos");
|
||||||
|
|
||||||
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||||
@@ -27,14 +28,13 @@
|
|||||||
const btnBitsDown = document.getElementById("btnBitsDown");
|
const btnBitsDown = document.getElementById("btnBitsDown");
|
||||||
|
|
||||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||||
const toolboxPanel = document.getElementById("toolboxPanel");
|
const binaryPage = document.getElementById("binaryPage");
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
STATE
|
STATE
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||||
let bits = new Array(bitCount).fill(false);
|
let bits = new Array(bitCount).fill(false);
|
||||||
|
|
||||||
let randomTimer = null;
|
let randomTimer = null;
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unsignedMaxExclusive(nBits) {
|
function unsignedMaxExclusive(nBits) {
|
||||||
return pow2Big(nBits);
|
return pow2Big(nBits);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsignedMaxValue(nBits) {
|
function unsignedMaxValue(nBits) {
|
||||||
@@ -94,7 +94,8 @@
|
|||||||
|
|
||||||
function signedBigIntToBitsTwos(vSigned) {
|
function signedBigIntToBitsTwos(vSigned) {
|
||||||
const span = pow2Big(bitCount);
|
const span = pow2Big(bitCount);
|
||||||
let v = ((vSigned % span) + span) % span;
|
let v = vSigned;
|
||||||
|
v = ((v % span) + span) % span;
|
||||||
unsignedBigIntToBits(v);
|
unsignedBigIntToBits(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,17 +103,33 @@
|
|||||||
let s = "";
|
let s = "";
|
||||||
for (let i = bitCount - 1; i >= 0; i--) {
|
for (let i = bitCount - 1; i >= 0; i--) {
|
||||||
s += bits[i] ? "1" : "0";
|
s += bits[i] ? "1" : "0";
|
||||||
const posFromRight = (bitCount - i);
|
const posFromLeft = (bitCount - i);
|
||||||
if (i !== 0 && posFromRight % 4 === 0) s += " ";
|
if (i !== 0 && posFromLeft % 4 === 0) s += " ";
|
||||||
}
|
}
|
||||||
return s;
|
return s.trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModeHint() {
|
function updateModeHint() {
|
||||||
if (!modeHint) return;
|
if (!modeHint) return;
|
||||||
modeHint.textContent = isTwosMode()
|
if (isTwosMode()) {
|
||||||
? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."
|
modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value.";
|
||||||
: "Tip: In unsigned binary, all bits represent positive values.";
|
} else {
|
||||||
|
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
RESPONSIVE GRID COLS
|
||||||
|
----------------------------- */
|
||||||
|
function computeColsForBitsGrid() {
|
||||||
|
if (!bitsGrid) return;
|
||||||
|
const wrap = bitsGrid.parentElement;
|
||||||
|
if (!wrap) return;
|
||||||
|
|
||||||
|
const width = wrap.getBoundingClientRect().width;
|
||||||
|
const minCell = 100;
|
||||||
|
const cols = clampInt(Math.floor(width / minCell), 1, 12);
|
||||||
|
bitsGrid.style.setProperty("--cols", String(Math.min(cols, bitCount)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@@ -127,18 +144,25 @@
|
|||||||
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
|
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
|
||||||
|
|
||||||
bitsGrid.innerHTML = "";
|
bitsGrid.innerHTML = "";
|
||||||
|
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
|
||||||
|
|
||||||
for (let i = bitCount - 1; i >= 0; i--) {
|
for (let i = bitCount - 1; i >= 0; i--) {
|
||||||
const bitEl = document.createElement("div");
|
const bitEl = document.createElement("div");
|
||||||
bitEl.className = "bit";
|
bitEl.className = "bit";
|
||||||
|
|
||||||
bitEl.innerHTML = `
|
bitEl.innerHTML = `
|
||||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
<div class="bulb" id="bulb-${i}" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
<div class="bitVal" id="bitLabel-${i}"></div>
|
||||||
<label class="switch" aria-label="Toggle bit ${i}">
|
<label class="switch" aria-label="Toggle bit ${i}">
|
||||||
<input type="checkbox" data-index="${i}">
|
<input type="checkbox" data-index="${i}">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
bitsGrid.appendChild(bitEl);
|
bitsGrid.appendChild(bitEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +174,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
computeColsForBitsGrid();
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,14 +186,14 @@
|
|||||||
const label = document.getElementById(`bitLabel-${i}`);
|
const label = document.getElementById(`bitLabel-${i}`);
|
||||||
if (!label) continue;
|
if (!label) continue;
|
||||||
|
|
||||||
// Keep label on ONE LINE (no wrapping)
|
let valStr;
|
||||||
label.style.whiteSpace = "nowrap";
|
|
||||||
|
|
||||||
if (isTwosMode() && i === bitCount - 1) {
|
if (isTwosMode() && i === bitCount - 1) {
|
||||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
valStr = `-${pow2Big(bitCount - 1).toString()}`;
|
||||||
} else {
|
} else {
|
||||||
label.textContent = pow2Big(i).toString();
|
valStr = pow2Big(i).toString();
|
||||||
}
|
}
|
||||||
|
label.textContent = valStr;
|
||||||
|
label.style.setProperty('--len', valStr.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,12 +214,23 @@
|
|||||||
|
|
||||||
function updateReadout() {
|
function updateReadout() {
|
||||||
if (!denaryEl || !binaryEl) return;
|
if (!denaryEl || !binaryEl) return;
|
||||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
if (isTwosMode()) {
|
||||||
|
denaryEl.textContent = bitsToSignedBigIntTwos().toString();
|
||||||
|
} else {
|
||||||
|
denaryEl.textContent = bitsToUnsignedBigInt().toString();
|
||||||
|
}
|
||||||
binaryEl.textContent = formatBinaryGrouped();
|
binaryEl.textContent = formatBinaryGrouped();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
updateModeHint();
|
updateModeHint();
|
||||||
|
|
||||||
|
// Toggle the glowing CSS class on the active mode text
|
||||||
|
if (lblUnsigned && lblTwos) {
|
||||||
|
lblUnsigned.classList.toggle("activeMode", !isTwosMode());
|
||||||
|
lblTwos.classList.toggle("activeMode", isTwosMode());
|
||||||
|
}
|
||||||
|
|
||||||
updateBitLabels();
|
updateBitLabels();
|
||||||
syncSwitchesToBits();
|
syncSwitchesToBits();
|
||||||
updateBulbs();
|
updateBulbs();
|
||||||
@@ -202,20 +238,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
INPUT SETTERS
|
SET FROM BINARY STRING
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setFromBinaryString(binStr) {
|
function setFromBinaryString(binStr) {
|
||||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
||||||
if (!/^[01]+$/.test(clean)) return false;
|
if (!/^[01]+$/.test(clean)) return false;
|
||||||
|
|
||||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||||
for (let i = 0; i < bitCount; i++) {
|
for (let i = 0; i < bitCount; i++) {
|
||||||
const charFromRight = padded[padded.length - 1 - i];
|
const charFromRight = padded[padded.length - 1 - i];
|
||||||
bits[i] = charFromRight === "1";
|
bits[i] = charFromRight === "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
SET FROM DENARY INPUT
|
||||||
|
----------------------------- */
|
||||||
function setFromDenaryInput(vStr) {
|
function setFromDenaryInput(vStr) {
|
||||||
const raw = String(vStr ?? "").trim();
|
const raw = String(vStr ?? "").trim();
|
||||||
if (!raw) return false;
|
if (!raw) return false;
|
||||||
@@ -224,9 +265,7 @@
|
|||||||
try {
|
try {
|
||||||
if (!/^-?\d+$/.test(raw)) return false;
|
if (!/^-?\d+$/.test(raw)) return false;
|
||||||
v = BigInt(raw);
|
v = BigInt(raw);
|
||||||
} catch {
|
} catch { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTwosMode()) {
|
if (isTwosMode()) {
|
||||||
const min = twosMin(bitCount);
|
const min = twosMin(bitCount);
|
||||||
@@ -234,8 +273,7 @@
|
|||||||
if (v < min || v > max) return false;
|
if (v < min || v > max) return false;
|
||||||
signedBigIntToBitsTwos(v);
|
signedBigIntToBitsTwos(v);
|
||||||
} else {
|
} else {
|
||||||
if (v < 0n) return false;
|
if (v < 0n || v > unsignedMaxValue(bitCount)) return false;
|
||||||
if (v > unsignedMaxValue(bitCount)) return false;
|
|
||||||
unsignedBigIntToBits(v);
|
unsignedBigIntToBits(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,18 +285,14 @@
|
|||||||
SHIFTS
|
SHIFTS
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function shiftLeft() {
|
function shiftLeft() {
|
||||||
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
|
for (let i = bitCount - 1; i >= 1; i--) { bits[i] = bits[i - 1]; }
|
||||||
bits[0] = false;
|
bits[0] = false;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function shiftRight() {
|
function shiftRight() {
|
||||||
// Unsigned: logical right shift (MSB becomes 0)
|
|
||||||
// Two's complement: arithmetic right shift (MSB preserved)
|
|
||||||
const msb = bits[bitCount - 1];
|
const msb = bits[bitCount - 1];
|
||||||
|
for (let i = 0; i < bitCount - 1; i++) { bits[i] = bits[i + 1]; }
|
||||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
|
||||||
|
|
||||||
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -267,8 +301,9 @@
|
|||||||
CLEAR / INC / DEC
|
CLEAR / INC / DEC
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
bits.fill(false);
|
bits = [];
|
||||||
updateUI();
|
if (modeToggle) modeToggle.checked = false;
|
||||||
|
buildBits(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
function increment() {
|
function increment() {
|
||||||
@@ -280,8 +315,7 @@
|
|||||||
signedBigIntToBitsTwos(v);
|
signedBigIntToBitsTwos(v);
|
||||||
} else {
|
} else {
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -295,25 +329,22 @@
|
|||||||
signedBigIntToBitsTwos(v);
|
signedBigIntToBitsTwos(v);
|
||||||
} else {
|
} else {
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
RANDOM (with running pulse + longer run)
|
RANDOM
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function cryptoRandomBigInt(maxExclusive) {
|
function cryptoRandomBigInt(maxExclusive) {
|
||||||
if (maxExclusive <= 0n) return 0n;
|
if (maxExclusive <= 0n) return 0n;
|
||||||
|
|
||||||
const bitLen = maxExclusive.toString(2).length;
|
const bitLen = maxExclusive.toString(2).length;
|
||||||
const byteLen = Math.ceil(bitLen / 8);
|
const byteLen = Math.ceil(bitLen / 8);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const bytes = new Uint8Array(byteLen);
|
const bytes = new Uint8Array(byteLen);
|
||||||
crypto.getRandomValues(bytes);
|
crypto.getRandomValues(bytes);
|
||||||
|
|
||||||
let x = 0n;
|
let x = 0n;
|
||||||
for (const b of bytes) x = (x << 8n) | BigInt(b);
|
for (const b of bytes) x = (x << 8n) | BigInt(b);
|
||||||
|
|
||||||
@@ -325,23 +356,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setRandomOnce() {
|
function setRandomOnce() {
|
||||||
const span = unsignedMaxExclusive(bitCount); // 2^n
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const u = cryptoRandomBigInt(span);
|
const u = cryptoRandomBigInt(span);
|
||||||
unsignedBigIntToBits(u);
|
unsignedBigIntToBits(u);
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRandomRunning(isRunning) {
|
||||||
|
if (!btnRandom) return;
|
||||||
|
btnRandom.classList.toggle("btnRandomRunning", !!isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
function runRandomBriefly() {
|
function runRandomBriefly() {
|
||||||
if (randomTimer) {
|
if (randomTimer) {
|
||||||
clearInterval(randomTimer);
|
clearInterval(randomTimer);
|
||||||
randomTimer = null;
|
randomTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pulse while running
|
setRandomRunning(true);
|
||||||
btnRandom?.classList.add("is-running");
|
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const durationMs = 1125; // 25% longer than 900ms
|
const durationMs = 1500;
|
||||||
const tickMs = 80;
|
const tickMs = 80;
|
||||||
|
|
||||||
randomTimer = setInterval(() => {
|
randomTimer = setInterval(() => {
|
||||||
@@ -349,25 +383,27 @@
|
|||||||
if (Date.now() - start >= durationMs) {
|
if (Date.now() - start >= durationMs) {
|
||||||
clearInterval(randomTimer);
|
clearInterval(randomTimer);
|
||||||
randomTimer = null;
|
randomTimer = null;
|
||||||
btnRandom?.classList.remove("is-running");
|
setRandomRunning(false);
|
||||||
}
|
}
|
||||||
}, tickMs);
|
}, tickMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
BIT WIDTH
|
BIT WIDTH CONTROLS
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setBitWidth(n) {
|
function setBitWidth(n) {
|
||||||
buildBits(clampInt(n, 1, 64));
|
const v = clampInt(n, 1, 64);
|
||||||
|
buildBits(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
TOOLBOX VISIBILITY
|
TOOLBOX TOGGLE
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setToolboxVisible(isVisible) {
|
function setToolboxCollapsed(collapsed) {
|
||||||
if (!toolboxPanel) return;
|
if (!binaryPage) return;
|
||||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
binaryPage.classList.toggle("toolboxCollapsed", !!collapsed);
|
||||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
const expanded = !collapsed;
|
||||||
|
toolboxToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@@ -384,8 +420,8 @@
|
|||||||
btnCustomDenary?.addEventListener("click", () => {
|
btnCustomDenary?.addEventListener("click", () => {
|
||||||
const v = prompt(
|
const v = prompt(
|
||||||
isTwosMode()
|
isTwosMode()
|
||||||
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
|
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
||||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
||||||
);
|
);
|
||||||
if (v === null) return;
|
if (v === null) return;
|
||||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
||||||
@@ -406,8 +442,12 @@
|
|||||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||||
|
|
||||||
toolboxToggle?.addEventListener("click", () => {
|
toolboxToggle?.addEventListener("click", () => {
|
||||||
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
const isCollapsed = binaryPage?.classList.contains("toolboxCollapsed");
|
||||||
setToolboxVisible(!isOpen);
|
setToolboxCollapsed(!isCollapsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
computeColsForBitsGrid();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@@ -415,5 +455,5 @@
|
|||||||
----------------------------- */
|
----------------------------- */
|
||||||
updateModeHint();
|
updateModeHint();
|
||||||
buildBits(bitCount);
|
buildBits(bitCount);
|
||||||
setToolboxVisible(true);
|
setToolboxCollapsed(false);
|
||||||
})();
|
})();
|
||||||
241
src/scripts/hexColours.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
// src/scripts/hexColours.js
|
||||||
|
// Computing:Box — Hex Colours logic
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/* -----------------------------
|
||||||
|
DOM
|
||||||
|
----------------------------- */
|
||||||
|
const colorGrid = document.getElementById("colorGrid");
|
||||||
|
const denaryEl = document.getElementById("denaryNumber");
|
||||||
|
const binaryEl = document.getElementById("binaryNumber");
|
||||||
|
const hexEl = document.getElementById("hexNumber");
|
||||||
|
const previewColor = document.getElementById("previewColor");
|
||||||
|
const previewInverted = document.getElementById("previewInverted");
|
||||||
|
|
||||||
|
const btnCustomHex = document.getElementById("btnCustomHex");
|
||||||
|
const btnCustomRGB = document.getElementById("btnCustomRGB");
|
||||||
|
const btnInvert = document.getElementById("btnInvert");
|
||||||
|
const btnRandom = document.getElementById("btnRandom");
|
||||||
|
const btnClear = document.getElementById("btnClear");
|
||||||
|
|
||||||
|
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||||
|
const colorPage = document.getElementById("colorPage");
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
STATE
|
||||||
|
----------------------------- */
|
||||||
|
// rgb[0]=Red, rgb[1]=Green, rgb[2]=Blue (Values 0-255)
|
||||||
|
let rgb = [0, 0, 0];
|
||||||
|
let randomTimer = null;
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
BUILD UI
|
||||||
|
----------------------------- */
|
||||||
|
function buildGrid() {
|
||||||
|
if (!colorGrid) return;
|
||||||
|
colorGrid.innerHTML = "";
|
||||||
|
|
||||||
|
const colorClasses = ['text-red', 'text-green', 'text-blue'];
|
||||||
|
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
const group = document.createElement("div");
|
||||||
|
group.className = "colorGroup";
|
||||||
|
|
||||||
|
for (let i = 1; i >= 0; i--) {
|
||||||
|
const col = document.createElement("div");
|
||||||
|
col.className = "hexCol";
|
||||||
|
|
||||||
|
let cardHTML = `
|
||||||
|
<div class="hexCard">
|
||||||
|
<div class="hexCardButtons">
|
||||||
|
<button class="hexCardBtn inc" id="colorInc-${c}-${i}">▲</button>
|
||||||
|
<button class="hexCardBtn dec" id="colorDec-${c}-${i}">▼</button>
|
||||||
|
</div>
|
||||||
|
<div class="hexDigitDisplay num ${colorClasses[c]}" id="colorDisplay-${c}-${i}">0</div>
|
||||||
|
<div class="hexNibbleRow">
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (let j = 3; j >= 0; j--) {
|
||||||
|
cardHTML += `
|
||||||
|
<div class="hexNibbleBit">
|
||||||
|
<div class="bulb hexNibbleBulb" id="colorBulb-${c}-${i}-${j}" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="hexNibbleLabel">${1 << j}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hexColWeight ${colorClasses[c]}">${16 ** i}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
col.innerHTML = cardHTML;
|
||||||
|
|
||||||
|
const incBtn = col.querySelector(`#colorInc-${c}-${i}`);
|
||||||
|
const decBtn = col.querySelector(`#colorDec-${c}-${i}`);
|
||||||
|
|
||||||
|
incBtn.addEventListener("click", () => {
|
||||||
|
const weight = 16 ** i;
|
||||||
|
rgb[c] = (rgb[c] + weight) % 256;
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
decBtn.addEventListener("click", () => {
|
||||||
|
const weight = 16 ** i;
|
||||||
|
rgb[c] = (rgb[c] - weight + 256) % 256;
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
group.appendChild(col);
|
||||||
|
}
|
||||||
|
colorGrid.appendChild(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
UI UPDATE
|
||||||
|
----------------------------- */
|
||||||
|
function updateUI() {
|
||||||
|
if (denaryEl) {
|
||||||
|
denaryEl.innerHTML = `
|
||||||
|
<span class="text-red">${rgb[0]}</span>
|
||||||
|
<span class="text-green">${rgb[1]}</span>
|
||||||
|
<span class="text-blue">${rgb[2]}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexVals = rgb.map(v => v.toString(16).padStart(2, '0').toUpperCase());
|
||||||
|
const fullHexString = `#${hexVals.join('')}`;
|
||||||
|
|
||||||
|
if (hexEl) {
|
||||||
|
hexEl.innerHTML = `
|
||||||
|
<span class="text-red"><span style="color:var(--muted)">#</span>${hexVals[0]}</span>
|
||||||
|
<span class="text-green">${hexVals[1]}</span>
|
||||||
|
<span class="text-blue">${hexVals[2]}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binaryEl) {
|
||||||
|
binaryEl.innerHTML = `
|
||||||
|
<span class="text-red">${rgb[0].toString(2).padStart(8, '0')}</span>
|
||||||
|
<span class="text-green">${rgb[1].toString(2).padStart(8, '0')}</span>
|
||||||
|
<span class="text-blue">${rgb[2].toString(2).padStart(8, '0')}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewColor) previewColor.style.backgroundColor = fullHexString;
|
||||||
|
|
||||||
|
const invertedHexString = "#" + rgb.map(v => (255 - v).toString(16).padStart(2, '0').toUpperCase()).join('');
|
||||||
|
if (previewInverted) previewInverted.style.backgroundColor = invertedHexString;
|
||||||
|
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
const val = rgb[c];
|
||||||
|
const nibbles = [val % 16, Math.floor(val / 16)];
|
||||||
|
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const display = document.getElementById(`colorDisplay-${c}-${i}`);
|
||||||
|
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
|
||||||
|
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
const bulb = document.getElementById(`colorBulb-${c}-${i}-${j}`);
|
||||||
|
if (bulb) {
|
||||||
|
const isOn = (nibbles[i] & (1 << j)) !== 0;
|
||||||
|
bulb.classList.toggle("on", isOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
ACTIONS
|
||||||
|
----------------------------- */
|
||||||
|
function clearAll() {
|
||||||
|
rgb = [0, 0, 0];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRandomOnce() {
|
||||||
|
const arr = new Uint8Array(3);
|
||||||
|
crypto.getRandomValues(arr);
|
||||||
|
rgb = [arr[0], arr[1], arr[2]];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRandomBriefly() {
|
||||||
|
if (randomTimer) {
|
||||||
|
clearInterval(randomTimer);
|
||||||
|
randomTimer = null;
|
||||||
|
}
|
||||||
|
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
const durationMs = 1125;
|
||||||
|
const tickMs = 80;
|
||||||
|
|
||||||
|
randomTimer = setInterval(() => {
|
||||||
|
setRandomOnce();
|
||||||
|
if (Date.now() - start >= durationMs) {
|
||||||
|
clearInterval(randomTimer);
|
||||||
|
randomTimer = null;
|
||||||
|
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
|
||||||
|
}
|
||||||
|
}, tickMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
EVENTS
|
||||||
|
----------------------------- */
|
||||||
|
btnCustomHex?.addEventListener("click", () => {
|
||||||
|
let v = prompt("Enter a 6-character hex code (e.g. FF0055):");
|
||||||
|
if (v === null) return;
|
||||||
|
v = v.replace(/\s+/g, "").replace(/^#/i, "").toUpperCase();
|
||||||
|
|
||||||
|
if (!/^[0-9A-F]{6}$/.test(v)) return alert("Invalid hex code. Please enter exactly 6 hexadecimal characters.");
|
||||||
|
|
||||||
|
rgb = [
|
||||||
|
parseInt(v.substring(0, 2), 16),
|
||||||
|
parseInt(v.substring(2, 4), 16),
|
||||||
|
parseInt(v.substring(4, 6), 16)
|
||||||
|
];
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCustomRGB?.addEventListener("click", () => {
|
||||||
|
const v = prompt("Enter R, G, B values (0-255) separated by commas (e.g. 255, 128, 0):");
|
||||||
|
if (v === null) return;
|
||||||
|
|
||||||
|
const parts = v.split(',').map(s => parseInt(s.trim(), 10));
|
||||||
|
if (parts.length !== 3 || parts.some(isNaN) || parts.some(n => n < 0 || n > 255)) {
|
||||||
|
return alert("Invalid input. Please provide three numbers between 0 and 255.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb = parts;
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnInvert?.addEventListener("click", () => {
|
||||||
|
rgb = rgb.map(v => 255 - v);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClear?.addEventListener("click", clearAll);
|
||||||
|
btnRandom?.addEventListener("click", runRandomBriefly);
|
||||||
|
|
||||||
|
toolboxToggle?.addEventListener("click", () => {
|
||||||
|
const isCollapsed = colorPage?.classList.contains("toolboxCollapsed");
|
||||||
|
colorPage.classList.toggle("toolboxCollapsed", !isCollapsed);
|
||||||
|
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
INIT
|
||||||
|
----------------------------- */
|
||||||
|
buildGrid();
|
||||||
|
updateUI();
|
||||||
|
})();
|
||||||
312
src/scripts/hexadecimal.js
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
// src/scripts/hexadecimal.js
|
||||||
|
// Computing:Box — Hexadecimal page logic
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/* -----------------------------
|
||||||
|
DOM
|
||||||
|
----------------------------- */
|
||||||
|
const hexGrid = document.getElementById("hexGrid");
|
||||||
|
const denaryEl = document.getElementById("denaryNumber");
|
||||||
|
const binaryEl = document.getElementById("binaryNumber");
|
||||||
|
const hexEl = document.getElementById("hexNumber");
|
||||||
|
const digitsInput = document.getElementById("digitsInput");
|
||||||
|
|
||||||
|
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||||
|
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||||
|
const btnCustomHex = document.getElementById("btnCustomHex");
|
||||||
|
|
||||||
|
const btnDec = document.getElementById("btnDec");
|
||||||
|
const btnInc = document.getElementById("btnInc");
|
||||||
|
const btnClear = document.getElementById("btnClear");
|
||||||
|
const btnRandom = document.getElementById("btnRandom");
|
||||||
|
|
||||||
|
const btnDigitsUp = document.getElementById("btnDigitsUp");
|
||||||
|
const btnDigitsDown = document.getElementById("btnDigitsDown");
|
||||||
|
|
||||||
|
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||||
|
const hexPage = document.getElementById("hexPage");
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
STATE
|
||||||
|
----------------------------- */
|
||||||
|
let hexCount = clampInt(Number(digitsInput?.value ?? 2), 1, 16);
|
||||||
|
let nibbles = new Array(hexCount).fill(0);
|
||||||
|
let randomTimer = null;
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
HELPERS
|
||||||
|
----------------------------- */
|
||||||
|
function clampInt(n, min, max) {
|
||||||
|
if (!Number.isFinite(n)) return min;
|
||||||
|
return Math.max(min, Math.min(max, Math.trunc(n)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxExclusive() {
|
||||||
|
return 1n << BigInt(hexCount * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxValue() {
|
||||||
|
return maxExclusive() - 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue() {
|
||||||
|
let v = 0n;
|
||||||
|
for (let i = 0; i < hexCount; i++) {
|
||||||
|
v += BigInt(nibbles[i]) << BigInt(i * 4);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(v) {
|
||||||
|
if (v < 0n) return false;
|
||||||
|
if (v > maxValue()) return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < hexCount; i++) {
|
||||||
|
nibbles[i] = Number((v >> BigInt(i * 4)) & 0xFn);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
RESPONSIVE GRID
|
||||||
|
----------------------------- */
|
||||||
|
function computeColsForHexGrid() {
|
||||||
|
if (!hexGrid) return;
|
||||||
|
hexGrid.style.setProperty("--cols", String(Math.min(hexCount, 8)));
|
||||||
|
hexGrid.classList.toggle("bitsFew", hexCount < 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
BUILD UI (CARDS + BULBS)
|
||||||
|
----------------------------- */
|
||||||
|
function buildGrid(count) {
|
||||||
|
hexCount = clampInt(count, 1, 16);
|
||||||
|
if (digitsInput) digitsInput.value = String(hexCount);
|
||||||
|
|
||||||
|
const oldNibbles = nibbles.slice();
|
||||||
|
nibbles = new Array(hexCount).fill(0);
|
||||||
|
for (let i = 0; i < Math.min(oldNibbles.length, hexCount); i++) {
|
||||||
|
nibbles[i] = oldNibbles[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
hexGrid.innerHTML = "";
|
||||||
|
|
||||||
|
for (let i = hexCount - 1; i >= 0; i--) {
|
||||||
|
const col = document.createElement("div");
|
||||||
|
col.className = "hexCol";
|
||||||
|
|
||||||
|
let cardHTML = `
|
||||||
|
<div class="hexCard">
|
||||||
|
<div class="hexCardButtons">
|
||||||
|
<button class="hexCardBtn inc" id="hexInc-${i}">▲</button>
|
||||||
|
<button class="hexCardBtn dec" id="hexDec-${i}">▼</button>
|
||||||
|
</div>
|
||||||
|
<div class="hexDigitDisplay num" id="hexDisplay-${i}">0</div>
|
||||||
|
<div class="hexNibbleRow">
|
||||||
|
`;
|
||||||
|
|
||||||
|
for(let j = 3; j >= 0; j--) {
|
||||||
|
cardHTML += `
|
||||||
|
<div class="hexNibbleBit">
|
||||||
|
<div class="bulb hexNibbleBulb" id="hexBulb-${i}-${j}" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="hexNibbleLabel">${1 << j}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hexColWeight">${(1n << BigInt(i * 4)).toString()}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
col.innerHTML = cardHTML;
|
||||||
|
|
||||||
|
const incBtn = col.querySelector(`#hexInc-${i}`);
|
||||||
|
const decBtn = col.querySelector(`#hexDec-${i}`);
|
||||||
|
|
||||||
|
incBtn.addEventListener("click", () => {
|
||||||
|
const span = maxExclusive();
|
||||||
|
const weight = 1n << BigInt(i * 4);
|
||||||
|
setValue((getValue() + weight) % span);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
decBtn.addEventListener("click", () => {
|
||||||
|
const span = maxExclusive();
|
||||||
|
const weight = 1n << BigInt(i * 4);
|
||||||
|
setValue((getValue() - weight + span) % span);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
hexGrid.appendChild(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeColsForHexGrid();
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
UI UPDATE
|
||||||
|
----------------------------- */
|
||||||
|
function updateUI() {
|
||||||
|
const val = getValue();
|
||||||
|
|
||||||
|
if (denaryEl) denaryEl.textContent = val.toString();
|
||||||
|
if (hexEl) hexEl.textContent = val.toString(16).toUpperCase().padStart(hexCount, '0');
|
||||||
|
|
||||||
|
if (binaryEl) {
|
||||||
|
let binStr = "";
|
||||||
|
for (let i = hexCount - 1; i >= 0; i--) {
|
||||||
|
binStr += nibbles[i].toString(2).padStart(4, '0') + " ";
|
||||||
|
}
|
||||||
|
binaryEl.textContent = binStr.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < hexCount; i++) {
|
||||||
|
const display = document.getElementById(`hexDisplay-${i}`);
|
||||||
|
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
|
||||||
|
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
const bulb = document.getElementById(`hexBulb-${i}-${j}`);
|
||||||
|
if (bulb) {
|
||||||
|
const isOn = (nibbles[i] & (1 << j)) !== 0;
|
||||||
|
bulb.classList.toggle("on", isOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
CLEAR / INC / DEC
|
||||||
|
----------------------------- */
|
||||||
|
function clearAll() {
|
||||||
|
nibbles.fill(0);
|
||||||
|
buildGrid(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
const span = maxExclusive();
|
||||||
|
setValue((getValue() + 1n) % span);
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement() {
|
||||||
|
const span = maxExclusive();
|
||||||
|
setValue((getValue() - 1n + span) % span);
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
RANDOM
|
||||||
|
----------------------------- */
|
||||||
|
function cryptoRandomBigInt(maxExcl) {
|
||||||
|
if (maxExcl <= 0n) return 0n;
|
||||||
|
const bitLen = maxExcl.toString(2).length;
|
||||||
|
const byteLen = Math.ceil(bitLen / 8);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const bytes = new Uint8Array(byteLen);
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
let x = 0n;
|
||||||
|
for (const b of bytes) x = (x << 8n) | BigInt(b);
|
||||||
|
|
||||||
|
const extraBits = BigInt(byteLen * 8 - bitLen);
|
||||||
|
if (extraBits > 0n) x = x >> extraBits;
|
||||||
|
if (x < maxExcl) return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRandomOnce() {
|
||||||
|
const u = cryptoRandomBigInt(maxExclusive());
|
||||||
|
setValue(u);
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRandomBriefly() {
|
||||||
|
if (randomTimer) {
|
||||||
|
clearInterval(randomTimer);
|
||||||
|
randomTimer = null;
|
||||||
|
}
|
||||||
|
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
const durationMs = 1125;
|
||||||
|
const tickMs = 80;
|
||||||
|
|
||||||
|
randomTimer = setInterval(() => {
|
||||||
|
setRandomOnce();
|
||||||
|
if (Date.now() - start >= durationMs) {
|
||||||
|
clearInterval(randomTimer);
|
||||||
|
randomTimer = null;
|
||||||
|
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
|
||||||
|
}
|
||||||
|
}, tickMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
EVENTS
|
||||||
|
----------------------------- */
|
||||||
|
btnCustomHex?.addEventListener("click", () => {
|
||||||
|
const v = prompt(`Enter hexadecimal (0-9, A-F). Current width: ${hexCount} digits`);
|
||||||
|
if (v === null) return;
|
||||||
|
const clean = v.replace(/\s+/g, "").toUpperCase();
|
||||||
|
|
||||||
|
if (!/^[0-9A-F]+$/.test(clean)) return alert("Invalid hexadecimal.");
|
||||||
|
if (clean.length > hexCount) return alert("Value too large for current digit width.");
|
||||||
|
|
||||||
|
if (!setValue(BigInt("0x" + clean))) alert("Value out of range.");
|
||||||
|
else updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCustomBinary?.addEventListener("click", () => {
|
||||||
|
const v = prompt(`Enter binary (0, 1). Current width: ${hexCount * 4} bits`);
|
||||||
|
if (v === null) return;
|
||||||
|
const clean = v.replace(/\s+/g, "");
|
||||||
|
|
||||||
|
if (!/^[01]+$/.test(clean)) return alert("Invalid binary.");
|
||||||
|
if (clean.length > hexCount * 4) return alert("Value too large for current digit width.");
|
||||||
|
|
||||||
|
if (!setValue(BigInt("0b" + clean))) alert("Value out of range.");
|
||||||
|
else updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCustomDenary?.addEventListener("click", () => {
|
||||||
|
const v = prompt(`Enter denary (0 to ${maxValue().toString()}):`);
|
||||||
|
if (v === null) return;
|
||||||
|
const clean = v.trim();
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(clean)) return alert("Invalid denary. Digits only.");
|
||||||
|
if (!setValue(BigInt(clean))) alert(`Value out of range. Enter a number between 0 and ${maxValue().toString()}.`);
|
||||||
|
else updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnInc?.addEventListener("click", increment);
|
||||||
|
btnDec?.addEventListener("click", decrement);
|
||||||
|
|
||||||
|
btnClear?.addEventListener("click", clearAll);
|
||||||
|
btnRandom?.addEventListener("click", runRandomBriefly);
|
||||||
|
|
||||||
|
btnDigitsUp?.addEventListener("click", () => buildGrid(hexCount + 1));
|
||||||
|
btnDigitsDown?.addEventListener("click", () => buildGrid(hexCount - 1));
|
||||||
|
|
||||||
|
digitsInput?.addEventListener("change", () => buildGrid(Number(digitsInput.value)));
|
||||||
|
|
||||||
|
toolboxToggle?.addEventListener("click", () => {
|
||||||
|
const isCollapsed = hexPage?.classList.contains("toolboxCollapsed");
|
||||||
|
hexPage.classList.toggle("toolboxCollapsed", !isCollapsed);
|
||||||
|
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", computeColsForHexGrid);
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
INIT
|
||||||
|
----------------------------- */
|
||||||
|
buildGrid(hexCount);
|
||||||
|
|
||||||
|
})();
|
||||||
555
src/scripts/logicGates.js
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
// src/scripts/logicGates.js
|
||||||
|
// Computing:Box — Drag & Drop Logic Builder
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/* --- DOM Elements --- */
|
||||||
|
const workspace = document.getElementById("workspace");
|
||||||
|
const viewport = document.getElementById("viewport");
|
||||||
|
const wireLayer = document.getElementById("wireLayer");
|
||||||
|
const ttContainer = document.getElementById("truthTableContainer");
|
||||||
|
const toolboxGrid = document.getElementById("toolboxGrid");
|
||||||
|
|
||||||
|
const btnClearBoard = document.getElementById("btnClearBoard");
|
||||||
|
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||||
|
const logicPage = document.getElementById("logicPage");
|
||||||
|
|
||||||
|
/* --- ANSI Gate SVGs --- */
|
||||||
|
const GATE_SVGS = {
|
||||||
|
'AND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M70,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/></g>`,
|
||||||
|
'OR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M70,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/></g>`,
|
||||||
|
'NOT': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,25 L30,25 M71,25 L100,25"/><path d="M30,10 L60,25 L30,40 Z" fill="var(--bg)"/><circle cx="65.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||||
|
'NAND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M80,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||||
|
'NOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M80,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||||
|
'XOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M75,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/></g>`,
|
||||||
|
'XNOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M85,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/><circle cx="79.5" cy="25" r="4.5" fill="var(--bg)"/></g>`
|
||||||
|
};
|
||||||
|
|
||||||
|
const INPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
|
||||||
|
const OUTPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
|
||||||
|
|
||||||
|
/* --- State --- */
|
||||||
|
let nodes = {};
|
||||||
|
let connections = [];
|
||||||
|
|
||||||
|
let nextNodeId = 1;
|
||||||
|
let nextWireId = 1;
|
||||||
|
let discoveredStates = new Set();
|
||||||
|
|
||||||
|
// Interaction State
|
||||||
|
let isDraggingNode = null;
|
||||||
|
let dragOffset = { x: 0, y: 0 };
|
||||||
|
let clickStartX = 0, clickStartY = 0;
|
||||||
|
|
||||||
|
let wiringStart = null;
|
||||||
|
let tempWirePath = null;
|
||||||
|
let selectedWireId = null;
|
||||||
|
let selectedNodeId = null;
|
||||||
|
|
||||||
|
// Camera State (Pan & Zoom)
|
||||||
|
let panX = 0, panY = 0, zoom = 1;
|
||||||
|
let isPanning = false;
|
||||||
|
let panStart = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
/* --- Setup Toolbox --- */
|
||||||
|
function initToolbox() {
|
||||||
|
if(!toolboxGrid) return;
|
||||||
|
let html = `
|
||||||
|
<div draggable="true" data-spawn="INPUT" class="drag-item tb-icon-box" title="Input Toggle">
|
||||||
|
<div class="switch" style="pointer-events:none;"><span class="slider"></span></div>
|
||||||
|
<div class="tb-icon-label">Input</div>
|
||||||
|
</div>
|
||||||
|
<div draggable="true" data-spawn="OUTPUT" class="drag-item tb-icon-box" title="Output Bulb">
|
||||||
|
<div class="bulb on" style="pointer-events:none; width:28px; height:28px;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
||||||
|
<div class="tb-icon-label">Output</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
Object.keys(GATE_SVGS).forEach(gate => {
|
||||||
|
html += `
|
||||||
|
<div draggable="true" data-spawn="GATE" data-gate="${gate}" class="drag-item tb-icon-box" title="${gate} Gate">
|
||||||
|
<svg viewBox="0 0 100 50" style="width:50px; height:25px; pointer-events:none;">${GATE_SVGS[gate]}</svg>
|
||||||
|
<div class="tb-icon-label">${gate}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
toolboxGrid.innerHTML = html;
|
||||||
|
|
||||||
|
document.querySelectorAll('.drag-item').forEach(item => {
|
||||||
|
item.addEventListener('dragstart', (e) => {
|
||||||
|
e.dataTransfer.setData('spawnType', item.dataset.spawn);
|
||||||
|
if(item.dataset.spawn === 'GATE') e.dataTransfer.setData('gateType', item.dataset.gate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Camera Math --- */
|
||||||
|
function updateViewport() {
|
||||||
|
viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
|
||||||
|
workspace.style.backgroundSize = `${24 * zoom}px ${24 * zoom}px`;
|
||||||
|
workspace.style.backgroundPosition = `${panX}px ${panY}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomWorkspace(factor, mouseX, mouseY) {
|
||||||
|
const newZoom = Math.min(Math.max(0.2, zoom * factor), 3);
|
||||||
|
panX = mouseX - (mouseX - panX) * (newZoom / zoom);
|
||||||
|
panY = mouseY - (mouseY - panY) * (newZoom / zoom);
|
||||||
|
zoom = newZoom;
|
||||||
|
updateViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPortCoords(nodeId, portDataAttr) {
|
||||||
|
const node = nodes[nodeId];
|
||||||
|
if (!node || !node.el) return {x:0, y:0};
|
||||||
|
|
||||||
|
const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`);
|
||||||
|
if (!portEl) return {x:0, y:0};
|
||||||
|
|
||||||
|
const wsRect = workspace.getBoundingClientRect();
|
||||||
|
const portRect = portEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate backwards through camera scale/pan to find true local coordinates
|
||||||
|
return {
|
||||||
|
x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom,
|
||||||
|
y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBezier(x1, y1, x2, y2) {
|
||||||
|
const cpDist = Math.abs(x2 - x1) * 0.6 + 20;
|
||||||
|
return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Rendering --- */
|
||||||
|
function renderWires() {
|
||||||
|
let svgHTML = '';
|
||||||
|
connections.forEach(conn => {
|
||||||
|
const from = getPortCoords(conn.fromNode, 'out');
|
||||||
|
const to = getPortCoords(conn.toNode, `in${conn.toPort}`);
|
||||||
|
const sourceNode = nodes[conn.fromNode];
|
||||||
|
const isActive = sourceNode && sourceNode.value === true;
|
||||||
|
const isSelected = conn.id === selectedWireId;
|
||||||
|
svgHTML += `<path class="lg-wire ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wiringStart && tempWirePath) {
|
||||||
|
svgHTML += `<path class="lg-wire lg-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||||
|
}
|
||||||
|
wireLayer.innerHTML = svgHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodePositions() {
|
||||||
|
Object.values(nodes).forEach(n => {
|
||||||
|
if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; }
|
||||||
|
});
|
||||||
|
renderWires();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedWireId = null; selectedNodeId = null;
|
||||||
|
document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected'));
|
||||||
|
renderWires();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Logic Evaluation --- */
|
||||||
|
function evaluateGraph(overrideInputs = null) {
|
||||||
|
let context = {};
|
||||||
|
Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => {
|
||||||
|
context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
let changed = true; let loops = 0;
|
||||||
|
while (changed && loops < 10) {
|
||||||
|
changed = false; loops++;
|
||||||
|
Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => {
|
||||||
|
let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1');
|
||||||
|
let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2');
|
||||||
|
let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false;
|
||||||
|
let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false;
|
||||||
|
|
||||||
|
let res = false;
|
||||||
|
switch(gate.gateType) {
|
||||||
|
case 'AND': res = val1 && val2; break;
|
||||||
|
case 'OR': res = val1 || val2; break;
|
||||||
|
case 'NOT': res = !val1; break;
|
||||||
|
case 'NAND': res = !(val1 && val2); break;
|
||||||
|
case 'NOR': res = !(val1 || val2); break;
|
||||||
|
case 'XOR': res = val1 !== val2; break;
|
||||||
|
case 'XNOR': res = val1 === val2; break;
|
||||||
|
}
|
||||||
|
if (context[gate.id] !== res) { context[gate.id] = res; changed = true; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let outStates = {};
|
||||||
|
Object.values(nodes).filter(n => n.type === 'OUTPUT').forEach(out => {
|
||||||
|
let conn = connections.find(c => c.toNode === out.id);
|
||||||
|
let res = conn ? (context[conn.fromNode] || false) : false;
|
||||||
|
outStates[out.id] = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!overrideInputs) {
|
||||||
|
Object.values(nodes).forEach(n => {
|
||||||
|
if (n.type === 'GATE') n.value = context[n.id] || false;
|
||||||
|
if (n.type === 'OUTPUT') {
|
||||||
|
n.value = outStates[n.id] || false;
|
||||||
|
const bulb = n.el.querySelector('.bulb');
|
||||||
|
if (bulb) bulb.classList.toggle('on', n.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return outStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Truth Table Generation --- */
|
||||||
|
function generateTruthTable() {
|
||||||
|
// 1. Find the target container
|
||||||
|
let container = document.getElementById("truthTableContainer");
|
||||||
|
|
||||||
|
// Fail-safe: Find the card if the specific ID is missing
|
||||||
|
if (!container) {
|
||||||
|
const cards = document.querySelectorAll('.card');
|
||||||
|
const ttCard = Array.from(cards).find(c => c.innerText.includes('LIVE TRUTH TABLE'));
|
||||||
|
if (ttCard) {
|
||||||
|
container = ttCard.querySelector('.cardBodyInner') || ttCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 2. Identify and sort Inputs and Outputs
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 3. Handle Empty State
|
||||||
|
if (inNodes.length === 0 || outNodes.length === 0) {
|
||||||
|
container.innerHTML = '<div style="padding: 20px; color: var(--muted); text-align:center; font-family: var(--bit-font); font-size: 12px; letter-spacing: 1px;">CONNECT INPUTS & OUTPUTS</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Build Table within the styled wrapper
|
||||||
|
let html = '<div class="tt-table-wrap"><table class="tt-table"><thead><tr>';
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
inNodes.forEach(n => html += `<th>${n.label}</th>`);
|
||||||
|
outNodes.forEach(n => html += `<th style="color:var(--text); border-left: 1px solid rgba(255,255,255,0.1);">${n.label}</th>`);
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
// 5. Generate Rows
|
||||||
|
const numRows = Math.pow(2, inNodes.length);
|
||||||
|
for (let i = 0; i < numRows; i++) {
|
||||||
|
let override = {};
|
||||||
|
let stateArr = [];
|
||||||
|
|
||||||
|
// Calculate binary state for this row
|
||||||
|
inNodes.forEach((n, idx) => {
|
||||||
|
let val = ((i >> (inNodes.length - 1 - idx)) & 1) === 1;
|
||||||
|
override[n.id] = val;
|
||||||
|
stateArr.push(val ? '1' : '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
let stateStr = stateArr.join('');
|
||||||
|
let isFound = discoveredStates.has(stateStr);
|
||||||
|
let outResults = evaluateGraph(override); // Simulate the board logic for this state
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
|
||||||
|
// Input Cells
|
||||||
|
inNodes.forEach(n => {
|
||||||
|
let v = override[n.id];
|
||||||
|
html += `<td class="${v ? 'tt-on' : ''}">${v ? 1 : 0}</td>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Output Cells (Discovery Logic)
|
||||||
|
outNodes.forEach(n => {
|
||||||
|
if (isFound) {
|
||||||
|
let v = outResults[n.id];
|
||||||
|
html += `<td class="${v ? 'tt-on' : ''}" style="font-weight:bold; border-left: 1px solid rgba(255,255,255,0.05);">${v ? 1 : 0}</td>`;
|
||||||
|
} else {
|
||||||
|
html += `<td style="color: #444; border-left: 1px solid rgba(255,255,255,0.05); opacity: 0.6;">?</td>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSimulation(topologyChanged = false) {
|
||||||
|
// If you add/remove wires, reset the table memory because the logic changed
|
||||||
|
if (topologyChanged) discoveredStates.clear();
|
||||||
|
|
||||||
|
evaluateGraph();
|
||||||
|
|
||||||
|
// Check the current board state (e.g., "10") and save it to memory
|
||||||
|
const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||||
|
if (inNodes.length > 0) {
|
||||||
|
let currentStateStr = inNodes.map(n => n.value ? '1' : '0').join('');
|
||||||
|
discoveredStates.add(currentStateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWires();
|
||||||
|
generateTruthTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Smart Label Generation --- */
|
||||||
|
function getNextInputLabel() {
|
||||||
|
let charCode = 65;
|
||||||
|
while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { charCode++; }
|
||||||
|
return String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextOutputLabel() {
|
||||||
|
let idx = 1;
|
||||||
|
while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { idx++; }
|
||||||
|
return 'Q' + idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Node Creation --- */
|
||||||
|
function createNodeElement(node) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `lg-node`; el.dataset.id = node.id;
|
||||||
|
el.style.left = `${node.x}px`; el.style.top = `${node.y}px`;
|
||||||
|
|
||||||
|
let innerHTML = `<div class="lg-header">${node.label}</div><div class="lg-gate-container">`;
|
||||||
|
|
||||||
|
if (node.type === 'INPUT') {
|
||||||
|
innerHTML += `
|
||||||
|
<div class="switch" style="margin:0;"><span class="slider"></span></div>
|
||||||
|
${INPUT_SVG}
|
||||||
|
<div class="lg-port" data-port="out" style="top: 25px; left: 86px;"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
else if (node.type === 'OUTPUT') {
|
||||||
|
innerHTML += `
|
||||||
|
<div class="lg-port" data-port="in1" style="top: 25px; left: 0;"></div>
|
||||||
|
${OUTPUT_SVG}
|
||||||
|
<div class="bulb" style="margin:0;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
else if (node.type === 'GATE') {
|
||||||
|
const isNot = node.gateType === 'NOT';
|
||||||
|
innerHTML += `
|
||||||
|
<div class="lg-port" data-port="in1" style="top: ${isNot ? '25px' : '15px'}; left: 0;"></div>
|
||||||
|
${!isNot ? `<div class="lg-port" data-port="in2" style="top: 35px; left: 0;"></div>` : ''}
|
||||||
|
<svg class="lg-gate-svg" viewBox="0 0 100 50">${GATE_SVGS[node.gateType]}</svg>
|
||||||
|
<div class="lg-port" data-port="out" style="top: 25px; left: 100px;"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
innerHTML += `</div>`;
|
||||||
|
el.innerHTML = innerHTML;
|
||||||
|
|
||||||
|
viewport.appendChild(el);
|
||||||
|
node.el = el;
|
||||||
|
|
||||||
|
if (node.type === 'INPUT') {
|
||||||
|
const sw = el.querySelector('.switch');
|
||||||
|
sw.addEventListener('click', (e) => {
|
||||||
|
// ... (keep your clickStartX/Y drag check) ...
|
||||||
|
|
||||||
|
node.value = !node.value;
|
||||||
|
|
||||||
|
// This targets the exact class your CSS needs for the glow and move
|
||||||
|
sw.classList.toggle('active-sim', node.value);
|
||||||
|
|
||||||
|
// This ensures the table and logic update
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Double check this line in logicGates.js
|
||||||
|
const id = `node_${Date.now()}_${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);
|
||||||
|
// Change the very last line to:
|
||||||
|
runSimulation(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Change the very last line of the if(wiringStart) block to:
|
||||||
|
wiringStart = null; tempWirePath = null;
|
||||||
|
runSimulation(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* --- 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];
|
||||||
|
// Change the two deletion triggers to:
|
||||||
|
clearSelection(); runSimulation(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* --- 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());
|
||||||
|
|
||||||
|
// Target your specific SVG layer class
|
||||||
|
const svgLayer = document.querySelector('.lg-svg-layer');
|
||||||
|
if (svgLayer) svgLayer.innerHTML = '';
|
||||||
|
|
||||||
|
nodes = {};
|
||||||
|
connections = [];
|
||||||
|
discoveredStates.clear();
|
||||||
|
runSimulation(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
})();
|
||||||
399
src/scripts/pcBuilder.js
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
// 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");
|
||||||
|
|
||||||
|
/* --- ULTRA-REALISTIC COMPONENT LIBRARY --- */
|
||||||
|
const PC_PARTS = {
|
||||||
|
'CASE': {
|
||||||
|
name: 'ATX PC Case', w: 600, h: 550, z: 5, ports: [],
|
||||||
|
slots: {
|
||||||
|
'MB1': { x: 20, y: 20, accepts: 'MB' },
|
||||||
|
'PSU1': { x: 20, y: 440, accepts: 'PSU' },
|
||||||
|
'HDD1': { x: 440, y: 20, accepts: 'HDD' },
|
||||||
|
'HDD2': { x: 440, y: 170, accepts: 'HDD' },
|
||||||
|
'SATA_SSD1': { x: 440, y: 320, accepts: 'SATA_SSD' },
|
||||||
|
'SATA_SSD2': { x: 440, y: 400, accepts: 'SATA_SSD' }
|
||||||
|
},
|
||||||
|
svg: `<rect width="600" height="550" fill="#15171c" rx="10" stroke="#333" stroke-width="4"/><rect x="20" y="20" width="380" height="400" fill="none" stroke="#222" stroke-width="2"/><rect x="20" y="440" width="180" height="90" fill="none" stroke="#222" stroke-width="2"/><rect x="440" y="20" width="140" height="510" fill="none" stroke="#222" stroke-width="2"/>`
|
||||||
|
},
|
||||||
|
'MB': {
|
||||||
|
name: 'Motherboard', w: 360, h: 400, z: 10,
|
||||||
|
ports: [
|
||||||
|
{ id: 'atx_pwr', x: 340, y: 150 }, { id: 'sata1', x: 340, y: 300 }, { id: 'sata2', x: 340, y: 330 },
|
||||||
|
{ id: 'usb1', x: 10, y: 40 }, { id: 'usb2', x: 10, y: 70 }, { id: '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' }
|
||||||
|
},
|
||||||
|
svg: `<rect width="360" height="400" fill="#2C303A" rx="8" stroke="#4b5060" stroke-width="3"/><rect x="120" y="40" width="80" height="80" fill="#1f2229" stroke="#4b5060"/><rect x="230" y="30" width="15" height="100" fill="#1f2229"/><rect x="250" y="30" width="15" height="100" fill="#1f2229"/><rect x="270" y="30" width="15" height="100" fill="#1f2229"/><rect x="290" y="30" width="15" height="100" fill="#1f2229"/><rect x="40" y="200" width="280" height="15" fill="#15171c"/><rect x="40" y="300" width="280" height="15" fill="#15171c"/><rect x="120" y="170" width="80" height="15" fill="#1f2229" stroke="#4b5060" stroke-dasharray="2 2"/><text x="160" y="182" fill="#555" font-size="10" font-family="sans-serif" text-anchor="middle">M.2_1</text><rect x="120" y="250" width="80" height="15" fill="#1f2229" stroke="#4b5060" stroke-dasharray="2 2"/><text x="160" y="262" fill="#555" font-size="10" font-family="sans-serif" text-anchor="middle">M.2_2</text>`
|
||||||
|
},
|
||||||
|
'CPU': {
|
||||||
|
name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {},
|
||||||
|
svg: `<rect width="80" height="80" fill="#0c4a22" rx="4"/><rect x="2" y="2" width="76" height="76" fill="none" stroke="#ffd700" stroke-width="1" stroke-dasharray="2 4"/><rect x="12" y="12" width="56" height="56" fill="#e0e4e8" rx="6" stroke="#b0b5b9" stroke-width="2"/><text x="40" y="35" fill="#666" font-family="sans-serif" font-size="10" font-weight="900" text-anchor="middle">INTEL</text><text x="40" y="50" fill="#555" font-family="sans-serif" font-size="16" font-weight="900" text-anchor="middle">CORE i9</text><text x="40" y="60" fill="#777" font-family="sans-serif" font-size="7" font-weight="bold" text-anchor="middle">14900K</text><polygon points="5,75 15,75 5,65" fill="#ffd700"/>`
|
||||||
|
},
|
||||||
|
'COOLER': {
|
||||||
|
name: 'Liquid AIO', w: 120, h: 120, z: 30, ports: [], slots: {},
|
||||||
|
svg: `<circle cx="60" cy="60" r="55" fill="#15171e" stroke="#2d313d" stroke-width="4"/><circle cx="60" cy="60" r="45" fill="#050505"/><text x="60" y="55" fill="#28f07a" font-family="var(--num-font)" font-size="20" font-weight="bold" text-anchor="middle">32°C</text><text x="60" y="75" fill="#55aaff" font-family="var(--ui-font)" font-size="10" text-anchor="middle">2400 RPM</text><path d="M 110 40 Q 140 40 140 10 M 110 80 Q 150 80 150 110" fill="none" stroke="#111" stroke-width="12" stroke-linecap="round"/><circle cx="60" cy="60" r="50" fill="none" stroke="cyan" stroke-width="2" opacity="0.8"/>`
|
||||||
|
},
|
||||||
|
'RAM': {
|
||||||
|
name: 'RGB Memory', w: 15, h: 100, z: 20, ports: [], slots: {},
|
||||||
|
svg: `<rect width="15" height="100" fill="#111" rx="2"/><rect x="0" y="90" width="15" height="10" fill="#ffd700"/><rect x="0" y="94" width="15" height="1" fill="#b8860b"/><path d="M -2 15 L 17 15 L 17 85 L -2 85 Z" fill="#2d313d" stroke="#111"/><path d="M 0 20 L 15 30 L 15 80 L 0 70 Z" fill="#1a1c23"/><path d="M -2 2 L 17 2 L 17 15 L -2 15 Z" fill="#ff0055"/><path d="M 0 2 L 5 10 L 10 2 L 15 10" fill="none" stroke="#fff" stroke-width="1" opacity="0.5"/>`
|
||||||
|
},
|
||||||
|
'GPU': {
|
||||||
|
name: 'Graphics Card', w: 280, h: 80, z: 40, slots: {}, ports: [{ id: 'pwr_in', x: 270, y: 10 }, { id: 'disp_out', x: 10, y: 40 }],
|
||||||
|
svg: `<rect width="280" height="80" rx="8" fill="#15171e" stroke="#333742" stroke-width="2"/><rect x="5" y="5" width="270" height="70" rx="6" fill="#0f1015"/><path d="M 20 5 L 60 75 M 110 5 L 150 75 M 200 5 L 240 75" stroke="#1a1c23" stroke-width="4"/><g transform="translate(50, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><g transform="translate(140, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><g transform="translate(230, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><rect x="20" y="80" width="160" height="8" fill="#ffd700" rx="2"/><rect x="100" y="32" width="80" height="16" fill="#000" rx="2" opacity="0.8"/><text x="140" y="43" fill="#28f07a" font-family="sans-serif" font-size="10" font-weight="900" text-anchor="middle">GEFORCE RTX</text>`
|
||||||
|
},
|
||||||
|
'M2_SSD': {
|
||||||
|
name: 'M.2 NVMe SSD', w: 80, h: 22, z: 20, ports: [], slots: {},
|
||||||
|
svg: `<rect width="80" height="22" fill="#111" rx="2"/><rect x="0" y="0" width="5" height="22" fill="#ffd700"/><rect x="3" y="14" width="3" height="4" fill="#111"/><rect x="15" y="4" width="18" height="14" fill="#1a1c23" rx="1"/><rect x="38" y="4" width="18" height="14" fill="#1a1c23" rx="1"/><rect x="60" y="6" width="10" height="10" fill="#2d313d" rx="1"/><rect x="10" y="8" width="50" height="6" fill="#fff" opacity="0.8"/><text x="35" y="13" fill="#000" font-family="sans-serif" font-size="4" font-weight="bold" text-anchor="middle">990 PRO 2TB</text><circle cx="76" cy="11" r="3" fill="#222"/>`
|
||||||
|
},
|
||||||
|
'SATA_SSD': {
|
||||||
|
name: '2.5" SATA SSD', w: 100, h: 70, z: 20, slots: {}, ports: [{id:'data', x:90, y:20}, {id:'pwr', x:90, y:50}],
|
||||||
|
svg: `<rect width="100" height="70" fill="#1a1c23" rx="4" stroke="#4b5162" stroke-width="1"/><rect x="2" y="2" width="96" height="66" fill="#2d313d" rx="2"/><rect x="15" y="15" width="70" height="40" fill="#111" rx="2"/><rect x="15" y="45" width="70" height="10" fill="#e74c3c"/><text x="50" y="35" fill="#fff" font-family="sans-serif" font-size="14" font-weight="900" text-anchor="middle" letter-spacing="1px">SAMSUNG</text><circle cx="5" cy="5" r="1.5" fill="#111"/><circle cx="95" cy="5" r="1.5" fill="#111"/><circle cx="5" cy="65" r="1.5" fill="#111"/><circle cx="95" cy="65" r="1.5" fill="#111"/>`
|
||||||
|
},
|
||||||
|
'HDD': {
|
||||||
|
name: '3.5" Mech HDD', w: 120, h: 140, z: 20, slots: {}, ports: [{id:'data', x:110, y:20}, {id:'pwr', x:110, y:120}],
|
||||||
|
svg: `<rect width="120" height="140" fill="#bdc3c7" rx="4" stroke="#7f8c8d" stroke-width="2"/><path d="M 5 5 L 115 5 L 115 110 C 80 120, 40 120, 5 110 Z" fill="#e0e4e8" stroke="#95a5a6" stroke-width="1"/><circle cx="60" cy="55" r="45" fill="none" stroke="#bdc3c7" stroke-width="2"/><circle cx="60" cy="55" r="12" fill="#bdc3c7" stroke="#95a5a6"/><circle cx="100" cy="100" r="8" fill="#bdc3c7" stroke="#95a5a6"/><path d="M 100 100 L 70 60" stroke="#7f8c8d" stroke-width="6" stroke-linecap="round"/><rect x="30" y="80" width="60" height="30" fill="#fff" rx="2"/><text x="60" y="92" fill="#000" font-family="sans-serif" font-size="8" font-weight="bold" text-anchor="middle">WD BLACK</text><text x="60" y="102" fill="#333" font-family="sans-serif" font-size="6" text-anchor="middle">12TB HDD</text><rect x="20" y="120" width="80" height="15" fill="#0b3d21" rx="2"/>`
|
||||||
|
},
|
||||||
|
'PSU': {
|
||||||
|
name: 'Power Supply', w: 160, h: 90, z: 20, slots: {}, ports: [{id:'out1',x:150,y:20}, {id:'out2',x:150,y:40}, {id:'out3',x:150,y:60}, {id:'out4',x:150,y:80}],
|
||||||
|
svg: `<rect width="160" height="90" fill="#15171e" rx="4" stroke="#333742" stroke-width="2"/><rect x="40" y="5" width="80" height="80" fill="#0a0a0a" rx="40"/><circle cx="80" cy="45" r="38" fill="none" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="28" fill="none" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="18" fill="none" stroke="#2d313d" stroke-width="2"/><path d="M 80 5 L 80 85 M 40 45 L 120 45 M 52 20 L 108 70 M 108 20 L 52 70" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="8" fill="#111" stroke="#e74c3c"/><rect x="145" y="10" width="15" height="70" fill="#0a0a0a"/><rect x="5" y="15" width="25" height="60" fill="#333742" rx="2"/><text x="17" y="45" fill="#fff" font-family="sans-serif" font-size="10" font-weight="bold" text-anchor="middle" transform="rotate(-90 17,45)">1200W</text>`
|
||||||
|
},
|
||||||
|
'MONITOR': {
|
||||||
|
name: 'Monitor', w: 240, h: 180, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}],
|
||||||
|
svg: `<rect width="240" height="160" fill="#1a1a1a" rx="6" stroke="#333"/><rect x="8" y="8" width="224" height="124" fill="#000" id="screen-bg"/><g id="boot-content"></g><rect x="6" y="140" width="228" height="15" fill="#1a1c23"/><text x="120" y="150" fill="#fff" font-family="sans-serif" font-size="6" text-anchor="middle">ASUS</text><circle cx="220" cy="147" r="2" fill="#28f07a"/><path d="M 100 160 L 110 180 L 130 180 L 140 160 Z" fill="#222"/><rect x="80" y="180" width="80" height="5" fill="#333" rx="2"/>`
|
||||||
|
},
|
||||||
|
'KEYBOARD': {
|
||||||
|
name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}],
|
||||||
|
svg: `<rect width="180" height="60" fill="#15171e" rx="4" stroke="#2d313d" stroke-width="2"/><rect x="0" y="45" width="180" height="15" fill="#111" rx="2"/><rect x="5" y="5" width="170" height="36" fill="#0a0a0a" rx="2"/><g fill="#222" stroke="#111" stroke-width="1"><rect x="8" y="8" width="10" height="10" rx="2"/><rect x="20" y="8" width="10" height="10" rx="2"/><rect x="32" y="8" width="10" height="10" rx="2"/><rect x="44" y="8" width="10" height="10" rx="2"/><rect x="56" y="8" width="10" height="10" rx="2"/><rect x="68" y="8" width="10" height="10" rx="2"/><rect x="80" y="8" width="10" height="10" rx="2"/><rect x="92" y="8" width="10" height="10" rx="2"/><rect x="104" y="8" width="10" height="10" rx="2"/><rect x="116" y="8" width="10" height="10" rx="2"/><rect x="128" y="8" width="10" height="10" rx="2"/><rect x="140" y="8" width="18" height="10" rx="2"/><rect x="8" y="20" width="14" height="10" rx="2"/><rect x="24" y="20" width="10" height="10" rx="2"/><rect x="36" y="20" width="10" height="10" rx="2"/><rect x="48" y="20" width="10" height="10" rx="2"/><rect x="60" y="20" width="10" height="10" rx="2"/><rect x="72" y="20" width="10" height="10" rx="2"/><rect x="84" y="20" width="10" height="10" rx="2"/><rect x="96" y="20" width="10" height="10" rx="2"/><rect x="108" y="20" width="10" height="10" rx="2"/><rect x="120" y="20" width="10" height="10" rx="2"/><rect x="132" y="20" width="26" height="10" rx="2"/></g><rect x="56" y="32" width="60" height="10" fill="#222" stroke="#111" rx="2"/><rect x="4" y="4" width="172" height="38" fill="none" stroke="cyan" stroke-width="1" opacity="0.3"/>`
|
||||||
|
},
|
||||||
|
'MOUSE': {
|
||||||
|
name: 'Mouse', w: 30, h: 54, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}],
|
||||||
|
svg: `<rect width="30" height="54" fill="#15171e" rx="15" stroke="#2d313d" stroke-width="2"/><path d="M 15 0 L 15 20 M 5 25 Q 15 30 25 25" stroke="#0a0a0a" stroke-width="2" fill="none"/><rect x="13" y="6" width="4" height="10" fill="#111" rx="2"/><rect x="14" y="7" width="2" height="8" fill="#28f07a"/><path d="M 10 45 Q 15 50 20 45" stroke="cyan" stroke-width="2" fill="none" opacity="0.8"/><path d="M 0 15 Q 4 25 0 35 M 30 15 Q 26 25 30 35" stroke="#111" stroke-width="2" fill="none"/>`
|
||||||
|
},
|
||||||
|
'SPEAKER': {
|
||||||
|
name: 'Speakers', w: 46, h: 90, z: 30, slots: {}, ports: [{id:'audio', x:23, y:10}],
|
||||||
|
svg: `<rect width="46" height="90" fill="#1a1c23" rx="4" stroke="#333742" stroke-width="2"/><rect x="4" y="4" width="38" height="82" fill="#111" rx="2"/><circle cx="23" cy="22" r="10" fill="#2d313d" stroke="#0a0a0a" stroke-width="2"/><circle cx="23" cy="22" r="4" fill="#15171e"/><circle cx="23" cy="58" r="16" fill="#2d313d" stroke="#0a0a0a" stroke-width="3"/><circle cx="23" cy="58" r="6" fill="#15171e"/><circle cx="23" cy="80" r="4" fill="#000"/>`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 }, isSystemBooted = false;
|
||||||
|
|
||||||
|
/* --- Toolbox & Base Init --- */
|
||||||
|
function initToolbox() {
|
||||||
|
if(!toolboxGrid) return;
|
||||||
|
let html = '';
|
||||||
|
Object.keys(PC_PARTS).forEach(partKey => {
|
||||||
|
html += `<div draggable="true" data-spawn="${partKey}" class="drag-item tb-icon-box" title="${PC_PARTS[partKey].name}">
|
||||||
|
<svg viewBox="0 0 ${PC_PARTS[partKey].w} ${PC_PARTS[partKey].h}" style="max-width:80%; max-height:40px; pointer-events:none;">${PC_PARTS[partKey].svg}</svg>
|
||||||
|
<div class="tb-icon-label">${partKey}</div></div>`;
|
||||||
|
});
|
||||||
|
toolboxGrid.innerHTML = html;
|
||||||
|
document.querySelectorAll('.drag-item').forEach(item => { item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); }); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Viewport 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);
|
||||||
|
svgHTML += `<path class="pb-wire active ${conn.id === selectedWireId ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||||
|
});
|
||||||
|
if (wiringStart && tempWirePath) svgHTML += `<path class="pb-wire pb-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||||
|
wireLayer.innerHTML = svgHTML;
|
||||||
|
}
|
||||||
|
function updateNodePositions() { Object.values(nodes).forEach(n => { if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; } }); renderWires(); }
|
||||||
|
function clearSelection() { selectedWireId = null; selectedNodeId = null; document.querySelectorAll('.pb-node.selected').forEach(el => el.classList.remove('selected')); renderWires(); }
|
||||||
|
|
||||||
|
/* --- Node Logic --- */
|
||||||
|
function createNodeElement(node) {
|
||||||
|
const el = document.createElement('div'); el.className = `pb-node`; el.dataset.id = node.id;
|
||||||
|
el.style.left = `${node.x}px`; el.style.top = `${node.y}px`;
|
||||||
|
el.style.width = `${PC_PARTS[node.type].w}px`; el.style.height = `${PC_PARTS[node.type].h}px`; el.style.zIndex = PC_PARTS[node.type].z;
|
||||||
|
let innerHTML = `<svg class="pb-part-svg" viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}">${PC_PARTS[node.type].svg}</svg>`;
|
||||||
|
PC_PARTS[node.type].ports.forEach(p => { innerHTML += `<div class="pb-port" data-port="${p.id}" style="left: ${p.x}px; top: ${p.y}px;"></div>`; });
|
||||||
|
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 }; for(let k in node.slots) { node.slots[k] = null; } }
|
||||||
|
nodes[id] = node; createNodeElement(node); evaluateBuild(); return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SYSTEM DIAGNOSTICS & VARIABLE BOOT SPEED --- */
|
||||||
|
function evaluateBuild() {
|
||||||
|
if(!specsContainer) return;
|
||||||
|
let hasCase=false, hasMB=false, hasCPU=false, hasCooler=false, hasRAM=false, hasPSU=false, hasStorage=false, hasGPU=false;
|
||||||
|
let mbPwr=false, gpuPwr=false, storPwr=false, storData=false, dispConn=false, usbCount=0;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
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; storPwr = true; storData = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
connections.forEach(c => {
|
||||||
|
let n1 = nodes[c.fromNode], n2 = nodes[c.toNode]; if(!n1 || !n2) return;
|
||||||
|
let types = [n1.type, n2.type], ports = [c.fromPort, c.toPort];
|
||||||
|
|
||||||
|
if(types.includes('MB') && types.includes('PSU')) mbPwr = true;
|
||||||
|
if(types.includes('GPU') && types.includes('PSU')) gpuPwr = true;
|
||||||
|
if(types.includes('PSU') && (types.includes('HDD') || types.includes('SATA_SSD')) && ports.includes('pwr')) storPwr = true;
|
||||||
|
if(types.includes('MB') && (types.includes('HDD') || types.includes('SATA_SSD')) && ports.includes('data')) storData = true;
|
||||||
|
if(types.includes('MB') && ['KEYBOARD','MOUSE'].some(t => types.includes(t))) usbCount++;
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Determine the Boot Speed based on the connected drive
|
||||||
|
let bootSpeed = 8000; // Default slow HDD
|
||||||
|
let activeDrive = 'HDD';
|
||||||
|
if (mbNode && (mbNode.slots['M2_1'] || mbNode.slots['M2_2'])) {
|
||||||
|
activeDrive = 'M2_SSD';
|
||||||
|
} else {
|
||||||
|
Object.values(nodes).forEach(n => {
|
||||||
|
if ((n.type === 'SATA_SSD' || n.type === 'HDD') && n.snappedTo) activeDrive = n.type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeDrive === 'M2_SSD') bootSpeed = 1500;
|
||||||
|
else if (activeDrive === 'SATA_SSD') bootSpeed = 3500;
|
||||||
|
|
||||||
|
// Auto-Trigger the Boot Animation
|
||||||
|
if (isBootable && !isSystemBooted) { isSystemBooted = true; triggerBootSequence(bootSpeed); }
|
||||||
|
else if (!isBootable) { isSystemBooted = false; resetMonitor(); }
|
||||||
|
|
||||||
|
specsContainer.innerHTML = `
|
||||||
|
<div class="diag-cat">CORE SYSTEM</div>
|
||||||
|
<div class="diag-row"><span>CHASSIS</span><span style="color: ${hasCase ? '#28f07a' : '#ff5555'}">${hasCase ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>MOTHERBOARD</span><span style="color: ${hasMB ? '#28f07a' : '#ff5555'}">${hasMB ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>CPU</span><span style="color: ${hasCPU ? '#28f07a' : '#ff5555'}">${hasCPU ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>COOLING</span><span style="color: ${hasCooler ? '#28f07a' : '#ff5555'}">${hasCooler ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>MEMORY</span><span style="color: ${hasRAM ? '#28f07a' : '#ff5555'}">${hasRAM ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>POWER SPLY</span><span style="color: ${hasPSU ? '#28f07a' : '#ff5555'}">${hasPSU ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-cat">CONNECTIONS</div>
|
||||||
|
<div class="diag-row"><span>MB POWER</span><span style="color: ${mbPwr ? '#28f07a' : '#ff5555'}">${mbPwr ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>STORAGE</span><span style="color: ${(hasStorage && storPwr && storData) ? '#28f07a' : '#ff5555'}">${(hasStorage && storPwr && storData) ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>GPU POWER</span><span style="color: ${!hasGPU ? '#888' : (gpuPwr ? '#28f07a' : '#ff5555')}">${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}</span></div>
|
||||||
|
<div class="diag-row"><span>DISPLAY</span><span style="color: ${dispConn ? '#28f07a' : '#ff5555'}">${dispConn ? 'OK' : 'ERR'}</span></div>
|
||||||
|
<div class="diag-row"><span>USB DEVS</span><span style="color: #55aaff">${usbCount}</span></div>
|
||||||
|
<hr style="border-color: rgba(255,255,255,0.1); margin: 12px 0 8px 0;">
|
||||||
|
<div style="text-align:center; font-size: 28px; color: ${isBootable ? '#28f07a' : '#ff5555'}; font-family: var(--bit-font); letter-spacing: 2px;">${isBootable ? 'BOOTING...' : 'HALTED'}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerBootSequence(duration) {
|
||||||
|
const monitor = Object.values(nodes).find(n => n.type === 'MONITOR'); if (!monitor) return;
|
||||||
|
const bootContent = monitor.el.querySelector('#boot-content');
|
||||||
|
const durSeconds = (duration / 1000).toFixed(1);
|
||||||
|
|
||||||
|
bootContent.innerHTML = `<text x="120" y="70" fill="white" font-family="sans-serif" font-size="12" text-anchor="middle">Starting Windows</text><rect x="85" y="85" width="0" height="4" fill="#28f07a" rx="2"><animate attributeName="width" from="0" to="70" dur="${durSeconds}s" fill="freeze" /></rect>`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
bootContent.innerHTML = `<image href="/Microsoft_Nostalgic_Windows_Wallpaper_4k.jpg" x="10" y="10" width="220" height="120" preserveAspectRatio="xMidYMid slice" />`;
|
||||||
|
}, duration + 300); // Small buffer to let the bar finish
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMonitor() { const monitor = Object.values(nodes).find(n => n.type === 'MONITOR'); if (monitor) monitor.el.querySelector('#boot-content').innerHTML = ''; }
|
||||||
|
|
||||||
|
|
||||||
|
/* --- INTERACTION (Drag, Drop, Snap, Wire) --- */
|
||||||
|
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 };
|
||||||
|
|
||||||
|
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; 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 & Toolbox UI --- */
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* --- Auto-Assemble Engine --- */
|
||||||
|
function autoAssemble(sT) {
|
||||||
|
btnClearBoard.click();
|
||||||
|
const mId = spawnNode('MONITOR', 200, 100), kId = spawnNode('KEYBOARD', 230, 320), moId = spawnNode('MOUSE', 450, 330), spId = spawnNode('SPEAKER', 150, 300);
|
||||||
|
const cId = spawnNode('CASE', 550, 100), mbId = spawnNode('MB', 1250, 250), pId = spawnNode('PSU', 1250, 100), cpId = spawnNode('CPU', 1450, 100), coId = spawnNode('COOLER', 1450, 250), rId = spawnNode('RAM', 1600, 100), gId = spawnNode('GPU', 1450, 400), stId = spawnNode(sT, 1600, 250);
|
||||||
|
|
||||||
|
const plan = [{c:mbId,p:cId,s:'MB1'},{c:pId,p:cId,s:'PSU1'},{c:cpId,p:mbId,s:'CPU1'},{c:coId,p:mbId,s:'COOLER1'},{c:rId,p:mbId,s:'RAM1'},{c:gId,p:mbId,s:'PCIE1'}];
|
||||||
|
if(sT==='HDD') plan.push({c:stId,p:cId,s:'HDD1'}); if(sT==='SATA_SSD') plan.push({c:stId,p:cId,s:'SATA_SSD1'}); if(sT==='M2_SSD') plan.push({c:stId,p:mbId,s:'M2_1'});
|
||||||
|
|
||||||
|
plan.forEach(s => { const ch = nodes[s.c], p = nodes[s.p]; const sD = PC_PARTS[p.type].slots[s.s]; moveNodeRecursive(ch.id, (p.x + sD.x) - ch.x, (p.y + sD.y) - ch.y); ch.snappedTo = { id: p.id, key: s.s }; p.slots[s.s] = ch.id; ch.el.style.zIndex = PC_PARTS[p.type].z + 5; });
|
||||||
|
|
||||||
|
const conn = (n1, p1, n2, p2) => connections.push({ id: `conn_${nextWireId++}`, fromNode: n1, fromPort: p1, toNode: n2, toPort: p2 });
|
||||||
|
conn(pId, 'out1', mbId, 'atx_pwr'); conn(pId, 'out2', gId, 'pwr_in');
|
||||||
|
if (sT !== 'M2_SSD') { conn(pId, 'out3', stId, 'pwr'); conn(mbId, 'sata1', stId, 'data'); }
|
||||||
|
conn(gId, 'disp_out', mId, 'disp'); conn(mbId, 'usb1', kId, 'usb'); conn(mbId, 'usb2', moId, 'usb'); conn(mbId, 'audio', spId, 'audio');
|
||||||
|
|
||||||
|
updateNodePositions();
|
||||||
|
evaluateBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnAssembleHDD')?.addEventListener('click', () => autoAssemble('HDD'));
|
||||||
|
document.getElementById('btnAssembleSATA')?.addEventListener('click', () => autoAssemble('SATA_SSD'));
|
||||||
|
document.getElementById('btnAssembleM2')?.addEventListener('click', () => autoAssemble('M2_SSD'));
|
||||||
|
|
||||||
|
initToolbox(); evaluateBuild();
|
||||||
|
})();
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,29 +0,0 @@
|
|||||||
<footer class="siteFooter">
|
|
||||||
<div class="inner">
|
|
||||||
<div class="title">Computer Science Concept Simulators</div>
|
|
||||||
<div class="meta">
|
|
||||||
© 2025 Computing:Box · Created with 💗 by Mr Lyall<br />
|
|
||||||
Powered by ADCM Networks
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.siteFooter{
|
|
||||||
border-top: 1px solid rgba(255,255,255,.10);
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
}
|
|
||||||
.inner{
|
|
||||||
max-width:1200px;
|
|
||||||
margin:0 auto;
|
|
||||||
padding: 18px 20px;
|
|
||||||
color: rgba(255,255,255,.65);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.title{
|
|
||||||
color: rgba(255,255,255,.80);
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<header class="siteHeader">
|
|
||||||
<div class="inner">
|
|
||||||
<div class="brand">
|
|
||||||
<a href="/">Computing:Box</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="nav">
|
|
||||||
<a href="/binary">Binary</a>
|
|
||||||
<a href="/hexadecimal">Hexadecimal</a>
|
|
||||||
<a href="/hex-colours">Hex Colours</a>
|
|
||||||
<a href="/logic-gates">Logic Gates</a>
|
|
||||||
<a href="/about">About</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.siteHeader{
|
|
||||||
height: 64px;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,.10);
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
.inner{
|
|
||||||
max-width: 1200px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
.brand a{
|
|
||||||
color:#fff;
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .02em;
|
|
||||||
}
|
|
||||||
.nav{
|
|
||||||
display:flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.nav a{
|
|
||||||
color: rgba(255,255,255,.78);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing:.02em;
|
|
||||||
}
|
|
||||||
.nav a:hover{
|
|
||||||
color:#fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
---
|
|
||||||
import astroLogo from '../assets/astro.svg';
|
|
||||||
import background from '../assets/background.svg';
|
|
||||||
---
|
|
||||||
|
|
||||||
<div id="container">
|
|
||||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
|
||||||
<main>
|
|
||||||
<section id="hero">
|
|
||||||
<a href="https://astro.build"
|
|
||||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
|
||||||
</h1>
|
|
||||||
<section id="links">
|
|
||||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
|
||||||
<a href="https://astro.build/chat"
|
|
||||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
|
|
||||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
|
||||||
fill="#111827"></path></svg
|
|
||||||
>
|
|
||||||
<h2>What's New in Astro 5.0?</h2>
|
|
||||||
<p>
|
|
||||||
From content layers to server islands, click to learn more about the new features and
|
|
||||||
improvements in Astro 5.0
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#background {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
filter: blur(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#container {
|
|
||||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hero {
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
color: #111827;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a:hover {
|
|
||||||
color: rgb(78, 80, 86);
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a svg {
|
|
||||||
height: 1em;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button {
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
|
||||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button:hover {
|
|
||||||
color: rgb(230, 230, 230);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
|
||||||
monospace;
|
|
||||||
font-weight: normal;
|
|
||||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0 0 1em;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #4b5563;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
letter-spacing: -0.006em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
display: inline-block;
|
|
||||||
background:
|
|
||||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
|
||||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
padding: 16px;
|
|
||||||
background: rgba(255, 255, 255, 1);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#news {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
max-width: 300px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 0.2s;
|
|
||||||
backdrop-filter: blur(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#news:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-height: 368px) {
|
|
||||||
#news {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
#container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hero {
|
|
||||||
display: block;
|
|
||||||
padding-top: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#links a.button {
|
|
||||||
padding: 14px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#news {
|
|
||||||
right: 16px;
|
|
||||||
left: 16px;
|
|
||||||
bottom: 2.5rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
import "./hex/hex-simulator.css";
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="hex-sim" data-hex-sim>
|
|
||||||
<div class="hex-main">
|
|
||||||
<div class="hex-readout">
|
|
||||||
<div class="hex-label">DENARY</div>
|
|
||||||
<div class="hex-number" data-out="denary">0</div>
|
|
||||||
|
|
||||||
<div class="hex-label hex-mt">HEXADECIMAL</div>
|
|
||||||
<div class="hex-number hex-number--small" data-out="hex">00</div>
|
|
||||||
|
|
||||||
<div class="hex-label hex-mt">BINARY</div>
|
|
||||||
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-divider"></div>
|
|
||||||
|
|
||||||
<div class="hex-digits" data-out="digitsRow"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toolbox button -->
|
|
||||||
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
|
|
||||||
<span class="hex-toolbox-icon" aria-hidden="true">
|
|
||||||
<!-- toolbox icon -->
|
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
|
|
||||||
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
||||||
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
TOOLBOX
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Toolbox panel -->
|
|
||||||
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">SETTINGS</div>
|
|
||||||
|
|
||||||
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
|
|
||||||
|
|
||||||
<div class="hex-width">
|
|
||||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus">−</button>
|
|
||||||
|
|
||||||
<div class="hex-width-readout">
|
|
||||||
<div class="hex-width-label">DIGITS</div>
|
|
||||||
<div class="hex-width-number" data-out="digitsCount">2</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">CUSTOM NUMBER</div>
|
|
||||||
|
|
||||||
<div class="hex-grid-2">
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Binary + Random on SAME row, same size -->
|
|
||||||
<div class="hex-grid-2 hex-mt-sm">
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
|
|
||||||
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-panel">
|
|
||||||
<div class="hex-panel-title">TOOLS</div>
|
|
||||||
|
|
||||||
<div class="hex-tools-top">
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Custom number dialog -->
|
|
||||||
<dialog class="hex-dialog" data-out="dialog">
|
|
||||||
<div class="hex-dialog-card">
|
|
||||||
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
|
|
||||||
|
|
||||||
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
|
|
||||||
|
|
||||||
<div class="hex-dialog-hint" data-out="dialogHint"></div>
|
|
||||||
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
|
|
||||||
|
|
||||||
<div class="hex-dialog-actions">
|
|
||||||
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
|
|
||||||
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
|
|
||||||
</section>
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
/* ================= Fonts to match Binary ================= */
|
|
||||||
/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
|
|
||||||
@font-face {
|
|
||||||
font-family: "DSEG7Classic";
|
|
||||||
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
|
||||||
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SevenSegment";
|
|
||||||
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
|
|
||||||
url("/fonts/Seven-Segment.woff") format("woff");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-sim {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #14151c;
|
|
||||||
color: #e7e8ee;
|
|
||||||
padding: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
|
|
||||||
.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
|
|
||||||
|
|
||||||
.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
|
|
||||||
|
|
||||||
.hex-readout { text-align: center; }
|
|
||||||
.hex-label {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.hex-mt { margin-top: 12px; }
|
|
||||||
|
|
||||||
.hex-number {
|
|
||||||
font-family: "DSEG7Classic", ui-monospace, monospace;
|
|
||||||
font-size: 76px;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #46ff8a;
|
|
||||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
|
||||||
}
|
|
||||||
.hex-number--small { font-size: 64px; }
|
|
||||||
.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
|
|
||||||
|
|
||||||
.hex-divider {
|
|
||||||
margin: 26px auto 18px;
|
|
||||||
height: 1px;
|
|
||||||
width: min(760px, 90%);
|
|
||||||
background: rgba(255,255,255,0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Main digit columns ================= */
|
|
||||||
.hex-digits {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 18px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-col {
|
|
||||||
width: 160px;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 12px;
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-controls {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-char {
|
|
||||||
font-size: 64px;
|
|
||||||
line-height: 1;
|
|
||||||
color: #46ff8a;
|
|
||||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-digit-place {
|
|
||||||
font-family: "SevenSegment", ui-monospace, monospace;
|
|
||||||
opacity: 0.65;
|
|
||||||
font-size: 14px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Bulbs (brightness changes) ================= */
|
|
||||||
.hex-bulbs {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb {
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
opacity: 0.35;
|
|
||||||
filter: grayscale(30%);
|
|
||||||
transition: opacity 160ms ease, filter 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-cap {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255,255,255,0.22);
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-glow {
|
|
||||||
width: 18px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(70,255,138,0.0);
|
|
||||||
box-shadow: 0 0 0 rgba(70,255,138,0.0);
|
|
||||||
transition: background 160ms ease, box-shadow 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb .hex-bulb-label {
|
|
||||||
font-family: "SevenSegment", ui-monospace, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-bulb.is-on {
|
|
||||||
opacity: 1;
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
.hex-bulb.is-on .hex-bulb-cap {
|
|
||||||
background: rgba(255,255,255,0.35);
|
|
||||||
}
|
|
||||||
.hex-bulb.is-on .hex-bulb-glow {
|
|
||||||
background: rgba(70,255,138,0.25);
|
|
||||||
box-shadow: 0 0 18px rgba(70,255,138,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Buttons (toolbox style reused everywhere) ================= */
|
|
||||||
.hex-btn {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
.hex-btn:hover { background: rgba(255,255,255,0.10); }
|
|
||||||
|
|
||||||
.hex-btn--square {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-btn--wide { width: 100%; }
|
|
||||||
|
|
||||||
.hex-btn--green {
|
|
||||||
background: rgba(46, 200, 120, 0.18);
|
|
||||||
border-color: rgba(46,200,120,0.35);
|
|
||||||
}
|
|
||||||
.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
|
|
||||||
|
|
||||||
.hex-btn--green2 {
|
|
||||||
background: rgba(46, 200, 120, 0.18);
|
|
||||||
border-color: rgba(46,200,120,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-btn--red {
|
|
||||||
background: rgba(220, 60, 70, 0.18);
|
|
||||||
border-color: rgba(220,60,70,0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Random = green pulse while running */
|
|
||||||
.hex-btn--random.is-running {
|
|
||||||
border-color: rgba(80, 255, 160, 0.55);
|
|
||||||
background: rgba(46, 200, 120, 0.22);
|
|
||||||
box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
|
|
||||||
animation: hexPulseGreen 900ms ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes hexPulseGreen {
|
|
||||||
0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
|
|
||||||
50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset = red background + pulse on hover */
|
|
||||||
.hex-btn--reset:hover {
|
|
||||||
background: rgba(220, 60, 70, 0.28);
|
|
||||||
border-color: rgba(255, 80, 90, 0.55);
|
|
||||||
animation: hexPulseRed 900ms ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes hexPulseRed {
|
|
||||||
0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
|
|
||||||
50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= Toolbox button + panel (slide) ================= */
|
|
||||||
.hex-toolbox-btn {
|
|
||||||
position: fixed;
|
|
||||||
top: 88px;
|
|
||||||
right: 28px;
|
|
||||||
z-index: 30;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-toolbox-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: #ff4fa6;
|
|
||||||
filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-toolbox {
|
|
||||||
position: fixed;
|
|
||||||
top: 140px;
|
|
||||||
right: 28px;
|
|
||||||
width: 340px;
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
z-index: 25;
|
|
||||||
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
transition: transform 220ms ease, opacity 220ms ease;
|
|
||||||
}
|
|
||||||
.hex-toolbox:not(.is-open) {
|
|
||||||
transform: translateX(380px);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-panel {
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255,255,255,0.04);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
.hex-panel-title {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
|
|
||||||
|
|
||||||
.hex-width {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 48px 1fr 48px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.hex-width-readout {
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(0,0,0,0.22);
|
|
||||||
border: 1px solid rgba(255,255,255,0.10);
|
|
||||||
padding: 10px 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
.hex-width-label {
|
|
||||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
|
||||||
opacity: 0.7;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
|
|
||||||
|
|
||||||
.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
||||||
.hex-mt-sm { margin-top: 10px; }
|
|
||||||
|
|
||||||
.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
|
|
||||||
.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
/* ================= Dialog ================= */
|
|
||||||
.hex-dialog { border: none; padding: 0; background: transparent; }
|
|
||||||
.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
|
|
||||||
|
|
||||||
.hex-dialog-card {
|
|
||||||
width: min(560px, 92vw);
|
|
||||||
border-radius: 18px;
|
|
||||||
background: #1a1b24;
|
|
||||||
border: 1px solid rgba(255,255,255,0.12);
|
|
||||||
padding: 16px;
|
|
||||||
color: #e7e8ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
|
|
||||||
.hex-dialog-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
background: rgba(0,0,0,0.25);
|
|
||||||
color: #e7e8ee;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
|
||||||
.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.hex-toolbox { width: min(360px, 92vw); right: 16px; }
|
|
||||||
.hex-toolbox-btn { right: 16px; }
|
|
||||||
.hex-number { font-size: 60px; }
|
|
||||||
.hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
type DialogMode = "hex" | "den" | "bin";
|
|
||||||
|
|
||||||
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
|
|
||||||
if (!root) throw new Error("Hex simulator root not found");
|
|
||||||
|
|
||||||
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
|
|
||||||
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
|
|
||||||
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
|
|
||||||
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
|
|
||||||
|
|
||||||
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
|
|
||||||
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
|
|
||||||
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
|
|
||||||
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
|
|
||||||
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
|
|
||||||
|
|
||||||
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
|
|
||||||
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
|
|
||||||
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
|
|
||||||
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
|
|
||||||
const dialogError = root.querySelector<HTMLElement>('[data-out="dialogError"]')!;
|
|
||||||
|
|
||||||
let digits = 2; // 1..8
|
|
||||||
let value = 0; // unsigned denary
|
|
||||||
let randomTimer: number | null = null;
|
|
||||||
let dialogMode: DialogMode | null = null;
|
|
||||||
|
|
||||||
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
|
|
||||||
const maxForDigits = (d: number) => (16 ** d) - 1;
|
|
||||||
|
|
||||||
const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
|
|
||||||
const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
|
|
||||||
const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
|
|
||||||
|
|
||||||
function stopRandom(): void {
|
|
||||||
if (randomTimer !== null) window.clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
randomBtn.classList.remove("is-running");
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRandom(): void {
|
|
||||||
stopRandom();
|
|
||||||
const max = maxForDigits(digits);
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
randomBtn.classList.add("is-running");
|
|
||||||
|
|
||||||
randomTimer = window.setInterval(() => {
|
|
||||||
value = Math.floor(Math.random() * (max + 1));
|
|
||||||
render();
|
|
||||||
if (Date.now() - start > 1600) stopRandom();
|
|
||||||
}, 90);
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(): void {
|
|
||||||
const bits = digits * 4;
|
|
||||||
|
|
||||||
digitsCount.textContent = String(digits);
|
|
||||||
bitsHint.textContent = `= ${bits} bits`;
|
|
||||||
|
|
||||||
outDen.textContent = String(value);
|
|
||||||
outHex.textContent = padHex(value, digits);
|
|
||||||
outBin.textContent = groupBin(padBin(value, bits));
|
|
||||||
|
|
||||||
renderDigitsRow();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDigitsRow(): void {
|
|
||||||
const hex = padHex(value, digits);
|
|
||||||
outDigitsRow.innerHTML = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < digits; i++) {
|
|
||||||
const pow = digits - 1 - i;
|
|
||||||
const placeValue = 16 ** pow;
|
|
||||||
|
|
||||||
const digitChar = hex[i];
|
|
||||||
const digitVal = parseInt(digitChar, 16);
|
|
||||||
const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
|
|
||||||
|
|
||||||
const col = document.createElement("div");
|
|
||||||
col.className = "hex-digit-col";
|
|
||||||
col.innerHTML = `
|
|
||||||
<div class="hex-digit-controls">
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
|
|
||||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-digit-char hex-font-number">${digitChar}</div>
|
|
||||||
|
|
||||||
<!-- bulbs: brightness changes based on nibble bits -->
|
|
||||||
<div class="hex-bulbs" aria-label="Nibble bits">
|
|
||||||
${[8,4,2,1].map((w, idx) => {
|
|
||||||
const on = nibbleBits[idx] === 1;
|
|
||||||
return `
|
|
||||||
<div class="hex-bulb ${on ? "is-on" : ""}">
|
|
||||||
<div class="hex-bulb-cap"></div>
|
|
||||||
<div class="hex-bulb-glow"></div>
|
|
||||||
<div class="hex-bulb-label">${w}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hex-digit-place">${placeValue}</div>
|
|
||||||
`;
|
|
||||||
outDigitsRow.appendChild(col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDialog(mode: DialogMode): void {
|
|
||||||
stopRandom();
|
|
||||||
dialogMode = mode;
|
|
||||||
|
|
||||||
dialogError.textContent = "";
|
|
||||||
dialogInput.value = "";
|
|
||||||
|
|
||||||
if (mode === "hex") {
|
|
||||||
dialogTitle.textContent = "Custom Hexadecimal";
|
|
||||||
dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`;
|
|
||||||
dialogInput.placeholder = "A1";
|
|
||||||
dialogInput.inputMode = "text";
|
|
||||||
} else if (mode === "den") {
|
|
||||||
dialogTitle.textContent = "Custom Denary";
|
|
||||||
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
|
|
||||||
dialogInput.placeholder = "42";
|
|
||||||
dialogInput.inputMode = "numeric";
|
|
||||||
} else {
|
|
||||||
dialogTitle.textContent = "Custom Binary";
|
|
||||||
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
|
|
||||||
dialogInput.placeholder = "00101010";
|
|
||||||
dialogInput.inputMode = "text";
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showModal();
|
|
||||||
window.setTimeout(() => dialogInput.focus(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDialog(): void {
|
|
||||||
dialogMode = null;
|
|
||||||
dialogError.textContent = "";
|
|
||||||
if (dialog.open) dialog.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDialog(): void {
|
|
||||||
const raw = (dialogInput.value || "").trim();
|
|
||||||
if (!dialogMode) return closeDialog();
|
|
||||||
if (raw.length === 0) return closeDialog();
|
|
||||||
|
|
||||||
const max = maxForDigits(digits);
|
|
||||||
const bits = digits * 4;
|
|
||||||
|
|
||||||
if (dialogMode === "hex") {
|
|
||||||
const v = raw.toUpperCase();
|
|
||||||
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; }
|
|
||||||
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
|
|
||||||
value = clamp(parseInt(v, 16), 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialogMode === "den") {
|
|
||||||
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
|
|
||||||
const n = Number(raw);
|
|
||||||
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
|
|
||||||
value = clamp(n, 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
// bin
|
|
||||||
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
|
|
||||||
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
|
|
||||||
value = clamp(parseInt(raw, 2), 0, max);
|
|
||||||
render();
|
|
||||||
return closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDigitDelta(i: number, delta: number): void {
|
|
||||||
stopRandom();
|
|
||||||
const hexArr = padHex(value, digits).split("");
|
|
||||||
let v = parseInt(hexArr[i], 16);
|
|
||||||
v = (v + delta) % 16;
|
|
||||||
if (v < 0) v += 16;
|
|
||||||
hexArr[i] = v.toString(16).toUpperCase();
|
|
||||||
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialog cancel / backdrop
|
|
||||||
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
|
|
||||||
dialog.addEventListener("click", (e) => {
|
|
||||||
const card = dialog.querySelector(".hex-dialog-card");
|
|
||||||
if (card && !card.contains(e.target as Node)) closeDialog();
|
|
||||||
});
|
|
||||||
dialogInput.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter") applyDialog();
|
|
||||||
if (e.key === "Escape") closeDialog();
|
|
||||||
});
|
|
||||||
|
|
||||||
// main click handler
|
|
||||||
root.addEventListener("click", (e) => {
|
|
||||||
const btn = (e.target as HTMLElement).closest<HTMLElement>("[data-action]");
|
|
||||||
if (!btn) return;
|
|
||||||
const action = btn.getAttribute("data-action")!;
|
|
||||||
|
|
||||||
if (action === "toggleToolbox") {
|
|
||||||
toolbox.classList.toggle("is-open");
|
|
||||||
toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
|
||||||
if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
|
||||||
|
|
||||||
if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
|
|
||||||
if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
|
|
||||||
|
|
||||||
if (action === "reset") { stopRandom(); value = 0; return render(); }
|
|
||||||
if (action === "random") { return startRandom(); }
|
|
||||||
|
|
||||||
if (action === "customHex") return openDialog("hex");
|
|
||||||
if (action === "customDenary") return openDialog("den");
|
|
||||||
if (action === "customBinary") return openDialog("bin");
|
|
||||||
|
|
||||||
if (action === "dialogCancel") return closeDialog();
|
|
||||||
if (action === "dialogApply") return applyDialog();
|
|
||||||
|
|
||||||
if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
|
|
||||||
if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
|
|
||||||
});
|
|
||||||
|
|
||||||
render();
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
---
|
|
||||||
const { title = "Computing:Box" } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<title>{title}</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root{
|
|
||||||
--nav-h: 108px; /* 3x-ish height */
|
|
||||||
--bg: #1f2027;
|
|
||||||
--text: #e8e8ee;
|
|
||||||
--muted: #a9acb8;
|
|
||||||
--line: rgba(255,255,255,.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin:0;
|
|
||||||
background:var(--bg);
|
|
||||||
color:var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteNav{
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 50;
|
|
||||||
height: var(--nav-h);
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navInner{
|
|
||||||
height: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand{
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
gap:12px;
|
|
||||||
text-decoration:none;
|
|
||||||
color:var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandLogo{
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandName{
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks{
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
gap:18px;
|
|
||||||
flex-wrap:wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks a{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-size: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navLinks a:hover{
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageWrap{
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="siteNav">
|
|
||||||
<div class="navInner">
|
|
||||||
<a class="brand" href="/">
|
|
||||||
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
|
|
||||||
<span class="brandName">COMPUTING:BOX</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="navLinks" aria-label="Site navigation">
|
|
||||||
<a href="/about">ABOUT</a>
|
|
||||||
<a href="/binary">BINARY</a>
|
|
||||||
<a href="/hexadecimal">HEXADECIMAL</a>
|
|
||||||
<a href="/hex-colours">HEX COLOURS</a>
|
|
||||||
<a href="/logic-gates">LOGIC GATES</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="pageWrap">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<title>Astro Basics</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<slot />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
||||||
import "../styles/binary.css";
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title="Binary Simulator">
|
|
||||||
<main class="wrap">
|
|
||||||
<!-- Toolbox toggle sits below navbar (navbar is in BaseLayout) -->
|
|
||||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
|
||||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
|
||||||
<span class="toolboxText">TOOLBOX</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<section class="topGrid">
|
|
||||||
<!-- LEFT -->
|
|
||||||
<div class="mainCol">
|
|
||||||
<div class="readout">
|
|
||||||
<div class="label">Denary</div>
|
|
||||||
<div id="denaryNumber" class="num denaryValue">0</div>
|
|
||||||
|
|
||||||
<div class="label">Binary</div>
|
|
||||||
<!-- NOTE: JS writes exact bit-width here, so initial value doesn't matter much -->
|
|
||||||
<div id="binaryNumber" class="num binaryValue">00000000</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<section class="bitsWrap" aria-label="Bit switches">
|
|
||||||
<div class="bitsGrid" id="bitsGrid"></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT (Toolbox panel) -->
|
|
||||||
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
|
||||||
<!-- Settings -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="cardTitle">Settings</div>
|
|
||||||
|
|
||||||
<div class="toggleRow">
|
|
||||||
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
|
||||||
|
|
||||||
<label class="switch" aria-label="Toggle mode">
|
|
||||||
<input id="modeToggle" type="checkbox" />
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- keep this on ONE line -->
|
|
||||||
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hint" id="modeHint">
|
|
||||||
Tip: In unsigned binary, all bits represent positive values.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subCard">
|
|
||||||
<div class="subTitle">Bit width</div>
|
|
||||||
|
|
||||||
<div class="bitWidthRow">
|
|
||||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
|
||||||
|
|
||||||
<div class="bitInputWrap">
|
|
||||||
<div class="bitInputLabel">Bits</div>
|
|
||||||
<input
|
|
||||||
id="bitsInput"
|
|
||||||
class="bitInput"
|
|
||||||
type="number"
|
|
||||||
inputmode="numeric"
|
|
||||||
min="1"
|
|
||||||
max="64"
|
|
||||||
step="1"
|
|
||||||
value="8"
|
|
||||||
aria-label="Number of bits"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Number -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="cardTitle">Custom Number</div>
|
|
||||||
|
|
||||||
<div class="controlsRow">
|
|
||||||
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
|
||||||
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
|
||||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tools -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="cardTitle">Tools</div>
|
|
||||||
|
|
||||||
<div class="toolRowCentered">
|
|
||||||
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
|
||||||
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolRow2">
|
|
||||||
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
|
||||||
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/binary.js"></script>
|
|
||||||
</BaseLayout>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
||||||
import HexSimulator from "../components/simulators/HexSimulator.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title="Hexadecimal | Computing:Box">
|
|
||||||
<HexSimulator />
|
|
||||||
</BaseLayout>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
import Welcome from '../components/Welcome.astro';
|
|
||||||
import Layout from '../layouts/Layout.astro';
|
|
||||||
|
|
||||||
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
|
|
||||||
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Welcome />
|
|
||||||
</Layout>
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
// src/scripts/binary.js
|
|
||||||
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
/* -----------------------------
|
|
||||||
DOM
|
|
||||||
----------------------------- */
|
|
||||||
const bitsGrid = document.getElementById("bitsGrid");
|
|
||||||
const denaryEl = document.getElementById("denaryNumber");
|
|
||||||
const binaryEl = document.getElementById("binaryNumber");
|
|
||||||
const bitsInput = document.getElementById("bitsInput");
|
|
||||||
|
|
||||||
const modeToggle = document.getElementById("modeToggle");
|
|
||||||
const modeHint = document.getElementById("modeHint");
|
|
||||||
|
|
||||||
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
|
||||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
|
||||||
const btnShiftLeft = document.getElementById("btnShiftLeft");
|
|
||||||
const btnShiftRight = document.getElementById("btnShiftRight");
|
|
||||||
|
|
||||||
const btnDec = document.getElementById("btnDec");
|
|
||||||
const btnInc = document.getElementById("btnInc");
|
|
||||||
const btnClear = document.getElementById("btnClear");
|
|
||||||
const btnRandom = document.getElementById("btnRandom");
|
|
||||||
|
|
||||||
const btnBitsUp = document.getElementById("btnBitsUp");
|
|
||||||
const btnBitsDown = document.getElementById("btnBitsDown");
|
|
||||||
|
|
||||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
|
||||||
const toolboxPanel = document.getElementById("toolboxPanel");
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
STATE
|
|
||||||
----------------------------- */
|
|
||||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
|
||||||
let bits = new Array(bitCount).fill(false);
|
|
||||||
let randomTimer = null;
|
|
||||||
|
|
||||||
// For responsive wrapping of the top binary display
|
|
||||||
let nibblesPerLine = null;
|
|
||||||
let wrapMeasureSpan = null;
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
HELPERS
|
|
||||||
----------------------------- */
|
|
||||||
function clampInt(n, min, max) {
|
|
||||||
if (!Number.isFinite(n)) return min;
|
|
||||||
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTwosMode() {
|
|
||||||
return !!modeToggle?.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pow2Big(n) {
|
|
||||||
return 1n << BigInt(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
function unsignedMaxExclusive(nBits) {
|
|
||||||
return pow2Big(nBits);
|
|
||||||
}
|
|
||||||
|
|
||||||
function unsignedMaxValue(nBits) {
|
|
||||||
return pow2Big(nBits) - 1n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function twosMin(nBits) {
|
|
||||||
return -pow2Big(nBits - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function twosMax(nBits) {
|
|
||||||
return pow2Big(nBits - 1) - 1n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bitsToUnsignedBigInt() {
|
|
||||||
let v = 0n;
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
if (bits[i]) v += pow2Big(i);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unsignedBigIntToBits(vUnsigned) {
|
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
|
||||||
const v = ((vUnsigned % span) + span) % span;
|
|
||||||
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bitsToSignedBigIntTwos() {
|
|
||||||
const u = bitsToUnsignedBigInt();
|
|
||||||
const signBit = bits[bitCount - 1] === true;
|
|
||||||
if (!signBit) return u;
|
|
||||||
return u - pow2Big(bitCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
function signedBigIntToBitsTwos(vSigned) {
|
|
||||||
const span = pow2Big(bitCount);
|
|
||||||
let v = ((vSigned % span) + span) % span;
|
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateModeHint() {
|
|
||||||
if (!modeHint) return;
|
|
||||||
modeHint.textContent = isTwosMode()
|
|
||||||
? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."
|
|
||||||
: "Tip: In unsigned binary, all bits represent positive values.";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
TOP BINARY DISPLAY: responsive wrap by nibble count
|
|
||||||
----------------------------- */
|
|
||||||
function ensureWrapMeasurer() {
|
|
||||||
if (wrapMeasureSpan || !binaryEl) return;
|
|
||||||
wrapMeasureSpan = document.createElement("span");
|
|
||||||
wrapMeasureSpan.style.position = "absolute";
|
|
||||||
wrapMeasureSpan.style.visibility = "hidden";
|
|
||||||
wrapMeasureSpan.style.whiteSpace = "pre";
|
|
||||||
wrapMeasureSpan.style.pointerEvents = "none";
|
|
||||||
// Inherit font/letterspacing from binaryEl
|
|
||||||
wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font;
|
|
||||||
wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing;
|
|
||||||
document.body.appendChild(wrapMeasureSpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeNibblesPerLine() {
|
|
||||||
if (!binaryEl) return null;
|
|
||||||
ensureWrapMeasurer();
|
|
||||||
|
|
||||||
// Available width = width of the readout area (binaryEl parent)
|
|
||||||
const host = binaryEl.parentElement;
|
|
||||||
if (!host) return null;
|
|
||||||
|
|
||||||
const hostW = host.getBoundingClientRect().width;
|
|
||||||
if (!Number.isFinite(hostW) || hostW <= 0) return null;
|
|
||||||
|
|
||||||
// Measure one nibble including trailing space ("0000 ")
|
|
||||||
wrapMeasureSpan.textContent = "0000 ";
|
|
||||||
const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1;
|
|
||||||
|
|
||||||
// Safety: keep at least 1 nibble per line
|
|
||||||
const max = Math.max(1, Math.floor(hostW / nibbleW));
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBinaryWrapped() {
|
|
||||||
// EXACT bitCount digits (no padding to 4)
|
|
||||||
let raw = "";
|
|
||||||
for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0";
|
|
||||||
|
|
||||||
// If <= 4 bits, do NOT insert spaces/newlines at all
|
|
||||||
if (bitCount <= 4) return raw;
|
|
||||||
|
|
||||||
const groups = [];
|
|
||||||
for (let i = 0; i < raw.length; i += 4) {
|
|
||||||
groups.push(raw.slice(i, i + 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
const perLine = nibblesPerLine ?? groups.length;
|
|
||||||
if (perLine >= groups.length) return groups.join(" ");
|
|
||||||
|
|
||||||
const lines = [];
|
|
||||||
for (let i = 0; i < groups.length; i += perLine) {
|
|
||||||
lines.push(groups.slice(i, i + perLine).join(" "));
|
|
||||||
}
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshBinaryWrap() {
|
|
||||||
const next = computeNibblesPerLine();
|
|
||||||
// Only update if it actually changes (prevents jitter)
|
|
||||||
if (next !== nibblesPerLine) nibblesPerLine = next;
|
|
||||||
updateReadout(); // re-render with new wrap
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
BUILD UI (BITS)
|
|
||||||
----------------------------- */
|
|
||||||
function buildBits(count) {
|
|
||||||
bitCount = clampInt(count, 1, 64);
|
|
||||||
if (bitsInput) bitsInput.value = String(bitCount);
|
|
||||||
|
|
||||||
const oldBits = bits.slice();
|
|
||||||
bits = new Array(bitCount).fill(false);
|
|
||||||
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
|
|
||||||
|
|
||||||
bitsGrid.innerHTML = "";
|
|
||||||
|
|
||||||
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
|
|
||||||
if (bitCount < 8) {
|
|
||||||
bitsGrid.style.setProperty("--cols", String(bitCount));
|
|
||||||
} else {
|
|
||||||
bitsGrid.style.removeProperty("--cols");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = bitCount - 1; i >= 0; i--) {
|
|
||||||
const bitEl = document.createElement("div");
|
|
||||||
bitEl.className = "bit";
|
|
||||||
|
|
||||||
bitEl.innerHTML = `
|
|
||||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
|
||||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
|
||||||
<label class="switch" aria-label="Toggle bit ${i}">
|
|
||||||
<input type="checkbox" data-index="${i}">
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
`;
|
|
||||||
|
|
||||||
bitsGrid.appendChild(bitEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
const i = Number(input.dataset.index);
|
|
||||||
bits[i] = input.checked;
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// bulb styling + 25% bigger (vs 26px previously)
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
const bulb = document.getElementById(`bulb-${i}`);
|
|
||||||
if (!bulb) continue;
|
|
||||||
bulb.style.width = "auto";
|
|
||||||
bulb.style.height = "auto";
|
|
||||||
bulb.style.border = "none";
|
|
||||||
bulb.style.background = "transparent";
|
|
||||||
bulb.style.borderRadius = "0";
|
|
||||||
bulb.style.boxShadow = "none";
|
|
||||||
bulb.style.opacity = "0.45";
|
|
||||||
bulb.style.fontSize = "32px";
|
|
||||||
bulb.style.lineHeight = "1";
|
|
||||||
bulb.style.display = "flex";
|
|
||||||
bulb.style.alignItems = "center";
|
|
||||||
bulb.style.justifyContent = "center";
|
|
||||||
bulb.style.filter = "grayscale(1)";
|
|
||||||
bulb.textContent = "💡";
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapping may change when bit width changes
|
|
||||||
refreshBinaryWrap();
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
UI UPDATE
|
|
||||||
----------------------------- */
|
|
||||||
function updateBitLabels() {
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
const label = document.getElementById(`bitLabel-${i}`);
|
|
||||||
if (!label) continue;
|
|
||||||
|
|
||||||
if (isTwosMode() && i === bitCount - 1) {
|
|
||||||
// Keep on one line (CSS: white-space:nowrap)
|
|
||||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
|
||||||
} else {
|
|
||||||
label.textContent = pow2Big(i).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSwitchesToBits() {
|
|
||||||
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
|
|
||||||
const i = Number(input.dataset.index);
|
|
||||||
input.checked = !!bits[i];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBulbs() {
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
const bulb = document.getElementById(`bulb-${i}`);
|
|
||||||
if (!bulb) continue;
|
|
||||||
|
|
||||||
const on = bits[i] === true;
|
|
||||||
if (on) {
|
|
||||||
bulb.style.opacity = "1";
|
|
||||||
bulb.style.filter = "grayscale(0)";
|
|
||||||
bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)";
|
|
||||||
} else {
|
|
||||||
bulb.style.opacity = "0.45";
|
|
||||||
bulb.style.filter = "grayscale(1)";
|
|
||||||
bulb.style.textShadow = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateReadout() {
|
|
||||||
if (!denaryEl || !binaryEl) return;
|
|
||||||
|
|
||||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
|
||||||
binaryEl.textContent = formatBinaryWrapped();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
updateModeHint();
|
|
||||||
updateBitLabels();
|
|
||||||
syncSwitchesToBits();
|
|
||||||
updateBulbs();
|
|
||||||
updateReadout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
SET FROM INPUT
|
|
||||||
----------------------------- */
|
|
||||||
function setFromBinaryString(binStr) {
|
|
||||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
|
||||||
if (!/^[01]+$/.test(clean)) return false;
|
|
||||||
|
|
||||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
const charFromRight = padded[padded.length - 1 - i];
|
|
||||||
bits[i] = charFromRight === "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFromDenaryInput(vStr) {
|
|
||||||
const raw = String(vStr ?? "").trim();
|
|
||||||
if (!raw) return false;
|
|
||||||
|
|
||||||
let v;
|
|
||||||
try {
|
|
||||||
if (!/^-?\d+$/.test(raw)) return false;
|
|
||||||
v = BigInt(raw);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTwosMode()) {
|
|
||||||
const min = twosMin(bitCount);
|
|
||||||
const max = twosMax(bitCount);
|
|
||||||
if (v < min || v > max) return false;
|
|
||||||
signedBigIntToBitsTwos(v);
|
|
||||||
} else {
|
|
||||||
if (v < 0n) return false;
|
|
||||||
if (v > unsignedMaxValue(bitCount)) return false;
|
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
SHIFTS
|
|
||||||
----------------------------- */
|
|
||||||
function shiftLeft() {
|
|
||||||
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
|
|
||||||
bits[0] = false;
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function shiftRight() {
|
|
||||||
if (isTwosMode()) {
|
|
||||||
// arithmetic right shift: keep MSB
|
|
||||||
const msb = bits[bitCount - 1];
|
|
||||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
|
||||||
bits[bitCount - 1] = msb;
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
|
||||||
bits[bitCount - 1] = false;
|
|
||||||
}
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
CLEAR / INC / DEC
|
|
||||||
----------------------------- */
|
|
||||||
function clearAll() {
|
|
||||||
bits.fill(false);
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function increment() {
|
|
||||||
if (isTwosMode()) {
|
|
||||||
const min = twosMin(bitCount);
|
|
||||||
const max = twosMax(bitCount);
|
|
||||||
let v = bitsToSignedBigIntTwos() + 1n;
|
|
||||||
if (v > max) v = min;
|
|
||||||
signedBigIntToBitsTwos(v);
|
|
||||||
} else {
|
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
|
||||||
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
|
||||||
}
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrement() {
|
|
||||||
if (isTwosMode()) {
|
|
||||||
const min = twosMin(bitCount);
|
|
||||||
const max = twosMax(bitCount);
|
|
||||||
let v = bitsToSignedBigIntTwos() - 1n;
|
|
||||||
if (v < min) v = max;
|
|
||||||
signedBigIntToBitsTwos(v);
|
|
||||||
} else {
|
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
|
||||||
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
|
||||||
}
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
RANDOM
|
|
||||||
----------------------------- */
|
|
||||||
function cryptoRandomBigInt(maxExclusive) {
|
|
||||||
if (maxExclusive <= 0n) return 0n;
|
|
||||||
const bitLen = maxExclusive.toString(2).length;
|
|
||||||
const byteLen = Math.ceil(bitLen / 8);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const bytes = new Uint8Array(byteLen);
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
|
|
||||||
let x = 0n;
|
|
||||||
for (const b of bytes) x = (x << 8n) | BigInt(b);
|
|
||||||
|
|
||||||
const extraBits = BigInt(byteLen * 8 - bitLen);
|
|
||||||
if (extraBits > 0n) x = x >> extraBits;
|
|
||||||
if (x < maxExclusive) return x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRandomOnce() {
|
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
|
||||||
const u = cryptoRandomBigInt(span);
|
|
||||||
unsignedBigIntToBits(u);
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function runRandomBriefly() {
|
|
||||||
if (randomTimer) {
|
|
||||||
clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
const durationMs = 1125; // (your “~25% longer” vs 900ms)
|
|
||||||
const tickMs = 80;
|
|
||||||
|
|
||||||
randomTimer = setInterval(() => {
|
|
||||||
setRandomOnce();
|
|
||||||
if (Date.now() - start >= durationMs) {
|
|
||||||
clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
}
|
|
||||||
}, tickMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
BIT WIDTH CONTROLS
|
|
||||||
----------------------------- */
|
|
||||||
function setBitWidth(n) {
|
|
||||||
buildBits(clampInt(n, 1, 64));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
TOOLBOX TOGGLE (simple open/close state)
|
|
||||||
----------------------------- */
|
|
||||||
function setToolboxOpen(open) {
|
|
||||||
document.body.classList.toggle("toolboxClosed", !open);
|
|
||||||
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
|
|
||||||
refreshBinaryWrap(); // width changes when toolbox closes/opens
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
EVENTS
|
|
||||||
----------------------------- */
|
|
||||||
modeToggle?.addEventListener("change", () => updateUI());
|
|
||||||
|
|
||||||
btnCustomBinary?.addEventListener("click", () => {
|
|
||||||
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
|
||||||
if (v === null) return;
|
|
||||||
if (!setFromBinaryString(v)) alert("Invalid binary");
|
|
||||||
});
|
|
||||||
|
|
||||||
btnCustomDenary?.addEventListener("click", () => {
|
|
||||||
const v = prompt(
|
|
||||||
isTwosMode()
|
|
||||||
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
|
||||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
|
||||||
);
|
|
||||||
if (v === null) return;
|
|
||||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
|
||||||
});
|
|
||||||
|
|
||||||
btnShiftLeft?.addEventListener("click", shiftLeft);
|
|
||||||
btnShiftRight?.addEventListener("click", shiftRight);
|
|
||||||
|
|
||||||
btnInc?.addEventListener("click", increment);
|
|
||||||
btnDec?.addEventListener("click", decrement);
|
|
||||||
|
|
||||||
btnClear?.addEventListener("click", clearAll);
|
|
||||||
btnRandom?.addEventListener("click", runRandomBriefly);
|
|
||||||
|
|
||||||
btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1));
|
|
||||||
btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1));
|
|
||||||
|
|
||||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
|
||||||
|
|
||||||
toolboxToggle?.addEventListener("click", () => {
|
|
||||||
const isOpen = !document.body.classList.contains("toolboxClosed");
|
|
||||||
setToolboxOpen(!isOpen);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Recompute wrapping live when the window size changes
|
|
||||||
let resizeT = null;
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
if (resizeT) clearTimeout(resizeT);
|
|
||||||
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
INIT
|
|
||||||
----------------------------- */
|
|
||||||
updateModeHint();
|
|
||||||
buildBits(bitCount);
|
|
||||||
setToolboxOpen(true);
|
|
||||||
})();
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
/*
|
|
||||||
Binary page styles (keeps the last-working simulator markup + binary.js).
|
|
||||||
|
|
||||||
Goals:
|
|
||||||
- Do NOT change any IDs/classes expected by src/scripts/binary.js
|
|
||||||
- Toolbox button toggles the ENTIRE right-hand column via body.toolboxClosed
|
|
||||||
- Fix toolbox button positioning (no overlap, consistent with header container)
|
|
||||||
- Fix spacing/consistency of cards + buttons
|
|
||||||
- Keep binary readout wrapping/bit-width behaviour from JS (\n in output)
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root{
|
|
||||||
--panel-w: 360px;
|
|
||||||
--gap: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page wrapper (inside BaseLayout .pageWrap) */
|
|
||||||
.wrap{
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 22px 20px 48px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbox toggle button (sits below navbar, aligned right, never overlaps) */
|
|
||||||
.toolboxToggle{
|
|
||||||
align-self: flex-end;
|
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--nav-h, 108px) + 14px);
|
|
||||||
z-index: 30;
|
|
||||||
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.14);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.toolboxToggle:hover{ background: rgba(255,255,255,.08); }
|
|
||||||
.toolboxText{
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main layout grid */
|
|
||||||
.topGrid{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr var(--panel-w);
|
|
||||||
gap: var(--gap);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide ENTIRE toolbox column when toggled closed */
|
|
||||||
body.toolboxClosed .topGrid{ grid-template-columns: 1fr; }
|
|
||||||
body.toolboxClosed #toolboxPanel{ display: none; }
|
|
||||||
|
|
||||||
.mainCol{ min-width: 0; }
|
|
||||||
|
|
||||||
/* Readout */
|
|
||||||
.readout{
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
|
||||||
.num{
|
|
||||||
display: inline-block;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: pre-line; /* allows JS \n wraps */
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.denaryValue{
|
|
||||||
font-size: 54px;
|
|
||||||
margin: 6px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binaryValue{
|
|
||||||
font-size: 56px;
|
|
||||||
margin: 4px 0 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider{
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,.10);
|
|
||||||
margin: 14px auto 24px;
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bits area */
|
|
||||||
.bitsWrap{ padding-top: 6px; }
|
|
||||||
|
|
||||||
.bitsGrid{
|
|
||||||
display: grid;
|
|
||||||
gap: 24px;
|
|
||||||
justify-content: center;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsGrid.bitsFew{ justify-content: center; }
|
|
||||||
|
|
||||||
.bit{
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulb{
|
|
||||||
font-size: 32px; /* JS also bumps this */
|
|
||||||
line-height: 1;
|
|
||||||
opacity: .45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitVal{
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.05;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap; /* keep -128 on one line */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Switch (existing classes assumed) */
|
|
||||||
.switch{
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 52px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
.switch input{ display:none; }
|
|
||||||
.slider{
|
|
||||||
position:absolute;
|
|
||||||
inset:0;
|
|
||||||
border-radius:999px;
|
|
||||||
background: rgba(255,255,255,.18);
|
|
||||||
border: 1px solid rgba(255,255,255,.14);
|
|
||||||
}
|
|
||||||
.slider:before{
|
|
||||||
content:"";
|
|
||||||
position:absolute;
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
left: 3px;
|
|
||||||
top: 2.5px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fff;
|
|
||||||
transition: transform .18s ease;
|
|
||||||
}
|
|
||||||
.switch input:checked + .slider:before{ transform: translateX(22px); }
|
|
||||||
|
|
||||||
/* Toolbox column */
|
|
||||||
.panelCol{
|
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--nav-h, 108px) + 72px); /* leaves space for sticky toolbox button */
|
|
||||||
align-self: start;
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.card{
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255,255,255,.05);
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint{
|
|
||||||
opacity: .7;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 10px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keep mode labels on one line */
|
|
||||||
.toggleRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleLabel{
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subCard{
|
|
||||||
margin-top: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(0,0,0,.12);
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitWidthRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 44px 1fr 44px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitInputWrap{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255,255,255,.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitInputLabel{
|
|
||||||
opacity: .75;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitInput{
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.miniBtn{
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.9);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.miniBtn:hover{ background: rgba(255,255,255,.08); }
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.controlsRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn{
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
padding: 12px 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .10em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn:hover{ background: rgba(255,255,255,.08); }
|
|
||||||
|
|
||||||
.btnWide{ width: 100%; }
|
|
||||||
|
|
||||||
.btnAccent{
|
|
||||||
background: rgba(0,255,140,.12);
|
|
||||||
border-color: rgba(0,255,140,.22);
|
|
||||||
}
|
|
||||||
.btnAccent:hover{ background: rgba(0,255,140,.16); }
|
|
||||||
|
|
||||||
.toolRowCentered{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 10px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolBtn{
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
|
|
||||||
.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
|
|
||||||
|
|
||||||
.toolRow2{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset stays white text */
|
|
||||||
.btnReset{ color: rgba(255,255,255,.92); }
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 980px){
|
|
||||||
.topGrid{ grid-template-columns: 1fr; }
|
|
||||||
.panelCol{ position: static; }
|
|
||||||
.toolboxToggle{ position: static; align-self: flex-start; }
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
:root{
|
|
||||||
--bg: #1f2027;
|
|
||||||
--panel: #22242d;
|
|
||||||
--panel2: rgba(255,255,255,.04);
|
|
||||||
--text: #e8e8ee;
|
|
||||||
--muted: #a9acb8;
|
|
||||||
--accent: #33ff7a;
|
|
||||||
--accent-dim: rgba(51,255,122,.15);
|
|
||||||
--line: rgba(255,255,255,.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
*{ box-sizing:border-box; }
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin:0;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteHeader{
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
background: rgba(0,0,0,.15);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteHeaderInner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 14px 20px;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand{
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing:.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav{
|
|
||||||
display:flex;
|
|
||||||
gap: 14px;
|
|
||||||
flex-wrap:wrap;
|
|
||||||
justify-content:flex-end;
|
|
||||||
}
|
|
||||||
.nav a{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.nav a:hover{ color: var(--text); }
|
|
||||||
|
|
||||||
.siteMain{
|
|
||||||
min-height: calc(100vh - 140px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteFooter{
|
|
||||||
border-top: 1px solid rgba(255,255,255,.08);
|
|
||||||
margin-top: 32px;
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteFooterInner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 18px 20px 26px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footerTitle{
|
|
||||||
color: var(--text);
|
|
||||||
opacity:.9;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
:root{
|
|
||||||
--bg: #1f2027;
|
|
||||||
--panel: rgba(255,255,255,.04);
|
|
||||||
--panel-border: rgba(255,255,255,.10);
|
|
||||||
--text: #e8e8ee;
|
|
||||||
--muted: #a9acb8;
|
|
||||||
--accent: #33ff7a;
|
|
||||||
--accent-dim: rgba(51,255,122,.15);
|
|
||||||
--line: rgba(255,255,255,.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
*{ box-sizing: border-box; }
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin:0;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header{
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
||||||
background: rgba(0,0,0,.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header__inner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 14px 20px;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:space-between;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand{
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav{
|
|
||||||
display:flex;
|
|
||||||
gap: 14px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content:flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav__link{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.nav__link:hover{ color: var(--text); }
|
|
||||||
|
|
||||||
.site-main{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 28px 20px 40px;
|
|
||||||
min-height: calc(100vh - 140px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-footer{
|
|
||||||
border-top: 1px solid rgba(255,255,255,.08);
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-footer__inner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 16px 20px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
/* Binary page styles (moved OUT of binary.astro) */
|
|
||||||
|
|
||||||
:root{
|
|
||||||
--panel-w: 360px;
|
|
||||||
--gap: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap{
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 26px 20px 48px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topGrid{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr var(--panel-w);
|
|
||||||
gap: var(--gap);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When toolbox is hidden, reclaim space + centre content */
|
|
||||||
body.toolboxClosed .topGrid{
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
body.toolboxClosed #toolboxPanel{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainCol{
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.readout{
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
|
||||||
.num{
|
|
||||||
display: inline-block;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: pre-line; /* allows JS \n wraps */
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.denaryValue{
|
|
||||||
font-size: 54px;
|
|
||||||
margin: 6px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binaryValue{
|
|
||||||
font-size: 56px;
|
|
||||||
margin: 4px 0 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider{
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,.10);
|
|
||||||
margin: 14px auto 24px;
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsWrap{
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsGrid{
|
|
||||||
display: grid;
|
|
||||||
gap: 24px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Default: a single row of bits (will wrap automatically as bit count grows) */
|
|
||||||
.bitsGrid{
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsGrid.bitsFew{
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bit tile */
|
|
||||||
.bit{
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulb{
|
|
||||||
font-size: 32px; /* JS also bumps this */
|
|
||||||
line-height: 1;
|
|
||||||
opacity: .45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitVal{
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.05;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap; /* keep -128 on one line */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Switch (existing classes assumed) */
|
|
||||||
.switch{
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 52px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
.switch input{ display:none; }
|
|
||||||
.slider{
|
|
||||||
position:absolute;
|
|
||||||
inset:0;
|
|
||||||
border-radius:999px;
|
|
||||||
background: rgba(255,255,255,.18);
|
|
||||||
border: 1px solid rgba(255,255,255,.14);
|
|
||||||
}
|
|
||||||
.slider:before{
|
|
||||||
content:"";
|
|
||||||
position:absolute;
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
left: 3px;
|
|
||||||
top: 2.5px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fff;
|
|
||||||
transition: transform .18s ease;
|
|
||||||
}
|
|
||||||
.switch input:checked + .slider:before{
|
|
||||||
transform: translateX(22px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbox toggle button */
|
|
||||||
.toolboxToggle{
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 18px;
|
|
||||||
z-index: 20;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.14);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.toolboxText{
|
|
||||||
letter-spacing: .12em;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbox panel */
|
|
||||||
.panelCol{
|
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--nav-h, 72px) + 18px);
|
|
||||||
align-self: start;
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.card{
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255,255,255,.05);
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
.cardTitle{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint{
|
|
||||||
opacity: .7;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 10px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keep mode labels on one line */
|
|
||||||
.toggleRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.toggleLabel{
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subCard{
|
|
||||||
margin-top: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(0,0,0,.12);
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
.subTitle{
|
|
||||||
opacity: .8;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitWidthRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 44px 1fr 44px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitInputWrap{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255,255,255,.04);
|
|
||||||
}
|
|
||||||
.bitInputLabel{
|
|
||||||
opacity: .75;
|
|
||||||
letter-spacing: .14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.bitInput{
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.miniBtn{
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.9);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.controlsRow{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn{
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
padding: 12px 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .10em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btnWide{ width: 100%; }
|
|
||||||
|
|
||||||
.btnAccent{
|
|
||||||
background: rgba(0,255,140,.12);
|
|
||||||
border-color: rgba(0,255,140,.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolRowCentered{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 10px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolBtn{
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
|
|
||||||
.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
|
|
||||||
|
|
||||||
.toolRow2{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset stays white text */
|
|
||||||
.btnReset{
|
|
||||||
color: rgba(255,255,255,.92);
|
|
||||||
}
|
|
||||||
@@ -1,85 +1,372 @@
|
|||||||
:root{
|
/* Global fonts */
|
||||||
|
@font-face {
|
||||||
|
font-family: "SevenSegment";
|
||||||
|
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
|
||||||
|
url("/fonts/Seven-Segment.woff") format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "DSEG7Classic";
|
||||||
|
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
||||||
|
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--nav-h: 92px;
|
||||||
--bg: #1f2027;
|
--bg: #1f2027;
|
||||||
--panel: #22242d;
|
|
||||||
--panel2: rgba(255,255,255,.04);
|
|
||||||
--text: #e8e8ee;
|
--text: #e8e8ee;
|
||||||
--muted: #a9acb8;
|
--muted: #a9acb8;
|
||||||
--accent: #33ff7a;
|
--line: rgba(255,255,255,.10);
|
||||||
--accent-dim: rgba(51,255,122,.15);
|
--accent: #28f07a;
|
||||||
--line: rgba(255,255,255,.12);
|
--ui-font: "Inter", system-ui, -apple-system, sans-serif;
|
||||||
|
--bit-font: "SevenSegment", monospace;
|
||||||
|
--num-font: "DSEG7Classic"
|
||||||
}
|
}
|
||||||
|
|
||||||
*{ box-sizing:border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--ui-font); display: flex; flex-direction: column; }
|
||||||
|
|
||||||
body{
|
/* --- BASE LAYOUT --- */
|
||||||
margin:0;
|
.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; }
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
.navInner { height: 90px; max-width: 1400px; margin: 0 auto; padding: 0 20px; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
|
||||||
background: var(--bg);
|
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); }
|
||||||
color: var(--text);
|
.brandLogo { width: 75px; height: 75px; 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%; 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; }
|
||||||
|
|
||||||
|
/* --- 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; }
|
||||||
|
.divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; }
|
||||||
|
|
||||||
|
/* --- GRIDS & BITS --- */
|
||||||
|
.bitsWrap { width: 100%; }
|
||||||
|
.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; }
|
||||||
|
.bitsGrid.bitsFew { justify-content: center; }
|
||||||
|
.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; }
|
||||||
|
.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; }
|
||||||
|
|
||||||
|
.bulb { width: 44px; height: 44px; color: rgba(255,255,255,.15); margin-bottom: 8px; flex-shrink: 0; transition: 0.2s ease; background: transparent; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.bulb svg { width: 100%; height: 100%; display: block; }
|
||||||
|
.bulb.on { color: #ffd86b !important; filter: drop-shadow(0 0 14px rgba(255, 216, 107, 1)) !important; }
|
||||||
|
.bulb.on svg { fill: #ffd86b !important; }
|
||||||
|
|
||||||
|
.switch { position: relative; width: 56px; height: 28px; display: inline-block; }
|
||||||
|
.switch input { display: none; }
|
||||||
|
.slider { position: absolute; inset: 0; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14); border-radius: 999px; transition: .2s ease; }
|
||||||
|
.slider::before { content: ""; position: absolute; width: 22px; height: 22px; left: 3px; top: 2px; background: rgba(255,255,255,.92); border-radius: 999px; transition: .2s ease; pointer-events: none; }
|
||||||
|
.switch input:checked + .slider { background: rgba(40,240,122,.30); border-color: rgba(40,240,122,.30); }
|
||||||
|
.switch input:checked + .slider::before { transform: translateX(28px); }
|
||||||
|
|
||||||
|
/* --- HEXADECIMAL --- */
|
||||||
|
.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; }
|
||||||
|
.hexGrid.bitsFew { justify-content: center; }
|
||||||
|
.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; }
|
||||||
|
|
||||||
|
/* --- HEX COLOURS SPECIFIC --- */
|
||||||
|
.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; }
|
||||||
|
.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; }
|
||||||
|
.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; }
|
||||||
|
.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||||
|
.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; }
|
||||||
|
.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
|
||||||
|
|
||||||
|
.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; }
|
||||||
|
.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; }
|
||||||
|
.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; }
|
||||||
|
|
||||||
|
/* HEX CARD */
|
||||||
|
.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); }
|
||||||
|
.hexCardButtons { display: flex; gap: 10px; }
|
||||||
|
.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; }
|
||||||
|
.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); }
|
||||||
|
.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); }
|
||||||
|
.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); }
|
||||||
|
.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); }
|
||||||
|
.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; }
|
||||||
|
.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; }
|
||||||
|
.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||||
|
.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; }
|
||||||
|
.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); }
|
||||||
|
.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; }
|
||||||
|
|
||||||
|
|
||||||
|
/* --- TOOLBOX --- */
|
||||||
|
.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; }
|
||||||
|
.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); }
|
||||||
|
.toolboxToggle:hover { border-color: rgba(255,255,255,.22); }
|
||||||
|
.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; }
|
||||||
|
.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; }
|
||||||
|
.card { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 16px; padding: 16px; backdrop-filter: blur(10px); }
|
||||||
|
.cardTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 18px; color: rgba(232,232,238,.9); margin-bottom: 12px; }
|
||||||
|
.hint { font-family: var(--bit-font); font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: rgba(232,232,238,.55); margin-top: 10px; line-height: 1.35; }
|
||||||
|
.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; }
|
||||||
|
.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); }
|
||||||
|
.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); }
|
||||||
|
.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; }
|
||||||
|
.bitWidthRow { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; }
|
||||||
|
.miniBtn:hover { border-color: rgba(255,255,255,.22); }
|
||||||
|
.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; }
|
||||||
|
.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; }
|
||||||
|
.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; }
|
||||||
|
.btn { border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); border-radius: 12px; padding: 10px 12px; font-family: var(--bit-font); font-size: 14px; letter-spacing: .12em; text-transform: uppercase; font-weight: 900; cursor: pointer; }
|
||||||
|
.btn:hover { border-color: rgba(255,255,255,.22); }
|
||||||
|
.btnAccent { background: rgba(40,240,122,.12); border-color: rgba(40,240,122,.22); }
|
||||||
|
.btnAccent:hover { border-color: rgba(40,240,122,.35); }
|
||||||
|
.btnHalf { width: calc(50% - 6px); }
|
||||||
|
.btnWide { width: 100%; }
|
||||||
|
.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; }
|
||||||
|
.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; }
|
||||||
|
.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; }
|
||||||
|
.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); }
|
||||||
|
.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); }
|
||||||
|
.btnReset { color: rgba(232,232,238,.95); }
|
||||||
|
.btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); }
|
||||||
|
|
||||||
|
/* === CONTAINER QUERIES === */
|
||||||
|
@container (max-width: 1050px) {
|
||||||
|
.readoutContainer { gap: 40px; }
|
||||||
|
.colorGroupWrap { gap: 10px; }
|
||||||
|
.colorGroup { padding: 10px; gap: 8px; border-radius: 16px; }
|
||||||
|
.hexCard { padding: 12px 8px; width: 140px; gap: 12px; }
|
||||||
|
.hexDigitDisplay { font-size: 40px; }
|
||||||
|
.hexNibbleBulb { width: 24px !important; height: 24px !important; }
|
||||||
|
.hexNibbleLabel { font-size: 20px; }
|
||||||
|
.hexColWeight { font-size: 26px; margin-top: 10px; }
|
||||||
|
.hexCardBtn { width: 34px; height: 34px; font-size: 14px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader{
|
@container (max-width: 800px) {
|
||||||
position: sticky;
|
.readoutContainer { flex-direction: column; gap: 24px; }
|
||||||
top: 0;
|
.colorPreviewSide { padding-top: 0; }
|
||||||
z-index: 10;
|
.colorGroupWrap { gap: 6px; }
|
||||||
background: rgba(0,0,0,.15);
|
.colorGroup { padding: 6px; gap: 6px; border-radius: 12px; }
|
||||||
backdrop-filter: blur(8px);
|
.hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; }
|
||||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
.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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeaderInner{
|
@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } }
|
||||||
max-width: 1200px;
|
@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)); } }
|
||||||
margin: 0 auto;
|
|
||||||
padding: 14px 20px;
|
.cc-sa:before {
|
||||||
display:flex;
|
background-image: url(https://creativecommons.org/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg#cc-sa);
|
||||||
align-items:center;
|
float: left;
|
||||||
justify-content:space-between;
|
margin-left: -2.5em;
|
||||||
gap: 16px;
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand{
|
.cc-nc:before {
|
||||||
color: var(--text);
|
background-image: url(https://creativecommons.org/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg#cc-nc);
|
||||||
text-decoration:none;
|
float: left;
|
||||||
font-weight: 900;
|
margin-left: -2.5em;
|
||||||
letter-spacing:.02em;
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav{
|
.cc-by:before {
|
||||||
display:flex;
|
background-image: url(https://creativecommons.org/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg#cc-by);
|
||||||
gap: 14px;
|
float: left;
|
||||||
flex-wrap:wrap;
|
margin-left: -2.5em;
|
||||||
justify-content:flex-end;
|
filter: invert(100%);
|
||||||
}
|
|
||||||
.nav a{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.nav a:hover{ color: var(--text); }
|
|
||||||
|
|
||||||
.siteMain{
|
|
||||||
min-height: calc(100vh - 140px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteFooter{
|
.cc-terms ul > li {
|
||||||
border-top: 1px solid rgba(255,255,255,.08);
|
padding-left: 2.5em;
|
||||||
margin-top: 32px;
|
clear: both;
|
||||||
background: rgba(0,0,0,.10);
|
list-style: none;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
min-height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteFooterInner{
|
.cc-terms ul {
|
||||||
max-width: 1200px;
|
padding: 0;
|
||||||
margin: 0 auto;
|
font-size: 1.5rem;
|
||||||
padding: 18px 20px 26px;
|
font-style: normal;
|
||||||
color: var(--muted);
|
font-weight: 400;
|
||||||
font-size: 12px;
|
line-height: 150%;
|
||||||
line-height: 1.6;
|
margin: 0 0 2em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerTitle{
|
.btnRandomRunning {
|
||||||
color: var(--text);
|
background: rgba(40,240,122,.18) !important;
|
||||||
opacity:.9;
|
border-color: rgba(40,240,122,.35) !important;
|
||||||
font-weight: 800;
|
color: rgba(232,232,238,.95) !important; /* Added this so the text pops like the reset button */
|
||||||
margin-bottom: 6px;
|
animation: randomPulse 750ms 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,.38); } /* Matched the .38 opacity peak */
|
||||||
|
100% { box-shadow: 0 0 0 rgba(40,240,122,0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ACCORDION ANIMATIONS FOR ALL TOOLBOXES --- */
|
||||||
|
.panelCol .cardTitle,
|
||||||
|
.pb-toolbox .cardTitle,
|
||||||
|
.lg-toolbox .cardTitle {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .cardTitle::after,
|
||||||
|
.pb-toolbox .cardTitle::after,
|
||||||
|
.lg-toolbox .cardTitle::after {
|
||||||
|
content: '▼';
|
||||||
|
font-size: 0.7em;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .card.collapsed .cardTitle::after,
|
||||||
|
.pb-toolbox .card.collapsed .cardTitle::after,
|
||||||
|
.lg-toolbox .card.collapsed .cardTitle::after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .cardBody,
|
||||||
|
.pb-toolbox .cardBody,
|
||||||
|
.lg-toolbox .cardBody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
transition: grid-template-rows 350ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .card.collapsed .cardBody,
|
||||||
|
.pb-toolbox .card.collapsed .cardBody,
|
||||||
|
.lg-toolbox .card.collapsed .cardBody {
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .cardBodyInner,
|
||||||
|
.pb-toolbox .cardBodyInner,
|
||||||
|
.lg-toolbox .cardBodyInner {
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 250ms ease 100ms, visibility 0s 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .card.collapsed .cardBodyInner,
|
||||||
|
.pb-toolbox .card.collapsed .cardBodyInner,
|
||||||
|
.lg-toolbox .card.collapsed .cardBodyInner {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 200ms ease 0s, visibility 0s 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- BASE STATE: Lock the scale so it never stretches --- */
|
||||||
|
.btnRandom, #btnRandom,
|
||||||
|
.btnReset, #btnReset {
|
||||||
|
transform: scale(1) !important;
|
||||||
|
transition: transform 0.1s ease-out, border-color 0.2s ease-out, color 0.2s ease-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- THE PULSE ANIMATIONS --- */
|
||||||
|
@keyframes neonPulseGreen {
|
||||||
|
0%, 100% {
|
||||||
|
background-color: rgba(40, 240, 122, 0.05);
|
||||||
|
box-shadow: inset 0 0 10px rgba(40, 240, 122, 0.4),
|
||||||
|
0 0 5px rgba(40, 240, 122, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(40, 240, 122, 0.15);
|
||||||
|
box-shadow: inset 0 0 22px rgba(40, 240, 122, 0.8),
|
||||||
|
0 0 12px rgba(40, 240, 122, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neonPulseRed {
|
||||||
|
0%, 100% {
|
||||||
|
background-color: rgba(255, 85, 85, 0.05);
|
||||||
|
box-shadow: inset 0 0 10px rgba(255, 85, 85, 0.4),
|
||||||
|
0 0 5px rgba(255, 85, 85, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(255, 85, 85, 0.15);
|
||||||
|
box-shadow: inset 0 0 22px rgba(255, 85, 85, 0.8),
|
||||||
|
0 0 12px rgba(255, 85, 85, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- HOVER STATES: Trigger the infinite pulse --- */
|
||||||
|
.btnRandom:hover, .btnRandom:focus, .btnRandom.active,
|
||||||
|
#btnRandom:hover, #btnRandom:focus, #btnRandom.active {
|
||||||
|
border-color: #28f07a !important;
|
||||||
|
color: #28f07a !important;
|
||||||
|
/* 1.5s cycle, infinite loop, smooth easing */
|
||||||
|
animation: neonPulseGreen 1.5s infinite ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnReset:hover, .btnReset:focus, .btnReset.active,
|
||||||
|
#btnReset:hover, #btnReset:focus, #btnReset.active {
|
||||||
|
border-color: #ff5555 !important;
|
||||||
|
color: #ff5555 !important;
|
||||||
|
/* 1.5s cycle, infinite loop, smooth easing */
|
||||||
|
animation: neonPulseRed 1.5s infinite ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- CLICK STATE: Interrupt the pulse and shrink --- */
|
||||||
|
.btnRandom:active, #btnRandom:active,
|
||||||
|
.btnReset:active, #btnReset:active {
|
||||||
|
transform: scale(0.96) !important;
|
||||||
|
animation: none !important; /* Immediately kill the pulse while clicked */
|
||||||
|
background-color: transparent !important;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
220
src/styles/logic-gates.css
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/* === 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; }
|
||||||
|
|
||||||
|
/* Update the SVG layer to sit ON TOP of nodes */
|
||||||
|
/* Move the wire layer behind the nodes */
|
||||||
|
.lg-svg-layer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1; /* Lower than nodes */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wires */
|
||||||
|
/* Wires - allow them to be clickable even behind the 'hit area' of ports */
|
||||||
|
.lg-wire {
|
||||||
|
stroke: #ffffff40;
|
||||||
|
stroke-width: 6;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke .1s ease,filter .1s ease,stroke-width .1s ease;
|
||||||
|
pointer-events: stroke;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.lg-wire:hover {
|
||||||
|
stroke: #fff9;
|
||||||
|
stroke-width: 10
|
||||||
|
}
|
||||||
|
.lg-wire.active {
|
||||||
|
stroke: #28f07a;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(40,240,122,.6))
|
||||||
|
}
|
||||||
|
.lg-wire.active:hover {
|
||||||
|
stroke: #5dff9e
|
||||||
|
}
|
||||||
|
.lg-wire.selected {
|
||||||
|
stroke: #f55!important;
|
||||||
|
stroke-width: 8!important;
|
||||||
|
stroke-dasharray: 8 8;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255,85,85,.8))!important;
|
||||||
|
animation: wireDash 1s linear infinite
|
||||||
|
}
|
||||||
|
@keyframes wireDash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lg-wire-temp {
|
||||||
|
stroke: #fff6;
|
||||||
|
stroke-dasharray: 8 8;
|
||||||
|
pointer-events: none
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nodes */
|
||||||
|
/* Nodes - move them in front of wires */
|
||||||
|
.lg-node {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10; /* Higher than wires */
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.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 */
|
||||||
|
/* Update ports to sit even higher so they stay clickable */
|
||||||
|
/* Ports - Ensure the dots are the top-most layer */
|
||||||
|
.lg-port {
|
||||||
|
width: 12px; /* Balanced size */
|
||||||
|
height: 12px;
|
||||||
|
background: #a9acb8;
|
||||||
|
border: 2px solid #1f2027; /* Dark border helps it 'pop' over glowing wires */
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100; /* Absolute top */
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; }
|
||||||
|
.lg-port.active {
|
||||||
|
background: #28f07a;
|
||||||
|
box-shadow: 0 0 8px rgba(40,240,122,.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 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); }
|
||||||
|
|
||||||
|
|
||||||
|
/* Ensure the active-sim class actually changes the color */
|
||||||
|
.active-sim .slider {
|
||||||
|
background-color: rgba(40,240,122,.25) !important; /* Bright Green */
|
||||||
|
box-shadow: 0 0 15px rgba(40, 240, 122, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-sim .slider::before {
|
||||||
|
transform: translateX(28px) !important;
|
||||||
|
}
|
||||||
145
src/styles/number-simulators.css
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/* --- 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 {
|
||||||
|
/* Your original layout and animations */
|
||||||
|
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;
|
||||||
|
|
||||||
|
/* THE FIX: Push the bottom edge higher up the screen */
|
||||||
|
/* If your footer is ~140px tall, 170px gives you a perfect 30px safe gap */
|
||||||
|
bottom: 110px;
|
||||||
|
|
||||||
|
/* Let the inside scroll smoothly */
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the scrollbar in Chrome/Safari */
|
||||||
|
.panelCol::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelCol .card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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)); } }
|
||||||
123
src/styles/pc-builder.css
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/* === 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 */
|
||||||
|
/* Wire layer - sits above case but below ports */
|
||||||
|
/* Wire layer - Physically on TOP of components */
|
||||||
|
.pb-svg-layer { z-index: 100 !important; pointer-events: none; position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||||
|
|
||||||
|
/* Cables */
|
||||||
|
/* Cables - make sure they have a width and color */
|
||||||
|
/* Cables - Styled for visibility */
|
||||||
|
.pb-wire { stroke: #55aaff; stroke-width: 6; fill: none; pointer-events: stroke; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.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 !important; 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)); }
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
:root{
|
|
||||||
--bg: #1f2027;
|
|
||||||
--panel: rgba(255,255,255,.04);
|
|
||||||
--panel-border: rgba(255,255,255,.10);
|
|
||||||
--text: #e8e8ee;
|
|
||||||
--muted: #a9acb8;
|
|
||||||
--accent: #33ff7a;
|
|
||||||
--accent-dim: rgba(51,255,122,.15);
|
|
||||||
--line: rgba(255,255,255,.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
*{ box-sizing: border-box; }
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin:0;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header{
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
||||||
background: rgba(0,0,0,.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header__inner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 14px 20px;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:space-between;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand{
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav{
|
|
||||||
display:flex;
|
|
||||||
gap: 14px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content:flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav__link{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.nav__link:hover{ color: var(--text); }
|
|
||||||
|
|
||||||
.site-main{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 28px 20px 40px;
|
|
||||||
min-height: calc(100vh - 140px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-footer{
|
|
||||||
border-top: 1px solid rgba(255,255,255,.08);
|
|
||||||
background: rgba(0,0,0,.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-footer__inner{
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 16px 20px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||