mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Merge branch 'main' into codex/create-types-directory-and-files
This commit is contained in:
@@ -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
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
@@ -115,16 +116,21 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{securityScanResult && securityScanResult.severity !== 'safe' && securityScanResult.severity !== 'low' && !showSecurityDialog && (
|
||||
<Alert className="border-yellow-200 bg-yellow-50">
|
||||
<Warning className="h-5 w-5 text-yellow-600" weight="fill" />
|
||||
<AlertDescription className="text-yellow-800">
|
||||
{securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'} detected.
|
||||
Click Security Scan to review.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{securityScanResult &&
|
||||
securityScanResult.severity !== 'safe' &&
|
||||
securityScanResult.severity !== 'low' &&
|
||||
!showSecurityDialog && (
|
||||
<Alert className="border-yellow-200 bg-yellow-50">
|
||||
<Warning className="h-5 w-5 text-yellow-600" weight="fill" />
|
||||
<AlertDescription className="text-yellow-800">
|
||||
{securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'}
|
||||
detected. Click Security Scan to review.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<SchemaSection schema={schema} />
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Editor
|
||||
height="600px"
|
||||
@@ -157,23 +163,12 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handleScan}>
|
||||
<ShieldCheck className="mr-2" />
|
||||
Security Scan
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleFormat}>
|
||||
Format JSON
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-accent text-accent-foreground hover:bg-accent/90">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Toolbar
|
||||
onScan={handleScan}
|
||||
onFormat={handleFormat}
|
||||
onCancel={onClose}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -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<ThemeConfig>('theme_config', {
|
||||
@@ -81,7 +17,7 @@ export function ThemeEditor() {
|
||||
dark: DEFAULT_DARK_THEME,
|
||||
radius: '0.5rem',
|
||||
})
|
||||
|
||||
|
||||
const [isDarkMode, setIsDarkMode] = useKV<boolean>('dark_mode_enabled', false)
|
||||
const [editingTheme, setEditingTheme] = useState<'light' | 'dark'>('light')
|
||||
const [localColors, setLocalColors] = useState<ThemeColors>(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 (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
@@ -196,9 +88,7 @@ export function ThemeEditor() {
|
||||
<Palette size={24} />
|
||||
Theme Editor
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the application theme colors and appearance
|
||||
</CardDescription>
|
||||
<CardDescription>Customize the application theme colors and appearance</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Sun size={18} className={!isDarkMode ? 'text-amber-500' : 'text-muted-foreground'} />
|
||||
@@ -207,52 +97,21 @@ export function ThemeEditor() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={editingTheme} onValueChange={(v) => setEditingTheme(v as 'light' | 'dark')}>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<Tabs value={editingTheme} onValueChange={(value) => setEditingTheme(value as 'light' | 'dark')}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="light">Light Theme</TabsTrigger>
|
||||
<TabsTrigger value="dark">Dark Theme</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={editingTheme} className="space-y-6 mt-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="radius">Border Radius</Label>
|
||||
<Input
|
||||
id="radius"
|
||||
value={localRadius}
|
||||
onChange={(e) => setLocalRadius(e.target.value)}
|
||||
placeholder="e.g., 0.5rem"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{colorGroups.map((group) => (
|
||||
<div key={group.title} className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">{group.title}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{group.colors.map(({ key, label }) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label htmlFor={key}>{label}</Label>
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
className="w-10 h-10 rounded border border-border shrink-0"
|
||||
style={{ background: localColors[key] }}
|
||||
/>
|
||||
<Input
|
||||
id={key}
|
||||
value={localColors[key]}
|
||||
onChange={(e) => handleColorChange(key, e.target.value)}
|
||||
placeholder="oklch(...)"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<TabsContent value={editingTheme} className="space-y-6">
|
||||
<PaletteEditor
|
||||
colors={localColors}
|
||||
radius={localRadius}
|
||||
onColorChange={handleColorChange}
|
||||
onRadiusChange={setLocalRadius}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-border">
|
||||
<Button onClick={handleSave} className="gap-2">
|
||||
@@ -267,26 +126,7 @@ export function ThemeEditor() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-6 p-4 border border-border rounded-lg bg-muted/30">
|
||||
<h4 className="text-sm font-semibold mb-3">Theme Preview</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm">Primary Button</Button>
|
||||
<Button size="sm" variant="secondary">Secondary</Button>
|
||||
<Button size="sm" variant="outline">Outline</Button>
|
||||
<Button size="sm" variant="destructive">Destructive</Button>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Example</CardTitle>
|
||||
<CardDescription>This is a card description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Card content with muted text</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<PreviewPane />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-1">
|
||||
<CardTitle>Schema</CardTitle>
|
||||
<CardDescription>Reference for the expected JSON structure</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="max-h-48 overflow-auto rounded border bg-muted px-3 py-2 text-xs leading-5 whitespace-pre-wrap">
|
||||
{formattedSchema}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
31
frontends/nextjs/src/components/editors/json/Toolbar.tsx
Normal file
31
frontends/nextjs/src/components/editors/json/Toolbar.tsx
Normal file
@@ -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 (
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={onScan}>
|
||||
<ShieldCheck className="mr-2" />
|
||||
Security Scan
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onFormat}>
|
||||
Format JSON
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave} className="bg-accent text-accent-foreground hover:bg-accent/90">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-6 mt-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="radius">Border Radius</Label>
|
||||
<Input
|
||||
id="radius"
|
||||
value={radius}
|
||||
onChange={(e) => onRadiusChange(e.target.value)}
|
||||
placeholder="e.g., 0.5rem"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{colorGroups.map((group) => (
|
||||
<div key={group.title} className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">{group.title}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{group.colors.map(({ key, label }) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label htmlFor={key}>{label}</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-10 h-10 rounded border border-border shrink-0" style={{ background: colors[key] }} />
|
||||
<Input
|
||||
id={key}
|
||||
value={colors[key]}
|
||||
onChange={(e) => onColorChange(key, e.target.value)}
|
||||
placeholder="oklch(...)"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
export function PreviewPane() {
|
||||
return (
|
||||
<div className="mt-6 p-4 border border-border rounded-lg bg-muted/30">
|
||||
<h4 className="text-sm font-semibold mb-3">Theme Preview</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm">Primary Button</Button>
|
||||
<Button size="sm" variant="secondary">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
Outline
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive">
|
||||
Destructive
|
||||
</Button>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Example</CardTitle>
|
||||
<CardDescription>This is a card description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Card content with muted text</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
frontends/nextjs/src/components/editors/theme/constants.ts
Normal file
41
frontends/nextjs/src/components/editors/theme/constants.ts
Normal file
@@ -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)',
|
||||
}
|
||||
25
frontends/nextjs/src/components/editors/theme/types.ts
Normal file
25
frontends/nextjs/src/components/editors/theme/types.ts
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user