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) {