trix-ui 0.2.2 → 0.2.4
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 +306 -306
- package/dist/commands/build.js +104 -104
- package/dist/index.js +0 -0
- package/package.json +10 -10
- package/registry/index.json +203 -180
- package/templates/components/ui/badge.tsx +182 -182
- package/templates/composites/music-player-card.tsx +583 -208
- package/templates/sections/signature-hero.tsx +92 -0
- package/templates/wrappers/Interative-wrapper.tsx +1 -1
- package/templates/wrappers/progress-wrapper.tsx +248 -0
- package/templates/wrappers/reveal-wrap.tsx +136 -0
- package/templates/sections/modern-hero.tsx +0 -1226
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
export type SignatureHeroProps = {
|
|
6
|
+
eyebrow?: string
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
primaryCta?: string
|
|
10
|
+
secondaryCta?: string
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SignatureHero({
|
|
15
|
+
eyebrow = "Signature",
|
|
16
|
+
title = "Design systems with editorial polish",
|
|
17
|
+
description = "Layered layouts, refined typography, and restrained motion for premium product experiences.",
|
|
18
|
+
primaryCta = "Get started",
|
|
19
|
+
secondaryCta = "View docs",
|
|
20
|
+
className
|
|
21
|
+
}: SignatureHeroProps) {
|
|
22
|
+
return (
|
|
23
|
+
<section
|
|
24
|
+
className={cn(
|
|
25
|
+
"relative overflow-hidden rounded-[32px] border border-border bg-background p-8 shadow-[0_30px_80px_-60px_rgba(15,23,42,0.6)] md:p-12",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
<div className="absolute inset-0">
|
|
30
|
+
<div className="absolute -left-24 -top-24 h-64 w-64 rounded-full bg-primary/15 blur-3xl" />
|
|
31
|
+
<div className="absolute -bottom-24 -right-24 h-72 w-72 rounded-full bg-muted/30 blur-3xl" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="relative z-10 grid gap-10 md:grid-cols-[1.2fr_0.8fr]">
|
|
35
|
+
<div className="space-y-5">
|
|
36
|
+
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
|
37
|
+
{eyebrow}
|
|
38
|
+
</p>
|
|
39
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground md:text-4xl">
|
|
40
|
+
{title}
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="max-w-xl text-sm text-muted-foreground md:text-base">
|
|
43
|
+
{description}
|
|
44
|
+
</p>
|
|
45
|
+
<div className="flex flex-wrap items-center gap-3 pt-2">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
className="rounded-full bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground shadow-sm"
|
|
49
|
+
>
|
|
50
|
+
{primaryCta}
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="rounded-full border border-border px-6 py-2 text-sm font-semibold text-foreground"
|
|
55
|
+
>
|
|
56
|
+
{secondaryCta}
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="grid gap-4">
|
|
62
|
+
{[
|
|
63
|
+
{
|
|
64
|
+
label: "Typography",
|
|
65
|
+
value: "System-led type scales"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: "Layout",
|
|
69
|
+
value: "Adaptive grid + spacing"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "Motion",
|
|
73
|
+
value: "Subtle, purposeful animations"
|
|
74
|
+
}
|
|
75
|
+
].map((item) => (
|
|
76
|
+
<div
|
|
77
|
+
key={item.label}
|
|
78
|
+
className="rounded-2xl border border-border/60 bg-card/70 p-4 backdrop-blur"
|
|
79
|
+
>
|
|
80
|
+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
|
|
81
|
+
{item.label}
|
|
82
|
+
</p>
|
|
83
|
+
<p className="mt-2 text-sm font-semibold text-foreground">
|
|
84
|
+
{item.value}
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
type Axis = "y" | "x";
|
|
7
|
+
|
|
8
|
+
export type ScrollProgressState = {
|
|
9
|
+
/** 0..1 (clamped if clamp=true) */
|
|
10
|
+
progress: number;
|
|
11
|
+
/** Raw (can go <0 or >1 when clamp=false) */
|
|
12
|
+
rawProgress: number;
|
|
13
|
+
/** True if any part of the element intersects the viewport */
|
|
14
|
+
inView: boolean;
|
|
15
|
+
/** Element rect in viewport coordinates */
|
|
16
|
+
rect: DOMRect | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface ScrollProgressWrapProps
|
|
20
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onProgress"> {
|
|
21
|
+
/** Render-prop child gets scroll progress state */
|
|
22
|
+
children: (state: ScrollProgressState) => React.ReactNode;
|
|
23
|
+
|
|
24
|
+
/** Scroll axis (default "y") */
|
|
25
|
+
axis?: Axis;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* When progress should start/end.
|
|
29
|
+
* - "cover": progress=0 when element just enters, progress=1 when it just leaves (default)
|
|
30
|
+
* - "contain": progress=0 when element top hits viewport top, progress=1 when bottom hits viewport bottom
|
|
31
|
+
*/
|
|
32
|
+
mode?: "cover" | "contain";
|
|
33
|
+
|
|
34
|
+
/** Clamp progress to 0..1 (default true) */
|
|
35
|
+
clamp?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Call on every progress change (rAF throttled)
|
|
39
|
+
* Receives clamped progress.
|
|
40
|
+
*/
|
|
41
|
+
onProgress?: (progress: number, state: ScrollProgressState) => void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Additional offset (px) applied to viewport size.
|
|
45
|
+
* Useful to trigger earlier/later without rootMargin complexity.
|
|
46
|
+
* Default 0.
|
|
47
|
+
*/
|
|
48
|
+
viewportOffset?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update frequency control. If true (default), uses requestAnimationFrame while scrolling/resizing.
|
|
52
|
+
* If false, uses scroll+resize events directly (still throttled by rAF internally).
|
|
53
|
+
*/
|
|
54
|
+
raf?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Debug overlay (default false)
|
|
58
|
+
* Shows progress and a small bar.
|
|
59
|
+
*/
|
|
60
|
+
debug?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clamp01(n: number) {
|
|
64
|
+
if (n < 0) return 0;
|
|
65
|
+
if (n > 1) return 1;
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function usePrefersReducedMotion(): boolean {
|
|
70
|
+
const [reduced, setReduced] = React.useState(false);
|
|
71
|
+
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (typeof window === "undefined") return;
|
|
74
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
75
|
+
const onChange = () => setReduced(media.matches);
|
|
76
|
+
onChange();
|
|
77
|
+
media.addEventListener?.("change", onChange);
|
|
78
|
+
return () => media.removeEventListener?.("change", onChange);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
return reduced;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ScrollProgressWrap
|
|
86
|
+
* Exposes element-local scroll progress (0..1) for timelines, progress bars, parallax, etc.
|
|
87
|
+
*/
|
|
88
|
+
export function ScrollProgressWrap({
|
|
89
|
+
axis = "y",
|
|
90
|
+
mode = "cover",
|
|
91
|
+
clamp = true,
|
|
92
|
+
onProgress,
|
|
93
|
+
viewportOffset = 0,
|
|
94
|
+
raf = true,
|
|
95
|
+
debug = false,
|
|
96
|
+
className,
|
|
97
|
+
style,
|
|
98
|
+
children,
|
|
99
|
+
...props
|
|
100
|
+
}: ScrollProgressWrapProps): React.JSX.Element {
|
|
101
|
+
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
102
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
103
|
+
|
|
104
|
+
const [state, setState] = React.useState<ScrollProgressState>({
|
|
105
|
+
progress: 0,
|
|
106
|
+
rawProgress: 0,
|
|
107
|
+
inView: false,
|
|
108
|
+
rect: null,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const last = React.useRef({
|
|
112
|
+
progress: Number.NaN,
|
|
113
|
+
rawProgress: Number.NaN,
|
|
114
|
+
inView: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const compute = React.useCallback(() => {
|
|
118
|
+
const el = ref.current;
|
|
119
|
+
if (!el) return;
|
|
120
|
+
|
|
121
|
+
const rect = el.getBoundingClientRect();
|
|
122
|
+
|
|
123
|
+
// viewport size with optional offset
|
|
124
|
+
const vw = window.innerWidth + viewportOffset * 2;
|
|
125
|
+
const vh = window.innerHeight + viewportOffset * 2;
|
|
126
|
+
|
|
127
|
+
// shift coordinates if viewportOffset is used (simulate bigger viewport)
|
|
128
|
+
const top = rect.top - viewportOffset;
|
|
129
|
+
const left = rect.left - viewportOffset;
|
|
130
|
+
|
|
131
|
+
const size = axis === "y" ? rect.height : rect.width;
|
|
132
|
+
const viewportSize = axis === "y" ? vh : vw;
|
|
133
|
+
const start = axis === "y" ? top : left;
|
|
134
|
+
const end = start + size;
|
|
135
|
+
|
|
136
|
+
const inView = end > 0 && start < viewportSize;
|
|
137
|
+
|
|
138
|
+
let rawProgress: number;
|
|
139
|
+
|
|
140
|
+
if (mode === "cover") {
|
|
141
|
+
// progress from "just entering" (end=0) to "just leaving" (start=viewport)
|
|
142
|
+
// When element end touches 0 => progress 0
|
|
143
|
+
// When element start touches viewport => progress 1
|
|
144
|
+
const denom = size + viewportSize || 1;
|
|
145
|
+
rawProgress = (viewportSize - start) / denom;
|
|
146
|
+
} else {
|
|
147
|
+
// "contain": from top hits viewport top to bottom hits viewport bottom
|
|
148
|
+
// top=0 => 0, bottom=viewport => 1
|
|
149
|
+
const denom = Math.max(1, viewportSize - size);
|
|
150
|
+
rawProgress = (-start) / denom;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const progress = clamp ? clamp01(rawProgress) : rawProgress;
|
|
154
|
+
|
|
155
|
+
// avoid excessive re-renders
|
|
156
|
+
const changed =
|
|
157
|
+
progress !== last.current.progress ||
|
|
158
|
+
rawProgress !== last.current.rawProgress ||
|
|
159
|
+
inView !== last.current.inView;
|
|
160
|
+
|
|
161
|
+
if (!changed) return;
|
|
162
|
+
|
|
163
|
+
last.current = { progress, rawProgress, inView };
|
|
164
|
+
|
|
165
|
+
const next: ScrollProgressState = {
|
|
166
|
+
progress,
|
|
167
|
+
rawProgress,
|
|
168
|
+
inView,
|
|
169
|
+
rect,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
setState(next);
|
|
173
|
+
onProgress?.(progress, next);
|
|
174
|
+
}, [axis, mode, clamp, onProgress, viewportOffset]);
|
|
175
|
+
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
if (typeof window === "undefined") return;
|
|
178
|
+
|
|
179
|
+
// Reduced motion doesn't mean "no updates", but we can still compute normally.
|
|
180
|
+
// Keeping this here in case you want to short-circuit later:
|
|
181
|
+
// if (reducedMotion) return;
|
|
182
|
+
|
|
183
|
+
let ticking = false;
|
|
184
|
+
|
|
185
|
+
const schedule = () => {
|
|
186
|
+
if (ticking) return;
|
|
187
|
+
ticking = true;
|
|
188
|
+
requestAnimationFrame(() => {
|
|
189
|
+
ticking = false;
|
|
190
|
+
compute();
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// initial
|
|
195
|
+
schedule();
|
|
196
|
+
|
|
197
|
+
const onScroll = () => schedule();
|
|
198
|
+
const onResize = () => schedule();
|
|
199
|
+
|
|
200
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
201
|
+
window.addEventListener("resize", onResize);
|
|
202
|
+
|
|
203
|
+
// If raf=true, we also keep things smooth during momentum scrolling on some devices
|
|
204
|
+
// by scheduling repeatedly while the page is scrolling.
|
|
205
|
+
// We still only update when events fire, but rAF ensures we render at the right time.
|
|
206
|
+
// (If you want "always-on" tracking, we can add an optional loop mode later.)
|
|
207
|
+
return () => {
|
|
208
|
+
window.removeEventListener("scroll", onScroll);
|
|
209
|
+
window.removeEventListener("resize", onResize);
|
|
210
|
+
};
|
|
211
|
+
}, [compute, reducedMotion, raf]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
ref={ref}
|
|
216
|
+
{...props}
|
|
217
|
+
className={cn("relative", className)}
|
|
218
|
+
style={style}
|
|
219
|
+
>
|
|
220
|
+
{children(state)}
|
|
221
|
+
|
|
222
|
+
{debug ? (
|
|
223
|
+
<div className="pointer-events-none absolute right-3 top-3 z-10 w-36 rounded-xl border border-border/60 bg-background/80 p-3 text-[11px] text-muted-foreground shadow-sm backdrop-blur">
|
|
224
|
+
<div className="flex items-center justify-between">
|
|
225
|
+
<span>progress</span>
|
|
226
|
+
<span className="font-medium text-foreground">
|
|
227
|
+
{Math.round(state.progress * 100)}%
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="mt-2 h-2 w-full overflow-hidden rounded-full bg-muted">
|
|
231
|
+
<div
|
|
232
|
+
className="h-full bg-foreground"
|
|
233
|
+
style={{ width: `${clamp01(state.progress) * 100}%` }}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="mt-2 flex items-center justify-between">
|
|
237
|
+
<span>inView</span>
|
|
238
|
+
<span className="font-medium text-foreground">
|
|
239
|
+
{state.inView ? "true" : "false"}
|
|
240
|
+
</span>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ScrollProgressWrap.displayName = "ScrollProgressWrap";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
type RevealDirection = "up" | "down" | "left" | "right" | "none"
|
|
7
|
+
|
|
8
|
+
export interface RevealWrapProps
|
|
9
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
/** Reveal only once (default true) */
|
|
11
|
+
once?: boolean
|
|
12
|
+
|
|
13
|
+
/** IntersectionObserver threshold (default 0.2) */
|
|
14
|
+
threshold?: number
|
|
15
|
+
|
|
16
|
+
/** IntersectionObserver rootMargin (default "0px 0px -10% 0px") */
|
|
17
|
+
rootMargin?: string
|
|
18
|
+
|
|
19
|
+
/** Transition delay in ms (default 0) */
|
|
20
|
+
delay?: number
|
|
21
|
+
|
|
22
|
+
/** Transition duration in ms (default 600) */
|
|
23
|
+
duration?: number
|
|
24
|
+
|
|
25
|
+
/** Direction to reveal from (default "up") */
|
|
26
|
+
direction?: RevealDirection
|
|
27
|
+
|
|
28
|
+
/** Offset distance in px (default 18) */
|
|
29
|
+
distance?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function usePrefersReducedMotion(): boolean {
|
|
33
|
+
const [reduced, setReduced] = React.useState(false)
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (typeof window === "undefined") return
|
|
37
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
38
|
+
const onChange = () => setReduced(media.matches)
|
|
39
|
+
onChange()
|
|
40
|
+
media.addEventListener?.("change", onChange)
|
|
41
|
+
return () => media.removeEventListener?.("change", onChange)
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
return reduced
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function RevealWrap({
|
|
48
|
+
once = true,
|
|
49
|
+
threshold = 0.2,
|
|
50
|
+
rootMargin = "0px 0px -10% 0px",
|
|
51
|
+
delay = 0,
|
|
52
|
+
duration = 600,
|
|
53
|
+
direction = "up",
|
|
54
|
+
distance = 18,
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
}: RevealWrapProps): React.JSX.Element {
|
|
60
|
+
const ref = React.useRef<HTMLDivElement | null>(null)
|
|
61
|
+
const [visible, setVisible] = React.useState(false)
|
|
62
|
+
const reducedMotion = usePrefersReducedMotion()
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (reducedMotion) {
|
|
66
|
+
setVisible(true)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const el = ref.current
|
|
71
|
+
if (!el) return
|
|
72
|
+
|
|
73
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
74
|
+
setVisible(true)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const observer = new IntersectionObserver(
|
|
79
|
+
(entries) => {
|
|
80
|
+
const entry = entries[0]
|
|
81
|
+
if (!entry) return
|
|
82
|
+
|
|
83
|
+
if (entry.isIntersecting) {
|
|
84
|
+
setVisible(true)
|
|
85
|
+
if (once) observer.unobserve(entry.target)
|
|
86
|
+
} else if (!once) {
|
|
87
|
+
setVisible(false)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{ threshold, rootMargin }
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
observer.observe(el)
|
|
94
|
+
return () => observer.disconnect()
|
|
95
|
+
}, [once, threshold, rootMargin, reducedMotion])
|
|
96
|
+
|
|
97
|
+
const clampedDistance = Math.max(0, distance)
|
|
98
|
+
const clampedDuration = Math.max(0, duration)
|
|
99
|
+
const clampedDelay = Math.max(0, delay)
|
|
100
|
+
|
|
101
|
+
const offsetClass =
|
|
102
|
+
direction === "up"
|
|
103
|
+
? "translate-y-[var(--reveal-distance)]"
|
|
104
|
+
: direction === "down"
|
|
105
|
+
? "-translate-y-[var(--reveal-distance)]"
|
|
106
|
+
: direction === "left"
|
|
107
|
+
? "translate-x-[var(--reveal-distance)]"
|
|
108
|
+
: direction === "right"
|
|
109
|
+
? "-translate-x-[var(--reveal-distance)]"
|
|
110
|
+
: ""
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
ref={ref}
|
|
115
|
+
{...props}
|
|
116
|
+
className={cn(
|
|
117
|
+
"transition-[opacity,transform] ease-out will-change-[opacity,transform]",
|
|
118
|
+
"motion-reduce:transition-none motion-reduce:transform-none",
|
|
119
|
+
visible
|
|
120
|
+
? "opacity-100 translate-x-0 translate-y-0"
|
|
121
|
+
: cn("opacity-0", offsetClass),
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
style={{
|
|
125
|
+
"--reveal-distance": `${clampedDistance}px`,
|
|
126
|
+
transitionDuration: `${clampedDuration}ms`,
|
|
127
|
+
transitionDelay: `${clampedDelay}ms`,
|
|
128
|
+
...style,
|
|
129
|
+
} as React.CSSProperties}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
RevealWrap.displayName = "RevealWrap"
|