mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
249 lines
5.3 KiB
TypeScript
249 lines
5.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Folder,
|
|
FolderGit2,
|
|
FolderOpen,
|
|
Server,
|
|
ServerCog,
|
|
Cloud,
|
|
CloudCog,
|
|
Database,
|
|
HardDrive,
|
|
Box,
|
|
Boxes,
|
|
Container,
|
|
Network,
|
|
Router,
|
|
Wifi,
|
|
Globe,
|
|
Globe2,
|
|
Terminal,
|
|
Cpu,
|
|
MemoryStick,
|
|
Shield,
|
|
ShieldCheck,
|
|
Lock,
|
|
Key,
|
|
Rocket,
|
|
FlaskConical,
|
|
Wrench,
|
|
Hammer,
|
|
Gauge,
|
|
Activity,
|
|
Layers,
|
|
Building,
|
|
Building2,
|
|
Home,
|
|
Briefcase,
|
|
Code,
|
|
GitBranch,
|
|
Bug,
|
|
Settings,
|
|
Cog,
|
|
Star,
|
|
Flag,
|
|
Bookmark,
|
|
Tag,
|
|
Users,
|
|
User,
|
|
Zap,
|
|
Flame,
|
|
Leaf,
|
|
Mail,
|
|
Monitor,
|
|
Smartphone,
|
|
Search,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { Input } from "@/components/input";
|
|
|
|
const FOLDER_ICON_MAP: Record<string, LucideIcon> = {
|
|
folder: Folder,
|
|
"folder-open": FolderOpen,
|
|
"folder-git": FolderGit2,
|
|
server: Server,
|
|
"server-cog": ServerCog,
|
|
cloud: Cloud,
|
|
"cloud-cog": CloudCog,
|
|
database: Database,
|
|
"hard-drive": HardDrive,
|
|
box: Box,
|
|
boxes: Boxes,
|
|
container: Container,
|
|
network: Network,
|
|
router: Router,
|
|
wifi: Wifi,
|
|
globe: Globe,
|
|
"globe-2": Globe2,
|
|
terminal: Terminal,
|
|
cpu: Cpu,
|
|
memory: MemoryStick,
|
|
shield: Shield,
|
|
"shield-check": ShieldCheck,
|
|
lock: Lock,
|
|
key: Key,
|
|
rocket: Rocket,
|
|
flask: FlaskConical,
|
|
wrench: Wrench,
|
|
hammer: Hammer,
|
|
gauge: Gauge,
|
|
activity: Activity,
|
|
layers: Layers,
|
|
building: Building,
|
|
"building-2": Building2,
|
|
home: Home,
|
|
briefcase: Briefcase,
|
|
code: Code,
|
|
"git-branch": GitBranch,
|
|
bug: Bug,
|
|
settings: Settings,
|
|
cog: Cog,
|
|
star: Star,
|
|
flag: Flag,
|
|
bookmark: Bookmark,
|
|
tag: Tag,
|
|
users: Users,
|
|
user: User,
|
|
zap: Zap,
|
|
flame: Flame,
|
|
leaf: Leaf,
|
|
mail: Mail,
|
|
monitor: Monitor,
|
|
smartphone: Smartphone,
|
|
};
|
|
|
|
const FOLDER_ICON_IDS = Object.keys(FOLDER_ICON_MAP);
|
|
|
|
export const DEFAULT_FOLDER_ICON = "folder";
|
|
export const DEFAULT_FOLDER_COLOR = "#f59145";
|
|
|
|
const SUGGESTED_FOLDER_COLORS = [
|
|
"#f59145",
|
|
"#ef4444",
|
|
"#eab308",
|
|
"#22c55e",
|
|
"#14b8a6",
|
|
"#3b82f6",
|
|
"#6366f1",
|
|
"#a855f7",
|
|
"#ec4899",
|
|
"#6b7280",
|
|
];
|
|
|
|
export function FolderIconEl({
|
|
icon,
|
|
className,
|
|
style,
|
|
}: {
|
|
icon?: string | null;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
}) {
|
|
const Icon = FOLDER_ICON_MAP[icon ?? ""] ?? Folder;
|
|
return <Icon className={className} style={style} />;
|
|
}
|
|
|
|
const HEX_RE = /^#([0-9a-fA-F]{6})$/;
|
|
|
|
function isValidHex(value: string): boolean {
|
|
return HEX_RE.test(value.trim());
|
|
}
|
|
|
|
export function HexColorPicker({
|
|
value,
|
|
onChange,
|
|
}: {
|
|
value: string;
|
|
onChange: (hex: string) => void;
|
|
}) {
|
|
const safe = isValidHex(value) ? value : DEFAULT_FOLDER_COLOR;
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={safe}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className="h-8 w-10 shrink-0 cursor-pointer rounded-none border border-input bg-transparent p-0.5"
|
|
/>
|
|
<Input
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder="#6b7280"
|
|
className="font-mono uppercase"
|
|
maxLength={7}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{SUGGESTED_FOLDER_COLORS.map((c) => (
|
|
<button
|
|
key={c}
|
|
type="button"
|
|
onClick={() => onChange(c)}
|
|
className={`size-6 rounded-none border transition-all ${
|
|
value.toLowerCase() === c.toLowerCase()
|
|
? "ring-2 ring-offset-1 ring-offset-background ring-white/60"
|
|
: "opacity-80 hover:opacity-100"
|
|
}`}
|
|
style={{ backgroundColor: c }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function IconPicker({
|
|
value,
|
|
color,
|
|
onChange,
|
|
}: {
|
|
value: string;
|
|
color?: string;
|
|
onChange: (icon: string) => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [query, setQuery] = useState("");
|
|
const filtered = query.trim()
|
|
? FOLDER_ICON_IDS.filter((id) =>
|
|
id.toLowerCase().includes(query.trim().toLowerCase()),
|
|
)
|
|
: FOLDER_ICON_IDS;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-2 px-2.5 h-8 bg-muted/60 border border-border/60 rounded-none">
|
|
<Search className="size-3 text-muted-foreground/60 shrink-0" />
|
|
<input
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder={t("hosts.iconSearchPlaceholder")}
|
|
className="flex-1 text-xs bg-transparent outline-none placeholder:text-muted-foreground/50 text-foreground min-w-0"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-8 gap-1.5 max-h-40 overflow-y-auto pr-1">
|
|
{filtered.map((id) => (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => onChange(id)}
|
|
title={id}
|
|
className={`flex items-center justify-center h-9 border transition-colors ${
|
|
value === id
|
|
? "border-accent-brand/50 bg-accent-brand/10"
|
|
: "border-border text-muted-foreground hover:text-foreground hover:border-muted-foreground"
|
|
}`}
|
|
>
|
|
<FolderIconEl
|
|
icon={id}
|
|
className="size-4"
|
|
style={value === id ? { color } : undefined}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|