From b6b48eafb35e827ee23c790ff490048066cb0273 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:30:57 +0000 Subject: [PATCH 1/2] feat: modularize theme editor --- .../src/components/editors/ThemeEditor.tsx | 210 +++--------------- .../editors/theme/PaletteEditor.tsx | 86 +++++++ .../components/editors/theme/PreviewPane.tsx | 33 +++ .../src/components/editors/theme/constants.ts | 41 ++++ .../src/components/editors/theme/types.ts | 25 +++ 5 files changed, 210 insertions(+), 185 deletions(-) create mode 100644 frontends/nextjs/src/components/editors/theme/PaletteEditor.tsx create mode 100644 frontends/nextjs/src/components/editors/theme/PreviewPane.tsx create mode 100644 frontends/nextjs/src/components/editors/theme/constants.ts create mode 100644 frontends/nextjs/src/components/editors/theme/types.ts diff --git a/frontends/nextjs/src/components/editors/ThemeEditor.tsx b/frontends/nextjs/src/components/editors/ThemeEditor.tsx index 03c56b878..9800bdb50 100644 --- a/frontends/nextjs/src/components/editors/ThemeEditor.tsx +++ b/frontends/nextjs/src/components/editors/ThemeEditor.tsx @@ -1,79 +1,15 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' -import { Label } from '@/components/ui' -import { Input } from '@/components/ui' import { Button } from '@/components/ui' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' import { Switch } from '@/components/ui' import { Palette, Sun, Moon, FloppyDisk, ArrowCounterClockwise } from '@phosphor-icons/react' import { toast } from 'sonner' import { useKV } from '@github/spark/hooks' - -interface ThemeColors { - background: string - foreground: string - card: string - cardForeground: string - primary: string - primaryForeground: string - secondary: string - secondaryForeground: string - muted: string - mutedForeground: string - accent: string - accentForeground: string - destructive: string - destructiveForeground: string - border: string - input: string - ring: string -} - -interface ThemeConfig { - light: ThemeColors - dark: ThemeColors - radius: string -} - -const DEFAULT_LIGHT_THEME: ThemeColors = { - background: 'oklch(0.92 0.03 290)', - foreground: 'oklch(0.25 0.02 260)', - card: 'oklch(1 0 0)', - cardForeground: 'oklch(0.25 0.02 260)', - primary: 'oklch(0.55 0.18 290)', - primaryForeground: 'oklch(0.98 0 0)', - secondary: 'oklch(0.35 0.02 260)', - secondaryForeground: 'oklch(0.90 0.01 260)', - muted: 'oklch(0.95 0.02 290)', - mutedForeground: 'oklch(0.50 0.02 260)', - accent: 'oklch(0.70 0.17 195)', - accentForeground: 'oklch(0.2 0.02 260)', - destructive: 'oklch(0.55 0.22 25)', - destructiveForeground: 'oklch(0.98 0 0)', - border: 'oklch(0.85 0.02 290)', - input: 'oklch(0.85 0.02 290)', - ring: 'oklch(0.70 0.17 195)', -} - -const DEFAULT_DARK_THEME: ThemeColors = { - background: 'oklch(0.145 0 0)', - foreground: 'oklch(0.985 0 0)', - card: 'oklch(0.205 0 0)', - cardForeground: 'oklch(0.985 0 0)', - primary: 'oklch(0.922 0 0)', - primaryForeground: 'oklch(0.205 0 0)', - secondary: 'oklch(0.269 0 0)', - secondaryForeground: 'oklch(0.985 0 0)', - muted: 'oklch(0.269 0 0)', - mutedForeground: 'oklch(0.708 0 0)', - accent: 'oklch(0.269 0 0)', - accentForeground: 'oklch(0.985 0 0)', - destructive: 'oklch(0.704 0.191 22.216)', - destructiveForeground: 'oklch(0.98 0 0)', - border: 'oklch(1 0 0 / 10%)', - input: 'oklch(1 0 0 / 15%)', - ring: 'oklch(0.556 0 0)', -} +import { PaletteEditor } from './theme/PaletteEditor' +import { PreviewPane } from './theme/PreviewPane' +import { DEFAULT_DARK_THEME, DEFAULT_LIGHT_THEME } from './theme/constants' +import { ThemeColors, ThemeConfig } from './theme/types' export function ThemeEditor() { const [themeConfig, setThemeConfig] = useKV('theme_config', { @@ -81,7 +17,7 @@ export function ThemeEditor() { dark: DEFAULT_DARK_THEME, radius: '0.5rem', }) - + const [isDarkMode, setIsDarkMode] = useKV('dark_mode_enabled', false) const [editingTheme, setEditingTheme] = useState<'light' | 'dark'>('light') const [localColors, setLocalColors] = useState(DEFAULT_LIGHT_THEME) @@ -95,30 +31,19 @@ export function ThemeEditor() { }, [editingTheme, themeConfig]) useEffect(() => { - if (themeConfig) { - applyTheme() - } - }, [themeConfig, isDarkMode]) - - const applyTheme = () => { if (!themeConfig) return - + const root = document.documentElement const colors = isDarkMode ? themeConfig.dark : themeConfig.light - + Object.entries(colors).forEach(([key, value]) => { const cssVarName = key.replace(/([A-Z])/g, '-$1').toLowerCase() root.style.setProperty(`--${cssVarName}`, value) }) - + root.style.setProperty('--radius', themeConfig.radius) - - if (isDarkMode) { - root.classList.add('dark') - } else { - root.classList.remove('dark') - } - } + root.classList.toggle('dark', isDarkMode) + }, [isDarkMode, themeConfig]) const handleColorChange = (colorKey: keyof ThemeColors, value: string) => { setLocalColors((current) => ({ @@ -130,12 +55,14 @@ export function ThemeEditor() { const handleSave = () => { setThemeConfig((current) => { if (!current) return { light: localColors, dark: DEFAULT_DARK_THEME, radius: localRadius } + return { ...current, [editingTheme]: localColors, radius: localRadius, } }) + toast.success('Theme saved successfully') } @@ -151,41 +78,6 @@ export function ThemeEditor() { toast.success(checked ? 'Dark mode enabled' : 'Light mode enabled') } - const colorGroups = [ - { - title: 'Base Colors', - colors: [ - { key: 'background' as const, label: 'Background' }, - { key: 'foreground' as const, label: 'Foreground' }, - { key: 'card' as const, label: 'Card' }, - { key: 'cardForeground' as const, label: 'Card Foreground' }, - ], - }, - { - title: 'Action Colors', - colors: [ - { key: 'primary' as const, label: 'Primary' }, - { key: 'primaryForeground' as const, label: 'Primary Foreground' }, - { key: 'secondary' as const, label: 'Secondary' }, - { key: 'secondaryForeground' as const, label: 'Secondary Foreground' }, - { key: 'accent' as const, label: 'Accent' }, - { key: 'accentForeground' as const, label: 'Accent Foreground' }, - { key: 'destructive' as const, label: 'Destructive' }, - { key: 'destructiveForeground' as const, label: 'Destructive Foreground' }, - ], - }, - { - title: 'Supporting Colors', - colors: [ - { key: 'muted' as const, label: 'Muted' }, - { key: 'mutedForeground' as const, label: 'Muted Foreground' }, - { key: 'border' as const, label: 'Border' }, - { key: 'input' as const, label: 'Input' }, - { key: 'ring' as const, label: 'Ring' }, - ], - }, - ] - return (
@@ -196,9 +88,7 @@ export function ThemeEditor() { Theme Editor - - Customize the application theme colors and appearance - + Customize the application theme colors and appearance
@@ -207,52 +97,21 @@ export function ThemeEditor() {
- - setEditingTheme(v as 'light' | 'dark')}> + + + setEditingTheme(value as 'light' | 'dark')}> Light Theme Dark Theme - - -
-
- - setLocalRadius(e.target.value)} - placeholder="e.g., 0.5rem" - className="mt-1.5" - /> -
-
- {colorGroups.map((group) => ( -
-

{group.title}

-
- {group.colors.map(({ key, label }) => ( -
- -
-
- handleColorChange(key, e.target.value)} - placeholder="oklch(...)" - className="font-mono text-sm" - /> -
-
- ))} -
-
- ))} + +
- - - -
- - - Card Example - This is a card description - - -

Card content with muted text

-
-
-
- +
diff --git a/frontends/nextjs/src/components/editors/theme/PaletteEditor.tsx b/frontends/nextjs/src/components/editors/theme/PaletteEditor.tsx new file mode 100644 index 000000000..2950b135b --- /dev/null +++ b/frontends/nextjs/src/components/editors/theme/PaletteEditor.tsx @@ -0,0 +1,86 @@ +import { Input, Label } from '@/components/ui' +import { ThemeColors } from './types' + +const colorGroups = [ + { + title: 'Base Colors', + colors: [ + { key: 'background' as const, label: 'Background' }, + { key: 'foreground' as const, label: 'Foreground' }, + { key: 'card' as const, label: 'Card' }, + { key: 'cardForeground' as const, label: 'Card Foreground' }, + ], + }, + { + title: 'Action Colors', + colors: [ + { key: 'primary' as const, label: 'Primary' }, + { key: 'primaryForeground' as const, label: 'Primary Foreground' }, + { key: 'secondary' as const, label: 'Secondary' }, + { key: 'secondaryForeground' as const, label: 'Secondary Foreground' }, + { key: 'accent' as const, label: 'Accent' }, + { key: 'accentForeground' as const, label: 'Accent Foreground' }, + { key: 'destructive' as const, label: 'Destructive' }, + { key: 'destructiveForeground' as const, label: 'Destructive Foreground' }, + ], + }, + { + title: 'Supporting Colors', + colors: [ + { key: 'muted' as const, label: 'Muted' }, + { key: 'mutedForeground' as const, label: 'Muted Foreground' }, + { key: 'border' as const, label: 'Border' }, + { key: 'input' as const, label: 'Input' }, + { key: 'ring' as const, label: 'Ring' }, + ], + }, +] + +interface PaletteEditorProps { + colors: ThemeColors + radius: string + onColorChange: (colorKey: keyof ThemeColors, value: string) => void + onRadiusChange: (value: string) => void +} + +export function PaletteEditor({ colors, radius, onColorChange, onRadiusChange }: PaletteEditorProps) { + return ( +
+
+
+ + onRadiusChange(e.target.value)} + placeholder="e.g., 0.5rem" + className="mt-1.5" + /> +
+
+ + {colorGroups.map((group) => ( +
+

{group.title}

+
+ {group.colors.map(({ key, label }) => ( +
+ +
+
+ onColorChange(key, e.target.value)} + placeholder="oklch(...)" + className="font-mono text-sm" + /> +
+
+ ))} +
+
+ ))} +
+ ) +} diff --git a/frontends/nextjs/src/components/editors/theme/PreviewPane.tsx b/frontends/nextjs/src/components/editors/theme/PreviewPane.tsx new file mode 100644 index 000000000..795ccf45d --- /dev/null +++ b/frontends/nextjs/src/components/editors/theme/PreviewPane.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' +import { Button } from '@/components/ui' + +export function PreviewPane() { + return ( +
+

Theme Preview

+
+
+ + + + +
+ + + Card Example + This is a card description + + +

Card content with muted text

+
+
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/editors/theme/constants.ts b/frontends/nextjs/src/components/editors/theme/constants.ts new file mode 100644 index 000000000..7e7b8f8b2 --- /dev/null +++ b/frontends/nextjs/src/components/editors/theme/constants.ts @@ -0,0 +1,41 @@ +import { ThemeColors } from './types' + +export const DEFAULT_LIGHT_THEME: ThemeColors = { + background: 'oklch(0.92 0.03 290)', + foreground: 'oklch(0.25 0.02 260)', + card: 'oklch(1 0 0)', + cardForeground: 'oklch(0.25 0.02 260)', + primary: 'oklch(0.55 0.18 290)', + primaryForeground: 'oklch(0.98 0 0)', + secondary: 'oklch(0.35 0.02 260)', + secondaryForeground: 'oklch(0.90 0.01 260)', + muted: 'oklch(0.95 0.02 290)', + mutedForeground: 'oklch(0.50 0.02 260)', + accent: 'oklch(0.70 0.17 195)', + accentForeground: 'oklch(0.2 0.02 260)', + destructive: 'oklch(0.55 0.22 25)', + destructiveForeground: 'oklch(0.98 0 0)', + border: 'oklch(0.85 0.02 290)', + input: 'oklch(0.85 0.02 290)', + ring: 'oklch(0.70 0.17 195)', +} + +export const DEFAULT_DARK_THEME: ThemeColors = { + background: 'oklch(0.145 0 0)', + foreground: 'oklch(0.985 0 0)', + card: 'oklch(0.205 0 0)', + cardForeground: 'oklch(0.985 0 0)', + primary: 'oklch(0.922 0 0)', + primaryForeground: 'oklch(0.205 0 0)', + secondary: 'oklch(0.269 0 0)', + secondaryForeground: 'oklch(0.985 0 0)', + muted: 'oklch(0.269 0 0)', + mutedForeground: 'oklch(0.708 0 0)', + accent: 'oklch(0.269 0 0)', + accentForeground: 'oklch(0.985 0 0)', + destructive: 'oklch(0.704 0.191 22.216)', + destructiveForeground: 'oklch(0.98 0 0)', + border: 'oklch(1 0 0 / 10%)', + input: 'oklch(1 0 0 / 15%)', + ring: 'oklch(0.556 0 0)', +} diff --git a/frontends/nextjs/src/components/editors/theme/types.ts b/frontends/nextjs/src/components/editors/theme/types.ts new file mode 100644 index 000000000..8440c06d8 --- /dev/null +++ b/frontends/nextjs/src/components/editors/theme/types.ts @@ -0,0 +1,25 @@ +export interface ThemeColors { + background: string + foreground: string + card: string + cardForeground: string + primary: string + primaryForeground: string + secondary: string + secondaryForeground: string + muted: string + mutedForeground: string + accent: string + accentForeground: string + destructive: string + destructiveForeground: string + border: string + input: string + ring: string +} + +export interface ThemeConfig { + light: ThemeColors + dark: ThemeColors + radius: string +} From 07efe7609a58e55349da4807cd4c85540e60cc12 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:31:22 +0000 Subject: [PATCH 2/2] refactor: extract json editor ui components --- .../src/components/editors/JsonEditor.tsx | 77 +++++++++---------- .../components/editors/json/SchemaSection.tsx | 26 +++++++ .../src/components/editors/json/Toolbar.tsx | 31 ++++++++ 3 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 frontends/nextjs/src/components/editors/json/SchemaSection.tsx create mode 100644 frontends/nextjs/src/components/editors/json/Toolbar.tsx diff --git a/frontends/nextjs/src/components/editors/JsonEditor.tsx b/frontends/nextjs/src/components/editors/JsonEditor.tsx index 7b6beb59a..f6ebcf593 100644 --- a/frontends/nextjs/src/components/editors/JsonEditor.tsx +++ b/frontends/nextjs/src/components/editors/JsonEditor.tsx @@ -1,12 +1,13 @@ -import { useState, useEffect } from 'react' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui' -import { Button } from '@/components/ui' -import { Alert, AlertDescription } from '@/components/ui' -import { FloppyDisk, X, Warning, ShieldCheck } from '@phosphor-icons/react' +import { useEffect, useState } from 'react' +import { Alert, AlertDescription, Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui' +import { Warning } from '@phosphor-icons/react' import Editor from '@monaco-editor/react' +import { toast } from 'sonner' + +import { SchemaSection } from './json/SchemaSection' +import { Toolbar } from './json/Toolbar' import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner' import { SecurityWarningDialog } from '@/components/organisms/security/SecurityWarningDialog' -import { toast } from 'sonner' interface JsonEditorProps { open: boolean @@ -32,10 +33,12 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json } }, [open, value]) + const parseJson = () => JSON.parse(jsonText) + const handleSave = () => { try { - const parsed = JSON.parse(jsonText) - + const parsed = parseJson() + const scanResult = securityScanner.scanJSON(jsonText) setSecurityScanResult(scanResult) @@ -66,8 +69,7 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json const handleForceSave = () => { try { - const parsed = JSON.parse(jsonText) - onSave(parsed) + onSave(parseJson()) setError(null) setPendingSave(false) setShowSecurityDialog(false) @@ -81,7 +83,7 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json const scanResult = securityScanner.scanJSON(jsonText) setSecurityScanResult(scanResult) setShowSecurityDialog(true) - + if (scanResult.safe) { toast.success('No security issues detected') } else { @@ -91,8 +93,7 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json const handleFormat = () => { try { - const parsed = JSON.parse(jsonText) - setJsonText(JSON.stringify(parsed, null, 2)) + setJsonText(JSON.stringify(parseJson(), null, 2)) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Invalid JSON - cannot format') @@ -106,7 +107,7 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json {title} - +
{error && ( @@ -115,16 +116,21 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json )} - {securityScanResult && securityScanResult.severity !== 'safe' && securityScanResult.severity !== 'low' && !showSecurityDialog && ( - - - - {securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'} detected. - Click Security Scan to review. - - - )} - + {securityScanResult && + securityScanResult.severity !== 'safe' && + securityScanResult.severity !== 'low' && + !showSecurityDialog && ( + + + + {securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'} +  detected. Click Security Scan to review. + + + )} + + +
- - - - - - + diff --git a/frontends/nextjs/src/components/editors/json/SchemaSection.tsx b/frontends/nextjs/src/components/editors/json/SchemaSection.tsx new file mode 100644 index 000000000..b77ca8486 --- /dev/null +++ b/frontends/nextjs/src/components/editors/json/SchemaSection.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' + +interface SchemaSectionProps { + schema?: unknown +} + +export function SchemaSection({ schema }: SchemaSectionProps) { + if (!schema) return null + + const formattedSchema = + typeof schema === 'string' ? schema : JSON.stringify(schema, null, 2) + + return ( + + + Schema + Reference for the expected JSON structure + + +
+          {formattedSchema}
+        
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/editors/json/Toolbar.tsx b/frontends/nextjs/src/components/editors/json/Toolbar.tsx new file mode 100644 index 000000000..a642ce6fc --- /dev/null +++ b/frontends/nextjs/src/components/editors/json/Toolbar.tsx @@ -0,0 +1,31 @@ +import { Button, DialogFooter } from '@/components/ui' +import { FloppyDisk, ShieldCheck, X } from '@phosphor-icons/react' + +interface ToolbarProps { + onScan: () => void + onFormat: () => void + onCancel: () => void + onSave: () => void +} + +export function Toolbar({ onScan, onFormat, onCancel, onSave }: ToolbarProps) { + return ( + + + + + + + ) +}