From 4e56dfd3a26afc878a80d79103e1b6dc51bb5f52 Mon Sep 17 00:00:00 2001
From: kitWarse <278602811+kitWarse@users.noreply.github.com>
Date: Mon, 27 Jul 2026 00:45:24 +0100
Subject: [PATCH] feat(dashboard): add channel keyword search for issue #497
- Add channelSearch state and ilteredChannels useMemo in NotificationPreferencesPage
- Render a labelled search input above the delivery matrix that filters channels by keyword
- Show a no-results empty state with a 'Clear search' button when nothing matches
- Dynamically update matrix grid-template-columns to match visible channel count
- Add accessible CSS styles for .channel-search, .channel-search__input,
.channel-search__clear, .channel-search__clear-inline,
and .notification-preferences__no-results in index.css
Closes #497
---
dashboard/src/index.css | 90 +++++++++++++++++++
.../src/pages/NotificationPreferencesPage.tsx | 75 ++++++++++++++--
2 files changed, 160 insertions(+), 5 deletions(-)
diff --git a/dashboard/src/index.css b/dashboard/src/index.css
index 2cfb1ef..e91633c 100644
--- a/dashboard/src/index.css
+++ b/dashboard/src/index.css
@@ -561,6 +561,96 @@ body {
transform: translateX(18px);
}
+/* ── Issue #497: Channel search ────────────────────────────────────────────── */
+.channel-search {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 20px;
+ margin-bottom: 4px;
+}
+
+.channel-search__label {
+ flex-shrink: 0;
+ font-size: 0.9rem;
+ color: #9aa0a6;
+ font-weight: 500;
+}
+
+.channel-search__input {
+ flex: 1;
+ max-width: 280px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 10px;
+ padding: 9px 14px;
+ background: #12151c;
+ color: inherit;
+ font-family: inherit;
+ font-size: 0.92rem;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.channel-search__input:focus-visible {
+ outline: none;
+ border-color: rgba(91, 125, 255, 0.6);
+ box-shadow: 0 0 0 3px rgba(91, 125, 255, 0.15);
+}
+
+.channel-search__clear {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ color: #9aa0a6;
+ cursor: pointer;
+ font-size: 0.9rem;
+ line-height: 1;
+ padding: 6px 10px;
+ transition: background 0.15s ease, color 0.15s ease;
+}
+
+.channel-search__clear:hover {
+ background: rgba(255, 255, 255, 0.06);
+ color: #e8eaed;
+}
+
+.notification-preferences__no-results {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ padding: 32px 20px;
+ border: 1px dashed rgba(255, 255, 255, 0.12);
+ border-radius: 14px;
+ text-align: center;
+ color: #9aa0a6;
+ margin-top: 4px;
+}
+
+.notification-preferences__no-results p {
+ margin: 0;
+ font-size: 0.97rem;
+}
+
+.notification-preferences__no-results strong {
+ color: #c2c8d3;
+}
+
+.channel-search__clear-inline {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 8px;
+ color: #5b7dff;
+ cursor: pointer;
+ font-size: 0.88rem;
+ padding: 6px 14px;
+ transition: background 0.15s ease;
+}
+
+.channel-search__clear-inline:hover {
+ background: rgba(91, 125, 255, 0.1);
+}
+
.notification-preferences__control-panel {
display: grid;
gap: 18px;
diff --git a/dashboard/src/pages/NotificationPreferencesPage.tsx b/dashboard/src/pages/NotificationPreferencesPage.tsx
index b001146..a9c14dd 100644
--- a/dashboard/src/pages/NotificationPreferencesPage.tsx
+++ b/dashboard/src/pages/NotificationPreferencesPage.tsx
@@ -1,4 +1,4 @@
-import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
+import { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react';
type ChannelKey = 'inApp' | 'email' | 'discord' | 'telegram';
type CategoryKey = 'security' | 'governance' | 'system' | 'custom';
@@ -46,6 +46,17 @@ export function NotificationPreferencesPage() {
const [simulateFailure, setSimulateFailure] = useState(false);
const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle');
+ // ── Issue #497: Channel search ────────────────────────────────────────────
+ const [channelSearch, setChannelSearch] = useState('');
+ const channelSearchRef = useRef
No channels match “{channelSearch}”.
+ +{category.description}