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: diff --git a/package.json b/package.json index e1654df..cbd81e7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "html-to-image": "^1.11.13", "lucide-react": "^0.554.0", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.9.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97661eb..9048824 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: ^19.1.0 version: 19.2.3 @@ -1350,66 +1353,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1479,24 +1495,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -1565,30 +1585,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.10.1': resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.10.1': resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.10.1': resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.10.1': resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} @@ -2327,24 +2352,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} diff --git a/src/App.css b/src/App.css index a491fc1..125d27c 100644 --- a/src/App.css +++ b/src/App.css @@ -197,7 +197,236 @@ 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; +} + +/* ── 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/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..db91e97 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import { Separator as SeparatorPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/features/chart/components/ChartConfigPanel.tsx b/src/features/chart/components/ChartConfigPanel.tsx index 415d52a..4b9b1d3 100644 --- a/src/features/chart/components/ChartConfigPanel.tsx +++ b/src/features/chart/components/ChartConfigPanel.tsx @@ -1,13 +1,13 @@ import React, { useMemo } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; -import { BarChart3, TrendingUp, PieChart, ScatterChart } from "lucide-react"; +import { BarChart3, TrendingUp, PieChart, AreaChart } from "lucide-react"; import { ColumnDetails } from '@/features/database/types'; import { cn } from "@/lib/utils"; interface ChartConfigPanelProps { - chartType: "bar" | "line" | "pie" | "scatter"; - setChartType: (type: "bar" | "line" | "pie" | "scatter") => void; + chartType: "bar" | "line" | "area" | "pie"; + setChartType: (type: "bar" | "line" | "area" | "pie") => void; xAxis: string; setXAxis: (axis: string) => void; yAxis: string; @@ -18,10 +18,10 @@ interface ChartConfigPanelProps { } const CHART_TYPES = [ - { value: "bar", label: "Bar", icon: BarChart3 }, - { value: "line", label: "Line", icon: TrendingUp }, - { value: "pie", label: "Pie", icon: PieChart }, - { value: "scatter", label: "Scatter", icon: ScatterChart }, + { value: "bar", label: "Bar", icon: BarChart3 }, + { value: "line", label: "Line", icon: TrendingUp }, + { value: "area", label: "Area", icon: AreaChart }, + { value: "pie", label: "Pie", icon: PieChart }, ] as const; export const ChartConfigPanel: React.FC = ({ @@ -35,56 +35,58 @@ export const ChartConfigPanel: React.FC = ({ setChartTitle, columns, }) => { - const xAxisColumns = useMemo(() => - columns.filter(col => !col.isPrimaryKey), - [columns] - ); - - const yAxisColumns = useMemo(() => - columns.filter(col => col.isPrimaryKey), - [columns] - ); + // Non-PK columns → good grouping dimensions (X axis) + const xAxisColumns = useMemo(() => columns.filter(col => !col.isPrimaryKey), [columns]); + // PK columns → good count targets (Y axis) + const yAxisColumns = useMemo(() => columns.filter(col => col.isPrimaryKey), [columns]); return (
- {/* Chart Type Selector - Icon buttons */} -
+ {/* Chart type toggle */} +
{CHART_TYPES.map(({ value, label, icon: Icon }) => ( ))}
- {/* 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"); 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 )}
- + 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, diff --git a/src/features/settings/components/ColorVariant.tsx b/src/features/settings/components/ColorVariant.tsx index 3789324..7de6011 100644 --- a/src/features/settings/components/ColorVariant.tsx +++ b/src/features/settings/components/ColorVariant.tsx @@ -1,58 +1,186 @@ 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', 'valorant', 'ghibli']; + +interface ThemeCardProps { + themeKey: ThemeVariant; + isActive: boolean; + onSelect: () => void; +} + +function ThemeCard({ themeKey, isActive, onSelect }: ThemeCardProps) { + const config = themeVariants[themeKey]; + + if (config.fullPalette) { + // 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 ( + + ); + } + + // 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..358da1c 100644 --- a/src/features/settings/hooks/useThemeVariant.ts +++ b/src/features/settings/hooks/useThemeVariant.ts @@ -1,8 +1,99 @@ 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'], + ['sidebarPrimary', '--sidebar-primary'], + ['sidebarPrimaryForeground', '--sidebar-primary-foreground'], + ['sidebarAccent', '--sidebar-accent'], + ['sidebarAccentForeground', '--sidebar-accent-foreground'], + ['sidebarBorder', '--sidebar-border'], + ['sidebarRing', '--sidebar-ring'], + ['cardShadow', '--card-shadow'], + ['glassBg', '--glass-bg'], + ['surface', '--surface'], + ['surfaceElevated', '--surface-elevated'], + ['surfaceSubtle', '--surface-subtle'], + ['borderSubtle', '--border-subtle'], +]; + +/** 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 isDarkMode(): boolean { + return document.documentElement.classList.contains('dark'); +} + +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: use explicit palette overrides when present, + // otherwise fall back to the main primary/ring values. + if (!palette.sidebarPrimary && palette.primary) { + root.style.setProperty('--sidebar-primary', palette.primary); + } + if (!palette.sidebarRing) { + 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'); +} + +/** + * Resolve which palette to apply for a given variant, respecting dark mode. + * Priority: lightPalette/darkPalette (mode-aware) → palette (single always-on) → null + */ +function resolvePalette(variant: ThemeVariant): ThemePalette | null { + const config = themeVariants[variant]; + if (!config.fullPalette) return null; + + const dark = isDarkMode(); + + // Mode-aware themes (e.g. Valorant) + if (config.lightPalette && config.darkPalette) { + return dark ? config.darkPalette : config.lightPalette; + } + + // Single-mode themes (e.g. Cyberpunk, VS Code — always dark) + if (config.palette) { + return config.palette; + } + + return null; +} + export function useThemeVariant() { const [variant, setVariantState] = useState(() => { const stored = localStorage.getItem(STORAGE_KEY); @@ -11,7 +102,42 @@ export function useThemeVariant() { useEffect(() => { const root = document.documentElement; - root.setAttribute('data-theme-variant', variant); + + const applyTheme = () => { + // Always update the data attribute so CSS [data-theme-variant] selectors still work + root.setAttribute('data-theme-variant', variant); + + const palette = resolvePalette(variant); + if (palette) { + applyPalette(root, palette); + } else { + // Accent-only theme: remove any full-palette inline overrides so the + // base :root / .dark stylesheet variables resume control. + clearPalette(root); + } + }; + + // Apply immediately + applyTheme(); + + // For mode-aware full-palette themes, watch for .dark class toggling on + // so we can swap lightPalette ↔ darkPalette without a page reload. + const config = themeVariants[variant]; + if (config.fullPalette && config.lightPalette && config.darkPalette) { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'class' + ) { + applyTheme(); + break; + } + } + }); + observer.observe(root, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + } }, [variant]); const setVariant = (newVariant: ThemeVariant) => { diff --git a/src/lib/themes.ts b/src/lib/themes.ts index d79dbab..f504c1c 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -1,13 +1,71 @@ -export type ThemeVariant = 'blue' | 'slate' | 'green' | 'purple' | 'orange' | 'rose'; +export type ThemeVariant = + | 'blue' | 'slate' | 'green' | 'purple' | 'orange' | 'rose' + | 'cyberpunk' | 'vscode' | 'valorant' | 'ghibli'; 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; + /** + * Single palette applied regardless of light/dark mode. + * Used by themes that are always dark (Cyberpunk, VS Code). + */ + palette?: ThemePalette; + /** + * Light-mode palette. When set alongside darkPalette, the hook will + * automatically switch between them as the user toggles light/dark mode. + */ + lightPalette?: ThemePalette; + /** Dark-mode palette. Used together with lightPalette. */ + darkPalette?: 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; + /** Overrides --sidebar-primary. Falls back to `primary` when omitted. */ + sidebarPrimary?: string; + sidebarPrimaryForeground?: string; + sidebarAccent: string; + sidebarAccentForeground: string; + sidebarBorder: string; + /** Overrides --sidebar-ring. Falls back to `ring` when omitted. */ + sidebarRing?: 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 +102,265 @@ 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', + }, + }, + + /** + * Valorant + * Inspired by Riot Games' Valorant UI — vivid red-orange primary on warm + * cream/rose backgrounds in light mode; deep dark reddish tones in dark mode. + * Supports both light and dark modes, switching automatically with the toggle. + */ + valorant: { + name: 'Valorant', + description: 'Riot red on warm cream & dark', + primary: 'oklch(0.67 0.22 21.34)', + primaryForeground: 'oklch(0.99 0 360)', + ring: 'oklch(0.92 0.04 12.39)', + fullPalette: true, + previewGradient: 'linear-gradient(135deg, oklch(0.97 0.02 12.78) 0%, oklch(0.67 0.22 21.34) 50%, oklch(0.16 0.03 17.48) 100%)', + // Light palette — warm cream/rose base with Valorant red accent + lightPalette: { + background: 'oklch(0.97 0.02 12.78)', + foreground: 'oklch(0.24 0.07 17.81)', + card: 'oklch(0.98 0.01 17.28)', + cardForeground: 'oklch(0.26 0.07 19)', + popover: 'oklch(0.98 0.01 17.28)', + popoverForeground: 'oklch(0.26 0.07 19)', + primary: 'oklch(0.67 0.22 21.34)', + primaryForeground: 'oklch(0.99 0.00 359.99)', + secondary: 'oklch(0.95 0.02 11.28)', + secondaryForeground: 'oklch(0.24 0.07 17.81)', + muted: 'oklch(0.98 0.01 17.28)', + mutedForeground: 'oklch(0.55 0.08 19)', + accent: 'oklch(0.99 0.00 359.99)', + accentForeground: '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)', + sidebarForeground: 'oklch(0.26 0.07 19)', + sidebarAccent: 'oklch(0.98 0.01 17.28)', + sidebarAccentForeground: 'oklch(0.43 0.13 20.62)', + sidebarBorder: 'oklch(0.91 0.05 11.40)', + cardShadow: '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)', + glassBg: 'rgba(253, 248, 247, 0.82)', + surface: 'oklch(0.97 0.02 12.78)', + surfaceElevated: 'oklch(0.98 0.01 17.28)', + surfaceSubtle: 'oklch(0.95 0.02 11.28)', + borderSubtle: 'oklch(0.91 0.05 11.40)', + }, + // Dark palette — deep near-black reddish backgrounds, same red accent + darkPalette: { + background: 'oklch(0.16 0.03 17.48)', + foreground: 'oklch(0.99 0.00 359.99)', + card: 'oklch(0.21 0.05 19.26)', + cardForeground: 'oklch(0.98 0 0)', + popover: 'oklch(0.26 0.07 19)', + popoverForeground: 'oklch(0.99 0.00 359.99)', + primary: 'oklch(0.67 0.22 21.34)', + primaryForeground: 'oklch(0.99 0.00 359.99)', + secondary: 'oklch(0.3 0.0891 19.6)', + secondaryForeground: 'oklch(0.95 0.02 11.28)', + muted: 'oklch(0.26 0.07 19)', + mutedForeground: 'oklch(0.72 0.04 18)', + accent: 'oklch(0.43 0.13 20.62)', + accentForeground: '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)', + sidebarForeground: 'oklch(0.99 0.00 359.99)', + sidebarAccent: 'oklch(0.43 0.13 20.62)', + sidebarAccentForeground: 'oklch(0.99 0.00 359.99)', + sidebarBorder: 'oklch(0.39 0.12 20.37)', + cardShadow: '0 1px 3px 0px oklch(0.00 0 0 / 0.10), 0 2px 4px -1px oklch(0.00 0 0 / 0.10)', + glassBg: 'rgba(33, 14, 12, 0.82)', + surface: 'oklch(0.21 0.05 19.26)', + surfaceElevated: 'oklch(0.26 0.07 19)', + surfaceSubtle: 'oklch(0.3 0.0891 19.6)', + borderSubtle: 'oklch(0.31 0.09 19.80)', + }, + }, + + /** + * Ghibli Studio + * Inspired by Studio Ghibli's warm, earthy colour palette — golden sandy + * backgrounds with muted olive-green accents in light mode; deep dark warm + * browns with sage green highlights in dark mode. + * Supports both light and dark modes. + */ + ghibli: { + name: 'Ghibli', + description: 'Earthy sage & golden warm tones', + primary: 'oklch(0.71 0.10 111.99)', + primaryForeground: 'oklch(0.98 0.01 3.71)', + ring: 'oklch(0.51 0.08 74.26)', + fullPalette: true, + previewGradient: 'linear-gradient(135deg, oklch(0.91 0.05 82.69) 0%, oklch(0.71 0.10 111.99) 50%, oklch(0.20 0.01 48.35) 100%)', + lightPalette: { + background: 'oklch(0.91 0.05 82.69)', + foreground: 'oklch(0.41 0.08 79.04)', + card: 'oklch(0.92 0.04 83.86)', + cardForeground: 'oklch(0.41 0.08 73.75)', + popover: 'oklch(0.92 0.04 83.86)', + popoverForeground: 'oklch(0.41 0.08 73.75)', + primary: 'oklch(0.71 0.10 111.99)', + primaryForeground: 'oklch(0.98 0.01 3.71)', + secondary: 'oklch(0.88 0.05 83.41)', + secondaryForeground: 'oklch(0.51 0.08 79.21)', + muted: 'oklch(0.86 0.06 83.48)', + mutedForeground: 'oklch(0.51 0.08 74.26)', + accent: 'oklch(0.86 0.05 84.50)', + accentForeground: '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)', + sidebarForeground: 'oklch(0.41 0.08 79.04)', + sidebarPrimary: 'oklch(0.26 0.02 358.42)', + sidebarPrimaryForeground: 'oklch(0.98 0.01 3.71)', + sidebarAccent: 'oklch(0.83 0.06 84.46)', + sidebarAccentForeground: 'oklch(0.26 0.02 358.42)', + sidebarBorder: 'oklch(0.91 0.00 0.43)', + sidebarRing: 'oklch(0.71 0.00 0.37)', + cardShadow: '0 1px 3px 0px oklch(0.00 0 0 / 0.08), 0 2px 4px -1px oklch(0.00 0 0 / 0.08)', + glassBg: 'rgba(244, 237, 216, 0.80)', + surface: 'oklch(0.91 0.05 82.69)', + surfaceElevated: 'oklch(0.92 0.04 83.86)', + surfaceSubtle: 'oklch(0.88 0.05 83.41)', + borderSubtle: 'oklch(0.74 0.06 79.81)', + }, + darkPalette: { + background: 'oklch(0.20 0.01 48.35)', + foreground: 'oklch(0.88 0.05 79.26)', + card: 'oklch(0.25 0.01 56.14)', + cardForeground: 'oklch(0.88 0.05 79.26)', + popover: 'oklch(0.25 0.01 56.14)', + popoverForeground: 'oklch(0.88 0.05 79.26)', + primary: 'oklch(0.64 0.05 115.39)', + primaryForeground: 'oklch(0.98 0.01 3.71)', + secondary: 'oklch(0.33 0.02 60.70)', + secondaryForeground: 'oklch(0.88 0.05 83.41)', + muted: 'oklch(0.27 0.01 39.35)', + mutedForeground: 'oklch(0.74 0.06 79.81)', + accent: 'oklch(0.33 0.02 60.70)', + accentForeground: '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)', + sidebarForeground: 'oklch(0.88 0.05 79.26)', + sidebarPrimary: 'oklch(0.64 0.05 115.39)', + sidebarPrimaryForeground: 'oklch(0.98 0.01 3.71)', + sidebarAccent: 'oklch(0.33 0.02 60.70)', + sidebarAccentForeground: 'oklch(0.86 0.05 84.50)', + sidebarBorder: 'oklch(0.33 0.02 60.70)', + sidebarRing: 'oklch(0.64 0.05 115.39)', + cardShadow: '0 1px 3px 0px oklch(0.00 0 0 / 0.20), 0 2px 4px -1px oklch(0.00 0 0 / 0.20)', + glassBg: 'rgba(33, 25, 14, 0.82)', + surface: 'oklch(0.25 0.01 56.14)', + surfaceElevated: 'oklch(0.27 0.01 39.35)', + surfaceSubtle: 'oklch(0.33 0.02 60.70)', + borderSubtle: 'oklch(0.33 0.02 60.70)', + }, + }, }; export const defaultVariant: ThemeVariant = 'blue';