mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
feat(design-files): add filter-by-kind dropdown to the design files viewer (#1990)
* feat(design-files): add filter-by-kind dropdown
The design files viewer already supports grouping and sorting by kind,
but on projects with many files of mixed kinds you still see everything
in one list. Adds a small dropdown next to the group-by toggle that
filters the visible files to one or more selected kinds.
Behavior:
- Dropdown only appears when the project has at least two distinct
kinds; on a single-kind project the control would be noise.
- Multi-select via checkboxes. Empty set means no filter (show all).
- Each row shows the kind glyph, label, and the per-kind file count.
- Trigger label collapses sensibly: "Filter by kind" when none selected,
the single kind's label when one selected, "{n} kinds" when many.
- Filter applies upstream of sort/group/page, so pagination ranges and
the "1-N of M" counter reflect the filtered total.
- Page resets to 0 on filter change so the user doesn't land on an
empty page.
- Selected kinds that disappear from the file list (after a delete or
refresh) are pruned automatically — a stale filter cannot silently
hide everything.
- Popover closes on outside click and Escape.
i18n keys: designFiles.filterBy, designFiles.filterClear,
designFiles.filterCount. All non-English locales inherit via the
existing `...en` spread pattern; zh-CN is the one fully-explicit
dictionary and gets translated values inline.
No behavior change to the group-by toggle, sort, pagination, selection,
or any other existing control. Filtering is layered alongside.
* fix(design-files): prune selection when filter hides selected rows
Codex review flagged that the filter-by-kind change only reset
pagination, not selection. If a user selected files, then narrowed the
filter to a kind that excludes some of them, the now-hidden rows
remained in `selected`. Bulk delete / download still operated on the
full set, so the user could destructively act on rows they could no
longer see — especially risky for the delete path.
Add a useEffect that prunes `selected` to the filtered set whenever
`filteredFiles` or `kindFilter` changes. The empty-filter branch is a
no-op so clearing the filter doesn't disturb any pre-existing
selection — only narrowing the filter prunes.
This commit is contained in:
parent
4b6ddda052
commit
fa9f1d37be
5 changed files with 349 additions and 24 deletions
|
|
@ -100,16 +100,55 @@ export function DesignFilesPanel({
|
||||||
>(new Set());
|
>(new Set());
|
||||||
const [renaming, setRenaming] = useState<{ name: string; draft: string; saving: boolean } | null>(null);
|
const [renaming, setRenaming] = useState<{ name: string; draft: string; saving: boolean } | null>(null);
|
||||||
const [dayBoundary, setDayBoundary] = useState(() => Date.now());
|
const [dayBoundary, setDayBoundary] = useState(() => Date.now());
|
||||||
|
const [kindFilter, setKindFilter] = useState<Set<ProjectFileKind>>(() => new Set());
|
||||||
|
const [filterMenuOpen, setFilterMenuOpen] = useState(false);
|
||||||
|
const filterMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const kindCounts = useMemo(() => {
|
||||||
|
const counts = new Map<ProjectFileKind, number>();
|
||||||
|
for (const f of files) counts.set(f.kind, (counts.get(f.kind) ?? 0) + 1);
|
||||||
|
return counts;
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const availableKinds = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(kindCounts.keys()).sort(
|
||||||
|
(a, b) => kindSortPriority(a) - kindSortPriority(b),
|
||||||
|
),
|
||||||
|
[kindCounts],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop any selected-filter kinds that no longer appear in the file list
|
||||||
|
// (e.g. after a delete leaves the kind empty). Keeps the filter UI honest
|
||||||
|
// and prevents a stale filter from silently hiding everything.
|
||||||
|
useEffect(() => {
|
||||||
|
setKindFilter((prev) => {
|
||||||
|
if (prev.size === 0) return prev;
|
||||||
|
const present = new Set(availableKinds);
|
||||||
|
const next = new Set<ProjectFileKind>();
|
||||||
|
let changed = false;
|
||||||
|
for (const k of prev) {
|
||||||
|
if (present.has(k)) next.add(k);
|
||||||
|
else changed = true;
|
||||||
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}, [availableKinds]);
|
||||||
|
|
||||||
|
const filteredFiles = useMemo(() => {
|
||||||
|
if (kindFilter.size === 0) return files;
|
||||||
|
return files.filter((f) => kindFilter.has(f.kind));
|
||||||
|
}, [files, kindFilter]);
|
||||||
|
|
||||||
const sortedFiles = useMemo(() => {
|
const sortedFiles = useMemo(() => {
|
||||||
return [...files].sort((a, b) => {
|
return [...filteredFiles].sort((a, b) => {
|
||||||
let cmp: number;
|
let cmp: number;
|
||||||
if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
|
if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
|
||||||
else if (sortKey === 'kind') cmp = kindSortPriority(a.kind) - kindSortPriority(b.kind);
|
else if (sortKey === 'kind') cmp = kindSortPriority(a.kind) - kindSortPriority(b.kind);
|
||||||
else cmp = a.mtime - b.mtime;
|
else cmp = a.mtime - b.mtime;
|
||||||
return sortDir === 'asc' ? cmp : -cmp;
|
return sortDir === 'asc' ? cmp : -cmp;
|
||||||
});
|
});
|
||||||
}, [files, sortKey, sortDir]);
|
}, [filteredFiles, sortKey, sortDir]);
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState<number | 'all'>(30);
|
const [pageSize, setPageSize] = useState<number | 'all'>(30);
|
||||||
|
|
@ -151,6 +190,63 @@ export function DesignFilesPanel({
|
||||||
setPage(0);
|
setPage(0);
|
||||||
}, [pageSize]);
|
}, [pageSize]);
|
||||||
|
|
||||||
|
// Reset to the first page when the filter changes — the previous page
|
||||||
|
// index may no longer exist (or may now sit past the new totalPages).
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(0);
|
||||||
|
}, [kindFilter]);
|
||||||
|
|
||||||
|
// Drop any selected files that fall outside the active filter. Without
|
||||||
|
// this, bulk delete / download would silently operate on rows the user
|
||||||
|
// can no longer see — particularly dangerous for destructive deletes.
|
||||||
|
// We keep the empty-filter branch a no-op so clearing the filter
|
||||||
|
// doesn't disturb existing selections.
|
||||||
|
useEffect(() => {
|
||||||
|
if (kindFilter.size === 0) return;
|
||||||
|
setSelected((prev) => {
|
||||||
|
if (prev.size === 0) return prev;
|
||||||
|
const visible = new Set(filteredFiles.map((f) => f.name));
|
||||||
|
const next = new Set<string>();
|
||||||
|
let changed = false;
|
||||||
|
for (const name of prev) {
|
||||||
|
if (visible.has(name)) next.add(name);
|
||||||
|
else changed = true;
|
||||||
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}, [filteredFiles, kindFilter]);
|
||||||
|
|
||||||
|
// Outside-click + escape to close the filter popover. Stops short of a
|
||||||
|
// full focus trap because the popover hosts only checkboxes plus a
|
||||||
|
// small clear button; the existing tab order through them is fine.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterMenuOpen) return;
|
||||||
|
const onMouseDown = (event: MouseEvent) => {
|
||||||
|
const root = filterMenuRef.current;
|
||||||
|
if (root && event.target instanceof Node && !root.contains(event.target)) {
|
||||||
|
setFilterMenuOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') setFilterMenuOpen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener('mousedown', onMouseDown);
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousedown', onMouseDown);
|
||||||
|
window.removeEventListener('keydown', onKey);
|
||||||
|
};
|
||||||
|
}, [filterMenuOpen]);
|
||||||
|
|
||||||
|
function toggleKindFilter(kind: ProjectFileKind): void {
|
||||||
|
setKindFilter((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(kind)) next.delete(kind);
|
||||||
|
else next.add(kind);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Number.isFinite(totalPages)) setPage((p) => Math.min(p, totalPages - 1));
|
if (Number.isFinite(totalPages)) setPage((p) => Math.min(p, totalPages - 1));
|
||||||
}, [totalPages]);
|
}, [totalPages]);
|
||||||
|
|
@ -695,28 +791,105 @@ export function DesignFilesPanel({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{files.length > 0 ? (
|
{files.length > 0 ? (
|
||||||
<div
|
<div className="df-toolbar-row">
|
||||||
className="df-group-toggle"
|
<div
|
||||||
role="group"
|
className="df-group-toggle"
|
||||||
aria-label={t('designFiles.groupBy')}
|
role="group"
|
||||||
>
|
aria-label={t('designFiles.groupBy')}
|
||||||
<span>{t('designFiles.groupBy')}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={groupMode === 'kind' ? 'active' : ''}
|
|
||||||
aria-pressed={groupMode === 'kind'}
|
|
||||||
onClick={() => setGroupMode('kind')}
|
|
||||||
>
|
>
|
||||||
{t('designFiles.groupByKind')}
|
<span>{t('designFiles.groupBy')}</span>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={groupMode === 'kind' ? 'active' : ''}
|
||||||
className={groupMode === 'modified' ? 'active' : ''}
|
aria-pressed={groupMode === 'kind'}
|
||||||
aria-pressed={groupMode === 'modified'}
|
onClick={() => setGroupMode('kind')}
|
||||||
onClick={() => setGroupMode('modified')}
|
>
|
||||||
>
|
{t('designFiles.groupByKind')}
|
||||||
{t('designFiles.groupByModified')}
|
</button>
|
||||||
</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={groupMode === 'modified' ? 'active' : ''}
|
||||||
|
aria-pressed={groupMode === 'modified'}
|
||||||
|
onClick={() => setGroupMode('modified')}
|
||||||
|
>
|
||||||
|
{t('designFiles.groupByModified')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{availableKinds.length > 1 ? (
|
||||||
|
<div className="df-kind-filter" ref={filterMenuRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`df-kind-filter-trigger${kindFilter.size > 0 ? ' active' : ''}`}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-expanded={filterMenuOpen}
|
||||||
|
aria-label={t('designFiles.filterBy')}
|
||||||
|
onClick={() => setFilterMenuOpen((open) => !open)}
|
||||||
|
>
|
||||||
|
<Icon name="sliders" size={13} />
|
||||||
|
<span className="df-kind-filter-trigger-label">
|
||||||
|
{kindFilter.size === 0
|
||||||
|
? t('designFiles.filterBy')
|
||||||
|
: kindFilter.size === 1
|
||||||
|
? kindLabel(Array.from(kindFilter)[0]!, t)
|
||||||
|
: t('designFiles.filterCount', { n: kindFilter.size })}
|
||||||
|
</span>
|
||||||
|
{kindFilter.size > 0 ? (
|
||||||
|
<span
|
||||||
|
className="df-kind-filter-count"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{kindFilter.size}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
{filterMenuOpen ? (
|
||||||
|
<div
|
||||||
|
className="df-kind-filter-popover"
|
||||||
|
role="dialog"
|
||||||
|
aria-label={t('designFiles.filterBy')}
|
||||||
|
>
|
||||||
|
<div className="df-kind-filter-header">
|
||||||
|
<span>{t('designFiles.filterBy')}</span>
|
||||||
|
{kindFilter.size > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="df-kind-filter-clear"
|
||||||
|
onClick={() => setKindFilter(new Set())}
|
||||||
|
>
|
||||||
|
{t('designFiles.filterClear')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<ul className="df-kind-filter-list">
|
||||||
|
{availableKinds.map((kind) => {
|
||||||
|
const checked = kindFilter.has(kind);
|
||||||
|
const count = kindCounts.get(kind) ?? 0;
|
||||||
|
return (
|
||||||
|
<li key={kind}>
|
||||||
|
<label className="df-kind-filter-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => toggleKindFilter(kind)}
|
||||||
|
/>
|
||||||
|
<span className="df-kind-filter-glyph" aria-hidden>
|
||||||
|
{kindGlyph(kind)}
|
||||||
|
</span>
|
||||||
|
<span className="df-kind-filter-label">
|
||||||
|
{kindLabel(kind, t)}
|
||||||
|
</span>
|
||||||
|
<span className="df-kind-filter-itemcount">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{liveArtifacts.length > 0 ? (
|
{liveArtifacts.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -833,6 +833,9 @@ export const en: Dict = {
|
||||||
'designFiles.groupBy': 'Group by',
|
'designFiles.groupBy': 'Group by',
|
||||||
'designFiles.groupByKind': 'Kind',
|
'designFiles.groupByKind': 'Kind',
|
||||||
'designFiles.groupByModified': 'Modified',
|
'designFiles.groupByModified': 'Modified',
|
||||||
|
'designFiles.filterBy': 'Filter by kind',
|
||||||
|
'designFiles.filterClear': 'Clear',
|
||||||
|
'designFiles.filterCount': '{n} kinds',
|
||||||
'designFiles.expandGroup': 'Expand',
|
'designFiles.expandGroup': 'Expand',
|
||||||
'designFiles.collapseGroup': 'Collapse',
|
'designFiles.collapseGroup': 'Collapse',
|
||||||
'designFiles.sectionPages': 'Pages',
|
'designFiles.sectionPages': 'Pages',
|
||||||
|
|
|
||||||
|
|
@ -824,6 +824,9 @@ export const zhCN: Dict = {
|
||||||
'designFiles.groupBy': '分组方式',
|
'designFiles.groupBy': '分组方式',
|
||||||
'designFiles.groupByKind': '类型',
|
'designFiles.groupByKind': '类型',
|
||||||
'designFiles.groupByModified': '修改时间',
|
'designFiles.groupByModified': '修改时间',
|
||||||
|
'designFiles.filterBy': '按类型筛选',
|
||||||
|
'designFiles.filterClear': '清除',
|
||||||
|
'designFiles.filterCount': '{n} 种类型',
|
||||||
'designFiles.expandGroup': '展开',
|
'designFiles.expandGroup': '展开',
|
||||||
'designFiles.collapseGroup': '折叠',
|
'designFiles.collapseGroup': '折叠',
|
||||||
'designFiles.sectionPages': '页面',
|
'designFiles.sectionPages': '页面',
|
||||||
|
|
|
||||||
|
|
@ -1102,6 +1102,9 @@ export interface Dict {
|
||||||
'designFiles.groupBy': string;
|
'designFiles.groupBy': string;
|
||||||
'designFiles.groupByKind': string;
|
'designFiles.groupByKind': string;
|
||||||
'designFiles.groupByModified': string;
|
'designFiles.groupByModified': string;
|
||||||
|
'designFiles.filterBy': string;
|
||||||
|
'designFiles.filterClear': string;
|
||||||
|
'designFiles.filterCount': string;
|
||||||
'designFiles.expandGroup': string;
|
'designFiles.expandGroup': string;
|
||||||
'designFiles.collapseGroup': string;
|
'designFiles.collapseGroup': string;
|
||||||
'designFiles.sectionPages': string;
|
'designFiles.sectionPages': string;
|
||||||
|
|
|
||||||
|
|
@ -9369,8 +9369,14 @@ button.connector-action.is-loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.df-group-toggle {
|
.df-toolbar-row {
|
||||||
margin: 0 20px 8px;
|
margin: 0 20px 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.df-group-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
@ -9402,6 +9408,143 @@ button.connector-action.is-loading {
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filter-by-kind dropdown. Lives in the .df-toolbar-row alongside the
|
||||||
|
group-by toggle. Trigger is a button with a count badge when active;
|
||||||
|
popover is a single-column checkbox list of available kinds. */
|
||||||
|
.df-kind-filter {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.df-kind-filter-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.df-kind-filter-trigger:hover {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.df-kind-filter-trigger.active {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border-color: var(--selected, var(--border));
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.df-kind-filter-trigger-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.df-kind-filter-count {
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--selected, var(--accent, var(--text)));
|
||||||
|
color: var(--bg-panel);
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.df-kind-filter-popover {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
width: max(220px, 100%);
|
||||||
|
max-height: min(360px, calc(100vh - 96px));
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: 130;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.df-kind-filter-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px 6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.df-kind-filter-clear {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.df-kind-filter-clear:hover {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.df-kind-filter-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.df-kind-filter-list li + li {
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||||
|
}
|
||||||
|
.df-kind-filter-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 18px 22px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.df-kind-filter-item:hover {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
.df-kind-filter-item input[type='checkbox'] {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.df-kind-filter-glyph {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--mono, ui-monospace, monospace);
|
||||||
|
}
|
||||||
|
.df-kind-filter-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.df-kind-filter-itemcount {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
.df-section { display: flex; flex-direction: column; gap: 0; }
|
.df-section { display: flex; flex-direction: column; gap: 0; }
|
||||||
.df-section + .df-section { margin-top: 6px; }
|
.df-section + .df-section { margin-top: 6px; }
|
||||||
.df-section-label {
|
.df-section-label {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue