server-devtools 0.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/LICENSE +202 -0
- package/README.md +544 -0
- package/dist/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/dashboard/assets/index-DvYSUjO3.css +2 -0
- package/dist/dashboard/assets/index-vQGCvuXC.js +9 -0
- package/dist/dashboard/favicon.svg +9 -0
- package/dist/dashboard/icons.svg +24 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/src/config/index.d.ts +21 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +30 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/core/execution-tree-builder.d.ts +6 -0
- package/dist/src/core/execution-tree-builder.d.ts.map +1 -0
- package/dist/src/core/execution-tree-builder.js +44 -0
- package/dist/src/core/execution-tree-builder.js.map +1 -0
- package/dist/src/dashboard/dashboard-errors-mapper.d.ts +5 -0
- package/dist/src/dashboard/dashboard-errors-mapper.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard-errors-mapper.js +19 -0
- package/dist/src/dashboard/dashboard-errors-mapper.js.map +1 -0
- package/dist/src/dashboard/dashboard-request-detail-mapper.d.ts +16 -0
- package/dist/src/dashboard/dashboard-request-detail-mapper.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard-request-detail-mapper.js +79 -0
- package/dist/src/dashboard/dashboard-request-detail-mapper.js.map +1 -0
- package/dist/src/dashboard/dashboard-response-detail-mapper.d.ts +6 -0
- package/dist/src/dashboard/dashboard-response-detail-mapper.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard-response-detail-mapper.js +39 -0
- package/dist/src/dashboard/dashboard-response-detail-mapper.js.map +1 -0
- package/dist/src/dashboard/dashboard-server.d.ts +21 -0
- package/dist/src/dashboard/dashboard-server.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard-server.js +676 -0
- package/dist/src/dashboard/dashboard-server.js.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +118 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/instrumentation/exporter.d.ts +11 -0
- package/dist/src/instrumentation/exporter.d.ts.map +1 -0
- package/dist/src/instrumentation/exporter.js +46 -0
- package/dist/src/instrumentation/exporter.js.map +1 -0
- package/dist/src/instrumentation/instrumentation.d.ts +11 -0
- package/dist/src/instrumentation/instrumentation.d.ts.map +1 -0
- package/dist/src/instrumentation/instrumentation.js +144 -0
- package/dist/src/instrumentation/instrumentation.js.map +1 -0
- package/dist/src/instrumentation/outbound-http/outbound-http-capture.d.ts +24 -0
- package/dist/src/instrumentation/outbound-http/outbound-http-capture.d.ts.map +1 -0
- package/dist/src/instrumentation/outbound-http/outbound-http-capture.js +245 -0
- package/dist/src/instrumentation/outbound-http/outbound-http-capture.js.map +1 -0
- package/dist/src/instrumentation/span-mapper.d.ts +8 -0
- package/dist/src/instrumentation/span-mapper.d.ts.map +1 -0
- package/dist/src/instrumentation/span-mapper.js +54 -0
- package/dist/src/instrumentation/span-mapper.js.map +1 -0
- package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.d.ts +6 -0
- package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.d.ts.map +1 -0
- package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.js +23 -0
- package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.js.map +1 -0
- package/dist/src/nestjs.d.ts +2 -0
- package/dist/src/nestjs.d.ts.map +1 -0
- package/dist/src/nestjs.js +9 -0
- package/dist/src/nestjs.js.map +1 -0
- package/dist/src/security/auth.service.d.ts +16 -0
- package/dist/src/security/auth.service.d.ts.map +1 -0
- package/dist/src/security/auth.service.js +96 -0
- package/dist/src/security/auth.service.js.map +1 -0
- package/dist/src/security/encrypt-decrypt.service.d.ts +10 -0
- package/dist/src/security/encrypt-decrypt.service.d.ts.map +1 -0
- package/dist/src/security/encrypt-decrypt.service.js +93 -0
- package/dist/src/security/encrypt-decrypt.service.js.map +1 -0
- package/dist/src/storage/sqlite-storage.d.ts +35 -0
- package/dist/src/storage/sqlite-storage.d.ts.map +1 -0
- package/dist/src/storage/sqlite-storage.js +583 -0
- package/dist/src/storage/sqlite-storage.js.map +1 -0
- package/dist/src/types/dashboard.d.ts +32 -0
- package/dist/src/types/dashboard.d.ts.map +1 -0
- package/dist/src/types/dashboard.js +3 -0
- package/dist/src/types/dashboard.js.map +1 -0
- package/dist/src/types/database.d.ts +16 -0
- package/dist/src/types/database.d.ts.map +1 -0
- package/dist/src/types/database.js +3 -0
- package/dist/src/types/database.js.map +1 -0
- package/dist/src/types/http-client.d.ts +8 -0
- package/dist/src/types/http-client.d.ts.map +1 -0
- package/dist/src/types/http-client.js +3 -0
- package/dist/src/types/http-client.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +23 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/misc.d.ts +25 -0
- package/dist/src/types/misc.d.ts.map +1 -0
- package/dist/src/types/misc.js +3 -0
- package/dist/src/types/misc.js.map +1 -0
- package/dist/src/types/span.d.ts +25 -0
- package/dist/src/types/span.d.ts.map +1 -0
- package/dist/src/types/span.js +3 -0
- package/dist/src/types/span.js.map +1 -0
- package/dist/src/types/trace.d.ts +32 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +4 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/src/utils/logger.d.ts +2 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +19 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
# ServerDevTools
|
|
2
|
+
|
|
3
|
+
**Chrome DevTools for your backend.**
|
|
4
|
+
|
|
5
|
+
ServerDevTools is an embeddable backend inspector for Node.js
|
|
6
|
+
applications.
|
|
7
|
+
|
|
8
|
+
It gives you a live dashboard for inspecting:
|
|
9
|
+
|
|
10
|
+
- incoming HTTP requests
|
|
11
|
+
- request headers, params, query, and body
|
|
12
|
+
- response status, headers, and body
|
|
13
|
+
- execution flow
|
|
14
|
+
- framework middleware and handlers
|
|
15
|
+
- outgoing HTTP requests
|
|
16
|
+
- database operations
|
|
17
|
+
- errors
|
|
18
|
+
- timing
|
|
19
|
+
|
|
20
|
+
Instead of jumping between logs, traces, and multiple observability
|
|
21
|
+
tools, ServerDevTools lets you inspect what your backend is doing from
|
|
22
|
+
one developer-focused interface.
|
|
23
|
+
|
|
24
|
+
------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
## Demo
|
|
27
|
+
|
|
28
|
+
> Add launch GIF/video here.
|
|
29
|
+
|
|
30
|
+
``` text
|
|
31
|
+
POST /users
|
|
32
|
+
│
|
|
33
|
+
├── Express middleware
|
|
34
|
+
├── Request handler
|
|
35
|
+
├── MongoDB query
|
|
36
|
+
└── External HTTP request
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open:
|
|
40
|
+
|
|
41
|
+
``` text
|
|
42
|
+
http://localhost:3000/_devtools
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
and inspect the complete request lifecycle.
|
|
46
|
+
|
|
47
|
+
------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
## Why ServerDevTools?
|
|
50
|
+
|
|
51
|
+
Frontend developers have Chrome DevTools.
|
|
52
|
+
|
|
53
|
+
Backend debugging often looks like this:
|
|
54
|
+
|
|
55
|
+
``` text
|
|
56
|
+
console.log(...)
|
|
57
|
+
tail logs
|
|
58
|
+
search logs
|
|
59
|
+
inspect database
|
|
60
|
+
check external API logs
|
|
61
|
+
open tracing platform
|
|
62
|
+
repeat
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
ServerDevTools provides a simpler workflow:
|
|
66
|
+
|
|
67
|
+
``` text
|
|
68
|
+
Run request
|
|
69
|
+
↓
|
|
70
|
+
Open /_devtools
|
|
71
|
+
↓
|
|
72
|
+
Inspect what happened
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
It is designed for **live backend debugging**, not as a replacement for
|
|
76
|
+
full production observability platforms.
|
|
77
|
+
|
|
78
|
+
------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
``` bash
|
|
83
|
+
npm install server-devtools
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
ServerDevTools uses an authenticated, optionally encrypted initialization file.
|
|
91
|
+
Keep the credentials and encryption key in environment variables.
|
|
92
|
+
|
|
93
|
+
The encryption key must be a base64-encoded 32-byte key. Generate one with:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
node -e 'console.log(require("node:crypto").randomBytes(32).toString("base64"))'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`fields` is optional. By default, ServerDevTools encrypts these field names:
|
|
100
|
+
|
|
101
|
+
- `password`
|
|
102
|
+
- `accessToken`
|
|
103
|
+
- `refreshToken`
|
|
104
|
+
- `authorization`
|
|
105
|
+
- `cookie`
|
|
106
|
+
|
|
107
|
+
Specifying `fields` replaces the default list; it does not extend it.
|
|
108
|
+
|
|
109
|
+
To customize the encrypted fields:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
encryption: {
|
|
113
|
+
key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY!,
|
|
114
|
+
fields: ["password", "authorization", "apiKey"],
|
|
115
|
+
},
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`getCurrentUser` is optional. Use it when your application already exposes the
|
|
119
|
+
authenticated user on the request; ServerDevTools does not require a
|
|
120
|
+
Passport, JWT, or other authentication framework.
|
|
121
|
+
|
|
122
|
+
### Node.js / Express TypeScript
|
|
123
|
+
|
|
124
|
+
Create `server-devtools.ts`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import ServerDevTools from "server-devtools";
|
|
128
|
+
|
|
129
|
+
export const devtools = new ServerDevTools({
|
|
130
|
+
auth: {
|
|
131
|
+
username: process.env.SERVER_DEVTOOLS_USERNAME!,
|
|
132
|
+
password: process.env.SERVER_DEVTOOLS_PASSWORD!,
|
|
133
|
+
},
|
|
134
|
+
encryption: {
|
|
135
|
+
key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY!,
|
|
136
|
+
},
|
|
137
|
+
getCurrentUser: (req) => req.user,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await devtools.start();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// src/main.ts
|
|
145
|
+
import { createServer } from "node:http";
|
|
146
|
+
import { devtools } from "../server-devtools";
|
|
147
|
+
|
|
148
|
+
const server = createServer((req, res) => {
|
|
149
|
+
devtools.middleware(req, res, () => {
|
|
150
|
+
res.statusCode = 404;
|
|
151
|
+
res.end("Not found");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
server.listen(3000);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
tsx watch --import ./server-devtools.ts src/main.ts
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The application can import the same instance normally and mount it in Express:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// src/main.ts
|
|
166
|
+
import express from "express";
|
|
167
|
+
import { devtools } from "../server-devtools";
|
|
168
|
+
|
|
169
|
+
const app = express();
|
|
170
|
+
app.use((req, res, next) => devtools.middleware(req, res, next));
|
|
171
|
+
|
|
172
|
+
app.get("/users/:id", async (req, res) => {
|
|
173
|
+
res.json({ id: req.params.id, name: "John Doe" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.listen(3000);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Node.js / Express JavaScript
|
|
180
|
+
|
|
181
|
+
For JavaScript projects, create `server-devtools.js`:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const ServerDevTools = require("server-devtools");
|
|
185
|
+
|
|
186
|
+
const devtools = new ServerDevTools({
|
|
187
|
+
auth: {
|
|
188
|
+
username: process.env.SERVER_DEVTOOLS_USERNAME,
|
|
189
|
+
password: process.env.SERVER_DEVTOOLS_PASSWORD,
|
|
190
|
+
},
|
|
191
|
+
encryption: {
|
|
192
|
+
key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY,
|
|
193
|
+
},
|
|
194
|
+
getCurrentUser: (req) => req.user,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
module.exports = { devtools };
|
|
198
|
+
|
|
199
|
+
void devtools.start();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Preload the JavaScript file normally:
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
node --import ./server-devtools.js src/main.js
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
In the application, import the same instance and mount it with:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
const express = require("express");
|
|
212
|
+
const { devtools } = require("../server-devtools.js");
|
|
213
|
+
|
|
214
|
+
const app = express();
|
|
215
|
+
app.use((req, res, next) => devtools.middleware(req, res, next));
|
|
216
|
+
app.listen(3000);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
JavaScript projects do not need `allowJs`.
|
|
220
|
+
|
|
221
|
+
### NestJS
|
|
222
|
+
|
|
223
|
+
NestJS uses a different preload pattern. Create a plain `server-devtools.js`
|
|
224
|
+
outside `src`:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const ServerDevTools = require("server-devtools");
|
|
228
|
+
|
|
229
|
+
const devtools = new ServerDevTools({
|
|
230
|
+
auth: {
|
|
231
|
+
username: process.env.SERVER_DEVTOOLS_USERNAME,
|
|
232
|
+
password: process.env.SERVER_DEVTOOLS_PASSWORD,
|
|
233
|
+
},
|
|
234
|
+
encryption: {
|
|
235
|
+
key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY,
|
|
236
|
+
},
|
|
237
|
+
getCurrentUser: (req) => req.user,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
globalThis.__serverDevtools = devtools;
|
|
241
|
+
|
|
242
|
+
void (async () => {
|
|
243
|
+
await devtools.start();
|
|
244
|
+
})();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Enable JavaScript files in the Nest TypeScript configuration:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"compilerOptions": {
|
|
252
|
+
"allowJs": true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Start Nest with the preload file:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
NODE_OPTIONS='--import ./server-devtools.js' nest start --watch
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Do not create another `ServerDevTools` instance in `main.ts`. Retrieve the
|
|
264
|
+
preloaded instance instead:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// src/main.ts
|
|
268
|
+
import { NestFactory } from "@nestjs/core";
|
|
269
|
+
import { AppModule } from "./app.module";
|
|
270
|
+
import { ServerDevToolsNestInterceptor } from "server-devtools/nestjs";
|
|
271
|
+
import ServerDevTools from "server-devtools";
|
|
272
|
+
|
|
273
|
+
declare global {
|
|
274
|
+
var __serverDevtools: ServerDevTools | undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function bootstrap() {
|
|
278
|
+
const devtools = globalThis.__serverDevtools;
|
|
279
|
+
if (!devtools) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"ServerDevTools was not preloaded. Start Nest with NODE_OPTIONS='--import ./server-devtools.js'.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const app = await NestFactory.create(AppModule);
|
|
286
|
+
app.use((req, res, next) => devtools.middleware(req, res, next));
|
|
287
|
+
app.useGlobalInterceptors(
|
|
288
|
+
new ServerDevToolsNestInterceptor(),
|
|
289
|
+
);
|
|
290
|
+
await app.listen(3000);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
bootstrap();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
This Nest-specific pattern initializes instrumentation before Nest and
|
|
297
|
+
application dependencies load, while avoiding a second ServerDevTools
|
|
298
|
+
initialization during watch mode.
|
|
299
|
+
|
|
300
|
+
Each example imports and mounts the same instance that was started by the
|
|
301
|
+
preloaded initialization file. Its middleware serves the dashboard from the
|
|
302
|
+
application's existing HTTP server at `http://localhost:<your-port>/_devtools`.
|
|
303
|
+
|
|
304
|
+
OpenTelemetry and outbound fetch capture start when `devtools.start()` is
|
|
305
|
+
called; constructing the instance alone does not wrap `globalThis.fetch`.
|
|
306
|
+
|
|
307
|
+
Then open:
|
|
308
|
+
|
|
309
|
+
``` text
|
|
310
|
+
http://localhost:3000/_devtools
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Make requests to your application and they will appear in the dashboard.
|
|
314
|
+
|
|
315
|
+
------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
## Request Inspector
|
|
318
|
+
|
|
319
|
+
Inspect the incoming request, including:
|
|
320
|
+
|
|
321
|
+
- method
|
|
322
|
+
- path
|
|
323
|
+
- route
|
|
324
|
+
- headers
|
|
325
|
+
- query parameters
|
|
326
|
+
- route parameters
|
|
327
|
+
- request body
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
|
|
331
|
+
``` json
|
|
332
|
+
{
|
|
333
|
+
"name": "John Doe",
|
|
334
|
+
"email": "john@example.com"
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
## Response Inspector
|
|
341
|
+
|
|
342
|
+
Inspect what your application returned:
|
|
343
|
+
|
|
344
|
+
- status code
|
|
345
|
+
- response headers
|
|
346
|
+
- response body
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
|
|
350
|
+
``` json
|
|
351
|
+
{
|
|
352
|
+
"received": {
|
|
353
|
+
"name": "John Doe",
|
|
354
|
+
"email": "john@example.com"
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
## Execution View
|
|
362
|
+
|
|
363
|
+
ServerDevTools reconstructs the execution hierarchy for each request.
|
|
364
|
+
|
|
365
|
+
``` text
|
|
366
|
+
POST /users
|
|
367
|
+
│
|
|
368
|
+
├── Express middleware
|
|
369
|
+
├── Request handler
|
|
370
|
+
├── MongoDB query
|
|
371
|
+
└── HTTP request
|
|
372
|
+
└── api.example.com
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
This makes it easier to understand what your backend executed during a
|
|
376
|
+
request and where time was spent.
|
|
377
|
+
|
|
378
|
+
------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
## How It Works
|
|
381
|
+
|
|
382
|
+
ServerDevTools uses OpenTelemetry instrumentation to observe supported
|
|
383
|
+
backend operations.
|
|
384
|
+
|
|
385
|
+
``` text
|
|
386
|
+
Application request and dependencies
|
|
387
|
+
↓
|
|
388
|
+
OpenTelemetry captures supported activity
|
|
389
|
+
↓
|
|
390
|
+
ServerDevTools maps captured spans and details
|
|
391
|
+
↓
|
|
392
|
+
Historical inspection data in SQLite
|
|
393
|
+
↓
|
|
394
|
+
Embedded dashboard on the application's HTTP server
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
OpenTelemetry provides the instrumentation layer. ServerDevTools
|
|
398
|
+
provides the developer-focused inspection experience on top of it.
|
|
399
|
+
|
|
400
|
+
------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
## Embedded Dashboard
|
|
403
|
+
|
|
404
|
+
ServerDevTools does not require a separate dashboard server. It mounts
|
|
405
|
+
directly into your existing application:
|
|
406
|
+
|
|
407
|
+
``` text
|
|
408
|
+
http://localhost:3000
|
|
409
|
+
│
|
|
410
|
+
├── /api/users
|
|
411
|
+
├── /api/orders
|
|
412
|
+
├── /api/...
|
|
413
|
+
└── /_devtools
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
## Current Support
|
|
419
|
+
|
|
420
|
+
Initial support focuses on Node.js applications:
|
|
421
|
+
|
|
422
|
+
- Node.js HTTP
|
|
423
|
+
- Express
|
|
424
|
+
- NestJS middleware and error interception
|
|
425
|
+
- MongoDB operations
|
|
426
|
+
- native HTTP/HTTPS and fetch/Undici requests, including headers and bodies
|
|
427
|
+
- OpenTelemetry-powered activity capture
|
|
428
|
+
|
|
429
|
+
Support will expand as the project develops.
|
|
430
|
+
|
|
431
|
+
------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
## What ServerDevTools Is Not
|
|
434
|
+
|
|
435
|
+
ServerDevTools is not intended to replace full observability,
|
|
436
|
+
monitoring, alerting, or log aggregation platforms.
|
|
437
|
+
|
|
438
|
+
Its focus is:
|
|
439
|
+
|
|
440
|
+
> **Interactive inspection of individual backend requests.**
|
|
441
|
+
|
|
442
|
+
------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
## Development Status
|
|
445
|
+
|
|
446
|
+
ServerDevTools is currently under active development.
|
|
447
|
+
|
|
448
|
+
The first release focuses on making this workflow excellent:
|
|
449
|
+
|
|
450
|
+
``` text
|
|
451
|
+
Install ServerDevTools
|
|
452
|
+
↓
|
|
453
|
+
Initialize + preload ServerDevTools
|
|
454
|
+
↓
|
|
455
|
+
Start application
|
|
456
|
+
↓
|
|
457
|
+
Open /_devtools
|
|
458
|
+
↓
|
|
459
|
+
Run and inspect requests
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Expect APIs and configuration to evolve before `1.0`.
|
|
463
|
+
|
|
464
|
+
------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
## Roadmap
|
|
467
|
+
|
|
468
|
+
Near-term:
|
|
469
|
+
|
|
470
|
+
- [x] HTTP request capture
|
|
471
|
+
- [x] Request headers
|
|
472
|
+
- [x] Query parameters
|
|
473
|
+
- [x] Route parameters
|
|
474
|
+
- [x] Request body
|
|
475
|
+
- [x] HTTP response status
|
|
476
|
+
- [x] Response headers
|
|
477
|
+
- [x] Response body
|
|
478
|
+
- [x] Execution tree
|
|
479
|
+
- [x] Native HTTP/HTTPS and fetch/Undici inspection
|
|
480
|
+
- [x] MongoDB inspection
|
|
481
|
+
- [x] Error inspection
|
|
482
|
+
- [x] NestJS integration and error interception
|
|
483
|
+
- [x] Encryption at rest for selected fields
|
|
484
|
+
- [ ] Request/response redaction
|
|
485
|
+
- [ ] Body-size limits
|
|
486
|
+
- [ ] Production-safe configuration
|
|
487
|
+
- [ ] Broader NestJS compatibility
|
|
488
|
+
- [x] Static dashboard packaging
|
|
489
|
+
|
|
490
|
+
Future ideas:
|
|
491
|
+
|
|
492
|
+
- Redis
|
|
493
|
+
- PostgreSQL
|
|
494
|
+
- MySQL
|
|
495
|
+
- additional Node frameworks
|
|
496
|
+
- trace filtering
|
|
497
|
+
- request search
|
|
498
|
+
- configurable retention
|
|
499
|
+
- multi-instance support
|
|
500
|
+
|
|
501
|
+
------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
## Security
|
|
504
|
+
|
|
505
|
+
ServerDevTools may capture sensitive application data, including request
|
|
506
|
+
headers, request bodies, response bodies, database operations, and
|
|
507
|
+
external request metadata.
|
|
508
|
+
|
|
509
|
+
Optional encryption at rest can protect configured fields in supported
|
|
510
|
+
captured data. It does not redact all captured data or replace application
|
|
511
|
+
security practices. The dashboard has authentication; use strong credentials
|
|
512
|
+
and restrict access to `/_devtools` through your network and deployment setup.
|
|
513
|
+
|
|
514
|
+
Automatic redaction and capture-size limits are not currently implemented.
|
|
515
|
+
|
|
516
|
+
------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
## Philosophy
|
|
519
|
+
|
|
520
|
+
ServerDevTools should remain:
|
|
521
|
+
|
|
522
|
+
- easy to install
|
|
523
|
+
- easy to understand
|
|
524
|
+
- framework-friendly
|
|
525
|
+
- developer-focused
|
|
526
|
+
- useful without requiring a full observability stack
|
|
527
|
+
|
|
528
|
+
The goal is simple:
|
|
529
|
+
|
|
530
|
+
> **Make backend debugging feel more like using browser DevTools.**
|
|
531
|
+
|
|
532
|
+
------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
## Contributing
|
|
535
|
+
|
|
536
|
+
The project is still early.
|
|
537
|
+
|
|
538
|
+
Issues, ideas, bug reports, and contributions are welcome.
|
|
539
|
+
|
|
540
|
+
------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
## License
|
|
543
|
+
|
|
544
|
+
Apache-2.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){:root,:host{--shimmer-angle:20deg}*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--scroll-fade-t:0px;--scroll-fade-b:0px;--scroll-fade-s:0px;--scroll-fade-e:0px;--scroll-fade-mask:initial;--shimmer-image:initial;--shimmer-text-fill:initial}}}@layer theme{:root,:host{--font-sans:"Geist Variable", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-green-500:oklch(72.3% .219 149.579);--color-black:#000;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-md:calc(var(--radius) * .8);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Geist Variable", sans-serif;--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab, var(--ring) 50%, transparent)}}body{background-color:var(--background);color:var(--foreground)}html{font-family:Geist Variable,sans-serif}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.top-1\/2{top:50%}.right-2{right:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.z-50{z-index:50}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-7{margin-bottom:calc(var(--spacing) * 7)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-1{margin-left:var(--spacing)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.h-0\.5{height:calc(var(--spacing) * .5)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-full{height:100%}.max-h-96{max-height:calc(var(--spacing) * 96)}.min-h-screen{min-height:100vh}.w-12{width:calc(var(--spacing) * 12)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:0}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[minmax\(8rem\,30\%\)_1fr\]{grid-template-columns:minmax(8rem,30%) 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-4xl{border-radius:calc(var(--radius) * 2.6)}.rounded-\[min\(var\(--radius-md\)\,10px\)\]{border-radius:min(var(--radius-md), 10px)}.rounded-\[min\(var\(--radius-md\)\,12px\)\]{border-radius:min(var(--radius-md), 12px)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) * .8)}.rounded-xl{border-radius:calc(var(--radius) * 1.4)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--border)}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.border-destructive\/30{border-color:color-mix(in oklab, var(--destructive) 30%, transparent)}}.border-input{border-color:var(--input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/5{background-color:color-mix(in oklab, var(--destructive) 5%, transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--destructive) 10%, transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-muted{background-color:var(--muted)}.bg-muted-foreground\/60{background-color:var(--muted-foreground)}@supports (color:color-mix(in lab, red, red)){.bg-muted-foreground\/60{background-color:color-mix(in oklab, var(--muted-foreground) 60%, transparent)}}.bg-muted\/20{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/20{background-color:color-mix(in oklab, var(--muted) 20%, transparent)}}.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/30{background-color:color-mix(in oklab, var(--muted) 30%, transparent)}}.bg-muted\/40{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.bg-muted\/70{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/70{background-color:color-mix(in oklab, var(--muted) 70%, transparent)}}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-clip-padding{background-clip:padding-box}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-9{padding-inline:calc(var(--spacing) * 9)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-9{padding-left:calc(var(--spacing) * 9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[10px\]{font-size:10px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-destructive{color:var(--destructive)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.file\:inline-flex::file-selector-button{display:inline-flex}.file\:h-6::file-selector-button{height:calc(var(--spacing) * 6)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:color-mix(in oklch,var(--secondary),var(--foreground) 5%)}}.hover\:bg-destructive\/20:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/20:hover{background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.hover\:bg-muted:hover,.hover\:bg-muted\/40:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/40:hover{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab, var(--primary) 80%, transparent)}}.hover\:text-destructive:hover{color:var(--destructive)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.hover\:underline:hover{text-decoration-line:underline}}.focus-visible\:border-destructive\/40:focus-visible{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:border-destructive\/40:focus-visible{border-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-3:focus-visible,.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]){--tw-translate-y:1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-input\/50:disabled{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.disabled\:bg-input\/50:disabled{background-color:color-mix(in oklab, var(--input) 50%, transparent)}}.disabled\:opacity-50:disabled{opacity:.5}:where([data-slot=button-group]) .in-data-\[slot\=button-group\]\:rounded-lg{border-radius:var(--radius)}.has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-end\]\:pr-2:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 2)}.has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-start\]\:pl-2:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 2)}.aria-expanded\:bg-muted[aria-expanded=true]{background-color:var(--muted)}.aria-expanded\:bg-secondary[aria-expanded=true]{background-color:var(--secondary)}.aria-expanded\:text-foreground[aria-expanded=true]{color:var(--foreground)}.aria-expanded\:text-secondary-foreground[aria-expanded=true]{color:var(--secondary-foreground)}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-3[aria-invalid=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}@media (width>=40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:p-8{padding:calc(var(--spacing) * 8)}}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (width>=64rem){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:bg-destructive\/20:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-destructive\/20:is(.dark *){background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab, var(--input) 30%, transparent)}}@media (hover:hover){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:color-mix(in oklab, var(--destructive) 30%, transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab, var(--input) 50%, transparent)}}.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:disabled\:bg-input\/80:is(.dark *):disabled{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:disabled\:bg-input\/80:is(.dark *):disabled{background-color:color-mix(in oklab, var(--input) 80%, transparent)}}.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:color-mix(in oklab, var(--destructive) 50%, transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 svg:not([class*=size-]){width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}@media (hover:hover){.\[a\]\:hover\:bg-destructive\/20:is(a):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-destructive\/20:is(a):hover{background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.\[a\]\:hover\:bg-muted:is(a):hover{background-color:var(--muted)}.\[a\]\:hover\:bg-primary\/80:is(a):hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-primary\/80:is(a):hover{background-color:color-mix(in oklab, var(--primary) 80%, transparent)}}.\[a\]\:hover\:bg-secondary\/80:is(a):hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-secondary\/80:is(a):hover{background-color:color-mix(in oklab, var(--secondary) 80%, transparent)}}.\[a\]\:hover\:text-muted-foreground:is(a):hover{color:var(--muted-foreground)}}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3\!>svg{width:calc(var(--spacing) * 3)!important;height:calc(var(--spacing) * 3)!important}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@property --scroll-fade-t{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-b{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-s{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-e{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-mask{syntax:"*";inherits:false}@property --shimmer-angle{syntax:"<angle>";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:"*";inherits:false}@property --shimmer-text-fill{syntax:"*";inherits:false}@media (prefers-reduced-motion:reduce){.shimmer{-webkit-text-fill-color:currentColor;background-image:none;animation:none}}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--radius:.625rem;--sidebar:oklch(98.5% 0 0);--sidebar-foreground:oklch(14.5% 0 0);--sidebar-primary:oklch(20.5% 0 0);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(97% 0 0);--sidebar-accent-foreground:oklch(20.5% 0 0);--sidebar-border:oklch(92.2% 0 0);--sidebar-ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--sidebar:oklch(20.5% 0 0);--sidebar-foreground:oklch(98.5% 0 0);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(26.9% 0 0);--sidebar-accent-foreground:oklch(98.5% 0 0);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.6% 0 0)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|