@zentauri-ui/zentauri-components 1.7.5 → 1.7.7

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 (124) hide show
  1. package/README.md +11 -6
  2. package/cli/registry.json +3 -0
  3. package/dist/chunk-DEZRB6DS.mjs +83 -0
  4. package/dist/chunk-DEZRB6DS.mjs.map +1 -0
  5. package/dist/chunk-GRJFGIZC.mjs +417 -0
  6. package/dist/chunk-GRJFGIZC.mjs.map +1 -0
  7. package/dist/chunk-QHEHBC6M.js +421 -0
  8. package/dist/chunk-QHEHBC6M.js.map +1 -0
  9. package/dist/chunk-V5JTDRV5.mjs +278 -0
  10. package/dist/chunk-V5JTDRV5.mjs.map +1 -0
  11. package/dist/chunk-Z4KHAD6Y.js +295 -0
  12. package/dist/chunk-Z4KHAD6Y.js.map +1 -0
  13. package/dist/chunk-ZX2IBIZT.js +92 -0
  14. package/dist/chunk-ZX2IBIZT.js.map +1 -0
  15. package/dist/design-system/context-menu.d.ts +41 -0
  16. package/dist/design-system/context-menu.d.ts.map +1 -0
  17. package/dist/design-system/index.d.ts +3 -0
  18. package/dist/design-system/index.d.ts.map +1 -1
  19. package/dist/design-system/timeline.d.ts +56 -0
  20. package/dist/design-system/timeline.d.ts.map +1 -0
  21. package/dist/design-system/tree-view.d.ts +66 -0
  22. package/dist/design-system/tree-view.d.ts.map +1 -0
  23. package/dist/ui/context-menu/context-menu.d.ts +11 -0
  24. package/dist/ui/context-menu/context-menu.d.ts.map +1 -0
  25. package/dist/ui/context-menu/index.d.ts +4 -0
  26. package/dist/ui/context-menu/index.d.ts.map +1 -0
  27. package/dist/ui/context-menu/types.d.ts +81 -0
  28. package/dist/ui/context-menu/types.d.ts.map +1 -0
  29. package/dist/ui/context-menu/variants.d.ts +7 -0
  30. package/dist/ui/context-menu/variants.d.ts.map +1 -0
  31. package/dist/ui/context-menu.js +500 -0
  32. package/dist/ui/context-menu.js.map +1 -0
  33. package/dist/ui/context-menu.mjs +488 -0
  34. package/dist/ui/context-menu.mjs.map +1 -0
  35. package/dist/ui/dropdown.js +9 -89
  36. package/dist/ui/dropdown.js.map +1 -1
  37. package/dist/ui/dropdown.mjs +1 -81
  38. package/dist/ui/dropdown.mjs.map +1 -1
  39. package/dist/ui/scroll-area/scroll-area.d.ts.map +1 -1
  40. package/dist/ui/scroll-area.js.map +1 -1
  41. package/dist/ui/scroll-area.mjs.map +1 -1
  42. package/dist/ui/timeline/animated/animations.d.ts +8 -0
  43. package/dist/ui/timeline/animated/animations.d.ts.map +1 -0
  44. package/dist/ui/timeline/animated/index.d.ts +6 -0
  45. package/dist/ui/timeline/animated/index.d.ts.map +1 -0
  46. package/dist/ui/timeline/animated/timeline-item-animated.d.ts +8 -0
  47. package/dist/ui/timeline/animated/timeline-item-animated.d.ts.map +1 -0
  48. package/dist/ui/timeline/animated/types.d.ts +12 -0
  49. package/dist/ui/timeline/animated/types.d.ts.map +1 -0
  50. package/dist/ui/timeline/animated.js +94 -0
  51. package/dist/ui/timeline/animated.js.map +1 -0
  52. package/dist/ui/timeline/animated.mjs +71 -0
  53. package/dist/ui/timeline/animated.mjs.map +1 -0
  54. package/dist/ui/timeline/index.d.ts +4 -0
  55. package/dist/ui/timeline/index.d.ts.map +1 -0
  56. package/dist/ui/timeline/timeline-base.d.ts +37 -0
  57. package/dist/ui/timeline/timeline-base.d.ts.map +1 -0
  58. package/dist/ui/timeline/timeline.d.ts +8 -0
  59. package/dist/ui/timeline/timeline.d.ts.map +1 -0
  60. package/dist/ui/timeline/types.d.ts +38 -0
  61. package/dist/ui/timeline/types.d.ts.map +1 -0
  62. package/dist/ui/timeline/variants.d.ts +19 -0
  63. package/dist/ui/timeline/variants.d.ts.map +1 -0
  64. package/dist/ui/timeline.js +63 -0
  65. package/dist/ui/timeline.js.map +1 -0
  66. package/dist/ui/timeline.mjs +14 -0
  67. package/dist/ui/timeline.mjs.map +1 -0
  68. package/dist/ui/tree-view/animated/animations.d.ts +6 -0
  69. package/dist/ui/tree-view/animated/animations.d.ts.map +1 -0
  70. package/dist/ui/tree-view/animated/index.d.ts +5 -0
  71. package/dist/ui/tree-view/animated/index.d.ts.map +1 -0
  72. package/dist/ui/tree-view/animated/tree-view-animated.d.ts +6 -0
  73. package/dist/ui/tree-view/animated/tree-view-animated.d.ts.map +1 -0
  74. package/dist/ui/tree-view/animated/types.d.ts +6 -0
  75. package/dist/ui/tree-view/animated/types.d.ts.map +1 -0
  76. package/dist/ui/tree-view/animated.js +53 -0
  77. package/dist/ui/tree-view/animated.js.map +1 -0
  78. package/dist/ui/tree-view/animated.mjs +50 -0
  79. package/dist/ui/tree-view/animated.mjs.map +1 -0
  80. package/dist/ui/tree-view/index.d.ts +5 -0
  81. package/dist/ui/tree-view/index.d.ts.map +1 -0
  82. package/dist/ui/tree-view/tree-view-base.d.ts +15 -0
  83. package/dist/ui/tree-view/tree-view-base.d.ts.map +1 -0
  84. package/dist/ui/tree-view/tree-view.d.ts +6 -0
  85. package/dist/ui/tree-view/tree-view.d.ts.map +1 -0
  86. package/dist/ui/tree-view/types.d.ts +61 -0
  87. package/dist/ui/tree-view/types.d.ts.map +1 -0
  88. package/dist/ui/tree-view/variants.d.ts +9 -0
  89. package/dist/ui/tree-view/variants.d.ts.map +1 -0
  90. package/dist/ui/tree-view.js +27 -0
  91. package/dist/ui/tree-view.js.map +1 -0
  92. package/dist/ui/tree-view.mjs +14 -0
  93. package/dist/ui/tree-view.mjs.map +1 -0
  94. package/package.json +1 -1
  95. package/src/design-system/context-menu.ts +44 -0
  96. package/src/design-system/index.ts +3 -0
  97. package/src/design-system/timeline.ts +87 -0
  98. package/src/design-system/tree-view.ts +113 -0
  99. package/src/ui/context-menu/context-menu.test.tsx +176 -0
  100. package/src/ui/context-menu/context-menu.tsx +536 -0
  101. package/src/ui/context-menu/index.ts +29 -0
  102. package/src/ui/context-menu/types.ts +110 -0
  103. package/src/ui/context-menu/variants.ts +26 -0
  104. package/src/ui/scroll-area/scroll-area.tsx +0 -2
  105. package/src/ui/timeline/animated/animations.ts +16 -0
  106. package/src/ui/timeline/animated/index.ts +22 -0
  107. package/src/ui/timeline/animated/timeline-item-animated.tsx +76 -0
  108. package/src/ui/timeline/animated/types.ts +21 -0
  109. package/src/ui/timeline/index.ts +30 -0
  110. package/src/ui/timeline/timeline-base.tsx +232 -0
  111. package/src/ui/timeline/timeline.test.tsx +262 -0
  112. package/src/ui/timeline/timeline.tsx +24 -0
  113. package/src/ui/timeline/types.ts +61 -0
  114. package/src/ui/timeline/variants.ts +60 -0
  115. package/src/ui/tree-view/animated/animations.ts +13 -0
  116. package/src/ui/tree-view/animated/index.ts +6 -0
  117. package/src/ui/tree-view/animated/tree-view-animated.tsx +52 -0
  118. package/src/ui/tree-view/animated/types.ts +6 -0
  119. package/src/ui/tree-view/index.ts +13 -0
  120. package/src/ui/tree-view/tree-view-base.tsx +496 -0
  121. package/src/ui/tree-view/tree-view.test.tsx +136 -0
  122. package/src/ui/tree-view/tree-view.tsx +9 -0
  123. package/src/ui/tree-view/types.ts +68 -0
  124. package/src/ui/tree-view/variants.ts +32 -0
@@ -0,0 +1,16 @@
1
+ import type { Transition } from "framer-motion";
2
+
3
+ /** Entrance presets for animated timeline items (fade + slide, connector draw). */
4
+ export type TimelineTransition = "none" | "default" | "smooth" | "slow";
5
+
6
+ export type TimelineTransitionPresets = Record<TimelineTransition, Transition>;
7
+
8
+ export const timelineItemTransitionPresets: TimelineTransitionPresets = {
9
+ none: { duration: 0 },
10
+ default: { duration: 0.4, ease: [0.22, 1, 0.36, 1] },
11
+ smooth: { duration: 0.55, ease: [0.22, 1, 0.36, 1] },
12
+ slow: { duration: 0.8, ease: [0.4, 0, 0.2, 1] },
13
+ };
14
+
15
+ /** Default per-item entrance delay multiplier (seconds) for staggered reveals. */
16
+ export const TIMELINE_DEFAULT_STAGGER = 0.08;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ export {
4
+ Timeline,
5
+ TimelineContent,
6
+ TimelineDescription,
7
+ TimelineIndicator,
8
+ TimelineTitle,
9
+ } from "../timeline-base";
10
+ export { TimelineItem } from "./timeline-item-animated";
11
+ export type {
12
+ TimelineAnimatedProps,
13
+ TimelineItemAnimatedProps,
14
+ } from "./types";
15
+ export {
16
+ TIMELINE_DEFAULT_STAGGER,
17
+ timelineItemTransitionPresets,
18
+ } from "./animations";
19
+ export type {
20
+ TimelineTransition,
21
+ TimelineTransitionPresets,
22
+ } from "./animations";
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { motion, useReducedMotion } from "framer-motion";
5
+
6
+ import { cn } from "../../../lib/utils";
7
+
8
+ import {
9
+ TimelineItemContext,
10
+ useTimelineContext,
11
+ useTimelineIndex,
12
+ } from "../timeline-base";
13
+ import type { TimelineItemCtx } from "../types";
14
+ import {
15
+ timelineConnectorVariants,
16
+ timelineItemVariants,
17
+ } from "../variants";
18
+ import {
19
+ TIMELINE_DEFAULT_STAGGER,
20
+ timelineItemTransitionPresets,
21
+ } from "./animations";
22
+ import type { TimelineItemAnimatedProps } from "./types";
23
+
24
+ export function TimelineItem({
25
+ className,
26
+ children,
27
+ transitionVariant = "default",
28
+ stagger = TIMELINE_DEFAULT_STAGGER,
29
+ ref,
30
+ ...rest
31
+ }: TimelineItemAnimatedProps & { ref?: React.Ref<HTMLLIElement> }) {
32
+ const { size, total } = useTimelineContext("TimelineItem");
33
+ const index = useTimelineIndex("TimelineItem");
34
+ const isLast = index === total - 1;
35
+ const itemCtx = useMemo<TimelineItemCtx>(
36
+ () => ({ index, isLast }),
37
+ [index, isLast],
38
+ );
39
+
40
+ const reducedMotion = useReducedMotion();
41
+ const motionless = transitionVariant === "none" || Boolean(reducedMotion);
42
+ const transition = timelineItemTransitionPresets[transitionVariant];
43
+ const delay = index * stagger;
44
+
45
+ return (
46
+ <TimelineItemContext.Provider value={itemCtx}>
47
+ <motion.li
48
+ ref={ref}
49
+ data-slot="timeline-item"
50
+ data-last={isLast ? "" : undefined}
51
+ className={cn(timelineItemVariants(), className)}
52
+ initial={motionless ? false : { opacity: 0, y: 12 }}
53
+ whileInView={motionless ? undefined : { opacity: 1, y: 0 }}
54
+ viewport={{ once: true, amount: 0.35 }}
55
+ transition={{ ...transition, delay }}
56
+ {...rest}
57
+ >
58
+ {!isLast ? (
59
+ <motion.span
60
+ aria-hidden="true"
61
+ data-slot="timeline-connector"
62
+ className={timelineConnectorVariants({ size })}
63
+ style={{ originY: 0 }}
64
+ initial={motionless ? false : { scaleY: 0, x: "-50%" }}
65
+ whileInView={motionless ? undefined : { scaleY: 1, x: "-50%" }}
66
+ viewport={{ once: true, amount: 0.35 }}
67
+ transition={{ ...transition, delay: delay + 0.12 }}
68
+ />
69
+ ) : null}
70
+ {children}
71
+ </motion.li>
72
+ </TimelineItemContext.Provider>
73
+ );
74
+ }
75
+
76
+ TimelineItem.displayName = "TimelineItem";
@@ -0,0 +1,21 @@
1
+ import type { TimelineItemProps, TimelineProps } from "../types";
2
+ import type { TimelineTransition } from "./animations";
3
+
4
+ export type TimelineAnimatedProps = TimelineProps;
5
+
6
+ /** Motion `li` uses different handler types than some React DOM events. */
7
+ export type TimelineItemMotionConflictProps =
8
+ | "onAnimationStart"
9
+ | "onDrag"
10
+ | "onDragEnd"
11
+ | "onDragStart";
12
+
13
+ export type TimelineItemAnimatedProps = Omit<
14
+ TimelineItemProps,
15
+ TimelineItemMotionConflictProps
16
+ > & {
17
+ /** Entrance easing/duration preset. `none` renders without motion. */
18
+ transitionVariant?: TimelineTransition;
19
+ /** Per-item entrance delay multiplier in seconds (index × stagger). */
20
+ stagger?: number;
21
+ };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ export {
4
+ Timeline,
5
+ TimelineContent,
6
+ TimelineDescription,
7
+ TimelineIndicator,
8
+ TimelineItem,
9
+ TimelineTitle,
10
+ } from "./timeline";
11
+ export type {
12
+ TimelineAppearance,
13
+ TimelineContentProps,
14
+ TimelineDescriptionProps,
15
+ TimelineIndicatorProps,
16
+ TimelineItemProps,
17
+ TimelineProps,
18
+ TimelineSize,
19
+ TimelineTitleProps,
20
+ TimelineTransition,
21
+ } from "./types";
22
+ export {
23
+ timelineConnectorVariants,
24
+ timelineContentVariants,
25
+ timelineDescriptionVariants,
26
+ timelineIndicatorVariants,
27
+ timelineItemVariants,
28
+ timelineTitleVariants,
29
+ timelineVariants,
30
+ } from "./variants";
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import {
4
+ Children,
5
+ createContext,
6
+ isValidElement,
7
+ useContext,
8
+ useMemo,
9
+ } from "react";
10
+
11
+ import { cn } from "../../lib/utils";
12
+
13
+ import type {
14
+ TimelineContentProps,
15
+ TimelineCtx,
16
+ TimelineDescriptionProps,
17
+ TimelineIndicatorProps,
18
+ TimelineItemCtx,
19
+ TimelineItemProps,
20
+ TimelineProps,
21
+ TimelineSize,
22
+ TimelineTitleProps,
23
+ } from "./types";
24
+ import {
25
+ timelineConnectorVariants,
26
+ timelineContentVariants,
27
+ timelineDescriptionVariants,
28
+ timelineIndicatorVariants,
29
+ timelineItemVariants,
30
+ timelineTitleVariants,
31
+ timelineVariants,
32
+ } from "./variants";
33
+
34
+ const TimelineContext = createContext<TimelineCtx | null>(null);
35
+
36
+ const TimelineIndexContext = createContext<number | null>(null);
37
+
38
+ export const TimelineItemContext = createContext<TimelineItemCtx | null>(null);
39
+
40
+ export function useTimelineContext(component: string): TimelineCtx {
41
+ const ctx = useContext(TimelineContext);
42
+ if (!ctx) {
43
+ throw new Error(`${component} must be used within <Timeline>`);
44
+ }
45
+ return ctx;
46
+ }
47
+
48
+ export function useTimelineIndex(component: string): number {
49
+ const index = useContext(TimelineIndexContext);
50
+ if (index === null) {
51
+ throw new Error(`${component} must be used within <Timeline>`);
52
+ }
53
+ return index;
54
+ }
55
+
56
+ function useTimelineSize(): TimelineSize {
57
+ return useContext(TimelineContext)?.size ?? "md";
58
+ }
59
+
60
+ function useTimelineIsLast(): boolean {
61
+ return useContext(TimelineItemContext)?.isLast ?? false;
62
+ }
63
+
64
+ export function Timeline({
65
+ appearance = "default",
66
+ size = "md",
67
+ className,
68
+ children,
69
+ ref,
70
+ ...rest
71
+ }: TimelineProps) {
72
+ const items = useMemo(
73
+ () => Children.toArray(children).filter(isValidElement),
74
+ [children],
75
+ );
76
+ const ctx = useMemo<TimelineCtx>(
77
+ () => ({
78
+ appearance: appearance ?? "default",
79
+ size: size ?? "md",
80
+ total: items.length,
81
+ }),
82
+ [appearance, size, items.length],
83
+ );
84
+
85
+ return (
86
+ <TimelineContext.Provider value={ctx}>
87
+ <ol
88
+ ref={ref}
89
+ data-slot="timeline"
90
+ className={cn(timelineVariants(), "m-0 list-none p-0", className)}
91
+ {...rest}
92
+ >
93
+ {items.map((child, index) => (
94
+ <TimelineIndexContext.Provider key={child.key} value={index}>
95
+ {child}
96
+ </TimelineIndexContext.Provider>
97
+ ))}
98
+ </ol>
99
+ </TimelineContext.Provider>
100
+ );
101
+ }
102
+
103
+ Timeline.displayName = "Timeline";
104
+
105
+ export function TimelineItem({
106
+ className,
107
+ children,
108
+ ref,
109
+ ...rest
110
+ }: TimelineItemProps) {
111
+ const { size, total } = useTimelineContext("TimelineItem");
112
+ const index = useTimelineIndex("TimelineItem");
113
+ const isLast = index === total - 1;
114
+ const itemCtx = useMemo<TimelineItemCtx>(
115
+ () => ({ index, isLast }),
116
+ [index, isLast],
117
+ );
118
+
119
+ return (
120
+ <TimelineItemContext.Provider value={itemCtx}>
121
+ <li
122
+ ref={ref}
123
+ data-slot="timeline-item"
124
+ data-last={isLast ? "" : undefined}
125
+ className={cn(timelineItemVariants(), className)}
126
+ {...rest}
127
+ >
128
+ {!isLast ? (
129
+ <span
130
+ aria-hidden="true"
131
+ data-slot="timeline-connector"
132
+ className={timelineConnectorVariants({ size })}
133
+ />
134
+ ) : null}
135
+ {children}
136
+ </li>
137
+ </TimelineItemContext.Provider>
138
+ );
139
+ }
140
+
141
+ TimelineItem.displayName = "TimelineItem";
142
+
143
+ export function TimelineIndicator({
144
+ className,
145
+ appearance,
146
+ children,
147
+ ref,
148
+ ...rest
149
+ }: TimelineIndicatorProps & { ref?: React.Ref<HTMLDivElement> }) {
150
+ const { appearance: rootAppearance, size } =
151
+ useTimelineContext("TimelineIndicator");
152
+ return (
153
+ <div
154
+ ref={ref}
155
+ data-slot="timeline-indicator"
156
+ className={cn(
157
+ timelineIndicatorVariants({
158
+ appearance: appearance ?? rootAppearance,
159
+ size,
160
+ }),
161
+ className,
162
+ )}
163
+ {...rest}
164
+ >
165
+ {children}
166
+ </div>
167
+ );
168
+ }
169
+
170
+ TimelineIndicator.displayName = "TimelineIndicator";
171
+
172
+ export function TimelineContent({
173
+ className,
174
+ children,
175
+ ref,
176
+ ...rest
177
+ }: TimelineContentProps & { ref?: React.Ref<HTMLDivElement> }) {
178
+ const size = useTimelineSize();
179
+ const isLast = useTimelineIsLast();
180
+ return (
181
+ <div
182
+ ref={ref}
183
+ data-slot="timeline-content"
184
+ className={cn(
185
+ timelineContentVariants({ size }),
186
+ isLast && "pb-0",
187
+ className,
188
+ )}
189
+ {...rest}
190
+ >
191
+ {children}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ TimelineContent.displayName = "TimelineContent";
197
+
198
+ export function TimelineTitle({
199
+ className,
200
+ ref,
201
+ ...rest
202
+ }: TimelineTitleProps & { ref?: React.Ref<HTMLDivElement> }) {
203
+ const size = useTimelineSize();
204
+ return (
205
+ <div
206
+ ref={ref}
207
+ data-slot="timeline-title"
208
+ className={cn(timelineTitleVariants({ size }), className)}
209
+ {...rest}
210
+ />
211
+ );
212
+ }
213
+
214
+ TimelineTitle.displayName = "TimelineTitle";
215
+
216
+ export function TimelineDescription({
217
+ className,
218
+ ref,
219
+ ...rest
220
+ }: TimelineDescriptionProps & { ref?: React.Ref<HTMLParagraphElement> }) {
221
+ const size = useTimelineSize();
222
+ return (
223
+ <p
224
+ ref={ref}
225
+ data-slot="timeline-description"
226
+ className={cn(timelineDescriptionVariants({ size }), className)}
227
+ {...rest}
228
+ />
229
+ );
230
+ }
231
+
232
+ TimelineDescription.displayName = "TimelineDescription";
@@ -0,0 +1,262 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import {
6
+ Timeline,
7
+ TimelineContent,
8
+ TimelineDescription,
9
+ TimelineIndicator,
10
+ TimelineItem,
11
+ TimelineTitle,
12
+ } from "./timeline";
13
+
14
+ describe("Timeline", () => {
15
+ it("should expose displayName", () => {
16
+ expect(Timeline.displayName).toBe("Timeline");
17
+ expect(TimelineItem.displayName).toBe("TimelineItem");
18
+ expect(TimelineIndicator.displayName).toBe("TimelineIndicator");
19
+ expect(TimelineContent.displayName).toBe("TimelineContent");
20
+ expect(TimelineTitle.displayName).toBe("TimelineTitle");
21
+ expect(TimelineDescription.displayName).toBe("TimelineDescription");
22
+ });
23
+
24
+ it("should stamp data-slot on timeline root and use ordered list markup", () => {
25
+ render(
26
+ <Timeline>
27
+ <TimelineItem>
28
+ <TimelineIndicator />
29
+ <TimelineContent>
30
+ <TimelineTitle>One</TimelineTitle>
31
+ </TimelineContent>
32
+ </TimelineItem>
33
+ </Timeline>,
34
+ );
35
+ const root = document.querySelector('[data-slot="timeline"]');
36
+ expect(root).toBeTruthy();
37
+ expect(root?.tagName).toBe("OL");
38
+ expect(document.querySelector('[data-slot="timeline-item"]')?.tagName).toBe(
39
+ "LI",
40
+ );
41
+ });
42
+
43
+ it("should apply default indicator appearance from the root", () => {
44
+ render(
45
+ <Timeline>
46
+ <TimelineItem>
47
+ <TimelineIndicator />
48
+ <TimelineContent>
49
+ <TimelineTitle>Cart</TimelineTitle>
50
+ </TimelineContent>
51
+ </TimelineItem>
52
+ </Timeline>,
53
+ );
54
+ const indicator = document.querySelector(
55
+ '[data-slot="timeline-indicator"]',
56
+ );
57
+ expect(indicator?.className).toContain(
58
+ "--zui-timeline-indicator-default-border",
59
+ );
60
+ });
61
+
62
+ it("should inherit appearance from the Timeline root", () => {
63
+ render(
64
+ <Timeline appearance="sky">
65
+ <TimelineItem>
66
+ <TimelineIndicator />
67
+ <TimelineContent>
68
+ <TimelineTitle>Sky</TimelineTitle>
69
+ </TimelineContent>
70
+ </TimelineItem>
71
+ </Timeline>,
72
+ );
73
+ const indicator = document.querySelector(
74
+ '[data-slot="timeline-indicator"]',
75
+ );
76
+ expect(indicator?.className).toContain(
77
+ "--zui-timeline-indicator-sky-border",
78
+ );
79
+ });
80
+
81
+ it("should let an indicator override the root appearance", () => {
82
+ render(
83
+ <Timeline appearance="sky">
84
+ <TimelineItem>
85
+ <TimelineIndicator />
86
+ <TimelineContent>
87
+ <TimelineTitle>Default sky</TimelineTitle>
88
+ </TimelineContent>
89
+ </TimelineItem>
90
+ <TimelineItem>
91
+ <TimelineIndicator appearance="rose" />
92
+ <TimelineContent>
93
+ <TimelineTitle>Override rose</TimelineTitle>
94
+ </TimelineContent>
95
+ </TimelineItem>
96
+ </Timeline>,
97
+ );
98
+ const indicators = document.querySelectorAll(
99
+ '[data-slot="timeline-indicator"]',
100
+ );
101
+ expect(indicators[0]?.className).toContain(
102
+ "--zui-timeline-indicator-sky-border",
103
+ );
104
+ expect(indicators[1]?.className).toContain(
105
+ "--zui-timeline-indicator-rose-border",
106
+ );
107
+ });
108
+
109
+ it("should pass size from Timeline context to indicators", () => {
110
+ render(
111
+ <Timeline size="sm">
112
+ <TimelineItem>
113
+ <TimelineIndicator />
114
+ <TimelineContent>
115
+ <TimelineTitle>Small</TimelineTitle>
116
+ </TimelineContent>
117
+ </TimelineItem>
118
+ </Timeline>,
119
+ );
120
+ const indicator = document.querySelector(
121
+ '[data-slot="timeline-indicator"]',
122
+ );
123
+ expect(indicator?.className).toMatch(/size-5/);
124
+ });
125
+
126
+ it("should render a connector on every item except the last", () => {
127
+ render(
128
+ <Timeline>
129
+ <TimelineItem>
130
+ <TimelineIndicator />
131
+ <TimelineContent>
132
+ <TimelineTitle>First</TimelineTitle>
133
+ </TimelineContent>
134
+ </TimelineItem>
135
+ <TimelineItem>
136
+ <TimelineIndicator />
137
+ <TimelineContent>
138
+ <TimelineTitle>Second</TimelineTitle>
139
+ </TimelineContent>
140
+ </TimelineItem>
141
+ <TimelineItem>
142
+ <TimelineIndicator />
143
+ <TimelineContent>
144
+ <TimelineTitle>Third</TimelineTitle>
145
+ </TimelineContent>
146
+ </TimelineItem>
147
+ </Timeline>,
148
+ );
149
+ const connectors = document.querySelectorAll(
150
+ '[data-slot="timeline-connector"]',
151
+ );
152
+ expect(connectors).toHaveLength(2);
153
+ const items = document.querySelectorAll('[data-slot="timeline-item"]');
154
+ expect(items[2]?.hasAttribute("data-last")).toBe(true);
155
+ expect(items[0]?.hasAttribute("data-last")).toBe(false);
156
+ });
157
+
158
+ it("should drop bottom padding on the last item's content", () => {
159
+ render(
160
+ <Timeline>
161
+ <TimelineItem>
162
+ <TimelineIndicator />
163
+ <TimelineContent data-testid="first">
164
+ <TimelineTitle>First</TimelineTitle>
165
+ </TimelineContent>
166
+ </TimelineItem>
167
+ <TimelineItem>
168
+ <TimelineIndicator />
169
+ <TimelineContent data-testid="last">
170
+ <TimelineTitle>Last</TimelineTitle>
171
+ </TimelineContent>
172
+ </TimelineItem>
173
+ </Timeline>,
174
+ );
175
+ expect(screen.getByTestId("first").className).toMatch(/pb-6/);
176
+ expect(screen.getByTestId("last").className).toMatch(/pb-0/);
177
+ expect(screen.getByTestId("last").className).not.toMatch(/pb-6/);
178
+ });
179
+
180
+ it("should render custom indicator children", () => {
181
+ render(
182
+ <Timeline>
183
+ <TimelineItem>
184
+ <TimelineIndicator>✓</TimelineIndicator>
185
+ <TimelineContent>
186
+ <TimelineTitle>Check</TimelineTitle>
187
+ </TimelineContent>
188
+ </TimelineItem>
189
+ </Timeline>,
190
+ );
191
+ expect(screen.getByText("✓")).toBeInTheDocument();
192
+ });
193
+
194
+ it("should stamp data-slot on content, title and description", () => {
195
+ render(
196
+ <Timeline>
197
+ <TimelineItem>
198
+ <TimelineIndicator />
199
+ <TimelineContent data-testid="c">
200
+ <TimelineTitle data-testid="t">T</TimelineTitle>
201
+ <TimelineDescription data-testid="d">D</TimelineDescription>
202
+ </TimelineContent>
203
+ </TimelineItem>
204
+ </Timeline>,
205
+ );
206
+ expect(document.querySelector('[data-slot="timeline-content"]')).toBe(
207
+ screen.getByTestId("c"),
208
+ );
209
+ expect(document.querySelector('[data-slot="timeline-title"]')).toBe(
210
+ screen.getByTestId("t"),
211
+ );
212
+ expect(document.querySelector('[data-slot="timeline-description"]')).toBe(
213
+ screen.getByTestId("d"),
214
+ );
215
+ });
216
+
217
+ it("should forward ref on Timeline", () => {
218
+ const ref = createRef<HTMLOListElement>();
219
+ render(
220
+ <Timeline ref={ref}>
221
+ <TimelineItem>
222
+ <TimelineIndicator />
223
+ <TimelineContent>
224
+ <TimelineTitle>A</TimelineTitle>
225
+ </TimelineContent>
226
+ </TimelineItem>
227
+ </Timeline>,
228
+ );
229
+ expect(ref.current?.getAttribute("data-slot")).toBe("timeline");
230
+ });
231
+
232
+ it("should forward ref on TimelineItem", () => {
233
+ const ref = createRef<HTMLLIElement>();
234
+ render(
235
+ <Timeline>
236
+ <TimelineItem ref={ref}>
237
+ <TimelineIndicator />
238
+ <TimelineContent>
239
+ <TimelineTitle>A</TimelineTitle>
240
+ </TimelineContent>
241
+ </TimelineItem>
242
+ </Timeline>,
243
+ );
244
+ expect(ref.current?.getAttribute("data-slot")).toBe("timeline-item");
245
+ });
246
+
247
+ it("should throw when TimelineItem is used outside Timeline", () => {
248
+ expect(() =>
249
+ render(
250
+ <TimelineItem>
251
+ <TimelineIndicator />
252
+ </TimelineItem>,
253
+ ),
254
+ ).toThrow(/must be used within <Timeline>/);
255
+ });
256
+
257
+ it("should throw when TimelineIndicator is used outside Timeline", () => {
258
+ expect(() => render(<TimelineIndicator />)).toThrow(
259
+ /must be used within <Timeline>/,
260
+ );
261
+ });
262
+ });
@@ -0,0 +1,24 @@
1
+ // timeline.tsx — default static entry (no framer-motion)
2
+ import {
3
+ Timeline as TimelineBase,
4
+ TimelineContent,
5
+ TimelineDescription,
6
+ TimelineIndicator,
7
+ TimelineItem,
8
+ TimelineTitle,
9
+ } from "./timeline-base";
10
+ import type { TimelineProps } from "./types";
11
+
12
+ export function Timeline(props: TimelineProps) {
13
+ return <TimelineBase {...props} />;
14
+ }
15
+
16
+ Timeline.displayName = "Timeline";
17
+
18
+ export {
19
+ TimelineContent,
20
+ TimelineDescription,
21
+ TimelineIndicator,
22
+ TimelineItem,
23
+ TimelineTitle,
24
+ };