diff --git a/apps/arc-web/app/routes/settings.tsx b/apps/arc-web/app/routes/settings.tsx index 776ba234e..e8fb8073f 100644 --- a/apps/arc-web/app/routes/settings.tsx +++ b/apps/arc-web/app/routes/settings.tsx @@ -1,3 +1,11 @@ +import { useState } from "react"; +import { + Cog6ToothIcon, + CodeBracketIcon, + CpuChipIcon, + BellIcon, + ShieldCheckIcon, +} from "@heroicons/react/24/outline"; import type { Route } from "./+types/settings"; export function meta({}: Route.MetaArgs) { @@ -19,6 +27,8 @@ interface SettingGroup { id: string; name: string; description: string; + icon: React.ComponentType<{ className?: string }>; + accentColor: string; fields: SettingField[]; } @@ -27,6 +37,8 @@ const settingGroups: SettingGroup[] = [ id: "general", name: "General", description: "Core platform settings and defaults.", + icon: Cog6ToothIcon, + accentColor: "teal", fields: [ { key: "org_name", label: "Organization name", value: "Acme Corp", type: "text" }, { key: "default_branch", label: "Default branch", value: "main", type: "text" }, @@ -38,6 +50,8 @@ const settingGroups: SettingGroup[] = [ id: "git", name: "Git & VCS", description: "Version control integration and repository settings.", + icon: CodeBracketIcon, + accentColor: "mint", fields: [ { key: "github_org", label: "GitHub organization", value: "acme-corp", type: "text" }, { key: "clone_protocol", label: "Clone protocol", value: "SSH", type: "select", options: ["SSH", "HTTPS"] }, @@ -50,6 +64,8 @@ const settingGroups: SettingGroup[] = [ id: "compute", name: "Compute", description: "Resource allocation and execution environment.", + icon: CpuChipIcon, + accentColor: "amber", fields: [ { key: "default_cpu", label: "Default CPU", value: "4", type: "select", options: ["2", "4", "8", "16"] }, { key: "default_memory", label: "Default memory", value: "8 GB", type: "select", options: ["4 GB", "8 GB", "16 GB", "32 GB"] }, @@ -62,6 +78,8 @@ const settingGroups: SettingGroup[] = [ id: "notifications", name: "Notifications", description: "Alerts and notification delivery preferences.", + icon: BellIcon, + accentColor: "teal", fields: [ { key: "slack_webhook", label: "Slack webhook URL", value: "https://hooks.slack.com/services/T00/B00/xxxx", type: "text" }, { key: "notify_on_failure", label: "Notify on failure", value: "true", type: "toggle" }, @@ -74,6 +92,8 @@ const settingGroups: SettingGroup[] = [ id: "security", name: "Security", description: "Access control and security policies.", + icon: ShieldCheckIcon, + accentColor: "coral", fields: [ { key: "sso_provider", label: "SSO provider", value: "Okta", type: "select", options: ["None", "Okta", "Azure AD", "Google Workspace", "OneLogin"] }, { key: "mfa_required", label: "Require MFA", value: "true", type: "toggle" }, @@ -84,24 +104,52 @@ const settingGroups: SettingGroup[] = [ }, ]; -function ToggleSwitch({ enabled }: { enabled: boolean }) { +const accentMap: Record = { + teal: { + icon: "text-teal-500", + glow: "bg-teal-500/8", + toggle: "bg-teal-500", + border: "border-teal-500/20", + }, + mint: { + icon: "text-mint", + glow: "bg-mint/8", + toggle: "bg-mint", + border: "border-mint/20", + }, + amber: { + icon: "text-amber", + glow: "bg-amber/8", + toggle: "bg-amber", + border: "border-amber/20", + }, + coral: { + icon: "text-coral", + glow: "bg-coral/8", + toggle: "bg-coral", + border: "border-coral/20", + }, +}; + +function ToggleSwitch({ enabled, accent }: { enabled: boolean; accent: string }) { + const colors = accentMap[accent]; return ( ); } -function SettingRow({ field }: { field: SettingField }) { +function SettingRow({ field, accent, isLast }: { field: SettingField; accent: string; isLast: boolean }) { return ( -
+

{field.label}

{field.description != null && ( @@ -111,11 +159,11 @@ function SettingRow({ field }: { field: SettingField }) {
{field.type === "toggle" ? ( - + ) : field.type === "select" ? ( )}
@@ -133,28 +181,103 @@ function SettingRow({ field }: { field: SettingField }) { ); } -function SettingsSection({ group }: { group: SettingGroup }) { +function SettingsSection({ group, isActive, onVisible }: { group: SettingGroup; isActive: boolean; onVisible: () => void }) { + const colors = accentMap[group.accentColor]; + const Icon = group.icon; + return ( -
-
-

{group.name}

-

{group.description}

+
{ + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { if (entry.isIntersecting) onVisible(); }, + { rootMargin: "-40% 0px -50% 0px" } + ); + observer.observe(el); + return () => observer.disconnect(); + }} + className={`rounded-xl border transition-colors duration-300 ${ + isActive + ? `${colors.border} bg-navy-800/60` + : "border-white/[0.06] bg-navy-800/40" + }`} + > +
+
+ +
+
+

{group.name}

+

{group.description}

+
-
- {group.fields.map((field) => ( - +
+ {group.fields.map((field, i) => ( + ))}
); } -export default function Settings() { +function SidebarNav({ activeId }: { activeId: string }) { return ( -
- {settingGroups.map((group) => ( - - ))} + + ); +} + +export default function Settings() { + const [activeSection, setActiveSection] = useState(settingGroups[0].id); + + return ( +
+ +
+ {settingGroups.map((group) => ( + setActiveSection(group.id)} + /> + ))} +
); }