Skip to content

Commit 31afa40

Browse files
feat(ops): chip filter toolbar + concern badge column + JS (A3a)
A3a of docs/specs/ops-workflows-page-refinement/. Mirror of the Specs page A3a (PR #535) for visual + behavioral consistency across the dashboard's two filtered-list surfaces. Surface added: - Chip filter toolbar above the workflows table (7 concern buckets + search input). All 7 chips active by default — unlike Specs page's Complete-off default, no Workflows concern is structurally "noise." - Per-row concern pill in a new column between Name and Tier map. Color-keyed off the same `bucket-<name>` class as the toolbar chip for visual continuity. - `data-concern` attribute on each <tr> for the JS filter. Files: - src/attune/ops/templates/workflows.html — toolbar + column + conditional script load - src/attune/ops/static/js/workflows_refined.js — chip toggle, search, empty-state, init. Pure DOM manipulation; no fetch. Exposes window.__attuneWorkflows for source-grep tests. - src/attune/ops/static/css/main.css — `.workflows-toolbar` block parallels `.specs-toolbar`. Per-concern colors (review purple, test green, docs orange, refactor pink, audit amber, meta cyan, other neutral) applied to BOTH the toolbar chip when active AND the per-row pill. Tests (16 new): - tests/unit/ops/test_workflows_refined_js.py — source-grep tests mirroring the test_specs_routes.py::test_specs_refined_js_* and test_runner_js_parsing conventions. Catches accidental removal of: - window.__attuneWorkflows namespace export - 7-bucket VALID_BUCKETS set - state / applyFilters / setChipState / init API surface - DOM selectors (`.workflows-table tbody`, `.workflows-toolbar .chip[data-bucket]`, `#workflows-search`) - Empty-state message text - Template references workflows_refined.js No regressions on existing Workflows page features (runner.js, bulletin.js, scope picker, recent runs strip, discovery-sweep chips). All 7 existing test_workflows_route_concerns tests pass unmodified. Stacks on A2 (#553). Next: A3b (kebab action menu) and A3c (URL param parsing). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 76e1131 commit 31afa40

4 files changed

Lines changed: 543 additions & 1 deletion

File tree

src/attune/ops/static/css/main.css

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,3 +2713,176 @@ a.kpi:hover {
27132713
opacity: 1;
27142714
transform: translateX(-50%) translateY(0);
27152715
}
2716+
2717+
/* ============================================================
2718+
A3a — Workflows page refinement: chip toolbar + concern pill.
2719+
Scoped via `.workflows-toolbar` and `.concern-pill` to avoid
2720+
collision with the global `.chip` class and the Specs page's
2721+
`.specs-toolbar` selectors. Mirrors the Specs page pattern
2722+
(PR #535) for visual consistency across the dashboard's two
2723+
filtered-list surfaces.
2724+
============================================================ */
2725+
2726+
.workflows-toolbar {
2727+
display: flex;
2728+
flex-wrap: wrap;
2729+
gap: 16px 24px;
2730+
align-items: center;
2731+
margin: 16px 0;
2732+
padding: 12px;
2733+
background: var(--bg, #fff);
2734+
border: 1px solid var(--border, #e5e7eb);
2735+
border-radius: 6px;
2736+
}
2737+
.workflows-toolbar .chip-row {
2738+
display: flex;
2739+
flex-wrap: wrap;
2740+
gap: 6px;
2741+
align-items: center;
2742+
}
2743+
.workflows-toolbar .chip-label,
2744+
.workflows-toolbar .search-label {
2745+
font-size: 12px;
2746+
color: var(--fg-muted, #6b7280);
2747+
margin-right: 4px;
2748+
text-transform: uppercase;
2749+
letter-spacing: 0.04em;
2750+
font-weight: 500;
2751+
}
2752+
.workflows-toolbar .chip {
2753+
display: inline-flex;
2754+
align-items: center;
2755+
gap: 4px;
2756+
padding: 4px 10px;
2757+
border-radius: 99px;
2758+
font-size: 12px;
2759+
font-weight: 500;
2760+
cursor: pointer;
2761+
border: 1px solid transparent;
2762+
transition: background 0.1s, color 0.1s, border-color 0.1s;
2763+
user-select: none;
2764+
margin: 0;
2765+
background: var(--bg-soft, #f9fafb);
2766+
color: var(--fg, #374151);
2767+
font-family: inherit;
2768+
line-height: 1.2;
2769+
text-transform: lowercase;
2770+
}
2771+
.workflows-toolbar .chip:focus-visible {
2772+
outline: 2px solid var(--accent, #2563eb);
2773+
outline-offset: 1px;
2774+
}
2775+
.workflows-toolbar .chip-inactive {
2776+
background: var(--bg-soft, #f9fafb);
2777+
color: var(--fg-muted, #9ca3af);
2778+
border-color: var(--border, #e5e7eb);
2779+
opacity: 0.7;
2780+
}
2781+
.workflows-toolbar .chip-hidden-mark {
2782+
font-size: 11px;
2783+
font-weight: 700;
2784+
margin-left: 2px;
2785+
opacity: 0.6;
2786+
}
2787+
.workflows-toolbar .chip-count {
2788+
font-variant-numeric: tabular-nums;
2789+
opacity: 0.7;
2790+
font-weight: 600;
2791+
}
2792+
2793+
/* Per-concern accent shades. Applied to BOTH the toolbar chip
2794+
(when active) AND the inline `.concern-pill` per-row badge.
2795+
Colors chosen to be distinguishable and to roughly cluster by
2796+
"type of work" — purple-leaning for review/refactor, green for
2797+
test, orange for docs, amber for audit, cyan for meta.
2798+
Matches the wireframe.html palette so the implementation
2799+
visually matches the design reference. */
2800+
.workflows-toolbar .chip-active.bucket-review,
2801+
.concern-pill.bucket-review {
2802+
background: #ede9fe;
2803+
color: #5b21b6;
2804+
border-color: #c4b5fd;
2805+
}
2806+
.workflows-toolbar .chip-active.bucket-test,
2807+
.concern-pill.bucket-test {
2808+
background: #d1fae5;
2809+
color: #065f46;
2810+
border-color: #6ee7b7;
2811+
}
2812+
.workflows-toolbar .chip-active.bucket-docs,
2813+
.concern-pill.bucket-docs {
2814+
background: #fed7aa;
2815+
color: #9a3412;
2816+
border-color: #fdba74;
2817+
}
2818+
.workflows-toolbar .chip-active.bucket-refactor,
2819+
.concern-pill.bucket-refactor {
2820+
background: #fce7f3;
2821+
color: #9d174d;
2822+
border-color: #f9a8d4;
2823+
}
2824+
.workflows-toolbar .chip-active.bucket-audit,
2825+
.concern-pill.bucket-audit {
2826+
background: #fef3c7;
2827+
color: #92400e;
2828+
border-color: #fcd34d;
2829+
}
2830+
.workflows-toolbar .chip-active.bucket-meta,
2831+
.concern-pill.bucket-meta {
2832+
background: #cffafe;
2833+
color: #155e75;
2834+
border-color: #67e8f9;
2835+
}
2836+
.workflows-toolbar .chip-active.bucket-other,
2837+
.concern-pill.bucket-other {
2838+
background: #e5e7eb;
2839+
color: #374151;
2840+
border-color: #d1d5db;
2841+
}
2842+
2843+
/* Per-row concern pill. Compact pill inside the new Concern column.
2844+
Renders even when the row is filtered hidden; visibility is
2845+
controlled by the parent <tr>'s hidden attribute. */
2846+
.concern-pill {
2847+
display: inline-block;
2848+
padding: 2px 8px;
2849+
border-radius: 99px;
2850+
font-size: 11px;
2851+
font-weight: 500;
2852+
text-transform: lowercase;
2853+
border: 1px solid transparent;
2854+
white-space: nowrap;
2855+
}
2856+
2857+
/* Empty-state injected by workflows_refined.js when all rows hide. */
2858+
#workflows-empty-state {
2859+
margin: 16px 0;
2860+
padding: 24px 16px;
2861+
text-align: center;
2862+
color: var(--fg-muted, #6b7280);
2863+
background: var(--bg-soft, #f9fafb);
2864+
border: 1px dashed var(--border, #d1d5db);
2865+
border-radius: 6px;
2866+
font-size: 13px;
2867+
}
2868+
2869+
/* Search input mirrors the Specs page's search-wrap layout. */
2870+
.workflows-toolbar .search-wrap {
2871+
display: flex;
2872+
align-items: center;
2873+
gap: 8px;
2874+
}
2875+
.workflows-toolbar .search-input {
2876+
border: 1px solid var(--border, #d1d5db);
2877+
border-radius: 4px;
2878+
padding: 4px 8px;
2879+
font-size: 13px;
2880+
width: 200px;
2881+
background: var(--bg, #fff);
2882+
color: var(--fg, #111);
2883+
}
2884+
.workflows-toolbar .search-input:focus {
2885+
outline: 2px solid var(--accent, #2563eb);
2886+
outline-offset: -1px;
2887+
border-color: var(--accent, #2563eb);
2888+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Workflows page — concern-chip filter + search behavior (A3a).
2+
//
3+
// Reads the server-rendered table at /workflows and applies client-side
4+
// filtering by concern bucket + workflow name substring.
5+
//
6+
// Default state per docs/specs/ops-workflows-page-refinement/decisions.md D1/D2:
7+
// - All 7 concern chips active (no chip is structurally "noise"
8+
// unlike Specs page's Complete-off default)
9+
// - Search empty
10+
//
11+
// No sort dropdown — workflows render alphabetical by name from the
12+
// server (decisions.md D7: alphabetical-by-name; v2 may add by-last-run).
13+
//
14+
// URL params ship in A3c. A3b adds the kebab action menu.
15+
//
16+
// Exports a `window.__attuneWorkflows` namespace so future PRs and
17+
// source-grep tests can verify the surface (matches the
18+
// `__attuneSpecs` + `__attuneRunner` convention).
19+
20+
(function () {
21+
"use strict";
22+
23+
// All 7 buckets per decisions.md D1. Stable order; chips render in
24+
// the template in this order so the JS doesn't need to enforce it.
25+
var VALID_BUCKETS = new Set([
26+
"review",
27+
"test",
28+
"docs",
29+
"refactor",
30+
"audit",
31+
"meta",
32+
"other",
33+
]);
34+
35+
// Mutable state. All buckets active by default. Search empty.
36+
var state = {
37+
buckets: new Set(VALID_BUCKETS),
38+
search: "",
39+
};
40+
41+
// ---------- DOM helpers ----------
42+
43+
function $(sel) {
44+
return document.querySelector(sel);
45+
}
46+
function $$(sel) {
47+
return Array.prototype.slice.call(document.querySelectorAll(sel));
48+
}
49+
50+
function rowConcern(tr) {
51+
return tr.getAttribute("data-concern") || "";
52+
}
53+
function rowName(tr) {
54+
return (tr.getAttribute("data-workflow") || "").toLowerCase();
55+
}
56+
57+
// ---------- Filter application ----------
58+
59+
function applyFilters() {
60+
var tbody = $(".workflows-table tbody");
61+
if (!tbody) return;
62+
var rows = $$(".workflows-table tbody tr");
63+
var query = state.search.trim().toLowerCase();
64+
var anyVisible = false;
65+
66+
rows.forEach(function (tr) {
67+
var inBucket = state.buckets.has(rowConcern(tr));
68+
var matchesQuery = !query || rowName(tr).indexOf(query) !== -1;
69+
var visible = inBucket && matchesQuery;
70+
if (visible) anyVisible = true;
71+
tr.hidden = !visible;
72+
});
73+
74+
updateEmptyState(anyVisible, query);
75+
}
76+
77+
function updateEmptyState(anyVisible, query) {
78+
// Reuse the existing .empty <p> from the template when present, or
79+
// inject an empty-state row inside the table body. Keeping the
80+
// affordance close to the table to mirror the Specs page UX.
81+
var box = $("#workflows-empty-state");
82+
if (!box) {
83+
box = document.createElement("div");
84+
box.id = "workflows-empty-state";
85+
box.className = "empty-state";
86+
box.setAttribute("role", "status");
87+
box.hidden = true;
88+
var table = $(".workflows-table");
89+
if (table && table.parentNode) {
90+
table.parentNode.insertBefore(box, table.nextSibling);
91+
}
92+
}
93+
if (anyVisible) {
94+
box.hidden = true;
95+
return;
96+
}
97+
box.hidden = false;
98+
if (state.buckets.size === 0) {
99+
box.textContent =
100+
"All concerns filtered out — re-enable at least one chip above.";
101+
} else if (query) {
102+
box.textContent =
103+
"No workflows in active concerns match '" + query + "'.";
104+
} else {
105+
box.textContent = "No workflows match the current filters.";
106+
}
107+
}
108+
109+
// ---------- Chip toggle ----------
110+
111+
function setChipState(chip, active) {
112+
chip.classList.toggle("chip-active", active);
113+
chip.classList.toggle("chip-inactive", !active);
114+
chip.setAttribute("aria-pressed", active ? "true" : "false");
115+
// Add or remove the "✗" hidden-count marker on inactive chips so
116+
// the visual state matches the Specs page convention.
117+
var mark = chip.querySelector(".chip-hidden-mark");
118+
if (!active && !mark) {
119+
var span = document.createElement("span");
120+
span.className = "chip-hidden-mark";
121+
span.setAttribute("aria-hidden", "true");
122+
span.textContent = "✗";
123+
chip.appendChild(span);
124+
} else if (active && mark) {
125+
mark.remove();
126+
}
127+
}
128+
129+
function wireChips() {
130+
$$(".workflows-toolbar .chip[data-bucket]").forEach(function (chip) {
131+
chip.addEventListener("click", function () {
132+
var bucket = chip.getAttribute("data-bucket");
133+
if (!VALID_BUCKETS.has(bucket)) return;
134+
if (state.buckets.has(bucket)) {
135+
state.buckets.delete(bucket);
136+
setChipState(chip, false);
137+
} else {
138+
state.buckets.add(bucket);
139+
setChipState(chip, true);
140+
}
141+
applyFilters();
142+
});
143+
});
144+
}
145+
146+
function wireSearch() {
147+
var input = $("#workflows-search");
148+
if (!input) return;
149+
input.addEventListener("input", function () {
150+
state.search = input.value;
151+
applyFilters();
152+
});
153+
}
154+
155+
// ---------- Init ----------
156+
157+
function init() {
158+
wireChips();
159+
wireSearch();
160+
// First-paint render — server already filtered nothing, but the
161+
// empty-state logic needs to run in case there's a stored search
162+
// (browser back/forward restored the input value).
163+
var input = $("#workflows-search");
164+
if (input && input.value) {
165+
state.search = input.value;
166+
}
167+
applyFilters();
168+
}
169+
170+
if (document.readyState === "loading") {
171+
document.addEventListener("DOMContentLoaded", init);
172+
} else {
173+
init();
174+
}
175+
176+
// Exports — namespace mirrors `window.__attuneSpecs` from
177+
// specs_refined.js so source-grep tests can verify the surface.
178+
window.__attuneWorkflows = {
179+
VALID_BUCKETS: VALID_BUCKETS,
180+
state: state,
181+
applyFilters: applyFilters,
182+
setChipState: setChipState,
183+
init: init,
184+
};
185+
})();

0 commit comments

Comments
 (0)