Board: every card inlines its own copy of eleven status icons; reference them from one sprite #1151

Open
opened 2026-10-31 03:04:22 +00:00 by tiagoagueda · 0 comments
Owner

What exists today

templates/applications/partials/application_card.html:19-35: each card carries a <c-dropdown-menu> whose panel is a form with one menuitemradio submit 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

  • A sprite of the icons that repeat (the status icons, check, pencil), built by scripts/sync-icons.mjs into static/icons/sprite.svg beside the single files it already copies, committed and checked by the CI step that already checks static/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 is default-src 'none' with img-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.
  • The stroke attributes Lucide sets on the root (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.html uses 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 keep currentColor under forced colours; the menu still opens and moves a card with scripts off. Guarded by tests/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

  • Sprite only the status menu, or every icon? Recommended: start with icons drawn once per row (status menu, row actions), measure, then decide; a single-use icon gains nothing from a second request.
  • External sprite file or an inline <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 main at daee30b15 (2026-10-30).

## What exists today `templates/applications/partials/application_card.html:19-35`: each card carries a `<c-dropdown-menu>` whose panel is a form with one `menuitemradio` submit 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 - A sprite of the icons that repeat (the status icons, check, pencil), built by `scripts/sync-icons.mjs` into `static/icons/sprite.svg` beside the single files it already copies, committed and checked by the CI step that already checks `static/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 is `default-src 'none'` with `img-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. - The stroke attributes Lucide sets on the root (`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.html` uses 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 keep `currentColor` under forced colours; the menu still opens and moves a card with scripts off. Guarded by `tests/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 - **Sprite only the status menu, or every icon?** Recommended: start with icons drawn once per row (status menu, row actions), measure, then decide; a single-use icon gains nothing from a second request. - **External sprite file or an inline `<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 `main` at `daee30b15` (2026-10-30).
tiagoagueda added this to the 1.0.0 milestone 2026-10-31 03:04:22 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#1151
No description provided.