@mdguggenbichler/slugbase-core 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/frontend/index.tsx +3 -3
  2. package/frontend/public/favicon.svg +1 -0
  3. package/frontend/public/slugbase_icon_blue.svg +1 -0
  4. package/frontend/public/slugbase_icon_white.png +0 -0
  5. package/frontend/public/slugbase_icon_white.svg +1 -0
  6. package/frontend/src/App.tsx +179 -0
  7. package/frontend/src/api/client.ts +134 -0
  8. package/frontend/src/components/AppSidebar.tsx +214 -0
  9. package/frontend/src/components/EmptyState.tsx +33 -0
  10. package/frontend/src/components/Favicon.tsx +76 -0
  11. package/frontend/src/components/FilterChips.tsx +60 -0
  12. package/frontend/src/components/FolderIcon.tsx +207 -0
  13. package/frontend/src/components/GlobalSearch.tsx +275 -0
  14. package/frontend/src/components/Layout.tsx +60 -0
  15. package/frontend/src/components/PageHeader.tsx +31 -0
  16. package/frontend/src/components/ScopeSegmentedControl.tsx +42 -0
  17. package/frontend/src/components/SentryDebug.tsx +32 -0
  18. package/frontend/src/components/StatCard.tsx +66 -0
  19. package/frontend/src/components/TopBar.tsx +63 -0
  20. package/frontend/src/components/UserDropdown.tsx +86 -0
  21. package/frontend/src/components/admin/AdminAI.tsx +207 -0
  22. package/frontend/src/components/admin/AdminOIDCProviders.tsx +183 -0
  23. package/frontend/src/components/admin/AdminSettings.tsx +413 -0
  24. package/frontend/src/components/admin/AdminTeams.tsx +177 -0
  25. package/frontend/src/components/admin/AdminUsers.tsx +225 -0
  26. package/frontend/src/components/bookmarks/BookmarkCard.tsx +312 -0
  27. package/frontend/src/components/bookmarks/BookmarkListItem.tsx +159 -0
  28. package/frontend/src/components/bookmarks/BookmarkTableView.tsx +419 -0
  29. package/frontend/src/components/bookmarks/BulkActionModals.tsx +162 -0
  30. package/frontend/src/components/bookmarks/FilterChips.tsx +5 -0
  31. package/frontend/src/components/modals/BookmarkModal.tsx +493 -0
  32. package/frontend/src/components/modals/FolderModal.tsx +306 -0
  33. package/frontend/src/components/modals/ImportModal.tsx +232 -0
  34. package/frontend/src/components/modals/OIDCProviderModal.tsx +284 -0
  35. package/frontend/src/components/modals/SharingModal.tsx +96 -0
  36. package/frontend/src/components/modals/TagModal.tsx +101 -0
  37. package/frontend/src/components/modals/TeamAssignmentModal.tsx +354 -0
  38. package/frontend/src/components/modals/TeamModal.tsx +117 -0
  39. package/frontend/src/components/modals/UserModal.tsx +225 -0
  40. package/frontend/src/components/profile/CreateTokenModal.tsx +172 -0
  41. package/frontend/src/components/sharing/ShareResourceDialog.tsx +422 -0
  42. package/frontend/src/components/ui/Autocomplete.tsx +155 -0
  43. package/frontend/src/components/ui/Button.tsx +68 -0
  44. package/frontend/src/components/ui/ConfirmDialog.tsx +79 -0
  45. package/frontend/src/components/ui/FormFieldWrapper.tsx +36 -0
  46. package/frontend/src/components/ui/ModalFooterActions.tsx +49 -0
  47. package/frontend/src/components/ui/ModalSection.tsx +34 -0
  48. package/frontend/src/components/ui/PageLoadingSkeleton.tsx +24 -0
  49. package/frontend/src/components/ui/Select.tsx +61 -0
  50. package/frontend/src/components/ui/SharingField.tsx +298 -0
  51. package/frontend/src/components/ui/Toast.tsx +47 -0
  52. package/frontend/src/components/ui/Tooltip.tsx +21 -0
  53. package/frontend/src/components/ui/alert-dialog.tsx +139 -0
  54. package/frontend/src/components/ui/badge.tsx +36 -0
  55. package/frontend/src/components/ui/button-base.tsx +57 -0
  56. package/frontend/src/components/ui/card.tsx +76 -0
  57. package/frontend/src/components/ui/command.tsx +161 -0
  58. package/frontend/src/components/ui/dialog.tsx +120 -0
  59. package/frontend/src/components/ui/dropdown-menu.tsx +199 -0
  60. package/frontend/src/components/ui/input.tsx +22 -0
  61. package/frontend/src/components/ui/label.tsx +24 -0
  62. package/frontend/src/components/ui/popover.tsx +33 -0
  63. package/frontend/src/components/ui/progress.tsx +26 -0
  64. package/frontend/src/components/ui/scroll-area.tsx +48 -0
  65. package/frontend/src/components/ui/select-base.tsx +159 -0
  66. package/frontend/src/components/ui/separator.tsx +29 -0
  67. package/frontend/src/components/ui/sheet.tsx +140 -0
  68. package/frontend/src/components/ui/sidebar.tsx +783 -0
  69. package/frontend/src/components/ui/skeleton.tsx +15 -0
  70. package/frontend/src/components/ui/sonner.tsx +46 -0
  71. package/frontend/src/components/ui/switch.tsx +28 -0
  72. package/frontend/src/components/ui/table.tsx +120 -0
  73. package/frontend/src/components/ui/tooltip-base.tsx +30 -0
  74. package/frontend/src/config/api.ts +16 -0
  75. package/frontend/src/config/mode.ts +6 -0
  76. package/frontend/src/contexts/AppConfigContext.tsx +39 -0
  77. package/frontend/src/contexts/AuthContext.tsx +137 -0
  78. package/frontend/src/contexts/SearchCommandContext.tsx +28 -0
  79. package/frontend/src/hooks/use-mobile.tsx +19 -0
  80. package/frontend/src/hooks/useConfirmDialog.ts +63 -0
  81. package/frontend/src/hooks/useMarketingTheme.ts +47 -0
  82. package/frontend/src/i18n.ts +39 -0
  83. package/frontend/src/index.css +117 -0
  84. package/frontend/src/instrument.ts +20 -0
  85. package/frontend/src/lib/utils.ts +6 -0
  86. package/frontend/src/locales/de.json +899 -0
  87. package/frontend/src/locales/en.json +937 -0
  88. package/frontend/src/locales/es.json +884 -0
  89. package/frontend/src/locales/fr.json +550 -0
  90. package/frontend/src/locales/it.json +535 -0
  91. package/frontend/src/locales/ja.json +535 -0
  92. package/frontend/src/locales/nl.json +550 -0
  93. package/frontend/src/locales/pl.json +535 -0
  94. package/frontend/src/locales/pt.json +535 -0
  95. package/frontend/src/locales/ru.json +535 -0
  96. package/frontend/src/locales/zh.json +535 -0
  97. package/frontend/src/main.tsx +44 -0
  98. package/frontend/src/pages/Bookmarks.tsx +1004 -0
  99. package/frontend/src/pages/Dashboard.tsx +427 -0
  100. package/frontend/src/pages/Folders.tsx +578 -0
  101. package/frontend/src/pages/GoPreferences.tsx +134 -0
  102. package/frontend/src/pages/Login.tsx +196 -0
  103. package/frontend/src/pages/PasswordReset.tsx +242 -0
  104. package/frontend/src/pages/Profile.tsx +593 -0
  105. package/frontend/src/pages/SearchEngineGuide.tsx +135 -0
  106. package/frontend/src/pages/Setup.tsx +210 -0
  107. package/frontend/src/pages/Signup.tsx +199 -0
  108. package/frontend/src/pages/Tags.tsx +421 -0
  109. package/frontend/src/pages/VerifyEmail.tsx +254 -0
  110. package/frontend/src/pages/admin/AdminAIPage.tsx +5 -0
  111. package/frontend/src/pages/admin/AdminLayout.tsx +40 -0
  112. package/frontend/src/pages/admin/AdminMembersPage.tsx +5 -0
  113. package/frontend/src/pages/admin/AdminOIDCPage.tsx +5 -0
  114. package/frontend/src/pages/admin/AdminSettingsPage.tsx +5 -0
  115. package/frontend/src/pages/admin/AdminTeamsPage.tsx +5 -0
  116. package/frontend/src/utils/favicon.ts +36 -0
  117. package/frontend/src/utils/formatRelativeTime.ts +37 -0
  118. package/frontend/src/utils/safeHref.ts +31 -0
  119. package/frontend/src/vite-env.d.ts +10 -0
  120. package/package.json +9 -1
@@ -0,0 +1,214 @@
1
+ import { Link, useLocation } from 'react-router-dom';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useState, useEffect } from 'react';
4
+ import {
5
+ Bookmark,
6
+ Folder,
7
+ Tag,
8
+ Settings,
9
+ LayoutDashboard,
10
+ ChevronLeft,
11
+ ChevronRight,
12
+ ChevronDown,
13
+ Github,
14
+ Users,
15
+ UserCog,
16
+ Key,
17
+ Sparkles,
18
+ } from 'lucide-react';
19
+ import {
20
+ Sidebar,
21
+ SidebarContent,
22
+ SidebarFooter,
23
+ SidebarGroup,
24
+ SidebarGroupContent,
25
+ SidebarMenu,
26
+ SidebarMenuButton,
27
+ SidebarMenuItem,
28
+ SidebarSeparator,
29
+ useSidebar,
30
+ } from './ui/sidebar';
31
+ import { useAppConfig } from '../contexts/AppConfigContext';
32
+ import type { User } from '../contexts/AuthContext';
33
+ import { cn } from '@/lib/utils';
34
+
35
+ const SIDEBAR_ADMIN_OPEN_KEY = 'slugbase_sidebar_admin_open';
36
+
37
+ interface AppSidebarProps {
38
+ user: User | null;
39
+ version?: string | null;
40
+ }
41
+
42
+ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
43
+ const { t } = useTranslation();
44
+ const location = useLocation();
45
+ const pathname = location.pathname;
46
+ const { appBasePath } = useAppConfig();
47
+ const { setOpenMobile, toggleSidebar, isMobile, state } = useSidebar();
48
+ const adminBase = `${appBasePath || ''}/admin`;
49
+
50
+ const adminNavItems = [
51
+ { path: `${adminBase}/members`, label: t('admin.users'), icon: Users },
52
+ { path: `${adminBase}/teams`, label: t('admin.teams'), icon: UserCog },
53
+ { path: `${adminBase}/oidc`, label: t('admin.oidcProviders'), icon: Key },
54
+ { path: `${adminBase}/settings`, label: t('admin.settings'), icon: Settings },
55
+ { path: `${adminBase}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
56
+ ];
57
+
58
+ const isOverviewActive =
59
+ pathname === appBasePath ||
60
+ pathname === appBasePath + '/' ||
61
+ pathname === (appBasePath || '/');
62
+
63
+ const showAdmin = user?.is_admin;
64
+ const [adminOpen, setAdminOpen] = useState(() => {
65
+ if (typeof window === 'undefined') return true;
66
+ const stored = localStorage.getItem(SIDEBAR_ADMIN_OPEN_KEY);
67
+ return stored !== 'false';
68
+ });
69
+
70
+ useEffect(() => {
71
+ localStorage.setItem(SIDEBAR_ADMIN_OPEN_KEY, String(adminOpen));
72
+ }, [adminOpen]);
73
+
74
+ const primaryNavItems = [
75
+ { path: appBasePath || '/', label: t('dashboard.overview'), icon: LayoutDashboard },
76
+ { path: `${appBasePath}/bookmarks`, label: t('bookmarks.title'), icon: Bookmark },
77
+ { path: `${appBasePath}/folders`, label: t('folders.title'), icon: Folder },
78
+ { path: `${appBasePath}/tags`, label: t('tags.title'), icon: Tag },
79
+ ];
80
+
81
+ const handleNavClick = () => {
82
+ if (isMobile) {
83
+ setOpenMobile(false);
84
+ }
85
+ };
86
+
87
+ return (
88
+ <Sidebar collapsible="icon" side="left">
89
+ <SidebarContent>
90
+ <SidebarGroup>
91
+ <SidebarGroupContent>
92
+ <SidebarMenu>
93
+ {primaryNavItems.map((item) => (
94
+ <SidebarMenuItem key={item.path}>
95
+ <SidebarMenuButton
96
+ asChild
97
+ isActive={
98
+ item.path === (appBasePath || '/') ? isOverviewActive : pathname === item.path
99
+ }
100
+ tooltip={item.label}
101
+ >
102
+ <Link to={item.path} onClick={handleNavClick} aria-current={pathname === item.path ? 'page' : undefined}>
103
+ <item.icon className="h-5 w-5" />
104
+ <span>{item.label}</span>
105
+ </Link>
106
+ </SidebarMenuButton>
107
+ </SidebarMenuItem>
108
+ ))}
109
+ </SidebarMenu>
110
+ </SidebarGroupContent>
111
+ </SidebarGroup>
112
+
113
+ {showAdmin && (
114
+ <>
115
+ <SidebarSeparator />
116
+ <SidebarGroup>
117
+ <button
118
+ type="button"
119
+ onClick={() => setAdminOpen((prev) => !prev)}
120
+ data-sidebar="group-label"
121
+ className={cn(
122
+ 'flex h-8 w-full shrink-0 items-center gap-2 overflow-hidden rounded-md px-2 text-left text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
123
+ 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
124
+ 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 group-data-[collapsible=icon]:!rounded-lg'
125
+ )}
126
+ aria-expanded={adminOpen}
127
+ >
128
+ {adminOpen ? (
129
+ <ChevronDown className="h-5 w-5 shrink-0" />
130
+ ) : (
131
+ <ChevronRight className="h-5 w-5 shrink-0" />
132
+ )}
133
+ <span className="truncate group-data-[collapsible=icon]:hidden">{t('admin.title')}</span>
134
+ </button>
135
+ {adminOpen && (
136
+ <SidebarGroupContent>
137
+ <SidebarMenu>
138
+ {adminNavItems.map((item) => {
139
+ const Icon = item.icon;
140
+ return (
141
+ <SidebarMenuItem key={item.path}>
142
+ <SidebarMenuButton
143
+ asChild
144
+ isActive={pathname === item.path}
145
+ tooltip={item.label}
146
+ >
147
+ <Link
148
+ to={item.path}
149
+ onClick={handleNavClick}
150
+ aria-current={pathname === item.path ? 'page' : undefined}
151
+ >
152
+ <Icon className="h-5 w-5" />
153
+ <span>{item.label}</span>
154
+ </Link>
155
+ </SidebarMenuButton>
156
+ </SidebarMenuItem>
157
+ );
158
+ })}
159
+ </SidebarMenu>
160
+ </SidebarGroupContent>
161
+ )}
162
+ </SidebarGroup>
163
+ </>
164
+ )}
165
+ </SidebarContent>
166
+
167
+ <SidebarFooter>
168
+ <SidebarGroup>
169
+ <SidebarGroupContent>
170
+ <SidebarMenu>
171
+ <SidebarMenuItem>
172
+ <SidebarMenuButton
173
+ asChild
174
+ tooltip="GitHub Repository"
175
+ >
176
+ <a
177
+ href="https://github.com/mdg-labs/slugbase"
178
+ target="_blank"
179
+ rel="noopener noreferrer"
180
+ aria-label="GitHub Repository"
181
+ >
182
+ <Github className="h-5 w-5" />
183
+ <span>GitHub</span>
184
+ {version && state === 'expanded' && (
185
+ <span className="ml-1 truncate text-xs text-muted-foreground font-mono">
186
+ {version}
187
+ </span>
188
+ )}
189
+ </a>
190
+ </SidebarMenuButton>
191
+ </SidebarMenuItem>
192
+ {!isMobile && (
193
+ <SidebarMenuItem>
194
+ <SidebarMenuButton
195
+ onClick={toggleSidebar}
196
+ tooltip={state === 'collapsed' ? t('common.expandSidebar') : t('common.collapseSidebar')}
197
+ aria-label={state === 'collapsed' ? t('common.expandSidebar') : t('common.collapseSidebar')}
198
+ >
199
+ {state === 'collapsed' ? (
200
+ <ChevronRight className="h-5 w-5" />
201
+ ) : (
202
+ <ChevronLeft className="h-5 w-5" />
203
+ )}
204
+ <span>{t('common.collapseSidebar')}</span>
205
+ </SidebarMenuButton>
206
+ </SidebarMenuItem>
207
+ )}
208
+ </SidebarMenu>
209
+ </SidebarGroupContent>
210
+ </SidebarGroup>
211
+ </SidebarFooter>
212
+ </Sidebar>
213
+ );
214
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { LucideIcon } from 'lucide-react';
3
+ import { Card, CardContent } from './ui/card';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface EmptyStateProps {
7
+ icon: LucideIcon;
8
+ title: string;
9
+ description?: string;
10
+ action?: React.ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ export function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps) {
15
+ return (
16
+ <Card className={cn('flex flex-col items-center justify-center py-16 px-6', className)}>
17
+ <CardContent className="flex flex-col items-center text-center">
18
+ <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
19
+ <Icon className="h-8 w-8 text-muted-foreground" />
20
+ </div>
21
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
22
+ {title}
23
+ </h3>
24
+ {description && (
25
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-6 max-w-md">
26
+ {description}
27
+ </p>
28
+ )}
29
+ {action && <div>{action}</div>}
30
+ </CardContent>
31
+ </Card>
32
+ );
33
+ }
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Bookmark as BookmarkIcon } from 'lucide-react';
3
+ import { fetchFavicon } from '../utils/favicon';
4
+
5
+ interface FaviconProps {
6
+ url: string;
7
+ className?: string;
8
+ size?: number;
9
+ }
10
+
11
+ export default function Favicon({ url, className = '', size = 20 }: FaviconProps) {
12
+ const [faviconUrl, setFaviconUrl] = useState<string>('');
13
+ const [error, setError] = useState(false);
14
+ const [loading, setLoading] = useState(true);
15
+
16
+ useEffect(() => {
17
+ if (url) {
18
+ setLoading(true);
19
+ setError(false);
20
+ setFaviconUrl('');
21
+ fetchFavicon(url)
22
+ .then((favicon) => {
23
+ if (favicon) {
24
+ setFaviconUrl(favicon);
25
+ setError(false);
26
+ } else {
27
+ setError(true);
28
+ setFaviconUrl('');
29
+ }
30
+ setLoading(false);
31
+ })
32
+ .catch(() => {
33
+ setError(true);
34
+ setFaviconUrl('');
35
+ setLoading(false);
36
+ });
37
+ } else {
38
+ setError(true);
39
+ setFaviconUrl('');
40
+ setLoading(false);
41
+ }
42
+ }, [url]);
43
+
44
+ const handleImageError = () => {
45
+ setError(true);
46
+ setFaviconUrl('');
47
+ setLoading(false);
48
+ };
49
+
50
+ if (loading) {
51
+ return (
52
+ <div className={`flex items-center justify-center ${className}`} style={{ width: `${size}px`, height: `${size}px` }}>
53
+ <div className="w-3 h-3 border-2 border-input border-t-primary rounded-full animate-spin" />
54
+ </div>
55
+ );
56
+ }
57
+
58
+ if (error || !faviconUrl) {
59
+ return (
60
+ <div className={`flex items-center justify-center ${className}`} style={{ width: `${size}px`, height: `${size}px` }}>
61
+ <BookmarkIcon className="text-primary" style={{ width: `${size}px`, height: `${size}px` }} />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <img
68
+ src={faviconUrl}
69
+ alt=""
70
+ className={`object-contain ${className}`}
71
+ style={{ width: `${size}px`, height: `${size}px`, minWidth: `${size}px`, minHeight: `${size}px` }}
72
+ onError={handleImageError}
73
+ loading="lazy"
74
+ />
75
+ );
76
+ }
@@ -0,0 +1,60 @@
1
+ import { X } from 'lucide-react';
2
+
3
+ export interface FilterChipItem {
4
+ key: string;
5
+ label: string;
6
+ ariaLabel: string;
7
+ }
8
+
9
+ interface FilterChipsProps {
10
+ chips: FilterChipItem[];
11
+ onRemove: (key: string) => void;
12
+ onClearAll: () => void;
13
+ clearAllLabel: string;
14
+ clearAllAriaLabel: string;
15
+ }
16
+
17
+ export function FilterChips({ chips, onRemove, onClearAll, clearAllLabel, clearAllAriaLabel }: FilterChipsProps) {
18
+ if (chips.length === 0) return null;
19
+
20
+ return (
21
+ <div className="flex flex-wrap items-center gap-2">
22
+ {chips.map(({ key, label, ariaLabel }) => (
23
+ <span
24
+ key={key}
25
+ className="inline-flex items-center gap-1.5 pl-2.5 pr-1.5 py-1 rounded-md bg-muted text-muted-foreground text-sm border border-border"
26
+ >
27
+ <span>{label}</span>
28
+ <button
29
+ type="button"
30
+ onClick={() => onRemove(key)}
31
+ onKeyDown={(e) => {
32
+ if (e.key === 'Enter' || e.key === ' ') {
33
+ e.preventDefault();
34
+ onRemove(key);
35
+ }
36
+ }}
37
+ className="p-0.5 rounded hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
38
+ aria-label={ariaLabel}
39
+ >
40
+ <X className="h-3.5 w-3.5" />
41
+ </button>
42
+ </span>
43
+ ))}
44
+ <button
45
+ type="button"
46
+ onClick={onClearAll}
47
+ onKeyDown={(e) => {
48
+ if (e.key === 'Enter' || e.key === ' ') {
49
+ e.preventDefault();
50
+ onClearAll();
51
+ }
52
+ }}
53
+ className="text-sm text-muted-foreground hover:text-foreground underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1"
54
+ aria-label={clearAllAriaLabel}
55
+ >
56
+ {clearAllLabel}
57
+ </button>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,207 @@
1
+ import React from 'react';
2
+ import * as LucideIcons from 'lucide-react';
3
+ import { Folder as DefaultFolderIcon } from 'lucide-react';
4
+
5
+ interface FolderIconProps {
6
+ iconName?: string | null;
7
+ className?: string;
8
+ size?: number;
9
+ }
10
+
11
+ // Get all available icon names from lucide-react
12
+ // Filter out non-icon exports and get all icon component names
13
+ // Lucide icons are React forward ref components (objects with $$typeof and render method)
14
+ function getAllIconNames(): string[] {
15
+ const iconNames: string[] = [];
16
+ const excludeNames = new Set([
17
+ 'createLucideIcon',
18
+ 'Icon',
19
+ 'IconNode',
20
+ 'IconProps',
21
+ 'LucideProps',
22
+ 'default',
23
+ // Exclude non-icon exports
24
+ 'defaultProps',
25
+ 'forwardRef',
26
+ 'memo',
27
+ 'lazy',
28
+ 'Suspense',
29
+ 'Fragment',
30
+ 'StrictMode',
31
+ ]);
32
+
33
+ for (const name in LucideIcons) {
34
+ // Skip excluded names
35
+ if (excludeNames.has(name)) continue;
36
+
37
+ // Skip names starting with lowercase (likely not icon components)
38
+ if (name[0] && name[0] === name[0].toLowerCase()) continue;
39
+
40
+ // Skip names ending with 'Icon' (these are typically type exports)
41
+ if (name.endsWith('Icon')) continue;
42
+
43
+ const component = (LucideIcons as any)[name];
44
+
45
+ // Check if it's a valid React component
46
+ // Lucide icons can be either:
47
+ // 1. Function components (typeof === 'function')
48
+ // 2. Forward ref components (object with $$typeof and render method)
49
+ const isFunctionComponent = typeof component === 'function';
50
+ const isForwardRefComponent = component &&
51
+ typeof component === 'object' &&
52
+ (component.$$typeof || component.render);
53
+
54
+ if (isFunctionComponent || isForwardRefComponent) {
55
+ iconNames.push(name);
56
+ }
57
+ }
58
+
59
+ return iconNames.sort();
60
+ }
61
+
62
+ // Cache the icon names
63
+ let cachedIconNames: string[] | null = null;
64
+
65
+ export function getAllIcons(): string[] {
66
+ if (!cachedIconNames) {
67
+ cachedIconNames = getAllIconNames();
68
+ }
69
+ return cachedIconNames;
70
+ }
71
+
72
+ // Popular/recommended icons for quick access
73
+ const popularIcons = [
74
+ 'Folder',
75
+ 'FolderOpen',
76
+ 'Archive',
77
+ 'Briefcase',
78
+ 'FileText',
79
+ 'Image',
80
+ 'Video',
81
+ 'Music',
82
+ 'Code',
83
+ 'Database',
84
+ 'Book',
85
+ 'GraduationCap',
86
+ 'Heart',
87
+ 'Star',
88
+ 'Home',
89
+ 'Calendar',
90
+ 'Mail',
91
+ 'Settings',
92
+ 'Users',
93
+ 'Package',
94
+ 'Wrench',
95
+ 'Tool',
96
+ 'Hammer',
97
+ 'Screwdriver',
98
+ 'FolderPlus',
99
+ 'FolderMinus',
100
+ 'FolderCheck',
101
+ 'FolderX',
102
+ 'File',
103
+ 'FileCode',
104
+ 'FileImage',
105
+ 'FileVideo',
106
+ 'FileAudio',
107
+ 'FileSpreadsheet',
108
+ 'FileType',
109
+ 'FolderKanban',
110
+ 'FolderTree',
111
+ 'FolderGit',
112
+ 'FolderGit2',
113
+ 'FolderSearch',
114
+ 'FolderSymlink',
115
+ 'FolderUp',
116
+ 'FolderDown',
117
+ 'FolderInput',
118
+ 'FolderOutput',
119
+ 'FolderRoot',
120
+ 'FolderLock',
121
+ 'FolderUnlock',
122
+ 'FolderHeart',
123
+ 'FolderKey',
124
+ 'FolderPen',
125
+ 'FolderArchive',
126
+ 'FolderOpenDot',
127
+ 'FolderDot',
128
+ 'FolderSync',
129
+ 'FolderClock',
130
+ 'FolderCog',
131
+ 'FolderPlus2',
132
+ 'FolderMinus2',
133
+ 'FolderCheck2',
134
+ 'FolderX2',
135
+ 'FolderQuestion',
136
+ 'FolderWarning',
137
+ 'FolderAlert',
138
+ 'FolderBan',
139
+ 'FolderOff',
140
+ 'FolderOn',
141
+ 'FolderUp2',
142
+ 'FolderDown2',
143
+ 'FolderInput2',
144
+ 'FolderOutput2',
145
+ 'FolderRoot2',
146
+ 'FolderLock2',
147
+ 'FolderUnlock2',
148
+ 'FolderHeart2',
149
+ 'FolderKey2',
150
+ 'FolderPen2',
151
+ 'FolderArchive2',
152
+ 'FolderOpenDot2',
153
+ 'FolderDot2',
154
+ 'FolderSync2',
155
+ 'FolderClock2',
156
+ 'FolderCog2',
157
+ 'FolderPlus2',
158
+ 'FolderMinus2',
159
+ 'FolderCheck2',
160
+ 'FolderX2',
161
+ 'FolderQuestion2',
162
+ 'FolderWarning2',
163
+ 'FolderAlert2',
164
+ 'FolderBan2',
165
+ 'FolderOff2',
166
+ 'FolderOn2',
167
+ ];
168
+
169
+ export default function FolderIcon({ iconName, className = '', size = 20 }: FolderIconProps) {
170
+ if (!iconName) {
171
+ return <DefaultFolderIcon className={className} style={{ width: `${size}px`, height: `${size}px` }} />;
172
+ }
173
+
174
+ // Try to get the icon from lucide-react (exact match first)
175
+ let IconComponent = (LucideIcons as any)[iconName] as React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
176
+
177
+ // If exact match not found, try case-insensitive lookup
178
+ if (!IconComponent) {
179
+ const iconNameLower = iconName.toLowerCase();
180
+ for (const key in LucideIcons) {
181
+ if (key.toLowerCase() === iconNameLower) {
182
+ const candidate = (LucideIcons as any)[key];
183
+ // Check if it's a valid component (function or forward ref object)
184
+ const isValid = typeof candidate === 'function' ||
185
+ (candidate && typeof candidate === 'object' && (candidate.$$typeof || candidate.render));
186
+ if (isValid) {
187
+ IconComponent = candidate as React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Verify IconComponent is valid before rendering
195
+ const isValidComponent = IconComponent &&
196
+ (typeof IconComponent === 'function' ||
197
+ (typeof IconComponent === 'object' && IconComponent !== null && ((IconComponent as any).$$typeof || (IconComponent as any).render)));
198
+
199
+ if (isValidComponent) {
200
+ return <IconComponent className={className} style={{ width: `${size}px`, height: `${size}px` }} />;
201
+ }
202
+
203
+ // Fallback to default folder icon
204
+ return <DefaultFolderIcon className={className} style={{ width: `${size}px`, height: `${size}px` }} />;
205
+ }
206
+
207
+ export { popularIcons };