Files
Joshua KingandClaude Opus 5 d9ed1898b9 Add the Extrudex Design System v3 bundle
Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the
self-hosted JetBrains Mono fonts, and interactive mockups of every screen
across three iterations. Iteration 3 is final: phone, tablet and desktop
tiers, device preview, and "empty spool weight" terminology. The build
scripts regenerate the screens byte-identically from source.

Implementation is tracked in Maestro feature UI Refresh - V3.0
(224b72a1-8d84-4970-97ea-5dafa4d8ea3b).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 11:31:50 -04:00

123 lines
5.7 KiB
Python

"""Assemble self-contained mockup pages: tokens + fonts (base64) + shell + page."""
import base64, pathlib, re
HERE = pathlib.Path(__file__).parent
OUT = pathlib.Path.home() / 'Design/extrudex-v3'
FONTS = OUT / 'fonts'
tokens = (OUT / 'design-system-v3.css').read_text()
def embed_font(match):
name = match.group(1)
data = base64.b64encode((FONTS / name).read_bytes()).decode()
return f"url('data:font/woff2;base64,{data}')"
tokens = re.sub(r"url\('\./fonts/([^']+)'\)", embed_font, tokens)
app_css = (HERE / 'app.css').read_text()
ICONS = {
'dashboard': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></svg>',
'filament': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="M12 3v6M12 15v6"/></svg>',
'prints': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>',
'printers': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 17h8M9 7h6v5H9z"/></svg>',
}
NAV = [('dashboard', 'Dashboard', '01-dashboard.html'), ('filament', 'Filament', '02-filament.html'),
('prints', 'Prints', None), ('printers', 'Printers', '04-printers.html')]
THEME_HEAD = """<script>
(function () {
var root = document.documentElement, key = 'extrudex-v3-theme', t = null;
try { t = localStorage.getItem(key); } catch (e) {}
var q = /[?&]theme=(light|dark)/.exec(location.search); if (q) t = q[1];
if (t !== 'light' && t !== 'dark') {
t = window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
root.setAttribute('data-theme', t);
function set(v) {
root.setAttribute('data-theme', v);
document.querySelectorAll('[data-set-theme]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-set-theme') === v));
});
try { localStorage.setItem(key, v); } catch (e) {}
}
document.addEventListener('DOMContentLoaded', function () { set(root.getAttribute('data-theme')); });
document.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('[data-set-theme]');
if (b) set(b.getAttribute('data-set-theme'));
});
})();
</script>"""
def rail(active):
links = []
for key, label, href in NAV:
cur = ' aria-current="page"' if key == active else ''
extra = ''
if key == 'dashboard': extra = '<span class="chip chip-accent nav-new">new</span>'
if href is None:
links.append(f'<a href="#" title="Prints history: not part of this proposal"{cur}>{ICONS[key]}<span class="lbl">{label}</span></a>')
else:
links.append(f'<a href="{href}"{cur}>{ICONS[key]}<span class="lbl">{label}</span>{extra}</a>')
return f"""<nav class="rail nm-raised" aria-label="Main">
<div class="wordmark"><span class="wordmark-mark" aria-hidden="true">X</span>Extrudex</div>
<div class="nav">{''.join(links)}</div>
<div class="rail-foot">
<div class="seg" role="group" aria-label="Color theme">
<button type="button" data-set-theme="light" aria-pressed="false">Light</button>
<button type="button" data-set-theme="dark" aria-pressed="true">Dark</button>
</div>
<span class="who muted">Signed in · operator</span>
</div>
</nav>"""
def page(slug, title, active, description):
src = (HERE / 'pages' / f'{slug}.html').read_text()
css_m = re.search(r'<style>(.*?)</style>', src, re.S)
page_css = css_m.group(1) if css_m else ''
body = re.sub(r'<style>.*?</style>', '', src, flags=re.S).strip()
js_m = re.search(r'<script>(.*?)</script>', body, re.S)
page_js = ''
if js_m:
page_js = f'<script>{js_m.group(1)}</script>'
body = re.sub(r'<script>.*?</script>', '', body, flags=re.S)
html = f"""<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<meta name="description" content="{description}">
{THEME_HEAD}
<style>
{tokens}
{app_css}
{page_css}
</style>
</head>
<body>
<div class="shell">
{rail(active)}
<main id="main">
<div class="proposal nm-inset" role="note">
<span class="chip chip-accent">Design System v3 proposal</span>
<span>Mockup with sample data from the real inventory. Nothing here is wired to the API.</span>
<span class="spacer"></span>
<span class="row" style="gap:12px"><span class="src"><span class="chip chip-neutral">dev</span> on dev today</span>
<span class="src"><span class="chip chip-info chip-dashed">feature</span> feature branch only</span>
<span class="src"><span class="chip chip-warn chip-dashed">API gap</span> stored, not served</span></span>
</div>
{body}
</main>
</div>
{page_js}
</body>
</html>
"""
(OUT / 'screens' / f'{slug}.html').write_text(html)
print(slug, len(html) // 1024, 'KB')
page('01-dashboard', 'Extrudex Dashboard', 'dashboard', 'Stock summary and recent prints, Soft UI v3 mockup.')
page('02-filament', 'Extrudex Filament', 'filament', 'Filament inventory list with search, add, edit and delete, Soft UI v3 mockup.')
page('03-spool-form', 'Extrudex Spool Form', 'filament', 'Add/Edit spool form with 409 conflict and field-level 400 errors, Soft UI v3 mockup.')
page('04-printers', 'Extrudex Printers', 'printers', 'Printers screen with stored fields, dropped MQTT state and a Future section, Soft UI v3 mockup.')