@wakastellar/ui 2.0.0 → 2.1.0

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 (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,835 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Button } from "../../components/button"
6
+ import { Input } from "../../components/input"
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/select"
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../components/collapsible"
9
+ import { ScrollArea } from "../../components/scroll-area"
10
+ import { useToast } from "../../hooks/useToast"
11
+ import { useTheme, ShadcnRegistryItem } from "../../context/theme-provider"
12
+ import { Login, type LoginConfig } from "../login"
13
+ import { WakaDashboard, defaultDashboardStats } from "../dashboard"
14
+ import { WakaChat, defaultChatUser, defaultChatConversations, defaultChatMessages } from "../chat"
15
+ import {
16
+ ChevronDown,
17
+ Sun,
18
+ Moon,
19
+ Undo2,
20
+ Redo2,
21
+ RotateCcw,
22
+ Upload,
23
+ Save,
24
+ Copy,
25
+ Check,
26
+ Loader2,
27
+ ImageIcon,
28
+ Trash2,
29
+ Palette,
30
+ Image as ImageLucide,
31
+ } from "lucide-react"
32
+
33
+ // ============================================================================
34
+ // Types & Interfaces
35
+ // ============================================================================
36
+
37
+ export interface ThemeBlockColors {
38
+ background: string
39
+ foreground: string
40
+ card: string
41
+ cardForeground: string
42
+ popover: string
43
+ popoverForeground: string
44
+ primary: string
45
+ primaryForeground: string
46
+ secondary: string
47
+ secondaryForeground: string
48
+ muted: string
49
+ mutedForeground: string
50
+ accent: string
51
+ accentForeground: string
52
+ destructive: string
53
+ destructiveForeground: string
54
+ border: string
55
+ input: string
56
+ ring: string
57
+ chart1: string
58
+ chart2: string
59
+ chart3: string
60
+ chart4: string
61
+ chart5: string
62
+ sidebarBackground?: string
63
+ sidebarForeground?: string
64
+ sidebarPrimary?: string
65
+ sidebarPrimaryForeground?: string
66
+ sidebarAccent?: string
67
+ sidebarAccentForeground?: string
68
+ sidebarBorder?: string
69
+ sidebarRing?: string
70
+ }
71
+
72
+ export interface ThemeBlockAssets {
73
+ logoLight?: string
74
+ logoDark?: string
75
+ backgroundLight?: string
76
+ backgroundDark?: string
77
+ favicon?: string
78
+ sponsorLight?: string
79
+ sponsorDark?: string
80
+ }
81
+
82
+ export interface ThemeCreatorBlockTheme {
83
+ id: string
84
+ name: string
85
+ description?: string
86
+ author?: string
87
+ lightColors: ThemeBlockColors
88
+ darkColors: ThemeBlockColors
89
+ assets?: ThemeBlockAssets
90
+ radius?: string
91
+ fonts?: { sans?: string; mono?: string }
92
+ }
93
+
94
+ export interface ThemeColorGroup {
95
+ id: string
96
+ label: string
97
+ icon?: React.ReactNode
98
+ colors: { key: keyof ThemeBlockColors; label: string; description?: string }[]
99
+ }
100
+
101
+ export interface WakaThemeCreatorBlockProps {
102
+ themes?: ThemeCreatorBlockTheme[]
103
+ initialTheme?: ThemeCreatorBlockTheme
104
+ onThemeChange?: (theme: ThemeCreatorBlockTheme) => void
105
+ onSave?: (theme: ThemeCreatorBlockTheme) => Promise<void>
106
+ onUploadAsset?: (file: File, assetType: keyof ThemeBlockAssets) => Promise<string>
107
+ onExport?: (theme: ThemeCreatorBlockTheme, format: 'json' | 'css') => void
108
+ showTypography?: boolean
109
+ showSidebarColors?: boolean
110
+ previewTabs?: ('login' | 'dashboard' | 'chat')[]
111
+ customPreview?: React.ReactNode
112
+ className?: string
113
+ }
114
+
115
+ // ============================================================================
116
+ // Hex to HSL Conversion
117
+ // ============================================================================
118
+
119
+ function hexToHSL(hex: string): string {
120
+ hex = hex.replace(/^#/, '')
121
+ const r = parseInt(hex.slice(0, 2), 16) / 255
122
+ const g = parseInt(hex.slice(2, 4), 16) / 255
123
+ const b = parseInt(hex.slice(4, 6), 16) / 255
124
+
125
+ const max = Math.max(r, g, b)
126
+ const min = Math.min(r, g, b)
127
+ let h = 0
128
+ let s = 0
129
+ const l = (max + min) / 2
130
+
131
+ if (max !== min) {
132
+ const d = max - min
133
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
134
+ switch (max) {
135
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break
136
+ case g: h = ((b - r) / d + 2) / 6; break
137
+ case b: h = ((r - g) / d + 4) / 6; break
138
+ }
139
+ }
140
+
141
+ return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
142
+ }
143
+
144
+ function colorsToRegistryItem(
145
+ themeId: string,
146
+ lightColors: ThemeBlockColors,
147
+ darkColors: ThemeBlockColors,
148
+ radius?: string
149
+ ): ShadcnRegistryItem {
150
+ const convertColors = (colors: ThemeBlockColors): Record<string, string> => ({
151
+ background: hexToHSL(colors.background),
152
+ foreground: hexToHSL(colors.foreground),
153
+ card: hexToHSL(colors.card),
154
+ "card-foreground": hexToHSL(colors.cardForeground),
155
+ popover: hexToHSL(colors.popover),
156
+ "popover-foreground": hexToHSL(colors.popoverForeground),
157
+ primary: hexToHSL(colors.primary),
158
+ "primary-foreground": hexToHSL(colors.primaryForeground),
159
+ secondary: hexToHSL(colors.secondary),
160
+ "secondary-foreground": hexToHSL(colors.secondaryForeground),
161
+ muted: hexToHSL(colors.muted),
162
+ "muted-foreground": hexToHSL(colors.mutedForeground),
163
+ accent: hexToHSL(colors.accent),
164
+ "accent-foreground": hexToHSL(colors.accentForeground),
165
+ destructive: hexToHSL(colors.destructive),
166
+ "destructive-foreground": hexToHSL(colors.destructiveForeground),
167
+ border: hexToHSL(colors.border),
168
+ input: hexToHSL(colors.input),
169
+ ring: hexToHSL(colors.ring),
170
+ "chart-1": hexToHSL(colors.chart1),
171
+ "chart-2": hexToHSL(colors.chart2),
172
+ "chart-3": hexToHSL(colors.chart3),
173
+ "chart-4": hexToHSL(colors.chart4),
174
+ "chart-5": hexToHSL(colors.chart5),
175
+ ...(colors.sidebarBackground && { "sidebar-background": hexToHSL(colors.sidebarBackground) }),
176
+ ...(colors.sidebarForeground && { "sidebar-foreground": hexToHSL(colors.sidebarForeground) }),
177
+ ...(colors.sidebarPrimary && { "sidebar-primary": hexToHSL(colors.sidebarPrimary) }),
178
+ ...(colors.sidebarPrimaryForeground && { "sidebar-primary-foreground": hexToHSL(colors.sidebarPrimaryForeground) }),
179
+ ...(colors.sidebarAccent && { "sidebar-accent": hexToHSL(colors.sidebarAccent) }),
180
+ ...(colors.sidebarAccentForeground && { "sidebar-accent-foreground": hexToHSL(colors.sidebarAccentForeground) }),
181
+ ...(colors.sidebarBorder && { "sidebar-border": hexToHSL(colors.sidebarBorder) }),
182
+ ...(colors.sidebarRing && { "sidebar-ring": hexToHSL(colors.sidebarRing) }),
183
+ })
184
+
185
+ return {
186
+ name: themeId,
187
+ type: "registry:theme",
188
+ cssVars: {
189
+ theme: { radius: radius || "0.5rem" },
190
+ light: convertColors(lightColors),
191
+ dark: convertColors(darkColors),
192
+ },
193
+ }
194
+ }
195
+
196
+ // ============================================================================
197
+ // Default Themes
198
+ // ============================================================================
199
+
200
+ const defaultLightColors: ThemeBlockColors = {
201
+ background: "#ffffff", foreground: "#0a0a0a",
202
+ card: "#ffffff", cardForeground: "#0a0a0a",
203
+ popover: "#ffffff", popoverForeground: "#0a0a0a",
204
+ primary: "#171717", primaryForeground: "#fafafa",
205
+ secondary: "#f5f5f5", secondaryForeground: "#171717",
206
+ muted: "#f5f5f5", mutedForeground: "#737373",
207
+ accent: "#f5f5f5", accentForeground: "#171717",
208
+ destructive: "#ef4444", destructiveForeground: "#fafafa",
209
+ border: "#e5e5e5", input: "#e5e5e5", ring: "#0a0a0a",
210
+ chart1: "#e76e50", chart2: "#2a9d90", chart3: "#274754", chart4: "#e8c468", chart5: "#f4a462",
211
+ }
212
+
213
+ const defaultDarkColors: ThemeBlockColors = {
214
+ background: "#0a0a0a", foreground: "#fafafa",
215
+ card: "#0a0a0a", cardForeground: "#fafafa",
216
+ popover: "#0a0a0a", popoverForeground: "#fafafa",
217
+ primary: "#fafafa", primaryForeground: "#171717",
218
+ secondary: "#262626", secondaryForeground: "#fafafa",
219
+ muted: "#262626", mutedForeground: "#a3a3a3",
220
+ accent: "#262626", accentForeground: "#fafafa",
221
+ destructive: "#7f1d1d", destructiveForeground: "#fafafa",
222
+ border: "#262626", input: "#262626", ring: "#d4d4d4",
223
+ chart1: "#2662d9", chart2: "#2eb88a", chart3: "#e88c30", chart4: "#af57db", chart5: "#e23670",
224
+ }
225
+
226
+ const oceanLightColors: ThemeBlockColors = {
227
+ background: "#f0fdfa", foreground: "#134e4a",
228
+ card: "#ffffff", cardForeground: "#134e4a",
229
+ popover: "#ffffff", popoverForeground: "#134e4a",
230
+ primary: "#14b8a6", primaryForeground: "#ffffff",
231
+ secondary: "#ccfbf1", secondaryForeground: "#134e4a",
232
+ muted: "#ccfbf1", mutedForeground: "#5eead4",
233
+ accent: "#2dd4bf", accentForeground: "#022c22",
234
+ destructive: "#ef4444", destructiveForeground: "#fafafa",
235
+ border: "#99f6e4", input: "#99f6e4", ring: "#14b8a6",
236
+ chart1: "#14b8a6", chart2: "#2dd4bf", chart3: "#5eead4", chart4: "#0d9488", chart5: "#0f766e",
237
+ }
238
+
239
+ const oceanDarkColors: ThemeBlockColors = {
240
+ background: "#042f2e", foreground: "#ccfbf1",
241
+ card: "#134e4a", cardForeground: "#ccfbf1",
242
+ popover: "#134e4a", popoverForeground: "#ccfbf1",
243
+ primary: "#2dd4bf", primaryForeground: "#042f2e",
244
+ secondary: "#115e59", secondaryForeground: "#ccfbf1",
245
+ muted: "#115e59", mutedForeground: "#5eead4",
246
+ accent: "#14b8a6", accentForeground: "#042f2e",
247
+ destructive: "#ef4444", destructiveForeground: "#fafafa",
248
+ border: "#115e59", input: "#115e59", ring: "#2dd4bf",
249
+ chart1: "#2dd4bf", chart2: "#14b8a6", chart3: "#5eead4", chart4: "#99f6e4", chart5: "#0d9488",
250
+ }
251
+
252
+ export const defaultThemes: ThemeCreatorBlockTheme[] = [
253
+ { id: "default", name: "Default", lightColors: defaultLightColors, darkColors: defaultDarkColors, radius: "0.5rem" },
254
+ { id: "ocean", name: "Ocean Breeze", lightColors: oceanLightColors, darkColors: oceanDarkColors, radius: "0.5rem" },
255
+ ]
256
+
257
+ // ============================================================================
258
+ // Color Groups Configuration
259
+ // ============================================================================
260
+
261
+ const colorGroups: ThemeColorGroup[] = [
262
+ { id: "primary", label: "Primary", colors: [
263
+ { key: "primary", label: "Primary" }, { key: "primaryForeground", label: "Foreground" }
264
+ ]},
265
+ { id: "secondary", label: "Secondary", colors: [
266
+ { key: "secondary", label: "Secondary" }, { key: "secondaryForeground", label: "Foreground" }
267
+ ]},
268
+ { id: "accent", label: "Accent", colors: [
269
+ { key: "accent", label: "Accent" }, { key: "accentForeground", label: "Foreground" }
270
+ ]},
271
+ { id: "base", label: "Base", colors: [
272
+ { key: "background", label: "Background" }, { key: "foreground", label: "Foreground" }
273
+ ]},
274
+ { id: "card", label: "Card", colors: [
275
+ { key: "card", label: "Card" }, { key: "cardForeground", label: "Foreground" }
276
+ ]},
277
+ { id: "popover", label: "Popover", colors: [
278
+ { key: "popover", label: "Popover" }, { key: "popoverForeground", label: "Foreground" }
279
+ ]},
280
+ { id: "muted", label: "Muted", colors: [
281
+ { key: "muted", label: "Muted" }, { key: "mutedForeground", label: "Foreground" }
282
+ ]},
283
+ { id: "destructive", label: "Destructive", colors: [
284
+ { key: "destructive", label: "Destructive" }, { key: "destructiveForeground", label: "Foreground" }
285
+ ]},
286
+ { id: "border", label: "Border & Input", colors: [
287
+ { key: "border", label: "Border" }, { key: "input", label: "Input" }, { key: "ring", label: "Ring" }
288
+ ]},
289
+ { id: "chart", label: "Charts", colors: [
290
+ { key: "chart1", label: "1" }, { key: "chart2", label: "2" },
291
+ { key: "chart3", label: "3" }, { key: "chart4", label: "4" }, { key: "chart5", label: "5" }
292
+ ]},
293
+ ]
294
+
295
+ // ============================================================================
296
+ // Compact Color Row Component
297
+ // ============================================================================
298
+
299
+ interface ColorRowProps {
300
+ label: string
301
+ value: string
302
+ onChange: (value: string) => void
303
+ }
304
+
305
+ function ColorRow({ label, value, onChange }: ColorRowProps) {
306
+ return (
307
+ <div className="flex items-center gap-2 py-1">
308
+ <div className="relative">
309
+ <input
310
+ type="color"
311
+ value={value}
312
+ onChange={(e) => onChange(e.target.value)}
313
+ className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent p-0"
314
+ style={{ WebkitAppearance: 'none', appearance: 'none' }}
315
+ />
316
+ <div
317
+ className="absolute inset-0.5 rounded pointer-events-none border border-border/20"
318
+ style={{ backgroundColor: value }}
319
+ />
320
+ </div>
321
+ <span className="text-[11px] text-muted-foreground flex-1 truncate">{label}</span>
322
+ <Input
323
+ value={value}
324
+ onChange={(e) => onChange(e.target.value)}
325
+ className="h-6 w-16 text-[10px] font-mono px-1.5 bg-muted/50 border-0"
326
+ />
327
+ </div>
328
+ )
329
+ }
330
+
331
+ // ============================================================================
332
+ // Asset Upload Component
333
+ // ============================================================================
334
+
335
+ interface AssetUploadProps {
336
+ label: string
337
+ value?: string
338
+ onUpload: (file: File) => Promise<void>
339
+ onRemove: () => void
340
+ accept?: string
341
+ isUploading?: boolean
342
+ }
343
+
344
+ function AssetUpload({ label, value, onUpload, onRemove, accept = "image/*", isUploading }: AssetUploadProps) {
345
+ const inputRef = React.useRef<HTMLInputElement>(null)
346
+
347
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
348
+ const file = e.target.files?.[0]
349
+ if (file) await onUpload(file)
350
+ if (inputRef.current) inputRef.current.value = ''
351
+ }
352
+
353
+ return (
354
+ <div className="flex items-center gap-2 p-2 rounded-lg border border-border/30 bg-muted/20">
355
+ {value ? (
356
+ <img src={value} alt={label} className="w-8 h-8 object-contain rounded bg-background" />
357
+ ) : (
358
+ <div className="w-8 h-8 rounded bg-muted/50 flex items-center justify-center">
359
+ <ImageIcon className="w-3 h-3 text-muted-foreground" />
360
+ </div>
361
+ )}
362
+ <div className="flex-1 min-w-0">
363
+ <p className="text-[11px] font-medium truncate">{label}</p>
364
+ </div>
365
+ <div className="flex gap-1">
366
+ <Button
367
+ variant="ghost"
368
+ size="sm"
369
+ onClick={() => inputRef.current?.click()}
370
+ disabled={isUploading}
371
+ className="h-6 w-6 p-0"
372
+ >
373
+ {isUploading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Upload className="h-3 w-3" />}
374
+ </Button>
375
+ {value && (
376
+ <Button variant="ghost" size="sm" onClick={onRemove} className="h-6 w-6 p-0 text-destructive">
377
+ <Trash2 className="h-3 w-3" />
378
+ </Button>
379
+ )}
380
+ </div>
381
+ <input ref={inputRef} type="file" accept={accept} onChange={handleFileChange} className="hidden" />
382
+ </div>
383
+ )
384
+ }
385
+
386
+ // ============================================================================
387
+ // Color Section Component
388
+ // ============================================================================
389
+
390
+ interface ColorSectionProps {
391
+ group: ThemeColorGroup
392
+ colors: ThemeBlockColors
393
+ onChange: (key: keyof ThemeBlockColors, value: string) => void
394
+ defaultOpen?: boolean
395
+ }
396
+
397
+ function ColorSection({ group, colors, onChange, defaultOpen = false }: ColorSectionProps) {
398
+ const [isOpen, setIsOpen] = React.useState(defaultOpen)
399
+
400
+ return (
401
+ <Collapsible open={isOpen} onOpenChange={setIsOpen}>
402
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-1.5 px-2 hover:bg-muted/30 rounded transition-colors text-left">
403
+ <span className="text-[11px] font-medium">{group.label}</span>
404
+ <div className="flex items-center gap-1">
405
+ {group.colors.slice(0, 3).map((c) => (
406
+ <div key={c.key} className="w-2.5 h-2.5 rounded-sm border border-border/30" style={{ backgroundColor: colors[c.key] || '#000' }} />
407
+ ))}
408
+ <ChevronDown className={cn("h-3 w-3 text-muted-foreground ml-1 transition-transform", isOpen && "rotate-180")} />
409
+ </div>
410
+ </CollapsibleTrigger>
411
+ <CollapsibleContent className="px-2 pb-1">
412
+ {group.colors.map((color) => (
413
+ <ColorRow
414
+ key={color.key}
415
+ label={color.label}
416
+ value={colors[color.key] || "#000000"}
417
+ onChange={(value) => onChange(color.key, value)}
418
+ />
419
+ ))}
420
+ </CollapsibleContent>
421
+ </Collapsible>
422
+ )
423
+ }
424
+
425
+ // ============================================================================
426
+ // Preview Components (using real blocks)
427
+ // ============================================================================
428
+
429
+ function PreviewLogin({ assets, isDark }: { assets?: ThemeBlockAssets; isDark: boolean }) {
430
+ return (
431
+ <div className="h-full w-full overflow-auto">
432
+ <Login
433
+ config={{
434
+ theme_json: {
435
+ name: "preview",
436
+ dark_mode: isDark,
437
+ },
438
+ assets: {
439
+ theme_logo_light: assets?.logoLight,
440
+ theme_logo_dark: assets?.logoDark,
441
+ theme_image_light: assets?.backgroundLight,
442
+ theme_image_dark: assets?.backgroundDark,
443
+ },
444
+ signup_options: {
445
+ allow_login: true,
446
+ require_MFA: false,
447
+ MFA_email: false,
448
+ MFA_whatsapp: false,
449
+ MFA_RCS: false,
450
+ allow_sso_google: true,
451
+ allow_sso_microsoft: false,
452
+ allow_sso_apple: false,
453
+ allow_sso_linkedIn: false,
454
+ allow_sso_github: true,
455
+ allow_sso_facebook: false,
456
+ allow_sso_instagram: false,
457
+ allow_sso_france_connect: false,
458
+ },
459
+ } as any}
460
+ showSocialLogin
461
+ onSSOGoogle={() => {}}
462
+ onSSOGithub={() => {}}
463
+ className="min-h-full"
464
+ />
465
+ </div>
466
+ )
467
+ }
468
+
469
+ function PreviewDashboard() {
470
+ return (
471
+ <div className="h-full w-full overflow-auto p-4">
472
+ <WakaDashboard
473
+ title="Tableau de bord"
474
+ description="Aperçu de vos statistiques"
475
+ stats={defaultDashboardStats}
476
+ showHeader
477
+ statsLayout="grid"
478
+ statsColumns={4}
479
+ />
480
+ </div>
481
+ )
482
+ }
483
+
484
+ function PreviewChat() {
485
+ return (
486
+ <div className="h-full w-full overflow-hidden">
487
+ <WakaChat
488
+ conversations={defaultChatConversations}
489
+ activeConversation={defaultChatConversations[0]}
490
+ messages={defaultChatMessages}
491
+ currentUser={defaultChatUser}
492
+ showConversationList={true}
493
+ showHeader={true}
494
+ layout="full"
495
+ className="h-full"
496
+ />
497
+ </div>
498
+ )
499
+ }
500
+
501
+ // ============================================================================
502
+ // Main Component
503
+ // ============================================================================
504
+
505
+ export function WakaThemeCreatorBlock({
506
+ themes = defaultThemes,
507
+ initialTheme,
508
+ onThemeChange,
509
+ onSave,
510
+ onUploadAsset,
511
+ showSidebarColors = false,
512
+ previewTabs = ['login', 'dashboard', 'chat'],
513
+ className,
514
+ }: WakaThemeCreatorBlockProps) {
515
+ const { toast } = useToast()
516
+ const { loadThemeFromJSON, isDarkMode, toggleDarkMode } = useTheme()
517
+
518
+ // Theme state
519
+ const [currentTheme, setCurrentTheme] = React.useState<ThemeCreatorBlockTheme>(
520
+ initialTheme || themes[0] || defaultThemes[0]
521
+ )
522
+ const [selectedThemeId, setSelectedThemeId] = React.useState(currentTheme.id)
523
+
524
+ // UI state
525
+ const [activeTab, setActiveTab] = React.useState<'colors' | 'assets'>('colors')
526
+ const [previewTab, setPreviewTab] = React.useState<string>(previewTabs[0])
527
+ const [editMode, setEditMode] = React.useState<'light' | 'dark'>('light')
528
+ const [isSaving, setIsSaving] = React.useState(false)
529
+ const [isCopied, setIsCopied] = React.useState(false)
530
+ const [uploadingAsset, setUploadingAsset] = React.useState<keyof ThemeBlockAssets | null>(null)
531
+
532
+ // History for undo/redo
533
+ const [history, setHistory] = React.useState<ThemeCreatorBlockTheme[]>([currentTheme])
534
+ const [historyIndex, setHistoryIndex] = React.useState(0)
535
+
536
+ // Current colors based on edit mode
537
+ const editColors = editMode === 'light' ? currentTheme.lightColors : currentTheme.darkColors
538
+
539
+ // Apply theme to global ThemeProvider (debounced)
540
+ const applyThemeRef = React.useRef<NodeJS.Timeout>()
541
+
542
+ const applyThemeToProvider = React.useCallback((theme: ThemeCreatorBlockTheme) => {
543
+ if (applyThemeRef.current) clearTimeout(applyThemeRef.current)
544
+
545
+ applyThemeRef.current = setTimeout(async () => {
546
+ try {
547
+ const registryItem = colorsToRegistryItem(
548
+ `theme-creator-preview`,
549
+ theme.lightColors,
550
+ theme.darkColors,
551
+ theme.radius
552
+ )
553
+ await loadThemeFromJSON(`theme-creator-preview`, registryItem)
554
+ } catch (error) {
555
+ console.error("Failed to apply theme:", error)
556
+ }
557
+ }, 50) // Debounce 50ms
558
+ }, [loadThemeFromJSON])
559
+
560
+ // Apply theme on mount and when it changes
561
+ React.useEffect(() => {
562
+ applyThemeToProvider(currentTheme)
563
+ return () => {
564
+ if (applyThemeRef.current) clearTimeout(applyThemeRef.current)
565
+ }
566
+ }, [currentTheme, applyThemeToProvider])
567
+
568
+ // Sync dark mode with edit mode
569
+ React.useEffect(() => {
570
+ const shouldBeDark = editMode === 'dark'
571
+ if (isDarkMode !== shouldBeDark) {
572
+ toggleDarkMode()
573
+ }
574
+ }, [editMode, isDarkMode, toggleDarkMode])
575
+
576
+ // Update color (instant)
577
+ const updateColor = React.useCallback((key: keyof ThemeBlockColors, value: string) => {
578
+ setCurrentTheme(prev => {
579
+ const colorKey = editMode === 'light' ? 'lightColors' : 'darkColors'
580
+ const updated = { ...prev, [colorKey]: { ...prev[colorKey], [key]: value } }
581
+ onThemeChange?.(updated)
582
+ return updated
583
+ })
584
+ }, [editMode, onThemeChange])
585
+
586
+ // Save to history (debounced)
587
+ const saveToHistory = React.useCallback(() => {
588
+ setHistory(h => [...h.slice(0, historyIndex + 1), currentTheme])
589
+ setHistoryIndex(i => i + 1)
590
+ }, [currentTheme, historyIndex])
591
+
592
+ React.useEffect(() => {
593
+ const timeout = setTimeout(saveToHistory, 500)
594
+ return () => clearTimeout(timeout)
595
+ }, [currentTheme, saveToHistory])
596
+
597
+ // Load theme
598
+ const loadTheme = React.useCallback((themeId: string) => {
599
+ const theme = themes.find(t => t.id === themeId)
600
+ if (theme) {
601
+ setCurrentTheme(theme)
602
+ setSelectedThemeId(themeId)
603
+ setHistory([theme])
604
+ setHistoryIndex(0)
605
+ onThemeChange?.(theme)
606
+ }
607
+ }, [themes, onThemeChange])
608
+
609
+ // Undo/Redo
610
+ const undo = React.useCallback(() => {
611
+ if (historyIndex > 0) {
612
+ setHistoryIndex(i => i - 1)
613
+ setCurrentTheme(history[historyIndex - 1])
614
+ }
615
+ }, [history, historyIndex])
616
+
617
+ const redo = React.useCallback(() => {
618
+ if (historyIndex < history.length - 1) {
619
+ setHistoryIndex(i => i + 1)
620
+ setCurrentTheme(history[historyIndex + 1])
621
+ }
622
+ }, [history, historyIndex])
623
+
624
+ // Reset
625
+ const reset = React.useCallback(() => {
626
+ const theme = themes.find(t => t.id === selectedThemeId)
627
+ if (theme) {
628
+ setCurrentTheme(theme)
629
+ setHistory([theme])
630
+ setHistoryIndex(0)
631
+ onThemeChange?.(theme)
632
+ toast({ title: "Theme reset" })
633
+ }
634
+ }, [themes, selectedThemeId, onThemeChange, toast])
635
+
636
+ // Save
637
+ const save = React.useCallback(async () => {
638
+ if (!onSave) return
639
+ setIsSaving(true)
640
+ try {
641
+ await onSave(currentTheme)
642
+ toast({ title: "Theme saved!" })
643
+ } catch {
644
+ toast({ title: "Error saving theme", variant: "destructive" })
645
+ } finally {
646
+ setIsSaving(false)
647
+ }
648
+ }, [currentTheme, onSave, toast])
649
+
650
+ // Copy CSS (HSL format for shadcn)
651
+ const copyCSS = React.useCallback(async () => {
652
+ const registryItem = colorsToRegistryItem(
653
+ currentTheme.id,
654
+ currentTheme.lightColors,
655
+ currentTheme.darkColors,
656
+ currentTheme.radius
657
+ )
658
+
659
+ const lightVars = Object.entries(registryItem.cssVars?.light || {})
660
+ .map(([k, v]) => ` --${k}: ${v};`)
661
+ .join('\n')
662
+ const darkVars = Object.entries(registryItem.cssVars?.dark || {})
663
+ .map(([k, v]) => ` --${k}: ${v};`)
664
+ .join('\n')
665
+
666
+ const css = `:root {\n${lightVars}\n}\n\n.dark {\n${darkVars}\n}`
667
+
668
+ await navigator.clipboard.writeText(css)
669
+ setIsCopied(true)
670
+ setTimeout(() => setIsCopied(false), 2000)
671
+ toast({ title: "CSS copied!" })
672
+ }, [currentTheme, toast])
673
+
674
+ // Upload asset
675
+ const handleUploadAsset = React.useCallback(async (file: File, assetType: keyof ThemeBlockAssets) => {
676
+ if (!onUploadAsset) return
677
+ setUploadingAsset(assetType)
678
+ try {
679
+ const url = await onUploadAsset(file, assetType)
680
+ setCurrentTheme(prev => {
681
+ const updated = { ...prev, assets: { ...prev.assets, [assetType]: url } }
682
+ onThemeChange?.(updated)
683
+ return updated
684
+ })
685
+ toast({ title: "Asset uploaded" })
686
+ } catch {
687
+ toast({ title: "Upload failed", variant: "destructive" })
688
+ } finally {
689
+ setUploadingAsset(null)
690
+ }
691
+ }, [onUploadAsset, onThemeChange, toast])
692
+
693
+ // Remove asset
694
+ const handleRemoveAsset = React.useCallback((assetType: keyof ThemeBlockAssets) => {
695
+ setCurrentTheme(prev => {
696
+ const newAssets = { ...prev.assets }
697
+ delete newAssets[assetType]
698
+ const updated = { ...prev, assets: newAssets }
699
+ onThemeChange?.(updated)
700
+ return updated
701
+ })
702
+ }, [onThemeChange])
703
+
704
+ return (
705
+ <div className={cn("flex h-[calc(100vh-180px)] min-h-[500px] border rounded-lg overflow-hidden bg-background", className)}>
706
+ {/* Left Panel - Editor */}
707
+ <div className="w-56 border-r flex flex-col bg-card/30 shrink-0">
708
+ {/* Header */}
709
+ <div className="p-2 border-b space-y-1.5">
710
+ <Select value={selectedThemeId} onValueChange={loadTheme}>
711
+ <SelectTrigger className="h-7 text-xs">
712
+ <SelectValue />
713
+ </SelectTrigger>
714
+ <SelectContent>
715
+ {themes.map(theme => (
716
+ <SelectItem key={theme.id} value={theme.id}>
717
+ <div className="flex items-center gap-2">
718
+ <div className="h-2 w-2 rounded-full" style={{ backgroundColor: theme.lightColors.primary }} />
719
+ <span className="text-xs">{theme.name}</span>
720
+ </div>
721
+ </SelectItem>
722
+ ))}
723
+ </SelectContent>
724
+ </Select>
725
+
726
+ {/* Tabs: Colors / Assets */}
727
+ <div className="flex gap-0.5 p-0.5 bg-muted rounded">
728
+ <button
729
+ onClick={() => setActiveTab('colors')}
730
+ className={cn("flex-1 flex items-center justify-center gap-1 px-2 py-0.5 rounded text-[10px] transition-all", activeTab === 'colors' ? "bg-background shadow-sm" : "text-muted-foreground hover:text-foreground")}
731
+ >
732
+ <Palette className="h-3 w-3" /> Colors
733
+ </button>
734
+ <button
735
+ onClick={() => setActiveTab('assets')}
736
+ className={cn("flex-1 flex items-center justify-center gap-1 px-2 py-0.5 rounded text-[10px] transition-all", activeTab === 'assets' ? "bg-background shadow-sm" : "text-muted-foreground hover:text-foreground")}
737
+ >
738
+ <ImageLucide className="h-3 w-3" /> Assets
739
+ </button>
740
+ </div>
741
+
742
+ {/* Light/Dark toggle */}
743
+ {activeTab === 'colors' && (
744
+ <div className="flex gap-0.5 p-0.5 bg-muted/50 rounded">
745
+ <button onClick={() => setEditMode('light')} className={cn("flex-1 flex items-center justify-center gap-1 px-2 py-0.5 rounded text-[10px] transition-all", editMode === 'light' ? "bg-background shadow-sm" : "text-muted-foreground")}>
746
+ <Sun className="h-3 w-3" /> Light
747
+ </button>
748
+ <button onClick={() => setEditMode('dark')} className={cn("flex-1 flex items-center justify-center gap-1 px-2 py-0.5 rounded text-[10px] transition-all", editMode === 'dark' ? "bg-background shadow-sm" : "text-muted-foreground")}>
749
+ <Moon className="h-3 w-3" /> Dark
750
+ </button>
751
+ </div>
752
+ )}
753
+ </div>
754
+
755
+ {/* Content */}
756
+ <ScrollArea className="flex-1">
757
+ {activeTab === 'colors' ? (
758
+ <div className="p-1">
759
+ {colorGroups.map((group, i) => (
760
+ <ColorSection key={group.id} group={group} colors={editColors} onChange={updateColor} defaultOpen={i < 3} />
761
+ ))}
762
+ </div>
763
+ ) : (
764
+ <div className="p-1.5 space-y-1.5">
765
+ <p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wider px-1">Logos</p>
766
+ <AssetUpload label="Logo Light" value={currentTheme.assets?.logoLight} onUpload={(f) => handleUploadAsset(f, 'logoLight')} onRemove={() => handleRemoveAsset('logoLight')} isUploading={uploadingAsset === 'logoLight'} />
767
+ <AssetUpload label="Logo Dark" value={currentTheme.assets?.logoDark} onUpload={(f) => handleUploadAsset(f, 'logoDark')} onRemove={() => handleRemoveAsset('logoDark')} isUploading={uploadingAsset === 'logoDark'} />
768
+
769
+ <p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wider px-1 pt-1">Backgrounds</p>
770
+ <AssetUpload label="Background Light" value={currentTheme.assets?.backgroundLight} onUpload={(f) => handleUploadAsset(f, 'backgroundLight')} onRemove={() => handleRemoveAsset('backgroundLight')} isUploading={uploadingAsset === 'backgroundLight'} />
771
+ <AssetUpload label="Background Dark" value={currentTheme.assets?.backgroundDark} onUpload={(f) => handleUploadAsset(f, 'backgroundDark')} onRemove={() => handleRemoveAsset('backgroundDark')} isUploading={uploadingAsset === 'backgroundDark'} />
772
+
773
+ <p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wider px-1 pt-1">Other</p>
774
+ <AssetUpload label="Favicon" value={currentTheme.assets?.favicon} onUpload={(f) => handleUploadAsset(f, 'favicon')} onRemove={() => handleRemoveAsset('favicon')} accept="image/x-icon,image/png,image/svg+xml" isUploading={uploadingAsset === 'favicon'} />
775
+ <AssetUpload label="Sponsor Light" value={currentTheme.assets?.sponsorLight} onUpload={(f) => handleUploadAsset(f, 'sponsorLight')} onRemove={() => handleRemoveAsset('sponsorLight')} isUploading={uploadingAsset === 'sponsorLight'} />
776
+ <AssetUpload label="Sponsor Dark" value={currentTheme.assets?.sponsorDark} onUpload={(f) => handleUploadAsset(f, 'sponsorDark')} onRemove={() => handleRemoveAsset('sponsorDark')} isUploading={uploadingAsset === 'sponsorDark'} />
777
+ </div>
778
+ )}
779
+ </ScrollArea>
780
+
781
+ {/* Actions */}
782
+ <div className="p-1.5 border-t space-y-1">
783
+ <div className="flex gap-1">
784
+ <Button variant="outline" size="sm" onClick={undo} disabled={historyIndex === 0} className="flex-1 h-6 text-[10px]"><Undo2 className="h-3 w-3" /></Button>
785
+ <Button variant="outline" size="sm" onClick={redo} disabled={historyIndex === history.length - 1} className="flex-1 h-6 text-[10px]"><Redo2 className="h-3 w-3" /></Button>
786
+ <Button variant="outline" size="sm" onClick={reset} className="flex-1 h-6 text-[10px]"><RotateCcw className="h-3 w-3" /></Button>
787
+ </div>
788
+ <div className="flex gap-1">
789
+ <Button variant="outline" size="sm" onClick={copyCSS} className="flex-1 h-6 text-[10px]">
790
+ {isCopied ? <Check className="h-3 w-3 mr-1" /> : <Copy className="h-3 w-3 mr-1" />}
791
+ {isCopied ? 'Copied' : 'CSS'}
792
+ </Button>
793
+ {onSave && (
794
+ <Button size="sm" onClick={save} disabled={isSaving} className="flex-1 h-6 text-[10px]">
795
+ {isSaving ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Save className="h-3 w-3 mr-1" />}
796
+ Save
797
+ </Button>
798
+ )}
799
+ </div>
800
+ </div>
801
+ </div>
802
+
803
+ {/* Right Panel - Preview */}
804
+ <div className="flex-1 flex flex-col min-w-0">
805
+ {/* Preview Header */}
806
+ <div className="h-9 border-b flex items-center justify-between px-2 shrink-0">
807
+ <div className="flex gap-0.5 p-0.5 bg-muted rounded">
808
+ {previewTabs.includes('login') && (
809
+ <button onClick={() => setPreviewTab('login')} className={cn("px-2 py-0.5 rounded text-[10px] transition-all", previewTab === 'login' ? "bg-background shadow-sm" : "text-muted-foreground")}>Login</button>
810
+ )}
811
+ {previewTabs.includes('dashboard') && (
812
+ <button onClick={() => setPreviewTab('dashboard')} className={cn("px-2 py-0.5 rounded text-[10px] transition-all", previewTab === 'dashboard' ? "bg-background shadow-sm" : "text-muted-foreground")}>Dashboard</button>
813
+ )}
814
+ {previewTabs.includes('chat') && (
815
+ <button onClick={() => setPreviewTab('chat')} className={cn("px-2 py-0.5 rounded text-[10px] transition-all", previewTab === 'chat' ? "bg-background shadow-sm" : "text-muted-foreground")}>Chat</button>
816
+ )}
817
+ </div>
818
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
819
+ <span>Mode: {editMode === 'light' ? 'Light' : 'Dark'}</span>
820
+ {editMode === 'light' ? <Sun className="h-3 w-3" /> : <Moon className="h-3 w-3" />}
821
+ </div>
822
+ </div>
823
+
824
+ {/* Preview Content */}
825
+ <div className="flex-1 overflow-hidden">
826
+ {previewTab === 'login' && <PreviewLogin assets={currentTheme.assets} isDark={editMode === 'dark'} />}
827
+ {previewTab === 'dashboard' && <PreviewDashboard />}
828
+ {previewTab === 'chat' && <PreviewChat />}
829
+ </div>
830
+ </div>
831
+ </div>
832
+ )
833
+ }
834
+
835
+ WakaThemeCreatorBlock.displayName = "WakaThemeCreatorBlock"