@zentauri-ui/zentauri-components 2.2.2 → 2.3.1
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/README.md +10 -6
- package/cli/props.json +526 -0
- package/cli/registry.json +11 -0
- package/dist/chunk-4U6PVVST.mjs +15 -0
- package/dist/chunk-4U6PVVST.mjs.map +1 -0
- package/dist/{chunk-DIAA5VH4.mjs → chunk-CIZQQ32L.mjs} +3 -3
- package/dist/chunk-CIZQQ32L.mjs.map +1 -0
- package/dist/chunk-EZNR7VLJ.js +65 -0
- package/dist/chunk-EZNR7VLJ.js.map +1 -0
- package/dist/{chunk-PQ2XTY3M.js → chunk-G36ZV446.js} +13 -13
- package/dist/{chunk-PQ2XTY3M.js.map → chunk-G36ZV446.js.map} +1 -1
- package/dist/{chunk-H3BJOK22.js → chunk-HNAUUCR5.js} +3 -3
- package/dist/chunk-HNAUUCR5.js.map +1 -0
- package/dist/{chunk-UZ6Y5CSV.js → chunk-K7UU3K54.js} +39 -18
- package/dist/chunk-K7UU3K54.js.map +1 -0
- package/dist/chunk-MY3DQVNF.js +19 -0
- package/dist/{chunk-ILPPXWR3.js.map → chunk-MY3DQVNF.js.map} +1 -1
- package/dist/{chunk-ATE5SCTR.mjs → chunk-N6Q4ZLQR.mjs} +3 -3
- package/dist/{chunk-ATE5SCTR.mjs.map → chunk-N6Q4ZLQR.mjs.map} +1 -1
- package/dist/{chunk-GFE6ZX5Y.mjs → chunk-PJATBFEK.mjs} +30 -9
- package/dist/chunk-PJATBFEK.mjs.map +1 -0
- package/dist/chunk-PVVYOIU2.js +38 -0
- package/dist/chunk-PVVYOIU2.js.map +1 -0
- package/dist/chunk-PWEZB53R.js +90 -0
- package/dist/chunk-PWEZB53R.js.map +1 -0
- package/dist/chunk-PWYEC3KY.mjs +30 -0
- package/dist/chunk-PWYEC3KY.mjs.map +1 -0
- package/dist/{chunk-5I4GAURE.js → chunk-RBNZNWYQ.js} +6 -6
- package/dist/{chunk-5I4GAURE.js.map → chunk-RBNZNWYQ.js.map} +1 -1
- package/dist/chunk-RDYR4DHG.mjs +62 -0
- package/dist/chunk-RDYR4DHG.mjs.map +1 -0
- package/dist/chunk-RWF3NVZP.mjs +29 -0
- package/dist/chunk-RWF3NVZP.mjs.map +1 -0
- package/dist/{chunk-NUV2I337.mjs → chunk-STWXN5EM.mjs} +3 -3
- package/dist/{chunk-NUV2I337.mjs.map → chunk-STWXN5EM.mjs.map} +1 -1
- package/dist/{chunk-W5MTZJPE.mjs → chunk-TTKTPERV.mjs} +3 -3
- package/dist/{chunk-W5MTZJPE.mjs.map → chunk-TTKTPERV.mjs.map} +1 -1
- package/dist/chunk-UBQY572I.mjs +81 -0
- package/dist/chunk-UBQY572I.mjs.map +1 -0
- package/dist/{chunk-IY72Z65Z.js → chunk-UN5RRNPV.js} +12 -12
- package/dist/{chunk-IY72Z65Z.js.map → chunk-UN5RRNPV.js.map} +1 -1
- package/dist/{chunk-N2G7IWHS.mjs → chunk-VMVG2RVZ.mjs} +4 -4
- package/dist/{chunk-N2G7IWHS.mjs.map → chunk-VMVG2RVZ.mjs.map} +1 -1
- package/dist/chunk-XUK5P37Y.js +19 -0
- package/dist/chunk-XUK5P37Y.js.map +1 -0
- package/dist/chunk-YRQN3AV4.js +38 -0
- package/dist/chunk-YRQN3AV4.js.map +1 -0
- package/dist/design-system/facade.js +12 -9
- package/dist/design-system/facade.js.map +1 -1
- package/dist/design-system/facade.mjs +11 -8
- package/dist/design-system/facade.mjs.map +1 -1
- package/dist/design-system/hash-generator.d.ts +15 -0
- package/dist/design-system/hash-generator.d.ts.map +1 -0
- package/dist/design-system/index.d.ts +3 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/qr-code.d.ts +4 -0
- package/dist/design-system/qr-code.d.ts.map +1 -0
- package/dist/design-system/qr-scanner.d.ts +11 -0
- package/dist/design-system/qr-scanner.d.ts.map +1 -0
- package/dist/design-system/secret-reveal.d.ts +1 -1
- package/dist/design-system/secret-reveal.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useHash/index.d.ts +2 -0
- package/dist/hooks/useHash/index.d.ts.map +1 -0
- package/dist/hooks/useHash/useHash.d.ts +20 -0
- package/dist/hooks/useHash/useHash.d.ts.map +1 -0
- package/dist/hooks/useHash.js +18 -0
- package/dist/hooks/useHash.js.map +1 -0
- package/dist/hooks/useHash.mjs +5 -0
- package/dist/hooks/useHash.mjs.map +1 -0
- package/dist/ui/buttons/animated.js +14 -11
- package/dist/ui/buttons/animated.js.map +1 -1
- package/dist/ui/buttons/animated.mjs +12 -9
- package/dist/ui/buttons/animated.mjs.map +1 -1
- package/dist/ui/buttons.js +15 -12
- package/dist/ui/buttons.mjs +13 -10
- package/dist/ui/data-table.js +24 -21
- package/dist/ui/data-table.js.map +1 -1
- package/dist/ui/data-table.mjs +14 -11
- package/dist/ui/data-table.mjs.map +1 -1
- package/dist/ui/dynamic-stepper.js +24 -21
- package/dist/ui/dynamic-stepper.js.map +1 -1
- package/dist/ui/dynamic-stepper.mjs +13 -10
- package/dist/ui/dynamic-stepper.mjs.map +1 -1
- package/dist/ui/hash-generator/hash-generator-base.d.ts +6 -0
- package/dist/ui/hash-generator/hash-generator-base.d.ts.map +1 -0
- package/dist/ui/hash-generator/hash-generator.d.ts +2 -0
- package/dist/ui/hash-generator/hash-generator.d.ts.map +1 -0
- package/dist/ui/hash-generator/index.d.ts +5 -0
- package/dist/ui/hash-generator/index.d.ts.map +1 -0
- package/dist/ui/hash-generator/types.d.ts +17 -0
- package/dist/ui/hash-generator/types.d.ts.map +1 -0
- package/dist/ui/hash-generator/variants.d.ts +10 -0
- package/dist/ui/hash-generator/variants.d.ts.map +1 -0
- package/dist/ui/hash-generator.js +126 -0
- package/dist/ui/hash-generator.js.map +1 -0
- package/dist/ui/hash-generator.mjs +117 -0
- package/dist/ui/hash-generator.mjs.map +1 -0
- package/dist/ui/pagination.js +16 -13
- package/dist/ui/pagination.mjs +13 -10
- package/dist/ui/qr-code/animated/animations.d.ts +8 -0
- package/dist/ui/qr-code/animated/animations.d.ts.map +1 -0
- package/dist/ui/qr-code/animated/index.d.ts +4 -0
- package/dist/ui/qr-code/animated/index.d.ts.map +1 -0
- package/dist/ui/qr-code/animated/qr-code-animated.d.ts +6 -0
- package/dist/ui/qr-code/animated/qr-code-animated.d.ts.map +1 -0
- package/dist/ui/qr-code/animated/types.d.ts +9 -0
- package/dist/ui/qr-code/animated/types.d.ts.map +1 -0
- package/dist/ui/qr-code/animated.js +156 -0
- package/dist/ui/qr-code/animated.js.map +1 -0
- package/dist/ui/qr-code/animated.mjs +149 -0
- package/dist/ui/qr-code/animated.mjs.map +1 -0
- package/dist/ui/qr-code/index.d.ts +5 -0
- package/dist/ui/qr-code/index.d.ts.map +1 -0
- package/dist/ui/qr-code/qr-code-base.d.ts +47 -0
- package/dist/ui/qr-code/qr-code-base.d.ts.map +1 -0
- package/dist/ui/qr-code/qr-code.d.ts +2 -0
- package/dist/ui/qr-code/qr-code.d.ts.map +1 -0
- package/dist/ui/qr-code/types.d.ts +14 -0
- package/dist/ui/qr-code/types.d.ts.map +1 -0
- package/dist/ui/qr-code/variants.d.ts +4 -0
- package/dist/ui/qr-code/variants.d.ts.map +1 -0
- package/dist/ui/qr-code.js +35 -0
- package/dist/ui/qr-code.js.map +1 -0
- package/dist/ui/qr-code.mjs +17 -0
- package/dist/ui/qr-code.mjs.map +1 -0
- package/dist/ui/qr-scanner/index.d.ts +4 -0
- package/dist/ui/qr-scanner/index.d.ts.map +1 -0
- package/dist/ui/qr-scanner/qr-scanner-base.d.ts +62 -0
- package/dist/ui/qr-scanner/qr-scanner-base.d.ts.map +1 -0
- package/dist/ui/qr-scanner/qr-scanner.d.ts +2 -0
- package/dist/ui/qr-scanner/qr-scanner.d.ts.map +1 -0
- package/dist/ui/qr-scanner/types.d.ts +28 -0
- package/dist/ui/qr-scanner/types.d.ts.map +1 -0
- package/dist/ui/qr-scanner/variants.d.ts +9 -0
- package/dist/ui/qr-scanner/variants.d.ts.map +1 -0
- package/dist/ui/qr-scanner.js +316 -0
- package/dist/ui/qr-scanner.js.map +1 -0
- package/dist/ui/qr-scanner.mjs +308 -0
- package/dist/ui/qr-scanner.mjs.map +1 -0
- package/dist/ui/secret-reveal/animated/secret-reveal-animated.d.ts.map +1 -1
- package/dist/ui/secret-reveal/animated.js +10 -7
- package/dist/ui/secret-reveal/animated.js.map +1 -1
- package/dist/ui/secret-reveal/animated.mjs +6 -3
- package/dist/ui/secret-reveal/animated.mjs.map +1 -1
- package/dist/ui/secret-reveal/secret-reveal-base.d.ts.map +1 -1
- package/dist/ui/secret-reveal.js +14 -11
- package/dist/ui/secret-reveal.js.map +1 -1
- package/dist/ui/secret-reveal.mjs +7 -4
- package/dist/ui/secret-reveal.mjs.map +1 -1
- package/dist/ui/split-button.js +26 -23
- package/dist/ui/split-button.js.map +1 -1
- package/dist/ui/split-button.mjs +13 -10
- package/dist/ui/split-button.mjs.map +1 -1
- package/package.json +5 -2
- package/src/design-system/hash-generator.ts +34 -0
- package/src/design-system/index.ts +3 -0
- package/src/design-system/qr-code.ts +13 -0
- package/src/design-system/qr-scanner.ts +32 -0
- package/src/design-system/secret-reveal.ts +1 -1
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useHash/index.ts +6 -0
- package/src/hooks/useHash/useHash.test.ts +77 -0
- package/src/hooks/useHash/useHash.ts +89 -0
- package/src/ui/hash-generator/hash-generator-base.tsx +106 -0
- package/src/ui/hash-generator/hash-generator.test.tsx +73 -0
- package/src/ui/hash-generator/hash-generator.tsx +1 -0
- package/src/ui/hash-generator/index.ts +18 -0
- package/src/ui/hash-generator/types.ts +29 -0
- package/src/ui/hash-generator/variants.ts +31 -0
- package/src/ui/qr-code/animated/animations.ts +51 -0
- package/src/ui/qr-code/animated/index.ts +5 -0
- package/src/ui/qr-code/animated/qr-code-animated.tsx +111 -0
- package/src/ui/qr-code/animated/types.ts +10 -0
- package/src/ui/qr-code/index.ts +10 -0
- package/src/ui/qr-code/qr-code-base.tsx +149 -0
- package/src/ui/qr-code/qr-code.test.tsx +58 -0
- package/src/ui/qr-code/qr-code.tsx +2 -0
- package/src/ui/qr-code/types.ts +22 -0
- package/src/ui/qr-code/variants.ts +11 -0
- package/src/ui/qr-scanner/index.ts +17 -0
- package/src/ui/qr-scanner/qr-scanner-base.tsx +568 -0
- package/src/ui/qr-scanner/qr-scanner.test.tsx +61 -0
- package/src/ui/qr-scanner/qr-scanner.tsx +2 -0
- package/src/ui/qr-scanner/types.ts +32 -0
- package/src/ui/qr-scanner/variants.ts +26 -0
- package/src/ui/secret-reveal/animated/secret-reveal-animated.tsx +4 -1
- package/src/ui/secret-reveal/secret-reveal-base.tsx +4 -1
- package/dist/chunk-DIAA5VH4.mjs.map +0 -1
- package/dist/chunk-GFE6ZX5Y.mjs.map +0 -1
- package/dist/chunk-H3BJOK22.js.map +0 -1
- package/dist/chunk-ILPPXWR3.js +0 -19
- package/dist/chunk-UZ6Y5CSV.js.map +0 -1
package/src/hooks/index.ts
CHANGED
|
@@ -53,6 +53,12 @@ export {
|
|
|
53
53
|
type UseDocumentTitleParams,
|
|
54
54
|
} from "./useDocumentTitle";
|
|
55
55
|
export { useHover } from "./useHover";
|
|
56
|
+
export {
|
|
57
|
+
useHash,
|
|
58
|
+
type HashGeneratorAlgorithm,
|
|
59
|
+
type UseHashResult,
|
|
60
|
+
ALGORITHM_LABELS,
|
|
61
|
+
} from "./useHash";
|
|
56
62
|
export {
|
|
57
63
|
useIdleTimeout,
|
|
58
64
|
type UseIdleTimeoutParams,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { useHash } from "./useHash";
|
|
5
|
+
|
|
6
|
+
vi.stubGlobal("crypto", {
|
|
7
|
+
subtle: {
|
|
8
|
+
digest: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4]).buffer),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("useHash", () => {
|
|
13
|
+
it("should return empty hash for empty input", () => {
|
|
14
|
+
const { result } = renderHook(() => useHash(""));
|
|
15
|
+
expect(result.current.hash).toBe("");
|
|
16
|
+
expect(result.current.isHashing).toBe(false);
|
|
17
|
+
expect(result.current.error).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should compute hash for non-empty input", async () => {
|
|
21
|
+
const { result } = renderHook(() => useHash("hello"));
|
|
22
|
+
await vi.waitFor(() => {
|
|
23
|
+
expect(result.current.hash).toBe("01020304");
|
|
24
|
+
});
|
|
25
|
+
expect(result.current.isHashing).toBe(false);
|
|
26
|
+
expect(result.current.error).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should recompute when algorithm changes", async () => {
|
|
30
|
+
const { result, rerender } = renderHook(
|
|
31
|
+
({ input, algo }: { input: string; algo: "sha256" | "sha512" }) =>
|
|
32
|
+
useHash(input, algo),
|
|
33
|
+
{ initialProps: { input: "hello", algo: "sha256" } },
|
|
34
|
+
);
|
|
35
|
+
await vi.waitFor(() => {
|
|
36
|
+
expect(result.current.hash).toBe("01020304");
|
|
37
|
+
});
|
|
38
|
+
rerender({ input: "hello", algo: "sha512" });
|
|
39
|
+
expect(result.current.isHashing).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should recompute when input changes", async () => {
|
|
43
|
+
const { result, rerender } = renderHook(
|
|
44
|
+
({ input }: { input: string }) => useHash(input),
|
|
45
|
+
{ initialProps: { input: "hello" } },
|
|
46
|
+
);
|
|
47
|
+
await vi.waitFor(() => {
|
|
48
|
+
expect(result.current.hash).toBe("01020304");
|
|
49
|
+
});
|
|
50
|
+
rerender({ input: "world" });
|
|
51
|
+
expect(result.current.isHashing).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should call recompute on demand", async () => {
|
|
55
|
+
const { result } = renderHook(() => useHash("hello"));
|
|
56
|
+
await vi.waitFor(() => {
|
|
57
|
+
expect(result.current.hash).toBe("01020304");
|
|
58
|
+
});
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.recompute();
|
|
61
|
+
});
|
|
62
|
+
expect(result.current.isHashing).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should clear hash when input becomes empty", async () => {
|
|
66
|
+
const { result, rerender } = renderHook(
|
|
67
|
+
({ input }: { input: string }) => useHash(input),
|
|
68
|
+
{ initialProps: { input: "hello" } },
|
|
69
|
+
);
|
|
70
|
+
await vi.waitFor(() => {
|
|
71
|
+
expect(result.current.hash).toBe("01020304");
|
|
72
|
+
});
|
|
73
|
+
rerender({ input: "" });
|
|
74
|
+
expect(result.current.hash).toBe("");
|
|
75
|
+
expect(result.current.isHashing).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export type HashGeneratorAlgorithm = "sha1" | "sha256" | "sha384" | "sha512";
|
|
6
|
+
|
|
7
|
+
export const ALGORITHM_LABELS: Record<HashGeneratorAlgorithm, string> = {
|
|
8
|
+
sha1: "SHA-1",
|
|
9
|
+
sha256: "SHA-256",
|
|
10
|
+
sha384: "SHA-384",
|
|
11
|
+
sha512: "SHA-512",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function computeHash(
|
|
15
|
+
algorithm: HashGeneratorAlgorithm,
|
|
16
|
+
input: string,
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
19
|
+
throw new Error("Web Crypto API is not supported in this environment.");
|
|
20
|
+
}
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const data = encoder.encode(input);
|
|
23
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
24
|
+
algorithm.toUpperCase().replace("SHA", "SHA-") as AlgorithmIdentifier,
|
|
25
|
+
data,
|
|
26
|
+
);
|
|
27
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
28
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type UseHashResult = {
|
|
32
|
+
hash: string;
|
|
33
|
+
isHashing: boolean;
|
|
34
|
+
error: Error | undefined;
|
|
35
|
+
recompute: () => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Computes a cryptographic hash of `input` using the Web Crypto API.
|
|
40
|
+
*
|
|
41
|
+
* Automatically recomputes whenever `input` or `algorithm` changes.
|
|
42
|
+
* Returns `{ hash, isHashing, error, recompute }` for rendering.
|
|
43
|
+
*
|
|
44
|
+
* @param input - The string to hash.
|
|
45
|
+
* @param algorithm - Hash algorithm (default `"sha256"`).
|
|
46
|
+
* @returns `{ hash, isHashing, error, recompute }`
|
|
47
|
+
*/
|
|
48
|
+
export function useHash(
|
|
49
|
+
input: string,
|
|
50
|
+
algorithm: HashGeneratorAlgorithm = "sha256",
|
|
51
|
+
): UseHashResult {
|
|
52
|
+
const [hash, setHash] = useState("");
|
|
53
|
+
const [isHashing, setIsHashing] = useState(false);
|
|
54
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
55
|
+
const [trigger, setTrigger] = useState(0);
|
|
56
|
+
const nonceRef = useRef(0);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!input) {
|
|
60
|
+
setHash("");
|
|
61
|
+
setIsHashing(false);
|
|
62
|
+
setError(undefined);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const nonce = ++nonceRef.current;
|
|
66
|
+
setIsHashing(true);
|
|
67
|
+
setError(undefined);
|
|
68
|
+
computeHash(algorithm, input).then(
|
|
69
|
+
(result) => {
|
|
70
|
+
if (nonce === nonceRef.current) {
|
|
71
|
+
setHash(result);
|
|
72
|
+
setIsHashing(false);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
(err) => {
|
|
76
|
+
if (nonce === nonceRef.current) {
|
|
77
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
78
|
+
setIsHashing(false);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}, [algorithm, input, trigger]);
|
|
83
|
+
|
|
84
|
+
const recompute = useCallback(() => {
|
|
85
|
+
setTrigger((prev) => prev + 1);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return { hash, isHashing, error, recompute };
|
|
89
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { useHash } from "../../hooks/useHash";
|
|
7
|
+
|
|
8
|
+
import type { HashGeneratorBaseProps } from "./types";
|
|
9
|
+
import { ALGORITHM_LABELS } from "./types";
|
|
10
|
+
import {
|
|
11
|
+
hashGeneratorHeaderVariants,
|
|
12
|
+
hashGeneratorInputVariants,
|
|
13
|
+
hashGeneratorLabelVariants,
|
|
14
|
+
hashGeneratorOutputTextVariants,
|
|
15
|
+
hashGeneratorOutputVariants,
|
|
16
|
+
hashGeneratorVariants,
|
|
17
|
+
} from "./variants";
|
|
18
|
+
|
|
19
|
+
export function HashGeneratorBase({
|
|
20
|
+
className,
|
|
21
|
+
appearance,
|
|
22
|
+
size,
|
|
23
|
+
algorithm = "sha256",
|
|
24
|
+
value,
|
|
25
|
+
onValueChange,
|
|
26
|
+
readOnly = false,
|
|
27
|
+
showCopyButton = true,
|
|
28
|
+
ref,
|
|
29
|
+
...rest
|
|
30
|
+
}: HashGeneratorBaseProps) {
|
|
31
|
+
const [internalValue, setInternalValue] = useState("");
|
|
32
|
+
const [copied, setCopied] = useState(false);
|
|
33
|
+
|
|
34
|
+
const inputValue = value ?? internalValue;
|
|
35
|
+
const handleChange = onValueChange ?? setInternalValue;
|
|
36
|
+
|
|
37
|
+
const { hash, error } = useHash(inputValue, algorithm);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!copied) return;
|
|
41
|
+
const timeout = setTimeout(() => setCopied(false), 2000);
|
|
42
|
+
return () => clearTimeout(timeout);
|
|
43
|
+
}, [copied]);
|
|
44
|
+
|
|
45
|
+
const handleCopy = useCallback(async () => {
|
|
46
|
+
if (!hash) return;
|
|
47
|
+
try {
|
|
48
|
+
await navigator.clipboard.writeText(hash);
|
|
49
|
+
setCopied(true);
|
|
50
|
+
} catch {
|
|
51
|
+
// Clipboard API not available
|
|
52
|
+
}
|
|
53
|
+
}, [hash]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
ref={ref}
|
|
58
|
+
data-slot="hash-generator"
|
|
59
|
+
className={cn(hashGeneratorVariants({ appearance, size }), className)}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
<div className={hashGeneratorHeaderVariants()}>
|
|
63
|
+
<span className={hashGeneratorLabelVariants()}>
|
|
64
|
+
{ALGORITHM_LABELS[algorithm]}
|
|
65
|
+
</span>
|
|
66
|
+
{showCopyButton && hash ? (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={handleCopy}
|
|
70
|
+
className="rounded px-2 py-0.5 text-xs font-medium transition-colors text-[color:var(--zui-hash-generator-label-fg,var(--zui-fg-muted,oklch(55.2%_0.046_257.417)))] dark:text-[color:var(--zui-hash-generator-label-fg-dark,var(--zui-fg-muted-dark,oklch(70.8%_0.015_256.243)))] hover:bg-[var(--zui-hash-generator-header-bg,var(--zui-surface-muted,oklch(92.9%_0.013_255.508)))] dark:hover:bg-[var(--zui-hash-generator-header-bg-dark,var(--zui-surface-muted-dark,oklch(27.9%_0.041_260.031)))]"
|
|
71
|
+
>
|
|
72
|
+
{copied ? "Copied!" : "Copy"}
|
|
73
|
+
</button>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
<textarea
|
|
77
|
+
data-slot="hash-generator-input"
|
|
78
|
+
value={inputValue}
|
|
79
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
80
|
+
readOnly={readOnly}
|
|
81
|
+
placeholder="Enter text to hash..."
|
|
82
|
+
rows={3}
|
|
83
|
+
aria-label={`Input text to hash using ${ALGORITHM_LABELS[algorithm]}`}
|
|
84
|
+
className={cn(hashGeneratorInputVariants(), "resize-y min-h-[5rem]")}
|
|
85
|
+
/>
|
|
86
|
+
<div className={hashGeneratorOutputVariants()}>
|
|
87
|
+
<span
|
|
88
|
+
role="status"
|
|
89
|
+
data-slot="hash-generator-output"
|
|
90
|
+
className={cn(
|
|
91
|
+
hashGeneratorOutputTextVariants(),
|
|
92
|
+
!hash && !error && "opacity-40",
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{error ? (
|
|
96
|
+
<span className="text-red-500">{error.message}</span>
|
|
97
|
+
) : (
|
|
98
|
+
hash || "Hash output"
|
|
99
|
+
)}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
HashGeneratorBase.displayName = "HashGenerator";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { HashGenerator } from "./hash-generator";
|
|
7
|
+
|
|
8
|
+
// Mock crypto.subtle.digest for jsdom
|
|
9
|
+
vi.stubGlobal("crypto", {
|
|
10
|
+
subtle: {
|
|
11
|
+
digest: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4]).buffer),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("HashGenerator", () => {
|
|
16
|
+
it("should expose displayName", () => {
|
|
17
|
+
expect(HashGenerator.displayName).toBe("HashGenerator");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should stamp data-slot", () => {
|
|
21
|
+
render(<HashGenerator />);
|
|
22
|
+
const root = document.querySelector('[data-slot="hash-generator"]');
|
|
23
|
+
expect(root).toBeTruthy();
|
|
24
|
+
expect(root?.getAttribute("data-slot")).toBe("hash-generator");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render algorithm label", () => {
|
|
28
|
+
render(<HashGenerator algorithm="sha256" />);
|
|
29
|
+
expect(screen.getByText("SHA-256")).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should render with sha512 algorithm", () => {
|
|
33
|
+
render(<HashGenerator algorithm="sha512" />);
|
|
34
|
+
expect(screen.getByText("SHA-512")).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should render textarea for input", () => {
|
|
38
|
+
render(<HashGenerator />);
|
|
39
|
+
expect(
|
|
40
|
+
screen.getByPlaceholderText("Enter text to hash..."),
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should display hash output", async () => {
|
|
45
|
+
render(<HashGenerator />);
|
|
46
|
+
const textarea = screen.getByPlaceholderText("Enter text to hash...");
|
|
47
|
+
await userEvent.type(textarea, "hello");
|
|
48
|
+
const output = document.querySelector(
|
|
49
|
+
'[data-slot="hash-generator-output"]',
|
|
50
|
+
);
|
|
51
|
+
expect(output).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should forward ref", () => {
|
|
55
|
+
const ref = createRef<HTMLDivElement>();
|
|
56
|
+
render(<HashGenerator ref={ref} />);
|
|
57
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("hash-generator");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should accept controlled value via value prop", () => {
|
|
61
|
+
render(<HashGenerator value="test text" readOnly />);
|
|
62
|
+
const textarea = screen.getByPlaceholderText("Enter text to hash...");
|
|
63
|
+
expect(textarea).toHaveValue("test text");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should call onValueChange when typing", async () => {
|
|
67
|
+
const handleChange = vi.fn();
|
|
68
|
+
render(<HashGenerator onValueChange={handleChange} />);
|
|
69
|
+
const textarea = screen.getByPlaceholderText("Enter text to hash...");
|
|
70
|
+
await userEvent.type(textarea, "a");
|
|
71
|
+
expect(handleChange).toHaveBeenCalledWith("a");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HashGeneratorBase as HashGenerator } from "./hash-generator-base";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { HashGenerator } from "./hash-generator";
|
|
4
|
+
export type {
|
|
5
|
+
HashGeneratorAlgorithm,
|
|
6
|
+
HashGeneratorBaseProps,
|
|
7
|
+
HashGeneratorProps,
|
|
8
|
+
HashGeneratorVariantProps,
|
|
9
|
+
} from "./types";
|
|
10
|
+
export { ALGORITHM_LABELS } from "./types";
|
|
11
|
+
export {
|
|
12
|
+
hashGeneratorHeaderVariants,
|
|
13
|
+
hashGeneratorInputVariants,
|
|
14
|
+
hashGeneratorLabelVariants,
|
|
15
|
+
hashGeneratorOutputTextVariants,
|
|
16
|
+
hashGeneratorOutputVariants,
|
|
17
|
+
hashGeneratorVariants,
|
|
18
|
+
} from "./variants";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentPropsWithRef } from "react";
|
|
3
|
+
|
|
4
|
+
import type { hashGeneratorVariants } from "./variants";
|
|
5
|
+
|
|
6
|
+
export type HashGeneratorAlgorithm = "sha1" | "sha256" | "sha384" | "sha512";
|
|
7
|
+
|
|
8
|
+
export type HashGeneratorVariantProps = VariantProps<
|
|
9
|
+
typeof hashGeneratorVariants
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export interface HashGeneratorBaseProps extends ComponentPropsWithRef<"div"> {
|
|
13
|
+
algorithm?: HashGeneratorAlgorithm;
|
|
14
|
+
value?: string;
|
|
15
|
+
onValueChange?: (value: string) => void;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
showCopyButton?: boolean;
|
|
18
|
+
appearance?: HashGeneratorVariantProps["appearance"];
|
|
19
|
+
size?: HashGeneratorVariantProps["size"];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type HashGeneratorProps = HashGeneratorBaseProps;
|
|
23
|
+
|
|
24
|
+
export const ALGORITHM_LABELS: Record<HashGeneratorAlgorithm, string> = {
|
|
25
|
+
sha1: "SHA-1",
|
|
26
|
+
sha256: "SHA-256",
|
|
27
|
+
sha384: "SHA-384",
|
|
28
|
+
sha512: "SHA-512",
|
|
29
|
+
} as const;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiHashGeneratorAppearances,
|
|
5
|
+
zuiHashGeneratorBase,
|
|
6
|
+
zuiHashGeneratorHeaderBase,
|
|
7
|
+
zuiHashGeneratorInputBase,
|
|
8
|
+
zuiHashGeneratorLabelBase,
|
|
9
|
+
zuiHashGeneratorOutputBase,
|
|
10
|
+
zuiHashGeneratorOutputTextBase,
|
|
11
|
+
zuiHashGeneratorSizes,
|
|
12
|
+
} from "../../design-system/hash-generator";
|
|
13
|
+
|
|
14
|
+
export const hashGeneratorVariants = cva(zuiHashGeneratorBase, {
|
|
15
|
+
variants: {
|
|
16
|
+
appearance: zuiHashGeneratorAppearances,
|
|
17
|
+
size: zuiHashGeneratorSizes,
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
appearance: "default",
|
|
21
|
+
size: "md",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const hashGeneratorHeaderVariants = cva(zuiHashGeneratorHeaderBase);
|
|
26
|
+
export const hashGeneratorLabelVariants = cva(zuiHashGeneratorLabelBase);
|
|
27
|
+
export const hashGeneratorInputVariants = cva(zuiHashGeneratorInputBase);
|
|
28
|
+
export const hashGeneratorOutputVariants = cva(zuiHashGeneratorOutputBase);
|
|
29
|
+
export const hashGeneratorOutputTextVariants = cva(
|
|
30
|
+
zuiHashGeneratorOutputTextBase,
|
|
31
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Transition, Variants } from "framer-motion";
|
|
2
|
+
|
|
3
|
+
export type QrCodeAnimation =
|
|
4
|
+
| "none"
|
|
5
|
+
| "fade-in"
|
|
6
|
+
| "zoom-in"
|
|
7
|
+
| "slide-up"
|
|
8
|
+
| "rotate-in";
|
|
9
|
+
|
|
10
|
+
export type QrCodeAnimationPresets = Record<
|
|
11
|
+
QrCodeAnimation,
|
|
12
|
+
{ transition: Transition; variants: Variants }
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export const qrCodeAnimationPresets: QrCodeAnimationPresets = {
|
|
16
|
+
none: {
|
|
17
|
+
transition: { duration: 0 },
|
|
18
|
+
variants: {
|
|
19
|
+
initial: { opacity: 1 },
|
|
20
|
+
animate: { opacity: 1 },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
"fade-in": {
|
|
24
|
+
transition: { duration: 0.3, ease: "easeOut" },
|
|
25
|
+
variants: {
|
|
26
|
+
initial: { opacity: 0 },
|
|
27
|
+
animate: { opacity: 1 },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"zoom-in": {
|
|
31
|
+
transition: { type: "spring", stiffness: 300, damping: 24 },
|
|
32
|
+
variants: {
|
|
33
|
+
initial: { opacity: 0, scale: 0.85 },
|
|
34
|
+
animate: { opacity: 1, scale: 1 },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
"slide-up": {
|
|
38
|
+
transition: { type: "spring", stiffness: 280, damping: 22 },
|
|
39
|
+
variants: {
|
|
40
|
+
initial: { opacity: 0, y: 20 },
|
|
41
|
+
animate: { opacity: 1, y: 0 },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"rotate-in": {
|
|
45
|
+
transition: { type: "spring", stiffness: 200, damping: 18 },
|
|
46
|
+
variants: {
|
|
47
|
+
initial: { opacity: 0, rotate: -90, scale: 0.8 },
|
|
48
|
+
animate: { opacity: 1, rotate: 0, scale: 1 },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import QRCode from "qrcode";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../../lib/utils";
|
|
8
|
+
import { qrCodeAnimationPresets } from "./animations";
|
|
9
|
+
import type { QrCodeAnimatedProps } from "./types";
|
|
10
|
+
import { QrCodeBase } from "../qr-code-base";
|
|
11
|
+
|
|
12
|
+
export function QrCodeAnimated({
|
|
13
|
+
animation = "none",
|
|
14
|
+
value,
|
|
15
|
+
canvasSize = 200,
|
|
16
|
+
level = "M",
|
|
17
|
+
bgColor = "#ffffff",
|
|
18
|
+
fgColor = "#000000",
|
|
19
|
+
margin = 2,
|
|
20
|
+
caption,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: QrCodeAnimatedProps) {
|
|
24
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const canvas = canvasRef.current;
|
|
28
|
+
if (!canvas || animation === "none") return;
|
|
29
|
+
|
|
30
|
+
if (!value) {
|
|
31
|
+
const ctx = canvas.getContext("2d");
|
|
32
|
+
if (ctx) {
|
|
33
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
QRCode.toCanvas(
|
|
39
|
+
canvas,
|
|
40
|
+
value,
|
|
41
|
+
{
|
|
42
|
+
width: canvasSize,
|
|
43
|
+
margin,
|
|
44
|
+
color: {
|
|
45
|
+
dark: fgColor,
|
|
46
|
+
light: bgColor,
|
|
47
|
+
},
|
|
48
|
+
errorCorrectionLevel: level,
|
|
49
|
+
},
|
|
50
|
+
(error) => {
|
|
51
|
+
if (error) {
|
|
52
|
+
console.error("QR Code generation error:", error);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
}, [value, canvasSize, level, bgColor, fgColor, margin, animation]);
|
|
57
|
+
|
|
58
|
+
if (animation === "none") {
|
|
59
|
+
return (
|
|
60
|
+
<QrCodeBase
|
|
61
|
+
value={value}
|
|
62
|
+
canvasSize={canvasSize}
|
|
63
|
+
level={level}
|
|
64
|
+
bgColor={bgColor}
|
|
65
|
+
fgColor={fgColor}
|
|
66
|
+
margin={margin}
|
|
67
|
+
caption={caption}
|
|
68
|
+
className={className}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const preset = qrCodeAnimationPresets[animation];
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<motion.div
|
|
78
|
+
data-slot="qr-code"
|
|
79
|
+
className={cn(
|
|
80
|
+
"inline-flex flex-col items-center gap-3 rounded-2xl border p-6 border-[color:var(--zui-qr-code-border,var(--zui-border,#0000001a))] dark:border-[color:var(--zui-qr-code-border-dark,var(--zui-border-dark,#ffffff1a))] bg-[var(--zui-qr-code-bg,var(--zui-surface,oklch(98.4%_0.003_247.858)))] dark:bg-[var(--zui-qr-code-bg-dark,var(--zui-surface-dark,oklch(12.9%_0.042_264.695)))]",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
variants={preset.variants}
|
|
84
|
+
initial="initial"
|
|
85
|
+
animate="animate"
|
|
86
|
+
transition={preset.transition}
|
|
87
|
+
{...(props as Record<string, unknown>)}
|
|
88
|
+
>
|
|
89
|
+
<div className="overflow-hidden rounded-xl bg-[var(--zui-qr-code-canvas-bg,var(--zui-surface-muted,oklch(92.9%_0.013_255.508)))] dark:bg-[var(--zui-qr-code-canvas-bg-dark,var(--zui-surface-muted-dark,oklch(27.9%_0.041_260.031)))]">
|
|
90
|
+
<canvas
|
|
91
|
+
ref={canvasRef}
|
|
92
|
+
width={canvasSize}
|
|
93
|
+
height={canvasSize}
|
|
94
|
+
data-slot="qr-code-canvas"
|
|
95
|
+
className="block h-auto max-w-full"
|
|
96
|
+
aria-label={`QR code for ${value}`}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
{caption ? (
|
|
100
|
+
<span
|
|
101
|
+
data-slot="qr-code-caption"
|
|
102
|
+
className="text-xs text-center text-[color:var(--zui-qr-code-caption-fg,var(--zui-fg-muted,oklch(55.2%_0.046_257.417)))] dark:text-[color:var(--zui-qr-code-caption-fg-dark,var(--zui-fg-muted-dark,oklch(70.8%_0.015_256.243)))] max-w-full truncate px-2"
|
|
103
|
+
>
|
|
104
|
+
{caption}
|
|
105
|
+
</span>
|
|
106
|
+
) : null}
|
|
107
|
+
</motion.div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
QrCodeAnimated.displayName = "QrCodeAnimated";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Ref } from "react";
|
|
2
|
+
import type { QrCodeBaseProps } from "../types";
|
|
3
|
+
import type { QrCodeAnimation } from "./animations";
|
|
4
|
+
|
|
5
|
+
export type { QrCodeAnimation };
|
|
6
|
+
|
|
7
|
+
export type QrCodeAnimatedProps = QrCodeBaseProps & {
|
|
8
|
+
animation?: QrCodeAnimation;
|
|
9
|
+
ref?: Ref<HTMLDivElement>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { QrCode } from "./qr-code";
|
|
4
|
+
export type { QrCodeBaseProps, QrCodeLevel, QrCodeProps } from "./types";
|
|
5
|
+
export { QR_CODE_LEVEL_LABELS } from "./types";
|
|
6
|
+
export {
|
|
7
|
+
qrCodeCanvasWrapperVariants,
|
|
8
|
+
qrCodeCaptionVariants,
|
|
9
|
+
qrCodeVariants,
|
|
10
|
+
} from "./variants";
|