SPB Git

Repository Public MIT Selected

ui.njk 15.2 KB 193 lines jinja

1{#2  ─────────────────────────────────────────────3   SPB Git — Personal Git Platform4  ─────────────────────────────────────────────5   Author  : Simon-Pierre Boucher6   Contact : contact@spboucher.ai7   File    : src/web/views/components/ui.njk8   Purpose : Component library v3.0 — icon set, tags, ruled figures, section heads, tabs, breadcrumbs, ref picker, states9   License : MIT © Simon-Pierre Boucher10  ─────────────────────────────────────────────11#}12{#- One icon set for the whole UI: Lucide (ISC licence), 24×24 stroke icons drawn with currentColor.13    The path data below is generated from lucide-static; add a name here before using it anywhere. -#}14{% macro icon(name, size=16, cls='') -%}15<svg class="ic {{ cls }}" width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">16{%- if name == 'search' -%}<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>17{%- elif name == 'menu' -%}<path d="M4 5h16"/><path d="M4 12h16"/><path d="M4 19h16"/>18{%- elif name == 'close' -%}<path d="M18 6 6 18"/><path d="m6 6 12 12"/>19{%- elif name == 'sun' -%}<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>20{%- elif name == 'moon' -%}<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>21{%- elif name == 'midnight' -%}<path d="M18 5h4"/><path d="M20 3v4"/><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>22{%- elif name == 'auto' -%}<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>23{%- elif name == 'code' -%}<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>24{%- elif name == 'commit' -%}<circle cx="12" cy="12" r="3"/><line x1="3" x2="9" y1="12" y2="12"/><line x1="15" x2="21" y1="12" y2="12"/>25{%- elif name == 'branch' -%}<path d="M15 6a9 9 0 0 0-9 9V3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>26{%- elif name == 'tag' -%}<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>27{%- elif name == 'folder' -%}<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>28{%- elif name == 'file' -%}<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/>29{%- elif name == 'file-text' -%}<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>30{%- elif name == 'copy' -%}<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>31{%- elif name == 'check' -%}<path d="M20 6 9 17l-5-5"/>32{%- elif name == 'download' -%}<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>33{%- elif name == 'external' -%}<path d="M7 7h10v10"/><path d="M7 17 17 7"/>34{%- elif name == 'chevron-right' -%}<path d="m9 18 6-6-6-6"/>35{%- elif name == 'chevron-down' -%}<path d="m6 9 6 6 6-6"/>36{%- elif name == 'rss' -%}<path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/>37{%- elif name == 'terminal' -%}<path d="M12 19h8"/><path d="m4 17 6-6-6-6"/>38{%- elif name == 'book' -%}<path d="M12 5v16"/><path d="M20.001 19A2 2 0 0022 17V5a2 2 0 00-1.999-2L16 3.002A5 5 0 0012 5a5 5 0 00-4-2H4a2 2 0 00-2 2v12a2 2 0 001.999 2H8a5 5 0 014 2 5 5 0 014-2z"/>39{%- elif name == 'beaker' -%}<path d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"/><path d="M6.453 15h11.094"/><path d="M8.5 2h7"/>40{%- elif name == 'sparkle' -%}<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>41{%- elif name == 'history' -%}<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>42{%- elif name == 'graph' -%}<path d="M5 21v-6"/><path d="M12 21V3"/><path d="M19 21V9"/>43{%- elif name == 'home' -%}<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>44{%- elif name == 'pin' -%}<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>45{%- elif name == 'info' -%}<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>46{%- elif name == 'image' -%}<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>47{%- elif name == 'wrap' -%}<path d="m16 16-3 3 3 3"/><path d="M3 12h14.5a1 1 0 0 1 0 7H13"/><path d="M3 19h6"/><path d="M3 5h18"/>48{%- elif name == 'arrow-left' -%}<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>49{%- elif name == 'arrow-right' -%}<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>50{%- elif name == 'mail' -%}<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"/><rect x="2" y="4" width="20" height="16" rx="2"/>51{%- elif name == 'archive' -%}<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/>52{%- elif name == 'scale' -%}<path d="M12 3v18"/><path d="m19 8 3 8a5 5 0 0 1-6 0zV7"/><path d="M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1"/><path d="m5 8 3 8a5 5 0 0 1-6 0zV7"/><path d="M7 21h10"/>53{%- elif name == 'clock' -%}<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>54{%- elif name == 'calendar' -%}<path d="M8 2v3"/><path d="M16 2v3"/><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M8 13h.01"/><path d="M12 13h.01"/><path d="M16 13h.01"/><path d="M8 17h.01"/><path d="M12 17h.01"/><path d="M16 17h.01"/>55{%- elif name == 'disk' -%}<path d="M10 16h.01"/><path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M21.946 12.013H2.054"/><path d="M6 16h.01"/>56{%- elif name == 'users' -%}<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/>57{%- elif name == 'braces' -%}<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"/>58{%- elif name == 'link' -%}<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>59{%- elif name == 'columns' -%}<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>60{%- elif name == 'rows' -%}<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 12h18"/>61{%- elif name == 'tree' -%}<path d="M8 5h13"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="M3 10a2 2 0 0 0 2 2h3"/><path d="M3 5v12a2 2 0 0 0 2 2h3"/>62{%- elif name == 'alert' -%}<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>63{%- elif name == 'enter' -%}<path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/>64{%- elif name == 'hash' -%}<line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/>65{%- elif name == 'activity' -%}<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>66{%- elif name == 'refresh' -%}<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>67{%- elif name == 'globe' -%}<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>68{%- elif name == 'filter' -%}<path d="M2 5h20"/><path d="M6 12h12"/><path d="M9 19h6"/>69{%- elif name == 'sort' -%}<path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="m21 8-4-4-4 4"/><path d="M17 4v16"/>70{%- elif name == 'x-circle' -%}<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>71{%- elif name == 'file-pdf' -%}<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M11 18h2"/><path d="M12 12v6"/><path d="M9 13v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5"/>72{%- elif name == 'layers' -%}<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/>73{%- else -%}<circle cx="12" cy="12" r="3"/>74{%- endif -%}75</svg>76{%- endmacro %}7778{#- Square mono tag. kind ∈ '', 'topic', 'ok', 'warn', 'danger', 'info', 'neutral', 'accent', 'ember' -#}79{% macro chip(label, kind='', href=null, title='') -%}80{%- if href -%}<a class="chip chip-{{ kind or 'neutral' }}" href="{{ href }}"{% if title %} title="{{ title }}"{% endif %}>{{ label }}</a>81{%- else -%}<span class="chip chip-{{ kind or 'neutral' }}"{% if title %} title="{{ title }}"{% endif %}>{{ label }}</span>{%- endif -%}82{%- endmacro %}8384{#- Ruled figure row: numbered marker, serif value, mono caption. items: [{value, label, href, title, sub}] -#}85{% macro statStrip(items, cls='') -%}86<ul class="stat-strip {{ cls }}" aria-label="Key figures">87  {%- for s in items -%}88  <li class="stat-item"{% if s.title %} title="{{ s.title }}"{% endif %}>89    {%- if s.href -%}<a class="stat-link" href="{{ s.href }}">{%- endif -%}90    <span class="stat-no" aria-hidden="true">{{ '0' if loop.index < 10 }}{{ loop.index }}</span>91    <span class="stat-value">{{ s.value }}</span>92    <span class="stat-label">{{ s.label }}{% if s.sub %} <span class="stat-sub">· {{ s.sub }}</span>{% endif %}</span>93    {%- if s.href -%}</a>{%- endif -%}94  </li>95  {%- endfor -%}96</ul>97{%- endmacro %}9899{#- Section opener: short rule + numbered mono eyebrow, serif title, optional lede. Trailing controls go in the caller block. -#}100{% macro sectionHead(id, no, eyebrow, title, sub='', count=null, countId='') -%}101<header class="section-head">102  <div class="section-head-text">103    <p class="eyebrow"><span class="eyebrow-rule" aria-hidden="true"></span>{% if no %}<span class="eyebrow-no">{{ no }}</span>{% endif %}{{ eyebrow }}</p>104    <h2 id="{{ id }}" class="section-title">{{ title }}{% if count !== null %} <span class="section-count"{% if countId %} id="{{ countId }}"{% endif %}>{{ count }}</span>{% endif %}</h2>105    {% if sub %}<p class="section-sub">{{ sub }}</p>{% endif %}106  </div>107  {{ caller() if caller }}108</header>109{%- endmacro %}110111{#- Horizontal tab strip. items: [{key, label, href, count, icon}] -#}112{% macro tabs(items, active, label='Sections') -%}113<nav class="tabs" aria-label="{{ label }}">114  <div class="tabs-track" tabindex="0">115  {%- for t in items -%}116  <a class="tab {{ 'is-active' if t.key == active }}" href="{{ t.href }}"{% if t.key == active %} aria-current="page"{% endif %}>117    <span>{{ t.label }}</span>{% if t.count is defined and t.count !== null %}<span class="tab-count">{{ t.count }}</span>{% endif %}118  </a>119  {%- endfor -%}120  </div>121</nav>122{%- endmacro %}123124{#- Path breadcrumbs. crumbs: [{name, href}] (last = current) -#}125{% macro breadcrumbs(rootLabel, rootHref, crumbs, label='Path') -%}126<nav class="crumbs" aria-label="{{ label }}">127  <ol>128    <li><a href="{{ rootHref }}">{{ rootLabel }}</a></li>129    {%- for c in crumbs -%}130    <li>{% if loop.last %}<span aria-current="page">{{ c.name }}</span>{% elif c.href %}<a href="{{ c.href }}">{{ c.name }}</a>{% else %}<span>{{ c.name }}</span>{% endif %}</li>131    {%- endfor -%}132  </ol>133</nav>134{%- endmacro %}135136{#- Branch / tag picker (native select for reliability on iOS) -#}137{% macro refSelect(repo, branches, tags, currentRef, kind='tree', path='') -%}138<label class="ref-select-wrap">139  <span class="sr-only">Branch or tag</span>140  {{ icon('branch', 14, 'ref-select-icon') }}141  <select id="ref-select" class="ref-select" data-repo="{{ repo }}" data-kind="{{ kind }}" data-path="{{ path }}" aria-label="Switch branch or tag">142    <optgroup label="Branches">143    {%- for b in branches -%}144    <option value="{{ b.name }}" {{ 'selected' if b.name == currentRef }}>{{ b.name }}{{ ' (default)' if b.isDefault }}</option>145    {%- endfor -%}146    </optgroup>147    {%- if tags.length -%}148    <optgroup label="Tags">149    {%- for t in tags -%}150    <option value="{{ t.name }}" {{ 'selected' if t.name == currentRef }}>{{ t.name }}</option>151    {%- endfor -%}152    </optgroup>153    {%- endif -%}154  </select>155  {{ icon('chevron-down', 14, 'ref-select-caret') }}156</label>157{%- endmacro %}158159{% macro emptyState(title, hint='', snippet='', iconName='folder') -%}160<div class="empty-state">161  <div class="empty-icon">{{ icon(iconName, 20) }}</div>162  <p class="empty-title">{{ title }}</p>163  {% if hint %}<p class="empty-hint">{{ hint }}</p>{% endif %}164  {% if snippet %}<pre class="snippet" tabindex="0">{{ snippet }}</pre>{% endif %}165  {{ caller() if caller }}166</div>167{%- endmacro %}168169{% macro errorState(title, message='') -%}170<div class="error-state" role="alert">171  <div class="empty-icon tone-danger">{{ icon('alert', 20) }}</div>172  <p class="empty-title">{{ title }}</p>173  {% if message %}<p class="empty-hint">{{ message }}</p>{% endif %}174</div>175{%- endmacro %}176177{#- Loading skeleton (used by client-side panels: palette, search, AI) -#}178{% macro skeleton(lines=3, cls='') -%}179<div class="skeleton {{ cls }}" aria-hidden="true">180  {%- for i in range(0, lines) -%}<span class="skeleton-line" style="width:{{ [92, 68, 80, 55, 74][i % 5] }}%"></span>{%- endfor -%}181</div>182{%- endmacro %}183184{#- Copy-to-clipboard button (44px target on touch) -#}185{% macro copyButton(text, label='Copy', title='Copy to clipboard', cls='') -%}186<button type="button" class="btn btn-sm copy-btn {{ cls }}" data-copy="{{ text }}" title="{{ title }}" aria-label="{{ title }}">{{ icon('copy', 14) }}<span class="copy-label">{{ label }}</span></button>187{%- endmacro %}188189{#- <time> with a relative label and the exact instant in the tooltip -#}190{% macro when(iso, prefix='') -%}191<time datetime="{{ iso }}" title="{{ iso | dateexact }}">{{ prefix }}{{ iso | reltime }}</time>192{%- endmacro %}193
Files main
Menu

Theme