feat: collapsible sidebar filter widgets

This commit is contained in:
2026-02-14 23:07:34 +01:00
parent fbfe62cbfd
commit e1f1a1cdeb
2 changed files with 146 additions and 17 deletions

View File

@@ -412,6 +412,28 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
.calendar-header.collapsible-header {
cursor: pointer;
padding: 4px 6px;
margin: 0 -6px 8px -6px;
border-radius: 3px;
user-select: none;
}
.calendar-header.collapsible-header:hover {
background-color: var(--vscode-list-hoverBackground);
}
.calendar-header.collapsible-header.collapsed {
margin-bottom: 0;
}
.calendar-header .collapse-icon {
font-size: 9px;
margin-right: 4px;
opacity: 0.7;
}
.calendar-header .clear-filter { .calendar-header .clear-filter {
background: transparent; background: transparent;
border: none; border: none;
@@ -523,6 +545,8 @@
} }
.filter-header { .filter-header {
display: flex;
align-items: center;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -531,6 +555,43 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
.filter-header.collapsible-header {
cursor: pointer;
padding: 4px 6px;
margin: 0 -6px 6px -6px;
border-radius: 3px;
user-select: none;
}
.filter-header.collapsible-header:hover {
background-color: var(--vscode-list-hoverBackground);
}
.filter-header.collapsible-header.collapsed {
margin-bottom: 0;
}
.filter-header .collapse-icon {
font-size: 9px;
margin-right: 4px;
opacity: 0.7;
}
.filter-header .clear-filter {
background: transparent;
border: none;
color: var(--vscode-descriptionForeground);
cursor: pointer;
font-size: 10px;
padding: 2px 4px;
margin-left: auto;
opacity: 0.7;
}
.filter-header .clear-filter:hover {
opacity: 1;
}
.filter-chips { .filter-chips {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -83,6 +83,7 @@ interface CalendarViewProps {
const CalendarView: React.FC<CalendarViewProps> = ({ onDateSelect, selectedYear, selectedMonth }) => { const CalendarView: React.FC<CalendarViewProps> = ({ onDateSelect, selectedYear, selectedMonth }) => {
const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]); const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]);
const [expandedYear, setExpandedYear] = useState<number | null>(null); const [expandedYear, setExpandedYear] = useState<number | null>(null);
const [isCollapsed, setIsCollapsed] = useState(true);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -107,15 +108,23 @@ const CalendarView: React.FC<CalendarViewProps> = ({ onDateSelect, selectedYear,
return ( return (
<div className="calendar-view"> <div className="calendar-view">
<div className="calendar-header"> <div
className={`calendar-header collapsible-header ${isCollapsed ? 'collapsed' : 'expanded'}`}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
<span>ARCHIVE</span> <span>ARCHIVE</span>
{(selectedYear || selectedMonth !== undefined) && ( {(selectedYear || selectedMonth !== undefined) && (
<button className="clear-filter" onClick={() => onDateSelect(0)} title="Clear filter"> <button
className="clear-filter"
onClick={(e) => { e.stopPropagation(); onDateSelect(0); }}
title="Clear filter"
>
</button> </button>
)} )}
</div> </div>
<div className="calendar-years"> {!isCollapsed && <div className="calendar-years">
{years.map(year => ( {years.map(year => (
<div key={year} className="calendar-year"> <div key={year} className="calendar-year">
<div <div
@@ -151,7 +160,7 @@ const CalendarView: React.FC<CalendarViewProps> = ({ onDateSelect, selectedYear,
{years.length === 0 && ( {years.length === 0 && (
<div className="calendar-empty">No posts yet</div> <div className="calendar-empty">No posts yet</div>
)} )}
</div> </div>}
</div> </div>
); );
}; };
@@ -175,12 +184,30 @@ const FilterPanel: React.FC<FilterPanelProps> = ({
onTagSelect, onTagSelect,
onCategorySelect, onCategorySelect,
}) => { }) => {
const [tagsCollapsed, setTagsCollapsed] = useState(true);
const [categoriesCollapsed, setCategoriesCollapsed] = useState(true);
return ( return (
<div className="filter-panel"> <div className="filter-panel">
{tags.length > 0 && ( {tags.length > 0 && (
<div className="filter-section"> <div className="filter-section">
<div className="filter-header">TAGS</div> <div
<div className="filter-chips"> className={`filter-header collapsible-header ${tagsCollapsed ? 'collapsed' : 'expanded'}`}
onClick={() => setTagsCollapsed(!tagsCollapsed)}
>
<span className="collapse-icon">{tagsCollapsed ? '▶' : '▼'}</span>
<span>TAGS</span>
{selectedTags.length > 0 && (
<button
className="clear-filter"
onClick={(e) => { e.stopPropagation(); onTagSelect([]); }}
title="Clear tags"
>
</button>
)}
</div>
{!tagsCollapsed && <div className="filter-chips">
{tags.map(tag => { {tags.map(tag => {
const color = tagColors.get(tag); const color = tagColors.get(tag);
const hasColor = !!color; const hasColor = !!color;
@@ -208,13 +235,28 @@ const FilterPanel: React.FC<FilterPanelProps> = ({
</button> </button>
); );
})} })}
</div> </div>}
</div> </div>
)} )}
{categories.length > 0 && ( {categories.length > 0 && (
<div className="filter-section"> <div className="filter-section">
<div className="filter-header">CATEGORIES</div> <div
<div className="filter-chips"> className={`filter-header collapsible-header ${categoriesCollapsed ? 'collapsed' : 'expanded'}`}
onClick={() => setCategoriesCollapsed(!categoriesCollapsed)}
>
<span className="collapse-icon">{categoriesCollapsed ? '▶' : '▼'}</span>
<span>CATEGORIES</span>
{selectedCategories.length > 0 && (
<button
className="clear-filter"
onClick={(e) => { e.stopPropagation(); onCategorySelect([]); }}
title="Clear categories"
>
</button>
)}
</div>
{!categoriesCollapsed && <div className="filter-chips">
{categories.map(cat => ( {categories.map(cat => (
<button <button
key={cat} key={cat}
@@ -230,7 +272,7 @@ const FilterPanel: React.FC<FilterPanelProps> = ({
{cat} {cat}
</button> </button>
))} ))}
</div> </div>}
</div> </div>
)} )}
</div> </div>
@@ -247,6 +289,7 @@ interface MediaCalendarViewProps {
const MediaCalendarView: React.FC<MediaCalendarViewProps> = ({ onDateSelect, selectedYear, selectedMonth }) => { const MediaCalendarView: React.FC<MediaCalendarViewProps> = ({ onDateSelect, selectedYear, selectedMonth }) => {
const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]); const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]);
const [expandedYear, setExpandedYear] = useState<number | null>(null); const [expandedYear, setExpandedYear] = useState<number | null>(null);
const [isCollapsed, setIsCollapsed] = useState(true);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -270,15 +313,23 @@ const MediaCalendarView: React.FC<MediaCalendarViewProps> = ({ onDateSelect, sel
return ( return (
<div className="calendar-view"> <div className="calendar-view">
<div className="calendar-header"> <div
className={`calendar-header collapsible-header ${isCollapsed ? 'collapsed' : 'expanded'}`}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
<span>ARCHIVE</span> <span>ARCHIVE</span>
{(selectedYear || selectedMonth !== undefined) && ( {(selectedYear || selectedMonth !== undefined) && (
<button className="clear-filter" onClick={() => onDateSelect(0)} title="Clear filter"> <button
className="clear-filter"
onClick={(e) => { e.stopPropagation(); onDateSelect(0); }}
title="Clear filter"
>
</button> </button>
)} )}
</div> </div>
<div className="calendar-years"> {!isCollapsed && <div className="calendar-years">
{years.map(year => ( {years.map(year => (
<div key={year} className="calendar-year"> <div key={year} className="calendar-year">
<div <div
@@ -314,7 +365,7 @@ const MediaCalendarView: React.FC<MediaCalendarViewProps> = ({ onDateSelect, sel
{years.length === 0 && ( {years.length === 0 && (
<div className="calendar-empty">No media yet</div> <div className="calendar-empty">No media yet</div>
)} )}
</div> </div>}
</div> </div>
); );
}; };
@@ -333,12 +384,29 @@ const MediaFilterPanel: React.FC<MediaFilterPanelProps> = ({
selectedTags, selectedTags,
onTagSelect, onTagSelect,
}) => { }) => {
const [tagsCollapsed, setTagsCollapsed] = useState(true);
return ( return (
<div className="filter-panel"> <div className="filter-panel">
{tags.length > 0 && ( {tags.length > 0 && (
<div className="filter-section"> <div className="filter-section">
<div className="filter-header">TAGS</div> <div
<div className="filter-chips"> className={`filter-header collapsible-header ${tagsCollapsed ? 'collapsed' : 'expanded'}`}
onClick={() => setTagsCollapsed(!tagsCollapsed)}
>
<span className="collapse-icon">{tagsCollapsed ? '▶' : '▼'}</span>
<span>TAGS</span>
{selectedTags.length > 0 && (
<button
className="clear-filter"
onClick={(e) => { e.stopPropagation(); onTagSelect([]); }}
title="Clear tags"
>
</button>
)}
</div>
{!tagsCollapsed && <div className="filter-chips">
{tags.map(tag => { {tags.map(tag => {
const color = tagColors.get(tag); const color = tagColors.get(tag);
const hasColor = !!color; const hasColor = !!color;
@@ -366,7 +434,7 @@ const MediaFilterPanel: React.FC<MediaFilterPanelProps> = ({
</button> </button>
); );
})} })}
</div> </div>}
</div> </div>
)} )}
</div> </div>