@vamos-ui/react 0.2.0 → 0.4.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/block-image.js +1 -1
- package/dist/blocks/block-link.js +1 -1
- package/dist/blocks/index.d.ts +2 -2
- package/dist/blocks/index.js +2 -2
- package/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +3 -1
- package/dist/blocks/site/announcement-bar.js +1 -1
- package/dist/blocks/site/article-layout.d.ts +1 -0
- package/dist/blocks/site/article-layout.js +2 -1
- package/dist/blocks/site/blog-list.d.ts +1 -0
- package/dist/blocks/site/blog-list.js +3 -2
- package/dist/blocks/site/contact-dock.js +3 -1
- package/dist/blocks/site/contact-form.js +1 -1
- package/dist/blocks/site/cta.d.ts +2 -2
- package/dist/blocks/site/cta.js +8 -5
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.d.ts +1 -0
- package/dist/blocks/site/feature-grid.js +7 -5
- package/dist/blocks/site/footer.d.ts +1 -1
- package/dist/blocks/site/footer.js +8 -7
- package/dist/blocks/site/gallery-section.d.ts +6 -1
- package/dist/blocks/site/gallery-section.js +6 -4
- package/dist/blocks/site/hero.d.ts +3 -2
- package/dist/blocks/site/hero.js +14 -6
- package/dist/blocks/site/logo-strip.d.ts +6 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/map-hours.d.ts +2 -2
- package/dist/blocks/site/map-hours.js +4 -2
- package/dist/blocks/site/media-text.d.ts +4 -1
- package/dist/blocks/site/media-text.js +6 -4
- package/dist/blocks/site/menu-list.d.ts +7 -3
- package/dist/blocks/site/menu-list.js +34 -7
- package/dist/blocks/site/navbar.d.ts +1 -0
- package/dist/blocks/site/navbar.js +6 -5
- package/dist/blocks/site/not-found.js +2 -1
- package/dist/blocks/site/page-intro.d.ts +1 -0
- package/dist/blocks/site/page-intro.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.d.ts +1 -0
- package/dist/blocks/site/process.js +4 -2
- package/dist/blocks/site/registry-1.d.ts +1 -1
- package/dist/blocks/site/registry-2.d.ts +5 -0
- package/dist/blocks/site/registry-2.js +2 -2
- package/dist/blocks/site/rich-text-section.js +2 -1
- package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
- package/dist/blocks/site/shared/heading-lines.js +32 -0
- package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
- package/dist/blocks/site/shared/image-sizes.js +37 -0
- package/dist/blocks/site/shared/island-keys.d.ts +1 -1
- package/dist/blocks/site/shared/island-keys.js +1 -1
- package/dist/blocks/site/shared/labels-2.d.ts +9 -1
- package/dist/blocks/site/shared/labels-2.js +24 -6
- package/dist/blocks/site/shared/open-now.d.ts +1 -1
- package/dist/blocks/site/shared/open-now.js +1 -1
- package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
- package/dist/blocks/site/shared/opening-hours.js +36 -7
- package/dist/blocks/site/shared/tap.d.ts +9 -0
- package/dist/blocks/site/shared/tap.js +16 -0
- 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/team.d.ts +1 -1
- package/dist/blocks/site/team.js +4 -3
- package/dist/blocks/site/testimonials.d.ts +2 -2
- package/dist/blocks/site/testimonials.js +5 -4
- 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 +6 -2
- package/dist/charts/chart-shell.d.ts +9 -2
- package/dist/charts/chart-shell.js +10 -4
- package/dist/charts/chart.js +1 -1
- package/dist/charts/combo-chart.js +5 -7
- package/dist/charts/utils.d.ts +10 -0
- package/dist/charts/utils.js +46 -0
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/activity-timeline.js +5 -5
- package/dist/components/aspect-ratio.d.ts +26 -0
- package/dist/components/aspect-ratio.js +42 -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/carousel.d.ts +12 -3
- package/dist/components/carousel.js +8 -5
- package/dist/components/collapsible.d.ts +36 -0
- package/dist/components/collapsible.js +37 -0
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/image.d.ts +18 -2
- package/dist/components/image.js +12 -5
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -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 +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/lib/value-format.js +1 -1
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/locales/ar.d.ts +2087 -0
- package/dist/locales/ar.js +2088 -0
- package/dist/locales/de.d.ts +2087 -0
- package/dist/locales/de.js +2088 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/es.d.ts +2087 -0
- package/dist/locales/es.js +2087 -0
- package/dist/locales/fr.d.ts +2087 -0
- package/dist/locales/fr.js +2088 -0
- package/dist/locales/tr.d.ts +1 -0
- package/dist/locales/tr.js +1 -0
- package/dist/media/gallery.js +3 -1
- package/dist/media/media-library.js +1 -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 +32 -0
- package/dist/motion/text-reveal.js +31 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/render/block-renderer.js +1 -1
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +21 -12
- package/dist/styles/editors.css +2 -1
- package/dist/styles/media.css +31 -5
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +41 -10
- package/package.json +5 -1
package/dist/locales/tr.d.ts
CHANGED
package/dist/locales/tr.js
CHANGED
package/dist/media/gallery.js
CHANGED
|
@@ -81,7 +81,9 @@ export function Gallery({ items, layout = "grid", columns = DEFAULT_COLUMNS, asp
|
|
|
81
81
|
/* --- edit mode ---------------------------------------------------- */
|
|
82
82
|
if (editable) {
|
|
83
83
|
const addTile = onAdd ? (_jsxs("button", { type: "button", "data-slot": "gallery-add", onClick: onAdd, className: cn("vui-gallery-add focus-ring group/add flex w-full flex-col items-center justify-center gap-1.5 border border-dashed border-field-border bg-surface-2 text-center transition-colors hover:border-field-border-hover hover:bg-hover", items.length === 0 ? "min-h-56 rounded-ui-lg p-8" : "mt-(--gallery-gap) min-h-14 rounded-ui px-4 py-3 sm:flex-row sm:gap-2.5"), children: [_jsx(ImagePlus, { "aria-hidden": true, className: cn("text-fg-muted transition-colors group-hover/add:text-fg", items.length === 0 ? "size-7" : "size-[18px]"), strokeWidth: 1.6 }), _jsx("span", { className: "text-sm font-medium text-fg", children: t("add") }), items.length === 0 ? _jsx("span", { className: "text-[13px] text-fg-muted", children: t("addHint") }) : null] })) : null;
|
|
84
|
-
return (_jsxs("div", { ...rootProps, children: [items.length ? (_jsx(SortableGrid, { "aria-label": listLabel, items: sources, getId: (s) => s.id, getLabel: (s) => s.alt || s.caption || filenameOf(items, s.id) || s.id, onReorder: (next) => onReorder?.(next.map((s) => s.id)), disabled: !onReorder, handle: true,
|
|
84
|
+
return (_jsxs("div", { ...rootProps, children: [items.length ? (_jsx(SortableGrid, { "aria-label": listLabel, items: sources, getId: (s) => s.id, getLabel: (s) => s.alt || s.caption || filenameOf(items, s.id) || s.id, onReorder: (next) => onReorder?.(next.map((s) => s.id)), disabled: !onReorder, handle: true,
|
|
85
|
+
// Honour `columns` while editing too: a fixed 10rem minimum fell back to one column on phones.
|
|
86
|
+
minItemWidth: `calc((100% - ${(Math.max(1, cols) - 1) * gapPx}px) / ${Math.max(1, cols)} - 0.5px)`, className: "gap-(--gallery-gap)", itemClassName: "overflow-hidden", renderActions: onRemove
|
|
85
87
|
? (s) => (_jsx(IconButton, { label: t("remove", { name: s.alt || filenameOf(items, s.id) || s.id }), size: "xs", variant: "ghost", className: "bg-popover text-fg-muted shadow-sm hover:text-danger", onClick: () => onRemove(s.id), children: _jsx(Trash2, {}) }))
|
|
86
88
|
: undefined, renderItem: (s) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(Image, { src: assetOf(items, s.id) ?? s.src, alt: s.alt, aspect: aspect, sizes: "12rem", widths: [320, 480] }), onCaptionChange ? (_jsx("input", { type: "text", defaultValue: s.caption ?? "", "aria-label": t("caption", { name: s.alt || filenameOf(items, s.id) || s.id }), placeholder: t("captionPlaceholder"), onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
87
89
|
e.stopPropagation();
|
|
@@ -717,7 +717,7 @@ export const MediaLibrary = React.forwardRef(function MediaLibrary({ adapter: ad
|
|
|
717
717
|
const isSel = selected.includes(a.id);
|
|
718
718
|
const disabled = isDisabled(a);
|
|
719
719
|
const missing = isAltMissing(a);
|
|
720
|
-
return (_jsxs("div", { role: "gridcell", "data-tile": index, "data-slot": "media-tile", "data-selected": isSel ? "" : undefined, "data-active": activeId === a.id ? "" : undefined, "aria-selected": selection === "none" ? undefined : isSel, "aria-disabled": disabled || undefined, "aria-label": [a.filename, missing ? t("missingAlt") : null, a.status === "processing" ? t("processing") : a.status === "failed" ? t("failed") : null].filter(Boolean).join(", "), tabIndex: index === focusIndex ? 0 : -1, onClick: (e) => onTileClick(e, index), onDoubleClick: () => detailsPanel && openDetails(a), onFocus: () => setFocusIndex(index), className: cn("vui-media-tile group/tile focus-ring relative grid min-w-0 cursor-pointer gap-1.5 rounded-ui outline-none", disabled && "cursor-not-allowed opacity-45"), children: [_jsxs("div", { className: "vui-media-tile-frame relative overflow-hidden rounded-ui bg-surface-2", children: [_jsx(MediaThumb, { asset: a, rounded: "none" }), a.kind === "video" && a.duration ? _jsx("span", { className: "tabular absolute end-1.5 bottom-1.5 rounded-sm bg-[color-mix(in_srgb,var(--scene)_82%,transparent)] px-1.5 py-0.5 text-[10.5px] font-medium text-fg", children: f.duration(a.duration) }) : null, a.status === "failed" ? _jsx(Badge, { tone: "danger", className: "absolute start-1.5 top-1.5", children: t("failed") }) : null, missing ? _jsx("span", { "aria-hidden": true, title: t("missingAlt"), className: "vui-media-alt-dot absolute end-2 top-2 size-2 rounded-full bg-warning" }) : null, selection !== "none" ? (_jsx("span", { "aria-hidden": true, "data-slot": "media-tile-check", className: cn("vui-media-check absolute start-1.5 top-1.5 flex size-5 items-center justify-center rounded-full", isSel ? "bg-accent text-accent-fg" : "border border-[color-mix(in_srgb,var(--fg)_30%,transparent)] bg-[color-mix(in_srgb,var(--scene)_70%,transparent)] text-transparent"), children: _jsx(Check, { className: "size-3", strokeWidth: 3 }) })) : null] }), _jsx("span", { className: cn("truncate px-0.5 text-[12px] leading-tight", isSel ? "text-fg" : "text-fg-muted"), children: a.filename })] }, a.id));
|
|
720
|
+
return (_jsxs("div", { role: "gridcell", "data-tile": index, "data-slot": "media-tile", "data-selected": isSel ? "" : undefined, "data-active": activeId === a.id ? "" : undefined, "aria-selected": selection === "none" ? undefined : isSel, "aria-disabled": disabled || undefined, "aria-label": [a.filename, a.kind === "video" && a.duration ? f.duration(a.duration) : null, missing ? t("missingAlt") : null, a.status === "processing" ? t("processing") : a.status === "failed" ? t("failed") : null].filter(Boolean).join(", "), tabIndex: index === focusIndex ? 0 : -1, onClick: (e) => onTileClick(e, index), onDoubleClick: () => detailsPanel && openDetails(a), onFocus: () => setFocusIndex(index), className: cn("vui-media-tile group/tile focus-ring relative grid min-w-0 cursor-pointer gap-1.5 rounded-ui outline-none", disabled && "cursor-not-allowed opacity-45"), children: [_jsxs("div", { className: "vui-media-tile-frame relative overflow-hidden rounded-ui bg-surface-2", children: [_jsx(MediaThumb, { asset: a, rounded: "none" }), a.kind === "video" && a.duration ? _jsx("span", { "aria-hidden": true, className: "tabular absolute end-1.5 bottom-1.5 rounded-sm bg-[color-mix(in_srgb,var(--scene)_82%,transparent)] px-1.5 py-0.5 text-[10.5px] font-medium text-fg", children: f.duration(a.duration) }) : null, a.status === "failed" ? _jsx(Badge, { "aria-hidden": true, tone: "danger", className: "absolute start-1.5 top-1.5", children: t("failed") }) : null, missing ? _jsx("span", { "aria-hidden": true, title: t("missingAlt"), className: "vui-media-alt-dot absolute end-2 top-2 size-2 rounded-full bg-warning" }) : null, selection !== "none" ? (_jsx("span", { "aria-hidden": true, "data-slot": "media-tile-check", className: cn("vui-media-check absolute start-1.5 top-1.5 flex size-5 items-center justify-center rounded-full", isSel ? "bg-accent text-accent-fg" : "border border-[color-mix(in_srgb,var(--fg)_30%,transparent)] bg-[color-mix(in_srgb,var(--scene)_70%,transparent)] text-transparent"), children: _jsx(Check, { className: "size-3", strokeWidth: 3 }) })) : null] }), _jsx("span", { className: cn("truncate px-0.5 text-[12px] leading-tight", isSel ? "text-fg" : "text-fg-muted"), children: a.filename })] }, a.id));
|
|
721
721
|
};
|
|
722
722
|
const gridStyle = { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap: GRID_GAP };
|
|
723
723
|
const gridEl = (_jsx("div", { ref: gridRef, role: "grid", "aria-label": t("files"), "aria-multiselectable": selection === "multiple" || undefined, "aria-rowcount": virtual ? rows.length : undefined, "aria-describedby": liveId, "data-slot": "media-grid", onKeyDown: onGridKeyDown, className: "relative", style: virtual ? { height: virtualizer.getTotalSize() } : undefined, children: virtual
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion core: level lookup, a shared IntersectionObserver pool and timing helpers.
|
|
3
|
+
* No React in here — every primitive in this folder builds on these.
|
|
4
|
+
*/
|
|
5
|
+
export type MotionLevel = "full" | "subtle" | "off";
|
|
6
|
+
/** `true` when the OS asks for reduced motion. `false` on the server. */
|
|
7
|
+
export declare function prefersReducedMotion(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The motion level that applies to an element: `data-motion="off"` on `<html>` always wins,
|
|
10
|
+
* then the closest `[data-motion]` ancestor (SiteShell sets one), then `"subtle"`.
|
|
11
|
+
* Reduced motion turns every level into `"off"`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function motionLevel(el?: Element | null): MotionLevel;
|
|
14
|
+
/** Shorthand: may this element animate at all? */
|
|
15
|
+
export declare const motionAllowed: (el?: Element | null) => boolean;
|
|
16
|
+
type Handler = (entry: IntersectionObserverEntry) => void;
|
|
17
|
+
export declare const ioSupported: () => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Observe an element with a pooled IntersectionObserver. Returns the unobserve function.
|
|
20
|
+
* Without IntersectionObserver the handler is never called — callers fall back themselves.
|
|
21
|
+
*/
|
|
22
|
+
export declare function observe(el: Element, handler: Handler, { rootMargin, threshold }?: {
|
|
23
|
+
rootMargin?: string;
|
|
24
|
+
threshold?: number;
|
|
25
|
+
}): () => void;
|
|
26
|
+
/** Is the element's box (at least partly) inside the viewport right now? */
|
|
27
|
+
export declare function inViewport(el: Element): boolean;
|
|
28
|
+
/** Longest `duration + delay` (ms) among the element's transitions — or its children's, for staggers. */
|
|
29
|
+
export declare function transitionEnd(el: Element, children?: boolean): number;
|
|
30
|
+
/** Easing used by the JS-driven primitives (count-up): ease-out quint. */
|
|
31
|
+
export declare const easeOutQuint: (t: number) => number;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion core: level lookup, a shared IntersectionObserver pool and timing helpers.
|
|
3
|
+
* No React in here — every primitive in this folder builds on these.
|
|
4
|
+
*/
|
|
5
|
+
const REDUCE = "(prefers-reduced-motion: reduce)";
|
|
6
|
+
/** `true` when the OS asks for reduced motion. `false` on the server. */
|
|
7
|
+
export function prefersReducedMotion() {
|
|
8
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(REDUCE).matches;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The motion level that applies to an element: `data-motion="off"` on `<html>` always wins,
|
|
12
|
+
* then the closest `[data-motion]` ancestor (SiteShell sets one), then `"subtle"`.
|
|
13
|
+
* Reduced motion turns every level into `"off"`.
|
|
14
|
+
*/
|
|
15
|
+
export function motionLevel(el) {
|
|
16
|
+
if (typeof document === "undefined" || prefersReducedMotion())
|
|
17
|
+
return "off";
|
|
18
|
+
if (document.documentElement.getAttribute("data-motion") === "off")
|
|
19
|
+
return "off";
|
|
20
|
+
const v = el?.closest("[data-motion]")?.getAttribute("data-motion");
|
|
21
|
+
return v === "off" || v === "full" ? v : "subtle";
|
|
22
|
+
}
|
|
23
|
+
/** Shorthand: may this element animate at all? */
|
|
24
|
+
export const motionAllowed = (el) => motionLevel(el) !== "off";
|
|
25
|
+
const pools = new Map();
|
|
26
|
+
export const ioSupported = () => typeof window !== "undefined" && typeof window.IntersectionObserver === "function";
|
|
27
|
+
/**
|
|
28
|
+
* Observe an element with a pooled IntersectionObserver. Returns the unobserve function.
|
|
29
|
+
* Without IntersectionObserver the handler is never called — callers fall back themselves.
|
|
30
|
+
*/
|
|
31
|
+
export function observe(el, handler, { rootMargin = "0px 0px -8% 0px", threshold = 0 } = {}) {
|
|
32
|
+
if (!ioSupported())
|
|
33
|
+
return () => { };
|
|
34
|
+
const key = `${rootMargin}|${threshold}`;
|
|
35
|
+
let pool = pools.get(key);
|
|
36
|
+
if (!pool) {
|
|
37
|
+
const handlers = new Map();
|
|
38
|
+
const io = new IntersectionObserver((entries) => entries.forEach((e) => handlers.get(e.target)?.(e)), { rootMargin, threshold });
|
|
39
|
+
pool = { io, handlers };
|
|
40
|
+
pools.set(key, pool);
|
|
41
|
+
}
|
|
42
|
+
const p = pool;
|
|
43
|
+
p.handlers.set(el, handler);
|
|
44
|
+
p.io.observe(el);
|
|
45
|
+
return () => {
|
|
46
|
+
p.handlers.delete(el);
|
|
47
|
+
p.io.unobserve(el);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Is the element's box (at least partly) inside the viewport right now? */
|
|
51
|
+
export function inViewport(el) {
|
|
52
|
+
if (typeof window === "undefined")
|
|
53
|
+
return false;
|
|
54
|
+
const r = el.getBoundingClientRect();
|
|
55
|
+
const h = window.innerHeight || document.documentElement.clientHeight;
|
|
56
|
+
const w = window.innerWidth || document.documentElement.clientWidth;
|
|
57
|
+
return r.bottom > 0 && r.top < h && r.right > 0 && r.left < w;
|
|
58
|
+
}
|
|
59
|
+
/** Longest `duration + delay` (ms) among the element's transitions — or its children's, for staggers. */
|
|
60
|
+
export function transitionEnd(el, children = false) {
|
|
61
|
+
if (typeof window === "undefined")
|
|
62
|
+
return 0;
|
|
63
|
+
const ms = (v) => v.split(",").map((s) => (s.trim().endsWith("ms") ? parseFloat(s) : parseFloat(s) * 1000) || 0);
|
|
64
|
+
const longest = (node) => {
|
|
65
|
+
const cs = window.getComputedStyle(node);
|
|
66
|
+
const d = ms(cs.transitionDuration);
|
|
67
|
+
const de = ms(cs.transitionDelay);
|
|
68
|
+
return d.reduce((m, v, i) => Math.max(m, v + (de[i] ?? de[0] ?? 0)), 0);
|
|
69
|
+
};
|
|
70
|
+
let max = longest(el);
|
|
71
|
+
if (children)
|
|
72
|
+
for (const c of Array.from(el.children))
|
|
73
|
+
max = Math.max(max, longest(c));
|
|
74
|
+
return max;
|
|
75
|
+
}
|
|
76
|
+
/** Easing used by the JS-driven primitives (count-up): ease-out quint. */
|
|
77
|
+
export const easeOutQuint = (t) => 1 - Math.pow(1 - t, 5);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface CountUpProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
3
|
+
/** The final number. This is what the server renders and what screen readers always hear. */
|
|
4
|
+
value: number;
|
|
5
|
+
/** Where the count starts. Default 0. */
|
|
6
|
+
from?: number;
|
|
7
|
+
/** Animation length in ms. Default 1400 (subtle) / 1800 (full). */
|
|
8
|
+
duration?: number;
|
|
9
|
+
/** Delay after it becomes visible, in ms. */
|
|
10
|
+
delay?: number;
|
|
11
|
+
/** BCP 47 locale. Default: the surrounding LocaleProvider (`en-US`). */
|
|
12
|
+
locale?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Intl.NumberFormat options. Default: as many fraction digits as `value` has (max 2),
|
|
15
|
+
* so `4.8` counts as 0.0 → 4.8 and `1240` as 0 → 1,240.
|
|
16
|
+
*/
|
|
17
|
+
format?: Intl.NumberFormatOptions;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A number that counts up to its value when it scrolls into view. Tabular figures and a hidden
|
|
21
|
+
* copy of the final value reserve the width, so nothing around it moves. Screen readers read the
|
|
22
|
+
* final value only; reduced motion, `data-motion="off"` and server-rendered numbers already on
|
|
23
|
+
* screen show the final value at once.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <CountUp value={1240} /> reviews · <CountUp value={4.8} locale="tr-TR" />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const CountUp: React.ForwardRefExoticComponent<CountUpProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { useLocale } from "../lib/locale.js";
|
|
6
|
+
import { easeOutQuint, inViewport, ioSupported, motionLevel, observe } from "./core.js";
|
|
7
|
+
import { useFreshMount, useIsoLayoutEffect } from "./reveal.js";
|
|
8
|
+
function decimalsOf(v) {
|
|
9
|
+
return Math.min(2, (String(v).split(".")[1] ?? "").length);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A number that counts up to its value when it scrolls into view. Tabular figures and a hidden
|
|
13
|
+
* copy of the final value reserve the width, so nothing around it moves. Screen readers read the
|
|
14
|
+
* final value only; reduced motion, `data-motion="off"` and server-rendered numbers already on
|
|
15
|
+
* screen show the final value at once.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <CountUp value={1240} /> reviews · <CountUp value={4.8} locale="tr-TR" />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const CountUp = React.forwardRef(function CountUp({ value, from = 0, duration, delay = 0, locale: localeProp, format, className, ...props }, ref) {
|
|
22
|
+
const ctx = useLocale();
|
|
23
|
+
const locale = localeProp ?? ctx?.locale ?? "en-US";
|
|
24
|
+
const fmt = React.useMemo(() => {
|
|
25
|
+
const d = decimalsOf(value);
|
|
26
|
+
return new Intl.NumberFormat(locale, format ?? { minimumFractionDigits: d, maximumFractionDigits: d });
|
|
27
|
+
}, [locale, format, value]);
|
|
28
|
+
const finalText = fmt.format(value);
|
|
29
|
+
const fresh = useFreshMount();
|
|
30
|
+
// Fresh mounts start at `from` (they are not on screen yet); hydrated ones at the final value.
|
|
31
|
+
const [display, setDisplay] = React.useState(() => (fresh ? fmt.format(from) : finalText));
|
|
32
|
+
const [running, setRunning] = React.useState(fresh);
|
|
33
|
+
const inner = React.useRef(null);
|
|
34
|
+
useIsoLayoutEffect(() => {
|
|
35
|
+
const el = inner.current;
|
|
36
|
+
if (!el)
|
|
37
|
+
return;
|
|
38
|
+
let raf = 0;
|
|
39
|
+
let timer;
|
|
40
|
+
const finish = () => {
|
|
41
|
+
setDisplay(finalText);
|
|
42
|
+
setRunning(false);
|
|
43
|
+
};
|
|
44
|
+
const level = motionLevel(el);
|
|
45
|
+
if (level === "off" || !ioSupported() || value === from) {
|
|
46
|
+
finish();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Hydrated and already on screen: animate only if the page has just loaded (nobody has
|
|
50
|
+
// read the number yet); otherwise leave it alone.
|
|
51
|
+
if (!fresh && inViewport(el) && performance.now() > 1500) {
|
|
52
|
+
finish();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const ms = duration ?? (level === "full" ? 1800 : 1400);
|
|
56
|
+
setDisplay(fmt.format(from));
|
|
57
|
+
setRunning(true);
|
|
58
|
+
const run = () => {
|
|
59
|
+
const start = performance.now();
|
|
60
|
+
const tick = (now) => {
|
|
61
|
+
const t = Math.min(1, (now - start) / ms);
|
|
62
|
+
const v = from + (value - from) * easeOutQuint(t);
|
|
63
|
+
if (t >= 1)
|
|
64
|
+
return finish();
|
|
65
|
+
setDisplay(fmt.format(v));
|
|
66
|
+
raf = requestAnimationFrame(tick);
|
|
67
|
+
};
|
|
68
|
+
raf = requestAnimationFrame(tick);
|
|
69
|
+
};
|
|
70
|
+
const stop = observe(el, (entry) => {
|
|
71
|
+
if (!entry.isIntersecting)
|
|
72
|
+
return;
|
|
73
|
+
stop();
|
|
74
|
+
timer = setTimeout(run, delay);
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
stop();
|
|
78
|
+
cancelAnimationFrame(raf);
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
};
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [value, from, finalText]);
|
|
83
|
+
// The widest of start and end reserves the box (generated content: not read, not selectable).
|
|
84
|
+
const ghost = fmt.format(from).length > finalText.length ? fmt.format(from) : finalText;
|
|
85
|
+
return (_jsx("span", { ref: (node) => {
|
|
86
|
+
inner.current = node;
|
|
87
|
+
if (typeof ref === "function")
|
|
88
|
+
ref(node);
|
|
89
|
+
else if (ref)
|
|
90
|
+
ref.current = node;
|
|
91
|
+
}, "data-count-up": "", "data-ghost": ghost, "data-running": running ? "" : undefined, className: cn("vm-count tabular", className), ...props, children: running ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: display }), _jsx("span", { className: "sr-only", children: finalText })] })) : (_jsx("span", { children: finalText })) }));
|
|
92
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { motionLevel, motionAllowed, prefersReducedMotion, type MotionLevel } from "./core.js";
|
|
2
|
+
export { MotionScope, type MotionScopeProps } from "./motion-scope.js";
|
|
3
|
+
export { Reveal, Stagger, useReveal, useFreshMount, type RevealProps, type StaggerProps, type RevealEffect, type UseRevealOptions } from "./reveal.js";
|
|
4
|
+
export { CountUp, type CountUpProps } from "./count-up.js";
|
|
5
|
+
export { Ticker, type TickerProps } from "./ticker.js";
|
|
6
|
+
export { Parallax, ScrollProgress, type ParallaxProps, type ScrollProgressProps } from "./parallax.js";
|
|
7
|
+
export { TextReveal, type TextRevealProps } from "./text-reveal.js";
|
|
8
|
+
export { Tilt, Magnetic, type TiltProps, type MagneticProps } from "./pointer.js";
|
|
9
|
+
export { PageTransition, startPageTransition, viewTransitionsSupported, type PageTransitionProps, type StartPageTransitionOptions } from "./page-transition.js";
|
|
10
|
+
export { SkeletonSwap, type SkeletonSwapProps } from "./skeleton-swap.js";
|
|
11
|
+
export { useScrollShrink, type UseScrollShrinkOptions } from "./scroll-shrink.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @vamos-ui/react/motion — CSS-first motion primitives. Styles live in styles/motion.css
|
|
2
|
+
// (already part of @vamos-ui/react/styles.css).
|
|
3
|
+
export { motionLevel, motionAllowed, prefersReducedMotion } from "./core.js";
|
|
4
|
+
export { MotionScope } from "./motion-scope.js";
|
|
5
|
+
export { Reveal, Stagger, useReveal, useFreshMount } from "./reveal.js";
|
|
6
|
+
export { CountUp } from "./count-up.js";
|
|
7
|
+
export { Ticker } from "./ticker.js";
|
|
8
|
+
export { Parallax, ScrollProgress } from "./parallax.js";
|
|
9
|
+
export { TextReveal } from "./text-reveal.js";
|
|
10
|
+
export { Tilt, Magnetic } from "./pointer.js";
|
|
11
|
+
export { PageTransition, startPageTransition, viewTransitionsSupported } from "./page-transition.js";
|
|
12
|
+
export { SkeletonSwap } from "./skeleton-swap.js";
|
|
13
|
+
export { useScrollShrink } from "./scroll-shrink.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { MotionLevel } from "./core.js";
|
|
3
|
+
export interface MotionScopeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** full: longer, wider, word-by-word · subtle: short fades and small rises (default) · off: final state, no animation. */
|
|
5
|
+
level: MotionLevel;
|
|
6
|
+
asChild?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sets the motion level for everything inside (`data-motion`). SiteShell does this for a site
|
|
10
|
+
* via its `motion` prop; use MotionScope for any other subtree. `data-motion="off"` on `<html>`
|
|
11
|
+
* overrides every scope, and so does the reduced-motion setting.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <MotionScope level="full"><Stagger>…</Stagger></MotionScope>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const MotionScope: React.ForwardRefExoticComponent<MotionScopeProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
/**
|
|
5
|
+
* Sets the motion level for everything inside (`data-motion`). SiteShell does this for a site
|
|
6
|
+
* via its `motion` prop; use MotionScope for any other subtree. `data-motion="off"` on `<html>`
|
|
7
|
+
* overrides every scope, and so does the reduced-motion setting.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <MotionScope level="full"><Stagger>…</Stagger></MotionScope>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const MotionScope = React.forwardRef(function MotionScope({ level, asChild, ...props }, ref) {
|
|
14
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
15
|
+
return _jsx(Comp, { ref: ref, "data-motion": level, ...props });
|
|
16
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export declare const viewTransitionsSupported: () => boolean;
|
|
3
|
+
export interface StartPageTransitionOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The element that changes. It gets `view-transition-name: vm-page` for the length of the
|
|
6
|
+
* transition, so only it cross-fades and the rest of the page stays live underneath.
|
|
7
|
+
*/
|
|
8
|
+
scope?: HTMLElement | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run a state update as a page transition: a View Transition (cross-fade + slight rise) where
|
|
12
|
+
* the browser has the API and motion is allowed, a plain synchronous update otherwise.
|
|
13
|
+
* Returns `"view-transition"` or `"none"`, so a caller can add its own fallback fade.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* startPageTransition(() => setPage("pricing"), { scope: rootRef.current });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function startPageTransition(update: () => void, { scope }?: StartPageTransitionOptions): "view-transition" | "none";
|
|
20
|
+
export interface PageTransitionProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
21
|
+
/** Changes when the page changes (a route, a tab id). The content fades in on every change after the first. */
|
|
22
|
+
pageKey: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fades its content in whenever `pageKey` changes — the fallback for apps that switch content
|
|
26
|
+
* without View Transitions. Pair it with `startPageTransition` when you control the update.
|
|
27
|
+
* The first render never animates, so server-rendered content stays put.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <PageTransition pageKey={tab}>{panels[tab]}</PageTransition>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const PageTransition: React.ForwardRefExoticComponent<PageTransitionProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { motionAllowed } from "./core.js";
|
|
7
|
+
export const viewTransitionsSupported = () => typeof document !== "undefined" && typeof document.startViewTransition === "function";
|
|
8
|
+
/**
|
|
9
|
+
* Run a state update as a page transition: a View Transition (cross-fade + slight rise) where
|
|
10
|
+
* the browser has the API and motion is allowed, a plain synchronous update otherwise.
|
|
11
|
+
* Returns `"view-transition"` or `"none"`, so a caller can add its own fallback fade.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* startPageTransition(() => setPage("pricing"), { scope: rootRef.current });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function startPageTransition(update, { scope } = {}) {
|
|
18
|
+
const doc = typeof document === "undefined" ? undefined : document;
|
|
19
|
+
if (!doc?.startViewTransition || !motionAllowed(scope ?? doc.documentElement)) {
|
|
20
|
+
update();
|
|
21
|
+
return "none";
|
|
22
|
+
}
|
|
23
|
+
if (scope)
|
|
24
|
+
scope.style.setProperty("view-transition-name", "vm-page");
|
|
25
|
+
doc.documentElement.setAttribute("data-vm-transition", "");
|
|
26
|
+
try {
|
|
27
|
+
const t = doc.startViewTransition(() => flushSync(update));
|
|
28
|
+
t.finished.finally(() => {
|
|
29
|
+
scope?.style.removeProperty("view-transition-name");
|
|
30
|
+
doc.documentElement.removeAttribute("data-vm-transition");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
scope?.style.removeProperty("view-transition-name");
|
|
35
|
+
doc.documentElement.removeAttribute("data-vm-transition");
|
|
36
|
+
update();
|
|
37
|
+
return "none";
|
|
38
|
+
}
|
|
39
|
+
return "view-transition";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fades its content in whenever `pageKey` changes — the fallback for apps that switch content
|
|
43
|
+
* without View Transitions. Pair it with `startPageTransition` when you control the update.
|
|
44
|
+
* The first render never animates, so server-rendered content stays put.
|
|
45
|
+
*
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <PageTransition pageKey={tab}>{panels[tab]}</PageTransition>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const PageTransition = React.forwardRef(function PageTransition({ pageKey, className, children, ...props }, ref) {
|
|
51
|
+
const first = React.useRef(pageKey);
|
|
52
|
+
// Inside a View Transition the browser already cross-fades: no second fade on top.
|
|
53
|
+
const inViewTransition = typeof document !== "undefined" && document.documentElement.hasAttribute("data-vm-transition");
|
|
54
|
+
const entering = first.current !== pageKey && !inViewTransition;
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
// From the second key on, every change animates.
|
|
57
|
+
if (first.current !== pageKey)
|
|
58
|
+
first.current = "\u0000";
|
|
59
|
+
}, [pageKey]);
|
|
60
|
+
return (_jsx("div", { ref: ref, "data-page-enter": entering ? "" : undefined, className: cn("vm-page", className), ...props, children: children }, pageKey));
|
|
61
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ParallaxProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/**
|
|
4
|
+
* How far the content travels each way while the frame crosses the viewport, in px.
|
|
5
|
+
* Positive moves slower than the page (depth), negative faster (foreground). Default 40.
|
|
6
|
+
*/
|
|
7
|
+
shift?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Clip to the frame and over-size the layer so no edge ever shows — for images. The frame
|
|
10
|
+
* needs its own size (an aspect ratio or a height). Default true.
|
|
11
|
+
*/
|
|
12
|
+
clip?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Scroll-linked depth. CSS `animation-timeline: view()` where supported (no JS on scroll), a
|
|
16
|
+
* passive rAF scroll listener elsewhere. Transform only; off under reduced motion and
|
|
17
|
+
* `data-motion="off"`.
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Parallax shift={48} className="aspect-[16/9] rounded-ui-lg"><img className="size-full object-cover" … /></Parallax>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const Parallax: React.ForwardRefExoticComponent<ParallaxProps & React.RefAttributes<HTMLDivElement>>;
|
|
24
|
+
export interface ScrollProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
25
|
+
/** Fixed to the top of the viewport (reading progress). With false it sits in the flow. Default true. */
|
|
26
|
+
fixed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A thin accent bar that fills as the page scrolls — `animation-timeline: scroll()` with a
|
|
30
|
+
* scroll-listener fallback. Decorative (`aria-hidden`); it follows the scroll directly, so it
|
|
31
|
+
* stays on under reduced motion.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ScrollProgress />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const ScrollProgress: React.ForwardRefExoticComponent<ScrollProgressProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { motionAllowed } from "./core.js";
|
|
6
|
+
const supports = (q) => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports(q);
|
|
7
|
+
/**
|
|
8
|
+
* Calls `onProgress(0…1)` on scroll (rAF-throttled, passive). Used as the fallback where CSS
|
|
9
|
+
* scroll-driven animations are missing. `measure` returns the progress for the current scroll.
|
|
10
|
+
*/
|
|
11
|
+
function useScrollFallback(enabled, ref, measure) {
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!enabled || !el)
|
|
15
|
+
return;
|
|
16
|
+
let raf = 0;
|
|
17
|
+
const update = () => {
|
|
18
|
+
raf = 0;
|
|
19
|
+
el.style.setProperty("--vm-progress", Math.min(1, Math.max(0, measure(el))).toFixed(4));
|
|
20
|
+
};
|
|
21
|
+
const onScroll = () => {
|
|
22
|
+
if (!raf)
|
|
23
|
+
raf = requestAnimationFrame(update);
|
|
24
|
+
};
|
|
25
|
+
el.setAttribute("data-js", "");
|
|
26
|
+
update();
|
|
27
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
28
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
29
|
+
return () => {
|
|
30
|
+
cancelAnimationFrame(raf);
|
|
31
|
+
window.removeEventListener("scroll", onScroll);
|
|
32
|
+
window.removeEventListener("resize", onScroll);
|
|
33
|
+
el.removeAttribute("data-js");
|
|
34
|
+
};
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, [enabled]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Scroll-linked depth. CSS `animation-timeline: view()` where supported (no JS on scroll), a
|
|
40
|
+
* passive rAF scroll listener elsewhere. Transform only; off under reduced motion and
|
|
41
|
+
* `data-motion="off"`.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <Parallax shift={48} className="aspect-[16/9] rounded-ui-lg"><img className="size-full object-cover" … /></Parallax>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const Parallax = React.forwardRef(function Parallax({ shift = 40, clip = true, className, style, children, ...props }, ref) {
|
|
48
|
+
const inner = React.useRef(null);
|
|
49
|
+
const [fallback, setFallback] = React.useState(false);
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
setFallback(!supports("animation-timeline: view()") && motionAllowed(inner.current));
|
|
52
|
+
}, []);
|
|
53
|
+
useScrollFallback(fallback, inner, (el) => {
|
|
54
|
+
const r = el.getBoundingClientRect();
|
|
55
|
+
const vh = window.innerHeight;
|
|
56
|
+
return (vh - r.top) / (vh + r.height);
|
|
57
|
+
});
|
|
58
|
+
return (_jsx("div", { ref: (node) => {
|
|
59
|
+
inner.current = node;
|
|
60
|
+
if (typeof ref === "function")
|
|
61
|
+
ref(node);
|
|
62
|
+
else if (ref)
|
|
63
|
+
ref.current = node;
|
|
64
|
+
}, "data-parallax": "", "data-clip": clip ? "" : undefined, className: cn("vm-parallax", className), style: { "--vm-parallax-shift": `${shift}px`, ...style }, ...props, children: _jsx("div", { className: "vm-parallax-layer", children: children }) }));
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* A thin accent bar that fills as the page scrolls — `animation-timeline: scroll()` with a
|
|
68
|
+
* scroll-listener fallback. Decorative (`aria-hidden`); it follows the scroll directly, so it
|
|
69
|
+
* stays on under reduced motion.
|
|
70
|
+
*
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <ScrollProgress />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const ScrollProgress = React.forwardRef(function ScrollProgress({ fixed = true, className, ...props }, ref) {
|
|
76
|
+
const inner = React.useRef(null);
|
|
77
|
+
const [fallback, setFallback] = React.useState(false);
|
|
78
|
+
React.useEffect(() => setFallback(!supports("animation-timeline: scroll()")), []);
|
|
79
|
+
useScrollFallback(fallback, inner, () => {
|
|
80
|
+
const max = document.documentElement.scrollHeight - window.innerHeight;
|
|
81
|
+
return max > 0 ? window.scrollY / max : 0;
|
|
82
|
+
});
|
|
83
|
+
return (_jsx("div", { ref: (node) => {
|
|
84
|
+
inner.current = node;
|
|
85
|
+
if (typeof ref === "function")
|
|
86
|
+
ref(node);
|
|
87
|
+
else if (ref)
|
|
88
|
+
ref.current = node;
|
|
89
|
+
}, "aria-hidden": "true", "data-fixed": fixed ? "" : undefined, className: cn("vm-scroll-progress", className), ...props, children: _jsx("div", { className: "vm-scroll-progress-bar" }) }));
|
|
90
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface TiltProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** Maximum rotation in degrees. Default 5 — enough to feel, not enough to read as a gimmick. */
|
|
4
|
+
max?: number;
|
|
5
|
+
/** A soft light that follows the pointer. Default false. */
|
|
6
|
+
glare?: boolean;
|
|
7
|
+
/** Lowest motion level that tilts. Default `"subtle"`; `"full"` keeps subtle sites flat. */
|
|
8
|
+
level?: "subtle" | "full";
|
|
9
|
+
asChild?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A card that leans toward the pointer in 3D. Mouse and trackpad only (`pointer: fine`); flat
|
|
13
|
+
* on touch, under reduced motion and with `data-motion="off"`.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Tilt max={6} glare><article className="surface rounded-ui-lg p-6">…</article></Tilt>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const Tilt: React.ForwardRefExoticComponent<TiltProps & React.RefAttributes<HTMLDivElement>>;
|
|
20
|
+
export interface MagneticProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
21
|
+
/** Share of the pointer's offset the element follows, 0–1. Default 0.3. */
|
|
22
|
+
strength?: number;
|
|
23
|
+
/** Cap on the travel, in px. Default 10. */
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** Lowest motion level that pulls. Default `"full"` — a call-to-action flourish, not a default. */
|
|
26
|
+
level?: "subtle" | "full";
|
|
27
|
+
/** Pull the single child (your Button or link) instead of wrapping it in an inline-block span. */
|
|
28
|
+
asChild?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pulls a call-to-action a few pixels toward the pointer and springs back on leave. Mouse
|
|
32
|
+
* and trackpad only; still on touch, under reduced motion and below its `level`.
|
|
33
|
+
*
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <Magnetic><Button variant="primary" size="xl">Book a table</Button></Magnetic>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const Magnetic: React.ForwardRefExoticComponent<MagneticProps & React.RefAttributes<HTMLSpanElement>>;
|