@rozenite/controls-plugin 1.13.0 → 2.1.0
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/CHANGELOG.md +25 -0
- package/dist/devtools/assets/panel-BvIa5kqW.css +1 -0
- package/dist/devtools/assets/panel-H7O6Qof-.js +18 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/useRozeniteControlsPlugin.require.js +12 -36
- package/dist/react-native/index.d.ts +2 -2
- package/dist/rozenite.json +1 -1
- package/package.json +28 -31
- package/sdk.ts +2 -8
- package/src/__tests__/serialization.test.ts +7 -7
- package/src/react-native/__tests__/controlsRegistry.test.ts +3 -11
- package/src/react-native/__tests__/useRozeniteControlsPlugin.test.tsx +4 -14
- package/src/react-native/controlsRegistry.ts +2 -6
- package/src/react-native/useControlsAgentTools.ts +8 -28
- package/src/react-native/useRozeniteControlsPlugin.ts +8 -24
- package/src/shared/agent-tools.ts +6 -22
- package/src/shared/serialization.ts +3 -6
- package/src/shared/types.ts +6 -18
- package/src/ui/panel.tsx +155 -189
- package/dist/devtools/assets/panel-B5zz0XK3.css +0 -1
- package/dist/devtools/assets/panel-BoQwJa2M.js +0 -17
- package/postcss.config.js +0 -6
- package/src/ui/globals.css +0 -86
- package/tailwind.config.ts +0 -53
package/src/ui/panel.tsx
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
EmptyState,
|
|
5
|
+
Input,
|
|
6
|
+
PluginShell,
|
|
7
|
+
Select,
|
|
8
|
+
Sidebar,
|
|
9
|
+
Split,
|
|
10
|
+
Switch,
|
|
11
|
+
} from '@rozenite/ui';
|
|
2
12
|
import type { ReactNode } from 'react';
|
|
3
13
|
import { useEffect, useRef, useState } from 'react';
|
|
4
14
|
import type {
|
|
@@ -7,7 +17,7 @@ import type {
|
|
|
7
17
|
ControlsUpdateResultEvent,
|
|
8
18
|
} from '../shared/messaging';
|
|
9
19
|
import type { ControlsItemSnapshot, ControlsSectionSnapshot } from '../shared/types';
|
|
10
|
-
import '
|
|
20
|
+
import '@rozenite/ui/styles.css';
|
|
11
21
|
|
|
12
22
|
type ItemUiState = {
|
|
13
23
|
pending: boolean;
|
|
@@ -32,13 +42,9 @@ const RowShell = ({
|
|
|
32
42
|
}) => (
|
|
33
43
|
<div className="flex items-start justify-between gap-4 py-3">
|
|
34
44
|
<div className="min-w-0">
|
|
35
|
-
<div className="text-sm font-medium text-
|
|
36
|
-
{description ?
|
|
37
|
-
|
|
38
|
-
) : null}
|
|
39
|
-
{errorMessage ? (
|
|
40
|
-
<div className="mt-1 text-xs text-red-400">{errorMessage}</div>
|
|
41
|
-
) : null}
|
|
45
|
+
<div className="text-sm font-medium text-foreground">{title}</div>
|
|
46
|
+
{description ? <div className="mt-1 text-xs text-muted-foreground">{description}</div> : null}
|
|
47
|
+
{errorMessage ? <div className="mt-1 text-xs text-destructive">{errorMessage}</div> : null}
|
|
42
48
|
</div>
|
|
43
49
|
{children}
|
|
44
50
|
</div>
|
|
@@ -56,21 +62,13 @@ const ToggleRow = ({
|
|
|
56
62
|
onToggle: (sectionId: string, itemId: string, value: boolean) => void;
|
|
57
63
|
}) => {
|
|
58
64
|
return (
|
|
59
|
-
<RowShell
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
type="checkbox"
|
|
67
|
-
className="peer sr-only"
|
|
68
|
-
checked={item.value}
|
|
69
|
-
disabled={item.disabled || uiState?.pending}
|
|
70
|
-
onChange={(event) => onToggle(sectionId, item.id, event.target.checked)}
|
|
71
|
-
/>
|
|
72
|
-
<div className="h-6 w-11 rounded-full bg-gray-700 transition peer-checked:bg-violet-500 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white after:transition-all after:content-[''] peer-checked:after:translate-x-5" />
|
|
73
|
-
</label>
|
|
65
|
+
<RowShell title={item.title} description={item.description} errorMessage={uiState?.message}>
|
|
66
|
+
<Switch
|
|
67
|
+
aria-label={item.title}
|
|
68
|
+
checked={item.value}
|
|
69
|
+
disabled={item.disabled || uiState?.pending}
|
|
70
|
+
onCheckedChange={(checked) => onToggle(sectionId, item.id, checked)}
|
|
71
|
+
/>
|
|
74
72
|
</RowShell>
|
|
75
73
|
);
|
|
76
74
|
};
|
|
@@ -87,18 +85,15 @@ const ButtonRow = ({
|
|
|
87
85
|
onPress: (sectionId: string, itemId: string) => void;
|
|
88
86
|
}) => {
|
|
89
87
|
return (
|
|
90
|
-
<RowShell
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
>
|
|
95
|
-
<button
|
|
96
|
-
className="rounded-md border border-violet-500/60 bg-violet-500/10 px-3 py-1.5 text-xs font-semibold text-violet-100 transition hover:bg-violet-500/20 disabled:cursor-not-allowed disabled:border-gray-700 disabled:bg-gray-800 disabled:text-gray-500"
|
|
88
|
+
<RowShell title={item.title} description={item.description} errorMessage={uiState?.message}>
|
|
89
|
+
<Button
|
|
90
|
+
size="compact"
|
|
91
|
+
variant="outline"
|
|
97
92
|
disabled={item.disabled || uiState?.pending}
|
|
98
93
|
onClick={() => onPress(sectionId, item.id)}
|
|
99
94
|
>
|
|
100
|
-
{uiState?.pending ? 'Running...' : item.actionLabel ?? 'Run'}
|
|
101
|
-
</
|
|
95
|
+
{uiState?.pending ? 'Running...' : (item.actionLabel ?? 'Run')}
|
|
96
|
+
</Button>
|
|
102
97
|
</RowShell>
|
|
103
98
|
);
|
|
104
99
|
};
|
|
@@ -115,35 +110,35 @@ const SelectRow = ({
|
|
|
115
110
|
onSelect: (sectionId: string, itemId: string, value: string) => void;
|
|
116
111
|
}) => {
|
|
117
112
|
return (
|
|
118
|
-
<RowShell
|
|
119
|
-
|
|
120
|
-
description={item.description}
|
|
121
|
-
errorMessage={uiState?.message}
|
|
122
|
-
>
|
|
123
|
-
<select
|
|
124
|
-
className="min-w-[160px] rounded-md border border-gray-700 bg-gray-950 px-3 py-2 text-xs text-gray-200 outline-none transition focus:border-violet-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
113
|
+
<RowShell title={item.title} description={item.description} errorMessage={uiState?.message}>
|
|
114
|
+
<Select
|
|
125
115
|
value={item.value}
|
|
126
116
|
disabled={item.disabled || uiState?.pending}
|
|
127
|
-
|
|
117
|
+
onValueChange={(value) => {
|
|
118
|
+
if (value !== null) {
|
|
119
|
+
onSelect(sectionId, item.id, value);
|
|
120
|
+
}
|
|
121
|
+
}}
|
|
128
122
|
>
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
<Select.Trigger className="w-40">
|
|
124
|
+
<Select.Value />
|
|
125
|
+
</Select.Trigger>
|
|
126
|
+
<Select.Content>
|
|
127
|
+
{item.options.map((option) => (
|
|
128
|
+
<Select.Item key={option.value} value={option.value}>
|
|
129
|
+
{option.label}
|
|
130
|
+
</Select.Item>
|
|
131
|
+
))}
|
|
132
|
+
</Select.Content>
|
|
133
|
+
</Select>
|
|
135
134
|
</RowShell>
|
|
136
135
|
);
|
|
137
136
|
};
|
|
138
137
|
|
|
139
|
-
const TextRow = ({
|
|
140
|
-
item,
|
|
141
|
-
}: {
|
|
142
|
-
item: Extract<ControlsItemSnapshot, { type: 'text' }>;
|
|
143
|
-
}) => {
|
|
138
|
+
const TextRow = ({ item }: { item: Extract<ControlsItemSnapshot, { type: 'text' }> }) => {
|
|
144
139
|
return (
|
|
145
140
|
<RowShell title={item.title} description={item.description}>
|
|
146
|
-
<div className="max-w-[50%] rounded-md bg-
|
|
141
|
+
<div className="max-w-[50%] rounded-md bg-muted px-3 py-1.5 text-right text-xs text-muted-foreground">
|
|
147
142
|
{item.value}
|
|
148
143
|
</div>
|
|
149
144
|
</RowShell>
|
|
@@ -168,29 +163,24 @@ const InputRow = ({
|
|
|
168
163
|
const isChanged = draftValue !== item.value;
|
|
169
164
|
|
|
170
165
|
return (
|
|
171
|
-
<RowShell
|
|
172
|
-
title={item.title}
|
|
173
|
-
description={item.description}
|
|
174
|
-
errorMessage={uiState?.message}
|
|
175
|
-
>
|
|
166
|
+
<RowShell title={item.title} description={item.description} errorMessage={uiState?.message}>
|
|
176
167
|
<div className="flex min-w-[240px] items-center gap-2">
|
|
177
|
-
<
|
|
178
|
-
className="flex-1
|
|
168
|
+
<Input
|
|
169
|
+
className="flex-1"
|
|
179
170
|
type="text"
|
|
180
171
|
value={draftValue}
|
|
181
172
|
placeholder={item.placeholder}
|
|
182
173
|
disabled={item.disabled || uiState?.pending}
|
|
183
|
-
onChange={(event) =>
|
|
184
|
-
onDraftChange(sectionId, item.id, event.target.value)
|
|
185
|
-
}
|
|
174
|
+
onChange={(event) => onDraftChange(sectionId, item.id, event.target.value)}
|
|
186
175
|
/>
|
|
187
|
-
<
|
|
188
|
-
|
|
176
|
+
<Button
|
|
177
|
+
size="compact"
|
|
178
|
+
variant="outline"
|
|
189
179
|
disabled={!isChanged || item.disabled || uiState?.pending}
|
|
190
180
|
onClick={() => onApply(sectionId, item.id)}
|
|
191
181
|
>
|
|
192
|
-
{uiState?.pending ? 'Applying...' : item.applyLabel ?? 'Apply'}
|
|
193
|
-
</
|
|
182
|
+
{uiState?.pending ? 'Applying...' : (item.applyLabel ?? 'Apply')}
|
|
183
|
+
</Button>
|
|
194
184
|
</div>
|
|
195
185
|
</RowShell>
|
|
196
186
|
);
|
|
@@ -222,25 +212,11 @@ const renderItem = ({
|
|
|
222
212
|
}
|
|
223
213
|
|
|
224
214
|
if (item.type === 'toggle') {
|
|
225
|
-
return
|
|
226
|
-
<ToggleRow
|
|
227
|
-
sectionId={sectionId}
|
|
228
|
-
item={item}
|
|
229
|
-
uiState={uiState}
|
|
230
|
-
onToggle={onToggle}
|
|
231
|
-
/>
|
|
232
|
-
);
|
|
215
|
+
return <ToggleRow sectionId={sectionId} item={item} uiState={uiState} onToggle={onToggle} />;
|
|
233
216
|
}
|
|
234
217
|
|
|
235
218
|
if (item.type === 'select') {
|
|
236
|
-
return
|
|
237
|
-
<SelectRow
|
|
238
|
-
sectionId={sectionId}
|
|
239
|
-
item={item}
|
|
240
|
-
uiState={uiState}
|
|
241
|
-
onSelect={onSelect}
|
|
242
|
-
/>
|
|
243
|
-
);
|
|
219
|
+
return <SelectRow sectionId={sectionId} item={item} uiState={uiState} onSelect={onSelect} />;
|
|
244
220
|
}
|
|
245
221
|
|
|
246
222
|
if (item.type === 'input') {
|
|
@@ -256,18 +232,12 @@ const renderItem = ({
|
|
|
256
232
|
);
|
|
257
233
|
}
|
|
258
234
|
|
|
259
|
-
return
|
|
260
|
-
<ButtonRow
|
|
261
|
-
sectionId={sectionId}
|
|
262
|
-
item={item}
|
|
263
|
-
uiState={uiState}
|
|
264
|
-
onPress={onPress}
|
|
265
|
-
/>
|
|
266
|
-
);
|
|
235
|
+
return <ButtonRow sectionId={sectionId} item={item} uiState={uiState} onPress={onPress} />;
|
|
267
236
|
};
|
|
268
237
|
|
|
269
238
|
export default function ControlsPanel() {
|
|
270
239
|
const [sections, setSections] = useState<ControlsSectionSnapshot[]>([]);
|
|
240
|
+
const [selectedSectionId, setSelectedSectionId] = useState<string | null>(null);
|
|
271
241
|
const [loading, setLoading] = useState(true);
|
|
272
242
|
const [itemUiState, setItemUiState] = useState<Map<string, ItemUiState>>(new Map());
|
|
273
243
|
const [inputDrafts, setInputDrafts] = useState<Map<string, string>>(new Map());
|
|
@@ -282,47 +252,48 @@ export default function ControlsPanel() {
|
|
|
282
252
|
return;
|
|
283
253
|
}
|
|
284
254
|
|
|
285
|
-
const snapshotSubscription = client.onMessage(
|
|
286
|
-
|
|
287
|
-
(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
255
|
+
const snapshotSubscription = client.onMessage('snapshot', (event: ControlsSnapshotEvent) => {
|
|
256
|
+
setSections(event.sections);
|
|
257
|
+
setSelectedSectionId((previous) =>
|
|
258
|
+
event.sections.some((section) => section.id === previous)
|
|
259
|
+
? previous
|
|
260
|
+
: (event.sections[0]?.id ?? null),
|
|
261
|
+
);
|
|
262
|
+
setLoading(false);
|
|
263
|
+
|
|
264
|
+
const nextCommittedValues = new Map<string, string>();
|
|
265
|
+
event.sections.forEach((section) => {
|
|
266
|
+
section.items.forEach((item) => {
|
|
267
|
+
if (item.type === 'input') {
|
|
268
|
+
nextCommittedValues.set(getItemKey(section.id, item.id), item.value);
|
|
269
|
+
}
|
|
298
270
|
});
|
|
271
|
+
});
|
|
299
272
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
next.forEach((_value, key) => {
|
|
304
|
-
if (!nextCommittedValues.has(key)) {
|
|
305
|
-
next.delete(key);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
273
|
+
setInputDrafts((previous) => {
|
|
274
|
+
const next = new Map(previous);
|
|
308
275
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
276
|
+
next.forEach((_value, key) => {
|
|
277
|
+
if (!nextCommittedValues.has(key)) {
|
|
278
|
+
next.delete(key);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
314
281
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
282
|
+
nextCommittedValues.forEach((committedValue, key) => {
|
|
283
|
+
const previousCommitted = committedInputValuesRef.current.get(key);
|
|
284
|
+
const previousDraft = previous.get(key);
|
|
285
|
+
const isDirty = previousDraft !== undefined && previousDraft !== previousCommitted;
|
|
319
286
|
|
|
320
|
-
|
|
287
|
+
if (!isDirty || previousDraft === committedValue) {
|
|
288
|
+
next.set(key, committedValue);
|
|
289
|
+
}
|
|
321
290
|
});
|
|
322
291
|
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
292
|
+
return next;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
committedInputValuesRef.current = nextCommittedValues;
|
|
296
|
+
});
|
|
326
297
|
const updateResultSubscription = client.onMessage(
|
|
327
298
|
'update-result',
|
|
328
299
|
(event: ControlsUpdateResultEvent) => {
|
|
@@ -336,7 +307,7 @@ export default function ControlsPanel() {
|
|
|
336
307
|
});
|
|
337
308
|
return next;
|
|
338
309
|
});
|
|
339
|
-
}
|
|
310
|
+
},
|
|
340
311
|
);
|
|
341
312
|
|
|
342
313
|
client.send('get-snapshot', {
|
|
@@ -349,11 +320,7 @@ export default function ControlsPanel() {
|
|
|
349
320
|
};
|
|
350
321
|
}, [client]);
|
|
351
322
|
|
|
352
|
-
const sendUpdateRequest = (
|
|
353
|
-
sectionId: string,
|
|
354
|
-
itemId: string,
|
|
355
|
-
value: boolean | string
|
|
356
|
-
) => {
|
|
323
|
+
const sendUpdateRequest = (sectionId: string, itemId: string, value: boolean | string) => {
|
|
357
324
|
if (!client) {
|
|
358
325
|
return;
|
|
359
326
|
}
|
|
@@ -400,11 +367,7 @@ export default function ControlsPanel() {
|
|
|
400
367
|
sendUpdateRequest(sectionId, itemId, value);
|
|
401
368
|
};
|
|
402
369
|
|
|
403
|
-
const handleInputDraftChange = (
|
|
404
|
-
sectionId: string,
|
|
405
|
-
itemId: string,
|
|
406
|
-
value: string
|
|
407
|
-
) => {
|
|
370
|
+
const handleInputDraftChange = (sectionId: string, itemId: string, value: string) => {
|
|
408
371
|
const key = getItemKey(sectionId, itemId);
|
|
409
372
|
|
|
410
373
|
setInputDrafts((previous) => {
|
|
@@ -437,65 +400,68 @@ export default function ControlsPanel() {
|
|
|
437
400
|
sendUpdateRequest(sectionId, itemId, draftValue);
|
|
438
401
|
};
|
|
439
402
|
|
|
440
|
-
|
|
441
|
-
<div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
|
|
442
|
-
<div className="flex items-center gap-2 border-b border-gray-700 bg-gray-800 p-2">
|
|
443
|
-
<span className="text-sm font-medium text-gray-200">Controls</span>
|
|
444
|
-
<div className="flex-1" />
|
|
445
|
-
<span className="text-xs text-gray-400">{sections.length} sections</span>
|
|
446
|
-
</div>
|
|
403
|
+
const selectedSection = sections.find((section) => section.id === selectedSectionId);
|
|
447
404
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
</div>
|
|
453
|
-
) : null}
|
|
405
|
+
return (
|
|
406
|
+
<PluginShell className="dark">
|
|
407
|
+
<PluginShell.Body>
|
|
408
|
+
{loading ? <EmptyState title="Loading controls…" /> : null}
|
|
454
409
|
|
|
455
410
|
{!loading && sections.length === 0 ? (
|
|
456
|
-
<
|
|
457
|
-
No controls registered
|
|
458
|
-
|
|
411
|
+
<EmptyState
|
|
412
|
+
title="No controls registered"
|
|
413
|
+
description="Register controls in the app to manage them here."
|
|
414
|
+
/>
|
|
459
415
|
) : null}
|
|
460
416
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
<
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
417
|
+
{!loading && selectedSection ? (
|
|
418
|
+
<Split direction="horizontal" autoSaveId="controls-sections">
|
|
419
|
+
<Split.Pane defaultSize={22} minSize={15} maxSize={35}>
|
|
420
|
+
<Sidebar className="w-full border-r-0">
|
|
421
|
+
<Sidebar.Group>
|
|
422
|
+
{sections.map((section) => (
|
|
423
|
+
<Sidebar.Item
|
|
424
|
+
key={section.id}
|
|
425
|
+
selected={section.id === selectedSection.id}
|
|
426
|
+
onClick={() => setSelectedSectionId(section.id)}
|
|
427
|
+
>
|
|
428
|
+
{section.title}
|
|
429
|
+
</Sidebar.Item>
|
|
430
|
+
))}
|
|
431
|
+
</Sidebar.Group>
|
|
432
|
+
</Sidebar>
|
|
433
|
+
</Split.Pane>
|
|
434
|
+
<Split.Handle />
|
|
435
|
+
<Split.Pane>
|
|
436
|
+
<section className="h-full overflow-auto p-6">
|
|
437
|
+
<h1 className="text-base font-semibold text-foreground">{selectedSection.title}</h1>
|
|
438
|
+
{selectedSection.description ? (
|
|
439
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
440
|
+
{selectedSection.description}
|
|
441
|
+
</p>
|
|
475
442
|
) : null}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
</div>
|
|
443
|
+
<div className="mt-5 divide-y divide-border">
|
|
444
|
+
{selectedSection.items.map((item) => (
|
|
445
|
+
<div key={item.id}>
|
|
446
|
+
{renderItem({
|
|
447
|
+
sectionId: selectedSection.id,
|
|
448
|
+
item,
|
|
449
|
+
uiState: itemUiState.get(getItemKey(selectedSection.id, item.id)),
|
|
450
|
+
inputDraft: inputDrafts.get(getItemKey(selectedSection.id, item.id)),
|
|
451
|
+
onToggle: handleToggle,
|
|
452
|
+
onPress: handlePress,
|
|
453
|
+
onSelect: handleSelect,
|
|
454
|
+
onInputDraftChange: handleInputDraftChange,
|
|
455
|
+
onInputApply: handleInputApply,
|
|
456
|
+
})}
|
|
457
|
+
</div>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
</section>
|
|
461
|
+
</Split.Pane>
|
|
462
|
+
</Split>
|
|
463
|
+
) : null}
|
|
464
|
+
</PluginShell.Body>
|
|
465
|
+
</PluginShell>
|
|
500
466
|
);
|
|
501
467
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 0 0% 9%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%;--radius: .5rem}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.relative{position:relative}.mt-1{margin-top:.25rem}.flex{display:flex}.inline-flex{display:inline-flex}.h-6{height:1.5rem}.h-screen{height:100vh}.w-11{width:2.75rem}.min-w-0{min-width:0px}.min-w-\[160px\]{min-width:160px}.min-w-\[240px\]{min-width:240px}.max-w-\[50\%\]{max-width:50%}.flex-1{flex:1 1 0%}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-4{gap:1rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-800>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(31 41 55 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-dashed{border-style:dashed}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-violet-500\/60{border-color:#8b5cf699}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/80{background-color:#1f2937cc}.bg-gray-800\/90{background-color:#1f2937e6}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-gray-950\/80{background-color:#030712cc}.bg-violet-500\/10{background-color:#8b5cf61a}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.text-right{text-align:right}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-violet-100{--tw-text-opacity: 1;color:rgb(237 233 254 / var(--tw-text-opacity, 1))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/10{--tw-shadow-color: rgb(0 0 0 / .1);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#1f2937;border-radius:4px}::-webkit-scrollbar-thumb{background:#374151;border-radius:4px;border:1px solid #1f2937}::-webkit-scrollbar-thumb:hover{background:#4b5563}::-webkit-scrollbar-thumb:active{background:#6b7280}::-webkit-scrollbar-corner{background:#1f2937}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:left-\[2px\]:after{content:var(--tw-content);left:2px}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:h-5:after{content:var(--tw-content);height:1.25rem}.after\:w-5:after{content:var(--tw-content);width:1.25rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content: "";content:var(--tw-content)}.hover\:bg-violet-500\/20:hover{background-color:#8b5cf633}.focus\:border-violet-500:focus{--tw-border-opacity: 1;border-color:rgb(139 92 246 / var(--tw-border-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:border-gray-700:disabled{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.disabled\:bg-gray-800:disabled{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.disabled\:text-gray-500:disabled{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.peer:checked~.peer-checked\:bg-violet-500{--tw-bg-opacity: 1;background-color:rgb(139 92 246 / var(--tw-bg-opacity, 1))}.peer:checked~.peer-checked\:after\:translate-x-5:after{content:var(--tw-content);--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-50{opacity:.5}
|