@wakastellar/ui 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +6014 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- package/src/components/waka-xp-bar/index.tsx +799 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
|
|
6
|
+
export interface InputOTPProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'size'> {
|
|
7
|
+
/** Nombre de champs OTP */
|
|
8
|
+
length?: number
|
|
9
|
+
/** Valeur contrôlée */
|
|
10
|
+
value?: string
|
|
11
|
+
/** Callback lors du changement */
|
|
12
|
+
onChange?: (value: string) => void
|
|
13
|
+
/** Callback quand l'OTP est complet */
|
|
14
|
+
onComplete?: (value: string) => void
|
|
15
|
+
/** Type de caractères acceptés */
|
|
16
|
+
pattern?: 'numeric' | 'alphanumeric' | 'alpha'
|
|
17
|
+
/** Afficher le contenu en clair ou masqué */
|
|
18
|
+
secure?: boolean
|
|
19
|
+
/** Désactiver le composant */
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
/** Classe CSS personnalisée */
|
|
22
|
+
className?: string
|
|
23
|
+
/** Message d'erreur */
|
|
24
|
+
error?: string
|
|
25
|
+
/** Taille du composant */
|
|
26
|
+
size?: 'sm' | 'md' | 'lg'
|
|
27
|
+
/** Séparer les groupes de champs */
|
|
28
|
+
separator?: number
|
|
29
|
+
/** Élément de séparation personnalisé */
|
|
30
|
+
separatorElement?: React.ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* InputOTP - Composant de saisie de code à usage unique (OTP)
|
|
35
|
+
*
|
|
36
|
+
* Composant accessible pour la saisie de codes de vérification, mots de passe temporaires, etc.
|
|
37
|
+
* Gère automatiquement le focus, la navigation au clavier et la validation des entrées.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // OTP numérique simple
|
|
42
|
+
* <InputOTP length={6} onChange={(value) => console.log(value)} />
|
|
43
|
+
*
|
|
44
|
+
* // OTP avec séparateur
|
|
45
|
+
* <InputOTP
|
|
46
|
+
* length={6}
|
|
47
|
+
* separator={3}
|
|
48
|
+
* separatorElement={<span>-</span>}
|
|
49
|
+
* onComplete={(code) => verifyCode(code)}
|
|
50
|
+
* />
|
|
51
|
+
*
|
|
52
|
+
* // OTP sécurisé
|
|
53
|
+
* <InputOTP length={4} secure pattern="numeric" />
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const InputOTP = React.forwardRef<HTMLDivElement, InputOTPProps>(
|
|
57
|
+
({
|
|
58
|
+
length = 6,
|
|
59
|
+
value = "",
|
|
60
|
+
onChange,
|
|
61
|
+
onComplete,
|
|
62
|
+
pattern = 'numeric',
|
|
63
|
+
secure = false,
|
|
64
|
+
disabled = false,
|
|
65
|
+
className,
|
|
66
|
+
error,
|
|
67
|
+
size = 'md',
|
|
68
|
+
separator,
|
|
69
|
+
separatorElement = <span className="text-muted-foreground">-</span>,
|
|
70
|
+
...props
|
|
71
|
+
}, ref) => {
|
|
72
|
+
const [internalValue, setInternalValue] = React.useState<string[]>(
|
|
73
|
+
value.split('').slice(0, length)
|
|
74
|
+
)
|
|
75
|
+
const inputsRef = React.useRef<(HTMLInputElement | null)[]>([])
|
|
76
|
+
|
|
77
|
+
// Synchroniser avec la valeur externe
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
const newValue = value.split('').slice(0, length)
|
|
80
|
+
setInternalValue(newValue)
|
|
81
|
+
}, [value, length])
|
|
82
|
+
|
|
83
|
+
// Obtenir le pattern de regex selon le type
|
|
84
|
+
const getPattern = () => {
|
|
85
|
+
switch (pattern) {
|
|
86
|
+
case 'numeric':
|
|
87
|
+
return /^[0-9]$/
|
|
88
|
+
case 'alpha':
|
|
89
|
+
return /^[a-zA-Z]$/
|
|
90
|
+
case 'alphanumeric':
|
|
91
|
+
return /^[a-zA-Z0-9]$/
|
|
92
|
+
default:
|
|
93
|
+
return /^.$/
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const regex = getPattern()
|
|
98
|
+
|
|
99
|
+
// Gérer le changement de valeur
|
|
100
|
+
const handleChange = (index: number, inputValue: string) => {
|
|
101
|
+
if (disabled) return
|
|
102
|
+
|
|
103
|
+
// Ne garder que le dernier caractère saisi
|
|
104
|
+
const char = inputValue.slice(-1)
|
|
105
|
+
|
|
106
|
+
// Vérifier si le caractère correspond au pattern
|
|
107
|
+
if (char && !regex.test(char)) return
|
|
108
|
+
|
|
109
|
+
const newValue = [...internalValue]
|
|
110
|
+
newValue[index] = char
|
|
111
|
+
|
|
112
|
+
setInternalValue(newValue)
|
|
113
|
+
|
|
114
|
+
// Appeler le callback onChange
|
|
115
|
+
const stringValue = newValue.join('')
|
|
116
|
+
onChange?.(stringValue)
|
|
117
|
+
|
|
118
|
+
// Si un caractère a été saisi et que ce n'est pas le dernier champ, passer au suivant
|
|
119
|
+
if (char && index < length - 1) {
|
|
120
|
+
inputsRef.current[index + 1]?.focus()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Si l'OTP est complet, appeler onComplete
|
|
124
|
+
if (newValue.every(v => v !== '') && newValue.length === length) {
|
|
125
|
+
onComplete?.(stringValue)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Gérer la suppression
|
|
130
|
+
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
131
|
+
if (disabled) return
|
|
132
|
+
|
|
133
|
+
// Backspace
|
|
134
|
+
if (e.key === 'Backspace') {
|
|
135
|
+
e.preventDefault()
|
|
136
|
+
const newValue = [...internalValue]
|
|
137
|
+
|
|
138
|
+
if (newValue[index]) {
|
|
139
|
+
// Supprimer le caractère actuel
|
|
140
|
+
newValue[index] = ''
|
|
141
|
+
setInternalValue(newValue)
|
|
142
|
+
onChange?.(newValue.join(''))
|
|
143
|
+
} else if (index > 0) {
|
|
144
|
+
// Si le champ est vide, aller au précédent et supprimer
|
|
145
|
+
newValue[index - 1] = ''
|
|
146
|
+
setInternalValue(newValue)
|
|
147
|
+
onChange?.(newValue.join(''))
|
|
148
|
+
inputsRef.current[index - 1]?.focus()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Delete
|
|
153
|
+
if (e.key === 'Delete') {
|
|
154
|
+
e.preventDefault()
|
|
155
|
+
const newValue = [...internalValue]
|
|
156
|
+
newValue[index] = ''
|
|
157
|
+
setInternalValue(newValue)
|
|
158
|
+
onChange?.(newValue.join(''))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Flèches gauche/droite
|
|
162
|
+
if (e.key === 'ArrowLeft' && index > 0) {
|
|
163
|
+
e.preventDefault()
|
|
164
|
+
inputsRef.current[index - 1]?.focus()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (e.key === 'ArrowRight' && index < length - 1) {
|
|
168
|
+
e.preventDefault()
|
|
169
|
+
inputsRef.current[index + 1]?.focus()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Home / End
|
|
173
|
+
if (e.key === 'Home') {
|
|
174
|
+
e.preventDefault()
|
|
175
|
+
inputsRef.current[0]?.focus()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (e.key === 'End') {
|
|
179
|
+
e.preventDefault()
|
|
180
|
+
inputsRef.current[length - 1]?.focus()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Gérer le collage
|
|
185
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
186
|
+
e.preventDefault()
|
|
187
|
+
if (disabled) return
|
|
188
|
+
|
|
189
|
+
const pastedData = e.clipboardData.getData('text/plain')
|
|
190
|
+
const chars = pastedData.split('').filter(char => regex.test(char)).slice(0, length)
|
|
191
|
+
|
|
192
|
+
const newValue = [...internalValue]
|
|
193
|
+
chars.forEach((char, i) => {
|
|
194
|
+
newValue[i] = char
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
setInternalValue(newValue)
|
|
198
|
+
onChange?.(newValue.join(''))
|
|
199
|
+
|
|
200
|
+
// Déplacer le focus sur le prochain champ vide ou le dernier
|
|
201
|
+
const nextEmptyIndex = newValue.findIndex(v => !v)
|
|
202
|
+
if (nextEmptyIndex !== -1) {
|
|
203
|
+
inputsRef.current[nextEmptyIndex]?.focus()
|
|
204
|
+
} else {
|
|
205
|
+
inputsRef.current[length - 1]?.focus()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Si l'OTP est complet, appeler onComplete
|
|
209
|
+
if (newValue.every(v => v !== '') && newValue.length === length) {
|
|
210
|
+
onComplete?.(newValue.join(''))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Gérer le focus - sélectionner tout le contenu
|
|
215
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
216
|
+
e.target.select()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Tailles
|
|
220
|
+
const sizeClasses = {
|
|
221
|
+
sm: "h-8 w-8 text-sm",
|
|
222
|
+
md: "h-12 w-12 text-base",
|
|
223
|
+
lg: "h-16 w-16 text-lg",
|
|
224
|
+
}[size]
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div ref={ref} className={cn("flex flex-col gap-2", className)}>
|
|
228
|
+
<div className="flex items-center gap-2">
|
|
229
|
+
{Array.from({ length }).map((_, index) => (
|
|
230
|
+
<React.Fragment key={index}>
|
|
231
|
+
<input
|
|
232
|
+
ref={(el) => (inputsRef.current[index] = el)}
|
|
233
|
+
type={secure ? 'password' : 'text'}
|
|
234
|
+
inputMode={pattern === 'numeric' ? 'numeric' : 'text'}
|
|
235
|
+
maxLength={1}
|
|
236
|
+
value={internalValue[index] || ''}
|
|
237
|
+
onChange={(e) => handleChange(index, e.target.value)}
|
|
238
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
239
|
+
onPaste={handlePaste}
|
|
240
|
+
onFocus={handleFocus}
|
|
241
|
+
disabled={disabled}
|
|
242
|
+
className={cn(
|
|
243
|
+
// Classes de base du thème
|
|
244
|
+
"flex items-center justify-center rounded-md border border-input bg-background text-center font-medium transition-colors",
|
|
245
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
246
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
247
|
+
// Taille
|
|
248
|
+
sizeClasses,
|
|
249
|
+
// Erreur
|
|
250
|
+
error && "border-destructive focus-visible:ring-destructive",
|
|
251
|
+
// Valeur présente
|
|
252
|
+
internalValue[index] && "border-primary"
|
|
253
|
+
)}
|
|
254
|
+
aria-label={`Caractère ${index + 1} sur ${length}`}
|
|
255
|
+
{...props}
|
|
256
|
+
/>
|
|
257
|
+
{/* Séparateur */}
|
|
258
|
+
{separator && (index + 1) % separator === 0 && index < length - 1 && (
|
|
259
|
+
<div className="flex items-center justify-center">
|
|
260
|
+
{separatorElement}
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</React.Fragment>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
{error && (
|
|
267
|
+
<p className="text-sm text-destructive" role="alert">
|
|
268
|
+
{error}
|
|
269
|
+
</p>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
InputOTP.displayName = "InputOTP"
|
|
277
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
|
|
6
|
+
const labelVariants = cva(
|
|
7
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const Label = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
13
|
+
VariantProps<typeof labelVariants>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<LabelPrimitive.Root
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(labelVariants(), className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
Label.displayName = LabelPrimitive.Root.displayName
|
|
22
|
+
|
|
23
|
+
export { Label }
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "../select"
|
|
12
|
+
import { Check, ChevronDown, Globe } from "lucide-react"
|
|
13
|
+
import { createPortal } from "react-dom"
|
|
14
|
+
|
|
15
|
+
export interface Language {
|
|
16
|
+
code: string
|
|
17
|
+
label: string
|
|
18
|
+
flag?: string // Emoji ou URL d'image
|
|
19
|
+
flagEmoji?: string // Emoji du drapeau
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LanguageSelectorProps {
|
|
23
|
+
/** Liste des langues disponibles */
|
|
24
|
+
languages: Language[]
|
|
25
|
+
|
|
26
|
+
/** Langue sélectionnée (code) */
|
|
27
|
+
value?: string
|
|
28
|
+
|
|
29
|
+
/** Callback quand la langue change */
|
|
30
|
+
onChange?: (code: string) => void
|
|
31
|
+
|
|
32
|
+
/** Afficher les drapeaux */
|
|
33
|
+
showFlags?: boolean
|
|
34
|
+
|
|
35
|
+
/** Afficher les labels */
|
|
36
|
+
showLabels?: boolean
|
|
37
|
+
|
|
38
|
+
/** Variant du composant */
|
|
39
|
+
variant?: "default" | "compact" | "minimal"
|
|
40
|
+
|
|
41
|
+
/** État de chargement */
|
|
42
|
+
isLoading?: boolean
|
|
43
|
+
|
|
44
|
+
/** Classe CSS personnalisée */
|
|
45
|
+
className?: string
|
|
46
|
+
|
|
47
|
+
/** Texte du placeholder */
|
|
48
|
+
placeholder?: string
|
|
49
|
+
|
|
50
|
+
/** Texte pour le screen reader */
|
|
51
|
+
ariaLabel?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* LanguageSelector
|
|
56
|
+
*
|
|
57
|
+
* Sélecteur de langue avec drapeaux et support du chargement dynamique.
|
|
58
|
+
*
|
|
59
|
+
* Features:
|
|
60
|
+
* - Drapeaux emoji ou images
|
|
61
|
+
* - 3 variantes : default (select), compact (bouton), minimal (icon only)
|
|
62
|
+
* - Support du chargement asynchrone
|
|
63
|
+
* - Intégration avec LanguageProvider pour S3
|
|
64
|
+
*/
|
|
65
|
+
export function LanguageSelector({
|
|
66
|
+
languages,
|
|
67
|
+
value,
|
|
68
|
+
onChange,
|
|
69
|
+
showFlags = true,
|
|
70
|
+
showLabels = true,
|
|
71
|
+
variant = "default",
|
|
72
|
+
isLoading = false,
|
|
73
|
+
className,
|
|
74
|
+
placeholder = "Select language",
|
|
75
|
+
ariaLabel = "Select language",
|
|
76
|
+
}: LanguageSelectorProps) {
|
|
77
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
78
|
+
const [mounted, setMounted] = React.useState(false)
|
|
79
|
+
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
|
80
|
+
const dropdownRef = React.useRef<HTMLDivElement>(null)
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
setMounted(true)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const selectedLanguage = languages.find(lang => lang.code === value)
|
|
87
|
+
|
|
88
|
+
const handleLanguageChange = (code: string) => {
|
|
89
|
+
onChange?.(code)
|
|
90
|
+
setIsOpen(false)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Position du dropdown
|
|
94
|
+
const [dropdownPosition, setDropdownPosition] = React.useState({ top: 0, left: 0 })
|
|
95
|
+
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (isOpen && buttonRef.current && mounted) {
|
|
98
|
+
const rect = buttonRef.current.getBoundingClientRect()
|
|
99
|
+
setDropdownPosition({
|
|
100
|
+
top: rect.bottom + 8,
|
|
101
|
+
left: rect.left
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}, [isOpen, mounted])
|
|
105
|
+
|
|
106
|
+
// Variant default - Select standard
|
|
107
|
+
if (variant === "default") {
|
|
108
|
+
return (
|
|
109
|
+
<Select value={value} onValueChange={onChange} disabled={isLoading}>
|
|
110
|
+
<SelectTrigger className={cn("w-[180px]", className)}>
|
|
111
|
+
<SelectValue placeholder={placeholder}>
|
|
112
|
+
{selectedLanguage && (
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
{showFlags && selectedLanguage.flagEmoji && (
|
|
115
|
+
<span className="text-lg">{selectedLanguage.flagEmoji}</span>
|
|
116
|
+
)}
|
|
117
|
+
{showFlags && selectedLanguage.flag && !selectedLanguage.flagEmoji && (
|
|
118
|
+
<img
|
|
119
|
+
src={selectedLanguage.flag}
|
|
120
|
+
alt={selectedLanguage.label}
|
|
121
|
+
className="h-4 w-6 object-cover rounded"
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
{!showFlags && <Globe className="h-4 w-4 text-muted-foreground" />}
|
|
125
|
+
{showLabels && <span>{selectedLanguage.label}</span>}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</SelectValue>
|
|
129
|
+
</SelectTrigger>
|
|
130
|
+
<SelectContent>
|
|
131
|
+
{languages.map((language) => (
|
|
132
|
+
<SelectItem key={language.code} value={language.code}>
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
{showFlags && language.flagEmoji && (
|
|
135
|
+
<span className="text-lg">{language.flagEmoji}</span>
|
|
136
|
+
)}
|
|
137
|
+
{showFlags && language.flag && !language.flagEmoji && (
|
|
138
|
+
<img
|
|
139
|
+
src={language.flag}
|
|
140
|
+
alt={language.label}
|
|
141
|
+
className="h-4 w-6 object-cover rounded"
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
{showLabels && language.label}
|
|
145
|
+
</div>
|
|
146
|
+
</SelectItem>
|
|
147
|
+
))}
|
|
148
|
+
</SelectContent>
|
|
149
|
+
</Select>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Variant minimal - Icon only avec dropdown
|
|
154
|
+
if (variant === "minimal") {
|
|
155
|
+
const dropdownContent = isOpen && mounted ? (
|
|
156
|
+
<div
|
|
157
|
+
ref={dropdownRef}
|
|
158
|
+
className="fixed z-[9999] min-w-[160px] rounded-md border border-border bg-popover text-popover-foreground p-2 shadow-lg"
|
|
159
|
+
style={{
|
|
160
|
+
top: `${dropdownPosition.top}px`,
|
|
161
|
+
left: `${dropdownPosition.left}px`,
|
|
162
|
+
}}
|
|
163
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
164
|
+
>
|
|
165
|
+
<div className="grid gap-1">
|
|
166
|
+
{languages.map((language) => (
|
|
167
|
+
<button
|
|
168
|
+
key={language.code}
|
|
169
|
+
onMouseDown={(e) => {
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
e.stopPropagation()
|
|
172
|
+
handleLanguageChange(language.code)
|
|
173
|
+
}}
|
|
174
|
+
type="button"
|
|
175
|
+
className={cn(
|
|
176
|
+
"flex items-center gap-2 rounded-sm px-2 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer",
|
|
177
|
+
value === language.code && "bg-accent"
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{language.flagEmoji && (
|
|
181
|
+
<span className="text-lg">{language.flagEmoji}</span>
|
|
182
|
+
)}
|
|
183
|
+
{language.flag && !language.flagEmoji && (
|
|
184
|
+
<img
|
|
185
|
+
src={language.flag}
|
|
186
|
+
alt={language.label}
|
|
187
|
+
className="h-4 w-6 object-cover rounded"
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
<span className="flex-1 text-left">{language.label}</span>
|
|
191
|
+
{value === language.code && (
|
|
192
|
+
<Check className="h-4 w-4 shrink-0" />
|
|
193
|
+
)}
|
|
194
|
+
</button>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
) : null
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
<button
|
|
203
|
+
ref={buttonRef}
|
|
204
|
+
onClick={(e) => {
|
|
205
|
+
e.preventDefault()
|
|
206
|
+
e.stopPropagation()
|
|
207
|
+
setIsOpen(!isOpen)
|
|
208
|
+
}}
|
|
209
|
+
className={cn(
|
|
210
|
+
"flex h-9 w-9 items-center justify-center rounded-md border border-input bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors",
|
|
211
|
+
className
|
|
212
|
+
)}
|
|
213
|
+
title="Changer de langue"
|
|
214
|
+
type="button"
|
|
215
|
+
disabled={isLoading}
|
|
216
|
+
>
|
|
217
|
+
{selectedLanguage?.flagEmoji ? (
|
|
218
|
+
<span className="text-lg">{selectedLanguage.flagEmoji}</span>
|
|
219
|
+
) : (
|
|
220
|
+
<Globe className="h-4 w-4" />
|
|
221
|
+
)}
|
|
222
|
+
<span className="sr-only">{ariaLabel}</span>
|
|
223
|
+
</button>
|
|
224
|
+
|
|
225
|
+
{mounted && typeof document !== "undefined" && dropdownContent && createPortal(
|
|
226
|
+
<>
|
|
227
|
+
<div
|
|
228
|
+
className="fixed inset-0 z-[9998]"
|
|
229
|
+
onMouseDown={(e) => {
|
|
230
|
+
e.preventDefault()
|
|
231
|
+
setIsOpen(false)
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
{dropdownContent}
|
|
235
|
+
</>,
|
|
236
|
+
document.body
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Variant compact - Bouton avec drapeau et chevron avec dropdown
|
|
243
|
+
const dropdownContent = isOpen && mounted ? (
|
|
244
|
+
<div
|
|
245
|
+
ref={dropdownRef}
|
|
246
|
+
className="fixed z-[9999] min-w-[180px] rounded-md border border-border bg-popover text-popover-foreground p-2 shadow-lg"
|
|
247
|
+
style={{
|
|
248
|
+
top: `${dropdownPosition.top}px`,
|
|
249
|
+
left: `${dropdownPosition.left}px`,
|
|
250
|
+
}}
|
|
251
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
252
|
+
>
|
|
253
|
+
<div className="grid gap-1">
|
|
254
|
+
{languages.map((language) => (
|
|
255
|
+
<button
|
|
256
|
+
key={language.code}
|
|
257
|
+
onMouseDown={(e) => {
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
e.stopPropagation()
|
|
260
|
+
handleLanguageChange(language.code)
|
|
261
|
+
}}
|
|
262
|
+
type="button"
|
|
263
|
+
className={cn(
|
|
264
|
+
"flex items-center gap-2 rounded-sm px-2 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer",
|
|
265
|
+
value === language.code && "bg-accent"
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
{showFlags && language.flagEmoji && (
|
|
269
|
+
<span className="text-lg">{language.flagEmoji}</span>
|
|
270
|
+
)}
|
|
271
|
+
{showFlags && language.flag && !language.flagEmoji && (
|
|
272
|
+
<img
|
|
273
|
+
src={language.flag}
|
|
274
|
+
alt={language.label}
|
|
275
|
+
className="h-4 w-6 object-cover rounded"
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
<span className="flex-1 text-left">{language.label}</span>
|
|
279
|
+
{value === language.code && (
|
|
280
|
+
<Check className="h-4 w-4 shrink-0" />
|
|
281
|
+
)}
|
|
282
|
+
</button>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
) : null
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<>
|
|
290
|
+
<button
|
|
291
|
+
ref={buttonRef}
|
|
292
|
+
onClick={(e) => {
|
|
293
|
+
e.preventDefault()
|
|
294
|
+
e.stopPropagation()
|
|
295
|
+
if (!isLoading) {
|
|
296
|
+
setIsOpen(!isOpen)
|
|
297
|
+
}
|
|
298
|
+
}}
|
|
299
|
+
className={cn(
|
|
300
|
+
"inline-flex items-center gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors",
|
|
301
|
+
isLoading && "opacity-50 cursor-not-allowed",
|
|
302
|
+
className
|
|
303
|
+
)}
|
|
304
|
+
disabled={isLoading}
|
|
305
|
+
type="button"
|
|
306
|
+
aria-label={ariaLabel}
|
|
307
|
+
>
|
|
308
|
+
{showFlags && selectedLanguage?.flagEmoji && (
|
|
309
|
+
<span className="text-lg">{selectedLanguage.flagEmoji}</span>
|
|
310
|
+
)}
|
|
311
|
+
{showFlags && selectedLanguage?.flag && !selectedLanguage?.flagEmoji && (
|
|
312
|
+
<img
|
|
313
|
+
src={selectedLanguage.flag}
|
|
314
|
+
alt={selectedLanguage.label}
|
|
315
|
+
className="h-4 w-6 object-cover rounded"
|
|
316
|
+
/>
|
|
317
|
+
)}
|
|
318
|
+
{!showFlags && <Globe className="h-4 w-4" />}
|
|
319
|
+
{showLabels && (
|
|
320
|
+
<span>{selectedLanguage?.label || "Langue"}</span>
|
|
321
|
+
)}
|
|
322
|
+
<ChevronDown className="h-4 w-4" />
|
|
323
|
+
</button>
|
|
324
|
+
|
|
325
|
+
{mounted && typeof document !== "undefined" && dropdownContent && createPortal(
|
|
326
|
+
<>
|
|
327
|
+
<div
|
|
328
|
+
className="fixed inset-0 z-[9998]"
|
|
329
|
+
onMouseDown={(e) => {
|
|
330
|
+
e.preventDefault()
|
|
331
|
+
setIsOpen(false)
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
{dropdownContent}
|
|
335
|
+
</>,
|
|
336
|
+
document.body
|
|
337
|
+
)}
|
|
338
|
+
</>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|