Board: every card inlines its own copy of eleven status icons; reference them from one sprite #1151
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1151
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
templates/applications/partials/application_card.html:19-35: each card carries a<c-dropdown-menu>whose panel is a form with onemenuitemradiosubmit button per status (applications/models.py:72-89, eleven statuses), each with{% icon icon %}, plus the pencil trigger and a check on the current row.{% icon %}(core/templatetags/postulo.py:78-98) inlines the whole Lucide SVG each time; the source is cached per process (_icon_source, line 49) but the bytes are repeated in the page.Measured from the icon files with the tag's own trimming: the eleven status icons with their attributes are 4,723 bytes; the menu per card is about 7,800 bytes. The board is not paginated (
applications/views.py:221-223: "A board is every card at once"), so the cost is linear in the open columns' cards. Thirty cards' menus are 165 kB raw and 2 kB gzip: on the wire the repetition compresses away when something compresses the HTML (A6), but the browser still parses and lays out every copy.Correction to the ledger: "same per table row" does not hold. Only the board card (
partials/board.html:207) draws the status menu; the table and the dashboard's gone-quiet widget (widgets/gone_quiet.html:50-53, one hidden status) do not.What would change
scripts/sync-icons.mjsintostatic/icons/sprite.svgbeside the single files it already copies, committed and checked by the CI step that already checksstatic/icons.{% icon %}gains a sprite form, for example{% icon "send" sprite=True %}, that emits<svg class=... aria-hidden="true" data-icon="send"><use href="{% static 'icons/sprite.svg' %}#send"/></svg>. Same origin; the policy isdefault-src 'none'withimg-src 'self' data:(config/settings/base.py:860-872), and which directive a browser applies to an external<use>must be confirmed; the browser suite runs under the policy and fails on a violation (#232), so it will say. The hashed name from the manifest keeps it cached for good.stroke="currentColor", width, caps) move to the<symbol>or stay on the outer<svg>, so forced colours and dark mode paint them as today.application_card.htmluses the sprite form for its menu rows. Other icons stay inline; this is for icons repeated per row.No library is needed: an SVG sprite is plain markup;
svgstore-style tools exist but the existing sync script can write the<symbol>elements.What it saves or protects
medium: about 4.7 kB of inline SVG per card (of a 7.8 kB status menu) and some 40 SVG elements per card leave the DOM; on an unpaginated board of 100 cards, about 470 kB of HTML when it is sent uncompressed (A6)
Accessibility
must be checked: every icon stays
aria-hidden="true"and the row's name stays its text; icons keepcurrentColorunder forced colours; the menu still opens and moves a card with scripts off. Guarded bytests/e2e/test_card_menu.py(keyboard, scripts off),tests/e2e/test_rendering_modes.py(forced colours, buttons only, not the menu icons),tests/test_icons.py(aria-hidden,data-icon)Decisions
<svg hidden>block per page? Recommended: external file; an inline block would have to be present in every htmx fragment's host page and is sent with every page.Found by the efficiency and reliability audit of 2026-10-30 (finding A5; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).