@rozenite/storage-plugin 1.9.0 → 1.11.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.
@@ -0,0 +1,62 @@
1
+ import type { StorageEntryType } from '../shared/types';
2
+
3
+ const TYPE_LABELS: Record<StorageEntryType, string> = {
4
+ string: 'Text',
5
+ number: 'Number',
6
+ boolean: 'Boolean',
7
+ buffer: 'Hex',
8
+ };
9
+
10
+ // Visual ordering of the pills. Keep `string` first because it's the
11
+ // most common landing type after a fresh read on an MMKV key.
12
+ const TYPE_ORDER: StorageEntryType[] = [
13
+ 'string',
14
+ 'number',
15
+ 'boolean',
16
+ 'buffer',
17
+ ];
18
+
19
+ export type EditorSwitcherProps = {
20
+ supportedTypes: StorageEntryType[];
21
+ value: StorageEntryType;
22
+ onChange: (type: StorageEntryType) => void;
23
+ };
24
+
25
+ export const EditorSwitcher = ({
26
+ supportedTypes,
27
+ value,
28
+ onChange,
29
+ }: EditorSwitcherProps) => {
30
+ const available = TYPE_ORDER.filter((type) => supportedTypes.includes(type));
31
+
32
+ // Adaptive hide: the switcher is meaningless when the backend only
33
+ // supports one type.
34
+ if (available.length <= 1) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <div
40
+ role="tablist"
41
+ aria-label="Edit as"
42
+ className="inline-flex rounded border border-gray-600 overflow-hidden"
43
+ >
44
+ {available.map((type) => (
45
+ <button
46
+ key={type}
47
+ type="button"
48
+ role="tab"
49
+ aria-selected={value === type}
50
+ onClick={() => onChange(type)}
51
+ className={`px-3 py-1 text-xs transition-colors ${
52
+ value === type
53
+ ? 'bg-blue-600 text-white'
54
+ : 'text-gray-300 hover:bg-gray-700'
55
+ }`}
56
+ >
57
+ {TYPE_LABELS[type]}
58
+ </button>
59
+ ))}
60
+ </div>
61
+ );
62
+ };
@@ -2,6 +2,7 @@ import { X, Info, Edit3 } from 'lucide-react';
2
2
  import { JSONTree } from 'react-json-tree';
3
3
  import { StorageEntry } from '../shared/types';
4
4
  import { useMemo } from 'react';
5
+ import { bytesToHexdump } from './binary';
5
6
 
6
7
  export type EntryDetailDialogProps = {
7
8
  isOpen: boolean;
@@ -29,7 +30,9 @@ const jsonTreeTheme = {
29
30
  base0F: '#f97316', // text-orange-500
30
31
  };
31
32
 
32
- const jsonSafeParse = (value: string): Record<string, unknown> | unknown[] | null => {
33
+ const jsonSafeParse = (
34
+ value: string,
35
+ ): Record<string, unknown> | unknown[] | null => {
33
36
  try {
34
37
  const parsed = JSON.parse(value) as unknown;
35
38
 
@@ -87,9 +90,14 @@ const formatValue = (entry: StorageEntry) => {
87
90
  case 'buffer': {
88
91
  const bufferArray = entry.value as number[];
89
92
  return (
90
- <span className="text-purple-300 font-mono">
91
- [{bufferArray.join(', ')}]
92
- </span>
93
+ <div className="space-y-2">
94
+ <div className="text-xs text-gray-400">
95
+ {bufferArray.length} {bufferArray.length === 1 ? 'byte' : 'bytes'}
96
+ </div>
97
+ <pre className="text-purple-200 font-mono text-xs whitespace-pre overflow-auto leading-snug">
98
+ {bytesToHexdump(bufferArray)}
99
+ </pre>
100
+ </div>
93
101
  );
94
102
  }
95
103
  default:
@@ -107,7 +115,7 @@ export const EntryDetailDialog = ({
107
115
  const stringValue = isStringValue ? (entry.value as string) : '';
108
116
  const jsonValue = useMemo(
109
117
  () => (isStringValue ? jsonSafeParse(stringValue) : null),
110
- [isStringValue, stringValue]
118
+ [isStringValue, stringValue],
111
119
  );
112
120
 
113
121
  const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -163,7 +171,7 @@ export const EntryDetailDialog = ({
163
171
  <div className="flex items-center">
164
172
  <span
165
173
  className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
166
- entry.type
174
+ entry.type,
167
175
  )}`}
168
176
  >
169
177
  {entry.type}
@@ -0,0 +1,105 @@
1
+ import type { StorageEntryType, StorageEntryValue } from '../shared/types';
2
+
3
+ const textEncoder = new TextEncoder();
4
+ const strictDecoder = new TextDecoder('utf-8', { fatal: true });
5
+
6
+ // Decode bytes to a string. Returns the empty string when the bytes
7
+ // are not valid UTF-8 — used when the user switches a Hex editor's
8
+ // non-UTF-8 buffer back to a text editor, where there's no faithful
9
+ // representation to preserve.
10
+ const tryDecode = (bytes: readonly number[]): string => {
11
+ try {
12
+ return strictDecoder.decode(new Uint8Array(bytes));
13
+ } catch {
14
+ return '';
15
+ }
16
+ };
17
+
18
+ const encode = (value: string): number[] =>
19
+ Array.from(textEncoder.encode(value));
20
+
21
+ // Convert a value between storage primitive types for the editor
22
+ // switcher. The headline transition is `string` ↔ `buffer`, which
23
+ // round-trips via UTF-8 — the user opens a string entry, picks Hex,
24
+ // and sees the bytes of that string. Other transitions preserve as
25
+ // much information as the destination type can carry, falling back to
26
+ // the destination's zero value when nothing meaningful remains.
27
+ export const convertValue = (
28
+ from: StorageEntryType,
29
+ to: StorageEntryType,
30
+ value: StorageEntryValue,
31
+ ): StorageEntryValue => {
32
+ if (from === to) return value;
33
+
34
+ if (from === 'string') {
35
+ const str = value as string;
36
+ switch (to) {
37
+ case 'number': {
38
+ const n = Number(str);
39
+ return Number.isNaN(n) ? 0 : n;
40
+ }
41
+ case 'boolean':
42
+ return str === 'true';
43
+ case 'buffer':
44
+ return encode(str);
45
+ }
46
+ }
47
+
48
+ if (from === 'number') {
49
+ const n = value as number;
50
+ switch (to) {
51
+ case 'string':
52
+ return String(n);
53
+ case 'boolean':
54
+ return n !== 0;
55
+ case 'buffer':
56
+ return encode(String(n));
57
+ }
58
+ }
59
+
60
+ if (from === 'boolean') {
61
+ const b = value as boolean;
62
+ switch (to) {
63
+ case 'string':
64
+ return String(b);
65
+ case 'number':
66
+ return b ? 1 : 0;
67
+ case 'buffer':
68
+ return encode(String(b));
69
+ }
70
+ }
71
+
72
+ if (from === 'buffer') {
73
+ const bytes = value as number[];
74
+ switch (to) {
75
+ case 'string':
76
+ return tryDecode(bytes);
77
+ case 'number': {
78
+ const decoded = tryDecode(bytes);
79
+ const n = Number(decoded);
80
+ return Number.isNaN(n) ? 0 : n;
81
+ }
82
+ case 'boolean':
83
+ return tryDecode(bytes) === 'true';
84
+ }
85
+ }
86
+
87
+ return value;
88
+ };
89
+
90
+ // Zero value for a type — used when seeding the add-entry dialog and
91
+ // when conversion has no meaningful starting point.
92
+ export const defaultValueForType = (
93
+ type: StorageEntryType,
94
+ ): StorageEntryValue => {
95
+ switch (type) {
96
+ case 'string':
97
+ return '';
98
+ case 'number':
99
+ return 0;
100
+ case 'boolean':
101
+ return false;
102
+ case 'buffer':
103
+ return [];
104
+ }
105
+ };
@@ -0,0 +1,96 @@
1
+ import type { StorageEntryType, StorageEntryValue } from '../shared/types';
2
+ import { BinaryValueEditor } from './binary-value-editor';
3
+ import { EditorSwitcher } from './editor-switcher';
4
+ import { convertValue, defaultValueForType } from './type-conversion';
5
+
6
+ export type TypedValueEditorProps = {
7
+ supportedTypes: StorageEntryType[];
8
+ type: StorageEntryType;
9
+ // `null` signals "the current input is unsavable" — used by the hex
10
+ // editor for unparseable hex or empty input. Non-buffer types never
11
+ // emit null. Callers should disable Save when value is null.
12
+ value: StorageEntryValue | null;
13
+ onChange: (type: StorageEntryType, value: StorageEntryValue | null) => void;
14
+ // `id` is forwarded to the underlying input so callers can wire up
15
+ // <label htmlFor>. The hex editor (which has no single input) ignores
16
+ // it — there's no useful target.
17
+ inputId?: string;
18
+ autoFocus?: boolean;
19
+ };
20
+
21
+ // Renders the switcher + the type-specific value editor. Switching
22
+ // type runs `convertValue` so the user lands on a sensible starting
23
+ // point in the new editor instead of an empty field.
24
+ export const TypedValueEditor = ({
25
+ supportedTypes,
26
+ type,
27
+ value,
28
+ onChange,
29
+ inputId,
30
+ autoFocus,
31
+ }: TypedValueEditorProps) => {
32
+ const handleTypeChange = (newType: StorageEntryType) => {
33
+ if (newType === type) return;
34
+ if (value === null) {
35
+ // Source value is currently unparseable — nothing meaningful to
36
+ // carry forward, land on the destination type's default.
37
+ onChange(newType, defaultValueForType(newType));
38
+ return;
39
+ }
40
+ onChange(newType, convertValue(type, newType, value));
41
+ };
42
+
43
+ return (
44
+ <div className="space-y-2">
45
+ <EditorSwitcher
46
+ supportedTypes={supportedTypes}
47
+ value={type}
48
+ onChange={handleTypeChange}
49
+ />
50
+
51
+ {type === 'buffer' ? (
52
+ <BinaryValueEditor
53
+ initialBytes={Array.isArray(value) ? value : undefined}
54
+ onChange={(bytes) => onChange('buffer', bytes)}
55
+ />
56
+ ) : type === 'boolean' ? (
57
+ <select
58
+ id={inputId}
59
+ value={String(value ?? false)}
60
+ onChange={(event) =>
61
+ onChange('boolean', event.target.value === 'true')
62
+ }
63
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
64
+ autoFocus={autoFocus}
65
+ >
66
+ <option value="true">true</option>
67
+ <option value="false">false</option>
68
+ </select>
69
+ ) : type === 'number' ? (
70
+ <input
71
+ id={inputId}
72
+ type="number"
73
+ value={String(value ?? '')}
74
+ onChange={(event) => {
75
+ const next = event.target.value;
76
+ const parsed = next === '' ? 0 : Number(next);
77
+ onChange('number', Number.isNaN(parsed) ? 0 : parsed);
78
+ }}
79
+ placeholder="Enter number value"
80
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
81
+ autoFocus={autoFocus}
82
+ />
83
+ ) : (
84
+ <input
85
+ id={inputId}
86
+ type="text"
87
+ value={String(value ?? '')}
88
+ onChange={(event) => onChange('string', event.target.value)}
89
+ placeholder="Enter string value"
90
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
91
+ autoFocus={autoFocus}
92
+ />
93
+ )}
94
+ </div>
95
+ );
96
+ };
@@ -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% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--chart-1: 12 76% 61%;--chart-2: 173 58% 39%;--chart-3: 197 37% 24%;--chart-4: 43 74% 66%;--chart-5: 27 87% 67%;--radius: .5rem;--sidebar-background: 0 0% 98%;--sidebar-foreground: 240 5.3% 26.1%;--sidebar-primary: 240 5.9% 10%;--sidebar-primary-foreground: 0 0% 98%;--sidebar-accent: 240 4.8% 95.9%;--sidebar-accent-foreground: 240 5.9% 10%;--sidebar-border: 220 13% 91%;--sidebar-ring: 217.2 91.2% 59.8%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.left-2{left:.5rem}.top-0{top:0}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-20{max-height:5rem}.max-h-24{max-height:6rem}.max-h-96{max-height:24rem}.max-h-\[90vh\]{max-height:90vh}.min-h-0{min-height:0px}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[28rem\]{width:28rem}.w-\[90vw\]{width:90vw}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;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))}.transform{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))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.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))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.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-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity, 1))}.border-yellow-700{--tw-border-opacity: 1;border-color:rgb(161 98 7 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.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-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-opacity-50{--tw-bg-opacity: .5}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-8{padding-left:2rem}.pr-3{padding-right:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-relaxed{line-height:1.625}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-cyan-500{--tw-text-opacity: 1;color:rgb(6 182 212 / var(--tw-text-opacity, 1))}.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-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-pink-500{--tw-text-opacity: 1;color:rgb(236 72 153 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.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}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;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}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-600:disabled{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.disabled\:bg-gray-800:disabled{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}