Share explorer sidebar components

Introduce reusable explorer layout, sidebar, chips, and multi-select components.

Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
2026-01-11 13:21:19 +00:00
parent 8a9c5395bd
commit 79d161afe1
6 changed files with 262 additions and 195 deletions

View File

@@ -0,0 +1,39 @@
import { ReactNode } from "react";
import FilterChips from "./FilterChips";
type ExplorerLayoutProps = {
title: string;
subtitle?: string;
chips?: string[];
onClearChips?: () => void;
sidebar: ReactNode;
children: ReactNode;
};
export default function ExplorerLayout({
title,
subtitle,
chips = [],
onClearChips,
sidebar,
children,
}: ExplorerLayoutProps) {
return (
<div>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h1 className="mb-1">{title}</h1>
{subtitle ? <div className="text-secondary">{subtitle}</div> : null}
</div>
{chips.length > 0 ? (
<FilterChips chips={chips} onClear={onClearChips} />
) : null}
</div>
<div className="row g-3">
<div className="col-lg-3">{sidebar}</div>
<div className="col-lg-9">{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
type FilterChipsProps = {
chips: string[];
onClear?: () => void;
clearLabel?: string;
};
export default function FilterChips({ chips, onClear, clearLabel = "Clear filters" }: FilterChipsProps) {
return (
<div className="d-flex flex-wrap gap-2 align-items-center">
{chips.map((chip) => (
<span key={chip} className="badge text-bg-light">{chip}</span>
))}
{onClear ? (
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={onClear}>
{clearLabel}
</button>
) : null}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { ReactNode } from "react";
type FilterSidebarProps = {
children: ReactNode;
};
export default function FilterSidebar({ children }: FilterSidebarProps) {
return (
<div className="card shadow-sm">
<div className="card-body">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
type ChipOption<T extends number | string> = {
id: T;
label: string;
};
type MultiSelectChipsProps<T extends number | string> = {
options: ChipOption<T>[];
selected: T[];
onToggle: (id: T) => void;
size?: "sm" | "md";
};
export default function MultiSelectChips<T extends number | string>({
options,
selected,
onToggle,
size = "sm",
}: MultiSelectChipsProps<T>) {
const btnSize = size === "sm" ? "btn-sm" : "";
return (
<div className="d-flex flex-wrap gap-2">
{options.map((option) => {
const active = selected.includes(option.id);
return (
<button
key={String(option.id)}
type="button"
className={`btn ${btnSize} ${active ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => onToggle(option.id)}
>
{option.label}
</button>
);
})}
</div>
);
}