clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

main
forge / site / design.njk
design.njk 14.2K
1---2permalink: /design/index.html3---4{#- The style guide: the tokens style.css defines (designTokens, read from its5 token block by lib/check-css.ts) and every control from ui.njk, drawn by6 the real stylesheet and the real macros, so it can't drift from them.7 The rules behind it are in DESIGN.md. -#}8{% extends "base.njk" %}9{% import "macros.njk" as m with context %}10{% import "ui.njk" as ui %}11{% block title %}Design - {{ forge.config.name }}{% endblock %}12{% block head %}{{ m.meta('Design - ' + forge.config.name, 'The tokens and controls every page of ' + forge.config.name + ' is made of.', '/design/') }}{% endblock %}13{% block heading %}<a href="/">{{ forge.config.name }}</a> / <b>design</b>14{% endblock %}15{% block content %}16{% css %}17/* the style guide (design.njk) */18.dg-intro { max-width: 46em; margin: 0 0 var(--space-6); color: var(--muted); }19.dg-intro p { margin: 0 0 var(--space-2); }20.dg-rules { margin: 0; padding-left: var(--space-5); }21.dg-rules li { margin: var(--space-1) 0; }22.dg-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-4); padding: var(--space-3); border-bottom: var(--hairline) solid var(--border); }23.dg-row:last-child { border-bottom: 0; }24.dg-row > .label { flex: 0 0 9em; }25.dg-swatch { display: inline-block; width: 28px; height: 20px; border: var(--hairline) solid var(--border-strong); vertical-align: middle; }26table.dg-tokens { table-layout: fixed; }27table.dg-tokens td { vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }28table.dg-tokens td:nth-child(1) { width: 34%; }29table.dg-tokens td:nth-child(2) { width: 22%; }30table.dg-tokens td.v { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); white-space: nowrap; }31.dg-sample { white-space: nowrap; }32.dg-bar { display: inline-block; height: var(--space-3); background: var(--accent); vertical-align: middle; }33.dg-fam { padding: var(--space-3); border-bottom: var(--hairline) solid var(--border); }34.dg-fam:last-child { border-bottom: 0; }35.dg-fam .big { font-size: var(--text-xl); line-height: 1.3; }36.dg-fam .use { font-size: var(--text-sm); color: var(--muted); }37.dg-legible { margin: 0; padding: var(--space-2) var(--space-3); border-top: var(--hairline) solid var(--border); font: var(--fw-regular) var(--text-code)/1.6 var(--mono); }38.dg-page { margin: 0 0 var(--space-4); }39.dg-avatars { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: var(--space-4); padding: var(--space-3); }40.dg-avatars figure { display: grid; grid-template-columns: auto auto; align-items: end; justify-content: start; gap: var(--space-2); margin: 0; }41.dg-avatars figcaption { grid-column: 1 / -1; font-size: var(--text-xs); color: var(--muted); }42.dg-legible span + span { margin-left: var(--space-4); }43@media (max-width: 720px) {44 .dg-row > .label { flex-basis: 100%; }45 table.dg-tokens td.v.dark { display: none; }46 table.dg-tokens td:nth-child(1) { width: 30%; }47 table.dg-tokens td:nth-child(2) { width: 40%; }48}49{% endcss %}50<div class="dg-intro">51<p>What every page here is made of: the tokens in <code>site/assets/style.css</code> and the controls in <code>site/_includes/ui.njk</code>, drawn below by that same CSS and those same macros. The rules are in <code>DESIGN.md</code>, and CI checks them (<code>lib/check-css.ts</code>).</p>52<ul class="dg-rules">53<li>Tokens only: no colour, size, weight or family is written anywhere else.</li>54<li>Open Sans for the interface, Noto Sans Mono for what is copied or compared, Noto Serif for prose people wrote.</li>55<li>Chrome is lowercase, written that way; no capitals by CSS, no letter-spacing. Hierarchy is weight, size and colour.</li>56<li>Content sits in panels: lists, file viewers, READMEs, diffs and discussions, each on a raised surface with a hairline edge and a tinted head strip naming what it holds. The header, a page's title, tabs and toolbars sit on the page. Square corners, no shadows.</li>57<li>Inside a panel, rows are between hairlines, none under the last; hover tints the whole row, and a row's own link stays in ink.</li>58<li>Tabs say where you are; checkboxes turn one thing on or off; segmented controls pick one of a few; buttons act; links go places. Words for where you are, symbols for how you look at it.</li>59</ul>60</div>6162{{ ui.heading('on the page', null, 'chrome: a toolbar, tabs') }}63<div class="bar dg-page"><details class="picker"><summary>{{ ui.icon('branch') }}<b>main</b></summary></details><span class="crumbs"><a href="#">forge</a> / <a href="#">site</a> / <a href="#">assets</a></span><span class="grow"></span>{{ ui.iconToggle('globe', 'only repos with a site') }}{{ ui.segmented('sort', [{icon: 'clock', label: 'newest first', pressed: true}, {icon: 'az', label: 'by name, a to z'}]) }}{{ ui.button('find file') }}</div>64<div class="dg-page">{{ ui.tabs('example', [{href: '#tabs', text: 'all', n: 12, cur: true}, {href: '#tabs', text: 'tools', n: 5}, {href: '#tabs', text: 'games', n: 4}, {href: '#tabs', text: 'other', n: 3}]) }}</div>6566{{ ui.heading('panels', null, 'where content sits: ui.panel') }}67{% set head %}<span class="grow"><b>cat</b> <a class="key" href="#panels">Keep a range-diff's marker at the left edge</a></span><span class="end"><a class="sha" href="#panels">8318d80</a><span class="ci pass">&#10003;</span><time>2h</time><a href="#panels">412 commits</a></span>{% endset %}68{% call ui.panel(head) %}69<table class="list files nav"><tbody>70<tr><td class="file-icon">{{ ui.icon('folder') }}</td><td class="main"><a href="#panels">lib</a></td><td class="num size"></td></tr>71<tr><td class="file-icon">{{ ui.icon('folder') }}</td><td class="main"><a href="#panels">site</a></td><td class="num size"></td></tr>72<tr><td class="file-icon">{{ ui.icon('file') }}</td><td class="main"><a href="#panels">DESIGN.md</a></td><td class="num size">5.1K</td></tr>73<tr><td class="file-icon">{{ ui.icon('file') }}</td><td class="main"><a href="#panels">README.md</a></td><td class="num size">9.8K</td></tr>74</tbody></table>75{% endcall %}76{% call ui.panel('<a class="key mono" href="#panels">README.md</a>') %}<div class="markdown">77<h2>A README in the serif</h2>78<p>Prose people wrote is set in <em>Noto Serif</em>: READMEs, the wiki, release notes, a review's description and its comments. Code inside it, like <code>forge attest --stack</code> or <code>lib/<wbr><span class="nobr">text-widths.sh</span></code>, is in the mono, and never breaks at a hyphen.</p>79<blockquote><p>Interface chrome never uses it.</p></blockquote>80</div>{% endcall %}81{% call ui.panel('<b>a panel that folds</b><span class="n">3</span><span class="grow">its head strip is the summary</span>', '', '', 'open') %}82<p class="empty">An empty state: a quiet sentence, centred, in the panel it would fill.</p>83{% endcall %}8485{{ ui.heading('controls') }}86{% call ui.panel('<b>in a panel</b><span class="grow">controls in a head strip shrink to fit it</span><span class="end">' + ui.checkbox('wrap lines') + ui.button('copy') + '</span>') %}87<div class="dg-row"><span class="label">buttons</span>{{ ui.button('copy') }}{{ ui.button('comment', true) }}{{ ui.button('approve', false, 'disabled') }}</div>88<div class="dg-row"><span class="label">icon toggle</span><span data-demo>{{ ui.iconToggle('globe', 'only repos with a site') }}</span><span data-demo>{{ ui.iconToggle('globe', 'only repos with a site, on', true) }}</span><span class="desc">a checkbox drawn as a symbol: aria-pressed, a label, a title</span></div>89<div class="dg-row"><span class="label">segmented</span><span data-demo>{{ ui.segmented('sort', [{icon: 'clock', label: 'newest first', pressed: true}, {icon: 'az', label: 'by name, a to z'}]) }}</span><span data-demo>{{ ui.segmented('view', [{label: 'unified', pressed: true}, {label: 'split'}]) }}</span></div>90<div class="dg-row"><span class="label">checkbox</span>{{ ui.checkbox('wrap lines') }}{{ ui.checkbox('viewed', true) }}</div>91<div class="dg-row"><span class="label">text box</span><span style="flex: 1 1 16em">{{ ui.input('ssh clone url', 'git@forge.example:cat/forge', 'readonly', true) }}</span><span style="flex: 1 1 16em">{{ ui.input('search', '', 'placeholder="search commits"') }}</span></div>92<div class="dg-row"><span class="label">text area</span><span style="flex: 1 1 24em">{{ ui.textarea('a comment', 'Comment on #3. Markdown is fine.') }}</span></div>93<div class="dg-row"><span class="label">tabs</span><span style="flex: 1 1 20em">{{ ui.tabs('example', [{href: '#tabs', text: 'all', n: 12, cur: true}, {href: '#tabs', text: 'tools', n: 5}, {href: '#tabs', text: 'games', n: 4}, {href: '#tabs', text: 'other', n: 3}]) }}</span></div>94<div class="dg-row"><span class="label">status labels</span>{{ ui.status('open') }}{{ ui.status('draft') }}{{ ui.status('merged') }}{{ ui.status('closed') }}</div>95<div class="dg-row"><span class="label">ci and diffs</span><span class="ci pass">&#10003; 2/2</span><span class="ci fail">&#10007; 1/2</span><span class="ci partial">&ndash; 1/2</span><span class="mono"><span class="i">+12</span> <span class="d">-3</span></span><span class="st A">A</span><span class="st M">M</span><span class="st D">D</span><span class="st R">R</span></div>96<div class="dg-row"><span class="label">icons</span>{% for i in ['globe', 'clock', 'az', 'branch', 'folder', 'file', 'link', 'up', 'search'] %}<span title="{{ i }}">{{ ui.icon(i, i) }}</span>{% endfor %}</div>97{% endcall %}9899{{ ui.heading('people and activity', null, 'avatars from a name; the activity grid') }}100{% set head %}<b>avatars</b><span class="grow">bauhaus tiles drawn from the name (lib/avatar.ts), 72px on a person's page, 36px in lists</span>{% endset %}101{% call ui.panel(head) %}102<div class="dg-avatars">103{% for n in ['cat', 'fox', 'bob', 'kim', 'lee', 'mo', 'jun', 'ola', 'sam', 'tess', 'yuki', 'ren'] %}<figure>{{ n | avatar(72) }}{{ n | avatar(36) }}<figcaption class="mono">{{ n }}</figcaption></figure>{% endfor %}104</div>105{% endcall %}106{% set grid = 'commit' | demoActivity %}107{% set head %}<b class="act-total">{{ grid.total | thousands }} commits</b><span>in the last year: ui.activity, a day a button; pick one</span>{% endset %}108{% call ui.panel(head, 'activity-panel') %}<div class="panel-body">{{ ui.activity(grid, 'commits a day') }}</div>{% endcall %}109{% set grid = 'event' | demoActivity %}110{% set head %}<b class="act-total">{{ grid.total | thousands }} events</b><span>in the last year: ui.activity, still, a chart with nothing to pick, as on a repo's insights</span>{% endset %}111{% call ui.panel(head, 'activity-panel') %}<div class="panel-body">{{ ui.activity(grid, 'events a day', '', true) }}</div>{% endcall %}112113{{ ui.heading('section heading', 4, 'a count, then a quieter note') }}114{% call ui.panel() %}115{% call ui.listTable(['commit', 'author', {text: '+', cls: 'num'}, {text: '-', cls: 'num'}, 'ci']) %}116<tr><td class="main"><a href="#list">Parse --name and --shout</a></td><td class="nowrap">cat</td><td class="num i">+12</td><td class="num d">-3</td><td><span class="ci pass">&#10003;</span></td></tr>117<tr><td class="main"><a href="#list">Keep a range-diff's marker at the left edge</a></td><td class="nowrap">fox</td><td class="num i">+4</td><td class="num d">-1</td><td><span class="ci fail">&#10007;</span></td></tr>118{% endcall %}119{% endcall %}120121{{ ui.heading('code') }}122<section class="panel file"><header class="panel-head fh"><span class="st M" title="modified">M</span> <a href="#code" class="fname">greet.sh</a> <span class="counts"><span class="i">+2</span> <span class="d">-1</span></span></header>123<div class="code diff" style="--w:2"><div class="rows">124<div class="hunk"><span>@@ -1,3 +1,4 @@ greet()</span></div>125<span class="l"><span class="ln">1</span><span class="ln">1</span><span class="mk"></span><span class="lc"><span class="hljs-meta">#!/bin/sh</span></span></span>126<span class="l del"><span class="ln">2</span><span class="ln"></span><span class="mk">-</span><span class="lc"><span class="hljs-built_in">echo</span> <span class="hljs-string">"hello, world"</span></span></span>127<span class="l add"><span class="ln"></span><span class="ln">2</span><span class="mk">+</span><span class="lc">name=<span class="hljs-variable">${1:-world}</span></span></span>128<span class="l add"><span class="ln"></span><span class="ln">3</span><span class="mk">+</span><span class="lc"><span class="hljs-built_in">echo</span> <span class="hljs-string">"hello, <span class="hljs-variable">$name</span>"</span> <span class="hljs-comment"># 0O 1lI 5S 8B rn m</span></span></span>129</div></div>130<p class="dg-legible">legibility at the code size: <span>0O o</span><span>1lI| i</span><span>rn m</span><span>{} () []</span><span>-- =&gt; !=</span></p>131</section>132133{{ ui.heading('type') }}134{% call ui.panel() %}135<div class="dg-fam"><div class="big" style="font-family: var(--sans)">Open Sans: tabs, lists, subjects, buttons</div><div class="use"><code>--sans</code> all interface text</div></div>136<div class="dg-fam"><div class="big" style="font-family: var(--mono)">Noto Sans Mono: 0f0215e cat/help +12</div><div class="use"><code>--mono</code> anything copied or compared: hashes, ids, branches, paths, counts, code, diffs, commands</div></div>137<div class="dg-fam"><div class="big" style="font-family: var(--serif)">Noto Serif: what people wrote</div><div class="use"><code>--serif</code> rendered prose only</div></div>138{% endcall %}139140{{ ui.heading('tokens') }}141{% for g in designTokens %}142{% set head %}<span>{{ g.title }}</span>{% endset %}143{% call ui.panel(head) %}144<table class="list dg-tokens">145<tbody>146{% for t in g.tokens %}147<tr>148{% if t.name.startsWith('--text-') %}<td class="main dg-sample" style="font-size: var({{ t.name }})">the quick brown fox, 0123</td>149{% elif t.name.startsWith('--fw-') %}<td class="main dg-sample" style="font-weight: var({{ t.name }})">the quick brown fox</td>150{% elif t.name == '--sans' or t.name == '--mono' or t.name == '--serif' %}<td class="main dg-sample" style="font-family: var({{ t.name }})">the quick brown fox, 0123</td>151{% elif t.name.startsWith('--space-') %}<td class="main"><span class="dg-bar" style="width: var({{ t.name }})"></span></td>152{% elif t.light.startsWith('#') or t.light.startsWith('rgb') %}<td><span class="dg-swatch" style="background: var({{ t.name }})"></span></td>153{% else %}<td></td>154{% endif %}155<td class="mono nowrap">{{ t.name }}</td><td class="v" title="{{ t.light }}">{{ t.light }}</td><td class="v dark"{% if t.dark %} title="{{ t.dark }}"{% endif %}>{{ t.dark or '' }}</td>156</tr>157{% endfor %}158</tbody>159</table>160{% endcall %}161{% endfor %}162{% endblock %}