@crvy/rprtr 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.5] - 2026-05-14
9
+
10
+ ### Added
11
+
12
+ - Classify passed visual assertions in report state
13
+ - Emit declared visual names for screenshot steps
14
+ - Refresh report state from disk changes
15
+ - Label passed visual fallback states in ui
16
+
17
+ ### Documentation
18
+
19
+ - Describe passed screenshot fallback modes
20
+
21
+ ### Fixed
22
+
23
+ - **ci:** Checkout hooks submodule
24
+
25
+ ### Miscellaneous
26
+
27
+ - Add you-lint-not-pass hook integration
28
+ - Exclude hooks repo from root checks
8
29
  ## [0.0.4] - 2026-04-23
9
30
 
10
31
  ### Added
package/README.md CHANGED
@@ -93,6 +93,15 @@ When the server isn't running during tests, the reporter automatically falls bac
93
93
  - On test completion, a self-contained `crvy-rprtr.html` is written for direct browser review
94
94
  - When the server starts, it loads and merges all `crvy-rprtr-*.json` files from the offline report directory
95
95
 
96
+ ## Passed Screenshot Modes
97
+
98
+ Crvy Rprtr keeps passed Playwright screenshot assertions visible in two fallback modes when Playwright does not emit a full passing comparison payload:
99
+
100
+ - `baseline-only`: Crvy Rprtr copied the expected snapshot into the screenshot directory, so the UI can still show the stored baseline.
101
+ - `declared-only`: the screenshot assertion was detected, but Playwright did not emit a passed artifact and no snapshot file could be resolved.
102
+
103
+ When the server is running, Crvy Rprtr also refreshes the UI after report JSON or screenshot artifacts change on disk.
104
+
96
105
  ## Programmatic API
97
106
 
98
107
  ```ts
@@ -10,10 +10,10 @@ import {
10
10
  createMutableReportState,
11
11
  finalizeRunEvent,
12
12
  safeParse
13
- } from "./chunk-IEYNAB6M.js";
13
+ } from "./chunk-MWLM3HWS.js";
14
14
 
15
15
  // src/server/app.ts
16
- import { dirname as dirname2, join as join3 } from "path";
16
+ import { dirname as dirname2, join as join4 } from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  import pLimit from "p-limit";
19
19
 
@@ -207,14 +207,122 @@ function handleSync(ctx) {
207
207
  broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
208
208
  }
209
209
 
210
- // src/server/routes.ts
210
+ // src/server/report-watch.ts
211
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
211
212
  import { join as join2 } from "path";
213
+ var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
214
+ function createDebouncedRefresh(reload, delayMs = 50) {
215
+ let timer = null;
216
+ return () => {
217
+ if (timer !== null) {
218
+ clearTimeout(timer);
219
+ }
220
+ timer = setTimeout(() => {
221
+ timer = null;
222
+ void reload();
223
+ }, delayMs);
224
+ };
225
+ }
226
+ function isFileNotFound2(error) {
227
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
228
+ }
229
+ async function describeFile(filePath, label) {
230
+ try {
231
+ const fileStats = await stat2(filePath);
232
+ return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
233
+ } catch (error) {
234
+ if (isFileNotFound2(error)) {
235
+ return null;
236
+ }
237
+ throw error;
238
+ }
239
+ }
240
+ async function createOfflineFingerprint(offlineReportDir) {
241
+ if (!await isDirectory(offlineReportDir)) {
242
+ return "offline:missing";
243
+ }
244
+ try {
245
+ const entries = await readdir2(offlineReportDir, { withFileTypes: true });
246
+ const relevantEntries = entries.filter(
247
+ (entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
248
+ ).sort((left, right) => left.name.localeCompare(right.name));
249
+ const parts = await Promise.all(
250
+ relevantEntries.map((entry) => describeFile(join2(offlineReportDir, entry.name), entry.name))
251
+ );
252
+ return `offline:${parts.filter((part) => part !== null).join("|")}`;
253
+ } catch (error) {
254
+ const errorMsg = error instanceof Error ? error.message : String(error);
255
+ console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
256
+ return "offline:error";
257
+ }
258
+ }
259
+ async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
260
+ const directoryPath = relativePath === "" ? screenshotDir : join2(screenshotDir, relativePath);
261
+ if (!await isDirectory(directoryPath)) {
262
+ return relativePath === "" ? ["screenshots:missing"] : [];
263
+ }
264
+ try {
265
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
266
+ const parts = await Promise.all(
267
+ entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
268
+ const entryRelativePath = relativePath === "" ? entry.name : join2(relativePath, entry.name);
269
+ if (entry.isDirectory()) {
270
+ return createScreenshotFingerprint(screenshotDir, entryRelativePath);
271
+ }
272
+ return describeFile(join2(screenshotDir, entryRelativePath), entryRelativePath);
273
+ })
274
+ );
275
+ return parts.flat().filter((part) => part !== null);
276
+ } catch (error) {
277
+ if (isFileNotFound2(error)) {
278
+ return relativePath === "" ? ["screenshots:missing"] : [];
279
+ }
280
+ const errorMsg = error instanceof Error ? error.message : String(error);
281
+ console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
282
+ return relativePath === "" ? ["screenshots:error"] : [];
283
+ }
284
+ }
285
+ async function createArtifactsFingerprint(options) {
286
+ const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
287
+ createOfflineFingerprint(options.offlineReportDir),
288
+ createScreenshotFingerprint(options.screenshotDir)
289
+ ]);
290
+ return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
291
+ }
292
+ async function watchReportArtifacts(options) {
293
+ let fingerprint = await createArtifactsFingerprint(options);
294
+ let isPolling = false;
295
+ const interval = setInterval(() => {
296
+ if (isPolling) {
297
+ return;
298
+ }
299
+ isPolling = true;
300
+ void createArtifactsFingerprint(options).then((nextFingerprint) => {
301
+ if (nextFingerprint === fingerprint) {
302
+ return;
303
+ }
304
+ fingerprint = nextFingerprint;
305
+ options.scheduleRefresh();
306
+ }).catch((error) => {
307
+ const errorMsg = error instanceof Error ? error.message : String(error);
308
+ console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
309
+ }).finally(() => {
310
+ isPolling = false;
311
+ });
312
+ }, 100);
313
+ return () => {
314
+ clearInterval(interval);
315
+ };
316
+ }
317
+
318
+ // src/server/routes.ts
319
+ import { join as join3 } from "path";
212
320
  var LIVE_UPDATES_WEBSOCKET_PATH = "/";
213
321
  function isWebSocketUpgradeRequest(req) {
214
322
  return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
215
323
  }
216
324
  async function handleRoot(ctx) {
217
- const html = await respondWithFile(join2(ctx.staticDir, "index.html"), "text/html");
325
+ const html = await respondWithFile(join3(ctx.staticDir, "index.html"), "text/html");
218
326
  return html ?? new Response("Not Found", { status: 404 });
219
327
  }
220
328
  async function handleAppCss() {
@@ -311,7 +419,7 @@ async function handleScreenshots(ctx, req) {
311
419
  }
312
420
  async function handleDist(ctx, req) {
313
421
  const path = new URL(req.url).pathname.slice("/dist/".length);
314
- const filePath = join2(ctx.staticDir, path);
422
+ const filePath = join3(ctx.staticDir, path);
315
423
  const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
316
424
  const file = await respondWithFile(filePath, contentType);
317
425
  return file ?? new Response("Not Found", { status: 404 });
@@ -414,6 +522,10 @@ async function loadOfflineReports(offlineReportDir, reportData) {
414
522
  screenshotsBaseUrl: "/screenshots/"
415
523
  });
416
524
  }
525
+ function resetReloadableReportData(reportData) {
526
+ reportData.tests = {};
527
+ reportData.isUpdateMode = false;
528
+ }
417
529
  async function handleParsedWebSocketMessage(ctx, msg) {
418
530
  switch (msg.type) {
419
531
  case "test-begin": {
@@ -465,16 +577,16 @@ async function resolveStaticDir(staticDir) {
465
577
  const currentDir = dirname2(fileURLToPath(import.meta.url));
466
578
  const candidates = staticDir === void 0 ? [
467
579
  currentDir,
468
- join3(currentDir, "dist"),
469
- join3(currentDir, "..", "dist"),
470
- join3(currentDir, "..", "..", "dist"),
471
- join3(currentDir, ".."),
472
- join3(currentDir, "..", "..")
473
- ] : [staticDir, join3(staticDir, "dist")];
580
+ join4(currentDir, "dist"),
581
+ join4(currentDir, "..", "dist"),
582
+ join4(currentDir, "..", "..", "dist"),
583
+ join4(currentDir, ".."),
584
+ join4(currentDir, "..", "..")
585
+ ] : [staticDir, join4(staticDir, "dist")];
474
586
  const resolvedCandidates = await Promise.all(
475
587
  candidates.map(async (candidate) => ({
476
588
  candidate,
477
- exists: await fileExists(join3(candidate, "index.html"))
589
+ exists: await fileExists(join4(candidate, "index.html"))
478
590
  }))
479
591
  );
480
592
  const resolved = resolvedCandidates.find(({ exists }) => exists);
@@ -485,7 +597,7 @@ async function resolveStaticDir(staticDir) {
485
597
  }
486
598
  async function resolveReportPath(reportPath) {
487
599
  if (await isDirectory(reportPath)) {
488
- return { reportFile: join3(reportPath, "report.json"), offlineReportDir: reportPath };
600
+ return { reportFile: join4(reportPath, "report.json"), offlineReportDir: reportPath };
489
601
  }
490
602
  return { reportFile: reportPath, offlineReportDir: dirname2(reportPath) };
491
603
  }
@@ -508,11 +620,22 @@ async function createServerApp(options = {}) {
508
620
  const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
509
621
  const handleRequest = (req) => handleHttpRequest(routesContext, req);
510
622
  const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
511
- await loadReport(reportFile, reportData);
512
- await loadOfflineReports(offlineReportDir, reportData);
623
+ const reloadFromDisk = async () => {
624
+ resetReloadableReportData(reportData);
625
+ await loadReport(reportFile, reportData);
626
+ await loadOfflineReports(offlineReportDir, reportData);
627
+ broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
628
+ };
629
+ await reloadFromDisk();
630
+ const close = await watchReportArtifacts({
631
+ offlineReportDir,
632
+ screenshotDir: reportData.screenshotDir,
633
+ scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
634
+ });
513
635
  return {
514
636
  port,
515
637
  wsClients,
638
+ close,
516
639
  handleRequest,
517
640
  handleWebSocketMessage
518
641
  };
@@ -2,6 +2,30 @@
2
2
  function normalizeScreenshotsBaseUrl(baseUrl) {
3
3
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4
4
  }
5
+ function classifyImage(image) {
6
+ if (image.actual !== void 0 || image.diff !== void 0) {
7
+ return "comparison";
8
+ }
9
+ if (image.expect !== void 0) {
10
+ return "baseline-only";
11
+ }
12
+ return "declared-only";
13
+ }
14
+ function withImageSource(image) {
15
+ return {
16
+ ...image,
17
+ source: classifyImage(image)
18
+ };
19
+ }
20
+ function mergeDeclaredImages(images, visualNames) {
21
+ const names = /* @__PURE__ */ new Set([...Object.keys(images), ...visualNames]);
22
+ return Object.fromEntries(
23
+ Array.from(names, (name) => {
24
+ const current = images[name] ?? {};
25
+ return [name, withImageSource(current)];
26
+ })
27
+ );
28
+ }
5
29
  function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/") {
6
30
  const images = {};
7
31
  const baseUrl = normalizeScreenshotsBaseUrl(screenshotsBaseUrl);
@@ -12,7 +36,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
12
36
  const baseName = match[1];
13
37
  const role = match[2];
14
38
  if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
15
- images[baseName] ??= { actual: "" };
39
+ images[baseName] ??= {};
16
40
  const url = `${baseUrl}${attachment.path}`;
17
41
  const img = images[baseName];
18
42
  if (img !== null && img !== void 0) {
@@ -23,10 +47,11 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
23
47
  }
24
48
  for (const key of Object.keys(images)) {
25
49
  const img = images[key];
26
- if (img?.actual !== null && img?.actual !== void 0 && img?.expect !== null && img?.expect !== void 0 && img?.diff === void 0)
50
+ if (img?.actual !== void 0 && img?.expect !== void 0 && img?.diff === void 0) {
27
51
  delete img.expect;
52
+ }
28
53
  }
29
- return images;
54
+ return mergeDeclaredImages(images, []);
30
55
  }
31
56
  function mapStatus(status) {
32
57
  switch (status) {
@@ -42,17 +67,39 @@ function mapStatus(status) {
42
67
  }
43
68
 
44
69
  // src/report-state.ts
45
- function hasReviewablePassingImages(images) {
46
- return Object.values(images).some(
47
- (img) => img !== null && img !== void 0 && img.actual !== null && img.actual !== void 0 && img.diff === void 0
48
- );
70
+ function isCurrentArtifact(image) {
71
+ return image?.actual !== void 0 || image?.expect !== void 0 || image?.diff !== void 0;
72
+ }
73
+ function isReusablePassingImage(image) {
74
+ const source = image.source ?? classifyImage(image);
75
+ if (source === "comparison") {
76
+ return image.actual !== void 0 && image.diff === void 0;
77
+ }
78
+ return source === "baseline-only";
79
+ }
80
+ function hasReusablePassingImages(images) {
81
+ return Object.values(images).some((img) => img !== null && img !== void 0 && isReusablePassingImage(img));
49
82
  }
50
83
  function preservePreviousPassingImages(test, status, images) {
51
- if (status !== "passed" || Object.keys(images).length > 0) {
84
+ if (status !== "passed") {
52
85
  return images;
53
86
  }
54
87
  const previousImages = test.results?.[0]?.images ?? {};
55
- return hasReviewablePassingImages(previousImages) ? previousImages : images;
88
+ if (!hasReusablePassingImages(previousImages)) {
89
+ return images;
90
+ }
91
+ return Object.entries(previousImages).reduce((currentImages, [name, previousImage]) => {
92
+ if (!Object.hasOwn(currentImages, name) || previousImage === void 0 || !isReusablePassingImage(previousImage) || isCurrentArtifact(currentImages[name])) {
93
+ return currentImages;
94
+ }
95
+ return {
96
+ ...currentImages,
97
+ [name]: {
98
+ ...previousImage,
99
+ source: previousImage.source ?? classifyImage(previousImage)
100
+ }
101
+ };
102
+ }, images);
56
103
  }
57
104
  function countDiffImages(images) {
58
105
  return Object.values(images).filter((img) => img?.diff !== null && img?.diff !== void 0).length;
@@ -88,10 +135,11 @@ function applyTestEndEvent(state, data, options = {}) {
88
135
  return null;
89
136
  }
90
137
  test.status = mapStatus(data.status);
138
+ const resultStatus = data.status === "passed" ? "success" : data.status === "failed" ? "failed" : "pending";
91
139
  const images = preservePreviousPassingImages(
92
140
  test,
93
141
  data.status,
94
- attachmentsToImages(data.attachments, options.screenshotsBaseUrl)
142
+ mergeDeclaredImages(attachmentsToImages(data.attachments, options.screenshotsBaseUrl), data.visualNames)
95
143
  );
96
144
  const diffCount = countDiffImages(images);
97
145
  const hasDiffs = diffCount > 0;
@@ -100,7 +148,7 @@ function applyTestEndEvent(state, data, options = {}) {
100
148
  }
101
149
  test.results = [
102
150
  {
103
- status: data.status === "passed" ? "success" : "failed",
151
+ status: resultStatus,
104
152
  retries: 0,
105
153
  images,
106
154
  error: data.error,
@@ -132,11 +180,13 @@ var LocationSchema = z.object({
132
180
  file: z.string(),
133
181
  line: z.number()
134
182
  });
183
+ var VisualSourceSchema = z.enum(["comparison", "baseline-only", "declared-only"]);
135
184
  var ImagesSchema = z.object({
136
- actual: z.string(),
185
+ actual: z.string().optional(),
137
186
  expect: z.string().optional(),
138
187
  diff: z.string().optional(),
139
- error: z.string().optional()
188
+ error: z.string().optional(),
189
+ source: VisualSourceSchema.optional()
140
190
  });
141
191
  var AttachmentSchema = z.object({
142
192
  name: z.string(),
@@ -144,10 +194,10 @@ var AttachmentSchema = z.object({
144
194
  contentType: z.string()
145
195
  });
146
196
  var TestStatusSchema = z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
197
+ var TestResultStatusSchema = z.enum(["failed", "success", "pending"]);
147
198
  var TestResultSchema = z.object({
148
- status: z.enum(["failed", "success"]),
199
+ status: TestResultStatusSchema,
149
200
  retries: z.number(),
150
- // eslint-disable-next-line typescript/no-unsafe-type-assertion
151
201
  images: z.record(z.string(), ImagesSchema).optional(),
152
202
  error: z.string().optional(),
153
203
  duration: z.number().optional()
@@ -176,7 +226,6 @@ var CrvyRprtrSuiteSchema = z.lazy(
176
226
  opened: z.boolean(),
177
227
  checked: z.boolean(),
178
228
  indeterminate: z.boolean(),
179
- // eslint-disable-next-line typescript/no-unsafe-type-assertion
180
229
  children: z.record(z.string(), z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
181
230
  })
182
231
  );
@@ -195,6 +244,7 @@ var TestEndDataSchema = z.object({
195
244
  id: z.string(),
196
245
  status: z.enum(["passed", "failed", "skipped"]),
197
246
  attachments: z.array(AttachmentSchema),
247
+ visualNames: z.array(z.string()).default([]),
198
248
  error: z.string().optional(),
199
249
  duration: z.number().optional()
200
250
  });
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-IH44LYNM.js";
5
+ import "./chunk-MWLM3HWS.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,6 +315,12 @@
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
  }
@@ -373,9 +388,15 @@
373
388
  .w-full {
374
389
  width: 100%;
375
390
  }
391
+ .max-w-2xl {
392
+ max-width: var(--container-2xl);
393
+ }
376
394
  .max-w-full {
377
395
  max-width: 100%;
378
396
  }
397
+ .max-w-xl {
398
+ max-width: var(--container-xl);
399
+ }
379
400
  .min-w-0 {
380
401
  min-width: calc(var(--spacing) * 0);
381
402
  }
@@ -517,6 +538,12 @@
517
538
  .border-edge {
518
539
  border-color: var(--color-edge);
519
540
  }
541
+ .border-edge\/70 {
542
+ border-color: var(--color-edge);
543
+ @supports (color: color-mix(in lab, red, red)) {
544
+ border-color: color-mix(in oklab, var(--color-edge) 70%, transparent);
545
+ }
546
+ }
520
547
  .border-error {
521
548
  border-color: var(--color-error);
522
549
  }
@@ -529,6 +556,12 @@
529
556
  border-color: color-mix(in oklab, var(--color-green-500) 60%, transparent);
530
557
  }
531
558
  }
559
+ .border-info\/40 {
560
+ border-color: var(--color-info);
561
+ @supports (color: color-mix(in lab, red, red)) {
562
+ border-color: color-mix(in oklab, var(--color-info) 40%, transparent);
563
+ }
564
+ }
532
565
  .border-purple-500\/60 {
533
566
  border-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 60%, transparent);
534
567
  @supports (color: color-mix(in lab, red, red)) {
@@ -583,6 +616,12 @@
583
616
  .bg-info {
584
617
  background-color: var(--color-info);
585
618
  }
619
+ .bg-info\/10 {
620
+ background-color: var(--color-info);
621
+ @supports (color: color-mix(in lab, red, red)) {
622
+ background-color: color-mix(in oklab, var(--color-info) 10%, transparent);
623
+ }
624
+ }
586
625
  .bg-purple-500\/25 {
587
626
  background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent);
588
627
  @supports (color: color-mix(in lab, red, red)) {
@@ -698,6 +737,10 @@
698
737
  .text-\[9px\] {
699
738
  font-size: 9px;
700
739
  }
740
+ .leading-6 {
741
+ --tw-leading: calc(var(--spacing) * 6);
742
+ line-height: calc(var(--spacing) * 6);
743
+ }
701
744
  .font-bold {
702
745
  --tw-font-weight: var(--font-weight-bold);
703
746
  font-weight: var(--font-weight-bold);
@@ -714,6 +757,10 @@
714
757
  --tw-font-weight: var(--font-weight-semibold);
715
758
  font-weight: var(--font-weight-semibold);
716
759
  }
760
+ .tracking-wide {
761
+ --tw-tracking: var(--tracking-wide);
762
+ letter-spacing: var(--tracking-wide);
763
+ }
717
764
  .tracking-wider {
718
765
  --tw-tracking: var(--tracking-wider);
719
766
  letter-spacing: var(--tracking-wider);
@@ -1128,6 +1175,10 @@
1128
1175
  inherits: false;
1129
1176
  initial-value: solid;
1130
1177
  }
1178
+ @property --tw-leading {
1179
+ syntax: "*";
1180
+ inherits: false;
1181
+ }
1131
1182
  @property --tw-font-weight {
1132
1183
  syntax: "*";
1133
1184
  inherits: false;
@@ -1295,6 +1346,7 @@
1295
1346
  --tw-translate-y: 0;
1296
1347
  --tw-translate-z: 0;
1297
1348
  --tw-border-style: solid;
1349
+ --tw-leading: initial;
1298
1350
  --tw-font-weight: initial;
1299
1351
  --tw-tracking: initial;
1300
1352
  --tw-ordinal: initial;