mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
354 lines
10 KiB
TypeScript
354 lines
10 KiB
TypeScript
import React from "react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { cn } from "@/shared/lib/utils";
|
|
|
|
export const STYLE_SELECT_TRIGGER_CLASS =
|
|
"h-10 w-full rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-900 shadow-sm transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800";
|
|
|
|
export const STYLE_SELECT_CONTENT_CLASS =
|
|
"rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-900";
|
|
|
|
export const STYLE_INPUT_CLASS =
|
|
"w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100";
|
|
|
|
export type StyleSelectOption = {
|
|
value: string;
|
|
label: React.ReactNode;
|
|
disabled?: boolean;
|
|
};
|
|
|
|
type StyleSettingToggleProps = {
|
|
isDefault: boolean;
|
|
onToggle: () => void;
|
|
defaultLabel?: string;
|
|
customLabel?: string;
|
|
className?: string;
|
|
};
|
|
|
|
export const StyleSettingToggle: React.FC<StyleSettingToggleProps> = ({
|
|
isDefault,
|
|
onToggle,
|
|
defaultLabel = "Default",
|
|
customLabel = "Custom",
|
|
className,
|
|
}) => (
|
|
<button
|
|
type="button"
|
|
onClick={onToggle}
|
|
className={cn(
|
|
"relative h-6 w-11 shrink-0 rounded-full transition-colors",
|
|
isDefault ? "bg-gray-200 dark:bg-gray-600" : "bg-purple-600",
|
|
className,
|
|
)}
|
|
aria-label={isDefault ? defaultLabel : customLabel}
|
|
title={isDefault ? defaultLabel : customLabel}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform",
|
|
isDefault ? "left-0.5" : "left-5",
|
|
)}
|
|
/>
|
|
</button>
|
|
);
|
|
|
|
type StyleSettingFieldLabelProps = {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
htmlFor?: string;
|
|
};
|
|
|
|
export const StyleSettingFieldLabel: React.FC<StyleSettingFieldLabelProps> = ({
|
|
children,
|
|
className,
|
|
htmlFor,
|
|
}) => (
|
|
<label
|
|
htmlFor={htmlFor}
|
|
className={cn(
|
|
"mb-1.5 block text-xs font-medium text-gray-600 dark:text-gray-300",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</label>
|
|
);
|
|
|
|
type StyleSettingSelectProps = {
|
|
label?: React.ReactNode;
|
|
value: string;
|
|
onValueChange: (value: string) => void;
|
|
options: StyleSelectOption[];
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
id?: string;
|
|
};
|
|
|
|
export const StyleSettingSelect: React.FC<StyleSettingSelectProps> = ({
|
|
label,
|
|
value,
|
|
onValueChange,
|
|
options,
|
|
placeholder,
|
|
disabled,
|
|
className,
|
|
id,
|
|
}) => (
|
|
<div className={className}>
|
|
{label ? <StyleSettingFieldLabel htmlFor={id}>{label}</StyleSettingFieldLabel> : null}
|
|
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
|
|
<SelectTrigger id={id} className={STYLE_SELECT_TRIGGER_CLASS}>
|
|
<SelectValue placeholder={placeholder} />
|
|
</SelectTrigger>
|
|
<SelectContent className={STYLE_SELECT_CONTENT_CLASS}>
|
|
{options.map((option) => (
|
|
<SelectItem
|
|
key={option.value}
|
|
value={option.value}
|
|
disabled={option.disabled}
|
|
className="rounded-md text-sm focus:bg-purple-50 dark:focus:bg-purple-900/30"
|
|
>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
);
|
|
|
|
type StyleSettingsSidebarProps = {
|
|
title: string;
|
|
hint?: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
compact?: boolean;
|
|
collapsed?: boolean;
|
|
};
|
|
|
|
export const StyleSettingsSidebar: React.FC<StyleSettingsSidebarProps> = ({
|
|
title,
|
|
hint,
|
|
children,
|
|
className,
|
|
compact = false,
|
|
collapsed = false,
|
|
}) => (
|
|
<aside
|
|
className={cn(
|
|
"shrink-0 overflow-y-auto border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800",
|
|
collapsed
|
|
? "w-16 border-b md:w-16 md:border-b-0 md:border-r"
|
|
: compact
|
|
? "w-full border-b md:w-52 md:border-b-0 md:border-r"
|
|
: "max-h-56 w-full border-b md:max-h-none md:w-56 md:border-b-0 md:border-r lg:w-64",
|
|
className,
|
|
)}
|
|
>
|
|
<div className={cn(collapsed ? "p-2" : compact ? "p-3" : "p-4")}>
|
|
{!collapsed ? (
|
|
<>
|
|
<h3 className="mb-1 text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
{title}
|
|
</h3>
|
|
{hint ? (
|
|
<p className="mb-3 text-xs text-gray-500 dark:text-gray-400">{hint}</p>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<p className="sr-only">{title}</p>
|
|
)}
|
|
<nav className={cn("space-y-1", collapsed && "space-y-2")}>{children}</nav>
|
|
</div>
|
|
</aside>
|
|
);
|
|
|
|
export type StyleSettingsSidebarItemProps = {
|
|
icon?: LucideIcon;
|
|
label: React.ReactNode;
|
|
meta?: React.ReactNode;
|
|
badge?: React.ReactNode;
|
|
isActive?: boolean;
|
|
disabled?: boolean;
|
|
onClick?: () => void;
|
|
compact?: boolean;
|
|
iconOnly?: boolean;
|
|
};
|
|
|
|
export const StyleSettingsSidebarItem: React.FC<StyleSettingsSidebarItemProps> = ({
|
|
icon: Icon,
|
|
label,
|
|
meta,
|
|
badge,
|
|
isActive = false,
|
|
disabled = false,
|
|
onClick,
|
|
compact = false,
|
|
iconOnly = false,
|
|
}) => {
|
|
const labelText = typeof label === "string" ? label : undefined;
|
|
|
|
if (iconOnly && Icon) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
title={labelText}
|
|
aria-label={labelText}
|
|
className={cn(
|
|
"flex w-full items-center justify-center rounded-xl transition-all disabled:cursor-not-allowed disabled:opacity-50",
|
|
compact ? "h-9 w-9" : "h-10 w-10",
|
|
isActive
|
|
? "bg-purple-100 text-purple-700 shadow-sm ring-1 ring-purple-200 dark:bg-purple-900/40 dark:text-purple-200 dark:ring-purple-800"
|
|
: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/80",
|
|
)}
|
|
>
|
|
<Icon className={compact ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"group w-full rounded-xl text-left text-sm transition-all disabled:cursor-not-allowed disabled:opacity-50",
|
|
compact ? "px-2.5 py-2" : "px-3 py-2.5",
|
|
isActive
|
|
? "bg-purple-100 shadow-sm ring-1 ring-purple-200 dark:bg-purple-900/40 dark:ring-purple-800"
|
|
: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/80",
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{Icon ? (
|
|
<span
|
|
className={cn(
|
|
"flex shrink-0 items-center justify-center rounded-lg transition-colors",
|
|
compact ? "h-7 w-7" : "h-8 w-8",
|
|
isActive
|
|
? "bg-purple-600 text-white"
|
|
: "bg-gray-100 text-gray-500 group-hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400",
|
|
)}
|
|
>
|
|
<Icon className={compact ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
</span>
|
|
) : null}
|
|
<div className="min-w-0 flex-1">
|
|
<div
|
|
className={cn(
|
|
"truncate font-medium",
|
|
isActive
|
|
? "text-purple-800 dark:text-purple-200"
|
|
: "text-gray-700 dark:text-gray-300",
|
|
)}
|
|
>
|
|
{label}
|
|
</div>
|
|
{meta || badge ? (
|
|
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-gray-400">
|
|
{meta}
|
|
{badge}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
type StyleSettingCardProps = {
|
|
title: React.ReactNode;
|
|
subtitle?: React.ReactNode;
|
|
code?: string;
|
|
isDefault: boolean;
|
|
onToggleDefault?: () => void;
|
|
showCustomBadge?: boolean;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
headerExtra?: React.ReactNode;
|
|
};
|
|
|
|
export const StyleSettingCard: React.FC<StyleSettingCardProps> = ({
|
|
title,
|
|
subtitle,
|
|
code,
|
|
isDefault,
|
|
onToggleDefault,
|
|
showCustomBadge = true,
|
|
children,
|
|
className,
|
|
headerExtra,
|
|
}) => (
|
|
<div
|
|
className={cn(
|
|
"mt-4 overflow-hidden rounded-xl border bg-white shadow-sm dark:bg-gray-800",
|
|
isDefault
|
|
? "border-gray-200 dark:border-gray-700"
|
|
: "border-purple-200 ring-1 ring-purple-100 dark:border-purple-800 dark:ring-purple-900/40",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between gap-4 border-b border-gray-100 px-4 py-3 dark:border-gray-700">
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{title}
|
|
</h4>
|
|
{code ? (
|
|
<span className="rounded-md bg-gray-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-gray-500 dark:bg-gray-700 dark:text-gray-400">
|
|
{code}
|
|
</span>
|
|
) : null}
|
|
{showCustomBadge && !isDefault ? (
|
|
<span className="rounded-full bg-purple-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-purple-700 dark:bg-purple-900/50 dark:text-purple-300">
|
|
Customized
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{subtitle ? (
|
|
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
{subtitle}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{headerExtra}
|
|
{onToggleDefault ? (
|
|
<StyleSettingToggle isDefault={isDefault} onToggle={onToggleDefault} />
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div className={cn("p-4", isDefault && "opacity-60")}>{children}</div>
|
|
</div>
|
|
);
|
|
|
|
export const StyleSettingPanel: React.FC<{
|
|
title?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}> = ({ title, children, className }) => (
|
|
<div
|
|
className={cn(
|
|
"rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/80",
|
|
className,
|
|
)}
|
|
>
|
|
{title ? (
|
|
<h5 className="mb-3 text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300">
|
|
{title}
|
|
</h5>
|
|
) : null}
|
|
{children}
|
|
</div>
|
|
);
|