feat(ui): slim click-to-filter stat strip on Tasks (reference)#157
Conversation
Replaces the tall stat-card grid at the top of Tasks with a slim, always-visible StatStrip whose chips are click-to-filter — the list gets the vertical space back while counts + filtering stay glanceable. New shared components/StatStrip.vue so the other registers can adopt the same treatment. Part of #156 (Tasks is the reference; other registers to follow).
unidoc-alip
left a comment
There was a problem hiding this comment.
Clean, low-risk UI refactor. StatStrip.vue cleanly extracts the click-to-filter chip pattern from the old 5-card grid with a minimal, well-typed prop contract (stats + v-model). Tasks.vue's swap is 1:1 — no data/logic changes, same statusStats/filterStatus wiring, no XSS/security concerns (no v-html, all interpolation is auto-escaped). The color convention (text-* classes) already matches other registers (e.g. Changes.vue), so it should generalize well to the planned follow-up PRs for the other registers. Both findings below are non-blocking nits aimed at hardening this component before it becomes the shared reference for ~10 more register rollouts.
1. [nit] web/src/components/StatStrip.vue line 20
No runtime shape validation on stats items (key/label/count/color) — a follow-up register PR could pass an item missing count or using a bg-*/dot color convention (like Dashboard.vue's activity types) instead of text-*, and it'd silently render wrong rather than fail loudly. Confirmed against real precedent: Dashboard.vue:659-663 already uses color: 'bg-purple-900/40 text-purple-300' paired with a separate dot field, a different convention from the plain text-* string this component expects — while Changes.vue:496-501 does match. So a register PR modeled on Dashboard's stats shape instead of Changes.vue's would silently mis-style rather than error.
Suggested fix — add a dev-time prop validator so a mismatch fails loudly instead of silently mis-rendering:
defineProps({
stats: {
type: Array,
required: true,
validator: (arr) => arr.every(s =>
typeof s.key === 'string' &&
typeof s.label === 'string' &&
typeof s.count === 'number' &&
(s.color === undefined || /^text-/.test(s.color))
),
},
modelValue: { type: String, default: '' },
})Vue logs a console warning in dev builds when a validator fails, catching a future register PR passing a bg-*/dot-style color before it ships. Pair with the existing // color is a text-* class for the count comment so the constraint is documented in both places.
2. [nit] web/src/components/StatStrip.vue line 8
The <button> has no aria-* attribute — active/filtered state is conveyed purely via the border/background/text-color class binding. Screen-reader users get no indication a chip is the active filter beyond the static title tooltip, which doesn't change when active.
Suggested fix — bind aria-pressed to the same active-state expression already used for styling, and make the tooltip reflect the toggle:
<button
v-for="s in stats"
:key="s.key"
type="button"
:aria-pressed="modelValue === s.key"
@click="$emit('update:modelValue', modelValue === s.key ? '' : s.key)"
class="inline-flex items-baseline gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors"
:class="modelValue === s.key
? 'border-blue-500/50 bg-blue-500/10 text-blue-200'
: 'border-slate-800 bg-slate-900 text-slate-400 hover:border-slate-700 hover:text-slate-300'"
:title="modelValue === s.key ? `Clear filter: ${s.label}` : `Filter: ${s.label}`">Since this is slated to become the shared pattern for every other register, landing this in the reference component now avoids repeating the fix in each of the ~10 follow-up PRs.
…156) (#158) Adopts the shared StatStrip (from #157) across Suppliers, Assets, Legal, Systems, Incidents, Corrective Actions, Objectives, Changes and Risks — replacing the tall stat-card grids that pushed each list far down. Counts + filtering stay glanceable in a fraction of the height. StatStrip gains an optional per-chip `static` flag for derived metrics that aren't a filter dimension (Critical / Severe / High), rendered as display-only chips. Risk's summary chips are display-only (they span different filter dimensions) and its Risk Map stays in its collapsible "More". Corrective Actions' summary cards become click-to-filter for the first time, matching the other registers. Closes #156.
Part of #156 — Tasks first, as the reference.
The tall stat-card grid at the top of the registers pushes the list down. This swaps it for a slim, always-visible StatStrip: chips (count + label), each click-to-filter, active chip highlighted. Counts + filtering stay glanceable (no discoverability loss) but take a fraction of the height — the list gets ~3× more rows above the fold.
components/StatStrip.vue(:stats+v-modelfor the active filter key) — reused by the other registers next.<StatStrip :stats="statusStats" v-model="filterStatus" />. Same data, same click-to-filter behaviour.Design approved from a before/after mock.
just build-webgreen. Follow-up PRs roll this out to Risks/Suppliers/Assets/Legal/Systems/Incidents/CAs/Changes/Objectives/Programs (Risk heat-map stays in its collapsible "More").