fix: pass data-icon prop through FontAwesome icon wrappers

Replaced all bare `FC<{ className?: string }>` and `FC<HTMLAttributes<...>>`
wrappers that only forwarded `className` with `faIcon()` from
`src/lib/icon-wrapper.ts`, ensuring props like `data-icon` needed by the
Button component's CSS selector `*:data-icon:size-5` are correctly forwarded.
Also widened `NavItemBaseProps.icon` and `NavItemType.icon` prop types to
`FC<Record<string, any>>` to stay compatible with `faIcon()` return type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 11:19:46 +05:30
parent 2ace6efae5
commit 631acf63dc
11 changed files with 65 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from "react"; import type { FC, MouseEventHandler, ReactNode } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown, faArrowUpRightFromSquare } from "@fortawesome/pro-duotone-svg-icons"; import { faChevronDown, faArrowUpRightFromSquare } from "@fortawesome/pro-duotone-svg-icons";
import { Link as AriaLink } from "react-aria-components"; import { Link as AriaLink } from "react-aria-components";
@@ -20,7 +20,7 @@ interface NavItemBaseProps {
/** Type of the nav item. */ /** Type of the nav item. */
type: "link" | "collapsible" | "collapsible-child"; type: "link" | "collapsible" | "collapsible-child";
/** Icon component to display. */ /** Icon component to display. */
icon?: FC<HTMLAttributes<HTMLOrSVGElement>>; icon?: FC<Record<string, any>>;
/** Badge to display. */ /** Badge to display. */
badge?: ReactNode; badge?: ReactNode;
/** Whether the nav item is currently active. */ /** Whether the nav item is currently active. */

View File

@@ -6,11 +6,11 @@ export type NavItemType = {
/** URL to navigate to when the nav item is clicked. */ /** URL to navigate to when the nav item is clicked. */
href?: string; href?: string;
/** Icon component to display. */ /** Icon component to display. */
icon?: FC<{ className?: string }>; icon?: FC<Record<string, any>>;
/** Badge to display. */ /** Badge to display. */
badge?: ReactNode; badge?: ReactNode;
/** List of sub-items to display. */ /** List of sub-items to display. */
items?: { label: string; href: string; icon?: FC<{ className?: string }>; badge?: ReactNode }[]; items?: { label: string; href: string; icon?: FC<Record<string, any>>; badge?: ReactNode }[];
/** Whether this nav item is a divider. */ /** Whether this nav item is a divider. */
divider?: boolean; divider?: boolean;
}; };

View File

@@ -1,10 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarPlus, faXmark } from '@fortawesome/pro-duotone-svg-icons'; import { faCalendarPlus, faXmark } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
const CalendarPlus02: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faCalendarPlus} className={className} />; const CalendarPlus02 = faIcon(faCalendarPlus);
const XClose: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faXmark} className={className} />; const XClose = faIcon(faXmark);
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Select } from '@/components/base/select/select'; import { Select } from '@/components/base/select/select';
import { TextArea } from '@/components/base/textarea/textarea'; import { TextArea } from '@/components/base/textarea/textarea';

View File

@@ -1,5 +1,4 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faPhone, faPhone,
faPhoneArrowDown, faPhoneArrowDown,
@@ -13,19 +12,19 @@ import {
faFloppyDisk, faFloppyDisk,
faCalendarPlus, faCalendarPlus,
} from '@fortawesome/pro-duotone-svg-icons'; } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
const Phone01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhone} className={className} />; const Phone01 = faIcon(faPhone);
const PhoneIncoming01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhoneArrowDown} className={className} />; const PhoneIncoming01 = faIcon(faPhoneArrowDown);
const PhoneOutgoing01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhoneArrowUp} className={className} />; const PhoneOutgoing01 = faIcon(faPhoneArrowUp);
const PhoneHangUp: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhoneHangup} className={className} />; const PhoneHangUp = faIcon(faPhoneHangup);
const PhoneX: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhoneXmark} className={className} />; const PhoneX = faIcon(faPhoneXmark);
const MicrophoneOff01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faMicrophoneSlash} className={className} />; const MicrophoneOff01 = faIcon(faMicrophoneSlash);
const Microphone01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faMicrophone} className={className} />; const Microphone01 = faIcon(faMicrophone);
const PauseCircle: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPause} className={className} />; const PauseCircle = faIcon(faPause);
const CheckCircle: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faCircleCheck} className={className} />; const CheckCircle = faIcon(faCircleCheck);
const Save01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faFloppyDisk} className={className} />; const Save01 = faIcon(faFloppyDisk);
const CalendarPlus02: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faCalendarPlus} className={className} />; const CalendarPlus02 = faIcon(faCalendarPlus);
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { TextArea } from '@/components/base/textarea/textarea'; import { TextArea } from '@/components/base/textarea/textarea';
import { AppointmentForm } from '@/components/call-desk/appointment-form'; import { AppointmentForm } from '@/components/call-desk/appointment-form';

View File

@@ -1,10 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { FC, HTMLAttributes } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPhoneArrowDown, faPhoneArrowUp, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; import { faPhoneArrowDown, faPhoneArrowUp, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons';
import { faIcon } from '@/lib/icon-wrapper';
import { Table } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
const SearchLg: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faMagnifyingGlass} className={className} />; const SearchLg = faIcon(faMagnifyingGlass);
import { Badge } from '@/components/base/badges/badges'; import { Badge } from '@/components/base/badges/badges';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
@@ -129,12 +128,8 @@ const formatSource = (source: string): string => {
return map[source] ?? source.replace(/_/g, ' '); return map[source] ?? source.replace(/_/g, ' ');
}; };
const IconInbound: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const IconInbound = faIcon(faPhoneArrowDown);
<FontAwesomeIcon icon={faPhoneArrowDown} className={className} /> const IconOutbound = faIcon(faPhoneArrowUp);
);
const IconOutbound: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faPhoneArrowUp} className={className} />
);
const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], leads: WorklistLead[]): WorklistRow[] => { const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], leads: WorklistLead[]): WorklistRow[] => {
const rows: WorklistRow[] = []; const rows: WorklistRow[] = [];

View File

@@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPenToSquare } from '@fortawesome/pro-duotone-svg-icons'; import { faPenToSquare } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu'; import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Select } from '@/components/base/select/select'; import { Select } from '@/components/base/select/select';
@@ -10,9 +9,7 @@ import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
import type { Campaign, CampaignStatus } from '@/types/entities'; import type { Campaign, CampaignStatus } from '@/types/entities';
const PenIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const PenIcon = faIcon(faPenToSquare);
<FontAwesomeIcon icon={faPenToSquare} className={className} />
);
type CampaignEditSlideoutProps = { type CampaignEditSlideoutProps = {
isOpen: boolean; isOpen: boolean;

View File

@@ -1,15 +1,13 @@
import { useState } from 'react'; import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGear, faCopy, faLink } from '@fortawesome/pro-duotone-svg-icons'; import { faGear, faCopy, faLink } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu'; import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
const GearIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const GearIcon = faIcon(faGear);
<FontAwesomeIcon icon={faGear} className={className} />
);
type IntegrationType = 'ozonetel' | 'whatsapp' | 'facebook' | 'google' | 'instagram' | 'website' | 'email'; type IntegrationType = 'ozonetel' | 'whatsapp' | 'facebook' | 'google' | 'instagram' | 'website' | 'email';
@@ -166,9 +164,7 @@ export const IntegrationEditSlideout = ({ isOpen, onOpenChange, integration }: I
<Button <Button
size="md" size="md"
color="primary" color="primary"
iconLeading={({ className }: { className?: string }) => ( iconLeading={faIcon(faLink)}
<FontAwesomeIcon icon={faLink} className={className} />
)}
onClick={handleOAuthConnect} onClick={handleOAuthConnect}
> >
Connect {integration.name} Connect {integration.name}
@@ -187,9 +183,7 @@ export const IntegrationEditSlideout = ({ isOpen, onOpenChange, integration }: I
<Button <Button
size="sm" size="sm"
color="secondary" color="secondary"
iconLeading={({ className }: { className?: string }) => ( iconLeading={faIcon(faCopy)}
<FontAwesomeIcon icon={faCopy} className={className} />
)}
onClick={() => handleCopy(values[field.key] ?? '')} onClick={() => handleCopy(values[field.key] ?? '')}
> >
Copy Copy

View File

@@ -1,4 +1,3 @@
import type { FC, HTMLAttributes } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faBullhorn, faBullhorn,
@@ -14,6 +13,7 @@ import {
faPlug, faPlug,
faUsers, faUsers,
} from "@fortawesome/pro-duotone-svg-icons"; } from "@fortawesome/pro-duotone-svg-icons";
import { faIcon } from "@/lib/icon-wrapper";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { MobileNavigationHeader } from "@/components/application/app-navigation/base-components/mobile-header"; import { MobileNavigationHeader } from "@/components/application/app-navigation/base-components/mobile-header";
@@ -30,36 +30,16 @@ import { cx } from "@/utils/cx";
const EXPANDED_WIDTH = 292; const EXPANDED_WIDTH = 292;
const COLLAPSED_WIDTH = 64; const COLLAPSED_WIDTH = 64;
const IconGrid2: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const IconGrid2 = faIcon(faGrid2);
<FontAwesomeIcon icon={faGrid2} className={className} /> const IconBullhorn = faIcon(faBullhorn);
); const IconCommentDots = faIcon(faCommentDots);
const IconBullhorn: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const IconChartMixed = faIcon(faChartMixed);
<FontAwesomeIcon icon={faBullhorn} className={className} /> const IconPlug = faIcon(faPlug);
); const IconGear = faIcon(faGear);
const IconCommentDots: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const IconPhone = faIcon(faPhone);
<FontAwesomeIcon icon={faCommentDots} className={className} /> const IconClockRewind = faIcon(faClockRotateLeft);
); const IconUsers = faIcon(faUsers);
const IconChartMixed: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const IconHospitalUser = faIcon(faHospitalUser);
<FontAwesomeIcon icon={faChartMixed} className={className} />
);
const IconPlug: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faPlug} className={className} />
);
const IconGear: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faGear} className={className} />
);
const IconPhone: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faPhone} className={className} />
);
const IconClockRewind: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faClockRotateLeft} className={className} />
);
const IconUsers: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faUsers} className={className} />
);
const IconHospitalUser: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faHospitalUser} className={className} />
);
type NavSection = { type NavSection = {
label: string; label: string;

View File

@@ -1,23 +1,16 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMagnifyingGlass, faUser, faCalendar } from '@fortawesome/pro-duotone-svg-icons'; import { faMagnifyingGlass, faUser, faCalendar } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Badge } from '@/components/base/badges/badges'; import { Badge } from '@/components/base/badges/badges';
import { useData } from '@/providers/data-provider'; import { useData } from '@/providers/data-provider';
import { formatPhone } from '@/lib/format'; import { formatPhone } from '@/lib/format';
import { cx } from '@/utils/cx'; import { cx } from '@/utils/cx';
const SearchIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => ( const SearchIcon = faIcon(faMagnifyingGlass);
<FontAwesomeIcon icon={faMagnifyingGlass} className={className} /> const UserIcon = faIcon(faUser);
); const CalendarIcon = faIcon(faCalendar);
const UserIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faUser} className={className} />
);
const CalendarIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
<FontAwesomeIcon icon={faCalendar} className={className} />
);
type SearchResultType = 'lead' | 'patient' | 'appointment'; type SearchResultType = 'lead' | 'patient' | 'appointment';
@@ -33,7 +26,7 @@ type GlobalSearchProps = {
onSelectResult?: (result: SearchResult) => void; onSelectResult?: (result: SearchResult) => void;
}; };
const TYPE_ICONS: Record<SearchResultType, FC<{ className?: string }>> = { const TYPE_ICONS: Record<SearchResultType, ReturnType<typeof faIcon>> = {
lead: UserIcon, lead: UserIcon,
patient: UserIcon, patient: UserIcon,
appointment: CalendarIcon, appointment: CalendarIcon,

14
src/lib/icon-wrapper.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { FC } from 'react';
import { createElement } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
// Creates a wrapper component that passes all props (including data-icon)
// to FontAwesomeIcon. This is needed because the Button component uses
// data-icon CSS selectors for sizing.
export const faIcon = (icon: IconDefinition): FC<Record<string, any>> => {
const IconComponent: FC<Record<string, any>> = (props) =>
createElement(FontAwesomeIcon, { icon, ...props });
IconComponent.displayName = `FAIcon(${icon.iconName})`;
return IconComponent;
};

View File

@@ -1,14 +1,13 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPhone, faEnvelope, faCalendar, faCommentDots, faPlus } from '@fortawesome/pro-duotone-svg-icons'; import { faPhone, faEnvelope, faCalendar, faCommentDots, faPlus } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react'; import { faIcon } from '@/lib/icon-wrapper';
const Phone01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPhone} className={className} />; const Phone01 = faIcon(faPhone);
const Mail01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faEnvelope} className={className} />; const Mail01 = faIcon(faEnvelope);
const Calendar: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faCalendar} className={className} />; const Calendar = faIcon(faCalendar);
const MessageTextSquare01: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faCommentDots} className={className} />; const MessageTextSquare01 = faIcon(faCommentDots);
const Plus: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => <FontAwesomeIcon icon={faPlus} className={className} />; const Plus = faIcon(faPlus);
import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs'; import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs';
import { TopBar } from '@/components/layout/top-bar'; import { TopBar } from '@/components/layout/top-bar';
import { Avatar } from '@/components/base/avatar/avatar'; import { Avatar } from '@/components/base/avatar/avatar';