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>
123 lines
5.7 KiB
Python
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.')
|