feat: Delete 13 duplicate molecule TSX files with JSON equivalents

Deleted molecules now using JSON-ui implementations:
- AppBranding, CodeExplanationDialog, ComponentBindingDialog
- DataSourceCard, DataSourceEditorDialog, GitHubBuildStatus
- LazyBarChart, LazyD3BarChart, LazyLineChart
- NavigationGroupHeader, SaveIndicator, StorageSettings

Updated src/components/molecules/index.ts to import these from
@/lib/json-ui/json-components instead of TSX files.

Updated src/components/CodeEditor.tsx to import CodeExplanationDialog
from json-components.

Organisms still depend on some molecules (CanvasRenderer, ComponentPalette,
ComponentTree, PropertyEditor, etc) so those remain as TSX.

Build passes successfully with no errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 02:53:31 +00:00
parent cd5f11df3a
commit d67301883b
14 changed files with 16 additions and 1194 deletions

View File

@@ -6,7 +6,7 @@ import { useAIOperations } from '@/hooks/use-ai-operations'
import { EditorToolbar } from '@/components/molecules/EditorToolbar' import { EditorToolbar } from '@/components/molecules/EditorToolbar'
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel' import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState' import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
import { CodeExplanationDialog } from '@/components/molecules/CodeExplanationDialog' import { CodeExplanationDialog } from '@/lib/json-ui/json-components'
interface CodeEditorProps { interface CodeEditorProps {
files: ProjectFile[] files: ProjectFile[]

View File

@@ -1,10 +0,0 @@
import { AppLogo } from '@/components/atoms'
export function AppBranding() {
return (
<div className="flex items-center gap-2">
<AppLogo size="sm" />
<span className="font-semibold text-lg">Spark</span>
</div>
)
}

View File

@@ -1,50 +0,0 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Sparkle } from '@phosphor-icons/react'
interface CodeExplanationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
fileName: string | undefined
explanation: string
isLoading: boolean
}
export function CodeExplanationDialog({
open,
onOpenChange,
fileName,
explanation,
isLoading,
}: CodeExplanationDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {fileName}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isLoading ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,98 +0,0 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { BindingEditor } from '@/lib/json-ui/json-components'
import { DataSource, UIComponent } from '@/types/json-ui'
import { Link } from '@phosphor-icons/react'
import { useComponentBindingDialog } from '@/hooks/use-component-binding-dialog'
interface ComponentBindingDialogProps {
open: boolean
component: UIComponent | null
dataSources: DataSource[]
onOpenChange: (open: boolean) => void
onSave: (component: UIComponent) => void
}
export function ComponentBindingDialog({
open,
component,
dataSources,
onOpenChange,
onSave,
}: ComponentBindingDialogProps) {
const { editingComponent, handleSave, updateBindings } = useComponentBindingDialog({
component,
open,
onOpenChange,
onSave,
})
if (!editingComponent) return null
const availableProps = ['children', 'value', 'checked', 'className', 'disabled', 'placeholder']
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Link className="w-5 h-5" />
Component Data Bindings
</DialogTitle>
<DialogDescription>
Connect component properties to data sources
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="p-4 bg-muted/30 rounded border border-border">
<div className="text-sm space-y-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Component:</span>
<span className="font-mono font-medium">{editingComponent.type}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">ID:</span>
<span className="font-mono text-xs">{editingComponent.id}</span>
</div>
</div>
</div>
<Tabs defaultValue="bindings">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="bindings">Property Bindings</TabsTrigger>
<TabsTrigger value="preview">Preview</TabsTrigger>
</TabsList>
<TabsContent value="bindings" className="space-y-4 mt-4">
<BindingEditor
bindings={editingComponent.bindings || {}}
dataSources={dataSources}
availableProps={availableProps}
onChange={updateBindings}
/>
</TabsContent>
<TabsContent value="preview" className="space-y-4 mt-4">
<div className="p-4 bg-muted/30 rounded border border-border">
<pre className="text-xs overflow-auto">
{JSON.stringify(editingComponent.bindings, null, 2)}
</pre>
</div>
</TabsContent>
</Tabs>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>
Save Bindings
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,69 +0,0 @@
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
import { DataSourceBadge } from '@/components/atoms'
import { DataSource } from '@/types/json-ui'
import { Pencil, Trash } from '@phosphor-icons/react'
interface DataSourceCardProps {
dataSource: DataSource
dependents?: DataSource[]
onEdit: (id: string) => void
onDelete: (id: string) => void
}
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
const renderTypeSpecificInfo = () => {
if (dataSource.type === 'kv') {
return (
<Text variant="caption" className="font-mono bg-muted/30 px-2 py-1 rounded">
Key: {dataSource.key || 'Not set'}
</Text>
)
}
return null
}
return (
<Card className="bg-card/50 backdrop-blur hover:bg-card/70 transition-colors">
<div className="p-4">
<Flex justify="between" align="start" gap="md">
<Stack spacing="sm" className="flex-1 min-w-0">
<Flex align="center" gap="sm">
<DataSourceBadge type={dataSource.type} />
<Text variant="small" className="font-mono font-medium truncate">
{dataSource.id}
</Text>
</Flex>
{renderTypeSpecificInfo()}
{dependents.length > 0 && (
<div className="pt-2 border-t border-border/50">
<Text variant="caption">
Used by {dependents.length} dependent {dependents.length === 1 ? 'source' : 'sources'}
</Text>
</div>
)}
</Stack>
<Flex align="center" gap="xs">
<IconButton
icon={<Pencil className="w-4 h-4" />}
variant="ghost"
size="sm"
onClick={() => onEdit(dataSource.id)}
/>
<IconButton
icon={<Trash className="w-4 h-4" />}
variant="ghost"
size="sm"
onClick={() => onDelete(dataSource.id)}
className="text-destructive hover:text-destructive"
disabled={dependents.length > 0}
/>
</Flex>
</Flex>
</div>
</Card>
)
}

View File

@@ -1,88 +0,0 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { DataSource } from '@/types/json-ui'
import { DataSourceBadge } from '@/components/atoms'
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
interface DataSourceEditorDialogProps {
open: boolean
dataSource: DataSource | null
onOpenChange: (open: boolean) => void
onSave: (dataSource: DataSource) => void
}
export function DataSourceEditorDialog({
open,
dataSource,
onOpenChange,
onSave,
}: DataSourceEditorDialogProps) {
const {
editingSource,
updateField,
} = useDataSourceEditor(dataSource)
const handleSave = () => {
if (!editingSource) return
onSave(editingSource)
onOpenChange(false)
}
if (!editingSource) return null
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{dataSourceEditorCopy.title}
<DataSourceBadge type={editingSource.type} />
</DialogTitle>
<DialogDescription>
{dataSourceEditorCopy.description}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<DataSourceIdField
editingSource={editingSource}
label={dataSourceEditorCopy.fields.id.label}
placeholder={dataSourceEditorCopy.fields.id.placeholder}
onChange={(value) => updateField('id', value)}
/>
{editingSource.type === 'kv' && (
<KvSourceFields
editingSource={editingSource}
copy={dataSourceEditorCopy.kv}
onUpdateField={updateField}
/>
)}
{editingSource.type === 'static' && (
<StaticSourceFields
editingSource={editingSource}
label={dataSourceEditorCopy.static.valueLabel}
placeholder={dataSourceEditorCopy.static.valuePlaceholder}
onUpdateField={updateField}
/>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{dataSourceEditorCopy.actions.cancel}
</Button>
<Button onClick={handleSave}>
{dataSourceEditorCopy.actions.save}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,396 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
GitBranch,
CheckCircle,
XCircle,
Clock,
ArrowSquareOut,
Warning,
Copy,
CheckSquare,
} from '@phosphor-icons/react'
import { Skeleton } from '@/components/ui/skeleton'
import copy from '@/data/github-build-status.json'
import { useGithubBuildStatus, Workflow, WorkflowRun } from '@/hooks/use-github-build-status'
interface GitHubBuildStatusProps {
owner: string
repo: string
defaultBranch?: string
}
interface WorkflowRunStatusProps {
status: string
conclusion: string | null
}
interface WorkflowRunDetailsProps {
branch: string
updatedAt: string
event: string
}
interface WorkflowRunItemProps {
workflow: WorkflowRun
renderStatus: (status: string, conclusion: string | null) => React.ReactNode
renderBadge: (status: string, conclusion: string | null) => React.ReactNode
formatTime: (dateString: string) => string
}
interface WorkflowBadgeListProps {
workflows: Workflow[]
copiedBadge: string | null
defaultBranch: string
onCopyBadge: (workflowPath: string, workflowName: string, branch?: string) => void
getBadgeUrl: (workflowPath: string, branch?: string) => string
getBadgeMarkdown: (workflowPath: string, workflowName: string, branch?: string) => string
}
interface BranchBadgeListProps {
branches: string[]
workflows: Workflow[]
copiedBadge: string | null
onCopyBadge: (workflowPath: string, workflowName: string, branch: string) => void
getBadgeUrl: (workflowPath: string, branch?: string) => string
}
const WorkflowRunStatus = ({ status, conclusion }: WorkflowRunStatusProps) => {
const getStatusIcon = () => {
if (status === 'completed') {
if (conclusion === 'success') {
return <CheckCircle size={20} weight="fill" className="text-green-500" />
}
if (conclusion === 'failure') {
return <XCircle size={20} weight="fill" className="text-red-500" />
}
if (conclusion === 'cancelled') {
return <Warning size={20} weight="fill" className="text-yellow-500" />
}
}
return <Clock size={20} weight="duotone" className="text-blue-500 animate-pulse" />
}
return <div className="flex items-center">{getStatusIcon()}</div>
}
const WorkflowRunBadge = ({ status, conclusion }: WorkflowRunStatusProps) => {
if (status === 'completed') {
if (conclusion === 'success') {
return (
<Badge className="bg-green-500/10 text-green-500 border-green-500/20">
{copy.status.success}
</Badge>
)
}
if (conclusion === 'failure') {
return <Badge variant="destructive">{copy.status.failed}</Badge>
}
if (conclusion === 'cancelled') {
return <Badge variant="secondary">{copy.status.cancelled}</Badge>
}
}
return (
<Badge variant="outline" className="border-blue-500/50 text-blue-500">
{copy.status.running}
</Badge>
)
}
const WorkflowRunDetails = ({ branch, updatedAt, event }: WorkflowRunDetailsProps) => (
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span className="truncate">{branch}</span>
<span></span>
<span>{updatedAt}</span>
<span></span>
<span className="truncate">{event}</span>
</div>
)
const WorkflowRunItem = ({ workflow, renderStatus, renderBadge, formatTime }: WorkflowRunItemProps) => (
<div
className="flex items-center justify-between p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
{renderStatus(workflow.status, workflow.conclusion)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium truncate">{workflow.name}</p>
{renderBadge(workflow.status, workflow.conclusion)}
</div>
<WorkflowRunDetails
branch={workflow.head_branch}
updatedAt={formatTime(workflow.updated_at)}
event={workflow.event}
/>
</div>
</div>
<Button size="sm" variant="ghost" asChild className="ml-2">
<a
href={workflow.html_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1"
>
<ArrowSquareOut size={16} />
</a>
</Button>
</div>
)
const WorkflowBadgeList = ({
workflows,
copiedBadge,
defaultBranch,
onCopyBadge,
getBadgeUrl,
getBadgeMarkdown,
}: WorkflowBadgeListProps) => (
<div>
<h3 className="text-sm font-medium mb-3">{copy.sections.workflowBadges}</h3>
<div className="space-y-3">
{workflows.map((workflow) => (
<div
key={workflow.id}
className="p-3 border border-border rounded-lg space-y-2"
>
<div className="flex items-center justify-between">
<p className="text-sm font-medium">{workflow.name}</p>
<Button
size="sm"
variant="ghost"
onClick={() => onCopyBadge(workflow.path, workflow.name)}
className="h-7 text-xs"
>
{copiedBadge === `${workflow.path}-${defaultBranch}` ? (
<CheckSquare size={14} className="text-green-500" />
) : (
<Copy size={14} />
)}
</Button>
</div>
<img
src={getBadgeUrl(workflow.path)}
alt={`${workflow.name} status`}
className="h-5"
/>
<p className="text-xs text-muted-foreground font-mono break-all">
{getBadgeMarkdown(workflow.path, workflow.name)}
</p>
</div>
))}
</div>
</div>
)
const BranchBadgeList = ({
branches,
workflows,
copiedBadge,
onCopyBadge,
getBadgeUrl,
}: BranchBadgeListProps) => (
<div>
<h3 className="text-sm font-medium mb-3">{copy.sections.branchBadges}</h3>
<div className="space-y-3">
{branches.slice(0, 3).map((branch) => (
<div key={branch} className="space-y-2">
<div className="flex items-center gap-2">
<GitBranch size={16} weight="duotone" />
<p className="text-sm font-medium">{branch}</p>
</div>
{workflows.slice(0, 2).map((workflow) => (
<div
key={`${workflow.id}-${branch}`}
className="p-3 border border-border rounded-lg space-y-2 ml-6"
>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">{workflow.name}</p>
<Button
size="sm"
variant="ghost"
onClick={() => onCopyBadge(workflow.path, workflow.name, branch)}
className="h-7 text-xs"
>
{copiedBadge === `${workflow.path}-${branch}` ? (
<CheckSquare size={14} className="text-green-500" />
) : (
<Copy size={14} />
)}
</Button>
</div>
<img
src={getBadgeUrl(workflow.path, branch)}
alt={`${workflow.name} status on ${branch}`}
className="h-5"
/>
</div>
))}
</div>
))}
</div>
</div>
)
export function GitHubBuildStatus({ owner, repo, defaultBranch = 'main' }: GitHubBuildStatusProps) {
const {
workflows,
allWorkflows,
loading,
error,
copiedBadge,
actions,
} = useGithubBuildStatus({ owner, repo, defaultBranch })
const { refresh, copyBadgeMarkdown, getBadgeUrl, getBadgeMarkdown, formatTime } = actions
if (loading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitBranch size={24} weight="duotone" />
{copy.title}
</CardTitle>
<CardDescription>{copy.loading.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-3 border border-border rounded-lg">
<div className="flex items-center gap-3 flex-1">
<Skeleton className="w-5 h-5 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
</div>
<Skeleton className="w-16 h-5 rounded" />
</div>
))}
</CardContent>
</Card>
)
}
if (error) {
return (
<Card className="bg-red-500/10 border-red-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitBranch size={24} weight="duotone" />
{copy.title}
</CardTitle>
<CardDescription>{copy.error.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-start gap-3">
<XCircle size={20} weight="fill" className="text-red-500 mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm text-red-500">{error}</p>
<Button size="sm" variant="outline" onClick={refresh} className="text-xs">
{copy.error.retry}
</Button>
</div>
</div>
</CardContent>
</Card>
)
}
if (workflows.length === 0 && allWorkflows.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitBranch size={24} weight="duotone" />
{copy.title}
</CardTitle>
<CardDescription>{copy.empty.description}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{copy.empty.body}</p>
</CardContent>
</Card>
)
}
const uniqueBranches = Array.from(new Set(workflows.map((workflow) => workflow.head_branch)))
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<GitBranch size={24} weight="duotone" />
{copy.title}
</CardTitle>
<CardDescription>{copy.header.description}</CardDescription>
</div>
<Button size="sm" variant="ghost" onClick={refresh} className="text-xs">
{copy.header.refresh}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
<Tabs defaultValue="badges" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="badges">{copy.tabs.badges}</TabsTrigger>
<TabsTrigger value="runs">{copy.tabs.runs}</TabsTrigger>
</TabsList>
<TabsContent value="badges" className="space-y-4 mt-4">
<div className="space-y-4">
<WorkflowBadgeList
workflows={allWorkflows}
copiedBadge={copiedBadge}
defaultBranch={defaultBranch}
onCopyBadge={copyBadgeMarkdown}
getBadgeUrl={getBadgeUrl}
getBadgeMarkdown={getBadgeMarkdown}
/>
{uniqueBranches.length > 0 && (
<BranchBadgeList
branches={uniqueBranches}
workflows={allWorkflows}
copiedBadge={copiedBadge}
onCopyBadge={copyBadgeMarkdown}
getBadgeUrl={getBadgeUrl}
/>
)}
</div>
</TabsContent>
<TabsContent value="runs" className="space-y-3 mt-4">
{workflows.map((workflow) => (
<WorkflowRunItem
key={workflow.id}
workflow={workflow}
renderStatus={(status, conclusion) => (
<WorkflowRunStatus status={status} conclusion={conclusion} />
)}
renderBadge={(status, conclusion) => (
<WorkflowRunBadge status={status} conclusion={conclusion} />
)}
formatTime={formatTime}
/>
))}
<Button size="sm" variant="outline" asChild className="w-full text-xs">
<a
href={`https://github.com/${owner}/${repo}/actions`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
{copy.actions.viewAllWorkflows}
<ArrowSquareOut size={14} />
</a>
</Button>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -1,63 +0,0 @@
import { useRecharts } from '@/hooks'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyBarChartProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number
height?: number
color?: string
}
export function LazyBarChart({
data,
xKey,
yKey,
width = 600,
height = 300,
color = '#8884d8'
}: LazyBarChartProps) {
const { library: recharts, loading, error } = useRecharts()
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load chart library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
if (!recharts) {
return null
}
const { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
return (
<ResponsiveContainer width={width} height={height}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey={yKey} fill={color} />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -1,87 +0,0 @@
import { useD3 } from '@/hooks'
import { useEffect, useRef } from 'react'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyD3ChartProps {
data: Array<{ label: string; value: number }>
width?: number
height?: number
color?: string
}
export function LazyD3BarChart({
data,
width = 600,
height = 300,
color = '#8884d8'
}: LazyD3ChartProps) {
const { library: d3, loading, error } = useD3()
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!d3 || !svgRef.current || !data.length) return
const svg = d3.select(svgRef.current)
svg.selectAll('*').remove()
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const chartWidth = width - margin.left - margin.right
const chartHeight = height - margin.top - margin.bottom
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const x = d3.scaleBand()
.range([0, chartWidth])
.padding(0.1)
.domain(data.map(d => d.label))
const y = d3.scaleLinear()
.range([chartHeight, 0])
.domain([0, d3.max(data, d => d.value) || 0])
g.append('g')
.attr('transform', `translate(0,${chartHeight})`)
.call(d3.axisBottom(x))
g.append('g')
.call(d3.axisLeft(y))
g.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', d => x(d.label) || 0)
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => chartHeight - y(d.value))
.attr('fill', color)
}, [d3, data, width, height, color])
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load D3 library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
return (
<svg ref={svgRef} width={width} height={height} />
)
}

View File

@@ -1,63 +0,0 @@
import { useRecharts } from '@/hooks'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyLineChartProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number
height?: number
color?: string
}
export function LazyLineChart({
data,
xKey,
yKey,
width = 600,
height = 300,
color = '#8884d8'
}: LazyLineChartProps) {
const { library: recharts, loading, error } = useRecharts()
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load chart library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
if (!recharts) {
return null
}
const { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
return (
<ResponsiveContainer width={width} height={height}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey={yKey} stroke={color} />
</LineChart>
</ResponsiveContainer>
)
}

View File

@@ -1,30 +0,0 @@
import { CaretDown } from '@phosphor-icons/react'
import { CollapsibleTrigger } from '@/components/ui/collapsible'
interface NavigationGroupHeaderProps {
label: string
count: number
isExpanded: boolean
}
export function NavigationGroupHeader({
label,
count,
isExpanded,
}: NavigationGroupHeaderProps) {
return (
<CollapsibleTrigger className="w-full flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-muted transition-colors group">
<CaretDown
size={16}
weight="bold"
className={`text-muted-foreground transition-transform ${
isExpanded ? 'rotate-0' : '-rotate-90'
}`}
/>
<h3 className="flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{label}
</h3>
<span className="text-xs text-muted-foreground">{count}</span>
</CollapsibleTrigger>
)
}

View File

@@ -1,19 +0,0 @@
import { StatusIcon } from '@/components/atoms'
import { useSaveIndicator } from '@/hooks/use-save-indicator'
interface SaveIndicatorProps {
lastSaved: number | null
}
export function SaveIndicator({ lastSaved }: SaveIndicatorProps) {
if (!lastSaved) return null
const { timeAgo, isRecent } = useSaveIndicator(lastSaved)
return (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<StatusIcon type={isRecent ? 'saved' : 'synced'} animate={isRecent} />
<span className="hidden sm:inline">{isRecent ? 'Saved' : timeAgo}</span>
</div>
)
}

View File

@@ -1,205 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Database, HardDrive, Cloud, Cpu, Download, Upload } from '@phosphor-icons/react'
import {
storageSettingsCopy,
getBackendCopy,
type StorageBackendKey,
} from '@/components/storage/storageSettingsConfig'
import { useStorageSettingsHandlers } from '@/components/storage/useStorageSettingsHandlers'
const getBackendIcon = (backend: StorageBackendKey | null) => {
switch (backend) {
case 'flask':
return <Cpu className="w-5 h-5" />
case 'indexeddb':
return <HardDrive className="w-5 h-5" />
case 'sqlite':
return <Database className="w-5 h-5" />
case 'sparkkv':
return <Cloud className="w-5 h-5" />
default:
return <Database className="w-5 h-5" />
}
}
type BackendCardProps = {
backend: StorageBackendKey | null
isLoading: boolean
flaskUrl: string
isSwitching: boolean
onFlaskUrlChange: (value: string) => void
onSwitchToFlask: () => void
onSwitchToIndexedDB: () => void
onSwitchToSQLite: () => void
}
const BackendCard = ({
backend,
isLoading,
flaskUrl,
isSwitching,
onFlaskUrlChange,
onSwitchToFlask,
onSwitchToIndexedDB,
onSwitchToSQLite,
}: BackendCardProps) => {
const backendCopy = getBackendCopy(backend)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getBackendIcon(backend)}
{storageSettingsCopy.molecule.title}
</CardTitle>
<CardDescription>{storageSettingsCopy.molecule.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">{storageSettingsCopy.molecule.currentBackendLabel}</span>
<Badge variant="secondary" className="flex items-center gap-1">
{getBackendIcon(backend)}
{backendCopy.moleculeLabel}
</Badge>
</div>
<div className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="flask-url">{storageSettingsCopy.molecule.flaskUrlLabel}</Label>
<div className="flex gap-2">
<Input
id="flask-url"
value={flaskUrl}
onChange={(e) => onFlaskUrlChange(e.target.value)}
placeholder={storageSettingsCopy.molecule.flaskUrlPlaceholder}
disabled={isSwitching || isLoading}
/>
<Button
onClick={onSwitchToFlask}
disabled={isSwitching || isLoading || backend === 'flask'}
variant={backend === 'flask' ? 'secondary' : 'default'}
>
<Cpu className="w-4 h-4 mr-2" />
{backend === 'flask'
? storageSettingsCopy.molecule.buttons.flaskActive
: storageSettingsCopy.molecule.buttons.flaskUse}
</Button>
</div>
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.flaskHelp}</p>
</div>
<div className="flex gap-2">
<Button
onClick={onSwitchToIndexedDB}
disabled={isSwitching || isLoading || backend === 'indexeddb'}
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
className="flex-1"
>
<HardDrive className="w-4 h-4 mr-2" />
{backend === 'indexeddb'
? storageSettingsCopy.molecule.buttons.indexeddbActive
: storageSettingsCopy.molecule.buttons.indexeddbUse}
</Button>
<Button
onClick={onSwitchToSQLite}
disabled={isSwitching || isLoading || backend === 'sqlite'}
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
className="flex-1"
>
<Database className="w-4 h-4 mr-2" />
{backend === 'sqlite'
? storageSettingsCopy.molecule.buttons.sqliteActive
: storageSettingsCopy.molecule.buttons.sqliteUse}
</Button>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p>{storageSettingsCopy.molecule.backendDetails.indexeddb}</p>
<p>{storageSettingsCopy.molecule.backendDetails.sqlite}</p>
<p>{storageSettingsCopy.molecule.backendDetails.flask}</p>
</div>
</div>
</CardContent>
</Card>
)
}
type DataManagementCardProps = {
isExporting: boolean
isImporting: boolean
onExport: () => void
onImport: () => void
}
const DataManagementCard = ({
isExporting,
isImporting,
onExport,
onImport,
}: DataManagementCardProps) => (
<Card>
<CardHeader>
<CardTitle>{storageSettingsCopy.molecule.dataTitle}</CardTitle>
<CardDescription>{storageSettingsCopy.molecule.dataDescription}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Button onClick={onExport} variant="outline" className="flex-1" disabled={isExporting}>
<Download className="w-4 h-4 mr-2" />
{storageSettingsCopy.molecule.buttons.export}
</Button>
<Button onClick={onImport} variant="outline" className="flex-1" disabled={isImporting}>
<Upload className="w-4 h-4 mr-2" />
{storageSettingsCopy.molecule.buttons.import}
</Button>
</div>
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.dataHelp}</p>
</CardContent>
</Card>
)
export function StorageSettings() {
const {
backend,
isLoading,
flaskUrl,
setFlaskUrl,
isSwitching,
handleSwitchToFlask,
handleSwitchToSQLite,
handleSwitchToIndexedDB,
isExporting,
isImporting,
handleExport,
handleImport,
} = useStorageSettingsHandlers({
defaultFlaskUrl: storageSettingsCopy.molecule.flaskUrlPlaceholder,
exportFilename: () => `${storageSettingsCopy.molecule.exportFilenamePrefix}-${Date.now()}.json`,
importAccept: '.json',
})
return (
<div className="space-y-6">
<BackendCard
backend={backend}
isLoading={isLoading}
flaskUrl={flaskUrl}
isSwitching={isSwitching}
onFlaskUrlChange={setFlaskUrl}
onSwitchToFlask={handleSwitchToFlask}
onSwitchToIndexedDB={handleSwitchToIndexedDB}
onSwitchToSQLite={handleSwitchToSQLite}
/>
<DataManagementCard
isExporting={isExporting}
isImporting={isImporting}
onExport={handleExport}
onImport={handleImport}
/>
</div>
)
}

View File

@@ -1,27 +1,27 @@
export { Breadcrumb } from './Breadcrumb' export { Breadcrumb } from './Breadcrumb'
export { AppBranding } from './AppBranding'
export { CanvasRenderer } from './CanvasRenderer' export { CanvasRenderer } from './CanvasRenderer'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { ComponentPalette } from './ComponentPalette' export { ComponentPalette } from './ComponentPalette'
export { GitHubBuildStatus } from './GitHubBuildStatus'
export { LazyLineChart } from './LazyLineChart'
export { LazyBarChart } from './LazyBarChart'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { PropertyEditor } from './PropertyEditor' export { PropertyEditor } from './PropertyEditor'
export { SearchInput } from './SearchInput'
export { ToolbarButton } from './ToolbarButton' export { ToolbarButton } from './ToolbarButton'
export { TreeFormDialog } from './TreeFormDialog' export { TreeFormDialog } from './TreeFormDialog'
export { SearchInput } from './SearchInput' export { preloadMonacoEditor } from './LazyMonacoEditor'
export { export { ComponentTree } from './ComponentTree'
LoadingFallback, export {
NavigationItem, LoadingFallback,
NavigationItem,
PageHeaderContent, PageHeaderContent,
AppBranding,
CodeExplanationDialog,
ComponentBindingDialog, ComponentBindingDialog,
DataSourceCard,
DataSourceEditorDialog, DataSourceEditorDialog,
GitHubBuildStatus as GitHubBuildStatusJSON, GitHubBuildStatus,
SaveIndicator, LazyBarChart,
ComponentTree,
SeedDataManager,
LazyD3BarChart, LazyD3BarChart,
LazyLineChart,
NavigationGroupHeader,
SaveIndicator,
SeedDataManager,
StorageSettings StorageSettings
} from '@/lib/json-ui/json-components' } from '@/lib/json-ui/json-components'
export { preloadMonacoEditor } from './LazyMonacoEditor'