Compare commits

..

10 Commits

Author SHA1 Message Date
Priec
b5a35ab198 shade on the text is fixed 2026-06-02 17:48:12 +02:00
Priec
7f42516fa3 dron footage in the website ascii animation 2026-06-02 17:43:16 +02:00
Priec
4212d8877e readme ready for prod 2026-06-02 17:38:37 +02:00
Priec
6ccf372f65 chafa is the resolution I actually wanted 2026-06-02 17:17:35 +02:00
Priec
55eb4bbf00 more website fixes done 2026-06-02 17:05:45 +02:00
Priec
1032d20080 website fixes 2026-06-02 17:01:36 +02:00
Priec
0f897354ec background of the main 2026-06-02 16:40:37 +02:00
Priec
695dad519d now the ascii animation is at the top 2026-06-02 14:46:28 +02:00
Priec
462a53853f video 2026-06-01 23:40:17 +02:00
Priec
99f255fc29 erase background 2026-06-01 22:47:24 +02:00
12 changed files with 35978 additions and 163 deletions

81
.gitignore vendored
View File

@@ -1,30 +1,89 @@
# Build artifacts
bin/
node_modules/
# ============================================================================
# Build artifacts — generated by `make video`, `make serve`, `nix build`
# ============================================================================
build/
dist/
*.min.css
*.min.js
out/
bin/
# Editor
# Nix build outputs (symlinks to /nix/store)
result
result-*
result-*.drv
.direnv/
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.venv/
venv/
# Node (if/when we add a Tailwind CLI build step)
node_modules/
.npm/
.yarn/
# ============================================================================
# Generated runtime assets — the .mp4 in video/ IS tracked, the build/ output
# is not. The static/js/mountain.js bundle is *committed* (regeneratable, but
# useful to have a built version in the repo so the site works without
# ffmpeg + chafa installed).
# ============================================================================
# ============================================================================
# Editor / IDE
# ============================================================================
.vscode/
.idea/
*.swp
.DS_Store
*.swo
*.swn
*~
# Helix
.helix/
# ============================================================================
# OS
# ============================================================================
.DS_Store
Thumbs.db
desktop.ini
# ============================================================================
# Logs
# ============================================================================
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pip-log.txt
pip-log/
# Env
# ============================================================================
# Env / secrets (in case a future step needs API keys)
# ============================================================================
.env
.env.local
.env.*.local
# Cache
# ============================================================================
# Caches
# ============================================================================
.cache/
.parcel-cache/
.eslintcache
.pytest_cache/
.ruff_cache/
.mypy_cache/
# Generated
static/css/site.compiled.css
# ============================================================================
# Tailwind (if/when we swap the Play CDN for a standalone build)
# ============================================================================
static/css/dist/
static/css/*.compiled.css
static/css/tailwind.config.js
video/

View File

@@ -1,4 +1,10 @@
.PHONY: help serve size validate clean
.PHONY: help serve size validate tidy video video-list video-frames video-bundle clean
# ASCII render resolution (override on the command line):
# make video NAME=nature1 SIZE=160x48 SCALE=640
SIZE ?= 80x24
SCALE ?= 320
FPS ?= 12
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
@@ -28,5 +34,39 @@ validate: ## Quick HTML sanity check (counts opening vs closing tags)
diff <(echo "$$open") <(echo "$$close") || true; \
done
clean: ## Remove generated artifacts
tidy: ## Run html-tidy on every .html
@for f in *.html; do echo "--- $$f ---"; tidy -e -q $$f || true; done
video: ## Convert a video from video/<name>.mp4 into ASCII bundle. Usage: make video NAME=nature1
@test -n "$(NAME)" || { echo "Usage: make video NAME=<name> (without .mp4)"; exit 1; }
@test -f "video/$(NAME).mp4" || { echo "video/$(NAME).mp4 not found. Available:"; $(MAKE) video-list; exit 1; }
@echo "==> Processing video/$(NAME).mp4"
@$(MAKE) video-frames NAME=$(NAME) --no-print-directory
@$(MAKE) video-bundle NAME=$(NAME) --no-print-directory
@echo "==> Done. static/js/mountain.js is now the ASCII version of $(NAME)."
video-list: ## List available videos in video/
@ls -1 video/*.mp4 2>/dev/null | sed 's|video/||; s|\.mp4$$||' || echo " (no videos found)"
video-frames: ## Extract PNG frames from video/<NAME>.mp4 and chafa each to ASCII
@test -n "$(NAME)" || { echo "NAME is required"; exit 1; }
@mkdir -p build/$(NAME)
@command -v ffmpeg >/dev/null 2>&1 || \
{ echo "ffmpeg not found. Run via: nix develop -c make video-frames NAME=$(NAME)"; exit 1; }
@command -v chafa >/dev/null 2>&1 || \
{ echo "chafa not found. Run via: nix develop -c make video-frames NAME=$(NAME)"; exit 1; }
@echo " extracting frames at $(FPS) fps from video/$(NAME).mp4"
@ffmpeg -y -loglevel error -i video/$(NAME).mp4 -vf "fps=$(FPS),scale=$(SCALE):-1" build/$(NAME)/frame-%03d.png
@echo " chafa: $(SIZE) ascii per frame"
@for f in build/$(NAME)/frame-*.png; do \
chafa -f symbols -c none -s $(SIZE) --symbols ascii --animate off "$$f" > "$${f%.png}.txt"; \
done
@echo " rendered $$(($$(ls build/$(NAME)/frame-*.png | wc -l))) PNG + ASCII frames in build/$(NAME)/"
video-bundle: ## Bundle ASCII frames from build/<NAME>/ into static/js/mountain.js
@test -n "$(NAME)" || { echo "NAME is required"; exit 1; }
@python3 tools/build_mountain_js.py build/$(NAME) static/js/mountain.js
@echo " bundled static/js/mountain.js (regenerated from build/$(NAME)/)"
clean: ## Remove build artefacts
@rm -rf bin/ node_modules/ dist/

127
README.md
View File

@@ -1,125 +1,46 @@
# tui-pages website
Static marketing site for the [`tui-pages`](https://gitlab.com/filipriec/tui-pages) Rust crate.
No build step required — open `index.html` in a browser and you're done.
No build step — it's plain HTML/CSS/JS.
## Stack
| Layer | Tech | Source |
| --- | --- | --- |
| HTML | Hand-written semantic | `*.html` |
| CSS framework | Tailwind CSS v3 (Play CDN) | runtime |
| Components | DaisyUI v4 (prebuilt CSS) | runtime |
| Interactivity | HTMX 2 | runtime |
| Light JS | Alpine.js 3 | runtime |
| Code highlight | highlight.js 11 (Rust, TOML, Bash) | runtime |
| Fonts | Inter + JetBrains Mono (Google Fonts) | runtime |
| Icons | Lucide (inline SVG) | hand-written |
| Page transitions | View Transitions API (native) | n/a |
Everything is loaded from public CDNs. The only thing served from this repo is
the HTML, our small `static/css/site.css` layer, and the SVG assets in
`static/img/`.
## Local development
## Build & serve
```bash
# Just open the file
xdg-open index.html # Linux
open index.html # macOS
# Or serve it (recommended — gives you a stable URL for htmx)
python3 -m http.server 8000
# then visit http://localhost:8000
make serve # serve the working tree on http://localhost:8000
make size # report file sizes
make validate # quick HTML syntax check
```
The `Makefile` wraps the Python server and a few convenience commands:
Just edit the `.html` / `static/` files and refresh.
```bash
make serve # python3 -m http.server 8000
make size # report file sizes
make validate # quick HTML syntax check (search for unclosed tags)
```
## ASCII video background
## Going to production
The full-page background is a video re-rendered into animated ASCII. `ffmpeg`
extracts frames, `chafa` turns each into an ASCII grid, and
`tools/build_mountain_js.py` bundles them into `static/js/mountain.js`, which
the page plays back.
The CDN approach is fine for marketing pages. For better performance
(smaller CSS, no runtime Tailwind compile), swap the Play CDN for the
**Tailwind standalone CLI**:
### Render a video
```bash
# 1. Download the standalone CLI
# https://tailwindcss.com/blog/standalone-cli
# 2. Put the binary in ./bin/tailwindcss
# 3. Create src/site.css that imports Tailwind and DaisyUI:
# @import "tailwindcss";
# @plugin "daisyui";
# 4. Build
./bin/tailwindcss -i src/site.css -o static/css/site.css --minify
```
Then in `index.html` remove the Tailwind Play CDN `<script>` and the DaisyUI
`<link>`, and ensure `/static/css/site.css` is loaded last in `<head>`.
## File layout
```
tui-pages-web/
├── index.html # landing page
├── examples.html # examples gallery
├── 404.html # not found
├── robots.txt # search engine hints
├── sitemap.xml # sitemap
├── static/
│ ├── css/
│ │ └── site.css # our custom layer (small)
│ ├── img/
│ │ ├── favicon.svg
│ │ ├── logo.svg
│ │ ├── og-image.svg
│ │ ├── terminal-default.svg
│ │ ├── terminal-canvas.svg
│ │ └── terminal-keybindings.svg
│ └── demos/ # (empty — drop asciinema .cast files here)
├── content/ # (empty — markdown for blog/changelog later)
├── Makefile
├── .gitignore
└── README.md
```
## Adding an asciinema demo to the hero
The hero currently shows a static SVG terminal mockup. To replace it with a
real asciinema recording:
1. Record one of the examples:
1. Drop your clip in `video/`, e.g. `video/nature1.mp4`.
2. Render + bundle (needs `ffmpeg` + `chafa`, so run in the nix shell):
```bash
cd ../komp_ac/tui-pages/examples/default
asciinema rec ../../tui-pages-web/static/demos/intro.cast
# ... run the app for a few seconds, hit ctrl-d to stop
nix develop -c make video NAME=nature1 SIZE=480x144 SCALE=1920
```
2. In `index.html`, replace the hero terminal block with:
3. `make serve` and hard-refresh the browser (Ctrl+Shift+R).
```html
<div class="tp-terminal">
<asciinema-player src="/static/demos/intro.cast" autoplay loop></asciinema-player>
</div>
```
- `NAME` — the file name without `.mp4`.
- `SIZE` — ASCII grid `cols×rows`. Bigger = sharper + heavier bundle. Keep
the ~3.33:1 ratio (e.g. `80x24`, `160x48`, `240x72`, `480x144`).
- `SCALE` — source pixel width fed to chafa; scale it up with `SIZE`.
3. Add the asciinema player to the `<head>`:
After changing `SIZE`, set a matching `font-size` in `static/css/ascii.css`
(it halves each time you double the grid) so the ASCII fills the screen.
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.css">
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
```
Current setup: `480x144`, font-size `max(0.35vw, 0.695vh)`.
## License
The website source is MIT-licensed. The terminal mockup SVGs in `static/img/`
are hand-drawn and original. The crate itself is at
<https://gitlab.com/filipriec/tui-pages>.
MIT. Crate: <https://gitlab.com/filipriec/tui-pages>.

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

189
flake.nix Normal file
View File

@@ -0,0 +1,189 @@
{
description = "Nix flake for the tui-pages website static HTML/CSS/SVG, CDN-loaded JS";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# NOTE: No rust-overlay — this is a pure-static site, no Rust toolchain required.
};
outputs = { self, nixpkgs, ... }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems f;
version = "0.1.0";
in
{
# `nix fmt` — format this flake
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
# -------------------------------------------------------------------------
# packages.<system>.default
#
# The whole website as a single derivation. `nix build` produces ./result/
# containing the static files, ready to:
# - serve with any static file server, or
# - upload to Netlify / Cloudflare Pages / GitHub Pages / S3 / etc.
# -------------------------------------------------------------------------
packages = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
src = ./.;
in {
default = pkgs.stdenvNoCC.mkDerivation {
pname = "tui-pages-website";
inherit version;
inherit src;
dontBuild = true;
installPhase = ''
runHook preInstall
mkdir -p $out
# Use cp -R with --no-preserve=mode so the read-only Nix-store
# permissions don't leak into $out (a few hosts reject the
# resulting 0444 file modes when serving).
cp -R --no-preserve=mode ${src}/. $out/
chmod -R u+w $out
runHook postInstall
'';
meta = with pkgs.lib; {
description = "Static website for the tui-pages Rust crate";
homepage = "https://tui-pages.dev";
license = licenses.mit;
platforms = platforms.unix;
};
};
});
# -------------------------------------------------------------------------
# apps.<system>.serve
#
# `nix run .#serve` — build the site and boot `python3 -m http.server`
# against the built result. Honours $PORT (defaults to 8000).
# -------------------------------------------------------------------------
apps = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
site = self.packages.${system}.default;
in {
serve = {
type = "app";
program = toString (pkgs.writeShellScript "tui-pages-web-serve" ''
echo "Serving ${site} on http://localhost:''${PORT:-8000}"
cd ${site}
exec ${pkgs.python3}/bin/python3 -m http.server "''${PORT:-8000}"
'');
meta = with pkgs.lib; {
description = "Build the tui-pages website and serve it on $PORT (default 8000)";
platforms = platforms.unix;
};
};
});
# -------------------------------------------------------------------------
# devShells.<system>.default
#
# `nix develop` — everything the Makefile, the README, and future
# workflows need. No Rust toolchain by design.
# -------------------------------------------------------------------------
devShells = forAllSystems (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
default = pkgs.mkShell {
packages = with pkgs; [
# --- Build / serve / validate (the Makefile uses these) ----
gnumake
python3
gawk
gnused
gnugrep
coreutils
# --- HTML validation (optional, for the tidy check) --------
html-tidy
# --- Smoke-test the local server ---------------------------
curl
# --- Record TUI demos (used to replace the SVG mockups) ----
asciinema
# --- Generate the animated ASCII background from a video.
# `make video NAME=<name>` extracts frames from video/<name>.mp4
# with ffmpeg at 12 fps, runs chafa on each to get 80x24 ASCII,
# and bundles them into static/js/mountain.js for the cycler.
ffmpeg
chafa
# --- `make video` also needs python3 (already in the default set)
# to run tools/build_mountain_js.py.
# --- Optional: standalone Tailwind CLI for production CSS --
# Uncomment if you switch from the Play CDN to a precompiled stylesheet.
# nodejs
];
shellHook = ''
# Some distros ship `python` but not `python3`. Make the
# `python3` invocation in the Makefile portable.
if ! command -v python3 >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
alias python3=python
fi
cat <<'EOF'
tui-pages website dev shell
make serve python3 -m http.server 8000
make size report file sizes
make validate HTML tag balance check
make tidy run html-tidy on every .html
make video NAME=foo regenerate the ASCII background from
video/foo.mp4 (12 fps, 80x24 ASCII)
asciinema rec static/demos/intro.cast record a demo
make video-list show available videos in video/
make video-frames extract + chafa frames for $NAME
make video-bundle bundle frames into mountain.js
nix build build site ./result/
nix run .#serve build + serve (honours $PORT)
nix fmt format this flake
nix flake check run all checks
EOF
'';
};
});
# -------------------------------------------------------------------------
# checks.<system>.tidy
#
# HTML sanity check using tidy. Non-blocking — prints warnings to the
# build log, never fails. `nix flake check` runs this.
# -------------------------------------------------------------------------
checks = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
site = self.packages.${system}.default;
in {
tidy = pkgs.runCommand "tui-pages-website-html-check"
{
nativeBuildInputs = [ pkgs.html-tidy ];
} ''
cd ${site}
for f in *.html; do
echo " $f "
${pkgs.html-tidy}/bin/tidy -e -q -utf8 "$f" 2>&1 | head -20 || true
done
touch $out
'';
});
};
}

View File

@@ -77,6 +77,8 @@
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
<link rel="stylesheet" href="/static/css/site.css">
<!-- Animated ASCII art layer (chafa-rendered) -->
<link rel="stylesheet" href="/static/css/ascii.css">
</head>
<body
@@ -93,9 +95,22 @@
class="min-h-screen bg-base-100 text-base-content antialiased"
>
<!-- ============================ ASCII BACKGROUND ============================ -->
<!-- Full-bleed 3D mountain flyover. tools/mountain.py renders 60 PNG
frames; chafa converts each to 80x24 ASCII; tools/build_mountain_js.py
bundles them into static/js/mountain.js. The background <pre> is
mounted by static/js/mountain-bg.js, which cycles the 60 frames at
12 fps. Style lives in static/css/ascii.css. -->
<pre id="tp-mountain-bg" class="tp-mountain-bg" aria-hidden="true"></pre>
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:bg-accent focus:text-accent-fg focus:rounded">Skip to content</a>
<!-- ============================ NAV ============================ -->
<!-- 3D mountain flyover: pre-baked ASCII frames cycled at 12 fps -->
<script src="/static/js/mountain.js" defer></script>
<script src="/static/js/mountain-bg.js" defer></script>
<!-- ============================ NAV ============================ -->
<header class="tp-nav sticky top-0 z-40">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
@@ -189,7 +204,7 @@
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
A framework for<br>
building <span class="text-accent">TUIs</span> in Rust<span class="tp-cursor" aria-hidden="true"></span>
building <span class="text-accent">TUIs</span> in Rust
</h1>
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
@@ -228,7 +243,6 @@
</button>
</div>
<p class="mt-3 text-xs text-zinc-500 font-mono">or: <code class="text-zinc-400">cargo install tui-pages-cli</code></p>
</div>
<!-- Hero terminal mockup -->

91
static/css/ascii.css Normal file
View File

@@ -0,0 +1,91 @@
/* ==========================================================================
tui-pages website - animated ASCII mountain background (chafa-rendered)
The mountain background is JS-driven: tools/mountain.py renders 60
procedural 3D mountain-flyover PNG frames; chafa converts each to 80x24
ASCII; tools/build_mountain_js.py bundles them into static/js/mountain.js.
static/js/mountain-bg.js cycles the frames at 12 fps into the
<pre id="tp-mountain-bg" class="tp-mountain-bg"> placeholder in index.html.
This CSS file:
- positions the frame as a full-screen top layer
- tints the dense characters rust-orange with text-shadow + glow
- adds a CRT scanline overlay
- disables the animation under prefers-reduced-motion
========================================================================== */
/* Full-screen ASCII mountain top layer ----------------------------------- */
.tp-mountain-bg {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100vw;
height: 100vh;
height: 100svh;
z-index: 0;
pointer-events: none;
margin: 0;
padding: 0;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: max(0.35vw, 0.695vh);
font-size: max(0.35vw, 0.695svh);
line-height: 1.0;
letter-spacing: 0;
color: #f4a26b;
text-shadow: 0 0 8px rgba(183, 65, 14, 0.35);
opacity: 0.45;
white-space: pre;
background: transparent;
overflow: hidden;
user-select: none;
}
/* CRT scanlines overlay - a thin moving line every 2px. Faint so it doesn't
fight the page content. */
.tp-mountain-bg::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 2px,
rgba(0, 0, 0, 0.20) 2px,
rgba(0, 0, 0, 0.20) 3px
);
pointer-events: none;
animation: tp-mtn-scanline 8s linear infinite;
mix-blend-mode: multiply;
}
@keyframes tp-mtn-scanline {
0% { background-position: 0 0; }
100% { background-position: 0 6px; }
}
/* Light theme: dim the background, drop the glow, switch to dark text. */
:root[data-theme="winter"] .tp-mountain-bg {
opacity: 0.20;
color: #2a1810;
text-shadow: none;
mix-blend-mode: multiply;
}
:root[data-theme="winter"] .tp-mountain-bg::after {
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 2px,
rgba(255, 255, 255, 0.12) 2px,
rgba(255, 255, 255, 0.12) 3px
);
mix-blend-mode: screen;
}
/* Reduced motion: keep the first frame static, no scanline drift. */
@media (prefers-reduced-motion: reduce) {
.tp-mountain-bg::after {
animation: none !important;
}
}

View File

@@ -29,14 +29,16 @@ html {
--tp-grid-line: rgba(63, 63, 70, 0.35);
--tp-hero-from: #0b0b0f;
--tp-hero-to: #18181b;
--tp-text-outline: rgba(0, 0, 0, 0.9);
}
:root[data-theme="light"] {
:root[data-theme="winter"] {
--tp-accent: #93330c;
--tp-accent-fg: #fff5f0;
--tp-grid-line: rgba(228, 228, 231, 0.7);
--tp-hero-from: #fafafa;
--tp-hero-to: #f4f4f5;
--tp-text-outline: rgba(255, 255, 255, 0.95);
}
/* ---------- Body --------------------------------------------------------- */
@@ -55,13 +57,30 @@ body {
/* ---------- Hero --------------------------------------------------------- */
.tp-hero {
background:
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(183, 65, 14, 0.15), transparent 70%),
linear-gradient(180deg, var(--tp-hero-from) 0%, var(--tp-hero-to) 100%);
background: transparent; /* let the mountain ASCII show through */
position: relative;
isolation: isolate;
}
.tp-hero > .max-w-7xl {
position: relative;
z-index: 1;
}
.tp-hero h1,
.tp-hero p {
text-shadow:
0 -1px 0 var(--tp-text-outline),
1px 0 0 var(--tp-text-outline),
0 1px 0 var(--tp-text-outline),
-1px 0 0 var(--tp-text-outline),
-1px -1px 0 var(--tp-text-outline),
1px -1px 0 var(--tp-text-outline),
-1px 1px 0 var(--tp-text-outline),
1px 1px 0 var(--tp-text-outline),
0 2px 16px rgba(0, 0, 0, 0.9);
}
.tp-hero::before {
content: "";
position: absolute;
@@ -75,6 +94,7 @@ body {
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 30%, transparent 80%);
pointer-events: none;
z-index: -1;
opacity: 0.5; /* subtle grid, not a wall */
}
/* Subtle floating code lines in the hero, behind the content */
@@ -89,30 +109,6 @@ body {
radial-gradient(circle at 85% 70%, rgba(126, 231, 135, 0.05), transparent 35%);
}
/* ---------- Cursor caret animation --------------------------------------- */
@keyframes tp-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.tp-cursor {
display: inline-block;
width: 0.55em;
height: 1em;
background: var(--tp-accent);
margin-left: 0.15em;
vertical-align: -0.12em;
animation: tp-blink 1.1s steps(1) infinite;
}
/* ---------- Soft float for the hero terminal ----------------------------- */
@keyframes tp-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.tp-float {
animation: tp-float 6s ease-in-out infinite;
}
/* ---------- Glass nav ---------------------------------------------------- */
.tp-nav {
background: rgba(9, 9, 11, 0.65);
@@ -120,7 +116,7 @@ body {
-webkit-backdrop-filter: saturate(160%) blur(12px);
border-bottom: 1px solid rgba(63, 63, 70, 0.4);
}
:root[data-theme="light"] .tp-nav {
:root[data-theme="winter"] .tp-nav {
background: rgba(255, 255, 255, 0.7);
border-bottom-color: rgba(228, 228, 231, 0.7);
}
@@ -139,11 +135,11 @@ body {
background: rgba(24, 24, 27, 0.75);
transform: translateY(-2px);
}
:root[data-theme="light"] .tp-card {
:root[data-theme="winter"] .tp-card {
background: rgba(255, 255, 255, 0.6);
border-color: rgba(228, 228, 231, 0.8);
}
:root[data-theme="light"] .tp-card:hover {
:root[data-theme="winter"] .tp-card:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(183, 65, 14, 0.4);
}
@@ -196,7 +192,7 @@ body {
color: #f4f4f5;
line-height: 1;
}
:root[data-theme="light"] .tp-kbd {
:root[data-theme="winter"] .tp-kbd {
background: #fff;
color: #18181b;
border-color: #d4d4d8;
@@ -206,13 +202,12 @@ body {
.tp-terminal {
position: relative;
border-radius: 0.875rem;
background: #0b0b0f;
background: transparent; /* let the body ASCII art show through */
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 30px 60px -20px rgba(0, 0, 0, 0.6),
0 18px 36px -18px rgba(0, 0, 0, 0.5);
0 0 0 1px rgba(244, 162, 107, 0.18), /* rust-orange hairline, brand */
0 30px 60px -20px rgba(0, 0, 0, 0.55),
0 18px 36px -18px rgba(0, 0, 0, 0.45);
overflow: hidden;
border: 1px solid rgba(63, 63, 70, 0.4);
}
.tp-terminal::after {
content: "";
@@ -220,7 +215,14 @@ body {
inset: 0;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
background: linear-gradient(
to bottom,
rgba(11, 11, 15, 0.55) 0%,
rgba(11, 11, 15, 0) 18%,
rgba(11, 11, 15, 0) 82%,
rgba(11, 11, 15, 0.45) 100%
); /* darken only the top + bottom strips, leave the middle transparent
so the body ASCII art shows through the terminal content */
}
/* ---------- Selection --------------------------------------------------- */
@@ -249,7 +251,7 @@ body {
/* ---------- No-FOUC theme flash ----------------------------------------- */
html:not([data-theme]) body { background: #09090b; }
:root[data-theme="light"] body { background: #fafafa; }
:root[data-theme="winter"] body { background: #fafafa; }
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
[x-cloak] { display: none !important; }

View File

@@ -1,17 +1,17 @@
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/default terminal screenshot">
<style>
.bg { fill: #18181b; }
.chrome { fill: #27272a; }
.bg { fill: transparent; stroke: #3f3f46; stroke-width: 1; }
.chrome { fill: transparent; }
.dot { fill: #52525b; }
.sep { stroke: #27272a; stroke-width: 1; }
.tab { fill: #18181b; }
.sep { stroke: #3f3f46; stroke-width: 1; }
.tab { fill: transparent; }
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
.t-md { font-size: 15px; }
.t-sm { font-size: 13px; }
.t-xs { font-size: 11px; }
.muted { fill: #a1a1aa; }
.dim { fill: #71717a; }
.sel-bg { fill: #b7410e; }
.sel-bg { fill: #b7410e; fill-opacity: 0.85; }
.sel-fg { fill: #fff5f0; }
</style>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

73
static/js/mountain-bg.js Normal file
View File

@@ -0,0 +1,73 @@
/*
* Mountain background animation.
*
* Cycles the 60 ASCII frames from window.TP_MOUNTAIN_FRAMES at 12 fps.
* Mounts a single <pre> in the body that gets its textContent swapped
* on each frame. Uses requestAnimationFrame to stay smooth and yields
* cleanly to the browser's frame budget. Pauses on prefers-reduced-motion
* and on document.hidden.
*/
(function () {
"use strict";
function init() {
if (typeof window.TP_MOUNTAIN_FRAMES === "undefined") return;
var pre = document.querySelector("#tp-mountain-bg");
if (!pre) return;
pre.setAttribute("data-tp-mountain", "");
pre.setAttribute("aria-hidden", "true");
pre.textContent = window.TP_MOUNTAIN_FRAMES[0];
var n = window.TP_MOUNTAIN_N_FRAMES;
var fps = window.TP_MOUNTAIN_FPS;
var i = 0;
var last = 0;
var acc = 0;
var interval = 1000 / fps;
var reduceMotion =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduceMotion) {
// Show a single representative frame; no animation.
pre.textContent = window.TP_MOUNTAIN_FRAMES[Math.floor(n / 2)];
return;
}
function step(ts) {
if (!last) last = ts;
acc += ts - last;
last = ts;
while (acc >= interval) {
i = (i + 1) % n;
pre.textContent = window.TP_MOUNTAIN_FRAMES[i];
acc -= interval;
}
if (!document.hidden) {
requestAnimationFrame(step);
} else {
// Resume on next visibility change.
document.addEventListener(
"visibilitychange",
function onVisible() {
if (!document.hidden) {
last = 0;
acc = 0;
document.removeEventListener("visibilitychange", onVisible);
requestAnimationFrame(step);
}
}
);
}
}
requestAnimationFrame(step);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

35337
static/js/mountain.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Build static/js/mountain.js with all ASCII frames from a build/<name>/ dir
embedded as a JS array.
Usage: build_mountain_js.py <src_dir> <dst_js_path>
Frames are read in sorted order, escaped for JS template literals, and
concatenated into a window.TP_MOUNTAIN_FRAMES array.
"""
import os
import sys
def js_escape(s: str) -> str:
return s.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
def main() -> int:
if len(sys.argv) != 3:
print(__doc__, file=sys.stderr)
return 2
src = sys.argv[1]
dst = sys.argv[2]
txts = sorted(p for p in os.listdir(src) if p.startswith("frame-") and p.endswith(".txt"))
if not txts:
print(f"no frame-*.txt files in {src}", file=sys.stderr)
return 1
frames = []
for name in txts:
with open(os.path.join(src, name)) as f:
frames.append(f.read().rstrip("\n"))
header = """/* Autogenerated by tools/build_mountain_js.py
ASCII frames from a video, chafa-rendered, concatenated into a JS array.
Cycled at 12 fps by static/js/mountain-bg.js. */
window.TP_MOUNTAIN_FRAMES = [
"""
footer = f"""];
window.TP_MOUNTAIN_N_FRAMES = {len(frames)};
window.TP_MOUNTAIN_FPS = 12;
"""
body = ",\n".join(f" `{js_escape(fr)}`" for fr in frames)
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "w") as f:
f.write(header)
f.write(body)
f.write("\n")
f.write(footer)
print(f" {os.path.getsize(dst):>7d} bytes {dst} ({len(frames)} frames)")
return 0
if __name__ == "__main__":
sys.exit(main())