{# ============================================================================= Plan 60/61 — Apple HIG component macros. Phase 2 agents compose pages from these primitives. Visual tokens live in static/css/tailwind.input.css; this file just stamps consistent markup. Quick reference: button(text, kind='primary', size='md', icon='', type='button', attrs='', extra_class='') badge(text, kind='neutral', icon='') input(name, label='', type='text', value='', placeholder='', required=False, attrs='') select(name, label='', options=[], value='', required=False, attrs='') textarea(name, label='', value='', placeholder='', rows=3, required=False, attrs='') card(title='', subtitle='', kind='default', extra_class='', footer='') page_header(title, subtitle='', actions=None, eyebrow='', help_anchor='', help_label='', kebab_items=None, step=None, step_total=None) section_header(title, action_label='', action_href='', help='') stat(label, value, hint='', delta='', delta_kind='neutral') empty_state(title, description, icon='', icon_path='', action_label='', action_href='') pagination(state, per_page_options=[25, 50, 100], extra_class='') modal(show, close, width='max-w-md', padding='p-6') drawer(show, close, width='max-w-2xl') tooltip(label, position='top', icon='circle-help', extra_class='') ← Plan 61/2a help_popover(title, body_html, position='bottom') ← Plan 61/2a Backwards-compatible aliases retained on `button` and `badge` so the pre-Plan-60 (variant=…) callers keep working. Don't remove until Phase 2 has rewritten every page that still uses the legacy spelling. Plan 61/2a additions: - `tooltip()` — inline hover/focus help icon for jargon (≤120 chars). - `help_popover()` — click-triggered popover for longer copy. - `page_header()` — `help_anchor` adds a `?` icon next to the H1 that links to `/admin/help/`; `kebab_items` adds a "…" menu for secondary actions; `step`/`step_total` render a "Step N of M" eyebrow (wizard chrome — used by Wave 2b add-machine/setup wizards). ============================================================================= #} {# ─────────────────────── button ─────────────────────── kinds: primary / secondary / ghost / danger sizes: sm (28px) / md (36px) / lg (40px) Legacy alias: variant=… still accepted (mapped to kind). #} {% macro button(text, kind='primary', size='md', icon='', type='button', attrs='', extra_class='', variant=None) %} {% set _kind = variant if variant else kind %} {% set _kind = {'default': 'secondary'}.get(_kind, _kind) %} {% endmacro %} {# ─────────────────────── badge ─────────────────────── kinds: success / warning / danger / info / neutral Legacy variants (default/green/blue/yellow/red) map onto the new kinds so existing call sites keep rendering. #} {% macro badge(text, kind='neutral', icon='', variant=None) %} {% set _alias = { 'default': 'neutral', 'green': 'success', 'blue': 'info', 'yellow': 'warning', 'amber': 'warning', 'red': 'danger', } %} {% set _kind = _alias.get(variant or kind, variant or kind) %} {% if icon %}{% endif %} {{ text }} {% endmacro %} {# ─────────────────────── input ─────────────────────── #} {% macro input(name, label='', type='text', value='', placeholder='', required=False, attrs='', help='') %}
{% if label %} {% endif %} {% if help %}

{{ help }}

{% endif %}
{% endmacro %} {# ─────────────────────── select ─────────────────────── #} {% macro select(name, label='', options=[], value='', required=False, attrs='') %}
{% if label %} {% endif %}
{% endmacro %} {# ─────────────────────── textarea ─────────────────────── #} {% macro textarea(name, label='', value='', placeholder='', rows=3, required=False, attrs='') %}
{% if label %} {% endif %}
{% endmacro %} {# ─────────────────────── card ─────────────────────── kinds: default / elevated / compact / roomy - default: padded with section header + body - compact: tighter padding (12px) for dense lists / side panels - roomy: page-level container (24px) — wraps tables, hero content - elevated: adds soft mac-style shadow on top of default sizing #} {% macro card(title='', subtitle='', kind='default', extra_class='', footer='') %} {% set _is_compact = kind == 'compact' %} {% set _is_roomy = kind == 'roomy' %} {% set _is_elev = kind == 'elevated' %} {% set _body_pad = 'px-3 py-3' if _is_compact else ('px-6 py-5' if _is_roomy else 'px-5 py-4') %} {% set _head_pad = 'px-3 py-2.5' if _is_compact else ('px-6 py-4' if _is_roomy else 'px-5 py-4') %}
{% if title %}

{{ title }}

{% if subtitle %}

{{ subtitle }}

{% endif %}
{% endif %}
{{ caller() }}
{% if footer %}
{{ footer|safe }}
{% endif %}
{% endmacro %} {# ─────────────────────── page_header ─────────────────────── Top-of-page title + subtitle + right-aligned action buttons. `actions` is a list of dicts: [{'label': 'Refresh', 'kind': 'secondary', 'icon': 'refresh-cw', 'attrs': '@click="load()"'}, {'label': '+ New fleet', 'kind': 'primary', 'attrs': '@click="open=true"'}] `eyebrow` is the tiny label above the title (e.g. "Workspace"). Plan 61/2a extensions: - `help_anchor` (str) — slug under `/admin/help/`. Renders a small `circle-help` icon next to the H1; click opens the doc in a new tab. Empty string = no icon. - `help_label` (str) — aria-label override; defaults to "Help for this page". - `kebab_items` (list[dict]) — secondary actions hidden under a "more-horizontal" menu. Each item: `{'label', 'href', 'icon'}`. - `step`, `step_total` (int) — when both set, render a "Step N of M" eyebrow line. Wave 2b wizards (add-machine / setup) use this. #} {% macro page_header(title, subtitle='', actions=None, eyebrow='', help_anchor='', help_label='', kebab_items=None, step=None, step_total=None) %}
{% if step is not none and step_total is not none %}

Step {{ step }} of {{ step_total }} {% if eyebrow %} · {{ eyebrow }}{% endif %}

{% elif eyebrow %}

{{ eyebrow }}

{% endif %}

{{ title }}

{% if help_anchor %} {% endif %}
{% if subtitle %}

{{ subtitle }}

{% endif %}
{% if actions or kebab_items %}
{% if actions %} {% for a in actions %} {% if a.href %} {% if a.icon %}{% endif %} {{ a.label }} {% else %} {% endif %} {% endfor %} {% endif %} {% if kebab_items %}
{% endif %}
{% endif %}
{% endmacro %} {# ─────────────────────── section_header ─────────────────────── Smaller header for sub-sections inside a page (e.g. "Recent activity"). Optional inline action (text link with right-chevron). #} {% macro section_header(title, action_label='', action_href='', help='') %}

{{ title }}

{% if help %}

{{ help }}

{% endif %}
{% if action_label %} {{ action_label }} {% endif %}
{% endmacro %} {# ─────────────────────── stat ─────────────────────── KPI tile: label + big tabular-number value + optional hint + delta chip. delta_kind: neutral / success / warning / danger. #} {% macro stat(label, value, hint='', delta='', delta_kind='neutral') %}

{{ label }}

{{ value }} {% if delta %} {{ delta }} {% endif %}
{% if hint %}

{{ hint }}

{% endif %}
{% endmacro %} {# ─────────────────────── empty_state ─────────────────────── Centered illustration for empty tables / lists. Either `icon` (lucide name) or `icon_path` (raw SVG path). Optional CTA. #} {% macro empty_state(title, description, icon='', icon_path='', action_label='', action_href='') %}
{% if icon %} {% elif icon_path %} {% endif %}

{{ title }}

{{ description }}

{% if action_label %} {{ action_label }} {% endif %}
{% endmacro %} {# ─────────────────────── drawer ─────────────────────── Plan 50 — right-side drawer chrome. Teleports to so `fixed inset-0` is anchored to the viewport, not to a transformed ancestor. `show` is the Alpine expression that controls visibility (e.g. `drawer.open`); `close` is the JS run on backdrop click + ESC. #} {% macro drawer(show, close, width='max-w-2xl') %} {% endmacro %} {# ─────────────────────── pagination ─────────────────────── Plan 60 Phase 1.5 — universal pagination control. Renders an offset-mode bar (Prev | Page X of N | Next + per-page selector + "Items 51-100 of 829") OR a cursor-mode "Load more" button depending on `state.mode`. `state` is an Alpine reactive object built by `paginationState({...})` in `static/admin/shared/pagination-state.js`. Page-level components instantiate it, the macro just stamps the controls. When `state.total === 0` the macro renders nothing — the parent is expected to show an `empty_state` instead. Args: state — Alpine expression naming the pagination state object (e.g. "pagination" if the component holds it as `this.pagination`). per_page_options — [25, 50, 100] by default. extra_class — extra Tailwind classes on the outer container. #} {% macro pagination(state, per_page_options=[25, 50, 100], extra_class='') %}
{# ── Offset mode ── #} {# ── Cursor mode ── #}
{% endmacro %} {# ─────────────────────── modal ─────────────────────── Plan 50/60 — centered modal chrome. Same teleport+backdrop pattern as `drawer()`. The inner card uses .modal-card for the lifted shadow + pop-in animation. #} {% macro modal(show, close, width='max-w-md', padding='p-6') %} {% endmacro %} {# ─────────────────────── tooltip ─────────────────────── Plan 61/2a — inline help affordance for jargon labels. `label` is plain text (cap at ~120 chars; use help_popover() for anything longer). Renders a small lucide icon; hover/focus shows a non-portal popover via .cmdop-tooltip CSS (tailwind.input.css). `position` is informational only (the CSS positions above; we leave the data attribute in case a future Alpine handler portals it). `icon` lets a caller swap "circle-help" for "info" when the affordance is informational rather than a question. #} {% macro tooltip(label, position='top', icon='circle-help', extra_class='') %} {{ label }} {% endmacro %} {# ─────────────────────── info_row ─────────────────────── Plan 61/2b/W1 — single label-value line for the Machine Info tab. Renders a small staleness dot (green/amber/gray) when `stale` is one of those keys; `hint` is an optional tooltip-style suffix. #} {% macro info_row(label, value, hint='', stale='') %}
{% if stale %} {% set _dot = 'bg-emerald-400' if stale == 'fresh' else ('bg-amber-300' if stale == 'stale' else 'bg-ink-500') %} {% endif %} {{ label }}
{{ value|safe }}{% if hint %} · {{ hint }}{% endif %}
{% endmacro %} {# ─────────────────────── help_popover ─────────────────────── Plan 61/2a — click-triggered popover for longer help copy. `title` is the popover heading; `body_html` is rendered as raw HTML (Wave 2b passes pre-rendered markdown or includes a Jinja partial). `position` controls placement on screen ("bottom" | "top" | "right"). Use this when tooltip() would exceed ~120 chars or when the help needs links / paragraphs. #} {% macro help_popover(title, body_html, position='bottom') %} {% endmacro %} {# ─────────────────────── collapsible_section ─────────────────────── A card whose body collapses behind a clickable header — for secondary blocks that shouldn't dominate the page but must stay reachable. Usage: {% call collapsible_section('Recent activity', key='overview.activity', subtitle='last 15 events', open=False, right_html='View all') %} …body… {% endcall %} - `key` (required) — localStorage slug so the open/closed choice sticks across loads (`cmdop:collapse:`). - `open` — default state on first visit (before any saved choice). - `right_html` — optional markup pinned to the right of the header (e.g. a "View all" link or a count badge); clicks there don't toggle. - `icon` — optional lucide icon name shown before the title. Uses @alpinejs/collapse (loaded in base.html) for the height animation. #} {% macro collapsible_section(title, key='', subtitle='', open=False, right_html='', icon='', extra_class='') %}
{% if right_html %}
{{ right_html|safe }}
{% endif %}
{{ caller() }}
{% endmacro %}