@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.
Files changed (194) hide show
  1. package/README.md +10 -6
  2. package/cli/props.json +526 -0
  3. package/cli/registry.json +11 -0
  4. package/dist/chunk-4U6PVVST.mjs +15 -0
  5. package/dist/chunk-4U6PVVST.mjs.map +1 -0
  6. package/dist/{chunk-DIAA5VH4.mjs → chunk-CIZQQ32L.mjs} +3 -3
  7. package/dist/chunk-CIZQQ32L.mjs.map +1 -0
  8. package/dist/chunk-EZNR7VLJ.js +65 -0
  9. package/dist/chunk-EZNR7VLJ.js.map +1 -0
  10. package/dist/{chunk-PQ2XTY3M.js → chunk-G36ZV446.js} +13 -13
  11. package/dist/{chunk-PQ2XTY3M.js.map → chunk-G36ZV446.js.map} +1 -1
  12. package/dist/{chunk-H3BJOK22.js → chunk-HNAUUCR5.js} +3 -3
  13. package/dist/chunk-HNAUUCR5.js.map +1 -0
  14. package/dist/{chunk-UZ6Y5CSV.js → chunk-K7UU3K54.js} +39 -18
  15. package/dist/chunk-K7UU3K54.js.map +1 -0
  16. package/dist/chunk-MY3DQVNF.js +19 -0
  17. package/dist/{chunk-ILPPXWR3.js.map → chunk-MY3DQVNF.js.map} +1 -1
  18. package/dist/{chunk-ATE5SCTR.mjs → chunk-N6Q4ZLQR.mjs} +3 -3
  19. package/dist/{chunk-ATE5SCTR.mjs.map → chunk-N6Q4ZLQR.mjs.map} +1 -1
  20. package/dist/{chunk-GFE6ZX5Y.mjs → chunk-PJATBFEK.mjs} +30 -9
  21. package/dist/chunk-PJATBFEK.mjs.map +1 -0
  22. package/dist/chunk-PVVYOIU2.js +38 -0
  23. package/dist/chunk-PVVYOIU2.js.map +1 -0
  24. package/dist/chunk-PWEZB53R.js +90 -0
  25. package/dist/chunk-PWEZB53R.js.map +1 -0
  26. package/dist/chunk-PWYEC3KY.mjs +30 -0
  27. package/dist/chunk-PWYEC3KY.mjs.map +1 -0
  28. package/dist/{chunk-5I4GAURE.js → chunk-RBNZNWYQ.js} +6 -6
  29. package/dist/{chunk-5I4GAURE.js.map → chunk-RBNZNWYQ.js.map} +1 -1
  30. package/dist/chunk-RDYR4DHG.mjs +62 -0
  31. package/dist/chunk-RDYR4DHG.mjs.map +1 -0
  32. package/dist/chunk-RWF3NVZP.mjs +29 -0
  33. package/dist/chunk-RWF3NVZP.mjs.map +1 -0
  34. package/dist/{chunk-NUV2I337.mjs → chunk-STWXN5EM.mjs} +3 -3
  35. package/dist/{chunk-NUV2I337.mjs.map → chunk-STWXN5EM.mjs.map} +1 -1
  36. package/dist/{chunk-W5MTZJPE.mjs → chunk-TTKTPERV.mjs} +3 -3
  37. package/dist/{chunk-W5MTZJPE.mjs.map → chunk-TTKTPERV.mjs.map} +1 -1
  38. package/dist/chunk-UBQY572I.mjs +81 -0
  39. package/dist/chunk-UBQY572I.mjs.map +1 -0
  40. package/dist/{chunk-IY72Z65Z.js → chunk-UN5RRNPV.js} +12 -12
  41. package/dist/{chunk-IY72Z65Z.js.map → chunk-UN5RRNPV.js.map} +1 -1
  42. package/dist/{chunk-N2G7IWHS.mjs → chunk-VMVG2RVZ.mjs} +4 -4
  43. package/dist/{chunk-N2G7IWHS.mjs.map → chunk-VMVG2RVZ.mjs.map} +1 -1
  44. package/dist/chunk-XUK5P37Y.js +19 -0
  45. package/dist/chunk-XUK5P37Y.js.map +1 -0
  46. package/dist/chunk-YRQN3AV4.js +38 -0
  47. package/dist/chunk-YRQN3AV4.js.map +1 -0
  48. package/dist/design-system/facade.js +12 -9
  49. package/dist/design-system/facade.js.map +1 -1
  50. package/dist/design-system/facade.mjs +11 -8
  51. package/dist/design-system/facade.mjs.map +1 -1
  52. package/dist/design-system/hash-generator.d.ts +15 -0
  53. package/dist/design-system/hash-generator.d.ts.map +1 -0
  54. package/dist/design-system/index.d.ts +3 -0
  55. package/dist/design-system/index.d.ts.map +1 -1
  56. package/dist/design-system/qr-code.d.ts +4 -0
  57. package/dist/design-system/qr-code.d.ts.map +1 -0
  58. package/dist/design-system/qr-scanner.d.ts +11 -0
  59. package/dist/design-system/qr-scanner.d.ts.map +1 -0
  60. package/dist/design-system/secret-reveal.d.ts +1 -1
  61. package/dist/design-system/secret-reveal.d.ts.map +1 -1
  62. package/dist/hooks/index.d.ts +1 -0
  63. package/dist/hooks/index.d.ts.map +1 -1
  64. package/dist/hooks/useHash/index.d.ts +2 -0
  65. package/dist/hooks/useHash/index.d.ts.map +1 -0
  66. package/dist/hooks/useHash/useHash.d.ts +20 -0
  67. package/dist/hooks/useHash/useHash.d.ts.map +1 -0
  68. package/dist/hooks/useHash.js +18 -0
  69. package/dist/hooks/useHash.js.map +1 -0
  70. package/dist/hooks/useHash.mjs +5 -0
  71. package/dist/hooks/useHash.mjs.map +1 -0
  72. package/dist/ui/buttons/animated.js +14 -11
  73. package/dist/ui/buttons/animated.js.map +1 -1
  74. package/dist/ui/buttons/animated.mjs +12 -9
  75. package/dist/ui/buttons/animated.mjs.map +1 -1
  76. package/dist/ui/buttons.js +15 -12
  77. package/dist/ui/buttons.mjs +13 -10
  78. package/dist/ui/data-table.js +24 -21
  79. package/dist/ui/data-table.js.map +1 -1
  80. package/dist/ui/data-table.mjs +14 -11
  81. package/dist/ui/data-table.mjs.map +1 -1
  82. package/dist/ui/dynamic-stepper.js +24 -21
  83. package/dist/ui/dynamic-stepper.js.map +1 -1
  84. package/dist/ui/dynamic-stepper.mjs +13 -10
  85. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  86. package/dist/ui/hash-generator/hash-generator-base.d.ts +6 -0
  87. package/dist/ui/hash-generator/hash-generator-base.d.ts.map +1 -0
  88. package/dist/ui/hash-generator/hash-generator.d.ts +2 -0
  89. package/dist/ui/hash-generator/hash-generator.d.ts.map +1 -0
  90. package/dist/ui/hash-generator/index.d.ts +5 -0
  91. package/dist/ui/hash-generator/index.d.ts.map +1 -0
  92. package/dist/ui/hash-generator/types.d.ts +17 -0
  93. package/dist/ui/hash-generator/types.d.ts.map +1 -0
  94. package/dist/ui/hash-generator/variants.d.ts +10 -0
  95. package/dist/ui/hash-generator/variants.d.ts.map +1 -0
  96. package/dist/ui/hash-generator.js +126 -0
  97. package/dist/ui/hash-generator.js.map +1 -0
  98. package/dist/ui/hash-generator.mjs +117 -0
  99. package/dist/ui/hash-generator.mjs.map +1 -0
  100. package/dist/ui/pagination.js +16 -13
  101. package/dist/ui/pagination.mjs +13 -10
  102. package/dist/ui/qr-code/animated/animations.d.ts +8 -0
  103. package/dist/ui/qr-code/animated/animations.d.ts.map +1 -0
  104. package/dist/ui/qr-code/animated/index.d.ts +4 -0
  105. package/dist/ui/qr-code/animated/index.d.ts.map +1 -0
  106. package/dist/ui/qr-code/animated/qr-code-animated.d.ts +6 -0
  107. package/dist/ui/qr-code/animated/qr-code-animated.d.ts.map +1 -0
  108. package/dist/ui/qr-code/animated/types.d.ts +9 -0
  109. package/dist/ui/qr-code/animated/types.d.ts.map +1 -0
  110. package/dist/ui/qr-code/animated.js +156 -0
  111. package/dist/ui/qr-code/animated.js.map +1 -0
  112. package/dist/ui/qr-code/animated.mjs +149 -0
  113. package/dist/ui/qr-code/animated.mjs.map +1 -0
  114. package/dist/ui/qr-code/index.d.ts +5 -0
  115. package/dist/ui/qr-code/index.d.ts.map +1 -0
  116. package/dist/ui/qr-code/qr-code-base.d.ts +47 -0
  117. package/dist/ui/qr-code/qr-code-base.d.ts.map +1 -0
  118. package/dist/ui/qr-code/qr-code.d.ts +2 -0
  119. package/dist/ui/qr-code/qr-code.d.ts.map +1 -0
  120. package/dist/ui/qr-code/types.d.ts +14 -0
  121. package/dist/ui/qr-code/types.d.ts.map +1 -0
  122. package/dist/ui/qr-code/variants.d.ts +4 -0
  123. package/dist/ui/qr-code/variants.d.ts.map +1 -0
  124. package/dist/ui/qr-code.js +35 -0
  125. package/dist/ui/qr-code.js.map +1 -0
  126. package/dist/ui/qr-code.mjs +17 -0
  127. package/dist/ui/qr-code.mjs.map +1 -0
  128. package/dist/ui/qr-scanner/index.d.ts +4 -0
  129. package/dist/ui/qr-scanner/index.d.ts.map +1 -0
  130. package/dist/ui/qr-scanner/qr-scanner-base.d.ts +62 -0
  131. package/dist/ui/qr-scanner/qr-scanner-base.d.ts.map +1 -0
  132. package/dist/ui/qr-scanner/qr-scanner.d.ts +2 -0
  133. package/dist/ui/qr-scanner/qr-scanner.d.ts.map +1 -0
  134. package/dist/ui/qr-scanner/types.d.ts +28 -0
  135. package/dist/ui/qr-scanner/types.d.ts.map +1 -0
  136. package/dist/ui/qr-scanner/variants.d.ts +9 -0
  137. package/dist/ui/qr-scanner/variants.d.ts.map +1 -0
  138. package/dist/ui/qr-scanner.js +316 -0
  139. package/dist/ui/qr-scanner.js.map +1 -0
  140. package/dist/ui/qr-scanner.mjs +308 -0
  141. package/dist/ui/qr-scanner.mjs.map +1 -0
  142. package/dist/ui/secret-reveal/animated/secret-reveal-animated.d.ts.map +1 -1
  143. package/dist/ui/secret-reveal/animated.js +10 -7
  144. package/dist/ui/secret-reveal/animated.js.map +1 -1
  145. package/dist/ui/secret-reveal/animated.mjs +6 -3
  146. package/dist/ui/secret-reveal/animated.mjs.map +1 -1
  147. package/dist/ui/secret-reveal/secret-reveal-base.d.ts.map +1 -1
  148. package/dist/ui/secret-reveal.js +14 -11
  149. package/dist/ui/secret-reveal.js.map +1 -1
  150. package/dist/ui/secret-reveal.mjs +7 -4
  151. package/dist/ui/secret-reveal.mjs.map +1 -1
  152. package/dist/ui/split-button.js +26 -23
  153. package/dist/ui/split-button.js.map +1 -1
  154. package/dist/ui/split-button.mjs +13 -10
  155. package/dist/ui/split-button.mjs.map +1 -1
  156. package/package.json +5 -2
  157. package/src/design-system/hash-generator.ts +34 -0
  158. package/src/design-system/index.ts +3 -0
  159. package/src/design-system/qr-code.ts +13 -0
  160. package/src/design-system/qr-scanner.ts +32 -0
  161. package/src/design-system/secret-reveal.ts +1 -1
  162. package/src/hooks/index.ts +6 -0
  163. package/src/hooks/useHash/index.ts +6 -0
  164. package/src/hooks/useHash/useHash.test.ts +77 -0
  165. package/src/hooks/useHash/useHash.ts +89 -0
  166. package/src/ui/hash-generator/hash-generator-base.tsx +106 -0
  167. package/src/ui/hash-generator/hash-generator.test.tsx +73 -0
  168. package/src/ui/hash-generator/hash-generator.tsx +1 -0
  169. package/src/ui/hash-generator/index.ts +18 -0
  170. package/src/ui/hash-generator/types.ts +29 -0
  171. package/src/ui/hash-generator/variants.ts +31 -0
  172. package/src/ui/qr-code/animated/animations.ts +51 -0
  173. package/src/ui/qr-code/animated/index.ts +5 -0
  174. package/src/ui/qr-code/animated/qr-code-animated.tsx +111 -0
  175. package/src/ui/qr-code/animated/types.ts +10 -0
  176. package/src/ui/qr-code/index.ts +10 -0
  177. package/src/ui/qr-code/qr-code-base.tsx +149 -0
  178. package/src/ui/qr-code/qr-code.test.tsx +58 -0
  179. package/src/ui/qr-code/qr-code.tsx +2 -0
  180. package/src/ui/qr-code/types.ts +22 -0
  181. package/src/ui/qr-code/variants.ts +11 -0
  182. package/src/ui/qr-scanner/index.ts +17 -0
  183. package/src/ui/qr-scanner/qr-scanner-base.tsx +568 -0
  184. package/src/ui/qr-scanner/qr-scanner.test.tsx +61 -0
  185. package/src/ui/qr-scanner/qr-scanner.tsx +2 -0
  186. package/src/ui/qr-scanner/types.ts +32 -0
  187. package/src/ui/qr-scanner/variants.ts +26 -0
  188. package/src/ui/secret-reveal/animated/secret-reveal-animated.tsx +4 -1
  189. package/src/ui/secret-reveal/secret-reveal-base.tsx +4 -1
  190. package/dist/chunk-DIAA5VH4.mjs.map +0 -1
  191. package/dist/chunk-GFE6ZX5Y.mjs.map +0 -1
  192. package/dist/chunk-H3BJOK22.js.map +0 -1
  193. package/dist/chunk-ILPPXWR3.js +0 -19
  194. package/dist/chunk-UZ6Y5CSV.js.map +0 -1
@@ -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,6 @@
1
+ export {
2
+ useHash,
3
+ type HashGeneratorAlgorithm,
4
+ type UseHashResult,
5
+ ALGORITHM_LABELS,
6
+ } from "./useHash";
@@ -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,5 @@
1
+ "use client";
2
+
3
+ export { QrCodeAnimated } from "./qr-code-animated";
4
+ export type { QrCodeAnimation, QrCodeAnimatedProps } from "./types";
5
+ export { qrCodeAnimationPresets } from "./animations";
@@ -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";