@zentauri-ui/zentauri-components 1.7.8 → 1.8.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 (49) hide show
  1. package/README.md +7 -4
  2. package/cli/registry.json +2 -0
  3. package/dist/design-system/animated-number.d.ts +32 -0
  4. package/dist/design-system/animated-number.d.ts.map +1 -0
  5. package/dist/design-system/index.d.ts +2 -0
  6. package/dist/design-system/index.d.ts.map +1 -1
  7. package/dist/design-system/marquee.d.ts +40 -0
  8. package/dist/design-system/marquee.d.ts.map +1 -0
  9. package/dist/ui/animated-number/animated-number.d.ts +4 -0
  10. package/dist/ui/animated-number/animated-number.d.ts.map +1 -0
  11. package/dist/ui/animated-number/animations.d.ts +59 -0
  12. package/dist/ui/animated-number/animations.d.ts.map +1 -0
  13. package/dist/ui/animated-number/index.d.ts +4 -0
  14. package/dist/ui/animated-number/index.d.ts.map +1 -0
  15. package/dist/ui/animated-number/types.d.ts +31 -0
  16. package/dist/ui/animated-number/types.d.ts.map +1 -0
  17. package/dist/ui/animated-number/variants.d.ts +5 -0
  18. package/dist/ui/animated-number/variants.d.ts.map +1 -0
  19. package/dist/ui/animated-number.js +181 -0
  20. package/dist/ui/animated-number.js.map +1 -0
  21. package/dist/ui/animated-number.mjs +177 -0
  22. package/dist/ui/animated-number.mjs.map +1 -0
  23. package/dist/ui/marquee/index.d.ts +4 -0
  24. package/dist/ui/marquee/index.d.ts.map +1 -0
  25. package/dist/ui/marquee/marquee.d.ts +6 -0
  26. package/dist/ui/marquee/marquee.d.ts.map +1 -0
  27. package/dist/ui/marquee/types.d.ts +15 -0
  28. package/dist/ui/marquee/types.d.ts.map +1 -0
  29. package/dist/ui/marquee/variants.d.ts +7 -0
  30. package/dist/ui/marquee/variants.d.ts.map +1 -0
  31. package/dist/ui/marquee.js +171 -0
  32. package/dist/ui/marquee.js.map +1 -0
  33. package/dist/ui/marquee.mjs +168 -0
  34. package/dist/ui/marquee.mjs.map +1 -0
  35. package/package.json +1 -1
  36. package/src/design-system/animated-number.ts +53 -0
  37. package/src/design-system/index.ts +2 -0
  38. package/src/design-system/marquee.ts +62 -0
  39. package/src/ui/animated-number/animated-number.test.tsx +64 -0
  40. package/src/ui/animated-number/animated-number.tsx +120 -0
  41. package/src/ui/animated-number/animations.ts +22 -0
  42. package/src/ui/animated-number/index.ts +4 -0
  43. package/src/ui/animated-number/types.ts +39 -0
  44. package/src/ui/animated-number/variants.ts +14 -0
  45. package/src/ui/marquee/index.ts +9 -0
  46. package/src/ui/marquee/marquee.test.tsx +138 -0
  47. package/src/ui/marquee/marquee.tsx +114 -0
  48. package/src/ui/marquee/types.ts +19 -0
  49. package/src/ui/marquee/variants.ts +24 -0
@@ -0,0 +1,138 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { Marquee } from "./marquee";
5
+
6
+ describe("Marquee", () => {
7
+ it("exposes a display name", () => {
8
+ expect(Marquee.displayName).toBe("Marquee");
9
+ });
10
+
11
+ it("renders children into duplicated marquee groups", () => {
12
+ render(
13
+ <Marquee>
14
+ <span>Acme</span>
15
+ </Marquee>,
16
+ );
17
+
18
+ expect(document.querySelector('[data-slot="marquee"]')).toBeTruthy();
19
+ expect(document.querySelector('[data-slot="marquee-track"]')).toBeTruthy();
20
+ expect(screen.getAllByText("Acme")).toHaveLength(2);
21
+
22
+ const groups = document.querySelectorAll(
23
+ '[data-slot="marquee-item-group"]',
24
+ );
25
+ expect(groups[1]).toHaveAttribute("aria-hidden", "true");
26
+ expect(groups[1]).toHaveAttribute("inert");
27
+ });
28
+
29
+ it("applies horizontal metadata by default", () => {
30
+ render(
31
+ <Marquee data-testid="marquee">
32
+ <span>Default direction</span>
33
+ </Marquee>,
34
+ );
35
+
36
+ const marquee = screen.getByTestId("marquee");
37
+ expect(marquee).toHaveAttribute("data-orientation", "horizontal");
38
+ expect(marquee).toHaveAttribute("data-direction", "left");
39
+ expect(marquee.className).toContain("w-full");
40
+ });
41
+
42
+ it("applies vertical metadata and down direction", () => {
43
+ render(
44
+ <Marquee orientation="vertical" direction="down" data-testid="marquee">
45
+ <span>Vertical direction</span>
46
+ </Marquee>,
47
+ );
48
+
49
+ const marquee = screen.getByTestId("marquee");
50
+ const track = document.querySelector('[data-slot="marquee-track"]');
51
+ expect(marquee).toHaveAttribute("data-orientation", "vertical");
52
+ expect(marquee).toHaveAttribute("data-direction", "down");
53
+ expect(track?.className).toContain("[animation-direction:reverse]");
54
+ });
55
+
56
+ it("infers vertical orientation from up or down directions", () => {
57
+ render(
58
+ <Marquee direction="up" data-testid="marquee">
59
+ <span>Inferred vertical</span>
60
+ </Marquee>,
61
+ );
62
+
63
+ expect(screen.getByTestId("marquee")).toHaveAttribute(
64
+ "data-orientation",
65
+ "vertical",
66
+ );
67
+ });
68
+
69
+ it("maps speed to track animation styles and gap to a CSS custom property", () => {
70
+ render(
71
+ <Marquee speed={42} gap={24} data-testid="marquee">
72
+ <span>Timing</span>
73
+ </Marquee>,
74
+ );
75
+
76
+ const marquee = screen.getByTestId("marquee");
77
+ expect(marquee).toHaveStyle({
78
+ "--zui-marquee-gap": "24px",
79
+ });
80
+
81
+ expect(document.querySelector('[data-slot="marquee-track"]')).toHaveStyle({
82
+ animationDuration: "42s",
83
+ animationName: "zui-marquee-x",
84
+ });
85
+ });
86
+
87
+ it("accepts string gap values", () => {
88
+ render(
89
+ <Marquee gap="2rem" data-testid="marquee">
90
+ <span>String gap</span>
91
+ </Marquee>,
92
+ );
93
+
94
+ expect(screen.getByTestId("marquee")).toHaveStyle({
95
+ "--zui-marquee-gap": "2rem",
96
+ });
97
+ });
98
+
99
+ it("adds pause-on-hover animation control when requested", () => {
100
+ render(
101
+ <Marquee pauseOnHover>
102
+ <span>Pause</span>
103
+ </Marquee>,
104
+ );
105
+
106
+ expect(
107
+ document.querySelector('[data-slot="marquee-track"]')?.className,
108
+ ).toContain("group-hover/marquee:[animation-play-state:paused]");
109
+ });
110
+
111
+ it("applies appearance, size, fade, and custom classes", () => {
112
+ render(
113
+ <Marquee
114
+ appearance="sky"
115
+ className="rounded-2xl"
116
+ fade={false}
117
+ itemClassName="min-w-max"
118
+ size="lg"
119
+ trackClassName="tracking-wide"
120
+ data-testid="marquee"
121
+ >
122
+ <span>Variants</span>
123
+ </Marquee>,
124
+ );
125
+
126
+ const marquee = screen.getByTestId("marquee");
127
+ expect(marquee.className).toContain("rounded-2xl");
128
+ expect(marquee.className).toContain("p-4");
129
+ expect(marquee.className).toContain("--zui-marquee-sky-border");
130
+ expect(marquee.className).not.toContain("mask-image");
131
+ expect(
132
+ document.querySelector('[data-slot="marquee-track"]')?.className,
133
+ ).toContain("tracking-wide");
134
+ expect(
135
+ document.querySelector('[data-slot="marquee-item-group"]')?.className,
136
+ ).toContain("min-w-max");
137
+ });
138
+ });
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import type { CSSProperties } from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ import type { MarqueeProps } from "./types";
8
+ import { marqueeVariants } from "./variants";
9
+
10
+ const marqueeKeyframes = `@keyframes zui-marquee-x{from{transform:translate3d(0,0,0)}to{transform:translate3d(calc(-50% - var(--zui-marquee-gap)/2),0,0)}}@keyframes zui-marquee-y{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,calc(-50% - var(--zui-marquee-gap)/2),0)}}`;
11
+
12
+ function toCssLength(value: number | string | undefined) {
13
+ if (value === undefined) {
14
+ return undefined;
15
+ }
16
+ return typeof value === "number" ? `${value}px` : value;
17
+ }
18
+
19
+ export function Marquee(props: MarqueeProps) {
20
+ const {
21
+ appearance,
22
+ children,
23
+ className,
24
+ direction,
25
+ fade,
26
+ gap,
27
+ itemClassName,
28
+ orientation,
29
+ pauseOnHover = false,
30
+ ref,
31
+ size,
32
+ speed = 30,
33
+ style,
34
+ trackClassName,
35
+ ...rest
36
+ } = props;
37
+
38
+ const resolvedOrientation =
39
+ orientation ??
40
+ (direction === "up" || direction === "down" ? "vertical" : "horizontal");
41
+ const resolvedDirection =
42
+ direction ?? (resolvedOrientation === "vertical" ? "up" : "left");
43
+ const isReverse =
44
+ resolvedDirection === "right" || resolvedDirection === "down";
45
+ const animationName =
46
+ resolvedOrientation === "vertical" ? "zui-marquee-y" : "zui-marquee-x";
47
+ const marqueeStyle = {
48
+ ...(gap !== undefined ? { "--zui-marquee-gap": toCssLength(gap) } : null),
49
+ ...style,
50
+ } as CSSProperties;
51
+
52
+ return (
53
+ <div
54
+ ref={ref}
55
+ data-direction={resolvedDirection}
56
+ data-orientation={resolvedOrientation}
57
+ data-slot="marquee"
58
+ className={cn(
59
+ marqueeVariants({
60
+ appearance,
61
+ fade,
62
+ orientation: resolvedOrientation,
63
+ size,
64
+ }),
65
+ className,
66
+ )}
67
+ style={marqueeStyle}
68
+ {...rest}
69
+ >
70
+ <style>{marqueeKeyframes}</style>
71
+ <div
72
+ data-slot="marquee-track"
73
+ className={cn(
74
+ "flex shrink-0 gap-[var(--zui-marquee-gap)] will-change-transform [animation-iteration-count:infinite] [animation-timing-function:linear] motion-reduce:[animation-play-state:paused]",
75
+ resolvedOrientation === "vertical" ? "flex-col" : "w-max flex-row",
76
+ pauseOnHover && "group-hover/marquee:[animation-play-state:paused]",
77
+ isReverse && "[animation-direction:reverse]",
78
+ trackClassName,
79
+ )}
80
+ style={
81
+ {
82
+ animationDuration: `${speed}s`,
83
+ animationName,
84
+ } as CSSProperties
85
+ }
86
+ >
87
+ <div
88
+ data-slot="marquee-item-group"
89
+ className={cn(
90
+ "flex shrink-0 items-center justify-around gap-[var(--zui-marquee-gap)]",
91
+ resolvedOrientation === "vertical" ? "flex-col" : "flex-row",
92
+ itemClassName,
93
+ )}
94
+ >
95
+ {children}
96
+ </div>
97
+ <div
98
+ aria-hidden="true"
99
+ inert
100
+ data-slot="marquee-item-group"
101
+ className={cn(
102
+ "flex shrink-0 items-center justify-around gap-[var(--zui-marquee-gap)]",
103
+ resolvedOrientation === "vertical" ? "flex-col" : "flex-row",
104
+ itemClassName,
105
+ )}
106
+ >
107
+ {children}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ Marquee.displayName = "Marquee";
@@ -0,0 +1,19 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+
4
+ import type { marqueeVariants } from "./variants";
5
+
6
+ export type MarqueeVariantProps = VariantProps<typeof marqueeVariants>;
7
+
8
+ export type MarqueeDirection = "left" | "right" | "up" | "down";
9
+
10
+ export type MarqueeProps = MarqueeVariantProps &
11
+ Omit<ComponentPropsWithRef<"div">, "children"> & {
12
+ children: ReactNode;
13
+ direction?: MarqueeDirection;
14
+ gap?: number | string;
15
+ pauseOnHover?: boolean;
16
+ speed?: number;
17
+ trackClassName?: string;
18
+ itemClassName?: string;
19
+ };
@@ -0,0 +1,24 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiMarqueeAppearances,
5
+ zuiMarqueeBase,
6
+ zuiMarqueeFade,
7
+ zuiMarqueeOrientations,
8
+ zuiMarqueeSizes,
9
+ } from "../../design-system/marquee";
10
+
11
+ export const marqueeVariants = cva(zuiMarqueeBase, {
12
+ variants: {
13
+ appearance: zuiMarqueeAppearances,
14
+ fade: zuiMarqueeFade,
15
+ orientation: zuiMarqueeOrientations,
16
+ size: zuiMarqueeSizes,
17
+ },
18
+ defaultVariants: {
19
+ appearance: "default",
20
+ fade: true,
21
+ orientation: "horizontal",
22
+ size: "md",
23
+ },
24
+ });