diff --git a/apps/server/package.json b/apps/server/package.json index 188264e..772b331 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -36,6 +36,7 @@ "@orpc/zod": "catalog:", "@t3-oss/env-core": "catalog:", "@tanstack/react-query": "catalog:", + "@tanstack/react-virtual": "catalog:", "@tanstack/react-router": "catalog:", "@tanstack/react-router-ssr-query": "catalog:", "@tanstack/react-start": "catalog:", diff --git a/apps/server/src/modules/bookmarks/components/IconPickerDialog.tsx b/apps/server/src/modules/bookmarks/components/IconPickerDialog.tsx index 3fade16..5bce1ec 100644 --- a/apps/server/src/modules/bookmarks/components/IconPickerDialog.tsx +++ b/apps/server/src/modules/bookmarks/components/IconPickerDialog.tsx @@ -1,5 +1,6 @@ +import { useVirtualizer } from '@tanstack/react-virtual' import * as icons from 'lucide-react' -import { Search } from 'lucide-react' +import { Search, X } from 'lucide-react' import { useState } from 'react' import { Button } from '@/components/ui/button' import { @@ -13,119 +14,134 @@ import { import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' -const ICON_NAMES = [ - 'Globe', - 'Home', - 'Code', - 'Database', - 'Mail', - 'MessageSquare', - 'Music', - 'Video', - 'Image', - 'FileText', - 'Folder', - 'Star', - 'Heart', - 'Bookmark', - 'Search', - 'Settings', - 'User', - 'Shield', - 'Key', - 'Terminal', - 'Github', - 'Chrome', - 'Cpu', - 'Server', - 'Cloud', - 'Wifi', - 'Zap', - 'Coffee', - 'BookOpen', - 'Briefcase', - 'Calendar', - 'Clock', - 'Download', - 'Edit', - 'ExternalLink', - 'Eye', - 'Film', - 'Gift', - 'Headphones', - 'Layout', - 'Link', - 'Map', - 'Monitor', - 'Package', - 'Phone', - 'ShoppingCart', - 'Smartphone', - 'Tv', - 'Upload', - 'Box', - 'Compass', - 'Rss', - 'Camera', - 'Printer', - 'Layers', - 'Activity', -] as const - const allIcons = icons as unknown as Record> +const ALL_ICON_NAMES: string[] = Object.keys(icons) + .filter((key) => /^[A-Z]/.test(key) && !key.endsWith('Icon')) + .sort() + +const COLUMNS = 8 +const ROW_HEIGHT = 40 + interface IconPickerDialogProps { value: string | null - onChange: (iconName: string) => void + onChange: (iconName: string | null) => void } export const IconPickerDialog = ({ value, onChange }: IconPickerDialogProps) => { const [open, setOpen] = useState(false) const [filter, setFilter] = useState('') + const [scrollElement, setScrollElement] = useState(null) + + const filteredIcons = filter + ? ALL_ICON_NAMES.filter((name) => name.toLowerCase().includes(filter.toLowerCase())) + : ALL_ICON_NAMES + + const rowCount = Math.ceil(filteredIcons.length / COLUMNS) + + const virtualizer = useVirtualizer({ + count: rowCount, + getScrollElement: () => scrollElement, + estimateSize: () => ROW_HEIGHT, + overscan: 5, + }) - const filteredIcons = ICON_NAMES.filter((name) => name.toLowerCase().includes(filter.toLowerCase())) const CurrentIcon = (value && allIcons[value]) || null return ( - - - + { + setOpen(isOpen) + if (!isOpen) setFilter('') + }} + > + }> + {CurrentIcon ? : } + {value ?? '选择图标'} 选择图标 - 搜索并选择一个 lucide 图标作为书签图标 + 搜索并选择一个图标 · 共 {ALL_ICON_NAMES.length} 个可用图标 - setFilter(e.target.value)} placeholder="搜索图标..." /> +
+ { + setFilter(e.target.value) + scrollElement?.scrollTo({ top: 0 }) + }} + placeholder="搜索图标..." + className="flex-1" + /> + {value && ( + + )} +
-
- {filteredIcons.map((name) => { - const Icon = allIcons[name] - if (!Icon) { - return null - } +
+
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const startIndex = virtualRow.index * COLUMNS + const rowIcons = filteredIcons.slice(startIndex, startIndex + COLUMNS) - return ( - - ) - })} + return ( +
+ {rowIcons.map((name) => { + const Icon = allIcons[name] + if (!Icon) return null + + return ( + + ) + })} +
+ ) + })} +
{filteredIcons.length === 0 &&

未找到匹配图标

} diff --git a/bun.lock b/bun.lock index 3836505..dfaf59d 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,7 @@ "@tanstack/react-router": "catalog:", "@tanstack/react-router-ssr-query": "catalog:", "@tanstack/react-start": "catalog:", + "@tanstack/react-virtual": "catalog:", "better-auth": "catalog:", "class-variance-authority": "catalog:", "clsx": "catalog:", @@ -90,6 +91,7 @@ "@tanstack/react-router-devtools": "^1.166.11", "@tanstack/react-router-ssr-query": "^1.166.10", "@tanstack/react-start": "^1.167.6", + "@tanstack/react-virtual": "^3.13.6", "@types/bun": "^1.3.11", "@vitejs/plugin-react": "^6.0.1", "babel-plugin-react-compiler": "^1.0.0", @@ -501,6 +503,8 @@ "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ=="], + "@tanstack/router-core": ["@tanstack/router-core@1.168.7", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-z4UEdlzMrFaKBsG4OIxlZEm+wsYBtEp//fnX6kW18jhQpETNcM6u2SXNdX+bcIYp6AaR7ERS3SBENzjC/xxwQQ=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.1", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.2", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w=="], @@ -525,6 +529,8 @@ "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], diff --git a/package.json b/package.json index 51a831a..6cfd60d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query": "^5.95.2", "@tanstack/react-query-devtools": "^5.95.2", + "@tanstack/react-virtual": "^3.13.6", "@tanstack/react-router": "^1.168.3", "@tanstack/react-router-devtools": "^1.166.11", "@tanstack/react-router-ssr-query": "^1.166.10",