@alepha/devtools 0.13.6 → 0.13.7

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.
Files changed (62) hide show
  1. package/dist/index.d.ts +249 -32
  2. package/dist/index.js +253 -22
  3. package/dist/index.js.map +1 -1
  4. package/package.json +12 -6
  5. package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
  6. package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
  7. package/src/api/schemas/DevAtomMetadata.ts +26 -0
  8. package/src/api/schemas/DevCommandMetadata.ts +9 -0
  9. package/src/api/schemas/DevEntityMetadata.ts +57 -0
  10. package/src/api/schemas/DevEnvMetadata.ts +22 -0
  11. package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
  12. package/src/api/schemas/DevRouteMetadata.ts +8 -0
  13. package/src/index.ts +23 -16
  14. package/src/ui/AppRouter.tsx +85 -2
  15. package/src/ui/components/DevAtomsViewer.tsx +636 -0
  16. package/src/ui/components/DevCacheInspector.tsx +423 -0
  17. package/src/ui/components/DevDashboard.tsx +188 -0
  18. package/src/ui/components/DevEnvExplorer.tsx +462 -0
  19. package/src/ui/components/DevLayout.tsx +65 -4
  20. package/src/ui/components/DevLogViewer.tsx +161 -163
  21. package/src/ui/components/DevQueueMonitor.tsx +51 -0
  22. package/src/ui/components/DevTopicsViewer.tsx +690 -0
  23. package/src/ui/components/actions/ActionGroup.tsx +37 -0
  24. package/src/ui/components/actions/ActionItem.tsx +138 -0
  25. package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
  26. package/src/ui/components/actions/MethodBadge.tsx +18 -0
  27. package/src/ui/components/actions/SchemaViewer.tsx +21 -0
  28. package/src/ui/components/actions/TryItPanel.tsx +140 -0
  29. package/src/ui/components/actions/constants.ts +7 -0
  30. package/src/ui/components/actions/helpers.ts +18 -0
  31. package/src/ui/components/actions/index.ts +8 -0
  32. package/src/ui/components/db/ColumnBadge.tsx +55 -0
  33. package/src/ui/components/db/DevDbStudio.tsx +485 -0
  34. package/src/ui/components/db/constants.ts +11 -0
  35. package/src/ui/components/db/index.ts +4 -0
  36. package/src/ui/components/db/types.ts +7 -0
  37. package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
  38. package/src/ui/components/graph/GraphControls.tsx +162 -0
  39. package/src/ui/components/graph/NodeDetails.tsx +181 -0
  40. package/src/ui/components/graph/ProviderNode.tsx +97 -0
  41. package/src/ui/components/graph/constants.ts +35 -0
  42. package/src/ui/components/graph/helpers.ts +443 -0
  43. package/src/ui/components/graph/index.ts +7 -0
  44. package/src/ui/components/graph/types.ts +28 -0
  45. package/src/ui/styles.css +0 -6
  46. package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
  47. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
  48. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
  49. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
  50. /package/src/{entities → api/entities}/logs.ts +0 -0
  51. /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
  52. /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
  53. /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
  54. /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
  55. /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
  56. /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
  57. /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
  58. /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
  59. /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
  60. /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
  61. /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
  62. /package/src/{schemas → api/schemas}/DevTopicMetadata.ts +0 -0
@@ -0,0 +1,636 @@
1
+ import { useInject } from "@alepha/react";
2
+ import { JsonViewer, ui } from "@alepha/ui";
3
+ import {
4
+ ActionIcon,
5
+ Badge,
6
+ Box,
7
+ Button,
8
+ Flex,
9
+ NumberInput,
10
+ ScrollArea,
11
+ Stack,
12
+ Switch,
13
+ Tabs,
14
+ Text,
15
+ Textarea,
16
+ TextInput,
17
+ Tooltip,
18
+ } from "@mantine/core";
19
+ import {
20
+ IconAtom,
21
+ IconDeviceFloppy,
22
+ IconEdit,
23
+ IconRefresh,
24
+ IconSearch,
25
+ IconX,
26
+ } from "@tabler/icons-react";
27
+ import { HttpClient } from "alepha/server";
28
+ import { useCallback, useEffect, useMemo, useState } from "react";
29
+ import type { DevAtomMetadata } from "../../api/schemas/DevAtomMetadata.ts";
30
+ import { devMetadataSchema } from "../../api/schemas/DevMetadata.ts";
31
+
32
+ const AtomSidebar = ({
33
+ atoms,
34
+ selectedAtom,
35
+ onSelectAtom,
36
+ search,
37
+ onSearchChange,
38
+ }: {
39
+ atoms: DevAtomMetadata[];
40
+ selectedAtom: DevAtomMetadata | null;
41
+ onSelectAtom: (atom: DevAtomMetadata) => void;
42
+ search: string;
43
+ onSearchChange: (s: string) => void;
44
+ }) => {
45
+ return (
46
+ <Stack gap={0} h="100%">
47
+ <Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
48
+ <TextInput
49
+ placeholder="Search atoms..."
50
+ leftSection={<IconSearch size={14} />}
51
+ size="xs"
52
+ value={search}
53
+ onChange={(e) => onSearchChange(e.target.value)}
54
+ />
55
+ </Box>
56
+ <ScrollArea style={{ flex: 1 }}>
57
+ <Box>
58
+ <Text
59
+ size="xs"
60
+ fw={600}
61
+ c="dimmed"
62
+ px="sm"
63
+ py="xs"
64
+ style={{
65
+ backgroundColor: ui.colors.background,
66
+ borderBottom: `1px solid ${ui.colors.border}`,
67
+ textTransform: "uppercase",
68
+ letterSpacing: "0.05em",
69
+ }}
70
+ >
71
+ Atoms ({atoms.length})
72
+ </Text>
73
+ {atoms.map((atom) => {
74
+ const isSelected = selectedAtom?.name === atom.name;
75
+ const hasValue = atom.currentValue !== undefined;
76
+ return (
77
+ <Flex
78
+ key={atom.name}
79
+ align="center"
80
+ gap="xs"
81
+ px="sm"
82
+ py={6}
83
+ onClick={() => onSelectAtom(atom)}
84
+ style={{
85
+ cursor: "pointer",
86
+ backgroundColor: isSelected ? "#228be615" : undefined,
87
+ borderLeft: isSelected
88
+ ? "2px solid #228be6"
89
+ : "2px solid transparent",
90
+ borderBottom: `1px solid ${ui.colors.border}`,
91
+ }}
92
+ >
93
+ <IconAtom size={14} opacity={0.5} />
94
+ <Text size="sm" style={{ flex: 1 }} truncate>
95
+ {atom.name}
96
+ </Text>
97
+ {hasValue && (
98
+ <Badge size="xs" variant="light" color="green">
99
+ set
100
+ </Badge>
101
+ )}
102
+ </Flex>
103
+ );
104
+ })}
105
+ </Box>
106
+ </ScrollArea>
107
+ </Stack>
108
+ );
109
+ };
110
+
111
+ // Simple schema-based editor for atom values
112
+ const SchemaEditor = ({
113
+ schema,
114
+ value,
115
+ onChange,
116
+ path = "",
117
+ }: {
118
+ schema: any;
119
+ value: any;
120
+ onChange: (value: any) => void;
121
+ path?: string;
122
+ }) => {
123
+ if (!schema) return null;
124
+
125
+ // Handle union types (optional)
126
+ let actualSchema = schema;
127
+ if (schema.anyOf) {
128
+ const nonNull = schema.anyOf.find((t: any) => t.type !== "null");
129
+ if (nonNull) actualSchema = nonNull;
130
+ }
131
+
132
+ const type = actualSchema.type;
133
+ const format = actualSchema.format;
134
+
135
+ // Object type
136
+ if (type === "object" && actualSchema.properties) {
137
+ return (
138
+ <Stack gap="sm">
139
+ {Object.entries(actualSchema.properties).map(
140
+ ([key, propSchema]: [string, any]) => (
141
+ <Box key={key}>
142
+ <Text size="sm" fw={500} mb={4}>
143
+ {propSchema.title || key}
144
+ {propSchema.description && (
145
+ <Text span size="xs" c="dimmed" ml="xs">
146
+ {propSchema.description}
147
+ </Text>
148
+ )}
149
+ </Text>
150
+ <SchemaEditor
151
+ schema={propSchema}
152
+ value={value?.[key]}
153
+ onChange={(v) => onChange({ ...value, [key]: v })}
154
+ path={path ? `${path}.${key}` : key}
155
+ />
156
+ </Box>
157
+ ),
158
+ )}
159
+ </Stack>
160
+ );
161
+ }
162
+
163
+ // Array type
164
+ if (type === "array") {
165
+ const items = value ?? [];
166
+ return (
167
+ <Stack gap="xs">
168
+ {items.map((item: any, idx: number) => (
169
+ <Flex key={idx} gap="xs" align="center">
170
+ <Box style={{ flex: 1 }}>
171
+ <SchemaEditor
172
+ schema={actualSchema.items}
173
+ value={item}
174
+ onChange={(v) => {
175
+ const newItems = [...items];
176
+ newItems[idx] = v;
177
+ onChange(newItems);
178
+ }}
179
+ path={`${path}[${idx}]`}
180
+ />
181
+ </Box>
182
+ <ActionIcon
183
+ size="sm"
184
+ variant="light"
185
+ color="red"
186
+ onClick={() => {
187
+ const newItems = items.filter((_: any, i: number) => i !== idx);
188
+ onChange(newItems);
189
+ }}
190
+ >
191
+ <IconX size={14} />
192
+ </ActionIcon>
193
+ </Flex>
194
+ ))}
195
+ <Button
196
+ size="xs"
197
+ variant="light"
198
+ onClick={() => onChange([...items, undefined])}
199
+ >
200
+ Add item
201
+ </Button>
202
+ </Stack>
203
+ );
204
+ }
205
+
206
+ // Boolean
207
+ if (type === "boolean") {
208
+ return (
209
+ <Switch
210
+ checked={value ?? false}
211
+ onChange={(e) => onChange(e.currentTarget.checked)}
212
+ />
213
+ );
214
+ }
215
+
216
+ // Number/Integer
217
+ if (type === "number" || type === "integer") {
218
+ return (
219
+ <NumberInput
220
+ size="sm"
221
+ value={value ?? ""}
222
+ onChange={(v) => onChange(v === "" ? undefined : v)}
223
+ allowDecimal={type === "number"}
224
+ />
225
+ );
226
+ }
227
+
228
+ // Enum
229
+ if (actualSchema.enum) {
230
+ return (
231
+ <select
232
+ value={value ?? ""}
233
+ onChange={(e) => onChange(e.target.value || undefined)}
234
+ style={{
235
+ padding: "6px 10px",
236
+ borderRadius: 4,
237
+ border: `1px solid ${ui.colors.border}`,
238
+ backgroundColor: ui.colors.surface,
239
+ width: "100%",
240
+ }}
241
+ >
242
+ <option value="">Select...</option>
243
+ {actualSchema.enum.map((opt: string) => (
244
+ <option key={opt} value={opt}>
245
+ {opt}
246
+ </option>
247
+ ))}
248
+ </select>
249
+ );
250
+ }
251
+
252
+ // Date/DateTime
253
+ if (format === "date" || format === "date-time") {
254
+ return (
255
+ <TextInput
256
+ size="sm"
257
+ type={format === "date" ? "date" : "datetime-local"}
258
+ value={value ?? ""}
259
+ onChange={(e) => onChange(e.target.value || undefined)}
260
+ />
261
+ );
262
+ }
263
+
264
+ // Long text (if format suggests or multiline)
265
+ if (format === "textarea" || actualSchema.multiline) {
266
+ return (
267
+ <Textarea
268
+ size="sm"
269
+ value={value ?? ""}
270
+ onChange={(e) => onChange(e.target.value || undefined)}
271
+ autosize
272
+ minRows={2}
273
+ maxRows={6}
274
+ />
275
+ );
276
+ }
277
+
278
+ // Default: string
279
+ return (
280
+ <TextInput
281
+ size="sm"
282
+ value={value ?? ""}
283
+ onChange={(e) => onChange(e.target.value || undefined)}
284
+ />
285
+ );
286
+ };
287
+
288
+ const InfoTab = ({ atom }: { atom: DevAtomMetadata }) => {
289
+ return (
290
+ <ScrollArea h="100%" p="md">
291
+ <Stack gap="lg">
292
+ {/* Description */}
293
+ {atom.description && (
294
+ <Box>
295
+ <Text size="sm" fw={600} mb="xs">
296
+ Description
297
+ </Text>
298
+ <Text size="sm" c="dimmed">
299
+ {atom.description}
300
+ </Text>
301
+ </Box>
302
+ )}
303
+
304
+ {/* Current Value */}
305
+ <Box>
306
+ <Text size="sm" fw={600} mb="xs">
307
+ Current Value
308
+ </Text>
309
+ {atom.currentValue !== undefined ? (
310
+ <JsonViewer data={atom.currentValue} maxDepth={3} />
311
+ ) : (
312
+ <Text size="sm" c="dimmed" fs="italic">
313
+ (not set - using default)
314
+ </Text>
315
+ )}
316
+ </Box>
317
+
318
+ {/* Default Value */}
319
+ <Box>
320
+ <Text size="sm" fw={600} mb="xs">
321
+ Default Value
322
+ </Text>
323
+ {atom.defaultValue !== undefined ? (
324
+ <JsonViewer data={atom.defaultValue} maxDepth={3} />
325
+ ) : (
326
+ <Text size="sm" c="dimmed" fs="italic">
327
+ (no default)
328
+ </Text>
329
+ )}
330
+ </Box>
331
+
332
+ {/* Schema */}
333
+ <Box>
334
+ <Text size="sm" fw={600} mb="xs">
335
+ Schema
336
+ </Text>
337
+ <JsonViewer data={atom.schema} maxDepth={2} />
338
+ </Box>
339
+ </Stack>
340
+ </ScrollArea>
341
+ );
342
+ };
343
+
344
+ const EditTab = ({
345
+ atom,
346
+ onSave,
347
+ }: {
348
+ atom: DevAtomMetadata;
349
+ onSave: (value: any) => void;
350
+ }) => {
351
+ const [editValue, setEditValue] = useState<any>(
352
+ atom.currentValue ?? atom.defaultValue,
353
+ );
354
+ const [jsonMode, setJsonMode] = useState(false);
355
+ const [jsonText, setJsonText] = useState("");
356
+ const [jsonError, setJsonError] = useState<string | null>(null);
357
+
358
+ // Sync editValue when atom changes
359
+ useEffect(() => {
360
+ const val = atom.currentValue ?? atom.defaultValue;
361
+ setEditValue(val);
362
+ setJsonText(JSON.stringify(val, null, 2));
363
+ setJsonError(null);
364
+ }, [atom]);
365
+
366
+ const handleJsonChange = (text: string) => {
367
+ setJsonText(text);
368
+ try {
369
+ const parsed = JSON.parse(text);
370
+ setEditValue(parsed);
371
+ setJsonError(null);
372
+ } catch {
373
+ setJsonError("Invalid JSON");
374
+ }
375
+ };
376
+
377
+ const handleSave = () => {
378
+ onSave(editValue);
379
+ };
380
+
381
+ const handleReset = () => {
382
+ const val = atom.defaultValue;
383
+ setEditValue(val);
384
+ setJsonText(JSON.stringify(val, null, 2));
385
+ setJsonError(null);
386
+ };
387
+
388
+ return (
389
+ <Flex direction="column" h="100%">
390
+ <Box
391
+ p="sm"
392
+ style={{
393
+ borderBottom: `1px solid ${ui.colors.border}`,
394
+ }}
395
+ >
396
+ <Flex align="center" justify="space-between">
397
+ <Flex gap="xs" align="center">
398
+ <Switch
399
+ size="xs"
400
+ label="JSON mode"
401
+ checked={jsonMode}
402
+ onChange={(e) => setJsonMode(e.currentTarget.checked)}
403
+ />
404
+ </Flex>
405
+ <Flex gap="xs">
406
+ <Tooltip label="Reset to default">
407
+ <ActionIcon
408
+ size="sm"
409
+ variant="light"
410
+ color="gray"
411
+ onClick={handleReset}
412
+ >
413
+ <IconRefresh size={14} />
414
+ </ActionIcon>
415
+ </Tooltip>
416
+ <Button
417
+ size="xs"
418
+ leftSection={<IconDeviceFloppy size={14} />}
419
+ onClick={handleSave}
420
+ disabled={jsonMode && !!jsonError}
421
+ >
422
+ Save
423
+ </Button>
424
+ </Flex>
425
+ </Flex>
426
+ </Box>
427
+
428
+ <ScrollArea style={{ flex: 1 }} p="md">
429
+ {jsonMode ? (
430
+ <Stack gap="xs">
431
+ <Textarea
432
+ value={jsonText}
433
+ onChange={(e) => handleJsonChange(e.target.value)}
434
+ autosize
435
+ minRows={10}
436
+ maxRows={20}
437
+ error={jsonError}
438
+ styles={{
439
+ input: {
440
+ fontFamily: "monospace",
441
+ },
442
+ }}
443
+ />
444
+ {jsonError && (
445
+ <Text size="xs" c="red">
446
+ {jsonError}
447
+ </Text>
448
+ )}
449
+ </Stack>
450
+ ) : (
451
+ <SchemaEditor
452
+ schema={atom.schema}
453
+ value={editValue}
454
+ onChange={setEditValue}
455
+ />
456
+ )}
457
+ </ScrollArea>
458
+ </Flex>
459
+ );
460
+ };
461
+
462
+ const AtomPanel = ({
463
+ atom,
464
+ onSave,
465
+ }: {
466
+ atom: DevAtomMetadata;
467
+ onSave: (atom: DevAtomMetadata, value: any) => void;
468
+ }) => {
469
+ const hasValue = atom.currentValue !== undefined;
470
+
471
+ return (
472
+ <Flex direction="column" h="100%">
473
+ {/* Header */}
474
+ <Box
475
+ px="md"
476
+ py="sm"
477
+ style={{
478
+ borderBottom: `1px solid ${ui.colors.border}`,
479
+ backgroundColor: "#228be608",
480
+ }}
481
+ >
482
+ <Flex align="center" gap="sm">
483
+ <IconAtom size={18} opacity={0.7} />
484
+ <Text size="md" fw={600}>
485
+ {atom.name}
486
+ </Text>
487
+ <Badge size="xs" variant="light" color={hasValue ? "green" : "gray"}>
488
+ {hasValue ? "has value" : "default"}
489
+ </Badge>
490
+ </Flex>
491
+ </Box>
492
+
493
+ {/* Tabs */}
494
+ <Tabs
495
+ defaultValue="info"
496
+ style={{ flex: 1, display: "flex", flexDirection: "column" }}
497
+ >
498
+ <Tabs.List px="md">
499
+ <Tabs.Tab value="info">Info</Tabs.Tab>
500
+ <Tabs.Tab value="edit" leftSection={<IconEdit size={14} />}>
501
+ Edit
502
+ </Tabs.Tab>
503
+ </Tabs.List>
504
+
505
+ <Tabs.Panel value="info" style={{ flex: 1, overflow: "hidden" }}>
506
+ <InfoTab atom={atom} />
507
+ </Tabs.Panel>
508
+
509
+ <Tabs.Panel value="edit" style={{ flex: 1, overflow: "hidden" }}>
510
+ <EditTab atom={atom} onSave={(value) => onSave(atom, value)} />
511
+ </Tabs.Panel>
512
+ </Tabs>
513
+ </Flex>
514
+ );
515
+ };
516
+
517
+ const EmptyState = () => (
518
+ <Flex align="center" justify="center" h="100%" c="dimmed">
519
+ <Stack align="center" gap="xs">
520
+ <IconAtom size={48} opacity={0.3} />
521
+ <Text size="sm">Select an atom to view its details</Text>
522
+ </Stack>
523
+ </Flex>
524
+ );
525
+
526
+ const NoAtomsState = () => (
527
+ <Flex align="center" justify="center" h="100%" c="dimmed">
528
+ <Stack align="center" gap="xs">
529
+ <IconAtom size={48} opacity={0.3} />
530
+ <Text>No atoms found</Text>
531
+ <Text size="sm" c="dimmed">
532
+ Use $atom primitive to define state atoms
533
+ </Text>
534
+ </Stack>
535
+ </Flex>
536
+ );
537
+
538
+ export const DevAtomsViewer = () => {
539
+ const http = useInject(HttpClient);
540
+ const [atoms, setAtoms] = useState<DevAtomMetadata[]>([]);
541
+ const [loading, setLoading] = useState(true);
542
+ const [selectedAtom, setSelectedAtom] = useState<DevAtomMetadata | null>(
543
+ null,
544
+ );
545
+ const [search, setSearch] = useState("");
546
+
547
+ const fetchAtoms = useCallback(() => {
548
+ http
549
+ .fetch("/devtools/api/metadata", {
550
+ schema: { response: devMetadataSchema },
551
+ })
552
+ .then((res) => {
553
+ setAtoms(res.data.atoms);
554
+ setLoading(false);
555
+
556
+ // Update selected atom if it exists
557
+ if (selectedAtom) {
558
+ const updated = res.data.atoms.find(
559
+ (a) => a.name === selectedAtom.name,
560
+ );
561
+ if (updated) setSelectedAtom(updated);
562
+ }
563
+ });
564
+ }, [selectedAtom?.name]);
565
+
566
+ useEffect(() => {
567
+ fetchAtoms();
568
+ }, []);
569
+
570
+ const filteredAtoms = useMemo(() => {
571
+ if (!search) return atoms;
572
+ const searchLower = search.toLowerCase();
573
+ return atoms.filter((a) => a.name.toLowerCase().includes(searchLower));
574
+ }, [atoms, search]);
575
+
576
+ const handleSave = useCallback(
577
+ async (atom: DevAtomMetadata, value: any) => {
578
+ // POST to update atom value
579
+ try {
580
+ await http.fetch("/devtools/api/atoms", {
581
+ method: "POST",
582
+ body: JSON.stringify({ name: atom.name, value }),
583
+ });
584
+ // Refresh data
585
+ fetchAtoms();
586
+ } catch (error) {
587
+ console.error("Failed to save atom:", error);
588
+ }
589
+ },
590
+ [fetchAtoms],
591
+ );
592
+
593
+ if (loading) {
594
+ return (
595
+ <Flex align="center" justify="center" h="100%">
596
+ <Text c="dimmed">Loading...</Text>
597
+ </Flex>
598
+ );
599
+ }
600
+
601
+ if (atoms.length === 0) {
602
+ return <NoAtomsState />;
603
+ }
604
+
605
+ return (
606
+ <Flex h="100%" style={{ overflow: "hidden" }}>
607
+ {/* Sidebar */}
608
+ <Box
609
+ w={260}
610
+ style={{
611
+ borderRight: `1px solid ${ui.colors.border}`,
612
+ backgroundColor: ui.colors.surface,
613
+ }}
614
+ >
615
+ <AtomSidebar
616
+ atoms={filteredAtoms}
617
+ selectedAtom={selectedAtom}
618
+ onSelectAtom={setSelectedAtom}
619
+ search={search}
620
+ onSearchChange={setSearch}
621
+ />
622
+ </Box>
623
+
624
+ {/* Main content */}
625
+ <Box style={{ flex: 1, overflow: "hidden" }}>
626
+ {selectedAtom ? (
627
+ <AtomPanel atom={selectedAtom} onSave={handleSave} />
628
+ ) : (
629
+ <EmptyState />
630
+ )}
631
+ </Box>
632
+ </Flex>
633
+ );
634
+ };
635
+
636
+ export default DevAtomsViewer;