feat: redesign UI with themes, animations, procedural ASCII engine
Build and Deploy / Build Image (push) Successful in 31s
Build and Deploy / Deploy to Portainer (push) Successful in 6s

- Split app.py into routes + ascii_engine.py (stdlib-only procedural
  generation: figlet banners, creatures, patterns, scenes, classics)
- API: /api/random?category=..., /api/categories, richer JSON payload
- New frontend: templates/index.html + static/ (vanilla JS/CSS)
- 4 themes (graphite/paper/phosphor/amber), localStorage persist,
  no-flash boot, prefers-color-scheme default
- Scramble-resolve art transition, reduced-motion support
- Responsive: rail collapses to chips <760px, art auto-fits width
- Fix grid blowout on mobile (min-width: 0 on stage children)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 22:18:14 -07:00
co-authored by Claude Fable 5
parent fdd7f9d751
commit 5e33a38dbb
8 changed files with 1230 additions and 198 deletions
+104
View File
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>asciigen — procedural ascii art</title>
<script>
/* Theme boot: runs before first paint, no flash. */
(function () {
var t;
try { t = localStorage.getItem("asciigen.theme"); } catch (e) {}
if (!t) {
t = (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches)
? "paper" : "graphite";
}
document.documentElement.dataset.theme = t;
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div class="scan" aria-hidden="true"></div>
<div class="drift" aria-hidden="true">
<span style="--x:6%; --d:34s; --dl:-4s; --s:1.1">{ }</span>
<span style="--x:16%; --d:46s; --dl:-20s; --s:0.8">#</span>
<span style="--x:28%; --d:38s; --dl:-11s; --s:1.4">&gt;_</span>
<span style="--x:41%; --d:52s; --dl:-30s; --s:0.9"></span>
<span style="--x:55%; --d:40s; --dl:-7s; --s:1.2">::</span>
<span style="--x:67%; --d:47s; --dl:-25s; --s:0.7">%</span>
<span style="--x:78%; --d:36s; --dl:-15s; --s:1.3">[]</span>
<span style="--x:90%; --d:50s; --dl:-2s; --s:1.0">\\</span>
</div>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">asciigen</span>
<span class="brand-tag">/dev/random</span>
</div>
<div class="themes" role="group" aria-label="Theme">
<button class="theme-btn" data-set-theme="graphite" aria-label="Graphite theme (dark)" title="graphite"><span class="sw"></span></button>
<button class="theme-btn" data-set-theme="paper" aria-label="Paper theme (light)" title="paper"><span class="sw"></span></button>
<button class="theme-btn" data-set-theme="phosphor" aria-label="Phosphor theme (green terminal)" title="phosphor"><span class="sw"></span></button>
<button class="theme-btn" data-set-theme="amber" aria-label="Amber theme (CRT)" title="amber"><span class="sw"></span></button>
</div>
</header>
<main class="stage">
<aside class="rail">
<p class="rail-label">channel</p>
<nav class="cats" aria-label="Art category">
{% for cat in categories %}
<button class="cat{% if cat == 'any' %} is-active{% endif %}" data-cat="{{ cat }}">
<span class="cat-cursor" aria-hidden="true"></span>{{ cat }}
</button>
{% endfor %}
</nav>
<div class="hints" aria-hidden="true">
<p><kbd>space</kbd> generate</p>
<p><kbd>c</kbd> copy</p>
<p><kbd>t</kbd> theme</p>
</div>
</aside>
<section class="canvas">
<div class="frame" id="frame">
<span class="corner c-tl" aria-hidden="true"></span>
<span class="corner c-tr" aria-hidden="true"></span>
<span class="corner c-bl" aria-hidden="true"></span>
<span class="corner c-br" aria-hidden="true"></span>
<div class="art-scroll" id="artScroll" role="img" aria-label="Generated ASCII artwork">
<pre id="art">{{ piece.art }}</pre>
</div>
</div>
<div class="status" aria-live="polite">
<span class="st-title" id="stTitle">{{ piece.title }}</span>
<span class="st-sep" aria-hidden="true">·</span>
<span class="st-dims" id="stDims">{{ piece.cols }}×{{ piece.rows }}</span>
<span class="st-msg" id="stMsg">ready</span>
</div>
<div class="actions">
<button class="btn btn-primary" id="genBtn">
<span class="btn-glyph" aria-hidden="true"></span> generate
</button>
<button class="btn" id="copyBtn">
<span class="btn-glyph" aria-hidden="true"></span> <span id="copyLabel">copy</span>
</button>
</div>
</section>
</main>
<footer class="foot">
<span>procedural · stdlib python · zero deps</span>
<span class="foot-api">GET /api/random?category=…</span>
</footer>
</div>
<script>window.__initial = {{ piece | tojson }};</script>
<script src="{{ url_for('static', filename='js/app.js') }}" defer></script>
</body>
</html>