Files
edr-platform/apps/edr-freight-web/backoffice/src/user-management/TemplateSample/components/styleSettingsUi.tsx
natib21 e6e44e773b fix ui
2026-07-10 11:25:59 +00:00

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>
);