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/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"
- />
-
-
- ))}
-
-
- ))}
+
+
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 (
+
+
+
+ Security Scan
+
+
+ Format JSON
+
+
+
+ Cancel
+
+
+
+ Save
+
+
+ )
+}
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
+
+
+ Primary Button
+
+ Secondary
+
+
+ Outline
+
+
+ Destructive
+
+
+
+
+ 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
+}