feat(cabinet): migrate all icons to the panel's Phosphor set

Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).

- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
  stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
  thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
  (checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
  default differed (211 call sites)
This commit is contained in:
c0mrade
2026-05-31 18:50:50 +03:00
parent a62d689fd3
commit d0e0b6b7e3
94 changed files with 1368 additions and 5059 deletions

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { promoApi, PromoOffer } from '../api/promo';
import { ClockIcon, CheckIcon } from './icons';
import { ClockIcon, CheckIcon, XCircleIcon } from './icons';
import { useDestructiveConfirm } from '@/platform/hooks/useNativeDialog';
// Helper functions
@@ -69,17 +69,6 @@ const getOfferDescription = (
return t('promo.offers.activateDiscountHint');
};
// Icons for deactivation
const XCircleIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
interface PromoOffersSectionProps {
className?: string;
}
@@ -237,7 +226,7 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
onClick={handleDeactivateClick}
className="flex items-center justify-center gap-1.5 rounded-xl border border-dark-600/50 bg-dark-900/50 px-4 py-2.5 text-sm text-dark-400 transition-colors hover:border-error-500/30 hover:bg-error-500/10 hover:text-error-400"
>
<XCircleIcon />
<XCircleIcon className="h-4 w-4" />
<span>{t('promo.deactivate.button')}</span>
</button>
</div>

View File

@@ -12,69 +12,14 @@ import { useCurrency } from '../hooks/useCurrency';
import { useTelegramSDK } from '../hooks/useTelegramSDK';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { useHaptic } from '@/platform';
// Icons
const CheckCircleIcon = () => (
<svg
className="h-16 w-16"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const WalletIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
);
const RocketIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"
/>
</svg>
);
const DevicesIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
/>
</svg>
);
const TrafficIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg>
);
const CloseIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
import {
CheckCircleIcon,
CloseIcon,
DevicesIcon,
RocketIcon,
TrafficIcon,
WalletIcon,
} from '@/components/icons';
export default function SuccessNotificationModal() {
const { t } = useTranslation();
@@ -161,33 +106,33 @@ export default function SuccessNotificationModal() {
// Determine title and message
let title = data.title;
const message = data.message;
let icon = <CheckCircleIcon />;
let icon = <CheckCircleIcon className="h-16 w-16" />;
let gradientClass = 'from-success-500 to-success-600';
if (!title) {
if (isBalanceTopup) {
title = t('successNotification.balanceTopup.title', 'Balance topped up!');
icon = <WalletIcon />;
icon = <WalletIcon className="h-8 w-8" />;
gradientClass = 'from-success-500 to-success-600';
} else if (data.type === 'subscription_activated') {
title = t('successNotification.subscriptionActivated.title', 'Subscription activated!');
icon = <RocketIcon />;
icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'subscription_renewed') {
title = t('successNotification.subscriptionRenewed.title', 'Subscription renewed!');
icon = <RocketIcon />;
icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'subscription_purchased') {
title = t('successNotification.subscriptionPurchased.title', 'Subscription purchased!');
icon = <RocketIcon />;
icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'devices_purchased') {
title = t('successNotification.devicesPurchased.title', 'Devices added!');
icon = <DevicesIcon />;
icon = <DevicesIcon className="h-8 w-8" />;
gradientClass = 'from-blue-500 to-cyan-600';
} else if (data.type === 'traffic_purchased') {
title = t('successNotification.trafficPurchased.title', 'Traffic added!');
icon = <TrafficIcon />;
icon = <TrafficIcon className="h-8 w-8" />;
gradientClass = 'from-success-500 to-success-600';
}
}
@@ -334,7 +279,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
>
<RocketIcon />
<RocketIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button>
)}
@@ -344,7 +289,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToBalance}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600"
>
<WalletIcon />
<WalletIcon className="h-8 w-8" />
<span>{t('successNotification.goToBalance', 'Go to Balance')}</span>
</button>
)}
@@ -354,7 +299,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
>
<DevicesIcon />
<DevicesIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button>
)}
@@ -364,7 +309,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600"
>
<TrafficIcon />
<TrafficIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button>
)}

View File

@@ -8,22 +8,7 @@ import { useToast } from './Toast';
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
import { useHeaderHeight } from '../hooks/useHeaderHeight';
import type { TicketNotification } from '../types';
const BellIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
);
const CheckIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);
import { BellIcon, CheckIcon } from '@/components/icons';
interface TicketNotificationBellProps {
isAdmin?: boolean;

View File

@@ -18,6 +18,15 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { PiCaretDownDuotone } from 'react-icons/pi';
import {
GripIcon,
TrashIcon,
PlusIcon,
LinkIcon,
ArrowUpIcon,
ArrowDownIcon,
} from '@/components/icons';
import {
menuLayoutApi,
type MenuConfig,
@@ -31,82 +40,10 @@ import { Toggle } from './Toggle';
import { useNotify } from '../../platform/hooks/useNotify';
import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
const GripIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
const TrashIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
);
const PlusIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg
<PiCaretDownDuotone
className={`h-3.5 w-3.5 transition-transform ${expanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
);
const LinkIcon = () => (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.54a4.5 4.5 0 00-1.242-7.244l-4.5-4.5a4.5 4.5 0 00-6.364 6.364L4.03 8.591"
/>
</svg>
);
const ArrowUpIcon = () => (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg>
);
const ArrowDownIcon = () => (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
/>
);
function generateId(): string {
@@ -200,7 +137,7 @@ function ButtonChip({
: 'cursor-default text-dark-700'
}`}
>
<ArrowUpIcon />
<ArrowUpIcon className="h-3.5 w-3.5" />
</button>
<button
onClick={onMoveDown ?? undefined}
@@ -212,7 +149,7 @@ function ButtonChip({
: 'cursor-default text-dark-700'
}`}
>
<ArrowDownIcon />
<ArrowDownIcon className="h-3.5 w-3.5" />
</button>
</div>
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${colorDotClass}`} />
@@ -221,7 +158,7 @@ function ButtonChip({
</span>
{!isBuiltin && (
<span className="text-dark-500" title="URL">
<LinkIcon />
<LinkIcon className="h-3.5 w-3.5" />
</span>
)}
<Toggle checked={button.enabled} onChange={() => onUpdate({ enabled: !button.enabled })} />
@@ -236,7 +173,7 @@ function ButtonChip({
onClick={onRemove}
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
>
<TrashIcon />
<TrashIcon className="h-4 w-4" />
</button>
)}
</div>
@@ -425,7 +362,7 @@ function SortableRow({
onClick={() => onRemoveRow(row.id)}
className="rounded-lg p-1.5 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
>
<TrashIcon />
<TrashIcon className="h-4 w-4" />
</button>
)}
</div>
@@ -481,7 +418,7 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In
onClick={() => setIsOpen(true)}
className="flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-dark-700/50 py-2.5 text-sm text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400"
>
<PlusIcon />
<PlusIcon className="h-4 w-4" />
{t('admin.menuEditor.addButton')}
</button>
);
@@ -516,7 +453,7 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In
}}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-dark-200 transition-colors hover:bg-dark-700/50"
>
<LinkIcon />
<LinkIcon className="h-3.5 w-3.5" />
{t('admin.menuEditor.addUrlButton')}
</button>
<button
@@ -843,7 +780,7 @@ export function MenuEditorTab() {
onClick={addRow}
className="flex w-full items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-dark-700/50 py-4 text-sm font-medium text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400"
>
<PlusIcon />
<PlusIcon className="h-4 w-4" />
{t('admin.menuEditor.addRow')}
</button>

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSortable } from '@dnd-kit/sortable';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { CSS } from '@dnd-kit/utilities';
import { cn } from '../../lib/utils';
import { GripIcon, TrashIcon } from '../icons/LandingIcons';
@@ -10,15 +11,7 @@ import type { PaymentMethodSubOptionInfo } from '../../types';
export type MethodWithId = AdminLandingPaymentMethod & { _id: string };
const ChevronDownIcon = ({ open }: { open: boolean }) => (
<svg
className={cn('h-5 w-5 transition-transform', open && 'rotate-180')}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
<PiCaretDownDuotone className={cn('h-5 w-5 transition-transform', open && 'rotate-180')} />
);
interface SortableSelectedMethodProps {

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { CheckIcon, XCloseIcon } from '@/components/icons';
import { useFocusTrap } from '@/hooks/useFocusTrap';
import { DropdownSelect } from './DropdownSelect';
import type { UserListItem } from '../../../api/adminUsers';
@@ -48,24 +49,6 @@ export interface ModalState {
progress: ProgressState | null;
}
const CheckIcon = () => (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
);
const XCloseIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
function ProgressView({ progress }: { progress: ProgressState }) {
const { t } = useTranslation();
const logEndRef = useRef<HTMLDivElement>(null);
@@ -520,7 +503,7 @@ export function ActionModal({
)}
aria-pressed={forceDeleteActivePaid}
>
{forceDeleteActivePaid && <CheckIcon />}
{forceDeleteActivePaid && <CheckIcon className="h-3 w-3" />}
</button>
<span
className={cn(
@@ -558,7 +541,7 @@ export function ActionModal({
)}
aria-pressed={deleteFromPanel}
>
{deleteFromPanel && <CheckIcon />}
{deleteFromPanel && <CheckIcon className="h-3 w-3" />}
</button>
<span
className={cn(

View File

@@ -1,3 +1,4 @@
import { ChevronDownIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
// ──────────────────────────────────────────────────────────────────
@@ -7,11 +8,10 @@ import { cn } from '@/lib/utils';
// chrome (border, focus ring, chevron).
// ──────────────────────────────────────────────────────────────────
export const ChevronDownIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
);
// Re-exported so the sibling FloatingActionBar / MultiSelectDropdown keep
// importing the chevron from this module while the glyph itself now comes
// from the central Phosphor barrel instead of a hand-written SVG.
export { ChevronDownIcon };
export interface DropdownOption {
value: string;
@@ -40,7 +40,7 @@ export function DropdownSelect({ value, options, onChange, className }: Dropdown
))}
</select>
<div className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-dark-500">
<ChevronDownIcon />
<ChevronDownIcon className="h-4 w-4" />
</div>
</div>
);

View File

@@ -1,164 +1,35 @@
// ──────────────────────────────────────────────────────────────────
// TrafficIcons — the inline SVG set used across AdminTrafficUsage
// (header controls, filter buttons, sort indicator, etc.). Page-local
// in spirit; co-located here so the parent page imports a single
// flat barrel rather than redefining 15 inline icons.
// TrafficIcons — the icon set used across AdminTrafficUsage (header
// controls, filter buttons, sort indicator, etc.). The glyphs now come
// from the central Phosphor barrel (`@/components/icons`); only the
// sort indicator keeps its custom `direction` prop and is therefore a
// thin local wrapper over the panel's Phosphor caret icons.
// ──────────────────────────────────────────────────────────────────
export const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
);
import { PiCaretDownDuotone, PiCaretUpDownDuotone, PiCaretUpDuotone } from 'react-icons/pi';
export const ChevronLeftIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
export {
CalendarIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
DownloadIcon,
FilterIcon,
GlobeIcon,
RefreshIcon,
SearchIcon,
ServerIcon,
ServerSmallIcon,
ShieldIcon,
StatusIcon,
XIcon,
} from '@/components/icons';
export const ChevronRightIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
);
export const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
);
export const DownloadIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
);
export const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) => (
<svg
className="ml-1 inline h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
{direction === 'asc' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
) : direction === 'desc' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
)}
</svg>
);
export const FilterIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
);
export const ChevronDownIcon = () => (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
);
export const ServerIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 00-.12-1.03l-2.268-9.64a3.375 3.375 0 00-3.285-2.602H7.923a3.375 3.375 0 00-3.285 2.602l-2.268 9.64a4.5 4.5 0 00-.12 1.03v.228m19.5 0a3 3 0 01-3 3H5.25a3 3 0 01-3-3m19.5 0a3 3 0 00-3-3H5.25a3 3 0 00-3 3m16.5 0h.008v.008h-.008v-.008zm-3 0h.008v.008h-.008v-.008z"
/>
</svg>
);
export const CalendarIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
export const XIcon = () => (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
export const StatusIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
export const GlobeIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
);
export const ShieldIcon = () => (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
export const ServerSmallIcon = () => (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
/>
</svg>
);
export const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) =>
direction === 'asc' ? (
<PiCaretUpDuotone className="ml-1 inline h-3 w-3" />
) : direction === 'desc' ? (
<PiCaretDownDuotone className="ml-1 inline h-3 w-3" />
) : (
<PiCaretUpDownDuotone className="ml-1 inline h-3 w-3" />
);

View File

@@ -49,13 +49,13 @@ export function CountryFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`}
>
<GlobeIcon />
<GlobeIcon className="h-4 w-4" />
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount}
</span>
)}
<ChevronDownIcon />
<ChevronDownIcon className="h-3 w-3" />
</button>
{open && (

View File

@@ -52,14 +52,14 @@ export function NodeFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`}
>
<ServerIcon />
<ServerIcon className="h-4 w-4" />
{t('admin.trafficUsage.nodes')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount}
</span>
)}
<ChevronDownIcon />
<ChevronDownIcon className="h-3 w-3" />
</button>
{open && (

View File

@@ -42,7 +42,7 @@ export function PeriodSelector({
if (dateMode) {
return (
<div className="flex items-center gap-2">
<CalendarIcon />
<CalendarIcon className="h-4 w-4" />
<span className="text-xs text-dark-400">{t('admin.trafficUsage.dateFrom')}</span>
<input
type="date"
@@ -66,7 +66,7 @@ export function PeriodSelector({
className="rounded-lg p-1 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
title={t('admin.trafficUsage.period')}
>
<XIcon />
<XIcon className="h-3 w-3" />
</button>
</div>
);
@@ -96,7 +96,7 @@ export function PeriodSelector({
className="rounded-lg border border-dark-700 bg-dark-800 p-1.5 text-dark-400 transition-colors hover:border-dark-600 hover:bg-dark-700 hover:text-dark-200"
title={t('admin.trafficUsage.customDates')}
>
<CalendarIcon />
<CalendarIcon className="h-4 w-4" />
</button>
</div>
);

View File

@@ -63,14 +63,14 @@ export function StatusFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`}
>
<StatusIcon />
<StatusIcon className="h-4 w-4" />
{t('admin.trafficUsage.status')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount}
</span>
)}
<ChevronDownIcon />
<ChevronDownIcon className="h-3 w-3" />
</button>
{open && (

View File

@@ -50,14 +50,14 @@ export function TariffFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`}
>
<FilterIcon />
<FilterIcon className="h-4 w-4" />
{t('admin.trafficUsage.tariff')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount}
</span>
)}
<ChevronDownIcon />
<ChevronDownIcon className="h-3 w-3" />
</button>
{open && (

View File

@@ -6,22 +6,7 @@ import { adminUsersApi, type UserDetailResponse } from '../../../api/adminUsers'
import { promocodesApi } from '../../../api/promocodes';
import { promoOffersApi } from '../../../api/promoOffers';
import { createNumberInputHandler, toNumber } from '../../../utils/inputHelpers';
// ──────────────────────────────────────────────────────────────────
// Icons — local; balance is the only consumer.
// ──────────────────────────────────────────────────────────────────
const PlusIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const MinusIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
</svg>
);
import { PlusIcon, MinusIcon } from '@/components/icons';
// ──────────────────────────────────────────────────────────────────
// Balance tab — current balance, add/subtract form, active promo
@@ -168,14 +153,14 @@ export function BalanceTab({
disabled={actionLoading || balanceAmount === ''}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-success-500 py-2 text-white transition-colors hover:bg-success-600 disabled:opacity-50"
>
<PlusIcon /> {t('admin.users.detail.balance.add')}
<PlusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.add')}
</button>
<button
onClick={() => handleUpdateBalance(false)}
disabled={actionLoading || balanceAmount === ''}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-error-500 py-2 text-white transition-colors hover:bg-error-600 disabled:opacity-50"
>
<MinusIcon /> {t('admin.users.detail.balance.subtract')}
<MinusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.subtract')}
</button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { MinusIcon, PlusIcon, RefreshIcon } from '@/components/icons';
import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
import { createNumberInputHandler } from '../../../utils/inputHelpers';
import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
@@ -37,28 +38,6 @@ function StatusBadge({ status }: { status: string }) {
);
}
const PlusIcon = () => (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const MinusIcon = () => (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
</svg>
);
const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
);
// Local device row type (matches the parent's inline type)
type DeviceRow = {
hwid: string;
@@ -390,7 +369,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
disabled={actionLoading || selectedSub.device_limit <= 1}
className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30"
>
<MinusIcon />
<MinusIcon className="h-3 w-3" />
</button>
<span className="min-w-[2ch] text-center text-dark-100">
{selectedSub.device_limit}
@@ -404,7 +383,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
}
className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30"
>
<PlusIcon />
<PlusIcon className="h-3 w-3" />
</button>
</div>
</div>

View File

@@ -1,26 +1,11 @@
import { useTranslation } from 'react-i18next';
import { ArrowDownIcon, ArrowUpIcon } from '@/components/icons';
import type {
UserDetailResponse,
UserSubscriptionInfo,
PanelSyncStatusResponse,
} from '../../../api/adminUsers';
// ──────────────────────────────────────────────────────────────────
// Icons (sync-tab-local — not used outside this view)
// ──────────────────────────────────────────────────────────────────
const ArrowDownIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
</svg>
);
const ArrowUpIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
</svg>
);
// ──────────────────────────────────────────────────────────────────
// Sync tab — compares bot DB vs panel data, offers a 2-way push
// ──────────────────────────────────────────────────────────────────

View File

@@ -12,6 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
import type { BlockRendererProps } from './blocks';
import TvQuickConnect from './TvQuickConnect';
import { BackIcon } from '@/components/icons';
const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
@@ -33,12 +34,6 @@ const RENDERERS: Record<string, React.ComponentType<BlockRendererProps>> = {
minimal: MinimalBlock,
};
const BackIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
);
interface Props {
appConfig: AppConfig;
onOpenDeepLink: (url: string) => void;
@@ -200,7 +195,7 @@ export default function InstallationGuide({
aria-label={t('common.back', 'Back')}
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
>
<BackIcon />
<BackIcon className="h-6 w-6" />
</button>
)}
<h2 className="flex-1 text-lg font-bold text-dark-100">

View File

@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { CheckIcon, CopyIcon } from '@/components/icons';
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
import { copyToClipboard } from '@/utils/clipboard';
@@ -20,22 +21,6 @@ function isValidExternalUrl(url: string | undefined): boolean {
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
}
const CopyIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
);
const CheckIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);
interface BlockButtonsProps {
buttons: RemnawaveButtonClient[] | undefined;
variant: 'light' | 'subtle';

View File

@@ -1,3 +1,4 @@
import { PiCaretRightDuotone } from 'react-icons/pi';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router';
import { useCurrency } from '../../hooks/useCurrency';
@@ -12,22 +13,7 @@ interface StatsGridProps {
}
const ChevronIcon = ({ color }: { color: string }) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
style={{ flexShrink: 0 }}
aria-hidden="true"
>
<path
d="M6 4l4 4-4 4"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<PiCaretRightDuotone width={16} height={16} style={{ flexShrink: 0, color }} aria-hidden="true" />
);
export default function StatsGrid({

View File

@@ -10,6 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
import { formatTraffic } from '../../utils/formatTraffic';
import { getGlassColors } from '../../utils/glassTheme';
import { HoverBorderGradient } from '../ui/hover-border-gradient';
import { RefreshIcon } from '@/components/icons';
import { useHaptic } from '../../platform';
import type { Subscription } from '../../types';
@@ -25,23 +26,6 @@ interface SubscriptionCardActiveProps {
connectedDevices: number;
}
const RefreshIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
);
export default function SubscriptionCardActive({
subscription,
trafficData,

View File

@@ -0,0 +1,85 @@
import {
PiTextBDuotone,
PiTextItalicDuotone,
PiTextUnderlineDuotone,
PiTextStrikethroughDuotone,
PiTextHOneDuotone,
PiTextHTwoDuotone,
PiTextHThreeDuotone,
PiListBulletsDuotone,
PiListNumbersDuotone,
PiQuotesDuotone,
PiCodeBlockDuotone,
PiTextAlignLeftDuotone,
PiTextAlignCenterDuotone,
PiHighlighterDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils';
interface IconProps {
className?: string;
}
/**
* Rich-text editor toolbar icons — Phosphor (react-icons/pi) Duotone, the
* panel's icon family. Shared by the TipTap toolbars in AdminNewsCreate and
* AdminInfoPageEditor. Names match the historical local definitions so the
* toolbars import instead of hand-rolling SVGs.
*/
export const BoldIcon = ({ className }: IconProps) => (
<PiTextBDuotone className={cn('h-5 w-5', className)} />
);
export const ItalicIcon = ({ className }: IconProps) => (
<PiTextItalicDuotone className={cn('h-5 w-5', className)} />
);
export const UnderlineIcon = ({ className }: IconProps) => (
<PiTextUnderlineDuotone className={cn('h-5 w-5', className)} />
);
export const StrikeIcon = ({ className }: IconProps) => (
<PiTextStrikethroughDuotone className={cn('h-5 w-5', className)} />
);
export const H1Icon = ({ className }: IconProps) => (
<PiTextHOneDuotone className={cn('h-5 w-5', className)} />
);
export const H2Icon = ({ className }: IconProps) => (
<PiTextHTwoDuotone className={cn('h-5 w-5', className)} />
);
export const H3Icon = ({ className }: IconProps) => (
<PiTextHThreeDuotone className={cn('h-5 w-5', className)} />
);
export const ListBulletIcon = ({ className }: IconProps) => (
<PiListBulletsDuotone className={cn('h-5 w-5', className)} />
);
export const ListOrderedIcon = ({ className }: IconProps) => (
<PiListNumbersDuotone className={cn('h-5 w-5', className)} />
);
export const QuoteIcon = ({ className }: IconProps) => (
<PiQuotesDuotone className={cn('h-5 w-5', className)} />
);
export const CodeBlockIcon = ({ className }: IconProps) => (
<PiCodeBlockDuotone className={cn('h-5 w-5', className)} />
);
export const AlignLeftIcon = ({ className }: IconProps) => (
<PiTextAlignLeftDuotone className={cn('h-5 w-5', className)} />
);
export const AlignCenterIcon = ({ className }: IconProps) => (
<PiTextAlignCenterDuotone className={cn('h-5 w-5', className)} />
);
export const HighlightIcon = ({ className }: IconProps) => (
<PiHighlighterDuotone className={cn('h-5 w-5', className)} />
);

View File

@@ -0,0 +1,415 @@
import {
PiSlidersHorizontalDuotone,
PiHeadsetDuotone,
PiArrowDownDuotone,
PiArrowRightDuotone,
PiArrowUpDuotone,
PiProhibitDuotone,
PiMoneyDuotone,
PiBellDuotone,
PiLightningDuotone,
PiRobotDuotone,
PiBroadcastDuotone,
PiAppWindowDuotone,
PiCalendarDotsDuotone,
PiCreditCardDuotone,
PiChartBarDuotone,
PiCheckCircleDuotone,
PiCaretUpDownDuotone,
PiCaretDownDuotone,
PiCaretLeftDuotone,
PiCaretUpDuotone,
PiCurrencyBtcDuotone,
PiDevicesDuotone,
PiFileTextDuotone,
PiCircleFill,
PiEnvelopeDuotone,
PiWarningDuotone,
PiArrowSquareOutDuotone,
PiEyeDuotone,
PiFunnelDuotone,
PiDotsSixDuotone,
PiDotsSixVerticalDuotone,
PiHeartbeatDuotone,
PiClockCounterClockwiseDuotone,
PiImageDuotone,
PiInfinityDuotone,
PiLinkDuotone,
PiMegaphoneDuotone,
PiMinusDuotone,
PiNewspaperDuotone,
PiHandshakeDuotone,
PiPushPinDuotone,
PiPlusDuotone,
PiPowerDuotone,
PiQuestionDuotone,
PiRepeatDuotone,
PiFlagDuotone,
PiArrowCounterClockwiseDuotone,
PiArrowClockwiseDuotone,
PiRocketDuotone,
PiFloppyDiskDuotone,
PiPaperPlaneTiltDuotone,
PiPercentDuotone,
PiHardDrivesDuotone,
PiGearSixDuotone,
PiShareNetworkDuotone,
PiSparkleDuotone,
PiChartLineDuotone,
PiGiftDuotone,
PiTimerDuotone,
PiCircleDuotone,
PiTagDuotone,
PiTelegramLogoDuotone,
PiTicketDuotone,
PiGaugeDuotone,
PiTrashDuotone,
PiTrophyDuotone,
PiPushPinSlashDuotone,
PiUserPlusDuotone,
PiUsersThreeDuotone,
PiVideoCameraDuotone,
PiXCircleDuotone,
PiXDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils';
interface IconProps {
className?: string;
}
/**
* Extended cabinet icon set — Phosphor (react-icons/pi) Duotone, the panel's
* icon family. These cover the icons that used to be hand-written inline across
* feature pages and components. Names match the historical local definitions so
* every page can import from the central barrel instead of redefining SVGs.
*/
export const AdjustmentsIcon = ({ className }: IconProps) => (
<PiSlidersHorizontalDuotone className={cn('h-5 w-5', className)} />
);
export const AgentIcon = ({ className }: IconProps) => (
<PiHeadsetDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowDownIcon = ({ className }: IconProps) => (
<PiArrowDownDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowIcon = ({ className }: IconProps) => (
<PiArrowRightDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowUpIcon = ({ className }: IconProps) => (
<PiArrowUpDuotone className={cn('h-5 w-5', className)} />
);
export const BanIcon = ({ className }: IconProps) => (
<PiProhibitDuotone className={cn('h-5 w-5', className)} />
);
export const BanknotesIcon = ({ className }: IconProps) => (
<PiMoneyDuotone className={cn('h-5 w-5', className)} />
);
export const BellIcon = ({ className }: IconProps) => (
<PiBellDuotone className={cn('h-5 w-5', className)} />
);
export const BoltIcon = ({ className }: IconProps) => (
<PiLightningDuotone className={cn('h-5 w-5', className)} />
);
export const BotIcon = ({ className }: IconProps) => (
<PiRobotDuotone className={cn('h-5 w-5', className)} />
);
export const BroadcastIcon = ({ className }: IconProps) => (
<PiBroadcastDuotone className={cn('h-5 w-5', className)} />
);
export const CabinetIcon = ({ className }: IconProps) => (
<PiAppWindowDuotone className={cn('h-5 w-5', className)} />
);
export const CalendarIcon = ({ className }: IconProps) => (
<PiCalendarDotsDuotone className={cn('h-5 w-5', className)} />
);
export const CardIcon = ({ className }: IconProps) => (
<PiCreditCardDuotone className={cn('h-5 w-5', className)} />
);
export const ChannelIcon = ({ className }: IconProps) => (
<PiBroadcastDuotone className={cn('h-5 w-5', className)} />
);
export const ChartBarIcon = ({ className }: IconProps) => (
<PiChartBarDuotone className={cn('h-5 w-5', className)} />
);
export const CheckCircleIcon = ({ className }: IconProps) => (
<PiCheckCircleDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronExpandIcon = ({ className }: IconProps) => (
<PiCaretUpDownDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronIcon = ({ className }: IconProps) => (
<PiCaretDownDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronLeftIcon = ({ className }: IconProps) => (
<PiCaretLeftDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronUpIcon = ({ className }: IconProps) => (
<PiCaretUpDuotone className={cn('h-5 w-5', className)} />
);
export const CryptoIcon = ({ className }: IconProps) => (
<PiCurrencyBtcDuotone className={cn('h-5 w-5', className)} />
);
export const DevicesIcon = ({ className }: IconProps) => (
<PiDevicesDuotone className={cn('h-5 w-5', className)} />
);
export const DocumentIcon = ({ className }: IconProps) => (
<PiFileTextDuotone className={cn('h-5 w-5', className)} />
);
export const DotIcon = ({ className }: IconProps) => (
<PiCircleFill className={cn('h-2 w-2', className)} />
);
export const EmailIcon = ({ className }: IconProps) => (
<PiEnvelopeDuotone className={cn('h-5 w-5', className)} />
);
export const ExclamationIcon = ({ className }: IconProps) => (
<PiWarningDuotone className={cn('h-5 w-5', className)} />
);
export const ExternalLinkIcon = ({ className }: IconProps) => (
<PiArrowSquareOutDuotone className={cn('h-5 w-5', className)} />
);
export const EyeIcon = ({ className }: IconProps) => (
<PiEyeDuotone className={cn('h-5 w-5', className)} />
);
export const FileTextIcon = ({ className }: IconProps) => (
<PiFileTextDuotone className={cn('h-5 w-5', className)} />
);
export const FilterIcon = ({ className }: IconProps) => (
<PiFunnelDuotone className={cn('h-5 w-5', className)} />
);
export const GripIcon = ({ className }: IconProps) => (
<PiDotsSixDuotone className={cn('h-5 w-5', className)} />
);
export const GripVerticalIcon = ({ className }: IconProps) => (
<PiDotsSixVerticalDuotone className={cn('h-5 w-5', className)} />
);
export const HealthIcon = ({ className }: IconProps) => (
<PiHeartbeatDuotone className={cn('h-5 w-5', className)} />
);
export const HistoryIcon = ({ className }: IconProps) => (
<PiClockCounterClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const ImageIcon = ({ className }: IconProps) => (
<PiImageDuotone className={cn('h-5 w-5', className)} />
);
export const InfinityIcon = ({ className }: IconProps) => (
<PiInfinityDuotone className={cn('h-5 w-5', className)} />
);
export const LinkIcon = ({ className }: IconProps) => (
<PiLinkDuotone className={cn('h-5 w-5', className)} />
);
export const MailIcon = ({ className }: IconProps) => (
<PiEnvelopeDuotone className={cn('h-5 w-5', className)} />
);
export const MegaphoneIcon = ({ className }: IconProps) => (
<PiMegaphoneDuotone className={cn('h-5 w-5', className)} />
);
export const MinusIcon = ({ className }: IconProps) => (
<PiMinusDuotone className={cn('h-5 w-5', className)} />
);
export const NewsIcon = ({ className }: IconProps) => (
<PiNewspaperDuotone className={cn('h-5 w-5', className)} />
);
export const PartnerIcon = ({ className }: IconProps) => (
<PiHandshakeDuotone className={cn('h-5 w-5', className)} />
);
export const PhotoIcon = ({ className }: IconProps) => (
<PiImageDuotone className={cn('h-5 w-5', className)} />
);
export const PercentIcon = ({ className }: IconProps) => (
<PiPercentDuotone className={cn('h-5 w-5', className)} />
);
export const PinIcon = ({ className }: IconProps) => (
<PiPushPinDuotone className={cn('h-5 w-5', className)} />
);
export const PlusSmallIcon = ({ className }: IconProps) => (
<PiPlusDuotone className={cn('h-4 w-4', className)} />
);
export const PowerIcon = ({ className }: IconProps) => (
<PiPowerDuotone className={cn('h-5 w-5', className)} />
);
export const QuestionIcon = ({ className }: IconProps) => (
<PiQuestionDuotone className={cn('h-5 w-5', className)} />
);
export const RepeatIcon = ({ className }: IconProps) => (
<PiRepeatDuotone className={cn('h-5 w-5', className)} />
);
export const ReportIcon = ({ className }: IconProps) => (
<PiFlagDuotone className={cn('h-5 w-5', className)} />
);
export const ResetIcon = ({ className }: IconProps) => (
<PiArrowCounterClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const RestartIcon = ({ className }: IconProps) => (
<PiArrowClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const RocketIcon = ({ className }: IconProps) => (
<PiRocketDuotone className={cn('h-5 w-5', className)} />
);
export const SaveIcon = ({ className }: IconProps) => (
<PiFloppyDiskDuotone className={cn('h-5 w-5', className)} />
);
export const SendIcon = ({ className }: IconProps) => (
<PiPaperPlaneTiltDuotone className={cn('h-5 w-5', className)} />
);
export const ServerSmallIcon = ({ className }: IconProps) => (
<PiHardDrivesDuotone className={cn('h-4 w-4', className)} />
);
export const SettingsIcon = ({ className }: IconProps) => (
<PiGearSixDuotone className={cn('h-5 w-5', className)} />
);
export const ShareIcon = ({ className }: IconProps) => (
<PiShareNetworkDuotone className={cn('h-5 w-5', className)} />
);
export const SparklesIcon = ({ className }: IconProps) => (
<PiSparkleDuotone className={cn('h-5 w-5', className)} />
);
export const StatBotIcon = ({ className }: IconProps) => (
<PiRobotDuotone className={cn('h-5 w-5', className)} />
);
export const StatCabinetIcon = ({ className }: IconProps) => (
<PiAppWindowDuotone className={cn('h-5 w-5', className)} />
);
export const StatPaidIcon = ({ className }: IconProps) => (
<PiMoneyDuotone className={cn('h-5 w-5', className)} />
);
export const StatsChartIcon = ({ className }: IconProps) => (
<PiChartLineDuotone className={cn('h-5 w-5', className)} />
);
export const StatTrialIcon = ({ className }: IconProps) => (
<PiGiftDuotone className={cn('h-5 w-5', className)} />
);
export const StatUptimeIcon = ({ className }: IconProps) => (
<PiTimerDuotone className={cn('h-5 w-5', className)} />
);
export const StatusIcon = ({ className }: IconProps) => (
<PiCircleDuotone className={cn('h-5 w-5', className)} />
);
export const TagIcon = ({ className }: IconProps) => (
<PiTagDuotone className={cn('h-5 w-5', className)} />
);
export const TelegramIcon = ({ className }: IconProps) => (
<PiTelegramLogoDuotone className={cn('h-5 w-5', className)} />
);
export const TelegramSmallIcon = ({ className }: IconProps) => (
<PiTelegramLogoDuotone className={cn('h-4 w-4', className)} />
);
export const TicketIcon = ({ className }: IconProps) => (
<PiTicketDuotone className={cn('h-5 w-5', className)} />
);
export const TrafficIcon = ({ className }: IconProps) => (
<PiGaugeDuotone className={cn('h-5 w-5', className)} />
);
export const TrashSmallIcon = ({ className }: IconProps) => (
<PiTrashDuotone className={cn('h-4 w-4', className)} />
);
export const TrophyIcon = ({ className }: IconProps) => (
<PiTrophyDuotone className={cn('h-5 w-5', className)} />
);
export const UnpinIcon = ({ className }: IconProps) => (
<PiPushPinSlashDuotone className={cn('h-5 w-5', className)} />
);
export const UserPlusIcon = ({ className }: IconProps) => (
<PiUserPlusDuotone className={cn('h-5 w-5', className)} />
);
export const UsersOnlineIcon = ({ className }: IconProps) => (
<PiUsersThreeDuotone className={cn('h-5 w-5', className)} />
);
export const VideoIcon = ({ className }: IconProps) => (
<PiVideoCameraDuotone className={cn('h-5 w-5', className)} />
);
export const WarningIcon = ({ className }: IconProps) => (
<PiWarningDuotone className={cn('h-5 w-5', className)} />
);
export const XCircleIcon = ({ className }: IconProps) => (
<PiXCircleDuotone className={cn('h-5 w-5', className)} />
);
export const XCloseIcon = ({ className }: IconProps) => (
<PiXDuotone className={cn('h-5 w-5', className)} />
);
export const XMarkIcon = ({ className }: IconProps) => (
<PiXDuotone className={cn('h-5 w-5', className)} />
);

View File

@@ -1,611 +1,248 @@
import type { CSSProperties } from 'react';
import {
PiArrowLeftDuotone,
PiArrowRightDuotone,
PiArrowsClockwiseDuotone,
PiCaretDownDuotone,
PiCaretRightDuotone,
PiChartBarDuotone,
PiChatCircleDuotone,
PiCheckDuotone,
PiClipboardTextDuotone,
PiClockDuotone,
PiCopyDuotone,
PiCreditCardDuotone,
PiDownloadSimpleDuotone,
PiGameControllerDuotone,
PiGearSixDuotone,
PiGiftDuotone,
PiGlobeDuotone,
PiHardDrivesDuotone,
PiHouseDuotone,
PiInfoDuotone,
PiListDuotone,
PiLockDuotone,
PiMagnifyingGlassDuotone,
PiMegaphoneDuotone,
PiMoonDuotone,
PiPaletteDuotone,
PiPauseCircleDuotone,
PiPencilDuotone,
PiPencilSimpleDuotone,
PiPlayDuotone,
PiPlusDuotone,
PiShieldDuotone,
PiSignOutDuotone,
PiSparkleDuotone,
PiStarDuotone,
PiStarFill,
PiSteeringWheelDuotone,
PiStopDuotone,
PiSunDuotone,
PiTrashDuotone,
PiUploadSimpleDuotone,
PiUserDuotone,
PiUsersDuotone,
PiWalletDuotone,
PiXDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils';
// Re-export the extended Phosphor icon sets so the whole cabinet imports the
// panel's icon family from a single barrel.
export * from './extended-icons';
export * from './editor-icons';
interface IconProps {
className?: string;
}
/**
* Cabinet icons are thin wrappers over the Remnawave panel's own icon library
* (Phosphor, via `react-icons/pi`, Duotone variants). Each export keeps the
* historical name + default Tailwind sizing so every consumer keeps working,
* while the cabinet now renders the exact icon set the panel uses instead of
* hand-written SVGs.
*
* react-icons components inherit `currentColor` and accept `className`, so
* Tailwind size (`h-5 w-5`) and text-color utilities control them as before.
*/
// Navigation & Layout
export const HomeIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
<PiHouseDuotone className={cn('h-5 w-5', className)} />
);
export const BackIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
<PiArrowLeftDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronRightIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
<PiCaretRightDuotone className={cn('h-5 w-5', className)} />
);
export const MenuIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
<PiListDuotone className={cn('h-5 w-5', className)} />
);
export const CloseIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<PiXDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronDownIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
<PiCaretDownDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowRightIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
<PiArrowRightDuotone className={cn('h-5 w-5', className)} />
);
// Actions
export const SearchIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
<PiMagnifyingGlassDuotone className={cn('h-5 w-5', className)} />
);
export const PlusIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<PiPlusDuotone className={cn('h-5 w-5', className)} />
);
export const EditIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125"
/>
</svg>
<PiPencilSimpleDuotone className={cn('h-4 w-4', className)} />
);
export const PencilIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<PiPencilDuotone className={cn('h-4 w-4', className)} />
);
export const TrashIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
<PiTrashDuotone className={cn('h-5 w-5', className)} />
);
export const UploadIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg>
<PiUploadSimpleDuotone className={cn('h-5 w-5', className)} />
);
export const DownloadIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<PiDownloadSimpleDuotone className={cn('h-5 w-5', className)} />
);
export const RefreshIcon = ({
className,
spinning = false,
}: IconProps & { spinning?: boolean }) => (
<svg
className={cn('h-4 w-4', spinning && 'animate-spin', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<PiArrowsClockwiseDuotone className={cn('h-4 w-4', spinning && 'animate-spin', className)} />
);
export const SyncIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<PiArrowsClockwiseDuotone className={cn('h-5 w-5', className)} />
);
// Status
export const CheckIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
<PiCheckDuotone className={cn('h-4 w-4', className)} />
);
export const CopyIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
<PiCopyDuotone className={cn('h-4 w-4', className)} />
);
export const XIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<PiXDuotone className={cn('h-4 w-4', className)} />
);
export const LockIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
/>
</svg>
<PiLockDuotone className={cn('h-4 w-4', className)} />
);
export const InfoIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
<PiInfoDuotone className={cn('h-5 w-5', className)} />
);
// User & People
export const UserIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
<PiUserDuotone className={cn('h-5 w-5', className)} />
);
export const UsersIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
<PiUsersDuotone className={cn('h-5 w-5', className)} />
);
export const LogoutIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"
/>
</svg>
<PiSignOutDuotone className={cn('h-5 w-5', className)} />
);
// Theme
export const SunIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
<PiSunDuotone className={cn('h-5 w-5', className)} />
);
export const MoonIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
<PiMoonDuotone className={cn('h-5 w-5', className)} />
);
export const PaletteIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"
/>
</svg>
<PiPaletteDuotone className={cn('h-5 w-5', className)} />
);
// Features & Content
export const SubscriptionIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
<PiSparkleDuotone className={cn('h-5 w-5', className)} />
);
export const WalletIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
<PiWalletDuotone className={cn('h-5 w-5', className)} />
);
export const ChatIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
<PiChatCircleDuotone className={cn('h-5 w-5', className)} />
);
export const GiftIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
<PiGiftDuotone className={cn('h-4 w-4', className)} />
);
export const ClockIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<PiClockDuotone className={cn('h-5 w-5', className)} />
);
export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) => (
<svg
className={cn('h-5 w-5', className)}
fill={filled ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) =>
filled ? (
<PiStarFill className={cn('h-5 w-5', className)} />
) : (
<PiStarDuotone className={cn('h-5 w-5', className)} />
);
export const GamepadIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.959.401v0a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.78.78 0 01.79-.869"
/>
</svg>
<PiGameControllerDuotone className={cn('h-5 w-5', className)} />
);
export const ClipboardIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
/>
</svg>
<PiClipboardTextDuotone className={cn('h-5 w-5', className)} />
);
export const CogIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<PiGearSixDuotone className={cn('h-5 w-5', className)} />
);
export const WheelIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
<PiSteeringWheelDuotone className={cn('h-5 w-5', className)} />
);
export const ShieldIcon = ({ className }: IconProps) => (
<PiShieldDuotone className={cn('h-5 w-5', className)} />
);
export const ServerIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 00-.12-1.03l-2.268-9.64a3.375 3.375 0 00-3.285-2.602H7.923a3.375 3.375 0 00-3.285 2.602l-2.268 9.64a4.5 4.5 0 00-.12 1.03v.228m19.5 0a3 3 0 01-3 3H5.25a3 3 0 01-3-3m19.5 0a3 3 0 00-3-3H5.25a3 3 0 00-3 3m16.5 0h.008v.008h-.008v-.008zm-3 0h.008v.008h-.008v-.008z"
/>
</svg>
<PiHardDrivesDuotone className={cn('h-5 w-5', className)} />
);
export const CampaignIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
<PiMegaphoneDuotone className={cn('h-5 w-5', className)} />
);
// Brand mark — the genuine Remnawave panel logo (kept as-is, this is the
// panel's own SVG, not a hand-drawn substitute).
export const RemnawaveIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
@@ -623,114 +260,29 @@ export const RemnawaveIcon = ({ className }: IconProps) => (
);
export const ChartIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
<PiChartBarDuotone className={cn('h-4 w-4', className)} />
);
export const GlobeIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
<PiGlobeDuotone className={cn('h-5 w-5', className)} />
);
export const PlayIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
/>
</svg>
<PiPlayDuotone className={cn('h-4 w-4', className)} />
);
export const StopIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z"
/>
</svg>
<PiStopDuotone className={cn('h-4 w-4', className)} />
);
export const ArrowPathIcon = ({ className }: IconProps) => (
<svg
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<PiArrowsClockwiseDuotone className={cn('h-4 w-4', className)} />
);
export const PauseIcon = ({ className, style }: IconProps & { style?: React.CSSProperties }) => (
<svg
className={cn('h-5 w-5', className)}
style={style}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
export const PauseIcon = ({ className, style }: IconProps & { style?: CSSProperties }) => (
<PiPauseCircleDuotone className={cn('h-5 w-5', className)} style={style} />
);
export const CreditCardIcon = ({ className }: IconProps) => (
<svg
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
/>
</svg>
<PiCreditCardDuotone className={cn('h-5 w-5', className)} />
);

View File

@@ -21,173 +21,25 @@ import SuccessNotificationModal from '@/components/SuccessNotificationModal';
import { PromptDialogHost } from '@/components/PromptDialogHost';
import LanguageSwitcher from '@/components/LanguageSwitcher';
import TicketNotificationBell from '@/components/TicketNotificationBell';
import { SubscriptionIcon, GiftIcon } from '@/components/icons';
import {
SubscriptionIcon,
GiftIcon,
HomeIcon,
CreditCardIcon,
ChatIcon,
UserIcon,
UsersIcon,
ShieldIcon,
InfoIcon,
LogoutIcon,
SunIcon,
MoonIcon,
} from '@/components/icons';
import { MobileBottomNav } from './MobileBottomNav';
import { AppHeader } from './AppHeader';
import { BackgroundRenderer } from '@/components/backgrounds/BackgroundRenderer';
// Desktop nav icons
const HomeIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
);
const CreditCardIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
/>
</svg>
);
const ChatIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
);
const UserIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
const UsersIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
);
const ShieldIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
const InfoIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
);
const LogoutIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75"
/>
</svg>
);
const SunIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
);
const MoonIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
);
interface AppShellProps {
children: React.ReactNode;
}

View File

@@ -6,6 +6,7 @@ import { motion } from 'framer-motion';
import { newsApi } from '../../api/news';
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
import { cn } from '../../lib/utils';
import { ArrowIcon } from '@/components/icons';
import type { NewsListItem } from '../../types/news';
// --- Security: hex color validation to prevent CSS injection ---
@@ -31,19 +32,6 @@ const fadeSlideUp = {
}),
};
// --- Icons ---
const ArrowIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M3 8h10M9 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// --- Sub-components ---
interface CategoryBadgeProps {

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { CheckIcon, CopyIcon } from '@/components/icons';
import type { PartnerCampaignInfo } from '../../api/partners';
import { PARTNER_STATS } from '../../constants/partner';
import { useCurrency } from '../../hooks/useCurrency';
@@ -9,32 +11,8 @@ import { copyToClipboard } from '../../utils/clipboard';
import { CampaignDetailStats } from './CampaignDetailStats';
import { StatCard } from '../stats/StatCard';
const CopyIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
);
const CheckIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg
className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
<PiCaretDownDuotone className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`} />
);
interface CampaignCardProps {

View File

@@ -1,20 +1,8 @@
import { Command as CommandPrimitive } from 'cmdk';
import { forwardRef, type ComponentPropsWithoutRef, type HTMLAttributes } from 'react';
import { SearchIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
// Search icon
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.9-2.9"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Root Command
export type CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>;

View File

@@ -8,6 +8,7 @@ import {
useState,
} from 'react';
import { cn } from '@/lib/utils';
import { CloseIcon } from '@/components/icons';
import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions';
export {
@@ -16,19 +17,6 @@ export {
Close as DialogClose,
} from '@radix-ui/react-dialog';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence
const DialogContext = createContext<{ open: boolean }>({ open: false });

View File

@@ -1,6 +1,7 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { CheckIcon, ChevronRightIcon, DotIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions';
@@ -14,37 +15,6 @@ export {
RadioGroup as DropdownMenuRadioGroup,
} from '@radix-ui/react-dropdown-menu';
// Icons
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const ChevronRightIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const DotIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="3" fill="currentColor" />
</svg>
);
// SubTrigger
export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.SubTrigger

View File

@@ -1,6 +1,7 @@
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { CloseIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
import { dropdown, dropdownTransition } from '../../motion/transitions';
@@ -11,19 +12,6 @@ export {
Close as PopoverClose,
} from '@radix-ui/react-popover';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Content
export interface PopoverContentProps extends ComponentPropsWithoutRef<
typeof PopoverPrimitive.Content

View File

@@ -2,36 +2,12 @@ import * as SelectPrimitive from '@radix-ui/react-select';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronDownIcon } from '@/components/icons';
import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions';
export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select';
// Icons
const ChevronDownIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-accent-400">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Trigger
export interface SelectTriggerProps extends ComponentPropsWithoutRef<
typeof SelectPrimitive.Trigger

View File

@@ -8,6 +8,7 @@ import {
useState,
useCallback,
} from 'react';
import { CloseIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import {
@@ -23,19 +24,6 @@ export {
Close as SheetClose,
} from '@radix-ui/react-dialog';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence and control
interface SheetContextValue {
open: boolean;