From d7548a4bab5f1c44392917fec1f10765ce2bbdd0 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:32:41 +0200 Subject: [PATCH] refactor: migrate icon set from Lucide to Tabler Icons --- app/(main)/[locale]/auth/callback/page.tsx | 2 +- app/(main)/[locale]/error.tsx | 2 +- app/(main)/[locale]/login/page.tsx | 2 +- .../[locale]/plugins/oauth/callback/page.tsx | 2 +- .../admin/_tabs/_jmap-servers-section.tsx | 2 +- app/(main)/admin/_tabs/auth.tsx | 2 +- app/(main)/admin/_tabs/branding.tsx | 2 +- app/(main)/admin/_tabs/dashboard.tsx | 2 +- app/(main)/admin/_tabs/logs.tsx | 2 +- app/(main)/admin/_tabs/marketplace.tsx | 2 +- .../admin/_tabs/plugin-config-panel.tsx | 2 +- app/(main)/admin/_tabs/plugins.tsx | 2 +- app/(main)/admin/_tabs/policy.tsx | 10 +- app/(main)/admin/_tabs/settings.tsx | 2 +- app/(main)/admin/_tabs/telemetry.tsx | 2 +- app/(main)/admin/_tabs/themes.tsx | 2 +- app/(main)/admin/_tabs/version.tsx | 2 +- app/(main)/admin/change-password/page.tsx | 2 +- app/(main)/admin/layout.tsx | 2 +- app/(main)/admin/login/page.tsx | 2 +- app/(main)/admin/marketplace/[slug]/page.tsx | 2 +- app/(main)/global-error.tsx | 2 +- app/(main)/setup/page.tsx | 2 +- app/globals.css | 22 +- components/auth/oauth-status-card.tsx | 6 +- components/calendar/calendar-agenda-view.tsx | 2 +- components/calendar/calendar-app.tsx | 2 +- components/calendar/calendar-day-view.tsx | 2 +- components/calendar/calendar-kind-picker.tsx | 2 +- .../calendar/calendar-sidebar-panel.tsx | 2 +- components/calendar/calendar-toolbar.tsx | 2 +- components/calendar/calendar-week-view.tsx | 2 +- components/calendar/create-calendar-modal.tsx | 2 +- .../calendar/empty-space-context-menu.tsx | 2 +- components/calendar/event-card.tsx | 2 +- components/calendar/event-context-menu.tsx | 2 +- components/calendar/event-detail-popover.tsx | 2 +- components/calendar/event-modal.tsx | 2 +- components/calendar/ical-import-modal.tsx | 2 +- .../calendar/ical-subscription-modal.tsx | 2 +- components/calendar/mini-calendar.tsx | 2 +- .../calendar/participant-availability.tsx | 2 +- components/calendar/participant-input.tsx | 2 +- .../calendar/recurrence-scope-dialog.tsx | 2 +- components/calendar/task-list-view.tsx | 2 +- components/calendar/task-modal.tsx | 2 +- components/calendar/task-toolbar.tsx | 2 +- components/contacts/contact-activity.tsx | 2 +- components/contacts/contact-context-menu.tsx | 2 +- components/contacts/contact-detail.tsx | 2 +- components/contacts/contact-form.tsx | 2 +- components/contacts/contact-group-detail.tsx | 2 +- components/contacts/contact-group-form.tsx | 2 +- components/contacts/contact-group-list.tsx | 2 +- components/contacts/contact-import-dialog.tsx | 2 +- components/contacts/contact-list-item.tsx | 2 +- components/contacts/contact-list.tsx | 2 +- components/contacts/contacts-app.tsx | 2 +- components/contacts/contacts-sidebar.tsx | 2 +- components/email/attachment-chips.tsx | 2 +- .../email/calendar-invitation-banner.tsx | 2 +- components/email/email-composer.tsx | 2 +- components/email/email-context-menu.tsx | 2 +- components/email/email-hover-actions.tsx | 2 +- components/email/email-identity-badge.tsx | 2 +- components/email/email-list.tsx | 2 +- components/email/email-viewer.tsx | 2 +- components/email/message-list-tabs.tsx | 4 +- components/email/read-receipt-banner.tsx | 2 +- components/email/recipient-popover.tsx | 2 +- components/email/rich-text-editor.tsx | 2 +- components/email/selectable-avatar.tsx | 2 +- components/email/tag-badge.tsx | 2 +- components/email/tag-picker.tsx | 2 +- components/email/thread-conversation-view.tsx | 2 +- components/email/thread-email-item.tsx | 2 +- components/email/thread-list-item.tsx | 8 +- components/email/unsubscribe-banner.tsx | 2 +- components/error/error-fallbacks.tsx | 2 +- components/files/eml-preview.tsx | 2 +- components/files/file-browser.tsx | 4 +- components/files/file-preview-modal.tsx | 2 +- components/files/file-upload-area.tsx | 2 +- components/files/files-app.tsx | 4 +- components/files/files-settings-dialog.tsx | 2 +- components/files/folder-tree-sidebar.tsx | 2 +- components/files/image-preview-modal.tsx | 2 +- components/files/pdf-mobile-viewer.tsx | 2 +- components/files/wopi-editor.tsx | 2 +- components/filters/filter-rule-modal.tsx | 2 +- components/filters/sieve-editor-modal.tsx | 2 +- .../global-search/global-search-palette.tsx | 2 +- .../global-search/palette-result-row.tsx | 2 +- components/global-search/search-preview.tsx | 2 +- .../global-search/search-result-list.tsx | 2 +- components/global-search/search-tab-body.tsx | 2 +- components/icons/__tests__/named.test.ts | 62 +++ components/icons/index.ts | 240 ++++++++++ components/icons/named.tsx | 429 ++++++++++++++++++ .../identity/identity-manager-modal.tsx | 2 +- components/identity/sub-address-helper.tsx | 2 +- components/keyboard-shortcuts-modal.tsx | 2 +- components/layout/account-switcher.tsx | 2 +- components/layout/icon-picker.tsx | 86 +--- components/layout/inline-app-view.tsx | 2 +- components/layout/mailbox-context-menu.tsx | 2 +- components/layout/mailbox-share-dialog.tsx | 2 +- components/layout/mobile-header.tsx | 2 +- components/layout/navigation-rail.tsx | 8 +- components/layout/sidebar-apps-modal.tsx | 10 +- components/layout/sidebar.tsx | 2 +- components/mail/mail-app.tsx | 2 +- components/pro/pro-folder-tab-body.tsx | 2 +- components/pro/pro-tab-bar.tsx | 4 +- .../protocol/protocol-account-picker.tsx | 2 +- components/push-notification-prompt.tsx | 2 +- components/pwa-install-prompt.tsx | 2 +- components/search/search-box.tsx | 2 +- components/search/search-chips.tsx | 2 +- components/search/search-suggestions.tsx | 2 +- components/settings/about-data-settings.tsx | 2 +- .../settings/account-security-settings.tsx | 2 +- components/settings/account-settings.tsx | 2 +- .../address-book-management-settings.tsx | 2 +- components/settings/appearance-settings.tsx | 2 +- .../settings/calendar-management-settings.tsx | 2 +- components/settings/composing-settings.tsx | 2 +- components/settings/contacts-settings.tsx | 2 +- .../settings/content-senders-settings.tsx | 2 +- components/settings/downloads-settings.tsx | 2 +- components/settings/files-settings.tsx | 2 +- components/settings/filter-settings.tsx | 2 +- components/settings/folder-settings.tsx | 12 +- components/settings/keyword-settings.tsx | 2 +- components/settings/layout-settings.tsx | 2 +- .../settings/message-list-order-settings.tsx | 2 +- components/settings/notification-settings.tsx | 2 +- components/settings/plugins-settings.tsx | 2 +- components/settings/reading-settings.tsx | 2 +- components/settings/settings-app.tsx | 8 +- components/settings/settings-section.tsx | 2 +- .../settings/share-collection-dialog.tsx | 2 +- components/settings/sidebar-apps-settings.tsx | 10 +- components/settings/spam-siege-game.tsx | 2 +- components/settings/template-settings.tsx | 2 +- components/settings/themes-settings.tsx | 2 +- components/settings/vacation-settings.tsx | 2 +- .../templates/placeholder-fill-modal.tsx | 2 +- components/templates/template-form.tsx | 2 +- .../templates/template-manager-modal.tsx | 2 +- components/templates/template-picker.tsx | 2 +- components/totp-reauth-dialog.tsx | 2 +- components/trusted-senders-modal.tsx | 2 +- components/ui/confirm-dialog.tsx | 2 +- components/ui/context-menu.tsx | 2 +- components/ui/language-switcher.tsx | 2 +- components/ui/pull-to-refresh-indicator.tsx | 2 +- components/ui/toast.tsx | 2 +- components/ui/welcome-banner.tsx | 2 +- lib/__tests__/sidebar-apps.test.ts | 4 +- lib/email-composer-utils.ts | 4 +- lib/plugin-types.ts | 6 +- lib/sidebar-apps.ts | 8 +- lib/theme-flat-fields.ts | 7 + package-lock.json | 46 +- package.json | 2 +- 166 files changed, 1005 insertions(+), 283 deletions(-) create mode 100644 components/icons/__tests__/named.test.ts create mode 100644 components/icons/index.ts create mode 100644 components/icons/named.tsx diff --git a/app/(main)/[locale]/auth/callback/page.tsx b/app/(main)/[locale]/auth/callback/page.tsx index a52a17ead..9acb765b1 100644 --- a/app/(main)/[locale]/auth/callback/page.tsx +++ b/app/(main)/[locale]/auth/callback/page.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"; import { useAuthStore } from "@/stores/auth-store"; import { apiFetch, getPathPrefix, toRouterPath } from "@/lib/browser-navigation"; import { buildSettingsPath } from "@/lib/deep-links"; -import { Loader2, AlertCircle } from "lucide-react"; +import { Loader2, AlertCircle } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { useParams } from "next/navigation"; diff --git a/app/(main)/[locale]/error.tsx b/app/(main)/[locale]/error.tsx index 7ddd9107e..e8a0caeac 100644 --- a/app/(main)/[locale]/error.tsx +++ b/app/(main)/[locale]/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useTranslations } from "next-intl"; -import { AlertCircle, RefreshCw, Home } from "lucide-react"; +import { AlertCircle, RefreshCw, Home } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { useRouter } from "@/i18n/navigation"; diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index ade7b925d..be851d606 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -14,7 +14,7 @@ import { useConfig } from "@/hooks/use-config"; import { useMenuNavigation } from "@/hooks/use-menu-navigation"; import { apiFetch, getPathPrefix, toRouterPath, withBasePath } from "@/lib/browser-navigation"; import { cn } from "@/lib/utils"; -import { AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor, Check, Shield, Play, Copy } from "lucide-react"; +import { AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor, Check, Shield, Play, Copy } from "@/components/icons"; import { type OAuthMetadata } from "@/lib/oauth/discovery"; import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce"; import { useUpdateStore, selectBanner } from "@/stores/update-store"; diff --git a/app/(main)/[locale]/plugins/oauth/callback/page.tsx b/app/(main)/[locale]/plugins/oauth/callback/page.tsx index 7ec25094c..12d229bff 100644 --- a/app/(main)/[locale]/plugins/oauth/callback/page.tsx +++ b/app/(main)/[locale]/plugins/oauth/callback/page.tsx @@ -32,7 +32,7 @@ import { PLUGIN_OAUTH_STORAGE_KEY, type OAuthCallbackPayload, } from "@/lib/plugin-oauth"; -import { CheckCircle2, XCircle } from "lucide-react"; +import { CheckCircle2, XCircle } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useSearchParams } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; diff --git a/app/(main)/admin/_tabs/_jmap-servers-section.tsx b/app/(main)/admin/_tabs/_jmap-servers-section.tsx index ef4e00ac2..da9f76aec 100644 --- a/app/(main)/admin/_tabs/_jmap-servers-section.tsx +++ b/app/(main)/admin/_tabs/_jmap-servers-section.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { Plus, Trash2, RotateCcw, ChevronDown, ChevronRight } from 'lucide-react'; +import { Plus, Trash2, RotateCcw, ChevronDown, ChevronRight } from '@/components/icons'; import type { JmapServerEntry } from '@/lib/admin/jmap-servers'; interface Props { diff --git a/app/(main)/admin/_tabs/auth.tsx b/app/(main)/admin/_tabs/auth.tsx index 523d979c6..0db191e82 100644 --- a/app/(main)/admin/_tabs/auth.tsx +++ b/app/(main)/admin/_tabs/auth.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Save, Loader2, RotateCcw, Sparkles } from 'lucide-react'; +import { Save, Loader2, RotateCcw, Sparkles } from '@/components/icons'; import { apiFetch } from '@/lib/browser-navigation'; interface ConfigEntry { diff --git a/app/(main)/admin/_tabs/branding.tsx b/app/(main)/admin/_tabs/branding.tsx index bfa87c130..c93da7367 100644 --- a/app/(main)/admin/_tabs/branding.tsx +++ b/app/(main)/admin/_tabs/branding.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { Save, Loader2, RotateCcw, ImageIcon, Upload, Trash2, Globe, Plus, X } from 'lucide-react'; +import { Save, Loader2, RotateCcw, ImageIcon, Upload, Trash2, Globe, Plus, X } from '@/components/icons'; import { apiFetch, withBasePath } from '@/lib/browser-navigation'; import { BRANDING_OVERRIDE_KEYS, diff --git a/app/(main)/admin/_tabs/dashboard.tsx b/app/(main)/admin/_tabs/dashboard.tsx index 9074e4744..3356c8d9e 100644 --- a/app/(main)/admin/_tabs/dashboard.tsx +++ b/app/(main)/admin/_tabs/dashboard.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { AlertTriangle } from 'lucide-react'; +import { AlertTriangle } from '@/components/icons'; import { SettingsSection, SettingItem, ToggleSwitch } from '@/components/settings/settings-section'; import type { AuditEntry } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; diff --git a/app/(main)/admin/_tabs/logs.tsx b/app/(main)/admin/_tabs/logs.tsx index 00a8bffdb..181891e25 100644 --- a/app/(main)/admin/_tabs/logs.tsx +++ b/app/(main)/admin/_tabs/logs.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; -import { RefreshCw } from 'lucide-react'; +import { RefreshCw } from '@/components/icons'; import type { AuditEntry } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; diff --git a/app/(main)/admin/_tabs/marketplace.tsx b/app/(main)/admin/_tabs/marketplace.tsx index 6bf82c389..d5bc588ae 100644 --- a/app/(main)/admin/_tabs/marketplace.tsx +++ b/app/(main)/admin/_tabs/marketplace.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react'; import Link from 'next/link'; -import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Eye, AlertTriangle, ArrowUpCircle } from 'lucide-react'; +import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Eye, AlertTriangle, ArrowUpCircle } from '@/components/icons'; import { apiFetch } from '@/lib/browser-navigation'; import { compareVersions, isVersionSatisfied } from '@/lib/version-compare'; diff --git a/app/(main)/admin/_tabs/plugin-config-panel.tsx b/app/(main)/admin/_tabs/plugin-config-panel.tsx index 2cc5fe7fd..d396c086a 100644 --- a/app/(main)/admin/_tabs/plugin-config-panel.tsx +++ b/app/(main)/admin/_tabs/plugin-config-panel.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Puzzle, ArrowLeft, Loader2, Eye, EyeOff } from 'lucide-react'; +import { Puzzle, ArrowLeft, Loader2, Eye, EyeOff } from '@/components/icons'; import { apiFetch } from '@/lib/browser-navigation'; import { usePluginSlotOffers } from '@/hooks/use-plugin-slot-offers'; import { PluginIframeSlot } from '@/components/plugins/plugin-iframe-slot'; diff --git a/app/(main)/admin/_tabs/plugins.tsx b/app/(main)/admin/_tabs/plugins.tsx index 5e03ccee6..55b25b642 100644 --- a/app/(main)/admin/_tabs/plugins.tsx +++ b/app/(main)/admin/_tabs/plugins.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState, useRef } from 'react'; -import { Upload, Trash2, Power, PowerOff, AlertTriangle, Loader2, Package, Save, Shield, Lock, LockOpen, Settings } from 'lucide-react'; +import { Upload, Trash2, Power, PowerOff, AlertTriangle, Loader2, Package, Save, Shield, Lock, LockOpen, Settings } from '@/components/icons'; import type { SettingsPolicy } from '@/lib/admin/types'; import { DEFAULT_POLICY } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; diff --git a/app/(main)/admin/_tabs/policy.tsx b/app/(main)/admin/_tabs/policy.tsx index 98ad51957..e40904a5c 100644 --- a/app/(main)/admin/_tabs/policy.tsx +++ b/app/(main)/admin/_tabs/policy.tsx @@ -1,8 +1,8 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Save, Loader2, Lock, Plus, Trash2, ArrowUp, ArrowDown } from 'lucide-react'; -import { icons as lucideIcons, type LucideIcon } from 'lucide-react'; +import { Save, Loader2, Lock, Plus, Trash2, ArrowUp, ArrowDown } from '@/components/icons'; +import { iconForName } from '@/components/icons'; import type { SettingsPolicy, FeatureGates, PushRelayOption, AdminSidebarApp } from '@/lib/admin/types'; import { DEFAULT_FEATURE_GATES, DEFAULT_POLICY } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; @@ -455,7 +455,7 @@ export function PolicyTab() { )} {defaultSidebarApps.map((app, index) => { - const AppIcon = lucideIcons[app.icon as keyof typeof lucideIcons] as LucideIcon | undefined; + const AppIcon = iconForName(app.icon); const urlInvalid = app.url.trim().length > 0 && !isValidDefaultAppUrl(app.url); return (
@@ -493,7 +493,7 @@ export function PolicyTab() {
@@ -504,7 +504,7 @@ export function PolicyTab() { maxLength={64} value={app.icon} onChange={(e) => updateDefaultApp(index, { icon: e.target.value })} - placeholder="Globe" + placeholder="world" className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" /> ` class per icon, so we target the + Tabler emits a `tabler-icon-` class per icon, so we target the directional ones only — vertical chevrons (up/down) are intentionally left. */ -[dir="rtl"] .lucide-chevron-left, -[dir="rtl"] .lucide-chevron-right, -[dir="rtl"] .lucide-chevrons-left, -[dir="rtl"] .lucide-chevrons-right, -[dir="rtl"] .lucide-arrow-left, -[dir="rtl"] .lucide-arrow-right, -[dir="rtl"] .lucide-arrow-big-left, -[dir="rtl"] .lucide-arrow-big-right, -[dir="rtl"] .lucide-panel-left, -[dir="rtl"] .lucide-panel-right { +[dir="rtl"] .tabler-icon-chevron-left, +[dir="rtl"] .tabler-icon-chevron-right, +[dir="rtl"] .tabler-icon-chevrons-left, +[dir="rtl"] .tabler-icon-chevrons-right, +[dir="rtl"] .tabler-icon-arrow-left, +[dir="rtl"] .tabler-icon-arrow-right, +[dir="rtl"] .tabler-icon-arrow-big-left, +[dir="rtl"] .tabler-icon-arrow-big-right, +[dir="rtl"] .tabler-icon-layout-sidebar-left, +[dir="rtl"] .tabler-icon-layout-sidebar-right { transform: scaleX(-1); } diff --git a/components/auth/oauth-status-card.tsx b/components/auth/oauth-status-card.tsx index d4b6348bf..df54e211a 100644 --- a/components/auth/oauth-status-card.tsx +++ b/components/auth/oauth-status-card.tsx @@ -9,8 +9,8 @@ import { useConfig } from "@/hooks/use-config"; import { withBasePath } from "@/lib/browser-navigation"; import { cn } from "@/lib/utils"; import { useThemeStore } from "@/stores/theme-store"; -import type { LucideIcon } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import type { AppIcon } from "@/components/icons"; +import { Loader2 } from "@/components/icons"; export interface OAuthStatusAction { label: string; @@ -19,7 +19,7 @@ export interface OAuthStatusAction { export interface OAuthStatusCardProps { /** Icon badge shown below the logo (defaults to a spinner) */ - icon?: LucideIcon; + icon?: AppIcon; /** Tailwind color classes for the icon badge, e.g. "bg-green-500/10 text-green-600" */ iconClassName?: string; title: string; diff --git a/components/calendar/calendar-agenda-view.tsx b/components/calendar/calendar-agenda-view.tsx index 615037049..4c39cb2c3 100644 --- a/components/calendar/calendar-agenda-view.tsx +++ b/components/calendar/calendar-agenda-view.tsx @@ -4,7 +4,7 @@ import { useMemo, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter"; import { format, isTomorrow, startOfDay } from "date-fns"; -import { MapPin, Users } from "lucide-react"; +import { MapPin, Users } from "@/components/icons"; import { cn } from "@/lib/utils"; import { getEventColor } from "./event-card"; import { getEventDayBounds, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index 57ccdbfae..d8b3ae5fe 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback, useRef, useMemo, type TouchEvent as ReactTouchEvent } from "react"; import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; -import { Plus } from "lucide-react"; +import { Plus } from "@/components/icons"; import { addMonths, subMonths, addWeeks, subWeeks, addDays, subDays, format, parseISO, diff --git a/components/calendar/calendar-day-view.tsx b/components/calendar/calendar-day-view.tsx index fa9508b17..945480eb0 100644 --- a/components/calendar/calendar-day-view.tsx +++ b/components/calendar/calendar-day-view.tsx @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl"; import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter"; import { format, isSameDay, parseISO, eachDayOfInterval, differenceInCalendarDays } from "date-fns"; import { cn } from "@/lib/utils"; -import { Check } from "lucide-react"; +import { Check } from "@/components/icons"; import { EventCard } from "./event-card"; import { QuickEventInput } from "./quick-event-input"; import { formatSnapTime, getEventDayBounds, getPrimaryCalendarId, isTimedEventFullDayOnDate, layoutOverlappingEvents } from "@/lib/calendar-utils"; diff --git a/components/calendar/calendar-kind-picker.tsx b/components/calendar/calendar-kind-picker.tsx index ac63b5865..6ff901142 100644 --- a/components/calendar/calendar-kind-picker.tsx +++ b/components/calendar/calendar-kind-picker.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { Calendar as CalendarIcon, CheckSquare, Layers } from "lucide-react"; +import { Calendar as CalendarIcon, CheckSquare, Layers } from "@/components/icons"; import { cn } from "@/lib/utils"; import type { CalendarComponentType } from "@/lib/jmap/types"; diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index 2f7ed28ab..3ad161ce0 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; -import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Star, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "lucide-react"; +import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Star, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "@/components/icons"; import { cn, formatDateTime } from "@/lib/utils"; import type { Calendar } from "@/lib/jmap/types"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index 921b12360..9d46f9286 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft, Menu } from "lucide-react"; +import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft, Menu } from "@/components/icons"; import { startOfWeek } from "date-fns"; import { cn } from "@/lib/utils"; import type { CalendarViewMode } from "@/stores/calendar-store"; diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx index a67054c69..522643d0d 100644 --- a/components/calendar/calendar-week-view.tsx +++ b/components/calendar/calendar-week-view.tsx @@ -7,7 +7,7 @@ import { startOfWeek, format, isSameDay, parseISO, eachDayOfInterval, differenceInCalendarDays, } from "date-fns"; import { cn } from "@/lib/utils"; -import { Check } from "lucide-react"; +import { Check } from "@/components/icons"; import { EventCard } from "./event-card"; import { QuickEventInput } from "./quick-event-input"; import { buildTimedFullDayWeekSegments, buildWeekSegmentsRaw, formatSnapTime, getEventDayBounds, getPrimaryCalendarId, isTimedEventFullDayOnDate, layoutOverlappingEvents, packWeekSegments } from "@/lib/calendar-utils"; diff --git a/components/calendar/create-calendar-modal.tsx b/components/calendar/create-calendar-modal.tsx index 31a2dfd44..d18a190f6 100644 --- a/components/calendar/create-calendar-modal.tsx +++ b/components/calendar/create-calendar-modal.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { X, Loader2, Calendar as CalendarIcon } from "lucide-react"; +import { X, Loader2, Calendar as CalendarIcon } from "@/components/icons"; import type { IJMAPClient } from "@/lib/jmap/client-interface"; import { useCalendarStore } from "@/stores/calendar-store"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; diff --git a/components/calendar/empty-space-context-menu.tsx b/components/calendar/empty-space-context-menu.tsx index c45b1ce36..ab2b79124 100644 --- a/components/calendar/empty-space-context-menu.tsx +++ b/components/calendar/empty-space-context-menu.tsx @@ -6,7 +6,7 @@ import { ContextMenuItem, ContextMenuSeparator, } from "@/components/ui/context-menu"; -import { Plus, CalendarDays, CheckSquare, Clock } from "lucide-react"; +import { Plus, CalendarDays, CheckSquare, Clock } from "@/components/icons"; interface Position { x: number; diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index b73aaee60..603fb38ca 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl"; import { cn } from "@/lib/utils"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import { format } from "date-fns"; -import { Users } from "lucide-react"; +import { Users } from "@/components/icons"; import { getParticipantCount } from "@/lib/calendar-participants"; import { getEventEndDate, getEventStartDate } from "@/lib/calendar-utils"; import { useSettingsStore } from "@/stores/settings-store"; diff --git a/components/calendar/event-context-menu.tsx b/components/calendar/event-context-menu.tsx index 429293df5..9bc109a3c 100644 --- a/components/calendar/event-context-menu.tsx +++ b/components/calendar/event-context-menu.tsx @@ -14,7 +14,7 @@ import { Link as LinkIcon, CalendarArrowUp as CalendarLinkIcon, Trash2, -} from "lucide-react"; +} from "@/components/icons"; import type { CalendarEvent } from "@/lib/jmap/types"; import { buildCalendarPath } from "@/lib/deep-links"; import { useCopyLink } from "@/hooks/use-copy-link"; diff --git a/components/calendar/event-detail-popover.tsx b/components/calendar/event-detail-popover.tsx index 651ebb1e4..fc458f9c7 100644 --- a/components/calendar/event-detail-popover.tsx +++ b/components/calendar/event-detail-popover.tsx @@ -8,7 +8,7 @@ import { LinkifiedText } from "@/components/ui/linkified-text"; import { X, Clock, MapPin, Video, Users, Repeat, Bell, AlignLeft, Pencil, Trash2, Copy, Send, Check, -} from "lucide-react"; +} from "@/components/icons"; import { format, isSameDay } from "date-fns"; import { cn } from "@/lib/utils"; import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types"; diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 3d2d0bdac..a05c31f66 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -5,7 +5,7 @@ import { useTranslations, useLocale } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { LinkifiedText } from "@/components/ui/linkified-text"; -import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft, Plus } from "lucide-react"; +import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft, Plus } from "@/components/icons"; import { format, parseISO, addHours, addDays, isSameDay } from "date-fns"; import type { CalendarEvent, Calendar, CalendarParticipant, CalendarEventAlert, CalendarRecurrenceRule } from "@/lib/jmap/types"; import { RecurrenceEditor, buildRecurrenceSummary, isSimpleRecurrenceRule } from "./recurrence-editor"; diff --git a/components/calendar/ical-import-modal.tsx b/components/calendar/ical-import-modal.tsx index dd8abb9da..494d7d7a0 100644 --- a/components/calendar/ical-import-modal.tsx +++ b/components/calendar/ical-import-modal.tsx @@ -3,7 +3,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { X, Upload, Check, Loader2, RefreshCw, Globe } from "lucide-react"; +import { X, Upload, Check, Loader2, RefreshCw, Globe } from "@/components/icons"; import { format } from "date-fns"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import type { IJMAPClient } from '@/lib/jmap/client-interface'; diff --git a/components/calendar/ical-subscription-modal.tsx b/components/calendar/ical-subscription-modal.tsx index d078355af..e1f515e68 100644 --- a/components/calendar/ical-subscription-modal.tsx +++ b/components/calendar/ical-subscription-modal.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { X, Loader2, Globe } from "lucide-react"; +import { X, Loader2, Globe } from "@/components/icons"; import type { IJMAPClient } from '@/lib/jmap/client-interface'; import { useCalendarStore, type ICalSubscription } from "@/stores/calendar-store"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; diff --git a/components/calendar/mini-calendar.tsx b/components/calendar/mini-calendar.tsx index 47421fe7f..10404986b 100644 --- a/components/calendar/mini-calendar.tsx +++ b/components/calendar/mini-calendar.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, Fragment } from "react"; import { useTranslations } from "next-intl"; -import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"; +import { ChevronLeft, ChevronRight, ChevronDown } from "@/components/icons"; import { addMonths, subMonths, addYears, subYears, setMonth, setYear, getISOWeek, getWeek, format, diff --git a/components/calendar/participant-availability.tsx b/components/calendar/participant-availability.tsx index 0df62169e..62c4ff3ac 100644 --- a/components/calendar/participant-availability.tsx +++ b/components/calendar/participant-availability.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslations } from "next-intl"; -import { Loader2 } from "lucide-react"; +import { Loader2 } from "@/components/icons"; import { useAuthStore } from "@/stores/auth-store"; import type { IJMAPClient } from "@/lib/jmap/client-interface"; import type { BusyPeriod, Principal } from "@/lib/jmap/types"; diff --git a/components/calendar/participant-input.tsx b/components/calendar/participant-input.tsx index 3c8dbe92e..83a8031ae 100644 --- a/components/calendar/participant-input.tsx +++ b/components/calendar/participant-input.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle } from "react"; import { useTranslations } from "next-intl"; -import { X } from "lucide-react"; +import { X } from "@/components/icons"; import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/ui/avatar"; import { useContactStore } from "@/stores/contact-store"; diff --git a/components/calendar/recurrence-scope-dialog.tsx b/components/calendar/recurrence-scope-dialog.tsx index 64293f133..c11970f8c 100644 --- a/components/calendar/recurrence-scope-dialog.tsx +++ b/components/calendar/recurrence-scope-dialog.tsx @@ -4,7 +4,7 @@ import { useState, useId } from "react"; import { useTranslations } from "next-intl"; import { useFocusTrap } from "@/hooks/use-focus-trap"; import { Button } from "@/components/ui/button"; -import { Repeat, Trash2 } from "lucide-react"; +import { Repeat, Trash2 } from "@/components/icons"; export type RecurrenceEditScope = "this" | "this_and_future" | "all"; diff --git a/components/calendar/task-list-view.tsx b/components/calendar/task-list-view.tsx index a16006d29..8d896f181 100644 --- a/components/calendar/task-list-view.tsx +++ b/components/calendar/task-list-view.tsx @@ -4,7 +4,7 @@ import { useMemo, useCallback, useState } from "react"; import { useTranslations } from "next-intl"; import { format, parseISO, isBefore, isTomorrow } from "date-fns"; import { displayNow, isDisplayToday } from "@/lib/timezone"; -import { Check, Flag, CalendarDays, ListTodo, Plus } from "lucide-react"; +import { Check, Flag, CalendarDays, ListTodo, Plus } from "@/components/icons"; import { cn } from "@/lib/utils"; import type { CalendarTask, Calendar } from "@/lib/jmap/types"; import type { TaskViewFilter } from "@/stores/task-store"; diff --git a/components/calendar/task-modal.tsx b/components/calendar/task-modal.tsx index ff7280153..8e648c490 100644 --- a/components/calendar/task-modal.tsx +++ b/components/calendar/task-modal.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { X, Trash2, CalendarDays, Bell, Flag } from "lucide-react"; +import { X, Trash2, CalendarDays, Bell, Flag } from "@/components/icons"; import { format, parseISO } from "date-fns"; import type { CalendarTask, Calendar, CalendarEventAlert } from "@/lib/jmap/types"; diff --git a/components/calendar/task-toolbar.tsx b/components/calendar/task-toolbar.tsx index cdda536ef..0619d464f 100644 --- a/components/calendar/task-toolbar.tsx +++ b/components/calendar/task-toolbar.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { Plus } from "lucide-react"; +import { Plus } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { TaskViewFilter } from "@/stores/task-store"; diff --git a/components/contacts/contact-activity.tsx b/components/contacts/contact-activity.tsx index d5d23e166..0285960bb 100644 --- a/components/contacts/contact-activity.tsx +++ b/components/contacts/contact-activity.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslations, useFormatter } from "next-intl"; -import { Loader2 } from "lucide-react"; +import { Loader2 } from "@/components/icons"; import { useRouter } from "@/i18n/navigation"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; diff --git a/components/contacts/contact-context-menu.tsx b/components/contacts/contact-context-menu.tsx index 19b032c93..adfe105c7 100644 --- a/components/contacts/contact-context-menu.tsx +++ b/components/contacts/contact-context-menu.tsx @@ -18,7 +18,7 @@ import { Trash2, Copy, Printer, -} from "lucide-react"; +} from "@/components/icons"; import type { ContactCard } from "@/lib/jmap/types"; import { getContactPrimaryEmail } from "@/stores/contact-store"; import { exportContact } from "./contact-export"; diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx index 8f1579fa4..0a5fc618b 100644 --- a/components/contacts/contact-detail.tsx +++ b/components/contacts/contact-detail.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; -import { Mail, Phone, Building, MapPin, StickyNote, Pencil, Trash2, BookUser, Copy, Send, Globe, Cake, KeyRound, Users, Briefcase, Heart, Languages, Calendar, UserCircle, Download, MoreHorizontal, Printer } from "lucide-react"; +import { Mail, Phone, Building, MapPin, StickyNote, Pencil, Trash2, BookUser, Copy, Send, Globe, Cake, KeyRound, Users, Briefcase, Heart, Languages, Calendar, UserCircle, Download, MoreHorizontal, Printer } from "@/components/icons"; import { Avatar } from "@/components/ui/avatar"; import { MailtoLink } from "@/components/ui/mailto-link"; import { Button } from "@/components/ui/button"; diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx index 74bcf1e9c..9d31800e4 100644 --- a/components/contacts/contact-form.tsx +++ b/components/contacts/contact-form.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; -import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2 } from "lucide-react"; +import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2 } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/ui/avatar"; diff --git a/components/contacts/contact-group-detail.tsx b/components/contacts/contact-group-detail.tsx index 6960e4f70..8f86174f9 100644 --- a/components/contacts/contact-group-detail.tsx +++ b/components/contacts/contact-group-detail.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { Users, Pencil, Trash2, UserMinus, Mail } from "lucide-react"; +import { Users, Pencil, Trash2, UserMinus, Mail } from "@/components/icons"; import { Avatar } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; diff --git a/components/contacts/contact-group-form.tsx b/components/contacts/contact-group-form.tsx index 9ed4c1522..8a8b605e7 100644 --- a/components/contacts/contact-group-form.tsx +++ b/components/contacts/contact-group-form.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from "react"; import { useTranslations } from "next-intl"; -import { Search, Check, X } from "lucide-react"; +import { Search, Check, X } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/ui/avatar"; diff --git a/components/contacts/contact-group-list.tsx b/components/contacts/contact-group-list.tsx index e5193e9d0..49ff5d42a 100644 --- a/components/contacts/contact-group-list.tsx +++ b/components/contacts/contact-group-list.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { useTranslations } from "next-intl"; -import { Users, Plus } from "lucide-react"; +import { Users, Plus } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { ContactCard } from "@/lib/jmap/types"; diff --git a/components/contacts/contact-import-dialog.tsx b/components/contacts/contact-import-dialog.tsx index 8e8331a11..5beb7f290 100644 --- a/components/contacts/contact-import-dialog.tsx +++ b/components/contacts/contact-import-dialog.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; -import { Upload, FileText, AlertTriangle, X, Check } from "lucide-react"; +import { Upload, FileText, AlertTriangle, X, Check } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { parseVCard, detectDuplicates } from "@/lib/vcard"; diff --git a/components/contacts/contact-list-item.tsx b/components/contacts/contact-list-item.tsx index 04e9c8e48..c68c4b2e5 100644 --- a/components/contacts/contact-list-item.tsx +++ b/components/contacts/contact-list-item.tsx @@ -5,7 +5,7 @@ import { Avatar } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; import type { ContactCard } from "@/lib/jmap/types"; import { getContactDisplayName, getContactPrimaryEmail } from "@/stores/contact-store"; -import { CheckSquare, Square } from "lucide-react"; +import { CheckSquare, Square } from "@/components/icons"; import type { Density } from "@/stores/settings-store"; interface ContactListItemProps { diff --git a/components/contacts/contact-list.tsx b/components/contacts/contact-list.tsx index b4e1fc0ed..b47e81f54 100644 --- a/components/contacts/contact-list.tsx +++ b/components/contacts/contact-list.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { useTranslations, useLocale } from "next-intl"; -import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, ArrowDownAZ } from "lucide-react"; +import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, ArrowDownAZ } from "@/components/icons"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ContactListItem } from "./contact-list-item"; diff --git a/components/contacts/contacts-app.tsx b/components/contacts/contacts-app.tsx index fb55878fa..2a82490da 100644 --- a/components/contacts/contacts-app.tsx +++ b/components/contacts/contacts-app.tsx @@ -4,7 +4,7 @@ import { Suspense, useState, useEffect, useCallback, useRef, useMemo } from "rea import { useTranslations } from "next-intl"; import { useSearchParams } from "next/navigation"; import { useRouter } from "@/i18n/navigation"; -import { ArrowLeft, Users, AlertTriangle } from "lucide-react"; +import { ArrowLeft, Users, AlertTriangle } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx index 24230b88f..ccdff5024 100644 --- a/components/contacts/contacts-sidebar.tsx +++ b/components/contacts/contacts-sidebar.tsx @@ -2,7 +2,7 @@ import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react"; import { useTranslations } from "next-intl"; -import { BookUser, User, Users, Plus, Share2, Book, BookPlus, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings, Mail, Star } from "lucide-react"; +import { BookUser, User, Users, Plus, Share2, Book, BookPlus, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings, Mail, Star } from "@/components/icons"; import { useRouter } from "@/i18n/navigation"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu"; diff --git a/components/email/attachment-chips.tsx b/components/email/attachment-chips.tsx index bb39f9ee4..0f4a1c959 100644 --- a/components/email/attachment-chips.tsx +++ b/components/email/attachment-chips.tsx @@ -1,6 +1,6 @@ "use client"; -import { FileText, FileSpreadsheet, FileImage, FileArchive, File as FileIcon } from "lucide-react"; +import { FileText, FileSpreadsheet, FileImage, FileArchive, File as FileIcon } from "@/components/icons"; import { cn } from "@/lib/utils"; import type { Attachment } from "@/lib/jmap/types"; diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index af13a1d45..963a9e1ba 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -17,7 +17,7 @@ import { AlertCircle, ChevronDown, ChevronUp, -} from 'lucide-react'; +} from '@/components/icons'; import { useTranslations, useFormatter } from 'next-intl'; import { useRouter } from '@/i18n/navigation'; import { isDocumentRTL } from '@/i18n/direction'; diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 899f62d01..8b30f557b 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -5,7 +5,7 @@ import { useFocusTrap } from "@/hooks/use-focus-trap"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, CalendarClock, ChevronDown, MailCheck, Search, Users, PackageCheck, LockKeyhole, Type } from "lucide-react"; +import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, CalendarClock, ChevronDown, MailCheck, Search, Users, PackageCheck, LockKeyhole, Type } from "@/components/icons"; import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils"; import { debug } from "@/lib/debug"; import { toast } from "@/stores/toast-store"; diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx index 3f128b7b4..3536a045e 100644 --- a/components/email/email-context-menu.tsx +++ b/components/email/email-context-menu.tsx @@ -34,7 +34,7 @@ import { Paperclip, Link as LinkIcon, MessagesSquare, -} from "lucide-react"; +} from "@/components/icons"; import { buildMailPath } from "@/lib/deep-links"; import { useCopyLink } from "@/hooks/use-copy-link"; import { buildMailboxTree, MailboxNode } from "@/lib/utils"; diff --git a/components/email/email-hover-actions.tsx b/components/email/email-hover-actions.tsx index a7cf5e866..3714dd55e 100644 --- a/components/email/email-hover-actions.tsx +++ b/components/email/email-hover-actions.tsx @@ -4,7 +4,7 @@ import { Email } from "@/lib/jmap/types"; import { useSettingsStore } from "@/stores/settings-store"; import type { HoverAction } from "@/stores/settings-store"; import { cn } from "@/lib/utils"; -import { Trash2, Star, Mail, MailOpen, Archive, Tag, ShieldAlert, ShieldCheck } from "lucide-react"; +import { Trash2, Star, Mail, MailOpen, Archive, Tag, ShieldAlert, ShieldCheck } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useIsMobile } from "@/hooks/use-media-query"; diff --git a/components/email/email-identity-badge.tsx b/components/email/email-identity-badge.tsx index 12dba29df..6f83fdc6d 100644 --- a/components/email/email-identity-badge.tsx +++ b/components/email/email-identity-badge.tsx @@ -1,7 +1,7 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { Mail, Tag } from 'lucide-react'; +import { Mail, Tag } from '@/components/icons'; import { cn } from '@/lib/utils'; import type { Email, Identity } from '@/lib/jmap/types'; import { parseSubAddress } from '@/lib/sub-addressing'; diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index 9ca9b8294..77775c44c 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -5,7 +5,7 @@ import { ThreadListItem } from "./thread-list-item"; import type { Attachment } from "@/lib/jmap/types"; import { EmailContextMenu } from "./email-context-menu"; import { cn } from "@/lib/utils"; -import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle, CalendarClock, ShieldCheck } from "lucide-react"; +import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle, CalendarClock, ShieldCheck } from "@/components/icons"; import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index eed6af672..0eeeb2122 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -82,7 +82,7 @@ import { Link as LinkIcon, Maximize2, Minimize2, -} from "lucide-react"; +} from "@/components/icons"; import { useTranslations } from "next-intl"; import { useRouter } from "@/i18n/navigation"; import type { Attachment as PostalMimeAttachment } from 'postal-mime'; diff --git a/components/email/message-list-tabs.tsx b/components/email/message-list-tabs.tsx index 9f0d0d747..c60f7be7c 100644 --- a/components/email/message-list-tabs.tsx +++ b/components/email/message-list-tabs.tsx @@ -7,7 +7,7 @@ // DEFINITIONS (stores/message-list-tabs-store.ts) - no plugin iframe here. import { useEffect, useRef } from 'react'; -import { icons as lucideIcons, type LucideIcon } from 'lucide-react'; +import { iconForName } from '@/components/icons'; import { useMessageListTabsStore } from '@/stores/message-list-tabs-store'; import { useEmailStore } from '@/stores/email-store'; import { useAuthStore } from '@/stores/auth-store'; @@ -75,7 +75,7 @@ export function MessageListTabs() { > {tabs.map((tab) => { const Icon = tab.icon - ? (lucideIcons[tab.icon as keyof typeof lucideIcons] as LucideIcon | undefined) + ? iconForName(tab.icon) : undefined; const unread = tabCounts[tab.id] ?? 0; const isActive = tab.id === activeTabId; diff --git a/components/email/read-receipt-banner.tsx b/components/email/read-receipt-banner.tsx index dfaff8919..079e3ed92 100644 --- a/components/email/read-receipt-banner.tsx +++ b/components/email/read-receipt-banner.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { MailCheck, Loader2, CheckCircle, X } from 'lucide-react'; +import { MailCheck, Loader2, CheckCircle, X } from '@/components/icons'; import { useTranslations } from 'next-intl'; interface ReadReceiptBannerProps { diff --git a/components/email/recipient-popover.tsx b/components/email/recipient-popover.tsx index 9d8029ea1..b665cdebb 100644 --- a/components/email/recipient-popover.tsx +++ b/components/email/recipient-popover.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; -import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "lucide-react"; +import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "@/components/icons"; import { Avatar } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; import { MailtoLink } from "@/components/ui/mailto-link"; diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index 8862bd7c6..ea6729ec7 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -49,7 +49,7 @@ import { Trash2, Rows3, Columns3, -} from "lucide-react"; +} from "@/components/icons"; export interface InlineImageUpload { src: string; diff --git a/components/email/selectable-avatar.tsx b/components/email/selectable-avatar.tsx index 8a5ef7924..97a359d9e 100644 --- a/components/email/selectable-avatar.tsx +++ b/components/email/selectable-avatar.tsx @@ -1,7 +1,7 @@ "use client"; import type { ComponentProps, MouseEvent } from "react"; -import { Check } from "lucide-react"; +import { Check } from "@/components/icons"; import { Avatar } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; diff --git a/components/email/tag-badge.tsx b/components/email/tag-badge.tsx index 0cfa385c9..9359c307a 100644 --- a/components/email/tag-badge.tsx +++ b/components/email/tag-badge.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { X } from "lucide-react"; +import { X } from "@/components/icons"; import { cn } from "@/lib/utils"; import { useKeywordFormat } from "@/hooks/use-keyword-format"; import { useShortenedText } from "@/hooks/use-shortened-text"; diff --git a/components/email/tag-picker.tsx b/components/email/tag-picker.tsx index 0f0948ef9..9fb063947 100644 --- a/components/email/tag-picker.tsx +++ b/components/email/tag-picker.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; -import { Check, Search } from "lucide-react"; +import { Check, Search } from "@/components/icons"; import { cn } from "@/lib/utils"; import { useSettingsStore } from "@/stores/settings-store"; import { buildKeywordTree, type KeywordNode } from "@/lib/keyword-nesting"; diff --git a/components/email/thread-conversation-view.tsx b/components/email/thread-conversation-view.tsx index e1e52523e..e56fdf8ce 100644 --- a/components/email/thread-conversation-view.tsx +++ b/components/email/thread-conversation-view.tsx @@ -31,7 +31,7 @@ import { FileArchive, File, Eye, -} from "lucide-react"; +} from "@/components/icons"; import { useTranslations } from "next-intl"; import { useSettingsStore } from "@/stores/settings-store"; import { useContactStore } from "@/stores/contact-store"; diff --git a/components/email/thread-email-item.tsx b/components/email/thread-email-item.tsx index 24c97d7e7..64f6e6d43 100644 --- a/components/email/thread-email-item.tsx +++ b/components/email/thread-email-item.tsx @@ -6,7 +6,7 @@ import { formatDate } from "@/lib/utils"; import { Email } from "@/lib/jmap/types"; import { cn } from "@/lib/utils"; import { Avatar } from "@/components/ui/avatar"; -import { Paperclip, Star, Circle, CheckSquare, Square, Reply, Forward } from "lucide-react"; +import { Paperclip, Star, Circle, CheckSquare, Square, Reply, Forward } from "@/components/icons"; import { useEmailDrag } from "@/hooks/use-email-drag"; import { useLongPress } from "@/hooks/use-long-press"; import { useEmailStore } from "@/stores/email-store"; diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index 19ac9c4da..b3e045e91 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -7,8 +7,8 @@ import { cn } from "@/lib/utils"; import { AttachmentChips } from "./attachment-chips"; import type { Attachment } from "@/lib/jmap/types"; import { SelectableAvatar } from "@/components/email/selectable-avatar"; -import { Paperclip, Star, Pin, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward, CalendarClock, Folder, Archive, Trash2, MailOpen, ShieldAlert } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; +import { Paperclip, Star, Pin, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward, CalendarClock, Folder, Archive, Trash2, MailOpen, ShieldAlert } from "@/components/icons"; +import type { AppIcon } from "@/components/icons"; import { useSettingsStore } from "@/stores/settings-store"; import { useUIStore } from "@/stores/ui-store"; import { useEmailStore } from "@/stores/email-store"; @@ -49,7 +49,7 @@ function UnreadDot({ density, compactAvatar }: { density: string; compactAvatar: ); } -function StatusIcon({ icon: Icon, label, className }: { icon: LucideIcon; label: string; className: string }) { +function StatusIcon({ icon: Icon, label, className }: { icon: AppIcon; label: string; className: string }) { return ( <> @@ -147,7 +147,7 @@ interface SingleEmailItemProps { // Visual + behaviour metadata for each mobile swipe action. Keyed by the // SwipeAction values that map to a row callback ('none' is handled inline). -const SWIPE_ACTION_META: Record, { icon: LucideIcon; bg: string }> = { +const SWIPE_ACTION_META: Record, { icon: AppIcon; bg: string }> = { archive: { icon: Archive, bg: 'bg-emerald-600' }, delete: { icon: Trash2, bg: 'bg-red-600' }, markRead: { icon: MailOpen, bg: 'bg-sky-600' }, diff --git a/components/email/unsubscribe-banner.tsx b/components/email/unsubscribe-banner.tsx index 491f3c449..d4a64949c 100644 --- a/components/email/unsubscribe-banner.tsx +++ b/components/email/unsubscribe-banner.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useRef, useEffect } from 'react'; -import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; +import { Loader2, CheckCircle, AlertCircle } from '@/components/icons'; import { useTranslations } from 'next-intl'; import { isValidUnsubscribeUrl, parseMailtoUrl } from '@/lib/validation'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; diff --git a/components/error/error-fallbacks.tsx b/components/error/error-fallbacks.tsx index 53bc07b75..a76afffb1 100644 --- a/components/error/error-fallbacks.tsx +++ b/components/error/error-fallbacks.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertCircle, RefreshCw, Inbox, Mail, Settings, FolderOpen } from "lucide-react"; +import { AlertCircle, RefreshCw, Inbox, Mail, Settings, FolderOpen } from "@/components/icons"; import { Button } from "@/components/ui/button"; import type { FallbackProps } from "./error-boundary"; diff --git a/components/files/eml-preview.tsx b/components/files/eml-preview.tsx index bc7379a87..49197d7da 100644 --- a/components/files/eml-preview.tsx +++ b/components/files/eml-preview.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { Paperclip, Download } from "lucide-react"; +import { Paperclip, Download } from "@/components/icons"; import { sanitizeEmailHtmlForIframe } from "@/lib/email-sanitization"; import { getEffectiveTimeZone } from "@/lib/timezone"; diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx index b31495e3c..84b5afc4f 100644 --- a/components/files/file-browser.tsx +++ b/components/files/file-browser.tsx @@ -12,7 +12,7 @@ import { FileArchive, FileSpreadsheet, Presentation, FileCode, Box, PenTool, Terminal as TerminalIcon, Database, Type as TypeIcon, Menu, Users, Share2, SquarePen, -} from "lucide-react"; +} from "@/components/icons"; import { useIsDesktop } from "@/hooks/use-media-query"; import { Button } from "@/components/ui/button"; import { cn, formatFileSize } from "@/lib/utils"; @@ -1248,7 +1248,7 @@ export function FileBrowser({ // open so they can expand/collapse without dismissing. const target = e.target as HTMLElement; const btn = target.closest('button'); - if (btn && !btn.querySelector('svg.lucide-chevron-right, svg.lucide-chevron-down')) { + if (btn && !btn.querySelector('svg.tabler-icon-chevron-right, svg.tabler-icon-chevron-down')) { setNarrowSidebarOpen(false); } }} diff --git a/components/files/file-preview-modal.tsx b/components/files/file-preview-modal.tsx index 4a34d00de..a6366bafc 100644 --- a/components/files/file-preview-modal.tsx +++ b/components/files/file-preview-modal.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; -import { X, Download, Loader2, ExternalLink } from "lucide-react"; +import { X, Download, Loader2, ExternalLink } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { getFilePreviewKind, isMimeTypeSafeForInlinePreview } from "@/lib/file-preview"; import dynamic from "next/dynamic"; diff --git a/components/files/file-upload-area.tsx b/components/files/file-upload-area.tsx index 5a06763b6..f14e05422 100644 --- a/components/files/file-upload-area.tsx +++ b/components/files/file-upload-area.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import { useTranslations } from "next-intl"; -import { Upload, FolderPlus, FilePlus } from "lucide-react"; +import { Upload, FolderPlus, FilePlus } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { getDroppedFilesAndFolders } from "@/lib/webdav/drop-utils"; diff --git a/components/files/files-app.tsx b/components/files/files-app.tsx index 17dbcb673..6f6a39b27 100644 --- a/components/files/files-app.tsx +++ b/components/files/files-app.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; @@ -31,7 +31,7 @@ import { useWopiStatus, fileExtension } from "@/hooks/use-wopi-status"; import { loadFilesSettings } from "@/components/files/files-settings-dialog"; import type { FolderLayout } from "@/components/files/files-settings-dialog"; import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot"; -import { AlertTriangle, Loader2 } from "lucide-react"; +import { AlertTriangle, Loader2 } from "@/components/icons"; import { isFilePreviewable } from "@/lib/file-preview"; import { appPath, buildFilesPath, parseFilesPath, type FilesDeepLink } from "@/lib/deep-links"; import { consumePendingDeepLinkEntry, subscribePendingDeepLink } from "@/lib/deep-link-handoff"; diff --git a/components/files/files-settings-dialog.tsx b/components/files/files-settings-dialog.tsx index 63281ea25..7aeda04a8 100644 --- a/components/files/files-settings-dialog.tsx +++ b/components/files/files-settings-dialog.tsx @@ -2,7 +2,7 @@ import { useRef, useEffect } from "react"; import { useTranslations } from "next-intl"; -import { X } from "lucide-react"; +import { X } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { SettingsSection, SettingItem, ToggleSwitch, RadioGroup } from "@/components/settings/settings-section"; diff --git a/components/files/folder-tree-sidebar.tsx b/components/files/folder-tree-sidebar.tsx index 52b1aa38d..af8339c69 100644 --- a/components/files/folder-tree-sidebar.tsx +++ b/components/files/folder-tree-sidebar.tsx @@ -9,7 +9,7 @@ import { ChevronDown, Home, Share2, -} from "lucide-react"; +} from "@/components/icons"; import { cn } from "@/lib/utils"; import { useFileStore, type FileResource } from "@/stores/file-store"; diff --git a/components/files/image-preview-modal.tsx b/components/files/image-preview-modal.tsx index ffd26a5c8..8cf009bcb 100644 --- a/components/files/image-preview-modal.tsx +++ b/components/files/image-preview-modal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { X, Download, ZoomIn, ZoomOut, RotateCw } from "lucide-react"; +import { X, Download, ZoomIn, ZoomOut, RotateCw } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { useTranslations } from "next-intl"; diff --git a/components/files/pdf-mobile-viewer.tsx b/components/files/pdf-mobile-viewer.tsx index cab79cb9d..1a1a604eb 100644 --- a/components/files/pdf-mobile-viewer.tsx +++ b/components/files/pdf-mobile-viewer.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; -import { Loader2, ExternalLink } from "lucide-react"; +import { Loader2, ExternalLink } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { liteMountedAssetUrl } from "@/lib/lite"; diff --git a/components/files/wopi-editor.tsx b/components/files/wopi-editor.tsx index 8cf5b4269..79671823f 100644 --- a/components/files/wopi-editor.tsx +++ b/components/files/wopi-editor.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; -import { Loader2, X } from "lucide-react"; +import { Loader2, X } from "@/components/icons"; import type { FileResource } from "@/stores/file-store"; interface WopiEditorProps { diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index cca7a2799..9f6fb2a7c 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -4,7 +4,7 @@ import { useState, useCallback, useMemo } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { X, Plus, Trash2 } from "lucide-react"; +import { X, Plus, Trash2 } from "@/components/icons"; import { useFocusTrap } from "@/hooks/use-focus-trap"; import { toast } from "@/stores/toast-store"; import type { diff --git a/components/filters/sieve-editor-modal.tsx b/components/filters/sieve-editor-modal.tsx index 064b74de1..0aeee9ecc 100644 --- a/components/filters/sieve-editor-modal.tsx +++ b/components/filters/sieve-editor-modal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { X, AlertTriangle, CheckCircle, Loader2 } from "lucide-react"; +import { X, AlertTriangle, CheckCircle, Loader2 } from "@/components/icons"; import { useFocusTrap } from "@/hooks/use-focus-trap"; interface SieveEditorModalProps { diff --git a/components/global-search/global-search-palette.tsx b/components/global-search/global-search-palette.tsx index 865d09e3d..d82ac22f6 100644 --- a/components/global-search/global-search-palette.tsx +++ b/components/global-search/global-search-palette.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useRef } from "react"; -import { AlertTriangle, Clock3, CornerDownLeft, Loader2, Search } from "lucide-react"; +import { AlertTriangle, Clock3, CornerDownLeft, Loader2, Search } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useRouter } from "@/i18n/navigation"; import { useGlobalSearch } from "@/hooks/use-global-search"; diff --git a/components/global-search/palette-result-row.tsx b/components/global-search/palette-result-row.tsx index 04267a484..642f3495a 100644 --- a/components/global-search/palette-result-row.tsx +++ b/components/global-search/palette-result-row.tsx @@ -1,6 +1,6 @@ "use client"; -import { CalendarDays, FileText, Folder, Repeat } from "lucide-react"; +import { CalendarDays, FileText, Folder, Repeat } from "@/components/icons"; import { useTranslations } from "next-intl"; import { Avatar } from "@/components/ui/avatar"; import { parseSearchSnippet, type SnippetSegment } from "@/lib/search-snippet"; diff --git a/components/global-search/search-preview.tsx b/components/global-search/search-preview.tsx index 9f0ba1169..6d8ff7e33 100644 --- a/components/global-search/search-preview.tsx +++ b/components/global-search/search-preview.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlignLeft, Building2, CalendarDays, Clock, ExternalLink, FileText, Folder, HardDrive, Mail, MapPin, Phone, Repeat, Users } from "lucide-react"; +import { AlignLeft, Building2, CalendarDays, Clock, ExternalLink, FileText, Folder, HardDrive, Mail, MapPin, Phone, Repeat, Users } from "@/components/icons"; import { useTranslations } from "next-intl"; import { Avatar } from "@/components/ui/avatar"; import { ProEmailView } from "@/components/pro/pro-email-tab-body"; diff --git a/components/global-search/search-result-list.tsx b/components/global-search/search-result-list.tsx index 348e1fd53..2aec10ce1 100644 --- a/components/global-search/search-result-list.tsx +++ b/components/global-search/search-result-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertTriangle, Loader2 } from "lucide-react"; +import { AlertTriangle, Loader2 } from "@/components/icons"; import { useTranslations } from "next-intl"; import { compareByRecency } from "@/lib/global-search/rank"; import { hitKey, SEARCH_KINDS, type GlobalSearchHit, type SearchKind, type SearchOutcome } from "@/lib/global-search/types"; diff --git a/components/global-search/search-tab-body.tsx b/components/global-search/search-tab-body.tsx index 17c5d1dbe..c6cc36ea2 100644 --- a/components/global-search/search-tab-body.tsx +++ b/components/global-search/search-tab-body.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { Loader2, Search } from "lucide-react"; +import { Loader2, Search } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useGlobalSearch } from "@/hooks/use-global-search"; import { openHit } from "@/lib/global-search/open-hit"; diff --git a/components/icons/__tests__/named.test.ts b/components/icons/__tests__/named.test.ts new file mode 100644 index 000000000..874208a22 --- /dev/null +++ b/components/icons/__tests__/named.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { icons } from '@tabler/icons-react'; +import { + LEGACY_LUCIDE, + POPULAR_ICON_NAMES, + iconForName, + toStoredIconName, + toTablerName, +} from '../named'; + +const componentKey = (name: string) => + 'Icon' + name.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(''); +const exists = (name: string) => componentKey(name) in icons; + +describe('toTablerName', () => { + it('reads the stored tabler: form', () => { + expect(toTablerName('tabler:inbox')).toBe('inbox'); + expect(toTablerName(toStoredIconName('brand-github'))).toBe('brand-github'); + }); + + it('translates Lucide names saved before the switch', () => { + expect(toTablerName('Globe')).toBe('world'); + expect(toTablerName('Inbox')).toBe('inbox'); + expect(toTablerName('Trash2')).toBe('trash'); + expect(toTablerName('Gamepad2')).toBe('device-gamepad-2'); + }); + + it('guesses unknown Lucide names by kebab-casing them', () => { + expect(toTablerName('CloudRain')).toBe('cloud-rain'); + expect(toTablerName('Dice6')).toBe('dice-6'); + }); + + it('accepts a bare Tabler name', () => { + expect(toTablerName('world')).toBe('world'); + expect(toTablerName('a-b-2')).toBe('a-b-2'); + }); + + it('rejects anything that is not a name', () => { + expect(toTablerName('')).toBeNull(); + expect(toTablerName(null)).toBeNull(); + expect(toTablerName('tabler:../etc')).toBeNull(); + expect(toTablerName('Globe/../../x')).toBeNull(); + }); +}); + +describe('icon tables', () => { + it('maps every legacy Lucide name to an icon Tabler has', () => { + const missing = Object.entries(LEGACY_LUCIDE).filter(([, t]) => !exists(t)); + expect(missing).toEqual([]); + }); + + it('only offers popular icons that exist', () => { + expect(POPULAR_ICON_NAMES.filter((n) => !exists(n))).toEqual([]); + }); + + it('returns a component for any usable name and nothing for junk', () => { + expect(iconForName('Globe')).toBe(icons.IconWorld); + expect(iconForName('tabler:zzz')).toBeDefined(); + expect(iconForName('tabler:zzz')).toBe(iconForName('tabler:zzz')); + expect(iconForName('')).toBeUndefined(); + }); +}); diff --git a/components/icons/index.ts b/components/icons/index.ts new file mode 100644 index 000000000..27059eddf --- /dev/null +++ b/components/icons/index.ts @@ -0,0 +1,240 @@ +// Bulwark's icon set. Every icon the UI draws is imported from here, so the +// pack behind it is a one-file decision. The export names are the app's own +// (they started out as Lucide names); the glyphs come from Tabler Icons (MIT). +// +// A few have no exact Tabler equivalent and use the closest glyph: +// BookPlus (book-2), FolderSync (folders), HardDrive (server-2), Mails (inbox), +// PowerOff (power), PlayCircle (player-play), ScrollText (file-text). +// +// Icons chosen by name at runtime (sidebar apps, message-list tabs) resolve +// through ./named instead. + +export type { TablerIcon as AppIcon, IconProps as AppIconProps } from '@tabler/icons-react'; +export { iconForName, toStoredIconName, toTablerName, POPULAR_ICON_NAMES, loadAllIconNames, TABLER_ICON_PREFIX } from './named'; + +export { + IconActivity as Activity, + IconAlertCircle as AlertCircle, + IconAlertTriangle as AlertTriangle, + IconAlignCenter as AlignCenter, + IconAlignLeft as AlignLeft, + IconAlignRight as AlignRight, + IconArchive as Archive, + IconArrowDown as ArrowDown, + IconSortAZ as ArrowDownAZ, + IconArrowLeft as ArrowLeft, + IconArrowLeftRight as ArrowLeftRight, + IconArrowRight as ArrowRight, + IconArrowUp as ArrowUp, + IconCircleArrowUp as ArrowUpCircle, + IconBan as Ban, + IconBaseline as Baseline, + IconBell as Bell, + IconBellOff as BellOff, + IconBold as Bold, + IconBook2 as Book, + IconBook as BookOpen, + IconBook2 as BookPlus, + IconAddressBook as BookUser, + IconBookmark as Bookmark, + IconBookmarkPlus as BookmarkPlus, + IconBox as Box, + IconBrain as Brain, + IconBriefcase as Briefcase, + IconBug as Bug, + IconBuilding as Building, + IconBuilding as Building2, + IconCake as Cake, + IconCalendar as Calendar, + IconCalendarUp as CalendarArrowUp, + IconCalendarCheck as CalendarCheck, + IconCalendarClock as CalendarClock, + IconCalendarMonth as CalendarDays, + IconCalendarX as CalendarX, + IconCamera as Camera, + IconCheck as Check, + IconChecks as CheckCheck, + IconCircleCheck as CheckCircle, + IconCircleCheck as CheckCircle2, + IconSquareCheck as CheckSquare, + IconChevronDown as ChevronDown, + IconChevronLeft as ChevronLeft, + IconChevronRight as ChevronRight, + IconChevronUp as ChevronUp, + IconChevronsLeft as ChevronsLeft, + IconChevronsRight as ChevronsRight, + IconCircle as Circle, + IconClipboard as Clipboard, + IconClipboardCopy as ClipboardCopy, + IconClock as Clock, + IconClockHour3 as Clock3, + IconCode as Code, + IconColumns3 as Columns3, + IconCopy as Copy, + IconCopyPlus as CopyPlus, + IconCornerDownLeft as CornerDownLeft, + IconDatabase as Database, + IconDownload as Download, + IconEdit as EditIcon, + IconDots as Ellipsis, + IconEraser as Eraser, + IconExternalLink as ExternalLink, + IconEye as Eye, + IconEyeOff as EyeOff, + IconFile as File, + IconFileZip as FileArchive, + IconFileMusic as FileAudio, + IconFileCode as FileCode, + IconPhoto as FileImage, + IconFilePlus as FilePlus, + IconFileSpreadsheet as FileSpreadsheet, + IconFileText as FileText, + IconVideo as FileVideo, + IconFilter as Filter, + IconFlag as Flag, + IconFlask as FlaskConical, + IconFolder as Folder, + IconFolderDown as FolderInput, + IconFolderOpen as FolderOpen, + IconFolderPlus as FolderPlus, + IconFolders as FolderSync, + IconFolderUp as FolderUp, + IconFolderX as FolderX, + IconArrowForwardUp as Forward, + IconWorld as Globe, + IconGripVertical as GripVertical, + IconServer2 as HardDrive, + IconH1 as Heading1, + IconH2 as Heading2, + IconHeart as Heart, + IconHelpCircle as HelpCircle, + IconHighlight as Highlighter, + IconHome as Home, + IconPhoto as Image, + IconPhoto as ImageIcon, + IconInbox as Inbox, + IconInfoCircle as Info, + IconItalic as Italic, + IconKey as Key, + IconKey as KeyRound, + IconKeyboard as Keyboard, + IconLanguage as Languages, + IconStack as Layers, + IconLayoutDashboard as LayoutDashboard, + IconLayoutGrid as LayoutGrid, + IconLayoutList as LayoutList, + IconBulb as Lightbulb, + IconLink as Link, + IconList as List, + IconListNumbers as ListOrdered, + IconChecklist as ListTodo, + IconLoader2 as Loader2, + IconLock as Lock, + IconLock as LockKeyhole, + IconLockOpen as LockOpen, + IconLogin as LogIn, + IconLogout as LogOut, + IconMail as Mail, + IconMailCheck as MailCheck, + IconMailOpened as MailOpen, + IconMailX as MailX, + IconInbox as Mails, + IconMapPin as MapPin, + IconMaximize as Maximize2, + IconMenu2 as Menu, + IconMessage as MessageSquare, + IconMessages as MessagesSquare, + IconMinimize as Minimize2, + IconMinus as Minus, + IconDeviceDesktop as Monitor, + IconMoon as Moon, + IconDots as MoreHorizontal, + IconDotsVertical as MoreVertical, + IconNotebook as NotebookPen, + IconPackage as Package, + IconPackage as PackageCheck, + IconPalette as Palette, + IconBeach as Palmtree, + IconBeach as PalmtreeIcon, + IconLayoutSidebarLeftCollapse as PanelLeftClose, + IconLayoutSidebarRight as PanelRight, + IconLayoutSidebarRightCollapse as PanelRightClose, + IconLayoutSidebarRightExpand as PanelRightOpen, + IconPaperclip as Paperclip, + IconPencil as PenLine, + IconEdit as PenSquare, + IconVector as PenTool, + IconPencil as Pencil, + IconPhone as Phone, + IconPinned as Pin, + IconPinnedOff as PinOff, + IconPlayerPlay as Play, + IconPlayerPlay as PlayCircle, + IconPlus as Plus, + IconPower as Power, + IconPower as PowerOff, + IconPresentation as Presentation, + IconPrinter as Printer, + IconPuzzle as Puzzle, + IconQrcode as QrCode, + IconQuote as Quote, + IconArrowForward as Redo, + IconRefresh as RefreshCw, + IconClearFormatting as RemoveFormatting, + IconRepeat as Repeat, + IconArrowBackUp as Reply, + IconArrowBackUpDouble as ReplyAll, + IconRotate as RotateCcw, + IconRotateClockwise as RotateCw, + IconLayoutRows as Rows3, + IconDeviceFloppy as Save, + IconScale as Scale, + IconScissors as Scissors, + IconFileText as ScrollText, + IconSearch as Search, + IconSearchOff as SearchX, + IconSend as Send, + IconServer as Server, + IconSettings as Settings, + IconShare2 as Share2, + IconShield as Shield, + IconShieldExclamation as ShieldAlert, + IconShieldCheck as ShieldCheck, + IconArrowsShuffle as Shuffle, + IconDeviceMobile as Smartphone, + IconSparkles as Sparkles, + IconSquare as Square, + IconEdit as SquarePen, + IconStar as Star, + IconNote as StickyNote, + IconBuildingStore as Store, + IconStrikethrough as Strikethrough, + IconSun as Sun, + IconColorSwatch as SwatchBook, + IconTable as Table, + IconTag as Tag, + IconTags as Tags, + IconTerminal as Terminal, + IconTrash as Trash, + IconTrash as Trash2, + IconAlertTriangle as TriangleAlert, + IconTypography as Type, + IconUnderline as Underline, + IconArrowBack as Undo, + IconLockOpen as Unlock, + IconUpload as Upload, + IconUser as User, + IconUserCircle as UserCircle, + IconUserMinus as UserMinus, + IconUserEdit as UserPen, + IconUserPlus as UserPlus, + IconUsers as Users, + IconUsers as UsersRound, + IconVideo as Video, + IconVolume2 as Volume2, + IconX as X, + IconCircleX as XCircle, + IconBolt as Zap, + IconZoomIn as ZoomIn, + IconZoomOut as ZoomOut, +} from '@tabler/icons-react'; diff --git a/components/icons/named.tsx b/components/icons/named.tsx new file mode 100644 index 000000000..9d255d5b8 --- /dev/null +++ b/components/icons/named.tsx @@ -0,0 +1,429 @@ +'use client'; + +// Icons chosen by name at runtime: sidebar apps, message-list tabs (a plugin +// API), and the admin's default apps. Names are stored as `tabler:`. +// Names saved before the switch to Tabler are Lucide names (`"Globe"`); they +// are translated on read, so old settings and existing plugins keep working. +// +// The popular icons ship in the main bundle. Any other name loads the full +// Tabler set once, lazily, the first time it is rendered or the picker's +// "show all" list opens. + +import { forwardRef, useEffect, useState } from 'react'; +import type { TablerIcon } from '@tabler/icons-react'; +import { + IconAddressBook, + IconAperture, + IconApple, + IconArchive, + IconAtom, + IconAward, + IconBeer, + IconBell, + IconBike, + IconBolt, + IconBook, + IconBookmark, + IconBox, + IconBraces, + IconBrain, + IconBrandTrello, + IconBrandYoutube, + IconBrush, + IconBug, + IconBuilding, + IconBuildingBank, + IconBuildingStore, + IconBuildingWarehouse, + IconBulb, + IconBus, + IconCake, + IconCalendar, + IconCamera, + IconCar, + IconChartBar, + IconChartColumn, + IconChartLine, + IconChartPie, + IconChecklist, + IconChristmasTree, + IconCircleDot, + IconClipboardList, + IconClock, + IconCloud, + IconCloudDownload, + IconCloudUpload, + IconCode, + IconCoffee, + IconCompass, + IconCpu, + IconCreditCard, + IconCrosshair, + IconCrown, + IconCurrencyDollar, + IconCurrencyEuro, + IconDatabase, + IconDeviceComputerCamera, + IconDeviceDesktop, + IconDeviceGamepad2, + IconDeviceLaptop, + IconDeviceMobile, + IconDeviceTablet, + IconDeviceTv, + IconDice5, + IconDna, + IconEdit, + IconExternalLink, + IconEye, + IconFileSpreadsheet, + IconFileText, + IconFingerprint, + IconFlag, + IconFlame, + IconFlask2, + IconFlower, + IconFolder, + IconGift, + IconGlass, + IconHammer, + IconHeadphones, + IconHeart, + IconHeartbeat, + IconHome, + IconInbox, + IconKey, + IconLayoutGrid, + IconLayoutKanban, + IconLeaf, + IconLink, + IconLock, + IconLockOpen, + IconMagnet, + IconMail, + IconMan, + IconMap, + IconMapPin, + IconMessage, + IconMessageCircle, + IconMessages, + IconMicrophone, + IconMicrophone2, + IconMicroscope, + IconMoon, + IconMountain, + IconMovie, + IconMusic, + IconNavigation, + IconNotebook, + IconPackage, + IconPalette, + IconPencil, + IconPhone, + IconPhoto, + IconPigMoney, + IconPill, + IconPizza, + IconPlane, + IconPlayerPlay, + IconPuzzle, + IconQrcode, + IconRadio, + IconReceipt, + IconRipple, + IconRobot, + IconRocket, + IconRss, + IconRuler, + IconScan, + IconScissors, + IconServer, + IconServer2, + IconShield, + IconShieldCheck, + IconShip, + IconSnowflake, + IconSoup, + IconSparkles, + IconSquareCheck, + IconStack, + IconStar, + IconStethoscope, + IconStopwatch, + IconSun, + IconSunWind, + IconTag, + IconTarget, + IconTerminal, + IconThermometer, + IconThumbUp, + IconTool, + IconTrain, + IconTrendingUp, + IconTrophy, + IconUserCircle, + IconUserPlus, + IconUsers, + IconVaccine, + IconVideo, + IconWallet, + IconWand, + IconWifi, + IconWorld, +} from '@tabler/icons-react'; + +export const TABLER_ICON_PREFIX = 'tabler:'; + +/** Tabler names shown first in the icon picker. */ +export const POPULAR_ICON_NAMES: readonly string[] = ["world","rss","radio","microphone-2","message-circle","message","messages","phone","video","device-computer-camera","headphones","microphone","file-text","file-spreadsheet","notebook","book","clipboard-list","checklist","square-check","layout-kanban","brand-trello","pencil","edit","code","terminal","braces","bug","database","server","cpu","server-2","device-desktop","device-laptop","device-mobile","device-tablet","wifi","cloud","cloud-download","cloud-upload","users","user-plus","user-circle","address-book","man","heart","thumb-up","star","award","trophy","crown","photo","camera","movie","music","player-play","device-tv","brand-youtube","palette","brush","map","map-pin","navigation","compass","home","building","building-bank","building-store","building-warehouse","currency-dollar","currency-euro","credit-card","wallet","receipt","pig-money","trending-up","chart-bar","chart-column","chart-line","chart-pie","shield","shield-check","lock","lock-open","key","fingerprint","eye","flask-2","atom","dna","microscope","stethoscope","heartbeat","pill","vaccine","thermometer","sun","moon","sun-wind","snowflake","bolt","flame","christmas-tree","flower","leaf","mountain","ripple","tool","hammer","scissors","ruler","magnet","package","gift","box","archive","car","bike","bus","train","plane","ship","rocket","coffee","glass","beer","pizza","apple","cake","soup","device-gamepad-2","dice-5","puzzle","sparkles","wand","robot","brain","bulb","bookmark","flag","bell","clock","stopwatch","link","external-link","qrcode","scan","layout-grid","stack","aperture","circle-dot","target","crosshair"]; + +const CURATED: Record = { + 'address-book': IconAddressBook, + 'aperture': IconAperture, + 'apple': IconApple, + 'archive': IconArchive, + 'atom': IconAtom, + 'award': IconAward, + 'beer': IconBeer, + 'bell': IconBell, + 'bike': IconBike, + 'bolt': IconBolt, + 'book': IconBook, + 'bookmark': IconBookmark, + 'box': IconBox, + 'braces': IconBraces, + 'brain': IconBrain, + 'brand-trello': IconBrandTrello, + 'brand-youtube': IconBrandYoutube, + 'brush': IconBrush, + 'bug': IconBug, + 'building': IconBuilding, + 'building-bank': IconBuildingBank, + 'building-store': IconBuildingStore, + 'building-warehouse': IconBuildingWarehouse, + 'bulb': IconBulb, + 'bus': IconBus, + 'cake': IconCake, + 'calendar': IconCalendar, + 'camera': IconCamera, + 'car': IconCar, + 'chart-bar': IconChartBar, + 'chart-column': IconChartColumn, + 'chart-line': IconChartLine, + 'chart-pie': IconChartPie, + 'checklist': IconChecklist, + 'christmas-tree': IconChristmasTree, + 'circle-dot': IconCircleDot, + 'clipboard-list': IconClipboardList, + 'clock': IconClock, + 'cloud': IconCloud, + 'cloud-download': IconCloudDownload, + 'cloud-upload': IconCloudUpload, + 'code': IconCode, + 'coffee': IconCoffee, + 'compass': IconCompass, + 'cpu': IconCpu, + 'credit-card': IconCreditCard, + 'crosshair': IconCrosshair, + 'crown': IconCrown, + 'currency-dollar': IconCurrencyDollar, + 'currency-euro': IconCurrencyEuro, + 'database': IconDatabase, + 'device-computer-camera': IconDeviceComputerCamera, + 'device-desktop': IconDeviceDesktop, + 'device-gamepad-2': IconDeviceGamepad2, + 'device-laptop': IconDeviceLaptop, + 'device-mobile': IconDeviceMobile, + 'device-tablet': IconDeviceTablet, + 'device-tv': IconDeviceTv, + 'dice-5': IconDice5, + 'dna': IconDna, + 'edit': IconEdit, + 'external-link': IconExternalLink, + 'eye': IconEye, + 'file-spreadsheet': IconFileSpreadsheet, + 'file-text': IconFileText, + 'fingerprint': IconFingerprint, + 'flag': IconFlag, + 'flame': IconFlame, + 'flask-2': IconFlask2, + 'flower': IconFlower, + 'folder': IconFolder, + 'gift': IconGift, + 'glass': IconGlass, + 'hammer': IconHammer, + 'headphones': IconHeadphones, + 'heart': IconHeart, + 'heartbeat': IconHeartbeat, + 'home': IconHome, + 'inbox': IconInbox, + 'key': IconKey, + 'layout-grid': IconLayoutGrid, + 'layout-kanban': IconLayoutKanban, + 'leaf': IconLeaf, + 'link': IconLink, + 'lock': IconLock, + 'lock-open': IconLockOpen, + 'magnet': IconMagnet, + 'mail': IconMail, + 'man': IconMan, + 'map': IconMap, + 'map-pin': IconMapPin, + 'message': IconMessage, + 'message-circle': IconMessageCircle, + 'messages': IconMessages, + 'microphone': IconMicrophone, + 'microphone-2': IconMicrophone2, + 'microscope': IconMicroscope, + 'moon': IconMoon, + 'mountain': IconMountain, + 'movie': IconMovie, + 'music': IconMusic, + 'navigation': IconNavigation, + 'notebook': IconNotebook, + 'package': IconPackage, + 'palette': IconPalette, + 'pencil': IconPencil, + 'phone': IconPhone, + 'photo': IconPhoto, + 'pig-money': IconPigMoney, + 'pill': IconPill, + 'pizza': IconPizza, + 'plane': IconPlane, + 'player-play': IconPlayerPlay, + 'puzzle': IconPuzzle, + 'qrcode': IconQrcode, + 'radio': IconRadio, + 'receipt': IconReceipt, + 'ripple': IconRipple, + 'robot': IconRobot, + 'rocket': IconRocket, + 'rss': IconRss, + 'ruler': IconRuler, + 'scan': IconScan, + 'scissors': IconScissors, + 'server': IconServer, + 'server-2': IconServer2, + 'shield': IconShield, + 'shield-check': IconShieldCheck, + 'ship': IconShip, + 'snowflake': IconSnowflake, + 'soup': IconSoup, + 'sparkles': IconSparkles, + 'square-check': IconSquareCheck, + 'stack': IconStack, + 'star': IconStar, + 'stethoscope': IconStethoscope, + 'stopwatch': IconStopwatch, + 'sun': IconSun, + 'sun-wind': IconSunWind, + 'tag': IconTag, + 'target': IconTarget, + 'terminal': IconTerminal, + 'thermometer': IconThermometer, + 'thumb-up': IconThumbUp, + 'tool': IconTool, + 'train': IconTrain, + 'trending-up': IconTrendingUp, + 'trophy': IconTrophy, + 'user-circle': IconUserCircle, + 'user-plus': IconUserPlus, + 'users': IconUsers, + 'vaccine': IconVaccine, + 'video': IconVideo, + 'wallet': IconWallet, + 'wand': IconWand, + 'wifi': IconWifi, + 'world': IconWorld, +}; + +/** Lucide names (pre-Tabler settings, plugin tab definitions) -> Tabler names. */ +export const LEGACY_LUCIDE: Readonly> ={"Activity":"activity","AlertCircle":"alert-circle","AlertTriangle":"alert-triangle","AlignCenter":"align-center","AlignLeft":"align-left","AlignRight":"align-right","Aperture":"aperture","Apple":"apple","Archive":"archive","ArrowDown":"arrow-down","ArrowDownAZ":"sort-a-z","ArrowLeft":"arrow-left","ArrowLeftRight":"arrow-left-right","ArrowRight":"arrow-right","ArrowUp":"arrow-up","ArrowUpCircle":"circle-arrow-up","ArrowUpRight":"arrow-up-right","Atom":"atom","Award":"award","Ban":"ban","BarChart":"chart-bar","BarChart3":"chart-column","Baseline":"baseline","Beaker":"flask-2","Beer":"beer","Bell":"bell","BellOff":"bell-off","Bike":"bike","Bold":"bold","Book":"book-2","Bookmark":"bookmark","BookmarkPlus":"bookmark-plus","BookOpen":"book","BookPlus":"book-2","BookUser":"address-book","Bot":"robot","Box":"box","Braces":"braces","Brain":"brain","BrainCircuit":"brain","Briefcase":"briefcase","Brush":"brush","Bug":"bug","Building":"building","Building2":"building","Bus":"bus","Cake":"cake","Calendar":"calendar","CalendarArrowUp":"calendar-up","CalendarCheck":"calendar-check","CalendarClock":"calendar-clock","CalendarDays":"calendar-month","CalendarX":"calendar-x","Camera":"camera","Car":"car","Check":"check","CheckCheck":"checks","CheckCircle":"circle-check","CheckCircle2":"circle-check","CheckSquare":"square-check","ChevronDown":"chevron-down","ChevronLeft":"chevron-left","ChevronRight":"chevron-right","ChevronsLeft":"chevrons-left","ChevronsRight":"chevrons-right","ChevronUp":"chevron-up","Circle":"circle","CircleDot":"circle-dot","Clapperboard":"movie","Clipboard":"clipboard","ClipboardCopy":"clipboard-copy","ClipboardList":"clipboard-list","Clock":"clock","Clock3":"clock-hour-3","Cloud":"cloud","CloudDownload":"cloud-download","CloudSun":"sun-wind","CloudUpload":"cloud-upload","Code":"code","Code2":"code","Coffee":"coffee","Columns3":"columns-3","Compass":"compass","Contact":"address-book","CookingPot":"soup","Copy":"copy","CopyPlus":"copy-plus","CornerDownLeft":"corner-down-left","Cpu":"cpu","CreditCard":"credit-card","Crosshair":"crosshair","Crown":"crown","Database":"database","Dice5":"dice-5","Dna":"dna","DollarSign":"currency-dollar","Download":"download","Edit":"edit","EditIcon":"edit","Ellipsis":"dots","Eraser":"eraser","Euro":"currency-euro","ExternalLink":"external-link","Eye":"eye","EyeOff":"eye-off","File":"file","FileArchive":"file-zip","FileAudio":"file-music","FileCode":"file-code","FileImage":"photo","FilePlus":"file-plus","FileSpreadsheet":"file-spreadsheet","FileText":"file-text","FileVideo":"video","Film":"movie","Filter":"filter","Fingerprint":"fingerprint","Flag":"flag","Flame":"flame","FlaskConical":"flask","Flower":"flower","Folder":"folder","FolderInput":"folder-down","FolderOpen":"folder-open","FolderPlus":"folder-plus","FolderSync":"folders","FolderUp":"folder-up","FolderX":"folder-x","Forward":"arrow-forward-up","Gamepad2":"device-gamepad-2","Gift":"gift","Globe":"world","GripVertical":"grip-vertical","Hammer":"hammer","HardDrive":"server-2","Heading1":"h-1","Heading2":"h-2","Headphones":"headphones","Heart":"heart","HeartPulse":"heartbeat","HelpCircle":"help-circle","Highlighter":"highlight","Home":"home","Image":"photo","ImageIcon":"photo","Inbox":"inbox","Info":"info-circle","Italic":"italic","Kanban":"layout-kanban","Key":"key","Keyboard":"keyboard","KeyRound":"key","Landmark":"building-bank","Languages":"language","Laptop":"device-laptop","Layers":"stack","LayoutDashboard":"layout-dashboard","LayoutGrid":"layout-grid","LayoutList":"layout-list","Leaf":"leaf","Lightbulb":"bulb","LineChart":"chart-line","Link":"link","List":"list","ListOrdered":"list-numbers","ListTodo":"checklist","Loader2":"loader-2","Lock":"lock","LockKeyhole":"lock","LockOpen":"lock-open","LogIn":"login","LogOut":"logout","Magnet":"magnet","Mail":"mail","MailCheck":"mail-check","MailOpen":"mail-opened","Mails":"inbox","MailX":"mail-x","Map":"map","MapPin":"map-pin","Maximize2":"maximize","Menu":"menu-2","MessageCircle":"message-circle","MessageSquare":"message","MessagesSquare":"messages","Mic":"microphone","Microscope":"microscope","Minimize2":"minimize","Minus":"minus","Monitor":"device-desktop","Moon":"moon","MoreHorizontal":"dots","MoreVertical":"dots-vertical","Mountain":"mountain","Music":"music","Navigation":"navigation","Notebook":"notebook","NotebookPen":"notebook","Package":"package","PackageCheck":"package","Paintbrush":"brush","Palette":"palette","Palmtree":"beach","PalmtreeIcon":"beach","PanelLeftClose":"layout-sidebar-left-collapse","PanelRight":"layout-sidebar-right","PanelRightClose":"layout-sidebar-right-collapse","PanelRightOpen":"layout-sidebar-right-expand","Paperclip":"paperclip","Pencil":"pencil","PenLine":"pencil","PenSquare":"edit","PenTool":"vector","PersonStanding":"man","Phone":"phone","PieChart":"chart-pie","PiggyBank":"pig-money","Pill":"pill","Pin":"pinned","PinOff":"pinned-off","Pizza":"pizza","Plane":"plane","Play":"player-play","PlayCircle":"player-play","Plus":"plus","Podcast":"microphone-2","Power":"power","PowerOff":"power","Presentation":"presentation","Printer":"printer","Puzzle":"puzzle","QrCode":"qrcode","Quote":"quote","Radio":"radio","Receipt":"receipt","Redo":"arrow-forward","RefreshCw":"refresh","RemoveFormatting":"clear-formatting","Repeat":"repeat","Reply":"arrow-back-up","ReplyAll":"arrow-back-up-double","Rocket":"rocket","RotateCcw":"rotate","RotateCw":"rotate-clockwise","Rows3":"layout-rows","Rss":"rss","Ruler":"ruler","Save":"device-floppy","Scale":"scale","Scan":"scan","Scissors":"scissors","ScrollText":"file-text","Search":"search","SearchX":"search-off","Send":"send","Server":"server","Settings":"settings","Share2":"share-2","Shield":"shield","ShieldAlert":"shield-exclamation","ShieldCheck":"shield-check","Ship":"ship","Shuffle":"arrows-shuffle","Smartphone":"device-mobile","Snowflake":"snowflake","Sparkles":"sparkles","Square":"square","SquareKanban":"layout-kanban","SquarePen":"edit","Star":"star","Stethoscope":"stethoscope","StickyNote":"note","Store":"building-store","Strikethrough":"strikethrough","Sun":"sun","SwatchBook":"color-swatch","Syringe":"vaccine","Table":"table","Tablet":"device-tablet","Tag":"tag","Tags":"tags","Target":"target","Terminal":"terminal","Thermometer":"thermometer","ThumbsUp":"thumb-up","Timer":"stopwatch","Train":"train","Trash":"trash","Trash2":"trash","TreePine":"christmas-tree","Trello":"brand-trello","TrendingUp":"trending-up","TriangleAlert":"alert-triangle","Trophy":"trophy","Tv":"device-tv","Type":"typography","Underline":"underline","Undo":"arrow-back","Unlock":"lock-open","Upload":"upload","User":"user","UserCircle":"user-circle","UserMinus":"user-minus","UserPen":"user-edit","UserPlus":"user-plus","Users":"users","UsersRound":"users","Video":"video","Volume2":"volume-2","Wallet":"wallet","Wand2":"wand","Warehouse":"building-warehouse","Waves":"ripple","Webcam":"device-computer-camera","Wifi":"wifi","Wine":"glass","Wrench":"tool","X":"x","XCircle":"circle-x","Youtube":"brand-youtube","Zap":"bolt","ZoomIn":"zoom-in","ZoomOut":"zoom-out"}; + +const TABLER_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const LUCIDE_NAME_RE = /^[A-Za-z][A-Za-z0-9]{0,63}$/; + +/** Stored form of a Tabler icon name. */ +export function toStoredIconName(tablerName: string): string { + return TABLER_ICON_PREFIX + tablerName; +} + +/** + * Resolve a stored icon name to a Tabler name: `tabler:` as is, a + * known Lucide name through the table, any other Lucide name by converting + * its PascalCase to kebab-case (most Lucide and Tabler names agree). + */ +export function toTablerName(stored: string | null | undefined): string | null { + if (!stored) return null; + if (stored.startsWith(TABLER_ICON_PREFIX)) { + const name = stored.slice(TABLER_ICON_PREFIX.length); + return TABLER_NAME_RE.test(name) ? name : null; + } + if (LEGACY_LUCIDE[stored]) return LEGACY_LUCIDE[stored]; + // A bare Tabler name, as an admin might type it. + if (TABLER_NAME_RE.test(stored)) return stored; + if (!LUCIDE_NAME_RE.test(stored)) return null; + return stored.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Za-z])(\d)/g, '$1-$2').toLowerCase(); +} + +const componentKey = (name: string) => + 'Icon' + name.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(''); + +let allIcons: Record | null = null; +let allIconsPromise: Promise> | null = null; + +function loadAllIcons(): Promise> { + allIconsPromise ??= import('@tabler/icons-react').then((m) => { + allIcons = m.icons as unknown as Record; + return allIcons; + }); + return allIconsPromise; +} + +/** Every outline icon name in Tabler, for the picker's "show all" list. */ +export async function loadAllIconNames(): Promise { + const m = await import('@tabler/icons-react'); + return m.iconsList.default.filter((n) => !n.endsWith('-filled')); +} + +const lazyIcons = new Map(); + +function lazyIcon(name: string): TablerIcon { + const LazyIcon = forwardRef[0]>(function LazyIcon(props, ref) { + const [Resolved, setResolved] = useState( + () => (allIcons ? allIcons[componentKey(name)] ?? IconWorld : null), + ); + useEffect(() => { + if (Resolved) return; + let live = true; + loadAllIcons().then((icons) => { + if (live) setResolved(() => icons[componentKey(name)] ?? IconWorld); + }); + return () => { + live = false; + }; + }, [Resolved]); + if (!Resolved) { + const size = props.size ?? 24; + return ; + } + return ; + }); + return LazyIcon as unknown as TablerIcon; +} + +/** + * The component for a stored icon name, or undefined when there is no usable + * name. Unknown names render the world icon once the full set has loaded. + */ +export function iconForName(stored: string | null | undefined): TablerIcon | undefined { + const name = toTablerName(stored); + if (!name) return undefined; + const curated = CURATED[name]; + if (curated) return curated; + let icon = lazyIcons.get(name); + if (!icon) { + icon = lazyIcon(name); + lazyIcons.set(name, icon); + } + return icon; +} diff --git a/components/identity/identity-manager-modal.tsx b/components/identity/identity-manager-modal.tsx index 5f42a5632..551550240 100644 --- a/components/identity/identity-manager-modal.tsx +++ b/components/identity/identity-manager-modal.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react'; import { useTranslations } from 'next-intl'; -import { X, Mail, Pencil, Trash2, Plus, AlertTriangle, Star } from 'lucide-react'; +import { X, Mail, Pencil, Trash2, Plus, AlertTriangle, Star } from '@/components/icons'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; diff --git a/components/identity/sub-address-helper.tsx b/components/identity/sub-address-helper.tsx index 50836dc69..75ffc85e5 100644 --- a/components/identity/sub-address-helper.tsx +++ b/components/identity/sub-address-helper.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useMemo } from 'react'; import { useTranslations } from 'next-intl'; -import { Plus, Tag, X } from 'lucide-react'; +import { Plus, Tag, X } from '@/components/icons'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; diff --git a/components/keyboard-shortcuts-modal.tsx b/components/keyboard-shortcuts-modal.tsx index 8dc07e2c8..dff59482b 100644 --- a/components/keyboard-shortcuts-modal.tsx +++ b/components/keyboard-shortcuts-modal.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; -import { X, Keyboard } from "lucide-react"; +import { X, Keyboard } from "@/components/icons"; import { KEYBOARD_SHORTCUTS } from "@/hooks/use-keyboard-shortcuts"; import { cn } from "@/lib/utils"; import { useFocusTrap } from "@/hooks/use-focus-trap"; diff --git a/components/layout/account-switcher.tsx b/components/layout/account-switcher.tsx index 24c0c58c8..2581f1f27 100644 --- a/components/layout/account-switcher.tsx +++ b/components/layout/account-switcher.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useLayoutEffect, useCallback, useMemo, useId } from "react"; import { createPortal } from "react-dom"; -import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical, X } from "lucide-react"; +import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical, X } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useAccountStore, type AccountEntry } from "@/stores/account-store"; import { useAuthStore } from "@/stores/auth-store"; diff --git a/components/layout/icon-picker.tsx b/components/layout/icon-picker.tsx index 70bc0fc82..f5467b4d5 100644 --- a/components/layout/icon-picker.tsx +++ b/components/layout/icon-picker.tsx @@ -1,59 +1,12 @@ 'use client'; -import { useState, useMemo, useRef, useCallback } from 'react'; +import { useState, useMemo, useRef, useCallback, useEffect } from 'react'; import { useTranslations } from 'next-intl'; -import { icons as lucideIcons, type LucideIcon } from 'lucide-react'; -import { Search, X } from 'lucide-react'; +import { iconForName, loadAllIconNames, POPULAR_ICON_NAMES, toStoredIconName, toTablerName } from '@/components/icons'; +import { Search, X } from '@/components/icons'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; -// Curated list of commonly useful icons, organized by category -const POPULAR_ICONS = [ - // Communication - 'Globe', 'Rss', 'Radio', 'Podcast', 'MessageCircle', 'MessageSquare', 'MessagesSquare', - 'Phone', 'Video', 'Webcam', 'Headphones', 'Mic', - // Productivity - 'FileText', 'FileSpreadsheet', 'Notebook', 'BookOpen', 'ClipboardList', - 'ListTodo', 'CheckSquare', 'SquareKanban', 'Kanban', 'Trello', - 'PenLine', 'Pencil', 'Edit', 'NotebookPen', - // Dev / Tech - 'Code', 'Terminal', 'Braces', 'Bug', 'Database', 'Server', 'Cpu', - 'HardDrive', 'Monitor', 'Laptop', 'Smartphone', 'Tablet', - 'Wifi', 'Cloud', 'CloudDownload', 'CloudUpload', - // Social / People - 'Users', 'UserPlus', 'UserCircle', 'Contact', 'PersonStanding', - 'Heart', 'ThumbsUp', 'Star', 'Award', 'Trophy', 'Crown', - // Media - 'Image', 'Camera', 'Film', 'Music', 'Play', 'Tv', 'Youtube', 'Clapperboard', - 'Palette', 'Paintbrush', 'Brush', - // Navigation / Location - 'Map', 'MapPin', 'Navigation', 'Compass', 'Home', 'Building', 'Building2', - 'Landmark', 'Store', 'Warehouse', - // Finance - 'DollarSign', 'Euro', 'CreditCard', 'Wallet', 'Receipt', 'PiggyBank', - 'TrendingUp', 'BarChart', 'BarChart3', 'LineChart', 'PieChart', - // Security - 'Shield', 'ShieldCheck', 'Lock', 'Unlock', 'Key', 'Fingerprint', 'Eye', - // Science / Health - 'Beaker', 'Atom', 'Dna', 'Microscope', 'Stethoscope', 'HeartPulse', 'Pill', - 'Syringe', 'Thermometer', - // Nature - 'Sun', 'Moon', 'CloudSun', 'Snowflake', 'Zap', 'Flame', - 'TreePine', 'Flower', 'Leaf', 'Mountain', 'Waves', - // Tools - 'Wrench', 'Hammer', 'Scissors', 'Ruler', 'Magnet', - 'Package', 'Gift', 'Box', 'Archive', - // Transport - 'Car', 'Bike', 'Bus', 'Train', 'Plane', 'Ship', 'Rocket', - // Food - 'Coffee', 'Wine', 'Beer', 'Pizza', 'Apple', 'Cake', 'CookingPot', - // Misc - 'Gamepad2', 'Dice5', 'Puzzle', 'Sparkles', 'Wand2', 'Bot', 'BrainCircuit', - 'Lightbulb', 'Bookmark', 'Flag', 'Bell', 'Clock', 'Timer', - 'Link', 'ExternalLink', 'QrCode', 'Scan', 'LayoutGrid', 'Layers', - 'Aperture', 'CircleDot', 'Target', 'Crosshair', -]; - interface IconPickerProps { value: string; onChange: (iconName: string) => void; @@ -66,22 +19,29 @@ export function IconPicker({ value, onChange, className }: IconPickerProps) { const [showAll, setShowAll] = useState(false); const gridRef = useRef(null); - // Get all available icon names - const allIconNames = useMemo(() => { - return Object.keys(lucideIcons).filter( - k => /^[A-Z]/.test(k) && k !== 'createLucideIcon' && k !== 'Icon' - ).sort(); - }, []); + // The full Tabler list is a separate chunk, fetched the first time "show all" + // is opened. Names are Tabler's kebab-case names; the stored value carries a + // `tabler:` prefix (older settings hold Lucide names, see toTablerName). + const [allIconNames, setAllIconNames] = useState(null); + useEffect(() => { + if (!showAll || allIconNames) return; + let live = true; + loadAllIconNames().then((names) => { if (live) setAllIconNames(names); }); + return () => { live = false; }; + }, [showAll, allIconNames]); + + const selected = toTablerName(value); const filteredIcons = useMemo(() => { - const source = showAll ? allIconNames : POPULAR_ICONS.filter(name => name in lucideIcons); - if (!search.trim()) return source; - const q = search.toLowerCase(); - return source.filter(name => name.toLowerCase().includes(q)); + const source = showAll ? allIconNames ?? POPULAR_ICON_NAMES : POPULAR_ICON_NAMES; + const q = search.trim().toLowerCase().replace(/\s+/g, '-'); + // Without a search the full list is capped; typing narrows it. + if (!q) return source.slice(0, 600); + return source.filter(name => name.includes(q)); }, [search, showAll, allIconNames]); const renderIcon = useCallback((name: string) => { - const IconComponent = lucideIcons[name as keyof typeof lucideIcons] as LucideIcon | undefined; + const IconComponent = iconForName(toStoredIconName(name)); if (!IconComponent) return null; return ; }, []); @@ -126,11 +86,11 @@ export function IconPicker({ value, onChange, className }: IconPickerProps) {