@vllnt/ui 0.1.7 → 0.1.8

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.
@@ -1,31 +1,27 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { cn } from "../../lib/utils";
4
+ const SEPARATOR_CHARS = {
5
+ arrow: "\u2192",
6
+ chevron: "\u203A",
7
+ slash: "/"
8
+ };
9
+ function SeparatorIcon({ type }) {
10
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: SEPARATOR_CHARS[type] ?? "\u203A" });
11
+ }
4
12
  function Breadcrumb({
5
13
  className,
6
14
  items,
7
15
  separator = "chevron",
8
16
  variant = "default"
9
17
  }) {
10
- const getSeparator = () => {
11
- switch (separator) {
12
- case "chevron":
13
- return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u203A" });
14
- case "slash":
15
- return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "/" });
16
- case "arrow":
17
- return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2192" });
18
- default:
19
- return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u203A" });
20
- }
21
- };
22
18
  return /* @__PURE__ */ jsx(
23
19
  "nav",
24
20
  {
25
21
  "aria-label": "Breadcrumb",
26
22
  className: cn("flex items-center space-x-1 text-sm", className),
27
23
  children: items.map((item, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
28
- index > 0 && /* @__PURE__ */ jsx("span", { className: "mx-2", children: getSeparator() }),
24
+ index > 0 && /* @__PURE__ */ jsx("span", { className: "mx-2", children: /* @__PURE__ */ jsx(SeparatorIcon, { type: separator }) }),
29
25
  item.href ? /* @__PURE__ */ jsxs(
30
26
  Link,
31
27
  {
@@ -71,11 +71,15 @@ function useCarouselLogic({
71
71
  if (!api) {
72
72
  return;
73
73
  }
74
- onSelect(api);
75
74
  api.on("reInit", onSelect);
76
75
  api.on("select", onSelect);
76
+ const rafId = requestAnimationFrame(() => {
77
+ onSelect(api);
78
+ });
77
79
  return () => {
78
80
  api?.off("select", onSelect);
81
+ api?.off("reInit", onSelect);
82
+ cancelAnimationFrame(rafId);
79
83
  };
80
84
  }, [api, onSelect]);
81
85
  return {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
4
  import { Check, Copy } from "lucide-react";
5
5
  import { useTheme } from "next-themes";
6
6
  import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
@@ -25,6 +25,11 @@ function extractTextFromChildren(children) {
25
25
  }
26
26
  return String(children ?? "");
27
27
  }
28
+ function findScrollableParent(element) {
29
+ if (!element) return void 0;
30
+ if (element.scrollHeight > element.clientHeight) return element;
31
+ return findScrollableParent(element.parentElement);
32
+ }
28
33
  function CodeBlock({
29
34
  children,
30
35
  className,
@@ -32,15 +37,28 @@ function CodeBlock({
32
37
  showLanguage = false
33
38
  }) {
34
39
  const [copied, setCopied] = useState(false);
35
- const [mounted, setMounted] = useState(false);
36
40
  const { systemTheme, theme } = useTheme();
37
- useEffect(() => {
38
- setMounted(true);
39
- }, []);
40
41
  const resolvedTheme = theme === "system" ? systemTheme : theme;
41
- const isDark = resolvedTheme === "dark";
42
+ const isDark = resolvedTheme !== "light";
42
43
  const codeStyle = isDark ? oneDark : oneLight;
43
44
  const code = extractTextFromChildren(children);
45
+ const scrollRef = useRef(null);
46
+ useEffect(() => {
47
+ const element = scrollRef.current;
48
+ if (!element) return;
49
+ const onWheel = (event) => {
50
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
51
+ const scrollable = findScrollableParent(element);
52
+ if (scrollable) {
53
+ scrollable.scrollTop += event.deltaY;
54
+ event.preventDefault();
55
+ }
56
+ };
57
+ element.addEventListener("wheel", onWheel, { passive: false });
58
+ return () => {
59
+ element.removeEventListener("wheel", onWheel);
60
+ };
61
+ }, []);
44
62
  const handleCopy = async () => {
45
63
  await navigator.clipboard.writeText(code);
46
64
  setCopied(true);
@@ -48,33 +66,6 @@ function CodeBlock({
48
66
  setCopied(false);
49
67
  }, 2e3);
50
68
  };
51
- if (!mounted) {
52
- return /* @__PURE__ */ jsx(
53
- "div",
54
- {
55
- className: cn(
56
- "relative w-full overflow-hidden rounded-md border bg-background",
57
- className
58
- ),
59
- children: /* @__PURE__ */ jsxs("div", { className: "relative overflow-x-auto", children: [
60
- /* @__PURE__ */ jsx("pre", { className: "p-4 text-sm font-mono bg-background", children: /* @__PURE__ */ jsx("code", { children: code }) }),
61
- /* @__PURE__ */ jsxs("div", { className: "absolute right-2 top-2 flex items-center gap-2", children: [
62
- showLanguage ? /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-muted-foreground uppercase tracking-wider", children: language }) : null,
63
- /* @__PURE__ */ jsx(
64
- Button,
65
- {
66
- className: "h-8 w-8",
67
- onClick: handleCopy,
68
- size: "icon",
69
- variant: "ghost",
70
- children: /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
71
- }
72
- )
73
- ] })
74
- ] })
75
- }
76
- );
77
- }
78
69
  return /* @__PURE__ */ jsx(
79
70
  "div",
80
71
  {
@@ -82,39 +73,51 @@ function CodeBlock({
82
73
  "relative w-full overflow-hidden rounded-md border bg-background",
83
74
  className
84
75
  ),
85
- children: /* @__PURE__ */ jsxs("div", { className: "relative overflow-x-auto", children: [
86
- /* @__PURE__ */ jsx(
87
- SyntaxHighlighter,
88
- {
89
- codeTagProps: {
90
- className: "font-mono text-sm"
91
- },
92
- customStyle: {
93
- background: "hsl(var(--background))",
94
- fontSize: "0.875rem",
95
- margin: 0,
96
- minWidth: "fit-content",
97
- padding: "1rem"
98
- },
99
- language,
100
- style: codeStyle,
101
- children: code
102
- }
103
- ),
104
- /* @__PURE__ */ jsxs("div", { className: "absolute right-2 top-2 flex items-center gap-2", children: [
105
- showLanguage ? /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-muted-foreground uppercase tracking-wider", children: language }) : null,
106
- /* @__PURE__ */ jsx(
107
- Button,
108
- {
109
- className: "h-8 w-8",
110
- onClick: handleCopy,
111
- size: "icon",
112
- variant: "ghost",
113
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
114
- }
115
- )
116
- ] })
117
- ] })
76
+ children: /* @__PURE__ */ jsxs(
77
+ "div",
78
+ {
79
+ className: "relative overflow-x-auto overflow-y-hidden touch-pan-y",
80
+ ref: scrollRef,
81
+ children: [
82
+ /* @__PURE__ */ jsx(
83
+ SyntaxHighlighter,
84
+ {
85
+ codeTagProps: {
86
+ className: "font-mono text-sm",
87
+ style: {
88
+ background: "transparent",
89
+ display: "block"
90
+ }
91
+ },
92
+ customStyle: {
93
+ background: "hsl(var(--background))",
94
+ fontSize: "0.875rem",
95
+ margin: 0,
96
+ minWidth: "fit-content",
97
+ overflowY: "hidden",
98
+ padding: "1rem"
99
+ },
100
+ language,
101
+ style: codeStyle,
102
+ children: code
103
+ }
104
+ ),
105
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-2 top-2 flex items-center gap-2", children: [
106
+ showLanguage ? /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-muted-foreground uppercase tracking-wider", children: language }) : null,
107
+ /* @__PURE__ */ jsx(
108
+ Button,
109
+ {
110
+ className: "h-8 w-8",
111
+ onClick: handleCopy,
112
+ size: "icon",
113
+ variant: "ghost",
114
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
115
+ }
116
+ )
117
+ ] })
118
+ ]
119
+ }
120
+ )
118
121
  }
119
122
  );
120
123
  }
@@ -22,7 +22,19 @@ const variantConfig = {
22
22
  iconClass: "text-muted-foreground"
23
23
  }
24
24
  };
25
- function Comparison({ after, before, title }) {
25
+ function Comparison({
26
+ after,
27
+ before,
28
+ title,
29
+ ...rest
30
+ }) {
31
+ if (!before || !after) {
32
+ const hint = "left" in rest || "right" in rest ? ' Did you mean "before" / "after" instead of "left" / "right"?' : "";
33
+ console.error(
34
+ `[Comparison] Missing required props "before" and "after".${hint}`
35
+ );
36
+ return null;
37
+ }
26
38
  const beforeConfig = variantConfig[before.variant || "bad"];
27
39
  const afterConfig = variantConfig[after.variant || "good"];
28
40
  const BeforeIcon = beforeConfig.icon;
@@ -50,9 +50,13 @@ const CookieConsent = forwardRef(
50
50
  return () => {
51
51
  clearTimeout(timer);
52
52
  };
53
- } else {
54
- setIsVisible(false);
55
53
  }
54
+ const rafId = requestAnimationFrame(() => {
55
+ setIsVisible(false);
56
+ });
57
+ return () => {
58
+ cancelAnimationFrame(rafId);
59
+ };
56
60
  }, [open]);
57
61
  const handleClose = useCallback(() => {
58
62
  setIsAnimatingOut(true);
@@ -25,8 +25,13 @@ function useZoomControls(reactFlow) {
25
25
  void reactFlow.fitView({ duration: 200, padding: 0.2 });
26
26
  }, [reactFlow]);
27
27
  useEffect(() => {
28
- const viewport = reactFlow.getViewport();
29
- setZoom(viewport.zoom);
28
+ const rafId = requestAnimationFrame(() => {
29
+ const viewport = reactFlow.getViewport();
30
+ setZoom(viewport.zoom);
31
+ });
32
+ return () => {
33
+ cancelAnimationFrame(rafId);
34
+ };
30
35
  }, [reactFlow]);
31
36
  return { fitView, zoom, zoomIn, zoomOut };
32
37
  }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { memo } from "react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { useHorizontalScroll } from "../../lib/use-horizontal-scroll";
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "../button/button";
8
+ const HorizontalScrollRow = memo(function HorizontalScrollRow2({
9
+ children,
10
+ className,
11
+ description,
12
+ title
13
+ }) {
14
+ const { canScrollLeft, canScrollRight, containerRef, scroll } = useHorizontalScroll();
15
+ const showControls = canScrollLeft || canScrollRight;
16
+ return /* @__PURE__ */ jsxs("section", { className: cn("space-y-4", className), children: [
17
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
18
+ /* @__PURE__ */ jsxs("div", { children: [
19
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: title }),
20
+ description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
21
+ ] }),
22
+ showControls ? /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
23
+ /* @__PURE__ */ jsx(
24
+ Button,
25
+ {
26
+ "aria-label": "Scroll left",
27
+ className: "h-8 w-8",
28
+ disabled: !canScrollLeft,
29
+ onClick: () => {
30
+ scroll("left");
31
+ },
32
+ size: "icon",
33
+ variant: "outline",
34
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx(
38
+ Button,
39
+ {
40
+ "aria-label": "Scroll right",
41
+ className: "h-8 w-8",
42
+ disabled: !canScrollRight,
43
+ onClick: () => {
44
+ scroll("right");
45
+ },
46
+ size: "icon",
47
+ variant: "outline",
48
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
49
+ }
50
+ )
51
+ ] }) : null
52
+ ] }),
53
+ /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ className: "flex gap-4 overflow-x-auto snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
57
+ ref: containerRef,
58
+ children
59
+ }
60
+ )
61
+ ] });
62
+ });
63
+ HorizontalScrollRow.displayName = "HorizontalScrollRow";
64
+ export {
65
+ HorizontalScrollRow
66
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ HorizontalScrollRow
3
+ } from "./horizontal-scroll-row";
4
+ export {
5
+ HorizontalScrollRow
6
+ };
@@ -340,6 +340,12 @@ import {
340
340
  SocialFAB,
341
341
  useSocialFab
342
342
  } from "./social-fab";
343
+ import {
344
+ HorizontalScrollRow
345
+ } from "./horizontal-scroll-row";
346
+ import {
347
+ ViewSwitcher
348
+ } from "./view-switcher";
343
349
  import {
344
350
  FlowControls,
345
351
  FlowDiagram,
@@ -480,6 +486,7 @@ export {
480
486
  FlowErrorBoundary,
481
487
  FlowFullscreen,
482
488
  Glossary,
489
+ HorizontalScrollRow,
483
490
  HoverCard,
484
491
  HoverCardContent,
485
492
  HoverCardTrigger,
@@ -618,6 +625,7 @@ export {
618
625
  TutorialIntroContent,
619
626
  TutorialMDX,
620
627
  VideoEmbed,
628
+ ViewSwitcher,
621
629
  alertVariants,
622
630
  badgeVariants,
623
631
  buttonVariants,
@@ -21,7 +21,21 @@ const MDXComponents = {
21
21
  children
22
22
  }
23
23
  ),
24
- code: ({ children, ...props }) => /* @__PURE__ */ jsx("code", { className: "bg-muted px-1 py-0.5 rounded text-sm font-mono", ...props, children }),
24
+ code: ({ children, className, ...props }) => {
25
+ if (typeof className === "string" && className.startsWith("language-")) {
26
+ const language = className.replace(/^language-/, "");
27
+ const text = typeof children === "string" ? children.replace(/\n$/, "") : String(children ?? "");
28
+ return /* @__PURE__ */ jsx(CodeBlock, { language, children: text });
29
+ }
30
+ return /* @__PURE__ */ jsx(
31
+ "code",
32
+ {
33
+ className: "bg-muted px-1 py-0.5 rounded text-sm font-mono",
34
+ ...props,
35
+ children
36
+ }
37
+ );
38
+ },
25
39
  em: ({ children, ...props }) => /* @__PURE__ */ jsx("em", { className: "italic", ...props, children }),
26
40
  h1: ({ children, ...props }) => /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold mt-8 mb-4", ...props, children }),
27
41
  h2: ({ children, ...props }) => /* @__PURE__ */ jsx("h2", { className: "text-xl font-bold mt-6 mb-3", ...props, children }),
@@ -51,14 +65,7 @@ const MDXComponents = {
51
65
  children
52
66
  }
53
67
  ),
54
- pre: ({ children, ...props }) => /* @__PURE__ */ jsx(
55
- "pre",
56
- {
57
- className: "bg-black text-white p-4 rounded-lg overflow-x-auto my-6 border shadow-lg font-mono text-sm dark:bg-zinc-900",
58
- ...props,
59
- children
60
- }
61
- ),
68
+ pre: ({ children }) => /* @__PURE__ */ jsx("div", { className: "contents", children }),
62
69
  strong: ({ children, ...props }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", ...props, children }),
63
70
  ul: ({ children, ...props }) => /* @__PURE__ */ jsx(
64
71
  "ul",
@@ -122,16 +129,20 @@ async function MDXContent({
122
129
  const customComponents = buildCustomComponents(components);
123
130
  const allComponents = { ...MDXComponents, ...customComponents };
124
131
  if (enableMDX && hasJSX) {
132
+ let Component;
125
133
  try {
126
- const { default: Component } = await evaluate(processedContent, {
134
+ const result = await evaluate(processedContent, {
127
135
  ...runtime,
128
136
  baseUrl: import.meta.url
129
137
  });
130
- return /* @__PURE__ */ jsx("div", { className: proseClasses, children: /* @__PURE__ */ jsx(Component, { components: allComponents }) });
138
+ Component = result.default;
131
139
  } catch (error) {
132
140
  console.error("Error rendering MDX:", error);
133
- return /* @__PURE__ */ jsx("div", { className: proseClasses, children: /* @__PURE__ */ jsx(ReactMarkdown, { components: allComponents, children: content }) });
134
141
  }
142
+ if (Component) {
143
+ return /* @__PURE__ */ jsx("div", { className: proseClasses, children: /* @__PURE__ */ jsx(Component, { components: allComponents }) });
144
+ }
145
+ return /* @__PURE__ */ jsx("div", { className: proseClasses, children: /* @__PURE__ */ jsx(ReactMarkdown, { components: allComponents, children: content }) });
135
146
  }
136
147
  return /* @__PURE__ */ jsx("div", { className: proseClasses, children: /* @__PURE__ */ jsx(ReactMarkdown, { components: allComponents, children: content }) });
137
148
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { memo, useEffect, useState } from "react";
4
+ import { useMounted } from "../../lib/use-mounted";
4
5
  import { Badge } from "../badge";
5
6
  import {
6
7
  Card,
@@ -29,11 +30,13 @@ function ContentCardImpl({
29
30
  title
30
31
  }) {
31
32
  const [progress, setProgress] = useState(null);
32
- const [isHydrated, setIsHydrated] = useState(false);
33
+ const isHydrated = useMounted();
33
34
  useEffect(() => {
34
- setIsHydrated(true);
35
35
  if (getProgress) {
36
- setProgress(getProgress());
36
+ const result = getProgress();
37
+ requestAnimationFrame(() => {
38
+ setProgress(result);
39
+ });
37
40
  }
38
41
  }, [getProgress]);
39
42
  const showProgress = isHydrated && progress && progress.completedCount > 0;
@@ -26,8 +26,10 @@ function SearchBar({
26
26
  return;
27
27
  }
28
28
  if (!isUserTyping.current && query !== searchParameter) {
29
- setQuery(searchParameter);
30
- lastDebouncedQueryReference.current = searchParameter;
29
+ requestAnimationFrame(() => {
30
+ setQuery(searchParameter);
31
+ lastDebouncedQueryReference.current = searchParameter;
32
+ });
31
33
  }
32
34
  }, [searchParameters, query]);
33
35
  useEffect(() => {
@@ -122,7 +122,7 @@ function Sidebar({ sections }) {
122
122
  "aside",
123
123
  {
124
124
  className: cn(
125
- "fixed lg:relative top-16 lg:top-0 left-0 z-40 h-full lg:h-full border-r bg-background transition-transform duration-300 ease-in-out",
125
+ "fixed lg:relative top-16 lg:top-0 bottom-0 lg:bottom-auto left-0 z-40 lg:h-full border-r bg-background transition-transform duration-300 ease-in-out",
126
126
  "flex flex-col",
127
127
  "overflow-hidden",
128
128
  "shrink-0",
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { memo, useCallback, useEffect, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
+ import { useMounted } from "../../lib/use-mounted";
5
6
  import { cn } from "../../lib/utils";
6
7
  import { CompletionDialog } from "../completion-dialog";
7
8
  const DEFAULT_LABELS = {
@@ -31,16 +32,13 @@ function SlideshowImpl({
31
32
  const [animationDirection, setAnimationDirection] = useState(null);
32
33
  const [isCompletionDialogOpen, setIsCompletionDialogOpen] = useState(false);
33
34
  const [isTocOpen, setIsTocOpen] = useState(false);
34
- const [mounted, setMounted] = useState(false);
35
+ const mounted = useMounted();
35
36
  const currentSection = sections[currentIndex];
36
37
  const isCurrentCompleted = currentSection ? completedSections.has(currentSection.id) : false;
37
38
  const isLastSection = currentIndex === sections.length - 1;
38
39
  const canGoNext = currentIndex < sections.length - 1;
39
40
  const canGoPrevious = currentIndex > 0;
40
41
  const progress = (currentIndex + 1) / sections.length * 100;
41
- useEffect(() => {
42
- setMounted(true);
43
- }, []);
44
42
  useEffect(() => {
45
43
  document.body.style.overflow = "hidden";
46
44
  return () => {
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useRef, useState } from "react";
4
4
  import { useTheme } from "next-themes";
5
+ import { useMounted } from "../../lib/use-mounted";
5
6
  import { Button } from "../button/button";
6
7
  import {
7
8
  DropdownMenu,
@@ -51,7 +52,7 @@ function ThemeMenuItem({
51
52
  }
52
53
  function ThemeToggle({ dict }) {
53
54
  const { setTheme, theme } = useTheme();
54
- const [mounted, setMounted] = useState(false);
55
+ const mounted = useMounted();
55
56
  const [open, setOpen] = useState(false);
56
57
  const closeTimerReference = useRef(null);
57
58
  const isHoveringOverMenuAreaReference = useRef(false);
@@ -73,9 +74,6 @@ function ThemeToggle({ dict }) {
73
74
  }
74
75
  setOpen(nextOpen);
75
76
  }, []);
76
- useEffect(() => {
77
- setMounted(true);
78
- }, []);
79
77
  const getThemeIcon = useCallback(() => {
80
78
  if (!mounted) return "\u2600";
81
79
  switch (theme) {
@@ -11,7 +11,11 @@ function ThinkingBlock({
11
11
  const [isExpanded, setIsExpanded] = useState(isStreaming);
12
12
  const contentId = useId();
13
13
  useEffect(() => {
14
- if (isStreaming) setIsExpanded(true);
14
+ if (isStreaming) {
15
+ requestAnimationFrame(() => {
16
+ setIsExpanded(true);
17
+ });
18
+ }
15
19
  }, [isStreaming]);
16
20
  const toggleExpanded = useCallback(() => {
17
21
  setIsExpanded((previous) => !previous);
@@ -8,15 +8,24 @@ function TLDRSection({ children, label }) {
8
8
  const timerReference = useRef(null);
9
9
  useEffect(() => {
10
10
  if (isExpanded && !hasBeenOpened) {
11
- setIsLoading(true);
12
- setHasBeenOpened(true);
13
11
  if (timerReference.current) {
14
12
  clearTimeout(timerReference.current);
15
13
  }
14
+ const rafId = requestAnimationFrame(() => {
15
+ setIsLoading(true);
16
+ setHasBeenOpened(true);
17
+ });
16
18
  timerReference.current = setTimeout(() => {
17
19
  setIsLoading(false);
18
20
  timerReference.current = null;
19
21
  }, 800);
22
+ return () => {
23
+ cancelAnimationFrame(rafId);
24
+ if (timerReference.current) {
25
+ clearTimeout(timerReference.current);
26
+ timerReference.current = null;
27
+ }
28
+ };
20
29
  }
21
30
  return () => {
22
31
  if (timerReference.current) {
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { memo, useEffect, useState } from "react";
4
+ import { useMounted } from "../../lib/use-mounted";
4
5
  import { Badge } from "../badge";
5
6
  import {
6
7
  Card,
@@ -29,11 +30,13 @@ function TutorialCardImpl({
29
30
  tutorial
30
31
  }) {
31
32
  const [progress, setProgress] = useState(null);
32
- const [isHydrated, setIsHydrated] = useState(false);
33
+ const isHydrated = useMounted();
33
34
  useEffect(() => {
34
- setIsHydrated(true);
35
35
  if (getProgress) {
36
- setProgress(getProgress(tutorial.id));
36
+ const result = getProgress(tutorial.id);
37
+ requestAnimationFrame(() => {
38
+ setProgress(result);
39
+ });
37
40
  }
38
41
  }, [getProgress, tutorial.id]);
39
42
  const difficultyVariant = DIFFICULTY_VARIANTS[tutorial.difficulty];
@@ -0,0 +1,6 @@
1
+ import {
2
+ ViewSwitcher
3
+ } from "./view-switcher";
4
+ export {
5
+ ViewSwitcher
6
+ };
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { memo, Suspense } from "react";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+ import { cn } from "../../lib/utils";
6
+ function ViewSwitcherInner({
7
+ className,
8
+ defaultKey,
9
+ options,
10
+ paramName: parameterName = "view"
11
+ }) {
12
+ const router = useRouter();
13
+ const pathname = usePathname();
14
+ const searchParameters = useSearchParams();
15
+ const resolvedDefault = defaultKey ?? options[0]?.key ?? "";
16
+ const currentKey = searchParameters.get(parameterName) ?? resolvedDefault;
17
+ function handleSelect(key) {
18
+ const parameters = new URLSearchParams(searchParameters.toString());
19
+ if (key === resolvedDefault) {
20
+ parameters.delete(parameterName);
21
+ } else {
22
+ parameters.set(parameterName, key);
23
+ }
24
+ const query = parameters.toString();
25
+ router.push(query ? `${pathname}?${query}` : pathname, { scroll: false });
26
+ }
27
+ return /* @__PURE__ */ jsx(
28
+ "div",
29
+ {
30
+ className: cn(
31
+ "inline-flex items-center rounded-lg border bg-muted p-1",
32
+ className
33
+ ),
34
+ role: "tablist",
35
+ children: options.map((option) => /* @__PURE__ */ jsx(
36
+ "button",
37
+ {
38
+ "aria-selected": currentKey === option.key,
39
+ className: cn(
40
+ "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
41
+ currentKey === option.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
42
+ ),
43
+ onClick: () => {
44
+ handleSelect(option.key);
45
+ },
46
+ role: "tab",
47
+ type: "button",
48
+ children: option.label
49
+ },
50
+ option.key
51
+ ))
52
+ }
53
+ );
54
+ }
55
+ function ViewSwitcherFallback({
56
+ className,
57
+ defaultKey,
58
+ options
59
+ }) {
60
+ const resolvedDefault = defaultKey ?? options[0]?.key ?? "";
61
+ return /* @__PURE__ */ jsx(
62
+ "div",
63
+ {
64
+ className: cn(
65
+ "inline-flex items-center rounded-lg border bg-muted p-1",
66
+ className
67
+ ),
68
+ role: "tablist",
69
+ children: options.map((option) => /* @__PURE__ */ jsx(
70
+ "button",
71
+ {
72
+ "aria-selected": resolvedDefault === option.key,
73
+ className: cn(
74
+ "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
75
+ resolvedDefault === option.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
76
+ ),
77
+ role: "tab",
78
+ type: "button",
79
+ children: option.label
80
+ },
81
+ option.key
82
+ ))
83
+ }
84
+ );
85
+ }
86
+ const ViewSwitcher = memo(function ViewSwitcher2(props) {
87
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewSwitcherFallback, { ...props }), children: /* @__PURE__ */ jsx(ViewSwitcherInner, { ...props }) });
88
+ });
89
+ ViewSwitcher.displayName = "ViewSwitcher";
90
+ export {
91
+ ViewSwitcher
92
+ };
package/dist/index.d.ts CHANGED
@@ -899,7 +899,7 @@ type ComparisonProps = {
899
899
  before: ComparisonSide;
900
900
  title?: string;
901
901
  };
902
- declare function Comparison({ after, before, title }: ComparisonProps): react_jsx_runtime.JSX.Element;
902
+ declare function Comparison({ after, before, title, ...rest }: ComparisonProps & Record<string, unknown>): react_jsx_runtime.JSX.Element | null;
903
903
  type BeforeAfterProps = {
904
904
  after: ReactNode;
905
905
  before: ReactNode;
@@ -1519,6 +1519,26 @@ declare function useSocialFab(options?: UseSocialFabOptions): {
1519
1519
  };
1520
1520
  };
1521
1521
 
1522
+ type HorizontalScrollRowProps = {
1523
+ children: ReactNode;
1524
+ className?: string;
1525
+ description?: string;
1526
+ title: string;
1527
+ };
1528
+ declare const HorizontalScrollRow: react.NamedExoticComponent<HorizontalScrollRowProps>;
1529
+
1530
+ type ViewOption = {
1531
+ key: string;
1532
+ label: string;
1533
+ };
1534
+ type ViewSwitcherProps = {
1535
+ className?: string;
1536
+ defaultKey?: string;
1537
+ options: ViewOption[];
1538
+ paramName?: string;
1539
+ };
1540
+ declare const ViewSwitcher: react.NamedExoticComponent<ViewSwitcherProps>;
1541
+
1522
1542
  type FlowDiagramNode = {
1523
1543
  data: {
1524
1544
  description?: string;
@@ -1832,6 +1852,28 @@ type Content<TSection extends ContentSectionMinimal = ContentSection> = ContentM
1832
1852
  */
1833
1853
  declare function useDebounce<T>(value: T, delay?: number): T;
1834
1854
 
1855
+ type UseHorizontalScrollReturn = {
1856
+ canScrollLeft: boolean;
1857
+ canScrollRight: boolean;
1858
+ containerRef: React.RefCallback<HTMLElement>;
1859
+ scroll: (direction: "left" | "right") => void;
1860
+ };
1861
+ /**
1862
+ * Hook for horizontal scroll containers with navigation state.
1863
+ *
1864
+ * @returns Scroll state, ref callback for the container, and scroll function.
1865
+ *
1866
+ * @example
1867
+ * ```tsx
1868
+ * const { canScrollLeft, canScrollRight, containerRef, scroll } = useHorizontalScroll();
1869
+ *
1870
+ * <div ref={containerRef} className="overflow-x-auto">
1871
+ * {children}
1872
+ * </div>
1873
+ * ```
1874
+ */
1875
+ declare function useHorizontalScroll(): UseHorizontalScrollReturn;
1876
+
1835
1877
  declare function cn(...inputs: ClassValue[]): string;
1836
1878
 
1837
- export { Accordion, AccordionContent, type AccordionContentProps, AccordionItem, type AccordionItemProps, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AreaChart, AspectRatio, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, BarChart, BeforeAfter, type BeforeAfterProps, BlogCard, Breadcrumb, type BreadcrumbItem, Button, type ButtonProps, Calendar, type CalendarProps, Callout, type CalloutProps, type CalloutVariant, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, CategoryFilter, Checkbox, Checklist, type ChecklistItem, type ChecklistProps, CodeBlock, CodePlayground, type CodePlaygroundProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, CommonMistake, type CommonMistakeProps, Comparison, type ComparisonProps, CompletionDialog, type CompletionDialogProps, Content, ContentCard, ContentIntro, type ContentIntroLabels, type ContentIntroProps, type ContentIntroSection, type ContentMeta, type ContentProgress, type ContentSection, type ContentSectionMinimal, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CookieConsent, type CookieConsentProps, type CopyStatus, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, type DifficultyLevel, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Exercise, type ExerciseProps, FAQ, FAQItem, type FAQItemProps, type FAQProps, FileTree, type FileTreeProps, FilterBar, type FilterBarLabels, type FilterBarProps, type FilterOption, type FilterUpdates, FloatingActionButton, type FloatingActionButtonProps, FlowControls, type FlowControlsProps, FlowDiagram, type FlowDiagramEdge, type FlowDiagramNode, type FlowDiagramProps, FlowErrorBoundary, FlowFullscreen, type FlowFullscreenProps, Glossary, type GlossaryProps, HoverCard, HoverCardContent, HoverCardTrigger, InlineInput, type InlineInputProps, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, KeyConcept, type KeyConceptProps, type KeyboardShortcut, KeyboardShortcutsHelp, type KeyboardShortcutsHelpProps, LANGUAGE_NAMES, Label, LangProvider, LearningObjectives, type LearningObjectivesProps, LineChart, MDXContent, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, type ModelInfo, ModelSelector, type ModelSelectorProps, type NavItem, NavbarSaas, type NavbarSaasProps, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, Pagination, type PaginationProps, type PlatformConfig, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Prerequisites, type PrerequisitesProps, ProTip, type ProTipProps, type ProTipVariant, ProfileSection, ProgressBar, type ProgressBarProps, Quiz, type QuizOption, type QuizProps, RadioGroup, RadioGroupItem, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, ScrollBar, SearchBar, SearchDialog, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, ShareDialog, type ShareDialogLabels, type SharePlatform as ShareDialogPlatform, type ShareDialogProps, type SharePlatform$1 as SharePlatform, type SharePlatformConfig, ShareSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, type SidebarItem, SidebarProvider, type SidebarSection, SidebarToggle, type SidebarToggleProps, SimpleTerminal, type SimpleTerminalProps, Skeleton, Slider, Slideshow, type SlideshowLabels, type SlideshowProps, type SlideshowSection, SocialFAB, type SocialFabActionConfig, type SocialFabLabels, type SocialFabProps, Spinner, type SpinnerProps, Step, StepByStep, type StepByStepProps, StepNavigation, type StepNavigationProps, type StepProps, Summary, type SummaryProps, Switch, TLDRSection, type TOCSection, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, TableOfContentsPanel, type TableOfContentsPanelProps, TableRow, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsListProps, type TabsProps, TabsTrigger, type TabsTriggerProps, Terminal, type TerminalLine, type TerminalProps, Textarea, type TextareaProps, ThemeProvider, ThemeToggle, ThinkingBlock, type ThinkingBlockProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastProps, ToastTitle, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TruncatedText, type TruncatedTextProps, TutorialCard, type TutorialCardLabels, type TutorialCardMeta, type TutorialCardProgress, type TutorialCardProps, TutorialComplete, type TutorialCompleteLabels, type TutorialCompleteProps, type TutorialCompleteRelatedContent, type TutorialCompleteSection, TutorialFilters, type TutorialFiltersLabels, type TutorialFiltersProps, TutorialIntroContent, type TutorialIntroContentProps, TutorialMDX, type TutorialMDXProps, type SupportedLanguage as UISupportedLanguage, type UseFlowDiagramOptions, type UseFlowDiagramReturn, VideoEmbed, type VideoEmbedProps, alertVariants, badgeVariants, buttonVariants, cn, cookieConsentVariants, getOtherLanguage, mdxComponents, navigationMenuTriggerStyle, toggleVariants, useDebounce, useFlowDiagram, useMobile, useSidebar, useSocialFab };
1879
+ export { Accordion, AccordionContent, type AccordionContentProps, AccordionItem, type AccordionItemProps, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AreaChart, AspectRatio, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, BarChart, BeforeAfter, type BeforeAfterProps, BlogCard, Breadcrumb, type BreadcrumbItem, Button, type ButtonProps, Calendar, type CalendarProps, Callout, type CalloutProps, type CalloutVariant, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, CategoryFilter, Checkbox, Checklist, type ChecklistItem, type ChecklistProps, CodeBlock, CodePlayground, type CodePlaygroundProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, CommonMistake, type CommonMistakeProps, Comparison, type ComparisonProps, CompletionDialog, type CompletionDialogProps, Content, ContentCard, ContentIntro, type ContentIntroLabels, type ContentIntroProps, type ContentIntroSection, type ContentMeta, type ContentProgress, type ContentSection, type ContentSectionMinimal, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CookieConsent, type CookieConsentProps, type CopyStatus, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, type DifficultyLevel, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Exercise, type ExerciseProps, FAQ, FAQItem, type FAQItemProps, type FAQProps, FileTree, type FileTreeProps, FilterBar, type FilterBarLabels, type FilterBarProps, type FilterOption, type FilterUpdates, FloatingActionButton, type FloatingActionButtonProps, FlowControls, type FlowControlsProps, FlowDiagram, type FlowDiagramEdge, type FlowDiagramNode, type FlowDiagramProps, FlowErrorBoundary, FlowFullscreen, type FlowFullscreenProps, Glossary, type GlossaryProps, HorizontalScrollRow, type HorizontalScrollRowProps, HoverCard, HoverCardContent, HoverCardTrigger, InlineInput, type InlineInputProps, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, KeyConcept, type KeyConceptProps, type KeyboardShortcut, KeyboardShortcutsHelp, type KeyboardShortcutsHelpProps, LANGUAGE_NAMES, Label, LangProvider, LearningObjectives, type LearningObjectivesProps, LineChart, MDXContent, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, type ModelInfo, ModelSelector, type ModelSelectorProps, type NavItem, NavbarSaas, type NavbarSaasProps, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, Pagination, type PaginationProps, type PlatformConfig, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Prerequisites, type PrerequisitesProps, ProTip, type ProTipProps, type ProTipVariant, ProfileSection, ProgressBar, type ProgressBarProps, Quiz, type QuizOption, type QuizProps, RadioGroup, RadioGroupItem, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, ScrollBar, SearchBar, SearchDialog, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, ShareDialog, type ShareDialogLabels, type SharePlatform as ShareDialogPlatform, type ShareDialogProps, type SharePlatform$1 as SharePlatform, type SharePlatformConfig, ShareSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, type SidebarItem, SidebarProvider, type SidebarSection, SidebarToggle, type SidebarToggleProps, SimpleTerminal, type SimpleTerminalProps, Skeleton, Slider, Slideshow, type SlideshowLabels, type SlideshowProps, type SlideshowSection, SocialFAB, type SocialFabActionConfig, type SocialFabLabels, type SocialFabProps, Spinner, type SpinnerProps, Step, StepByStep, type StepByStepProps, StepNavigation, type StepNavigationProps, type StepProps, Summary, type SummaryProps, Switch, TLDRSection, type TOCSection, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, TableOfContentsPanel, type TableOfContentsPanelProps, TableRow, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsListProps, type TabsProps, TabsTrigger, type TabsTriggerProps, Terminal, type TerminalLine, type TerminalProps, Textarea, type TextareaProps, ThemeProvider, ThemeToggle, ThinkingBlock, type ThinkingBlockProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastProps, ToastTitle, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TruncatedText, type TruncatedTextProps, TutorialCard, type TutorialCardLabels, type TutorialCardMeta, type TutorialCardProgress, type TutorialCardProps, TutorialComplete, type TutorialCompleteLabels, type TutorialCompleteProps, type TutorialCompleteRelatedContent, type TutorialCompleteSection, TutorialFilters, type TutorialFiltersLabels, type TutorialFiltersProps, TutorialIntroContent, type TutorialIntroContentProps, TutorialMDX, type TutorialMDXProps, type SupportedLanguage as UISupportedLanguage, type UseFlowDiagramOptions, type UseFlowDiagramReturn, VideoEmbed, type VideoEmbedProps, type ViewOption, ViewSwitcher, type ViewSwitcherProps, alertVariants, badgeVariants, buttonVariants, cn, cookieConsentVariants, getOtherLanguage, mdxComponents, navigationMenuTriggerStyle, toggleVariants, useDebounce, useFlowDiagram, useHorizontalScroll, useMobile, useSidebar, useSocialFab };
package/dist/index.js CHANGED
@@ -4,10 +4,12 @@ import {
4
4
  LANGUAGE_NAMES
5
5
  } from "./lib/types";
6
6
  import { useDebounce } from "./lib/use-debounce";
7
+ import { useHorizontalScroll } from "./lib/use-horizontal-scroll";
7
8
  import { cn } from "./lib/utils";
8
9
  export {
9
10
  LANGUAGE_NAMES,
10
11
  cn,
11
12
  getOtherLanguage,
12
- useDebounce
13
+ useDebounce,
14
+ useHorizontalScroll
13
15
  };
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ function useHorizontalScroll() {
4
+ const scrollRef = useRef(void 0);
5
+ const observerRef = useRef(void 0);
6
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
7
+ const [canScrollRight, setCanScrollRight] = useState(false);
8
+ const updateScrollState = useCallback(() => {
9
+ const element = scrollRef.current;
10
+ if (!element) return;
11
+ setCanScrollLeft(element.scrollLeft > 0);
12
+ setCanScrollRight(
13
+ element.scrollLeft + element.clientWidth < element.scrollWidth - 1
14
+ );
15
+ }, []);
16
+ const containerRef = useCallback(
17
+ (node) => {
18
+ if (scrollRef.current) {
19
+ scrollRef.current.removeEventListener("scroll", updateScrollState);
20
+ }
21
+ if (observerRef.current) {
22
+ observerRef.current.disconnect();
23
+ observerRef.current = void 0;
24
+ }
25
+ scrollRef.current = node ?? void 0;
26
+ if (node) {
27
+ node.addEventListener("scroll", updateScrollState, { passive: true });
28
+ if (typeof ResizeObserver !== "undefined") {
29
+ observerRef.current = new ResizeObserver(updateScrollState);
30
+ observerRef.current.observe(node);
31
+ }
32
+ updateScrollState();
33
+ }
34
+ },
35
+ [updateScrollState]
36
+ );
37
+ useEffect(() => {
38
+ return () => {
39
+ if (scrollRef.current) {
40
+ scrollRef.current.removeEventListener("scroll", updateScrollState);
41
+ }
42
+ if (observerRef.current) {
43
+ observerRef.current.disconnect();
44
+ }
45
+ };
46
+ }, [updateScrollState]);
47
+ const scroll = useCallback((direction) => {
48
+ const element = scrollRef.current;
49
+ if (!element) return;
50
+ const amount = element.clientWidth * 0.8;
51
+ element.scrollBy({
52
+ behavior: "smooth",
53
+ left: direction === "left" ? -amount : amount
54
+ });
55
+ }, []);
56
+ return { canScrollLeft, canScrollRight, containerRef, scroll };
57
+ }
58
+ export {
59
+ useHorizontalScroll
60
+ };
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ function noop() {
4
+ }
5
+ function subscribe() {
6
+ return noop;
7
+ }
8
+ function useMounted() {
9
+ return useSyncExternalStore(
10
+ subscribe,
11
+ () => true,
12
+ () => false
13
+ );
14
+ }
15
+ export {
16
+ useMounted
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vllnt/ui",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "React component library — 93 components built on Radix UI, Tailwind CSS, and CVA",
5
5
  "license": "MIT",
6
6
  "author": "vllnt",
@@ -23,21 +23,40 @@
23
23
  "shadcn"
24
24
  ],
25
25
  "type": "module",
26
- "main": "./dist/index.js",
27
- "module": "./dist/index.js",
28
- "types": "./dist/index.d.ts",
26
+ "main": "./src/index.ts",
27
+ "module": "./src/index.ts",
28
+ "types": "./src/index.ts",
29
29
  "exports": {
30
30
  ".": {
31
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.js"
31
+ "types": "./src/index.ts",
32
+ "import": "./src/index.ts"
33
33
  },
34
34
  "./tailwind-preset": {
35
- "types": "./dist/tailwind-preset.d.ts",
36
- "import": "./dist/tailwind-preset.js"
35
+ "types": "./src/tailwind-preset.ts",
36
+ "import": "./src/tailwind-preset.ts"
37
37
  },
38
38
  "./styles.css": "./styles.css",
39
39
  "./themes/default.css": "./themes/default.css"
40
40
  },
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org",
43
+ "access": "public",
44
+ "main": "./dist/index.js",
45
+ "module": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "exports": {
48
+ ".": {
49
+ "types": "./dist/index.d.ts",
50
+ "import": "./dist/index.js"
51
+ },
52
+ "./tailwind-preset": {
53
+ "types": "./dist/tailwind-preset.d.ts",
54
+ "import": "./dist/tailwind-preset.js"
55
+ },
56
+ "./styles.css": "./styles.css",
57
+ "./themes/default.css": "./themes/default.css"
58
+ }
59
+ },
41
60
  "files": [
42
61
  "dist",
43
62
  "styles.css",
@@ -46,6 +65,20 @@
46
65
  "sideEffects": [
47
66
  "*.css"
48
67
  ],
68
+ "scripts": {
69
+ "build": "tsup",
70
+ "clean": "rm -rf dist node_modules coverage .snapshots playwright-report test-results",
71
+ "lint": "eslint .",
72
+ "lint:fix": "eslint . --fix",
73
+ "check:circular": "madge --circular --extensions ts,tsx src",
74
+ "test": "vitest",
75
+ "test:once": "vitest run",
76
+ "test:coverage": "vitest run --coverage",
77
+ "test:visual": "playwright test -c playwright-ct.config.ts",
78
+ "test:visual:update": "playwright test -c playwright-ct.config.ts --update-snapshots",
79
+ "test:all": "pnpm test:coverage && pnpm test:visual",
80
+ "test:generate": "tsx scripts/generate-tests.ts"
81
+ },
49
82
  "peerDependencies": {
50
83
  "react": ">=18.0.0",
51
84
  "react-dom": ">=18.0.0",
@@ -125,19 +158,5 @@
125
158
  "tsx": "^4.21.0",
126
159
  "typescript": "^5.9.3",
127
160
  "vitest": "^4.0.16"
128
- },
129
- "scripts": {
130
- "build": "tsup",
131
- "clean": "rm -rf dist node_modules coverage .snapshots playwright-report test-results",
132
- "lint": "eslint .",
133
- "lint:fix": "eslint . --fix",
134
- "check:circular": "madge --circular --extensions ts,tsx src",
135
- "test": "vitest",
136
- "test:once": "vitest run",
137
- "test:coverage": "vitest run --coverage",
138
- "test:visual": "playwright test -c playwright-ct.config.ts",
139
- "test:visual:update": "playwright test -c playwright-ct.config.ts --update-snapshots",
140
- "test:all": "pnpm test:coverage && pnpm test:visual",
141
- "test:generate": "tsx scripts/generate-tests.ts"
142
161
  }
143
- }
162
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 vllnt
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.