@vamos-ui/react 0.2.0 → 0.3.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.
- package/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +2 -0
- package/dist/blocks/site/cta.js +2 -1
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.js +2 -1
- package/dist/blocks/site/gallery-section.js +3 -2
- package/dist/blocks/site/hero.js +12 -6
- package/dist/blocks/site/logo-strip.d.ts +5 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/media-text.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.js +2 -1
- package/dist/blocks/site/site-shell.d.ts +11 -0
- package/dist/blocks/site/site-shell.js +43 -16
- package/dist/blocks/site/stats-band.d.ts +3 -1
- package/dist/blocks/site/stats-band.js +11 -7
- package/dist/blocks/site/testimonials.js +2 -1
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.js +2 -0
- package/dist/business/signer-status.d.ts +53 -0
- package/dist/business/signer-status.js +76 -0
- package/dist/charts/bar-chart.js +5 -1
- package/dist/charts/combo-chart.js +1 -1
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/backup-codes.d.ts +42 -0
- package/dist/components/backup-codes.js +67 -0
- package/dist/components/breadcrumb.d.ts +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/qr-code.d.ts +36 -0
- package/dist/components/qr-code.js +27 -0
- package/dist/components/record-detail-layout.d.ts +2 -2
- package/dist/components/record-detail-layout.js +1 -1
- package/dist/components/sidebar.js +8 -8
- package/dist/components/sla-timer.d.ts +75 -0
- package/dist/components/sla-timer.js +142 -0
- package/dist/components/slug-input.js +1 -1
- package/dist/components/tour.d.ts +60 -0
- package/dist/components/tour.js +139 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/media/gallery.js +3 -1
- package/dist/motion/core.d.ts +32 -0
- package/dist/motion/core.js +77 -0
- package/dist/motion/count-up.d.ts +29 -0
- package/dist/motion/count-up.js +92 -0
- package/dist/motion/index.d.ts +11 -0
- package/dist/motion/index.js +13 -0
- package/dist/motion/motion-scope.d.ts +17 -0
- package/dist/motion/motion-scope.js +16 -0
- package/dist/motion/page-transition.d.ts +33 -0
- package/dist/motion/page-transition.js +61 -0
- package/dist/motion/parallax.d.ts +37 -0
- package/dist/motion/parallax.js +90 -0
- package/dist/motion/pointer.d.ts +38 -0
- package/dist/motion/pointer.js +117 -0
- package/dist/motion/reveal.d.ts +72 -0
- package/dist/motion/reveal.js +133 -0
- package/dist/motion/scroll-shrink.d.ts +18 -0
- package/dist/motion/scroll-shrink.js +47 -0
- package/dist/motion/skeleton-swap.d.ts +17 -0
- package/dist/motion/skeleton-swap.js +43 -0
- package/dist/motion/text-reveal.d.ts +30 -0
- package/dist/motion/text-reveal.js +29 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +3 -3
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +2 -1
- package/package.json +5 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type RevealEffect = "fade" | "rise" | "blur" | "scale" | "clip";
|
|
3
|
+
export declare const useIsoLayoutEffect: typeof React.useLayoutEffect;
|
|
4
|
+
/**
|
|
5
|
+
* `true` when the component mounted on the client after hydration (a page switch, a tab, a
|
|
6
|
+
* replay); `false` when it was server-rendered and is being hydrated. Server-rendered content
|
|
7
|
+
* is already on screen, so it is never hidden again; fresh content may start hidden.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useFreshMount(): boolean;
|
|
10
|
+
export interface UseRevealOptions {
|
|
11
|
+
/** Reveal once and stop observing. Default true. With false it hides again when scrolled back above it. */
|
|
12
|
+
once?: boolean;
|
|
13
|
+
/** IntersectionObserver rootMargin. Default `"0px 0px -8% 0px"` (a little before the bottom edge). */
|
|
14
|
+
rootMargin?: string;
|
|
15
|
+
/** IntersectionObserver threshold. Default 0. */
|
|
16
|
+
threshold?: number;
|
|
17
|
+
/** The children animate (Stagger): wait for the last child before settling. */
|
|
18
|
+
stagger?: boolean;
|
|
19
|
+
/** Skip the animation; the element just shows. */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Called once the element starts revealing. */
|
|
22
|
+
onReveal?: () => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scroll-reveal state machine on any element: `data-reveal-state` goes
|
|
26
|
+
* `pending → in → done`. CSS (motion.css) draws the states, so the hook only flips an attribute.
|
|
27
|
+
*
|
|
28
|
+
* - Server-rendered content that is already in view stays as it is (no flash, CLS 0).
|
|
29
|
+
* - Server-rendered content below the fold is hidden just before paint and revealed on scroll.
|
|
30
|
+
* - Freshly mounted content starts hidden and reveals when it enters the viewport.
|
|
31
|
+
* - Reduced motion, `data-motion="off"` or no IntersectionObserver → shown at once.
|
|
32
|
+
*
|
|
33
|
+
* Returns the props for the first render (spread them on the element).
|
|
34
|
+
*/
|
|
35
|
+
export declare function useReveal<T extends HTMLElement>(ref: React.RefObject<T | null>, { once, rootMargin, threshold, stagger, disabled, onReveal }?: UseRevealOptions): {
|
|
36
|
+
readonly "data-reveal-state": "pending" | undefined;
|
|
37
|
+
};
|
|
38
|
+
interface RevealBaseProps extends React.HTMLAttributes<HTMLDivElement>, Omit<UseRevealOptions, "stagger"> {
|
|
39
|
+
/** fade · rise (default) · blur · scale · clip (wipes up from the bottom edge). */
|
|
40
|
+
effect?: RevealEffect;
|
|
41
|
+
/** Delay before it starts, in ms. */
|
|
42
|
+
delay?: number;
|
|
43
|
+
/** Override the duration, in ms. Default: the motion token for the level (`--motion-reveal`). */
|
|
44
|
+
duration?: number;
|
|
45
|
+
/** Render the single child instead of a `div` (keeps your `ul`, `section`, `Container`…). */
|
|
46
|
+
asChild?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export type RevealProps = RevealBaseProps;
|
|
49
|
+
/**
|
|
50
|
+
* Shows its content when it scrolls into view — opacity and transform only, so nothing moves
|
|
51
|
+
* (CLS 0). Server-rendered content is never hidden before hydration; reduced motion and
|
|
52
|
+
* `data-motion="off"` show it at once.
|
|
53
|
+
*
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Reveal effect="rise" delay={80}><Card>…</Card></Reveal>
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare const Reveal: React.ForwardRefExoticComponent<RevealBaseProps & React.RefAttributes<HTMLDivElement>>;
|
|
59
|
+
export interface StaggerProps extends RevealBaseProps {
|
|
60
|
+
/** Time between two children, in ms. Default: the level's `--motion-stagger` (60 ms subtle, 90 ms full). */
|
|
61
|
+
step?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reveals its direct children one after another (the 13th and later share the last slot, so a
|
|
65
|
+
* long list never keeps people waiting). Use `asChild` on a list to keep its semantics.
|
|
66
|
+
*
|
|
67
|
+
* ```tsx
|
|
68
|
+
* <Stagger asChild step={70}><ul className="grid grid-cols-3">{plans.map(…)}</ul></Stagger>
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare const Stagger: React.ForwardRefExoticComponent<StaggerProps & React.RefAttributes<HTMLDivElement>>;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "radix-ui";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { inViewport, ioSupported, motionAllowed, observe, transitionEnd } from "./core.js";
|
|
7
|
+
const noopSubscribe = () => () => { };
|
|
8
|
+
export const useIsoLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
9
|
+
/**
|
|
10
|
+
* `true` when the component mounted on the client after hydration (a page switch, a tab, a
|
|
11
|
+
* replay); `false` when it was server-rendered and is being hydrated. Server-rendered content
|
|
12
|
+
* is already on screen, so it is never hidden again; fresh content may start hidden.
|
|
13
|
+
*/
|
|
14
|
+
export function useFreshMount() {
|
|
15
|
+
const hydrating = React.useSyncExternalStore(noopSubscribe, () => false, () => true);
|
|
16
|
+
const [fresh] = React.useState(!hydrating);
|
|
17
|
+
return fresh;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Scroll-reveal state machine on any element: `data-reveal-state` goes
|
|
21
|
+
* `pending → in → done`. CSS (motion.css) draws the states, so the hook only flips an attribute.
|
|
22
|
+
*
|
|
23
|
+
* - Server-rendered content that is already in view stays as it is (no flash, CLS 0).
|
|
24
|
+
* - Server-rendered content below the fold is hidden just before paint and revealed on scroll.
|
|
25
|
+
* - Freshly mounted content starts hidden and reveals when it enters the viewport.
|
|
26
|
+
* - Reduced motion, `data-motion="off"` or no IntersectionObserver → shown at once.
|
|
27
|
+
*
|
|
28
|
+
* Returns the props for the first render (spread them on the element).
|
|
29
|
+
*/
|
|
30
|
+
export function useReveal(ref, { once = true, rootMargin, threshold, stagger, disabled, onReveal } = {}) {
|
|
31
|
+
const fresh = useFreshMount();
|
|
32
|
+
const onRevealRef = React.useRef(onReveal);
|
|
33
|
+
onRevealRef.current = onReveal;
|
|
34
|
+
useIsoLayoutEffect(() => {
|
|
35
|
+
const el = ref.current;
|
|
36
|
+
if (!el)
|
|
37
|
+
return;
|
|
38
|
+
let timer;
|
|
39
|
+
let frame = 0;
|
|
40
|
+
const set = (s) => el.setAttribute("data-reveal-state", s);
|
|
41
|
+
const show = () => {
|
|
42
|
+
if (el.getAttribute("data-reveal-state") !== "pending")
|
|
43
|
+
return;
|
|
44
|
+
set("in");
|
|
45
|
+
onRevealRef.current?.();
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
// After the transition, drop the reveal styles so the element's own transitions
|
|
48
|
+
// (hover, focus) are its own again.
|
|
49
|
+
timer = setTimeout(() => el.getAttribute("data-reveal-state") === "in" && set("done"), transitionEnd(el, stagger) + 60);
|
|
50
|
+
};
|
|
51
|
+
if (disabled || !motionAllowed(el)) {
|
|
52
|
+
set("done");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!ioSupported()) {
|
|
56
|
+
if (fresh)
|
|
57
|
+
frame = requestAnimationFrame(() => show());
|
|
58
|
+
else
|
|
59
|
+
set("done");
|
|
60
|
+
return () => cancelAnimationFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
if (!fresh && inViewport(el)) {
|
|
63
|
+
set("done");
|
|
64
|
+
if (once)
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
set("pending");
|
|
69
|
+
const stop = observe(el, (entry) => {
|
|
70
|
+
if (entry.isIntersecting) {
|
|
71
|
+
show();
|
|
72
|
+
if (once)
|
|
73
|
+
stop();
|
|
74
|
+
}
|
|
75
|
+
else if (!once && entry.boundingClientRect.top > 0)
|
|
76
|
+
set("pending");
|
|
77
|
+
}, { rootMargin, threshold });
|
|
78
|
+
return () => {
|
|
79
|
+
stop();
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
}, [fresh, once, rootMargin, threshold, stagger, disabled]);
|
|
84
|
+
return { "data-reveal-state": fresh && !disabled ? "pending" : undefined };
|
|
85
|
+
}
|
|
86
|
+
function useComposedRef(...refs) {
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
return React.useCallback((node) => {
|
|
89
|
+
for (const r of refs) {
|
|
90
|
+
if (typeof r === "function")
|
|
91
|
+
r(node);
|
|
92
|
+
else if (r)
|
|
93
|
+
r.current = node;
|
|
94
|
+
}
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, refs);
|
|
97
|
+
}
|
|
98
|
+
const timing = (delay, duration, extra) => ({
|
|
99
|
+
...(delay ? { "--vm-delay": `${delay}ms` } : null),
|
|
100
|
+
...(duration ? { "--vm-duration": `${duration}ms` } : null),
|
|
101
|
+
...extra,
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Shows its content when it scrolls into view — opacity and transform only, so nothing moves
|
|
105
|
+
* (CLS 0). Server-rendered content is never hidden before hydration; reduced motion and
|
|
106
|
+
* `data-motion="off"` show it at once.
|
|
107
|
+
*
|
|
108
|
+
* ```tsx
|
|
109
|
+
* <Reveal effect="rise" delay={80}><Card>…</Card></Reveal>
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export const Reveal = React.forwardRef(function Reveal({ effect = "rise", delay, duration, once, rootMargin, threshold, disabled, onReveal, asChild, className, style, ...props }, ref) {
|
|
113
|
+
const inner = React.useRef(null);
|
|
114
|
+
const setRef = useComposedRef(ref, inner);
|
|
115
|
+
const initial = useReveal(inner, { once, rootMargin, threshold, disabled, onReveal });
|
|
116
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
117
|
+
return _jsx(Comp, { ref: setRef, "data-reveal": effect, ...initial, className: cn("vm-reveal", className), style: { ...timing(delay, duration), ...style }, ...props });
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* Reveals its direct children one after another (the 13th and later share the last slot, so a
|
|
121
|
+
* long list never keeps people waiting). Use `asChild` on a list to keep its semantics.
|
|
122
|
+
*
|
|
123
|
+
* ```tsx
|
|
124
|
+
* <Stagger asChild step={70}><ul className="grid grid-cols-3">{plans.map(…)}</ul></Stagger>
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const Stagger = React.forwardRef(function Stagger({ effect = "rise", step, delay, duration, once, rootMargin, threshold, disabled, onReveal, asChild, className, style, ...props }, ref) {
|
|
128
|
+
const inner = React.useRef(null);
|
|
129
|
+
const setRef = useComposedRef(ref, inner);
|
|
130
|
+
const initial = useReveal(inner, { once, rootMargin, threshold, disabled, onReveal, stagger: true });
|
|
131
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
132
|
+
return (_jsx(Comp, { ref: setRef, "data-reveal": effect, "data-reveal-stagger": "", ...initial, className: cn("vm-stagger", className), style: { ...timing(delay, duration, step ? { "--vm-step": `${step}ms` } : undefined), ...style }, ...props }));
|
|
133
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface UseScrollShrinkOptions {
|
|
3
|
+
/** Scroll distance in px after which the header counts as scrolled. Default 24. */
|
|
4
|
+
threshold?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Sticky header shrink: sets `data-scrolled` on the element once the page has scrolled past
|
|
8
|
+
* `threshold` (passive listener, rAF-throttled, no re-render). Pair it with the `vm-shrink`
|
|
9
|
+
* class, which tightens the bar with transforms only, so the content below never moves (CLS 0).
|
|
10
|
+
* Returns whether it is scrolled, for anything else you want to switch.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const ref = React.useRef<HTMLElement>(null);
|
|
14
|
+
* useScrollShrink(ref);
|
|
15
|
+
* <header ref={ref} className="vm-shrink sticky top-0">…</header>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function useScrollShrink(ref: React.RefObject<HTMLElement | null>, { threshold }?: UseScrollShrinkOptions): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Sticky header shrink: sets `data-scrolled` on the element once the page has scrolled past
|
|
5
|
+
* `threshold` (passive listener, rAF-throttled, no re-render). Pair it with the `vm-shrink`
|
|
6
|
+
* class, which tightens the bar with transforms only, so the content below never moves (CLS 0).
|
|
7
|
+
* Returns whether it is scrolled, for anything else you want to switch.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const ref = React.useRef<HTMLElement>(null);
|
|
11
|
+
* useScrollShrink(ref);
|
|
12
|
+
* <header ref={ref} className="vm-shrink sticky top-0">…</header>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useScrollShrink(ref, { threshold = 24 } = {}) {
|
|
16
|
+
const [scrolled, setScrolled] = React.useState(false);
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
let raf = 0;
|
|
19
|
+
let last = null;
|
|
20
|
+
const update = () => {
|
|
21
|
+
raf = 0;
|
|
22
|
+
const next = window.scrollY > threshold;
|
|
23
|
+
if (next === last)
|
|
24
|
+
return;
|
|
25
|
+
last = next;
|
|
26
|
+
const el = ref.current;
|
|
27
|
+
if (el) {
|
|
28
|
+
if (next)
|
|
29
|
+
el.setAttribute("data-scrolled", "");
|
|
30
|
+
else
|
|
31
|
+
el.removeAttribute("data-scrolled");
|
|
32
|
+
}
|
|
33
|
+
setScrolled(next);
|
|
34
|
+
};
|
|
35
|
+
const onScroll = () => {
|
|
36
|
+
if (!raf)
|
|
37
|
+
raf = requestAnimationFrame(update);
|
|
38
|
+
};
|
|
39
|
+
update();
|
|
40
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
41
|
+
return () => {
|
|
42
|
+
cancelAnimationFrame(raf);
|
|
43
|
+
window.removeEventListener("scroll", onScroll);
|
|
44
|
+
};
|
|
45
|
+
}, [ref, threshold]);
|
|
46
|
+
return scrolled;
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface SkeletonSwapProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** While true the skeleton shows; when it turns false the content cross-fades in over it. */
|
|
4
|
+
loading: boolean;
|
|
5
|
+
/** The placeholder. Give it the content's size so the box does not jump when it leaves. */
|
|
6
|
+
skeleton: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Skeleton → content without a flash: both share one grid cell, the content fades in while the
|
|
10
|
+
* skeleton fades out, then the skeleton unmounts. `aria-busy` while loading. Content that is
|
|
11
|
+
* there from the first render never animates; reduced motion swaps at once.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <SkeletonSwap loading={isLoading} skeleton={<InvoiceSkeleton />}><InvoiceTable rows={rows} /></SkeletonSwap>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const SkeletonSwap: React.ForwardRefExoticComponent<SkeletonSwapProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { motionAllowed } from "./core.js";
|
|
6
|
+
import { useIsoLayoutEffect } from "./reveal.js";
|
|
7
|
+
/**
|
|
8
|
+
* Skeleton → content without a flash: both share one grid cell, the content fades in while the
|
|
9
|
+
* skeleton fades out, then the skeleton unmounts. `aria-busy` while loading. Content that is
|
|
10
|
+
* there from the first render never animates; reduced motion swaps at once.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <SkeletonSwap loading={isLoading} skeleton={<InvoiceSkeleton />}><InvoiceTable rows={rows} /></SkeletonSwap>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const SkeletonSwap = React.forwardRef(function SkeletonSwap({ loading, skeleton, className, children, ...props }, ref) {
|
|
17
|
+
const inner = React.useRef(null);
|
|
18
|
+
const [prevLoading, setPrevLoading] = React.useState(loading);
|
|
19
|
+
const [leaving, setLeaving] = React.useState(false);
|
|
20
|
+
// Derived from the previous prop (React's "adjust state while rendering" pattern): the frame
|
|
21
|
+
// that flips loading → false already renders both layers, so nothing blinks.
|
|
22
|
+
if (prevLoading !== loading) {
|
|
23
|
+
setPrevLoading(loading);
|
|
24
|
+
setLeaving(!loading);
|
|
25
|
+
}
|
|
26
|
+
useIsoLayoutEffect(() => {
|
|
27
|
+
if (!leaving)
|
|
28
|
+
return;
|
|
29
|
+
if (!motionAllowed(inner.current)) {
|
|
30
|
+
setLeaving(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const t = setTimeout(() => setLeaving(false), 700); // safety net if animationend never fires
|
|
34
|
+
return () => clearTimeout(t);
|
|
35
|
+
}, [leaving]);
|
|
36
|
+
return (_jsxs("div", { ref: (node) => {
|
|
37
|
+
inner.current = node;
|
|
38
|
+
if (typeof ref === "function")
|
|
39
|
+
ref(node);
|
|
40
|
+
else if (ref)
|
|
41
|
+
ref.current = node;
|
|
42
|
+
}, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, className: cn("vm-swap", className), ...props, children: [loading ? null : (_jsx("div", { className: "vm-swap-content", "data-entering": leaving ? "" : undefined, children: children })), loading || leaving ? (_jsx("div", { className: "vm-swap-skeleton", "aria-hidden": !loading || undefined, "data-leaving": loading ? undefined : "", onAnimationEnd: (e) => e.target === e.currentTarget && setLeaving(false), children: skeleton })) : null] }));
|
|
43
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type UseRevealOptions } from "./reveal.js";
|
|
3
|
+
type TextTag = "h1" | "h2" | "h3" | "h4" | "p" | "span" | "div" | "blockquote";
|
|
4
|
+
export interface TextRevealProps extends Omit<React.HTMLAttributes<HTMLElement>, "children">, Omit<UseRevealOptions, "stagger"> {
|
|
5
|
+
/** The text. In `by="line"` mode, `\n` separates lines. */
|
|
6
|
+
children: string;
|
|
7
|
+
/** Element to render. Default `"h2"`. */
|
|
8
|
+
as?: TextTag;
|
|
9
|
+
/** Split into words (default) or lines. */
|
|
10
|
+
by?: "word" | "line";
|
|
11
|
+
/**
|
|
12
|
+
* mask: each unit rises from behind its own baseline (default) · fade · blur.
|
|
13
|
+
*/
|
|
14
|
+
effect?: "mask" | "fade" | "blur";
|
|
15
|
+
/** ms between two units. Default 45 (words) / 110 (lines). */
|
|
16
|
+
step?: number;
|
|
17
|
+
/** Delay before the first unit, in ms. */
|
|
18
|
+
delay?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A heading that reveals word by word (or line by line) as it scrolls into view. Assistive
|
|
22
|
+
* tech gets one plain string: the split copy is `aria-hidden`, the real text is a visually
|
|
23
|
+
* hidden twin. Transform and opacity only; reduced motion shows it at once.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <TextReveal as="h2">Small rooms, slow mornings, nothing to scroll.</TextReveal>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const TextReveal: React.ForwardRefExoticComponent<TextRevealProps & React.RefAttributes<HTMLElement>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { useReveal } from "./reveal.js";
|
|
6
|
+
/**
|
|
7
|
+
* A heading that reveals word by word (or line by line) as it scrolls into view. Assistive
|
|
8
|
+
* tech gets one plain string: the split copy is `aria-hidden`, the real text is a visually
|
|
9
|
+
* hidden twin. Transform and opacity only; reduced motion shows it at once.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <TextReveal as="h2">Small rooms, slow mornings, nothing to scroll.</TextReveal>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const TextReveal = React.forwardRef(function TextReveal({ children, as: Tag = "h2", by = "word", effect = "mask", step, delay, once, rootMargin, threshold, disabled, onReveal, className, style, ...props }, ref) {
|
|
16
|
+
const inner = React.useRef(null);
|
|
17
|
+
const initial = useReveal(inner, { once, rootMargin, threshold, disabled, onReveal });
|
|
18
|
+
const text = String(children);
|
|
19
|
+
const units = by === "line" ? text.split("\n") : text.split(/\s+/).filter(Boolean);
|
|
20
|
+
const gap = step ?? (by === "line" ? 110 : 45);
|
|
21
|
+
const Comp = Tag;
|
|
22
|
+
return (_jsxs(Comp, { ref: (node) => {
|
|
23
|
+
inner.current = node;
|
|
24
|
+
if (typeof ref === "function")
|
|
25
|
+
ref(node);
|
|
26
|
+
else if (ref)
|
|
27
|
+
ref.current = node;
|
|
28
|
+
}, "data-reveal": effect === "mask" ? "text" : effect, "data-text-reveal": by, ...initial, className: cn("vm-text-reveal", className), style: { "--vm-step": `${gap}ms`, ...(delay ? { "--vm-delay": `${delay}ms` } : null), ...style }, ...props, children: [_jsx("span", { className: "sr-only", children: by === "line" ? units.join(" ") : text }), _jsx("span", { "aria-hidden": "true", className: "vm-text-reveal-visual", children: units.map((u, i) => (_jsxs(React.Fragment, { children: [_jsx("span", { className: "vm-tr-mask", children: _jsx("span", { className: "vm-tr-unit", style: { "--vm-i": i }, children: u }) }), by === "word" && i < units.length - 1 ? " " : null] }, i))) })] }));
|
|
29
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface TickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** Seconds for one full loop. Default 40. */
|
|
4
|
+
duration?: number;
|
|
5
|
+
/** Which way the strip travels (mirrored in RTL). Default `"start"`. */
|
|
6
|
+
direction?: "start" | "end";
|
|
7
|
+
/** Element for each copy of the items, e.g. `"ul"` when the children are `li`. Default `"div"`. */
|
|
8
|
+
as?: "div" | "ul" | "ol";
|
|
9
|
+
/** Space between items (any CSS length). Default `clamp(2.5rem, 6cqi, 4.5rem)`. */
|
|
10
|
+
gap?: string;
|
|
11
|
+
/** Fade the start and end edges. Default true. */
|
|
12
|
+
fade?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A small pause control (a native checkbox — works without JavaScript). Moving content that
|
|
15
|
+
* lasts longer than five seconds needs one (WCAG 2.2.2). Default true.
|
|
16
|
+
*/
|
|
17
|
+
controls?: boolean;
|
|
18
|
+
/** Accessible name of the pause control. Default `"Pause animation"`. */
|
|
19
|
+
pauseLabel?: string;
|
|
20
|
+
/** Names the list of items (it lands on the first copy, e.g. the `ul`). */
|
|
21
|
+
"aria-label"?: string;
|
|
22
|
+
/** Classes for each copy of the items. */
|
|
23
|
+
groupClassName?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The motion kit's marquee: an endless strip of logos or words that follows the motion level. Pure CSS (server component): the items are rendered
|
|
27
|
+
* twice and the track slides by one copy. The copy is `aria-hidden` and `inert`, so assistive
|
|
28
|
+
* tech and the keyboard meet every item once. Pauses on hover and focus and with its pause
|
|
29
|
+
* control; under reduced motion or `data-motion="off"` it becomes a static, wrapped row.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <Ticker as="ul" duration={30}>{logos.map((l) => <li key={l.name}>…</li>)}</Ticker>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const Ticker: React.ForwardRefExoticComponent<TickerProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
/**
|
|
5
|
+
* The motion kit's marquee: an endless strip of logos or words that follows the motion level. Pure CSS (server component): the items are rendered
|
|
6
|
+
* twice and the track slides by one copy. The copy is `aria-hidden` and `inert`, so assistive
|
|
7
|
+
* tech and the keyboard meet every item once. Pauses on hover and focus and with its pause
|
|
8
|
+
* control; under reduced motion or `data-motion="off"` it becomes a static, wrapped row.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Ticker as="ul" duration={30}>{logos.map((l) => <li key={l.name}>…</li>)}</Ticker>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const Ticker = React.forwardRef(function Ticker({ duration = 40, direction = "start", as: Group = "div", gap, fade = true, controls = true, pauseLabel = "Pause animation", groupClassName, className, style, children, "aria-label": ariaLabel, ...props }, ref) {
|
|
15
|
+
return (_jsxs("div", { ref: ref, "data-direction": direction, "data-fade": fade ? "" : undefined, className: cn("vm-marquee", className), style: { "--vm-marquee-duration": `${duration}s`, ...(gap ? { "--vm-marquee-gap": gap } : null), ...style }, ...props, children: [_jsx("div", { className: "vm-marquee-viewport", children: _jsxs("div", { className: "vm-marquee-track", children: [_jsx(Group, { className: cn("vm-marquee-group", groupClassName), "aria-label": ariaLabel, children: children }), _jsx(Group, { className: cn("vm-marquee-group", groupClassName), "aria-hidden": "true", inert: true, "data-marquee-copy": "", children: children })] }) }), controls ? (_jsxs("label", { className: "vm-marquee-toggle", children: [_jsx("input", { type: "checkbox", className: "vm-marquee-pause sr-only", "aria-label": pauseLabel }), _jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", className: "vm-marquee-icon-pause", children: [_jsx("rect", { x: "4", y: "3", width: "2.5", height: "10", rx: "0.5" }), _jsx("rect", { x: "9.5", y: "3", width: "2.5", height: "10", rx: "0.5" })] }), _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", className: "vm-marquee-icon-play", children: _jsx("path", { d: "M5 3.2v9.6a.5.5 0 0 0 .77.42l7.2-4.8a.5.5 0 0 0 0-.84l-7.2-4.8A.5.5 0 0 0 5 3.2Z" }) })] })) : null] }));
|
|
16
|
+
});
|
|
@@ -83,6 +83,10 @@ export interface SortableTreeProps<N extends SortableTreeNode> {
|
|
|
83
83
|
moveMenu?: boolean;
|
|
84
84
|
/** Shown when `nodes` is empty. */
|
|
85
85
|
empty?: React.ReactNode;
|
|
86
|
+
/** Id of the selected row (`aria-selected`). Only used together with `onSelect`. */
|
|
87
|
+
selected?: string | null;
|
|
88
|
+
/** Click or Enter on a row selects it (Space still picks it up for a keyboard move). */
|
|
89
|
+
onSelect?: (node: N) => void;
|
|
86
90
|
labels?: Partial<SortableTreeLabels>;
|
|
87
91
|
"aria-label"?: string;
|
|
88
92
|
"aria-labelledby"?: string;
|
|
@@ -110,7 +110,7 @@ const allParents = (nodes) => {
|
|
|
110
110
|
* ```
|
|
111
111
|
*/
|
|
112
112
|
export function SortableTree(props) {
|
|
113
|
-
const { nodes: nodesProp, onMove, getLabel = defaultLabel, maxDepth, canDrop, disabled, expanded: expandedProp, defaultExpanded, onExpandedChange, indent = 24, guides = true, size = "md", renderItem, renderActions, moveMenu = true, empty, labels, className, ...aria } = props;
|
|
113
|
+
const { nodes: nodesProp, onMove, getLabel = defaultLabel, maxDepth, canDrop, disabled, expanded: expandedProp, defaultExpanded, onExpandedChange, indent = 24, guides = true, size = "md", renderItem, renderActions, moveMenu = true, empty, selected, onSelect, labels, className, ...aria } = props;
|
|
114
114
|
const t = useLabels("sortableTree", sortableTreeLabels, labels);
|
|
115
115
|
const { dir } = useLocale();
|
|
116
116
|
const reducedMotion = useReducedMotion();
|
|
@@ -325,6 +325,11 @@ export function SortableTree(props) {
|
|
|
325
325
|
announce(t("invalid", { item: labelOf(f.id), reason: op.ok }));
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
|
+
if (e.key === "Enter" && onSelect && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
onSelect(f.node);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
328
333
|
if ((e.shiftKey && e.key === "F10") || e.key === "ContextMenu") {
|
|
329
334
|
e.preventDefault();
|
|
330
335
|
setMenuId(f.id);
|
|
@@ -388,7 +393,7 @@ export function SortableTree(props) {
|
|
|
388
393
|
const me = info.get(f.id);
|
|
389
394
|
const isActive = f.id === activeId;
|
|
390
395
|
const depth = isActive && projection ? projection.depth : f.depth;
|
|
391
|
-
return (_jsx(TreeRow, { item: f, depth: depth, info: me, t: t, label: labelOf(f.id), expanded: openSet.has(f.id), onToggle: () => setOpen(f.id, !openSet.has(f.id)), tabbable: tabId === f.id, onFocus: () => setFocusedId(f.id), onKeyDown: onRowKeyDown, rowRef: (el) => {
|
|
396
|
+
return (_jsx(TreeRow, { item: f, depth: depth, info: me, t: t, label: labelOf(f.id), expanded: openSet.has(f.id), onToggle: () => setOpen(f.id, !openSet.has(f.id)), tabbable: tabId === f.id, onFocus: () => setFocusedId(f.id), onKeyDown: onRowKeyDown, selected: onSelect ? selected === f.id : undefined, onSelect: onSelect ? () => onSelect(f.node) : undefined, rowRef: (el) => {
|
|
392
397
|
if (el)
|
|
393
398
|
rowRefs.current.set(f.id, el);
|
|
394
399
|
else
|
|
@@ -434,10 +439,17 @@ function TreeRow(p) {
|
|
|
434
439
|
s.setNodeRef(el);
|
|
435
440
|
s.setActivatorNodeRef(el);
|
|
436
441
|
p.rowRef(el);
|
|
437
|
-
}, role: "treeitem", "aria-level": item.depth + 1, "aria-setsize": info.siblings, "aria-posinset": info.index + 1, "aria-expanded": hasKids ? expanded : undefined, "aria-labelledby": labelId, "aria-disabled": disabled || undefined, tabIndex: tabbable ? 0 : -1, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown Alt+ArrowLeft Alt+ArrowRight Shift+F10", ...attrs, ...(disabled ? {} : pointerListeners), onKeyDown: (e) => p.onKeyDown(e, item, disabled ? undefined : dndKeyDown), onFocus: (e) => {
|
|
442
|
+
}, role: "treeitem", "aria-level": item.depth + 1, "aria-setsize": info.siblings, "aria-posinset": info.index + 1, "aria-expanded": hasKids ? expanded : undefined, "aria-labelledby": labelId, "aria-disabled": disabled || undefined, "aria-selected": p.selected, tabIndex: tabbable ? 0 : -1, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown Alt+ArrowLeft Alt+ArrowRight Shift+F10", ...attrs, ...(disabled ? {} : pointerListeners), onKeyDown: (e) => p.onKeyDown(e, item, disabled ? undefined : dndKeyDown), onFocus: (e) => {
|
|
438
443
|
if (e.target === e.currentTarget)
|
|
439
444
|
p.onFocus();
|
|
440
|
-
},
|
|
445
|
+
}, onClick: p.onSelect
|
|
446
|
+
? (e) => {
|
|
447
|
+
// The expand toggle, row actions and the Move menu keep their own click.
|
|
448
|
+
if (e.target.closest('button, a, input, [data-slot="sortable-tree-toggle"]'))
|
|
449
|
+
return;
|
|
450
|
+
p.onSelect?.();
|
|
451
|
+
}
|
|
452
|
+
: undefined, "data-slot": "sortable-tree-item", "data-selected": p.selected ? "" : undefined, "data-depth": depth, "data-dragging": s.isDragging ? "" : undefined, "data-invalid": invalid ? "" : undefined, "data-disabled": disabled ? "" : undefined, style: { transform: CSS.Translate.toString(s.transform), transition: reducedMotion ? undefined : s.transition, paddingInlineStart: depth * indent, "--depth-px": `${depth * indent}px` }, className: cn("vui-tree-row group/row relative flex min-w-0 items-center gap-1 rounded-ui-sm pe-1 text-fg outline-none", "transition-colors hover:bg-hover focus-visible:bg-hover focus-visible:shadow-[inset_0_0_0_2px_var(--ring)]", size === "sm" ? "h-8 text-[13px]" : "h-9 text-sm", !disabled && "cursor-grab active:cursor-grabbing"), children: [guides && !s.isDragging
|
|
441
453
|
? Array.from({ length: depth }, (_, l) => (_jsx("span", { "aria-hidden": true, "data-slot": "sortable-tree-guide", className: "pointer-events-none absolute inset-y-0 w-px bg-line", style: { insetInlineStart: l * indent + 12 } }, l)))
|
|
442
454
|
: null, s.isDragging ? _jsx("span", { "aria-hidden": true, "data-slot": "sortable-tree-indicator", className: "vui-tree-indicator" }) : null, _jsx("span", { className: "inline-flex size-6 shrink-0 items-center justify-center", children: hasKids ? (_jsx("span", { "aria-hidden": true, "data-slot": "sortable-tree-toggle", onClick: onToggle, onMouseDown: (e) => e.stopPropagation(), title: expanded ? t("collapse") : t("expand"), className: "inline-flex size-6 cursor-pointer items-center justify-center rounded-ui-sm text-fg-subtle hover:bg-hover hover:text-fg", children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform motion-reduce:transition-none rtl:-scale-x-100", expanded && "rotate-90 rtl:rotate-90") }) })) : null }), disabled ? (_jsx("span", { "aria-hidden": true, className: "w-4 shrink-0" })) : (_jsx(GripVertical, { "aria-hidden": true, className: "size-4 shrink-0 text-fg-subtle opacity-50 transition-opacity group-hover/row:opacity-100 group-focus-visible/row:opacity-100" })), _jsx("span", { id: labelId, className: "flex min-w-0 flex-1 items-center gap-2 ps-1", children: p.renderItem ? p.renderItem(item.node, { depth, isDragging: s.isDragging, overlay: false, expanded, childCount }) : _jsx("span", { className: "truncate", children: label }) }), p.actions, p.menu] }));
|
|
443
455
|
}
|
package/dist/styles/blocks.css
CHANGED
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
}
|
|
260
260
|
[data-density="compact"] .vb-navbar,
|
|
261
261
|
.vb-navbar[data-density="compact"] { --vb-nav-h: 3.75rem; }
|
|
262
|
-
[data-material="glass"] .vb-navbar { --vb-nav-fill: color-mix(in oklch, var(--scene)
|
|
262
|
+
[data-material="glass"] .vb-navbar { --vb-nav-fill: color-mix(in oklch, var(--scene) 90%, transparent); }
|
|
263
263
|
.vb-navbar[data-variant="rail"],
|
|
264
264
|
.vb-navbar[data-variant="minimal"] { border-block-end-color: transparent; }
|
|
265
265
|
.vb-navbar-row { min-height: var(--vb-nav-h); }
|
|
@@ -540,8 +540,8 @@
|
|
|
540
540
|
}
|
|
541
541
|
.vb-navbar[data-sticky]:not([data-overlay]) { background-color: var(--scene); }
|
|
542
542
|
[data-material="glass"] .vb-navbar[data-sticky] {
|
|
543
|
-
-webkit-backdrop-filter: blur(var(--blur, 0px)) saturate(160%);
|
|
544
|
-
backdrop-filter: blur(var(--blur, 0px)) saturate(160%);
|
|
543
|
+
-webkit-backdrop-filter: blur(max(var(--blur, 0px), 14px)) saturate(160%);
|
|
544
|
+
backdrop-filter: blur(max(var(--blur, 0px), 14px)) saturate(160%);
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
547
|
@supports not (animation-timeline: scroll()) {
|