Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/registry/app/report/report-bug-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,16 @@ function TextField({
);
}

function useInitialState<T>(initialValue: T) {
return useState(initialValue);
}

export function ReportBugForm({
initialComponent = "",
}: {
initialComponent?: string;
}) {
const [component, setComponent] = useState(initialComponent);
const [component, setComponent] = useInitialState(initialComponent);
const [summary, setSummary] = useState("");
const [repro, setRepro] = useState("");
const [expected, setExpected] = useState("");
Expand Down
5 changes: 1 addition & 4 deletions apps/registry/components/storybook-embed/storybook-embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ function StorybookIframe({
}): React.ReactElement {
const [isLoaded, setIsLoaded] = React.useState(false);

React.useEffect(() => {
setIsLoaded(false);
}, [iframeSource]);

return (
<div style={{ minHeight: height, position: "relative" }}>
{isLoaded ? null : (
Expand Down Expand Up @@ -201,6 +197,7 @@ export function StorybookEmbed({
/>
{iframeSource ? (
<StorybookIframe
key={iframeSource}
componentName={componentName}
height={height}
iframeSource={iframeSource}
Expand Down
15 changes: 8 additions & 7 deletions apps/registry/registry/default/ai-sidebar/ai-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
useContext,
useEffect,
useMemo,
useState,
} from "react";

import { Bot, MessageSquarePlus, X } from "lucide-react";

import { useControllableState } from "@vllnt/ui";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registry snippet now imports useControllableState from the public @vllnt/ui root, but that symbol is not exported by the package root (packages/ui/src/index.ts). The same pattern appears in the other migrated snippets for useEventCallback, useDocumentEventListener, useWindowEventListener, and useUncontrolledState. Because apps/registry/tsconfig.json excludes registry/**, the regular registry typecheck does not catch it; a targeted snippet typecheck fails with TS2305 missing-export errors. Please either export these helpers publicly or keep the registry snippets self-contained so copied examples compile for consumers.

import { cn } from "@vllnt/ui";
import { Button } from "@vllnt/ui";

Expand Down Expand Up @@ -131,16 +131,17 @@ export function AISidebarProvider({
() => ({ ...DEFAULT_LABELS, ...labels }),
[labels],
);
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const openState = isControlled ? controlledOpen : uncontrolled;
const [openState, setOpenState] = useControllableState({
defaultValue: defaultOpen,
onChange: onOpenChange,
value: controlledOpen,
});

const setOpen = useCallback(
(next: boolean) => {
if (!isControlled) setUncontrolled(next);
onOpenChange?.(next);
setOpenState(next);
},
[isControlled, onOpenChange],
[setOpenState],
);

const open = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ function useRevealProgress(active: boolean, length: number, stagger: number) {

React.useEffect(() => {
if (!active) {
setProgress(length);
return;
}

Expand All @@ -191,7 +190,7 @@ function useRevealProgress(active: boolean, length: number, stagger: number) {
};
}, [active, length, stagger]);

return progress;
return active ? progress : length;
}

function useMatrixFrame({
Expand Down
5 changes: 2 additions & 3 deletions apps/registry/registry/default/faq/faq.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";

import { useState } from "react";

import { ChevronDown, HelpCircle } from "lucide-react";
import type { ReactNode } from "react";

import { useUncontrolledState } from "@vllnt/ui";
import { cn } from "@vllnt/ui";

export type FAQItemProps = {
Expand All @@ -14,7 +13,7 @@ export type FAQItemProps = {
};

function FAQItem({ children, defaultOpen = false, question }: FAQItemProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isOpen, setIsOpen] = useUncontrolledState(defaultOpen);

return (
<div className="border-b border-border last:border-b-0">
Expand Down
5 changes: 2 additions & 3 deletions apps/registry/registry/default/flashcard/flashcard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";

import { useState } from "react";

import { RefreshCcw } from "lucide-react";
import type { ReactNode } from "react";

import { useUncontrolledState } from "@vllnt/ui";
import { cn } from "@vllnt/ui";
import { Badge } from "@vllnt/ui";
import { Button } from "@vllnt/ui";
Expand All @@ -31,7 +30,7 @@ export function Flashcard({
question,
title = "Flashcard",
}: FlashcardProps): ReactNode {
const [flipped, setFlipped] = useState(defaultFlipped);
const [flipped, setFlipped] = useUncontrolledState(defaultFlipped);

const toggleFlipped = (): void => {
const nextValue = !flipped;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { memo, useEffect, useRef } from "react";

import type { ReactNode } from "react";

import {
useEventCallback,
useWindowEventListener,
} from "@vllnt/ui";
import { cn } from "@vllnt/ui";

export type KeyboardShortcut = {
Expand Down Expand Up @@ -32,25 +36,21 @@ function KeyboardShortcutsHelpImpl({
title = "Keyboard Shortcuts",
}: KeyboardShortcutsHelpProps): React.ReactNode {
const closeButtonRef = useRef<HTMLButtonElement>(null);
const handleEscapeKey = useEventCallback((event: KeyboardEvent): void => {
if (event.key === "Escape") {
event.preventDefault();
onClose();
}
});

// Focus trap and close on Escape
useEffect(() => {
if (!isOpen) return;

closeButtonRef.current?.focus();
}, [isOpen]);

const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === "Escape") {
event.preventDefault();
onClose();
}
};

window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen, onClose]);
useWindowEventListener("keydown", handleEscapeKey, { enabled: isOpen });

// Prevent body scroll when open
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,6 @@ function usePlayback(arguments_: {
const next = yearRef.current + delta * speed;
if (next >= endYear) {
setYear(endYear);
setIsPlaying(false);
return;
}
setYear(next);
Expand Down
17 changes: 5 additions & 12 deletions apps/registry/registry/default/progress-card/progress-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { memo, useEffect, useState } from "react";
import { memo, useMemo } from "react";

import type { ReactNode } from "react";

Expand Down Expand Up @@ -76,18 +76,11 @@ function ContentCardImpl({
tags = EMPTY_PROGRESS_CARD_LIST,
title,
}: ContentCardProps): React.ReactNode {
const [progress, setProgress] = useState<ContentCardProgress | null>(null);
const isHydrated = useMounted();

// Load progress after hydration
useEffect(() => {
if (getProgress) {
const result = getProgress();
requestAnimationFrame(() => {
setProgress(result);
});
}
}, [getProgress]);
const progress = useMemo(
() => (isHydrated && getProgress ? getProgress() : null),
[getProgress, isHydrated],
);

const showProgress = isHydrated && progress && progress.completedCount > 0;

Expand Down
15 changes: 7 additions & 8 deletions apps/registry/registry/default/rating/rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
import { Star } from "lucide-react";
import type { ReactNode } from "react";

import { useControllableState } from "@vllnt/ui";
import { cn } from "@vllnt/ui";

const sizeClasses = {
Expand Down Expand Up @@ -113,20 +114,18 @@ export function Rating({
size = "md",
value,
}: RatingProps): ReactNode {
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
const [hoveredValue, setHoveredValue] = useState(0);
const activeValue = isControlled ? (value ?? 0) : internalValue;
const [activeValue, setActiveValue] = useControllableState({
defaultValue,
onChange: onValueChange,
value,
});

const handleSelect = (nextValue: number): void => {
const resolvedValue =
allowClear && activeValue === nextValue ? 0 : nextValue;

if (!isControlled) {
setInternalValue(resolvedValue);
}

onValueChange?.(resolvedValue);
setActiveValue(resolvedValue);
};

return (
Expand Down
8 changes: 6 additions & 2 deletions apps/registry/registry/default/search-bar/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Suspense, useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";

import { useDebounce } from "@vllnt/ui";
import { useEventCallback } from "@vllnt/ui";
import { Button } from "@vllnt/ui";
import { Input } from "@vllnt/ui";

Expand Down Expand Up @@ -58,6 +59,9 @@ function SearchBarInner({
const debouncedQuery = useDebounce(query, 300);
const isInitialMount = useRef(true);
const isUserTyping = useRef(false);
const runSearch = useEventCallback((searchQuery: string): void => {
onSearch?.(searchQuery);
});

const typingTimeoutReference = useRef<NodeJS.Timeout | undefined>(undefined);
const lastSetSearchParameterReference = useRef<string>("");
Expand Down Expand Up @@ -103,7 +107,7 @@ function SearchBarInner({
lastDebouncedQueryReference.current = trimmedQuery;

if (onSearch) {
onSearch(trimmedQuery);
runSearch(trimmedQuery);
return;
}

Expand All @@ -130,7 +134,7 @@ function SearchBarInner({
// hard redirects, not Next router.replace — but ESLint doesn't
// know that rule, so we don't add an eslint-disable for it.
router.replace(`?${newUrl}`);
}, [debouncedQuery, router, onSearch, searchParameters]);
}, [debouncedQuery, onSearch, router, runSearch, searchParameters]);

// Cleanup timeout on unmount
useEffect(() => {
Expand Down
70 changes: 51 additions & 19 deletions apps/registry/registry/default/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
"use client";

import { useEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from "react";

import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";

import { useUncontrolledState } from "@vllnt/ui";
import { cn } from "@vllnt/ui";
import { useSidebar } from "@vllnt/ui";

Expand All @@ -25,26 +32,51 @@ type SidebarProps = {
sections: SidebarSection[];
};

function useMobile(setOpen: (open: boolean) => void) {
const [isMobile, setIsMobile] = useState(false);
function noop(): void {
return undefined;
}

useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 1024;
setIsMobile(mobile);
if (mobile) {
setOpen(false);
} else {
setOpen(true);
}
};
function getMobileSnapshot(): boolean {
return typeof window === "undefined" ? false : window.innerWidth < 1024;
}

checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
function subscribeToResize(
onStoreChange: () => void,
onEnterMobile: () => void,
): () => void {
if (typeof window === "undefined") return noop;

let previousIsMobile = getMobileSnapshot();
const handleResize = (): void => {
const nextIsMobile = getMobileSnapshot();
if (nextIsMobile && !previousIsMobile) {
onEnterMobile();
}
previousIsMobile = nextIsMobile;
onStoreChange();
};

window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}

function useMobile(setOpen: (open: boolean) => void) {
const handleEnterMobile = useCallback(() => {
setOpen(false);
}, [setOpen]);
const subscribe = useCallback(
(onStoreChange: () => void) =>
subscribeToResize(onStoreChange, handleEnterMobile),
[handleEnterMobile],
);

const isMobile = useSyncExternalStore(
subscribe,
getMobileSnapshot,
() => false,
);

return isMobile;
}
Expand Down Expand Up @@ -88,7 +120,7 @@ function CollapsibleSection({
defaultOpen = true,
title,
}: CollapsibleSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isOpen, setIsOpen] = useUncontrolledState(defaultOpen);

if (!collapsible) {
return (
Expand Down
Loading
Loading