@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.
Files changed (291) hide show
  1. package/README.md +71 -8
  2. package/dist/cli/commands/add.d.ts +7 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/list.d.ts +5 -0
  5. package/dist/cli/commands/search.d.ts +1 -0
  6. package/dist/cli/index.cjs +6014 -0
  7. package/dist/cli/index.d.ts +1 -0
  8. package/dist/cli/utils/config.d.ts +29 -0
  9. package/dist/cli/utils/logger.d.ts +20 -0
  10. package/dist/cli/utils/registry.d.ts +23 -0
  11. package/package.json +14 -3
  12. package/src/blocks/activity-timeline/index.tsx +586 -0
  13. package/src/blocks/calendar-view/index.tsx +756 -0
  14. package/src/blocks/chat/index.tsx +1018 -0
  15. package/src/blocks/chat/widget.tsx +504 -0
  16. package/src/blocks/dashboard/index.tsx +522 -0
  17. package/src/blocks/empty-states/index.tsx +452 -0
  18. package/src/blocks/error-pages/index.tsx +426 -0
  19. package/src/blocks/faq/index.tsx +479 -0
  20. package/src/blocks/file-manager/index.tsx +890 -0
  21. package/src/blocks/footer/index.tsx +133 -0
  22. package/src/blocks/header/index.tsx +357 -0
  23. package/src/blocks/headtab/index.tsx +139 -0
  24. package/src/blocks/i18n-editor/index.tsx +1016 -0
  25. package/src/blocks/index.ts +80 -0
  26. package/src/blocks/kanban-board/index.tsx +779 -0
  27. package/src/blocks/landing/index.tsx +677 -0
  28. package/src/blocks/language-selector/index.tsx +88 -0
  29. package/src/blocks/layout/index.tsx +159 -0
  30. package/src/blocks/login/index.tsx +339 -0
  31. package/src/blocks/login/types.ts +131 -0
  32. package/src/blocks/pricing/index.tsx +564 -0
  33. package/src/blocks/profile/index.tsx +746 -0
  34. package/src/blocks/settings/index.tsx +558 -0
  35. package/src/blocks/sidebar/index.tsx +713 -0
  36. package/src/blocks/theme-creator-block/index.tsx +835 -0
  37. package/src/blocks/user-management/index.tsx +1037 -0
  38. package/src/blocks/wizard/index.tsx +719 -0
  39. package/src/components/DataTable/DataTable.tsx +406 -0
  40. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  41. package/src/components/DataTable/DataTableBody.tsx +216 -0
  42. package/src/components/DataTable/DataTableCell.tsx +172 -0
  43. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  44. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  45. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  46. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  47. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  48. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  49. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  50. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  51. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  52. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  53. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  54. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  55. package/src/components/DataTable/README.md +446 -0
  56. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  57. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  58. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  59. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  60. package/src/components/DataTable/formatters/index.ts +335 -0
  61. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  62. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  64. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  65. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  66. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  67. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  68. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  69. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  70. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  71. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  72. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  73. package/src/components/DataTable/index.ts +81 -0
  74. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  75. package/src/components/DataTable/templates/index.tsx +803 -0
  76. package/src/components/DataTable/types.ts +504 -0
  77. package/src/components/DataTable/utils.ts +164 -0
  78. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  79. package/src/components/accordion/index.tsx +61 -0
  80. package/src/components/alert/index.tsx +61 -0
  81. package/src/components/alert-dialog/index.tsx +146 -0
  82. package/src/components/aspect-ratio/index.tsx +12 -0
  83. package/src/components/avatar/index.tsx +54 -0
  84. package/src/components/badge/Badge.stories.tsx +64 -0
  85. package/src/components/badge/index.tsx +38 -0
  86. package/src/components/button/Button.stories.tsx +173 -0
  87. package/src/components/button/index.tsx +56 -0
  88. package/src/components/calendar/index.tsx +73 -0
  89. package/src/components/card/index.tsx +78 -0
  90. package/src/components/checkbox/index.tsx +34 -0
  91. package/src/components/code/index.tsx +229 -0
  92. package/src/components/collapsible/index.tsx +16 -0
  93. package/src/components/command/index.tsx +162 -0
  94. package/src/components/context-menu/index.tsx +204 -0
  95. package/src/components/dialog/index.tsx +126 -0
  96. package/src/components/dropdown-menu/index.tsx +204 -0
  97. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  98. package/src/components/error-boundary/index.ts +7 -0
  99. package/src/components/form/index.tsx +183 -0
  100. package/src/components/hover-card/index.tsx +33 -0
  101. package/src/components/index.ts +368 -0
  102. package/src/components/input/Input.stories.tsx +100 -0
  103. package/src/components/input/index.tsx +27 -0
  104. package/src/components/input-otp/index.tsx +277 -0
  105. package/src/components/label/index.tsx +30 -0
  106. package/src/components/language-selector/index.tsx +341 -0
  107. package/src/components/menubar/index.tsx +240 -0
  108. package/src/components/navigation-menu/index.tsx +134 -0
  109. package/src/components/popover/index.tsx +35 -0
  110. package/src/components/progress/index.tsx +32 -0
  111. package/src/components/radio-group/index.tsx +48 -0
  112. package/src/components/scroll-area/index.tsx +52 -0
  113. package/src/components/select/index.tsx +164 -0
  114. package/src/components/separator/index.tsx +35 -0
  115. package/src/components/sheet/index.tsx +147 -0
  116. package/src/components/skeleton/index.tsx +22 -0
  117. package/src/components/slider/index.tsx +32 -0
  118. package/src/components/switch/index.tsx +33 -0
  119. package/src/components/table/index.tsx +117 -0
  120. package/src/components/tabs/index.tsx +59 -0
  121. package/src/components/textarea/index.tsx +30 -0
  122. package/src/components/theme-selector/index.tsx +327 -0
  123. package/src/components/toast/index.tsx +133 -0
  124. package/src/components/toaster/index.tsx +34 -0
  125. package/src/components/toggle/index.tsx +49 -0
  126. package/src/components/tooltip/index.tsx +34 -0
  127. package/src/components/typography/index.tsx +276 -0
  128. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  129. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  130. package/src/components/waka-activity-feed/index.tsx +686 -0
  131. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  132. package/src/components/waka-admincrumb/index.tsx +349 -0
  133. package/src/components/waka-alert-stack/index.tsx +827 -0
  134. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  135. package/src/components/waka-approval-chain/index.tsx +766 -0
  136. package/src/components/waka-audit-log/index.tsx +1475 -0
  137. package/src/components/waka-autocomplete/index.tsx +358 -0
  138. package/src/components/waka-badge-showcase/index.tsx +704 -0
  139. package/src/components/waka-barcode/index.tsx +260 -0
  140. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  141. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  142. package/src/components/waka-breadcrumb/index.tsx +376 -0
  143. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  144. package/src/components/waka-budget-burn/index.tsx +1234 -0
  145. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  146. package/src/components/waka-carousel/index.tsx +893 -0
  147. package/src/components/waka-cart-summary/index.tsx +1055 -0
  148. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  149. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  150. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  151. package/src/components/waka-charts/WakaChart.tsx +124 -0
  152. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  153. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  154. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  155. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  156. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  157. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  158. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  159. package/src/components/waka-charts/index.ts +90 -0
  160. package/src/components/waka-charts/types.ts +330 -0
  161. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  162. package/src/components/waka-checklist/index.tsx +1067 -0
  163. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  164. package/src/components/waka-cohort-table/index.tsx +1011 -0
  165. package/src/components/waka-color-picker/index.tsx +447 -0
  166. package/src/components/waka-combo-counter/index.tsx +864 -0
  167. package/src/components/waka-combobox/index.tsx +497 -0
  168. package/src/components/waka-command-bar/index.tsx +403 -0
  169. package/src/components/waka-compare-period/index.tsx +1230 -0
  170. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  171. package/src/components/waka-contribution-graph/index.tsx +552 -0
  172. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  173. package/src/components/waka-coupon-input/index.tsx +592 -0
  174. package/src/components/waka-credit-card-input/index.tsx +982 -0
  175. package/src/components/waka-daily-reward/index.tsx +762 -0
  176. package/src/components/waka-date-range-picker/index.tsx +378 -0
  177. package/src/components/waka-datetime-picker/index.tsx +793 -0
  178. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  179. package/src/components/waka-deployment-lane/index.tsx +673 -0
  180. package/src/components/waka-device-trust/index.tsx +1259 -0
  181. package/src/components/waka-dock/index.tsx +285 -0
  182. package/src/components/waka-drawer/index.tsx +319 -0
  183. package/src/components/waka-empty-state/index.tsx +545 -0
  184. package/src/components/waka-error-shake/index.tsx +398 -0
  185. package/src/components/waka-feature-announcement/index.tsx +991 -0
  186. package/src/components/waka-file-upload/index.tsx +437 -0
  187. package/src/components/waka-floating-nav/index.tsx +413 -0
  188. package/src/components/waka-flow-diagram/index.tsx +508 -0
  189. package/src/components/waka-funnel-chart/index.tsx +823 -0
  190. package/src/components/waka-glow-card/index.tsx +246 -0
  191. package/src/components/waka-goal-progress/index.tsx +1025 -0
  192. package/src/components/waka-haptic-button/index.tsx +388 -0
  193. package/src/components/waka-health-pulse/index.tsx +451 -0
  194. package/src/components/waka-heatmap/index.tsx +1026 -0
  195. package/src/components/waka-hotspot/index.tsx +682 -0
  196. package/src/components/waka-image/index.tsx +373 -0
  197. package/src/components/waka-incident-timeline/index.tsx +686 -0
  198. package/src/components/waka-invoice-preview/index.tsx +829 -0
  199. package/src/components/waka-kanban/index.tsx +646 -0
  200. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  201. package/src/components/waka-leaderboard/index.tsx +746 -0
  202. package/src/components/waka-level-progress/index.tsx +665 -0
  203. package/src/components/waka-liquid-button/index.tsx +520 -0
  204. package/src/components/waka-loading-orbit/index.tsx +478 -0
  205. package/src/components/waka-loot-box/index.tsx +1091 -0
  206. package/src/components/waka-magic-link/index.tsx +321 -0
  207. package/src/components/waka-magnetic-button/index.tsx +567 -0
  208. package/src/components/waka-mention-input/index.tsx +953 -0
  209. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  210. package/src/components/waka-milestone-road/index.tsx +1064 -0
  211. package/src/components/waka-modal/index.tsx +374 -0
  212. package/src/components/waka-morph-button/index.tsx +495 -0
  213. package/src/components/waka-network-topology/index.tsx +801 -0
  214. package/src/components/waka-notifications/index.tsx +414 -0
  215. package/src/components/waka-number-input/index.tsx +373 -0
  216. package/src/components/waka-orbital-menu/index.tsx +445 -0
  217. package/src/components/waka-order-tracker/index.tsx +1041 -0
  218. package/src/components/waka-pagination/index.tsx +393 -0
  219. package/src/components/waka-password-strength/index.tsx +824 -0
  220. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  221. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  222. package/src/components/waka-phone-input/index.tsx +801 -0
  223. package/src/components/waka-pipeline-view/index.tsx +604 -0
  224. package/src/components/waka-player-card/index.tsx +691 -0
  225. package/src/components/waka-points-popup/index.tsx +366 -0
  226. package/src/components/waka-power-up/index.tsx +1155 -0
  227. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  228. package/src/components/waka-pricing-table/index.tsx +755 -0
  229. package/src/components/waka-product-card/index.tsx +786 -0
  230. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  231. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  232. package/src/components/waka-qrcode/index.tsx +232 -0
  233. package/src/components/waka-quest-card/index.tsx +1275 -0
  234. package/src/components/waka-quota-bar/index.tsx +693 -0
  235. package/src/components/waka-radar-score/index.tsx +512 -0
  236. package/src/components/waka-rank-badge/index.tsx +813 -0
  237. package/src/components/waka-rating-input/index.tsx +560 -0
  238. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  239. package/src/components/waka-region-map/index.tsx +730 -0
  240. package/src/components/waka-resource-gauge/index.tsx +654 -0
  241. package/src/components/waka-resource-pool/index.tsx +1035 -0
  242. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  243. package/src/components/waka-rollback-slider/index.tsx +891 -0
  244. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  245. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  246. package/src/components/waka-scratch-card/index.tsx +914 -0
  247. package/src/components/waka-season-pass/index.tsx +886 -0
  248. package/src/components/waka-security-score/index.tsx +1126 -0
  249. package/src/components/waka-segmented-control/index.tsx +238 -0
  250. package/src/components/waka-server-rack/index.tsx +764 -0
  251. package/src/components/waka-session-manager/index.tsx +815 -0
  252. package/src/components/waka-signature-pad/index.tsx +744 -0
  253. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  254. package/src/components/waka-skill-tree/index.tsx +1031 -0
  255. package/src/components/waka-sla-tracker/index.tsx +798 -0
  256. package/src/components/waka-slider-range/index.tsx +765 -0
  257. package/src/components/waka-spin-wheel/index.tsx +671 -0
  258. package/src/components/waka-spinner/index.tsx +284 -0
  259. package/src/components/waka-spotlight/index.tsx +410 -0
  260. package/src/components/waka-stat/index.tsx +428 -0
  261. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  262. package/src/components/waka-status-matrix/index.tsx +565 -0
  263. package/src/components/waka-stepper/index.tsx +489 -0
  264. package/src/components/waka-streak-counter/index.tsx +334 -0
  265. package/src/components/waka-success-explosion/index.tsx +453 -0
  266. package/src/components/waka-swipe-card/index.tsx +574 -0
  267. package/src/components/waka-tabs-morph/index.tsx +509 -0
  268. package/src/components/waka-tag-input/index.tsx +877 -0
  269. package/src/components/waka-team-banner/index.tsx +1183 -0
  270. package/src/components/waka-terminal-output/index.tsx +836 -0
  271. package/src/components/waka-theme-creator/index.tsx +762 -0
  272. package/src/components/waka-theme-manager/index.tsx +654 -0
  273. package/src/components/waka-thread-view/index.tsx +874 -0
  274. package/src/components/waka-tilt-card/index.tsx +250 -0
  275. package/src/components/waka-time-picker/index.tsx +479 -0
  276. package/src/components/waka-timeline/index.tsx +385 -0
  277. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  278. package/src/components/waka-tour-guide/index.tsx +920 -0
  279. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  280. package/src/components/waka-tree/index.tsx +557 -0
  281. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  282. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  283. package/src/components/waka-typewriter/index.tsx +566 -0
  284. package/src/components/waka-typing-indicator/index.tsx +649 -0
  285. package/src/components/waka-versus-card/index.tsx +1026 -0
  286. package/src/components/waka-video/index.tsx +557 -0
  287. package/src/components/waka-video-call/index.tsx +1087 -0
  288. package/src/components/waka-virtual-list/index.tsx +327 -0
  289. package/src/components/waka-voice-message/index.tsx +1019 -0
  290. package/src/components/waka-welcome-modal/index.tsx +790 -0
  291. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,803 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Templates de colonnes prédéfinis pour le DataTable
5
+ * @module templates
6
+ */
7
+
8
+ import * as React from "react"
9
+ import { Check, X, Copy, ExternalLink, Mail, Phone, MoreHorizontal, Star, StarHalf } from "lucide-react"
10
+ import { Badge } from "../../badge"
11
+ import { Avatar, AvatarFallback, AvatarImage } from "../../avatar"
12
+ import { Progress } from "../../progress"
13
+ import { Button } from "../../button"
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ } from "../../dropdown-menu"
20
+ import {
21
+ AlertDialog,
22
+ AlertDialogAction,
23
+ AlertDialogCancel,
24
+ AlertDialogContent,
25
+ AlertDialogDescription,
26
+ AlertDialogFooter,
27
+ AlertDialogHeader,
28
+ AlertDialogTitle,
29
+ } from "../../alert-dialog"
30
+ import { cn } from "../../../utils/cn"
31
+ import { formatters } from "../formatters"
32
+ import type { ColumnTemplate, ColumnTemplateOptions, ColumnTemplateAction } from "../types"
33
+
34
+ // ============================================
35
+ // HELPER COMPONENTS
36
+ // ============================================
37
+
38
+ /**
39
+ * Composant pour afficher une valeur vide
40
+ */
41
+ const EmptyValue = ({ className }: { className?: string }) => (
42
+ <span className={cn("text-muted-foreground", className)}>—</span>
43
+ )
44
+
45
+ /**
46
+ * Composant pour copier une valeur
47
+ */
48
+ const CopyButton = ({ value }: { value: string }) => {
49
+ const [copied, setCopied] = React.useState(false)
50
+
51
+ const handleCopy = async (e: React.MouseEvent) => {
52
+ e.stopPropagation()
53
+ try {
54
+ await navigator.clipboard.writeText(value)
55
+ setCopied(true)
56
+ setTimeout(() => setCopied(false), 2000)
57
+ } catch {
58
+ // Fallback silencieux
59
+ }
60
+ }
61
+
62
+ return (
63
+ <button
64
+ onClick={handleCopy}
65
+ className="ml-1 p-1 rounded hover:bg-muted transition-colors"
66
+ title={copied ? "Copié !" : "Copier"}
67
+ >
68
+ {copied ? (
69
+ <Check className="h-3 w-3 text-green-500" />
70
+ ) : (
71
+ <Copy className="h-3 w-3 text-muted-foreground" />
72
+ )}
73
+ </button>
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Composant d'action avec confirmation
79
+ */
80
+ function ActionWithConfirm<TData>({
81
+ action,
82
+ row,
83
+ }: {
84
+ action: ColumnTemplateAction<TData>
85
+ row: TData
86
+ }) {
87
+ const [open, setOpen] = React.useState(false)
88
+
89
+ const handleClick = () => {
90
+ if (action.requiresConfirm) {
91
+ setOpen(true)
92
+ } else {
93
+ action.onClick(row)
94
+ }
95
+ }
96
+
97
+ const handleConfirm = () => {
98
+ action.onClick(row)
99
+ setOpen(false)
100
+ }
101
+
102
+ const isDisabled = typeof action.disabled === "function"
103
+ ? action.disabled(row)
104
+ : action.disabled
105
+
106
+ return (
107
+ <>
108
+ <DropdownMenuItem
109
+ onClick={handleClick}
110
+ disabled={isDisabled}
111
+ className={action.variant === "destructive" ? "text-destructive focus:text-destructive" : ""}
112
+ >
113
+ {action.icon && <span className="mr-2">{action.icon}</span>}
114
+ {action.label}
115
+ </DropdownMenuItem>
116
+
117
+ <AlertDialog open={open} onOpenChange={setOpen}>
118
+ <AlertDialogContent>
119
+ <AlertDialogHeader>
120
+ <AlertDialogTitle>
121
+ {action.confirmTitle || "Confirmer l'action"}
122
+ </AlertDialogTitle>
123
+ <AlertDialogDescription>
124
+ {action.confirmMessage || "Êtes-vous sûr de vouloir effectuer cette action ?"}
125
+ </AlertDialogDescription>
126
+ </AlertDialogHeader>
127
+ <AlertDialogFooter>
128
+ <AlertDialogCancel>Annuler</AlertDialogCancel>
129
+ <AlertDialogAction
130
+ onClick={handleConfirm}
131
+ className={action.variant === "destructive" ? "bg-destructive hover:bg-destructive/90" : ""}
132
+ >
133
+ Confirmer
134
+ </AlertDialogAction>
135
+ </AlertDialogFooter>
136
+ </AlertDialogContent>
137
+ </AlertDialog>
138
+ </>
139
+ )
140
+ }
141
+
142
+ // ============================================
143
+ // COLUMN TEMPLATES
144
+ // ============================================
145
+
146
+ /**
147
+ * Template Date
148
+ */
149
+ export const dateTemplate: ColumnTemplate<unknown, unknown> = {
150
+ cell: (value, _row, options) => {
151
+ if (!value) return <EmptyValue />
152
+ const formatted = formatters.date(value, options?.format || "PP", options?.locale)
153
+ return formatted || <EmptyValue />
154
+ },
155
+ sortingFn: "datetime",
156
+ size: 120,
157
+ enableSorting: true,
158
+ enableFiltering: true,
159
+ }
160
+
161
+ /**
162
+ * Template DateTime
163
+ */
164
+ export const datetimeTemplate: ColumnTemplate<unknown, unknown> = {
165
+ cell: (value, _row, options) => {
166
+ if (!value) return <EmptyValue />
167
+ const formatted = formatters.datetime(value, options?.format || "Pp", options?.locale)
168
+ return formatted || <EmptyValue />
169
+ },
170
+ sortingFn: "datetime",
171
+ size: 160,
172
+ enableSorting: true,
173
+ enableFiltering: true,
174
+ }
175
+
176
+ /**
177
+ * Template Time
178
+ */
179
+ export const timeTemplate: ColumnTemplate<unknown, unknown> = {
180
+ cell: (value, _row, options) => {
181
+ if (!value) return <EmptyValue />
182
+ const formatted = formatters.time(value, options?.format || "HH:mm", options?.locale)
183
+ return formatted || <EmptyValue />
184
+ },
185
+ sortingFn: "datetime",
186
+ size: 80,
187
+ enableSorting: true,
188
+ }
189
+
190
+ /**
191
+ * Template Currency
192
+ */
193
+ export const currencyTemplate: ColumnTemplate<unknown, unknown> = {
194
+ cell: (value, _row, options) => {
195
+ if (value == null || value === "") return <EmptyValue />
196
+ const formatted = formatters.currency(
197
+ value,
198
+ options?.currency || "EUR",
199
+ options?.locale || "fr-FR",
200
+ {
201
+ minimumFractionDigits: options?.minimumFractionDigits,
202
+ maximumFractionDigits: options?.maximumFractionDigits,
203
+ }
204
+ )
205
+ return <span className="font-mono tabular-nums">{formatted}</span>
206
+ },
207
+ sortingFn: "basic",
208
+ size: 120,
209
+ enableSorting: true,
210
+ meta: { align: "right" },
211
+ }
212
+
213
+ /**
214
+ * Template Percentage
215
+ */
216
+ export const percentageTemplate: ColumnTemplate<unknown, unknown> = {
217
+ cell: (value, _row, options) => {
218
+ if (value == null) return <EmptyValue />
219
+ const numValue = typeof value === "number" ? value : parseFloat(String(value))
220
+ if (isNaN(numValue)) return <EmptyValue />
221
+
222
+ const displayValue = options?.showValue !== false ? formatters.percentage(numValue) : null
223
+
224
+ return (
225
+ <div className="flex items-center gap-2 min-w-[80px]">
226
+ <Progress
227
+ value={Math.min(100, Math.max(0, numValue))}
228
+ className={cn("h-2 flex-1", options?.trackColor)}
229
+ style={options?.progressColor ? { ['--progress-color' as string]: options.progressColor } : undefined}
230
+ />
231
+ {displayValue && <span className="text-sm tabular-nums w-10 text-right">{displayValue}</span>}
232
+ </div>
233
+ )
234
+ },
235
+ sortingFn: "basic",
236
+ size: 140,
237
+ enableSorting: true,
238
+ meta: { align: "right" },
239
+ }
240
+
241
+ /**
242
+ * Template Number
243
+ */
244
+ export const numberTemplate: ColumnTemplate<unknown, unknown> = {
245
+ cell: (value, _row, options) => {
246
+ if (value == null || value === "") return <EmptyValue />
247
+ const formatted = formatters.number(value, options?.locale || "fr-FR", {
248
+ notation: options?.notation,
249
+ minimumFractionDigits: options?.minimumFractionDigits,
250
+ maximumFractionDigits: options?.maximumFractionDigits,
251
+ })
252
+ return <span className="font-mono tabular-nums">{formatted}</span>
253
+ },
254
+ sortingFn: "basic",
255
+ size: 100,
256
+ enableSorting: true,
257
+ meta: { align: "right" },
258
+ }
259
+
260
+ /**
261
+ * Template Status/Badge
262
+ */
263
+ export const statusTemplate: ColumnTemplate<unknown, unknown> = {
264
+ cell: (value, _row, options) => {
265
+ if (!value) return <EmptyValue />
266
+ const strValue = String(value)
267
+
268
+ const colors = options?.statusColors || {}
269
+ const labels = options?.statusLabels || {}
270
+ const icons = options?.statusIcons || {}
271
+
272
+ const colorClass = colors[strValue] || "bg-gray-100 text-gray-800"
273
+ const label = labels[strValue] || strValue
274
+ const icon = icons[strValue]
275
+
276
+ return (
277
+ <Badge variant="outline" className={cn("capitalize gap-1", colorClass)}>
278
+ {icon && <span className="mr-1">{icon}</span>}
279
+ {label}
280
+ </Badge>
281
+ )
282
+ },
283
+ size: 120,
284
+ enableSorting: true,
285
+ enableFiltering: true,
286
+ }
287
+
288
+ /**
289
+ * Template Badge (alias de status)
290
+ */
291
+ export const badgeTemplate = statusTemplate
292
+
293
+ /**
294
+ * Template Email
295
+ */
296
+ export const emailTemplate: ColumnTemplate<unknown, unknown> = {
297
+ cell: (value, _row, options) => {
298
+ if (!value) return <EmptyValue />
299
+ const strValue = String(value)
300
+
301
+ const displayValue = options?.truncate && options?.maxLength
302
+ ? formatters.truncate(strValue, options.maxLength)
303
+ : strValue
304
+
305
+ return (
306
+ <div className="flex items-center gap-1 group">
307
+ <a
308
+ href={`mailto:${strValue}`}
309
+ className="text-primary hover:underline flex items-center gap-1"
310
+ onClick={(e) => e.stopPropagation()}
311
+ >
312
+ <Mail className="h-3 w-3 flex-shrink-0" />
313
+ <span className="truncate max-w-[200px]">{displayValue}</span>
314
+ </a>
315
+ {options?.copyable && (
316
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity">
317
+ <CopyButton value={strValue} />
318
+ </span>
319
+ )}
320
+ </div>
321
+ )
322
+ },
323
+ size: 200,
324
+ enableSorting: true,
325
+ enableFiltering: true,
326
+ }
327
+
328
+ /**
329
+ * Template Phone
330
+ */
331
+ export const phoneTemplate: ColumnTemplate<unknown, unknown> = {
332
+ cell: (value, _row, options) => {
333
+ if (!value) return <EmptyValue />
334
+ const strValue = String(value)
335
+
336
+ const formatted = formatters.phone(strValue, options?.countryCode || "FR")
337
+
338
+ return (
339
+ <div className="flex items-center gap-1 group">
340
+ <a
341
+ href={`tel:${strValue}`}
342
+ className="text-primary hover:underline flex items-center gap-1"
343
+ onClick={(e) => e.stopPropagation()}
344
+ >
345
+ <Phone className="h-3 w-3 flex-shrink-0" />
346
+ <span className="font-mono">{formatted}</span>
347
+ </a>
348
+ {options?.copyable !== false && (
349
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity">
350
+ <CopyButton value={strValue} />
351
+ </span>
352
+ )}
353
+ </div>
354
+ )
355
+ },
356
+ size: 150,
357
+ enableSorting: false,
358
+ enableFiltering: true,
359
+ }
360
+
361
+ /**
362
+ * Template URL
363
+ */
364
+ export const urlTemplate: ColumnTemplate<unknown, unknown> = {
365
+ cell: (value, _row, options) => {
366
+ if (!value) return <EmptyValue />
367
+ const strValue = String(value)
368
+
369
+ const displayValue = options?.truncate && options?.maxLength
370
+ ? formatters.truncate(strValue, options.maxLength)
371
+ : strValue
372
+
373
+ return (
374
+ <a
375
+ href={strValue}
376
+ target={options?.target || "_blank"}
377
+ rel="noopener noreferrer"
378
+ className="text-primary hover:underline flex items-center gap-1"
379
+ onClick={(e) => e.stopPropagation()}
380
+ >
381
+ <span className="truncate max-w-[200px]">{displayValue}</span>
382
+ {options?.showIcon !== false && <ExternalLink className="h-3 w-3 flex-shrink-0" />}
383
+ </a>
384
+ )
385
+ },
386
+ size: 200,
387
+ enableSorting: false,
388
+ enableFiltering: true,
389
+ }
390
+
391
+ /**
392
+ * Template Avatar
393
+ */
394
+ export const avatarTemplate: ColumnTemplate<unknown, unknown> = {
395
+ cell: (value, _row, options) => {
396
+ if (!value) return <EmptyValue />
397
+
398
+ let name: string | undefined
399
+ let image: string | undefined
400
+
401
+ if (typeof value === "string") {
402
+ name = value
403
+ } else if (typeof value === "object" && value !== null) {
404
+ const objValue = value as Record<string, unknown>
405
+ const nameKey = options?.nameKey || "name"
406
+ const imageKey = options?.imageKey || "image"
407
+ name = objValue[nameKey] as string
408
+ image = objValue[imageKey] as string
409
+ }
410
+
411
+ const sizeClasses = {
412
+ sm: "h-6 w-6 text-xs",
413
+ md: "h-8 w-8 text-sm",
414
+ lg: "h-10 w-10 text-base",
415
+ }
416
+ const size = options?.avatarSize || "md"
417
+
418
+ return (
419
+ <div className="flex items-center gap-2">
420
+ <Avatar className={sizeClasses[size]}>
421
+ <AvatarImage src={image} alt={name} />
422
+ <AvatarFallback>
423
+ {options?.fallbackInitials !== false && name ? formatters.initials(name) : "?"}
424
+ </AvatarFallback>
425
+ </Avatar>
426
+ {options?.showName && name && (
427
+ <span className="truncate max-w-[150px]">{name}</span>
428
+ )}
429
+ </div>
430
+ )
431
+ },
432
+ size: 60,
433
+ enableSorting: false,
434
+ enableFiltering: false,
435
+ }
436
+
437
+ /**
438
+ * Template Boolean
439
+ */
440
+ export const booleanTemplate: ColumnTemplate<unknown, unknown> = {
441
+ cell: (value, _row, options) => {
442
+ if (value == null) return <EmptyValue />
443
+ const boolValue = Boolean(value)
444
+
445
+ const TrueIcon = options?.trueIcon || Check
446
+ const FalseIcon = options?.falseIcon || X
447
+ const trueLabel = options?.trueLabel
448
+ const falseLabel = options?.falseLabel
449
+ const trueColor = options?.trueColor || "text-green-600"
450
+ const falseColor = options?.falseColor || "text-gray-400"
451
+
452
+ const Icon = boolValue ? TrueIcon : FalseIcon
453
+ const label = boolValue ? trueLabel : falseLabel
454
+ const colorClass = boolValue ? trueColor : falseColor
455
+
456
+ return (
457
+ <div className={cn("flex items-center gap-1", colorClass)}>
458
+ {typeof Icon === "function" ? <Icon className="h-4 w-4" /> : Icon}
459
+ {label && <span className="text-sm">{label}</span>}
460
+ </div>
461
+ )
462
+ },
463
+ size: 80,
464
+ enableSorting: true,
465
+ enableFiltering: true,
466
+ }
467
+
468
+ /**
469
+ * Template Actions
470
+ */
471
+ export function createActionsTemplate<TData>(): ColumnTemplate<TData, unknown> {
472
+ return {
473
+ cell: (_value, row, options) => {
474
+ const actions = (options?.actions || []) as ColumnTemplateAction<TData>[]
475
+
476
+ if (!actions.length) return null
477
+
478
+ return (
479
+ <DropdownMenu>
480
+ <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
481
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
482
+ <span className="sr-only">Actions</span>
483
+ <MoreHorizontal className="h-4 w-4" />
484
+ </Button>
485
+ </DropdownMenuTrigger>
486
+ <DropdownMenuContent align={options?.dropdownAlign || "end"}>
487
+ {actions.map((action) => (
488
+ <ActionWithConfirm key={action.id} action={action} row={row} />
489
+ ))}
490
+ </DropdownMenuContent>
491
+ </DropdownMenu>
492
+ )
493
+ },
494
+ size: 50,
495
+ enableSorting: false,
496
+ enableFiltering: false,
497
+ enableHiding: false,
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Template Actions par défaut
503
+ */
504
+ export const actionsTemplate: ColumnTemplate<unknown, unknown> = createActionsTemplate<unknown>()
505
+
506
+ /**
507
+ * Template Index (numéro de ligne)
508
+ */
509
+ export const indexTemplate: ColumnTemplate<unknown, unknown> = {
510
+ cell: (_value, _row, _options) => {
511
+ // Note: l'index sera passé via meta ou calculé côté hook
512
+ return <span className="text-muted-foreground tabular-nums">#</span>
513
+ },
514
+ size: 50,
515
+ enableSorting: false,
516
+ enableFiltering: false,
517
+ enableHiding: false,
518
+ }
519
+
520
+ /**
521
+ * Template ID
522
+ */
523
+ export const idTemplate: ColumnTemplate<unknown, unknown> = {
524
+ cell: (value, _row, options) => {
525
+ if (!value) return <EmptyValue />
526
+ const strValue = String(value)
527
+
528
+ const displayValue = options?.truncate && options?.maxLength
529
+ ? formatters.truncate(strValue, options.maxLength)
530
+ : strValue
531
+
532
+ return (
533
+ <div className="flex items-center gap-1 group">
534
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
535
+ {displayValue}
536
+ </code>
537
+ {options?.copyable && (
538
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity">
539
+ <CopyButton value={strValue} />
540
+ </span>
541
+ )}
542
+ </div>
543
+ )
544
+ },
545
+ size: 100,
546
+ enableSorting: true,
547
+ enableFiltering: true,
548
+ }
549
+
550
+ /**
551
+ * Template Rating (étoiles)
552
+ */
553
+ export const ratingTemplate: ColumnTemplate<unknown, unknown> = {
554
+ cell: (value, _row, options) => {
555
+ if (value == null) return <EmptyValue />
556
+ const numValue = typeof value === "number" ? value : parseFloat(String(value))
557
+ if (isNaN(numValue)) return <EmptyValue />
558
+
559
+ const maxRating = options?.maxRating || 5
560
+ const allowHalf = options?.allowHalf || false
561
+ const color = options?.color || "text-yellow-400"
562
+
563
+ const fullStars = Math.floor(numValue)
564
+ const hasHalf = allowHalf && numValue % 1 >= 0.5
565
+ const emptyStars = maxRating - fullStars - (hasHalf ? 1 : 0)
566
+
567
+ return (
568
+ <div className="flex items-center gap-0.5">
569
+ {Array.from({ length: fullStars }).map((_, i) => (
570
+ <Star key={`full-${i}`} className={cn("h-4 w-4 fill-current", color)} />
571
+ ))}
572
+ {hasHalf && <StarHalf className={cn("h-4 w-4 fill-current", color)} />}
573
+ {Array.from({ length: emptyStars }).map((_, i) => (
574
+ <Star key={`empty-${i}`} className="h-4 w-4 text-muted-foreground" />
575
+ ))}
576
+ <span className="ml-1 text-sm text-muted-foreground">{numValue.toFixed(1)}</span>
577
+ </div>
578
+ )
579
+ },
580
+ sortingFn: "basic",
581
+ size: 140,
582
+ enableSorting: true,
583
+ }
584
+
585
+ /**
586
+ * Template Progress
587
+ */
588
+ export const progressTemplate: ColumnTemplate<unknown, unknown> = {
589
+ cell: (value, _row, options) => {
590
+ if (value == null) return <EmptyValue />
591
+
592
+ const numValue = typeof value === "number" ? value : parseFloat(String(value))
593
+ if (isNaN(numValue)) return <EmptyValue />
594
+
595
+ const clampedValue = Math.min(100, Math.max(0, numValue))
596
+ const showLabel = options?.showLabel !== false
597
+
598
+ return (
599
+ <div className="flex items-center gap-2 min-w-[100px]">
600
+ <Progress
601
+ value={clampedValue}
602
+ className="h-2 flex-1"
603
+ />
604
+ {showLabel && (
605
+ <span className="text-sm tabular-nums w-10 text-right">{Math.round(clampedValue)}%</span>
606
+ )}
607
+ </div>
608
+ )
609
+ },
610
+ sortingFn: "basic",
611
+ size: 140,
612
+ enableSorting: true,
613
+ }
614
+
615
+ /**
616
+ * Template Tags
617
+ */
618
+ export const tagsTemplate: ColumnTemplate<unknown, unknown> = {
619
+ cell: (value, _row, options) => {
620
+ if (!value) return <EmptyValue />
621
+
622
+ let tags: string[]
623
+ if (typeof value === "string") {
624
+ const separator = options?.separator || ","
625
+ tags = value.split(separator).map((t) => t.trim()).filter(Boolean)
626
+ } else if (Array.isArray(value)) {
627
+ tags = value.map(String)
628
+ } else {
629
+ return <EmptyValue />
630
+ }
631
+
632
+ if (!tags.length) return <EmptyValue />
633
+
634
+ const maxTags = options?.maxTags || 3
635
+ const displayTags = tags.slice(0, maxTags)
636
+ const remainingCount = tags.length - maxTags
637
+ const tagColors = options?.tagColors || {}
638
+
639
+ return (
640
+ <div className="flex flex-wrap gap-1">
641
+ {displayTags.map((tag, i) => (
642
+ <Badge
643
+ key={i}
644
+ variant="secondary"
645
+ className={cn("text-xs", tagColors[tag])}
646
+ >
647
+ {tag}
648
+ </Badge>
649
+ ))}
650
+ {remainingCount > 0 && (
651
+ <Badge variant="outline" className="text-xs">
652
+ +{remainingCount}
653
+ </Badge>
654
+ )}
655
+ </div>
656
+ )
657
+ },
658
+ size: 200,
659
+ enableSorting: false,
660
+ enableFiltering: true,
661
+ }
662
+
663
+ /**
664
+ * Template JSON
665
+ */
666
+ export const jsonTemplate: ColumnTemplate<unknown, unknown> = {
667
+ cell: (value, _row, options) => {
668
+ if (value == null) return <EmptyValue />
669
+
670
+ const [expanded, setExpanded] = React.useState(false)
671
+ const formatted = formatters.json(value, 2)
672
+ const isExpandable = options?.expandable !== false && formatted.length > 50
673
+
674
+ if (!isExpandable) {
675
+ return (
676
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono whitespace-pre-wrap">
677
+ {formatted}
678
+ </code>
679
+ )
680
+ }
681
+
682
+ return (
683
+ <div className="relative">
684
+ <button
685
+ onClick={(e) => {
686
+ e.stopPropagation()
687
+ setExpanded(!expanded)
688
+ }}
689
+ className="text-left w-full"
690
+ >
691
+ <code className={cn(
692
+ "text-xs bg-muted px-1.5 py-0.5 rounded font-mono block",
693
+ expanded ? "whitespace-pre-wrap" : "truncate max-w-[200px]"
694
+ )}>
695
+ {formatted}
696
+ </code>
697
+ </button>
698
+ </div>
699
+ )
700
+ },
701
+ size: 200,
702
+ enableSorting: false,
703
+ enableFiltering: false,
704
+ }
705
+
706
+ /**
707
+ * Template Text (par défaut)
708
+ */
709
+ export const textTemplate: ColumnTemplate<unknown, unknown> = {
710
+ cell: (value, _row, options) => {
711
+ if (value == null || value === "") return <EmptyValue />
712
+
713
+ const strValue = String(value)
714
+ const displayValue = options?.truncate && options?.maxLength
715
+ ? formatters.truncate(strValue, options.maxLength)
716
+ : strValue
717
+
718
+ // Highlight si spécifié
719
+ if (options?.highlight) {
720
+ const regex = new RegExp(`(${options.highlight})`, "gi")
721
+ const parts = displayValue.split(regex)
722
+
723
+ return (
724
+ <div className="flex items-center gap-1 group">
725
+ <span>
726
+ {parts.map((part, i) =>
727
+ regex.test(part) ? (
728
+ <mark key={i} className="bg-yellow-200 dark:bg-yellow-800">{part}</mark>
729
+ ) : (
730
+ <React.Fragment key={i}>{part}</React.Fragment>
731
+ )
732
+ )}
733
+ </span>
734
+ {options?.copyable && (
735
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity">
736
+ <CopyButton value={strValue} />
737
+ </span>
738
+ )}
739
+ </div>
740
+ )
741
+ }
742
+
743
+ return (
744
+ <div className="flex items-center gap-1 group">
745
+ <span className={options?.truncate ? "truncate" : ""}>{displayValue}</span>
746
+ {options?.copyable && (
747
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity">
748
+ <CopyButton value={strValue} />
749
+ </span>
750
+ )}
751
+ </div>
752
+ )
753
+ },
754
+ enableSorting: true,
755
+ enableFiltering: true,
756
+ }
757
+
758
+ // ============================================
759
+ // TEMPLATES MAP
760
+ // ============================================
761
+
762
+ /**
763
+ * Map des templates par nom
764
+ * Note: Les templates sont typés avec `unknown` pour permettre l'accès dynamique par nom.
765
+ * Le typage précis est assuré lors de l'utilisation via useDataTableColumnTemplates.
766
+ */
767
+ export const templatesMap: Record<string, ColumnTemplate<unknown, unknown>> = {
768
+ date: dateTemplate,
769
+ datetime: datetimeTemplate,
770
+ time: timeTemplate,
771
+ currency: currencyTemplate,
772
+ percentage: percentageTemplate,
773
+ number: numberTemplate,
774
+ status: statusTemplate,
775
+ badge: badgeTemplate,
776
+ email: emailTemplate,
777
+ phone: phoneTemplate,
778
+ url: urlTemplate,
779
+ avatar: avatarTemplate,
780
+ boolean: booleanTemplate,
781
+ actions: actionsTemplate,
782
+ index: indexTemplate,
783
+ id: idTemplate,
784
+ rating: ratingTemplate,
785
+ progress: progressTemplate,
786
+ tags: tagsTemplate,
787
+ json: jsonTemplate,
788
+ text: textTemplate,
789
+ }
790
+
791
+ /**
792
+ * Récupère un template par son nom
793
+ */
794
+ export function getTemplate(name: string): ColumnTemplate<unknown, unknown> | undefined {
795
+ return templatesMap[name]
796
+ }
797
+
798
+ /**
799
+ * Liste des noms de templates disponibles
800
+ */
801
+ export const availableTemplateNames = Object.keys(templatesMap)
802
+
803
+ export default templatesMap