From 659ee297133ba0d773172e9aa555a6f459780d09 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 30 May 2026 17:55:25 +0530 Subject: [PATCH 1/6] feat: add loading state component to MonitoringPanel for improved user experience --- .../monitoring/components/MonitoringPanel.tsx | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/features/monitoring/components/MonitoringPanel.tsx b/src/features/monitoring/components/MonitoringPanel.tsx index 05a15eb..d4c7a4d 100644 --- a/src/features/monitoring/components/MonitoringPanel.tsx +++ b/src/features/monitoring/components/MonitoringPanel.tsx @@ -113,6 +113,8 @@ export function MonitoringPanel({ dbId, databaseName, databaseType }: Monitoring return "text-emerald-500"; }, [data]); + const isInitialLoading = !data && !error && (isConnecting || isStreaming || state === "idle"); + if (!supported) { return (
@@ -170,10 +172,8 @@ export function MonitoringPanel({ dbId, databaseName, databaseType }: Monitoring )} - {isConnecting && !data ? ( -
- -
+ {isInitialLoading ? ( + ) : data ? ( ) : null} @@ -182,6 +182,71 @@ export function MonitoringPanel({ dbId, databaseName, databaseType }: Monitoring ); } +function MonitoringLoadingState() { + return ( +
+
+ {Array.from({ length: 4 }).map((_, index) => ( + + +
+
+
+
+
+
+
+ + +
+ {index === 1 ?
: null} + + + ))} +
+ +
+ + +
+
+
+
+
+
+
+ + +
+ +

Connecting to live monitoring stream

+
+
+ + + + +
+
+
+
+ + + {Array.from({ length: 5 }).map((_, index) => ( +
+
+
+
+
+
+ ))} + + +
+
+ ); +} + function MonitoringDashboard({ data, history, From 6c355aafe86d11daa3d94a2cb44371f1aac01a4d Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 30 May 2026 18:34:17 +0530 Subject: [PATCH 2/6] feat: enhance spinner size and alignment in WelcomeView for better visibility --- src/features/home/components/WelcomeView.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/features/home/components/WelcomeView.tsx b/src/features/home/components/WelcomeView.tsx index ec1358a..cb97e87 100644 --- a/src/features/home/components/WelcomeView.tsx +++ b/src/features/home/components/WelcomeView.tsx @@ -14,8 +14,7 @@ import { WelcomeViewProps } from "../types"; import { formatRelativeTime } from "../utils"; import { DiscoveredDatabasesCard } from "./DiscoveredDatabasesCard"; import { Spinner } from "@/components/ui/spinner"; -import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardAction, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; const DB_COLORS: Record = { @@ -99,7 +98,9 @@ export function WelcomeView({ Total Tables {statsLoading ? ( - +
+ +
) : ( totalTables )} @@ -117,14 +118,16 @@ export function WelcomeView({ Data Size {statsLoading ? ( - +
+ +
) : ( totalSize )}
- + From be5e9d3e5236cb9d6690ead8f8285748050e6e14 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 30 May 2026 21:21:29 +0530 Subject: [PATCH 3/6] feat: Added Vs-code and Cyberpunk based themes --- src/App.css | 86 +++++++++ .../settings/components/ColorVariant.tsx | 182 ++++++++++++++---- .../settings/hooks/useThemeVariant.ts | 77 +++++++- src/lib/themes.ts | 139 ++++++++++++- 4 files changed, 442 insertions(+), 42 deletions(-) diff --git a/src/App.css b/src/App.css index a491fc1..478afdb 100644 --- a/src/App.css +++ b/src/App.css @@ -197,6 +197,92 @@ body, --ring: oklch(0.72 0.2 10); } +/* ── Full-palette theme: Cyberpunk ─────────────────────────────────────── */ +/* These serve as a cascade-level backup. The hook also injects inline styles + on :root so they take precedence over .dark rules without needing specificity + tricks. */ +[data-theme-variant="cyberpunk"], +.dark[data-theme-variant="cyberpunk"] { + --background: #070b14; + --foreground: #e2f0ff; + --card: #0d1526; + --card-foreground: #e2f0ff; + --popover: #0d1526; + --popover-foreground: #e2f0ff; + --primary: #00f5ff; + --primary-foreground: #030c12; + --secondary: #0f1e35; + --secondary-foreground: #a8cfef; + --muted: #0d1a2e; + --muted-foreground: #6a8faf; + --accent: #ff00c8; + --accent-foreground: #fff; + --destructive: #ff3366; + --border: #0e2540; + --input: #0a1a2e; + --ring: #00f5ff; + --sidebar: #060d1c; + --sidebar-foreground: #cde8ff; + --sidebar-primary: #00f5ff; + --sidebar-primary-foreground: #030c12; + --sidebar-accent: #0f2240; + --sidebar-accent-foreground: #00f5ff; + --sidebar-border: rgba(0,245,255,0.08); + --sidebar-ring: #00f5ff; + --card-shadow: 0 0 0 1px rgba(0,245,255,0.06), 0 8px 32px rgba(0,0,0,0.6); + --card-shadow-hover: 0 0 0 1px rgba(0,245,255,0.14), 0 12px 44px rgba(0,0,0,0.7); + --glass-bg: rgba(7,11,20,0.85); + --surface: #0a1220; + --surface-elevated: #0d1526; + --surface-subtle: #0f1d32; + --border-subtle: rgba(0,245,255,0.07); +} + +/* Cyberpunk neon glow on focused/primary interactive elements */ +[data-theme-variant="cyberpunk"] button[data-slot="button"]:not([data-variant="ghost"]):not([data-variant="outline"]):focus-visible, +[data-theme-variant="cyberpunk"] [role="button"]:focus-visible { + box-shadow: 0 0 0 2px #00f5ff, 0 0 12px rgba(0,245,255,0.4); +} + +/* ── Full-palette theme: VS Code ──────────────────────────────────────── */ +[data-theme-variant="vscode"], +.dark[data-theme-variant="vscode"] { + --background: #1e1e1e; + --foreground: #d4d4d4; + --card: #252526; + --card-foreground: #d4d4d4; + --popover: #2d2d30; + --popover-foreground: #d4d4d4; + --primary: #569cd6; + --primary-foreground: #1e1e1e; + --secondary: #2d2d30; + --secondary-foreground: #cccccc; + --muted: #333333; + --muted-foreground: #808080; + --accent: #264f78; + --accent-foreground: #d4d4d4; + --destructive: #f44747; + --border: #3e3e42; + --input: #3c3c3c; + --ring: #569cd6; + --sidebar: #252526; + --sidebar-foreground: #bbbbbb; + --sidebar-primary: #569cd6; + --sidebar-primary-foreground: #1e1e1e; + --sidebar-accent: #37373d; + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #3e3e42; + --sidebar-ring: #569cd6; + --card-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px rgba(0,0,0,0.4); + --card-shadow-hover: 0 1px 0 rgba(255,255,255,0.06) inset, 0 12px 32px rgba(0,0,0,0.5); + --glass-bg: rgba(37,37,38,0.92); + --surface: #252526; + --surface-elevated: #2d2d30; + --surface-subtle: #333333; + --border-subtle: #3e3e42; +} + + @layer base { * { @apply border-border outline-ring/50; diff --git a/src/features/settings/components/ColorVariant.tsx b/src/features/settings/components/ColorVariant.tsx index 3789324..feb5336 100644 --- a/src/features/settings/components/ColorVariant.tsx +++ b/src/features/settings/components/ColorVariant.tsx @@ -1,58 +1,160 @@ import { useThemeVariant } from '@/features/settings/hooks/useThemeVariant'; import { themeVariants, ThemeVariant } from "@/lib/themes"; -import { Check, Palette } from 'lucide-react'; +import { Check, Palette, Layers } from 'lucide-react'; -export default function ColorVariant() { +const ACCENT_THEMES: ThemeVariant[] = ['blue', 'slate', 'green', 'purple', 'orange', 'rose']; +const FULL_THEMES: ThemeVariant[] = ['cyberpunk', 'vscode']; + +interface ThemeCardProps { + themeKey: ThemeVariant; + isActive: boolean; + onSelect: () => void; +} + +function ThemeCard({ themeKey, isActive, onSelect }: ThemeCardProps) { + const config = themeVariants[themeKey]; + + if (config.fullPalette) { + // Rich preview card for full-palette themes + return ( + + ); + } + + // Simple swatch card for accent-only themes + return ( + + ); +} +export default function ColorVariant() { const { variant, setVariant } = useThemeVariant(); + return ( -
-
+
+
-

Accent Color

+

Theme

- Select your preferred color theme + Choose an accent color or a full UI theme

-
- {Object.entries(themeVariants).map(([key, config]) => { - const isActive = variant === key; + {/* Accent colors */} +
+

+ Accent Colors +

+
+ {ACCENT_THEMES.map((key) => ( + setVariant(key)} + /> + ))} +
+
- return ( - - ); - })} + themeKey={key} + isActive={variant === key} + onSelect={() => setVariant(key)} + /> + ))} +
- ) + ); } diff --git a/src/features/settings/hooks/useThemeVariant.ts b/src/features/settings/hooks/useThemeVariant.ts index 4cb1753..9d386d1 100644 --- a/src/features/settings/hooks/useThemeVariant.ts +++ b/src/features/settings/hooks/useThemeVariant.ts @@ -1,8 +1,69 @@ import { useEffect, useState } from 'react'; -import { ThemeVariant, defaultVariant } from '@/lib/themes'; +import { ThemeVariant, ThemePalette, defaultVariant, themeVariants } from '@/lib/themes'; const STORAGE_KEY = 'relwave-theme-variant'; +/** CSS variable names that map 1-to-1 to ThemePalette keys */ +const PALETTE_CSS_VARS: Array<[keyof ThemePalette, string]> = [ + ['background', '--background'], + ['foreground', '--foreground'], + ['card', '--card'], + ['cardForeground', '--card-foreground'], + ['popover', '--popover'], + ['popoverForeground', '--popover-foreground'], + ['primary', '--primary'], + ['primaryForeground', '--primary-foreground'], + ['secondary', '--secondary'], + ['secondaryForeground', '--secondary-foreground'], + ['muted', '--muted'], + ['mutedForeground', '--muted-foreground'], + ['accent', '--accent'], + ['accentForeground', '--accent-foreground'], + ['destructive', '--destructive'], + ['border', '--border'], + ['input', '--input'], + ['ring', '--ring'], + ['sidebar', '--sidebar'], + ['sidebarForeground', '--sidebar-foreground'], + ['sidebarAccent', '--sidebar-accent'], + ['sidebarAccentForeground', '--sidebar-accent-foreground'], + ['sidebarBorder', '--sidebar-border'], + ['cardShadow', '--card-shadow'], + ['glassBg', '--glass-bg'], + ['surface', '--surface'], + ['surfaceElevated', '--surface-elevated'], + ['surfaceSubtle', '--surface-subtle'], + ['borderSubtle', '--border-subtle'], +]; + +/** CSS variable names that the data-theme-variant attribute overrides for simple accent themes */ +const ACCENT_ONLY_VARS = ['--primary', '--primary-foreground', '--ring', '--sidebar-primary', '--sidebar-ring']; + +/** All CSS var names that a full-palette theme can set (used for cleanup when switching away) */ +const ALL_PALETTE_CSS_VARS = PALETTE_CSS_VARS.map(([, cssVar]) => cssVar); + +function applyPalette(root: HTMLElement, palette: ThemePalette) { + for (const [key, cssVar] of PALETTE_CSS_VARS) { + const value = palette[key]; + if (value !== undefined) { + root.style.setProperty(cssVar, value as string); + } + } + // Sidebar primary/ring mirror the primary by default + if (palette.primary) { + root.style.setProperty('--sidebar-primary', palette.primary); + root.style.setProperty('--sidebar-ring', palette.ring ?? palette.primary); + } +} + +function clearPalette(root: HTMLElement) { + for (const cssVar of ALL_PALETTE_CSS_VARS) { + root.style.removeProperty(cssVar); + } + root.style.removeProperty('--sidebar-primary'); + root.style.removeProperty('--sidebar-ring'); +} + export function useThemeVariant() { const [variant, setVariantState] = useState(() => { const stored = localStorage.getItem(STORAGE_KEY); @@ -11,7 +72,21 @@ export function useThemeVariant() { useEffect(() => { const root = document.documentElement; + const config = themeVariants[variant]; + + // Always update the data attribute so CSS [data-theme-variant] selectors still work root.setAttribute('data-theme-variant', variant); + + if (config.fullPalette && config.palette) { + // Full-palette theme: inject all variables via inline style overrides. + // Clear first to remove any previously set accent-only vars. + clearPalette(root); + applyPalette(root, config.palette); + } else { + // Accent-only theme: remove any full-palette overrides from a previous + // full-palette theme so the base :root / .dark variables take over again. + clearPalette(root); + } }, [variant]); const setVariant = (newVariant: ThemeVariant) => { diff --git a/src/lib/themes.ts b/src/lib/themes.ts index d79dbab..750630e 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -1,13 +1,56 @@ -export type ThemeVariant = 'blue' | 'slate' | 'green' | 'purple' | 'orange' | 'rose'; +export type ThemeVariant = + | 'blue' | 'slate' | 'green' | 'purple' | 'orange' | 'rose' + | 'cyberpunk' | 'vscode'; export interface ThemeVariantConfig { name: string; + description?: string; + /** Swatch color shown in the picker */ primary: string; primaryForeground: string; ring: string; + /** If true, this theme overrides the full palette (not just the accent) */ + fullPalette?: boolean; + /** Dark-mode palette override. Only used when fullPalette=true. */ + palette?: ThemePalette; + /** Optional gradient/icon style for the picker card */ + previewGradient?: string; +} + +export interface ThemePalette { + background: string; + foreground: string; + card: string; + cardForeground: string; + popover: string; + popoverForeground: string; + primary: string; + primaryForeground: string; + secondary: string; + secondaryForeground: string; + muted: string; + mutedForeground: string; + accent: string; + accentForeground: string; + destructive: string; + border: string; + input: string; + ring: string; + sidebar: string; + sidebarForeground: string; + sidebarAccent: string; + sidebarAccentForeground: string; + sidebarBorder: string; + cardShadow?: string; + glassBg?: string; + surface?: string; + surfaceElevated?: string; + surfaceSubtle?: string; + borderSubtle?: string; } export const themeVariants: Record = { + // ── Standard accent-only themes ────────────────────────────────────────── blue: { name: 'Blue', primary: 'oklch(0.7 0.15 250)', @@ -44,6 +87,100 @@ export const themeVariants: Record = { primaryForeground: 'oklch(0.98 0 0)', ring: 'oklch(0.7 0.22 10)', }, + + // ── Full-palette themes ─────────────────────────────────────────────────── + + /** + * Cyberpunk + * Electric neon-cyan on a near-black background with magenta accents. + * Always dark — ignores the system light/dark toggle. + */ + cyberpunk: { + name: 'Cyberpunk', + description: 'Neon cyan & magenta on deep dark', + primary: '#00f5ff', + primaryForeground: '#000', + ring: '#00f5ff', + fullPalette: true, + previewGradient: 'linear-gradient(135deg, #00f5ff 0%, #ff00c8 100%)', + palette: { + background: '#070b14', + foreground: '#e2f0ff', + card: '#0d1526', + cardForeground: '#e2f0ff', + popover: '#0d1526', + popoverForeground: '#e2f0ff', + primary: '#00f5ff', + primaryForeground: '#030c12', + secondary: '#0f1e35', + secondaryForeground: '#a8cfef', + muted: '#0d1a2e', + mutedForeground: '#6a8faf', + accent: '#ff00c8', + accentForeground: '#fff', + destructive: '#ff3366', + border: '#0e2540', + input: '#0a1a2e', + ring: '#00f5ff', + sidebar: '#060d1c', + sidebarForeground: '#cde8ff', + sidebarAccent: '#0f2240', + sidebarAccentForeground: '#00f5ff', + sidebarBorder: 'rgba(0,245,255,0.08)', + cardShadow: '0 0 0 1px rgba(0,245,255,0.06), 0 8px 32px rgba(0,0,0,0.6)', + glassBg: 'rgba(7,11,20,0.85)', + surface: '#0a1220', + surfaceElevated: '#0d1526', + surfaceSubtle: '#0f1d32', + borderSubtle: 'rgba(0,245,255,0.07)', + }, + }, + + /** + * VS Code + * Faithful recreation of VS Code's default Dark+ palette. + * Deep navy backgrounds, cool gray text, blue accent. + */ + vscode: { + name: 'VS Code', + description: 'Inspired by VS Code Dark+', + primary: '#569cd6', + primaryForeground: '#1e1e1e', + ring: '#569cd6', + fullPalette: true, + previewGradient: 'linear-gradient(135deg, #1e1e1e 0%, #264f78 50%, #569cd6 100%)', + palette: { + background: '#1e1e1e', + foreground: '#d4d4d4', + card: '#252526', + cardForeground: '#d4d4d4', + popover: '#2d2d30', + popoverForeground: '#d4d4d4', + primary: '#569cd6', + primaryForeground: '#1e1e1e', + secondary: '#2d2d30', + secondaryForeground: '#cccccc', + muted: '#333333', + mutedForeground: '#808080', + accent: '#264f78', + accentForeground: '#d4d4d4', + destructive: '#f44747', + border: '#3e3e42', + input: '#3c3c3c', + ring: '#569cd6', + sidebar: '#252526', + sidebarForeground: '#bbbbbb', + sidebarAccent: '#37373d', + sidebarAccentForeground: '#ffffff', + sidebarBorder: '#3e3e42', + cardShadow: '0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px rgba(0,0,0,0.4)', + glassBg: 'rgba(37,37,38,0.92)', + surface: '#252526', + surfaceElevated: '#2d2d30', + surfaceSubtle: '#333333', + borderSubtle: '#3e3e42', + }, + }, }; export const defaultVariant: ThemeVariant = 'blue'; From 11be95390df4dded21bf0696a5376edafa91b54c Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 30 May 2026 23:01:17 +0530 Subject: [PATCH 4/6] feat: added valorant and ghibli studio theme --- src/App.css | 143 ++++++++++++++ .../settings/components/ColorVariant.tsx | 60 ++++-- .../settings/hooks/useThemeVariant.ts | 145 +++++++++----- src/lib/themes.ts | 184 +++++++++++++++++- 4 files changed, 466 insertions(+), 66 deletions(-) diff --git a/src/App.css b/src/App.css index 478afdb..125d27c 100644 --- a/src/App.css +++ b/src/App.css @@ -282,8 +282,151 @@ body, --border-subtle: #3e3e42; } +/* ── Full-palette theme: Valorant ────────────────────────────────────── */ +[data-theme-variant="valorant"] { + --background: oklch(0.97 0.02 12.78); + --foreground: oklch(0.24 0.07 17.81); + --card: oklch(0.98 0.01 17.28); + --card-foreground: oklch(0.26 0.07 19); + --popover: oklch(0.98 0.01 17.28); + --popover-foreground: oklch(0.26 0.07 19); + --primary: oklch(0.67 0.22 21.34); + --primary-foreground: oklch(0.99 0.00 359.99); + --secondary: oklch(0.95 0.02 11.28); + --secondary-foreground: oklch(0.24 0.07 17.81); + --muted: oklch(0.98 0.01 17.28); + --muted-foreground: oklch(0.55 0.08 19); + --accent: oklch(0.99 0.00 359.99); + --accent-foreground: oklch(0.43 0.13 20.62); + --destructive: oklch(0.80 0.17 73.27); + --border: oklch(0.91 0.05 11.40); + --input: oklch(0.90 0.05 12.59); + --ring: oklch(0.92 0.04 12.39); + --sidebar: oklch(0.97 0.02 12.78); + --sidebar-foreground: oklch(0.26 0.07 19); + --sidebar-primary: oklch(0.67 0.22 21.34); + --sidebar-primary-foreground: oklch(0.99 0.00 359.99); + --sidebar-accent: oklch(0.98 0.01 17.28); + --sidebar-accent-foreground: oklch(0.43 0.13 20.62); + --sidebar-border: oklch(0.91 0.05 11.40); + --sidebar-ring: oklch(0.92 0.04 12.39); + --card-shadow: 0px 0px 3px 0px oklch(0.3 0.0891 19.6 / 0.08), 0px 2px 4px -1px oklch(0.3 0.0891 19.6 / 0.08); + --glass-bg: rgba(253, 248, 247, 0.82); + --surface: oklch(0.97 0.02 12.78); + --surface-elevated: oklch(0.98 0.01 17.28); + --surface-subtle: oklch(0.95 0.02 11.28); + --border-subtle: oklch(0.91 0.05 11.40); +} + +.dark[data-theme-variant="valorant"] { + --background: oklch(0.16 0.03 17.48); + --foreground: oklch(0.99 0.00 359.99); + --card: oklch(0.21 0.05 19.26); + --card-foreground: oklch(0.98 0 0); + --popover: oklch(0.26 0.07 19); + --popover-foreground: oklch(0.99 0.00 359.99); + --primary: oklch(0.67 0.22 21.34); + --primary-foreground: oklch(0.99 0.00 359.99); + --secondary: oklch(0.3 0.0891 19.6); + --secondary-foreground: oklch(0.95 0.02 11.28); + --muted: oklch(0.26 0.07 19); + --muted-foreground: oklch(0.72 0.04 18); + --accent: oklch(0.43 0.13 20.62); + --accent-foreground: oklch(0.99 0.00 359.99); + --destructive: oklch(0.80 0.17 73.27); + --border: oklch(0.31 0.09 19.80); + --input: oklch(0.39 0.12 20.37); + --ring: oklch(0.50 0.16 20.89); + --sidebar: oklch(0.26 0.07 19); + --sidebar-foreground: oklch(0.99 0.00 359.99); + --sidebar-primary: oklch(0.67 0.22 21.34); + --sidebar-primary-foreground: oklch(0.99 0.00 359.99); + --sidebar-accent: oklch(0.43 0.13 20.62); + --sidebar-accent-foreground: oklch(0.99 0.00 359.99); + --sidebar-border: oklch(0.39 0.12 20.37); + --sidebar-ring: oklch(0.50 0.16 20.89); + --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.10), 0 2px 4px -1px oklch(0.00 0 0 / 0.10); + --glass-bg: rgba(33, 14, 12, 0.82); + --surface: oklch(0.21 0.05 19.26); + --surface-elevated: oklch(0.26 0.07 19); + --surface-subtle: oklch(0.3 0.0891 19.6); + --border-subtle: oklch(0.31 0.09 19.80); +} + +/* ── Full-palette theme: Ghibli Studio ──────────────────────────────── */ +[data-theme-variant="ghibli"] { + --background: oklch(0.91 0.05 82.69); + --foreground: oklch(0.41 0.08 79.04); + --card: oklch(0.92 0.04 83.86); + --card-foreground: oklch(0.41 0.08 73.75); + --popover: oklch(0.92 0.04 83.86); + --popover-foreground: oklch(0.41 0.08 73.75); + --primary: oklch(0.71 0.10 111.99); + --primary-foreground: oklch(0.98 0.01 3.71); + --secondary: oklch(0.88 0.05 83.41); + --secondary-foreground: oklch(0.51 0.08 79.21); + --muted: oklch(0.86 0.06 83.48); + --muted-foreground: oklch(0.51 0.08 74.26); + --accent: oklch(0.86 0.05 84.50); + --accent-foreground: oklch(0.26 0.02 358.42); + --destructive: oklch(0.63 0.24 29.21); + --border: oklch(0.74 0.06 79.81); + --input: oklch(0.74 0.06 79.81); + --ring: oklch(0.51 0.08 74.26); + --sidebar: oklch(0.87 0.06 83.96); + --sidebar-foreground: oklch(0.41 0.08 79.04); + --sidebar-primary: oklch(0.26 0.02 358.42); + --sidebar-primary-foreground: oklch(0.98 0.01 3.71); + --sidebar-accent: oklch(0.83 0.06 84.46); + --sidebar-accent-foreground: oklch(0.26 0.02 358.42); + --sidebar-border: oklch(0.91 0.00 0.43); + --sidebar-ring: oklch(0.71 0.00 0.37); + --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.08), 0 2px 4px -1px oklch(0.00 0 0 / 0.08); + --glass-bg: rgba(244, 237, 216, 0.80); + --surface: oklch(0.91 0.05 82.69); + --surface-elevated: oklch(0.92 0.04 83.86); + --surface-subtle: oklch(0.88 0.05 83.41); + --border-subtle: oklch(0.74 0.06 79.81); +} + +.dark[data-theme-variant="ghibli"] { + --background: oklch(0.20 0.01 48.35); + --foreground: oklch(0.88 0.05 79.26); + --card: oklch(0.25 0.01 56.14); + --card-foreground: oklch(0.88 0.05 79.26); + --popover: oklch(0.25 0.01 56.14); + --popover-foreground: oklch(0.88 0.05 79.26); + --primary: oklch(0.64 0.05 115.39); + --primary-foreground: oklch(0.98 0.01 3.71); + --secondary: oklch(0.33 0.02 60.70); + --secondary-foreground: oklch(0.88 0.05 83.41); + --muted: oklch(0.27 0.01 39.35); + --muted-foreground: oklch(0.74 0.06 79.81); + --accent: oklch(0.33 0.02 60.70); + --accent-foreground: oklch(0.86 0.05 84.50); + --destructive: oklch(0.63 0.24 29.21); + --border: oklch(0.33 0.02 60.70); + --input: oklch(0.33 0.02 60.70); + --ring: oklch(0.64 0.05 115.39); + --sidebar: oklch(0.23 0.01 56.09); + --sidebar-foreground: oklch(0.88 0.05 79.26); + --sidebar-primary: oklch(0.64 0.05 115.39); + --sidebar-primary-foreground: oklch(0.98 0.01 3.71); + --sidebar-accent: oklch(0.33 0.02 60.70); + --sidebar-accent-foreground: oklch(0.86 0.05 84.50); + --sidebar-border: oklch(0.33 0.02 60.70); + --sidebar-ring: oklch(0.64 0.05 115.39); + --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.20), 0 2px 4px -1px oklch(0.00 0 0 / 0.20); + --glass-bg: rgba(33, 25, 14, 0.82); + --surface: oklch(0.25 0.01 56.14); + --surface-elevated: oklch(0.27 0.01 39.35); + --surface-subtle: oklch(0.33 0.02 60.70); + --border-subtle: oklch(0.33 0.02 60.70); +} + @layer base { + * { @apply border-border outline-ring/50; } diff --git a/src/features/settings/components/ColorVariant.tsx b/src/features/settings/components/ColorVariant.tsx index feb5336..7de6011 100644 --- a/src/features/settings/components/ColorVariant.tsx +++ b/src/features/settings/components/ColorVariant.tsx @@ -3,7 +3,7 @@ import { themeVariants, ThemeVariant } from "@/lib/themes"; import { Check, Palette, Layers } from 'lucide-react'; const ACCENT_THEMES: ThemeVariant[] = ['blue', 'slate', 'green', 'purple', 'orange', 'rose']; -const FULL_THEMES: ThemeVariant[] = ['cyberpunk', 'vscode']; +const FULL_THEMES: ThemeVariant[] = ['cyberpunk', 'vscode', 'valorant', 'ghibli']; interface ThemeCardProps { themeKey: ThemeVariant; @@ -15,7 +15,11 @@ function ThemeCard({ themeKey, isActive, onSelect }: ThemeCardProps) { const config = themeVariants[themeKey]; if (config.fullPalette) { - // Rich preview card for full-palette themes + // Determine which palette(s) to draw from for the preview + const previewLight = config.lightPalette ?? config.palette; + const previewDark = config.darkPalette ?? config.palette; + const isDual = Boolean(config.lightPalette && config.darkPalette); + return ( ))}
- {/* Axis Configuration */} + {/* Axis + title controls */}
+ {/* X Axis */}
-
+ {/* Y Axis */}
-
+ {/* Title */}
-
diff --git a/src/features/chart/components/ChartRenderer.tsx b/src/features/chart/components/ChartRenderer.tsx index 4bb54d6..eaa9dc5 100644 --- a/src/features/chart/components/ChartRenderer.tsx +++ b/src/features/chart/components/ChartRenderer.tsx @@ -6,17 +6,20 @@ import { LineChart, Pie, PieChart, - Scatter, - ScatterChart, + Area, + AreaChart, XAxis, YAxis, CartesianGrid, Cell, + LabelList, } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent, + ChartLegend, + ChartLegendContent, type ChartConfig, } from "@/components/ui/chart"; @@ -26,21 +29,44 @@ interface DataProps { } interface ChartRendererProps { - chartType: "bar" | "line" | "pie" | "scatter"; + chartType: "bar" | "line" | "area" | "pie"; xAxis: string; yAxis: string; data: DataProps[]; } -// Single theme-aware color using CSS variable -const CHART_COLOR = "var(--primary)"; +// A curated palette using chart tokens so it respects the active theme +const PALETTE_VARS = [ + "var(--color-chart-1)", + "var(--color-chart-2)", + "var(--color-chart-3)", + "var(--color-chart-4)", + "var(--color-chart-5)", +]; -// Chart config using theme color -const chartConfig: ChartConfig = { - value: { - label: "Count", - color: "hsl(var(--primary))", - }, +const buildChartConfig = (data: { name: string; value: number }[]): ChartConfig => { + const cfg: ChartConfig = { + value: { label: "Count", color: "var(--color-chart-1)" }, + }; + data.forEach((item, i) => { + cfg[item.name] = { + label: item.name, + color: PALETTE_VARS[i % PALETTE_VARS.length], + }; + }); + return cfg; +}; + +const formatTick = (v: string | number) => { + if (typeof v === "number" && v >= 1000) return `${(v / 1000).toFixed(1)}k`; + if (typeof v === "string" && v.length > 14) return v.slice(0, 13) + "…"; + return v; +}; + +const axisStyle = { + fontSize: 11, + fill: "var(--color-muted-foreground)", + fontFamily: "var(--font-mono, ui-monospace)", }; const ChartRendererComponent = ({ @@ -51,154 +77,162 @@ const ChartRendererComponent = ({ }: ChartRendererProps) => { const chartData = useMemo(() => { if (!data || !Array.isArray(data) || !xAxis) return []; - return data.map((item) => ({ - name: item.name != null ? String(item.name) : "N/A", - value: Number(item.count ?? item.COUNT ?? item.Count ?? 0) || 0, - })); + return data + .map((item) => ({ + name: item.name != null ? String(item.name) : "N/A", + value: Number(item.count ?? item.COUNT ?? item.Count ?? 0) || 0, + })) + .slice(0, 40); }, [data, xAxis]); - if (!xAxis || chartData.length === 0) { - return ( -
-
- - - -
-

Configure axes to visualize

-
- ); - } + const chartConfig = useMemo(() => buildChartConfig(chartData), [chartData]); + + if (!xAxis || chartData.length === 0) return null; + + const commonProps = { accessibilityLayer: true, data: chartData }; + const commonGrid = ; + const commonXAxis = ( + + ); + const commonYAxis = ( + + ); + const commonTooltip = ( + } + /> + ); - // Bar Chart + // ── Bar Chart ──────────────────────────────────────────────────────────── if (chartType === "bar") { return ( - - - - - value.length > 12 ? value.slice(0, 12) + "…" : value - } - /> - - value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value - } - /> - } - /> + + + {commonGrid} + {commonXAxis} + {commonYAxis} + {commonTooltip} + radius={[6, 6, 0, 0]} + maxBarSize={56} + > + {chartData.map((_, i) => ( + + ))} + ); } - // Line Chart + // ── Line Chart ─────────────────────────────────────────────────────────── if (chartType === "line") { return ( - - - - - value.length > 12 ? value.slice(0, 12) + "…" : value - } - /> - - value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value - } - /> - } - /> + + + {commonGrid} + {commonXAxis} + {commonYAxis} + {commonTooltip} ); } - // Pie Chart - if (chartType === "pie") { + // ── Area Chart ─────────────────────────────────────────────────────────── + if (chartType === "area") { return ( - - - } - /> - + + + + + + + + {commonGrid} + {commonXAxis} + {commonYAxis} + {commonTooltip} + `${((percent ?? 0) * 100).toFixed(0)}%`} - labelLine={false} + type="monotone" + stroke="var(--color-chart-1)" + strokeWidth={2.5} + fill="url(#areaGradient)" + dot={{ fill: "var(--color-chart-1)", r: 3, strokeWidth: 0 }} + activeDot={{ r: 5, fill: "var(--color-chart-1)", strokeWidth: 0 }} /> - + ); } - // Scatter Chart - if (chartType === "scatter") { + // ── Pie / Donut Chart ──────────────────────────────────────────────────── + if (chartType === "pie") { + const top = chartData.slice(0, 8); return ( - - - - - + + } /> + - value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value - } - /> - } - /> - + {top.map((_, i) => ( + + ))} + + + } /> - + ); } diff --git a/src/features/chart/components/ChartVisualization.tsx b/src/features/chart/components/ChartVisualization.tsx index 2292703..8f1245d 100644 --- a/src/features/chart/components/ChartVisualization.tsx +++ b/src/features/chart/components/ChartVisualization.tsx @@ -1,63 +1,110 @@ -import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Loader2, BarChart3, Download, ChevronDown } from "lucide-react"; +import { + Loader2, + BarChart3, + Download, + ChevronDown, + Sparkles, + AlertCircle, +} from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Separator } from "@/components/ui/separator"; import { ChartConfigPanel } from "./ChartConfigPanel"; import ChartRenderer from "./ChartRenderer"; import { useChartVisualization } from "../hooks/useChartVisualization"; import { SelectedTable } from "@/features/database/types"; - +import { cn } from "@/lib/utils"; interface ChartVisualizationProps { selectedTable: SelectedTable; dbId?: string; } +export const ChartVisualization = ({ + selectedTable, + dbId, +}: ChartVisualizationProps) => { + const { + handleExport, + chartType, + chartTitle, + setChartTitle, + setChartType, + xAxis, + yAxis, + setXAxis, + setYAxis, + columnData, + isExecuting, + errorMessage, + rowData, + } = useChartVisualization(selectedTable, dbId); -export const ChartVisualization = ({ selectedTable, dbId }: ChartVisualizationProps) => { - - - - const { handleExport, chartType, chartTitle, setChartTitle, setChartType, xAxis, yAxis, setXAxis, setYAxis, columnData, isExecuting, errorMessage, rowData } = useChartVisualization(selectedTable, dbId); - - + const hasData = rowData.length > 0; + const isReady = !isExecuting && !errorMessage && hasData; return ( -
- {/* Config Panel */} -
-
-
-
- -
- Configure Chart +
+ {/* ── Header toolbar ─────────────────────────────────────────────── */} +
+
+
+ +
+
+

Visualize

+

+ {selectedTable?.name ?? "Select a table"} +

+
+ +
+ {/* Row count badge */} + {isReady && ( + + {rowData.length} rows + + )} - - - handleExport("png")} className="text-xs"> + + handleExport("png")} + className="gap-2" + > Export as PNG - handleExport("svg")} className="text-xs"> + handleExport("svg")} + className="gap-2" + > Export as SVG
+
+ {/* ── Config panel ───────────────────────────────────────────────── */} +
- {/* Chart Container */} + {/* ── Chart canvas ───────────────────────────────────────────────── */}
- {chartTitle && ( -

+ {/* Title */} + {chartTitle && isReady && ( +

{chartTitle} -

+

)} + {/* States */} {isExecuting ? ( -
- -

Processing data...

-
+ } + title="Processing…" + subtitle="Querying data" + /> ) : errorMessage ? ( -
-
- -
-

{errorMessage}

-
- ) : !rowData.length ? ( -
-
- -
-

Select axes to visualize data

-
+ } + title="Query error" + subtitle={errorMessage} + variant="error" + /> + ) : !xAxis || !yAxis ? ( + } + title="Configure axes" + subtitle="Select X and Y columns above to render the chart" + /> + ) : !hasData ? ( + } + title="No data returned" + subtitle="The query returned no rows for these axes" + /> ) : ( )} + + {/* Summary stats strip — shown only when data is ready */} + {isReady && ( + <> + +
+ + Number(r.count ?? 0)))} + /> + s + Number(r.count ?? 0), 0) / + rowData.length + ).toFixed(1)} + /> + s + Number(r.count ?? 0), 0)} + /> +
+ + )} +
+
+ ); +}; + +/* ── Small helpers ──────────────────────────────────────────────────────── */ + +function EmptyState({ + icon, + title, + subtitle, + variant = "default", +}: { + icon: React.ReactNode; + title: string; + subtitle?: string; + variant?: "default" | "error"; +}) { + return ( +
+
+ {icon}
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ ); +} + +function Stat({ label, value }: { label: string; value: string | number }) { + return ( +
+ + {typeof value === "number" && value >= 1000 + ? `${(value / 1000).toFixed(1)}k` + : value} + + + {label} +
); -}; \ No newline at end of file +} diff --git a/src/features/chart/hooks/useChartVisualization.ts b/src/features/chart/hooks/useChartVisualization.ts index f61867e..a30cb72 100644 --- a/src/features/chart/hooks/useChartVisualization.ts +++ b/src/features/chart/hooks/useChartVisualization.ts @@ -24,7 +24,7 @@ export interface QueryResultEventDetail { export const useChartVisualization = (selectedTable: SelectedTable, dbId?: string) => { - const [chartType, setChartType] = useState<"bar" | "line" | "pie" | "scatter">("bar"); + const [chartType, setChartType] = useState<"bar" | "line" | "area" | "pie">("bar"); const [xAxis, setXAxis] = useState(""); const [yAxis, setYAxis] = useState(""); const [chartTitle, setChartTitle] = useState("Query Results Visualization"); From 558a1c752ccaa2b9cea8a627d6a126a7349f52c9 Mon Sep 17 00:00:00 2001 From: Yash Date: Sun, 31 May 2026 18:26:34 +0530 Subject: [PATCH 6/6] refactor: added paths into test.yml --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c42add..0ebd8da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,15 @@ on: branches: - master - develop + paths: + - "bridge/**" + - ".github/workflows/test.yml" push: branches: - master + paths: + - "bridge/**" + - ".github/workflows/test.yml" jobs: test: