Enhance layout

This commit is contained in:
decolua 2026-01-31 12:58:04 +07:00
parent 8c37b39eed
commit 8897df5036
23 changed files with 458 additions and 326 deletions

View file

@ -3,12 +3,12 @@
import { cn } from "@/shared/utils/cn";
const variants = {
default: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300",
default: "bg-black/5 dark:bg-white/10 text-text-muted",
primary: "bg-primary/10 text-primary",
success: "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border border-green-100 dark:border-green-800/30",
warning: "bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-500 border border-yellow-100 dark:border-yellow-800/30",
error: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border border-red-100 dark:border-red-800/30",
info: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border border-blue-100 dark:border-blue-800/30",
success: "bg-green-500/10 text-green-600 dark:text-green-400",
warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
error: "bg-red-500/10 text-red-600 dark:text-red-400",
info: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
};
const sizes = {

View file

@ -3,17 +3,17 @@
import { cn } from "@/shared/utils/cn";
const variants = {
primary: "bg-primary text-white hover:bg-primary-hover shadow-warm",
secondary: "bg-surface border border-border text-text-main hover:bg-black/5 shadow-sm",
outline: "border border-border text-text-main hover:bg-black/5",
ghost: "text-text-muted hover:bg-black/5 hover:text-text-main",
primary: "bg-gradient-to-b from-primary to-primary-hover text-white shadow-sm",
secondary: "bg-white dark:bg-white/10 border border-black/10 dark:border-white/10 text-text-main hover:bg-black/5 dark:hover:bg-white/5",
outline: "border border-black/15 dark:border-white/15 text-text-main hover:bg-black/5",
ghost: "text-text-muted hover:bg-black/5 dark:hover:bg-white/5 hover:text-text-main",
danger: "bg-red-500 text-white hover:bg-red-600 shadow-sm",
};
const sizes = {
sm: "h-8 px-3 text-xs rounded-md",
md: "h-10 px-5 text-sm rounded-lg",
lg: "h-12 px-8 text-base rounded-xl",
sm: "h-7 px-3 text-xs rounded-md",
md: "h-9 px-4 text-sm rounded-lg",
lg: "h-11 px-6 text-sm rounded-lg",
};
export default function Button({
@ -32,7 +32,7 @@ export default function Button({
<button
className={cn(
"inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 cursor-pointer",
"active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100",
"active:scale-[0.99] disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100",
variants[variant],
sizes[size],
fullWidth && "w-full",

View file

@ -24,9 +24,9 @@ export default function Card({
<div
className={cn(
"bg-surface",
"border border-border",
"rounded-xl shadow-soft",
hover && "hover:shadow-warm hover:border-primary/30 transition-all cursor-pointer",
"border border-black/5 dark:border-white/5",
"rounded-lg shadow-sm",
hover && "hover:shadow-md hover:border-primary/30 transition-all cursor-pointer",
paddings[padding],
className
)}
@ -63,8 +63,8 @@ Card.Section = function CardSection({ children, className, ...props }) {
<div
className={cn(
"p-4 rounded-lg",
"bg-surface",
"border border-border",
"bg-black/[0.02] dark:bg-white/[0.02]",
"border border-black/5 dark:border-white/5",
className
)}
{...props}
@ -79,8 +79,9 @@ Card.Row = function CardRow({ children, className, ...props }) {
return (
<div
className={cn(
"p-3 -mx-3 px-3 border-b border-border last:border-b-0 transition-colors",
"hover:bg-sidebar",
"p-3 -mx-3 px-3 transition-colors",
"border-b border-black/5 dark:border-white/5 last:border-b-0",
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02]",
className
)}
{...props}
@ -90,3 +91,31 @@ Card.Row = function CardRow({ children, className, ...props }) {
);
};
// Sub-component: List item with hover actions (macOS style)
Card.ListItem = function CardListItem({
children,
actions,
className,
...props
}) {
return (
<div
className={cn(
"group flex items-center justify-between p-3 -mx-3 px-3",
"border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0",
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02]",
"transition-colors",
className
)}
{...props}
>
<div className="flex-1 min-w-0">{children}</div>
{actions && (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{actions}
</div>
)}
</div>
);
};

View file

@ -55,7 +55,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
};
return (
<header className="flex items-center justify-between px-8 py-5 border-b border-border bg-bg/80 backdrop-blur-md z-10 sticky top-0">
<header className="flex items-center justify-between px-8 py-5 border-b border-black/5 dark:border-white/5 bg-bg/80 backdrop-blur-xl z-10 sticky top-0">
{/* Mobile menu button */}
<div className="flex items-center gap-3 lg:hidden">
{showMenuButton && (

View file

@ -38,17 +38,17 @@ export default function Input({
onChange={onChange}
disabled={disabled}
className={cn(
"w-full py-2.5 px-4 text-sm text-text-main",
"bg-surface border rounded-lg",
"w-full py-2 px-3 text-sm text-text-main",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md",
"placeholder-text-muted/60",
"focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none",
"transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed",
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
"transition-all shadow-inner disabled:opacity-50 disabled:cursor-not-allowed",
// iOS zoom fix
"text-[16px] sm:text-sm",
icon && "pl-10",
error
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
: "border-border",
: "",
inputClassName
)}
{...props}

View file

@ -52,7 +52,7 @@ export default function Modal({
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
onClick={closeOnOverlay ? onClose : undefined}
/>
@ -60,8 +60,8 @@ export default function Modal({
<div
className={cn(
"relative w-full bg-surface",
"border border-border",
"rounded-2xl shadow-elevated",
"border border-black/10 dark:border-white/10",
"rounded-xl shadow-2xl",
"animate-in fade-in zoom-in-95 duration-200",
sizes[size],
className
@ -69,16 +69,23 @@ export default function Modal({
>
{/* Header */}
{(title || showCloseButton) && (
<div className="flex items-center justify-between p-6 border-b border-border">
{title && (
<h2 className="text-lg font-semibold text-text-main">
{title}
</h2>
)}
<div className="flex items-center justify-between p-6 border-b border-black/5 dark:border-white/5">
<div className="flex items-center">
<div className="flex items-center gap-2 mr-4">
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
</div>
{title && (
<h2 className="text-lg font-semibold text-text-main">
{title}
</h2>
)}
</div>
{showCloseButton && (
<button
onClick={onClose}
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 transition-colors"
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
@ -91,7 +98,7 @@ export default function Modal({
{/* Footer */}
{footer && (
<div className="flex items-center justify-end gap-3 p-6 border-t border-border">
<div className="flex items-center justify-end gap-3 p-6 border-t border-black/5 dark:border-white/5">
{footer}
</div>
)}

View file

@ -0,0 +1,48 @@
"use client";
import { cn } from "@/shared/utils/cn";
export default function SegmentedControl({
options = [],
value,
onChange,
size = "md",
className,
}) {
const sizes = {
sm: "h-7 text-xs",
md: "h-9 text-sm",
lg: "h-11 text-base",
};
return (
<div
className={cn(
"inline-flex items-center p-1 rounded-lg",
"bg-black/5 dark:bg-white/5",
className
)}
>
{options.map((option) => (
<button
key={option.value}
onClick={() => onChange(option.value)}
className={cn(
"px-4 rounded-md font-medium transition-all",
sizes[size],
value === option.value
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
)}
>
{option.icon && (
<span className="material-symbols-outlined text-[16px] mr-1.5">
{option.icon}
</span>
)}
{option.label}
</button>
))}
</div>
);
}

View file

@ -30,14 +30,14 @@ export default function Select({
onChange={onChange}
disabled={disabled}
className={cn(
"w-full py-2.5 px-4 pr-10 text-sm text-text-main",
"bg-surface border rounded-lg appearance-none",
"focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none",
"transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed",
"w-full py-2 px-3 pr-10 text-sm text-text-main",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md appearance-none",
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
"transition-all disabled:opacity-50 disabled:cursor-not-allowed",
"text-[16px] sm:text-sm",
error
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
: "border-border",
: "",
selectClassName
)}
{...props}

View file

@ -62,9 +62,16 @@ export default function Sidebar({ onClose }) {
return (
<>
<aside className="flex w-72 flex-col border-r border-border bg-sidebar transition-colors duration-300">
<aside className="flex w-72 flex-col border-r border-black/5 dark:border-white/5 bg-vibrancy backdrop-blur-xl transition-colors duration-300">
{/* Traffic lights */}
<div className="flex items-center gap-2 px-6 pt-5 pb-2">
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
</div>
{/* Logo */}
<div className="p-8">
<div className="px-6 py-4">
<Link href="/dashboard" className="flex items-center gap-3">
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a]">
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
@ -83,15 +90,15 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
isActive(item.href)
? "bg-surface text-primary shadow-sm border border-border"
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span
className={cn(
"material-symbols-outlined text-[20px]",
"material-symbols-outlined text-[18px]",
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
)}
>
@ -103,8 +110,8 @@ export default function Sidebar({ onClose }) {
{/* Debug section (only show when ENABLE_REQUEST_LOGS=true) */}
{showDebug && (
<div className="pt-6 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
<div className="pt-4 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
Debug
</p>
{debugItems.map((item) => (
@ -113,15 +120,15 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
isActive(item.href)
? "bg-surface text-primary shadow-sm border border-border"
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span
className={cn(
"material-symbols-outlined text-[20px]",
"material-symbols-outlined text-[18px]",
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
)}
>
@ -134,8 +141,8 @@ export default function Sidebar({ onClose }) {
)}
{/* System section */}
<div className="pt-6 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
<div className="pt-4 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
System
</p>
{systemItems.map((item) => (
@ -144,13 +151,18 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
isActive(item.href)
? "bg-surface text-primary shadow-sm border border-border"
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[20px] group-hover:text-primary transition-colors">
<span
className={cn(
"material-symbols-outlined text-[18px]",
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
)}
>
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
@ -160,11 +172,11 @@ export default function Sidebar({ onClose }) {
</nav>
{/* Footer section */}
<div className="p-4 border-t border-border">
<div className="p-3 border-t border-black/5 dark:border-white/5">
{/* Info message */}
<div className="flex items-start gap-3 p-3 rounded-xl bg-surface border border-border mb-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-blue-500/10 text-blue-500 shrink-0 mt-0.5">
<span className="material-symbols-outlined text-[18px]">info</span>
<div className="flex items-start gap-2 p-2 rounded-lg bg-surface/50 mb-2">
<div className="flex items-center justify-center size-6 rounded-md bg-blue-500/10 text-blue-500 shrink-0 mt-0.5">
<span className="material-symbols-outlined text-[14px]">info</span>
</div>
<div className="flex flex-col">
<span className="text-xs font-medium text-text-main leading-relaxed">

View file

@ -52,11 +52,10 @@ export default function Toggle({
className={cn(
"relative inline-flex shrink-0 cursor-pointer rounded-full",
"transition-colors duration-200 ease-in-out",
"focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2",
"dark:focus:ring-offset-surface-dark",
"focus:outline-none focus:ring-1 focus:ring-primary/30",
checked
? "bg-primary"
: "bg-border",
: "bg-black/10 dark:bg-white/20",
sizes[size].track,
disabled && "cursor-not-allowed"
)}

View file

@ -21,6 +21,7 @@ export { default as RequestLogger } from "./RequestLogger";
export { default as KiroAuthModal } from "./KiroAuthModal";
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
export { default as SegmentedControl } from "./SegmentedControl";
// Layouts
export * from "./layouts";

View file

@ -12,7 +12,7 @@ export default function DashboardLayout({ children }) {
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 lg:hidden"
className="fixed inset-0 z-40 bg-black/20 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}