@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.
Files changed (166) hide show
  1. package/dist/blocks/block-image.js +1 -1
  2. package/dist/blocks/block-link.js +1 -1
  3. package/dist/blocks/index.d.ts +2 -2
  4. package/dist/blocks/index.js +2 -2
  5. package/dist/blocks/site/_kit-1.d.ts +1 -0
  6. package/dist/blocks/site/_kit-1.js +3 -1
  7. package/dist/blocks/site/announcement-bar.js +1 -1
  8. package/dist/blocks/site/article-layout.d.ts +1 -0
  9. package/dist/blocks/site/article-layout.js +2 -1
  10. package/dist/blocks/site/blog-list.d.ts +1 -0
  11. package/dist/blocks/site/blog-list.js +3 -2
  12. package/dist/blocks/site/contact-dock.js +3 -1
  13. package/dist/blocks/site/contact-form.js +1 -1
  14. package/dist/blocks/site/cta.d.ts +2 -2
  15. package/dist/blocks/site/cta.js +8 -5
  16. package/dist/blocks/site/faq.js +2 -1
  17. package/dist/blocks/site/feature-grid.d.ts +1 -0
  18. package/dist/blocks/site/feature-grid.js +7 -5
  19. package/dist/blocks/site/footer.d.ts +1 -1
  20. package/dist/blocks/site/footer.js +8 -7
  21. package/dist/blocks/site/gallery-section.d.ts +6 -1
  22. package/dist/blocks/site/gallery-section.js +6 -4
  23. package/dist/blocks/site/hero.d.ts +3 -2
  24. package/dist/blocks/site/hero.js +14 -6
  25. package/dist/blocks/site/logo-strip.d.ts +6 -3
  26. package/dist/blocks/site/logo-strip.js +8 -7
  27. package/dist/blocks/site/map-hours.d.ts +2 -2
  28. package/dist/blocks/site/map-hours.js +4 -2
  29. package/dist/blocks/site/media-text.d.ts +4 -1
  30. package/dist/blocks/site/media-text.js +6 -4
  31. package/dist/blocks/site/menu-list.d.ts +7 -3
  32. package/dist/blocks/site/menu-list.js +34 -7
  33. package/dist/blocks/site/navbar.d.ts +1 -0
  34. package/dist/blocks/site/navbar.js +6 -5
  35. package/dist/blocks/site/not-found.js +2 -1
  36. package/dist/blocks/site/page-intro.d.ts +1 -0
  37. package/dist/blocks/site/page-intro.js +2 -1
  38. package/dist/blocks/site/pricing-table.js +2 -1
  39. package/dist/blocks/site/process.d.ts +1 -0
  40. package/dist/blocks/site/process.js +4 -2
  41. package/dist/blocks/site/registry-1.d.ts +1 -1
  42. package/dist/blocks/site/registry-2.d.ts +5 -0
  43. package/dist/blocks/site/registry-2.js +2 -2
  44. package/dist/blocks/site/rich-text-section.js +2 -1
  45. package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
  46. package/dist/blocks/site/shared/heading-lines.js +32 -0
  47. package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
  48. package/dist/blocks/site/shared/image-sizes.js +37 -0
  49. package/dist/blocks/site/shared/island-keys.d.ts +1 -1
  50. package/dist/blocks/site/shared/island-keys.js +1 -1
  51. package/dist/blocks/site/shared/labels-2.d.ts +9 -1
  52. package/dist/blocks/site/shared/labels-2.js +24 -6
  53. package/dist/blocks/site/shared/open-now.d.ts +1 -1
  54. package/dist/blocks/site/shared/open-now.js +1 -1
  55. package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
  56. package/dist/blocks/site/shared/opening-hours.js +36 -7
  57. package/dist/blocks/site/shared/tap.d.ts +9 -0
  58. package/dist/blocks/site/shared/tap.js +16 -0
  59. package/dist/blocks/site/site-shell.d.ts +11 -0
  60. package/dist/blocks/site/site-shell.js +43 -16
  61. package/dist/blocks/site/stats-band.d.ts +3 -1
  62. package/dist/blocks/site/stats-band.js +11 -7
  63. package/dist/blocks/site/team.d.ts +1 -1
  64. package/dist/blocks/site/team.js +4 -3
  65. package/dist/blocks/site/testimonials.d.ts +2 -2
  66. package/dist/blocks/site/testimonials.js +5 -4
  67. package/dist/business/index.d.ts +1 -0
  68. package/dist/business/index.js +2 -0
  69. package/dist/business/signer-status.d.ts +53 -0
  70. package/dist/business/signer-status.js +76 -0
  71. package/dist/charts/bar-chart.js +6 -2
  72. package/dist/charts/chart-shell.d.ts +9 -2
  73. package/dist/charts/chart-shell.js +10 -4
  74. package/dist/charts/chart.js +1 -1
  75. package/dist/charts/combo-chart.js +5 -7
  76. package/dist/charts/utils.d.ts +10 -0
  77. package/dist/charts/utils.js +46 -0
  78. package/dist/components/_qr-core.d.ts +26 -0
  79. package/dist/components/_qr-core.js +376 -0
  80. package/dist/components/activity-timeline.js +5 -5
  81. package/dist/components/aspect-ratio.d.ts +26 -0
  82. package/dist/components/aspect-ratio.js +42 -0
  83. package/dist/components/backup-codes.d.ts +42 -0
  84. package/dist/components/backup-codes.js +67 -0
  85. package/dist/components/breadcrumb.d.ts +2 -2
  86. package/dist/components/breadcrumb.js +1 -1
  87. package/dist/components/carousel.d.ts +12 -3
  88. package/dist/components/carousel.js +8 -5
  89. package/dist/components/collapsible.d.ts +36 -0
  90. package/dist/components/collapsible.js +37 -0
  91. package/dist/components/command.d.ts +8 -2
  92. package/dist/components/command.js +7 -2
  93. package/dist/components/image.d.ts +18 -2
  94. package/dist/components/image.js +12 -5
  95. package/dist/components/input.d.ts +1 -1
  96. package/dist/components/input.js +1 -1
  97. package/dist/components/menubar.d.ts +96 -0
  98. package/dist/components/menubar.js +123 -0
  99. package/dist/components/pin-map.d.ts +58 -0
  100. package/dist/components/pin-map.js +167 -0
  101. package/dist/components/placeholder-art.d.ts +51 -0
  102. package/dist/components/placeholder-art.js +82 -0
  103. package/dist/components/qr-code.d.ts +36 -0
  104. package/dist/components/qr-code.js +27 -0
  105. package/dist/components/record-detail-layout.d.ts +2 -2
  106. package/dist/components/record-detail-layout.js +1 -1
  107. package/dist/components/sidebar.js +8 -8
  108. package/dist/components/sla-timer.d.ts +75 -0
  109. package/dist/components/sla-timer.js +142 -0
  110. package/dist/components/slug-input.js +1 -1
  111. package/dist/components/tour.d.ts +60 -0
  112. package/dist/components/tour.js +139 -0
  113. package/dist/index.d.ts +9 -0
  114. package/dist/index.js +9 -0
  115. package/dist/lib/public.d.ts +1 -0
  116. package/dist/lib/public.js +1 -0
  117. package/dist/lib/value-format.js +1 -1
  118. package/dist/locale-data/core/holidays.d.ts +1 -1
  119. package/dist/locale-data/core/holidays.js +1 -1
  120. package/dist/locales/ar.d.ts +2087 -0
  121. package/dist/locales/ar.js +2088 -0
  122. package/dist/locales/de.d.ts +2087 -0
  123. package/dist/locales/de.js +2088 -0
  124. package/dist/locales/en.d.ts +1 -0
  125. package/dist/locales/en.js +1 -0
  126. package/dist/locales/es.d.ts +2087 -0
  127. package/dist/locales/es.js +2087 -0
  128. package/dist/locales/fr.d.ts +2087 -0
  129. package/dist/locales/fr.js +2088 -0
  130. package/dist/locales/tr.d.ts +1 -0
  131. package/dist/locales/tr.js +1 -0
  132. package/dist/media/gallery.js +3 -1
  133. package/dist/media/media-library.js +1 -1
  134. package/dist/motion/core.d.ts +32 -0
  135. package/dist/motion/core.js +77 -0
  136. package/dist/motion/count-up.d.ts +29 -0
  137. package/dist/motion/count-up.js +92 -0
  138. package/dist/motion/index.d.ts +11 -0
  139. package/dist/motion/index.js +13 -0
  140. package/dist/motion/motion-scope.d.ts +17 -0
  141. package/dist/motion/motion-scope.js +16 -0
  142. package/dist/motion/page-transition.d.ts +33 -0
  143. package/dist/motion/page-transition.js +61 -0
  144. package/dist/motion/parallax.d.ts +37 -0
  145. package/dist/motion/parallax.js +90 -0
  146. package/dist/motion/pointer.d.ts +38 -0
  147. package/dist/motion/pointer.js +117 -0
  148. package/dist/motion/reveal.d.ts +72 -0
  149. package/dist/motion/reveal.js +133 -0
  150. package/dist/motion/scroll-shrink.d.ts +18 -0
  151. package/dist/motion/scroll-shrink.js +47 -0
  152. package/dist/motion/skeleton-swap.d.ts +17 -0
  153. package/dist/motion/skeleton-swap.js +43 -0
  154. package/dist/motion/text-reveal.d.ts +32 -0
  155. package/dist/motion/text-reveal.js +31 -0
  156. package/dist/motion/ticker.d.ts +35 -0
  157. package/dist/motion/ticker.js +16 -0
  158. package/dist/render/block-renderer.js +1 -1
  159. package/dist/sortable/sortable-tree.d.ts +4 -0
  160. package/dist/sortable/sortable-tree.js +16 -4
  161. package/dist/styles/blocks.css +21 -12
  162. package/dist/styles/editors.css +2 -1
  163. package/dist/styles/media.css +31 -5
  164. package/dist/styles/motion.css +389 -0
  165. package/dist/styles.css +41 -10
  166. package/package.json +5 -1
@@ -717,6 +717,7 @@ export declare const tr: {
717
717
  thumbs: string;
718
718
  empty: string;
719
719
  loading: string;
720
+ marqueePause: string;
720
721
  };
721
722
  savedViews: {
722
723
  label: string;
@@ -717,6 +717,7 @@ export const tr = {
717
717
  thumbs: "Slayt küçük resimleri",
718
718
  empty: "Henüz gösterilecek bir şey yok",
719
719
  loading: "Slaytlar yükleniyor",
720
+ marqueePause: "Kaydırmayı duraklat",
720
721
  },
721
722
  savedViews: {
722
723
  label: "Görünümler",
@@ -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, minItemWidth: "10rem", className: "gap-(--gallery-gap)", itemClassName: "overflow-hidden", renderActions: onRemove
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>>;