statikapi 0.1.4 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statikapi",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zonayedpca/statikapi",
@@ -17,11 +17,15 @@
17
17
  "keywords": ["static", "json", "api", "cli", "ssg"],
18
18
  "publishConfig": { "access": "public", "provenance": true },
19
19
  "dependencies": {
20
- "chokidar": "^3.6.0"
20
+ "chokidar": "^3.6.0",
21
+ "sirv": "^2.0.4",
22
+ "polka": "^0.5.2",
23
+ "esbuild": "^0.23.0"
21
24
  },
22
25
  "scripts": {
23
- "build": "node ./scripts/build.js || true",
24
- "sync-ui": "rm -rf ui && mkdir -p ui && cp -R ../ui/dist/* ui/",
25
- "prepack": "pnpm -w --filter @statikapi/ui build && pnpm run sync-ui"
26
+ "dev": "node bin/statikapi.js dev --port 8788",
27
+ "build": "node bin/statikapi.js build",
28
+ "prepack": "node scripts/embed-ui.js",
29
+ "test": "node --test"
26
30
  }
27
31
  }
@@ -99,7 +99,6 @@ export default async function buildCmd(argv) {
99
99
  bytes: Buffer.byteLength(json),
100
100
  mtime: st ? st.mtimeMs : Date.now(),
101
101
  hash: digest(json),
102
- revalidate: null,
103
102
  };
104
103
  manifest.push(entry);
105
104
  }
@@ -1,7 +1,10 @@
1
1
  import chokidar from 'chokidar';
2
2
  import path from 'node:path';
3
3
  import fs from 'node:fs/promises';
4
+ import fss from 'node:fs'; // NEW: for createReadStream
4
5
  import crypto from 'node:crypto';
6
+ import http from 'node:http'; // NEW: tiny HTTP server
7
+ import { fileURLToPath } from 'node:url'; // NEW: resolve UI dist
5
8
 
6
9
  import { loadConfig } from '../config/loadConfig.js';
7
10
  import { loadModuleValue } from '../loader/loadModuleValue.js';
@@ -11,6 +14,12 @@ import { readFlags } from '../util/readFlags.js';
11
14
  import { writeFileEnsured } from '../util/fsx.js';
12
15
  import { routeToOutPath } from '../build/routeOutPath.js';
13
16
 
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function hasIndex(dir) {
20
+ return fss.existsSync(path.join(dir, 'index.html'));
21
+ }
22
+
14
23
  function clearScreen() {
15
24
  process.stdout.write('\x1Bc'); // ANSI "clear screen"
16
25
  }
@@ -46,31 +55,45 @@ function toParams(segTokens, concreteRoute) {
46
55
  }
47
56
 
48
57
  export default async function devCmd(argv) {
49
- // In non-TTY (like node --test), behave like the old stub so tests don't hang.
50
- if (!process.stdout.isTTY) {
58
+ const flags = readFlags(argv);
59
+
60
+ // Allow forcing long-running behavior even in non-TTY (e.g., under `concurrently`)
61
+ const forceKeepAlive =
62
+ !!(flags['keep-alive'] || flags.keepAlive || flags.serve) ||
63
+ process.env.STATIKAPI_FORCE_DEV === '1';
64
+
65
+ // In non-TTY (like node --test), behave like a stub unless explicitly forced.
66
+ if (!process.stdout.isTTY && !forceKeepAlive) {
51
67
  console.log('statikapi dev → starting dev server (stub)');
52
68
 
53
69
  return 0;
54
70
  }
55
71
 
56
- const flags = readFlags(argv);
57
72
  const { config } = await loadConfig({ flags });
58
73
 
59
74
  // Where to notify preview
60
- const previewHost = String(flags.previewHost ?? '127.0.0.1');
61
- const previewPort = Number.isFinite(flags.previewPort) ? Number(flags.previewPort) : 8788;
62
- const notifyOrigin = `http://${previewHost}:${previewPort}`;
63
-
64
- async function notifyChanged(route) {
65
- try {
66
- // Node 18+ has global fetch
67
- const u = `${notifyOrigin}/_ui/changed?route=${encodeURIComponent(route)}`;
68
-
69
- await fetch(u, { method: 'POST' }).catch(() => {});
70
- } catch {
71
- // Ignore if preview isn't running
75
+ // NEW: dev server + UI defaults
76
+ const host = String(flags.host ?? '127.0.0.1');
77
+ const port = Number.isFinite(flags.port) ? Number(flags.port) : 8788;
78
+ const noUi = !!(flags['no-ui'] || flags.noUi);
79
+ const noOpen = !!(flags['no-open'] || flags.noOpen);
80
+
81
+ // NEW: live SSE clients
82
+ const sseClients = new Set(); // each entry: { id, res }
83
+ function sseBroadcast(msg) {
84
+ const line = `data: ${msg}\n\n`;
85
+ for (const c of sseClients) {
86
+ try {
87
+ c.res.write(line);
88
+ } catch {
89
+ /* ignore */
90
+ }
72
91
  }
73
92
  }
93
+ async function notifyChanged(route) {
94
+ // Push to connected UIs
95
+ sseBroadcast(`changed:${route}`);
96
+ }
74
97
 
75
98
  // Cache of outputs per source file (for deletions on subsequent rebuilds)
76
99
  const lastEmitted = new Map(); // fileAbs -> Set<concreteRoute>
@@ -111,7 +134,6 @@ export default async function devCmd(argv) {
111
134
  bytes: Buffer.byteLength(json),
112
135
  mtime: st ? st.mtimeMs : Date.now(),
113
136
  hash: digest(json),
114
- revalidate: null,
115
137
  };
116
138
 
117
139
  manifestByRoute.set(route, entry);
@@ -187,7 +209,7 @@ export default async function devCmd(argv) {
187
209
  if (rel.startsWith('_')) return false;
188
210
  const ext = path.extname(rel);
189
211
 
190
- return ext === '.js' || ext === '.mjs' || ext === '.cjs';
212
+ return ['.js', '.mjs', '.cjs', '.ts', '.tsx'].includes(ext);
191
213
  }
192
214
 
193
215
  async function buildOne(fileAbs, kind) {
@@ -252,6 +274,116 @@ export default async function devCmd(argv) {
252
274
  await writeManifest();
253
275
  console.log(`[statikapi] ready. Watching ${path.relative(process.cwd(), config.paths.srcAbs)}/`);
254
276
 
277
+ // NEW: start HTTP server (UI + JSON helpers + SSE)
278
+ const server = http.createServer(async (req, res) => {
279
+ try {
280
+ let url;
281
+ try {
282
+ url = new URL(req.url || '/', `http://${host}:${port}`);
283
+ } catch {
284
+ // Extremely defensive fallback
285
+ url = new URL('/', `http://${host}:${port}`);
286
+ }
287
+ const pathname = url.pathname;
288
+
289
+ // 1) SSE: /_ui/events
290
+ if (pathname === '/_ui/events') {
291
+ res.writeHead(200, {
292
+ 'Content-Type': 'text/event-stream',
293
+ 'Cache-Control': 'no-cache',
294
+ Connection: 'keep-alive',
295
+ 'X-Accel-Buffering': 'no', // for proxies
296
+ });
297
+ res.write('\n');
298
+ const client = { id: Date.now() + Math.random(), res };
299
+ sseClients.add(client);
300
+ req.on('close', () => sseClients.delete(client));
301
+ return;
302
+ }
303
+
304
+ // 2) Manifest JSON for UI: /ui/index
305
+ if (pathname === '/ui/index' && req.method === 'GET') {
306
+ const list = Array.from(manifestByRoute.values()).sort((a, b) =>
307
+ a.route.localeCompare(b.route)
308
+ );
309
+ const body = JSON.stringify(list);
310
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
311
+ res.end(body);
312
+ return;
313
+ }
314
+
315
+ // 3) Serve built file content: /_ui/file?route=/path
316
+ if (pathname === '/_ui/file' && req.method === 'GET') {
317
+ const route = url.searchParams.get('route') || '';
318
+ const outFile = routeToOutPath({ outAbs: config.paths.outAbs, route });
319
+ // best-effort headers
320
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
321
+ try {
322
+ const rs = fss.createReadStream(outFile);
323
+ rs.on('error', () => {
324
+ res.statusCode = 404;
325
+ res.end(`Not found: ${route}`);
326
+ });
327
+ rs.pipe(res);
328
+ } catch {
329
+ res.statusCode = 404;
330
+ res.end(`Not found: ${route}`);
331
+ }
332
+ return;
333
+ }
334
+
335
+ // 4) Static React UI at /_ui/* (unless --no-ui)
336
+ if (!noUi && pathname.startsWith('/_ui/')) {
337
+ const uiRoot = resolveUiDist();
338
+ const rel = pathname.replace(/^\/_ui\//, '') || 'index.html';
339
+ const file = path.join(uiRoot, rel);
340
+ if (!file.startsWith(uiRoot)) {
341
+ res.statusCode = 403;
342
+ res.end('Forbidden');
343
+ return;
344
+ }
345
+ try {
346
+ const stat = await fs.stat(file);
347
+ if (stat.isDirectory()) {
348
+ // try index.html inside subdir
349
+ const idx = path.join(file, 'index.html');
350
+ await fs.access(idx);
351
+ streamFile(idx, res);
352
+ } else {
353
+ streamFile(file, res);
354
+ }
355
+ } catch {
356
+ // Fallback to index.html for SPA routes
357
+ const fallback = path.join(uiRoot, 'index.html');
358
+ streamFile(fallback, res);
359
+ }
360
+ return;
361
+ }
362
+
363
+ // 5) Root → redirect to UI (unless --no-ui)
364
+ if (!noUi && pathname === '/') {
365
+ res.writeHead(302, { Location: '/_ui/' });
366
+ res.end();
367
+ return;
368
+ }
369
+
370
+ // Otherwise: 404
371
+ res.statusCode = 404;
372
+ res.end('Not Found');
373
+ } catch (e) {
374
+ console.log(e);
375
+ res.statusCode = 500;
376
+ res.end('Internal Server Error');
377
+ }
378
+ });
379
+
380
+ server.listen(port, host, () => {
381
+ console.log(`statikapi dev → serving on http://${host}:${port}${noUi ? '' : '/_ui/'}`);
382
+ if (!noUi && !noOpen) {
383
+ openInBrowser(`http://${host}:${port}/_ui/`).catch(() => {});
384
+ }
385
+ });
386
+
255
387
  const watcher = chokidar.watch(config.paths.srcAbs, {
256
388
  ignoreInitial: true,
257
389
  ignored: (p) => path.basename(p).startsWith('_'),
@@ -263,10 +395,62 @@ export default async function devCmd(argv) {
263
395
 
264
396
  // Keep process alive until SIGINT
265
397
  await new Promise((resolve) => {
266
- const stop = () => watcher.close().then(resolve).catch(resolve);
398
+ const stop = () =>
399
+ Promise.allSettled([watcher.close(), new Promise((r) => server.close(() => r()))]).then(() =>
400
+ resolve()
401
+ );
267
402
  process.on('SIGINT', stop);
268
403
  process.on('SIGTERM', stop);
269
404
  });
270
405
 
271
406
  return 0;
272
407
  }
408
+
409
+ // NEW: helpers (static file & UI dist resolver & opener)
410
+ function streamFile(file, res) {
411
+ const ext = path.extname(file).toLowerCase();
412
+ const ctype =
413
+ ext === '.html'
414
+ ? 'text/html; charset=utf-8'
415
+ : ext === '.js'
416
+ ? 'text/javascript; charset=utf-8'
417
+ : ext === '.css'
418
+ ? 'text/css; charset=utf-8'
419
+ : ext === '.json'
420
+ ? 'application/json; charset=utf-8'
421
+ : ext === '.svg'
422
+ ? 'image/svg+xml'
423
+ : ext === '.map'
424
+ ? 'application/json; charset=utf-8'
425
+ : 'application/octet-stream';
426
+ res.setHeader('Content-Type', ctype);
427
+ fss.createReadStream(file).pipe(res);
428
+ }
429
+
430
+ function resolveUiDist() {
431
+ // 0) Optional override for power users
432
+ const fromEnv = process.env.STATIKAPI_UI_DIR;
433
+ if (fromEnv && hasIndex(fromEnv)) return fromEnv;
434
+
435
+ // 1) Bundled with the CLI: packages/cli/ui/ (your screenshot)
436
+ const bundled = path.resolve(__dirname, '..', '..', 'ui');
437
+ if (hasIndex(bundled)) return bundled;
438
+
439
+ // 2) Monorepo dev fallback: packages/ui/dist
440
+ const monorepoDist = path.resolve(__dirname, '..', '..', '..', 'ui', 'dist');
441
+ if (hasIndex(monorepoDist)) return monorepoDist;
442
+
443
+ // 3) Last resort: throw with a helpful hint
444
+ throw new Error(
445
+ 'StatikAPI UI build not found. ' +
446
+ 'Either keep a built UI at packages/cli/ui/ (index.html present), ' +
447
+ 'or run: pnpm -w --filter @statikapi/ui build'
448
+ );
449
+ }
450
+
451
+ async function openInBrowser(url) {
452
+ const { exec } = await import('node:child_process');
453
+ const cmd =
454
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
455
+ exec(`${cmd} "${url}"`);
456
+ }
package/src/help.js CHANGED
@@ -14,8 +14,6 @@ Global options:
14
14
  -v, --version Show version
15
15
 
16
16
  Examples:
17
- statikapi init
18
17
  statikapi build
19
18
  statikapi dev
20
- statikapi preview
21
19
  `;
package/src/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { HELP } from './help.js';
3
- import initCmd from './commands/init.js';
4
3
  import buildCmd from './commands/build.js';
5
4
  import devCmd from './commands/dev.js';
6
- import previewCmd from './commands/preview.js';
7
5
 
8
6
  const require = createRequire(import.meta.url);
9
7
  const { version } = require('../package.json');
@@ -24,14 +22,10 @@ export async function run(argv = process.argv.slice(2)) {
24
22
  }
25
23
 
26
24
  switch (cmd) {
27
- case 'init':
28
- return await initCmd(rest);
29
25
  case 'build':
30
26
  return await buildCmd(rest);
31
27
  case 'dev':
32
28
  return await devCmd(rest);
33
- case 'preview':
34
- return await previewCmd(rest);
35
29
  default:
36
30
  console.error(`Unknown command: ${cmd}\n`);
37
31
  console.log(HELP);
@@ -0,0 +1,38 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { transform } from 'esbuild';
5
+
6
+ export async function importModule(fileAbs, { fresh = false } = {}) {
7
+ const ext = path.extname(fileAbs).toLowerCase();
8
+ const isTs = ext === '.ts' || ext === '.tsx';
9
+
10
+ // Non-TS: import by file URL; OK to use ?v= for cache-busting here.
11
+ if (!isTs) {
12
+ const u = pathToFileURL(fileAbs);
13
+ if (fresh) u.search = `v=${Date.now()}-${Math.random()}`;
14
+ return import(u.href);
15
+ }
16
+
17
+ // TS / TSX: transpile, then import via data: URL (no query params allowed!)
18
+ const src = await readFile(fileAbs, 'utf8');
19
+ const isTsx = ext === '.tsx';
20
+
21
+ // Make the module body unique when fresh=true so Node doesn’t reuse cache.
22
+ const nonce = fresh ? `\n/*__statikapi_v__=${Date.now()}-${Math.random()}*/` : '';
23
+
24
+ const { code } = await transform(src + nonce, {
25
+ loader: isTsx ? 'tsx' : 'ts',
26
+ format: 'esm',
27
+ sourcemap: 'inline',
28
+ target: 'es2022',
29
+ jsx: 'automatic',
30
+ sourcefile: fileAbs, // helps stack traces
31
+ });
32
+
33
+ // IMPORTANT: no ?query. Include charset to keep Node happy.
34
+ const href =
35
+ 'data:text/javascript;charset=utf-8;base64,' + Buffer.from(code, 'utf8').toString('base64');
36
+
37
+ return import(href);
38
+ }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
2
 
4
3
  import { LoaderError } from './errors.js';
4
+ import { importModule } from './importModule.js';
5
5
  import { assertSerializable } from './serializeGuard.js';
6
6
 
7
7
  /**
@@ -18,9 +18,7 @@ export async function loadModuleValue(fileAbs, args = {}) {
18
18
  let mod;
19
19
 
20
20
  try {
21
- const u = new URL(pathToFileURL(fileAbs).href);
22
- if (fresh) u.search = `v=${Date.now()}-${Math.random()}`;
23
- mod = await import(u.href);
21
+ mod = await importModule(fileAbs, { fresh });
24
22
  } catch (e) {
25
23
  throw new LoaderError(fileInfo, `Failed to import: ${e.message}`);
26
24
  }
@@ -1,16 +1,14 @@
1
1
  import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
2
 
4
3
  import { LoaderError } from './errors.js';
4
+ import { importModule } from './importModule.js';
5
5
 
6
6
  export async function loadPaths(fileAbs, { route, type, segments }, { fresh = false } = {}) {
7
7
  const fileInfo = short(fileAbs);
8
8
  let mod;
9
9
 
10
10
  try {
11
- const u = new URL(pathToFileURL(fileAbs).href);
12
- if (fresh) u.search = `v=${Date.now()}-${Math.random()}`;
13
- mod = await import(u.href);
11
+ mod = await importModule(fileAbs, { fresh });
14
12
  } catch (e) {
15
13
  throw new LoaderError(fileInfo, `Failed to import for paths(): ${e.message}`);
16
14
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- const VALID_EXT = new Set(['.js', '.mjs', '.cjs']);
4
+ const VALID_EXT = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx']);
5
5
 
6
6
  export async function mapRoutes({ srcAbs }) {
7
7
  const entries = await walk(srcAbs);
@@ -0,0 +1,2 @@
1
+ This folder is generated during publish by packages/cli/scripts/embed-ui.js
2
+ and contains the prebuilt StatikAPI UI that the CLI serves at /_ui.
@@ -0,0 +1 @@
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}.dark{--background: 0 0% 3.9%;--foreground: 0 0% 98%;--card: 0 0% 3.9%;--card-foreground: 0 0% 98%;--popover: 0 0% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 9%;--secondary: 0 0% 14.9%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 14.9%;--muted-foreground: 0 0% 63.9%;--accent: 0 0% 14.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%;--chart-1: 220 70% 50%;--chart-2: 160 60% 45%;--chart-3: 30 80% 55%;--chart-4: 280 65% 60%;--chart-5: 340 75% 55%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.left-2{left:.5rem}.top-0{top:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[1px\]{height:1px}.h-\[calc\(100vh-12rem\)\]{height:calc(100vh - 12rem)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\[var\(--radix-dropdown-menu-content-available-height\)\]{max-height:var(--radix-dropdown-menu-content-available-height)}.min-h-0{min-height:0px}.w-10{width:2.5rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[1px\]{width:1px}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-10{min-width:2.5rem}.min-w-11{min-width:2.75rem}.min-w-9{min-width:2.25rem}.min-w-\[8rem\]{min-width:8rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.origin-\[--radix-dropdown-menu-content-transform-origin\]{transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\[--radix-tooltip-content-transform-origin\]{transform-origin:var(--radix-tooltip-content-transform-origin)}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-\[20rem_minmax\(0\,1fr\)\]{grid-template-columns:20rem minmax(0,1fr)}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-input{border-color:hsl(var(--input))}.border-transparent{border-color:transparent}.border-l-transparent{border-left-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-accent\/50{background-color:hsl(var(--accent) / .5)}.bg-background{background-color:hsl(var(--background))}.bg-background\/80{background-color:hsl(var(--background) / .8)}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-muted{background-color:hsl(var(--muted))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.fill-current{fill:currentColor}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-\[1px\]{padding:1px}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.text-left{text-align:left}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[12px\]{font-size:12px}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-widest{letter-spacing:.1em}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.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)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.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}.transition-opacity{transition-property:opacity;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))}}.animate-in{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.fade-in-0{--tw-enter-opacity: 0}.zoom-in-95{--tw-enter-scale: .95}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-foreground::file-selector-button{color:hsl(var(--foreground))}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-accent\/40:hover{background-color:hsl(var(--accent) / .4)}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted:hover{background-color:hsl(var(--muted))}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-muted-foreground:hover{color:hsl(var(--muted-foreground))}.hover\:underline:hover{text-decoration-line:underline}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.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-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--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-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=on\]\:bg-accent[data-state=on],.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=on\]\:text-accent-foreground[data-state=on]{color:hsl(var(--accent-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.dark\:text-sky-300:is(class *){--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:inline{display:inline}.sm\:inline-flex{display:inline-flex}}@media (min-width: 768px){.md\:text-sm{font-size:.875rem;line-height:1.25rem}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}:root{color-scheme:light dark}html,body,#root{height:100%}