@zentauri-ui/zentauri-components 2.2.0 → 2.2.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 (97) hide show
  1. package/README.md +6 -5
  2. package/cli/props.json +400 -0
  3. package/cli/registry.json +6 -0
  4. package/dist/{chunk-TACF7MJE.mjs → chunk-45ZHGDT2.mjs} +3 -3
  5. package/dist/{chunk-TACF7MJE.mjs.map → chunk-45ZHGDT2.mjs.map} +1 -1
  6. package/dist/chunk-46HJFCP7.js +87 -0
  7. package/dist/chunk-46HJFCP7.js.map +1 -0
  8. package/dist/{chunk-AMNJ35TT.mjs → chunk-5HLEHSPM.mjs} +14 -5
  9. package/dist/chunk-5HLEHSPM.mjs.map +1 -0
  10. package/dist/{chunk-YTCVWOBC.js → chunk-DUH2YLH2.js} +12 -12
  11. package/dist/{chunk-YTCVWOBC.js.map → chunk-DUH2YLH2.js.map} +1 -1
  12. package/dist/chunk-HR6MGXNI.mjs +212 -0
  13. package/dist/chunk-HR6MGXNI.mjs.map +1 -0
  14. package/dist/chunk-LOEKM3FL.mjs +78 -0
  15. package/dist/chunk-LOEKM3FL.mjs.map +1 -0
  16. package/dist/{chunk-EPJYLBXV.js → chunk-NW5BSLR2.js} +6 -6
  17. package/dist/{chunk-EPJYLBXV.js.map → chunk-NW5BSLR2.js.map} +1 -1
  18. package/dist/{chunk-F3G3RL2N.js → chunk-UJZ7JQBQ.js} +14 -5
  19. package/dist/chunk-UJZ7JQBQ.js.map +1 -0
  20. package/dist/chunk-WMM42MAC.js +222 -0
  21. package/dist/chunk-WMM42MAC.js.map +1 -0
  22. package/dist/chunk-YBKNXDZU.js +19 -0
  23. package/dist/{chunk-VQJXOJ7G.js.map → chunk-YBKNXDZU.js.map} +1 -1
  24. package/dist/{chunk-5XSW5JYA.mjs → chunk-YSQW56JX.mjs} +4 -4
  25. package/dist/{chunk-5XSW5JYA.mjs.map → chunk-YSQW56JX.mjs.map} +1 -1
  26. package/dist/{chunk-37KMH77M.mjs → chunk-Z4Y5IPR3.mjs} +3 -3
  27. package/dist/{chunk-37KMH77M.mjs.map → chunk-Z4Y5IPR3.mjs.map} +1 -1
  28. package/dist/design-system/facade.js +6 -5
  29. package/dist/design-system/facade.js.map +1 -1
  30. package/dist/design-system/facade.mjs +5 -4
  31. package/dist/design-system/facade.mjs.map +1 -1
  32. package/dist/design-system/index.d.ts +1 -0
  33. package/dist/design-system/index.d.ts.map +1 -1
  34. package/dist/design-system/password-strength-meter.d.ts +74 -0
  35. package/dist/design-system/password-strength-meter.d.ts.map +1 -0
  36. package/dist/ui/buttons/animated.js +8 -7
  37. package/dist/ui/buttons/animated.js.map +1 -1
  38. package/dist/ui/buttons/animated.mjs +6 -5
  39. package/dist/ui/buttons/animated.mjs.map +1 -1
  40. package/dist/ui/buttons.js +9 -8
  41. package/dist/ui/buttons.mjs +7 -6
  42. package/dist/ui/data-table.js +19 -18
  43. package/dist/ui/data-table.js.map +1 -1
  44. package/dist/ui/data-table.mjs +9 -8
  45. package/dist/ui/data-table.mjs.map +1 -1
  46. package/dist/ui/dynamic-stepper.js +18 -17
  47. package/dist/ui/dynamic-stepper.js.map +1 -1
  48. package/dist/ui/dynamic-stepper.mjs +7 -6
  49. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  50. package/dist/ui/pagination.js +10 -9
  51. package/dist/ui/pagination.mjs +7 -6
  52. package/dist/ui/password-strength-meter/animated/animations.d.ts +3 -0
  53. package/dist/ui/password-strength-meter/animated/animations.d.ts.map +1 -0
  54. package/dist/ui/password-strength-meter/animated/index.d.ts +4 -0
  55. package/dist/ui/password-strength-meter/animated/index.d.ts.map +1 -0
  56. package/dist/ui/password-strength-meter/animated/password-strength-meter-animated.d.ts +14 -0
  57. package/dist/ui/password-strength-meter/animated/password-strength-meter-animated.d.ts.map +1 -0
  58. package/dist/ui/password-strength-meter/animated/types.d.ts +21 -0
  59. package/dist/ui/password-strength-meter/animated/types.d.ts.map +1 -0
  60. package/dist/ui/password-strength-meter/animated.js +169 -0
  61. package/dist/ui/password-strength-meter/animated.js.map +1 -0
  62. package/dist/ui/password-strength-meter/animated.mjs +165 -0
  63. package/dist/ui/password-strength-meter/animated.mjs.map +1 -0
  64. package/dist/ui/password-strength-meter/index.d.ts +5 -0
  65. package/dist/ui/password-strength-meter/index.d.ts.map +1 -0
  66. package/dist/ui/password-strength-meter/password-strength-meter-base.d.ts +17 -0
  67. package/dist/ui/password-strength-meter/password-strength-meter-base.d.ts.map +1 -0
  68. package/dist/ui/password-strength-meter/password-strength-meter.d.ts +6 -0
  69. package/dist/ui/password-strength-meter/password-strength-meter.d.ts.map +1 -0
  70. package/dist/ui/password-strength-meter/types.d.ts +33 -0
  71. package/dist/ui/password-strength-meter/types.d.ts.map +1 -0
  72. package/dist/ui/password-strength-meter/variants.d.ts +13 -0
  73. package/dist/ui/password-strength-meter/variants.d.ts.map +1 -0
  74. package/dist/ui/password-strength-meter.js +37 -0
  75. package/dist/ui/password-strength-meter.js.map +1 -0
  76. package/dist/ui/password-strength-meter.mjs +16 -0
  77. package/dist/ui/password-strength-meter.mjs.map +1 -0
  78. package/dist/ui/split-button.js +20 -19
  79. package/dist/ui/split-button.js.map +1 -1
  80. package/dist/ui/split-button.mjs +7 -6
  81. package/dist/ui/split-button.mjs.map +1 -1
  82. package/package.json +1 -1
  83. package/src/design-system/index.ts +1 -0
  84. package/src/design-system/password-strength-meter.ts +115 -0
  85. package/src/ui/password-strength-meter/animated/animations.ts +10 -0
  86. package/src/ui/password-strength-meter/animated/index.ts +14 -0
  87. package/src/ui/password-strength-meter/animated/password-strength-meter-animated.tsx +186 -0
  88. package/src/ui/password-strength-meter/animated/types.ts +35 -0
  89. package/src/ui/password-strength-meter/index.ts +18 -0
  90. package/src/ui/password-strength-meter/password-strength-meter-base.tsx +202 -0
  91. package/src/ui/password-strength-meter/password-strength-meter.test.tsx +103 -0
  92. package/src/ui/password-strength-meter/password-strength-meter.tsx +8 -0
  93. package/src/ui/password-strength-meter/types.ts +40 -0
  94. package/src/ui/password-strength-meter/variants.ts +49 -0
  95. package/dist/chunk-AMNJ35TT.mjs.map +0 -1
  96. package/dist/chunk-F3G3RL2N.js.map +0 -1
  97. package/dist/chunk-VQJXOJ7G.js +0 -19
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useMemo, useId } from "react";
4
+
5
+ import { cn, clamp } from "../../lib/utils";
6
+
7
+ import type {
8
+ PasswordStrengthMeterProps,
9
+ PasswordStrengthMeterSectionProps,
10
+ PasswordStrengthMeterCtx,
11
+ } from "./types";
12
+ import {
13
+ passwordStrengthMeterBarVariants,
14
+ passwordStrengthMeterTrackVariants,
15
+ passwordStrengthMeterVariants,
16
+ } from "./variants";
17
+
18
+ export const PasswordStrengthMeterContext =
19
+ createContext<PasswordStrengthMeterCtx | null>(null);
20
+
21
+ export function usePasswordStrengthMeterContext(
22
+ component: string,
23
+ ): PasswordStrengthMeterCtx {
24
+ const ctx = useContext(PasswordStrengthMeterContext);
25
+ if (!ctx) {
26
+ throw new Error(`${component} must be used within <PasswordStrengthMeter>`);
27
+ }
28
+ return ctx;
29
+ }
30
+
31
+ export function getStrengthLabel(percent: number): string {
32
+ if (percent <= 20) return "Weak";
33
+ if (percent <= 40) return "Fair";
34
+ if (percent <= 60) return "Good";
35
+ if (percent <= 80) return "Strong";
36
+ return "Very strong";
37
+ }
38
+
39
+ export function getStrengthColor(percent: number): string {
40
+ if (percent <= 20)
41
+ return "text-[var(--zui-status-error,#dc2626)] dark:text-[var(--zui-status-error-dark,#ef4444)]";
42
+ if (percent <= 40)
43
+ return "text-[var(--zui-color-orange,#ea580c)] dark:text-[var(--zui-color-orange-dark,#fb923c)]";
44
+ if (percent <= 60)
45
+ return "text-[var(--zui-color-yellow,#ca8a04)] dark:text-[var(--zui-color-yellow-dark,#facc15)]";
46
+ if (percent <= 80)
47
+ return "text-[var(--zui-color-emerald,#16a34a)] dark:text-[var(--zui-color-emerald-dark,#22c55e)]";
48
+ return "text-[var(--zui-color-indigo,#4338ca)] dark:text-[var(--zui-color-indigo-dark,#818cf8)]";
49
+ }
50
+
51
+ export function PasswordStrengthMeterBase(props: PasswordStrengthMeterProps) {
52
+ const {
53
+ className,
54
+ appearance = "default",
55
+ size = "md",
56
+ shape = "rounded",
57
+ animated = false,
58
+ segmented = false,
59
+ value = 0,
60
+ min = 0,
61
+ max = 100,
62
+ label,
63
+ scoreLabel,
64
+ showScoreLabel = true,
65
+ children,
66
+ ref,
67
+ as: Wrapper = "div",
68
+ ...rest
69
+ } = props;
70
+ const clamped = clamp(value, min, max);
71
+ const percent = max === min ? 0 : ((clamped - min) / (max - min)) * 100;
72
+ const labelSlotId = `${useId()}-password-strength-meter-label`;
73
+ const hasInlineLabelProp = Boolean(label?.trim().length);
74
+
75
+ const labelingProps = useMemo(() => {
76
+ if (hasInlineLabelProp) {
77
+ return { "aria-label": label?.trim() ?? "Password strength" };
78
+ }
79
+ return { "aria-label": "Password strength" };
80
+ }, [hasInlineLabelProp, label]);
81
+
82
+ const ctx = useMemo(
83
+ () => ({
84
+ value: clamped,
85
+ min,
86
+ max,
87
+ size: size ?? "md",
88
+ shape: shape ?? "rounded",
89
+ animated: Boolean(animated),
90
+ segmented: Boolean(segmented),
91
+ appearance: appearance ?? "default",
92
+ labelSlotId,
93
+ }),
94
+ [
95
+ animated,
96
+ appearance,
97
+ clamped,
98
+ labelSlotId,
99
+ max,
100
+ min,
101
+ shape,
102
+ size,
103
+ segmented,
104
+ ],
105
+ );
106
+
107
+ return (
108
+ <PasswordStrengthMeterContext.Provider value={ctx}>
109
+ <Wrapper
110
+ ref={ref}
111
+ data-slot="password-strength-meter"
112
+ role="meter"
113
+ aria-valuemin={min}
114
+ aria-valuemax={max}
115
+ aria-valuenow={clamped}
116
+ aria-valuetext={getStrengthLabel(percent)}
117
+ {...labelingProps}
118
+ className={cn(
119
+ passwordStrengthMeterVariants({ appearance, size, shape }),
120
+ className,
121
+ )}
122
+ {...rest}
123
+ >
124
+ {children ?? (
125
+ <>
126
+ {(label || showScoreLabel) && (
127
+ <div className="flex items-center justify-between mb-1.5">
128
+ {label && (
129
+ <span
130
+ id={labelSlotId}
131
+ data-slot="password-strength-meter-label"
132
+ className="text-xs font-medium"
133
+ >
134
+ {label}
135
+ </span>
136
+ )}
137
+ {showScoreLabel && (
138
+ <span
139
+ data-slot="password-strength-meter-score-label"
140
+ className={cn(
141
+ "text-xs font-semibold",
142
+ getStrengthColor(percent),
143
+ )}
144
+ >
145
+ {scoreLabel ?? getStrengthLabel(percent)}
146
+ </span>
147
+ )}
148
+ </div>
149
+ )}
150
+ <PasswordStrengthMeterBar
151
+ style={{ transform: `scaleX(${percent / 100})` }}
152
+ />
153
+ </>
154
+ )}
155
+ </Wrapper>
156
+ </PasswordStrengthMeterContext.Provider>
157
+ );
158
+ }
159
+
160
+ PasswordStrengthMeterBase.displayName = "PasswordStrengthMeter";
161
+
162
+ export function PasswordStrengthMeterBar({
163
+ className,
164
+ style,
165
+ ref,
166
+ as: Wrapper = "div",
167
+ ...rest
168
+ }: PasswordStrengthMeterSectionProps & {
169
+ style?: React.CSSProperties;
170
+ ref?: React.Ref<HTMLDivElement>;
171
+ }) {
172
+ const { size, shape, animated, segmented } = usePasswordStrengthMeterContext(
173
+ "PasswordStrengthMeterBar",
174
+ );
175
+
176
+ return (
177
+ <Wrapper
178
+ data-slot="password-strength-meter-track"
179
+ className={cn(
180
+ passwordStrengthMeterTrackVariants({ size, shape }),
181
+ "text-current",
182
+ )}
183
+ >
184
+ <div
185
+ ref={ref}
186
+ data-slot="password-strength-meter-bar"
187
+ className={cn(
188
+ passwordStrengthMeterBarVariants({ segmented }),
189
+ animated ? "animate-pulse" : undefined,
190
+ className,
191
+ )}
192
+ style={{
193
+ transformOrigin: "left center",
194
+ ...style,
195
+ }}
196
+ {...rest}
197
+ />
198
+ </Wrapper>
199
+ );
200
+ }
201
+
202
+ PasswordStrengthMeterBar.displayName = "PasswordStrengthMeterBar";
@@ -0,0 +1,103 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { PasswordStrengthMeter } from "./password-strength-meter";
6
+ import { PasswordStrengthMeterBar } from "./password-strength-meter-base";
7
+
8
+ describe("PasswordStrengthMeter", () => {
9
+ it("should expose displayName on compound parts", () => {
10
+ expect(PasswordStrengthMeter.displayName).toBe("PasswordStrengthMeter");
11
+ expect(PasswordStrengthMeterBar.displayName).toBe(
12
+ "PasswordStrengthMeterBar",
13
+ );
14
+ });
15
+
16
+ it("should stamp data-slot on the root and default bar track", () => {
17
+ render(<PasswordStrengthMeter value={40} />);
18
+ expect(
19
+ document.querySelector('[data-slot="password-strength-meter"]'),
20
+ ).toBeTruthy();
21
+ expect(
22
+ document.querySelector('[data-slot="password-strength-meter-track"]'),
23
+ ).toBeTruthy();
24
+ expect(
25
+ document.querySelector('[data-slot="password-strength-meter-bar"]'),
26
+ ).toBeTruthy();
27
+ });
28
+
29
+ it("should expose meter semantics with clamped value", () => {
30
+ render(<PasswordStrengthMeter value={120} min={0} max={100} />);
31
+ const meter = screen.getByRole("meter");
32
+ expect(meter.getAttribute("aria-valuemin")).toBe("0");
33
+ expect(meter.getAttribute("aria-valuemax")).toBe("100");
34
+ expect(meter.getAttribute("aria-valuenow")).toBe("100");
35
+ });
36
+
37
+ it("should clamp below the minimum", () => {
38
+ render(<PasswordStrengthMeter value={-5} min={0} max={100} />);
39
+ expect(screen.getByRole("meter").getAttribute("aria-valuenow")).toBe("0");
40
+ });
41
+
42
+ it("should render strength label based on value", () => {
43
+ render(<PasswordStrengthMeter value={10} showScoreLabel />);
44
+ expect(
45
+ document.querySelector(
46
+ '[data-slot="password-strength-meter-score-label"]',
47
+ ),
48
+ ).toHaveTextContent("Weak");
49
+ });
50
+
51
+ it("should render 'Very strong' for high values", () => {
52
+ render(<PasswordStrengthMeter value={95} showScoreLabel />);
53
+ expect(
54
+ document.querySelector(
55
+ '[data-slot="password-strength-meter-score-label"]',
56
+ ),
57
+ ).toHaveTextContent("Very strong");
58
+ });
59
+
60
+ it("should not show score label when showScoreLabel is false", () => {
61
+ render(<PasswordStrengthMeter value={50} showScoreLabel={false} />);
62
+ expect(
63
+ document.querySelector(
64
+ '[data-slot="password-strength-meter-score-label"]',
65
+ ),
66
+ ).toBeNull();
67
+ });
68
+
69
+ it("should render custom label", () => {
70
+ render(<PasswordStrengthMeter value={50} label="Password" />);
71
+ expect(
72
+ document.querySelector('[data-slot="password-strength-meter-label"]'),
73
+ ).toHaveTextContent("Password");
74
+ });
75
+
76
+ it("should render custom scoreLabel", () => {
77
+ render(<PasswordStrengthMeter value={50} scoreLabel="Medium" />);
78
+ expect(
79
+ document.querySelector(
80
+ '[data-slot="password-strength-meter-score-label"]',
81
+ ),
82
+ ).toHaveTextContent("Medium");
83
+ });
84
+
85
+ it("should apply appearance classes from the variant recipe", () => {
86
+ render(<PasswordStrengthMeter value={50} appearance="emerald" />);
87
+ const root = document.querySelector(
88
+ '[data-slot="password-strength-meter"]',
89
+ ) as HTMLElement;
90
+ expect(root.className).toMatch(/--psm-fill/);
91
+ });
92
+
93
+ describe("ref forwarding", () => {
94
+ it("should forward ref to the root", () => {
95
+ const ref = createRef<HTMLDivElement>();
96
+ render(<PasswordStrengthMeter ref={ref} value={5} />);
97
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
98
+ expect(ref.current?.getAttribute("data-slot")).toBe(
99
+ "password-strength-meter",
100
+ );
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,8 @@
1
+ import { PasswordStrengthMeterBase } from "./password-strength-meter-base";
2
+ import type { PasswordStrengthMeterProps } from "./types";
3
+
4
+ export const PasswordStrengthMeter = (props: PasswordStrengthMeterProps) => {
5
+ return <PasswordStrengthMeterBase {...props} />;
6
+ };
7
+
8
+ PasswordStrengthMeter.displayName = "PasswordStrengthMeter";
@@ -0,0 +1,40 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ElementType, ReactNode } from "react";
3
+
4
+ import type { passwordStrengthMeterVariants } from "./variants";
5
+
6
+ export type PasswordStrengthMeterVariantProps = VariantProps<
7
+ typeof passwordStrengthMeterVariants
8
+ >;
9
+
10
+ export type PasswordStrengthMeterProps = PasswordStrengthMeterVariantProps &
11
+ (Omit<ComponentPropsWithRef<"div">, "children"> & {
12
+ value: number;
13
+ min?: number;
14
+ max?: number;
15
+ label?: string;
16
+ scoreLabel?: string;
17
+ showScoreLabel?: boolean;
18
+ animated?: boolean;
19
+ segmented?: boolean;
20
+ children?: ReactNode;
21
+ as?: ElementType;
22
+ });
23
+
24
+ export type PasswordStrengthMeterSectionProps = {
25
+ className?: string;
26
+ children?: ReactNode;
27
+ as?: ElementType;
28
+ };
29
+
30
+ export type PasswordStrengthMeterCtx = {
31
+ value: number;
32
+ min: number;
33
+ max: number;
34
+ size: NonNullable<PasswordStrengthMeterProps["size"]>;
35
+ shape: NonNullable<PasswordStrengthMeterProps["shape"]>;
36
+ animated: boolean;
37
+ segmented: boolean;
38
+ appearance: NonNullable<PasswordStrengthMeterProps["appearance"]>;
39
+ labelSlotId: string;
40
+ };
@@ -0,0 +1,49 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiPasswordStrengthMeterAppearances,
5
+ zuiPasswordStrengthMeterBarBase,
6
+ zuiPasswordStrengthMeterBarSegmented,
7
+ zuiPasswordStrengthMeterBase,
8
+ zuiPasswordStrengthMeterShapes,
9
+ zuiPasswordStrengthMeterSizes,
10
+ zuiPasswordStrengthMeterTrackBase,
11
+ zuiPasswordStrengthMeterTrackSizes,
12
+ } from "../../design-system/password-strength-meter";
13
+
14
+ export const passwordStrengthMeterVariants = cva(zuiPasswordStrengthMeterBase, {
15
+ variants: {
16
+ appearance: zuiPasswordStrengthMeterAppearances,
17
+ size: zuiPasswordStrengthMeterSizes,
18
+ shape: zuiPasswordStrengthMeterShapes,
19
+ },
20
+ defaultVariants: {
21
+ appearance: "default",
22
+ size: "md",
23
+ shape: "rounded",
24
+ },
25
+ });
26
+
27
+ export const passwordStrengthMeterTrackVariants = cva(
28
+ zuiPasswordStrengthMeterTrackBase,
29
+ {
30
+ variants: {
31
+ size: zuiPasswordStrengthMeterTrackSizes,
32
+ shape: zuiPasswordStrengthMeterShapes,
33
+ },
34
+ defaultVariants: {
35
+ size: "md",
36
+ shape: "rounded",
37
+ },
38
+ },
39
+ );
40
+
41
+ export const passwordStrengthMeterBarVariants = cva(
42
+ zuiPasswordStrengthMeterBarBase,
43
+ {
44
+ variants: {
45
+ segmented: zuiPasswordStrengthMeterBarSegmented,
46
+ },
47
+ defaultVariants: { segmented: false },
48
+ },
49
+ );