@kohryan/moodui 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +53 -2
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +229 -34
- package/dist/index.mjs +227 -34
- package/dist/ui/assets/index-C2YFmKz4.js +64 -0
- package/dist/ui/index.html +1 -1
- package/package.json +2 -1
- package/src/ui/main.tsx +831 -7
- package/dist/ui/assets/index-CMk6XQXy.js +0 -58
package/src/ui/main.tsx
CHANGED
|
@@ -1,11 +1,835 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
-
import {
|
|
3
|
+
import { generateReactFromPrompt, MoodUIIcon, MOODUI_ICON_NAMES, MoodUIRuntime } from "../index";
|
|
4
|
+
import type { MoodUISpec } from "../spec";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
type Provider = "gemini" | "ollama" | "openai-compatible";
|
|
7
|
+
type SaveStatus = "idle" | "saving" | "success" | "error";
|
|
8
|
+
|
|
9
|
+
const promptPresets = [
|
|
10
|
+
{
|
|
11
|
+
title: "SaaS Dashboard",
|
|
12
|
+
description: "Stat cards, chart summary, navigation, CTA",
|
|
13
|
+
prompt:
|
|
14
|
+
"Buat dashboard SaaS modern bergaya clean seperti Untitled UI: header, 3 stat card, section aktivitas terbaru, chart summary, tombol primary, dan gunakan icon chart-bar, bell, dan arrow-right."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: "CRM Workspace",
|
|
18
|
+
description: "Sidebar, table preview, status cards",
|
|
19
|
+
prompt:
|
|
20
|
+
"Buat UI CRM workspace modern dengan sidebar, top bar pencarian, card pipeline singkat, daftar customer, dan gunakan icon search, user, mail, plus, dan settings."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "E-commerce Landing",
|
|
24
|
+
description: "Hero, feature grid, trust badges",
|
|
25
|
+
prompt:
|
|
26
|
+
"Buat landing page e-commerce modern dengan hero section, badge promo, 3 fitur utama, CTA beli sekarang, dan gunakan icon sparkles, shield, heart, dan arrow-right."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "Productivity App",
|
|
30
|
+
description: "Tasks, reminders, quick actions",
|
|
31
|
+
prompt:
|
|
32
|
+
"Buat dashboard productivity app dengan welcome card, daftar task hari ini, reminder, action button, dan gunakan icon check, calendar, bell, dan message-circle."
|
|
33
|
+
}
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
const modernPromptHint =
|
|
37
|
+
"Gunakan layout modern bergaya Flowbite/Untitled UI: card yang rapi, hierarchy kuat, spacing konsisten, warna netral, dan icon reusable seperlunya.";
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
const [provider, setProvider] = React.useState<Provider>("gemini");
|
|
41
|
+
const [model, setModel] = React.useState("gemini-3-flash-preview");
|
|
42
|
+
const [apiKey, setApiKey] = React.useState("");
|
|
43
|
+
const [baseUrl, setBaseUrl] = React.useState("");
|
|
44
|
+
const [prompt, setPrompt] = React.useState(
|
|
45
|
+
"Buat dashboard mood tracker modern: top summary, form input mood, riwayat mood, CTA utama, dan gunakan icon heart, calendar, dan chart-bar."
|
|
46
|
+
);
|
|
47
|
+
const [componentName, setComponentName] = React.useState("MoodScreen");
|
|
48
|
+
const [outputPath, setOutputPath] = React.useState("src/generated/MoodScreen.tsx");
|
|
49
|
+
const [loading, setLoading] = React.useState(false);
|
|
50
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
51
|
+
const [spec, setSpec] = React.useState<MoodUISpec | null>(null);
|
|
52
|
+
const [code, setCode] = React.useState("");
|
|
53
|
+
const [copied, setCopied] = React.useState(false);
|
|
54
|
+
const [saveStatus, setSaveStatus] = React.useState<SaveStatus>("idle");
|
|
55
|
+
const suggestedPathRef = React.useRef(outputPath);
|
|
56
|
+
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
const nextSuggestedPath = `src/generated/${componentName || "MoodScreen"}.tsx`;
|
|
59
|
+
setOutputPath((current) => (current === suggestedPathRef.current ? nextSuggestedPath : current));
|
|
60
|
+
suggestedPathRef.current = nextSuggestedPath;
|
|
61
|
+
}, [componentName]);
|
|
62
|
+
|
|
63
|
+
const onGenerate = React.useCallback(async () => {
|
|
64
|
+
setLoading(true);
|
|
65
|
+
setError(null);
|
|
66
|
+
setCopied(false);
|
|
67
|
+
setSaveStatus("idle");
|
|
68
|
+
try {
|
|
69
|
+
const result =
|
|
70
|
+
provider === "ollama"
|
|
71
|
+
? await generateReactFromPrompt({
|
|
72
|
+
provider,
|
|
73
|
+
model,
|
|
74
|
+
baseUrl: baseUrl || undefined,
|
|
75
|
+
prompt,
|
|
76
|
+
componentName
|
|
77
|
+
})
|
|
78
|
+
: provider === "openai-compatible"
|
|
79
|
+
? await generateReactFromPrompt({
|
|
80
|
+
provider,
|
|
81
|
+
model,
|
|
82
|
+
apiKey,
|
|
83
|
+
baseUrl,
|
|
84
|
+
prompt,
|
|
85
|
+
componentName
|
|
86
|
+
})
|
|
87
|
+
: await generateReactFromPrompt({
|
|
88
|
+
provider,
|
|
89
|
+
model,
|
|
90
|
+
apiKey,
|
|
91
|
+
baseUrl: baseUrl || undefined,
|
|
92
|
+
prompt,
|
|
93
|
+
componentName
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
setSpec(result.spec);
|
|
97
|
+
setCode(result.code);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
100
|
+
setError(msg);
|
|
101
|
+
} finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}, [apiKey, baseUrl, componentName, model, prompt, provider]);
|
|
105
|
+
|
|
106
|
+
const onCopyCode = React.useCallback(async () => {
|
|
107
|
+
if (!code) return;
|
|
108
|
+
try {
|
|
109
|
+
await navigator.clipboard.writeText(code);
|
|
110
|
+
setCopied(true);
|
|
111
|
+
window.setTimeout(() => setCopied(false), 1800);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error("Failed to copy:", e);
|
|
114
|
+
}
|
|
115
|
+
}, [code]);
|
|
116
|
+
|
|
117
|
+
const onDownloadCode = React.useCallback(() => {
|
|
118
|
+
if (!code) return;
|
|
119
|
+
const blob = new Blob([code], { type: "text/plain" });
|
|
120
|
+
const url = URL.createObjectURL(blob);
|
|
121
|
+
const anchor = document.createElement("a");
|
|
122
|
+
anchor.href = url;
|
|
123
|
+
anchor.download = `${componentName || "MoodScreen"}.tsx`;
|
|
124
|
+
document.body.appendChild(anchor);
|
|
125
|
+
anchor.click();
|
|
126
|
+
document.body.removeChild(anchor);
|
|
127
|
+
URL.revokeObjectURL(url);
|
|
128
|
+
}, [code, componentName]);
|
|
129
|
+
|
|
130
|
+
const onSaveToProject = React.useCallback(async () => {
|
|
131
|
+
if (!code) return;
|
|
132
|
+
setSaveStatus("saving");
|
|
133
|
+
setError(null);
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch("/api/save-file", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify({ path: outputPath, code })
|
|
139
|
+
});
|
|
140
|
+
const result = (await response.json()) as { success: boolean; error?: string };
|
|
141
|
+
if (!result.success) throw new Error(result.error ?? "Gagal menyimpan file");
|
|
142
|
+
setSaveStatus("success");
|
|
143
|
+
window.setTimeout(() => setSaveStatus("idle"), 2200);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
146
|
+
setError(msg);
|
|
147
|
+
setSaveStatus("error");
|
|
148
|
+
window.setTimeout(() => setSaveStatus("idle"), 2600);
|
|
149
|
+
}
|
|
150
|
+
}, [code, outputPath]);
|
|
151
|
+
|
|
152
|
+
const applyPreset = React.useCallback((value: string) => {
|
|
153
|
+
setPrompt(value);
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
const appendModernHint = React.useCallback(() => {
|
|
157
|
+
setPrompt((current) => `${current.trim()}\n\n${modernPromptHint}`);
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const appendIconHint = React.useCallback(() => {
|
|
161
|
+
setPrompt(
|
|
162
|
+
(current) =>
|
|
163
|
+
`${current.trim()}\n\nGunakan icon reusable dari MoodUI seperti search, chart-bar, bell, user, calendar, home, plus, shield, sparkles, dan arrow-right jika relevan.`
|
|
164
|
+
);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const codeLineCount = React.useMemo(() => (code ? code.split("\n").length : 0), [code]);
|
|
168
|
+
const saveButtonLabel =
|
|
169
|
+
saveStatus === "saving"
|
|
170
|
+
? "Menyimpan..."
|
|
171
|
+
: saveStatus === "success"
|
|
172
|
+
? "Tersimpan"
|
|
173
|
+
: saveStatus === "error"
|
|
174
|
+
? "Gagal"
|
|
175
|
+
: "Save to Project";
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
style={{
|
|
180
|
+
minHeight: "100vh",
|
|
181
|
+
background:
|
|
182
|
+
"radial-gradient(circle at top left, rgba(124,58,237,0.22), transparent 28%), radial-gradient(circle at top right, rgba(14,165,233,0.18), transparent 32%), linear-gradient(180deg, #0f172a 0%, #111827 100%)",
|
|
183
|
+
color: "#0f172a",
|
|
184
|
+
padding: 28
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<div style={{ maxWidth: 1440, margin: "0 auto", display: "flex", flexDirection: "column", gap: 20 }}>
|
|
188
|
+
<section
|
|
189
|
+
style={{
|
|
190
|
+
borderRadius: 28,
|
|
191
|
+
padding: 28,
|
|
192
|
+
background: "rgba(255,255,255,0.94)",
|
|
193
|
+
border: "1px solid rgba(255,255,255,0.65)",
|
|
194
|
+
boxShadow: "0 24px 80px rgba(15,23,42,0.26)",
|
|
195
|
+
backdropFilter: "blur(14px)"
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<div
|
|
199
|
+
style={{
|
|
200
|
+
display: "grid",
|
|
201
|
+
gridTemplateColumns: "minmax(0, 1.5fr) minmax(320px, 0.9fr)",
|
|
202
|
+
gap: 20,
|
|
203
|
+
alignItems: "stretch"
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
|
207
|
+
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
|
208
|
+
<Badge icon="sparkles" text="MoodUI Browser CLI" tone="purple" />
|
|
209
|
+
<Badge icon="check" text="Generate to Repo" tone="green" />
|
|
210
|
+
<Badge icon="shield" text="Reusable Icon Registry" tone="blue" />
|
|
211
|
+
</div>
|
|
212
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
213
|
+
<h1 style={{ margin: 0, fontSize: 34, lineHeight: 1.05, fontWeight: 800 }}>
|
|
214
|
+
Generate UI modern, simpan langsung ke repo, dan pakai icon reusable bawaan MoodUI.
|
|
215
|
+
</h1>
|
|
216
|
+
<p style={{ margin: 0, color: "#475467", fontSize: 15, lineHeight: 1.7, maxWidth: 760 }}>
|
|
217
|
+
Tampilan ini saya rapikan ke arah product tool yang lebih dekat ke rasa Flowbite atau Untitled UI:
|
|
218
|
+
panel jelas, quick templates, output path eksplisit, preview langsung, dan daftar icon yang bisa
|
|
219
|
+
diulang pemakaiannya di banyak screen.
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
|
|
223
|
+
<StatCard icon="plus" label="Quick Templates" value={String(promptPresets.length)} />
|
|
224
|
+
<StatCard icon="sparkles" label="Reusable Icons" value={String(MOODUI_ICON_NAMES.length)} />
|
|
225
|
+
<StatCard icon="check" label="Generated Lines" value={String(codeLineCount)} />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div
|
|
230
|
+
style={{
|
|
231
|
+
borderRadius: 24,
|
|
232
|
+
padding: 18,
|
|
233
|
+
background: "linear-gradient(180deg, #111827 0%, #0f172a 100%)",
|
|
234
|
+
color: "#ffffff",
|
|
235
|
+
display: "flex",
|
|
236
|
+
flexDirection: "column",
|
|
237
|
+
gap: 14,
|
|
238
|
+
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)"
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
242
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
243
|
+
<div
|
|
244
|
+
style={{
|
|
245
|
+
width: 40,
|
|
246
|
+
height: 40,
|
|
247
|
+
borderRadius: 12,
|
|
248
|
+
display: "grid",
|
|
249
|
+
placeItems: "center",
|
|
250
|
+
background: "linear-gradient(135deg, #7c3aed 0%, #2563eb 100%)"
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<MoodUIIcon name="sparkles" color="#ffffff" size={20} />
|
|
254
|
+
</div>
|
|
255
|
+
<div>
|
|
256
|
+
<div style={{ fontSize: 14, fontWeight: 700 }}>Recommended Workflow</div>
|
|
257
|
+
<div style={{ fontSize: 12, color: "#cbd5e1" }}>Generate, review, lalu save ke repo webapp</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
<ChecklistItem icon="search" text="Tulis prompt UI yang spesifik dan modern" />
|
|
262
|
+
<ChecklistItem icon="chart-bar" text="Gunakan preset kalau butuh struktur awal cepat" />
|
|
263
|
+
<ChecklistItem icon="calendar" text="Sesuaikan component name dan output path" />
|
|
264
|
+
<ChecklistItem icon="check" text="Preview hasil sebelum simpan ke project" />
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</section>
|
|
268
|
+
|
|
269
|
+
<div
|
|
270
|
+
style={{
|
|
271
|
+
display: "grid",
|
|
272
|
+
gridTemplateColumns: "minmax(380px, 0.95fr) minmax(0, 1.45fr)",
|
|
273
|
+
gap: 20,
|
|
274
|
+
alignItems: "start"
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
|
278
|
+
<Panel title="Generate UI" icon="sparkles" subtitle="Form utama untuk menghasilkan komponen React">
|
|
279
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
|
280
|
+
<Field label="Component Name">
|
|
281
|
+
<input
|
|
282
|
+
value={componentName}
|
|
283
|
+
onChange={(e) => setComponentName(e.target.value)}
|
|
284
|
+
placeholder="SalesDashboard"
|
|
285
|
+
style={inputStyle}
|
|
286
|
+
/>
|
|
287
|
+
</Field>
|
|
288
|
+
<Field label="Output Path">
|
|
289
|
+
<input
|
|
290
|
+
value={outputPath}
|
|
291
|
+
onChange={(e) => setOutputPath(e.target.value)}
|
|
292
|
+
placeholder="src/generated/SalesDashboard.tsx"
|
|
293
|
+
style={inputStyle}
|
|
294
|
+
/>
|
|
295
|
+
</Field>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
|
299
|
+
<Field label="Provider">
|
|
300
|
+
<select value={provider} onChange={(e) => setProvider(e.target.value as Provider)} style={inputStyle}>
|
|
301
|
+
<option value="gemini">gemini</option>
|
|
302
|
+
<option value="ollama">ollama</option>
|
|
303
|
+
<option value="openai-compatible">openai-compatible</option>
|
|
304
|
+
</select>
|
|
305
|
+
</Field>
|
|
306
|
+
<Field label="Model">
|
|
307
|
+
<input
|
|
308
|
+
value={model}
|
|
309
|
+
onChange={(e) => setModel(e.target.value)}
|
|
310
|
+
placeholder="gemini-3-flash-preview"
|
|
311
|
+
style={inputStyle}
|
|
312
|
+
/>
|
|
313
|
+
</Field>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{provider !== "ollama" ? (
|
|
317
|
+
<Field label="API Key">
|
|
318
|
+
<input
|
|
319
|
+
value={apiKey}
|
|
320
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
321
|
+
placeholder="Masukkan API key"
|
|
322
|
+
type="password"
|
|
323
|
+
style={inputStyle}
|
|
324
|
+
/>
|
|
325
|
+
</Field>
|
|
326
|
+
) : null}
|
|
327
|
+
|
|
328
|
+
<Field label="Base URL">
|
|
329
|
+
<input
|
|
330
|
+
value={baseUrl}
|
|
331
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
332
|
+
placeholder={provider === "ollama" ? "http://localhost:11434" : "Optional"}
|
|
333
|
+
style={inputStyle}
|
|
334
|
+
/>
|
|
335
|
+
</Field>
|
|
336
|
+
|
|
337
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
|
|
338
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: "#101828" }}>Prompt UI</div>
|
|
339
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
340
|
+
<MiniButton icon="sparkles" label="Modernize" onClick={appendModernHint} />
|
|
341
|
+
<MiniButton icon="shield" label="Add Icons" onClick={appendIconHint} />
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<textarea
|
|
346
|
+
value={prompt}
|
|
347
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
348
|
+
rows={11}
|
|
349
|
+
style={{
|
|
350
|
+
...inputStyle,
|
|
351
|
+
minHeight: 240,
|
|
352
|
+
resize: "vertical",
|
|
353
|
+
lineHeight: 1.65,
|
|
354
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
|
|
358
|
+
<button type="button" onClick={onGenerate} disabled={loading} style={primaryButtonStyle(loading)}>
|
|
359
|
+
<MoodUIIcon name={loading ? "settings" : "sparkles"} color="#ffffff" size={16} />
|
|
360
|
+
{loading ? "Generating..." : "Generate Component"}
|
|
361
|
+
</button>
|
|
362
|
+
|
|
363
|
+
<div
|
|
364
|
+
style={{
|
|
365
|
+
display: "grid",
|
|
366
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
367
|
+
gap: 10
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{promptPresets.map((preset) => (
|
|
371
|
+
<button
|
|
372
|
+
key={preset.title}
|
|
373
|
+
type="button"
|
|
374
|
+
onClick={() => applyPreset(preset.prompt)}
|
|
375
|
+
style={presetCardStyle}
|
|
376
|
+
>
|
|
377
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
378
|
+
<div
|
|
379
|
+
style={{
|
|
380
|
+
width: 36,
|
|
381
|
+
height: 36,
|
|
382
|
+
borderRadius: 12,
|
|
383
|
+
display: "grid",
|
|
384
|
+
placeItems: "center",
|
|
385
|
+
background: "#eef2ff",
|
|
386
|
+
color: "#4338ca"
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<MoodUIIcon name="sparkles" size={18} />
|
|
390
|
+
</div>
|
|
391
|
+
<div style={{ textAlign: "left" }}>
|
|
392
|
+
<div style={{ fontWeight: 700, fontSize: 13 }}>{preset.title}</div>
|
|
393
|
+
<div style={{ fontSize: 12, color: "#667085" }}>{preset.description}</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</button>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{error ? (
|
|
401
|
+
<div
|
|
402
|
+
style={{
|
|
403
|
+
borderRadius: 16,
|
|
404
|
+
padding: 14,
|
|
405
|
+
border: "1px solid #fda29b",
|
|
406
|
+
background: "#fef3f2",
|
|
407
|
+
color: "#b42318",
|
|
408
|
+
fontSize: 13,
|
|
409
|
+
lineHeight: 1.6,
|
|
410
|
+
whiteSpace: "pre-wrap"
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
{error}
|
|
414
|
+
</div>
|
|
415
|
+
) : null}
|
|
416
|
+
</Panel>
|
|
417
|
+
|
|
418
|
+
<Panel title="Icon Library" icon="shield" subtitle="Referensi icon reusable yang dikenali MoodUI">
|
|
419
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
|
|
420
|
+
{MOODUI_ICON_NAMES.map((iconName) => (
|
|
421
|
+
<button
|
|
422
|
+
key={iconName}
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={() => setPrompt((current) => `${current.trim()}\n- gunakan icon ${iconName}`)}
|
|
425
|
+
style={iconChipStyle}
|
|
426
|
+
>
|
|
427
|
+
<MoodUIIcon name={iconName} size={16} color="#344054" />
|
|
428
|
+
<span>{iconName}</span>
|
|
429
|
+
</button>
|
|
430
|
+
))}
|
|
431
|
+
</div>
|
|
432
|
+
<div
|
|
433
|
+
style={{
|
|
434
|
+
borderRadius: 16,
|
|
435
|
+
padding: 14,
|
|
436
|
+
background: "#f8fafc",
|
|
437
|
+
border: "1px solid #e2e8f0",
|
|
438
|
+
fontSize: 13,
|
|
439
|
+
lineHeight: 1.7,
|
|
440
|
+
color: "#475467"
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
Tips: cukup minta model memakai icon berdasarkan nama, misalnya{" "}
|
|
444
|
+
<code style={inlineCodeStyle}>search</code>, <code style={inlineCodeStyle}>calendar</code>,{" "}
|
|
445
|
+
<code style={inlineCodeStyle}>chart-bar</code>, atau <code style={inlineCodeStyle}>arrow-right</code>.
|
|
446
|
+
</div>
|
|
447
|
+
</Panel>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
|
451
|
+
<Panel title="Live Preview" icon="home" subtitle="Hasil render MoodUIRuntime dari spec terbaru">
|
|
452
|
+
<div
|
|
453
|
+
style={{
|
|
454
|
+
minHeight: 460,
|
|
455
|
+
borderRadius: 24,
|
|
456
|
+
padding: 18,
|
|
457
|
+
background: "linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%)",
|
|
458
|
+
border: "1px solid #dbe4ff",
|
|
459
|
+
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.7)"
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
{spec ? (
|
|
463
|
+
<div
|
|
464
|
+
style={{
|
|
465
|
+
minHeight: 420,
|
|
466
|
+
borderRadius: 20,
|
|
467
|
+
padding: 18,
|
|
468
|
+
background: "#ffffff",
|
|
469
|
+
boxShadow: "0 18px 48px rgba(15,23,42,0.08)",
|
|
470
|
+
overflow: "auto"
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
<MoodUIRuntime spec={spec} />
|
|
474
|
+
</div>
|
|
475
|
+
) : (
|
|
476
|
+
<EmptyState
|
|
477
|
+
icon="sparkles"
|
|
478
|
+
title="Belum ada preview"
|
|
479
|
+
description="Generate komponen dulu. Preview dan code akan muncul di sini."
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</Panel>
|
|
484
|
+
|
|
485
|
+
<Panel title="Generated Code" icon="arrow-right" subtitle="Copy, download, atau simpan langsung ke repo">
|
|
486
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
|
|
487
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
488
|
+
<InfoPill icon="check" label={`${codeLineCount} lines`} />
|
|
489
|
+
<InfoPill icon="home" label={outputPath} />
|
|
490
|
+
</div>
|
|
491
|
+
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
|
492
|
+
<button type="button" onClick={onCopyCode} disabled={!code} style={secondaryActionStyle(!code)}>
|
|
493
|
+
<MoodUIIcon name="plus" size={15} />
|
|
494
|
+
{copied ? "Copied" : "Copy"}
|
|
495
|
+
</button>
|
|
496
|
+
<button type="button" onClick={onDownloadCode} disabled={!code} style={secondaryActionStyle(!code)}>
|
|
497
|
+
<MoodUIIcon name="arrow-right" size={15} />
|
|
498
|
+
Download
|
|
499
|
+
</button>
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
onClick={onSaveToProject}
|
|
503
|
+
disabled={!code || saveStatus === "saving"}
|
|
504
|
+
style={saveActionStyle(saveStatus, !code)}
|
|
505
|
+
>
|
|
506
|
+
<MoodUIIcon
|
|
507
|
+
name={saveStatus === "success" ? "check" : saveStatus === "error" ? "shield" : "home"}
|
|
508
|
+
size={15}
|
|
509
|
+
color={saveStatus === "error" ? "#b42318" : undefined}
|
|
510
|
+
/>
|
|
511
|
+
{saveButtonLabel}
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<textarea
|
|
517
|
+
value={code}
|
|
518
|
+
readOnly
|
|
519
|
+
rows={18}
|
|
520
|
+
style={{
|
|
521
|
+
...inputStyle,
|
|
522
|
+
minHeight: 360,
|
|
523
|
+
background: "#0f172a",
|
|
524
|
+
color: "#e2e8f0",
|
|
525
|
+
border: "1px solid #1e293b",
|
|
526
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
527
|
+
lineHeight: 1.55
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
<div style={{ fontSize: 12, color: "#667085" }}>
|
|
531
|
+
Jalankan <code style={inlineCodeStyle}>npx moodui ui</code> dari root repo webapp agar tombol{" "}
|
|
532
|
+
<code style={inlineCodeStyle}>Save to Project</code> menulis file ke repo tersebut.
|
|
533
|
+
</div>
|
|
534
|
+
</Panel>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function Panel(props: { title: string; subtitle: string; icon: React.ComponentProps<typeof MoodUIIcon>["name"]; children: React.ReactNode }) {
|
|
543
|
+
return (
|
|
544
|
+
<section
|
|
545
|
+
style={{
|
|
546
|
+
borderRadius: 24,
|
|
547
|
+
padding: 20,
|
|
548
|
+
background: "rgba(255,255,255,0.96)",
|
|
549
|
+
border: "1px solid rgba(255,255,255,0.72)",
|
|
550
|
+
boxShadow: "0 16px 48px rgba(15,23,42,0.18)"
|
|
551
|
+
}}
|
|
552
|
+
>
|
|
553
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, marginBottom: 16 }}>
|
|
554
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
555
|
+
<div
|
|
556
|
+
style={{
|
|
557
|
+
width: 40,
|
|
558
|
+
height: 40,
|
|
559
|
+
borderRadius: 14,
|
|
560
|
+
display: "grid",
|
|
561
|
+
placeItems: "center",
|
|
562
|
+
background: "#eef2ff",
|
|
563
|
+
color: "#4338ca"
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
<MoodUIIcon name={props.icon} size={18} />
|
|
567
|
+
</div>
|
|
568
|
+
<div>
|
|
569
|
+
<div style={{ fontSize: 16, fontWeight: 800, color: "#101828" }}>{props.title}</div>
|
|
570
|
+
<div style={{ fontSize: 13, color: "#667085" }}>{props.subtitle}</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>{props.children}</div>
|
|
575
|
+
</section>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function Field(props: { label: string; children: React.ReactNode }) {
|
|
580
|
+
return (
|
|
581
|
+
<label style={{ display: "flex", flexDirection: "column", gap: 7 }}>
|
|
582
|
+
<span style={{ fontSize: 12, fontWeight: 700, color: "#344054", letterSpacing: 0.2 }}>{props.label}</span>
|
|
583
|
+
{props.children}
|
|
584
|
+
</label>
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function Badge(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; text: string; tone: "purple" | "green" | "blue" }) {
|
|
589
|
+
const tones = {
|
|
590
|
+
purple: { background: "#f5f3ff", color: "#6d28d9", border: "#ddd6fe" },
|
|
591
|
+
green: { background: "#ecfdf3", color: "#027a48", border: "#abefc6" },
|
|
592
|
+
blue: { background: "#eff8ff", color: "#175cd3", border: "#b2ddff" }
|
|
593
|
+
} as const;
|
|
594
|
+
const tone = tones[props.tone];
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<div
|
|
598
|
+
style={{
|
|
599
|
+
display: "inline-flex",
|
|
600
|
+
alignItems: "center",
|
|
601
|
+
gap: 8,
|
|
602
|
+
padding: "8px 12px",
|
|
603
|
+
borderRadius: 999,
|
|
604
|
+
background: tone.background,
|
|
605
|
+
border: `1px solid ${tone.border}`,
|
|
606
|
+
color: tone.color,
|
|
607
|
+
fontSize: 12,
|
|
608
|
+
fontWeight: 700
|
|
609
|
+
}}
|
|
610
|
+
>
|
|
611
|
+
<MoodUIIcon name={props.icon} size={14} />
|
|
612
|
+
{props.text}
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function StatCard(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; label: string; value: string }) {
|
|
618
|
+
return (
|
|
619
|
+
<div
|
|
620
|
+
style={{
|
|
621
|
+
borderRadius: 18,
|
|
622
|
+
padding: 16,
|
|
623
|
+
background: "#f8fafc",
|
|
624
|
+
border: "1px solid #e2e8f0",
|
|
625
|
+
display: "flex",
|
|
626
|
+
flexDirection: "column",
|
|
627
|
+
gap: 10
|
|
628
|
+
}}
|
|
629
|
+
>
|
|
630
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
631
|
+
<MoodUIIcon name={props.icon} size={16} color="#475467" />
|
|
632
|
+
<span style={{ fontSize: 12, color: "#667085", fontWeight: 700 }}>{props.label}</span>
|
|
633
|
+
</div>
|
|
634
|
+
<div style={{ fontSize: 26, lineHeight: 1, fontWeight: 800, color: "#101828" }}>{props.value}</div>
|
|
635
|
+
</div>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function ChecklistItem(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; text: string }) {
|
|
640
|
+
return (
|
|
641
|
+
<div
|
|
642
|
+
style={{
|
|
643
|
+
display: "flex",
|
|
644
|
+
alignItems: "center",
|
|
645
|
+
gap: 10,
|
|
646
|
+
borderRadius: 14,
|
|
647
|
+
padding: "10px 12px",
|
|
648
|
+
background: "rgba(255,255,255,0.05)",
|
|
649
|
+
border: "1px solid rgba(255,255,255,0.08)"
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
<MoodUIIcon name={props.icon} size={15} color="#c4b5fd" />
|
|
653
|
+
<span style={{ fontSize: 13, color: "#e2e8f0" }}>{props.text}</span>
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function MiniButton(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; label: string; onClick: () => void }) {
|
|
659
|
+
return (
|
|
660
|
+
<button type="button" onClick={props.onClick} style={miniButtonStyle}>
|
|
661
|
+
<MoodUIIcon name={props.icon} size={14} />
|
|
662
|
+
{props.label}
|
|
663
|
+
</button>
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function InfoPill(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; label: string }) {
|
|
668
|
+
return (
|
|
669
|
+
<div
|
|
670
|
+
style={{
|
|
671
|
+
display: "inline-flex",
|
|
672
|
+
alignItems: "center",
|
|
673
|
+
gap: 8,
|
|
674
|
+
padding: "8px 12px",
|
|
675
|
+
borderRadius: 999,
|
|
676
|
+
background: "#f8fafc",
|
|
677
|
+
border: "1px solid #e4e7ec",
|
|
678
|
+
fontSize: 12,
|
|
679
|
+
color: "#475467",
|
|
680
|
+
maxWidth: 320
|
|
681
|
+
}}
|
|
682
|
+
>
|
|
683
|
+
<MoodUIIcon name={props.icon} size={14} />
|
|
684
|
+
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{props.label}</span>
|
|
685
|
+
</div>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function EmptyState(props: { icon: React.ComponentProps<typeof MoodUIIcon>["name"]; title: string; description: string }) {
|
|
690
|
+
return (
|
|
691
|
+
<div
|
|
692
|
+
style={{
|
|
693
|
+
minHeight: 380,
|
|
694
|
+
display: "grid",
|
|
695
|
+
placeItems: "center",
|
|
696
|
+
textAlign: "center",
|
|
697
|
+
color: "#667085",
|
|
698
|
+
padding: 24
|
|
699
|
+
}}
|
|
700
|
+
>
|
|
701
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 14, maxWidth: 280 }}>
|
|
702
|
+
<div
|
|
703
|
+
style={{
|
|
704
|
+
width: 56,
|
|
705
|
+
height: 56,
|
|
706
|
+
borderRadius: 20,
|
|
707
|
+
display: "grid",
|
|
708
|
+
placeItems: "center",
|
|
709
|
+
background: "#eef2ff",
|
|
710
|
+
color: "#4338ca"
|
|
711
|
+
}}
|
|
712
|
+
>
|
|
713
|
+
<MoodUIIcon name={props.icon} size={24} />
|
|
714
|
+
</div>
|
|
715
|
+
<div style={{ fontSize: 16, fontWeight: 800, color: "#101828" }}>{props.title}</div>
|
|
716
|
+
<div style={{ fontSize: 13, lineHeight: 1.7 }}>{props.description}</div>
|
|
717
|
+
</div>
|
|
9
718
|
</div>
|
|
10
|
-
|
|
11
|
-
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const inputStyle: React.CSSProperties = {
|
|
723
|
+
width: "100%",
|
|
724
|
+
padding: "12px 14px",
|
|
725
|
+
borderRadius: 14,
|
|
726
|
+
border: "1px solid #d0d5dd",
|
|
727
|
+
background: "#ffffff",
|
|
728
|
+
color: "#101828",
|
|
729
|
+
fontSize: 14,
|
|
730
|
+
outline: "none",
|
|
731
|
+
boxSizing: "border-box"
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const miniButtonStyle: React.CSSProperties = {
|
|
735
|
+
display: "inline-flex",
|
|
736
|
+
alignItems: "center",
|
|
737
|
+
gap: 8,
|
|
738
|
+
padding: "8px 12px",
|
|
739
|
+
borderRadius: 999,
|
|
740
|
+
border: "1px solid #d0d5dd",
|
|
741
|
+
background: "#ffffff",
|
|
742
|
+
color: "#344054",
|
|
743
|
+
fontSize: 12,
|
|
744
|
+
fontWeight: 700,
|
|
745
|
+
cursor: "pointer"
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const presetCardStyle: React.CSSProperties = {
|
|
749
|
+
padding: 14,
|
|
750
|
+
borderRadius: 18,
|
|
751
|
+
border: "1px solid #e4e7ec",
|
|
752
|
+
background: "#ffffff",
|
|
753
|
+
cursor: "pointer"
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const iconChipStyle: React.CSSProperties = {
|
|
757
|
+
display: "inline-flex",
|
|
758
|
+
alignItems: "center",
|
|
759
|
+
gap: 8,
|
|
760
|
+
padding: "10px 12px",
|
|
761
|
+
borderRadius: 999,
|
|
762
|
+
border: "1px solid #e4e7ec",
|
|
763
|
+
background: "#ffffff",
|
|
764
|
+
color: "#344054",
|
|
765
|
+
fontSize: 12,
|
|
766
|
+
fontWeight: 700,
|
|
767
|
+
cursor: "pointer"
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const inlineCodeStyle: React.CSSProperties = {
|
|
771
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
772
|
+
background: "#f2f4f7",
|
|
773
|
+
padding: "2px 6px",
|
|
774
|
+
borderRadius: 6
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
function primaryButtonStyle(disabled: boolean): React.CSSProperties {
|
|
778
|
+
return {
|
|
779
|
+
display: "inline-flex",
|
|
780
|
+
alignItems: "center",
|
|
781
|
+
justifyContent: "center",
|
|
782
|
+
gap: 10,
|
|
783
|
+
width: "100%",
|
|
784
|
+
padding: "14px 18px",
|
|
785
|
+
borderRadius: 16,
|
|
786
|
+
border: "1px solid #111827",
|
|
787
|
+
background: disabled ? "#475467" : "linear-gradient(135deg, #111827 0%, #1d4ed8 100%)",
|
|
788
|
+
color: "#ffffff",
|
|
789
|
+
fontSize: 14,
|
|
790
|
+
fontWeight: 800,
|
|
791
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
792
|
+
boxShadow: disabled ? "none" : "0 14px 32px rgba(29,78,216,0.22)"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function secondaryActionStyle(disabled: boolean): React.CSSProperties {
|
|
797
|
+
return {
|
|
798
|
+
display: "inline-flex",
|
|
799
|
+
alignItems: "center",
|
|
800
|
+
gap: 8,
|
|
801
|
+
padding: "10px 14px",
|
|
802
|
+
borderRadius: 12,
|
|
803
|
+
border: "1px solid #d0d5dd",
|
|
804
|
+
background: disabled ? "#f2f4f7" : "#ffffff",
|
|
805
|
+
color: disabled ? "#98a2b3" : "#344054",
|
|
806
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
807
|
+
fontSize: 13,
|
|
808
|
+
fontWeight: 700
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function saveActionStyle(status: SaveStatus, disabled: boolean): React.CSSProperties {
|
|
813
|
+
const tone =
|
|
814
|
+
status === "success"
|
|
815
|
+
? { background: "#ecfdf3", border: "#abefc6", color: "#027a48" }
|
|
816
|
+
: status === "error"
|
|
817
|
+
? { background: "#fef3f2", border: "#fecdca", color: "#b42318" }
|
|
818
|
+
: { background: "#111827", border: "#111827", color: "#ffffff" };
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
display: "inline-flex",
|
|
822
|
+
alignItems: "center",
|
|
823
|
+
gap: 8,
|
|
824
|
+
padding: "10px 14px",
|
|
825
|
+
borderRadius: 12,
|
|
826
|
+
border: `1px solid ${disabled && status === "idle" ? "#d0d5dd" : tone.border}`,
|
|
827
|
+
background: disabled && status === "idle" ? "#f2f4f7" : tone.background,
|
|
828
|
+
color: disabled && status === "idle" ? "#98a2b3" : tone.color,
|
|
829
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
830
|
+
fontSize: 13,
|
|
831
|
+
fontWeight: 700
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
createRoot(document.getElementById("root")!).render(<App />);
|