tempest-react-sdk 0.26.0 → 0.27.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 (35) hide show
  1. package/README.md +1 -1
  2. package/bin/lib/doctor/doctor.e2e.test.mjs +201 -0
  3. package/bin/lib/doctor/lucide.mjs +74 -0
  4. package/bin/lib/doctor/lucide.test.mjs +107 -0
  5. package/bin/tempest.mjs +119 -29
  6. package/dist/components/ImageCropper/ImageCropper.cjs +2 -0
  7. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -0
  8. package/dist/components/ImageCropper/ImageCropper.js +240 -0
  9. package/dist/components/ImageCropper/ImageCropper.js.map +1 -0
  10. package/dist/components/ImageCropper/ImageCropper.module.cjs +2 -0
  11. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -0
  12. package/dist/components/ImageCropper/ImageCropper.module.js +15 -0
  13. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -0
  14. package/dist/components/ImageCropper/crop-geometry.cjs +2 -0
  15. package/dist/components/ImageCropper/crop-geometry.cjs.map +1 -0
  16. package/dist/components/ImageCropper/crop-geometry.js +52 -0
  17. package/dist/components/ImageCropper/crop-geometry.js.map +1 -0
  18. package/dist/components/Scheduler/Scheduler.cjs +2 -0
  19. package/dist/components/Scheduler/Scheduler.cjs.map +1 -0
  20. package/dist/components/Scheduler/Scheduler.js +134 -0
  21. package/dist/components/Scheduler/Scheduler.js.map +1 -0
  22. package/dist/components/Scheduler/Scheduler.module.cjs +2 -0
  23. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -0
  24. package/dist/components/Scheduler/Scheduler.module.js +26 -0
  25. package/dist/components/Scheduler/Scheduler.module.js.map +1 -0
  26. package/dist/components/Scheduler/scheduler-layout.cjs +2 -0
  27. package/dist/components/Scheduler/scheduler-layout.cjs.map +1 -0
  28. package/dist/components/Scheduler/scheduler-layout.js +97 -0
  29. package/dist/components/Scheduler/scheduler-layout.js.map +1 -0
  30. package/dist/styles.css +1 -1
  31. package/dist/tempest-react-sdk.cjs +1 -1
  32. package/dist/tempest-react-sdk.d.ts +162 -0
  33. package/dist/tempest-react-sdk.js +151 -149
  34. package/package.json +1 -1
  35. package/template/package.json +1 -1
package/README.md CHANGED
@@ -149,7 +149,7 @@ Via `package.json`:
149
149
  ```json
150
150
  {
151
151
  "dependencies": {
152
- "tempest-react-sdk": "^0.26.0"
152
+ "tempest-react-sdk": "^0.27.0"
153
153
  }
154
154
  }
155
155
  ```
@@ -0,0 +1,201 @@
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 — a project that does use the SDK", () => {
159
+ beforeEach(() => {
160
+ thirdPartyApp();
161
+ write("package.json", {
162
+ name: "sdk-app",
163
+ type: "module",
164
+ dependencies: {
165
+ "tempest-react-sdk": "^0.26.1",
166
+ react: "^19.0.0",
167
+ "react-dom": "^19.0.0",
168
+ },
169
+ });
170
+ installed("tempest-react-sdk", "0.26.1", { dependencies: { "lucide-react": "^1.26.0" } });
171
+ installed("lucide-react", "1.26.0");
172
+ });
173
+
174
+ it("checks the SDK's conventions again", () => {
175
+ write("tsconfig.json", {
176
+ compilerOptions: { strict: true, jsx: "react-jsx", moduleResolution: "bundler" },
177
+ });
178
+ const { out } = doctor();
179
+ expect(out).toContain('tsconfig "@/*" alias');
180
+ });
181
+
182
+ it("asks for the stylesheet import, which is a real defect without it", () => {
183
+ write("src/main.tsx", "export const main = 1;");
184
+ expect(doctor().out).toContain("styles.css");
185
+ });
186
+
187
+ it("omits the adoption hint — it has already adopted", () => {
188
+ expect(doctor().out).not.toContain("Adopting the SDK (optional)");
189
+ });
190
+
191
+ it("warns when the SDK is installed but undeclared", () => {
192
+ write("package.json", {
193
+ name: "sdk-app",
194
+ dependencies: { react: "^19.0.0", "react-dom": "^19.0.0" },
195
+ });
196
+ const { out, code } = doctor();
197
+ expect(out).toContain("tempest-react-sdk not in dependencies");
198
+ // Undeclared-but-present is a warning, not a blocking failure.
199
+ expect(code).toBe(0);
200
+ });
201
+ });
@@ -0,0 +1,74 @@
1
+ // The `lucide-react` single-instance check for `tempest doctor`.
2
+ //
3
+ // Kept out of the generic stateful-deps check on purpose: two copies of React or
4
+ // of react-query break hooks and context, which is a different failure from what
5
+ // two copies of lucide cause. Lucide is stateless, so nothing "breaks" at
6
+ // runtime — instead the bytes are duplicated and, far worse, the generated slug
7
+ // tables behind `tempest-react-sdk/icons` can reference exports the second copy
8
+ // does not have.
9
+
10
+ /** Major version number from a semver string or range, or null. */
11
+ function major(spec) {
12
+ if (!spec) return null;
13
+ const match = /(\d+)\./.exec(String(spec).replace(/^[^\d]*/, ""));
14
+ return match ? Number(match[1]) : null;
15
+ }
16
+
17
+ /**
18
+ * Audit how many copies of `lucide-react` a project will end up with.
19
+ *
20
+ * @param {object} params
21
+ * @param {string | null} params.appSpec - Range the app declares, or `null`.
22
+ * @param {string | null} params.sdkSpec - Range the installed SDK declares, or `null`.
23
+ * @param {string | null} params.installedVersion - Version resolved at the app root, or `null`.
24
+ * @param {boolean} params.nestedCopy - Whether a copy exists under the SDK's own `node_modules`.
25
+ * @returns {Array<[status: string, label: string, detail?: string]>} Check rows.
26
+ */
27
+ export function checkLucide({ appSpec, sdkSpec, installedVersion, nestedCopy }) {
28
+ const rows = [];
29
+
30
+ // The SDK not declaring it means this project does not use the icons surface
31
+ // at all (or the SDK is not installed) — there is nothing to audit.
32
+ if (!sdkSpec) return rows;
33
+
34
+ if (nestedCopy) {
35
+ rows.push([
36
+ "warn",
37
+ "two copies of lucide-react",
38
+ "nested copy under tempest-react-sdk — duplicated bytes, and the generated " +
39
+ "icon slug tables may not match it; run `npm dedupe` or drop lucide-react " +
40
+ "from your package.json (the SDK ships it)",
41
+ ]);
42
+ }
43
+
44
+ if (appSpec) {
45
+ const sameRange = appSpec === sdkSpec;
46
+ rows.push([
47
+ sameRange ? "info" : "warn",
48
+ "lucide-react declared by the app",
49
+ sameRange
50
+ ? `matches the SDK's ${sdkSpec} so a single copy installs — the declaration is ` +
51
+ "redundant, but harmless (npm/yarn resolve it from the SDK without it)"
52
+ : `you declare ${appSpec}, the SDK declares ${sdkSpec} — two copies install. ` +
53
+ "Run `npm uninstall lucide-react`; only declare it under pnpm, and then " +
54
+ `use ${sdkSpec}`,
55
+ ]);
56
+ }
57
+
58
+ const installedMajor = major(installedVersion);
59
+ const requiredMajor = major(sdkSpec);
60
+ if (installedMajor !== null && requiredMajor !== null && installedMajor < requiredMajor) {
61
+ rows.push([
62
+ "fail",
63
+ `lucide-react v${installedVersion} is older than the SDK needs`,
64
+ `the icon slug tables are generated against ${sdkSpec}, so exports they ` +
65
+ "reference can be missing — the build fails with `… is not exported by " +
66
+ "lucide-react` pointing inside the SDK",
67
+ ]);
68
+ }
69
+
70
+ if (rows.length === 0) {
71
+ rows.push(["ok", "single lucide-react instance", `from the SDK — ${sdkSpec}`]);
72
+ }
73
+ return rows;
74
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { checkLucide } from "./lucide.mjs";
4
+
5
+ /** The healthy shape: the SDK brings lucide, the app declares nothing. */
6
+ const CLEAN = {
7
+ appSpec: null,
8
+ sdkSpec: "^1.26.0",
9
+ installedVersion: "1.26.0",
10
+ nestedCopy: false,
11
+ };
12
+
13
+ /** Statuses of the returned rows, in order. */
14
+ const statuses = (rows) => rows.map(([status]) => status);
15
+
16
+ /** All detail text joined, for asserting the advice is actually present. */
17
+ const details = (rows) => rows.map(([, , detail]) => detail ?? "").join(" ");
18
+
19
+ describe("checkLucide — nothing to audit", () => {
20
+ it("returns no rows when the SDK does not declare lucide", () => {
21
+ expect(checkLucide({ ...CLEAN, sdkSpec: null })).toEqual([]);
22
+ });
23
+
24
+ it("stays silent even if the app declares it, with no SDK to compare against", () => {
25
+ expect(checkLucide({ ...CLEAN, sdkSpec: null, appSpec: "^1.0.0" })).toEqual([]);
26
+ });
27
+ });
28
+
29
+ describe("checkLucide — healthy", () => {
30
+ it("reports a single instance and names where it came from", () => {
31
+ const rows = checkLucide(CLEAN);
32
+ expect(statuses(rows)).toEqual(["ok"]);
33
+ expect(rows[0][1]).toBe("single lucide-react instance");
34
+ expect(rows[0][2]).toContain("^1.26.0");
35
+ });
36
+
37
+ it("stays ok when the installed version is newer than required", () => {
38
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "2.4.0" }))).toEqual(["ok"]);
39
+ });
40
+
41
+ it("stays ok when the version cannot be resolved at all", () => {
42
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: null }))).toEqual(["ok"]);
43
+ });
44
+ });
45
+
46
+ describe("checkLucide — the app declares it too", () => {
47
+ it("warns and says to uninstall when the range differs from the SDK's", () => {
48
+ const rows = checkLucide({ ...CLEAN, appSpec: "^0.575.0" });
49
+ expect(statuses(rows)).toEqual(["warn"]);
50
+ expect(details(rows)).toContain("npm uninstall lucide-react");
51
+ expect(details(rows)).toContain("^0.575.0");
52
+ expect(details(rows)).toContain("^1.26.0");
53
+ });
54
+
55
+ it("mentions pnpm, the one case where declaring it is correct", () => {
56
+ expect(details(checkLucide({ ...CLEAN, appSpec: "^0.575.0" }))).toContain("pnpm");
57
+ });
58
+
59
+ it("downgrades to info when the range matches — redundant, not broken", () => {
60
+ const rows = checkLucide({ ...CLEAN, appSpec: "^1.26.0" });
61
+ expect(statuses(rows)).toEqual(["info"]);
62
+ expect(details(rows)).toContain("redundant");
63
+ });
64
+ });
65
+
66
+ describe("checkLucide — a second physical copy", () => {
67
+ it("warns about the nested copy and points at both remedies", () => {
68
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true });
69
+ expect(statuses(rows)).toEqual(["warn"]);
70
+ expect(rows[0][1]).toBe("two copies of lucide-react");
71
+ expect(details(rows)).toContain("npm dedupe");
72
+ expect(details(rows)).toContain("the SDK ships it");
73
+ });
74
+
75
+ it("reports the nested copy and the declaration as separate rows", () => {
76
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true, appSpec: "^0.575.0" });
77
+ expect(statuses(rows)).toEqual(["warn", "warn"]);
78
+ });
79
+ });
80
+
81
+ describe("checkLucide — version older than the generated tables need", () => {
82
+ it("fails, because this one breaks the build rather than wasting bytes", () => {
83
+ const rows = checkLucide({ ...CLEAN, installedVersion: "0.575.0" });
84
+ expect(statuses(rows)).toEqual(["fail"]);
85
+ expect(rows[0][1]).toContain("0.575.0");
86
+ expect(details(rows)).toContain("is not exported by");
87
+ });
88
+
89
+ it("explains that the error surfaces inside the SDK, not in app code", () => {
90
+ expect(details(checkLucide({ ...CLEAN, installedVersion: "0.575.0" }))).toContain(
91
+ "inside the SDK",
92
+ );
93
+ });
94
+
95
+ it("reports the skew alongside the declaration that caused it", () => {
96
+ const rows = checkLucide({
97
+ ...CLEAN,
98
+ appSpec: "^0.575.0",
99
+ installedVersion: "0.575.0",
100
+ });
101
+ expect(statuses(rows)).toEqual(["warn", "fail"]);
102
+ });
103
+
104
+ it("does not fail on a malformed version string", () => {
105
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "latest" }))).toEqual(["ok"]);
106
+ });
107
+ });
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 { checkLucide } from "./lib/doctor/lucide.mjs";
17
18
  import { generateRegistry, loadIconTables } from "./lib/icons/generate.mjs";
18
19
  import { generate } from "./lib/openapi/generate.mjs";
19
20
  import { loadSpec } from "./lib/openapi/load.mjs";
@@ -311,16 +312,45 @@ function doctor() {
311
312
  checks.push(["section", "Project"]);
312
313
  checks.push(["ok", "package.json found"]);
313
314
  const sdkInstalled = installedVersion("tempest-react-sdk");
314
- checks.push(
315
- deps["tempest-react-sdk"]
316
- ? ["ok", "tempest-react-sdk in dependencies", deps["tempest-react-sdk"]]
317
- : ["fail", "tempest-react-sdk in dependencies", "npm install tempest-react-sdk"],
318
- );
319
- checks.push(
320
- sdkInstalled
321
- ? ["ok", "tempest-react-sdk installed", `v${sdkInstalled}`]
322
- : ["fail", "tempest-react-sdk installed", "run npm install"],
323
- );
315
+
316
+ /**
317
+ * Whether this project has adopted the SDK.
318
+ *
319
+ * When it has not, `doctor` runs in **generic mode**: the checks that only make
320
+ * sense once you use the SDK (its `@/*` alias, `createViteConfig`, the
321
+ * `styles.css` import, its optional peers) drop out, and not having the SDK is
322
+ * reported as information rather than as a defect.
323
+ *
324
+ * Without this the tool is useless on the exact project it should help most —
325
+ * somebody's existing app, being evaluated. It reported two hard failures for the
326
+ * single fact "you have not installed this yet", exited non-zero, and buried the
327
+ * findings that *were* actionable (no lockfile, no plugin-react, no linter) among
328
+ * warnings that were really just the SDK's own conventions.
329
+ */
330
+ const usesSdk = Boolean(deps["tempest-react-sdk"] || sdkInstalled);
331
+
332
+ if (usesSdk) {
333
+ checks.push(
334
+ deps["tempest-react-sdk"]
335
+ ? ["ok", "tempest-react-sdk in dependencies", deps["tempest-react-sdk"]]
336
+ : [
337
+ "warn",
338
+ "tempest-react-sdk not in dependencies",
339
+ `installed as v${sdkInstalled} but undeclared — add it to package.json`,
340
+ ],
341
+ );
342
+ checks.push(
343
+ sdkInstalled
344
+ ? ["ok", "tempest-react-sdk installed", `v${sdkInstalled}`]
345
+ : ["fail", "tempest-react-sdk installed", "run npm install"],
346
+ );
347
+ } else {
348
+ checks.push([
349
+ "info",
350
+ "tempest-react-sdk not installed",
351
+ "checking generic React/Vite health only — `npm i tempest-react-sdk` to adopt it",
352
+ ]);
353
+ }
324
354
  const reactV = installedVersion("react");
325
355
  checks.push(
326
356
  deps.react && deps["react-dom"]
@@ -351,6 +381,21 @@ function doctor() {
351
381
  "nested copy under tempest-react-sdk — run `npm dedupe` or align versions; two instances break hooks/context",
352
382
  ],
353
383
  );
384
+
385
+ // lucide-react gets its own check: two copies of it do not break hooks the
386
+ // way a second React does, but they duplicate bytes and can leave the
387
+ // generated icon slug tables pointing at exports the older copy lacks.
388
+ const sdkPkg = readJSON(join(ROOT, "node_modules", "tempest-react-sdk", "package.json"));
389
+ checks.push(
390
+ ...checkLucide({
391
+ appSpec: deps["lucide-react"] ? String(deps["lucide-react"]) : null,
392
+ sdkSpec: sdkPkg?.dependencies?.["lucide-react"]
393
+ ? String(sdkPkg.dependencies["lucide-react"])
394
+ : null,
395
+ installedVersion: installedVersion("lucide-react"),
396
+ nestedCopy: nestedDupe("lucide-react"),
397
+ }),
398
+ );
354
399
  }
355
400
 
356
401
  // Declared-but-not-installed (package.json ↔ node_modules drift).
@@ -422,11 +467,15 @@ function doctor() {
422
467
  if (tsc) {
423
468
  checks.push(["section", "TypeScript"]);
424
469
  const co = tsc.compilerOptions;
425
- checks.push(
426
- co.paths?.["@/*"]
427
- ? ["ok", 'tsconfig "@/*" alias']
428
- : ["warn", 'tsconfig "@/*" alias', 'add "paths": { "@/*": ["./src/*"] }'],
429
- );
470
+ // The `@/*` alias is the SDK's convention, not a health property: a project
471
+ // that has not adopted the SDK is not wrong for lacking it.
472
+ if (usesSdk) {
473
+ checks.push(
474
+ co.paths?.["@/*"]
475
+ ? ["ok", 'tsconfig "@/*" alias']
476
+ : ["warn", 'tsconfig "@/*" alias', 'add "paths": { "@/*": ["./src/*"] }'],
477
+ );
478
+ }
430
479
  const mr = String(co.moduleResolution ?? "").toLowerCase();
431
480
  checks.push(
432
481
  ["bundler", "node16", "nodenext"].includes(mr)
@@ -434,7 +483,9 @@ function doctor() {
434
483
  : [
435
484
  "warn",
436
485
  `moduleResolution: ${co.moduleResolution ?? "(unset)"}`,
437
- 'use "bundler" — otherwise subpath types (tempest-react-sdk/br, /charts…) won\'t resolve',
486
+ usesSdk
487
+ ? 'use "bundler" — otherwise subpath types (tempest-react-sdk/br, /charts…) won\'t resolve'
488
+ : 'use "bundler" — a bundled app needs it for any package that ships subpath exports',
438
489
  ],
439
490
  );
440
491
  checks.push(
@@ -475,11 +526,21 @@ function doctor() {
475
526
  if (!viteCfg) {
476
527
  checks.push(["warn", "vite config", "no vite.config.* found"]);
477
528
  } else {
478
- checks.push(
479
- fileIncludes(join(ROOT, viteCfg), "createViteConfig")
480
- ? ["ok", `${viteCfg} uses createViteConfig`]
481
- : ["warn", `${viteCfg}`, "not using createViteConfig from tempest-react-sdk/vite"],
482
- );
529
+ // `createViteConfig` is a convenience, not a requirement — only worth
530
+ // mentioning to a project that already uses the SDK, and even then as info.
531
+ if (usesSdk) {
532
+ checks.push(
533
+ fileIncludes(join(ROOT, viteCfg), "createViteConfig")
534
+ ? ["ok", `${viteCfg} uses createViteConfig`]
535
+ : [
536
+ "info",
537
+ `${viteCfg}`,
538
+ "not using createViteConfig from tempest-react-sdk/vite — optional",
539
+ ],
540
+ );
541
+ } else {
542
+ checks.push(["ok", `${viteCfg} found`]);
543
+ }
483
544
  // React plugin is required for JSX/Fast Refresh in a Vite React app.
484
545
  checks.push(
485
546
  installedVersion("@vitejs/plugin-react")
@@ -492,14 +553,23 @@ function doctor() {
492
553
  );
493
554
  }
494
555
  const entry = firstExisting(["src/main.tsx", "src/main.ts", "src/index.tsx", "src/index.ts"]);
495
- if (entry) {
496
- checks.push(
497
- fileIncludes(join(ROOT, entry), "tempest-react-sdk/styles.css")
498
- ? ["ok", `${entry} imports styles.css`]
499
- : ["warn", `${entry}`, 'add import "tempest-react-sdk/styles.css"'],
500
- );
501
- } else {
502
- checks.push(["warn", "app entry", "no src/main.tsx found"]);
556
+ if (usesSdk) {
557
+ // Without the stylesheet every component renders unstyled, so for an SDK
558
+ // project this is a real defect. For anyone else the entry's filename is
559
+ // their business, not ours.
560
+ if (entry) {
561
+ checks.push(
562
+ fileIncludes(join(ROOT, entry), "tempest-react-sdk/styles.css")
563
+ ? ["ok", `${entry} imports styles.css`]
564
+ : ["warn", `${entry}`, 'add import "tempest-react-sdk/styles.css"'],
565
+ );
566
+ } else {
567
+ checks.push([
568
+ "warn",
569
+ "app entry",
570
+ "none of src/main.tsx, src/main.ts, src/index.tsx, src/index.ts found — cannot verify the styles.css import",
571
+ ]);
572
+ }
503
573
  }
504
574
  // styles.css should be imported once — duplicates re-inject the whole sheet.
505
575
  const stylesImports = (src.match(/tempest-react-sdk\/styles\.css/g) ?? []).length;
@@ -584,6 +654,26 @@ function doctor() {
584
654
  checks.push(["ok", "client env vars use the VITE_ prefix"]);
585
655
  }
586
656
 
657
+ /*
658
+ * In generic mode, close with what adoption would actually take. A tool that
659
+ * audits somebody's project and then says nothing about the next step reads as an
660
+ * ad for a product they cannot find the door to.
661
+ */
662
+ if (!usesSdk) {
663
+ checks.push(["section", "Adopting the SDK (optional)"]);
664
+ checks.push(["info", "install", "npm i tempest-react-sdk"]);
665
+ checks.push([
666
+ "info",
667
+ "import the stylesheet once, in your entry",
668
+ 'import "tempest-react-sdk/styles.css"',
669
+ ]);
670
+ checks.push([
671
+ "info",
672
+ "not all-or-nothing",
673
+ "one component at a time works — the @/* alias, createViteConfig and the scaffold are all optional",
674
+ ]);
675
+ }
676
+
587
677
  return report(checks);
588
678
  }
589
679
 
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("./crop-geometry.cjs"),n=require("./ImageCropper.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=12,o=.15;function s({src:s,aspect:c=1,maxZoom:l=4,maxSize:u,outputType:d=`image/png`,outputQuality:f=.92,shape:p=`rect`,onCropChange:m,label:h=`Área de recorte`,className:g,ref:_,...v}){let y=`${(0,r.useId)()}-hint`,b=(0,r.useRef)(null),x=(0,r.useRef)(null),S=(0,r.useRef)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(null),[D,O]=(0,r.useState)({width:0,height:0}),[k,A]=(0,r.useState)(1),[j,M]=(0,r.useState)({x:0,y:0});(0,r.useEffect)(()=>{if(typeof s==`string`){w(s);return}let e=URL.createObjectURL(s);return w(e),()=>URL.revokeObjectURL(e)},[s]),(0,r.useEffect)(()=>{E(null),A(1),M({x:0,y:0})},[C]),(0,r.useEffect)(()=>{let e=b.current;if(!e)return;let t=()=>O({width:e.clientWidth,height:e.clientHeight});if(t(),typeof ResizeObserver>`u`)return;let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let N=T?t.coverScale(T,D)*k:0,P=(0,r.useMemo)(()=>T?{width:T.width*N,height:T.height*N}:{width:0,height:0},[T,N]),F=(0,r.useCallback)(e=>{M(n=>{let r=t.clampOffset(e,P,D);return r.x===n.x&&r.y===n.y?n:(m?.({zoom:k,offset:r}),r)})},[P,D,m,k]),I=(0,r.useCallback)(e=>{let n=Math.min(l,Math.max(1,e));if(A(n),!T)return;let r=t.coverScale(T,D)*n,i={width:T.width*r,height:T.height*r};M(e=>{let r=t.clampOffset(e,i,D);return m?.({zoom:n,offset:r}),r})},[D,l,T,m]),L=(0,r.useCallback)(()=>{A(1),M({x:0,y:0}),m?.({zoom:1,offset:{x:0,y:0}})},[m]),R=(0,r.useCallback)(async()=>{let e=x.current;if(!e||!T)return null;let n=t.computeCropRect({image:T,frame:D,zoom:k,offset:j});if(n.sWidth<=0||n.sHeight<=0)return null;let r=t.outputSize(n,u),i=document.createElement(`canvas`);i.width=r.width,i.height=r.height;let a=i.getContext(`2d`);return a?(a.drawImage(e,n.sx,n.sy,n.sWidth,n.sHeight,0,0,r.width,r.height),new Promise(e=>{i.toBlob(t=>e(t),d,f)})):null},[D,u,T,j,f,d,k]);(0,r.useImperativeHandle)(_,()=>({crop:R,reset:L}),[R,L]);let z=e=>{T&&(S.current={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,from:j},e.currentTarget.setPointerCapture?.(e.pointerId))},B=e=>{let t=S.current;!t||t.pointerId!==e.pointerId||F({x:t.from.x+(e.clientX-t.startX),y:t.from.y+(e.clientY-t.startY)})},V=e=>{S.current?.pointerId===e.pointerId&&(S.current=null)},H=e=>{let t=e.shiftKey?a*4:a,n={ArrowLeft:{x:-t,y:0},ArrowRight:{x:t,y:0},ArrowUp:{x:0,y:-t},ArrowDown:{x:0,y:t}}[e.key];if(n){e.preventDefault(),F({x:j.x+n.x,y:j.y+n.y});return}e.key===`+`||e.key===`=`?(e.preventDefault(),I(k+o)):e.key===`-`||e.key===`_`?(e.preventDefault(),I(k-o)):e.key===`0`&&(e.preventDefault(),L())};return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,g),...v,children:[(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.frame,p===`circle`&&n.default.circle),style:{aspectRatio:String(c)},role:`group`,"aria-label":h,"aria-describedby":y,tabIndex:0,onPointerDown:z,onPointerMove:B,onPointerUp:V,onPointerCancel:V,onKeyDown:H,onWheel:e=>{e.preventDefault(),I(k+(e.deltaY<0?o:-.15))},children:C&&(0,i.jsx)(`img`,{ref:x,src:C,alt:``,draggable:!1,className:n.default.image,style:{width:P.width||void 0,height:P.height||void 0,transform:`translate(${j.x}px, ${j.y}px)`},onLoad:e=>{let t=e.currentTarget;t.naturalWidth>0&&t.naturalHeight>0&&E({width:t.naturalWidth,height:t.naturalHeight})}})}),(0,i.jsxs)(`div`,{className:n.default.controls,children:[(0,i.jsx)(`input`,{type:`range`,className:n.default.zoom,min:1,max:l,step:.01,value:k,onChange:e=>I(Number(e.target.value)),"aria-label":`Zoom`,disabled:!T}),(0,i.jsx)(`button`,{type:`button`,className:n.default.reset,onClick:L,disabled:!T,children:`Centralizar`})]}),(0,i.jsxs)(`p`,{id:y,className:n.default.hint,children:[`Arraste para reposicionar. Setas movem, `,(0,i.jsx)(`kbd`,{children:`+`}),` e `,(0,i.jsx)(`kbd`,{children:`−`}),` dão zoom,`,` `,(0,i.jsx)(`kbd`,{children:`0`}),` centraliza.`]})]})}exports.ImageCropper=s;
2
+ //# sourceMappingURL=ImageCropper.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageCropper.cjs","names":[],"sources":["../../../src/components/ImageCropper/ImageCropper.tsx"],"sourcesContent":["import {\n type HTMLAttributes,\n type KeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n clampOffset,\n computeCropRect,\n coverScale,\n type Offset,\n outputSize,\n type Size,\n} from \"./crop-geometry\";\nimport styles from \"./ImageCropper.module.css\";\n\n/** Imperative handle for exporting the current crop. */\nexport interface ImageCropperHandle {\n /**\n * Render the current crop and resolve with it.\n *\n * Resolves `null` when the image has not loaded yet or the browser refuses to\n * encode — never throws, so a submit handler does not need a try/catch.\n */\n crop: () => Promise<Blob | null>;\n /** Recentre at zoom 1. */\n reset: () => void;\n}\n\nexport interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** The image to crop: a `File`/`Blob` from an input, or a URL. */\n src: File | Blob | string;\n /** Crop aspect ratio as `width / height`. Default `1` (square). */\n aspect?: number;\n /** Maximum zoom over the cover scale. Default `4`. */\n maxZoom?: number;\n /**\n * Cap on the longest edge of the exported image, in px.\n *\n * Without it the export keeps the source resolution, which is right for a\n * document scan and wasteful for an avatar.\n */\n maxSize?: number;\n /** Output MIME type. Default `\"image/png\"`. */\n outputType?: string;\n /** Output quality for lossy types, `0`–`1`. Default `0.92`. */\n outputQuality?: number;\n /** Overlay shape. `\"circle\"` for an avatar, `\"rect\"` for a document. Default `\"rect\"`. */\n shape?: \"rect\" | \"circle\";\n /** Called whenever the crop changes, e.g. to enable a submit button. */\n onCropChange?: (state: { zoom: number; offset: Offset }) => void;\n /** Accessible name for the crop area. */\n label?: string;\n ref?: React.Ref<ImageCropperHandle>;\n}\n\n/** Pixels moved per arrow-key press. */\nconst KEY_STEP = 12;\n\n/** Zoom added or removed per `+`/`-` press, and per wheel notch. */\nconst ZOOM_STEP = 0.15;\n\n/**\n * Crop an image to a fixed aspect ratio.\n *\n * The frame stays put and the image pans and zooms behind it — the model an avatar\n * or document-photo flow wants, where the output shape is decided by the app and\n * the user only chooses what lands inside it. (A free-form draggable rectangle is a\n * different component; this one cannot produce an off-ratio crop by construction.)\n *\n * The export reads the *natural* pixels through a canvas, so a 4000 px photo is not\n * silently downsampled to whatever the on-screen preview measured. The image is\n * also always clamped to cover the frame, so an export can never contain the empty\n * bands you get from panning past an edge.\n *\n * Works by pointer, wheel and keyboard: arrows pan, `+`/`-` zoom, `0` resets.\n *\n * @example\n * const cropper = useRef<ImageCropperHandle>(null);\n *\n * <ImageCropper ref={cropper} src={file} aspect={1} shape=\"circle\" maxSize={512} />\n * <Button onClick={async () => upload(await cropper.current?.crop())}>Salvar</Button>\n */\nexport function ImageCropper({\n src,\n aspect = 1,\n maxZoom = 4,\n maxSize,\n outputType = \"image/png\",\n outputQuality = 0.92,\n shape = \"rect\",\n onCropChange,\n label = \"Área de recorte\",\n className,\n ref,\n ...rest\n}: ImageCropperProps) {\n /**\n * Id for the shortcut hint.\n *\n * Generated rather than derived from `label`: a label with spaces would produce\n * an id with spaces, and `aria-describedby` splits on whitespace — so the\n * description would silently never associate with the frame.\n */\n const hintId = `${useId()}-hint`;\n\n const frameRef = useRef<HTMLDivElement>(null);\n const imageRef = useRef<HTMLImageElement | null>(null);\n const dragRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n from: Offset;\n } | null>(null);\n\n const [url, setUrl] = useState<string | null>(null);\n const [natural, setNatural] = useState<Size | null>(null);\n const [frame, setFrame] = useState<Size>({ width: 0, height: 0 });\n const [zoom, setZoom] = useState(1);\n const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });\n\n /**\n * Resolve `src` to a displayable URL.\n *\n * A `File`/`Blob` needs `createObjectURL`, and the URL must be revoked when the\n * source changes or the component unmounts — otherwise every re-pick of a photo\n * leaks the previous one for the lifetime of the document.\n */\n useEffect(() => {\n if (typeof src === \"string\") {\n setUrl(src);\n return;\n }\n const objectUrl = URL.createObjectURL(src);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [src]);\n\n // A new source invalidates the previous framing.\n useEffect(() => {\n setNatural(null);\n setZoom(1);\n setOffset({ x: 0, y: 0 });\n }, [url]);\n\n /** Track the frame's rendered size — the crop math is all relative to it. */\n useEffect(() => {\n const element = frameRef.current;\n if (!element) return;\n const measure = () =>\n setFrame({ width: element.clientWidth, height: element.clientHeight });\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n const scale = natural ? coverScale(natural, frame) * zoom : 0;\n\n /**\n * On-screen image size.\n *\n * Memoized because the pan callback depends on it: a fresh object every render\n * would rebuild that callback every render, and the pointer handlers close over\n * it during a drag.\n */\n const displayed = useMemo<Size>(\n () =>\n natural\n ? { width: natural.width * scale, height: natural.height * scale }\n : { width: 0, height: 0 },\n [natural, scale],\n );\n\n /** Apply a pan, clamped so the frame stays covered. */\n const pan = useCallback(\n (next: Offset) => {\n setOffset((current) => {\n const clamped = clampOffset(next, displayed, frame);\n if (clamped.x === current.x && clamped.y === current.y) return current;\n onCropChange?.({ zoom, offset: clamped });\n return clamped;\n });\n },\n [displayed, frame, onCropChange, zoom],\n );\n\n /**\n * Apply a zoom, then re-clamp the offset.\n *\n * Re-clamping is not optional: zooming *out* shrinks the image, so an offset\n * that was legal a moment ago can now expose background.\n */\n const applyZoom = useCallback(\n (next: number) => {\n const clampedZoom = Math.min(maxZoom, Math.max(1, next));\n setZoom(clampedZoom);\n if (!natural) return;\n const nextScale = coverScale(natural, frame) * clampedZoom;\n const nextDisplayed = {\n width: natural.width * nextScale,\n height: natural.height * nextScale,\n };\n setOffset((current) => {\n const clamped = clampOffset(current, nextDisplayed, frame);\n onCropChange?.({ zoom: clampedZoom, offset: clamped });\n return clamped;\n });\n },\n [frame, maxZoom, natural, onCropChange],\n );\n\n const reset = useCallback(() => {\n setZoom(1);\n setOffset({ x: 0, y: 0 });\n onCropChange?.({ zoom: 1, offset: { x: 0, y: 0 } });\n }, [onCropChange]);\n\n /**\n * Draw the current crop and encode it.\n *\n * Returns `null` rather than throwing on the paths a caller cannot do anything\n * about: no image yet, no 2D context, or an encoder that declined.\n */\n const crop = useCallback(async (): Promise<Blob | null> => {\n const image = imageRef.current;\n if (!image || !natural) return null;\n\n const rect = computeCropRect({ image: natural, frame, zoom, offset });\n if (rect.sWidth <= 0 || rect.sHeight <= 0) return null;\n\n const out = outputSize(rect, maxSize);\n const canvas = document.createElement(\"canvas\");\n canvas.width = out.width;\n canvas.height = out.height;\n const context = canvas.getContext(\"2d\");\n if (!context) return null;\n\n context.drawImage(\n image,\n rect.sx,\n rect.sy,\n rect.sWidth,\n rect.sHeight,\n 0,\n 0,\n out.width,\n out.height,\n );\n\n return new Promise((resolve) => {\n canvas.toBlob((blob) => resolve(blob), outputType, outputQuality);\n });\n }, [frame, maxSize, natural, offset, outputQuality, outputType, zoom]);\n\n useImperativeHandle(ref, () => ({ crop, reset }), [crop, reset]);\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>): void => {\n if (!natural) return;\n dragRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n from: offset,\n };\n event.currentTarget.setPointerCapture?.(event.pointerId);\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== event.pointerId) return;\n pan({\n x: drag.from.x + (event.clientX - drag.startX),\n y: drag.from.y + (event.clientY - drag.startY),\n });\n };\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>): void => {\n if (dragRef.current?.pointerId !== event.pointerId) return;\n dragRef.current = null;\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n const step = event.shiftKey ? KEY_STEP * 4 : KEY_STEP;\n const moves: Record<string, Offset> = {\n ArrowLeft: { x: -step, y: 0 },\n ArrowRight: { x: step, y: 0 },\n ArrowUp: { x: 0, y: -step },\n ArrowDown: { x: 0, y: step },\n };\n const move = moves[event.key];\n if (move) {\n event.preventDefault();\n pan({ x: offset.x + move.x, y: offset.y + move.y });\n return;\n }\n if (event.key === \"+\" || event.key === \"=\") {\n event.preventDefault();\n applyZoom(zoom + ZOOM_STEP);\n } else if (event.key === \"-\" || event.key === \"_\") {\n event.preventDefault();\n applyZoom(zoom - ZOOM_STEP);\n } else if (event.key === \"0\") {\n event.preventDefault();\n reset();\n }\n };\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n ref={frameRef}\n className={cn(styles.frame, shape === \"circle\" && styles.circle)}\n style={{ aspectRatio: String(aspect) }}\n role=\"group\"\n aria-label={label}\n aria-describedby={hintId}\n tabIndex={0}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={onKeyDown}\n onWheel={(event) => {\n event.preventDefault();\n applyZoom(zoom + (event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP));\n }}\n >\n {url && (\n <img\n ref={imageRef}\n src={url}\n alt=\"\"\n draggable={false}\n className={styles.image}\n style={{\n width: displayed.width || undefined,\n height: displayed.height || undefined,\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n }}\n onLoad={(event) => {\n const element = event.currentTarget;\n // A decode can succeed and still report no intrinsic size —\n // an SVG without a viewBox is the common case. Accepting\n // that would enable the controls over an image the crop\n // maths can do nothing with.\n if (element.naturalWidth > 0 && element.naturalHeight > 0) {\n setNatural({\n width: element.naturalWidth,\n height: element.naturalHeight,\n });\n }\n }}\n />\n )}\n </div>\n\n <div className={styles.controls}>\n <input\n type=\"range\"\n className={styles.zoom}\n min={1}\n max={maxZoom}\n step={0.01}\n value={zoom}\n onChange={(event) => applyZoom(Number(event.target.value))}\n aria-label=\"Zoom\"\n disabled={!natural}\n />\n <button type=\"button\" className={styles.reset} onClick={reset} disabled={!natural}>\n Centralizar\n </button>\n </div>\n\n <p id={hintId} className={styles.hint}>\n Arraste para reposicionar. Setas movem, <kbd>+</kbd> e <kbd>−</kbd> dão zoom,{\" \"}\n <kbd>0</kbd> centraliza.\n </p>\n </div>\n );\n}\n"],"mappings":"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"}