Design review — July 2026
A full design-review pass over flower's Livewire UI against the bloom design
system (Brief #38). Method: read every page's blade + the shared components +
app.css tokens, and screenshot each page live (dashboard, briefs, feedback,
roster, docs, analytics, prompts, config, inbox, nav/layout).
Verdict: the UI is in strong shape. The bloom system is cohesive and distinctive — one token vocabulary, a global
:focus-visiblebaseline, centralized.nav-link/.pill/.chip/.data-table, good empty states, responsive grids, and the "warmth = liveness/recency" identity carried consistently. Findings are polish + accessibility micro-issues, not structural problems.
Severity is high (real a11y barrier or broken state), med (noticeable inconsistency), low (nit / subjective).
Applied in this pass (safe fixes)
Only safe, obviously-correct, non-structural fixes were applied.
| Fix | Files | Severity | Status |
|---|---|---|---|
Accessible names (aria-label) for unlabeled <select> / <textarea> controls |
briefs index (project scope, project filter, merge-into), briefs detail (model, status, epic parent, add-dependency), roster (project filter), prompts (template body, sample brief/project) | high | ✅ fixed (Brief: #38) |
Before, a screen reader announced these controls only by their first option (e.g. "all projects") with no idea what the control does. They're now named. No visual or behavior change.
Findings — accessibility
| # | Finding | Where | Severity | Disposition |
|---|---|---|---|---|
| A1 | Unlabeled form controls | briefs / roster / prompts | high | Fixed above |
| A2 | Same gap, other pages: <select>/<input> without an accessible name |
inbox/index.blade.php (setNoteStatus ~L109, reassign ~L116, project filter ~L61); config/index.blade.php (max-age ~L59, summaryModel ~L84, briefModel ~L111, pinModel ~L220, pinNewProvider ~L254) |
high | Recommended — same one-line aria-label fix; not applied because these pages were outside the named review scope and a few controls are wrapped in a <label> already (verify before labeling to avoid a redundant name). |
| A3 | Clickable table rows use <tr onclick="window.location=…"> |
dashboard.blade.php ~L173, feedback/index.blade.php ~L111 |
low | Acceptable, leave as-is. Each row already contains a focusable inner <a wire:navigate> (the primary link, with event.stopPropagation()), so keyboard users have a path; the row onclick is a mouse convenience. Adding tabindex/role to the row would create a competing focus target — worse. |
| A4 | Chart bars are hover-only, not keyboard-focusable | analytics/index.blade.php ~L52 |
low | Recommend, don't apply. These are decorative data-viz segments; the numbers are already in the legend/summary. Per-bar focus is noisy + opinionated. If desired, add a descriptive aria-label/summary to the chart container instead. |
Findings — consistency & polish
| # | Finding | Where | Severity | Disposition |
|---|---|---|---|---|
| P1 | Dynamic status colors applied via inline style="background: {{ … }}" |
briefs index/show, roster rows | low | Note, don't apply. These correctly use bloom tokens (var(--…)), just inline because the value is per-row dynamic — legitimate. A shared helper (a <x-ui.dot :color> or a data-state → CSS map) would centralize them if maintainability becomes a concern. |
| P2 | Two pill treatments for "kind" on /feedback — the composer toggle (Bug/Idea/Note) vs. the filter chips (All/Bug/Note/…) | feedback/index.blade.php |
low | Note. Minor; both read fine. Unifying them (or a shared segmented-control component) is a small consistency win. |
| P3 | Roster row-action icons (poke · compaction · retire) are very low contrast | roster/index.blade.php |
low | Note. Poke/retire are intentionally-disabled stubs (faint is correct), but the enabled request-compaction action is hard to discover at rest. Consider a slightly higher rest contrast (or hover-reveal) for the enabled action only. |
Recommendations — structural / opinionated (operator review, NOT applied)
These are genuine improvements but are structural or a matter of taste, so per the review's remit they are left for you to decide, not applied.
FluxUI Pro + slide-over Note/Report (your note on Brief #38). Moving the top-nav Note and Report actions from full-page navigations to a slide-over (keep the user on the page they're referencing) is a real UX win. It's structural, though — it means either adopting FluxUI Pro or building a reusable Alpine slide-over + extracting the two forms into embeddable partials. Worth its own brief. Note: I did not fetch the FluxUI license
auth.jsonfrom the vodmanager project — installing a licensed dependency is outside a safe-polish pass and is your call.
- R1 — Slide-over Note/Report (above). If you like it, a good first FluxUI
fit; otherwise a ~1-file Alpine
x-showslide-over +@includeof each form. - R2 — Shared "segmented control" component for the repeated button-group
pattern (analytics range 7d/30d/90d/All, feedback/brief filter toggles, dispatch
fresh|resume). One<x-ui.segmented>would unify styling + keyboard support. - R3 — Inline-style → token-utility refactor (P1) if you want the dynamic color logic out of the markup.
- R4 — Verified a11y label pass across inbox/config (A2) — safe, just needs
each control checked for an existing wrapping
<label>first.
What's strong (keep doing this)
- Token discipline — no hardcoded hex / raw Tailwind palette colors found anywhere; everything routes through the bloom variables.
- A global
:focus-visibleoutline so keyboard focus is never invisible, and icon-only buttons already carryaria-labels. - Empty states everywhere (briefs, roster, feedback, inbox, prompts, config, search) and responsive grids that collapse cleanly to one column.
- A distinctive, coherent identity — the "the light's still on" warmth model, the accent glow on genuinely-live things, mono for data, the left-rail active indicator. It doesn't read as a template.