@rozenite/storage-plugin 1.9.0 → 1.10.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 +15 -0
- package/dist/devtools/assets/panel-Bm-SWF7d.js +33 -0
- package/dist/devtools/assets/panel-DIqI4WSp.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +38 -56
- package/dist/rozenite.json +1 -1
- package/package.json +9 -6
- package/src/react-native/adapters/__tests__/mmkv.test.ts +300 -5
- package/src/react-native/adapters/mmkv.ts +51 -40
- package/src/ui/__tests__/binary-value-editor-state.test.ts +199 -0
- package/src/ui/__tests__/binary.test.ts +251 -0
- package/src/ui/__tests__/type-conversion.test.ts +123 -0
- package/src/ui/add-entry-dialog.tsx +83 -173
- package/src/ui/binary-value-editor-state.ts +125 -0
- package/src/ui/binary-value-editor.tsx +168 -0
- package/src/ui/binary.ts +123 -0
- package/src/ui/edit-entry-dialog.tsx +64 -161
- package/src/ui/editable-table.tsx +12 -10
- package/src/ui/editor-switcher.tsx +62 -0
- package/src/ui/entry-detail-dialog.tsx +14 -6
- package/src/ui/type-conversion.ts +105 -0
- package/src/ui/typed-value-editor.tsx +96 -0
- package/dist/devtools/assets/panel-DVxtKLI9.css +0 -1
- package/dist/devtools/assets/panel-DnjbFMGo.js +0 -22
- package/src/react-native/is-garbled.ts +0 -17
|
@@ -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 = (
|
|
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
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
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}
|