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:
39
src/components/explorer/ExplorerLayout.tsx
Normal file
39
src/components/explorer/ExplorerLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
src/components/explorer/FilterChips.tsx
Normal file
20
src/components/explorer/FilterChips.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/components/explorer/FilterSidebar.tsx
Normal file
13
src/components/explorer/FilterSidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
37
src/components/explorer/MultiSelectChips.tsx
Normal file
37
src/components/explorer/MultiSelectChips.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user