You've already forked computing-box
Compare commits
13 Commits
da28b5767d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aed8a80b02 | |||
| 13d15796cf | |||
| d9976c0a72 | |||
| 69420030a7 | |||
| ff63d9d8f5 | |||
| 273586137d | |||
| 89b0f733b1 | |||
| b90008d13c | |||
| 47fb7779d9 | |||
| 3de0ae1fd3 | |||
| fea9c3a6bf | |||
| e0c7dce9c8 | |||
| 9a6088fc9b |
@@ -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 "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 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"
|
|
||||||
534
LICENSE
534
LICENSE
@@ -1,121 +1,437 @@
|
|||||||
Creative Commons Legal Code
|
Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
|
||||||
CC0 1.0 Universal
|
=======================================================================
|
||||||
|
|
||||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
does not provide legal services or legal advice. Distribution of
|
||||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
other relationship. Creative Commons makes its licenses and related
|
||||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
warranties regarding its licenses, any material licensed under their
|
||||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
terms and conditions, or any related information. Creative Commons
|
||||||
HEREUNDER.
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
Statement of Purpose
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
The laws of most jurisdictions throughout the world automatically confer
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
conditions that creators and other rights holders may use to share
|
||||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
original works of authorship and other material subject to copyright
|
||||||
authorship and/or a database (each, a "Work").
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
Certain owners wish to permanently relinquish those rights to a Work for
|
Considerations for licensors: Our public licenses are
|
||||||
the purpose of contributing to a commons of creative, cultural and
|
intended for use by those authorized to give the public
|
||||||
scientific works ("Commons") that the public can reliably and without fear
|
permission to use material in ways otherwise restricted by
|
||||||
of later claims of infringement build upon, modify, incorporate in other
|
copyright and certain other rights. Our licenses are
|
||||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
irrevocable. Licensors should read and understand the terms
|
||||||
and for any purposes, including without limitation commercial purposes.
|
and conditions of the license they choose before applying it.
|
||||||
These owners may contribute to the Commons to promote the ideal of a free
|
Licensors should also secure all rights necessary before
|
||||||
culture and the further production of creative, cultural and scientific
|
applying our licenses so that the public can reuse the
|
||||||
works, or to gain reputation or greater distribution for their Work in
|
material as expected. Licensors should clearly mark any
|
||||||
part through the use and efforts of others.
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
For these and/or other purposes and motivations, and without any
|
Considerations for the public: By using one of our public
|
||||||
expectation of additional consideration or compensation, the person
|
licenses, a licensor grants the public permission to use the
|
||||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
licensed material under specified terms and conditions. If
|
||||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
the licensor's permission is not necessary for any reason--for
|
||||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
example, because of any applicable exception or limitation to
|
||||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
copyright--then that use is not regulated by the license. Our
|
||||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
=======================================================================
|
||||||
protected by copyright and related or neighboring rights ("Copyright and
|
|
||||||
Related Rights"). Copyright and Related Rights include, but are not
|
|
||||||
limited to, the following:
|
|
||||||
|
|
||||||
i. the right to reproduce, adapt, distribute, perform, display,
|
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
communicate, and translate a Work;
|
Public License
|
||||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
||||||
iii. publicity and privacy rights pertaining to a person's image or
|
|
||||||
likeness depicted in a Work;
|
|
||||||
iv. rights protecting against unfair competition in regards to a Work,
|
|
||||||
subject to the limitations in paragraph 4(a), below;
|
|
||||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
||||||
in a Work;
|
|
||||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
||||||
European Parliament and of the Council of 11 March 1996 on the legal
|
|
||||||
protection of databases, and under any national implementation
|
|
||||||
thereof, including any amended or successor version of such
|
|
||||||
directive); and
|
|
||||||
vii. other similar, equivalent or corresponding rights throughout the
|
|
||||||
world based on applicable law or treaty, and any national
|
|
||||||
implementations thereof.
|
|
||||||
|
|
||||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
||||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
("Public License"). To the extent this Public License may be
|
||||||
of action, whether now known or unknown (including existing as well as
|
interpreted as a contract, You are granted the Licensed Rights in
|
||||||
future claims and causes of action), in the Work (i) in all territories
|
consideration of Your acceptance of these terms and conditions, and the
|
||||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
Licensor grants You such rights in consideration of benefits the
|
||||||
treaty (including future time extensions), (iii) in any current or future
|
Licensor receives from making the Licensed Material available under
|
||||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
these terms and conditions.
|
||||||
including without limitation commercial, advertising or promotional
|
|
||||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
||||||
member of the public at large and to the detriment of Affirmer's heirs and
|
|
||||||
successors, fully intending that such Waiver shall not be subject to
|
|
||||||
revocation, rescission, cancellation, termination, or any other legal or
|
|
||||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
||||||
as contemplated by Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
||||||
be judged legally invalid or ineffective under applicable law, then the
|
|
||||||
Waiver shall be preserved to the maximum extent permitted taking into
|
|
||||||
account Affirmer's express Statement of Purpose. In addition, to the
|
|
||||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
||||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
||||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
||||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
||||||
maximum duration provided by applicable law or treaty (including future
|
|
||||||
time extensions), (iii) in any current or future medium and for any number
|
|
||||||
of copies, and (iv) for any purpose whatsoever, including without
|
|
||||||
limitation commercial, advertising or promotional purposes (the
|
|
||||||
"License"). The License shall be deemed effective as of the date CC0 was
|
|
||||||
applied by Affirmer to the Work. Should any part of the License for any
|
|
||||||
reason be judged legally invalid or ineffective under applicable law, such
|
|
||||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
||||||
of the License, and in such case Affirmer hereby affirms that he or she
|
|
||||||
will not (i) exercise any of his or her remaining Copyright and Related
|
|
||||||
Rights in the Work or (ii) assert any associated claims and causes of
|
|
||||||
action with respect to the Work, in either case contrary to Affirmer's
|
|
||||||
express Statement of Purpose.
|
|
||||||
|
|
||||||
4. Limitations and Disclaimers.
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
surrendered, licensed or otherwise affected by this document.
|
Rights that is derived from or based upon the Licensed Material
|
||||||
b. Affirmer offers the Work as-is and makes no representations or
|
and in which the Licensed Material is translated, altered,
|
||||||
warranties of any kind concerning the Work, express, implied,
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
statutory or otherwise, including without limitation warranties of
|
permission under the Copyright and Similar Rights held by the
|
||||||
title, merchantability, fitness for a particular purpose, non
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
infringement, or the absence of latent or other defects, accuracy, or
|
Material is a musical work, performance, or sound recording,
|
||||||
the present or absence of errors, whether or not discoverable, all to
|
Adapted Material is always produced where the Licensed Material is
|
||||||
the greatest extent permissible under applicable law.
|
synched in timed relation with a moving image.
|
||||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
||||||
that may apply to the Work or any use thereof, including without
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
limitation any person's Copyright and Related Rights in the Work.
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
accordance with the terms and conditions of this Public License.
|
||||||
consents, permissions or other rights required for any use of the
|
|
||||||
Work.
|
c. BY-NC-SA Compatible License means a license listed at
|
||||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
party to this document and has no duty or obligation with respect to
|
Commons as essentially the equivalent of this Public License.
|
||||||
this CC0 or use of the Work.
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name
|
||||||
|
of a Creative Commons Public License. The License Elements of this
|
||||||
|
Public License are Attribution, NonCommercial, and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
k. NonCommercial means not primarily intended for or directed towards
|
||||||
|
commercial advantage or monetary compensation. For purposes of
|
||||||
|
this Public License, the exchange of the Licensed Material for
|
||||||
|
other material subject to Copyright and Similar Rights by digital
|
||||||
|
file-sharing or similar means is NonCommercial provided there is
|
||||||
|
no payment of monetary compensation in connection with the
|
||||||
|
exchange.
|
||||||
|
|
||||||
|
l. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
m. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
n. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part, for NonCommercial purposes only; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material for
|
||||||
|
NonCommercial purposes only.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. Additional offer from the Licensor -- Adapted Material.
|
||||||
|
Every recipient of Adapted Material from You
|
||||||
|
automatically receives an offer from the Licensor to
|
||||||
|
exercise the Licensed Rights in the Adapted Material
|
||||||
|
under the conditions of the Adapter's License You apply.
|
||||||
|
|
||||||
|
c. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties, including when
|
||||||
|
the Licensed Material is used other than for NonCommercial
|
||||||
|
purposes.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
b. ShareAlike.
|
||||||
|
|
||||||
|
In addition to the conditions in Section 3(a), if You Share
|
||||||
|
Adapted Material You produce, the following conditions also apply.
|
||||||
|
|
||||||
|
1. The Adapter's License You apply must be a Creative Commons
|
||||||
|
license with the same License Elements, this version or
|
||||||
|
later, or a BY-NC-SA Compatible License.
|
||||||
|
|
||||||
|
2. You must include the text of, or the URI or hyperlink to, the
|
||||||
|
Adapter's License You apply. You may satisfy this condition
|
||||||
|
in any reasonable manner based on the medium, means, and
|
||||||
|
context in which You Share Adapted Material.
|
||||||
|
|
||||||
|
3. You may not offer or impose any additional or different terms
|
||||||
|
or conditions on, or apply any Effective Technological
|
||||||
|
Measures to, Adapted Material that restrict exercise of the
|
||||||
|
rights granted under the Adapter's License You apply.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database for NonCommercial purposes
|
||||||
|
only;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material,
|
||||||
|
including for purposes of Section 3(b); and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
21
README.md
21
README.md
@@ -1,7 +1,13 @@
|
|||||||
# Computing:Box
|
# Computing:Box
|
||||||
An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Computing Curriculum
|
An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Computing Curriculum
|
||||||
|
|
||||||

|
### Available at:
|
||||||
|
- [Personal Gitea Instance](https://git.adcmnetworks.co.uk/alexander.lyall/computing-box),
|
||||||
|
- [GitHub](https://github.com/MrLyallCSIT/Computing-Box)
|
||||||
|
- [The Official Computing:Box Website](https://www.computingbox.co.uk)
|
||||||
|
|
||||||
|
|
||||||
|
<img src="/assets/img/ComputingBox-Logo.png" alt="Computing:Box Logo" width="250px" height="250px"/>
|
||||||
|
|
||||||
## Upcoming Features
|
## Upcoming Features
|
||||||
### Original Bit:Box Features (October 2024)
|
### Original Bit:Box Features (October 2024)
|
||||||
@@ -30,4 +36,15 @@ An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Com
|
|||||||
- [ ] Computer Components Simulator
|
- [ ] Computer Components Simulator
|
||||||
|
|
||||||
## 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> February 2025
|
## Version 2.0 Release Date (Goal): 1<sup>st</sup> April 2026
|
||||||
|
|
||||||
|
Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]
|
||||||
|
|
||||||
|
This work is licensed under a
|
||||||
|
[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa].
|
||||||
|
|
||||||
|
[![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa]
|
||||||
|
|
||||||
|
[cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
|
[cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png
|
||||||
|
[cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "computing-box",
|
"name": "computing-box",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^5.16.5"
|
"astro": "^5.16.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/compiler": {
|
"node_modules/@astrojs/compiler": {
|
||||||
@@ -1496,6 +1496,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
|
||||||
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
|
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -1649,9 +1650,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astro": {
|
"node_modules/astro": {
|
||||||
"version": "5.16.5",
|
"version": "5.16.6",
|
||||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.6.tgz",
|
||||||
"integrity": "sha512-QeuM4xzTR0QuXFDNlGVW0BW7rcquKFIkylaPeM4ufii0/RRiPTYtwxDYVZ3KfiMRuuc+nbLD0214kMKTvz/yvQ==",
|
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.13.0",
|
"@astrojs/compiler": "^2.13.0",
|
||||||
@@ -4170,7 +4171,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Computing:Box",
|
"name": "computing-box",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.0.0 Alpha",
|
"version": "0.0.1",
|
||||||
"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.5"
|
"astro": "^5.16.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
public/css/tools/binary.css
Normal file
137
public/css/tools/binary.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/* ---------- DSEG7 font ---------- */
|
||||||
|
/* Put your font file here:
|
||||||
|
public/fonts/DSEG7Classic-Regular.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: "DSEG7ClassicRegular";
|
||||||
|
src: url("/fonts/DSEG7Classic-Regular.woff2") format("woff2");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Layout ---------- */
|
||||||
|
.tool-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #0b0f14;
|
||||||
|
color: #e7eaf0;
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
width: min(1100px, 100%);
|
||||||
|
background: #111824;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-header h1 { margin: 0 0 .25rem 0; font-size: 1.4rem; }
|
||||||
|
.tool-header p { margin: 0 0 1rem 0; opacity: .85; }
|
||||||
|
|
||||||
|
.display-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: .75rem;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-box {
|
||||||
|
background: #0b0f14;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-label { font-size: .9rem; opacity: .8; margin-bottom: .25rem; }
|
||||||
|
|
||||||
|
.sevenseg {
|
||||||
|
font-family: "DSEG7ClassicRegular", monospace;
|
||||||
|
font-size: clamp(2rem, 4vw, 3.2rem);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons under denary/binary (your request) */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Simple MD3-ish buttons ---------- */
|
||||||
|
.md3-btn {
|
||||||
|
border: 1px solid rgba(255,255,255,0.16);
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: #e7eaf0;
|
||||||
|
padding: .6rem .9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.md3-btn:hover { background: rgba(255,255,255,0.10); }
|
||||||
|
.md3-btn--tonal { background: rgba(255,255,255,0.10); }
|
||||||
|
|
||||||
|
/* ---------- Switches row ---------- */
|
||||||
|
.switch-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, minmax(90px, 1fr));
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-col {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-label { opacity: .85; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ---------- “Light switch” rocker ---------- */
|
||||||
|
.rocker {
|
||||||
|
position: relative;
|
||||||
|
width: 70px;
|
||||||
|
height: 46px;
|
||||||
|
display: inline-block;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rocker input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rocker-body {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #1a2331;
|
||||||
|
border: 1px solid rgba(255,255,255,0.14);
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the “toggle” */
|
||||||
|
.rocker-body::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 6px;
|
||||||
|
width: 58px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255,255,255,0.20);
|
||||||
|
transition: transform 180ms ease, background 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ON position */
|
||||||
|
.rocker input:checked + .rocker-body::after {
|
||||||
|
transform: translateY(16px);
|
||||||
|
background: rgba(255,255,255,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.switch-row { grid-template-columns: repeat(4, minmax(90px, 1fr)); }
|
||||||
|
}
|
||||||
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 732 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 732 KiB |
115
public/js/binary/unsigned-binary.js
Normal file
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
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
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); }
|
||||||
@@ -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>
|
|
||||||
381
src/components/tools/BinarySimulator.astro
Normal file
381
src/components/tools/BinarySimulator.astro
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
---
|
||||||
|
const { mode = "unsigned", defaultBits = 8 } = Astro.props;
|
||||||
|
|
||||||
|
// For unsigned: min 1 bit, max 16 bits (tweak if you want)
|
||||||
|
const minBits = 4;
|
||||||
|
const maxBits = 16;
|
||||||
|
const initialBits = Math.min(Math.max(defaultBits, minBits), maxBits);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="binary-sim" data-mode={mode} data-bits={initialBits}>
|
||||||
|
<div class="top">
|
||||||
|
<div class="left-brand">
|
||||||
|
<!-- Replace with your logo/img -->
|
||||||
|
<div class="brand-box" aria-hidden="true">Computing:Box</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="display" aria-live="polite">
|
||||||
|
<div class="label">DENARY</div>
|
||||||
|
<div id="denaryNumber" class="value">0</div>
|
||||||
|
|
||||||
|
<div class="label">BINARY</div>
|
||||||
|
<div id="binaryNumber" class="value mono">00000000</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" type="button" data-action="custom-binary">Custom Binary</button>
|
||||||
|
<button class="btn" type="button" data-action="custom-denary">Custom Denary</button>
|
||||||
|
<button class="btn" type="button" data-action="shift-left">Left Shift</button>
|
||||||
|
<button class="btn" type="button" data-action="shift-right">Right Shift</button>
|
||||||
|
|
||||||
|
<div class="bits-control">
|
||||||
|
<div class="bits-title">Bits</div>
|
||||||
|
<div class="bits-buttons">
|
||||||
|
<button class="btn small" type="button" data-action="bits-minus" aria-label="Decrease bits">−</button>
|
||||||
|
<span id="bitsCount" class="bits-count">{initialBits}</span>
|
||||||
|
<button class="btn small" type="button" data-action="bits-plus" aria-label="Increase bits">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bitsRow" class="bits-row" aria-label="Binary bit switches"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
const root = document.currentScript.closest(".binary-sim");
|
||||||
|
const bitsRow = root.querySelector("#bitsRow");
|
||||||
|
const binaryEl = root.querySelector("#binaryNumber");
|
||||||
|
const denaryEl = root.querySelector("#denaryNumber");
|
||||||
|
const bitsCountEl = root.querySelector("#bitsCount");
|
||||||
|
|
||||||
|
let bitCount = parseInt(root.dataset.bits || "8", 10);
|
||||||
|
const minBits = 4;
|
||||||
|
const maxBits = 16;
|
||||||
|
|
||||||
|
// state: array of 0/1, MSB at index 0
|
||||||
|
let bits = new Array(bitCount).fill(0);
|
||||||
|
|
||||||
|
function placeValues(n) {
|
||||||
|
// unsigned place values: [2^(n-1), ..., 2^0]
|
||||||
|
return Array.from({ length: n }, (_, i) => 2 ** (n - 1 - i));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bitsToDenary(bitsArr) {
|
||||||
|
const pv = placeValues(bitsArr.length);
|
||||||
|
return bitsArr.reduce((acc, b, i) => acc + (b ? pv[i] : 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const binaryStr = bits.join("");
|
||||||
|
binaryEl.textContent = binaryStr;
|
||||||
|
denaryEl.textContent = String(bitsToDenary(bits));
|
||||||
|
bitsCountEl.textContent = String(bitCount);
|
||||||
|
|
||||||
|
const pv = placeValues(bitCount);
|
||||||
|
|
||||||
|
bitsRow.innerHTML = pv.map((value, i) => {
|
||||||
|
const id = `bit_${bitCount}_${i}`;
|
||||||
|
const checked = bits[i] === 1 ? "checked" : "";
|
||||||
|
return `
|
||||||
|
<div class="bit-col">
|
||||||
|
<div class="bulb ${bits[i] ? "on" : "off"}" aria-hidden="true"></div>
|
||||||
|
<div class="place">${value}</div>
|
||||||
|
|
||||||
|
<label class="switch" for="${id}">
|
||||||
|
<input id="${id}" type="checkbox" ${checked} data-index="${i}">
|
||||||
|
<span class="slider" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">Toggle bit value ${value}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBitsFromBinaryString(str) {
|
||||||
|
// allow shorter input; pad left
|
||||||
|
const clean = (str || "").trim();
|
||||||
|
if (!/^[01]+$/.test(clean) || clean.length > bitCount) return false;
|
||||||
|
const padded = clean.padStart(bitCount, "0");
|
||||||
|
bits = padded.split("").map(c => c === "1" ? 1 : 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBitsFromDenary(num) {
|
||||||
|
if (!Number.isInteger(num)) return false;
|
||||||
|
const max = (2 ** bitCount) - 1;
|
||||||
|
if (num < 0 || num > max) return false;
|
||||||
|
|
||||||
|
const bin = num.toString(2).padStart(bitCount, "0");
|
||||||
|
bits = bin.split("").map(c => c === "1" ? 1 : 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftLeft() {
|
||||||
|
bits = bits.slice(1).concat(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftRight() {
|
||||||
|
bits = [0].concat(bits.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeBits(newCount) {
|
||||||
|
const clamped = Math.max(minBits, Math.min(maxBits, newCount));
|
||||||
|
if (clamped === bitCount) return;
|
||||||
|
|
||||||
|
// keep value as best as possible: preserve LSBs when resizing
|
||||||
|
const currentBinary = bits.join("");
|
||||||
|
bitCount = clamped;
|
||||||
|
bits = new Array(bitCount).fill(0);
|
||||||
|
|
||||||
|
const take = currentBinary.slice(-bitCount);
|
||||||
|
setBitsFromBinaryString(take);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch input handler
|
||||||
|
bitsRow.addEventListener("change", (e) => {
|
||||||
|
const input = e.target;
|
||||||
|
if (!(input instanceof HTMLInputElement)) return;
|
||||||
|
const idx = parseInt(input.dataset.index || "-1", 10);
|
||||||
|
if (idx < 0) return;
|
||||||
|
bits[idx] = input.checked ? 1 : 0;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button actions
|
||||||
|
root.querySelector(".actions").addEventListener("click", (e) => {
|
||||||
|
const btn = e.target.closest("button[data-action]");
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
|
||||||
|
if (action === "custom-binary") {
|
||||||
|
const entered = prompt(`Enter a ${bitCount}-bit binary value (0s and 1s):`, bits.join(""));
|
||||||
|
if (entered === null) return;
|
||||||
|
if (!setBitsFromBinaryString(entered)) {
|
||||||
|
alert(`Invalid binary. Use only 0 and 1, up to ${bitCount} digits.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "custom-denary") {
|
||||||
|
const max = (2 ** bitCount) - 1;
|
||||||
|
const entered = prompt(`Enter a denary value (0 to ${max}):`, String(bitsToDenary(bits)));
|
||||||
|
if (entered === null) return;
|
||||||
|
const num = Number.parseInt(entered, 10);
|
||||||
|
if (!setBitsFromDenary(num)) {
|
||||||
|
alert(`Invalid denary. Enter a whole number from 0 to ${max}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "shift-left") { shiftLeft(); render(); }
|
||||||
|
if (action === "shift-right") { shiftRight(); render(); }
|
||||||
|
|
||||||
|
if (action === "bits-minus") { resizeBits(bitCount - 1); render(); }
|
||||||
|
if (action === "bits-plus") { resizeBits(bitCount + 1); render(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial
|
||||||
|
render();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Layout */
|
||||||
|
.binary-sim {
|
||||||
|
padding: 2rem 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr 220px;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.top {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand placeholder */
|
||||||
|
.brand-box {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
opacity: 0.9;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display */
|
||||||
|
.display {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin: 0.25rem 0 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover { background: rgba(255,255,255,0.12); }
|
||||||
|
|
||||||
|
.btn.small {
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bits-control {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bits-title { opacity: 0.8; margin-bottom: 0.35rem; }
|
||||||
|
|
||||||
|
.bits-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bits-count {
|
||||||
|
min-width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bits row */
|
||||||
|
.bits-row {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-col {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place {
|
||||||
|
margin: 0.5rem 0 0.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulb */
|
||||||
|
.bulb {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin: 0 auto 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
.bulb.off { opacity: 0.15; }
|
||||||
|
.bulb.on { opacity: 1; }
|
||||||
|
|
||||||
|
/* Light switch (accessible checkbox) */
|
||||||
|
.switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 36px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
width: 64px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.10);
|
||||||
|
border: 1px solid rgba(255,255,255,0.14);
|
||||||
|
position: relative;
|
||||||
|
transition: transform 120ms ease, background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
|
transition: transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .slider {
|
||||||
|
background: rgba(255,255,255,0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .slider::after {
|
||||||
|
transform: translateX(28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* focus */
|
||||||
|
.switch input:focus-visible + .slider {
|
||||||
|
outline: 3px solid rgba(255,255,255,0.35);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden; clip: rect(0,0,0,0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,118 +1,17 @@
|
|||||||
---
|
---
|
||||||
|
// src/layouts/BaseLayout.astro
|
||||||
const { title = "Computing:Box" } = Astro.props;
|
const { title = "Computing:Box" } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<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>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
<style>
|
<link rel="stylesheet" href="/fonts/DSEG7Classic-Regular.css">
|
||||||
:root{
|
</head>
|
||||||
--nav-h: 108px; /* 3x-ish height */
|
<body>
|
||||||
--bg: #1f2027;
|
<slot />
|
||||||
--text: #e8e8ee;
|
</body>
|
||||||
--muted: #a9acb8;
|
</html>
|
||||||
--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,112 +1,642 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
/**
|
||||||
import "../styles/binary.css";
|
* src/pages/binary.astro
|
||||||
|
* Single-file binary simulator (Unsigned + Two's complement)
|
||||||
|
* Bit width: 4..64
|
||||||
|
*
|
||||||
|
* Requires:
|
||||||
|
* public/fonts/DSEG7Classic-Regular.woff
|
||||||
|
* public/fonts/DSEG7Classic-Regular.ttf
|
||||||
|
*/
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Binary Simulator">
|
<html lang="en">
|
||||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
<head>
|
||||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
<meta charset="utf-8" />
|
||||||
<span class="toolboxLabel">TOOLBOX</span>
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
</button>
|
<title>Binary | Computing:Box</title>
|
||||||
|
|
||||||
<main class="wrap">
|
<style>
|
||||||
<section class="topGrid">
|
:root{
|
||||||
<!-- LEFT -->
|
--bg: #1f2027;
|
||||||
<div>
|
--panel: #22242d;
|
||||||
<div class="readout">
|
--panel2:#262833;
|
||||||
<div class="label">Denary</div>
|
--text: #e8e8ee;
|
||||||
<div id="denaryNumber" class="num denaryValue">0</div>
|
--muted: #a9acb8;
|
||||||
|
--accent: #33ff7a;
|
||||||
|
--accent-dim: rgba(51,255,122,.15);
|
||||||
|
--line: rgba(255,255,255,.12);
|
||||||
|
}
|
||||||
|
|
||||||
<div class="label">Binary</div>
|
@font-face{
|
||||||
<div id="binaryNumber" class="num binaryValue">0000 0000</div>
|
font-family: "DSEG7ClassicRegular";
|
||||||
|
src:
|
||||||
|
url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
||||||
|
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap{
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout: readout left, settings panel right (like your screenshot) */
|
||||||
|
.layout{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: 1fr 360px;
|
||||||
|
gap: 22px;
|
||||||
|
align-items:start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readout{
|
||||||
|
text-align:center;
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label{
|
||||||
|
letter-spacing: .18em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num{
|
||||||
|
font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent);
|
||||||
|
text-shadow: 0 0 18px var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value{
|
||||||
|
font-size: 70px;
|
||||||
|
line-height: 1.0;
|
||||||
|
margin: 6px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary{
|
||||||
|
font-size: 54px;
|
||||||
|
letter-spacing: .12em;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons under readout (as requested) */
|
||||||
|
.controls{
|
||||||
|
margin-top: 16px;
|
||||||
|
display:flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content:center;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn{
|
||||||
|
background: rgba(255,255,255,.06);
|
||||||
|
border: 1px solid rgba(255,255,255,.14);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.btn:active{ transform: translateY(1px); }
|
||||||
|
|
||||||
|
/* Settings panel cards */
|
||||||
|
.panel{
|
||||||
|
background: rgba(255,255,255,.04);
|
||||||
|
border: 1px solid rgba(255,255,255,.10);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 16px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.panelTitle{
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: .16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.panelRow{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.hint{
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reusable toggle switch (for MODE and for each BIT) */
|
||||||
|
.switch{
|
||||||
|
position: relative;
|
||||||
|
width: 56px;
|
||||||
|
height: 34px;
|
||||||
|
display:inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.switch input{
|
||||||
|
opacity:0;
|
||||||
|
width:0;
|
||||||
|
height:0;
|
||||||
|
}
|
||||||
|
.slider{
|
||||||
|
position:absolute;
|
||||||
|
inset:0;
|
||||||
|
background: rgba(255,255,255,.10);
|
||||||
|
border: 1px solid rgba(255,255,255,.14);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: .18s ease;
|
||||||
|
}
|
||||||
|
.slider::before{
|
||||||
|
content:"";
|
||||||
|
position:absolute;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
left: 3px;
|
||||||
|
top: 2px;
|
||||||
|
background: rgba(255,255,255,.92);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: .18s ease;
|
||||||
|
}
|
||||||
|
.switch input:checked + .slider{
|
||||||
|
background: rgba(51,255,122,.20);
|
||||||
|
border-color: rgba(51,255,122,.55);
|
||||||
|
}
|
||||||
|
.switch input:checked + .slider::before{
|
||||||
|
transform: translateX(22px);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bit width controls */
|
||||||
|
.bwControls{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: 44px 1fr 44px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items:center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.bwBtn{
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255,255,255,.14);
|
||||||
|
background: rgba(255,255,255,.06);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bwBtn:active{ transform: translateY(1px); }
|
||||||
|
|
||||||
|
.bwInput{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255,255,255,.05);
|
||||||
|
border: 1px solid rgba(255,255,255,.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.bwLabel{
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.bwField{
|
||||||
|
width: 100px;
|
||||||
|
text-align:right;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: "DSEG7ClassicRegular", ui-monospace, monospace;
|
||||||
|
font-size: 26px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.bwField::-webkit-outer-spin-button,
|
||||||
|
.bwField::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||||
|
.bwField[type=number] { -moz-appearance: textfield; }
|
||||||
|
|
||||||
|
/* Bits grid */
|
||||||
|
.bits{
|
||||||
|
margin-top: 34px;
|
||||||
|
padding-top: 26px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
display:grid;
|
||||||
|
gap: 18px;
|
||||||
|
align-items:end;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
align-items:center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bitVal{
|
||||||
|
font-size: 34px;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulb (must come back!) */
|
||||||
|
.bulb{
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,.08);
|
||||||
|
border: 1px solid rgba(255,255,255,.12);
|
||||||
|
box-shadow: none;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.bulb.on{
|
||||||
|
background: #ffd86b;
|
||||||
|
border-color: rgba(255,216,107,.7);
|
||||||
|
box-shadow: 0 0 18px rgba(255,216,107,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1000px){
|
||||||
|
.layout{ grid-template-columns: 1fr; }
|
||||||
|
.value{ font-size: 60px; }
|
||||||
|
.binary{ font-size: 44px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 900px){
|
||||||
|
.value{ font-size: 54px; }
|
||||||
|
.binary{ font-size: 36px; }
|
||||||
|
.btn{ min-width: 140px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="layout">
|
||||||
|
<!-- LEFT: readout + buttons -->
|
||||||
|
<div>
|
||||||
|
<div class="readout">
|
||||||
|
<div class="label">Denary</div>
|
||||||
|
<div id="denaryNumber" class="value num">0</div>
|
||||||
|
|
||||||
|
<div class="label">Binary</div>
|
||||||
|
<div id="binaryNumber" class="value binary num">00000000</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||||
|
<button class="btn" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||||
|
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
||||||
|
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bits grid (built by JS so bit-width 4..64 works) -->
|
||||||
|
<section id="bitsGrid" class="bits" aria-label="Bit switches"></section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<!-- RIGHT: settings panel -->
|
||||||
|
<aside>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panelTitle">Mode</div>
|
||||||
|
<div class="panelRow">
|
||||||
|
<span style="font-weight:800;">Unsigned</span>
|
||||||
|
|
||||||
<section class="bitsWrap" aria-label="Bit switches">
|
<!-- MODE TOGGLE uses SAME switch style -->
|
||||||
<div class="bitsGrid" id="bitsGrid"></div>
|
<label class="switch" aria-label="Toggle Two's complement mode">
|
||||||
</section>
|
<input id="modeToggle" type="checkbox" />
|
||||||
</div>
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<!-- RIGHT TOOLBOX -->
|
<span style="font-weight:800;">Two’s complement</span>
|
||||||
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
|
</div>
|
||||||
<!-- SETTINGS -->
|
<div class="hint">
|
||||||
<div class="card">
|
Tip: In two’s complement, the left-most bit (MSB) represents a negative value.
|
||||||
<div class="cardTitle">Settings</div>
|
</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>
|
||||||
|
|
||||||
<div class="hint" id="modeHint">
|
<div class="panel">
|
||||||
Tip: In unsigned binary, all bits represent positive values.
|
<div class="panelTitle">Bit width</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subCard">
|
<div class="bwControls">
|
||||||
<div class="subTitle">Bit width</div>
|
<button class="bwBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||||
<div class="bitWidthRow">
|
|
||||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
|
||||||
|
|
||||||
<div class="bitInputWrap">
|
<div class="bwInput">
|
||||||
<div class="bitInputLabel">Bits</div>
|
<div class="bwLabel">Bits</div>
|
||||||
<input
|
<input
|
||||||
id="bitsInput"
|
id="bitsField"
|
||||||
class="bitInput"
|
class="bwField"
|
||||||
type="number"
|
type="number"
|
||||||
inputmode="numeric"
|
min="4"
|
||||||
min="1"
|
|
||||||
max="64"
|
max="64"
|
||||||
step="1"
|
step="1"
|
||||||
value="8"
|
value="8"
|
||||||
aria-label="Number of bits"
|
inputmode="numeric"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
<button class="bwBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hint">
|
||||||
|
Minimum 4 bits, maximum 64 bits.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- CUSTOM -->
|
<script type="module">
|
||||||
<div class="card">
|
// ---------- State ----------
|
||||||
<div class="cardTitle">Custom</div>
|
let bitsCount = 8; // 4..64
|
||||||
|
let isTwos = false; // false = unsigned
|
||||||
|
let bitStates = []; // MSB -> LSB booleans
|
||||||
|
|
||||||
<div class="twoBtnRow">
|
// ---------- Helpers ----------
|
||||||
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
|
function clamp(n, min, max){ return Math.min(max, Math.max(min, n)); }
|
||||||
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
function pow2(n){
|
||||||
Random
|
// for up to 64 bits, Number is still OK for *visual simulator*.
|
||||||
</button>
|
// if you later want exact 64-bit maths, swap to BigInt end-to-end.
|
||||||
|
return 2 ** n;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
function getPlaceValues(){
|
||||||
</div>
|
// MSB -> LSB
|
||||||
|
// Unsigned: [2^(n-1), ..., 1]
|
||||||
|
// Two's: [-2^(n-1), 2^(n-2), ..., 1]
|
||||||
|
const vals = [];
|
||||||
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const power = bitsCount - 1 - i;
|
||||||
|
if (isTwos && i === 0){
|
||||||
|
vals.push(-pow2(power));
|
||||||
|
} else {
|
||||||
|
vals.push(pow2(power));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals;
|
||||||
|
}
|
||||||
|
|
||||||
<!-- TOOLS -->
|
function binaryString(){
|
||||||
<div class="card">
|
return bitStates.map(b => (b ? "1" : "0")).join("");
|
||||||
<div class="cardTitle">Tools</div>
|
}
|
||||||
|
|
||||||
<div class="toolsTopRow">
|
function denaryValue(){
|
||||||
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
const values = getPlaceValues();
|
||||||
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
|
let sum = 0;
|
||||||
</div>
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
if (bitStates[i]) sum += values[i];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="twoBtnRow">
|
function rangeForMode(){
|
||||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
if (!isTwos){
|
||||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
return { min: 0, max: pow2(bitsCount) - 1 };
|
||||||
</div>
|
}
|
||||||
|
return { min: -pow2(bitsCount - 1), max: pow2(bitsCount - 1) - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
|
function updateReadout(){
|
||||||
</div>
|
document.getElementById("binaryNumber").innerText = binaryString();
|
||||||
</aside>
|
document.getElementById("denaryNumber").innerText = String(denaryValue());
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/binary.js"></script>
|
// bulb on/off updates:
|
||||||
</BaseLayout>
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const bulb = document.getElementById(`bulb-${i}`);
|
||||||
|
if (bulb) bulb.classList.toggle("on", !!bitStates[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DOM build for bits grid ----------
|
||||||
|
function setBitsGridColumns(){
|
||||||
|
const grid = document.getElementById("bitsGrid");
|
||||||
|
// make it adapt (up to 16 per row max, then wraps)
|
||||||
|
const cols = clamp(bitsCount, 4, 16);
|
||||||
|
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBitsGrid(){
|
||||||
|
const grid = document.getElementById("bitsGrid");
|
||||||
|
grid.innerHTML = "";
|
||||||
|
|
||||||
|
setBitsGridColumns();
|
||||||
|
|
||||||
|
const values = getPlaceValues();
|
||||||
|
|
||||||
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const v = values[i];
|
||||||
|
|
||||||
|
const bit = document.createElement("div");
|
||||||
|
bit.className = "bit";
|
||||||
|
|
||||||
|
const bulb = document.createElement("div");
|
||||||
|
bulb.className = "bulb";
|
||||||
|
bulb.id = `bulb-${i}`;
|
||||||
|
bulb.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
const val = document.createElement("div");
|
||||||
|
val.className = "bitVal num";
|
||||||
|
// show absolute label for MSB in twos? No — show the actual negative value (clear to students).
|
||||||
|
val.textContent = String(v);
|
||||||
|
|
||||||
|
// IMPORTANT: bit switch must be the SAME style as MODE switch
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "switch";
|
||||||
|
label.setAttribute("aria-label", `Toggle bit ${v}`);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.dataset.index = String(i);
|
||||||
|
input.checked = !!bitStates[i];
|
||||||
|
|
||||||
|
const slider = document.createElement("span");
|
||||||
|
slider.className = "slider";
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
bitStates[i] = input.checked;
|
||||||
|
updateReadout();
|
||||||
|
});
|
||||||
|
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(slider);
|
||||||
|
|
||||||
|
bit.appendChild(bulb);
|
||||||
|
bit.appendChild(val);
|
||||||
|
bit.appendChild(label);
|
||||||
|
grid.appendChild(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReadout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Set state from binary / denary ----------
|
||||||
|
function setFromBinary(bin){
|
||||||
|
const clean = String(bin).replace(/\s+/g, "");
|
||||||
|
if (!/^[01]+$/.test(clean)) return false;
|
||||||
|
|
||||||
|
const padded = clean.slice(-bitsCount).padStart(bitsCount, "0");
|
||||||
|
bitStates = [...padded].map(ch => ch === "1");
|
||||||
|
|
||||||
|
// update toggles without rebuilding
|
||||||
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const toggle = document.querySelector(`input[type="checkbox"][data-index="${i}"]`);
|
||||||
|
if (toggle) toggle.checked = bitStates[i];
|
||||||
|
}
|
||||||
|
updateReadout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFromDenary(n){
|
||||||
|
const num = Number(n);
|
||||||
|
if (!Number.isInteger(num)) return false;
|
||||||
|
|
||||||
|
const { min, max } = rangeForMode();
|
||||||
|
if (num < min || num > max) return false;
|
||||||
|
|
||||||
|
// convert to bit pattern
|
||||||
|
let unsignedVal;
|
||||||
|
if (!isTwos){
|
||||||
|
unsignedVal = num;
|
||||||
|
} else {
|
||||||
|
// two's complement representation
|
||||||
|
unsignedVal = num < 0 ? (pow2(bitsCount) + num) : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build MSB->LSB bits from unsignedVal
|
||||||
|
const newBits = [];
|
||||||
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const power = bitsCount - 1 - i;
|
||||||
|
const bit = Math.floor(unsignedVal / pow2(power)) % 2;
|
||||||
|
newBits.push(bit === 1);
|
||||||
|
}
|
||||||
|
bitStates = newBits;
|
||||||
|
|
||||||
|
for (let i = 0; i < bitsCount; i++){
|
||||||
|
const toggle = document.querySelector(`input[type="checkbox"][data-index="${i}"]`);
|
||||||
|
if (toggle) toggle.checked = bitStates[i];
|
||||||
|
}
|
||||||
|
updateReadout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Shifts ----------
|
||||||
|
function shiftLeft(){
|
||||||
|
// logical left shift: drop MSB, add 0 at LSB
|
||||||
|
bitStates = bitStates.slice(1).concat(false);
|
||||||
|
buildBitsGrid(); // rebuild keeps labels correct if mode changed (also updates bulbs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftRight(){
|
||||||
|
if (!isTwos){
|
||||||
|
// logical right shift
|
||||||
|
bitStates = [false].concat(bitStates.slice(0, -1));
|
||||||
|
} else {
|
||||||
|
// arithmetic right shift (preserve MSB)
|
||||||
|
const msb = bitStates[0];
|
||||||
|
bitStates = [msb].concat(bitStates.slice(0, -1));
|
||||||
|
}
|
||||||
|
buildBitsGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Bit width ----------
|
||||||
|
function applyBitsCount(next){
|
||||||
|
const wanted = clamp(Number(next) || 8, 4, 64);
|
||||||
|
if (wanted === bitsCount) return;
|
||||||
|
|
||||||
|
// keep the right-most (LSB) bits when resizing (feels natural)
|
||||||
|
const old = binaryString();
|
||||||
|
bitsCount = wanted;
|
||||||
|
|
||||||
|
// rebuild state from old binary, keeping LSBs
|
||||||
|
const padded = old.slice(-bitsCount).padStart(bitsCount, "0");
|
||||||
|
bitStates = [...padded].map(ch => ch === "1");
|
||||||
|
|
||||||
|
document.getElementById("bitsField").value = String(bitsCount);
|
||||||
|
buildBitsGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Mode toggle ----------
|
||||||
|
function applyMode(nextIsTwos){
|
||||||
|
const prevDenary = denaryValue(); // keep the *value* if possible
|
||||||
|
isTwos = !!nextIsTwos;
|
||||||
|
|
||||||
|
// Try to keep the denary value, but clamp if it becomes invalid in the new mode
|
||||||
|
const { min, max } = rangeForMode();
|
||||||
|
const kept = clamp(prevDenary, min, max);
|
||||||
|
setFromDenary(kept);
|
||||||
|
|
||||||
|
buildBitsGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Wire up UI ----------
|
||||||
|
function wireUI(){
|
||||||
|
document.getElementById("btnShiftLeft").addEventListener("click", shiftLeft);
|
||||||
|
document.getElementById("btnShiftRight").addEventListener("click", shiftRight);
|
||||||
|
|
||||||
|
document.getElementById("btnCustomBinary").addEventListener("click", () => {
|
||||||
|
const val = prompt(`Enter a binary number (up to ${bitsCount} bits):`);
|
||||||
|
if (val === null) return;
|
||||||
|
if (!setFromBinary(val)) alert("Invalid input. Use only 0 and 1.");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btnCustomDenary").addEventListener("click", () => {
|
||||||
|
const { min, max } = rangeForMode();
|
||||||
|
const val = prompt(`Enter a denary number (${min} to ${max}):`);
|
||||||
|
if (val === null) return;
|
||||||
|
if (!setFromDenary(val)) alert(`Invalid input. Enter an integer from ${min} to ${max}.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mode toggle
|
||||||
|
const modeToggle = document.getElementById("modeToggle");
|
||||||
|
modeToggle.addEventListener("change", () => applyMode(modeToggle.checked));
|
||||||
|
|
||||||
|
// Bit width +/- and manual entry
|
||||||
|
document.getElementById("btnBitsDown").addEventListener("click", () => applyBitsCount(bitsCount - 1));
|
||||||
|
document.getElementById("btnBitsUp").addEventListener("click", () => applyBitsCount(bitsCount + 1));
|
||||||
|
|
||||||
|
const bitsField = document.getElementById("bitsField");
|
||||||
|
bitsField.addEventListener("change", () => applyBitsCount(bitsField.value));
|
||||||
|
bitsField.addEventListener("blur", () => applyBitsCount(bitsField.value));
|
||||||
|
bitsField.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") applyBitsCount(bitsField.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Init ----------
|
||||||
|
function init(){
|
||||||
|
// default: 8-bit unsigned
|
||||||
|
bitsCount = 8;
|
||||||
|
isTwos = false;
|
||||||
|
bitStates = Array(bitsCount).fill(false);
|
||||||
|
|
||||||
|
document.getElementById("modeToggle").checked = false;
|
||||||
|
document.getElementById("bitsField").value = String(bitsCount);
|
||||||
|
|
||||||
|
wireUI();
|
||||||
|
buildBitsGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -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,419 +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;
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
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 formatBinaryGrouped() {
|
|
||||||
let s = "";
|
|
||||||
for (let i = bitCount - 1; i >= 0; i--) {
|
|
||||||
s += bits[i] ? "1" : "0";
|
|
||||||
const posFromRight = (bitCount - i);
|
|
||||||
if (i !== 0 && posFromRight % 4 === 0) s += " ";
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
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 = "";
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
UI UPDATE
|
|
||||||
----------------------------- */
|
|
||||||
function updateBitLabels() {
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
|
||||||
const label = document.getElementById(`bitLabel-${i}`);
|
|
||||||
if (!label) continue;
|
|
||||||
|
|
||||||
// Keep label on ONE LINE (no wrapping)
|
|
||||||
label.style.whiteSpace = "nowrap";
|
|
||||||
|
|
||||||
if (isTwosMode() && i === bitCount - 1) {
|
|
||||||
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;
|
|
||||||
bulb.classList.toggle("on", bits[i] === true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateReadout() {
|
|
||||||
if (!denaryEl || !binaryEl) return;
|
|
||||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
|
||||||
binaryEl.textContent = formatBinaryGrouped();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
updateModeHint();
|
|
||||||
updateBitLabels();
|
|
||||||
syncSwitchesToBits();
|
|
||||||
updateBulbs();
|
|
||||||
updateReadout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
INPUT SETTERS
|
|
||||||
----------------------------- */
|
|
||||||
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() {
|
|
||||||
// Unsigned: logical right shift (MSB becomes 0)
|
|
||||||
// Two's complement: arithmetic right shift (MSB preserved)
|
|
||||||
const msb = bits[bitCount - 1];
|
|
||||||
|
|
||||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
|
||||||
|
|
||||||
bits[bitCount - 1] = isTwosMode() ? msb : 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);
|
|
||||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
RANDOM (with running pulse + longer run)
|
|
||||||
----------------------------- */
|
|
||||||
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); // 2^n
|
|
||||||
const u = cryptoRandomBigInt(span);
|
|
||||||
unsignedBigIntToBits(u);
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function runRandomBriefly() {
|
|
||||||
if (randomTimer) {
|
|
||||||
clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pulse while running
|
|
||||||
btnRandom?.classList.add("is-running");
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
const durationMs = 1125; // 25% longer than 900ms
|
|
||||||
const tickMs = 80;
|
|
||||||
|
|
||||||
randomTimer = setInterval(() => {
|
|
||||||
setRandomOnce();
|
|
||||||
if (Date.now() - start >= durationMs) {
|
|
||||||
clearInterval(randomTimer);
|
|
||||||
randomTimer = null;
|
|
||||||
btnRandom?.classList.remove("is-running");
|
|
||||||
}
|
|
||||||
}, tickMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
BIT WIDTH
|
|
||||||
----------------------------- */
|
|
||||||
function setBitWidth(n) {
|
|
||||||
buildBits(clampInt(n, 1, 64));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
TOOLBOX VISIBILITY
|
|
||||||
----------------------------- */
|
|
||||||
function setToolboxVisible(isVisible) {
|
|
||||||
if (!toolboxPanel) return;
|
|
||||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
|
||||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
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)} to ${twosMax(bitCount)}):`
|
|
||||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
|
||||||
);
|
|
||||||
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 = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
|
||||||
setToolboxVisible(!isOpen);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* -----------------------------
|
|
||||||
INIT
|
|
||||||
----------------------------- */
|
|
||||||
updateModeHint();
|
|
||||||
buildBits(bitCount);
|
|
||||||
setToolboxVisible(true);
|
|
||||||
})();
|
|
||||||
54
src/scripts/unsignedBinary.js
Normal file
54
src/scripts/unsignedBinary.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
let bits = [128,64,32,16,8,4,2,1];
|
||||||
|
let state = Array(8).fill(0);
|
||||||
|
|
||||||
|
const denaryEl = document.getElementById("denaryNumber");
|
||||||
|
const binaryEl = document.getElementById("binaryNumber");
|
||||||
|
const bitEls = document.querySelectorAll(".bit");
|
||||||
|
|
||||||
|
bitEls.forEach((el, i) => {
|
||||||
|
el.addEventListener("click", () => {
|
||||||
|
state[i] = state[i] ? 0 : 1;
|
||||||
|
el.classList.toggle("on");
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const denary = state.reduce((sum, bit, i) => sum + bit * bits[i], 0);
|
||||||
|
denaryEl.textContent = denary;
|
||||||
|
binaryEl.textContent = state.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestBinary() {
|
||||||
|
const input = prompt("Enter 8-bit binary:");
|
||||||
|
if (!/^[01]{8}$/.test(input)) return;
|
||||||
|
state = input.split("").map(Number);
|
||||||
|
bitEls.forEach((el,i)=>el.classList.toggle("on",state[i]));
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDenary() {
|
||||||
|
const input = parseInt(prompt("Enter denary (0–255)"),10);
|
||||||
|
if (isNaN(input) || input < 0 || input > 255) return;
|
||||||
|
|
||||||
|
let value = input;
|
||||||
|
state = bits.map(b => {
|
||||||
|
if (value >= b) {
|
||||||
|
value -= b;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
bitEls.forEach((el,i)=>el.classList.toggle("on",state[i]));
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftBinary(dir) {
|
||||||
|
if (dir === "left") state.shift(), state.push(0);
|
||||||
|
if (dir === "right") state.pop(), state.unshift(0);
|
||||||
|
bitEls.forEach((el,i)=>el.classList.toggle("on",state[i]));
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
52
src/styles/base.css
Normal file
52
src/styles/base.css
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* 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); }
|
||||||
@@ -1,326 +1,68 @@
|
|||||||
/* Binary page styles (moved OUT of binary.astro) */
|
.binary-container {
|
||||||
|
max-width: 1100px;
|
||||||
:root{
|
margin: auto;
|
||||||
--panel-w: 360px;
|
padding: 2rem;
|
||||||
--gap: 22px;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap{
|
.display {
|
||||||
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;
|
text-align: center;
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label{
|
.label {
|
||||||
opacity: .8;
|
font-size: 1.2rem;
|
||||||
letter-spacing: .12em;
|
opacity: 0.7;
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
.number {
|
||||||
.num{
|
font-size: 3rem;
|
||||||
display: inline-block;
|
margin-bottom: 1rem;
|
||||||
width: fit-content;
|
color: #00ff66;
|
||||||
max-width: 100%;
|
|
||||||
white-space: pre-line; /* allows JS \n wraps */
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.denaryValue{
|
.controls {
|
||||||
font-size: 54px;
|
margin-top: 1rem;
|
||||||
margin: 6px 0 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.binaryValue{
|
.controls button {
|
||||||
font-size: 56px;
|
margin: 0.25rem;
|
||||||
margin: 4px 0 18px;
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider{
|
.bits {
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,.10);
|
|
||||||
margin: 14px auto 24px;
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsWrap{
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bitsGrid{
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 24px;
|
grid-template-columns: repeat(8, 1fr);
|
||||||
justify-content: center;
|
gap: 1rem;
|
||||||
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default: a single row of bits (will wrap automatically as bit count grows) */
|
.bit {
|
||||||
.bitsGrid{
|
width: 40px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
height: 80px;
|
||||||
max-width: 1200px;
|
background: #333;
|
||||||
margin: 0 auto;
|
border-radius: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
position: relative;
|
||||||
display: inline-block;
|
cursor: pointer;
|
||||||
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 */
|
.bit::after {
|
||||||
.toolboxToggle{
|
content: "";
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
bottom: 6px;
|
||||||
top: 18px;
|
left: 4px;
|
||||||
z-index: 20;
|
transition: all 0.2s ease;
|
||||||
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 */
|
.bit.on {
|
||||||
.panelCol{
|
background: #00c853;
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--nav-h, 72px) + 18px);
|
|
||||||
align-self: start;
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cards */
|
.bit.on::after {
|
||||||
.card{
|
bottom: 42px;
|
||||||
border: 1px solid rgba(255,255,255,.12);
|
background: #eaffea;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/styles/fonts.css
Normal file
11
src/styles/fonts.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "DSEG7";
|
||||||
|
src: url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dseg {
|
||||||
|
font-family: "DSEG7", monospace;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
43
src/styles/md3-tokens.css
Normal file
43
src/styles/md3-tokens.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user