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