Enhance layout
This commit is contained in:
@@ -191,23 +191,23 @@ export default function ClaudeToolCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden">
|
<Card padding="sm" className="overflow-hidden">
|
||||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-12 flex items-center justify-center">
|
<div className="size-8 flex items-center justify-center shrink-0">
|
||||||
<Image src="/providers/claude.png" alt={tool.name} width={40} height={40} className="size-12 object-contain rounded-xl max-w-[48px] max-h-[48px]" sizes="48px" onError={(e) => { e.target.style.display = "none"; }} />
|
<Image src="/providers/claude.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||||
{configStatus === "configured" && <span className="px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||||
{configStatus === "not_configured" && <span className="px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||||
{configStatus === "other" && <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other endpoint</span>}
|
{configStatus === "other" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other</span>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
|
|||||||
@@ -164,23 +164,23 @@ wire_api = "responses"
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden">
|
<Card padding="sm" className="overflow-hidden">
|
||||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-12 flex items-center justify-center">
|
<div className="size-8 flex items-center justify-center shrink-0">
|
||||||
<Image src="/providers/codex.png" alt={tool.name} width={40} height={40} className="size-12 object-contain rounded-xl max-w-[48px] max-h-[48px]" sizes="48px" onError={(e) => { e.target.style.display = "none"; }} />
|
<Image src="/providers/codex.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||||
{configStatus === "configured" && <span className="px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||||
{configStatus === "not_configured" && <span className="px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||||
{configStatus === "other" && <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other endpoint</span>}
|
{configStatus === "other" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other</span>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
|
|||||||
@@ -233,43 +233,43 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
<Image
|
<Image
|
||||||
src={tool.image}
|
src={tool.image}
|
||||||
alt={tool.name}
|
alt={tool.name}
|
||||||
width={40}
|
width={32}
|
||||||
height={40}
|
height={32}
|
||||||
className="size-12 object-contain rounded-xl bg-gray-500 max-w-[48px] max-h-[48px]"
|
className="size-8 object-contain rounded-lg"
|
||||||
sizes="48px"
|
sizes="32px"
|
||||||
onError={(e) => { e.target.style.display = "none"; }}
|
onError={(e) => { e.target.style.display = "none"; }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tool.icon) {
|
if (tool.icon) {
|
||||||
return <span className="material-symbols-outlined text-3xl" style={{ color: tool.color }}>{tool.icon}</span>;
|
return <span className="material-symbols-outlined text-xl" style={{ color: tool.color }}>{tool.icon}</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
src={`/providers/${toolId}.png`}
|
src={`/providers/${toolId}.png`}
|
||||||
alt={tool.name}
|
alt={tool.name}
|
||||||
width={40}
|
width={32}
|
||||||
height={40}
|
height={32}
|
||||||
className="size-10 object-contain rounded-xl max-w-[40px] max-h-[40px]"
|
className="size-8 object-contain rounded-lg"
|
||||||
sizes="40px"
|
sizes="32px"
|
||||||
onError={(e) => { e.target.style.display = "none"; }}
|
onError={(e) => { e.target.style.display = "none"; }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden">
|
<Card padding="sm" className="overflow-hidden">
|
||||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-12 rounded-xl flex items-center justify-center">
|
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
|
|||||||
@@ -120,12 +120,13 @@ export default function CombosPage() {
|
|||||||
{combos.length === 0 ? (
|
{combos.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<span className="material-symbols-outlined text-5xl text-text-muted mb-3 block">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||||
layers
|
<span className="material-symbols-outlined text-[32px]">layers</span>
|
||||||
</span>
|
</div>
|
||||||
<p className="text-text-muted mb-4">No combos yet</p>
|
<p className="text-text-main font-medium mb-1">No combos yet</p>
|
||||||
|
<p className="text-sm text-text-muted mb-4">Create model combos with fallback support</p>
|
||||||
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
||||||
Create your first combo
|
Create Combo
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -168,62 +169,57 @@ export default function CombosPage() {
|
|||||||
|
|
||||||
function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) {
|
function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card padding="sm" className="group">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
{/* Name + Copy */}
|
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
|
||||||
<span className="material-symbols-outlined text-primary">layers</span>
|
|
||||||
<code className="text-lg font-semibold font-mono">{combo.name}</code>
|
|
||||||
<button
|
|
||||||
onClick={() => onCopy(combo.name, `combo-${combo.id}`)}
|
|
||||||
className="p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary"
|
|
||||||
title="Copy combo name"
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined text-sm">
|
|
||||||
{copied === `combo-${combo.id}` ? "check" : "content_copy"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
{/* Models list */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-col gap-1.5">
|
<code className="text-sm font-medium font-mono truncate">{combo.name}</code>
|
||||||
{combo.models.length === 0 ? (
|
<button
|
||||||
<p className="text-sm text-text-muted italic">No models added</p>
|
onClick={(e) => { e.stopPropagation(); onCopy(combo.name, `combo-${combo.id}`); }}
|
||||||
) : (
|
className="p-0.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors opacity-0 group-hover:opacity-100"
|
||||||
combo.models.map((model, index) => (
|
title="Copy combo name"
|
||||||
<div key={index} className="flex items-center gap-2">
|
>
|
||||||
<span className="text-xs text-text-muted w-5">{index + 1}.</span>
|
<span className="material-symbols-outlined text-[14px]">
|
||||||
<code className="text-sm font-mono bg-sidebar px-2 py-0.5 rounded">
|
{copied === `combo-${combo.id}` ? "check" : "content_copy"}
|
||||||
{model}
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||||
|
{combo.models.length === 0 ? (
|
||||||
|
<span className="text-xs text-text-muted italic">No models</span>
|
||||||
|
) : (
|
||||||
|
combo.models.slice(0, 3).map((model, index) => (
|
||||||
|
<code key={index} className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1.5 py-0.5 rounded text-text-muted">
|
||||||
|
{model.split("/").pop()}
|
||||||
</code>
|
</code>
|
||||||
{index === 0 && (
|
))
|
||||||
<span className="text-xs text-primary font-medium">Primary</span>
|
)}
|
||||||
)}
|
{combo.models.length > 3 && (
|
||||||
{index > 0 && (
|
<span className="text-[10px] text-text-muted">+{combo.models.length - 3} more</span>
|
||||||
<span className="text-xs text-text-muted">Fallback</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
className="p-2 hover:bg-sidebar rounded text-text-muted hover:text-primary"
|
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-lg">edit</span>
|
<span className="material-symbols-outlined text-[16px]">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
className="p-2 hover:bg-red-50 rounded text-red-500"
|
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-lg">delete</span>
|
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -321,9 +317,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={isEdit ? "Edit Combo" : "Create Combo"}
|
title={isEdit ? "Edit Combo" : "Create Combo"}
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@@ -333,87 +328,94 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
|||||||
placeholder="my-combo"
|
placeholder="my-combo"
|
||||||
error={nameError}
|
error={nameError}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-text-muted mt-1">
|
<p className="text-[10px] text-text-muted mt-0.5">
|
||||||
Only letters, numbers, - and _ allowed
|
Only letters, numbers, - and _ allowed
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Models */}
|
{/* Models */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<label className="text-sm font-medium mb-1.5 block">Models</label>
|
||||||
<label className="text-sm font-medium">Models</label>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
icon="add"
|
|
||||||
onClick={() => setShowModelSelect(true)}
|
|
||||||
>
|
|
||||||
Add Model
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<div className="text-center py-6 border border-dashed border-border rounded-lg">
|
<div className="text-center py-4 border border-dashed border-black/10 dark:border-white/10 rounded-lg bg-black/[0.01] dark:bg-white/[0.01]">
|
||||||
<p className="text-sm text-text-muted">No models added</p>
|
<span className="material-symbols-outlined text-text-muted text-xl mb-1">layers</span>
|
||||||
<p className="text-xs text-text-muted mt-1">Click "Add Model" to add</p>
|
<p className="text-xs text-text-muted">No models added yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 max-h-[240px] overflow-y-auto">
|
<div className="flex flex-col gap-1 max-h-[200px] overflow-y-auto">
|
||||||
{models.map((model, index) => (
|
{models.map((model, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center gap-2"
|
className="group flex items-center gap-1.5 px-2 py-1 rounded-md bg-black/[0.02] dark:bg-white/[0.02] hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-colors"
|
||||||
>
|
>
|
||||||
{/* Priority arrows */}
|
{/* Index badge */}
|
||||||
<div className="flex flex-col gap-0">
|
<span className="text-[10px] font-medium text-text-muted w-3 text-center shrink-0">{index + 1}</span>
|
||||||
|
|
||||||
|
{/* Model Input */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => handleModelChange(index, e.target.value)}
|
||||||
|
placeholder="provider/model"
|
||||||
|
className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-transparent border-0 focus:outline-none text-text-main placeholder:text-text-muted/50"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Priority arrows - horizontal, always visible */}
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMoveUp(index)}
|
onClick={() => handleMoveUp(index)}
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
className={`p-0.5 rounded ${index === 0 ? "text-text-muted/30" : "hover:bg-surface text-text-muted hover:text-primary"}`}
|
className={`p-0.5 rounded ${index === 0 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||||
|
title="Move up"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-sm leading-none">keyboard_arrow_up</span>
|
<span className="material-symbols-outlined text-[12px]">arrow_upward</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMoveDown(index)}
|
onClick={() => handleMoveDown(index)}
|
||||||
disabled={index === models.length - 1}
|
disabled={index === models.length - 1}
|
||||||
className={`p-0.5 rounded ${index === models.length - 1 ? "text-text-muted/30" : "hover:bg-surface text-text-muted hover:text-primary"}`}
|
className={`p-0.5 rounded ${index === models.length - 1 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||||
|
title="Move down"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-sm leading-none">keyboard_arrow_down</span>
|
<span className="material-symbols-outlined text-[12px]">arrow_downward</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Model Input */}
|
{/* Remove - always visible */}
|
||||||
<Input
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => handleModelChange(index, e.target.value)}
|
|
||||||
placeholder="model-name"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Remove */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveModel(index)}
|
onClick={() => handleRemoveModel(index)}
|
||||||
className="p-2 hover:bg-red-50 rounded text-red-500"
|
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 transition-all"
|
||||||
|
title="Remove"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-sm">close</span>
|
<span className="material-symbols-outlined text-[12px]">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Model button - moved to bottom */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModelSelect(true)}
|
||||||
|
className="w-full mt-2 py-2 border border-dashed border-black/10 dark:border-white/10 rounded-lg text-xs text-text-muted hover:text-primary hover:border-primary/30 transition-colors flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[16px]">add</span>
|
||||||
|
Add Model
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-1">
|
||||||
|
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
size="sm"
|
||||||
disabled={!name.trim() || !!nameError || saving}
|
disabled={!name.trim() || !!nameError || saving}
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Apply"}
|
{saving ? "Saving..." : isEdit ? "Save" : "Create"}
|
||||||
</Button>
|
|
||||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -293,58 +293,48 @@ export default function APIPageClient({ machineId }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{keys.length === 0 ? (
|
{keys.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-12">
|
||||||
<span className="material-symbols-outlined text-4xl text-text-muted mb-2">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||||
vpn_key
|
<span className="material-symbols-outlined text-[32px]">vpn_key</span>
|
||||||
</span>
|
</div>
|
||||||
<p className="text-sm text-text-muted">No API keys yet</p>
|
<p className="text-text-main font-medium mb-1">No API keys yet</p>
|
||||||
|
<p className="text-sm text-text-muted mb-4">Create your first API key to get started</p>
|
||||||
|
<Button icon="add" onClick={() => setShowAddModal(true)}>
|
||||||
|
Create Key
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="flex flex-col">
|
||||||
<table className="w-full">
|
{keys.map((key) => (
|
||||||
<thead>
|
<div
|
||||||
<tr className="border-b border-border">
|
key={key.id}
|
||||||
<th className="text-left p-3 font-medium">Name</th>
|
className="group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0"
|
||||||
<th className="text-left p-3 font-medium">Key</th>
|
>
|
||||||
<th className="text-left p-3 font-medium">Created</th>
|
<div className="flex-1 min-w-0">
|
||||||
<th className="text-left p-3 font-medium">Actions</th>
|
<p className="text-sm font-medium">{key.name}</p>
|
||||||
</tr>
|
<div className="flex items-center gap-2 mt-1">
|
||||||
</thead>
|
<code className="text-xs text-text-muted font-mono">{key.key}</code>
|
||||||
<tbody>
|
<button
|
||||||
{keys.map((key) => (
|
onClick={() => copy(key.key, key.id)}
|
||||||
<tr key={key.id} className="border-b border-border hover:bg-sidebar/30">
|
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
|
||||||
<td className="p-3 text-sm">{key.name}</td>
|
>
|
||||||
<td className="p-3">
|
<span className="material-symbols-outlined text-[14px]">
|
||||||
<div className="flex items-center gap-2">
|
{copied === key.id ? "check" : "content_copy"}
|
||||||
<span className="font-mono text-xs text-text-muted">
|
</span>
|
||||||
{key.key}
|
</button>
|
||||||
</span>
|
</div>
|
||||||
<Button
|
<p className="text-xs text-text-muted mt-1">
|
||||||
size="sm"
|
Created {new Date(key.createdAt).toLocaleDateString()}
|
||||||
variant="ghost"
|
</p>
|
||||||
icon={copied === key.id ? "check" : "content_copy"}
|
</div>
|
||||||
onClick={() => copy(key.key, key.id)}
|
<button
|
||||||
/>
|
onClick={() => handleDeleteKey(key.id)}
|
||||||
</div>
|
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-0 group-hover:opacity-100 transition-all"
|
||||||
</td>
|
>
|
||||||
<td className="p-3 text-sm text-text-muted">
|
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||||
{new Date(key.createdAt).toLocaleDateString()}
|
</button>
|
||||||
</td>
|
</div>
|
||||||
<td className="p-3">
|
))}
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon="delete"
|
|
||||||
className="text-red-500"
|
|
||||||
onClick={() => handleDeleteKey(key.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, Button, Badge, Toggle, Input } from "@/shared/components";
|
import { Card, Button, Badge, Toggle, Input } from "@/shared/components";
|
||||||
import { useTheme } from "@/shared/hooks/useTheme";
|
import { useTheme } from "@/shared/hooks/useTheme";
|
||||||
|
import { cn } from "@/shared/utils/cn";
|
||||||
import { APP_CONFIG } from "@/shared/constants/config";
|
import { APP_CONFIG } from "@/shared/constants/config";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
@@ -117,7 +118,12 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{/* Routing Preferences */}
|
{/* Routing Preferences */}
|
||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-lg font-semibold mb-4">Security</h3>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||||
|
<span className="material-symbols-outlined text-[20px]">shield</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold">Security</h3>
|
||||||
|
</div>
|
||||||
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
|
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-sm font-medium">Current Password</label>
|
<label className="text-sm font-medium">Current Password</label>
|
||||||
@@ -168,7 +174,12 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{/* Routing Preferences */}
|
{/* Routing Preferences */}
|
||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-lg font-semibold mb-4">Routing Strategy</h3>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||||
|
<span className="material-symbols-outlined text-[20px]">route</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold">Routing Strategy</h3>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -215,7 +226,12 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{/* Theme Preferences */}
|
{/* Theme Preferences */}
|
||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-lg font-semibold mb-4">Appearance</h3>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||||
|
<span className="material-symbols-outlined text-[20px]">palette</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold">Appearance</h3>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -231,34 +247,38 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme Options */}
|
{/* Theme Options */}
|
||||||
<div className="flex gap-3 pt-4 border-t border-border">
|
<div className="pt-4 border-t border-border">
|
||||||
{["light", "dark", "system"].map((option) => (
|
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
||||||
<button
|
{["light", "dark", "system"].map((option) => (
|
||||||
key={option}
|
<button
|
||||||
onClick={() => setTheme(option)}
|
key={option}
|
||||||
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border transition-all ${
|
onClick={() => setTheme(option)}
|
||||||
theme === option
|
className={cn(
|
||||||
? "border-primary bg-primary/5"
|
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all",
|
||||||
: "border-border hover:border-primary/50"
|
theme === option
|
||||||
}`}
|
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||||
>
|
: "text-text-muted hover:text-text-main"
|
||||||
<span className="material-symbols-outlined text-2xl">
|
)}
|
||||||
{option === "light"
|
>
|
||||||
? "light_mode"
|
<span className="material-symbols-outlined text-[20px]">
|
||||||
: option === "dark"
|
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
||||||
? "dark_mode"
|
</span>
|
||||||
: "contrast"}
|
<span className="capitalize">{option}</span>
|
||||||
</span>
|
</button>
|
||||||
<span className="text-sm font-medium capitalize">{option}</span>
|
))}
|
||||||
</button>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Data Management */}
|
{/* Data Management */}
|
||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-lg font-semibold mb-4">Data</h3>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-lg bg-green-500/10 text-green-500">
|
||||||
|
<span className="material-symbols-outlined text-[20px]">database</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold">Data</h3>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-bg border border-border">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-bg border border-border">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -303,14 +303,18 @@ export default function ProviderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{connections.length === 0 ? (
|
{connections.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-12">
|
||||||
<span className="material-symbols-outlined text-4xl text-text-muted mb-2">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||||
{isOAuth ? "lock" : "key"}
|
<span className="material-symbols-outlined text-[32px]">{isOAuth ? "lock" : "key"}</span>
|
||||||
</span>
|
</div>
|
||||||
<p className="text-sm text-text-muted">No connections yet</p>
|
<p className="text-text-main font-medium mb-1">No connections yet</p>
|
||||||
|
<p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
|
||||||
|
<Button icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
|
||||||
|
Add Connection
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
||||||
{connections
|
{connections
|
||||||
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
||||||
.map((conn, index) => (
|
.map((conn, index) => (
|
||||||
@@ -618,7 +622,7 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center justify-between p-3 rounded-lg border border-border hover:bg-sidebar/50 ${connection.isActive === false ? 'opacity-60' : ''}`}>
|
<div className={`group flex items-center justify-between p-3 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
{/* Priority arrows */}
|
{/* Priority arrows */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -666,12 +670,12 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
|||||||
onChange={onToggleActive}
|
onChange={onToggleActive}
|
||||||
title={(connection.isActive ?? true) ? "Disable connection" : "Enable connection"}
|
title={(connection.isActive ?? true) ? "Disable connection" : "Enable connection"}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-1 ml-1">
|
<div className="flex gap-1 ml-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button onClick={onEdit} className="p-2 hover:bg-sidebar rounded">
|
<button onClick={onEdit} className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary">
|
||||||
<span className="material-symbols-outlined text-base">edit</span>
|
<span className="material-symbols-outlined text-[18px]">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onDelete} className="p-2 hover:bg-red-50 rounded text-red-500">
|
<button onClick={onDelete} className="p-2 hover:bg-red-500/10 rounded text-red-500">
|
||||||
<span className="material-symbols-outlined text-base">delete</span>
|
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ function ProviderCard({ providerId, provider, stats }) {
|
|||||||
const [imgError, setImgError] = useState(false);
|
const [imgError, setImgError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/providers/${providerId}`}>
|
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||||
<Card padding="sm" className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
<Card padding="sm" className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -169,7 +169,7 @@ function ProviderCard({ providerId, provider, stats }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="material-symbols-outlined text-text-muted">
|
<span className="material-symbols-outlined text-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
chevron_right
|
chevron_right
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,8 +199,8 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
|||||||
const { connected, error, errorCode, errorTime } = stats;
|
const { connected, error, errorCode, errorTime } = stats;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/providers/${providerId}`}>
|
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||||
<Card padding="sm" className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
<Card padding="sm" className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -222,7 +222,7 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="material-symbols-outlined text-text-muted">
|
<span className="material-symbols-outlined text-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
chevron_right
|
chevron_right
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, Suspense } from "react";
|
import { useState, Suspense } from "react";
|
||||||
import { UsageStats, RequestLogger, CardSkeleton } from "@/shared/components";
|
import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||||
|
|
||||||
export default function UsagePage() {
|
export default function UsagePage() {
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Tabs */}
|
<SegmentedControl
|
||||||
<div className="flex border-b border-border">
|
options={[
|
||||||
<button
|
{ value: "overview", label: "Overview" },
|
||||||
onClick={() => setActiveTab("overview")}
|
{ value: "logs", label: "Logger" },
|
||||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
]}
|
||||||
activeTab === "overview"
|
value={activeTab}
|
||||||
? "border-primary text-primary"
|
onChange={setActiveTab}
|
||||||
: "border-transparent text-text-muted hover:text-text-primary"
|
/>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Overview
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("logs")}
|
|
||||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === "logs"
|
|
||||||
? "border-primary text-primary"
|
|
||||||
: "border-transparent text-text-muted hover:text-text-primary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Logger
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{activeTab === "overview" ? (
|
{activeTab === "overview" ? (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
/* Claude-inspired Color Palette */
|
/* macOS-inspired Color Palette with Terracotta Primary */
|
||||||
:root {
|
:root {
|
||||||
/* Primary - Warm Coral/Terracotta */
|
/* Primary - Warm Coral/Terracotta */
|
||||||
--color-primary: #D97757;
|
--color-primary: #D97757;
|
||||||
@@ -13,15 +13,15 @@
|
|||||||
--color-bg: #FBF9F6;
|
--color-bg: #FBF9F6;
|
||||||
--color-bg-alt: #F5F1ED;
|
--color-bg-alt: #F5F1ED;
|
||||||
--color-surface: #FFFFFF;
|
--color-surface: #FFFFFF;
|
||||||
--color-sidebar: #F0EFEC;
|
--color-sidebar: rgba(246, 246, 246, 0.8);
|
||||||
--color-border: #E6E4DD;
|
--color-border: rgba(0, 0, 0, 0.1);
|
||||||
--color-text-main: #383733;
|
--color-text-main: #383733;
|
||||||
--color-text-muted: #75736E;
|
--color-text-muted: #75736E;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows - subtle macOS style */
|
||||||
--shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.03), 0 10px 25px rgba(0, 0, 0, 0.02);
|
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.015);
|
||||||
--shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.15);
|
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.12);
|
||||||
--shadow-elevated: 0 20px 40px -4px rgba(60, 50, 45, 0.08);
|
--shadow-elevated: 0 12px 28px -4px rgba(60, 50, 45, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -29,15 +29,15 @@
|
|||||||
--color-bg: #191918;
|
--color-bg: #191918;
|
||||||
--color-bg-alt: #1F1F1E;
|
--color-bg-alt: #1F1F1E;
|
||||||
--color-surface: #242423;
|
--color-surface: #242423;
|
||||||
--color-sidebar: #1F1F1E;
|
--color-sidebar: rgba(30, 30, 30, 0.8);
|
||||||
--color-border: #333331;
|
--color-border: rgba(255, 255, 255, 0.1);
|
||||||
--color-text-main: #ECEBE8;
|
--color-text-main: #ECEBE8;
|
||||||
--color-text-muted: #9E9D99;
|
--color-text-muted: #9E9D99;
|
||||||
|
|
||||||
/* Dark shadows */
|
/* Dark shadows - subtle macOS style */
|
||||||
--shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.2), 0 10px 25px rgba(0, 0, 0, 0.15);
|
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
--shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.2);
|
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.15);
|
||||||
--shadow-elevated: 0 20px 40px -4px rgba(0, 0, 0, 0.4);
|
--shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
--shadow-warm: var(--shadow-warm);
|
--shadow-warm: var(--shadow-warm);
|
||||||
--shadow-elevated: var(--shadow-elevated);
|
--shadow-elevated: var(--shadow-elevated);
|
||||||
|
|
||||||
/* Font */
|
/* Font - macOS system fonts */
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles */
|
/* Base styles */
|
||||||
@@ -182,3 +182,38 @@ body {
|
|||||||
.animate-border-glow {
|
.animate-border-glow {
|
||||||
animation: border-glow 2s ease-in-out infinite;
|
animation: border-glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* macOS Vibrancy/Blur Effect */
|
||||||
|
.bg-vibrancy {
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-vibrancy {
|
||||||
|
background: rgba(30, 30, 30, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* macOS Traffic Lights */
|
||||||
|
.traffic-lights {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-light {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-light.red {
|
||||||
|
background: #FF5F56;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-light.yellow {
|
||||||
|
background: #FFBD2E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-light.green {
|
||||||
|
background: #27C93F;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
import { cn } from "@/shared/utils/cn";
|
import { cn } from "@/shared/utils/cn";
|
||||||
|
|
||||||
const variants = {
|
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",
|
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",
|
success: "bg-green-500/10 text-green-600 dark:text-green-400",
|
||||||
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",
|
warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
|
||||||
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",
|
error: "bg-red-500/10 text-red-600 dark:text-red-400",
|
||||||
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",
|
info: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
import { cn } from "@/shared/utils/cn";
|
import { cn } from "@/shared/utils/cn";
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: "bg-primary text-white hover:bg-primary-hover shadow-warm",
|
primary: "bg-gradient-to-b from-primary to-primary-hover text-white shadow-sm",
|
||||||
secondary: "bg-surface border border-border text-text-main hover:bg-black/5 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-border text-text-main hover:bg-black/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 hover:text-text-main",
|
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",
|
danger: "bg-red-500 text-white hover:bg-red-600 shadow-sm",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: "h-8 px-3 text-xs rounded-md",
|
sm: "h-7 px-3 text-xs rounded-md",
|
||||||
md: "h-10 px-5 text-sm rounded-lg",
|
md: "h-9 px-4 text-sm rounded-lg",
|
||||||
lg: "h-12 px-8 text-base rounded-xl",
|
lg: "h-11 px-6 text-sm rounded-lg",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
@@ -32,7 +32,7 @@ export default function Button({
|
|||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 cursor-pointer",
|
"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],
|
variants[variant],
|
||||||
sizes[size],
|
sizes[size],
|
||||||
fullWidth && "w-full",
|
fullWidth && "w-full",
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export default function Card({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-surface",
|
"bg-surface",
|
||||||
"border border-border",
|
"border border-black/5 dark:border-white/5",
|
||||||
"rounded-xl shadow-soft",
|
"rounded-lg shadow-sm",
|
||||||
hover && "hover:shadow-warm hover:border-primary/30 transition-all cursor-pointer",
|
hover && "hover:shadow-md hover:border-primary/30 transition-all cursor-pointer",
|
||||||
paddings[padding],
|
paddings[padding],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -63,8 +63,8 @@ Card.Section = function CardSection({ children, className, ...props }) {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-4 rounded-lg",
|
"p-4 rounded-lg",
|
||||||
"bg-surface",
|
"bg-black/[0.02] dark:bg-white/[0.02]",
|
||||||
"border border-border",
|
"border border-black/5 dark:border-white/5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -79,8 +79,9 @@ Card.Row = function CardRow({ children, className, ...props }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-3 -mx-3 px-3 border-b border-border last:border-b-0 transition-colors",
|
"p-3 -mx-3 px-3 transition-colors",
|
||||||
"hover:bg-sidebar",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Mobile menu button */}
|
||||||
<div className="flex items-center gap-3 lg:hidden">
|
<div className="flex items-center gap-3 lg:hidden">
|
||||||
{showMenuButton && (
|
{showMenuButton && (
|
||||||
|
|||||||
@@ -38,17 +38,17 @@ export default function Input({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full py-2.5 px-4 text-sm text-text-main",
|
"w-full py-2 px-3 text-sm text-text-main",
|
||||||
"bg-surface border rounded-lg",
|
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md",
|
||||||
"placeholder-text-muted/60",
|
"placeholder-text-muted/60",
|
||||||
"focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none",
|
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
|
||||||
"transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed",
|
"transition-all shadow-inner disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
// iOS zoom fix
|
// iOS zoom fix
|
||||||
"text-[16px] sm:text-sm",
|
"text-[16px] sm:text-sm",
|
||||||
icon && "pl-10",
|
icon && "pl-10",
|
||||||
error
|
error
|
||||||
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
||||||
: "border-border",
|
: "",
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function Modal({
|
|||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
<div
|
<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}
|
onClick={closeOnOverlay ? onClose : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ export default function Modal({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full bg-surface",
|
"relative w-full bg-surface",
|
||||||
"border border-border",
|
"border border-black/10 dark:border-white/10",
|
||||||
"rounded-2xl shadow-elevated",
|
"rounded-xl shadow-2xl",
|
||||||
"animate-in fade-in zoom-in-95 duration-200",
|
"animate-in fade-in zoom-in-95 duration-200",
|
||||||
sizes[size],
|
sizes[size],
|
||||||
className
|
className
|
||||||
@@ -69,16 +69,23 @@ export default function Modal({
|
|||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
{(title || showCloseButton) && (
|
{(title || showCloseButton) && (
|
||||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
<div className="flex items-center justify-between p-6 border-b border-black/5 dark:border-white/5">
|
||||||
{title && (
|
<div className="flex items-center">
|
||||||
<h2 className="text-lg font-semibold text-text-main">
|
<div className="flex items-center gap-2 mr-4">
|
||||||
{title}
|
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
|
||||||
</h2>
|
<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 && (
|
{showCloseButton && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
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>
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -91,7 +98,7 @@ export default function Modal({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
{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}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
48
src/shared/components/SegmentedControl.js
Normal file
48
src/shared/components/SegmentedControl.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,14 +30,14 @@ export default function Select({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full py-2.5 px-4 pr-10 text-sm text-text-main",
|
"w-full py-2 px-3 pr-10 text-sm text-text-main",
|
||||||
"bg-surface border rounded-lg appearance-none",
|
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md appearance-none",
|
||||||
"focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none",
|
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
|
||||||
"transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed",
|
"transition-all disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
"text-[16px] sm:text-sm",
|
"text-[16px] sm:text-sm",
|
||||||
error
|
error
|
||||||
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
||||||
: "border-border",
|
: "",
|
||||||
selectClassName
|
selectClassName
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -62,9 +62,16 @@ export default function Sidebar({ onClose }) {
|
|||||||
|
|
||||||
return (
|
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 */}
|
{/* Logo */}
|
||||||
<div className="p-8">
|
<div className="px-6 py-4">
|
||||||
<Link href="/dashboard" className="flex items-center gap-3">
|
<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]">
|
<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>
|
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
|
||||||
@@ -83,15 +90,15 @@ export default function Sidebar({ onClose }) {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
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)
|
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"
|
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"material-symbols-outlined text-[20px]",
|
"material-symbols-outlined text-[18px]",
|
||||||
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
|
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) */}
|
{/* Debug section (only show when ENABLE_REQUEST_LOGS=true) */}
|
||||||
{showDebug && (
|
{showDebug && (
|
||||||
<div className="pt-6 mt-2">
|
<div className="pt-4 mt-2">
|
||||||
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
|
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
|
||||||
Debug
|
Debug
|
||||||
</p>
|
</p>
|
||||||
{debugItems.map((item) => (
|
{debugItems.map((item) => (
|
||||||
@@ -113,15 +120,15 @@ export default function Sidebar({ onClose }) {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
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)
|
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"
|
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"material-symbols-outlined text-[20px]",
|
"material-symbols-outlined text-[18px]",
|
||||||
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
|
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -134,8 +141,8 @@ export default function Sidebar({ onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* System section */}
|
{/* System section */}
|
||||||
<div className="pt-6 mt-2">
|
<div className="pt-4 mt-2">
|
||||||
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
|
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
|
||||||
System
|
System
|
||||||
</p>
|
</p>
|
||||||
{systemItems.map((item) => (
|
{systemItems.map((item) => (
|
||||||
@@ -144,13 +151,18 @@ export default function Sidebar({ onClose }) {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
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)
|
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"
|
: "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}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">{item.label}</span>
|
<span className="text-sm font-medium">{item.label}</span>
|
||||||
@@ -160,11 +172,11 @@ export default function Sidebar({ onClose }) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Footer section */}
|
{/* 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 */}
|
{/* Info message */}
|
||||||
<div className="flex items-start gap-3 p-3 rounded-xl bg-surface border border-border mb-3">
|
<div className="flex items-start gap-2 p-2 rounded-lg bg-surface/50 mb-2">
|
||||||
<div className="flex items-center justify-center size-8 rounded-lg bg-blue-500/10 text-blue-500 shrink-0 mt-0.5">
|
<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-[18px]">info</span>
|
<span className="material-symbols-outlined text-[14px]">info</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs font-medium text-text-main leading-relaxed">
|
<span className="text-xs font-medium text-text-main leading-relaxed">
|
||||||
|
|||||||
@@ -52,11 +52,10 @@ export default function Toggle({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex shrink-0 cursor-pointer rounded-full",
|
"relative inline-flex shrink-0 cursor-pointer rounded-full",
|
||||||
"transition-colors duration-200 ease-in-out",
|
"transition-colors duration-200 ease-in-out",
|
||||||
"focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2",
|
"focus:outline-none focus:ring-1 focus:ring-primary/30",
|
||||||
"dark:focus:ring-offset-surface-dark",
|
|
||||||
checked
|
checked
|
||||||
? "bg-primary"
|
? "bg-primary"
|
||||||
: "bg-border",
|
: "bg-black/10 dark:bg-white/20",
|
||||||
sizes[size].track,
|
sizes[size].track,
|
||||||
disabled && "cursor-not-allowed"
|
disabled && "cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export { default as RequestLogger } from "./RequestLogger";
|
|||||||
export { default as KiroAuthModal } from "./KiroAuthModal";
|
export { default as KiroAuthModal } from "./KiroAuthModal";
|
||||||
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
|
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
|
||||||
export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
|
export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
|
||||||
|
export { default as SegmentedControl } from "./SegmentedControl";
|
||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
export * from "./layouts";
|
export * from "./layouts";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function DashboardLayout({ children }) {
|
|||||||
{/* Mobile sidebar overlay */}
|
{/* Mobile sidebar overlay */}
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div
|
<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)}
|
onClick={() => setSidebarOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export const COLORS = {
|
|||||||
bg: "#FBF9F6",
|
bg: "#FBF9F6",
|
||||||
bgAlt: "#F5F1ED",
|
bgAlt: "#F5F1ED",
|
||||||
surface: "#FFFFFF",
|
surface: "#FFFFFF",
|
||||||
sidebar: "#F0EFEC",
|
sidebar: "rgba(246, 246, 246, 0.8)",
|
||||||
border: "#E6E4DD",
|
border: "rgba(0, 0, 0, 0.1)",
|
||||||
textMain: "#383733",
|
textMain: "#383733",
|
||||||
textMuted: "#75736E",
|
textMuted: "#75736E",
|
||||||
},
|
},
|
||||||
@@ -27,8 +27,8 @@ export const COLORS = {
|
|||||||
bg: "#191918",
|
bg: "#191918",
|
||||||
bgAlt: "#1F1F1E",
|
bgAlt: "#1F1F1E",
|
||||||
surface: "#242423",
|
surface: "#242423",
|
||||||
sidebar: "#1F1F1E",
|
sidebar: "rgba(30, 30, 30, 0.8)",
|
||||||
border: "#333331",
|
border: "rgba(255, 255, 255, 0.1)",
|
||||||
textMain: "#ECEBE8",
|
textMain: "#ECEBE8",
|
||||||
textMuted: "#9E9D99",
|
textMuted: "#9E9D99",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user