Compare commits
20 Commits
d8b622860c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51beca2776 | ||
|
|
0b9df4549e | ||
|
|
78407ae9e8 | ||
|
|
bc670d11dc | ||
|
|
d22dca0a27 | ||
|
|
3266c944ba | ||
|
|
93769b3129 | ||
|
|
83a80b6e31 | ||
|
|
7c85022686 | ||
|
|
5f354bda1c | ||
|
|
b5a35ab198 | ||
|
|
7f42516fa3 | ||
|
|
4212d8877e | ||
|
|
6ccf372f65 | ||
|
|
55eb4bbf00 | ||
|
|
1032d20080 | ||
|
|
0f897354ec | ||
|
|
695dad519d | ||
|
|
462a53853f | ||
|
|
99f255fc29 |
81
.gitignore
vendored
@@ -1,30 +1,89 @@
|
|||||||
# Build artifacts
|
# ============================================================================
|
||||||
bin/
|
# Build artifacts — generated by `make video`, `make serve`, `nix build`
|
||||||
node_modules/
|
# ============================================================================
|
||||||
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.min.css
|
out/
|
||||||
*.min.js
|
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/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
*.swo
|
||||||
|
*.swn
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Helix
|
||||||
|
.helix/
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# OS
|
# OS
|
||||||
|
# ============================================================================
|
||||||
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# Logs
|
# Logs
|
||||||
|
# ============================================================================
|
||||||
*.log
|
*.log
|
||||||
npm-debug.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
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
# Cache
|
# ============================================================================
|
||||||
|
# Caches
|
||||||
|
# ============================================================================
|
||||||
.cache/
|
.cache/
|
||||||
.parcel-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/
|
||||||
|
|||||||
23
Caddyfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
tui-pages.farmeris.sk {
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
# mdbook served under /book (book.toml has site-url = "/book/")
|
||||||
|
redir /book /book/
|
||||||
|
handle /book/* {
|
||||||
|
root * /app/tui-pages-docs
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
# website at the root (catch-all — must come last)
|
||||||
|
handle {
|
||||||
|
root * /app/tui-pages-web
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
# custom 404 page for the website
|
||||||
|
handle_errors {
|
||||||
|
root * /app/tui-pages-web
|
||||||
|
rewrite * /404.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
10
LICENSE
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2026 Filip Priečinský
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
44
Makefile
@@ -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
|
help: ## Show this help
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@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; \
|
diff <(echo "$$open") <(echo "$$close") || true; \
|
||||||
done
|
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/
|
@rm -rf bin/ node_modules/ dist/
|
||||||
|
|||||||
138
README.md
@@ -1,125 +1,57 @@
|
|||||||
# tui-pages website
|
# tui-pages website
|
||||||
|
|
||||||
Static marketing site for the [`tui-pages`](https://gitlab.com/filipriec/tui-pages) Rust crate.
|
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
|
## Build & serve
|
||||||
|
|
||||||
| 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Just open the file
|
make serve # serve the working tree on http://localhost:8000
|
||||||
xdg-open index.html # Linux
|
make size # report file sizes
|
||||||
open index.html # macOS
|
make validate # quick HTML syntax check
|
||||||
|
|
||||||
# Or serve it (recommended — gives you a stable URL for htmx)
|
|
||||||
python3 -m http.server 8000
|
|
||||||
# then visit http://localhost:8000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Makefile` wraps the Python server and a few convenience commands:
|
Just edit the `.html` / `static/` files and refresh.
|
||||||
|
|
||||||
```bash
|
## ASCII video background
|
||||||
make serve # python3 -m http.server 8000
|
|
||||||
make size # report file sizes
|
|
||||||
make validate # quick HTML syntax check (search for unclosed tags)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
### Render a video
|
||||||
(smaller CSS, no runtime Tailwind compile), swap the Play CDN for the
|
|
||||||
**Tailwind standalone CLI**:
|
|
||||||
|
|
||||||
```bash
|
1. Drop your clip in `video/`, e.g. `video/nature1.mp4`.
|
||||||
# 1. Download the standalone CLI
|
2. Render + bundle (needs `ffmpeg` + `chafa`, so run in the nix shell):
|
||||||
# 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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ../komp_ac/tui-pages/examples/default
|
nix develop -c make video NAME=nature1 SIZE=480x144 SCALE=1920
|
||||||
asciinema rec ../../tui-pages-web/static/demos/intro.cast
|
|
||||||
# ... run the app for a few seconds, hit ctrl-d to stop
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. In `index.html`, replace the hero terminal block with:
|
3. `make serve` and hard-refresh the browser (Ctrl+Shift+R).
|
||||||
|
|
||||||
```html
|
- `NAME` — the file name without `.mp4`.
|
||||||
<div class="tp-terminal">
|
- `SIZE` — ASCII grid `cols×rows`. Bigger = sharper + heavier bundle. Keep
|
||||||
<asciinema-player src="/static/demos/intro.cast" autoplay loop></asciinema-player>
|
the ~3.33:1 ratio (e.g. `80x24`, `160x48`, `240x72`, `480x144`).
|
||||||
</div>
|
- `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`
|
||||||
|
(lines 32–33, the two `font-size: max(...)` declarations) so the ASCII fills
|
||||||
|
the screen. The font-size scales **inversely** with the grid: halve the grid →
|
||||||
|
double the font-size. The two `max()` multipliers are:
|
||||||
|
|
||||||
```html
|
- vw multiplier ≈ `1 / (cols × 0.6)` (0.6 ≈ JetBrains Mono char aspect ratio)
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.css">
|
- vh multiplier ≈ `1 / rows`
|
||||||
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
|
|
||||||
```
|
| `SIZE` | `font-size` in `ascii.css` |
|
||||||
|
| --------- | ------------------------------ |
|
||||||
|
| `480x144` | `max(0.35vw, 0.695vh)` |
|
||||||
|
| `360x108` | `max(0.463vw, 0.926vh)` |
|
||||||
|
| `240x72` | `max(0.7vw, 1.39vh)` |
|
||||||
|
|
||||||
|
Current setup: `240x72`, font-size `max(0.7vw, 1.39vh)`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The website source is MIT-licensed. The terminal mockup SVGs in `static/img/`
|
MIT. Crate: <https://gitlab.com/filipriec/tui-pages>.
|
||||||
are hand-drawn and original. The crate itself is at
|
|
||||||
<https://gitlab.com/filipriec/tui-pages>.
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<title>Examples — tui-pages</title>
|
<title>Examples — tui-pages</title>
|
||||||
<meta name="description" content="Three runnable TUI apps built with tui-pages: a default multi-page app, a canvas login form, and a keybindings modal. Clone, cargo run, learn.">
|
<meta name="description" content="Three runnable TUI apps built with tui-pages: a default multi-page app, a canvas login form, and a keybindings modal. Clone, cargo run, learn.">
|
||||||
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
||||||
<link rel="canonical" href="https://tui-pages.dev/examples.html">
|
<link rel="canonical" href="https://tui-pages.farmeris.sk/examples.html">
|
||||||
|
|
||||||
<meta property="og:title" content="Examples — tui-pages">
|
<meta property="og:title" content="Examples — tui-pages">
|
||||||
<meta property="og:description" content="Three runnable TUI apps built with tui-pages. Clone, cargo run, learn.">
|
<meta property="og:description" content="Three runnable TUI apps built with tui-pages. Clone, cargo run, learn.">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<ul class="hidden md:flex items-center gap-1 ml-4 text-sm">
|
<ul class="hidden md:flex items-center gap-1 ml-4 text-sm">
|
||||||
<li><a href="/examples.html" class="px-3 py-1.5 text-white rounded-md bg-zinc-800/60" aria-current="page">Examples</a></li>
|
<li><a href="/examples.html" class="px-3 py-1.5 text-white rounded-md bg-zinc-800/60" aria-current="page">Examples</a></li>
|
||||||
<li><a href="https://tui-pages.farmeris.sk" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Book</a></li>
|
<li><a href="https://tui-pages.farmeris.sk/book" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Book</a></li>
|
||||||
<li><a href="https://docs.rs/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">API</a></li>
|
<li><a href="https://docs.rs/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">API</a></li>
|
||||||
<li><a href="https://gitlab.com/filipriec/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">GitLab</a></li>
|
<li><a href="https://gitlab.com/filipriec/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">GitLab</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<p class="text-sm font-medium text-accent uppercase tracking-widest">Examples</p>
|
<p class="text-sm font-medium text-accent uppercase tracking-widest">Examples</p>
|
||||||
<h1 class="mt-3 text-4xl sm:text-5xl font-bold tracking-tight text-zinc-50">
|
<h1 class="mt-3 text-4xl sm:text-5xl font-bold tracking-tight text-zinc-50">
|
||||||
Three apps in the box.
|
Rich examples out of the box.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 text-lg text-zinc-300 max-w-2xl">
|
<p class="mt-4 text-lg text-zinc-300 max-w-2xl">
|
||||||
Each example is a standalone <code class="font-mono text-zinc-200">cargo</code> project under <code class="font-mono text-zinc-200">examples/</code> in the repo. Clone, run, read, fork.
|
Each example is a standalone <code class="font-mono text-zinc-200">cargo</code> project under <code class="font-mono text-zinc-200">examples/</code> in the repo. Clone, run, read, fork.
|
||||||
@@ -129,7 +129,12 @@
|
|||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<div class="tp-card !p-2">
|
<div class="tp-card !p-2">
|
||||||
<div class="tp-terminal">
|
<div class="tp-terminal">
|
||||||
<img src="/static/img/terminal-default.svg" alt="examples/default TUI screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-default.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-default.svg' : '/static/img/terminal-default-light.svg'"
|
||||||
|
alt="examples/default TUI screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,21 +149,7 @@
|
|||||||
A multi-page app with a focusable list, page navigation, and quit. The minimal end-to-end example — the one to read first to understand the architecture.
|
A multi-page app with a focusable list, page navigation, and quit. The minimal end-to-end example — the one to read first to understand the architecture.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">Run it</h3>
|
|
||||||
<div
|
|
||||||
x-data="{ copied: false }"
|
|
||||||
class="mt-2 inline-flex items-stretch w-full max-w-md rounded-lg border border-zinc-800 bg-zinc-950/80 overflow-hidden font-mono text-sm"
|
|
||||||
>
|
|
||||||
<code class="px-3 py-2.5 text-zinc-100 whitespace-nowrap">cargo run --example default</code>
|
|
||||||
<button type="button"
|
|
||||||
@click="navigator.clipboard.writeText('cargo run --example default').then(() => { copied = true; setTimeout(() => copied = false, 1400) })"
|
|
||||||
class="ml-auto px-3 border-l border-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-900 transition-colors flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<svg x-show="!copied" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
||||||
<svg x-show="copied" x-cloak width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
<span x-text="copied ? 'Copied' : 'Copy'" class="text-xs"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
||||||
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
||||||
@@ -184,7 +175,12 @@
|
|||||||
<div class="lg:col-span-3 lg:order-2">
|
<div class="lg:col-span-3 lg:order-2">
|
||||||
<div class="tp-card !p-2">
|
<div class="tp-card !p-2">
|
||||||
<div class="tp-terminal">
|
<div class="tp-terminal">
|
||||||
<img src="/static/img/terminal-canvas.svg" alt="examples/canvas TUI screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-canvas.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-canvas.svg' : '/static/img/terminal-canvas-light.svg'"
|
||||||
|
alt="examples/canvas TUI screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,22 +195,6 @@
|
|||||||
A login form with validated inputs, a submit button, and a canvas-owned keymap. Demonstrates the <code class="font-mono text-zinc-200">canvas</code> feature flag: GUI renderers, suggestions, cursor style, validation, computed fields, textareas, and text inputs.
|
A login form with validated inputs, a submit button, and a canvas-owned keymap. Demonstrates the <code class="font-mono text-zinc-200">canvas</code> feature flag: GUI renderers, suggestions, cursor style, validation, computed fields, textareas, and text inputs.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">Run it</h3>
|
|
||||||
<div
|
|
||||||
x-data="{ copied: false }"
|
|
||||||
class="mt-2 inline-flex items-stretch w-full max-w-md rounded-lg border border-zinc-800 bg-zinc-950/80 overflow-hidden font-mono text-sm"
|
|
||||||
>
|
|
||||||
<code class="px-3 py-2.5 text-zinc-100 whitespace-nowrap">cargo run --example canvas --features canvas</code>
|
|
||||||
<button type="button"
|
|
||||||
@click="navigator.clipboard.writeText('cargo run --example canvas --features canvas').then(() => { copied = true; setTimeout(() => copied = false, 1400) })"
|
|
||||||
class="ml-auto px-3 border-l border-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-900 transition-colors flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<svg x-show="!copied" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
||||||
<svg x-show="copied" x-cloak width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
<span x-text="copied ? 'Copied' : 'Copy'" class="text-xs"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
||||||
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
||||||
<li class="flex gap-2"><span class="text-accent">▸</span> Field-level focus with <span class="tp-kbd">tab</span> / <span class="tp-kbd">shift+tab</span></li>
|
<li class="flex gap-2"><span class="text-accent">▸</span> Field-level focus with <span class="tp-kbd">tab</span> / <span class="tp-kbd">shift+tab</span></li>
|
||||||
@@ -239,7 +219,12 @@
|
|||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<div class="tp-card !p-2">
|
<div class="tp-card !p-2">
|
||||||
<div class="tp-terminal">
|
<div class="tp-terminal">
|
||||||
<img src="/static/img/terminal-keybindings.svg" alt="examples/keybindings TUI screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-keybindings.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-keybindings.svg' : '/static/img/terminal-keybindings-light.svg'"
|
||||||
|
alt="examples/keybindings TUI screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,21 +239,6 @@
|
|||||||
A modal showing all keybindings, opened with <span class="tp-kbd">?</span>. Demonstrates the built-in <code class="font-mono text-zinc-200">dialog</code> feature: content and result types, plus a ratatui renderer.
|
A modal showing all keybindings, opened with <span class="tp-kbd">?</span>. Demonstrates the built-in <code class="font-mono text-zinc-200">dialog</code> feature: content and result types, plus a ratatui renderer.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">Run it</h3>
|
|
||||||
<div
|
|
||||||
x-data="{ copied: false }"
|
|
||||||
class="mt-2 inline-flex items-stretch w-full max-w-md rounded-lg border border-zinc-800 bg-zinc-950/80 overflow-hidden font-mono text-sm"
|
|
||||||
>
|
|
||||||
<code class="px-3 py-2.5 text-zinc-100 whitespace-nowrap">cargo run --example keybindings --features dialog</code>
|
|
||||||
<button type="button"
|
|
||||||
@click="navigator.clipboard.writeText('cargo run --example keybindings --features dialog').then(() => { copied = true; setTimeout(() => copied = false, 1400) })"
|
|
||||||
class="ml-auto px-3 border-l border-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-900 transition-colors flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<svg x-show="!copied" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
||||||
<svg x-show="copied" x-cloak width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
<span x-text="copied ? 'Copied' : 'Copy'" class="text-xs"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
<h3 class="mt-6 text-sm font-semibold uppercase tracking-widest text-zinc-400">What it shows</h3>
|
||||||
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
|
||||||
@@ -295,7 +265,7 @@
|
|||||||
The book walks you from <code class="font-mono text-zinc-300">cargo new</code> to a working multi-page app. Or read the API reference and dive in.
|
The book walks you from <code class="font-mono text-zinc-300">cargo new</code> to a working multi-page app. Or read the API reference and dive in.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||||
<a href="https://tui-pages.farmeris.sk" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
|
<a href="https://tui-pages.farmeris.sk/book" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
|
||||||
Start the tutorial
|
Start the tutorial
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
27
flake.lock
generated
Normal 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
@@ -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
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
74
index.html
@@ -8,25 +8,27 @@
|
|||||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)">
|
||||||
|
|
||||||
<title>tui-pages — a framework for building TUIs in Rust</title>
|
<title>tui-pages — a framework for building TUIs in Rust</title>
|
||||||
<meta name="description" content="A complete framework for building TUIs in Rust on top of ratatui. Pre-programmed focus manager, input pipeline, keymaps, and page navigation. Stop rewriting the same architecture for every project.">
|
<meta name="description" content="A complete UX framework for building TUIs in Rust. Stop rewriting the same architecture for every project.">
|
||||||
|
|
||||||
<meta name="keywords" content="rust, tui, ratatui, terminal, framework, ui, cargo, crate">
|
<meta name="keywords" content="rust, tui, ratatui, terminal, framework, ui, cargo, crate">
|
||||||
<meta name="author" content="Filip Riečický">
|
<meta name="author" content="Filip Riečický">
|
||||||
|
|
||||||
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
||||||
<link rel="canonical" href="https://tui-pages.dev/">
|
<link rel="canonical" href="https://tui-pages.farmeris.sk/">
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="tui-pages — a framework for building TUIs in Rust">
|
<meta property="og:title" content="tui-pages — a framework for building TUIs in Rust">
|
||||||
<meta property="og:description" content="A complete framework for building TUIs in Rust on top of ratatui. Pre-programmed focus manager, input pipeline, keymaps, and page navigation.">
|
<meta property="og:description" content="A complete framework for building TUIs in Rust on top of ratatui. Pre-programmed focus manager, input pipeline, keymaps, and page navigation.">
|
||||||
<meta property="og:url" content="https://tui-pages.dev/">
|
<meta property="og:url" content="https://tui-pages.farmeris.sk/">
|
||||||
<meta property="og:image" content="/static/img/og-image.svg">
|
<meta property="og:image" content="/static/img/og-image.svg">
|
||||||
<meta property="og:site_name" content="tui-pages">
|
<meta property="og:site_name" content="tui-pages">
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="tui-pages — a framework for building TUIs in Rust">
|
<meta name="twitter:title" content="tui-pages — a framework for building TUIs in Rust">
|
||||||
<meta name="twitter:description" content="Pre-programmed focus, keymaps, and page navigation. Stop rewriting the same architecture for every TUI project.">
|
<meta name="twitter:description" content="UX for TUIs in Rust. Stop rewriting the same architecture for every project.">
|
||||||
|
|
||||||
<meta name="twitter:image" content="/static/img/og-image.svg">
|
<meta name="twitter:image" content="/static/img/og-image.svg">
|
||||||
|
|
||||||
<!-- Preconnect -->
|
<!-- Preconnect -->
|
||||||
@@ -77,6 +79,8 @@
|
|||||||
|
|
||||||
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
|
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
|
||||||
<link rel="stylesheet" href="/static/css/site.css">
|
<link rel="stylesheet" href="/static/css/site.css">
|
||||||
|
<!-- Animated ASCII art layer (chafa-rendered) -->
|
||||||
|
<link rel="stylesheet" href="/static/css/ascii.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
@@ -93,9 +97,22 @@
|
|||||||
class="min-h-screen bg-base-100 text-base-content antialiased"
|
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>
|
<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">
|
<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">
|
<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">
|
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
|
||||||
@@ -112,7 +129,7 @@
|
|||||||
|
|
||||||
<ul class="hidden md:flex items-center gap-1 ml-4 text-sm">
|
<ul class="hidden md:flex items-center gap-1 ml-4 text-sm">
|
||||||
<li><a href="/examples.html" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Examples</a></li>
|
<li><a href="/examples.html" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Examples</a></li>
|
||||||
<li><a href="https://tui-pages.farmeris.sk" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Book</a></li>
|
<li><a href="https://tui-pages.farmeris.sk/book" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Book</a></li>
|
||||||
<li><a href="https://docs.rs/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">API</a></li>
|
<li><a href="https://docs.rs/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">API</a></li>
|
||||||
<li><a href="https://gitlab.com/filipriec/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">GitLab</a></li>
|
<li><a href="https://gitlab.com/filipriec/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">GitLab</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -162,7 +179,7 @@
|
|||||||
>
|
>
|
||||||
<ul class="px-4 py-3 space-y-1 text-sm">
|
<ul class="px-4 py-3 space-y-1 text-sm">
|
||||||
<li><a href="/examples.html" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Examples</a></li>
|
<li><a href="/examples.html" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Examples</a></li>
|
||||||
<li><a href="https://tui-pages.farmeris.sk" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Book</a></li>
|
<li><a href="https://tui-pages.farmeris.sk/book" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Book</a></li>
|
||||||
<li><a href="https://docs.rs/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">API</a></li>
|
<li><a href="https://docs.rs/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">API</a></li>
|
||||||
<li><a href="https://gitlab.com/filipriec/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">GitLab</a></li>
|
<li><a href="https://gitlab.com/filipriec/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">GitLab</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -189,11 +206,11 @@
|
|||||||
|
|
||||||
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
|
<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>
|
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>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
|
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
|
||||||
<code class="font-mono text-accent">tui-pages</code> gives you a pre-programmed focus manager, input pipeline, keymaps, and page navigation on top of <a href="https://ratatui.rs" class="text-zinc-100 underline decoration-zinc-700 underline-offset-4 hover:decoration-accent">ratatui</a>. Stop rewriting the same architecture for every project.
|
<code class="font-mono text-accent">tui-pages</code> gives you full UX you would ever need. Stop rewriting the same architecture for every project.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||||
@@ -201,7 +218,7 @@
|
|||||||
Get started
|
Get started
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://tui-pages.farmeris.sk" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg border border-zinc-700 bg-zinc-900/40 text-zinc-100 font-medium hover:border-zinc-500 hover:bg-zinc-900 transition-colors">
|
<a href="https://tui-pages.farmeris.sk/book" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg border border-zinc-700 bg-zinc-900/40 text-zinc-100 font-medium hover:border-zinc-500 hover:bg-zinc-900 transition-colors">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||||||
Read the book
|
Read the book
|
||||||
</a>
|
</a>
|
||||||
@@ -228,7 +245,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Hero terminal mockup -->
|
<!-- Hero terminal mockup -->
|
||||||
@@ -236,7 +252,12 @@
|
|||||||
<div class="absolute -inset-4 -z-10 bg-gradient-to-br from-accent/15 via-transparent to-emerald-500/5 rounded-3xl blur-2xl"></div>
|
<div class="absolute -inset-4 -z-10 bg-gradient-to-br from-accent/15 via-transparent to-emerald-500/5 rounded-3xl blur-2xl"></div>
|
||||||
<div class="tp-float">
|
<div class="tp-float">
|
||||||
<div class="tp-terminal">
|
<div class="tp-terminal">
|
||||||
<img src="/static/img/terminal-default.svg" alt="Screenshot of the examples/default TUI app built with tui-pages" class="block w-full h-auto" loading="eager" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-default.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-default.svg' : '/static/img/terminal-default-light.svg'"
|
||||||
|
alt="Screenshot of the examples/default TUI app built with tui-pages"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="eager" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-center text-xs text-zinc-500 font-mono">examples/default · cargo run</p>
|
<p class="mt-3 text-center text-xs text-zinc-500 font-mono">examples/default · cargo run</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -549,8 +570,8 @@ pub fn keymaps() -> KeyMap {
|
|||||||
<div class="flex flex-wrap items-end justify-between gap-4">
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div class="max-w-2xl">
|
<div class="max-w-2xl">
|
||||||
<p class="text-sm font-medium text-accent uppercase tracking-widest">Examples</p>
|
<p class="text-sm font-medium text-accent uppercase tracking-widest">Examples</p>
|
||||||
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">Three apps in the box.</h2>
|
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">Rich examples.</h2>
|
||||||
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">Each one is a full <code class="font-mono text-zinc-300">cargo run</code> away. Read the source, run it, fork it.</p>
|
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">Each one is a simple <code class="font-mono text-zinc-300">cargo run</code> away. Read the source, run it, copy it.</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/examples.html" class="inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
|
<a href="/examples.html" class="inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
|
||||||
All examples
|
All examples
|
||||||
@@ -561,7 +582,12 @@ pub fn keymaps() -> KeyMap {
|
|||||||
<div class="mt-12 grid md:grid-cols-3 gap-6">
|
<div class="mt-12 grid md:grid-cols-3 gap-6">
|
||||||
<a href="/examples.html#default" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
<a href="/examples.html#default" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
||||||
<div class="tp-terminal m-3">
|
<div class="tp-terminal m-3">
|
||||||
<img src="/static/img/terminal-default.svg" alt="default example screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-default.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-default.svg' : '/static/img/terminal-default-light.svg'"
|
||||||
|
alt="default example screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5 pt-2">
|
<div class="p-5 pt-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -574,7 +600,12 @@ pub fn keymaps() -> KeyMap {
|
|||||||
|
|
||||||
<a href="/examples.html#canvas" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
<a href="/examples.html#canvas" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
||||||
<div class="tp-terminal m-3">
|
<div class="tp-terminal m-3">
|
||||||
<img src="/static/img/terminal-canvas.svg" alt="canvas example screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-canvas.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-canvas.svg' : '/static/img/terminal-canvas-light.svg'"
|
||||||
|
alt="canvas example screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5 pt-2">
|
<div class="p-5 pt-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -587,7 +618,12 @@ pub fn keymaps() -> KeyMap {
|
|||||||
|
|
||||||
<a href="/examples.html#keybindings" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
<a href="/examples.html#keybindings" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
|
||||||
<div class="tp-terminal m-3">
|
<div class="tp-terminal m-3">
|
||||||
<img src="/static/img/terminal-keybindings.svg" alt="keybindings example screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
|
<img
|
||||||
|
src="/static/img/terminal-keybindings.svg"
|
||||||
|
:src="theme === 'night' ? '/static/img/terminal-keybindings.svg' : '/static/img/terminal-keybindings-light.svg'"
|
||||||
|
alt="keybindings example screenshot"
|
||||||
|
class="block w-full h-auto"
|
||||||
|
loading="lazy" width="640" height="400">
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5 pt-2">
|
<div class="p-5 pt-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -646,7 +682,7 @@ rt.run() // framework handles the rest</code></pre>
|
|||||||
The book walks you from <code class="font-mono text-zinc-300">cargo new</code> to a working multi-page app. Or read the API reference and dive in.
|
The book walks you from <code class="font-mono text-zinc-300">cargo new</code> to a working multi-page app. Or read the API reference and dive in.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||||
<a href="https://tui-pages.farmeris.sk" class="inline-flex items-center gap-2 h-12 px-6 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
|
<a href="https://tui-pages.farmeris.sk/book" class="inline-flex items-center gap-2 h-12 px-6 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
|
||||||
Start the tutorial
|
Start the tutorial
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||||||
</a>
|
</a>
|
||||||
@@ -677,7 +713,7 @@ rt.run() // framework handles the rest</code></pre>
|
|||||||
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Resources</h4>
|
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Resources</h4>
|
||||||
<ul class="mt-3 space-y-2 text-sm">
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
<li><a href="https://docs.rs/tui-pages" class="text-zinc-300 hover:text-white">API reference</a></li>
|
<li><a href="https://docs.rs/tui-pages" class="text-zinc-300 hover:text-white">API reference</a></li>
|
||||||
<li><a href="https://tui-pages.farmeris.sk" class="text-zinc-300 hover:text-white">The Book</a></li>
|
<li><a href="https://tui-pages.farmeris.sk/book" class="text-zinc-300 hover:text-white">The Book</a></li>
|
||||||
<li><a href="/examples.html" class="text-zinc-300 hover:text-white">Examples</a></li>
|
<li><a href="/examples.html" class="text-zinc-300 hover:text-white">Examples</a></li>
|
||||||
<li><a href="https://crates.io/crates/tui-pages" class="text-zinc-300 hover:text-white">crates.io</a></li>
|
<li><a href="https://crates.io/crates/tui-pages" class="text-zinc-300 hover:text-white">crates.io</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: https://tui-pages.dev/sitemap.xml
|
Sitemap: https://tui-pages.farmeris.sk/sitemap.xml
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://tui-pages.dev/</loc>
|
<loc>https://tui-pages.farmeris.sk/</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://tui-pages.dev/examples.html</loc>
|
<loc>https://tui-pages.farmeris.sk/examples.html</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
96
static/css/ascii.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
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
|
||||||
|
- swaps the tint for a soft sepia wash in light mode (paper & ink feel)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* 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.7vw, 1.39vh); */ /* 240p */
|
||||||
|
font-size: max(0.463vw, 0.926vh); /* 360p */
|
||||||
|
line-height: 1.0;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--tp-mountain-color, #f4a26b);
|
||||||
|
text-shadow: 0 0 8px var(--tp-mountain-glow, rgba(183, 65, 14, 0.35));
|
||||||
|
opacity: var(--tp-mountain-opacity, 0.45);
|
||||||
|
white-space: pre;
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
transition: color 250ms ease, opacity 250ms ease, text-shadow 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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: faded ink wash on paper ------------------------
|
||||||
|
In winter mode the ASCII becomes a soft sepia watermark — a hint of
|
||||||
|
"topography" rather than a glowing graphic. The scanlines swap to
|
||||||
|
light-on-dark so they still add texture without darkening the page. */
|
||||||
|
:root[data-theme="winter"] .tp-mountain-bg {
|
||||||
|
opacity: 0.12; /* much fainter than the dark mode rust */
|
||||||
|
color: #6b5236; /* warm sepia, harmonizes with cream bg */
|
||||||
|
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(28, 25, 23, 0.05) 2px,
|
||||||
|
rgba(28, 25, 23, 0.05) 3px
|
||||||
|
);
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion: keep the first frame static, no scanline drift. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tp-mountain-bg::after {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,28 +21,128 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Theme (default to dark) -------------------------------------- */
|
/* ---------- Theme tokens -------------------------------------------------
|
||||||
|
Dark (night) is the default. The `[data-theme="winter"]` block redefines
|
||||||
|
the same tokens for the light theme. Everything below reads from these. */
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark light;
|
color-scheme: dark light;
|
||||||
--tp-accent: #b7410e;
|
|
||||||
--tp-accent-fg: #fff5f0;
|
/* Brand (rust) */
|
||||||
--tp-grid-line: rgba(63, 63, 70, 0.35);
|
--tp-accent: #b7410e; /* primary accent */
|
||||||
--tp-hero-from: #0b0b0f;
|
--tp-accent-strong: #d24e15; /* hover/active */
|
||||||
--tp-hero-to: #18181b;
|
--tp-accent-fg: #fff5f0; /* text-on-accent */
|
||||||
|
--tp-accent-soft: rgba(183, 65, 14, 0.12);
|
||||||
|
--tp-accent-glow: rgba(183, 65, 14, 0.35);
|
||||||
|
|
||||||
|
/* Surfaces (dark) */
|
||||||
|
--tp-bg: #09090b; /* page bg */
|
||||||
|
--tp-bg-elev: #18181b; /* raised surface */
|
||||||
|
--tp-bg-soft: rgba(24, 24, 27, 0.5);
|
||||||
|
--tp-bg-card: rgba(24, 24, 27, 0.5);
|
||||||
|
--tp-bg-card-hover: rgba(24, 24, 27, 0.78);
|
||||||
|
--tp-bg-nav: rgba(9, 9, 11, 0.65);
|
||||||
|
--tp-bg-mobile: rgba(9, 9, 11, 0.95);
|
||||||
|
--tp-bg-section: rgba(24, 24, 27, 0.3); /* alt section tint */
|
||||||
|
--tp-bg-code: #0b0b0f;
|
||||||
|
|
||||||
|
/* Text (dark) */
|
||||||
|
--tp-fg: #fafafa; /* primary text */
|
||||||
|
--tp-fg-strong: #ffffff;
|
||||||
|
--tp-fg-secondary: #d4d4d8; /* body */
|
||||||
|
--tp-fg-muted: #a1a1aa;
|
||||||
|
--tp-fg-subtle: #71717a;
|
||||||
|
--tp-fg-code: #f4f4f5;
|
||||||
|
|
||||||
|
/* Borders (dark) */
|
||||||
|
--tp-border: rgba(63, 63, 70, 0.5);
|
||||||
|
--tp-border-strong: rgba(63, 63, 70, 0.7);
|
||||||
|
--tp-border-section:rgba(63, 63, 70, 0.4);
|
||||||
|
|
||||||
|
/* Hero / grid (dark) */
|
||||||
|
--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);
|
||||||
|
--tp-hero-glow-a: rgba(183, 65, 14, 0.10);
|
||||||
|
--tp-hero-glow-b: rgba(126, 231, 135, 0.05);
|
||||||
|
|
||||||
|
/* Mountain background (dark — rust on black) */
|
||||||
|
--tp-mountain-color: #f4a26b;
|
||||||
|
--tp-mountain-glow: rgba(183, 65, 14, 0.35);
|
||||||
|
--tp-mountain-opacity:0.45;
|
||||||
|
|
||||||
|
/* Shadows (dark — soft + deep) */
|
||||||
|
--tp-card-shadow: 0 1px 0 rgba(255, 255, 255, 0.02) inset,
|
||||||
|
0 12px 24px -16px rgba(0, 0, 0, 0.6);
|
||||||
|
--tp-card-shadow-hover:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||||||
|
0 18px 36px -18px rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] {
|
/* Light theme — warm paper. We swap to a stone (warm-gray) palette so the
|
||||||
--tp-accent: #93330c;
|
text and surfaces harmonize with the rust accent instead of fighting it. */
|
||||||
--tp-accent-fg: #fff5f0;
|
:root[data-theme="winter"] {
|
||||||
--tp-grid-line: rgba(228, 228, 231, 0.7);
|
/* Brand — slightly darker rust so it has bite on a light bg */
|
||||||
--tp-hero-from: #fafafa;
|
--tp-accent: #9a3a0e;
|
||||||
--tp-hero-to: #f4f4f5;
|
--tp-accent-strong: #7f2f08;
|
||||||
|
--tp-accent-fg: #fff7ed;
|
||||||
|
--tp-accent-soft: rgba(154, 58, 14, 0.08);
|
||||||
|
--tp-accent-glow: rgba(154, 58, 14, 0.0);
|
||||||
|
|
||||||
|
/* Surfaces (warm off-white, not stark) */
|
||||||
|
--tp-bg: #fbfaf7; /* page bg, slight cream */
|
||||||
|
--tp-bg-elev: #ffffff; /* cards */
|
||||||
|
--tp-bg-soft: rgba(255, 255, 255, 0.7);
|
||||||
|
--tp-bg-card: #ffffff;
|
||||||
|
--tp-bg-card-hover: #ffffff;
|
||||||
|
--tp-bg-nav: rgba(255, 255, 255, 0.78);
|
||||||
|
--tp-bg-mobile: rgba(255, 255, 255, 0.97);
|
||||||
|
--tp-bg-section: #f3f1ec; /* warm section alt */
|
||||||
|
--tp-bg-code: #0b0b0f; /* keep code blocks dark in both themes */
|
||||||
|
|
||||||
|
/* Text (warm dark — stone, not zinc, so it doesn't look like a science lab) */
|
||||||
|
--tp-fg: #1c1917; /* primary (stone-900) */
|
||||||
|
--tp-fg-strong: #0c0a09; /* stone-950 */
|
||||||
|
--tp-fg-secondary: #44403c; /* stone-700 */
|
||||||
|
--tp-fg-muted: #57534e; /* stone-600 */
|
||||||
|
--tp-fg-subtle: #78716c; /* stone-500 */
|
||||||
|
--tp-fg-code: #f4f4f5;
|
||||||
|
|
||||||
|
/* Borders (warm taupe) */
|
||||||
|
--tp-border: rgba(214, 211, 209, 0.9); /* stone-300 */
|
||||||
|
--tp-border-strong: rgba(168, 162, 158, 0.8); /* stone-400 */
|
||||||
|
--tp-border-section:rgba(214, 211, 209, 0.65);
|
||||||
|
|
||||||
|
/* Hero / grid (warm, very subtle) */
|
||||||
|
--tp-grid-line: rgba(180, 130, 80, 0.14);
|
||||||
|
--tp-hero-from: #fbfaf7;
|
||||||
|
--tp-hero-to: #f5f3ee;
|
||||||
|
--tp-text-outline: rgba(255, 255, 255, 0.95);
|
||||||
|
--tp-hero-glow-a: rgba(154, 58, 14, 0.07);
|
||||||
|
--tp-hero-glow-b: rgba(120, 140, 90, 0.05);
|
||||||
|
|
||||||
|
/* Mountain — very subtle, like a faded ink wash on paper */
|
||||||
|
--tp-mountain-color: #8a6a44; /* warm sepia */
|
||||||
|
--tp-mountain-glow: rgba(138, 106, 68, 0);
|
||||||
|
--tp-mountain-opacity:0.10;
|
||||||
|
|
||||||
|
/* Shadows (soft, warm-tinted, like a printed page) */
|
||||||
|
--tp-card-shadow: 0 1px 0 rgba(255, 255, 255, 1) inset,
|
||||||
|
0 1px 2px rgba(28, 25, 23, 0.04),
|
||||||
|
0 8px 24px -12px rgba(28, 25, 23, 0.10);
|
||||||
|
--tp-card-shadow-hover:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 1) inset,
|
||||||
|
0 1px 2px rgba(28, 25, 23, 0.05),
|
||||||
|
0 14px 32px -14px rgba(154, 58, 14, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Body --------------------------------------------------------- */
|
/* ---------- Body --------------------------------------------------------- */
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
||||||
|
background-color: var(--tp-bg);
|
||||||
|
color: var(--tp-fg);
|
||||||
|
transition: background-color 200ms ease, color 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- View Transitions (progressive enhancement) ------------------ */
|
/* ---------- View Transitions (progressive enhancement) ------------------ */
|
||||||
@@ -55,13 +155,43 @@ body {
|
|||||||
|
|
||||||
/* ---------- Hero --------------------------------------------------------- */
|
/* ---------- Hero --------------------------------------------------------- */
|
||||||
.tp-hero {
|
.tp-hero {
|
||||||
background:
|
background: transparent; /* let the mountain ASCII show through */
|
||||||
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%);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
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);
|
||||||
|
}
|
||||||
|
:root[data-theme="winter"] .tp-hero h1,
|
||||||
|
:root[data-theme="winter"] .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 12px rgba(154, 58, 14, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
.tp-hero::before {
|
.tp-hero::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -75,9 +205,13 @@ body {
|
|||||||
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 30%, transparent 80%);
|
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 30%, transparent 80%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
opacity: 0.5; /* subtle grid, not a wall */
|
||||||
|
}
|
||||||
|
:root[data-theme="winter"] .tp-hero::before {
|
||||||
|
opacity: 0.7; /* show a touch more on paper */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle floating code lines in the hero, behind the content */
|
/* Subtle floating glow in the hero, behind the content */
|
||||||
.tp-hero-glow {
|
.tp-hero-glow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -85,67 +219,37 @@ body {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 15% 30%, rgba(183, 65, 14, 0.08), transparent 35%),
|
radial-gradient(circle at 15% 30%, var(--tp-hero-glow-a), transparent 35%),
|
||||||
radial-gradient(circle at 85% 70%, rgba(126, 231, 135, 0.05), transparent 35%);
|
radial-gradient(circle at 85% 70%, var(--tp-hero-glow-b), 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 ---------------------------------------------------- */
|
/* ---------- Glass nav ---------------------------------------------------- */
|
||||||
.tp-nav {
|
.tp-nav {
|
||||||
background: rgba(9, 9, 11, 0.65);
|
background: var(--tp-bg-nav);
|
||||||
backdrop-filter: saturate(160%) blur(12px);
|
backdrop-filter: saturate(160%) blur(12px);
|
||||||
-webkit-backdrop-filter: saturate(160%) blur(12px);
|
-webkit-backdrop-filter: saturate(160%) blur(12px);
|
||||||
border-bottom: 1px solid rgba(63, 63, 70, 0.4);
|
border-bottom: 1px solid var(--tp-border);
|
||||||
}
|
transition: background 200ms ease, border-color 200ms ease;
|
||||||
:root[data-theme="light"] .tp-nav {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
border-bottom-color: rgba(228, 228, 231, 0.7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Feature cards ------------------------------------------------ */
|
/* ---------- Feature cards ------------------------------------------------ */
|
||||||
.tp-card {
|
.tp-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(24, 24, 27, 0.5);
|
background: var(--tp-bg-card);
|
||||||
border: 1px solid rgba(63, 63, 70, 0.5);
|
border: 1px solid var(--tp-border);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
transition: border-color 200ms ease, transform 200ms ease, background 200ms ease;
|
box-shadow: var(--tp-card-shadow);
|
||||||
|
transition: border-color 200ms ease, transform 200ms ease,
|
||||||
|
background 200ms ease, box-shadow 200ms ease;
|
||||||
}
|
}
|
||||||
.tp-card:hover {
|
.tp-card:hover {
|
||||||
border-color: rgba(183, 65, 14, 0.6);
|
border-color: rgba(183, 65, 14, 0.6);
|
||||||
background: rgba(24, 24, 27, 0.75);
|
background: var(--tp-bg-card-hover);
|
||||||
transform: translateY(-2px);
|
box-shadow: var(--tp-card-shadow-hover);
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-card {
|
:root[data-theme="winter"] .tp-card:hover {
|
||||||
background: rgba(255, 255, 255, 0.6);
|
border-color: rgba(154, 58, 14, 0.4);
|
||||||
border-color: rgba(228, 228, 231, 0.8);
|
|
||||||
}
|
|
||||||
:root[data-theme="light"] .tp-card:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-color: rgba(183, 65, 14, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tp-card-icon {
|
.tp-card-icon {
|
||||||
@@ -155,11 +259,14 @@ body {
|
|||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
background: rgba(183, 65, 14, 0.12);
|
background: var(--tp-accent-soft);
|
||||||
color: var(--tp-accent);
|
color: var(--tp-accent);
|
||||||
border: 1px solid rgba(183, 65, 14, 0.25);
|
border: 1px solid var(--tp-accent-glow);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
:root[data-theme="winter"] .tp-card-icon {
|
||||||
|
border-color: rgba(154, 58, 14, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Code blocks (kitchen sink — hljs + custom) ------------------ */
|
/* ---------- Code blocks (kitchen sink — hljs + custom) ------------------ */
|
||||||
.tp-code {
|
.tp-code {
|
||||||
@@ -169,13 +276,19 @@ body {
|
|||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
.tp-code pre {
|
.tp-code pre {
|
||||||
background: #0b0b0f;
|
background: var(--tp-bg-code);
|
||||||
border: 1px solid rgba(63, 63, 70, 0.5);
|
border: 1px solid var(--tp-border);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.25rem 1.5rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
:root[data-theme="winter"] .tp-code pre {
|
||||||
|
/* keep the code panel a little warmer on light pages, like a printed
|
||||||
|
code listing on cream paper */
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: 0 12px 28px -16px rgba(28, 25, 23, 0.35);
|
||||||
|
}
|
||||||
.tp-code pre code.hljs {
|
.tp-code pre code.hljs {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -189,30 +302,36 @@ body {
|
|||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: 1px solid rgba(63, 63, 70, 0.7);
|
border: 1px solid var(--tp-border-strong);
|
||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
background: rgba(39, 39, 42, 0.6);
|
background: rgba(39, 39, 42, 0.6);
|
||||||
color: #f4f4f5;
|
color: var(--tp-fg-code);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-kbd {
|
:root[data-theme="winter"] .tp-kbd {
|
||||||
background: #fff;
|
background: #ffffff;
|
||||||
color: #18181b;
|
color: #1c1917;
|
||||||
border-color: #d4d4d8;
|
border-color: #d6d3d1;
|
||||||
|
box-shadow: 0 1px 0 rgba(28, 25, 23, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Terminal window wrapper -------------------------------------- */
|
/* ---------- Terminal window wrapper -------------------------------------- */
|
||||||
.tp-terminal {
|
.tp-terminal {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0.875rem;
|
border-radius: 0.875rem;
|
||||||
background: #0b0b0f;
|
background: transparent; /* let the body ASCII art show through */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
0 0 0 1px rgba(244, 162, 107, 0.18), /* rust-orange hairline, brand */
|
||||||
0 30px 60px -20px rgba(0, 0, 0, 0.6),
|
0 30px 60px -20px rgba(0, 0, 0, 0.55),
|
||||||
0 18px 36px -18px rgba(0, 0, 0, 0.5);
|
0 18px 36px -18px rgba(0, 0, 0, 0.45);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(63, 63, 70, 0.4);
|
}
|
||||||
|
:root[data-theme="winter"] .tp-terminal {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(154, 58, 14, 0.18),
|
||||||
|
0 24px 48px -20px rgba(154, 58, 14, 0.22),
|
||||||
|
0 12px 24px -16px rgba(28, 25, 23, 0.10);
|
||||||
}
|
}
|
||||||
.tp-terminal::after {
|
.tp-terminal::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -220,7 +339,23 @@ body {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: inherit;
|
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 */
|
||||||
|
}
|
||||||
|
:root[data-theme="winter"] .tp-terminal::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0.10) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 18%,
|
||||||
|
rgba(255, 255, 255, 0) 82%,
|
||||||
|
rgba(255, 255, 255, 0.10) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Selection --------------------------------------------------- */
|
/* ---------- Selection --------------------------------------------------- */
|
||||||
@@ -240,27 +375,153 @@ body {
|
|||||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(63, 63, 70, 0.6);
|
background: var(--tp-border-strong);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover { background-color: rgba(82, 82, 91, 0.8); background-clip: padding-box; }
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--tp-fg-subtle);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- No-FOUC theme flash ----------------------------------------- */
|
/* ---------- No-FOUC theme flash ----------------------------------------- */
|
||||||
html:not([data-theme]) body { background: #09090b; }
|
html:not([data-theme]) body { background: #09090b; }
|
||||||
:root[data-theme="light"] body { background: #fafafa; }
|
|
||||||
|
|
||||||
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
|
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
|
|
||||||
/* ---------- DaisyUI overrides for our theme ----------------------------- */
|
/* ==========================================================================
|
||||||
/* Bring DaisyUI's primary to our accent so .btn-primary etc. look on-brand. */
|
THEME-AWARE TAILWIND OVERRIDES
|
||||||
[data-theme="night"] {
|
--------------------------------------------------------------------------
|
||||||
--p: 28 70% 36%;
|
The HTML uses hardcoded `text-zinc-*`, `bg-zinc-*`, `border-zinc-*` etc.
|
||||||
--pc: 0 0% 100%;
|
(so the source reads like a normal dark-mode-first site). In light mode
|
||||||
--b1: 240 5% 8%;
|
we transparently remap those zinc slots to our warm stone palette.
|
||||||
--b2: 240 4% 12%;
|
This block has higher selector specificity (`:root[data-theme=...] .foo`)
|
||||||
--b3: 240 4% 16%;
|
than the Tailwind utility class on its own, so it wins without `!important`.
|
||||||
--bc: 0 0% 96%;
|
========================================================================== */
|
||||||
|
:root[data-theme="winter"] {
|
||||||
|
|
||||||
|
/* ---- DaisyUI body tokens ----------------------------------------------
|
||||||
|
daisyUI 4 uses OKLCH under the hood (`oklch(var(--b1) / .N)`); our custom
|
||||||
|
HSL-flavoured values don't parse, so `bg-base-100` would resolve to
|
||||||
|
transparent. We override the class directly with our token-driven colour. */
|
||||||
|
.bg-base-100 { background-color: var(--tp-bg); }
|
||||||
|
.text-base-content { color: var(--tp-fg); }
|
||||||
|
|
||||||
|
/* ---- text ---- */
|
||||||
|
.text-zinc-50 { color: #0c0a09; }
|
||||||
|
.text-zinc-100 { color: #1c1917; }
|
||||||
|
.text-zinc-200 { color: #292524; }
|
||||||
|
.text-zinc-300 { color: #44403c; }
|
||||||
|
.text-zinc-400 { color: #57534e; }
|
||||||
|
.text-zinc-500 { color: #78716c; }
|
||||||
|
.text-zinc-600 { color: #57534e; }
|
||||||
|
.text-zinc-700 { color: #d6d3d1; } /* dividers + rare text */
|
||||||
|
.text-zinc-800 { color: #e7e5e4; }
|
||||||
|
.text-zinc-900 { color: #ffffff; }
|
||||||
|
.text-zinc-950 { color: #ffffff; }
|
||||||
|
.text-white { color: #1c1917; }
|
||||||
|
|
||||||
|
/* ---- backgrounds ---- */
|
||||||
|
.bg-zinc-50 { background-color: #fafaf9; }
|
||||||
|
.bg-zinc-100 { background-color: #f5f5f4; }
|
||||||
|
.bg-zinc-200 { background-color: #e7e5e4; }
|
||||||
|
.bg-zinc-300 { background-color: #d6d3d1; }
|
||||||
|
.bg-zinc-400 { background-color: #a8a29e; }
|
||||||
|
.bg-zinc-500 { background-color: #78716c; }
|
||||||
|
.bg-zinc-600 { background-color: #57534e; }
|
||||||
|
.bg-zinc-700 { background-color: #d6d3d1; }
|
||||||
|
.bg-zinc-800 { background-color: #e7e5e4; }
|
||||||
|
.bg-zinc-900 { background-color: #ffffff; }
|
||||||
|
.bg-zinc-900\/40 { background-color: rgba(255, 255, 255, 0.65); }
|
||||||
|
.bg-zinc-900\/50 { background-color: rgba(255, 255, 255, 0.75); }
|
||||||
|
.bg-zinc-900\/60 { background-color: rgba(255, 255, 255, 0.85); }
|
||||||
|
.bg-zinc-950 { background-color: #ffffff; }
|
||||||
|
.bg-zinc-950\/30 { background-color: #f3f1ec; } /* section alt */
|
||||||
|
.bg-zinc-950\/40 { background-color: #f5f3ee; } /* trust strip */
|
||||||
|
.bg-zinc-950\/50 { background-color: #ffffff; }
|
||||||
|
.bg-zinc-950\/60 { background-color: #ffffff; }
|
||||||
|
.bg-zinc-950\/80 { background-color: #ffffff; }
|
||||||
|
.bg-zinc-950\/95 { background-color: #ffffff; }
|
||||||
|
|
||||||
|
/* ---- borders ---- */
|
||||||
|
.border-zinc-700 { border-color: #d6d3d1; }
|
||||||
|
.border-zinc-800 { border-color: #e7e5e4; }
|
||||||
|
.border-zinc-800\/40 { border-color: rgba(214, 211, 209, 0.55); }
|
||||||
|
.border-zinc-800\/60 { border-color: rgba(214, 211, 209, 0.75); }
|
||||||
|
.border-zinc-900\/50 { border-color: rgba(214, 211, 209, 0.7); }
|
||||||
|
|
||||||
|
/* The decoration on links (ratatui etc) */
|
||||||
|
.decoration-zinc-700 { text-decoration-color: #d6d3d1; }
|
||||||
|
|
||||||
|
/* ---- hover states (mirror the text/bg map, with a touch more contrast) ---- */
|
||||||
|
.hover\:text-white:hover { color: #0c0a09; }
|
||||||
|
.hover\:text-zinc-100:hover { color: #0c0a09; }
|
||||||
|
.hover\:text-zinc-200:hover { color: #1c1917; }
|
||||||
|
.hover\:text-zinc-300:hover { color: #1c1917; }
|
||||||
|
.hover\:text-zinc-400:hover { color: #1c1917; }
|
||||||
|
.hover\:bg-zinc-800\/50:hover { background-color: rgba(231, 229, 228, 0.7); }
|
||||||
|
.hover\:bg-zinc-800\/60:hover { background-color: rgba(231, 229, 228, 0.85); }
|
||||||
|
.hover\:bg-zinc-900:hover { background-color: #f5f3ee; }
|
||||||
|
.hover\:border-zinc-500:hover { border-color: #a8a29e; }
|
||||||
|
.hover\:border-zinc-700:hover { border-color: #a8a29e; }
|
||||||
|
.hover\:decoration-accent:hover { text-decoration-color: var(--tp-accent); }
|
||||||
|
|
||||||
|
/* ---- accent (rust) — slightly darker on light for legibility ---- */
|
||||||
|
.text-accent { color: #9a3a0e; }
|
||||||
|
.bg-accent { background-color: #9a3a0e; }
|
||||||
|
.bg-accent\/5 { background-color: rgba(154, 58, 14, 0.05); }
|
||||||
|
.bg-accent\/10 { background-color: rgba(154, 58, 14, 0.08); }
|
||||||
|
.bg-accent\/15 { background-color: rgba(154, 58, 14, 0.12); }
|
||||||
|
.border-accent { border-color: #9a3a0e; }
|
||||||
|
.border-accent\/30 { border-color: rgba(154, 58, 14, 0.25); }
|
||||||
|
.border-accent\/40 { border-color: rgba(154, 58, 14, 0.35); }
|
||||||
|
.border-accent\/60 { border-color: rgba(154, 58, 14, 0.55); }
|
||||||
|
.hover\:bg-accent\/90:hover { background-color: #7f2f08; }
|
||||||
|
.hover\:border-accent\/60:hover { border-color: rgba(154, 58, 14, 0.55); }
|
||||||
|
.hover\:text-accent:hover { color: #9a3a0e; }
|
||||||
|
.from-accent\/15 { --tw-gradient-from: rgba(154, 58, 14, 0.18); --tw-gradient-to: rgba(154, 58, 14, 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
|
||||||
|
.to-emerald-500\/5 { --tw-gradient-to: rgba(16, 185, 129, 0.06); }
|
||||||
|
|
||||||
|
/* ---- tab pill in code-example tab bar ---- */
|
||||||
|
.bg-zinc-800 { background-color: #e7e5e4; }
|
||||||
|
.bg-zinc-800.text-white { color: #1c1917; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
DaisyUI overrides for our theme
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
DaisyUI 4 ships a built-in `night` theme — we already use it for dark.
|
||||||
|
`winter` is our own theme, so we register its HSL tokens so things like
|
||||||
|
`.bg-base-100` and `.text-base-content` resolve correctly.
|
||||||
|
Format is "H S% L%" (no hsl() wrapper) — DaisyUI concatenates.
|
||||||
|
========================================================================== */
|
||||||
|
[data-theme="night"] {
|
||||||
|
--p: 20 75% 38%; /* primary — rust */
|
||||||
|
--pc: 20 100% 97%;
|
||||||
|
--b1: 240 6% 7%; /* base 100 */
|
||||||
|
--b2: 240 5% 11%;
|
||||||
|
--b3: 240 5% 14%;
|
||||||
|
--bc: 0 0% 96%; /* base content */
|
||||||
|
/* DaisyUI 4 expects OKLCH and ignores HSL on modern browsers. Provide
|
||||||
|
the fallback so the body has a real colour even if OKLCH parsing fails. */
|
||||||
|
--fallback-b1: #09090b;
|
||||||
|
--fallback-bc: #fafafa;
|
||||||
|
/* Make DaisyUI semantic tokens resolve to our rust accent. */
|
||||||
|
.bg-base-100 { background-color: var(--tp-bg); }
|
||||||
|
.text-base-content { color: var(--tp-fg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="winter"] {
|
||||||
|
--p: 19 82% 32%; /* primary — darker rust, sits on cream */
|
||||||
|
--pc: 30 100% 97%;
|
||||||
|
--b1: 36 25% 97%; /* base 100 — warm paper */
|
||||||
|
--b2: 36 18% 94%;
|
||||||
|
--b3: 36 12% 89%;
|
||||||
|
--bc: 24 12% 12%; /* base content — warm near-black */
|
||||||
|
--n: 24 10% 18%; /* neutral */
|
||||||
|
--nc: 36 25% 97%;
|
||||||
|
--fallback-b1: #fbfaf7;
|
||||||
|
--fallback-bc: #1c1917;
|
||||||
}
|
}
|
||||||
|
|||||||
120
static/img/terminal-canvas-light.svg
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A text editor built with the tui-pages textarea: relative line numbers and Rust syntax highlighting">
|
||||||
|
<!-- Light-mode twin of terminal-canvas.svg. Same 80x25 grid, light neutral palette. -->
|
||||||
|
<style>
|
||||||
|
.text { fill: #26272b; }
|
||||||
|
.muted { fill: #5a5a62; }
|
||||||
|
.dim { fill: #8a8a90; }
|
||||||
|
.blue { fill: #1f7fb8; }
|
||||||
|
.gold { fill: #a8862a; }
|
||||||
|
.green { fill: #1f9d3f; }
|
||||||
|
.pink { fill: #d6398b; }
|
||||||
|
.greenfg{ fill: #f2fff2; }
|
||||||
|
.curfg { fill: #f4f4f2; }
|
||||||
|
.b { font-weight: 700; }
|
||||||
|
text {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<g xml:space="preserve">
|
||||||
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#f4f4f2" stroke="#d8d8d2"/>
|
||||||
|
|
||||||
|
<rect x="0" y="2" width="72" height="20" class="green"/>
|
||||||
|
<text x="8" y="16" class="greenfg b">main.rs</text>
|
||||||
|
<text x="80" y="16" class="muted">lib.rs</text>
|
||||||
|
<text x="144" y="16" class="muted">Cargo.toml</text>
|
||||||
|
|
||||||
|
<rect x="0" y="96" width="640" height="16" fill="#e7e7e3"/>
|
||||||
|
|
||||||
|
<text x="16" y="44" class="dim" text-anchor="end">4</text>
|
||||||
|
<text x="16" y="60" class="dim" text-anchor="end">3</text>
|
||||||
|
<text x="16" y="76" class="dim" text-anchor="end">2</text>
|
||||||
|
<text x="16" y="92" class="dim" text-anchor="end">1</text>
|
||||||
|
<text x="8" y="108" class="gold b">5</text>
|
||||||
|
<text x="16" y="124" class="dim" text-anchor="end">1</text>
|
||||||
|
<text x="16" y="140" class="dim" text-anchor="end">2</text>
|
||||||
|
<text x="16" y="156" class="dim" text-anchor="end">3</text>
|
||||||
|
<text x="16" y="172" class="dim" text-anchor="end">4</text>
|
||||||
|
<text x="16" y="188" class="dim" text-anchor="end">5</text>
|
||||||
|
<text x="16" y="204" class="dim" text-anchor="end">6</text>
|
||||||
|
|
||||||
|
<text x="32" y="44" class="pink">use</text>
|
||||||
|
<text x="64" y="44" class="text">tui_pages</text>
|
||||||
|
<text x="136" y="44" class="muted">::</text>
|
||||||
|
<text x="152" y="44" class="muted">{</text>
|
||||||
|
<text x="160" y="44" class="gold">Page</text>
|
||||||
|
<text x="192" y="44" class="muted">,</text>
|
||||||
|
<text x="208" y="44" class="gold">TextArea</text>
|
||||||
|
<text x="272" y="44" class="muted">}</text>
|
||||||
|
<text x="280" y="44" class="muted">;</text>
|
||||||
|
|
||||||
|
<text x="32" y="76" class="pink">pub</text>
|
||||||
|
<text x="64" y="76" class="pink">fn</text>
|
||||||
|
<text x="88" y="76" class="blue">editor</text>
|
||||||
|
<text x="136" y="76" class="muted">()</text>
|
||||||
|
<text x="160" y="76" class="muted">-></text>
|
||||||
|
<text x="184" y="76" class="gold">Page</text>
|
||||||
|
<text x="224" y="76" class="muted">{</text>
|
||||||
|
|
||||||
|
<text x="64" y="92" class="pink">let</text>
|
||||||
|
<text x="96" y="92" class="text">title</text>
|
||||||
|
<text x="144" y="92" class="muted">=</text>
|
||||||
|
<text x="160" y="92" class="green">"Editor"</text>
|
||||||
|
<text x="224" y="92" class="muted">;</text>
|
||||||
|
|
||||||
|
<text x="64" y="108" class="pink">let</text>
|
||||||
|
<text x="96" y="108" class="pink">mut</text>
|
||||||
|
<rect x="128" y="96" width="8" height="16" fill="#26272b"/>
|
||||||
|
<text x="128" y="108" class="curfg">a</text>
|
||||||
|
<text x="136" y="108" class="text">rea</text>
|
||||||
|
<text x="168" y="108" class="muted">=</text>
|
||||||
|
<text x="184" y="108" class="gold">TextArea</text>
|
||||||
|
<text x="248" y="108" class="muted">::</text>
|
||||||
|
<text x="264" y="108" class="blue">default</text>
|
||||||
|
<text x="320" y="108" class="muted">()</text>
|
||||||
|
<text x="336" y="108" class="muted">;</text>
|
||||||
|
|
||||||
|
<text x="64" y="124" class="text">area</text>
|
||||||
|
<text x="96" y="124" class="muted">.</text>
|
||||||
|
<text x="104" y="124" class="blue">set_placeholder</text>
|
||||||
|
<text x="224" y="124" class="muted">(</text>
|
||||||
|
<text x="232" y="124" class="green">"Type here…"</text>
|
||||||
|
<text x="328" y="124" class="muted">)</text>
|
||||||
|
<text x="336" y="124" class="muted">;</text>
|
||||||
|
|
||||||
|
<text x="64" y="156" class="gold">Page</text>
|
||||||
|
<text x="96" y="156" class="muted">::</text>
|
||||||
|
<text x="112" y="156" class="blue">new</text>
|
||||||
|
<text x="136" y="156" class="muted">(</text>
|
||||||
|
<text x="144" y="156" class="text">title</text>
|
||||||
|
<text x="184" y="156" class="muted">)</text>
|
||||||
|
|
||||||
|
<text x="96" y="172" class="muted">.</text>
|
||||||
|
<text x="104" y="172" class="blue">child</text>
|
||||||
|
<text x="144" y="172" class="muted">(</text>
|
||||||
|
<text x="152" y="172" class="text">area</text>
|
||||||
|
<text x="184" y="172" class="muted">)</text>
|
||||||
|
|
||||||
|
<text x="96" y="188" class="muted">.</text>
|
||||||
|
<text x="104" y="188" class="blue">on_key</text>
|
||||||
|
<text x="152" y="188" class="muted">(</text>
|
||||||
|
<text x="160" y="188" class="green">'q'</text>
|
||||||
|
<text x="184" y="188" class="muted">,</text>
|
||||||
|
<text x="200" y="188" class="muted">|</text>
|
||||||
|
<text x="208" y="188" class="text">app</text>
|
||||||
|
<text x="232" y="188" class="muted">|</text>
|
||||||
|
<text x="248" y="188" class="text">app</text>
|
||||||
|
<text x="272" y="188" class="muted">.</text>
|
||||||
|
<text x="280" y="188" class="blue">quit</text>
|
||||||
|
<text x="312" y="188" class="muted">())</text>
|
||||||
|
|
||||||
|
<text x="32" y="204" class="muted">}</text>
|
||||||
|
|
||||||
|
<text x="32" y="380" class="green b">NOR</text>
|
||||||
|
<text x="120" y="380" class="muted">main.rs</text>
|
||||||
|
<text x="256" y="380" class="muted">Ln 5, Col 18</text>
|
||||||
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,52 +1,138 @@
|
|||||||
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/canvas terminal screenshot">
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A text editor built with the tui-pages textarea: relative line numbers and Rust syntax highlighting">
|
||||||
|
<!--
|
||||||
|
tui-pages textarea showcased as a modal-editor (vim/helix style) on a strict
|
||||||
|
80x25 monospace grid (cell = 8x16 px). Each cell carries at most two colors.
|
||||||
|
Gutter: current line shows its absolute number, others are relative.
|
||||||
|
Syntax: kw=pink, type=gold, fn=blue, string=green, punctuation=muted.
|
||||||
|
-->
|
||||||
<style>
|
<style>
|
||||||
.bg { fill: #18181b; }
|
.text { fill: #d7d7db; }
|
||||||
.chrome { fill: #27272a; }
|
.muted { fill: #9a9aa2; }
|
||||||
.dot { fill: #52525b; }
|
.dim { fill: #6b6b72; }
|
||||||
.field { fill: #0e0e10; stroke: #3f3f46; stroke-width: 1; }
|
.blue { fill: #3ba7e0; }
|
||||||
.field-focus { fill: #1c130f; stroke: #b7410e; stroke-width: 2; }
|
.gold { fill: #d9b54a; }
|
||||||
.btn { fill: #b7410e; }
|
.green { fill: #46d160; }
|
||||||
.sep { stroke: #27272a; stroke-width: 1; }
|
.pink { fill: #f25fa0; }
|
||||||
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
|
.greenfg{ fill: #0c130c; }
|
||||||
.t-md { font-size: 14px; }
|
.curfg { fill: #1a1b1e; }
|
||||||
.t-sm { font-size: 12px; }
|
.b { font-weight: 700; }
|
||||||
.t-xs { font-size: 11px; }
|
text {
|
||||||
.muted { fill: #a1a1aa; }
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
.dim { fill: #71717a; }
|
font-size: 13px;
|
||||||
.label { fill: #a1a1aa; letter-spacing: 0.08em; }
|
text-rendering: geometricPrecision;
|
||||||
.btn-fg { fill: #fff5f0; }
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<rect class="bg" width="640" height="400" rx="12"/>
|
<g xml:space="preserve">
|
||||||
<rect class="chrome" width="640" height="36" rx="12"/>
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#1a1b1e" stroke="#2a2b30"/>
|
||||||
<rect class="chrome" y="24" width="640" height="12"/>
|
|
||||||
<line class="sep" x1="0" y1="36" x2="640" y2="36"/>
|
|
||||||
|
|
||||||
<circle class="dot" cx="20" cy="18" r="5.5"/>
|
<!-- buffer tabs (main.rs active) -->
|
||||||
<circle class="dot" cx="40" cy="18" r="5.5"/>
|
<rect x="0" y="2" width="72" height="20" class="green"/>
|
||||||
<circle class="dot" cx="60" cy="18" r="5.5"/>
|
<text x="8" y="16" class="greenfg b">main.rs</text>
|
||||||
|
<text x="80" y="16" class="muted">lib.rs</text>
|
||||||
|
<text x="144" y="16" class="muted">Cargo.toml</text>
|
||||||
|
|
||||||
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/canvas — login page</text>
|
<!-- cursorline -->
|
||||||
|
<rect x="0" y="96" width="640" height="16" fill="#232428"/>
|
||||||
|
|
||||||
<text x="40" y="80" font-size="20" font-weight="700">Sign in</text>
|
<!-- gutter: relative numbers, current line absolute -->
|
||||||
<text x="40" y="104" font-size="12" class="muted">Use Tab to move between fields. Enter to submit.</text>
|
<text x="16" y="44" class="dim" text-anchor="end">4</text>
|
||||||
|
<text x="16" y="60" class="dim" text-anchor="end">3</text>
|
||||||
|
<text x="16" y="76" class="dim" text-anchor="end">2</text>
|
||||||
|
<text x="16" y="92" class="dim" text-anchor="end">1</text>
|
||||||
|
<text x="8" y="108" class="gold b">5</text>
|
||||||
|
<text x="16" y="124" class="dim" text-anchor="end">1</text>
|
||||||
|
<text x="16" y="140" class="dim" text-anchor="end">2</text>
|
||||||
|
<text x="16" y="156" class="dim" text-anchor="end">3</text>
|
||||||
|
<text x="16" y="172" class="dim" text-anchor="end">4</text>
|
||||||
|
<text x="16" y="188" class="dim" text-anchor="end">5</text>
|
||||||
|
<text x="16" y="204" class="dim" text-anchor="end">6</text>
|
||||||
|
|
||||||
<text x="40" y="156" font-size="10" class="label">USERNAME</text>
|
<!-- line 1: use tui_pages::{Page, TextArea}; -->
|
||||||
<rect class="field-focus" x="40" y="164" width="560" height="38" rx="6"/>
|
<text x="32" y="44" class="pink">use</text>
|
||||||
<text x="56" y="189" font-size="14">filip</text>
|
<text x="64" y="44" class="text">tui_pages</text>
|
||||||
<rect x="92" y="172" width="2" height="22" fill="#b7410e">
|
<text x="136" y="44" class="muted">::</text>
|
||||||
<animate attributeName="opacity" values="1;0;1" dur="1s" repeatCount="indefinite"/>
|
<text x="152" y="44" class="muted">{</text>
|
||||||
</rect>
|
<text x="160" y="44" class="gold">Page</text>
|
||||||
|
<text x="192" y="44" class="muted">,</text>
|
||||||
|
<text x="208" y="44" class="gold">TextArea</text>
|
||||||
|
<text x="272" y="44" class="muted">}</text>
|
||||||
|
<text x="280" y="44" class="muted">;</text>
|
||||||
|
|
||||||
<text x="40" y="232" font-size="10" class="label">PASSWORD</text>
|
<!-- line 3: pub fn editor() -> Page { -->
|
||||||
<rect class="field" x="40" y="240" width="560" height="38" rx="6"/>
|
<text x="32" y="76" class="pink">pub</text>
|
||||||
<text x="56" y="265" font-size="14" class="muted">••••••••••••</text>
|
<text x="64" y="76" class="pink">fn</text>
|
||||||
|
<text x="88" y="76" class="blue">editor</text>
|
||||||
|
<text x="136" y="76" class="muted">()</text>
|
||||||
|
<text x="160" y="76" class="muted">-></text>
|
||||||
|
<text x="184" y="76" class="gold">Page</text>
|
||||||
|
<text x="224" y="76" class="muted">{</text>
|
||||||
|
|
||||||
<rect class="btn" x="40" y="300" width="140" height="38" rx="6"/>
|
<!-- line 4: let title = "Editor"; -->
|
||||||
<text x="110" y="324" text-anchor="middle" font-size="13" class="btn-fg" font-weight="600">Sign in</text>
|
<text x="64" y="92" class="pink">let</text>
|
||||||
<text x="200" y="324" font-size="11" class="dim">esc · cancel</text>
|
<text x="96" y="92" class="text">title</text>
|
||||||
|
<text x="144" y="92" class="muted">=</text>
|
||||||
|
<text x="160" y="92" class="green">"Editor"</text>
|
||||||
|
<text x="224" y="92" class="muted">;</text>
|
||||||
|
|
||||||
<line class="sep" x1="0" y1="358" x2="640" y2="358"/>
|
<!-- line 5 (current): let mut area = TextArea::default(); -->
|
||||||
<text x="20" y="380" font-size="11" class="dim">tab next · shift+tab prev · enter submit</text>
|
<text x="64" y="108" class="pink">let</text>
|
||||||
<text x="620" y="380" text-anchor="end" font-size="11" class="dim">canvas feature enabled</text>
|
<text x="96" y="108" class="pink">mut</text>
|
||||||
|
<rect x="128" y="96" width="8" height="16" fill="#d7d7db"/>
|
||||||
|
<text x="128" y="108" class="curfg">a</text>
|
||||||
|
<text x="136" y="108" class="text">rea</text>
|
||||||
|
<text x="168" y="108" class="muted">=</text>
|
||||||
|
<text x="184" y="108" class="gold">TextArea</text>
|
||||||
|
<text x="248" y="108" class="muted">::</text>
|
||||||
|
<text x="264" y="108" class="blue">default</text>
|
||||||
|
<text x="320" y="108" class="muted">()</text>
|
||||||
|
<text x="336" y="108" class="muted">;</text>
|
||||||
|
|
||||||
|
<!-- line 6: area.set_placeholder("Type here…"); -->
|
||||||
|
<text x="64" y="124" class="text">area</text>
|
||||||
|
<text x="96" y="124" class="muted">.</text>
|
||||||
|
<text x="104" y="124" class="blue">set_placeholder</text>
|
||||||
|
<text x="224" y="124" class="muted">(</text>
|
||||||
|
<text x="232" y="124" class="green">"Type here…"</text>
|
||||||
|
<text x="328" y="124" class="muted">)</text>
|
||||||
|
<text x="336" y="124" class="muted">;</text>
|
||||||
|
|
||||||
|
<!-- line 8: Page::new(title) -->
|
||||||
|
<text x="64" y="156" class="gold">Page</text>
|
||||||
|
<text x="96" y="156" class="muted">::</text>
|
||||||
|
<text x="112" y="156" class="blue">new</text>
|
||||||
|
<text x="136" y="156" class="muted">(</text>
|
||||||
|
<text x="144" y="156" class="text">title</text>
|
||||||
|
<text x="184" y="156" class="muted">)</text>
|
||||||
|
|
||||||
|
<!-- line 9: .child(area) -->
|
||||||
|
<text x="96" y="172" class="muted">.</text>
|
||||||
|
<text x="104" y="172" class="blue">child</text>
|
||||||
|
<text x="144" y="172" class="muted">(</text>
|
||||||
|
<text x="152" y="172" class="text">area</text>
|
||||||
|
<text x="184" y="172" class="muted">)</text>
|
||||||
|
|
||||||
|
<!-- line 10: .on_key('q', |app| app.quit()) -->
|
||||||
|
<text x="96" y="188" class="muted">.</text>
|
||||||
|
<text x="104" y="188" class="blue">on_key</text>
|
||||||
|
<text x="152" y="188" class="muted">(</text>
|
||||||
|
<text x="160" y="188" class="green">'q'</text>
|
||||||
|
<text x="184" y="188" class="muted">,</text>
|
||||||
|
<text x="200" y="188" class="muted">|</text>
|
||||||
|
<text x="208" y="188" class="text">app</text>
|
||||||
|
<text x="232" y="188" class="muted">|</text>
|
||||||
|
<text x="248" y="188" class="text">app</text>
|
||||||
|
<text x="272" y="188" class="muted">.</text>
|
||||||
|
<text x="280" y="188" class="blue">quit</text>
|
||||||
|
<text x="312" y="188" class="muted">())</text>
|
||||||
|
|
||||||
|
<!-- line 11: } -->
|
||||||
|
<text x="32" y="204" class="muted">}</text>
|
||||||
|
|
||||||
|
<!-- status line -->
|
||||||
|
<text x="32" y="380" class="green b">NOR</text>
|
||||||
|
<text x="120" y="380" class="muted">main.rs</text>
|
||||||
|
<text x="256" y="380" class="muted">Ln 5, Col 18</text>
|
||||||
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 6.0 KiB |
80
static/img/terminal-default-light.svg
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A register screen built with tui-pages: tab bar, rounded panel, and first-class role autocomplete">
|
||||||
|
<!-- Light-mode twin of terminal-default.svg. Same 80x25 grid, light neutral palette. -->
|
||||||
|
<style>
|
||||||
|
.text { fill: #26272b; }
|
||||||
|
.muted { fill: #5a5a62; }
|
||||||
|
.dim { fill: #8a8a90; }
|
||||||
|
.ghost { fill: #9a9aa2; }
|
||||||
|
.blue { fill: #1f7fb8; }
|
||||||
|
.gold { fill: #a8862a; }
|
||||||
|
.green { fill: #1f9d3f; }
|
||||||
|
.peachfg{ fill: #2a1c08; }
|
||||||
|
.greenfg{ fill: #f2fff2; }
|
||||||
|
.b { font-weight: 700; }
|
||||||
|
text {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<g xml:space="preserve">
|
||||||
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#f4f4f2" stroke="#d8d8d2"/>
|
||||||
|
|
||||||
|
<text x="8" y="16" class="muted">Login</text>
|
||||||
|
<rect x="56" y="2" width="80" height="20" class="green"/>
|
||||||
|
<text x="64" y="16" class="greenfg b">Register</text>
|
||||||
|
<text x="144" y="16" class="muted">Admin_Panel</text>
|
||||||
|
<text x="248" y="16" class="muted">Quit</text>
|
||||||
|
|
||||||
|
<text x="168" y="60" class="blue b" textLength="304" lengthAdjust="spacing">╭─ Register ─────────────────────────╮</text>
|
||||||
|
<text x="168" y="76" class="blue">│</text><text x="464" y="76" class="blue">│</text>
|
||||||
|
<text x="168" y="92" class="blue">│</text><text x="464" y="92" class="blue">│</text>
|
||||||
|
<text x="168" y="108" class="blue">│</text><text x="464" y="108" class="blue">│</text>
|
||||||
|
<text x="168" y="124" class="blue">│</text><text x="464" y="124" class="blue">│</text>
|
||||||
|
<text x="168" y="140" class="blue">│</text><text x="464" y="140" class="blue">│</text>
|
||||||
|
<text x="168" y="156" class="blue">│</text><text x="464" y="156" class="blue">│</text>
|
||||||
|
<text x="168" y="172" class="blue">│</text><text x="464" y="172" class="blue">│</text>
|
||||||
|
<text x="168" y="188" class="blue">│</text><text x="464" y="188" class="blue">│</text>
|
||||||
|
<text x="168" y="204" class="blue">│</text><text x="464" y="204" class="blue">│</text>
|
||||||
|
<text x="168" y="220" class="blue">│</text><text x="464" y="220" class="blue">│</text>
|
||||||
|
<text x="168" y="236" class="blue">│</text><text x="464" y="236" class="blue">│</text>
|
||||||
|
<text x="168" y="252" class="blue">│</text><text x="464" y="252" class="blue">│</text>
|
||||||
|
<text x="168" y="268" class="blue">│</text><text x="464" y="268" class="blue">│</text>
|
||||||
|
<text x="168" y="284" class="blue">│</text><text x="464" y="284" class="blue">│</text>
|
||||||
|
<text x="168" y="300" class="blue">│</text><text x="464" y="300" class="blue">│</text>
|
||||||
|
<text x="168" y="316" class="blue">│</text><text x="464" y="316" class="blue">│</text>
|
||||||
|
<text x="168" y="332" class="blue">│</text><text x="464" y="332" class="blue">│</text>
|
||||||
|
<text x="168" y="348" class="blue" textLength="304" lengthAdjust="spacing">╰────────────────────────────────────╯</text>
|
||||||
|
|
||||||
|
<text x="184" y="92" class="muted">Create your account</text>
|
||||||
|
|
||||||
|
<text x="184" y="124" class="muted">Email</text>
|
||||||
|
<text x="280" y="124" class="text">ada@acme.io</text>
|
||||||
|
|
||||||
|
<text x="184" y="156" class="muted">Password</text>
|
||||||
|
<text x="280" y="156" class="muted">••••••••••</text>
|
||||||
|
|
||||||
|
<text x="184" y="188" class="muted">Confirm</text>
|
||||||
|
<text x="280" y="188" class="muted">••••••••••</text>
|
||||||
|
|
||||||
|
<text x="184" y="220" class="muted">Role</text>
|
||||||
|
<text x="280" y="220" class="text">a</text>
|
||||||
|
<rect x="288" y="208" width="2" height="15" fill="#c98a2a"/>
|
||||||
|
<text x="288" y="220" class="ghost">dmin</text>
|
||||||
|
|
||||||
|
<rect x="280" y="224" width="96" height="32" fill="#ffffff"/>
|
||||||
|
<rect x="280" y="224" width="96" height="16" fill="#f0c27a"/>
|
||||||
|
<text x="280" y="236" class="peachfg b">admin</text>
|
||||||
|
<text x="280" y="252" class="text">accountant</text>
|
||||||
|
|
||||||
|
<text x="184" y="300" class="green b" textLength="144" lengthAdjust="spacing">╭────────────────╮</text>
|
||||||
|
<text x="184" y="316" class="green b" textLength="144" lengthAdjust="spacing">│ Create account │</text>
|
||||||
|
<text x="184" y="332" class="green b" textLength="144" lengthAdjust="spacing">╰────────────────╯</text>
|
||||||
|
<text x="360" y="316" class="dim">enter ↵</text>
|
||||||
|
|
||||||
|
<text x="32" y="380" class="green b">INS</text>
|
||||||
|
<text x="72" y="380" class="muted">[+]</text>
|
||||||
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -1,57 +1,93 @@
|
|||||||
<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">
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A register screen built with tui-pages: tab bar, rounded panel, and first-class role autocomplete">
|
||||||
|
<!--
|
||||||
|
tui-pages UX showcase on a strict 80x25 monospace grid (cell = 8x16 px).
|
||||||
|
Each cell carries at most two colors (fg + bg), like a real terminal buffer.
|
||||||
|
Frames use rounded box-drawing chars; fills/highlights are grid-snapped rects.
|
||||||
|
-->
|
||||||
<style>
|
<style>
|
||||||
.bg { fill: #18181b; }
|
.text { fill: #d7d7db; }
|
||||||
.chrome { fill: #27272a; }
|
.muted { fill: #9a9aa2; }
|
||||||
.dot { fill: #52525b; }
|
.dim { fill: #6b6b72; }
|
||||||
.sep { stroke: #27272a; stroke-width: 1; }
|
.ghost { fill: #6b6b72; }
|
||||||
.tab { fill: #18181b; }
|
.blue { fill: #3ba7e0; }
|
||||||
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
|
.gold { fill: #d9b54a; }
|
||||||
.t-md { font-size: 15px; }
|
.green { fill: #46d160; }
|
||||||
.t-sm { font-size: 13px; }
|
.peachfg{ fill: #1a1206; }
|
||||||
.t-xs { font-size: 11px; }
|
.greenfg{ fill: #0c130c; }
|
||||||
.muted { fill: #a1a1aa; }
|
.b { font-weight: 700; }
|
||||||
.dim { fill: #71717a; }
|
text {
|
||||||
.sel-bg { fill: #b7410e; }
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
.sel-fg { fill: #fff5f0; }
|
font-size: 13px;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<rect class="bg" width="640" height="400" rx="12"/>
|
<g xml:space="preserve">
|
||||||
<rect class="chrome" width="640" height="36" rx="12"/>
|
<!-- ===== terminal frame ===== -->
|
||||||
<rect class="chrome" y="24" width="640" height="12"/>
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#1a1b1e" stroke="#2a2b30"/>
|
||||||
<line class="sep" x1="0" y1="36" x2="640" y2="36"/>
|
|
||||||
|
|
||||||
<circle class="dot" cx="20" cy="18" r="5.5"/>
|
<!-- ===== tab bar ===== -->
|
||||||
<circle class="dot" cx="40" cy="18" r="5.5"/>
|
<text x="8" y="16" class="muted">Login</text>
|
||||||
<circle class="dot" cx="60" cy="18" r="5.5"/>
|
<rect x="56" y="2" width="80" height="20" class="green"/>
|
||||||
|
<text x="64" y="16" class="greenfg b">Register</text>
|
||||||
|
<text x="144" y="16" class="muted">Admin_Panel</text>
|
||||||
|
<text x="248" y="16" class="muted">Quit</text>
|
||||||
|
|
||||||
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/default — cargo run</text>
|
<!-- ===== Register panel (cols 21..58, rows 3..21) ===== -->
|
||||||
|
<text x="168" y="60" class="blue b" textLength="304" lengthAdjust="spacing">╭─ Register ─────────────────────────╮</text>
|
||||||
|
<text x="168" y="76" class="blue">│</text><text x="464" y="76" class="blue">│</text>
|
||||||
|
<text x="168" y="92" class="blue">│</text><text x="464" y="92" class="blue">│</text>
|
||||||
|
<text x="168" y="108" class="blue">│</text><text x="464" y="108" class="blue">│</text>
|
||||||
|
<text x="168" y="124" class="blue">│</text><text x="464" y="124" class="blue">│</text>
|
||||||
|
<text x="168" y="140" class="blue">│</text><text x="464" y="140" class="blue">│</text>
|
||||||
|
<text x="168" y="156" class="blue">│</text><text x="464" y="156" class="blue">│</text>
|
||||||
|
<text x="168" y="172" class="blue">│</text><text x="464" y="172" class="blue">│</text>
|
||||||
|
<text x="168" y="188" class="blue">│</text><text x="464" y="188" class="blue">│</text>
|
||||||
|
<text x="168" y="204" class="blue">│</text><text x="464" y="204" class="blue">│</text>
|
||||||
|
<text x="168" y="220" class="blue">│</text><text x="464" y="220" class="blue">│</text>
|
||||||
|
<text x="168" y="236" class="blue">│</text><text x="464" y="236" class="blue">│</text>
|
||||||
|
<text x="168" y="252" class="blue">│</text><text x="464" y="252" class="blue">│</text>
|
||||||
|
<text x="168" y="268" class="blue">│</text><text x="464" y="268" class="blue">│</text>
|
||||||
|
<text x="168" y="284" class="blue">│</text><text x="464" y="284" class="blue">│</text>
|
||||||
|
<text x="168" y="300" class="blue">│</text><text x="464" y="300" class="blue">│</text>
|
||||||
|
<text x="168" y="316" class="blue">│</text><text x="464" y="316" class="blue">│</text>
|
||||||
|
<text x="168" y="332" class="blue">│</text><text x="464" y="332" class="blue">│</text>
|
||||||
|
<text x="168" y="348" class="blue" textLength="304" lengthAdjust="spacing">╰────────────────────────────────────╯</text>
|
||||||
|
|
||||||
<text x="20" y="68" font-size="14" class="muted">Pages:</text>
|
<!-- subtitle -->
|
||||||
<rect class="sel-bg" x="80" y="55" width="60" height="20" rx="3"/>
|
<text x="184" y="92" class="muted">Create your account</text>
|
||||||
<text x="110" y="69" text-anchor="middle" font-size="12" class="sel-fg" font-weight="600">Home</text>
|
|
||||||
<text x="150" y="69" font-size="12" class="muted">Settings</text>
|
|
||||||
<text x="220" y="69" font-size="12" class="muted">About</text>
|
|
||||||
<text x="270" y="69" font-size="12" class="muted">Quit</text>
|
|
||||||
|
|
||||||
<line class="sep" x1="0" y1="92" x2="640" y2="92"/>
|
<!-- fields: label col 23, value col 35 -->
|
||||||
|
<text x="184" y="124" class="muted">Email</text>
|
||||||
|
<text x="280" y="124" class="text">ada@acme.io</text>
|
||||||
|
|
||||||
<text x="20" y="120" font-size="18" font-weight="700">Home</text>
|
<text x="184" y="156" class="muted">Password</text>
|
||||||
<text x="20" y="142" font-size="11" class="dim">Choose a section to open.</text>
|
<text x="280" y="156" class="muted">••••••••••</text>
|
||||||
|
|
||||||
<line class="sep" x1="0" y1="160" x2="640" y2="160"/>
|
<text x="184" y="188" class="muted">Confirm</text>
|
||||||
|
<text x="280" y="188" class="muted">••••••••••</text>
|
||||||
|
|
||||||
<g font-size="14">
|
<!-- role: typed prefix white, block cursor, ghost completion gray -->
|
||||||
<rect class="sel-bg" x="0" y="170" width="640" height="34"/>
|
<text x="184" y="220" class="muted">Role</text>
|
||||||
<text x="32" y="192" class="sel-fg" font-weight="600">▸ Dashboard</text>
|
<text x="280" y="220" class="text">a</text>
|
||||||
<text x="600" y="192" text-anchor="end" class="sel-fg" font-size="11">enter</text>
|
<rect x="288" y="208" width="2" height="15" fill="#f3c690"/>
|
||||||
|
<text x="288" y="220" class="ghost">dmin</text>
|
||||||
|
|
||||||
<text x="32" y="226" class="muted"> Accounts</text>
|
<!-- role dropdown: matches typed "a"; no border, no left pad -->
|
||||||
<text x="32" y="254" class="muted"> Transactions</text>
|
<rect x="280" y="224" width="96" height="32" fill="#111217"/>
|
||||||
<text x="32" y="282" class="muted"> Reports</text>
|
<rect x="280" y="224" width="96" height="16" fill="#f3c690"/>
|
||||||
<text x="32" y="310" class="muted"> Settings</text>
|
<text x="280" y="236" class="peachfg b">admin</text>
|
||||||
|
<text x="280" y="252" class="text">accountant</text>
|
||||||
|
|
||||||
|
<!-- primary button (bordered, green) -->
|
||||||
|
<text x="184" y="300" class="green b" textLength="144" lengthAdjust="spacing">╭────────────────╮</text>
|
||||||
|
<text x="184" y="316" class="green b" textLength="144" lengthAdjust="spacing">│ Create account │</text>
|
||||||
|
<text x="184" y="332" class="green b" textLength="144" lengthAdjust="spacing">╰────────────────╯</text>
|
||||||
|
<text x="360" y="316" class="dim">enter ↵</text>
|
||||||
|
|
||||||
|
<!-- ===== footer ===== -->
|
||||||
|
<text x="32" y="380" class="green b">INS</text>
|
||||||
|
<text x="72" y="380" class="muted">[+]</text>
|
||||||
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<line class="sep" x1="0" y1="358" x2="640" y2="358"/>
|
|
||||||
<text x="20" y="380" font-size="11" class="dim">↑↓ move · enter select · q quit</text>
|
|
||||||
<text x="620" y="380" text-anchor="end" font-size="11" class="dim">tui-pages v0.7.2</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 5.3 KiB |
73
static/img/terminal-keybindings-light.svg
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A keybindings help screen built with tui-pages">
|
||||||
|
<!-- Light-mode twin of terminal-keybindings.svg. Same 80x25 grid, light neutral palette. -->
|
||||||
|
<style>
|
||||||
|
.text { fill: #26272b; }
|
||||||
|
.muted { fill: #5a5a62; }
|
||||||
|
.dim { fill: #8a8a90; }
|
||||||
|
.blue { fill: #1f7fb8; }
|
||||||
|
.gold { fill: #a8862a; }
|
||||||
|
.green { fill: #1f9d3f; }
|
||||||
|
.greenfg{ fill: #f2fff2; }
|
||||||
|
.b { font-weight: 700; }
|
||||||
|
text {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<g xml:space="preserve">
|
||||||
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#f4f4f2" stroke="#d8d8d2"/>
|
||||||
|
|
||||||
|
<rect x="0" y="2" width="56" height="20" class="green"/>
|
||||||
|
<text x="8" y="16" class="greenfg b">Login</text>
|
||||||
|
<text x="64" y="16" class="muted">Register</text>
|
||||||
|
<text x="144" y="16" class="muted">Admin_Panel</text>
|
||||||
|
<text x="248" y="16" class="muted">Quit</text>
|
||||||
|
|
||||||
|
<text x="168" y="60" class="blue b" textLength="304" lengthAdjust="spacing">╭─ Keybindings ──────────────────────╮</text>
|
||||||
|
<text x="168" y="76" class="blue">│</text><text x="464" y="76" class="blue">│</text>
|
||||||
|
<text x="168" y="92" class="blue">│</text><text x="464" y="92" class="blue">│</text>
|
||||||
|
<text x="168" y="108" class="blue">│</text><text x="464" y="108" class="blue">│</text>
|
||||||
|
<text x="168" y="124" class="blue">│</text><text x="464" y="124" class="blue">│</text>
|
||||||
|
<text x="168" y="140" class="blue">│</text><text x="464" y="140" class="blue">│</text>
|
||||||
|
<text x="168" y="156" class="blue">│</text><text x="464" y="156" class="blue">│</text>
|
||||||
|
<text x="168" y="172" class="blue">│</text><text x="464" y="172" class="blue">│</text>
|
||||||
|
<text x="168" y="188" class="blue">│</text><text x="464" y="188" class="blue">│</text>
|
||||||
|
<text x="168" y="204" class="blue">│</text><text x="464" y="204" class="blue">│</text>
|
||||||
|
<text x="168" y="220" class="blue">│</text><text x="464" y="220" class="blue">│</text>
|
||||||
|
<text x="168" y="236" class="blue">│</text><text x="464" y="236" class="blue">│</text>
|
||||||
|
<text x="168" y="252" class="blue">│</text><text x="464" y="252" class="blue">│</text>
|
||||||
|
<text x="168" y="268" class="blue">│</text><text x="464" y="268" class="blue">│</text>
|
||||||
|
<text x="168" y="284" class="blue">│</text><text x="464" y="284" class="blue">│</text>
|
||||||
|
<text x="168" y="300" class="blue">│</text><text x="464" y="300" class="blue">│</text>
|
||||||
|
<text x="168" y="316" class="blue">│</text><text x="464" y="316" class="blue">│</text>
|
||||||
|
<text x="168" y="332" class="blue">│</text><text x="464" y="332" class="blue">│</text>
|
||||||
|
<text x="168" y="348" class="blue" textLength="304" lengthAdjust="spacing">╰────────────────────────────────────╯</text>
|
||||||
|
|
||||||
|
<text x="184" y="92" class="gold b">MOVEMENT</text>
|
||||||
|
<text x="200" y="108" class="text">j / k</text>
|
||||||
|
<text x="304" y="108" class="muted">move down / up</text>
|
||||||
|
<text x="200" y="124" class="text">gg / G</text>
|
||||||
|
<text x="304" y="124" class="muted">jump to top / bottom</text>
|
||||||
|
|
||||||
|
<text x="184" y="156" class="gold b">ACTIONS</text>
|
||||||
|
<text x="200" y="172" class="text">enter</text>
|
||||||
|
<text x="304" y="172" class="muted">select / open</text>
|
||||||
|
<text x="200" y="188" class="text">/</text>
|
||||||
|
<text x="304" y="188" class="muted">search</text>
|
||||||
|
<text x="200" y="204" class="text">space</text>
|
||||||
|
<text x="304" y="204" class="muted">toggle mark</text>
|
||||||
|
|
||||||
|
<text x="184" y="236" class="gold b">APP</text>
|
||||||
|
<text x="200" y="252" class="text">?</text>
|
||||||
|
<text x="304" y="252" class="muted">toggle this help</text>
|
||||||
|
<text x="200" y="268" class="text">:q</text>
|
||||||
|
<text x="304" y="268" class="muted">quit</text>
|
||||||
|
|
||||||
|
<text x="184" y="316" class="dim">press ? or esc to close</text>
|
||||||
|
|
||||||
|
<text x="32" y="380" class="green b">NOR</text>
|
||||||
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1,71 +1,82 @@
|
|||||||
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/keybindings terminal screenshot">
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="A keybindings help screen built with tui-pages">
|
||||||
|
<!--
|
||||||
|
tui-pages UX showcase on a strict 80x25 monospace grid (cell = 8x16 px).
|
||||||
|
Each cell carries at most two colors (fg + bg), like a real terminal buffer.
|
||||||
|
-->
|
||||||
<style>
|
<style>
|
||||||
.bg { fill: #18181b; }
|
.text { fill: #d7d7db; }
|
||||||
.chrome { fill: #27272a; }
|
.muted { fill: #9a9aa2; }
|
||||||
.dot { fill: #52525b; }
|
.dim { fill: #6b6b72; }
|
||||||
.kbd { fill: #27272a; stroke: #3f3f46; stroke-width: 1; }
|
.blue { fill: #3ba7e0; }
|
||||||
.modal { fill: #1f1f23; stroke: #b7410e; stroke-width: 2; }
|
.gold { fill: #d9b54a; }
|
||||||
.sep { stroke: #27272a; stroke-width: 1; }
|
.green { fill: #46d160; }
|
||||||
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
|
.greenfg{ fill: #0c130c; }
|
||||||
.t-sm { font-size: 12px; }
|
.b { font-weight: 700; }
|
||||||
.t-xs { font-size: 11px; }
|
text {
|
||||||
.muted { fill: #a1a1aa; }
|
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
.dim { fill: #71717a; }
|
font-size: 13px;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<rect class="bg" width="640" height="400" rx="12"/>
|
<g xml:space="preserve">
|
||||||
<rect class="chrome" width="640" height="36" rx="12"/>
|
<rect x="0.5" y="0.5" width="639" height="399" rx="10" fill="#1a1b1e" stroke="#2a2b30"/>
|
||||||
<rect class="chrome" y="24" width="640" height="12"/>
|
|
||||||
<line class="sep" x1="0" y1="36" x2="640" y2="36"/>
|
|
||||||
|
|
||||||
<circle class="dot" cx="20" cy="18" r="5.5"/>
|
<!-- tab bar (Login active) -->
|
||||||
<circle class="dot" cx="40" cy="18" r="5.5"/>
|
<rect x="0" y="2" width="56" height="20" class="green"/>
|
||||||
<circle class="dot" cx="60" cy="18" r="5.5"/>
|
<text x="8" y="16" class="greenfg b">Login</text>
|
||||||
|
<text x="64" y="16" class="muted">Register</text>
|
||||||
|
<text x="144" y="16" class="muted">Admin_Panel</text>
|
||||||
|
<text x="248" y="16" class="muted">Quit</text>
|
||||||
|
|
||||||
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/keybindings — modal open</text>
|
<!-- panel -->
|
||||||
|
<text x="168" y="60" class="blue b" textLength="304" lengthAdjust="spacing">╭─ Keybindings ──────────────────────╮</text>
|
||||||
|
<text x="168" y="76" class="blue">│</text><text x="464" y="76" class="blue">│</text>
|
||||||
|
<text x="168" y="92" class="blue">│</text><text x="464" y="92" class="blue">│</text>
|
||||||
|
<text x="168" y="108" class="blue">│</text><text x="464" y="108" class="blue">│</text>
|
||||||
|
<text x="168" y="124" class="blue">│</text><text x="464" y="124" class="blue">│</text>
|
||||||
|
<text x="168" y="140" class="blue">│</text><text x="464" y="140" class="blue">│</text>
|
||||||
|
<text x="168" y="156" class="blue">│</text><text x="464" y="156" class="blue">│</text>
|
||||||
|
<text x="168" y="172" class="blue">│</text><text x="464" y="172" class="blue">│</text>
|
||||||
|
<text x="168" y="188" class="blue">│</text><text x="464" y="188" class="blue">│</text>
|
||||||
|
<text x="168" y="204" class="blue">│</text><text x="464" y="204" class="blue">│</text>
|
||||||
|
<text x="168" y="220" class="blue">│</text><text x="464" y="220" class="blue">│</text>
|
||||||
|
<text x="168" y="236" class="blue">│</text><text x="464" y="236" class="blue">│</text>
|
||||||
|
<text x="168" y="252" class="blue">│</text><text x="464" y="252" class="blue">│</text>
|
||||||
|
<text x="168" y="268" class="blue">│</text><text x="464" y="268" class="blue">│</text>
|
||||||
|
<text x="168" y="284" class="blue">│</text><text x="464" y="284" class="blue">│</text>
|
||||||
|
<text x="168" y="300" class="blue">│</text><text x="464" y="300" class="blue">│</text>
|
||||||
|
<text x="168" y="316" class="blue">│</text><text x="464" y="316" class="blue">│</text>
|
||||||
|
<text x="168" y="332" class="blue">│</text><text x="464" y="332" class="blue">│</text>
|
||||||
|
<text x="168" y="348" class="blue" textLength="304" lengthAdjust="spacing">╰────────────────────────────────────╯</text>
|
||||||
|
|
||||||
<!-- dimmed background content -->
|
<!-- MOVEMENT -->
|
||||||
<text x="40" y="80" font-size="14" class="dim">▸ Accounts</text>
|
<text x="184" y="92" class="gold b">MOVEMENT</text>
|
||||||
<text x="40" y="108" font-size="14" class="dim"> Transactions</text>
|
<text x="200" y="108" class="text">j / k</text>
|
||||||
<text x="40" y="136" font-size="14" class="dim"> Reports</text>
|
<text x="304" y="108" class="muted">move down / up</text>
|
||||||
<text x="40" y="164" font-size="14" class="dim"> Settings</text>
|
<text x="200" y="124" class="text">gg / G</text>
|
||||||
<text x="40" y="320" font-size="11" class="dim">Press ? for help</text>
|
<text x="304" y="124" class="muted">jump to top / bottom</text>
|
||||||
|
|
||||||
<!-- modal -->
|
<!-- ACTIONS -->
|
||||||
<rect class="modal" x="120" y="60" width="400" height="280" rx="10"/>
|
<text x="184" y="156" class="gold b">ACTIONS</text>
|
||||||
<text x="140" y="94" font-size="14" font-weight="700">Keybindings</text>
|
<text x="200" y="172" class="text">enter</text>
|
||||||
<line class="sep" x1="140" y1="104" x2="500" y2="104"/>
|
<text x="304" y="172" class="muted">select / open</text>
|
||||||
|
<text x="200" y="188" class="text">/</text>
|
||||||
|
<text x="304" y="188" class="muted">search</text>
|
||||||
|
<text x="200" y="204" class="text">space</text>
|
||||||
|
<text x="304" y="204" class="muted">toggle mark</text>
|
||||||
|
|
||||||
<text x="140" y="130" font-size="11" class="muted">MOVEMENT</text>
|
<!-- APP -->
|
||||||
<rect class="kbd" x="140" y="140" width="34" height="24" rx="4"/>
|
<text x="184" y="236" class="gold b">APP</text>
|
||||||
<text x="157" y="157" text-anchor="middle" font-size="12">j</text>
|
<text x="200" y="252" class="text">?</text>
|
||||||
<text x="184" y="157" font-size="12" class="muted">move down</text>
|
<text x="304" y="252" class="muted">toggle this help</text>
|
||||||
|
<text x="200" y="268" class="text">:q</text>
|
||||||
|
<text x="304" y="268" class="muted">quit</text>
|
||||||
|
|
||||||
<rect class="kbd" x="140" y="170" width="34" height="24" rx="4"/>
|
<text x="184" y="316" class="dim">press ? or esc to close</text>
|
||||||
<text x="157" y="187" text-anchor="middle" font-size="12">k</text>
|
|
||||||
<text x="184" y="187" font-size="12" class="muted">move up</text>
|
|
||||||
|
|
||||||
<rect class="kbd" x="140" y="200" width="34" height="24" rx="4"/>
|
<!-- footer -->
|
||||||
<text x="157" y="217" text-anchor="middle" font-size="12">gg</text>
|
<text x="32" y="380" class="green b">NOR</text>
|
||||||
<text x="184" y="217" font-size="12" class="muted">top of list</text>
|
<text x="632" y="380" class="gold" text-anchor="end">tui-pages v0.7.2 │ 60 FPS</text>
|
||||||
|
</g>
|
||||||
<text x="290" y="130" font-size="11" class="muted">ACTIONS</text>
|
|
||||||
<rect class="kbd" x="290" y="140" width="60" height="24" rx="4"/>
|
|
||||||
<text x="320" y="157" text-anchor="middle" font-size="11">enter</text>
|
|
||||||
<text x="360" y="157" font-size="12" class="muted">select</text>
|
|
||||||
|
|
||||||
<rect class="kbd" x="290" y="170" width="34" height="24" rx="4"/>
|
|
||||||
<text x="307" y="187" text-anchor="middle" font-size="11">/</text>
|
|
||||||
<text x="334" y="187" font-size="12" class="muted">search</text>
|
|
||||||
|
|
||||||
<rect class="kbd" x="290" y="200" width="60" height="24" rx="4"/>
|
|
||||||
<text x="320" y="217" text-anchor="middle" font-size="11">ctrl+s</text>
|
|
||||||
<text x="360" y="217" font-size="12" class="muted">save</text>
|
|
||||||
|
|
||||||
<line class="sep" x1="140" y1="300" x2="500" y2="300"/>
|
|
||||||
<text x="320" y="322" text-anchor="middle" font-size="11" class="dim">press esc to close</text>
|
|
||||||
|
|
||||||
<line class="sep" x1="0" y1="358" x2="640" y2="358"/>
|
|
||||||
<text x="20" y="380" font-size="11" class="dim">esc close · ? toggle this dialog</text>
|
|
||||||
<text x="620" y="380" text-anchor="end" font-size="11" class="dim">dialog feature enabled</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.4 KiB |
73
static/js/mountain-bg.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
26505
static/js/mountain.js
Normal file
62
tools/build_mountain_js.py
Normal 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())
|
||||||