@crvy/rprtr 0.0.4 → 0.0.7

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.
@@ -9,11 +9,12 @@ import {
9
9
  applyTestEndEvent,
10
10
  createMutableReportState,
11
11
  finalizeRunEvent,
12
+ resolveBaselineTargets,
12
13
  safeParse
13
- } from "./chunk-IEYNAB6M.js";
14
+ } from "./chunk-JGS2VWQP.js";
14
15
 
15
16
  // src/server/app.ts
16
- import { dirname as dirname2, join as join3 } from "path";
17
+ import { dirname as dirname3, join as join4 } from "path";
17
18
  import { fileURLToPath } from "url";
18
19
  import pLimit from "p-limit";
19
20
 
@@ -207,14 +208,123 @@ function handleSync(ctx) {
207
208
  broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
208
209
  }
209
210
 
210
- // src/server/routes.ts
211
+ // src/server/report-watch.ts
212
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
211
213
  import { join as join2 } from "path";
214
+ var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
215
+ function createDebouncedRefresh(reload, delayMs = 50) {
216
+ let timer = null;
217
+ return () => {
218
+ if (timer !== null) {
219
+ clearTimeout(timer);
220
+ }
221
+ timer = setTimeout(() => {
222
+ timer = null;
223
+ void reload();
224
+ }, delayMs);
225
+ };
226
+ }
227
+ function isFileNotFound2(error) {
228
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
229
+ }
230
+ async function describeFile(filePath, label) {
231
+ try {
232
+ const fileStats = await stat2(filePath);
233
+ return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
234
+ } catch (error) {
235
+ if (isFileNotFound2(error)) {
236
+ return null;
237
+ }
238
+ throw error;
239
+ }
240
+ }
241
+ async function createOfflineFingerprint(offlineReportDir) {
242
+ if (!await isDirectory(offlineReportDir)) {
243
+ return "offline:missing";
244
+ }
245
+ try {
246
+ const entries = await readdir2(offlineReportDir, { withFileTypes: true });
247
+ const relevantEntries = entries.filter(
248
+ (entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
249
+ ).sort((left, right) => left.name.localeCompare(right.name));
250
+ const parts = await Promise.all(
251
+ relevantEntries.map((entry) => describeFile(join2(offlineReportDir, entry.name), entry.name))
252
+ );
253
+ return `offline:${parts.filter((part) => part !== null).join("|")}`;
254
+ } catch (error) {
255
+ const errorMsg = error instanceof Error ? error.message : String(error);
256
+ console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
257
+ return "offline:error";
258
+ }
259
+ }
260
+ async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
261
+ const directoryPath = relativePath === "" ? screenshotDir : join2(screenshotDir, relativePath);
262
+ if (!await isDirectory(directoryPath)) {
263
+ return relativePath === "" ? ["screenshots:missing"] : [];
264
+ }
265
+ try {
266
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
267
+ const parts = await Promise.all(
268
+ entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
269
+ const entryRelativePath = relativePath === "" ? entry.name : join2(relativePath, entry.name);
270
+ if (entry.isDirectory()) {
271
+ return createScreenshotFingerprint(screenshotDir, entryRelativePath);
272
+ }
273
+ return describeFile(join2(screenshotDir, entryRelativePath), entryRelativePath);
274
+ })
275
+ );
276
+ return parts.flat().filter((part) => part !== null);
277
+ } catch (error) {
278
+ if (isFileNotFound2(error)) {
279
+ return relativePath === "" ? ["screenshots:missing"] : [];
280
+ }
281
+ const errorMsg = error instanceof Error ? error.message : String(error);
282
+ console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
283
+ return relativePath === "" ? ["screenshots:error"] : [];
284
+ }
285
+ }
286
+ async function createArtifactsFingerprint(options) {
287
+ const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
288
+ createOfflineFingerprint(options.offlineReportDir),
289
+ createScreenshotFingerprint(options.screenshotDir)
290
+ ]);
291
+ return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
292
+ }
293
+ async function watchReportArtifacts(options) {
294
+ let fingerprint = await createArtifactsFingerprint(options);
295
+ let isPolling = false;
296
+ const interval = setInterval(() => {
297
+ if (isPolling) {
298
+ return;
299
+ }
300
+ isPolling = true;
301
+ void createArtifactsFingerprint(options).then((nextFingerprint) => {
302
+ if (nextFingerprint === fingerprint) {
303
+ return;
304
+ }
305
+ fingerprint = nextFingerprint;
306
+ options.scheduleRefresh();
307
+ }).catch((error) => {
308
+ const errorMsg = error instanceof Error ? error.message : String(error);
309
+ console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
310
+ }).finally(() => {
311
+ isPolling = false;
312
+ });
313
+ }, 100);
314
+ return () => {
315
+ clearInterval(interval);
316
+ };
317
+ }
318
+
319
+ // src/server/routes.ts
320
+ import { existsSync } from "fs";
321
+ import { dirname as dirname2, join as join3 } from "path";
212
322
  var LIVE_UPDATES_WEBSOCKET_PATH = "/";
213
323
  function isWebSocketUpgradeRequest(req) {
214
324
  return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
215
325
  }
216
326
  async function handleRoot(ctx) {
217
- const html = await respondWithFile(join2(ctx.staticDir, "index.html"), "text/html");
327
+ const html = await respondWithFile(join3(ctx.staticDir, "index.html"), "text/html");
218
328
  return html ?? new Response("Not Found", { status: 404 });
219
329
  }
220
330
  async function handleAppCss() {
@@ -231,6 +341,37 @@ async function handleSrcFiles(req) {
231
341
  function handleApiReport(ctx) {
232
342
  return Response.json(ctx.reportData);
233
343
  }
344
+ var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
345
+ function actualPathFromUrl(ctx, actualUrl) {
346
+ return actualUrl.startsWith("/screenshots/") ? join3(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length)) : actualUrl;
347
+ }
348
+ function reporterTitlePath(test) {
349
+ const testFile = test.location?.file;
350
+ return ["", test.browser, testFile ?? "", ...test.titlePath, test.title];
351
+ }
352
+ function resolveApprovalTarget(ctx, test, retry, imageName) {
353
+ const testFile = test.location?.file;
354
+ const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
355
+ if (ctx.approvalRouting === void 0 || testFile === void 0 || declaration === void 0) {
356
+ return null;
357
+ }
358
+ const targets = resolveBaselineTargets({
359
+ testFile,
360
+ reporterTitlePath: reporterTitlePath(test),
361
+ declarations: [declaration],
362
+ config: {
363
+ configDir: ctx.approvalRouting.configDir,
364
+ testDir: ctx.approvalRouting.playwrightTestDir ?? dirname2(testFile),
365
+ snapshotDir: ctx.approvalRouting.playwrightSnapshotDir ?? dirname2(testFile),
366
+ projectName: test.browser,
367
+ snapshotSuffix: process.platform,
368
+ snapshotPathTemplate: ctx.approvalRouting.playwrightSnapshotPathTemplate,
369
+ toHaveScreenshotPathTemplate: ctx.approvalRouting.playwrightToHaveScreenshotPathTemplate
370
+ },
371
+ snapshotPathExists: existsSync
372
+ });
373
+ return targets.length === 1 ? targets[0]?.snapshotPath ?? null : null;
374
+ }
234
375
  async function handleApiApprove(ctx, req) {
235
376
  try {
236
377
  const rawBody = await req.json();
@@ -241,61 +382,96 @@ async function handleApiApprove(ctx, req) {
241
382
  }
242
383
  const { id, retry, image } = parsed;
243
384
  const test = ctx.reportData.tests[id];
244
- if (test !== null && test !== void 0) {
245
- test.approved ??= {};
246
- test.approved[image] = retry;
385
+ if (test === void 0) {
386
+ return Response.json({ success: false, error: "Test not found" }, { status: 404 });
387
+ }
388
+ const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
389
+ if (actualUrl === void 0) {
390
+ return Response.json({ success: false, error: "Actual image not found" }, { status: 409 });
391
+ }
392
+ const snapshotPath = resolveApprovalTarget(ctx, test, retry, image);
393
+ if (snapshotPath === null) {
394
+ return Response.json({ success: false, error: APPROVAL_TARGET_ERROR }, { status: 409 });
395
+ }
396
+ try {
397
+ await copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath);
398
+ test.approved = { ...test.approved ?? {}, [image]: retry };
247
399
  await ctx.saveReport();
248
- const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
249
- if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
250
- const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
251
- const snapshotPath = `${test.location.file}-snapshots/${image}-${test.browser}-${process.platform}.png`;
252
- try {
253
- await copyFilePortable(actualPath, snapshotPath);
254
- console.log(` \u2714 Updated baseline: ${snapshotPath}`);
255
- } catch (err) {
256
- const errorMsg = err instanceof Error ? err.message : String(err);
257
- console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
258
- }
259
- }
400
+ console.log(` \u2714 Updated baseline: ${snapshotPath}`);
260
401
  console.log(` \u2714 Approved [${test.browser}] ${test.title} \u2014 ${image}`);
402
+ return Response.json({ success: true });
403
+ } catch (err) {
404
+ const errorMsg = err instanceof Error ? err.message : String(err);
405
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
406
+ return Response.json({ success: false, error: "Failed to update baseline" }, { status: 500 });
261
407
  }
262
- return Response.json({ success: true });
263
408
  } catch {
264
409
  return Response.json({ success: false, error: "Invalid request" }, { status: 400 });
265
410
  }
266
411
  }
267
- async function handleApiApproveAll(ctx) {
268
- let approvedCount = 0;
269
- const baselineUpdates = [];
270
- Object.values(ctx.reportData.tests).forEach((test) => {
271
- if (!test?.results) return;
272
- const approved = {};
412
+ function createBulkApprovalUpdates(ctx) {
413
+ return Object.values(ctx.reportData.tests).flatMap((test) => {
414
+ if (!test.results || test.results.length === 0) {
415
+ return [];
416
+ }
273
417
  const lastRetry = test.results.length - 1;
274
418
  const lastResult = test.results[lastRetry];
275
- if (!lastResult?.images) return;
276
- Object.keys(lastResult.images).forEach((imageName) => {
277
- approved[imageName] = lastRetry;
278
- approvedCount++;
419
+ if (!lastResult?.images) {
420
+ return [];
421
+ }
422
+ return Object.keys(lastResult.images).flatMap((imageName) => {
279
423
  const actualUrl = lastResult.images?.[imageName]?.actual;
280
- if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
281
- const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
282
- const snapshotPath = `${test.location.file}-snapshots/${imageName}-${test.browser}-${process.platform}.png`;
283
- baselineUpdates.push(
284
- copyFilePortable(actualPath, snapshotPath).then(() => {
285
- console.log(` \u2714 Updated baseline: ${snapshotPath}`);
286
- }).catch((err) => {
287
- const errorMsg = err instanceof Error ? err.message : String(err);
288
- console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
289
- })
290
- );
424
+ if (actualUrl === void 0) {
425
+ return [Promise.resolve({ kind: "unresolved" })];
426
+ }
427
+ const snapshotPath = resolveApprovalTarget(ctx, test, lastRetry, imageName);
428
+ if (snapshotPath === null) {
429
+ return [Promise.resolve({ kind: "unresolved" })];
291
430
  }
431
+ return [
432
+ copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath).then(
433
+ () => ({
434
+ kind: "approved",
435
+ imageName,
436
+ retry: lastRetry,
437
+ snapshotPath,
438
+ test
439
+ })
440
+ ).catch((err) => {
441
+ const errorMsg = err instanceof Error ? err.message : String(err);
442
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
443
+ return { kind: "failed" };
444
+ })
445
+ ];
292
446
  });
293
- test.approved = approved;
294
447
  });
448
+ }
449
+ function summarizeBulkApprovalOutcomes(outcomes) {
450
+ return outcomes.reduce(
451
+ (summary, outcome) => {
452
+ switch (outcome.kind) {
453
+ case "approved": {
454
+ outcome.test.approved = { ...outcome.test.approved ?? {}, [outcome.imageName]: outcome.retry };
455
+ console.log(` \u2714 Updated baseline: ${outcome.snapshotPath}`);
456
+ return { ...summary, approved: summary.approved + 1 };
457
+ }
458
+ case "unresolved":
459
+ return { ...summary, unresolved: summary.unresolved + 1 };
460
+ case "failed":
461
+ return { ...summary, failed: summary.failed + 1 };
462
+ }
463
+ },
464
+ { approved: 0, unresolved: 0, failed: 0 }
465
+ );
466
+ }
467
+ async function handleApiApproveAll(ctx) {
468
+ const outcomes = await Promise.all(createBulkApprovalUpdates(ctx));
469
+ const counts = summarizeBulkApprovalOutcomes(outcomes);
295
470
  await ctx.saveReport();
296
- await Promise.all(baselineUpdates);
297
- console.log(` \u2714 Approved all \u2014 ${approvedCount} image(s)`);
298
- return Response.json({ success: true });
471
+ console.log(
472
+ ` \u2714 Approved all \u2014 approved: ${counts.approved}, unresolved: ${counts.unresolved}, failed: ${counts.failed}`
473
+ );
474
+ return Response.json({ success: counts.failed === 0, ...counts });
299
475
  }
300
476
  async function handleApiImages(req) {
301
477
  const path = new URL(req.url).pathname.slice("/api/images/".length);
@@ -311,7 +487,7 @@ async function handleScreenshots(ctx, req) {
311
487
  }
312
488
  async function handleDist(ctx, req) {
313
489
  const path = new URL(req.url).pathname.slice("/dist/".length);
314
- const filePath = join2(ctx.staticDir, path);
490
+ const filePath = join3(ctx.staticDir, path);
315
491
  const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
316
492
  const file = await respondWithFile(filePath, contentType);
317
493
  return file ?? new Response("Not Found", { status: 404 });
@@ -330,10 +506,10 @@ function handleHttpRequest(ctx, req) {
330
506
  if (pathname === "/api/report") {
331
507
  return Promise.resolve(handleApiReport(ctx));
332
508
  }
333
- if (pathname === "/api/approve") {
509
+ if (pathname === "/api/approve" && req.method === "POST") {
334
510
  return handleApiApprove(ctx, req);
335
511
  }
336
- if (pathname === "/api/approve-all") {
512
+ if (pathname === "/api/approve-all" && req.method === "POST") {
337
513
  return handleApiApproveAll(ctx);
338
514
  }
339
515
  if (pathname.startsWith("/api/images/")) {
@@ -414,6 +590,10 @@ async function loadOfflineReports(offlineReportDir, reportData) {
414
590
  screenshotsBaseUrl: "/screenshots/"
415
591
  });
416
592
  }
593
+ function resetReloadableReportData(reportData) {
594
+ reportData.tests = {};
595
+ reportData.isUpdateMode = false;
596
+ }
417
597
  async function handleParsedWebSocketMessage(ctx, msg) {
418
598
  switch (msg.type) {
419
599
  case "test-begin": {
@@ -462,19 +642,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
462
642
  };
463
643
  }
464
644
  async function resolveStaticDir(staticDir) {
465
- const currentDir = dirname2(fileURLToPath(import.meta.url));
645
+ const currentDir = dirname3(fileURLToPath(import.meta.url));
466
646
  const candidates = staticDir === void 0 ? [
467
647
  currentDir,
468
- join3(currentDir, "dist"),
469
- join3(currentDir, "..", "dist"),
470
- join3(currentDir, "..", "..", "dist"),
471
- join3(currentDir, ".."),
472
- join3(currentDir, "..", "..")
473
- ] : [staticDir, join3(staticDir, "dist")];
648
+ join4(currentDir, "dist"),
649
+ join4(currentDir, "..", "dist"),
650
+ join4(currentDir, "..", "..", "dist"),
651
+ join4(currentDir, ".."),
652
+ join4(currentDir, "..", "..")
653
+ ] : [staticDir, join4(staticDir, "dist")];
474
654
  const resolvedCandidates = await Promise.all(
475
655
  candidates.map(async (candidate) => ({
476
656
  candidate,
477
- exists: await fileExists(join3(candidate, "index.html"))
657
+ exists: await fileExists(join4(candidate, "index.html"))
478
658
  }))
479
659
  );
480
660
  const resolved = resolvedCandidates.find(({ exists }) => exists);
@@ -485,9 +665,23 @@ async function resolveStaticDir(staticDir) {
485
665
  }
486
666
  async function resolveReportPath(reportPath) {
487
667
  if (await isDirectory(reportPath)) {
488
- return { reportFile: join3(reportPath, "report.json"), offlineReportDir: reportPath };
668
+ return { reportFile: join4(reportPath, "report.json"), offlineReportDir: reportPath };
489
669
  }
490
- return { reportFile: reportPath, offlineReportDir: dirname2(reportPath) };
670
+ return { reportFile: reportPath, offlineReportDir: dirname3(reportPath) };
671
+ }
672
+ function createRoutesContext(reportData, staticDir, saveReport, options) {
673
+ return {
674
+ reportData,
675
+ staticDir,
676
+ saveReport,
677
+ approvalRouting: {
678
+ configDir: options.configDir ?? process.cwd(),
679
+ playwrightTestDir: options.playwrightTestDir,
680
+ playwrightSnapshotDir: options.playwrightSnapshotDir,
681
+ playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
682
+ playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
683
+ }
684
+ };
491
685
  }
492
686
  async function createServerApp(options = {}) {
493
687
  const port = options.port ?? 3e3;
@@ -500,19 +694,26 @@ async function createServerApp(options = {}) {
500
694
  async function saveReport() {
501
695
  await writeJsonFile(reportFile, reportData);
502
696
  }
503
- const routesContext = {
504
- reportData,
505
- staticDir,
506
- saveReport
507
- };
697
+ const routesContext = createRoutesContext(reportData, staticDir, saveReport, options);
508
698
  const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
509
699
  const handleRequest = (req) => handleHttpRequest(routesContext, req);
510
700
  const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
511
- await loadReport(reportFile, reportData);
512
- await loadOfflineReports(offlineReportDir, reportData);
701
+ const reloadFromDisk = async () => {
702
+ resetReloadableReportData(reportData);
703
+ await loadReport(reportFile, reportData);
704
+ await loadOfflineReports(offlineReportDir, reportData);
705
+ broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
706
+ };
707
+ await reloadFromDisk();
708
+ const close = await watchReportArtifacts({
709
+ offlineReportDir,
710
+ screenshotDir: reportData.screenshotDir,
711
+ scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
712
+ });
513
713
  return {
514
714
  port,
515
715
  wsClients,
716
+ close,
516
717
  handleRequest,
517
718
  handleWebSocketMessage
518
719
  };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startServer
4
- } from "./chunk-BRFA7JCQ.js";
5
- import "./chunk-IEYNAB6M.js";
4
+ } from "./chunk-OMDYTNWY.js";
5
+ import "./chunk-JGS2VWQP.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { join } from "path";
package/dist/index.css CHANGED
@@ -23,6 +23,8 @@
23
23
  --color-purple-800: oklch(43.8% 0.218 303.724);
24
24
  --color-white: #fff;
25
25
  --spacing: 0.25rem;
26
+ --container-xl: 36rem;
27
+ --container-2xl: 42rem;
26
28
  --text-xs: 0.75rem;
27
29
  --text-xs--line-height: calc(1 / 0.75);
28
30
  --text-sm: 0.875rem;
@@ -33,6 +35,7 @@
33
35
  --font-weight-medium: 500;
34
36
  --font-weight-semibold: 600;
35
37
  --font-weight-bold: 700;
38
+ --tracking-wide: 0.025em;
36
39
  --tracking-wider: 0.05em;
37
40
  --radius-sm: 0.25rem;
38
41
  --radius-md: 0.375rem;
@@ -213,6 +216,9 @@
213
216
  .pointer-events-none {
214
217
  pointer-events: none;
215
218
  }
219
+ .visible {
220
+ visibility: visible;
221
+ }
216
222
  .absolute {
217
223
  position: absolute;
218
224
  }
@@ -285,6 +291,9 @@
285
291
  .mt-2 {
286
292
  margin-top: calc(var(--spacing) * 2);
287
293
  }
294
+ .mt-3 {
295
+ margin-top: calc(var(--spacing) * 3);
296
+ }
288
297
  .mr-3 {
289
298
  margin-right: calc(var(--spacing) * 3);
290
299
  }
@@ -306,9 +315,18 @@
306
315
  .grid {
307
316
  display: grid;
308
317
  }
318
+ .hidden {
319
+ display: none;
320
+ }
321
+ .inline {
322
+ display: inline;
323
+ }
309
324
  .inline-block {
310
325
  display: inline-block;
311
326
  }
327
+ .table {
328
+ display: table;
329
+ }
312
330
  .size-2 {
313
331
  width: calc(var(--spacing) * 2);
314
332
  height: calc(var(--spacing) * 2);
@@ -373,9 +391,15 @@
373
391
  .w-full {
374
392
  width: 100%;
375
393
  }
394
+ .max-w-2xl {
395
+ max-width: var(--container-2xl);
396
+ }
376
397
  .max-w-full {
377
398
  max-width: 100%;
378
399
  }
400
+ .max-w-xl {
401
+ max-width: var(--container-xl);
402
+ }
379
403
  .min-w-0 {
380
404
  min-width: calc(var(--spacing) * 0);
381
405
  }
@@ -517,6 +541,12 @@
517
541
  .border-edge {
518
542
  border-color: var(--color-edge);
519
543
  }
544
+ .border-edge\/70 {
545
+ border-color: var(--color-edge);
546
+ @supports (color: color-mix(in lab, red, red)) {
547
+ border-color: color-mix(in oklab, var(--color-edge) 70%, transparent);
548
+ }
549
+ }
520
550
  .border-error {
521
551
  border-color: var(--color-error);
522
552
  }
@@ -529,6 +559,12 @@
529
559
  border-color: color-mix(in oklab, var(--color-green-500) 60%, transparent);
530
560
  }
531
561
  }
562
+ .border-info\/40 {
563
+ border-color: var(--color-info);
564
+ @supports (color: color-mix(in lab, red, red)) {
565
+ border-color: color-mix(in oklab, var(--color-info) 40%, transparent);
566
+ }
567
+ }
532
568
  .border-purple-500\/60 {
533
569
  border-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 60%, transparent);
534
570
  @supports (color: color-mix(in lab, red, red)) {
@@ -583,6 +619,12 @@
583
619
  .bg-info {
584
620
  background-color: var(--color-info);
585
621
  }
622
+ .bg-info\/10 {
623
+ background-color: var(--color-info);
624
+ @supports (color: color-mix(in lab, red, red)) {
625
+ background-color: color-mix(in oklab, var(--color-info) 10%, transparent);
626
+ }
627
+ }
586
628
  .bg-purple-500\/25 {
587
629
  background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent);
588
630
  @supports (color: color-mix(in lab, red, red)) {
@@ -698,6 +740,10 @@
698
740
  .text-\[9px\] {
699
741
  font-size: 9px;
700
742
  }
743
+ .leading-6 {
744
+ --tw-leading: calc(var(--spacing) * 6);
745
+ line-height: calc(var(--spacing) * 6);
746
+ }
701
747
  .font-bold {
702
748
  --tw-font-weight: var(--font-weight-bold);
703
749
  font-weight: var(--font-weight-bold);
@@ -714,6 +760,10 @@
714
760
  --tw-font-weight: var(--font-weight-semibold);
715
761
  font-weight: var(--font-weight-semibold);
716
762
  }
763
+ .tracking-wide {
764
+ --tw-tracking: var(--tracking-wide);
765
+ letter-spacing: var(--tracking-wide);
766
+ }
717
767
  .tracking-wider {
718
768
  --tw-tracking: var(--tracking-wider);
719
769
  letter-spacing: var(--tracking-wider);
@@ -1128,6 +1178,10 @@
1128
1178
  inherits: false;
1129
1179
  initial-value: solid;
1130
1180
  }
1181
+ @property --tw-leading {
1182
+ syntax: "*";
1183
+ inherits: false;
1184
+ }
1131
1185
  @property --tw-font-weight {
1132
1186
  syntax: "*";
1133
1187
  inherits: false;
@@ -1295,6 +1349,7 @@
1295
1349
  --tw-translate-y: 0;
1296
1350
  --tw-translate-z: 0;
1297
1351
  --tw-border-style: solid;
1352
+ --tw-leading: initial;
1298
1353
  --tw-font-weight: initial;
1299
1354
  --tw-tracking: initial;
1300
1355
  --tw-ordinal: initial;