tempest-react-sdk 0.26.1 → 0.28.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.
Files changed (132) hide show
  1. package/README.md +25 -1
  2. package/bin/lib/css/analyze.mjs +149 -0
  3. package/bin/lib/css/analyze.test.mjs +136 -0
  4. package/bin/lib/css/collect.mjs +152 -0
  5. package/bin/lib/css/findings.mjs +79 -0
  6. package/bin/lib/css/fix.e2e.test.mjs +99 -0
  7. package/bin/lib/css/fix.mjs +138 -0
  8. package/bin/lib/css/fix.test.mjs +108 -0
  9. package/bin/lib/css/index.mjs +16 -0
  10. package/bin/lib/css/parse.mjs +398 -0
  11. package/bin/lib/css/parse.test.mjs +146 -0
  12. package/bin/lib/css/properties.mjs +399 -0
  13. package/bin/lib/css/repetition.mjs +213 -0
  14. package/bin/lib/css/repetition.test.mjs +166 -0
  15. package/bin/lib/css/semantic.mjs +327 -0
  16. package/bin/lib/css/semantic.test.mjs +201 -0
  17. package/bin/lib/css/tokens.mjs +119 -0
  18. package/bin/lib/doctor/doctor.e2e.test.mjs +246 -0
  19. package/bin/tempest.mjs +272 -36
  20. package/dist/charts/scales.cjs +2 -0
  21. package/dist/charts/scales.cjs.map +1 -0
  22. package/dist/charts/scales.js +29 -0
  23. package/dist/charts/scales.js.map +1 -0
  24. package/dist/charts.cjs +1 -1
  25. package/dist/charts.d.ts +91 -0
  26. package/dist/charts.js +9 -8
  27. package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
  28. package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
  29. package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
  30. package/dist/components/Carousel/Carousel.module.js.map +1 -1
  31. package/dist/components/CodeBlock/CodeBlock.cjs +3 -0
  32. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -0
  33. package/dist/components/CodeBlock/CodeBlock.js +53 -0
  34. package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
  35. package/dist/components/CodeBlock/CodeBlock.module.cjs +2 -0
  36. package/dist/components/CodeBlock/CodeBlock.module.cjs.map +1 -0
  37. package/dist/components/CodeBlock/CodeBlock.module.js +29 -0
  38. package/dist/components/CodeBlock/CodeBlock.module.js.map +1 -0
  39. package/dist/components/CodeBlock/tokenize.cjs +3 -0
  40. package/dist/components/CodeBlock/tokenize.cjs.map +1 -0
  41. package/dist/components/CodeBlock/tokenize.js +267 -0
  42. package/dist/components/CodeBlock/tokenize.js.map +1 -0
  43. package/dist/components/ImageCropper/ImageCropper.cjs +2 -0
  44. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -0
  45. package/dist/components/ImageCropper/ImageCropper.js +240 -0
  46. package/dist/components/ImageCropper/ImageCropper.js.map +1 -0
  47. package/dist/components/ImageCropper/ImageCropper.module.cjs +2 -0
  48. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -0
  49. package/dist/components/ImageCropper/ImageCropper.module.js +15 -0
  50. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -0
  51. package/dist/components/ImageCropper/crop-geometry.cjs +2 -0
  52. package/dist/components/ImageCropper/crop-geometry.cjs.map +1 -0
  53. package/dist/components/ImageCropper/crop-geometry.js +52 -0
  54. package/dist/components/ImageCropper/crop-geometry.js.map +1 -0
  55. package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
  56. package/dist/components/Navbar/Navbar.module.js.map +1 -1
  57. package/dist/components/QRCode/QRCode.cjs +2 -0
  58. package/dist/components/QRCode/QRCode.cjs.map +1 -0
  59. package/dist/components/QRCode/QRCode.js +49 -0
  60. package/dist/components/QRCode/QRCode.js.map +1 -0
  61. package/dist/components/QRCode/QRCode.module.cjs +2 -0
  62. package/dist/components/QRCode/QRCode.module.cjs.map +1 -0
  63. package/dist/components/QRCode/QRCode.module.js +9 -0
  64. package/dist/components/QRCode/QRCode.module.js.map +1 -0
  65. package/dist/components/QRCode/qr-encode.cjs +2 -0
  66. package/dist/components/QRCode/qr-encode.cjs.map +1 -0
  67. package/dist/components/QRCode/qr-encode.js +295 -0
  68. package/dist/components/QRCode/qr-encode.js.map +1 -0
  69. package/dist/components/QRCode/qr-tables.cjs +2 -0
  70. package/dist/components/QRCode/qr-tables.cjs.map +1 -0
  71. package/dist/components/QRCode/qr-tables.js +366 -0
  72. package/dist/components/QRCode/qr-tables.js.map +1 -0
  73. package/dist/components/Scheduler/Scheduler.cjs +2 -0
  74. package/dist/components/Scheduler/Scheduler.cjs.map +1 -0
  75. package/dist/components/Scheduler/Scheduler.js +134 -0
  76. package/dist/components/Scheduler/Scheduler.js.map +1 -0
  77. package/dist/components/Scheduler/Scheduler.module.cjs +2 -0
  78. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -0
  79. package/dist/components/Scheduler/Scheduler.module.js +26 -0
  80. package/dist/components/Scheduler/Scheduler.module.js.map +1 -0
  81. package/dist/components/Scheduler/scheduler-layout.cjs +2 -0
  82. package/dist/components/Scheduler/scheduler-layout.cjs.map +1 -0
  83. package/dist/components/Scheduler/scheduler-layout.js +97 -0
  84. package/dist/components/Scheduler/scheduler-layout.js.map +1 -0
  85. package/dist/components/ScrollArea/ScrollArea.cjs +1 -1
  86. package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
  87. package/dist/components/ScrollArea/ScrollArea.js +22 -16
  88. package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
  89. package/dist/components/ScrollArea/ScrollArea.module.cjs.map +1 -1
  90. package/dist/components/ScrollArea/ScrollArea.module.js.map +1 -1
  91. package/dist/components/Sparkline/Sparkline.cjs +2 -0
  92. package/dist/components/Sparkline/Sparkline.cjs.map +1 -0
  93. package/dist/components/Sparkline/Sparkline.js +64 -0
  94. package/dist/components/Sparkline/Sparkline.js.map +1 -0
  95. package/dist/components/Sparkline/Sparkline.module.cjs +2 -0
  96. package/dist/components/Sparkline/Sparkline.module.cjs.map +1 -0
  97. package/dist/components/Sparkline/Sparkline.module.js +10 -0
  98. package/dist/components/Sparkline/Sparkline.module.js.map +1 -0
  99. package/dist/components/Sparkline/sparkline-geometry.cjs +2 -0
  100. package/dist/components/Sparkline/sparkline-geometry.cjs.map +1 -0
  101. package/dist/components/Sparkline/sparkline-geometry.js +56 -0
  102. package/dist/components/Sparkline/sparkline-geometry.js.map +1 -0
  103. package/dist/components/Table/Table.cjs +1 -1
  104. package/dist/components/Table/Table.cjs.map +1 -1
  105. package/dist/components/Table/Table.js +37 -30
  106. package/dist/components/Table/Table.js.map +1 -1
  107. package/dist/components/Table/Table.module.cjs.map +1 -1
  108. package/dist/components/Table/Table.module.js.map +1 -1
  109. package/dist/components/VirtualList/VirtualList.cjs +1 -1
  110. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  111. package/dist/components/VirtualList/VirtualList.js +13 -11
  112. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  113. package/dist/components/VirtualList/VirtualList.module.cjs.map +1 -1
  114. package/dist/components/VirtualList/VirtualList.module.js.map +1 -1
  115. package/dist/hooks/use-scroll-overflow.cjs +2 -0
  116. package/dist/hooks/use-scroll-overflow.cjs.map +1 -0
  117. package/dist/hooks/use-scroll-overflow.js +22 -0
  118. package/dist/hooks/use-scroll-overflow.js.map +1 -0
  119. package/dist/styles.css +1 -1
  120. package/dist/tempest-react-sdk.cjs +1 -1
  121. package/dist/tempest-react-sdk.d.ts +607 -2
  122. package/dist/tempest-react-sdk.js +197 -188
  123. package/dist/theme/create-theme.cjs +3 -3
  124. package/dist/theme/create-theme.cjs.map +1 -1
  125. package/dist/theme/create-theme.js +52 -28
  126. package/dist/theme/create-theme.js.map +1 -1
  127. package/dist/theme/data-viz-ramps.cjs +2 -0
  128. package/dist/theme/data-viz-ramps.cjs.map +1 -0
  129. package/dist/theme/data-viz-ramps.js +58 -0
  130. package/dist/theme/data-viz-ramps.js.map +1 -0
  131. package/package.json +2 -1
  132. package/template/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+
8
+ const CLI = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "tempest.mjs");
9
+
10
+ let root;
11
+
12
+ beforeEach(() => {
13
+ root = mkdtempSync(join(tmpdir(), "tempest-doctor-"));
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(root, { recursive: true, force: true });
18
+ });
19
+
20
+ /** Write a file under the fixture, creating parent directories. */
21
+ function write(rel, contents) {
22
+ const path = join(root, rel);
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, typeof contents === "string" ? contents : JSON.stringify(contents));
25
+ }
26
+
27
+ /** Fake an installed package so `installedVersion` finds it. */
28
+ function installed(name, version, extra = {}) {
29
+ write(`node_modules/${name}/package.json`, { name, version, ...extra });
30
+ }
31
+
32
+ /**
33
+ * Run the real CLI in the fixture directory.
34
+ *
35
+ * A subprocess rather than an imported function on purpose: `doctor` reads
36
+ * `process.cwd()` and reports through the exit code, and the exit code is the part
37
+ * that matters most — it decides whether a project "fails" the audit. Only running
38
+ * it the way a user does tests that.
39
+ */
40
+ function doctor() {
41
+ const result = spawnSync(process.execPath, [CLI, "doctor"], {
42
+ cwd: root,
43
+ encoding: "utf8",
44
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
45
+ });
46
+ return { code: result.status, out: `${result.stdout}${result.stderr}` };
47
+ }
48
+
49
+ /** A plain, healthy React + Vite app that has never heard of the SDK. */
50
+ function thirdPartyApp() {
51
+ write("package.json", {
52
+ name: "app-de-terceiro",
53
+ type: "module",
54
+ dependencies: { react: "^19.0.0", "react-dom": "^19.0.0" },
55
+ devDependencies: { vite: "^7.0.0", "@vitejs/plugin-react": "^5.0.0", eslint: "^9.0.0" },
56
+ });
57
+ write("tsconfig.json", {
58
+ compilerOptions: { strict: true, jsx: "react-jsx", moduleResolution: "bundler" },
59
+ });
60
+ write(
61
+ "vite.config.ts",
62
+ 'import { defineConfig } from "vite";\nexport default defineConfig({});',
63
+ );
64
+ write("src/App.tsx", "export const App = () => null;");
65
+ write("package-lock.json", "{}");
66
+ write("eslint.config.js", "export default [];");
67
+ installed("react", "19.0.0");
68
+ installed("react-dom", "19.0.0");
69
+ installed("vite", "7.0.0");
70
+ installed("@vitejs/plugin-react", "5.0.0");
71
+ installed("eslint", "9.0.0");
72
+ installed("prettier", "3.0.0");
73
+ installed(".bin/eslint", "0.0.0");
74
+ }
75
+
76
+ describe("tempest doctor — a third-party project that does not use the SDK", () => {
77
+ beforeEach(thirdPartyApp);
78
+
79
+ it("does not fail the audit just for not having the SDK", () => {
80
+ const { code, out } = doctor();
81
+ expect(out).toContain("tempest-react-sdk not installed");
82
+ expect(out).toContain("generic React/Vite health only");
83
+ expect(code).toBe(0);
84
+ });
85
+
86
+ it("never reports the missing SDK as a problem", () => {
87
+ const { out } = doctor();
88
+ expect(out).not.toMatch(/✗.*tempest-react-sdk/);
89
+ });
90
+
91
+ it("drops the SDK's own conventions from the report", () => {
92
+ const { out } = doctor();
93
+ // The `@/*` alias and `createViteConfig` are preferences, not health.
94
+ expect(out).not.toContain('tsconfig "@/*" alias');
95
+ expect(out).not.toContain("not using createViteConfig");
96
+ // The stylesheet is only *demanded* of an SDK project; the adoption hint at
97
+ // the end still mentions it, which is the point of that section.
98
+ expect(out).not.toContain('add import "tempest-react-sdk/styles.css"');
99
+ });
100
+
101
+ it("does not demand a src/main.tsx it has no reason to expect", () => {
102
+ const { out } = doctor();
103
+ expect(out).not.toContain("app entry");
104
+ });
105
+
106
+ it("explains moduleResolution without naming SDK subpaths", () => {
107
+ write("tsconfig.json", { compilerOptions: { strict: true, jsx: "react-jsx" } });
108
+ const { out } = doctor();
109
+ expect(out).toContain("moduleResolution");
110
+ expect(out).not.toContain("tempest-react-sdk/br");
111
+ expect(out).toContain("subpath exports");
112
+ });
113
+
114
+ it("closes with how to adopt, so the audit is not a dead end", () => {
115
+ const { out } = doctor();
116
+ expect(out).toContain("Adopting the SDK (optional)");
117
+ expect(out).toContain("npm i tempest-react-sdk");
118
+ expect(out).toContain("not all-or-nothing");
119
+ });
120
+ });
121
+
122
+ describe("tempest doctor — generic findings still surface", () => {
123
+ it("flags a missing lockfile", () => {
124
+ thirdPartyApp();
125
+ rmSync(join(root, "package-lock.json"));
126
+ expect(doctor().out).toContain("no lockfile");
127
+ });
128
+
129
+ it("flags a missing @vitejs/plugin-react", () => {
130
+ thirdPartyApp();
131
+ rmSync(join(root, "node_modules", "@vitejs", "plugin-react"), {
132
+ recursive: true,
133
+ force: true,
134
+ });
135
+ expect(doctor().out).toContain("@vitejs/plugin-react");
136
+ });
137
+
138
+ it("flags declared dependencies that are not installed", () => {
139
+ thirdPartyApp();
140
+ rmSync(join(root, "node_modules", "react"), { recursive: true, force: true });
141
+ expect(doctor().out).toMatch(/dependency\(ies\) not installed/);
142
+ });
143
+
144
+ it("flags env vars that Vite will not expose", () => {
145
+ thirdPartyApp();
146
+ write("src/api.ts", "export const url = import.meta.env.API_URL;");
147
+ expect(doctor().out).toContain("client env without VITE_ prefix");
148
+ });
149
+
150
+ it("still fails on a genuinely broken project", () => {
151
+ write("package.json", { name: "broken", dependencies: {} });
152
+ const { code, out } = doctor();
153
+ expect(out).toContain("react + react-dom present");
154
+ expect(code).toBe(1);
155
+ });
156
+ });
157
+
158
+ describe("tempest doctor — stylesheets", () => {
159
+ beforeEach(thirdPartyApp);
160
+
161
+ it("says nothing about CSS in a project with no stylesheet", () => {
162
+ expect(doctor().out).not.toContain("Stylesheets");
163
+ });
164
+
165
+ it("reports a clean sheet as clean", () => {
166
+ write("src/app.css", ".a {\n color: red;\n}\n");
167
+ const { out, code } = doctor();
168
+ expect(out).toContain("Stylesheets");
169
+ expect(out).toContain("no CSS problems found");
170
+ expect(code).toBe(0);
171
+ });
172
+
173
+ it("fails the audit on CSS the browser cannot parse", () => {
174
+ write("src/app.css", ".a {\n color: red;\n");
175
+ const { out, code } = doctor();
176
+ expect(out).toContain("src/app.css:1");
177
+ expect(out).toContain("never closed");
178
+ expect(code).toBe(1);
179
+ });
180
+
181
+ it("warns without failing on CSS that is valid but wrong", () => {
182
+ write("src/app.css", ".a {\n bacground-color: red;\n}\n");
183
+ const { out, code } = doctor();
184
+ expect(out).toContain("did you mean `background-color`");
185
+ expect(code).toBe(0);
186
+ });
187
+
188
+ it("points at the fix command when something is auto-fixable", () => {
189
+ write("src/app.css", ".a {\n color: red;\n color: red;\n}\n");
190
+ const { out } = doctor();
191
+ expect(out).toMatch(/finding\(s\) are auto-fixable/);
192
+ });
193
+
194
+ it("suggests one global class over repeated local copies", () => {
195
+ const row = ".row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n";
196
+ write("src/A.module.css", row);
197
+ write("src/B.module.css", row);
198
+ write("src/C.module.css", row);
199
+ expect(doctor().out).toContain("one global class beats");
200
+ });
201
+ });
202
+
203
+ describe("tempest doctor — a project that does use the SDK", () => {
204
+ beforeEach(() => {
205
+ thirdPartyApp();
206
+ write("package.json", {
207
+ name: "sdk-app",
208
+ type: "module",
209
+ dependencies: {
210
+ "tempest-react-sdk": "^0.26.1",
211
+ react: "^19.0.0",
212
+ "react-dom": "^19.0.0",
213
+ },
214
+ });
215
+ installed("tempest-react-sdk", "0.26.1", { dependencies: { "lucide-react": "^1.26.0" } });
216
+ installed("lucide-react", "1.26.0");
217
+ });
218
+
219
+ it("checks the SDK's conventions again", () => {
220
+ write("tsconfig.json", {
221
+ compilerOptions: { strict: true, jsx: "react-jsx", moduleResolution: "bundler" },
222
+ });
223
+ const { out } = doctor();
224
+ expect(out).toContain('tsconfig "@/*" alias');
225
+ });
226
+
227
+ it("asks for the stylesheet import, which is a real defect without it", () => {
228
+ write("src/main.tsx", "export const main = 1;");
229
+ expect(doctor().out).toContain("styles.css");
230
+ });
231
+
232
+ it("omits the adoption hint — it has already adopted", () => {
233
+ expect(doctor().out).not.toContain("Adopting the SDK (optional)");
234
+ });
235
+
236
+ it("warns when the SDK is installed but undeclared", () => {
237
+ write("package.json", {
238
+ name: "sdk-app",
239
+ dependencies: { react: "^19.0.0", "react-dom": "^19.0.0" },
240
+ });
241
+ const { out, code } = doctor();
242
+ expect(out).toContain("tempest-react-sdk not in dependencies");
243
+ // Undeclared-but-present is a warning, not a blocking failure.
244
+ expect(code).toBe(0);
245
+ });
246
+ });
package/bin/tempest.mjs CHANGED
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { aliasImports } from "./lib/alias/index.mjs";
15
15
  import { loadTypeScript } from "./lib/alias/typescript.mjs";
16
16
  import { readTsconfig } from "./lib/alias/tsconfig.mjs";
17
+ import { analyzeCss, applyCssFixes } from "./lib/css/index.mjs";
17
18
  import { checkLucide } from "./lib/doctor/lucide.mjs";
18
19
  import { generateRegistry, loadIconTables } from "./lib/icons/generate.mjs";
19
20
  import { generate } from "./lib/openapi/generate.mjs";
@@ -250,6 +251,74 @@ const STATEFUL_DEPS = [
250
251
  "react-router-dom",
251
252
  ];
252
253
 
254
+ /**
255
+ * Doctor rows for the CSS analysis.
256
+ *
257
+ * Findings are capped per severity because a report nobody reads to the end is a
258
+ * report that hides its own first line. The cap is stated, and the pointer to the
259
+ * uncapped list (`tempest fix --dry-run`) goes with it — a silent truncation
260
+ * would read as "that is all of them".
261
+ *
262
+ * @param {number} [limit] - Findings shown per severity.
263
+ * @returns {Array<[status: string, label: string, detail?: string]>} Check rows.
264
+ */
265
+ function cssChecks(limit = 6) {
266
+ let analysis;
267
+ try {
268
+ analysis = analyzeCss({ root: ROOT, targets: ["."], selfDir: SELF_DIR });
269
+ } catch (err) {
270
+ return [["warn", "CSS analysis failed", String(err?.message ?? err)]];
271
+ }
272
+ if (analysis.stats.files === 0) return [];
273
+
274
+ const rows = [["section", "Stylesheets"]];
275
+ rows.push([
276
+ "info",
277
+ `${analysis.stats.files} stylesheet(s) · ${analysis.stats.rules} rules · ${analysis.stats.declarations} declarations`,
278
+ analysis.tokens.source ? "" : "SDK token table not found — token checks skipped",
279
+ ]);
280
+ if (analysis.truncated) {
281
+ rows.push([
282
+ "info",
283
+ "file cap reached",
284
+ "only the first 600 stylesheets were analyzed — narrow the run with `tempest fix <path> --dry-run`",
285
+ ]);
286
+ }
287
+
288
+ const status = { error: "fail", warn: "warn", info: "info" };
289
+ let hidden = 0;
290
+ for (const severity of ["error", "warn", "info"]) {
291
+ const group = analysis.findings.filter((f) => f.severity === severity);
292
+ for (const f of group.slice(0, limit)) {
293
+ rows.push([status[severity], `${f.file}:${f.line}`, f.message]);
294
+ }
295
+ hidden += Math.max(0, group.length - limit);
296
+ }
297
+ if (hidden > 0) {
298
+ rows.push([
299
+ "info",
300
+ `${hidden} more CSS finding(s) not shown`,
301
+ "run `tempest fix --dry-run` for the full list",
302
+ ]);
303
+ }
304
+ if (analysis.findings.length === 0) {
305
+ rows.push(["ok", "no CSS problems found"]);
306
+ } else {
307
+ const fixable = analysis.findings.filter((f) => f.fixable).length;
308
+ if (fixable > 0) {
309
+ rows.push([
310
+ "info",
311
+ `${fixable} finding(s) are auto-fixable`,
312
+ "run `tempest fix` — it removes only what is provably dead",
313
+ ]);
314
+ }
315
+ }
316
+ for (const skip of analysis.skipped.slice(0, 3)) {
317
+ rows.push(["info", `skipped ${skip.file}`, skip.reason]);
318
+ }
319
+ return rows;
320
+ }
321
+
253
322
  function doctor() {
254
323
  const checks = [];
255
324
  const pkg = readJSON(join(ROOT, "package.json"));
@@ -312,16 +381,45 @@ function doctor() {
312
381
  checks.push(["section", "Project"]);
313
382
  checks.push(["ok", "package.json found"]);
314
383
  const sdkInstalled = installedVersion("tempest-react-sdk");
315
- checks.push(
316
- deps["tempest-react-sdk"]
317
- ? ["ok", "tempest-react-sdk in dependencies", deps["tempest-react-sdk"]]
318
- : ["fail", "tempest-react-sdk in dependencies", "npm install tempest-react-sdk"],
319
- );
320
- checks.push(
321
- sdkInstalled
322
- ? ["ok", "tempest-react-sdk installed", `v${sdkInstalled}`]
323
- : ["fail", "tempest-react-sdk installed", "run npm install"],
324
- );
384
+
385
+ /**
386
+ * Whether this project has adopted the SDK.
387
+ *
388
+ * When it has not, `doctor` runs in **generic mode**: the checks that only make
389
+ * sense once you use the SDK (its `@/*` alias, `createViteConfig`, the
390
+ * `styles.css` import, its optional peers) drop out, and not having the SDK is
391
+ * reported as information rather than as a defect.
392
+ *
393
+ * Without this the tool is useless on the exact project it should help most —
394
+ * somebody's existing app, being evaluated. It reported two hard failures for the
395
+ * single fact "you have not installed this yet", exited non-zero, and buried the
396
+ * findings that *were* actionable (no lockfile, no plugin-react, no linter) among
397
+ * warnings that were really just the SDK's own conventions.
398
+ */
399
+ const usesSdk = Boolean(deps["tempest-react-sdk"] || sdkInstalled);
400
+
401
+ if (usesSdk) {
402
+ checks.push(
403
+ deps["tempest-react-sdk"]
404
+ ? ["ok", "tempest-react-sdk in dependencies", deps["tempest-react-sdk"]]
405
+ : [
406
+ "warn",
407
+ "tempest-react-sdk not in dependencies",
408
+ `installed as v${sdkInstalled} but undeclared — add it to package.json`,
409
+ ],
410
+ );
411
+ checks.push(
412
+ sdkInstalled
413
+ ? ["ok", "tempest-react-sdk installed", `v${sdkInstalled}`]
414
+ : ["fail", "tempest-react-sdk installed", "run npm install"],
415
+ );
416
+ } else {
417
+ checks.push([
418
+ "info",
419
+ "tempest-react-sdk not installed",
420
+ "checking generic React/Vite health only — `npm i tempest-react-sdk` to adopt it",
421
+ ]);
422
+ }
325
423
  const reactV = installedVersion("react");
326
424
  checks.push(
327
425
  deps.react && deps["react-dom"]
@@ -438,11 +536,15 @@ function doctor() {
438
536
  if (tsc) {
439
537
  checks.push(["section", "TypeScript"]);
440
538
  const co = tsc.compilerOptions;
441
- checks.push(
442
- co.paths?.["@/*"]
443
- ? ["ok", 'tsconfig "@/*" alias']
444
- : ["warn", 'tsconfig "@/*" alias', 'add "paths": { "@/*": ["./src/*"] }'],
445
- );
539
+ // The `@/*` alias is the SDK's convention, not a health property: a project
540
+ // that has not adopted the SDK is not wrong for lacking it.
541
+ if (usesSdk) {
542
+ checks.push(
543
+ co.paths?.["@/*"]
544
+ ? ["ok", 'tsconfig "@/*" alias']
545
+ : ["warn", 'tsconfig "@/*" alias', 'add "paths": { "@/*": ["./src/*"] }'],
546
+ );
547
+ }
446
548
  const mr = String(co.moduleResolution ?? "").toLowerCase();
447
549
  checks.push(
448
550
  ["bundler", "node16", "nodenext"].includes(mr)
@@ -450,7 +552,9 @@ function doctor() {
450
552
  : [
451
553
  "warn",
452
554
  `moduleResolution: ${co.moduleResolution ?? "(unset)"}`,
453
- 'use "bundler" — otherwise subpath types (tempest-react-sdk/br, /charts…) won\'t resolve',
555
+ usesSdk
556
+ ? 'use "bundler" — otherwise subpath types (tempest-react-sdk/br, /charts…) won\'t resolve'
557
+ : 'use "bundler" — a bundled app needs it for any package that ships subpath exports',
454
558
  ],
455
559
  );
456
560
  checks.push(
@@ -491,11 +595,21 @@ function doctor() {
491
595
  if (!viteCfg) {
492
596
  checks.push(["warn", "vite config", "no vite.config.* found"]);
493
597
  } else {
494
- checks.push(
495
- fileIncludes(join(ROOT, viteCfg), "createViteConfig")
496
- ? ["ok", `${viteCfg} uses createViteConfig`]
497
- : ["warn", `${viteCfg}`, "not using createViteConfig from tempest-react-sdk/vite"],
498
- );
598
+ // `createViteConfig` is a convenience, not a requirement — only worth
599
+ // mentioning to a project that already uses the SDK, and even then as info.
600
+ if (usesSdk) {
601
+ checks.push(
602
+ fileIncludes(join(ROOT, viteCfg), "createViteConfig")
603
+ ? ["ok", `${viteCfg} uses createViteConfig`]
604
+ : [
605
+ "info",
606
+ `${viteCfg}`,
607
+ "not using createViteConfig from tempest-react-sdk/vite — optional",
608
+ ],
609
+ );
610
+ } else {
611
+ checks.push(["ok", `${viteCfg} found`]);
612
+ }
499
613
  // React plugin is required for JSX/Fast Refresh in a Vite React app.
500
614
  checks.push(
501
615
  installedVersion("@vitejs/plugin-react")
@@ -508,14 +622,23 @@ function doctor() {
508
622
  );
509
623
  }
510
624
  const entry = firstExisting(["src/main.tsx", "src/main.ts", "src/index.tsx", "src/index.ts"]);
511
- if (entry) {
512
- checks.push(
513
- fileIncludes(join(ROOT, entry), "tempest-react-sdk/styles.css")
514
- ? ["ok", `${entry} imports styles.css`]
515
- : ["warn", `${entry}`, 'add import "tempest-react-sdk/styles.css"'],
516
- );
517
- } else {
518
- checks.push(["warn", "app entry", "no src/main.tsx found"]);
625
+ if (usesSdk) {
626
+ // Without the stylesheet every component renders unstyled, so for an SDK
627
+ // project this is a real defect. For anyone else the entry's filename is
628
+ // their business, not ours.
629
+ if (entry) {
630
+ checks.push(
631
+ fileIncludes(join(ROOT, entry), "tempest-react-sdk/styles.css")
632
+ ? ["ok", `${entry} imports styles.css`]
633
+ : ["warn", `${entry}`, 'add import "tempest-react-sdk/styles.css"'],
634
+ );
635
+ } else {
636
+ checks.push([
637
+ "warn",
638
+ "app entry",
639
+ "none of src/main.tsx, src/main.ts, src/index.tsx, src/index.ts found — cannot verify the styles.css import",
640
+ ]);
641
+ }
519
642
  }
520
643
  // styles.css should be imported once — duplicates re-inject the whole sheet.
521
644
  const stylesImports = (src.match(/tempest-react-sdk\/styles\.css/g) ?? []).length;
@@ -527,6 +650,9 @@ function doctor() {
527
650
  ]);
528
651
  }
529
652
 
653
+ // ── Stylesheets ───────────────────────────────────────────────────────────
654
+ checks.push(...cssChecks());
655
+
530
656
  // ── Tooling ────────────────────────────────────────────────────────────────
531
657
  checks.push(["section", "Tooling"]);
532
658
  checks.push(
@@ -600,6 +726,26 @@ function doctor() {
600
726
  checks.push(["ok", "client env vars use the VITE_ prefix"]);
601
727
  }
602
728
 
729
+ /*
730
+ * In generic mode, close with what adoption would actually take. A tool that
731
+ * audits somebody's project and then says nothing about the next step reads as an
732
+ * ad for a product they cannot find the door to.
733
+ */
734
+ if (!usesSdk) {
735
+ checks.push(["section", "Adopting the SDK (optional)"]);
736
+ checks.push(["info", "install", "npm i tempest-react-sdk"]);
737
+ checks.push([
738
+ "info",
739
+ "import the stylesheet once, in your entry",
740
+ 'import "tempest-react-sdk/styles.css"',
741
+ ]);
742
+ checks.push([
743
+ "info",
744
+ "not all-or-nothing",
745
+ "one component at a time works — the @/* alias, createViteConfig and the scaffold are all optional",
746
+ ]);
747
+ }
748
+
603
749
  return report(checks);
604
750
  }
605
751
 
@@ -668,7 +814,7 @@ function lint(args) {
668
814
  }
669
815
 
670
816
  /** Flags `fix` owns itself; anything else is rejected rather than forwarded. */
671
- const FIX_FLAGS = new Set(["--no-alias", "--dry-run"]);
817
+ const FIX_FLAGS = new Set(["--no-alias", "--no-css", "--dry-run"]);
672
818
 
673
819
  /**
674
820
  * Print the alias pass result.
@@ -711,6 +857,91 @@ function reportAlias(result, dryRun) {
711
857
  );
712
858
  }
713
859
 
860
+ /** Prefix for a finding line: red for a browser-visible defect, yellow otherwise. */
861
+ function severityMark(severity) {
862
+ if (severity === "error") return `${c.red}✗${c.reset}`;
863
+ return severity === "warn" ? `${c.yellow}!${c.reset}` : `${c.cyan}i${c.reset}`;
864
+ }
865
+
866
+ /**
867
+ * The CSS pass of `fix`: report what the analysis found, then remove the subset
868
+ * that is provably dead.
869
+ *
870
+ * Findings that need a human are printed in full here rather than capped, because
871
+ * `--dry-run` is the review surface — `doctor` is the summary, this is the list
872
+ * you read before letting the tool write. Only the advisory (`info`) tail is
873
+ * capped, since "this could be a token" scales with the size of the project.
874
+ *
875
+ * @param {object} params
876
+ * @param {string[]} params.targets - Positional paths from the command line.
877
+ * @param {boolean} params.dryRun
878
+ * @returns {number} Exit status contribution.
879
+ */
880
+ function cssPass({ targets, dryRun }) {
881
+ console.log(
882
+ `${c.dim}→ css (dedupe declarations · drop dead rules)${dryRun ? " [dry-run]" : ""}${c.reset}`,
883
+ );
884
+ let analysis;
885
+ try {
886
+ analysis = analyzeCss({ root: ROOT, targets, selfDir: SELF_DIR });
887
+ } catch (err) {
888
+ console.log(
889
+ ` ${c.red}✗ analysis failed${c.reset} ${c.dim}${err?.message ?? err}${c.reset}`,
890
+ );
891
+ return 1;
892
+ }
893
+ if (analysis.stats.files === 0) {
894
+ console.log(` ${c.dim}no stylesheets found${c.reset}`);
895
+ return 0;
896
+ }
897
+
898
+ const manual = analysis.findings.filter((f) => !f.fixable);
899
+ const infoLimit = 10;
900
+ let shownInfo = 0;
901
+ let hiddenInfo = 0;
902
+ for (const f of manual) {
903
+ if (f.severity === "info") {
904
+ if (shownInfo >= infoLimit) {
905
+ hiddenInfo += 1;
906
+ continue;
907
+ }
908
+ shownInfo += 1;
909
+ }
910
+ console.log(
911
+ ` [${severityMark(f.severity)}] ${f.file}:${f.line} ${c.dim}${f.message}${c.reset}`,
912
+ );
913
+ }
914
+ if (hiddenInfo > 0) {
915
+ console.log(` ${c.dim}…+${hiddenInfo} more suggestion(s)${c.reset}`);
916
+ }
917
+
918
+ const result = applyCssFixes({ analysis, dryRun });
919
+ for (const file of result.files) {
920
+ console.log(` ${file.file} ${c.dim}${file.changes.length}${c.reset}`);
921
+ for (const change of file.changes) {
922
+ console.log(` ${c.dim}${change.line}:${c.reset} ${change.message}`);
923
+ }
924
+ }
925
+ for (const err of result.errors) {
926
+ console.log(` ${c.red}✗ ${err.file}${c.reset} ${c.dim}${err.message}${c.reset}`);
927
+ }
928
+
929
+ if (result.total === 0) {
930
+ console.log(` ${c.dim}nothing to remove${c.reset}`);
931
+ } else {
932
+ const verb = dryRun ? "would remove" : "removed";
933
+ console.log(
934
+ ` ${c.green}✓${c.reset} ${verb} ${result.total} dead declaration(s)/rule(s) in ${result.files.length} file(s)`,
935
+ );
936
+ }
937
+ if (analysis.counts.error > 0) {
938
+ console.log(
939
+ ` ${c.red}✗ ${analysis.counts.error} CSS syntax error(s)${c.reset} ${c.dim}— those files were not touched; fix them and run again${c.reset}`,
940
+ );
941
+ }
942
+ return result.errors.length || analysis.counts.error ? 1 : 0;
943
+ }
944
+
714
945
  function fix(args) {
715
946
  const { flags, paths } = splitArgs(args);
716
947
  const unknown = flags.filter((f) => !FIX_FLAGS.has(f));
@@ -730,7 +961,10 @@ function fix(args) {
730
961
  reportAlias(result, dryRun);
731
962
  aliasStatus = result.status === "error" ? 1 : 0;
732
963
  }
733
- if (dryRun) return aliasStatus;
964
+
965
+ const cssStatus = flags.includes("--no-css") ? 0 : cssPass({ targets, dryRun });
966
+
967
+ if (dryRun) return aliasStatus || cssStatus;
734
968
 
735
969
  console.log(`${c.dim}→ eslint --fix (sort imports · drop unused · tidy whitespace)${c.reset}`);
736
970
  const eslintStatus = run(requireBin("eslint"), [...targets, "--fix"]);
@@ -742,7 +976,7 @@ function fix(args) {
742
976
  } else {
743
977
  console.log(`${c.yellow}! prettier not installed — skipping format pass${c.reset}`);
744
978
  }
745
- return aliasStatus || eslintStatus || prettierStatus;
979
+ return aliasStatus || cssStatus || eslintStatus || prettierStatus;
746
980
  }
747
981
 
748
982
  function format(args) {
@@ -878,8 +1112,8 @@ ${c.bold}Usage${c.reset}
878
1112
  ${c.bold}Commands${c.reset}
879
1113
  ${c.bold}doctor${c.reset} Health-check the current project
880
1114
  ${c.bold}lint${c.reset} [paths] Run ESLint (report only)
881
- ${c.bold}fix${c.reset} [paths] Alias imports (../ → @/) + ESLint --fix (sort imports, remove
882
- unused, tidy whitespace) + Prettier
1115
+ ${c.bold}fix${c.reset} [paths] Alias imports (../ → @/) + CSS dead-code removal + ESLint --fix
1116
+ (sort imports, remove unused, tidy whitespace) + Prettier
883
1117
  ${c.bold}format${c.reset} [paths] Prettier --write
884
1118
  ${c.bold}gen api${c.reset} <src> Generate Zod schemas + types + service classes from an OpenAPI spec
885
1119
  (e.g. tempest gen api http://127.0.0.1:8000/openapi.json --out src/api)
@@ -887,8 +1121,10 @@ ${c.bold}Commands${c.reset}
887
1121
  (e.g. tempest gen icons --out src/icons.generated.ts --dir src)
888
1122
 
889
1123
  ${c.bold}fix options${c.reset}
890
- --dry-run List the imports that would be rewritten; write nothing
891
- --no-alias Skip the alias pass (ESLint + Prettier only)
1124
+ --dry-run List every CSS finding and the imports that would be rewritten;
1125
+ write nothing
1126
+ --no-alias Skip the alias pass
1127
+ --no-css Skip the CSS pass (analysis and dead-code removal)
892
1128
 
893
1129
  ${c.bold}Options${c.reset}
894
1130
  -h, --help Show this help
@@ -0,0 +1,2 @@
1
+ var e=7,t=9,n=3;function r(e){return`var(--tempest-chart-${e})`}function i(e,t,n){if(!Number.isFinite(e))return 0;let r=Math.min(t,n),i=Math.max(t,n);return i===r?.5:Math.min(1,Math.max(0,(e-r)/(i-r)))}function a(e,t){return Math.min(t,Math.max(1,Math.round(e*(t-1))+1))}function o(e){let{min:t,max:n,ordinal:o=!1}=e,s=o?3:1,c=7-s+1;return e=>r(`sequential-${s+a(i(e,t,n),c)-1}`)}function s(e){let{min:t,max:n,center:i=0}=e,o=Math.abs(i-Math.min(t,n)),s=Math.abs(Math.max(t,n)-i);return e=>r(!Number.isFinite(e)||e===i?`diverging-5`:e<i?o===0?`diverging-5`:`diverging-${5-a(Math.min(1,(i-e)/o),4)}`:s===0?`diverging-5`:`diverging-${5+a(Math.min(1,(e-i)/s),4)}`)}function c(e){let t=e===`sequential`?7:9;return Array.from({length:t},(t,n)=>r(`${e}-${n+1}`))}exports.DIVERGING_STEP_COUNT=t,exports.ORDINAL_START_STEP=n,exports.SEQUENTIAL_STEP_COUNT=e,exports.divergingScale=s,exports.scaleSteps=c,exports.sequentialScale=o;
2
+ //# sourceMappingURL=scales.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scales.cjs","names":[],"sources":["../../src/charts/scales.ts"],"sourcesContent":["/** How many steps the sequential token scale has. */\nexport const SEQUENTIAL_STEP_COUNT = 7;\n\n/** How many steps the diverging token scale has, midpoint included. */\nexport const DIVERGING_STEP_COUNT = 9;\n\n/**\n * First sequential step that clears 2:1 against the chart surface.\n *\n * A sequential scale may let its near-zero end recede into the surface — on a\n * heatmap that is exactly what \"almost nothing\" should look like. An **ordinal**\n * scale may not: every step is a discrete mark someone has to see. Starting an\n * ordinal ramp here is the difference.\n */\nexport const ORDINAL_START_STEP = 3;\n\n/** A `var(--tempest-chart-…)` reference, so the value follows the active theme. */\nexport type ChartColorToken = string;\n\n/** Reference a token by name rather than interpolating the string at each call site. */\nfunction token(name: string): ChartColorToken {\n return `var(--tempest-chart-${name})`;\n}\n\n/**\n * Clamp `value` into `0…1` against a domain, tolerating a reversed or empty one.\n *\n * A zero-width domain (every datum equal) would otherwise divide by zero and paint\n * the whole chart `NaN`; it resolves to the middle of the scale instead, which is\n * the honest reading of \"no variation\".\n */\nfunction normalize(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return 0;\n const lo = Math.min(min, max);\n const hi = Math.max(min, max);\n if (hi === lo) return 0.5;\n return Math.min(1, Math.max(0, (value - lo) / (hi - lo)));\n}\n\n/** Map a `0…1` position onto `1…steps`, inclusive. */\nfunction stepOf(t: number, steps: number): number {\n return Math.min(steps, Math.max(1, Math.round(t * (steps - 1)) + 1));\n}\n\nexport interface SequentialScaleOptions {\n /** Lowest value in the data. */\n min: number;\n /** Highest value in the data. */\n max: number;\n /**\n * Keep every step visible against the surface, for discrete ordered marks.\n *\n * Off by default: a heatmap *wants* its near-zero cells to recede. Turn it on\n * for tiers, funnel stages or anything where each step is its own mark.\n */\n ordinal?: boolean;\n}\n\n/**\n * Build a magnitude scale over the sequential tokens.\n *\n * Returns `var(--tempest-chart-sequential-N)` rather than a hex string, so a\n * heatmap painted once follows the theme — including dark mode, whose steps are\n * chosen for the dark surface rather than flipped.\n *\n * @example\n * const color = sequentialScale({ min: 0, max: 250 });\n * <rect fill={color(value)} />\n *\n * @param options - The data domain, and whether every step must stay visible.\n * @returns A function from value to a CSS colour reference.\n */\nexport function sequentialScale(\n options: SequentialScaleOptions,\n): (value: number) => ChartColorToken {\n const { min, max, ordinal = false } = options;\n const first = ordinal ? ORDINAL_START_STEP : 1;\n const span = SEQUENTIAL_STEP_COUNT - first + 1;\n return (value) => token(`sequential-${first + stepOf(normalize(value, min, max), span) - 1}`);\n}\n\nexport interface DivergingScaleOptions {\n /** Lowest value in the data. */\n min: number;\n /** Highest value in the data. */\n max: number;\n /**\n * The value that means \"no deviation\". Default `0`.\n *\n * It is a parameter because the interesting midpoint is often not zero — a\n * budget variance diverges around the target, not around nothing.\n */\n center?: number;\n}\n\n/**\n * Build a polarity scale over the diverging tokens.\n *\n * Each arm is scaled against its **own** distance from the centre, so an asymmetric\n * domain (say −5…+80) still uses the full cool arm for its small negatives. Scaling\n * both arms by the wider one — the easy mistake — would collapse every negative\n * into the step next to the midpoint and hide the sign entirely.\n *\n * @example\n * const color = divergingScale({ min: -12, max: 40 }); // centre 0\n * const budget = divergingScale({ min: 80, max: 130, center: 100 });\n *\n * @param options - The data domain and the neutral centre.\n * @returns A function from value to a CSS colour reference.\n */\nexport function divergingScale(options: DivergingScaleOptions): (value: number) => ChartColorToken {\n const { min, max, center = 0 } = options;\n const mid = Math.ceil(DIVERGING_STEP_COUNT / 2);\n const armSteps = mid - 1;\n const coolSpan = Math.abs(center - Math.min(min, max));\n const warmSpan = Math.abs(Math.max(min, max) - center);\n\n return (value) => {\n if (!Number.isFinite(value) || value === center) return token(`diverging-${mid}`);\n if (value < center) {\n if (coolSpan === 0) return token(`diverging-${mid}`);\n const t = Math.min(1, (center - value) / coolSpan);\n // Step 1 is the cool extreme, so a bigger deviation walks toward it.\n return token(`diverging-${mid - stepOf(t, armSteps)}`);\n }\n if (warmSpan === 0) return token(`diverging-${mid}`);\n const t = Math.min(1, (value - center) / warmSpan);\n return token(`diverging-${mid + stepOf(t, armSteps)}`);\n };\n}\n\n/**\n * Every step of a token scale, in order — for rendering a legend.\n *\n * A continuous scale needs a legend showing the ramp with its end labels; without\n * one the reader has no way to turn a colour back into a number.\n *\n * @param kind - Which scale.\n * @returns The token references, lightest/coolest first.\n */\nexport function scaleSteps(kind: \"sequential\" | \"diverging\"): ChartColorToken[] {\n const count = kind === \"sequential\" ? SEQUENTIAL_STEP_COUNT : DIVERGING_STEP_COUNT;\n return Array.from({ length: count }, (_, i) => token(`${kind}-${i + 1}`));\n}\n"],"mappings":"AACA,IAAa,EAAwB,EAGxB,EAAuB,EAUvB,EAAqB,EAMlC,SAAS,EAAM,EAA+B,CAC1C,MAAO,uBAAuB,EAAK,EACvC,CASA,SAAS,EAAU,EAAe,EAAa,EAAqB,CAChE,GAAI,CAAC,OAAO,SAAS,CAAK,EAAG,MAAO,GACpC,IAAM,EAAK,KAAK,IAAI,EAAK,CAAG,EACtB,EAAK,KAAK,IAAI,EAAK,CAAG,EAE5B,OADI,IAAO,EAAW,GACf,KAAK,IAAI,EAAG,KAAK,IAAI,GAAI,EAAQ,IAAO,EAAK,EAAG,CAAC,CAC5D,CAGA,SAAS,EAAO,EAAW,EAAuB,CAC9C,OAAO,KAAK,IAAI,EAAO,KAAK,IAAI,EAAG,KAAK,MAAM,GAAK,EAAQ,EAAE,EAAI,CAAC,CAAC,CACvE,CA8BA,SAAgB,EACZ,EACkC,CAClC,GAAM,CAAE,MAAK,MAAK,UAAU,IAAU,EAChC,EAAQ,EAAA,EAA+B,EACvC,EAAA,EAA+B,EAAQ,EAC7C,MAAQ,IAAU,EAAM,cAAc,EAAQ,EAAO,EAAU,EAAO,EAAK,CAAG,EAAG,CAAI,EAAI,GAAG,CAChG,CA+BA,SAAgB,EAAe,EAAoE,CAC/F,GAAM,CAAE,MAAK,MAAK,SAAS,GAAM,EAG3B,EAAW,KAAK,IAAI,EAAS,KAAK,IAAI,EAAK,CAAG,CAAC,EAC/C,EAAW,KAAK,IAAI,KAAK,IAAI,EAAK,CAAG,EAAI,CAAM,EAErD,MAAQ,IACoD,EAApD,CAAC,OAAO,SAAS,CAAK,GAAK,IAAU,EAAqB,cAC1D,EAAQ,EACJ,IAAa,EAAgB,cAGpB,aAAa,EAAM,EAFtB,KAAK,IAAI,GAAI,EAAS,GAAS,CAEF,EAAG,CAAQ,IAElD,IAAa,EAAgB,cAEpB,aAAa,EAAM,EADtB,KAAK,IAAI,GAAI,EAAQ,GAAU,CACF,EAAG,CAAQ,GAT8B,CAWxF,CAWA,SAAgB,EAAW,EAAqD,CAC5E,IAAM,EAAQ,IAAS,aAAA,EAAA,EACvB,OAAO,MAAM,KAAK,CAAE,OAAQ,CAAM,GAAI,EAAG,IAAM,EAAM,GAAG,EAAK,GAAG,EAAI,GAAG,CAAC,CAC5E"}