mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 17:27:37 +08:00
feat: add sidebar
This commit is contained in:
138
web/src/components/ui/confirm-dialog.tsx
Normal file
138
web/src/components/ui/confirm-dialog.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function ConfirmDialog({
|
||||
cancelLabel = "Cancel",
|
||||
confirmLabel = "Confirm",
|
||||
description,
|
||||
destructive = false,
|
||||
loading = false,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
open,
|
||||
title,
|
||||
}: ConfirmDialogProps) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Focus the confirm button when opened; trap ESC to cancel.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const prevActive = document.activeElement as HTMLElement | null;
|
||||
dialogRef.current
|
||||
?.querySelector<HTMLButtonElement>("[data-confirm]")
|
||||
?.focus();
|
||||
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKey);
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
prevActive?.focus?.();
|
||||
};
|
||||
}, [open, onCancel]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby={description ? "confirm-dialog-desc" : undefined}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onCancel();
|
||||
}}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 flex items-center justify-center",
|
||||
"bg-black/60 backdrop-blur-sm",
|
||||
"animate-[fade-in_150ms_ease-out]",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
className={cn(
|
||||
"relative w-full max-w-md mx-4",
|
||||
"border border-border bg-card shadow-lg",
|
||||
"animate-[dialog-in_180ms_ease-out]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3 p-4 border-b border-border">
|
||||
{destructive && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="mt-0.5 shrink-0 text-destructive"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
||||
<h2
|
||||
id="confirm-dialog-title"
|
||||
className="font-expanded text-sm font-bold tracking-[0.08em] uppercase blend-lighter"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{description && (
|
||||
<p
|
||||
id="confirm-dialog-desc"
|
||||
className="font-mondwest text-xs text-muted-foreground leading-relaxed"
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 p-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
data-confirm
|
||||
type="button"
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
size="sm"
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "…" : confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: string;
|
||||
description?: string;
|
||||
destructive?: boolean;
|
||||
loading?: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
open: boolean;
|
||||
title: string;
|
||||
}
|
||||
80
web/src/components/ui/segmented.tsx
Normal file
80
web/src/components/ui/segmented.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Segmented<T extends string>({
|
||||
className,
|
||||
onChange,
|
||||
options,
|
||||
size = "sm",
|
||||
value,
|
||||
}: SegmentedProps<T>) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
className={cn(
|
||||
"inline-flex border border-border bg-background/30",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const active = opt.value === value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
onClick={() => onChange(opt.value)}
|
||||
className={cn(
|
||||
"font-mondwest tracking-[0.1em] uppercase",
|
||||
"transition-colors cursor-pointer whitespace-nowrap",
|
||||
"border-r border-border last:border-r-0",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30",
|
||||
size === "sm" && "h-7 px-2.5 text-[0.65rem]",
|
||||
size === "md" && "h-8 px-3 text-xs",
|
||||
active
|
||||
? "bg-foreground/90 text-background"
|
||||
: "text-muted-foreground hover:bg-foreground/10 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterGroup({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
}: FilterGroupProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<span className="font-mondwest text-[0.65rem] tracking-[0.12em] uppercase text-muted-foreground/70">
|
||||
{label}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterGroupProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SegmentedOption<T extends string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
interface SegmentedProps<T extends string> {
|
||||
className?: string;
|
||||
onChange: (value: T) => void;
|
||||
options: SegmentedOption<T>[];
|
||||
size?: "sm" | "md";
|
||||
value: T;
|
||||
}
|
||||
@@ -5,15 +5,18 @@ export function Switch({
|
||||
onCheckedChange,
|
||||
className,
|
||||
disabled,
|
||||
id,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onCheckedChange: (v: boolean) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
|
||||
Reference in New Issue
Block a user