Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ next-env.d.ts
*.tgz

.playwright-mcp

# Ruflo / claude-flow — generated files & runtime data (env already ignored above)
.agents/
.claude-flow/
.swarm/
.hive-mind/
ruvector.db
CLAUDE.md
47 changes: 35 additions & 12 deletions app/icons/_components/iconlist/IconList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"use client";

import { AnimatePresence } from "motion/react";
import React from "react";

import { ICON_LIST as HUGE_ICON_LIST } from "@/icons/huge";
import { ICON_LIST as LUCIDE_ICON_LIST } from "@/icons/lucide";
import React, { useEffect, useState } from "react";

import { useIconLibrary } from "@/hooks/useIconLibrary";
import { useCategory } from "../../_contexts/CategoryContext";
Expand All @@ -13,23 +10,41 @@ import { useIconSearchResult } from "../../_contexts/IconSearchContext";
import { useIconSearchFilter } from "@/hooks/useIconFilter";
import { IconTileProvider } from "../../_contexts/IconTileContext";
import IconLibraryEmptyState from "./IconLibraryEmptyState";
import IconListSkeleton from "./IconListSkeleton";
import IconsNotFound from "./IconsNotFound";
import IconTile from "./IconTile";

const ICON_LIST_MAP = {
lucide: LUCIDE_ICON_LIST,
huge: HUGE_ICON_LIST,
} as const;

const IconList: React.FC = () => {
const { debouncedQuery } = useIconSearchResult();
const { library } = useIconLibrary();
const { category } = useCategory();

const baseIcons: IconListItem[] = library ? ICON_LIST_MAP[library] : [];
const [loaded, setLoaded] = useState<{
library: string;
icons: IconMeta[];
getIcon: (name: string) => React.ElementType;
} | null>(null);

useEffect(() => {
if (!library) return;
let alive = true;
const load =
library === "huge"
? import("@/icons/huge/meta")
: import("@/icons/lucide/meta");
load.then((m) => {
if (alive) setLoaded({ library, icons: m.ICON_META, getIcon: m.getIcon });
});
return () => {
alive = false;
};
}, [library]);

const active = loaded && loaded.library === library ? loaded : null;
const baseIcons = active ? active.icons : null;

const filteredItems = useIconSearchFilter({
icons: baseIcons,
icons: baseIcons ?? [],
category,
query: debouncedQuery,
});
Expand All @@ -38,14 +53,22 @@ const IconList: React.FC = () => {
return <IconLibraryEmptyState />;
}

if (baseIcons === null || active === null) {
return <IconListSkeleton />;
}

return (
<IconTileProvider>
<AnimatePresence>
{filteredItems.length > 0 ? (
<>
<div className="576:grid-cols-2 900:grid-cols-3 mt-3 grid w-full grid-cols-1 gap-4 pb-10 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
{filteredItems.map((item) => (
<IconTile key={item.name} item={item} />
<IconTile
key={item.name}
item={item}
getIcon={active.getIcon}
/>
))}
</div>

Expand Down
40 changes: 36 additions & 4 deletions app/icons/_components/iconlist/IconTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,44 @@ import IconTileActions from "./IconTileActions";

type Props = {
item: IconFilteredItem;
getIcon: (name: string) => React.ElementType;
};

const IconTile: React.FC<Props> = ({ item }) => {
const IconTile: React.FC<Props> = ({ item, getIcon }) => {
const { library, prefix } = useIconLibrary();
const { openPlayground } = usePlayground();
const iconRef = React.useRef<IconHandle>(null);
const tileRef = React.useRef<HTMLDivElement>(null);
const [inView, setInView] = React.useState(false);

React.useEffect(() => {
const el = tileRef.current;
if (!el || inView) return;
const io = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setInView(true);
io.disconnect();
}
},
{ rootMargin: "800px 0px" },
);
io.observe(el);
return () => io.disconnect();
}, [inView]);

if (!library || !prefix) {
throw new Error("useIconLibrary used outside /icons route");
}

const tileId = `${library}-${item.name}`;
const IconComponent = item.icon;
// getIcon returns a module-cached React.lazy component (stable per name),
// so this dynamic reference is safe despite the static-components heuristic.
// eslint-disable-next-line react-hooks/static-components
const IconComponent = getIcon(item.name) as React.ComponentType<{
size?: number;
ref?: React.Ref<IconHandle>;
}>;

const handleOpen = () =>
openPlayground({
Expand All @@ -48,7 +73,10 @@ const IconTile: React.FC<Props> = ({ item }) => {
});

return (
<div className="bg-surfaceElevated/65 border-border hover:bg-surfaceHover relative flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-md border p-4 text-sm text-white shadow-lg transition-all hover:scale-102">
<div
ref={tileRef}
className="bg-surfaceElevated/65 border-border hover:bg-surfaceHover relative flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-md border p-4 text-sm text-white shadow-lg transition-all hover:scale-102"
>
{item.isNew && (
<span className="bg-primary/12 text-primary border-primary/25 absolute top-0 right-0 rounded-bl-md border-b border-l px-2 py-0.5 text-[10px] font-semibold tracking-wide uppercase">
New
Expand All @@ -70,7 +98,11 @@ const IconTile: React.FC<Props> = ({ item }) => {
onMouseLeave={(e) => handleHover(e, iconRef)}
className="hover:bg-surface inline-flex size-12 cursor-pointer items-center justify-center rounded-xl p-3"
>
<IconComponent ref={iconRef} size={23} />
{inView ? (
<React.Suspense fallback={null}>
<IconComponent ref={iconRef} size={23} />
</React.Suspense>
) : null}
</div>
<p className="line-clamp-1 text-gray-300">{item.name}</p>

Expand Down
4 changes: 2 additions & 2 deletions app/icons/_components/navbar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
} from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
import { useIconLibrary } from "@/hooks/useIconLibrary";
import { ICON_COUNT as HUGE_ICON_COUNT } from "@/icons/huge";
import { ICON_COUNT as LUCIDE_ICON_COUNT } from "@/icons/lucide";
import { ICON_COUNT as HUGE_ICON_COUNT } from "@/icons/huge/meta";
import { ICON_COUNT as LUCIDE_ICON_COUNT } from "@/icons/lucide/meta";
import { SearchIcon } from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import { useIconSearch } from "../../_contexts/IconSearchContext";
Expand Down
16 changes: 9 additions & 7 deletions app/icons/_components/playground/PlaygroundSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { cn } from "@/lib/utils";
import type { IconHandle } from "@/types/icon";
import handleHover from "@/utils/handleHover";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { usePlayground } from "../../_contexts/PlaygroundContext";
import HighlightedCode from "./HighlightedCode";
import PlaygroundControls from "./PlaygroundControls";
Expand Down Expand Up @@ -188,12 +188,14 @@ const PlaygroundSheet: React.FC = () => {
<span className="text-textMuted absolute top-3 left-4 font-mono text-[10px] tracking-wider uppercase">
{icon.library} · {config.size}px
</span>
<IconComponent
ref={iconRef}
size={config.size}
duration={config.duration}
color={config.color}
/>
<Suspense fallback={null}>
<IconComponent
ref={iconRef}
size={config.size}
duration={config.duration}
color={config.color}
/>
</Suspense>
</div>

{/* Controls */}
Expand Down
51 changes: 32 additions & 19 deletions app/icons/_components/sidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useIconLibrary } from "@/hooks/useIconLibrary";
import { ICON_LIST as HUGE_ICON_LIST } from "@/icons/huge";
import { ICON_LIST as LUCIDE_ICON_LIST } from "@/icons/lucide";
import { ICON_META as HUGE_ICON_META } from "@/icons/huge/meta";
import { ICON_META as LUCIDE_ICON_META } from "@/icons/lucide/meta";
import { getCategories } from "@/utils/getCategories";
import { isIconNew } from "@/utils/isIconNew";
import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import React from "react";
import { useCategory } from "../../_contexts/CategoryContext";
import { sidebarConfig } from "./sidebar.config";
import SidebarRequest from "./SidebarRequest";

const libraryIconMap: Record<string, React.ReactNode> = {
"Lucide Icons": <LucideIcon className="size-4" />,
Expand All @@ -35,11 +35,22 @@ const AppSidebar: React.FC = () => {
const { category, setCategory } = useCategory();
const router = useRouter();
const pathname = usePathname();
const icons = library === "huge" ? HUGE_ICON_LIST : LUCIDE_ICON_LIST;
const icons = library === "huge" ? HUGE_ICON_META : LUCIDE_ICON_META;

const categories = React.useMemo(() => getCategories(icons), [icons]);
const totalCount = icons.length;

// New-icon count per library, keyed by the library `name` used in the
// sidebar config, so the chip sits beside its library regardless of which
// one is active.
const newCountByLibrary = React.useMemo<Record<string, number>>(
() => ({
lucide: LUCIDE_ICON_META.filter((icon) => isIconNew(icon.addedAt)).length,
huge: HUGE_ICON_META.filter((icon) => isIconNew(icon.addedAt)).length,
}),
[],
);

// ponytail: docs render their own shell (app/icons/docs/layout.tsx), so the
// gallery's category sidebar steps aside on /icons/docs routes.
if (pathname?.startsWith("/icons/docs")) return null;
Expand Down Expand Up @@ -95,18 +106,22 @@ const AppSidebar: React.FC = () => {
</Link>
</SidebarHeader>

<SidebarContent className="bg-bgDark gap-2">
<SidebarContent className="bg-bgDark gap-3 overscroll-contain">
{sidebarConfig.map((group) => (
<SidebarGroup
key={group.label}
className={group.scrollable ? "flex-1 overflow-y-auto" : ""}
className={
group.scrollable
? "flex-1 overflow-y-auto overscroll-contain"
: ""
}
>
<SidebarGroupLabel className="text-textSecondary text-xs">
<SidebarGroupLabel className="text-textMuted text-[10px] font-semibold tracking-[0.14em] uppercase">
{group.label}
</SidebarGroupLabel>

<SidebarGroupContent>
<SidebarMenu className="gap-[0.563rem]">
<SidebarMenu className="gap-1">
{group.items.map((item) => {
const Icon = item.icon;
const customIcon = libraryIconMap[item.label];
Expand All @@ -128,9 +143,9 @@ const AppSidebar: React.FC = () => {

<span className="flex items-center gap-2">
{item.label}
{item.isBeta && (
<span className="border-primary/40 text-primary rounded border px-1.5 py-0.5 text-[10px] leading-none font-semibold">
BETA
{item.name && newCountByLibrary[item.name] > 0 && (
<span className="bg-primary/12 text-primary border-primary/25 rounded-sm border px-1.5 py-px text-[9px] font-semibold tracking-wide uppercase">
{newCountByLibrary[item.name]} New
</span>
)}
</span>
Expand Down Expand Up @@ -178,12 +193,12 @@ const AppSidebar: React.FC = () => {
</SidebarGroup>
))}

<SidebarGroup className="min-h-50 flex-1 overflow-y-auto">
<SidebarGroupLabel className="text-textSecondary text-xs">
<SidebarGroup className="flex min-h-0 flex-1 flex-col">
<SidebarGroupLabel className="text-textMuted shrink-0 text-[10px] font-semibold tracking-[0.14em] uppercase">
Categories
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu className="gap-[0.563rem]">
<SidebarGroupContent className="min-h-0 flex-1 scrollbar-gutter-stable overflow-y-scroll overscroll-contain">
<SidebarMenu className="gap-1">
<SidebarMenuItem key="all">
<SidebarMenuButton
variant="dark"
Expand All @@ -192,7 +207,7 @@ const AppSidebar: React.FC = () => {
onClick={() => handleCategory("all")}
>
<span className="flex items-center gap-2">All</span>
<span className="text-textSecondary text-xs">
<span className="text-textSecondary rounded-md bg-white/6 px-1.5 py-0.5 text-[11px] font-medium tabular-nums">
{totalCount}
</span>
</SidebarMenuButton>
Expand All @@ -207,7 +222,7 @@ const AppSidebar: React.FC = () => {
onClick={() => handleCategory(cat.name)}
>
<span className="flex items-center gap-2">{cat.name}</span>
<span className="text-textSecondary text-xs">
<span className="text-textSecondary rounded-md bg-white/6 px-1.5 py-0.5 text-[11px] font-medium tabular-nums">
{cat.count}
</span>
</SidebarMenuButton>
Expand All @@ -216,8 +231,6 @@ const AppSidebar: React.FC = () => {
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

<SidebarRequest />
</SidebarContent>
</Sidebar>
);
Expand Down
43 changes: 0 additions & 43 deletions app/icons/_components/sidebar/SidebarRequest.tsx

This file was deleted.

Loading
Loading