diff --git a/src/__tests__/panels/selectorsPanel.test.tsx b/src/__tests__/panels/selectorsPanel.test.tsx index 8e2b73e39..2c37490ec 100644 --- a/src/__tests__/panels/selectorsPanel.test.tsx +++ b/src/__tests__/panels/selectorsPanel.test.tsx @@ -266,6 +266,21 @@ describe('SelectorsPanel', () => { expect(within(panel).queryByRole('button', { name: /edit selector \.cta-button/i })).toBeNull() }) + it('filters to only used selectors', () => { + loadSiteWithSelectors() + render() + + const panel = screen.getByTestId('selectors-panel') + fireEvent.click(within(panel).getByRole('button', { name: /^used$/i })) + + // hero-title and cta-button are referenced by page nodes; unused-card and + // text-m have no nodes referencing them. + expect(within(panel).getByRole('button', { name: /edit selector \.hero-title/i })).toBeDefined() + expect(within(panel).getByRole('button', { name: /edit selector \.cta-button/i })).toBeDefined() + expect(within(panel).queryByRole('button', { name: /edit selector \.unused-card/i })).toBeNull() + expect(within(panel).queryByRole('button', { name: /edit selector \.text-m/i })).toBeNull() + }) + it('searches selector property names and values, not just names', () => { loadSiteWithSelectors() render() diff --git a/src/admin/pages/site/panels/SelectorsPanel/SelectorsPanel.tsx b/src/admin/pages/site/panels/SelectorsPanel/SelectorsPanel.tsx index 7123e9067..2f548e9c3 100644 --- a/src/admin/pages/site/panels/SelectorsPanel/SelectorsPanel.tsx +++ b/src/admin/pages/site/panels/SelectorsPanel/SelectorsPanel.tsx @@ -37,7 +37,7 @@ interface SelectorsPanelProps { variant?: 'docked' } -type SelectorFilter = 'all' | 'user' | 'utility' | 'unused' +type SelectorFilter = 'all' | 'user' | 'utility' | 'used' | 'unused' /** * How many selector rows to mount per batch. A generated framework (e.g. the @@ -55,6 +55,7 @@ const SELECTOR_FILTER_ITEMS: FilterBarItem[] = [ { value: 'all', label: 'All' }, { value: 'user', label: 'User' }, { value: 'utility', label: 'Utility' }, + { value: 'used', label: 'Used' }, { value: 'unused', label: 'Unused' }, ] @@ -77,6 +78,7 @@ function getEmptyFilterMessage(filter: SelectorFilter, query: string): string { if (normalized) return `No selectors match “${normalized}”.` if (filter === 'user') return 'No user selectors yet.' if (filter === 'utility') return 'No utility selectors yet.' + if (filter === 'used') return 'No used selectors yet.' if (filter === 'unused') return 'No unused selectors — every selector is in use.' return 'No selectors match the current filters.' } @@ -136,6 +138,8 @@ export function SelectorsPanel({ variant = 'docked' }: SelectorsPanelProps) { const filteredClasses = reusableClasses.filter((cls) => { if (filter === 'user' && isGeneratedClass(cls)) return false if (filter === 'utility' && !isGeneratedClass(cls)) return false + if (filter === 'used' && resolveSelectorUsage(cls, usageMap, classTokenUsage).unused) + return false if (filter === 'unused' && !resolveSelectorUsage(cls, usageMap, classTokenUsage).unused) return false // Search matches the selector name AND its declared CSS (property names and