pi-mono
This commit is contained in:
@@ -12,21 +12,52 @@ export type SettingsSection = {
|
||||
content: ReactNode;
|
||||
};
|
||||
|
||||
export type SettingsSectionGroup = {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
sections: SettingsSection[];
|
||||
};
|
||||
|
||||
type SettingsSidebarProps = {
|
||||
globalKey: string;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
sections: SettingsSection[];
|
||||
groups?: SettingsSectionGroup[];
|
||||
};
|
||||
|
||||
export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections }: SettingsSidebarProps) => {
|
||||
const [selectedKey, setSelectedKey] = useGlobal<string | null>(globalKey, sections[0]?.key ?? null);
|
||||
const SectionButton = ({
|
||||
section,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
section: SettingsSection;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<button
|
||||
key={section.key}
|
||||
onClick={onClick}
|
||||
className={`flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${
|
||||
isActive ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<section.icon className={`h-3.5 w-3.5 shrink-0 mt-0.5 ${isActive ? 'text-duck-teal' : 'text-duck-dark/40 dark:text-foreground/40'}`} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">{section.title}</div>
|
||||
<div className="text-xs text-duck-dark/40 dark:text-foreground/40 truncate">{section.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections, groups }: SettingsSidebarProps) => {
|
||||
const allSections = groups ? groups.flatMap((g) => g.sections) : sections;
|
||||
const [selectedKey, setSelectedKey] = useGlobal<string | null>(globalKey, allSections[0]?.key ?? null);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const query = search.toLowerCase();
|
||||
const filtered = sections.filter(
|
||||
(s) => s.title.toLowerCase().includes(query) || s.description.toLowerCase().includes(query),
|
||||
);
|
||||
const matchesSearch = (s: SettingsSection) =>
|
||||
s.title.toLowerCase().includes(query) || s.description.toLowerCase().includes(query);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
@@ -40,24 +71,37 @@ export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections }: Sett
|
||||
<Input placeholder="Search..." value={search} onChange={(ev) => setSearch(ev.target.value)} className="h-8 text-xs" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-3 overflow-y-auto flex-1">
|
||||
{filtered.map((s) => {
|
||||
const isActive = selectedKey === s.key;
|
||||
return (
|
||||
<button
|
||||
key={s.key}
|
||||
onClick={() => setSelectedKey(s.key)}
|
||||
className={`flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${
|
||||
isActive ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<s.icon className={`h-3.5 w-3.5 shrink-0 mt-0.5 ${isActive ? 'text-duck-teal' : 'text-duck-dark/40 dark:text-foreground/40'}`} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">{s.title}</div>
|
||||
<div className="text-xs text-duck-dark/40 dark:text-foreground/40 truncate">{s.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{groups
|
||||
? groups.map((group) => {
|
||||
const filtered = group.sections.filter(matchesSearch);
|
||||
if (filtered.length === 0) return null;
|
||||
return (
|
||||
<div key={group.label}>
|
||||
<div className="flex items-center gap-2 px-3 pt-3 pb-1">
|
||||
<group.icon className="h-3 w-3 text-duck-dark/30 dark:text-foreground/30" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-duck-dark/30 dark:text-foreground/30">
|
||||
{group.label}
|
||||
</span>
|
||||
</div>
|
||||
{filtered.map((s) => (
|
||||
<SectionButton
|
||||
key={s.key}
|
||||
section={s}
|
||||
isActive={selectedKey === s.key}
|
||||
onClick={() => setSelectedKey(s.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: sections.filter(matchesSearch).map((s) => (
|
||||
<SectionButton
|
||||
key={s.key}
|
||||
section={s}
|
||||
isActive={selectedKey === s.key}
|
||||
onClick={() => setSelectedKey(s.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -96,13 +140,15 @@ type CreateSettingsPanelParams = {
|
||||
globalKey: string;
|
||||
sidebarIcon: LucideIcon;
|
||||
sidebarLabel: string;
|
||||
sections: SettingsSection[];
|
||||
sections?: SettingsSection[];
|
||||
groups?: SettingsSectionGroup[];
|
||||
};
|
||||
|
||||
export const createSettingsPanelComponents = ({ globalKey, sidebarIcon, sidebarLabel, sections }: CreateSettingsPanelParams) => {
|
||||
export const createSettingsPanelComponents = ({ globalKey, sidebarIcon, sidebarLabel, sections = [], groups }: CreateSettingsPanelParams) => {
|
||||
const allSections = groups ? groups.flatMap((g) => g.sections) : sections;
|
||||
const Sidebar: ComponentType = () => (
|
||||
<SettingsSidebar globalKey={globalKey} icon={sidebarIcon} label={sidebarLabel} sections={sections} />
|
||||
<SettingsSidebar globalKey={globalKey} icon={sidebarIcon} label={sidebarLabel} sections={allSections} groups={groups} />
|
||||
);
|
||||
const Content: ComponentType = () => <SettingsContent globalKey={globalKey} sections={sections} />;
|
||||
const Content: ComponentType = () => <SettingsContent globalKey={globalKey} sections={allSections} />;
|
||||
return { Sidebar, Content };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user