-
Notifications
You must be signed in to change notification settings - Fork 2
Brand Table Standard
Single source of truth for how every list table (full-page *_list.html, the
HTMX *_table_body.html partials, and detail-page sub-tables) must look and behave.
Derived from the canonical examples (assets/essential_asset_list.html,
assets/dependency_list.html). Follow it exactly so every table in the app shares
the same appearance, columns and ergonomics, in both light and dark themes.
{% extends "base.html" %}
{% load i18n ui help_tags table_tags workflow_tags %} {# + entity-specific libs #}
{% block title %}{% trans "<Plural entity>" %} - Cairn{% endblock %}
{% block content %}
{% page_header _("<Plural entity>") eyebrow=_("<Module>") accent="<module-accent>" %}
{# primary action button(s), permission-gated, in the page header #}
{% endpage_header %}
{% help_modal "<app>.<entity>_list" %}
{# OPTIONAL filter chips - keep when the list already had them #}
{% include "includes/table_search.html" %}
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead> <tr> …columns… </tr> </thead>
<tbody>
{% for obj in objects %}
<tr> …cells… </tr>
{% empty %}
{% empty_state title=_("…") message=_("…") colspan=N %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% include "includes/pagination.html" %}
{% endblock %}-
Card wrapper, always.
<div class="card"><div class="table-responsive"><table class="table table-hover mb-0">. Never a raw<table>without the card, never addalign-middle(the base stylesheet already vertical-centers cells). -
Search toolbar, always.
{% include "includes/table_search.html" %}directly above the card on every list page. -
Pagination, always.
{% include "includes/pagination.html" %}as the last line before{% endblock %}. -
Reference column = clickable pill.
<td><a href="{detail url}" class="ref">{{ obj.reference }}</a></td>. Header:{% sortable_th "reference" "Ref." %}(label is alwaysRef., neverReference). Do not addstyle="text-decoration:none"-a.refalready handles it. If a row has no reference, omit the column entirely (don't fake one). -
Name column = clickable accent link.
<td><a href="{detail url}" class="cell-link">{{ obj.name }}</a></td>. Header:{% sortable_th "name" "Name" %}. -
Cross-references to another record use
class="cell-link"too (e.g. an essential asset linked from a dependency row). Never reuse the old inlinestyle="color:var(--accent);text-decoration:none;font-weight:500"- replace every occurrence withclass="cell-link". -
Empty-cell placeholder = ASCII hyphen.
{{ value|default:"-" }}. Never the em-dash character (U+2014): use a plain-for every empty-celldefault:"..."filter and for any bare empty-cell marker inside a<td>. -
Status column (workflow entities):
<td>{% workflow_badge obj %}</td>, header{% sortable_th "workflow_state" "Status" %}. -
Tags column (entities with tags): second-to-last column.
Header
<th>{% trans "Tags" %}</th>, cell<td>{% include "includes/tags_badge.html" with tags=obj.tags.all %}</td>. Replace any inline{% for tag in … %}<span class="badge" …>…</span>{% endfor %}loop with that include. -
Actions column = always last, right-aligned.
Header
<th class="text-end">{% trans "Actions" %}</th>, cell<td class="text-end">.- Edit:
class="btn btn-sm btn-outline-primary" title="{% trans 'Edit' %}", icon<i class="bi bi-pencil"></i>. - Delete (only when the entity is deletable):
class="btn btn-sm btn-outline-danger" title="{% trans 'Delete' %}", icon<i class="bi bi-trash"></i>. -
Keep the existing edit mechanism: HTMX drawer (
hx-get→#drawer-form-content) where the list already uses it, full-pagehrefotherwise. Keep existing permission gates ({% has_perm %}). - Lists currently missing an Actions column must gain one.
- Edit:
-
colspan in
{% empty_state %}must equal the exact number of<th>columns. -
i18n: every visible string wrapped in
{% trans %}/_(). Do not edit anylocale/*.pofile in this pass - instead report any new English string you introduce so it can be translated centrally.
By default a cell is a single value (rules 4-10). When a row carries several closely related fields, group them into one cell on two lines instead of spending a column on each : a primary line over a muted secondary line. This keeps wide entities (people, contacts) scannable and the table narrow.
Use the shared helpers (defined in base.html, theme-aware in light/dark) :
-
.cell-stack: the two-line wrapper (a flex column, tightline-height). -
.cell-sub: the muted, smaller secondary line (var(--text-muted),.8125rem). A.cell-linkplaced directly in a.cell-stackis automatically bold (600).
Line-length rule (enforced by the helpers, do not override):
- The primary line is always a single line : it never wraps and is truncated
with an ellipsis (
.cell-stack > :not(.cell-sub)). - The secondary line is at most two lines : longer text is clamped with an
ellipsis (
.cell-stack > .cell-sub, except horizontald-inline-flexsub-lines which stay on one line). - Do not pre-truncate the text with
|truncatechars/|truncatewordsto fit - let the CSS clamp it, and add atitle="{{ full_value }}"so the full text is available on hover. The stack is also width-capped (max-width) so long values truncate instead of widening the column.
<td>
<span class="cell-stack">
<span>{{ obj.job_title }}</span>
{% if obj.department %}<span class="cell-sub">{{ obj.department }}</span>{% endif %}
</span>
</td>People identity (avatar + name + secondary line) uses .cell-people :
<td>
<a href="{detail url}" class="cell-people">
{% if u.avatar %}
<img src="{{ u.avatar_64|default:u.avatar }}" alt="" class="cell-avatar">
{% else %}
<span class="cell-avatar-fallback">{{ u.display_name|initials }}</span>
{% endif %}
<span class="cell-stack">
<span class="cell-link">{{ u.display_name }}</span>
<span class="cell-sub">{{ u.email }}</span>
</span>
</a>
</td>Rules for two-line / people cells :
- The avatar is 40px (
.cell-avatar/.cell-avatar-fallback), sized to align with the two-line text block. Do not hand-roll avatar markup or inline sizes ; use the helpers (or{% user_badge %}for a single-line avatar+name elsewhere). - The whole people cell is a single link to the detail page (
<a class="cell-people">), not just the name. - Keep the secondary line secondary : one muted field only, never a second link or a badge. Status, tags and actions stay in their own columns (rules 8-10).
- Fold the secondary field's standalone column and its sort header away when you
merge it (e.g. merging Email into Name drops the Email
sortable_th). The primary field stays sortable. Note the dropped sort in the PR description. - Graceful fallback when the primary field is empty : promote the secondary field to
the single line, or render
<span class="cell-empty">-</span>if both are empty. - Still no
align-middle(rule 1) : the base stylesheet centres cells, and the helpers keep the two lines tight on their own. - Canonical example :
accounts/user_list.html.
Use the shared {% progress_bar %} tag (core.templatetags.ui) for every
percentage bar in a list table - never hand-roll the .progress markup. It
renders a 1rem-tall bar with the percentage inside, a 60px min width, and
automatic text contrast (dark, semibold text on the light info / warning
bars; white on success / danger).
{% load ui %}
{% progress_bar obj.progress_percentage scheme="progress" %} {# completion #}
{% progress_bar fw.compliance_level scheme="score" %} {# quality score #}
{% progress_bar a.coverage_pct variant="info" count=a.covered_count total=a.applicable_count %}-
scheme="progress"(default):>=100success,>=50info, else warning. Use for completion/advancement (objectives, treatment plans, action plans). -
scheme="score":>=80success,>=50warning, else danger. Use for a quality rating (compliance level). -
variant="…"forces a fixed colour (e.g. coverage is alwaysinfo). -
count/total(ortitle) set the hover tooltip (e.g.82/89); prefer a tooltip over a second visible line for the underlying counts.
Some lists render the <tbody> from a *_table_body.html partial loaded via HTMX.
In that case rules 4-11 about the cells apply to the partial; rules 1-3 and the
<thead> live in the *_list.html. Both files must stay column-aligned.
- View/Python logic, URLs, sortable-field definitions.
- Per-list density tweaks already present (e.g.
style="font-size:.85rem"on dense numeric cells) - leave them. -
base.htmlandlocale/*.po(edited centrally). - The
permission_list.htmlcard/badge layout (intentionally not a table).
Built from docs/ at v0.36.0. Edits made here are overwritten by the next release : open a pull request against the source instead.
- Administration
- Ask Cairn
- Assets and suppliers
- Compliance
- The dashboard
- Finding your way
- Getting started
- Incidents
- How records move
- Organisational context
- Reports and management review
- Risks
- Trust Center
- Architecture
- Configuration
- Contributing
- The documentation system
- Installation
- Internationalisation
- Operations
- Release process
- Security
- Testing
- Adding an assistant provider
- Adding a dashboard widget
- Adding a domain entity
- Declaring a lifecycle
- Adding an MCP tool
- Adding a REST endpoint
- Adding a report
- Interface conventions
- Dashboard widgets
- Lifecycles
- MCP tools
- MCP tool parameters : Assets
- MCP tool parameters : Compliance
- MCP tool parameters : Governance and context
- MCP tool parameters : General
- MCP tool parameters : Incidents
- MCP tool parameters : Reports and management review
- MCP tool parameters : Risks
- MCP tool parameters : System and administration
- MCP tool parameters : Trust Center
- Management commands
- Models
- Permissions
- REST endpoints
- Environment variables
- MCP server
- REST API
- Assistant module (Ask Cairn)
- Module 0: User Management and Access Control
- Module 1: Context and Organization
- Module 2: Asset Management
- Module 3: Compliance
- Module 4: Risk Management
- Module 4 bis - EBIOS Risk Manager
- Module 5 : Trust Center
- Module 6 : Security Incident Management
- Management review : ISO 27001:2022 compliance (clause 9.3)