@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/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 './globals.css';
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-gray-100">{title}</div>
36
- {description ? (
37
- <div className="mt-1 text-xs text-gray-400">{description}</div>
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
- title={item.title}
61
- description={item.description}
62
- errorMessage={uiState?.message}
63
- >
64
- <label className="relative inline-flex cursor-pointer items-center">
65
- <input
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
- title={item.title}
92
- description={item.description}
93
- errorMessage={uiState?.message}
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
- </button>
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
- title={item.title}
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
- onChange={(event) => onSelect(sectionId, item.id, event.target.value)}
117
+ onValueChange={(value) => {
118
+ if (value !== null) {
119
+ onSelect(sectionId, item.id, value);
120
+ }
121
+ }}
128
122
  >
129
- {item.options.map((option) => (
130
- <option key={option.value} value={option.value}>
131
- {option.label}
132
- </option>
133
- ))}
134
- </select>
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-gray-950/80 px-3 py-1.5 text-right text-xs text-gray-300">
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
- <input
178
- className="flex-1 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"
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
- <button
188
- className="rounded-md border border-violet-500/60 bg-violet-500/10 px-3 py-2 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"
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
- </button>
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
- 'snapshot',
287
- (event: ControlsSnapshotEvent) => {
288
- setSections(event.sections);
289
- setLoading(false);
290
-
291
- const nextCommittedValues = new Map<string, string>();
292
- event.sections.forEach((section) => {
293
- section.items.forEach((item) => {
294
- if (item.type === 'input') {
295
- nextCommittedValues.set(getItemKey(section.id, item.id), item.value);
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
- setInputDrafts((previous) => {
301
- const next = new Map(previous);
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
- nextCommittedValues.forEach((committedValue, key) => {
310
- const previousCommitted = committedInputValuesRef.current.get(key);
311
- const previousDraft = previous.get(key);
312
- const isDirty =
313
- previousDraft !== undefined && previousDraft !== previousCommitted;
276
+ next.forEach((_value, key) => {
277
+ if (!nextCommittedValues.has(key)) {
278
+ next.delete(key);
279
+ }
280
+ });
314
281
 
315
- if (!isDirty || previousDraft === committedValue) {
316
- next.set(key, committedValue);
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
- return next;
287
+ if (!isDirty || previousDraft === committedValue) {
288
+ next.set(key, committedValue);
289
+ }
321
290
  });
322
291
 
323
- committedInputValuesRef.current = nextCommittedValues;
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
- return (
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
- <div className="flex-1 overflow-auto p-4">
449
- {loading ? (
450
- <div className="rounded-lg border border-gray-800 bg-gray-800 p-6 text-sm text-gray-400">
451
- Loading controls snapshot...
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
- <div className="rounded-lg border border-dashed border-gray-700 bg-gray-800/80 p-6 text-sm text-gray-400">
457
- No controls registered on the device.
458
- </div>
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
- <div className="space-y-4">
462
- {sections.map((section) => (
463
- <section
464
- key={section.id}
465
- className="rounded-xl border border-gray-800 bg-gray-800/90 shadow-lg shadow-black/10"
466
- >
467
- <div className="border-b border-gray-800 px-4 py-3">
468
- <div className="text-sm font-semibold text-gray-100">
469
- {section.title}
470
- </div>
471
- {section.description ? (
472
- <div className="mt-1 text-xs text-gray-400">
473
- {section.description}
474
- </div>
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
- </div>
477
-
478
- <div className="divide-y divide-gray-800 px-4">
479
- {section.items.map((item) => (
480
- <div key={item.id}>
481
- {renderItem({
482
- sectionId: section.id,
483
- item,
484
- uiState: itemUiState.get(getItemKey(section.id, item.id)),
485
- inputDraft: inputDrafts.get(getItemKey(section.id, item.id)),
486
- onToggle: handleToggle,
487
- onPress: handlePress,
488
- onSelect: handleSelect,
489
- onInputDraftChange: handleInputDraftChange,
490
- onInputApply: handleInputApply,
491
- })}
492
- </div>
493
- ))}
494
- </div>
495
- </section>
496
- ))}
497
- </div>
498
- </div>
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}