website created

This commit is contained in:
Priec
2026-06-01 20:01:15 +02:00
commit d8b622860c
15 changed files with 1830 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Build artifacts
bin/
node_modules/
dist/
*.min.css
*.min.js
# Editor
.vscode/
.idea/
*.swp
.DS_Store
# OS
Thumbs.db
# Logs
*.log
npm-debug.log*
# Env
.env
.env.local
# Cache
.cache/
.parcel-cache/
# Generated
static/css/site.compiled.css

77
404.html Normal file
View File

@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en" data-theme="night" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#09090b">
<title>404 — page not found · tui-pages</title>
<meta name="robots" content="noindex">
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: { extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
colors: { accent: { DEFAULT: '#b7410e', fg: '#fff5f0' } },
}},
};
</script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/static/css/site.css">
</head>
<body
x-data="{ theme: localStorage.getItem('tp-theme') || 'night' }"
x-init="
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.toggle('dark', theme === 'night');
"
class="min-h-screen bg-base-100 text-base-content antialiased grid place-items-center"
>
<main class="max-w-xl mx-auto px-6 text-center">
<pre class="font-mono text-xs sm:text-sm text-zinc-500 text-left leading-snug select-none">
┌──────────────────────────────────────────┐
│ │
│ error: route not found │
│ errno 404 │
│ │
└──────────────────────────────────────────┘
</pre>
<h1 class="mt-8 text-6xl sm:text-7xl font-bold tracking-tight text-zinc-50">
4<span class="text-accent">0</span>4<span class="tp-cursor" aria-hidden="true"></span>
</h1>
<p class="mt-4 text-zinc-400 text-lg">
That route doesn't exist. Maybe the page moved, or the link is stale.
</p>
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
<a href="/" 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">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Back home
</a>
<a href="/examples.html" 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">
See examples
</a>
</div>
<p class="mt-12 text-xs text-zinc-500 font-mono">
hint: try <code class="text-zinc-300">cargo add tui-pages</code>
</p>
</main>
</body>
</html>

32
Makefile Normal file
View File

@@ -0,0 +1,32 @@
.PHONY: help serve size validate clean
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
serve: ## Start a local static server on :8000
@command -v python3 >/dev/null 2>&1 && \
python3 -m http.server 8000 || \
(command -v npx >/dev/null 2>&1 && npx --yes http-server -p 8000) || \
echo "Install python3 or node to serve locally."
size: ## Report file sizes
@echo "HTML:"
@find . -maxdepth 2 -name "*.html" -printf " %-30p %s bytes\n"
@echo
@echo "CSS:"
@find static/css -name "*.css" -printf " %-30p %s bytes\n"
@echo
@echo "Images (svg):"
@find static/img -name "*.svg" -printf " %-30p %s bytes\n"
validate: ## Quick HTML sanity check (counts opening vs closing tags)
@for f in *.html; do \
open=$(grep -oE '<[a-zA-Z][a-zA-Z0-9-]*' "$$f" | grep -vE '^<(br|hr|img|input|meta|link|source|area|base|col|embed|param|track|wbr|!--)' | sed 's/^<//' | sort -u); \
close=$$(grep -oE '</[a-zA-Z][a-zA-Z0-9-]*' "$$f" | sed 's/^<\///' | sort -u); \
echo "$$f"; \
diff <(echo "$$open") <(echo "$$close") || true; \
done
clean: ## Remove generated artifacts
@rm -rf bin/ node_modules/ dist/

125
README.md Normal file
View File

@@ -0,0 +1,125 @@
# tui-pages website
Static marketing site for the [`tui-pages`](https://gitlab.com/filipriec/tui-pages) Rust crate.
No build step required — open `index.html` in a browser and you're done.
## Stack
| Layer | Tech | Source |
| --- | --- | --- |
| HTML | Hand-written semantic | `*.html` |
| CSS framework | Tailwind CSS v3 (Play CDN) | runtime |
| Components | DaisyUI v4 (prebuilt CSS) | runtime |
| Interactivity | HTMX 2 | runtime |
| Light JS | Alpine.js 3 | runtime |
| Code highlight | highlight.js 11 (Rust, TOML, Bash) | runtime |
| Fonts | Inter + JetBrains Mono (Google Fonts) | runtime |
| Icons | Lucide (inline SVG) | hand-written |
| Page transitions | View Transitions API (native) | n/a |
Everything is loaded from public CDNs. The only thing served from this repo is
the HTML, our small `static/css/site.css` layer, and the SVG assets in
`static/img/`.
## Local development
```bash
# Just open the file
xdg-open index.html # Linux
open index.html # macOS
# Or serve it (recommended — gives you a stable URL for htmx)
python3 -m http.server 8000
# then visit http://localhost:8000
```
The `Makefile` wraps the Python server and a few convenience commands:
```bash
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 CDN approach is fine for marketing pages. For better performance
(smaller CSS, no runtime Tailwind compile), swap the Play CDN for the
**Tailwind standalone CLI**:
```bash
# 1. Download the standalone CLI
# https://tailwindcss.com/blog/standalone-cli
# 2. Put the binary in ./bin/tailwindcss
# 3. Create src/site.css that imports Tailwind and DaisyUI:
# @import "tailwindcss";
# @plugin "daisyui";
# 4. Build
./bin/tailwindcss -i src/site.css -o static/css/site.css --minify
```
Then in `index.html` remove the Tailwind Play CDN `<script>` and the DaisyUI
`<link>`, and ensure `/static/css/site.css` is loaded last in `<head>`.
## File layout
```
tui-pages-web/
├── index.html # landing page
├── examples.html # examples gallery
├── 404.html # not found
├── robots.txt # search engine hints
├── sitemap.xml # sitemap
├── static/
│ ├── css/
│ │ └── site.css # our custom layer (small)
│ ├── img/
│ │ ├── favicon.svg
│ │ ├── logo.svg
│ │ ├── og-image.svg
│ │ ├── terminal-default.svg
│ │ ├── terminal-canvas.svg
│ │ └── terminal-keybindings.svg
│ └── demos/ # (empty — drop asciinema .cast files here)
├── content/ # (empty — markdown for blog/changelog later)
├── Makefile
├── .gitignore
└── README.md
```
## Adding an asciinema demo to the hero
The hero currently shows a static SVG terminal mockup. To replace it with a
real asciinema recording:
1. Record one of the examples:
```bash
cd ../komp_ac/tui-pages/examples/default
asciinema rec ../../tui-pages-web/static/demos/intro.cast
# ... run the app for a few seconds, hit ctrl-d to stop
```
2. In `index.html`, replace the hero terminal block with:
```html
<div class="tp-terminal">
<asciinema-player src="/static/demos/intro.cast" autoplay loop></asciinema-player>
</div>
```
3. Add the asciinema player to the `<head>`:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.css">
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
```
## License
The website source is MIT-licensed. The terminal mockup SVGs in `static/img/`
are hand-drawn and original. The crate itself is at
<https://gitlab.com/filipriec/tui-pages>.

330
examples.html Normal file
View File

@@ -0,0 +1,330 @@
<!doctype html>
<html lang="en" data-theme="night" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)">
<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.">
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link rel="canonical" href="https://tui-pages.dev/examples.html">
<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:image" content="/static/img/og-image.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://unpkg.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: { extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', 'sans-serif'],
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
colors: { accent: { DEFAULT: '#b7410e', fg: '#fff5f0' } },
}},
};
</script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark-dimmed.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/rust.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/toml.min.js" defer></script>
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/static/css/site.css">
</head>
<body
x-data="{ theme: localStorage.getItem('tp-theme') || 'night' }"
x-init="
$watch('theme', t => {
localStorage.setItem('tp-theme', t);
document.documentElement.setAttribute('data-theme', t);
document.documentElement.classList.toggle('dark', t === 'night');
});
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.toggle('dark', theme === 'night');
"
class="min-h-screen bg-base-100 text-base-content antialiased"
>
<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 (mirrors index.html, kept in sync manually for static) -->
<header class="tp-nav sticky top-0 z-40">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-accent/10 border border-accent/30 text-accent">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="7 7 13 12 7 17"/>
<rect x="15" y="7" width="2.5" height="10" fill="currentColor" stroke="none"/>
</svg>
</span>
<span class="font-mono font-bold text-base tracking-tight text-zinc-100 group-hover:text-white transition-colors">tui-pages</span>
</a>
<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="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://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>
</ul>
<div class="ml-auto flex items-center gap-2">
<button type="button" @click="theme = (theme === 'night' ? 'winter' : 'night')"
:aria-label="theme === 'night' ? 'Switch to light theme' : 'Switch to dark theme'"
class="w-9 h-9 inline-flex items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/50 text-zinc-300 hover:text-white hover:border-zinc-700 transition-colors">
<svg x-show="theme === 'night'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg x-show="theme !== 'night'" x-cloak width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<a href="https://gitlab.com/filipriec/tui-pages" class="hidden sm:inline-flex items-center gap-1.5 h-9 px-3.5 rounded-md bg-accent text-accent-fg text-sm font-medium hover:bg-accent/90 transition-colors">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21.5l3.7-11.4H8.3L12 21.5zM2.5 10.1L1 14.4c-.2.6 0 1.3.5 1.6L12 21.5 2.5 10.1zm19 0L21.5 14.4c.2.6 0 1.3-.5 1.6L12 21.5l10-11.4zM15.7 8.6L17.4 2.3c.1-.4-.4-.7-.7-.4L13 4.6c-.4.3-.9.3-1.3.3s-.9 0-1.3-.3L7 1.9c-.3-.3-.8 0-.7.4l1.7 6.3h7.7z"/></svg>
View source
</a>
</div>
</nav>
</header>
<main id="main">
<!-- Page header -->
<section class="tp-hero">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-16 pb-12">
<nav class="text-sm text-zinc-500 mb-6">
<a href="/" class="hover:text-zinc-300">Home</a>
<span class="mx-2 text-zinc-700">/</span>
<span class="text-zinc-300">Examples</span>
</nav>
<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">
Three apps in the box.
</h1>
<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.
</p>
</div>
</section>
<!-- ============ Example 1: default ============ -->
<article id="default" class="py-20 scroll-mt-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-5 gap-10 items-start">
<div class="lg:col-span-3">
<div class="tp-card !p-2">
<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">
</div>
</div>
</div>
<div class="lg:col-span-2">
<div class="flex items-center gap-2">
<span class="tp-kbd">basic</span>
<span class="text-xs text-zinc-500 font-mono">no features</span>
</div>
<h2 class="mt-3 text-2xl sm:text-3xl font-bold tracking-tight text-zinc-50">examples/default</h2>
<p class="mt-3 text-zinc-300 leading-relaxed">
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>
<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>
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
<li class="flex gap-2"><span class="text-accent"></span> Multi-page routing with <code class="font-mono text-zinc-200">register_page</code></li>
<li class="flex gap-2"><span class="text-accent"></span> Focus list with movement (<span class="tp-kbd">j</span> <span class="tp-kbd">k</span>)</li>
<li class="flex gap-2"><span class="text-accent"></span> Default keymaps in action</li>
<li class="flex gap-2"><span class="text-accent"></span> Clean <code class="font-mono text-zinc-200">Runtime::run()</code> loop</li>
</ul>
<a href="https://gitlab.com/filipriec/tui-pages/-/tree/main/examples/default" class="mt-6 inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
Read source
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</div>
</article>
<!-- ============ Example 2: canvas ============ -->
<article id="canvas" class="py-20 border-t border-zinc-800/40 bg-zinc-950/30 scroll-mt-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-5 gap-10 items-start">
<div class="lg:col-span-3 lg:order-2">
<div class="tp-card !p-2">
<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">
</div>
</div>
</div>
<div class="lg:col-span-2 lg:order-1">
<div class="flex items-center gap-2">
<span class="tp-kbd">canvas</span>
<span class="text-xs text-zinc-500 font-mono">+ratatui</span>
</div>
<h2 class="mt-3 text-2xl sm:text-3xl font-bold tracking-tight text-zinc-50">examples/canvas</h2>
<p class="mt-3 text-zinc-300 leading-relaxed">
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>
<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>
<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> Cursor style + blinking caret from canvas</li>
<li class="flex gap-2"><span class="text-accent"></span> Submit button as a focusable element</li>
<li class="flex gap-2"><span class="text-accent"></span> Validation wiring (sketch in <code class="font-mono text-zinc-200">examples/canvas</code>)</li>
</ul>
<a href="https://gitlab.com/filipriec/tui-pages/-/tree/main/examples/canvas" class="mt-6 inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
Read source
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</div>
</article>
<!-- ============ Example 3: keybindings ============ -->
<article id="keybindings" class="py-20 scroll-mt-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-5 gap-10 items-start">
<div class="lg:col-span-3">
<div class="tp-card !p-2">
<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">
</div>
</div>
</div>
<div class="lg:col-span-2">
<div class="flex items-center gap-2">
<span class="tp-kbd">dialog</span>
<span class="text-xs text-zinc-500 font-mono">+ratatui</span>
</div>
<h2 class="mt-3 text-2xl sm:text-3xl font-bold tracking-tight text-zinc-50">examples/keybindings</h2>
<p class="mt-3 text-zinc-300 leading-relaxed">
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>
<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>
<ul class="mt-2 space-y-1.5 text-sm text-zinc-300">
<li class="flex gap-2"><span class="text-accent"></span> Modal lifecycle: open, focus, close</li>
<li class="flex gap-2"><span class="text-accent"></span> Background app dimmed behind the modal</li>
<li class="flex gap-2"><span class="text-accent"></span> Custom keymaps rendered with the <code class="font-mono text-zinc-200">kbd</code> widget</li>
<li class="flex gap-2"><span class="text-accent"></span> <code class="font-mono text-zinc-200">Dialog::Keybindings</code> content type</li>
</ul>
<a href="https://gitlab.com/filipriec/tui-pages/-/tree/main/examples/keybindings" class="mt-6 inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
Read source
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</div>
</article>
<!-- CTA -->
<section class="py-24 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl font-bold tracking-tight text-zinc-50">Build your own.</h2>
<p class="mt-4 text-zinc-400">
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>
<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">
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>
</a>
<a href="https://gitlab.com/filipriec/tui-pages/-/tree/main/examples" 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">
All examples on GitLab
</a>
</div>
</div>
</section>
</main>
<!-- FOOTER -->
<footer class="border-t border-zinc-800/60 py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs text-zinc-500">
<p>© 2026 Filip Riečický. Released under the MIT License.</p>
<p class="font-mono">crafted with <span class="text-accent">htmx</span> + <span class="text-accent">alpine</span> + <span class="text-accent">tailwind</span></p>
</div>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.hljs) {
document.querySelectorAll('pre code').forEach(el => {
if (!el.classList.contains('hljs')) hljs.highlightElement(el);
});
}
});
</script>
</body>
</html>

712
index.html Normal file
View File

@@ -0,0 +1,712 @@
<!doctype html>
<html lang="en" data-theme="night" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)">
<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="keywords" content="rust, tui, ratatui, terminal, framework, ui, cargo, crate">
<meta name="author" content="Filip Riečický">
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link rel="canonical" href="https://tui-pages.dev/">
<!-- Open Graph -->
<meta property="og:type" content="website">
<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:url" content="https://tui-pages.dev/">
<meta property="og:image" content="/static/img/og-image.svg">
<meta property="og:site_name" content="tui-pages">
<!-- Twitter -->
<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:description" content="Pre-programmed focus, keymaps, and page navigation. Stop rewriting the same architecture for every TUI project.">
<meta name="twitter:image" content="/static/img/og-image.svg">
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://unpkg.com" crossorigin>
<!-- Fonts: Inter (UI) + JetBrains Mono (code) -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<!-- Tailwind v3 Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', 'sans-serif'],
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
colors: {
accent: { DEFAULT: '#b7410e', fg: '#fff5f0' },
},
maxWidth: { '8xl': '88rem' },
},
},
};
</script>
<!-- DaisyUI 4 (prebuilt CSS, loaded before custom CSS) -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
<!-- highlight.js for code blocks -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark-dimmed.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/rust.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/toml.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js" defer></script>
<!-- HTMX 2 -->
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
<!-- Alpine.js 3 (theme toggle, mobile menu, tabs, copy buttons) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
<link rel="stylesheet" href="/static/css/site.css">
</head>
<body
x-data="{ theme: localStorage.getItem('tp-theme') || 'night' }"
x-init="
$watch('theme', t => {
localStorage.setItem('tp-theme', t);
document.documentElement.setAttribute('data-theme', t);
document.documentElement.classList.toggle('dark', t === 'night');
});
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.toggle('dark', theme === 'night');
"
class="min-h-screen bg-base-100 text-base-content antialiased"
>
<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 ============================ -->
<header class="tp-nav sticky top-0 z-40">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-accent/10 border border-accent/30 text-accent">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="7 7 13 12 7 17"/>
<rect x="15" y="7" width="2.5" height="10" fill="currentColor" stroke="none"/>
</svg>
</span>
<span class="font-mono font-bold text-base tracking-tight text-zinc-100 group-hover:text-white transition-colors">
tui-pages
</span>
</a>
<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="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://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>
</ul>
<div class="ml-auto flex items-center gap-2">
<button
type="button"
@click="theme = (theme === 'night' ? 'winter' : 'night')"
:aria-label="theme === 'night' ? 'Switch to light theme' : 'Switch to dark theme'"
class="w-9 h-9 inline-flex items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/50 text-zinc-300 hover:text-white hover:border-zinc-700 transition-colors"
>
<svg x-show="theme === 'night'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
<svg x-show="theme !== 'night'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-cloak>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
<a href="https://gitlab.com/filipriec/tui-pages" class="hidden sm:inline-flex items-center gap-1.5 h-9 px-3.5 rounded-md bg-accent text-accent-fg text-sm font-medium hover:bg-accent/90 transition-colors">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21.5l3.7-11.4H8.3L12 21.5zM2.5 10.1L1 14.4c-.2.6 0 1.3.5 1.6L12 21.5 2.5 10.1zm19 0L21.5 14.4c.2.6 0 1.3-.5 1.6L12 21.5l10-11.4zM15.7 8.6L17.4 2.3c.1-.4-.4-.7-.7-.4L13 4.6c-.4.3-.9.3-1.3.3s-.9 0-1.3-.3L7 1.9c-.3-.3-.8 0-.7.4l1.7 6.3h7.7z"/></svg>
View source
</a>
<button
type="button"
x-data
@click="$dispatch('open-mobile')"
class="md:hidden w-9 h-9 inline-flex items-center justify-center rounded-md border border-zinc-800 text-zinc-300 hover:text-white"
aria-label="Open menu"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</div>
</nav>
<!-- Mobile menu -->
<div
x-data="{ open: false }"
@open-mobile.window="open = !open"
x-show="open"
x-transition.opacity
x-cloak
class="md:hidden border-t border-zinc-800 bg-zinc-950/95 backdrop-blur"
>
<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="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://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>
</ul>
</div>
</header>
<main id="main">
<!-- ============================ HERO ============================ -->
<section class="tp-hero relative overflow-hidden">
<div class="tp-hero-glow"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24 lg:pt-28 lg:pb-32">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<div>
<a href="https://crates.io/crates/tui-pages" class="inline-flex items-center gap-2 rounded-full border border-zinc-800 bg-zinc-900/60 px-3 py-1 text-xs text-zinc-300 hover:border-accent/60 hover:text-white transition-colors">
<span class="inline-block w-1.5 h-1.5 rounded-full bg-accent"></span>
<span class="font-mono">v0.7.2</span>
<span class="text-zinc-500">·</span>
<span>MIT</span>
<span class="text-zinc-500">·</span>
<span class="text-zinc-400">on crates.io</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
A framework for<br>
building <span class="text-accent">TUIs</span> in Rust<span class="tp-cursor" aria-hidden="true"></span>
</h1>
<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.
</p>
<div class="mt-8 flex flex-wrap items-center gap-3">
<a href="https://docs.rs/tui-pages" 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">
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>
</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">
<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
</a>
</div>
<!-- Install command with copy button -->
<div
x-data="{ copied: false }"
class="mt-8 inline-flex items-stretch max-w-full rounded-lg border border-zinc-800 bg-zinc-950/80 overflow-hidden font-mono text-sm"
>
<span class="flex items-center gap-2 pl-3 pr-2 text-zinc-500 select-none border-r border-zinc-800">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
</span>
<code class="px-3 py-2.5 text-zinc-100 whitespace-nowrap">cargo add tui-pages</code>
<button
type="button"
@click="navigator.clipboard.writeText('cargo add tui-pages').then(() => { copied = true; setTimeout(() => copied = false, 1400) })"
class="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"
:aria-label="copied ? 'Copied' : 'Copy command'"
>
<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>
<p class="mt-3 text-xs text-zinc-500 font-mono">or: <code class="text-zinc-400">cargo install tui-pages-cli</code></p>
</div>
<!-- Hero terminal mockup -->
<div class="relative">
<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-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">
</div>
<p class="mt-3 text-center text-xs text-zinc-500 font-mono">examples/default · cargo run</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============================ TRUST STRIP ============================ -->
<section class="border-y border-zinc-800/60 bg-zinc-950/40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<dl class="grid grid-cols-2 sm:grid-cols-4 gap-6 sm:gap-8 text-center">
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Version</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">0.7.2</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">License</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">MIT</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Features</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">3 opt-in</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Renderers</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">agnostic</dd>
</div>
</dl>
</div>
</section>
<!-- ============================ FEATURES ============================ -->
<section id="features" class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">What's inside</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
Everything a multi-page TUI needs,<br class="hidden sm:block"> already wired up.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
The crate separates the framework from your pages. You assign element IDs to a list, the framework handles the rest: focus, movement, keymaps, modal dialogs, and page navigation.
</p>
</div>
<div class="mt-14 grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Card: Focus Manager -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M3 7V5a2 2 0 0 1 2-2h2M21 7V5a2 2 0 0 0-2-2h-2M3 17v2a2 2 0 0 0 2 2h2M21 17v2a2 2 0 0 1-2 2h-2"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Focus Manager</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
One global focus model. Be dumb — tell the manager what to focus, don't do it yourself. The system inside it handles library-vs-page focus conflicts.
</p>
</div>
<!-- Card: Input Pipeline -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Input Pipeline</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Map type-safe <code class="font-mono text-zinc-300">Command::Save</code> to a key chord. KeyEvents are flushed into the pipeline; you never write key matching by hand.
</p>
</div>
<!-- Card: Page Navigation -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Page Navigation</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Pages are first-class. Move between them, swap state, render any way you want. The runtime stays out of your way.
</p>
</div>
<!-- Card: Keymaps -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Default Keymaps</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Vim-style <span class="tp-kbd">j</span> <span class="tp-kbd">k</span> <span class="tp-kbd">gg</span> movement and VS Code-style navigation come pre-wired. Override or extend per page.
</p>
</div>
<!-- Card: Canvas -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Canvas integration</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
One feature flag. Enables GUI renderers, suggestions, cursor style, validation, computed fields, textareas, text inputs, and a canvas-owned keymap.
</p>
</div>
<!-- Card: Modal dialog -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Modal Dialog</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Built-in dialog with content and result types plus a ratatui renderer. Enable it, write the content, you're done.
</p>
</div>
</div>
</div>
</section>
<!-- ============================ HOW IT WORKS ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">How it works</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
A primitive list, a pipeline,<br class="hidden sm:block"> and an executor.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
Imagine <code class="font-mono text-zinc-300">0..n</code> element IDs. You're at ID 1 and want to move next, so you go to 2. That's the whole abstraction. Everything else is built on top of it.
</p>
</div>
<ol class="mt-14 grid md:grid-cols-2 lg:grid-cols-4 gap-4">
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">01</span>
<h3 class="text-base font-semibold text-zinc-100">KeyEvent</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">A key binding like <code class="font-mono text-zinc-300">"ctrl+s"</code> or a plain letter is flushed into the input pipeline.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">02</span>
<h3 class="text-base font-semibold text-zinc-100">InputPipeline</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Maps a key chord to a type-safe <code class="font-mono text-zinc-300">Command::Save</code>.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">03</span>
<h3 class="text-base font-semibold text-zinc-100">Orchestrator</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Decides where the request goes. <code class="font-mono text-zinc-300">j</code><code class="font-mono text-zinc-300">Movement::Down</code> → FocusManager.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">04</span>
<h3 class="text-base font-semibold text-zinc-100">Executor</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Calls the page's function. Login page logs you in. The page owns its own logic.</p>
</li>
</ol>
</div>
</section>
<!-- ============================ CODE EXAMPLE ============================ -->
<section class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">Show me the code</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
Three lines of focus, three lines of pages.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
Define a page with an element list, register keymaps, run the loop. The framework handles focus, movement, and routing.
</p>
</div>
<div
x-data="{ tab: 'main' }"
class="mt-10 rounded-2xl border border-zinc-800 bg-zinc-950/60 overflow-hidden"
>
<!-- Tab bar -->
<div class="flex items-center gap-1 border-b border-zinc-800 bg-zinc-900/40 px-2 py-1.5 overflow-x-auto">
<button
type="button"
@click="tab = 'main'"
:class="tab === 'main' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>src/main.rs</button>
<button
type="button"
@click="tab = 'cargo'"
:class="tab === 'cargo' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>Cargo.toml</button>
<button
type="button"
@click="tab = 'page'"
:class="tab === 'page' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>pages/login.rs</button>
<button
type="button"
@click="tab = 'keys'"
:class="tab === 'keys' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>keymaps.rs</button>
</div>
<!-- Tab panels (use htmx to lazy-load from static partials) -->
<div class="tp-code relative">
<button
type="button"
x-data
@click="navigator.clipboard.writeText($refs.codeblock.textContent).then(() => { $el.querySelector('[data-label]').textContent = 'Copied'; setTimeout(() => $el.querySelector('[data-label]').textContent = 'Copy', 1200) })"
class="absolute top-3 right-3 z-10 inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-zinc-800 bg-zinc-900/80 text-zinc-400 hover:text-white text-xs transition-colors"
aria-label="Copy code"
>
<svg width="12" height="12" 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>
<span data-label>Copy</span>
</button>
<div x-show="tab === 'main'" x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
fn main() -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {
// 1. Build the runtime with default keymaps and a terminal.
let mut rt = Runtime::new(Terminal::default())?;
// 2. Register pages. Each page owns its own focus list and logic.
rt.register_page("login", pages::login::build());
rt.register_page("home", pages::home::build());
rt.register_page("settings",pages::settings::build());
// 3. Run the main loop. Framework handles focus, movement, and routing.
rt.run()
}
</code></pre>
</div>
<div x-show="tab === 'cargo'" x-cloak x-ref="codeblock">
<pre><code class="language-toml">[package]
name = "my-tui"
version = "0.1.0"
edition = "2021"
[dependencies]
tui-pages = "0.7"
ratatui = "0.29" # only if you enable the `canvas` or `dialog` feature
crossterm = "0.28"
# Pick what you need:
# tui-pages = { version = "0.7", features = ["canvas"] } # +ratatui, GUI surfaces
# tui-pages = { version = "0.7", features = ["dialog"] } # +ratatui, built-in dialog
# tui-pages = { version = "0.7", features = ["serde"] } # +serde on input/mode types
</code></pre>
</div>
<div x-show="tab === 'page'" x-cloak x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
/// A page is just a focus list + a render fn + an executor.
pub fn build() -&gt; Page {
Page::new("login", render, executor)
// Elements are 0..n. The framework moves through them for you.
.with_elements(|s| s
.input("username")
.input("password")
.button("submit"))
.with_focus(0)
}
fn render(f: &amp;mut Frame, area: Rect, focus: &amp;Focus) {
// Your ratatui render. The focus index is handed to you.
let focused = focus.current();
// ... draw inputs and the button, highlight `focused`
}
fn executor(cmd: Command, focus: &amp;mut Focus, state: &amp;mut State) -&gt; Outcome {
match cmd {
Command::Submit =&gt; state.navigate("home"),
_ =&gt; Outcome::Next,
}
}
</code></pre>
</div>
<div x-show="tab === 'keys'" x-cloak x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
/// Map key chords to type-safe commands. Override defaults or add your own.
pub fn keymaps() -&gt; KeyMap {
KeyMap::new()
.bind("j", Command::Move(Movement::Down))
.bind("k", Command::Move(Movement::Up))
.bind("gg", Command::Move(Movement::Top))
.bind("G", Command::Move(Movement::Bottom))
.bind("enter", Command::Submit)
.bind("ctrl+s", Command::Save)
.bind("?", Command::OpenDialog(Dialog::Keybindings))
.bind("q", Command::Quit)
}
</code></pre>
</div>
</div>
</div>
</div>
</section>
<!-- ============================ EXAMPLES GALLERY ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-wrap items-end justify-between gap-4">
<div class="max-w-2xl">
<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>
<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>
</div>
<a href="/examples.html" class="inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
All examples
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
</div>
<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">
<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">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/default</h3>
<span class="tp-kbd">basic</span>
</div>
<p class="mt-2 text-sm text-zinc-400">A multi-page app with a focusable list, page navigation, and quit. The minimal end-to-end example.</p>
</div>
</a>
<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">
<img src="/static/img/terminal-canvas.svg" alt="canvas example screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/canvas</h3>
<span class="tp-kbd">canvas</span>
</div>
<p class="mt-2 text-sm text-zinc-400">Login form with validated inputs, a submit button, and a canvas-owned keymap. Enable the <code class="font-mono text-zinc-300">canvas</code> feature.</p>
</div>
</a>
<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">
<img src="/static/img/terminal-keybindings.svg" alt="keybindings example screenshot" class="block w-full h-auto" loading="lazy" width="640" height="400">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/keybindings</h3>
<span class="tp-kbd">dialog</span>
</div>
<p class="mt-2 text-sm text-zinc-400">A modal showing all keybindings, opened with <span class="tp-kbd">?</span>. Demonstrates the built-in dialog feature.</p>
</div>
</a>
</div>
</div>
</section>
<!-- ============================ WHY ============================ -->
<section class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">Why</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
The architecture you've already written, generalized.
</h2>
</div>
<div class="mt-10 grid md:grid-cols-2 gap-6">
<div class="tp-card">
<p class="text-sm font-mono text-zinc-500 mb-3">// what most TUI code looks like</p>
<pre class="tp-code"><code class="language-rust text-zinc-300">let mut app = App::new();
loop {
app.handle_event(event);
app.focus.update();
app.ui.draw(&amp;mut app);
// shared &amp;mut references
// everywhere
// god object strikes again
}</code></pre>
</div>
<div class="tp-card !border-accent/40">
<p class="text-sm font-mono text-accent mb-3">// what tui-pages looks like</p>
<pre class="tp-code"><code class="language-rust text-zinc-300">let mut rt = Runtime::new(terminal)?;
rt.register_page("home", pages::home::build());
rt.register_page("login", pages::login::build());
rt.register_page("settings",pages::settings::build());
rt.run() // framework handles the rest</code></pre>
</div>
</div>
</div>
</section>
<!-- ============================ CTA ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl sm:text-5xl font-bold tracking-tight text-zinc-50">
Build your first TUI page<br class="hidden sm:block"> in the next five minutes.
</h2>
<p class="mt-5 text-lg text-zinc-400 max-w-2xl mx-auto">
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>
<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">
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>
</a>
<a href="https://docs.rs/tui-pages" class="inline-flex items-center gap-2 h-12 px-6 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">
Read the API
</a>
</div>
</div>
</section>
</main>
<!-- ============================ FOOTER ============================ -->
<footer class="border-t border-zinc-800/60 py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid sm:grid-cols-2 md:grid-cols-4 gap-8">
<div class="sm:col-span-2">
<a href="/" class="inline-flex items-center gap-2.5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent/10 border border-accent/30 text-accent">
<svg 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="7 7 13 12 7 17"/><rect x="15" y="7" width="2.5" height="10" fill="currentColor" stroke="none"/></svg>
</span>
<span class="font-mono font-bold text-zinc-100">tui-pages</span>
</a>
<p class="mt-3 text-sm text-zinc-500 max-w-md">
A framework for building TUIs in Rust on top of ratatui. MIT licensed. Originally extracted from a production TUI accounting system.
</p>
</div>
<div>
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Resources</h4>
<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://tui-pages.farmeris.sk" 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="https://crates.io/crates/tui-pages" class="text-zinc-300 hover:text-white">crates.io</a></li>
</ul>
</div>
<div>
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Project</h4>
<ul class="mt-3 space-y-2 text-sm">
<li><a href="https://gitlab.com/filipriec/tui-pages" class="text-zinc-300 hover:text-white">GitLab repo</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages/-/issues" class="text-zinc-300 hover:text-white">Issues</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages/-/blob/main/LICENSE" class="text-zinc-300 hover:text-white">MIT License</a></li>
</ul>
</div>
</div>
<div class="mt-10 pt-6 border-t border-zinc-800/60 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs text-zinc-500">
<p>© 2026 Filip Riečický. Released under the MIT License.</p>
<p class="font-mono">crafted with <span class="text-accent">htmx</span> + <span class="text-accent">alpine</span> + <span class="text-accent">tailwind</span></p>
</div>
</div>
</footer>
<!-- Initialise highlight.js after the deferred scripts load -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.hljs) {
document.querySelectorAll('pre code').forEach(el => {
if (!el.classList.contains('hljs')) hljs.highlightElement(el);
});
}
});
</script>
</body>
</html>

4
robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://tui-pages.dev/sitemap.xml

13
sitemap.xml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://tui-pages.dev/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://tui-pages.dev/examples.html</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>

266
static/css/site.css Normal file
View File

@@ -0,0 +1,266 @@
/* ==========================================================================
tui-pages website — custom layer on top of Tailwind + DaisyUI
Loaded AFTER Tailwind Play CDN compiles and DaisyUI prebuilt.
Keep this small. Tailwind utility classes do the heavy lifting.
========================================================================== */
/* ---------- Smooth scroll, native feel ----------------------------------- */
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
/* ---------- Theme (default to dark) -------------------------------------- */
:root {
color-scheme: dark light;
--tp-accent: #b7410e;
--tp-accent-fg: #fff5f0;
--tp-grid-line: rgba(63, 63, 70, 0.35);
--tp-hero-from: #0b0b0f;
--tp-hero-to: #18181b;
}
:root[data-theme="light"] {
--tp-accent: #93330c;
--tp-accent-fg: #fff5f0;
--tp-grid-line: rgba(228, 228, 231, 0.7);
--tp-hero-from: #fafafa;
--tp-hero-to: #f4f4f5;
}
/* ---------- Body --------------------------------------------------------- */
body {
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
font-feature-settings: 'cv11', 'ss01', 'ss03';
}
/* ---------- View Transitions (progressive enhancement) ------------------ */
@supports (view-transition-name: foo) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 220ms;
}
}
/* ---------- Hero --------------------------------------------------------- */
.tp-hero {
background:
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(183, 65, 14, 0.15), transparent 70%),
linear-gradient(180deg, var(--tp-hero-from) 0%, var(--tp-hero-to) 100%);
position: relative;
isolation: isolate;
}
.tp-hero::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(var(--tp-grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--tp-grid-line) 1px, transparent 1px);
background-size: 56px 56px;
background-position: -1px -1px;
-webkit-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;
z-index: -1;
}
/* Subtle floating code lines in the hero, behind the content */
.tp-hero-glow {
position: absolute;
inset: 0;
pointer-events: none;
z-index: -1;
opacity: 0.6;
background:
radial-gradient(circle at 15% 30%, rgba(183, 65, 14, 0.08), transparent 35%),
radial-gradient(circle at 85% 70%, rgba(126, 231, 135, 0.05), transparent 35%);
}
/* ---------- Cursor caret animation --------------------------------------- */
@keyframes tp-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.tp-cursor {
display: inline-block;
width: 0.55em;
height: 1em;
background: var(--tp-accent);
margin-left: 0.15em;
vertical-align: -0.12em;
animation: tp-blink 1.1s steps(1) infinite;
}
/* ---------- Soft float for the hero terminal ----------------------------- */
@keyframes tp-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.tp-float {
animation: tp-float 6s ease-in-out infinite;
}
/* ---------- Glass nav ---------------------------------------------------- */
.tp-nav {
background: rgba(9, 9, 11, 0.65);
backdrop-filter: saturate(160%) blur(12px);
-webkit-backdrop-filter: saturate(160%) blur(12px);
border-bottom: 1px solid rgba(63, 63, 70, 0.4);
}
:root[data-theme="light"] .tp-nav {
background: rgba(255, 255, 255, 0.7);
border-bottom-color: rgba(228, 228, 231, 0.7);
}
/* ---------- Feature cards ------------------------------------------------ */
.tp-card {
position: relative;
background: rgba(24, 24, 27, 0.5);
border: 1px solid rgba(63, 63, 70, 0.5);
border-radius: 1rem;
padding: 1.5rem;
transition: border-color 200ms ease, transform 200ms ease, background 200ms ease;
}
.tp-card:hover {
border-color: rgba(183, 65, 14, 0.6);
background: rgba(24, 24, 27, 0.75);
transform: translateY(-2px);
}
:root[data-theme="light"] .tp-card {
background: rgba(255, 255, 255, 0.6);
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 {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.75rem;
background: rgba(183, 65, 14, 0.12);
color: var(--tp-accent);
border: 1px solid rgba(183, 65, 14, 0.25);
margin-bottom: 1rem;
}
/* ---------- Code blocks (kitchen sink — hljs + custom) ------------------ */
.tp-code {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.85rem;
line-height: 1.6;
tab-size: 4;
}
.tp-code pre {
background: #0b0b0f;
border: 1px solid rgba(63, 63, 70, 0.5);
border-radius: 0.75rem;
padding: 1.25rem 1.5rem;
overflow-x: auto;
margin: 0;
}
.tp-code pre code.hljs {
background: transparent;
padding: 0;
}
/* ---------- Inline kbd --------------------------------------------------- */
.tp-kbd {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.5rem;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid rgba(63, 63, 70, 0.7);
border-bottom-width: 2px;
border-radius: 0.375rem;
background: rgba(39, 39, 42, 0.6);
color: #f4f4f5;
line-height: 1;
}
:root[data-theme="light"] .tp-kbd {
background: #fff;
color: #18181b;
border-color: #d4d4d8;
}
/* ---------- Terminal window wrapper -------------------------------------- */
.tp-terminal {
position: relative;
border-radius: 0.875rem;
background: #0b0b0f;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 30px 60px -20px rgba(0, 0, 0, 0.6),
0 18px 36px -18px rgba(0, 0, 0, 0.5);
overflow: hidden;
border: 1px solid rgba(63, 63, 70, 0.4);
}
.tp-terminal::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
/* ---------- Selection --------------------------------------------------- */
::selection {
background: var(--tp-accent);
color: var(--tp-accent-fg);
}
/* ---------- Focus ring -------------------------------------------------- */
:focus-visible {
outline: 2px solid var(--tp-accent);
outline-offset: 2px;
border-radius: 0.25rem;
}
/* ---------- Scrollbar (subtle) ----------------------------------------- */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(63, 63, 70, 0.6);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background-color: rgba(82, 82, 91, 0.8); background-clip: padding-box; }
/* ---------- No-FOUC theme flash ----------------------------------------- */
html:not([data-theme]) body { background: #09090b; }
:root[data-theme="light"] body { background: #fafafa; }
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
[x-cloak] { display: none !important; }
/* ---------- DaisyUI overrides for our theme ----------------------------- */
/* Bring DaisyUI's primary to our accent so .btn-primary etc. look on-brand. */
[data-theme="night"] {
--p: 28 70% 36%;
--pc: 0 0% 100%;
--b1: 240 5% 8%;
--b2: 240 4% 12%;
--b3: 240 4% 16%;
--bc: 0 0% 96%;
}

7
static/img/favicon.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tui-pages">
<rect width="32" height="32" rx="7" fill="#09090b"/>
<path d="M9.5 11.5 L14.5 16 L9.5 20.5" stroke="#f4f4f5" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<rect x="17.5" y="11.5" width="3" height="9" fill="#b7410e">
<animate attributeName="opacity" values="1;0;1" dur="1.1s" repeatCount="indefinite"/>
</rect>
</svg>

After

Width:  |  Height:  |  Size: 481 B

11
static/img/logo.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="172" height="36" viewBox="0 0 172 36" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tui-pages">
<style>
.wm { font: 700 22px/1 ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; letter-spacing: -0.5px; }
.cur { fill: #b7410e; }
</style>
<text x="0" y="25" class="wm">tui-pages</text>
<rect class="cur" x="132" y="8" width="3" height="22">
<animate attributeName="opacity" values="1;0;1" dur="1.1s" repeatCount="indefinite"/>
</rect>
<rect class="cur" x="139" y="8" width="3" height="22"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

43
static/img/og-image.svg Normal file
View File

@@ -0,0 +1,43 @@
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tui-pages — a complete framework for building TUIs in Rust">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0b0b0f"/>
<stop offset="100%" stop-color="#18181b"/>
</linearGradient>
<radialGradient id="glow" cx="50%" cy="0%" r="60%">
<stop offset="0%" stop-color="#b7410e" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#b7410e" stop-opacity="0"/>
</radialGradient>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#27272a" stroke-width="1" stroke-opacity="0.3"/>
</pattern>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect width="1200" height="630" fill="url(#grid)"/>
<rect width="1200" height="630" fill="url(#glow)"/>
<g transform="translate(80, 90)">
<text font-family="ui-monospace, 'JetBrains Mono', monospace" font-weight="700" font-size="22" fill="#f4f4f5" x="0" y="0">tui-pages</text>
<rect x="140" y="-15" width="4" height="24" fill="#b7410e"/>
</g>
<text font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="84" fill="#f4f4f5" x="80" y="280" letter-spacing="-2">
A framework for
</text>
<text font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="84" fill="#b7410e" x="80" y="370" letter-spacing="-2">
building TUIs in Rust.
</text>
<text font-family="Inter, system-ui, sans-serif" font-weight="400" font-size="28" fill="#a1a1aa" x="80" y="440">
Pre-programmed focus, keymaps, and page navigation.
</text>
<text font-family="Inter, system-ui, sans-serif" font-weight="400" font-size="28" fill="#a1a1aa" x="80" y="480">
Stop rewriting the same architecture for every project.
</text>
<g transform="translate(80, 540)">
<rect x="0" y="0" width="280" height="56" rx="8" fill="#18181b" stroke="#3f3f46"/>
<text font-family="ui-monospace, 'JetBrains Mono', monospace" font-size="20" fill="#f4f4f5" x="20" y="36">$ cargo add tui-pages</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,52 @@
<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">
<style>
.bg { fill: #18181b; }
.chrome { fill: #27272a; }
.dot { fill: #52525b; }
.field { fill: #0e0e10; stroke: #3f3f46; stroke-width: 1; }
.field-focus { fill: #1c130f; stroke: #b7410e; stroke-width: 2; }
.btn { fill: #b7410e; }
.sep { stroke: #27272a; stroke-width: 1; }
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
.t-md { font-size: 14px; }
.t-sm { font-size: 12px; }
.t-xs { font-size: 11px; }
.muted { fill: #a1a1aa; }
.dim { fill: #71717a; }
.label { fill: #a1a1aa; letter-spacing: 0.08em; }
.btn-fg { fill: #fff5f0; }
</style>
<rect class="bg" width="640" height="400" rx="12"/>
<rect class="chrome" width="640" height="36" rx="12"/>
<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"/>
<circle class="dot" cx="40" cy="18" r="5.5"/>
<circle class="dot" cx="60" cy="18" r="5.5"/>
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/canvas — login page</text>
<text x="40" y="80" font-size="20" font-weight="700">Sign in</text>
<text x="40" y="104" font-size="12" class="muted">Use Tab to move between fields. Enter to submit.</text>
<text x="40" y="156" font-size="10" class="label">USERNAME</text>
<rect class="field-focus" x="40" y="164" width="560" height="38" rx="6"/>
<text x="56" y="189" font-size="14">filip</text>
<rect x="92" y="172" width="2" height="22" fill="#b7410e">
<animate attributeName="opacity" values="1;0;1" dur="1s" repeatCount="indefinite"/>
</rect>
<text x="40" y="232" font-size="10" class="label">PASSWORD</text>
<rect class="field" x="40" y="240" width="560" height="38" rx="6"/>
<text x="56" y="265" font-size="14" class="muted">••••••••••••</text>
<rect class="btn" x="40" y="300" width="140" height="38" rx="6"/>
<text x="110" y="324" text-anchor="middle" font-size="13" class="btn-fg" font-weight="600">Sign in</text>
<text x="200" y="324" font-size="11" class="dim">esc · cancel</text>
<line class="sep" x1="0" y1="358" x2="640" y2="358"/>
<text x="20" y="380" font-size="11" class="dim">tab next · shift+tab prev · enter submit</text>
<text x="620" y="380" text-anchor="end" font-size="11" class="dim">canvas feature enabled</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,57 @@
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/default terminal screenshot">
<style>
.bg { fill: #18181b; }
.chrome { fill: #27272a; }
.dot { fill: #52525b; }
.sep { stroke: #27272a; stroke-width: 1; }
.tab { fill: #18181b; }
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
.t-md { font-size: 15px; }
.t-sm { font-size: 13px; }
.t-xs { font-size: 11px; }
.muted { fill: #a1a1aa; }
.dim { fill: #71717a; }
.sel-bg { fill: #b7410e; }
.sel-fg { fill: #fff5f0; }
</style>
<rect class="bg" width="640" height="400" rx="12"/>
<rect class="chrome" width="640" height="36" rx="12"/>
<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"/>
<circle class="dot" cx="40" cy="18" r="5.5"/>
<circle class="dot" cx="60" cy="18" r="5.5"/>
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/default — cargo run</text>
<text x="20" y="68" font-size="14" class="muted">Pages:</text>
<rect class="sel-bg" x="80" y="55" width="60" height="20" rx="3"/>
<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"/>
<text x="20" y="120" font-size="18" font-weight="700">Home</text>
<text x="20" y="142" font-size="11" class="dim">Choose a section to open.</text>
<line class="sep" x1="0" y1="160" x2="640" y2="160"/>
<g font-size="14">
<rect class="sel-bg" x="0" y="170" width="640" height="34"/>
<text x="32" y="192" class="sel-fg" font-weight="600">▸ Dashboard</text>
<text x="600" y="192" text-anchor="end" class="sel-fg" font-size="11">enter</text>
<text x="32" y="226" class="muted"> Accounts</text>
<text x="32" y="254" class="muted"> Transactions</text>
<text x="32" y="282" class="muted"> Reports</text>
<text x="32" y="310" class="muted"> Settings</text>
</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>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,71 @@
<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">
<style>
.bg { fill: #18181b; }
.chrome { fill: #27272a; }
.dot { fill: #52525b; }
.kbd { fill: #27272a; stroke: #3f3f46; stroke-width: 1; }
.modal { fill: #1f1f23; stroke: #b7410e; stroke-width: 2; }
.sep { stroke: #27272a; stroke-width: 1; }
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
.t-sm { font-size: 12px; }
.t-xs { font-size: 11px; }
.muted { fill: #a1a1aa; }
.dim { fill: #71717a; }
</style>
<rect class="bg" width="640" height="400" rx="12"/>
<rect class="chrome" width="640" height="36" rx="12"/>
<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"/>
<circle class="dot" cx="40" cy="18" r="5.5"/>
<circle class="dot" cx="60" cy="18" r="5.5"/>
<text x="320" y="22" text-anchor="middle" font-size="11" class="dim">examples/keybindings — modal open</text>
<!-- dimmed background content -->
<text x="40" y="80" font-size="14" class="dim">▸ Accounts</text>
<text x="40" y="108" font-size="14" class="dim"> Transactions</text>
<text x="40" y="136" font-size="14" class="dim"> Reports</text>
<text x="40" y="164" font-size="14" class="dim"> Settings</text>
<text x="40" y="320" font-size="11" class="dim">Press ? for help</text>
<!-- modal -->
<rect class="modal" x="120" y="60" width="400" height="280" rx="10"/>
<text x="140" y="94" font-size="14" font-weight="700">Keybindings</text>
<line class="sep" x1="140" y1="104" x2="500" y2="104"/>
<text x="140" y="130" font-size="11" class="muted">MOVEMENT</text>
<rect class="kbd" x="140" y="140" width="34" height="24" rx="4"/>
<text x="157" y="157" text-anchor="middle" font-size="12">j</text>
<text x="184" y="157" font-size="12" class="muted">move down</text>
<rect class="kbd" x="140" y="170" width="34" height="24" rx="4"/>
<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"/>
<text x="157" y="217" text-anchor="middle" font-size="12">gg</text>
<text x="184" y="217" font-size="12" class="muted">top of list</text>
<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>

After

Width:  |  Height:  |  Size: 3.5 KiB