返回 presentation-ai
1 "use client";
2
3 import { GitBranch, Plus, Trash2 } from "lucide-react";
4
5 import { Button } from "@/components/ui/button";
6 import { Input } from "@/components/ui/input";
7 import { Label } from "@/components/ui/label";
8 import { Textarea } from "@/components/ui/textarea";
9 import { InfographicItemList } from "./item-list";
10 import { InfographicTreeEditor } from "./tree-editor";
11 import { type EditableInfographicData } from "./types";
12 import { countNestedItems } from "./utils";
13
14 interface InfographicDataEditorProps {
15 data: EditableInfographicData;
16 onChange: (data: EditableInfographicData) => void;
17 }
18
19 export function InfographicDataEditor({
20 data,
21 onChange,
22 }: InfographicDataEditorProps) {
23 const isHierarchy = data.sourceField === "root";
24 const isCompare = data.sourceField === "compares";
25 const showRelations = data.sourceField === "nodes";
26
27 const updateData = (patch: Partial<EditableInfographicData>) => {
28 onChange({ ...data, ...patch });
29 };
30
31 const relations = data.relations ?? [];
32
33 return (
34 <div className="flex flex-col gap-8 pb-8">
35 {/* Header Section */}
36 <section className="space-y-5">
37 <div>
38 <h3 className="text-sm font-semibold tracking-tight text-foreground">
39 General Properties
40 </h3>
41 <p className="text-xs text-muted-foreground">
42 Basic information about your infographic.
43 </p>
44 </div>
45
46 <div className="grid gap-4 rounded-xl border border-border/60 bg-card p-4 shadow-sm sm:grid-cols-2">
47 <div className="space-y-2 sm:col-span-2">
48 <Label
49 htmlFor="infographic-title"
50 className="text-xs text-muted-foreground"
51 >
52 TITLE
53 </Label>
54 <Input
55 id="infographic-title"
56 value={data.title ?? ""}
57 onChange={(event) => updateData({ title: event.target.value })}
58 placeholder="e.g. Q3 Performance Summary"
59 className="bg-background/50 font-medium"
60 />
61 </div>
62 <div className="space-y-2 sm:col-span-2">
63 <Label
64 htmlFor="infographic-description"
65 className="text-xs text-muted-foreground"
66 >
67 DESCRIPTION
68 </Label>
69 <Textarea
70 id="infographic-description"
71 value={data.desc ?? ""}
72 onChange={(event) => updateData({ desc: event.target.value })}
73 placeholder="Provide a brief context or subtitle..."
74 className="min-h-20 resize-none bg-background/50 leading-relaxed"
75 />
76 </div>
77 </div>
78 </section>
79
80 {/* Data Section */}
81 <section className="space-y-5">
82 <div className="flex flex-wrap items-end justify-between gap-4">
83 <div>
84 <h3 className="text-sm font-semibold tracking-tight text-foreground">
85 Data Structure
86 </h3>
87 <p className="text-xs text-muted-foreground mt-1">
88 Configure items, hierarchies, and values for the diagram.
89 </p>
90 </div>
91 <div className="flex items-center gap-2 text-xs">
92 <span className="flex items-center rounded-full border border-border/50 bg-muted/40 px-2.5 py-0.5 font-medium text-muted-foreground">
93 Type: {data.sourceField}
94 </span>
95 <span className="flex items-center rounded-full border border-border/50 bg-primary/10 px-2.5 py-0.5 font-medium text-primary">
96 {countNestedItems(data.items)} Item(s)
97 </span>
98 </div>
99 </div>
100
101 <div className="rounded-xl border border-border/60 bg-card/50 p-4">
102 {isHierarchy || isCompare ? (
103 <InfographicTreeEditor
104 items={data.items}
105 variant={isCompare ? "compare" : "hierarchy"}
106 onChange={(items) => updateData({ items })}
107 />
108 ) : (
109 <InfographicItemList
110 items={data.items}
111 mode={data.sourceField}
112 onChange={(items) => updateData({ items })}
113 />
114 )}
115 </div>
116 </section>
117
118 {showRelations ? (
119 <RelationEditor
120 relations={relations}
121 onChange={(nextRelations) => updateData({ relations: nextRelations })}
122 />
123 ) : null}
124 </div>
125 );
126 }
127
128 function RelationEditor({
129 relations,
130 onChange,
131 }: {
132 relations: string[];
133 onChange: (relations: string[]) => void;
134 }) {
135 const updateRelation = (index: number, value: string) => {
136 onChange(
137 relations.map((relation, relationIndex) =>
138 relationIndex === index ? value : relation,
139 ),
140 );
141 };
142
143 const removeRelation = (index: number) => {
144 onChange(relations.filter((_, relationIndex) => relationIndex !== index));
145 };
146
147 const addRelation = () => {
148 onChange([...relations, "node-1 -> node-2"]);
149 };
150
151 return (
152 <section className="space-y-4">
153 <div className="flex items-center justify-between gap-3 border-b border-border/50 pb-3">
154 <div>
155 <p className="flex items-center gap-2 text-sm font-semibold tracking-tight text-foreground">
156 <GitBranch className="size-4" />
157 Relations
158 </p>
159 <p className="text-xs text-muted-foreground mt-1">
160 Connect node ids with arrows, labels, or plain lines.
161 </p>
162 </div>
163 <Button
164 type="button"
165 variant="secondary"
166 size="sm"
167 onClick={addRelation}
168 className="gap-2"
169 >
170 <Plus className="size-3.5" />
171 Add Relation
172 </Button>
173 </div>
174
175 <div className="space-y-3">
176 {relations.map((relation, index) => (
177 <div
178 key={`relation-${index}`}
179 className="group relative grid grid-cols-[minmax(0,1fr)_auto] gap-2 rounded-lg border border-border/60 bg-card p-2 shadow-sm transition-all focus-within:border-ring focus-within:ring-1 focus-within:ring-ring hover:bg-muted/20"
180 >
181 <Input
182 value={relation}
183 onChange={(event) => updateRelation(index, event.target.value)}
184 placeholder="source - label -> target"
185 className="border-0 bg-transparent shadow-none focus-visible:ring-0"
186 />
187 <Button
188 type="button"
189 variant="ghost"
190 size="icon"
191 className="size-9 text-muted-foreground opacity-50 transition-opacity hover:text-destructive group-hover:opacity-100"
192 onClick={() => removeRelation(index)}
193 aria-label="Remove relation"
194 >
195 <Trash2 className="size-4" />
196 </Button>
197 </div>
198 ))}
199 {relations.length === 0 ? (
200 <div className="rounded-xl border border-dashed border-border/60 bg-muted/10 p-6 text-center text-sm text-muted-foreground">
201 No relations defined yet. Add one to connect nodes.
202 </div>
203 ) : null}
204 </div>
205 </section>
206 );
207 }
208
209 export * from "./types";
210
210 lines Plain Text