clone

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

main
forge / site / _includes / ui.njk
ui.njk 9.2K
1{#- The forge's controls, one macro each; their CSS is in style.css under2 "controls". Pages use these rather than writing the markup again, so a3 control looks and behaves the same everywhere; /design/ shows each one.4 Import it with: {% import "ui.njk" as ui %}56 The rule: content sits in panels (ui.panel), chrome on the page; tabs7 say where you are; a checkbox turns one thing on or off;8 a segmented control picks one of a few; an icon toggle is a checkbox9 drawn as a symbol (with aria-pressed, a label and a title); buttons do10 things; links only go places. Chrome is written in lowercase here and11 in templates; nothing is case-transformed by CSS.1213 attrs, where a macro takes it, is extra attributes as written, e.g.14 'data-wrap' or 'id="signin"'. -#}1516{# A 16px line icon in the text's colour. With a label it is an image with17 that name; without, decoration beside words that say the same. #}18{% macro icon(name, label = '') -%}19<svg class="icon" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"{% if label %} role="img" aria-label="{{ label }}"{% else %} aria-hidden="true"{% endif %}>20{%- if name == 'globe' %}<circle cx="8" cy="8" r="6.3"/><ellipse cx="8" cy="8" rx="2.7" ry="6.3"/><path d="M1.9 6h12.2M1.9 10h12.2"/>21{%- elif name == 'clock' %}<circle cx="8" cy="8" r="6.3"/><path d="M8 4.4V8l2.6 1.6"/>22{%- elif name == 'az' %}<path d="M1.8 13 4.6 3.5 7.4 13M2.8 10h3.6M9.4 3.5h4.4l-4.4 9.5h4.6"/>23{%- elif name == 'folder' %}<path d="M1.8 4.2c0-.6.4-1 1-1h3.4l1.5 1.7h5.5c.6 0 1 .4 1 1v6.9c0 .6-.4 1-1 1H2.8c-.6 0-1-.4-1-1z"/>24{%- elif name == 'file' %}<path d="M3.5 1.8h5.6l3.4 3.4v9H3.5zM9 1.8v3.5h3.5"/>25{%- elif name == 'link' %}<path d="M6.7 9.3a2.8 2.8 0 0 0 4 0l2.1-2.1a2.8 2.8 0 0 0-4-4l-.8.8M9.3 6.7a2.8 2.8 0 0 0-4 0L3.2 8.8a2.8 2.8 0 0 0 4 4l.8-.8"/>26{%- elif name == 'up' %}<path d="M6.5 3.2 3 6.7l3.5 3.5M3.2 6.7h6.3a3.5 3.5 0 0 1 0 7H8"/>27{%- elif name == 'branch' %}<circle cx="4.5" cy="3.5" r="1.6"/><circle cx="4.5" cy="12.5" r="1.6"/><circle cx="11.5" cy="5" r="1.6"/><path d="M4.5 5.1v5.8M11.5 6.6c0 2.6-2.2 3.4-5.4 4.3"/>28{%- elif name == 'search' %}<circle cx="6.8" cy="6.8" r="4.6"/><path d="M10.3 10.3 14 14"/>29{%- endif %}</svg>30{%- endmacro %}3132{# A button. primary: the one main action of its place, filled with the33 accent; otherwise secondary. #}34{% macro button(text, primary = false, attrs = '', type = 'button') -%}35<button type="{{ type }}"{% if primary %} class="primary"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>{{ text }}</button>36{%- endmacro %}3738{# An icon toggle: on or off, drawn as a symbol; filled with ink when on.39 label names what it does when on; it is the button's name and title. #}40{% macro iconToggle(name, label, pressed = false, attrs = '') -%}41<button type="button" class="icon-btn" aria-pressed="{{ 'true' if pressed else 'false' }}" aria-label="{{ label }}" title="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>{{ icon(name) }}</button>42{%- endmacro %}4344{# A segmented control: one of a few, as joined buttons, the chosen one45 filled with ink. items: {label, icon (optional: the label becomes the46 name and title), pressed, attrs}. #}47{% macro segmented(label, items, attrs = '') -%}48<span class="segmented" role="group" aria-label="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>49{%- for it in items %}<button type="button"{% if it.icon %} class="icon-btn" aria-label="{{ it.label }}" title="{{ it.label }}"{% endif %} aria-pressed="{{ 'true' if it.pressed else 'false' }}"{% if it.attrs %} {{ it.attrs | safe }}{% endif %}>{{ icon(it.icon) if it.icon else it.label }}</button>{% endfor -%}50</span>51{%- endmacro %}5253{# A checkbox with its words: on or off. #}54{% macro checkbox(text, checked = false, attrs = '') -%}55<label class="check"><input type="checkbox"{% if checked %} checked{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>{{ text }}</label>56{%- endmacro %}5758{# Tabs: where you are. items: {href, text, cur, key (a keys.ts shortcut),59 n (a count after the words), off (leave it out), attrs}. #}60{% macro tabs(label, items, cls = '', attrs = '') -%}61<nav class="tabs{% if cls %} {{ cls }}{% endif %}" aria-label="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>62{% for t in items %}{% if not t.off %}<a href="{{ t.href }}"{% if t.key %} data-key="{{ t.key }}"{% endif %}{% if t.cur %} class="cur" aria-current="page"{% endif %}{% if t.attrs %} {{ t.attrs | safe }}{% endif %}>{{ t.text }}{% if t.n != null %}<span class="n">{{ t.n }}</span>{% endif %}</a>63{% endif %}{% endfor -%}64</nav>65{%- endmacro %}6667{# A text box. mono: for what is copied, like a URL or a command. #}68{% macro input(label, value = '', attrs = '', mono = false) -%}69<input class="input{% if mono %} mono{% endif %}" aria-label="{{ label }}"{% if value %} value="{{ value }}"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>70{%- endmacro %}7172{# A box of lines of text. mono: for what is copied, like a key. #}73{% macro textarea(label, placeholder = '', attrs = '', mono = false) -%}74<textarea class="input{% if mono %} mono{% endif %}" aria-label="{{ label }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}></textarea>75{%- endmacro %}7677{# A panel: where content sits (lists, file viewers, READMEs, diffs,78 discussions), raised over the page; the header, tabs and toolbars stay on79 the page. head: the markup of its head strip, naming what it holds: quiet80 words, the key item in <b> (or a link with class "key"), shas and ages in81 .sha and time; capture it with {% set head %}...{% endset %} so what users82 wrote is escaped. fold: it folds, its head strip the summary ('open'83 starts it open). The body is the call's, if any:84 {% call ui.panel(head) %}{{ m.files(entries) }}{% endcall %} #}85{% macro panel(head = '', cls = '', attrs = '', fold = '') -%}86{% if fold %}<details class="panel{% if cls %} {{ cls }}{% endif %}"{% if fold == 'open' %} open{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}><summary class="panel-head">{{ head | safe }}</summary>87{% else %}<section class="panel{% if cls %} {{ cls }}{% endif %}"{% if attrs %} {{ attrs | safe }}{% endif %}>{% if head %}<header class="panel-head">{{ head | safe }}</header>{% endif %}88{% endif %}{% if caller %}{{ caller() }}{% endif %}89{% if fold %}</details>{% else %}</section>{% endif %}90{%- endmacro %}9192{# A list: a table with rows between hairlines that restacks on phones93 (stack). heads: column names, or {text, cls, title}. The rows are the94 call's body: {% call ui.listTable(['branch', 'updated']) %}<tr>...{% endcall %} #}95{% macro listTable(heads = [], cls = 'stack nav', attrs = '') -%}96<table class="list{% if cls %} {{ cls }}{% endif %}"{% if attrs %} {{ attrs | safe }}{% endif %}>97{% if heads.length %}<thead><tr>{% for h in heads %}<th{% if h.cls %} class="{{ h.cls }}"{% endif %}{% if h.title %} title="{{ h.title }}"{% endif %}>{{ h.text if h.text != null else h }}</th>{% endfor %}</tr></thead>98{% endif %}<tbody>99{{ caller() }}</tbody>100</table>101{%- endmacro %}102103{# A review's status: the one filled label, white on its colour. #}104{% macro status(s) %}<span class="status {{ s }}">{{ s }}</span>{% endmacro %}105106{# A section heading on the page, over the panels it names: its words, then107 a count (n) and a quieter note (desc). A single panel's name goes in its108 head strip instead. #}109{% macro heading(text, n = null, desc = '', id = '') -%}110<h2{% if id %} id="{{ id }}"{% endif %}>{{ text }}{% if n != null %} <span class="n">{{ n }}</span>{% endif %}{% if desc %} <span class="desc">{{ desc }}</span>{% endif %}</h2>111{%- endmacro %}112113{# The activity grid: a year of days, a column a week (Sunday at the top),114 each day a button coloured by how much happened (--heat-0 to -4), named115 "2026-10-08: 11 commits". grid is lib/activity.ts's. Without JS it is a116 picture; with it (ui.ts) clicking a day, or Enter or Space on it, picks117 it (an ink outline) and narrows the list under it, the arrow keys move118 between days, and the address keeps the day (#2026-10-08, or119 #<shelf>/2026-10-08 on an owner's page with a shelf picked). The 53120 weeks fill the width; narrower, cells shrink to 10px, then the oldest121 weeks go (style.css). attrs: data-src="activity.json", what ui.ts122 fetches for fresher counts. still: a chart, with nothing to pick (a123 repo's insights): each day a span, named and titled the same, and a124 picture with JS too. #}125{% macro activity(grid, label, attrs = '', still = false) -%}126<div class="activity-fit"><div class="activity" role="group" aria-label="{{ label }}" data-activity data-noun="{{ grid.noun }}"{% if attrs %} {{ attrs | safe }}{% endif %}>127{%- for w in grid.weeks %}{% for c in w %}{% set what = c.day + ': ' + (c.n | plural(grid.noun)) %}{% if still %}<span class="l{{ c.level }} a{{ c.age }}" data-day="{{ c.day }}" data-n="{{ c.n }}" role="img" aria-label="{{ what }}" title="{{ what }}"></span>{% else %}<button type="button" class="l{{ c.level }} a{{ c.age }}" data-day="{{ c.day }}" data-n="{{ c.n }}" aria-pressed="false" tabindex="{{ '0' if c.day == grid.today else '-1' }}" aria-label="{{ what }}" title="{{ what }}"></button>{% endif %}{% endfor %}{% endfor -%}128</div></div>129{%- endmacro %}