diff --git a/apps/registry/app/components/[slug]/page.tsx b/apps/registry/app/components/[slug]/page.tsx index 827ecfa4..cea84bc9 100644 --- a/apps/registry/app/components/[slug]/page.tsx +++ b/apps/registry/app/components/[slug]/page.tsx @@ -45,13 +45,12 @@ const STORYBOOK_URL = process.env.NEXT_PUBLIC_STORYBOOK_URL ?? "http://localhost:6006"; export async function generateStaticParams() { - return registry.items - .filter( - (item): item is RegistryComponent => item.type === "registry:component", - ) - .map((item) => ({ - slug: item.name, - })); + return registry.items.reduce<{ slug: string }[]>((params, item) => { + if (item.type === "registry:component") { + params.push({ slug: item.name }); + } + return params; + }, []); } function getNpmUrl(packageName: string): string { diff --git a/apps/registry/app/llms-full.txt/route.ts b/apps/registry/app/llms-full.txt/route.ts index c1c5fb42..660fafbe 100644 --- a/apps/registry/app/llms-full.txt/route.ts +++ b/apps/registry/app/llms-full.txt/route.ts @@ -65,8 +65,15 @@ async function buildLlmsFullTxt(): Promise { lines.push("```"); lines.push(""); - for (const page of DOC_PAGES) { - const body = await readDocPage(page.slug); + const docPages = await Promise.all( + DOC_PAGES.map(async (page) => ({ + ...page, + body: await readDocPage(page.slug), + })), + ); + + for (const page of docPages) { + const { body } = page; if (!body) continue; lines.push(`## ${page.title}`); lines.push(""); @@ -83,7 +90,7 @@ async function buildLlmsFullTxt(): Promise { ); lines.push(""); - for (const item of [...items].sort((a, b) => a.name.localeCompare(b.name))) { + for (const item of items.toSorted((a, b) => a.name.localeCompare(b.name))) { lines.push(`### ${item.title}`); lines.push(""); lines.push(`- Slug: \`${item.name}\``); diff --git a/apps/registry/app/llms.txt/route.ts b/apps/registry/app/llms.txt/route.ts index 32130fb2..39aa3af6 100644 --- a/apps/registry/app/llms.txt/route.ts +++ b/apps/registry/app/llms.txt/route.ts @@ -102,7 +102,7 @@ function buildLlmsTxt(): string { const label = CATEGORY_LABEL[category] ?? category; lines.push(`## Components — ${label}`); lines.push(""); - for (const item of [...bucket].sort((a, b) => a.name.localeCompare(b.name))) { + for (const item of bucket.toSorted((a, b) => a.name.localeCompare(b.name))) { lines.push( `- [${item.title}](${SITE_URL}/components/${item.name}): ${item.description}`, ); diff --git a/apps/registry/components/component-preview/component-preview.tsx b/apps/registry/components/component-preview/component-preview.tsx index d3bf3011..9642a5d8 100644 --- a/apps/registry/components/component-preview/component-preview.tsx +++ b/apps/registry/components/component-preview/component-preview.tsx @@ -1033,10 +1033,10 @@ function StepNavigationPreview() { canPrev={step > 1} currentStep={step} onNext={() => { - setStep(step + 1); + setStep((currentStep) => currentStep + 1); }} onPrev={() => { - setStep(step - 1); + setStep((currentStep) => currentStep - 1); }} stepLabel="Step" totalSteps={5} diff --git a/apps/registry/components/header/header.tsx b/apps/registry/components/header/header.tsx index 54842b4b..f4f6665e 100644 --- a/apps/registry/components/header/header.tsx +++ b/apps/registry/components/header/header.tsx @@ -22,13 +22,18 @@ export function Header() { { href: "/components", title: "Components" }, ]; - const searchItems = registry.items - .filter((item) => item.type === "registry:component") - .map((item) => ({ - description: item.description, - id: item.name, - title: item.title, - })); + const searchItems = registry.items.reduce< + { description?: string; id: string; title: string }[] + >((items, item) => { + if (item.type === "registry:component") { + items.push({ + description: item.description, + id: item.name, + title: item.title, + }); + } + return items; + }, []); return ( ( null, ); const resolvedStoryId = storyId ?? toStoryId(componentName); React.useEffect(() => { - if (hasManualThemeSelection) { + if (hasManualThemeSelectionRef.current) { return; } @@ -180,7 +179,7 @@ export function StorybookEmbed({ observer.disconnect(); mediaQuery.removeEventListener("change", updateTheme); }; - }, [hasManualThemeSelection]); + }, []); const iframeSource = React.useMemo(() => { if (previewTheme === null) { @@ -194,7 +193,7 @@ export function StorybookEmbed({
{ - setHasManualThemeSelection(true); + hasManualThemeSelectionRef.current = true; setPreviewTheme(value); }} value={previewTheme} diff --git a/apps/registry/lib/sidebar-sections.ts b/apps/registry/lib/sidebar-sections.ts index 3113e4a8..c966bee0 100644 --- a/apps/registry/lib/sidebar-sections.ts +++ b/apps/registry/lib/sidebar-sections.ts @@ -8,14 +8,19 @@ import type { const registry = registryData as Registry; const components = registry.items - .filter( - (item): item is RegistryComponent => item.type === "registry:component", + .reduce<{ category?: ComponentCategory; name: string; title: string }[]>( + (items, item) => { + if (item.type === "registry:component") { + items.push({ + category: item.category, + name: item.name, + title: item.title, + }); + } + return items; + }, + [], ) - .map((item) => ({ - category: item.category, - name: item.name, - title: item.title, - })) .sort((a, b) => a.title.localeCompare(b.title)); const categoryLabels: Record = { @@ -53,13 +58,24 @@ function groupComponentsByCategory( return accumulator; }, new Map()); - return categoryOrder - .filter((cat) => grouped.has(cat)) - .map((category) => ({ + const sections: { + category: ComponentCategory; + items: { name: string; title: string }[]; + label: string; + }[] = []; + + for (const category of categoryOrder) { + const items = grouped.get(category); + if (items) { + sections.push({ category, - items: grouped.get(category) ?? [], + items, label: categoryLabels[category], - })); + }); + } + } + + return sections; } const groupedComponents = groupComponentsByCategory(components); diff --git a/apps/registry/lib/stats.ts b/apps/registry/lib/stats.ts index e1f09538..7e51964a 100644 --- a/apps/registry/lib/stats.ts +++ b/apps/registry/lib/stats.ts @@ -56,7 +56,17 @@ export function getFeaturedComponents(): readonly RegistryItem[] { "timeline", "globe-3d", ]; - return featuredSlugs - .map((slug) => REGISTRY.items.find((item) => item.name === slug)) - .filter((item): item is RegistryItem => item !== undefined); + const registryByName = new Map(); + for (const item of REGISTRY.items) { + registryByName.set(item.name, item); + } + + const featured: RegistryItem[] = []; + for (const slug of featuredSlugs) { + const item = registryByName.get(slug); + if (item) { + featured.push(item); + } + } + return featured; } diff --git a/apps/registry/registry/default/auto-reload/auto-reload.tsx b/apps/registry/registry/default/auto-reload/auto-reload.tsx index 58de5b0a..c09b0bcc 100644 --- a/apps/registry/registry/default/auto-reload/auto-reload.tsx +++ b/apps/registry/registry/default/auto-reload/auto-reload.tsx @@ -131,7 +131,7 @@ function getCurrencyFormatter( const key = `${locale}|${currency}`; let formatter = CURRENCY_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, { + formatter = Intl.NumberFormat(locale, { currency, style: "currency", }); diff --git a/apps/registry/registry/default/carousel/carousel.tsx b/apps/registry/registry/default/carousel/carousel.tsx index 01918acb..c6761020 100644 --- a/apps/registry/registry/default/carousel/carousel.tsx +++ b/apps/registry/registry/default/carousel/carousel.tsx @@ -7,6 +7,7 @@ import { useContext, useEffect, useMemo, + useRef, useState, } from "react"; @@ -82,6 +83,11 @@ function useCarouselLogic({ setCanScrollPrevious(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }, []); + const onSelectReference = useRef(onSelect); + + useEffect(() => { + onSelectReference.current = onSelect; + }, [onSelect]); const scrollPrevious = useCallback(() => { api?.scrollPrev(); @@ -117,19 +123,23 @@ function useCarouselLogic({ return; } - api.on("reInit", onSelect); - api.on("select", onSelect); + const notifySelection = (selectedApi: CarouselApi) => { + onSelectReference.current(selectedApi); + }; + + api.on("reInit", notifySelection); + api.on("select", notifySelection); const rafId = requestAnimationFrame(() => { - onSelect(api); + notifySelection(api); }); return () => { - api?.off("select", onSelect); - api?.off("reInit", onSelect); + api?.off("select", notifySelection); + api?.off("reInit", notifySelection); cancelAnimationFrame(rafId); }; - }, [api, onSelect]); + }, [api]); return { api, diff --git a/apps/registry/registry/default/completion-dialog/completion-dialog.tsx b/apps/registry/registry/default/completion-dialog/completion-dialog.tsx index f3c4f334..23c942d2 100644 --- a/apps/registry/registry/default/completion-dialog/completion-dialog.tsx +++ b/apps/registry/registry/default/completion-dialog/completion-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useEffect, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import type { ReactNode } from "react"; @@ -125,8 +125,12 @@ function CompletionDialogImpl({ onConfirm, title, }: CompletionDialogProps): React.ReactNode { - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { + const keyDownHandlerRef = useRef<(event: KeyboardEvent) => void>(() => { + return; + }); + + useEffect(() => { + keyDownHandlerRef.current = (event: KeyboardEvent) => { if (!isOpen) return; if (event.key === "Escape") { event.preventDefault(); @@ -148,17 +152,20 @@ function CompletionDialogImpl({ event.stopPropagation(); onCancel(); } - }, - [isOpen, onClose, onConfirm, onCancel, confirmShortcut, cancelShortcut], - ); + }; + }, [cancelShortcut, confirmShortcut, isOpen, onCancel, onClose, onConfirm]); useEffect(() => { if (!isOpen) return; - document.addEventListener("keydown", handleKeyDown, true); + const onDocumentKeyDown = (event: KeyboardEvent) => { + keyDownHandlerRef.current(event); + }; + + document.addEventListener("keydown", onDocumentKeyDown, true); return () => { - document.removeEventListener("keydown", handleKeyDown, true); + document.removeEventListener("keydown", onDocumentKeyDown, true); }; - }, [isOpen, handleKeyDown]); + }, [isOpen]); if (!isOpen) return null; diff --git a/apps/registry/registry/default/content-intro/content-intro.tsx b/apps/registry/registry/default/content-intro/content-intro.tsx index 0996a51b..93bb2357 100644 --- a/apps/registry/registry/default/content-intro/content-intro.tsx +++ b/apps/registry/registry/default/content-intro/content-intro.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useEffect } from "react"; +import { memo, useEffect, useRef } from "react"; import type { ReactNode } from "react"; @@ -64,23 +64,25 @@ function ContentIntroImpl({ }: ContentIntroProps): React.ReactNode { const mergedLabels = { ...DEFAULT_LABELS, ...labels }; const hasProgress = completedSections.size > 0; + const onStartRef = useRef(onStart); - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { + useEffect(() => { + onStartRef.current = onStart; + }, [onStart]); + + useEffect(() => { + const onDocumentKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); - onStart(); + onStartRef.current(); } - }, - [onStart], - ); + }; - useEffect(() => { - document.addEventListener("keydown", handleKeyDown); + document.addEventListener("keydown", onDocumentKeyDown); return () => { - document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("keydown", onDocumentKeyDown); }; - }, [handleKeyDown]); + }, []); return ( <> diff --git a/apps/registry/registry/default/filter-bar/filter-bar.tsx b/apps/registry/registry/default/filter-bar/filter-bar.tsx index 13db2565..2ca1447c 100644 --- a/apps/registry/registry/default/filter-bar/filter-bar.tsx +++ b/apps/registry/registry/default/filter-bar/filter-bar.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useState } from "react"; +import { memo, useCallback, useTransition } from "react"; import { cn } from "@vllnt/ui"; import { Badge } from "@vllnt/ui"; @@ -263,16 +263,16 @@ function FilterBarImpl({ searchQuery, tags, }: FilterBarProps): React.ReactNode { - const [isPending, setIsPending] = useState(false); + const [isPending, startTransition] = useTransition(); const mergedLabels = { ...DEFAULT_LABELS, ...labels }; const handleDifficultyChange = useCallback( (difficulty: string): void => { - setIsPending(true); - onFiltersChange({ difficulty }); - setIsPending(false); + startTransition(() => { + onFiltersChange({ difficulty }); + }); }, - [onFiltersChange], + [onFiltersChange, startTransition], ); const handleSearchChange = useCallback( diff --git a/apps/registry/registry/default/gantt-chart/gantt-chart.tsx b/apps/registry/registry/default/gantt-chart/gantt-chart.tsx index 11dc49f7..de9ba4b8 100644 --- a/apps/registry/registry/default/gantt-chart/gantt-chart.tsx +++ b/apps/registry/registry/default/gantt-chart/gantt-chart.tsx @@ -181,7 +181,7 @@ function getTickDateTimeFormatter( scale === "month" ? { month: "short", year: "numeric" } : { day: "2-digit", month: "short" }; - formatter = new Intl.DateTimeFormat(locale, options); + formatter = Intl.DateTimeFormat(locale, options); TICK_FORMATTER_CACHE.set(key, formatter); } return formatter; @@ -240,16 +240,16 @@ function buildTicks(input: TicksInput): { label: string; offset: number }[] { const formatter = buildTickFormatter(scale, locale); const stepDays = getTickStep(scale); const tickCount = Math.floor(totalDays / stepDays); - return Array.from({ length: tickCount + 1 }) - .map((_, index) => { - const day = index * stepDays; - return { - date: new Date(start.getTime() + day * MS_PER_DAY), - offset: day, - }; - }) - .filter((tick) => tick.date.getTime() <= end.getTime()) - .map((tick) => ({ label: formatter(tick.date), offset: tick.offset })); + return Array.from({ length: tickCount + 1 }).reduce< + { label: string; offset: number }[] + >((ticks, _, index) => { + const day = index * stepDays; + const date = new Date(start.getTime() + day * MS_PER_DAY); + if (date.getTime() <= end.getTime()) { + ticks.push({ label: formatter(date), offset: day }); + } + return ticks; + }, []); } type GeometryOptions = { diff --git a/apps/registry/registry/default/globe-3d/globe-3d.tsx b/apps/registry/registry/default/globe-3d/globe-3d.tsx index 4d2ce555..7982b5c0 100644 --- a/apps/registry/registry/default/globe-3d/globe-3d.tsx +++ b/apps/registry/registry/default/globe-3d/globe-3d.tsx @@ -280,20 +280,22 @@ function buildLine(arguments_: { rotationLng: number; }): string { const { points, rotationLat, rotationLng } = arguments_; - return points - .map((coord) => project(coord, rotationLng, rotationLat)) - .reduce<{ path: string; pen: "down" | "up" }>( - (state, projected) => { - if (!projected.visible) return { path: state.path, pen: "up" }; - const head = state.pen === "up" ? "M" : "L"; - const separator = state.path.length > 0 ? " " : ""; - return { - path: `${state.path}${separator}${head}${projected.x.toString()},${projected.y.toString()}`, - pen: "down", - }; - }, - { path: "", pen: "up" }, - ).path; + return points.reduce<{ path: string; pen: "down" | "up" }>( + (state, coord) => { + const projected = project(coord, rotationLng, rotationLat); + if (!projected.visible) { + return { path: state.path, pen: "up" }; + } + + const head = state.pen === "up" ? "M" : "L"; + const separator = state.path.length > 0 ? " " : ""; + return { + path: `${state.path}${separator}${head}${projected.x.toString()},${projected.y.toString()}`, + pen: "down", + }; + }, + { path: "", pen: "up" }, + ).path; } function range(start: number, end: number, step: number): number[] { @@ -308,9 +310,16 @@ function Graticule({ rotationLat, rotationLng }: GraticuleProps): ReactNode { const meridians = range(-150, 180, 30).map((lng) => range(-85, 85, 5).map((lat) => ({ lat, lng })), ); - const lines = [...parallels, ...meridians] - .map((points) => buildLine({ points, rotationLat, rotationLng })) - .filter((path) => path.length > 0); + const lines = [...parallels, ...meridians].reduce( + (paths, points) => { + const path = buildLine({ points, rotationLat, rotationLng }); + if (path.length > 0) { + paths.push(path); + } + return paths; + }, + [], + ); return ( - new Set( - categories - .filter((category) => !hidden.has(category.id)) - .map((category) => category.id), - ), + categories.reduce>((visible, category) => { + if (!hidden.has(category.id)) { + visible.add(category.id); + } + return visible; + }, new Set()), [categories, hidden], ); diff --git a/apps/registry/registry/default/live-feed/live-feed.tsx b/apps/registry/registry/default/live-feed/live-feed.tsx index 8f93fd7a..21dac9d7 100644 --- a/apps/registry/registry/default/live-feed/live-feed.tsx +++ b/apps/registry/registry/default/live-feed/live-feed.tsx @@ -116,7 +116,7 @@ function formatAbsolute(eventDate: Date): string { } function sortEventsDesc(events: LiveFeedEvent[]): LiveFeedEvent[] { - return [...events].sort( + return events.toSorted( (a, b) => normalizeDate(b.timestamp).getTime() - normalizeDate(a.timestamp).getTime(), diff --git a/apps/registry/registry/default/map-2d/map-2d.tsx b/apps/registry/registry/default/map-2d/map-2d.tsx index 820573b9..afb323e5 100644 --- a/apps/registry/registry/default/map-2d/map-2d.tsx +++ b/apps/registry/registry/default/map-2d/map-2d.tsx @@ -122,7 +122,7 @@ function useMapState(arguments_: { }; }, [center]); const [pan, setPan] = useState<{ x: number; y: number }>(initialPan); - const [zoom, setZoom] = useState( + const [zoom, setZoom] = useState(() => clamp(initialZoom, MIN_ZOOM, MAX_ZOOM), ); diff --git a/apps/registry/registry/default/map-timeline/map-timeline.tsx b/apps/registry/registry/default/map-timeline/map-timeline.tsx index d42f8e3d..b0f8b699 100644 --- a/apps/registry/registry/default/map-timeline/map-timeline.tsx +++ b/apps/registry/registry/default/map-timeline/map-timeline.tsx @@ -506,7 +506,7 @@ function useTimelineState(arguments_: { year: number; } { const { endYear, initialYear, onYearChange, startYear } = arguments_; - const [year, setYear] = useState( + const [year, setYear] = useState(() => clamp(initialYear ?? startYear, startYear, endYear), ); const [isPlaying, setIsPlaying] = useState(false); diff --git a/apps/registry/registry/default/number-ticker/number-ticker.tsx b/apps/registry/registry/default/number-ticker/number-ticker.tsx index 6deaaf85..e4f043dd 100644 --- a/apps/registry/registry/default/number-ticker/number-ticker.tsx +++ b/apps/registry/registry/default/number-ticker/number-ticker.tsx @@ -21,7 +21,7 @@ function getNumberTickerFormatter( const key = `${locale ?? ""}|${formatOptions ? JSON.stringify(formatOptions) : ""}`; let formatter = NUMBER_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, formatOptions); + formatter = Intl.NumberFormat(locale, formatOptions); NUMBER_FORMATTER_CACHE.set(key, formatter); } return formatter; diff --git a/apps/registry/registry/default/search-dialog/search-dialog.tsx b/apps/registry/registry/default/search-dialog/search-dialog.tsx index b261593a..12334d4e 100644 --- a/apps/registry/registry/default/search-dialog/search-dialog.tsx +++ b/apps/registry/registry/default/search-dialog/search-dialog.tsx @@ -76,7 +76,7 @@ export function SearchDialog({ }: SearchDialogProps) { const [open, setOpen] = useState(false); - const sortedItems = [...items].sort((a, b) => a.title.localeCompare(b.title)); + const sortedItems = items.toSorted((a, b) => a.title.localeCompare(b.title)); useKeyboardShortcut(() => { if (enableKeyboardShortcut) { diff --git a/apps/registry/registry/default/sidebar/sidebar.tsx b/apps/registry/registry/default/sidebar/sidebar.tsx index 915e1f65..3a19f139 100644 --- a/apps/registry/registry/default/sidebar/sidebar.tsx +++ b/apps/registry/registry/default/sidebar/sidebar.tsx @@ -66,7 +66,7 @@ function useScrollFade( }; checkScroll(); - container.addEventListener("scroll", checkScroll); + container.addEventListener("scroll", checkScroll, { passive: true }); return () => { container.removeEventListener("scroll", checkScroll); }; diff --git a/apps/registry/registry/default/status-board/status-board.tsx b/apps/registry/registry/default/status-board/status-board.tsx index 38021ecb..566d91f8 100644 --- a/apps/registry/registry/default/status-board/status-board.tsx +++ b/apps/registry/registry/default/status-board/status-board.tsx @@ -107,10 +107,16 @@ function getSummary(items: StatusBoardItem[]) { }, ); - return STATUS_ORDER.map((status) => ({ - count: counts[status], - status, - })).filter((entry) => entry.count > 0); + return STATUS_ORDER.reduce<{ count: number; status: StatusBoardStatus }[]>( + (summary, status) => { + const count = counts[status]; + if (count > 0) { + summary.push({ count, status }); + } + return summary; + }, + [], + ); } function StatusBoardSummary({ items }: { items: StatusBoardItem[] }) { diff --git a/apps/registry/registry/default/tags-input/tags-input.tsx b/apps/registry/registry/default/tags-input/tags-input.tsx index 351152fb..013a0923 100644 --- a/apps/registry/registry/default/tags-input/tags-input.tsx +++ b/apps/registry/registry/default/tags-input/tags-input.tsx @@ -11,11 +11,18 @@ function normalizeTag(tag: string) { } function getNormalizedTags(tags: string[]) { - return tags - .map(normalizeTag) - .filter( - (tag, index, values) => tag.length > 0 && values.indexOf(tag) === index, - ); + return tags.reduce<{ seen: Set; tags: string[] }>( + (state, tag) => { + const normalizedTag = normalizeTag(tag); + if (normalizedTag.length > 0 && !state.seen.has(normalizedTag)) { + state.seen.add(normalizedTag); + state.tags.push(normalizedTag); + } + + return state; + }, + { seen: new Set(), tags: [] }, + ).tags; } function shouldAddTagFromKey(key: string) { diff --git a/apps/registry/registry/default/terminal/terminal.tsx b/apps/registry/registry/default/terminal/terminal.tsx index e7d899b2..91413e58 100644 --- a/apps/registry/registry/default/terminal/terminal.tsx +++ b/apps/registry/registry/default/terminal/terminal.tsx @@ -17,6 +17,15 @@ export type TerminalProps = { title?: string; }; +function getCommandContents(lines: TerminalLine[]): string[] { + return lines.reduce((commands, line) => { + if (line.type === "command") { + commands.push(line.content); + } + return commands; + }, []); +} + export function Terminal({ copyable = true, lines, @@ -24,9 +33,7 @@ export function Terminal({ }: TerminalProps) { const [copied, setCopied] = useState(false); - const commands = lines - .filter((l) => l.type === "command") - .map((l) => l.content); + const commands = getCommandContents(lines); const handleCopy = async () => { await navigator.clipboard.writeText(commands.join("\n")); @@ -114,9 +121,7 @@ export function SimpleTerminal({ return { content: line, type: "output" }; }); - const commands = lines - .filter((l) => l.type === "command") - .map((l) => l.content); + const commands = getCommandContents(lines); const handleCopy = async () => { await navigator.clipboard.writeText(commands.join("\n")); diff --git a/apps/registry/registry/default/tldr-section/tldr-section.tsx b/apps/registry/registry/default/tldr-section/tldr-section.tsx index 5dfc0e37..148692b6 100644 --- a/apps/registry/registry/default/tldr-section/tldr-section.tsx +++ b/apps/registry/registry/default/tldr-section/tldr-section.tsx @@ -9,25 +9,25 @@ type TLDRSectionProps = { export function TLDRSection({ children, label }: TLDRSectionProps) { const [isExpanded, setIsExpanded] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [hasBeenOpened, setHasBeenOpened] = useState(false); + const [showSkeleton, setShowSkeleton] = useState(false); + const hasBeenOpenedRef = useRef(false); const timerReference = useRef(null); useEffect(() => { - if (isExpanded && !hasBeenOpened) { + if (isExpanded && !hasBeenOpenedRef.current) { // Clear any existing timer if (timerReference.current) { clearTimeout(timerReference.current); } const rafId = requestAnimationFrame(() => { - setIsLoading(true); - setHasBeenOpened(true); + setShowSkeleton(true); + hasBeenOpenedRef.current = true; }); // Simulate loading with skeleton timerReference.current = setTimeout(() => { - setIsLoading(false); + setShowSkeleton(false); timerReference.current = null; }, 800); @@ -37,6 +37,7 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { clearTimeout(timerReference.current); timerReference.current = null; } + setShowSkeleton(false); }; } @@ -45,8 +46,9 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { clearTimeout(timerReference.current); timerReference.current = null; } + setShowSkeleton(false); }; - }, [isExpanded]); // eslint-disable-line react-hooks/exhaustive-deps + }, [isExpanded]); return (
@@ -91,7 +93,7 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { {isExpanded ? (
- {isLoading ? ( + {showSkeleton ? (
diff --git a/apps/registry/registry/default/transaction-list/transaction-list.tsx b/apps/registry/registry/default/transaction-list/transaction-list.tsx index 4bb2c209..36ed935f 100644 --- a/apps/registry/registry/default/transaction-list/transaction-list.tsx +++ b/apps/registry/registry/default/transaction-list/transaction-list.tsx @@ -19,7 +19,7 @@ function getCurrencyFormatter( const key = `${locale}|${currency}`; let formatter = CURRENCY_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, { + formatter = Intl.NumberFormat(locale, { currency, style: "currency", }); @@ -32,7 +32,7 @@ const DATE_FORMATTER_CACHE = new Map(); function getTransactionDateFormatter(locale: string): Intl.DateTimeFormat { let formatter = DATE_FORMATTER_CACHE.get(locale); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric", diff --git a/apps/registry/registry/default/usage-breakdown/usage-breakdown.tsx b/apps/registry/registry/default/usage-breakdown/usage-breakdown.tsx index c4870af2..137d4973 100644 --- a/apps/registry/registry/default/usage-breakdown/usage-breakdown.tsx +++ b/apps/registry/registry/default/usage-breakdown/usage-breakdown.tsx @@ -174,9 +174,7 @@ function UsageBreakdownRow({ } function getSortedItems(items: UsageBreakdownItem[], maxItems?: number) { - const rankedItems = [...items].sort( - (left, right) => right.value - left.value, - ); + const rankedItems = items.toSorted((left, right) => right.value - left.value); return typeof maxItems === "number" ? rankedItems.slice(0, maxItems) : rankedItems; diff --git a/apps/registry/registry/default/world-clock-bar/world-clock-bar.tsx b/apps/registry/registry/default/world-clock-bar/world-clock-bar.tsx index 6ff3039b..fd45edd5 100644 --- a/apps/registry/registry/default/world-clock-bar/world-clock-bar.tsx +++ b/apps/registry/registry/default/world-clock-bar/world-clock-bar.tsx @@ -60,7 +60,7 @@ function getTimeFormatter( const key = `${locale}|${timeZone}`; let formatter = TIME_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone, @@ -79,7 +79,7 @@ function getDateFormatter( const key = `${locale}|${timeZone}`; let formatter = DATE_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { day: "numeric", month: "short", timeZone, diff --git a/apps/registry/scripts/generate-component-metadata.ts b/apps/registry/scripts/generate-component-metadata.ts index 7bc1fc85..9714fe3b 100644 --- a/apps/registry/scripts/generate-component-metadata.ts +++ b/apps/registry/scripts/generate-component-metadata.ts @@ -77,11 +77,14 @@ function slugifyTitleSegment(segment: string): string { } function titleToStoryIdPrefix(title: string): string { - return title - .split("/") - .map((segment) => slugifyTitleSegment(segment)) - .filter(Boolean) - .join("-"); + const segments: string[] = []; + for (const segment of title.split("/")) { + const slug = slugifyTitleSegment(segment); + if (slug) { + segments.push(slug); + } + } + return segments.join("-"); } function exportNameToSlug(exportName: string): string { diff --git a/packages/ui/scripts/check-use-client.ts b/packages/ui/scripts/check-use-client.ts index ae741431..a199f5de 100644 --- a/packages/ui/scripts/check-use-client.ts +++ b/packages/ui/scripts/check-use-client.ts @@ -24,6 +24,8 @@ const REACT_HOOK_PATTERN = /\b(?:[A-Za-z_$][A-Za-z0-9_$]*\s*\.\s*)?use[A-Z][A-Za-z0-9_]*(?:\s*<[^\n]*>)?\s*\(/ const USE_CLIENT_PATTERN = /^['"]use client['"];?$/ +const ARROW_TOKEN_PATTERN = /=>/ +const BLOCK_COMMENT_END_PATTERN = /\*\// const EXCLUDED_SUFFIXES = [ '.stories.tsx', @@ -170,8 +172,10 @@ export function collapseMultilineGenericHookCalls(source: string): string { const gap = source.slice(cursor, suffixCursor) result += source.slice(lastIndex, start) - if (inner.includes('\n') || gap.includes('\n')) { - const newlines = (inner.match(/\n/g)?.length ?? 0) + (gap.match(/\n/g)?.length ?? 0) + const innerNewlines = inner.match(/\n/g)?.length ?? 0 + const gapNewlines = gap.match(/\n/g)?.length ?? 0 + if (innerNewlines > 0 || gapNewlines > 0) { + const newlines = innerNewlines + gapNewlines result += source.slice(start, typeArgsStart) + inner.replace(/\n/g, ' ') + @@ -219,7 +223,7 @@ export function fileUsesHooks(source: string): boolean { pendingHookAssignment = false if (HOOK_DEF_CONTINUATION_PATTERN.test(trimmed)) { pendingHookDef = true - pendingArrowHookDef = trimmed.includes('=>') + pendingArrowHookDef = ARROW_TOKEN_PATTERN.test(trimmed) } } @@ -249,7 +253,7 @@ export function fileUsesHooks(source: string): boolean { if (HOOK_DEF_LINE_PATTERN.test(line)) { const inlineBody = ARROW_INLINE_BODY_PATTERN.test(line) - const isArrowHookDef = line.includes('=>') + const isArrowHookDef = ARROW_TOKEN_PATTERN.test(line) const balancedBraces = opens > 0 && opens === closes if (opens > closes) { hookBodyDepth = depth @@ -289,7 +293,7 @@ export function hasUseClientDirective(source: string): boolean { if (trimmed.length === 0) continue if (inBlockComment) { - const blockEnd = trimmed.indexOf('*/') + const blockEnd = trimmed.search(BLOCK_COMMENT_END_PATTERN) if (blockEnd === -1) continue inBlockComment = false trimmed = trimmed.slice(blockEnd + 2).trim() @@ -299,7 +303,7 @@ export function hasUseClientDirective(source: string): boolean { if (trimmed.startsWith('//')) continue while (trimmed.startsWith('/*')) { - const blockEnd = trimmed.indexOf('*/') + const blockEnd = trimmed.search(BLOCK_COMMENT_END_PATTERN) if (blockEnd === -1) { inBlockComment = true trimmed = '' diff --git a/packages/ui/scripts/fix-stories.ts b/packages/ui/scripts/fix-stories.ts index e10afce1..5f78fe67 100644 --- a/packages/ui/scripts/fix-stories.ts +++ b/packages/ui/scripts/fix-stories.ts @@ -14,6 +14,12 @@ import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const COMPONENTS_DIR = join(__dirname, '../src/components') +const REQUIRED_PROP_NAMES = new Set(['id', 'label', 'title', 'name']) +const IGNORED_REQUIRED_PROP_NAMES = new Set(['className', 'ref', 'key', 'style', 'children']) +const DELIMITER_OPEN = new Set(['{', '[', '(']) +const DELIMITER_CLOSE = new Set(['}', ']', ')']) +const NON_IMPLEMENTATION_FILE_PATTERN = /\.(?:stories|test|visual)\./ +const CHILDREN_REFERENCE_PATTERN = /\{children\}/ interface PropInfo { name: string @@ -153,7 +159,7 @@ function generateObjectValue(typeDef: TypeDef, allTypes: TypeDef[], indent: numb const fields: string[] = [] for (const field of typeDef.fields) { - if (!field.required && field.name !== 'id' && field.name !== 'label' && field.name !== 'title' && field.name !== 'name') continue + if (!field.required && !REQUIRED_PROP_NAMES.has(field.name)) continue const val = generateValue(field, allTypes, indent + 2) fields.push(`${innerPad}${field.name}: ${val},`) } @@ -180,7 +186,7 @@ function extractRequiredProps(source: string, componentName: string): PropInfo[] const block = extractTypeBlock(source, match.index + match[0].length - 1) if (block) { return parsePropsFromBlock(block).filter( - (p) => p.required && !['className', 'ref', 'key', 'style', 'children'].includes(p.name) + (p) => p.required && !IGNORED_REQUIRED_PROP_NAMES.has(p.name) ) } } @@ -207,8 +213,8 @@ function extractStoryArgs(storySource: string): Set { let innerDepth = 0 let pos = 0 while (pos < block.length) { - if (block[pos] === '{' || block[pos] === '[' || block[pos] === '(') { innerDepth++; pos++; continue } - if (block[pos] === '}' || block[pos] === ']' || block[pos] === ')') { innerDepth--; pos++; continue } + if (DELIMITER_OPEN.has(block[pos] ?? '')) { innerDepth++; pos++; continue } + if (DELIMITER_CLOSE.has(block[pos] ?? '')) { innerDepth--; pos++; continue } if (innerDepth === 0) { const m = block.slice(pos).match(/^(\w+)\s*:/) if (m?.[1]) { @@ -266,11 +272,25 @@ function main(): void { for (const dir of componentDirs) { const dirPath = join(COMPONENTS_DIR, dir) const name = toPascalCase(dir) - const files = readdirSync(dirPath) - const mainFile = files.find((f) => f === `${dir}.tsx`) ?? files.find((f) => - f.endsWith('.tsx') && !f.includes('.test.') && !f.includes('.visual.') && !f.includes('.stories.') - ) - const storyFileName = files.find((f) => f.endsWith('.stories.tsx')) + let fallbackMainFile: string | undefined + let mainFile: string | undefined + let storyFileName: string | undefined + for (const file of readdirSync(dirPath)) { + if (file === `${dir}.tsx`) { + mainFile = file + } else if ( + !fallbackMainFile && + file.endsWith('.tsx') && + !NON_IMPLEMENTATION_FILE_PATTERN.test(file) + ) { + fallbackMainFile = file + } + + if (!storyFileName && file.endsWith('.stories.tsx')) { + storyFileName = file + } + } + mainFile ??= fallbackMainFile if (!mainFile || !storyFileName) continue const source = readFileSync(join(dirPath, mainFile), 'utf-8') @@ -303,11 +323,11 @@ function main(): void { const patched = patchStory(storySource, argsToAdd) - let cleanedArgs = storySource.match(/args\s*:\s*\{[^}]*children\s*:\s*"[^"]*"[^}]*\}/s) + const cleanedArgs = storySource.match(/args\s*:\s*\{[^}]*children\s*:\s*"[^"]*"[^}]*\}/s) let finalSource = patched if (cleanedArgs) { const childrenPropInComponent = requiredProps.some((p) => p.name === 'children') || - source.includes('{children}') + CHILDREN_REFERENCE_PATTERN.test(source) if (!childrenPropInComponent) { finalSource = finalSource.replace(/\s*children\s*:\s*"[^"]*",?\n?/g, '\n') } diff --git a/packages/ui/scripts/generate-docs.ts b/packages/ui/scripts/generate-docs.ts index ee861487..ca13ec6e 100644 --- a/packages/ui/scripts/generate-docs.ts +++ b/packages/ui/scripts/generate-docs.ts @@ -23,6 +23,9 @@ const __dirname = dirname(__filename) const COMPONENTS_DIR = join(__dirname, '../src/components') const REGISTRY_PATH = join(__dirname, '../../../apps/registry/registry.json') +const REACT_NODE_IGNORED_PROP_NAMES = new Set(['children', 'className', 'ref', 'key', 'style']) +const NON_TRIVIAL_PROP_NAMES = new Set(['className', 'children', 'ref', 'key', 'style']) +const NON_TRIVIAL_NO_CHILDREN_PROP_NAMES = new Set(['className', 'ref', 'key', 'style']) interface RegistryItem { name: string @@ -262,7 +265,7 @@ function parsePropsFromBlock(block: string): PropInfo[] { let type = (propMatch[3] ?? '').trim().replace(/;$/, '').replace(/,$/, '') if (!name || !type) continue - if (['children', 'className', 'ref', 'key', 'style'].includes(name) && type === 'ReactNode') continue + if (REACT_NODE_IGNORED_PROP_NAMES.has(name) && type === 'ReactNode') continue props.push({ name, type, required, description: '' }) } @@ -327,13 +330,14 @@ function extractExports(source: string): string[] { const namedExports = source.matchAll(/export\s*\{\s*([^}]+)\s*\}/g) for (const match of namedExports) { if (!match[1]) continue - const names = match[1] - .split(',') - .map((n) => n.trim()) - .filter((n) => !n.startsWith('type ')) - .map((n) => n.split(/\s+as\s+/)[0]?.trim() ?? '') - .filter((n) => n.length > 0 && /^[A-Z]/.test(n)) - exports.push(...names) + for (const rawName of match[1].split(',')) { + const trimmed = rawName.trim() + if (trimmed.startsWith('type ')) continue + const name = trimmed.split(/\s+as\s+/)[0]?.trim() ?? '' + if (name.length > 0 && /^[A-Z]/.test(name)) { + exports.push(name) + } + } } const constExports = source.matchAll(/export\s+(?:const|function)\s+([A-Z]\w+)/g) @@ -360,6 +364,10 @@ function extractStoryExports(storySource: string): string[] { function extractSubComponents(source: string, mainName: string, allTypes: TypeDefinition[]): SubComponentInfo[] { const subComponents: SubComponentInfo[] = [] const attachedImplNames = new Set() + const typesByName = new Map() + for (const typeDef of allTypes) { + typesByName.set(typeDef.name, typeDef) + } const attachRegex = new RegExp(`${mainName}\\.(\\w+)\\s*=\\s*(\\w+)`, 'g') let match @@ -368,7 +376,7 @@ function extractSubComponents(source: string, mainName: string, allTypes: TypeDe if (!implName) continue attachedImplNames.add(implName) const propsTypeName = `${implName}Props` - const typeDef = allTypes.find((t) => t.name === propsTypeName) + const typeDef = typesByName.get(propsTypeName) subComponents.push({ name: `${mainName}.${match[1]}`, props: typeDef?.fields ?? [], @@ -384,7 +392,7 @@ function extractSubComponents(source: string, mainName: string, allTypes: TypeDe if (name.startsWith(mainName)) { const propsTypeName = `${name}Props` - const typeDef = allTypes.find((t) => t.name === propsTypeName) + const typeDef = typesByName.get(propsTypeName) if (typeDef && typeDef.fields.length > 0) { subComponents.push({ name, @@ -437,7 +445,7 @@ function generateUsageExample(component: ComponentMeta): string { lines.push(`<${name}`) const nonTrivialProps = props.filter( - (p) => p.required && !['className', 'children', 'ref', 'key', 'style'].includes(p.name) + (p) => p.required && !NON_TRIVIAL_PROP_NAMES.has(p.name) ) for (const prop of nonTrivialProps) { @@ -492,7 +500,7 @@ function generateCompoundExample(component: ComponentMeta): string { const lines: string[] = [] const mainProps = props.filter( - (p) => p.required && !['children', 'className', 'ref', 'key', 'style'].includes(p.name) + (p) => p.required && !NON_TRIVIAL_PROP_NAMES.has(p.name) ) const mainPropsStr = mainProps.map((p) => { const kind = classifyType(p.type) @@ -505,18 +513,24 @@ function generateCompoundExample(component: ComponentMeta): string { lines.push(`<${name}${mainPropsStr ? ` ${mainPropsStr}` : ''}>`) - const hasDotSubs = subComponents.some((sc) => sc.name.includes('.')) - const subs = hasDotSubs - ? subComponents.filter((sc) => sc.name.includes('.')) - : subComponents + const dotSubs: SubComponentInfo[] = [] + for (const sub of subComponents) { + if (sub.name.split('.').length > 1) { + dotSubs.push(sub) + } + } + const subs = dotSubs.length > 0 ? dotSubs : subComponents for (const sub of subs) { - const hasValue = sub.props.some((p) => p.name === 'value') - const hasChildren = sub.props.some((p) => p.name === 'children') - const hasQuestion = sub.props.some((p) => p.name === 'question') + let hasChildren = false + let hasQuestion = false + for (const prop of sub.props) { + if (prop.name === 'children') hasChildren = true + if (prop.name === 'question') hasQuestion = true + } const requiredProps = sub.props.filter( - (p) => p.required && !['children', 'className', 'ref', 'key', 'style'].includes(p.name) + (p) => p.required && !NON_TRIVIAL_PROP_NAMES.has(p.name) ) const propsStr = requiredProps.map((p) => { if (p.name === 'value') return `value="item-1"` @@ -562,7 +576,7 @@ function generateArrayPropsExample(component: ComponentMeta): string { for (const prop of props) { if (prop.name === arrayProp.name) { lines.push(` ${prop.name}={${arrayProp.name}}`) - } else if (prop.required && prop.name !== 'className' && prop.name !== 'children') { + } else if (prop.required && !NON_TRIVIAL_PROP_NAMES.has(prop.name)) { const kind = classifyType(prop.type) if (kind === 'primitive') { lines.push(` ${prop.name}="${exampleValue(prop)}"`) @@ -658,7 +672,8 @@ function analyzeComponent(dirPath: string, registryItems: RegistryItem[]): Compo const propsTypeNames = new Set([`${name}Props`]) for (const sc of subComponents) { - const scName = sc.name.includes('.') ? sc.name.split('.').pop() ?? '' : sc.name + const parts = sc.name.split('.') + const scName = parts.length > 1 ? parts[parts.length - 1] ?? '' : sc.name propsTypeNames.add(`${scName}Props`) } const attachRegex2 = new RegExp(`${name}\\.(\\w+)\\s*=\\s*(\\w+)`, 'g') @@ -763,10 +778,13 @@ function generateMdx(component: ComponentMeta): string { lines.push('') lines.push('```tsx') const allExports = [...exports] + const allExportNames = new Set(allExports) for (const sub of subComponents) { - const subExportName = sub.name.includes('.') ? '' : sub.name - if (subExportName && !allExports.includes(subExportName)) { + const parts = sub.name.split('.') + const subExportName = parts.length > 1 ? '' : sub.name + if (subExportName && !allExportNames.has(subExportName)) { allExports.push(subExportName) + allExportNames.add(subExportName) } } if (allExports.length > 3) { @@ -809,7 +827,7 @@ function generateMdx(component: ComponentMeta): string { lines.push('') const nonTrivialProps = props.filter( - (p) => !['className', 'ref', 'key', 'style'].includes(p.name) + (p) => !NON_TRIVIAL_NO_CHILDREN_PROP_NAMES.has(p.name) ) if (nonTrivialProps.length > 0) { @@ -833,7 +851,7 @@ function generateMdx(component: ComponentMeta): string { lines.push('') if (sub.props.length > 0) { const nonTrivialProps = sub.props.filter( - (p) => !['className', 'ref', 'key', 'style'].includes(p.name) + (p) => !NON_TRIVIAL_NO_CHILDREN_PROP_NAMES.has(p.name) ) if (nonTrivialProps.length > 0) { lines.push(`| Prop | Type | Required | Description |`) diff --git a/packages/ui/scripts/generate-tests.ts b/packages/ui/scripts/generate-tests.ts index ab97ae67..2d78a719 100644 --- a/packages/ui/scripts/generate-tests.ts +++ b/packages/ui/scripts/generate-tests.ts @@ -15,6 +15,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const COMPONENTS_DIR = join(__dirname, '../src/components') +const MANUALLY_MAINTAINED_TESTS = new Set(['cookie-consent']) interface VariantInfo { name: string @@ -427,8 +428,7 @@ async function main(): Promise { const visualPath = join(dirPath, `${component.fileName}.visual.tsx`) // Skip manually crafted tests (CookieConsent) even with --force - const manuallyMaintained = ['cookie-consent'] - if (manuallyMaintained.includes(component.dirName)) { + if (MANUALLY_MAINTAINED_TESTS.has(component.dirName)) { console.log(`⏭️ ${component.name}: Manually maintained test`) skipped++ continue diff --git a/packages/ui/scripts/verify-stories.ts b/packages/ui/scripts/verify-stories.ts index 763cfa9f..620d2141 100644 --- a/packages/ui/scripts/verify-stories.ts +++ b/packages/ui/scripts/verify-stories.ts @@ -14,6 +14,10 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const COMPONENTS_DIR = join(__dirname, "../src/components"); +const IGNORED_REQUIRED_PROP_NAMES = new Set(["className", "key", "ref", "style"]); +const DELIMITER_OPEN = new Set(["{", "[", "("]); +const DELIMITER_CLOSE = new Set(["}", "]", ")"]); +const NON_IMPLEMENTATION_FILE_PATTERN = /\.(?:stories|test|visual)\./; interface PropInfo { name: string; @@ -99,7 +103,7 @@ function extractRequiredProps(source: string, componentName: string): PropInfo[] const block = extractNamedPropsBlock(source, candidateName); if (block) { return parsePropsFromBlock(block).filter( - (prop) => !["className", "key", "ref", "style"].includes(prop.name) && prop.required, + (prop) => !IGNORED_REQUIRED_PROP_NAMES.has(prop.name) && prop.required, ); } } @@ -109,7 +113,7 @@ function extractRequiredProps(source: string, componentName: string): PropInfo[] const block = extractNamedPropsBlock(source, fallbackMatch[1]); if (block) { return parsePropsFromBlock(block).filter( - (prop) => !["className", "key", "ref", "style"].includes(prop.name) && prop.required, + (prop) => !IGNORED_REQUIRED_PROP_NAMES.has(prop.name) && prop.required, ); } } @@ -142,13 +146,13 @@ function extractTopLevelKeys(block: string): Set { let index = 0; while (index < block.length) { - if (["{", "[", "("].includes(block[index] ?? "")) { + if (DELIMITER_OPEN.has(block[index] ?? "")) { depth++; index++; continue; } - if (["}", "]", ")"].includes(block[index] ?? "")) { + if (DELIMITER_CLOSE.has(block[index] ?? "")) { depth--; index++; continue; @@ -219,17 +223,27 @@ function verify(): void { for (const dir of componentDirs) { const dirPath = join(COMPONENTS_DIR, dir); const componentName = toPascalCase(dir); - const files = readdirSync(dirPath); - const mainFile = - files.find((file) => file === `${dir}.tsx`) ?? - files.find( - (file) => - file.endsWith(".tsx") && - !file.includes(".stories.") && - !file.includes(".test.") && - !file.includes(".visual."), - ); - const storyFile = files.find((file) => file.endsWith(".stories.tsx")); + let fallbackMainFile: string | undefined; + let mainFile: string | undefined; + let storyFile: string | undefined; + + for (const file of readdirSync(dirPath)) { + if (file === `${dir}.tsx`) { + mainFile = file; + } else if ( + !fallbackMainFile && + file.endsWith(".tsx") && + !NON_IMPLEMENTATION_FILE_PATTERN.test(file) + ) { + fallbackMainFile = file; + } + + if (!storyFile && file.endsWith(".stories.tsx")) { + storyFile = file; + } + } + + mainFile ??= fallbackMainFile; if (!mainFile || !storyFile) continue; checked++; diff --git a/packages/ui/src/components/auto-reload/auto-reload.tsx b/packages/ui/src/components/auto-reload/auto-reload.tsx index 80045ce8..1a2cfb4c 100644 --- a/packages/ui/src/components/auto-reload/auto-reload.tsx +++ b/packages/ui/src/components/auto-reload/auto-reload.tsx @@ -131,7 +131,7 @@ function getCurrencyFormatter( const key = `${locale}|${currency}`; let formatter = CURRENCY_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, { + formatter = Intl.NumberFormat(locale, { currency, style: "currency", }); diff --git a/packages/ui/src/components/carousel/carousel.tsx b/packages/ui/src/components/carousel/carousel.tsx index 65b41711..931cfac6 100644 --- a/packages/ui/src/components/carousel/carousel.tsx +++ b/packages/ui/src/components/carousel/carousel.tsx @@ -7,6 +7,7 @@ import { useContext, useEffect, useMemo, + useRef, useState, } from "react"; @@ -82,6 +83,11 @@ function useCarouselLogic({ setCanScrollPrevious(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }, []); + const onSelectReference = useRef(onSelect); + + useEffect(() => { + onSelectReference.current = onSelect; + }, [onSelect]); const scrollPrevious = useCallback(() => { api?.scrollPrev(); @@ -117,19 +123,23 @@ function useCarouselLogic({ return; } - api.on("reInit", onSelect); - api.on("select", onSelect); + const notifySelection = (selectedApi: CarouselApi) => { + onSelectReference.current(selectedApi); + }; + + api.on("reInit", notifySelection); + api.on("select", notifySelection); const rafId = requestAnimationFrame(() => { - onSelect(api); + notifySelection(api); }); return () => { - api?.off("select", onSelect); - api?.off("reInit", onSelect); + api?.off("select", notifySelection); + api?.off("reInit", notifySelection); cancelAnimationFrame(rafId); }; - }, [api, onSelect]); + }, [api]); return { api, diff --git a/packages/ui/src/components/client-directives.test.ts b/packages/ui/src/components/client-directives.test.ts index b31b59d7..5f5c01da 100644 --- a/packages/ui/src/components/client-directives.test.ts +++ b/packages/ui/src/components/client-directives.test.ts @@ -479,17 +479,22 @@ describe("hasUseClientDirective", () => { describe("client directives", () => { it("marks hook-based shipped components as client components", () => { - const missingDirectiveFiles = listTypeScriptFiles(COMPONENTS_ROOT) - .filter((filePath) => - SKIPPED_SUFFIXES.every((suffix) => !filePath.endsWith(suffix)), - ) - .filter((filePath) => { - const source = readFileSync(filePath, "utf8"); - return fileUsesHooks(source) && !hasUseClientDirective(source); - }) - .map((filePath) => - filePath.replace(`${COMPONENTS_ROOT}/`, "components/"), - ); + const missingDirectiveFiles = listTypeScriptFiles(COMPONENTS_ROOT).reduce< + string[] + >((missingFiles, filePath) => { + if (SKIPPED_SUFFIXES.some((suffix) => filePath.endsWith(suffix))) { + return missingFiles; + } + + const source = readFileSync(filePath, "utf8"); + if (fileUsesHooks(source) && !hasUseClientDirective(source)) { + missingFiles.push( + filePath.replace(`${COMPONENTS_ROOT}/`, "components/"), + ); + } + + return missingFiles; + }, []); expect(missingDirectiveFiles).toEqual([]); }); diff --git a/packages/ui/src/components/completion-dialog/completion-dialog.tsx b/packages/ui/src/components/completion-dialog/completion-dialog.tsx index 05c1a8c2..f463a0f3 100644 --- a/packages/ui/src/components/completion-dialog/completion-dialog.tsx +++ b/packages/ui/src/components/completion-dialog/completion-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useEffect, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import type { ReactNode } from "react"; @@ -125,8 +125,12 @@ function CompletionDialogImpl({ onConfirm, title, }: CompletionDialogProps): React.ReactNode { - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { + const keyDownHandlerRef = useRef<(event: KeyboardEvent) => void>(() => { + return; + }); + + useEffect(() => { + keyDownHandlerRef.current = (event: KeyboardEvent) => { if (!isOpen) return; if (event.key === "Escape") { event.preventDefault(); @@ -148,17 +152,20 @@ function CompletionDialogImpl({ event.stopPropagation(); onCancel(); } - }, - [isOpen, onClose, onConfirm, onCancel, confirmShortcut, cancelShortcut], - ); + }; + }, [cancelShortcut, confirmShortcut, isOpen, onCancel, onClose, onConfirm]); useEffect(() => { if (!isOpen) return; - document.addEventListener("keydown", handleKeyDown, true); + const onDocumentKeyDown = (event: KeyboardEvent) => { + keyDownHandlerRef.current(event); + }; + + document.addEventListener("keydown", onDocumentKeyDown, true); return () => { - document.removeEventListener("keydown", handleKeyDown, true); + document.removeEventListener("keydown", onDocumentKeyDown, true); }; - }, [isOpen, handleKeyDown]); + }, [isOpen]); if (!isOpen) return null; diff --git a/packages/ui/src/components/content-intro/content-intro.tsx b/packages/ui/src/components/content-intro/content-intro.tsx index df02931a..4cc928ab 100644 --- a/packages/ui/src/components/content-intro/content-intro.tsx +++ b/packages/ui/src/components/content-intro/content-intro.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useEffect } from "react"; +import { memo, useEffect, useRef } from "react"; import type { ReactNode } from "react"; @@ -64,23 +64,25 @@ function ContentIntroImpl({ }: ContentIntroProps): React.ReactNode { const mergedLabels = { ...DEFAULT_LABELS, ...labels }; const hasProgress = completedSections.size > 0; + const onStartRef = useRef(onStart); - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { + useEffect(() => { + onStartRef.current = onStart; + }, [onStart]); + + useEffect(() => { + const onDocumentKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); - onStart(); + onStartRef.current(); } - }, - [onStart], - ); + }; - useEffect(() => { - document.addEventListener("keydown", handleKeyDown); + document.addEventListener("keydown", onDocumentKeyDown); return () => { - document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("keydown", onDocumentKeyDown); }; - }, [handleKeyDown]); + }, []); return ( <> diff --git a/packages/ui/src/components/conversation-thread/conversation-thread.tsx b/packages/ui/src/components/conversation-thread/conversation-thread.tsx index 95b14a85..db44c197 100644 --- a/packages/ui/src/components/conversation-thread/conversation-thread.tsx +++ b/packages/ui/src/components/conversation-thread/conversation-thread.tsx @@ -521,14 +521,14 @@ export const ConversationLoading = forwardRef< role="status" > - +
); }); diff --git a/packages/ui/src/components/filter-bar/filter-bar.tsx b/packages/ui/src/components/filter-bar/filter-bar.tsx index 2e92fc97..92196423 100644 --- a/packages/ui/src/components/filter-bar/filter-bar.tsx +++ b/packages/ui/src/components/filter-bar/filter-bar.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useCallback, useState } from "react"; +import { memo, useCallback, useTransition } from "react"; import { cn } from "../../lib/utils"; import { Badge } from "../badge"; @@ -263,16 +263,16 @@ function FilterBarImpl({ searchQuery, tags, }: FilterBarProps): React.ReactNode { - const [isPending, setIsPending] = useState(false); + const [isPending, startTransition] = useTransition(); const mergedLabels = { ...DEFAULT_LABELS, ...labels }; const handleDifficultyChange = useCallback( (difficulty: string): void => { - setIsPending(true); - onFiltersChange({ difficulty }); - setIsPending(false); + startTransition(() => { + onFiltersChange({ difficulty }); + }); }, - [onFiltersChange], + [onFiltersChange, startTransition], ); const handleSearchChange = useCallback( diff --git a/packages/ui/src/components/gantt-chart/gantt-chart.tsx b/packages/ui/src/components/gantt-chart/gantt-chart.tsx index 28cffbe2..1336dfa5 100644 --- a/packages/ui/src/components/gantt-chart/gantt-chart.tsx +++ b/packages/ui/src/components/gantt-chart/gantt-chart.tsx @@ -181,7 +181,7 @@ function getTickDateTimeFormatter( scale === "month" ? { month: "short", year: "numeric" } : { day: "2-digit", month: "short" }; - formatter = new Intl.DateTimeFormat(locale, options); + formatter = Intl.DateTimeFormat(locale, options); TICK_FORMATTER_CACHE.set(key, formatter); } return formatter; @@ -240,16 +240,16 @@ function buildTicks(input: TicksInput): { label: string; offset: number }[] { const formatter = buildTickFormatter(scale, locale); const stepDays = getTickStep(scale); const tickCount = Math.floor(totalDays / stepDays); - return Array.from({ length: tickCount + 1 }) - .map((_, index) => { - const day = index * stepDays; - return { - date: new Date(start.getTime() + day * MS_PER_DAY), - offset: day, - }; - }) - .filter((tick) => tick.date.getTime() <= end.getTime()) - .map((tick) => ({ label: formatter(tick.date), offset: tick.offset })); + return Array.from({ length: tickCount + 1 }).reduce< + { label: string; offset: number }[] + >((ticks, _, index) => { + const day = index * stepDays; + const date = new Date(start.getTime() + day * MS_PER_DAY); + if (date.getTime() <= end.getTime()) { + ticks.push({ label: formatter(date), offset: day }); + } + return ticks; + }, []); } type GeometryOptions = { diff --git a/packages/ui/src/components/globe-3d/globe-3d.tsx b/packages/ui/src/components/globe-3d/globe-3d.tsx index 37fddb6b..05b037d5 100644 --- a/packages/ui/src/components/globe-3d/globe-3d.tsx +++ b/packages/ui/src/components/globe-3d/globe-3d.tsx @@ -280,20 +280,22 @@ function buildLine(arguments_: { rotationLng: number; }): string { const { points, rotationLat, rotationLng } = arguments_; - return points - .map((coord) => project(coord, rotationLng, rotationLat)) - .reduce<{ path: string; pen: "down" | "up" }>( - (state, projected) => { - if (!projected.visible) return { path: state.path, pen: "up" }; - const head = state.pen === "up" ? "M" : "L"; - const separator = state.path.length > 0 ? " " : ""; - return { - path: `${state.path}${separator}${head}${projected.x.toString()},${projected.y.toString()}`, - pen: "down", - }; - }, - { path: "", pen: "up" }, - ).path; + return points.reduce<{ path: string; pen: "down" | "up" }>( + (state, coord) => { + const projected = project(coord, rotationLng, rotationLat); + if (!projected.visible) { + return { path: state.path, pen: "up" }; + } + + const head = state.pen === "up" ? "M" : "L"; + const separator = state.path.length > 0 ? " " : ""; + return { + path: `${state.path}${separator}${head}${projected.x.toString()},${projected.y.toString()}`, + pen: "down", + }; + }, + { path: "", pen: "up" }, + ).path; } function range(start: number, end: number, step: number): number[] { @@ -308,9 +310,16 @@ function Graticule({ rotationLat, rotationLng }: GraticuleProps): ReactNode { const meridians = range(-150, 180, 30).map((lng) => range(-85, 85, 5).map((lat) => ({ lat, lng })), ); - const lines = [...parallels, ...meridians] - .map((points) => buildLine({ points, rotationLat, rotationLng })) - .filter((path) => path.length > 0); + const lines = [...parallels, ...meridians].reduce( + (paths, points) => { + const path = buildLine({ points, rotationLat, rotationLng }); + if (path.length > 0) { + paths.push(path); + } + return paths; + }, + [], + ); return ( - new Set( - categories - .filter((category) => !hidden.has(category.id)) - .map((category) => category.id), - ), + categories.reduce>((visible, category) => { + if (!hidden.has(category.id)) { + visible.add(category.id); + } + return visible; + }, new Set()), [categories, hidden], ); diff --git a/packages/ui/src/components/live-feed/live-feed.tsx b/packages/ui/src/components/live-feed/live-feed.tsx index 5cd8db3b..069c3340 100644 --- a/packages/ui/src/components/live-feed/live-feed.tsx +++ b/packages/ui/src/components/live-feed/live-feed.tsx @@ -116,7 +116,7 @@ function formatAbsolute(eventDate: Date): string { } function sortEventsDesc(events: LiveFeedEvent[]): LiveFeedEvent[] { - return [...events].sort( + return events.toSorted( (a, b) => normalizeDate(b.timestamp).getTime() - normalizeDate(a.timestamp).getTime(), diff --git a/packages/ui/src/components/map-2d/map-2d.tsx b/packages/ui/src/components/map-2d/map-2d.tsx index 49480455..fbb2d015 100644 --- a/packages/ui/src/components/map-2d/map-2d.tsx +++ b/packages/ui/src/components/map-2d/map-2d.tsx @@ -122,7 +122,7 @@ function useMapState(arguments_: { }; }, [center]); const [pan, setPan] = useState<{ x: number; y: number }>(initialPan); - const [zoom, setZoom] = useState( + const [zoom, setZoom] = useState(() => clamp(initialZoom, MIN_ZOOM, MAX_ZOOM), ); diff --git a/packages/ui/src/components/map-timeline/map-timeline.tsx b/packages/ui/src/components/map-timeline/map-timeline.tsx index 44873321..581d38d5 100644 --- a/packages/ui/src/components/map-timeline/map-timeline.tsx +++ b/packages/ui/src/components/map-timeline/map-timeline.tsx @@ -506,7 +506,7 @@ function useTimelineState(arguments_: { year: number; } { const { endYear, initialYear, onYearChange, startYear } = arguments_; - const [year, setYear] = useState( + const [year, setYear] = useState(() => clamp(initialYear ?? startYear, startYear, endYear), ); const [isPlaying, setIsPlaying] = useState(false); diff --git a/packages/ui/src/components/number-ticker/number-ticker.tsx b/packages/ui/src/components/number-ticker/number-ticker.tsx index 8beffd65..95e88fbf 100644 --- a/packages/ui/src/components/number-ticker/number-ticker.tsx +++ b/packages/ui/src/components/number-ticker/number-ticker.tsx @@ -21,7 +21,7 @@ function getNumberTickerFormatter( const key = `${locale ?? ""}|${formatOptions ? JSON.stringify(formatOptions) : ""}`; let formatter = NUMBER_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, formatOptions); + formatter = Intl.NumberFormat(locale, formatOptions); NUMBER_FORMATTER_CACHE.set(key, formatter); } return formatter; diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.tsx index f7079a0c..862752ce 100644 --- a/packages/ui/src/components/progress-tracker/progress-tracker.tsx +++ b/packages/ui/src/components/progress-tracker/progress-tracker.tsx @@ -103,9 +103,10 @@ function readPersistedChecklistItems(persistKey?: string): string[] { } function areStringArraysEqual(left: string[], right: string[]): boolean { - if (left.length !== right.length) return false; - - return left.every((value, index) => value === right[index]); + return ( + left.length === right.length && + left.every((value, index) => value === right[index]) + ); } function getChecklistPersistKey(event?: Event): null | string { @@ -257,7 +258,13 @@ function ProgressTrackerOverview({ const { modules, overallProgress, streak, title } = useProgressTrackerContext(); const trackedPersistKeys = React.useMemo( - () => modules.map((module) => module.persistKey).filter(Boolean), + () => + modules.reduce((keys, module) => { + if (module.persistKey) { + keys.push(module.persistKey); + } + return keys; + }, []), [modules], ); const [, forceChecklistRefresh] = React.useState(0); diff --git a/packages/ui/src/components/search-dialog/search-dialog.tsx b/packages/ui/src/components/search-dialog/search-dialog.tsx index b2a7b7f6..16d99a9f 100644 --- a/packages/ui/src/components/search-dialog/search-dialog.tsx +++ b/packages/ui/src/components/search-dialog/search-dialog.tsx @@ -76,7 +76,7 @@ export function SearchDialog({ }: SearchDialogProps) { const [open, setOpen] = useState(false); - const sortedItems = [...items].sort((a, b) => a.title.localeCompare(b.title)); + const sortedItems = items.toSorted((a, b) => a.title.localeCompare(b.title)); useKeyboardShortcut(() => { if (enableKeyboardShortcut) { diff --git a/packages/ui/src/components/sidebar/sidebar.tsx b/packages/ui/src/components/sidebar/sidebar.tsx index c1114e2b..7b99792a 100644 --- a/packages/ui/src/components/sidebar/sidebar.tsx +++ b/packages/ui/src/components/sidebar/sidebar.tsx @@ -66,7 +66,7 @@ function useScrollFade( }; checkScroll(); - container.addEventListener("scroll", checkScroll); + container.addEventListener("scroll", checkScroll, { passive: true }); return () => { container.removeEventListener("scroll", checkScroll); }; diff --git a/packages/ui/src/components/status-board/status-board.tsx b/packages/ui/src/components/status-board/status-board.tsx index e3dc703b..d5cc15b2 100644 --- a/packages/ui/src/components/status-board/status-board.tsx +++ b/packages/ui/src/components/status-board/status-board.tsx @@ -107,10 +107,16 @@ function getSummary(items: StatusBoardItem[]) { }, ); - return STATUS_ORDER.map((status) => ({ - count: counts[status], - status, - })).filter((entry) => entry.count > 0); + return STATUS_ORDER.reduce<{ count: number; status: StatusBoardStatus }[]>( + (summary, status) => { + const count = counts[status]; + if (count > 0) { + summary.push({ count, status }); + } + return summary; + }, + [], + ); } function StatusBoardSummary({ items }: { items: StatusBoardItem[] }) { diff --git a/packages/ui/src/components/tags-input/tags-input.tsx b/packages/ui/src/components/tags-input/tags-input.tsx index ad614e58..c4194c75 100644 --- a/packages/ui/src/components/tags-input/tags-input.tsx +++ b/packages/ui/src/components/tags-input/tags-input.tsx @@ -11,11 +11,18 @@ function normalizeTag(tag: string) { } function getNormalizedTags(tags: string[]) { - return tags - .map(normalizeTag) - .filter( - (tag, index, values) => tag.length > 0 && values.indexOf(tag) === index, - ); + return tags.reduce<{ seen: Set; tags: string[] }>( + (state, tag) => { + const normalizedTag = normalizeTag(tag); + if (normalizedTag.length > 0 && !state.seen.has(normalizedTag)) { + state.seen.add(normalizedTag); + state.tags.push(normalizedTag); + } + + return state; + }, + { seen: new Set(), tags: [] }, + ).tags; } function shouldAddTagFromKey(key: string) { diff --git a/packages/ui/src/components/terminal/terminal.tsx b/packages/ui/src/components/terminal/terminal.tsx index f874c4f4..eebfb7e2 100644 --- a/packages/ui/src/components/terminal/terminal.tsx +++ b/packages/ui/src/components/terminal/terminal.tsx @@ -17,6 +17,15 @@ export type TerminalProps = { title?: string; }; +function getCommandContents(lines: TerminalLine[]): string[] { + return lines.reduce((commands, line) => { + if (line.type === "command") { + commands.push(line.content); + } + return commands; + }, []); +} + export function Terminal({ copyable = true, lines, @@ -24,9 +33,7 @@ export function Terminal({ }: TerminalProps) { const [copied, setCopied] = useState(false); - const commands = lines - .filter((l) => l.type === "command") - .map((l) => l.content); + const commands = getCommandContents(lines); const handleCopy = async () => { await navigator.clipboard.writeText(commands.join("\n")); @@ -114,9 +121,7 @@ export function SimpleTerminal({ return { content: line, type: "output" }; }); - const commands = lines - .filter((l) => l.type === "command") - .map((l) => l.content); + const commands = getCommandContents(lines); const handleCopy = async () => { await navigator.clipboard.writeText(commands.join("\n")); diff --git a/packages/ui/src/components/tldr-section/tldr-section.tsx b/packages/ui/src/components/tldr-section/tldr-section.tsx index 5dfc0e37..148692b6 100644 --- a/packages/ui/src/components/tldr-section/tldr-section.tsx +++ b/packages/ui/src/components/tldr-section/tldr-section.tsx @@ -9,25 +9,25 @@ type TLDRSectionProps = { export function TLDRSection({ children, label }: TLDRSectionProps) { const [isExpanded, setIsExpanded] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [hasBeenOpened, setHasBeenOpened] = useState(false); + const [showSkeleton, setShowSkeleton] = useState(false); + const hasBeenOpenedRef = useRef(false); const timerReference = useRef(null); useEffect(() => { - if (isExpanded && !hasBeenOpened) { + if (isExpanded && !hasBeenOpenedRef.current) { // Clear any existing timer if (timerReference.current) { clearTimeout(timerReference.current); } const rafId = requestAnimationFrame(() => { - setIsLoading(true); - setHasBeenOpened(true); + setShowSkeleton(true); + hasBeenOpenedRef.current = true; }); // Simulate loading with skeleton timerReference.current = setTimeout(() => { - setIsLoading(false); + setShowSkeleton(false); timerReference.current = null; }, 800); @@ -37,6 +37,7 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { clearTimeout(timerReference.current); timerReference.current = null; } + setShowSkeleton(false); }; } @@ -45,8 +46,9 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { clearTimeout(timerReference.current); timerReference.current = null; } + setShowSkeleton(false); }; - }, [isExpanded]); // eslint-disable-line react-hooks/exhaustive-deps + }, [isExpanded]); return (
@@ -91,7 +93,7 @@ export function TLDRSection({ children, label }: TLDRSectionProps) { {isExpanded ? (
- {isLoading ? ( + {showSkeleton ? (
diff --git a/packages/ui/src/components/transaction-list/transaction-list.tsx b/packages/ui/src/components/transaction-list/transaction-list.tsx index cbc822fd..4c9320f5 100644 --- a/packages/ui/src/components/transaction-list/transaction-list.tsx +++ b/packages/ui/src/components/transaction-list/transaction-list.tsx @@ -19,7 +19,7 @@ function getCurrencyFormatter( const key = `${locale}|${currency}`; let formatter = CURRENCY_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.NumberFormat(locale, { + formatter = Intl.NumberFormat(locale, { currency, style: "currency", }); @@ -32,7 +32,7 @@ const DATE_FORMATTER_CACHE = new Map(); function getTransactionDateFormatter(locale: string): Intl.DateTimeFormat { let formatter = DATE_FORMATTER_CACHE.get(locale); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric", diff --git a/packages/ui/src/components/usage-breakdown/usage-breakdown.tsx b/packages/ui/src/components/usage-breakdown/usage-breakdown.tsx index 31763f9b..b3a6f0f5 100644 --- a/packages/ui/src/components/usage-breakdown/usage-breakdown.tsx +++ b/packages/ui/src/components/usage-breakdown/usage-breakdown.tsx @@ -174,9 +174,7 @@ function UsageBreakdownRow({ } function getSortedItems(items: UsageBreakdownItem[], maxItems?: number) { - const rankedItems = [...items].sort( - (left, right) => right.value - left.value, - ); + const rankedItems = items.toSorted((left, right) => right.value - left.value); return typeof maxItems === "number" ? rankedItems.slice(0, maxItems) : rankedItems; diff --git a/packages/ui/src/components/world-clock-bar/world-clock-bar.tsx b/packages/ui/src/components/world-clock-bar/world-clock-bar.tsx index 582b813c..0c497eec 100644 --- a/packages/ui/src/components/world-clock-bar/world-clock-bar.tsx +++ b/packages/ui/src/components/world-clock-bar/world-clock-bar.tsx @@ -60,7 +60,7 @@ function getTimeFormatter( const key = `${locale}|${timeZone}`; let formatter = TIME_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone, @@ -79,7 +79,7 @@ function getDateFormatter( const key = `${locale}|${timeZone}`; let formatter = DATE_FORMATTER_CACHE.get(key); if (!formatter) { - formatter = new Intl.DateTimeFormat(locale, { + formatter = Intl.DateTimeFormat(locale, { day: "numeric", month: "short", timeZone,