@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/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 { MoodUIPromptPlayground } from "../index";
3
+ import { generateReactFromPrompt, MoodUIIcon, MOODUI_ICON_NAMES, MoodUIRuntime } from "../index";
4
+ import type { MoodUISpec } from "../spec";
4
5
 
5
- createRoot(document.getElementById("root")!).render(
6
- <div style={{ minHeight: "100vh", padding: 24, background: "#0b1220" }}>
7
- <div style={{ maxWidth: 1100, margin: "0 auto", background: "#fff", padding: 16, borderRadius: 16 }}>
8
- <MoodUIPromptPlayground provider="gemini" model="gemini-3-flash-preview" />
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
- </div>
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 />);