From a7885f074d40c2de2a652d5542e4ee0632359698 Mon Sep 17 00:00:00 2001 From: lunaxoxooo Date: Mon, 1 Jun 2026 06:05:38 +0000 Subject: [PATCH] feat(settings): responsive settings layout and reusable confirmation dialog --- UI-DESIGN.md | 43 ++++++++++++ .../__tests__/confirmation-dialog.test.tsx | 43 ++++++++++++ .../app/settings/components/APIKeyManager.tsx | 63 ++++++++++++----- .../settings/components/NotificationPrefs.tsx | 16 +++-- .../app/settings/components/ThemeSelector.tsx | 6 +- soroscan-frontend/app/settings/page.tsx | 69 ++++++++----------- .../components/ui/confirmation-dialog.tsx | 67 ++++++++++++++++++ 7 files changed, 238 insertions(+), 69 deletions(-) create mode 100644 soroscan-frontend/__tests__/confirmation-dialog.test.tsx create mode 100644 soroscan-frontend/components/ui/confirmation-dialog.tsx diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 2db7ff960..30c87cd38 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -13,6 +13,23 @@ --- +## 🧩 Figma Source + +Figma file: https://www.figma.com/file/qkTJWD2iKj4W2BVztdCrHt/SoroScan-UI-Design?node-id=0-1 + +This repository follows the Figma master file as the single source of truth for colors, typography, spacing, and component behavior. + +--- + +## 📱 Responsive System + +- Mobile: single-column stacked cards, large touch targets, high-contrast controls +- Tablet: grouped panels with compact button rows +- Desktop: wider layouts with side-by-side sections and status surfaces +- Breakpoints: mobile first, then `sm` and `md` scale-up layouts across the UI + +--- + ## 📐 Design System Foundation ### Color Palette @@ -226,6 +243,32 @@ SPECS └──────────────────────────────────────────────────────────┘ ``` +## 📊 Comparison & Diff View + +``` +┌───────────────────────────────────────────────────────────────────────────┐ +│ [EVENT A] │ [EVENT B] │ +├───────────────────────────────────────────────────────────────────────────┤ +│ Timestamp: 2026-05-29 12:24 │ Timestamp: 2026-05-29 12:44 │ +│ Event Type: Transfer │ Event Type: Transfer │ +├───────────────────────────────────────────────────────────────────────────┤ +│ Field │ A Value │ B Value │ Status │ +├───────────────────────────────────────────────────────────────────────────┤ +│ from │ 0xabc...123 │ 0xabc...123 │ unchanged │ +│ to │ 0xdef...456 │ 0xdef...789 │ modified │ +│ amount │ 100 │ 250 │ modified │ +│ memo │ "swap" │ — │ deleted │ +│ reward │ — │ 10 │ added │ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +- Side-by-side layout shows event details in parallel columns. +- Additions: green background, deletions: red, modified: amber, unchanged: neutral. +- Alternative unified diff option uses inline change markers (+/−) with syntax-highlighted JSON/code blocks. +- Mobile stacks event sections vertically with clear headers and toggle buttons to switch between A/B views. +- Includes summary metrics: lines added, removed, changed. +- Supports expandable rows for large payloads and collapsed sections for unchanged fields. + ### 5. Modal / Dialog ``` diff --git a/soroscan-frontend/__tests__/confirmation-dialog.test.tsx b/soroscan-frontend/__tests__/confirmation-dialog.test.tsx new file mode 100644 index 000000000..99d262606 --- /dev/null +++ b/soroscan-frontend/__tests__/confirmation-dialog.test.tsx @@ -0,0 +1,43 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; + +describe("ConfirmationDialog", () => { + it("renders title, description, and buttons", () => { + render( + + ); + + expect(screen.getByText("Delete API key")).toBeInTheDocument(); + expect(screen.getByText("This action cannot be undone.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /revoke/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /keep/i })).toBeInTheDocument(); + }); + + it("calls callbacks when buttons are clicked", () => { + const onConfirm = jest.fn(); + const onCancel = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByRole("button", { name: /cancel/i })); + expect(onCancel).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole("button", { name: /confirm/i })); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); +}); diff --git a/soroscan-frontend/app/settings/components/APIKeyManager.tsx b/soroscan-frontend/app/settings/components/APIKeyManager.tsx index c44e10476..5d1fc8860 100644 --- a/soroscan-frontend/app/settings/components/APIKeyManager.tsx +++ b/soroscan-frontend/app/settings/components/APIKeyManager.tsx @@ -1,5 +1,6 @@ "use client"; import { useState } from "react"; +import { ConfirmationDialog } from "@/components/ui/confirmation-dialog"; type APIKey = { id: string; @@ -22,6 +23,8 @@ export default function APIKeyManager() { return saved ? JSON.parse(saved) : []; }); const [copied, setCopied] = useState(null); + const [confirmingKey, setConfirmingKey] = useState(null); + const [isRevoking, setIsRevoking] = useState(false); // Commented out useEffect to avoid localStorage dependency // useEffect(() => { @@ -38,13 +41,24 @@ export default function APIKeyManager() { const newKey: APIKey = { id: Date.now().toString(), key: generateKey(), - createdAt: new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + createdAt: new Date().toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), }; saveKeys([...keys, newKey]); }; - const handleRevoke = (id: string) => { - saveKeys(keys.filter((k) => k.id !== id)); + const requestRevoke = (id: string) => setConfirmingKey(id); + + const handleConfirmRevoke = () => { + if (!confirmingKey) return; + setIsRevoking(true); + const nextKeys = keys.filter((k) => k.id !== confirmingKey); + saveKeys(nextKeys); + setConfirmingKey(null); + setIsRevoking(false); }; const handleCopy = (key: string) => { @@ -54,47 +68,58 @@ export default function APIKeyManager() { }; return ( -
+

[ API KEYS ]

{keys.length === 0 ? (

No API keys yet.

) : ( -
-
+
+
KEY CREATED - ACTIONS + ACTIONS
{keys.map((k) => ( -
- - {k.key.slice(0, 16)}... - - {k.createdAt} -
+
+
{k.key.slice(0, 16)}…
+
{k.createdAt}
+
))}
)} + setConfirmingKey(null)} + loading={isRevoking} + />
); } diff --git a/soroscan-frontend/app/settings/components/NotificationPrefs.tsx b/soroscan-frontend/app/settings/components/NotificationPrefs.tsx index 439f11c12..12ff0abd3 100644 --- a/soroscan-frontend/app/settings/components/NotificationPrefs.tsx +++ b/soroscan-frontend/app/settings/components/NotificationPrefs.tsx @@ -29,7 +29,7 @@ export default function NotificationPrefs() { }; return ( -
+

[ NOTIFICATIONS ]

{([ @@ -37,14 +37,18 @@ export default function NotificationPrefs() { { key: "inApp", label: "In-App Notifications" }, { key: "webhook", label: "Webhook" }, ] as const).map(({ key, label }) => ( -
+
{label} diff --git a/soroscan-frontend/app/settings/components/ThemeSelector.tsx b/soroscan-frontend/app/settings/components/ThemeSelector.tsx index cbaba8f94..bfa5fcb1f 100644 --- a/soroscan-frontend/app/settings/components/ThemeSelector.tsx +++ b/soroscan-frontend/app/settings/components/ThemeSelector.tsx @@ -18,14 +18,14 @@ export default function ThemeSelector() { }; return ( -
+

[ THEME ]

-
+
{(["dark", "light"] as const).map((t) => (
-
+ - {/* Notifications */} - - {/* API Keys */}
diff --git a/soroscan-frontend/components/ui/confirmation-dialog.tsx b/soroscan-frontend/components/ui/confirmation-dialog.tsx new file mode 100644 index 000000000..32435a7c0 --- /dev/null +++ b/soroscan-frontend/components/ui/confirmation-dialog.tsx @@ -0,0 +1,67 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { + Modal, + ModalContent, + ModalHeader, + ModalTitle, + ModalDescription, +} from "@/components/ui/modal"; + +interface ConfirmationDialogProps { + open: boolean; + title?: string; + description?: string; + confirmText?: string; + cancelText?: string; + loading?: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export function ConfirmationDialog({ + open, + title = "Confirm action", + description, + confirmText = "Confirm", + cancelText = "Cancel", + loading = false, + onConfirm, + onCancel, +}: ConfirmationDialogProps) { + return ( + !isOpen && onCancel()}> + + + {title} + {description ? ( + {description} + ) : null} + + +
+
+ + +
+
+
+
+ ); +}