Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions docs/blacklist-filters-saved-views.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Blacklist: Filter Chips & Saved Views (Design Spec)

Status: Draft
Status: Implemented (Issue #431)

Purpose
- Provide repeatable filters and saved views for large blacklists.
Expand All @@ -21,6 +21,34 @@ Edge cases
- Many chips wrapping: collapse into "+N" overflow menu.
- Keyboard accessibility for overflow menu and chip removal.

Next steps
- Implement `src/pages/DistributionDashboard.tsx` UI and saved-views API hooks.
- Add tests and accessibility checks.
---

## Implementation notes

Implemented on the Distribution Dashboard in a self-contained `BlacklistFiltersPanel`:

- `src/components/BlacklistFilterChips/` β€” multi-select chip bar
- Chip states: default, active (`aria-pressed="true"`), disabled (`:disabled`), and overflow ("+N more" popover menu).
- Multi-select per group (source, severity, region, created date); Shift+Click selects a contiguous range within a group.
- Keyboard: ArrowLeft/Right/Up/Down navigation, Home/End, Enter/Space toggle, Delete/Backspace removes the focused chip.
- WCAG 2.1 AA: every chip is a real `<button>` with `aria-pressed`; focus is visible (`:focus-visible` ring); group boundaries use a visual separator plus `role="group"`.
- `src/components/BlacklistSavedViews/` β€” saved-views dropdown
- Actions per view: apply, rename (inline), set default (star), share (copies a shareable URL), delete.
- "Save current filters" footer input; empty state and error state documented and rendered.
- `src/components/BlacklistFiltersPanel/` β€” orchestration
- Persists views per-user in `localStorage` (mock backend) and syncs the active selection to URL params
(`?blSource=…&blSeverity=…&blRegion=…&blCreated=…&blView=…`) so views are shareable via URL.
- Filters a mock blacklist entry list and renders an empty state when nothing matches.
- Reset button restores the default selection and clears URL params.

Accessibility & responsive
- RTL: overflow menu and saved-views panel are anchored with logical properties (`inset-inline-*`).
- Responsive: chips compact on narrow screens; the saved-views panel is capped to viewport width.
- Axe checks: `jest-axe` runs against the chip bar, saved views, and panel tests.

URL parameter contract (for sharing)
- `blSource` β€” multi-value (e.g. `blSource=wallet&blSource=ip`)
- `blSeverity` β€” multi-value (e.g. `critical`, `high`)
- `blRegion` β€” region id (`na`, `eu`, `apac`, `latam`, `mea`)
- `blCreated` β€” date range (`today`, `7d`, `30d`, `90d`)
- `blView` β€” id of the applied saved view
184 changes: 184 additions & 0 deletions src/components/BlacklistFilterChips/BlacklistFilterChips.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* BlacklistFilterChips β€” Issue #431
*
* Multi-select filter chip bar. Chip states: default, active, disabled,
* overflow. Uses existing design-system tokens from index.css :root.
*
* Responsive:
* - Desktop: chips wrap naturally in a flex row.
* - Mobile: same bar, chips shrink with a compact padding (see media query).
* - RTL: overflow menu and separators use logical/`[dir="rtl"]` overrides.
*/

:root {
--bl-chip-default-bg: rgba(30, 41, 59, 0.6);
--bl-chip-default-border: rgba(148, 163, 184, 0.18);
--bl-chip-hover-bg: rgba(51, 65, 85, 0.7);
--bl-chip-active-bg: rgba(59, 130, 246, 0.18);
--bl-chip-active-border: rgba(59, 130, 246, 0.45);
--bl-chip-active-text: #38bdf8;
--bl-chip-danger-border: rgba(239, 68, 68, 0.35);
}

/* ─── Wrapper ─────────────────────────────────────────────────────── */

.blacklist-chips {
display: flex;
flex-direction: column;
gap: var(--spacing-xs, 0.5rem);
}

.blacklist-chips-header {
display: flex;
align-items: center;
gap: var(--spacing-sm, 0.75rem);
}

.blacklist-chips-title {
font-size: var(--font-size-sm, 0.875rem);
font-weight: var(--font-weight-semibold, 600);
color: var(--text-muted, #cbd5e1);
text-transform: uppercase;
letter-spacing: 0.08em;
}

.blacklist-chips-active-count {
font-size: var(--font-size-xs, 0.75rem);
font-weight: var(--font-weight-medium, 500);
color: var(--bl-chip-active-text);
background: var(--bl-chip-active-bg);
border: 1px solid var(--bl-chip-active-border);
border-radius: var(--radius-full, 9999px);
padding: 0.125rem 0.5rem;
}

/* ─── Chip Bar ────────────────────────────────────────────────────── */

.blacklist-chips-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-xs, 0.5rem);
}

.blacklist-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: var(--radius-full, 9999px);
font-size: var(--font-size-sm, 0.875rem);
line-height: var(--line-height-snug, 1.375);
background: var(--bl-chip-default-bg);
border: 1px solid var(--bl-chip-default-border);
color: var(--text-main, #e5e7eb);
cursor: pointer;
transition:
background 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
}

.blacklist-chip:hover:not(:disabled) {
background: var(--bl-chip-hover-bg);
border-color: var(--glass-border-bright, rgba(148, 163, 184, 0.35));
}

/* Active (selected) state */
.blacklist-chip[aria-pressed='true'] {
background: var(--bl-chip-active-bg);
border-color: var(--bl-chip-active-border);
color: var(--bl-chip-active-text);
}

/* Disabled state */
.blacklist-chip:disabled {
opacity: 0.45;
cursor: not-allowed;
border-style: dashed;
}

.blacklist-chip:focus-visible {
outline: 2px solid var(--primary, #3b82f6);
outline-offset: 2px;
}

.blacklist-chip-count {
font-size: var(--font-size-xs, 0.75rem);
font-weight: var(--font-weight-semibold, 600);
color: inherit;
background: rgba(15, 23, 42, 0.4);
border-radius: var(--radius-full, 9999px);
padding: 0.0625rem 0.375rem;
}

/* Group separator between distinct filter groups */
.blacklist-chips-separator {
width: 1px;
height: 1.5rem;
background: var(--glass-border, rgba(148, 163, 184, 0.15));
margin-inline: var(--spacing-2xs, 0.25rem);
}

/* ─── Overflow ("+N more") ────────────────────────────────────────── */

.blacklist-chips-overflow {
position: relative;
}

.blacklist-chips-overflow-trigger {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
border-radius: var(--radius-full, 9999px);
font-size: var(--font-size-sm, 0.875rem);
background: transparent;
border: 1px dashed var(--glass-border-bright, rgba(148, 163, 184, 0.35));
color: var(--text-muted, #cbd5e1);
cursor: pointer;
transition: all 0.15s ease;
}

.blacklist-chips-overflow-trigger:hover,
.blacklist-chips-overflow-trigger[aria-expanded='true'] {
border-color: var(--primary, #3b82f6);
color: var(--text-accent, #38bdf8);
}

.blacklist-chips-overflow-trigger:focus-visible {
outline: 2px solid var(--primary, #3b82f6);
outline-offset: 2px;
}

.blacklist-chips-overflow-menu {
position: absolute;
top: calc(100% + 4px);
inset-inline-start: 0;
z-index: 30;
min-width: 200px;
background: var(--glass-bg, rgba(15, 23, 42, 0.85));
border: 1px solid var(--glass-border, rgba(148, 163, 184, 0.15));
border-radius: var(--radius-lg, 0.75rem);
padding: var(--spacing-xs, 0.5rem);
display: flex;
flex-direction: column;
gap: 2px;
box-shadow: var(--shadow-xl);
backdrop-filter: var(--glass-blur, blur(12px));
}

.blacklist-chips-overflow-menu .blacklist-chip {
border-radius: var(--radius-md, 0.5rem);
justify-content: flex-start;
width: 100%;
}

/* Mobile: compact chips on narrow screens */
@media (max-width: 640px) {
.blacklist-chip,
.blacklist-chips-overflow-trigger {
padding: 0.25rem 0.5rem;
font-size: var(--font-size-xs, 0.75rem);
}
}
Loading