vome-core 0.3.65 → 0.3.67

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 (342) hide show
  1. package/dist/admin/api/client.js +7 -2
  2. package/dist/admin/components/ui/avatar/Avatar.vue +18 -18
  3. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -21
  4. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -16
  5. package/dist/admin/components/ui/avatar/index.js +1 -1
  6. package/dist/admin/components/ui/badge/Badge.vue +26 -26
  7. package/dist/admin/components/ui/button/Button.vue +31 -31
  8. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -35
  9. package/dist/admin/components/ui/checkbox/index.js +1 -1
  10. package/dist/admin/components/ui/dialog/Dialog.vue +19 -19
  11. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -15
  12. package/dist/admin/components/ui/dialog/DialogContent.vue +267 -267
  13. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -23
  14. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -27
  15. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -17
  16. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -21
  17. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -60
  18. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -23
  19. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -15
  20. package/dist/admin/components/ui/dialog/index.js +1 -1
  21. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -19
  22. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -39
  23. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -39
  24. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -15
  25. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -31
  26. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -23
  27. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -21
  28. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -40
  29. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -23
  30. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -17
  31. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -18
  32. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -27
  33. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -31
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -17
  35. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  36. package/dist/admin/components/ui/input/Input.vue +183 -183
  37. package/dist/admin/components/ui/input/index.js +1 -1
  38. package/dist/admin/components/ui/label/Label.vue +26 -26
  39. package/dist/admin/components/ui/label/index.js +1 -1
  40. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -33
  41. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -32
  42. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  43. package/dist/admin/components/ui/select/Select.vue +19 -19
  44. package/dist/admin/components/ui/select/SelectContent.vue +51 -51
  45. package/dist/admin/components/ui/select/SelectGroup.vue +15 -15
  46. package/dist/admin/components/ui/select/SelectItem.vue +53 -53
  47. package/dist/admin/components/ui/select/SelectItemText.vue +15 -15
  48. package/dist/admin/components/ui/select/SelectLabel.vue +17 -17
  49. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -26
  50. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -26
  51. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -19
  52. package/dist/admin/components/ui/select/SelectTrigger.vue +44 -44
  53. package/dist/admin/components/ui/select/SelectValue.vue +15 -15
  54. package/dist/admin/components/ui/select/index.js +1 -1
  55. package/dist/admin/components/ui/separator/Separator.vue +29 -29
  56. package/dist/admin/components/ui/separator/index.js +1 -1
  57. package/dist/admin/components/ui/sheet/Sheet.vue +19 -19
  58. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -15
  59. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -63
  60. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -21
  61. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -16
  62. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -15
  63. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -21
  64. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -21
  65. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -15
  66. package/dist/admin/components/ui/sheet/index.js +1 -1
  67. package/dist/admin/components/ui/sonner/Toaster.vue +117 -117
  68. package/dist/admin/components/ui/sonner/index.js +1 -1
  69. package/dist/admin/components/ui/table/Table.vue +16 -16
  70. package/dist/admin/components/ui/table/TableBody.vue +17 -17
  71. package/dist/admin/components/ui/table/TableCaption.vue +17 -17
  72. package/dist/admin/components/ui/table/TableCell.vue +22 -22
  73. package/dist/admin/components/ui/table/TableEmpty.vue +34 -34
  74. package/dist/admin/components/ui/table/TableFooter.vue +17 -17
  75. package/dist/admin/components/ui/table/TableHead.vue +17 -17
  76. package/dist/admin/components/ui/table/TableHeader.vue +17 -17
  77. package/dist/admin/components/ui/table/TableRow.vue +17 -17
  78. package/dist/admin/components/ui/table/index.js +1 -1
  79. package/dist/admin/components/ui/tabs/Tabs.vue +24 -24
  80. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -21
  81. package/dist/admin/components/ui/tabs/TabsList.vue +24 -24
  82. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -26
  83. package/dist/admin/components/ui/tabs/index.js +1 -1
  84. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -19
  85. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -34
  86. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -14
  87. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -15
  88. package/dist/admin/components/ui/tooltip/index.js +1 -1
  89. package/dist/admin/components/vm-action-btn.vue +136 -136
  90. package/dist/admin/components/vm-aside-list-item.vue +136 -136
  91. package/dist/admin/components/vm-aside.vue +229 -229
  92. package/dist/admin/components/vm-auto-perm-dialog.vue +371 -371
  93. package/dist/admin/components/vm-card-grid.vue +37 -37
  94. package/dist/admin/components/vm-card-head.vue +45 -45
  95. package/dist/admin/components/vm-card-meta.vue +22 -22
  96. package/dist/admin/components/vm-card-title.vue +56 -56
  97. package/dist/admin/components/vm-card.vue +176 -176
  98. package/dist/admin/components/vm-check-tree.vue +340 -340
  99. package/dist/admin/components/vm-clamp-text.vue +39 -39
  100. package/dist/admin/components/vm-dept-tree.vue +497 -497
  101. package/dist/admin/components/vm-empty.vue +68 -68
  102. package/dist/admin/components/vm-eps-perm-picker.vue +123 -123
  103. package/dist/admin/components/vm-form-dialog.vue +632 -632
  104. package/dist/admin/components/vm-group-cascader.vue +560 -560
  105. package/dist/admin/components/vm-icon-btn.vue +88 -88
  106. package/dist/admin/components/vm-icon-picker.vue +647 -647
  107. package/dist/admin/components/vm-inline-search.vue +61 -61
  108. package/dist/admin/components/vm-loading.vue +114 -114
  109. package/dist/admin/components/vm-markdown.vue +405 -405
  110. package/dist/admin/components/vm-marquee.vue +265 -265
  111. package/dist/admin/components/vm-page-tabs.vue +94 -94
  112. package/dist/admin/components/vm-perm-section.vue +57 -57
  113. package/dist/admin/components/vm-ri-icon.vue +23 -23
  114. package/dist/admin/components/vm-role-picker.vue +314 -314
  115. package/dist/admin/components/vm-split-layout.vue +106 -106
  116. package/dist/admin/components/vm-thumb.vue +49 -49
  117. package/dist/admin/components/vm-view-path-picker.vue +59 -59
  118. package/dist/admin/crud/auto-fields.js +1 -1
  119. package/dist/admin/crud/components/vm-avatar.vue +104 -104
  120. package/dist/admin/crud/components/vm-color-picker.vue +1626 -1626
  121. package/dist/admin/crud/components/vm-column-custom.vue +193 -193
  122. package/dist/admin/crud/components/vm-context-menu.vue +105 -105
  123. package/dist/admin/crud/components/vm-copy-btn.vue +102 -102
  124. package/dist/admin/crud/components/vm-date-calendar-panel.vue +537 -537
  125. package/dist/admin/crud/components/vm-date-picker.vue +75 -75
  126. package/dist/admin/crud/components/vm-date-range.vue +88 -88
  127. package/dist/admin/crud/components/vm-date-text.vue +42 -42
  128. package/dist/admin/crud/components/vm-datetime-field.vue +215 -215
  129. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -9
  130. package/dist/admin/crud/components/vm-dict-tag.vue +112 -112
  131. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -34
  132. package/dist/admin/crud/components/vm-form-hint.vue +81 -81
  133. package/dist/admin/crud/components/vm-json-code.vue +41 -41
  134. package/dist/admin/crud/components/vm-json-editor.vue +161 -161
  135. package/dist/admin/crud/components/vm-json-kv-editor.vue +318 -318
  136. package/dist/admin/crud/components/vm-kv-row.vue +51 -51
  137. package/dist/admin/crud/components/vm-multi-select.vue +279 -279
  138. package/dist/admin/crud/components/vm-number-range.vue +204 -204
  139. package/dist/admin/crud/components/vm-preview-viewer.vue +894 -894
  140. package/dist/admin/crud/components/vm-radio.vue +87 -87
  141. package/dist/admin/crud/components/vm-richtext.vue +313 -313
  142. package/dist/admin/crud/components/vm-select.vue +570 -570
  143. package/dist/admin/crud/components/vm-status-tag.vue +158 -158
  144. package/dist/admin/crud/components/vm-switch.vue +158 -158
  145. package/dist/admin/crud/components/vm-tag-list.vue +108 -108
  146. package/dist/admin/crud/components/vm-text-link.vue +52 -52
  147. package/dist/admin/crud/components/vm-toggle-switch.vue +84 -84
  148. package/dist/admin/crud/components/vm-tree-select.vue +450 -450
  149. package/dist/admin/crud/components/vm-upload-item.vue +298 -298
  150. package/dist/admin/crud/components/vm-upload.vue +436 -436
  151. package/dist/admin/crud/config.js +1 -1
  152. package/dist/admin/crud/confirm.js +1 -1
  153. package/dist/admin/crud/crud-page-request.js +1 -1
  154. package/dist/admin/crud/data-i18n.js +1 -1
  155. package/dist/admin/crud/dict.js +1 -1
  156. package/dist/admin/crud/export-fetch.js +1 -1
  157. package/dist/admin/crud/index.js +1 -0
  158. package/dist/admin/crud/key.js +1 -1
  159. package/dist/admin/crud/plugins.js +1 -1
  160. package/dist/admin/crud/span.js +1 -1
  161. package/dist/admin/crud/style.js +1 -1
  162. package/dist/admin/crud/useCrud.js +3 -0
  163. package/dist/admin/crud/validate.js +1 -1
  164. package/dist/admin/crud/vm-add-btn.vue +70 -70
  165. package/dist/admin/crud/vm-adv-search.vue +83 -83
  166. package/dist/admin/crud/vm-crud.vue +711 -711
  167. package/dist/admin/crud/vm-data-i18n-btn.vue +456 -456
  168. package/dist/admin/crud/vm-data-i18n-lang.vue +183 -183
  169. package/dist/admin/crud/vm-export-btn.vue +183 -183
  170. package/dist/admin/crud/vm-flex.vue +14 -14
  171. package/dist/admin/crud/vm-form.vue +33 -15
  172. package/dist/admin/crud/vm-import-btn.vue +747 -747
  173. package/dist/admin/crud/vm-multi-delete-btn.vue +49 -49
  174. package/dist/admin/crud/vm-pagination.vue +212 -212
  175. package/dist/admin/crud/vm-refresh-btn.vue +35 -35
  176. package/dist/admin/crud/vm-row.vue +55 -55
  177. package/dist/admin/crud/vm-search-key.vue +78 -78
  178. package/dist/admin/crud/vm-search.vue +466 -466
  179. package/dist/admin/crud/vm-table.vue +1390 -1390
  180. package/dist/admin/crud/vm-tabs.vue +94 -94
  181. package/dist/admin/crud/vm-toolbar.vue +357 -357
  182. package/dist/admin/crud/vm-upsert.vue +765 -765
  183. package/dist/admin/directives/perm.js +1 -1
  184. package/dist/admin/env.d.ts +1 -1
  185. package/dist/admin/hooks/useUpload.js +1 -1
  186. package/dist/admin/index.d.ts +1 -31
  187. package/dist/admin/lib/browser.js +1 -1
  188. package/dist/admin/lib/cn.js +1 -1
  189. package/dist/admin/lib/dialog-float.js +1 -1
  190. package/dist/admin/lib/export-excel.js +1 -1
  191. package/dist/admin/lib/import-excel.js +1 -1
  192. package/dist/admin/lib/json.js +1 -1
  193. package/dist/admin/lib/menu.js +1 -1
  194. package/dist/admin/lib/micro-locale.js +1 -1
  195. package/dist/admin/lib/micro-theme.js +1 -1
  196. package/dist/admin/lib/shell.js +1 -1
  197. package/dist/admin/lib/theme-color-options.js +1 -1
  198. package/dist/admin/lib/tree.js +1 -1
  199. package/dist/admin/lib/upload.js +1 -1
  200. package/dist/admin/lib/video-frame.js +1 -1
  201. package/dist/admin/router/create.js +1 -1
  202. package/dist/admin/router/guards.js +1 -1
  203. package/dist/admin/router/index.d.ts +1 -5
  204. package/dist/admin/service/index.d.ts +1 -11
  205. package/dist/admin/styles/base.css +121 -121
  206. package/dist/admin/vendor/browser-image-compression.js +1144 -1144
  207. package/dist/admin/vendor/decimal-js.js +12 -12
  208. package/dist/admin/vendor/xlsx.js +263 -263
  209. package/dist/ai/index.js +1 -1
  210. package/dist/client/admin-vendor.d.ts +1 -13
  211. package/dist/client/ai-model-registry.js +1 -1
  212. package/dist/client/base.js +1 -1
  213. package/dist/client/configure.js +1 -1
  214. package/dist/client/dict.js +1 -1
  215. package/dist/client/eps-payload.js +1 -1
  216. package/dist/client/eps.js +1 -1
  217. package/dist/client/index.d.ts +1 -7
  218. package/dist/client/index.js +1 -1
  219. package/dist/client/locale-change.js +1 -1
  220. package/dist/client/locale-store.js +1 -1
  221. package/dist/client/service.js +1 -1
  222. package/dist/client/vite-admin.d.ts +1 -40
  223. package/dist/client/vite-admin.js +1 -1
  224. package/dist/client/vite-auto-import.d.ts +1 -2
  225. package/dist/client/vite-auto-import.js +1 -1
  226. package/dist/client/vite-micro-proxy.d.ts +1 -20
  227. package/dist/client/vite-micro-proxy.js +1 -1
  228. package/dist/client/vite-plugin-eps.d.ts +1 -20
  229. package/dist/client/vite-plugin-eps.js +1 -1
  230. package/dist/client/vite-resolve.d.ts +1 -16
  231. package/dist/client/vite-resolve.js +1 -1
  232. package/dist/config/index.js +1 -1
  233. package/dist/index.js +1 -1
  234. package/dist/server/index.js +1 -1
  235. package/dist/shared/datetime-picker.js +1 -1
  236. package/dist/shared/decimal.js +1 -1
  237. package/dist/shared/excel.js +1 -1
  238. package/dist/shared/index.js +1 -1
  239. package/dist/shared/locale-json.js +1 -1
  240. package/dist/shared/tree.js +1 -1
  241. package/package.json +1 -1
  242. package/typings/admin/base/auth.ts +24 -24
  243. package/typings/admin/base/permission.ts +19 -19
  244. package/typings/admin/comm/cascader.ts +7 -7
  245. package/typings/admin/comm/check-tree.ts +7 -7
  246. package/typings/admin/comm/crud.ts +427 -427
  247. package/typings/admin/comm/eps.ts +86 -86
  248. package/typings/admin/comm/request.ts +16 -16
  249. package/typings/admin/comm/service.ts +43 -43
  250. package/typings/admin/comm/upload.ts +39 -39
  251. package/typings/admin/theme.ts +10 -10
  252. package/typings/admin/vome/browser.ts +11 -11
  253. package/typings/admin/vome/module.ts +13 -13
  254. package/typings/admin/vome/tags.ts +9 -9
  255. package/typings/comm/crud.ts +83 -83
  256. package/typings/comm/page.ts +18 -18
  257. package/typings/comm/scan.ts +6 -6
  258. package/typings/common.ts +4 -4
  259. package/typings/context/data.ts +10 -10
  260. package/typings/ext/market-seat.ts +42 -42
  261. package/typings/ext/market.ts +55 -55
  262. package/typings/ext/module.ts +64 -64
  263. package/typings/ext/plugin.ts +38 -38
  264. package/typings/ioc/metadata.ts +34 -34
  265. package/typings/logger/config.ts +6 -6
  266. package/typings/routing/crud.ts +199 -199
  267. package/typings/routing/eps.ts +88 -88
  268. package/typings/routing/meta.ts +63 -63
  269. package/typings/routing/register.ts +21 -21
  270. package/typings/routing/scan-context.ts +7 -7
  271. package/dist/admin/api/client.d.ts +0 -50
  272. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  273. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  274. package/dist/admin/components/ui/button/index.d.ts +0 -7
  275. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  276. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  277. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  278. package/dist/admin/components/ui/input/index.d.ts +0 -1
  279. package/dist/admin/components/ui/label/index.d.ts +0 -1
  280. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  281. package/dist/admin/components/ui/select/index.d.ts +0 -11
  282. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  283. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  284. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  285. package/dist/admin/components/ui/table/index.d.ts +0 -9
  286. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  287. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  288. package/dist/admin/crud/auto-fields.d.ts +0 -68
  289. package/dist/admin/crud/config.d.ts +0 -5
  290. package/dist/admin/crud/confirm.d.ts +0 -19
  291. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  292. package/dist/admin/crud/data-i18n.d.ts +0 -62
  293. package/dist/admin/crud/dict.d.ts +0 -4
  294. package/dist/admin/crud/export-fetch.d.ts +0 -22
  295. package/dist/admin/crud/index.d.ts +0 -60
  296. package/dist/admin/crud/json-kv.d.ts +0 -9
  297. package/dist/admin/crud/key.d.ts +0 -22
  298. package/dist/admin/crud/paging.d.ts +0 -2
  299. package/dist/admin/crud/plugins.d.ts +0 -63
  300. package/dist/admin/crud/registry.d.ts +0 -3
  301. package/dist/admin/crud/span.d.ts +0 -17
  302. package/dist/admin/crud/style.d.ts +0 -82
  303. package/dist/admin/crud/useCrud.d.ts +0 -21
  304. package/dist/admin/crud/validate.d.ts +0 -7
  305. package/dist/admin/directives/perm.d.ts +0 -3
  306. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  307. package/dist/admin/hooks/useUpload.d.ts +0 -8
  308. package/dist/admin/hooks/useVome.d.ts +0 -10
  309. package/dist/admin/lib/browser.d.ts +0 -3
  310. package/dist/admin/lib/cn.d.ts +0 -2
  311. package/dist/admin/lib/compress-image.d.ts +0 -12
  312. package/dist/admin/lib/dialog-float.d.ts +0 -8
  313. package/dist/admin/lib/eps.d.ts +0 -2
  314. package/dist/admin/lib/export-excel.d.ts +0 -9
  315. package/dist/admin/lib/file-preview.d.ts +0 -9
  316. package/dist/admin/lib/import-excel.d.ts +0 -21
  317. package/dist/admin/lib/json.d.ts +0 -11
  318. package/dist/admin/lib/menu.d.ts +0 -3
  319. package/dist/admin/lib/micro-locale.d.ts +0 -5
  320. package/dist/admin/lib/micro-theme.d.ts +0 -14
  321. package/dist/admin/lib/module.d.ts +0 -21
  322. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  323. package/dist/admin/lib/shell.d.ts +0 -8
  324. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  325. package/dist/admin/lib/tree.d.ts +0 -2
  326. package/dist/admin/lib/upload.d.ts +0 -17
  327. package/dist/admin/lib/video-frame.d.ts +0 -6
  328. package/dist/admin/router/create.d.ts +0 -16
  329. package/dist/admin/router/guards.d.ts +0 -6
  330. package/dist/admin/router/menu-routes.d.ts +0 -13
  331. package/dist/admin/stores/app.d.ts +0 -13
  332. package/dist/admin/stores/tags.d.ts +0 -14
  333. package/dist/admin/stores/user.d.ts +0 -48
  334. package/dist/client/ai-model-registry.d.ts +0 -16
  335. package/dist/client/base.d.ts +0 -28
  336. package/dist/client/configure.d.ts +0 -33
  337. package/dist/client/dict.d.ts +0 -31
  338. package/dist/client/eps-payload.d.ts +0 -3
  339. package/dist/client/eps.d.ts +0 -17
  340. package/dist/client/locale-change.d.ts +0 -16
  341. package/dist/client/locale-store.d.ts +0 -105
  342. package/dist/client/service.d.ts +0 -29
@@ -1,1626 +1,1626 @@
1
- <template>
2
- <div
3
- ref="rootRef"
4
- class="vm-color-picker"
5
- :class="{
6
- 'is-disabled': disabled,
7
- 'is-sm': size === 'sm',
8
- 'is-open': open,
9
- 'is-swatch-only': swatchOnly,
10
- }"
11
- >
12
- <button
13
- ref="triggerRef"
14
- type="button"
15
- class="vm-color-picker__swatch"
16
- :disabled="disabled"
17
- :title="disabled ? undefined : crudLabel('pickColor')"
18
- @click="toggle"
19
- >
20
- <span
21
- class="vm-color-picker__swatch-fill"
22
- :style="{ background: cssColor }"
23
- />
24
- </button>
25
-
26
- <input
27
- v-if="!swatchOnly"
28
- type="text"
29
- class="vm-color-picker__input"
30
- :value="inlineDisplay"
31
- :placeholder="placeholderText"
32
- :disabled="disabled"
33
- spellcheck="false"
34
- autocomplete="off"
35
- :title="isThemeBound ? themeBoundLabel : undefined"
36
- @input="onInlineInput"
37
- @focus="openPanel"
38
- @blur="onInlineBlur"
39
- @keydown.esc.stop="onEscEnd"
40
- />
41
-
42
- <Teleport to="body">
43
- <FocusScope
44
- v-if="open"
45
- as-child
46
- loop
47
- :trapped="true"
48
- @unmount-auto-focus="onPanelUnmountAutoFocus"
49
- >
50
- <div
51
- ref="panelRef"
52
- class="vm-color-picker__panel vm-dialog-float"
53
- :style="panelStyle"
54
- @pointerdown.stop
55
- @mousedown.stop
56
- >
57
- <div class="vm-color-picker__toolbar">
58
- <div
59
- v-if="hasThemeOptions"
60
- class="vm-color-picker__modes"
61
- role="tablist"
62
- >
63
- <button
64
- type="button"
65
- class="vm-color-picker__mode"
66
- :class="{ 'is-on': panelMode === 'theme' }"
67
- role="tab"
68
- @click="panelMode = 'theme'"
69
- >
70
- {{ crudLabel('theme') }}
71
- </button>
72
- <button
73
- type="button"
74
- class="vm-color-picker__mode"
75
- :class="{ 'is-on': panelMode === 'picker' }"
76
- role="tab"
77
- @click="panelMode = 'picker'"
78
- >
79
- {{ crudLabel('colorPickerTab') }}
80
- </button>
81
- </div>
82
- <span v-else class="vm-color-picker__toolbar-spacer" />
83
- <button
84
- type="button"
85
- class="vm-color-picker__close"
86
- :title="crudLabel('dismiss')"
87
- :aria-label="crudLabel('dismiss')"
88
- @click="closePanel"
89
- >
90
- <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
91
- <path
92
- fill="currentColor"
93
- d="M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"
94
- />
95
- </svg>
96
- </button>
97
- </div>
98
-
99
- <div v-if="panelMode === 'theme' && hasThemeOptions" class="vm-color-picker__theme">
100
- <button
101
- v-for="opt in themeOptions"
102
- :key="opt.key"
103
- type="button"
104
- class="vm-color-picker__theme-item"
105
- :class="{ 'is-on': themeBoundKey === opt.key }"
106
- @click="pickTheme(opt.key)"
107
- >
108
- <span
109
- class="vm-color-picker__theme-swatch"
110
- :style="{ background: opt.value }"
111
- />
112
- <span class="vm-color-picker__theme-meta">
113
- <span class="vm-color-picker__theme-label">{{ opt.label }}</span>
114
- <span class="vm-color-picker__theme-key">{{ opt.key }}</span>
115
- </span>
116
- </button>
117
- </div>
118
-
119
- <template v-else>
120
- <div
121
- ref="svRef"
122
- class="vm-color-picker__sv"
123
- :style="{ background: svBg }"
124
- @pointerdown="onSvPointer"
125
- >
126
- <span
127
- class="vm-color-picker__sv-cursor"
128
- :style="svCursorStyle"
129
- />
130
- </div>
131
-
132
- <div class="vm-color-picker__sliders">
133
- <button
134
- v-if="hasEyeDropper"
135
- type="button"
136
- class="vm-color-picker__eyedropper"
137
- :title="crudLabel('eyedropper')"
138
- @click="pickEyeDropper"
139
- >
140
- <!-- 内联 SVG:不依赖 remixicon 字体是否已加载 -->
141
- <svg
142
- class="vm-color-picker__eyedropper-ico"
143
- viewBox="0 0 24 24"
144
- width="16"
145
- height="16"
146
- aria-hidden="true"
147
- >
148
- <path
149
- fill="currentColor"
150
- d="M19.07 3.66a2.5 2.5 0 0 1 0 3.54l-1.42 1.41 2.12 2.12-1.41 1.42-2.12-2.13-7.07 7.07c-.3.3-.68.48-1.08.54l-3.36.48a.75.75 0 0 1-.84-.84l.48-3.36c.06-.4.24-.78.54-1.08l7.07-7.07-1.41-1.41 1.41-1.42 2.12 2.13 1.42-1.42a2.5 2.5 0 0 1 3.54 0Zm-2.12 2.12.71-.7a1 1 0 0 0-1.42-1.42l-.7.71 1.41 1.41ZM8.48 15.9l-.17.17-.28 1.9 1.9-.28.17-.17 5.66-5.66-1.62-1.62-5.66 5.66Z"
151
- />
152
- </svg>
153
- </button>
154
- <div class="vm-color-picker__slider-col">
155
- <div
156
- ref="hueRef"
157
- class="vm-color-picker__hue"
158
- @pointerdown="onHuePointer"
159
- >
160
- <span
161
- class="vm-color-picker__slider-thumb"
162
- :style="{ left: thumbLeft(hue / 360) }"
163
- />
164
- </div>
165
- <div
166
- ref="alphaRef"
167
- class="vm-color-picker__alpha"
168
- @pointerdown="onAlphaPointer"
169
- >
170
- <span
171
- class="vm-color-picker__alpha-fill"
172
- :style="{ background: alphaBg }"
173
- />
174
- <span
175
- class="vm-color-picker__slider-thumb"
176
- :style="{ left: thumbLeft(alpha) }"
177
- />
178
- </div>
179
- </div>
180
- </div>
181
-
182
- <div class="vm-color-picker__footer">
183
- <div class="vm-color-picker__format-wrap">
184
- <button
185
- type="button"
186
- class="vm-color-picker__format"
187
- :class="{ 'is-open': formatOpen }"
188
- :title="crudLabel('colorFormat')"
189
- @click.stop="toggleFormatMenu"
190
- >
191
- {{ formatLabel }}
192
- <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
193
- <path fill="currentColor" d="M12 15.4 6.6 10l1.4-1.4 4 4 4-4L17.4 10 12 15.4Z" />
194
- </svg>
195
- </button>
196
- <div v-if="formatOpen" class="vm-color-picker__format-menu" role="listbox">
197
- <button
198
- v-for="opt in FORMAT_OPTS"
199
- :key="opt.id"
200
- type="button"
201
- class="vm-color-picker__format-item"
202
- :class="{ 'is-active': colorFormat === opt.id }"
203
- role="option"
204
- :aria-selected="colorFormat === opt.id"
205
- @click.stop="setFormat(opt.id)"
206
- >
207
- <span class="vm-color-picker__format-check" aria-hidden="true">
208
- <svg v-if="colorFormat === opt.id" viewBox="0 0 24 24" width="14" height="14">
209
- <path
210
- fill="currentColor"
211
- d="M9.55 17.6 4.9 12.95l1.4-1.4 3.25 3.25 7.15-7.15 1.4 1.4-8.55 8.55Z"
212
- />
213
- </svg>
214
- </span>
215
- {{ opt.label }}
216
- </button>
217
- </div>
218
- </div>
219
- <input
220
- type="text"
221
- class="vm-color-picker__value"
222
- :value="panelValue"
223
- spellcheck="false"
224
- autocomplete="off"
225
- @input="onPanelValueInput"
226
- @blur="onPanelValueBlur"
227
- @keydown.esc.stop="onEscEnd"
228
- />
229
- <input
230
- type="text"
231
- class="vm-color-picker__alpha-pct"
232
- :value="alphaPctText"
233
- spellcheck="false"
234
- autocomplete="off"
235
- @input="onAlphaPctInput"
236
- @blur="onAlphaPctBlur"
237
- @keydown.esc.stop="onEscEnd"
238
- />
239
- </div>
240
- </template>
241
- </div>
242
- </FocusScope>
243
- </Teleport>
244
- </div>
245
- </template>
246
-
247
- <script setup lang="ts">
248
- import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
249
- import { FocusScope } from 'reka-ui'
250
- import { POPUP_TRIGGER_GAP } from '../../lib/dialog-float'
251
- import {
252
- ADMIN_THEME_COLOR_DEFS,
253
- resolveCssThemeColorOptions,
254
- type VmColorThemeOption,
255
- } from '../../lib/theme-color-options'
256
- import { crudLabel, getCrudConfig } from '../style'
257
-
258
- defineOptions({ name: 'vm-color-picker' })
259
-
260
- export type { VmColorThemeOption } from '../../lib/theme-color-options'
261
-
262
- const props = withDefaults(
263
- defineProps<{
264
- modelValue?: string | null
265
- disabled?: boolean
266
- placeholder?: string
267
- size?: 'default' | 'sm'
268
- themeOptions?: VmColorThemeOption[]
269
- /** 只显示色块触发器(工具栏等窄位) */
270
- swatchOnly?: boolean
271
- /** 为 false 时不展示主题色(正文 HTML 勿写 CSS 变量) */
272
- enableTheme?: boolean
273
- }>(),
274
- {
275
- placeholder: '#000000',
276
- size: 'default',
277
- themeOptions: () => [],
278
- swatchOnly: false,
279
- enableTheme: true,
280
- },
281
- )
282
-
283
- const emit = defineEmits<{
284
- 'update:modelValue': [v: string]
285
- change: [v: string]
286
- }>()
287
-
288
- type Rgba = { r: number; g: number; b: number; a: number }
289
- type Hsv = { h: number; s: number; v: number }
290
- type ColorFormat = 'hex' | 'rgb' | 'css' | 'hsl' | 'hsb'
291
-
292
- function parseCssVarRef(raw: string): string | null {
293
- const s = String(raw || '').trim()
294
- const m = s.match(/^var\(\s*--([a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)$/i)
295
- return m ? m[1]! : null
296
- }
297
-
298
- function toCssVarRef(key: string): string {
299
- const k = String(key || '')
300
- .trim()
301
- .replace(/^--/, '')
302
- return k ? `var(--${k})` : ''
303
- }
304
-
305
- const FORMAT_OPTS: { id: ColorFormat; label: string }[] = [
306
- { id: 'hex', label: 'Hex' },
307
- { id: 'rgb', label: 'RGB' },
308
- { id: 'css', label: 'CSS' },
309
- { id: 'hsl', label: 'HSL' },
310
- { id: 'hsb', label: 'HSB' },
311
- ]
312
-
313
- const THUMB_PX = 14
314
-
315
- const rootRef = ref<HTMLElement | null>(null)
316
- const triggerRef = ref<HTMLElement | null>(null)
317
- const panelRef = ref<HTMLElement | null>(null)
318
- const svRef = ref<HTMLElement | null>(null)
319
- const hueRef = ref<HTMLElement | null>(null)
320
- const alphaRef = ref<HTMLElement | null>(null)
321
-
322
- const open = ref(false)
323
- const suppressOpen = ref(false)
324
- const panelStyle = ref<Record<string, string>>({})
325
- const formatOpen = ref(false)
326
- const colorFormat = ref<ColorFormat>('hex')
327
- /** 面板:主题变量 | 取色器 */
328
- const panelMode = ref<'theme' | 'picker'>('picker')
329
-
330
- const hue = ref(0)
331
- const sat = ref(0)
332
- const val = ref(0)
333
- const alpha = ref(1)
334
-
335
- /** 行内 Hex(不含 #);面板按当前格式展示 */
336
- const inlineHex = ref('')
337
- const panelValue = ref('')
338
- const alphaPctText = ref('100%')
339
-
340
- const hasEyeDropper = ref(false)
341
-
342
- const themeOptions = computed(() => {
343
- if (props.enableTheme === false) return []
344
- if (props.themeOptions?.length) return props.themeOptions
345
- // open 变化时重读 CSS 变量(亮暗切换后预览色同步)
346
- void open.value
347
- const fromConfig = getCrudConfig().listThemeColorOptions?.()
348
- if (fromConfig?.length) return fromConfig
349
- return resolveCssThemeColorOptions(ADMIN_THEME_COLOR_DEFS)
350
- })
351
- const hasThemeOptions = computed(() => themeOptions.value.length > 0)
352
- const themeBoundKey = computed(() => parseCssVarRef(props.modelValue ?? ''))
353
- const isThemeBound = computed(() => Boolean(themeBoundKey.value))
354
- const themeBoundOpt = computed(() => {
355
- const k = themeBoundKey.value
356
- if (!k) return null
357
- return themeOptions.value.find((o) => o.key === k) || null
358
- })
359
- const themeBoundLabel = computed(() => {
360
- const opt = themeBoundOpt.value
361
- if (opt) return opt.label
362
- const k = themeBoundKey.value
363
- return k ? k : ''
364
- })
365
- const inlineDisplay = computed(() => {
366
- if (isThemeBound.value) {
367
- return String(props.modelValue || '').trim() || themeBoundLabel.value
368
- }
369
- return inlineHex.value
370
- })
371
-
372
- /** 行内:主题变量 / mustache,不当作 hex 强行加 # */
373
- function isInlineExpr(raw: string): boolean {
374
- const s = String(raw || '').trim()
375
- if (!s) return false
376
- if (/\{\{[\s\S]*?\}\}/.test(s)) return true
377
- if (parseCssVarRef(s)) return true
378
- if (/^var\s*\(/i.test(s)) return true
379
- return false
380
- }
381
-
382
- const placeholderText = computed(() => {
383
- const p = props.placeholder || '#000000'
384
- return p.startsWith('#') ? p.slice(1) : p
385
- })
386
-
387
- const formatLabel = computed(
388
- () => FORMAT_OPTS.find((o) => o.id === colorFormat.value)?.label ?? 'Hex',
389
- )
390
-
391
- function clamp(n: number, min: number, max: number) {
392
- return Math.min(max, Math.max(min, n))
393
- }
394
-
395
- /** 拇指中心落在轨道内,两端不溢出 */
396
- function thumbLeft(ratio: number) {
397
- const t = clamp(ratio, 0, 1)
398
- return `calc(${t} * (100% - ${THUMB_PX}px) + ${THUMB_PX / 2}px)`
399
- }
400
-
401
- function toHex2(n: number) {
402
- return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0')
403
- }
404
-
405
- function rgbToHex(r: number, g: number, b: number, a = 1): string {
406
- const base = `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`
407
- if (a >= 1 - 1e-6) return base
408
- return `${base}${toHex2(a * 255)}`
409
- }
410
-
411
- function hsvToRgb(h: number, s: number, v: number): { r: number; g: number; b: number } {
412
- const hh = ((h % 360) + 360) % 360
413
- const c = v * s
414
- const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
415
- const m = v - c
416
- let rp = 0
417
- let gp = 0
418
- let bp = 0
419
- if (hh < 60) {
420
- rp = c
421
- gp = x
422
- } else if (hh < 120) {
423
- rp = x
424
- gp = c
425
- } else if (hh < 180) {
426
- gp = c
427
- bp = x
428
- } else if (hh < 240) {
429
- gp = x
430
- bp = c
431
- } else if (hh < 300) {
432
- rp = x
433
- bp = c
434
- } else {
435
- rp = c
436
- bp = x
437
- }
438
- return {
439
- r: (rp + m) * 255,
440
- g: (gp + m) * 255,
441
- b: (bp + m) * 255,
442
- }
443
- }
444
-
445
- function rgbToHsv(r: number, g: number, b: number): Hsv {
446
- const rr = r / 255
447
- const gg = g / 255
448
- const bb = b / 255
449
- const max = Math.max(rr, gg, bb)
450
- const min = Math.min(rr, gg, bb)
451
- const d = max - min
452
- let h = 0
453
- if (d !== 0) {
454
- if (max === rr) h = ((gg - bb) / d) % 6
455
- else if (max === gg) h = (bb - rr) / d + 2
456
- else h = (rr - gg) / d + 4
457
- h *= 60
458
- if (h < 0) h += 360
459
- }
460
- const s = max === 0 ? 0 : d / max
461
- return { h, s, v: max }
462
- }
463
-
464
- function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
465
- const rr = r / 255
466
- const gg = g / 255
467
- const bb = b / 255
468
- const max = Math.max(rr, gg, bb)
469
- const min = Math.min(rr, gg, bb)
470
- const l = (max + min) / 2
471
- const d = max - min
472
- let h = 0
473
- let s = 0
474
- if (d !== 0) {
475
- s = d / (1 - Math.abs(2 * l - 1))
476
- if (max === rr) h = ((gg - bb) / d) % 6
477
- else if (max === gg) h = (bb - rr) / d + 2
478
- else h = (rr - gg) / d + 4
479
- h *= 60
480
- if (h < 0) h += 360
481
- }
482
- return { h, s, l }
483
- }
484
-
485
- function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
486
- const hh = ((h % 360) + 360) % 360
487
- const c = (1 - Math.abs(2 * l - 1)) * s
488
- const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
489
- const m = l - c / 2
490
- let rp = 0
491
- let gp = 0
492
- let bp = 0
493
- if (hh < 60) {
494
- rp = c
495
- gp = x
496
- } else if (hh < 120) {
497
- rp = x
498
- gp = c
499
- } else if (hh < 180) {
500
- gp = c
501
- bp = x
502
- } else if (hh < 240) {
503
- gp = x
504
- bp = c
505
- } else if (hh < 300) {
506
- rp = x
507
- bp = c
508
- } else {
509
- rp = c
510
- bp = x
511
- }
512
- return {
513
- r: (rp + m) * 255,
514
- g: (gp + m) * 255,
515
- b: (bp + m) * 255,
516
- }
517
- }
518
-
519
- /** 解析为 rgba;无效返回 null */
520
- function parseColor(raw: string): Rgba | null {
521
- const s = raw.trim()
522
- if (!s) return null
523
-
524
- if (s.startsWith('#')) {
525
- let h = s.slice(1)
526
- if (/^[0-9a-fA-F]{3}$/.test(h)) {
527
- h = h
528
- .split('')
529
- .map((c) => c + c)
530
- .join('')
531
- }
532
- if (/^[0-9a-fA-F]{6}$/.test(h)) {
533
- return {
534
- r: parseInt(h.slice(0, 2), 16),
535
- g: parseInt(h.slice(2, 4), 16),
536
- b: parseInt(h.slice(4, 6), 16),
537
- a: 1,
538
- }
539
- }
540
- if (/^[0-9a-fA-F]{8}$/.test(h)) {
541
- return {
542
- r: parseInt(h.slice(0, 2), 16),
543
- g: parseInt(h.slice(2, 4), 16),
544
- b: parseInt(h.slice(4, 6), 16),
545
- a: parseInt(h.slice(6, 8), 16) / 255,
546
- }
547
- }
548
- return null
549
- }
550
-
551
- if (/^[0-9a-fA-F]{3}$/.test(s) || /^[0-9a-fA-F]{6}$/.test(s) || /^[0-9a-fA-F]{8}$/.test(s)) {
552
- return parseColor(`#${s}`)
553
- }
554
-
555
- const m = s.match(
556
- /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i,
557
- )
558
- if (m) {
559
- return {
560
- r: clamp(Number(m[1]), 0, 255),
561
- g: clamp(Number(m[2]), 0, 255),
562
- b: clamp(Number(m[3]), 0, 255),
563
- a: m[4] != null ? clamp(Number(m[4]), 0, 1) : 1,
564
- }
565
- }
566
-
567
- const hsl = s.match(
568
- /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/i,
569
- )
570
- if (hsl) {
571
- const rgb = hslToRgb(Number(hsl[1]), Number(hsl[2]) / 100, Number(hsl[3]) / 100)
572
- return {
573
- r: rgb.r,
574
- g: rgb.g,
575
- b: rgb.b,
576
- a: hsl[4] != null ? clamp(Number(hsl[4]), 0, 1) : 1,
577
- }
578
- }
579
-
580
- return null
581
- }
582
-
583
- /** 按当前格式解析面板中间输入(不改变 alpha,除非 CSS/rgba 自带) */
584
- function parsePanelValue(raw: string, keepAlpha: number): Rgba | null {
585
- const s = raw.trim()
586
- if (!s) return null
587
- const fmt = colorFormat.value
588
-
589
- if (fmt === 'hex' || fmt === 'css') {
590
- const parsed = parseColor(s)
591
- if (!parsed) return null
592
- if (fmt === 'hex') parsed.a = keepAlpha
593
- return parsed
594
- }
595
-
596
- if (fmt === 'rgb') {
597
- const parts = s.split(/[,\s]+/).filter(Boolean)
598
- if (parts.length < 3) return null
599
- const r = Number(parts[0])
600
- const g = Number(parts[1])
601
- const b = Number(parts[2])
602
- if (![r, g, b].every(Number.isFinite)) return null
603
- return { r: clamp(r, 0, 255), g: clamp(g, 0, 255), b: clamp(b, 0, 255), a: keepAlpha }
604
- }
605
-
606
- if (fmt === 'hsl') {
607
- const parts = s.replace(/%/g, '').split(/[,\s]+/).filter(Boolean)
608
- if (parts.length < 3) return null
609
- const h = Number(parts[0])
610
- const satPct = Number(parts[1])
611
- const lPct = Number(parts[2])
612
- if (![h, satPct, lPct].every(Number.isFinite)) return null
613
- const rgb = hslToRgb(h, clamp(satPct, 0, 100) / 100, clamp(lPct, 0, 100) / 100)
614
- return { ...rgb, a: keepAlpha }
615
- }
616
-
617
- if (fmt === 'hsb') {
618
- const parts = s.replace(/%/g, '').split(/[,\s]+/).filter(Boolean)
619
- if (parts.length < 3) return null
620
- const h = Number(parts[0])
621
- const sPct = Number(parts[1])
622
- const bPct = Number(parts[2])
623
- if (![h, sPct, bPct].every(Number.isFinite)) return null
624
- const rgb = hsvToRgb(h, clamp(sPct, 0, 100) / 100, clamp(bPct, 0, 100) / 100)
625
- return { ...rgb, a: keepAlpha }
626
- }
627
-
628
- return null
629
- }
630
-
631
- function currentRgb() {
632
- return hsvToRgb(hue.value, sat.value, val.value)
633
- }
634
-
635
- function currentHex(): string {
636
- const { r, g, b } = currentRgb()
637
- return rgbToHex(r, g, b, alpha.value)
638
- }
639
-
640
- function formatPanelValue(): string {
641
- const { r, g, b } = currentRgb()
642
- const rr = Math.round(r)
643
- const gg = Math.round(g)
644
- const bb = Math.round(b)
645
- const a = alpha.value
646
- switch (colorFormat.value) {
647
- case 'rgb':
648
- return `${rr}, ${gg}, ${bb}`
649
- case 'css':
650
- return a < 1 - 1e-6
651
- ? `rgba(${rr}, ${gg}, ${bb}, ${Math.round(a * 100) / 100})`
652
- : `#${toHex2(rr)}${toHex2(gg)}${toHex2(bb)}`
653
- case 'hsl': {
654
- const hsl = rgbToHsl(rr, gg, bb)
655
- return `${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%`
656
- }
657
- case 'hsb':
658
- return `${Math.round(hue.value)}, ${Math.round(sat.value * 100)}%, ${Math.round(val.value * 100)}%`
659
- case 'hex':
660
- default:
661
- return `${toHex2(rr)}${toHex2(gg)}${toHex2(bb)}`
662
- }
663
- }
664
-
665
- const cssColor = computed(() => {
666
- const themePreview = themeBoundOpt.value?.value
667
- if (themePreview) {
668
- const parsed = parseColor(themePreview)
669
- if (parsed) {
670
- const { r, g, b, a } = parsed
671
- return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`
672
- }
673
- return String(themePreview)
674
- }
675
- if (themeBoundKey.value) {
676
- return `var(--${themeBoundKey.value})`
677
- }
678
- const raw = props.modelValue ?? ''
679
- const parsed = parseColor(raw)
680
- if (!parsed && !String(raw).trim()) return 'transparent'
681
- if (!parsed) return 'transparent'
682
- const { r, g, b, a } = parsed
683
- return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`
684
- })
685
-
686
- function pickTheme(key: string) {
687
- const ref = toCssVarRef(key)
688
- if (!ref) return
689
- commit(ref)
690
- closePanel()
691
- }
692
-
693
- const svBg = computed(() => {
694
- const { r, g, b } = hsvToRgb(hue.value, 1, 1)
695
- return `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)}))`
696
- })
697
-
698
- const alphaBg = computed(() => {
699
- const { r, g, b } = currentRgb()
700
- const c = `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`
701
- return `linear-gradient(to right, transparent, ${c})`
702
- })
703
-
704
- const svCursorStyle = computed(() => ({
705
- left: `${sat.value * 100}%`,
706
- top: `${(1 - val.value) * 100}%`,
707
- }))
708
-
709
- function syncTextsFromState() {
710
- const hex = currentHex()
711
- const bare = hex.startsWith('#') ? hex.slice(1) : hex
712
- inlineHex.value = bare.slice(0, 6)
713
- panelValue.value = formatPanelValue()
714
- alphaPctText.value = `${Math.round(alpha.value * 100)}%`
715
- }
716
-
717
- function applyRgba(c: Rgba, emitValue = true) {
718
- const hsv = rgbToHsv(c.r, c.g, c.b)
719
- hue.value = hsv.h
720
- sat.value = hsv.s
721
- val.value = hsv.v
722
- alpha.value = clamp(c.a, 0, 1)
723
- syncTextsFromState()
724
- if (emitValue) commit(currentHex())
725
- }
726
-
727
- /** 3/4 位短 hex:可预览,但行内禁止展开成 6/8 位 */
728
- function hexBodyOf(raw: string) {
729
- return String(raw || '')
730
- .trim()
731
- .replace(/^#/, '')
732
- }
733
-
734
- function isShortHexBody(body: string) {
735
- return /^[0-9a-fA-F]{3}$/.test(body) || /^[0-9a-fA-F]{4}$/.test(body)
736
- }
737
-
738
- function applyHexPreview(body: string) {
739
- const parsed = parseColor(`#${body}`)
740
- if (!parsed) return
741
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
742
- hue.value = hsv.h
743
- sat.value = hsv.s
744
- val.value = hsv.v
745
- if (body.length === 4 || body.length === 8) alpha.value = parsed.a
746
- }
747
-
748
- function loadFromModel(raw: string | null | undefined) {
749
- const s = raw ?? ''
750
- const varKey = parseCssVarRef(s)
751
- if (varKey) {
752
- const opt = themeOptions.value.find((o) => o.key === varKey)
753
- const preview = opt?.value || ''
754
- if (preview) {
755
- const parsed = parseColor(preview)
756
- if (parsed) {
757
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
758
- hue.value = hsv.h
759
- sat.value = hsv.s
760
- val.value = hsv.v
761
- alpha.value = parsed.a
762
- syncTextsFromState()
763
- return
764
- }
765
- }
766
- inlineHex.value = varKey
767
- panelValue.value = varKey
768
- return
769
- }
770
- if (!s.trim()) {
771
- hue.value = 0
772
- sat.value = 0
773
- val.value = 0
774
- alpha.value = 1
775
- inlineHex.value = ''
776
- panelValue.value = ''
777
- alphaPctText.value = '100%'
778
- return
779
- }
780
- const body = hexBodyOf(s)
781
- // 短 hex / 未满 6 位:保留位数,勿 sync 成 6 位
782
- if (/^[0-9a-fA-F]+$/.test(body) && body.length !== 6 && body.length !== 8) {
783
- if (isShortHexBody(body)) applyHexPreview(body)
784
- inlineHex.value = body
785
- panelValue.value = body
786
- alphaPctText.value = `${Math.round(alpha.value * 100)}%`
787
- return
788
- }
789
- const parsed = parseColor(s)
790
- if (!parsed) {
791
- inlineHex.value = s.startsWith('#') ? s.slice(1) : s
792
- return
793
- }
794
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
795
- hue.value = hsv.h
796
- sat.value = hsv.s
797
- val.value = hsv.v
798
- alpha.value = parsed.a
799
- syncTextsFromState()
800
- }
801
-
802
- watch(
803
- () => props.modelValue,
804
- (v) => {
805
- if (open.value) {
806
- const cur = currentHex()
807
- const parsed = parseColor(v ?? '')
808
- if (parsed && rgbToHex(parsed.r, parsed.g, parsed.b, parsed.a) === cur) return
809
- }
810
- loadFromModel(v)
811
- },
812
- { immediate: true },
813
- )
814
-
815
- function commit(v: string) {
816
- emit('update:modelValue', v)
817
- emit('change', v)
818
- }
819
-
820
- function commitFromState() {
821
- syncTextsFromState()
822
- commit(currentHex())
823
- }
824
-
825
- function onInlineInput(e: Event) {
826
- const rawInput = (e.target as HTMLInputElement).value
827
- if (isInlineExpr(rawInput)) {
828
- inlineHex.value = rawInput
829
- commit(rawInput.trim())
830
- return
831
- }
832
- const raw = hexBodyOf(rawInput)
833
- inlineHex.value = raw
834
- if (!raw) {
835
- commit('')
836
- return
837
- }
838
- // 纯 hex:仅 6/8 位规范化;3 位等中间态原样写入,禁止 3→6 自动补齐
839
- if (/^[0-9a-fA-F]+$/.test(raw)) {
840
- if (raw.length === 6 || raw.length === 8) {
841
- const parsed = parseColor(`#${raw}`)
842
- if (parsed) {
843
- if (raw.length === 6) parsed.a = alpha.value
844
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
845
- hue.value = hsv.h
846
- sat.value = hsv.s
847
- val.value = hsv.v
848
- if (raw.length === 8) alpha.value = parsed.a
849
- panelValue.value = formatPanelValue()
850
- commit(currentHex())
851
- return
852
- }
853
- }
854
- if (isShortHexBody(raw)) applyHexPreview(raw)
855
- commit(`#${raw}`)
856
- return
857
- }
858
- const parsed = parseColor(rawInput)
859
- if (parsed) {
860
- parsed.a = alpha.value
861
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
862
- hue.value = hsv.h
863
- sat.value = hsv.s
864
- val.value = hsv.v
865
- panelValue.value = formatPanelValue()
866
- commit(currentHex())
867
- return
868
- }
869
- commit(raw)
870
- }
871
-
872
- function onInlineBlur() {
873
- const t = inlineHex.value.trim()
874
- if (!t) {
875
- commit('')
876
- hue.value = 0
877
- sat.value = 0
878
- val.value = 0
879
- alpha.value = 1
880
- inlineHex.value = ''
881
- panelValue.value = ''
882
- alphaPctText.value = '100%'
883
- return
884
- }
885
- if (isInlineExpr(t)) {
886
- commit(t)
887
- return
888
- }
889
- const body = hexBodyOf(t)
890
- if (/^[0-9a-fA-F]+$/.test(body)) {
891
- if (body.length === 6 || body.length === 8) {
892
- const parsed = parseColor(`#${body}`)
893
- if (parsed) {
894
- if (body.length === 6) parsed.a = alpha.value
895
- applyRgba(parsed, true)
896
- return
897
- }
898
- }
899
- if (isShortHexBody(body)) applyHexPreview(body)
900
- inlineHex.value = body
901
- commit(`#${body}`)
902
- return
903
- }
904
- const parsed = parseColor(t)
905
- if (parsed) {
906
- parsed.a = alpha.value
907
- applyRgba(parsed, true)
908
- return
909
- }
910
- loadFromModel(props.modelValue)
911
- }
912
-
913
- function onPanelValueInput(e: Event) {
914
- panelValue.value = (e.target as HTMLInputElement).value
915
- const parsed = parsePanelValue(panelValue.value, alpha.value)
916
- if (!parsed) return
917
- const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
918
- hue.value = hsv.h
919
- sat.value = hsv.s
920
- val.value = hsv.v
921
- if (colorFormat.value === 'css') alpha.value = parsed.a
922
- const hex = currentHex()
923
- inlineHex.value = hex.replace(/^#/, '').slice(0, 6)
924
- alphaPctText.value = `${Math.round(alpha.value * 100)}%`
925
- commit(hex)
926
- }
927
-
928
- function onPanelValueBlur() {
929
- const parsed = parsePanelValue(panelValue.value, alpha.value)
930
- if (parsed) {
931
- if (colorFormat.value !== 'css') parsed.a = alpha.value
932
- applyRgba(parsed, true)
933
- } else {
934
- syncTextsFromState()
935
- }
936
- }
937
-
938
- function onAlphaPctInput(e: Event) {
939
- alphaPctText.value = (e.target as HTMLInputElement).value
940
- const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
941
- if (!Number.isFinite(n)) return
942
- alpha.value = clamp(n / 100, 0, 1)
943
- commitFromState()
944
- }
945
-
946
- function onAlphaPctBlur() {
947
- const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
948
- if (Number.isFinite(n)) {
949
- alpha.value = clamp(n / 100, 0, 1)
950
- }
951
- syncTextsFromState()
952
- commit(currentHex())
953
- }
954
-
955
- function toggleFormatMenu() {
956
- formatOpen.value = !formatOpen.value
957
- }
958
-
959
- function setFormat(id: ColorFormat) {
960
- colorFormat.value = id
961
- formatOpen.value = false
962
- syncTextsFromState()
963
- }
964
-
965
- function ratioFromEvent(el: HTMLElement, e: PointerEvent, axis: 'x' | 'y' | 'both') {
966
- const r = el.getBoundingClientRect()
967
- const x = clamp((e.clientX - r.left) / r.width, 0, 1)
968
- const y = clamp((e.clientY - r.top) / r.height, 0, 1)
969
- if (axis === 'x') return { x, y: 0 }
970
- if (axis === 'y') return { x: 0, y }
971
- return { x, y }
972
- }
973
-
974
- function bindDrag(
975
- el: HTMLElement,
976
- e: PointerEvent,
977
- onMove: (ev: PointerEvent) => void,
978
- ) {
979
- e.preventDefault()
980
- el.setPointerCapture(e.pointerId)
981
- onMove(e)
982
- const move = (ev: PointerEvent) => onMove(ev)
983
- const up = (ev: PointerEvent) => {
984
- el.releasePointerCapture(ev.pointerId)
985
- el.removeEventListener('pointermove', move)
986
- el.removeEventListener('pointerup', up)
987
- el.removeEventListener('pointercancel', up)
988
- }
989
- el.addEventListener('pointermove', move)
990
- el.addEventListener('pointerup', up)
991
- el.addEventListener('pointercancel', up)
992
- }
993
-
994
- function onSvPointer(e: PointerEvent) {
995
- const el = svRef.value
996
- if (!el) return
997
- bindDrag(el, e, (ev) => {
998
- const { x, y } = ratioFromEvent(el, ev, 'both')
999
- sat.value = x
1000
- val.value = 1 - y
1001
- commitFromState()
1002
- })
1003
- }
1004
-
1005
- function onHuePointer(e: PointerEvent) {
1006
- const el = hueRef.value
1007
- if (!el) return
1008
- bindDrag(el, e, (ev) => {
1009
- const { x } = ratioFromEvent(el, ev, 'x')
1010
- hue.value = x * 360
1011
- commitFromState()
1012
- })
1013
- }
1014
-
1015
- function onAlphaPointer(e: PointerEvent) {
1016
- const el = alphaRef.value
1017
- if (!el) return
1018
- bindDrag(el, e, (ev) => {
1019
- const { x } = ratioFromEvent(el, ev, 'x')
1020
- alpha.value = x
1021
- commitFromState()
1022
- })
1023
- }
1024
-
1025
- async function pickEyeDropper() {
1026
- const ED = (
1027
- window as unknown as {
1028
- EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> }
1029
- }
1030
- ).EyeDropper
1031
- if (!ED) return
1032
- try {
1033
- const result = await new ED().open()
1034
- const parsed = parseColor(result.sRGBHex)
1035
- if (parsed) {
1036
- parsed.a = alpha.value
1037
- applyRgba(parsed, true)
1038
- }
1039
- } catch {
1040
- /* 用户取消 */
1041
- }
1042
- }
1043
-
1044
- function syncPanelPos() {
1045
- const el = triggerRef.value
1046
- if (!el) return
1047
- const r = el.getBoundingClientRect()
1048
- const width = 240
1049
- // 贴触发框左侧 + 底部
1050
- panelStyle.value = {
1051
- position: 'fixed',
1052
- top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
1053
- left: `${r.left}px`,
1054
- width: `${width}px`,
1055
- zIndex: '100',
1056
- }
1057
- }
1058
-
1059
- async function openPanel() {
1060
- if (props.disabled || suppressOpen.value) return
1061
- loadFromModel(props.modelValue)
1062
- formatOpen.value = false
1063
- if (hasThemeOptions.value) {
1064
- const bound = parseCssVarRef(props.modelValue ?? '')
1065
- const empty = !(props.modelValue ?? '').trim()
1066
- panelMode.value = bound || empty ? 'theme' : 'picker'
1067
- } else {
1068
- panelMode.value = 'picker'
1069
- }
1070
- open.value = true
1071
- await nextTick()
1072
- syncPanelPos()
1073
- }
1074
-
1075
- function onPanelUnmountAutoFocus(event: Event) {
1076
- event.preventDefault()
1077
- }
1078
-
1079
- function closePanel() {
1080
- if (!open.value) return
1081
- suppressOpen.value = true
1082
- formatOpen.value = false
1083
- open.value = false
1084
- nextTick(() => {
1085
- requestAnimationFrame(() => {
1086
- suppressOpen.value = false
1087
- })
1088
- })
1089
- }
1090
-
1091
- /** Esc:收起面板并失焦 */
1092
- function onEscEnd(e: KeyboardEvent) {
1093
- closePanel()
1094
- const el = e.target
1095
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) el.blur()
1096
- }
1097
-
1098
- async function toggle() {
1099
- if (props.disabled) return
1100
- if (open.value) {
1101
- closePanel()
1102
- return
1103
- }
1104
- await openPanel()
1105
- }
1106
-
1107
- function onDocPointer(e: Event) {
1108
- if (!open.value) return
1109
- const t = e.target as Node | null
1110
- if (!t) return
1111
- if (rootRef.value?.contains(t)) return
1112
- if (panelRef.value?.contains(t)) return
1113
- closePanel()
1114
- }
1115
-
1116
- function onWinChange() {
1117
- if (open.value) syncPanelPos()
1118
- }
1119
-
1120
- onMounted(() => {
1121
- hasEyeDropper.value = typeof window !== 'undefined' && 'EyeDropper' in window
1122
- document.addEventListener('pointerdown', onDocPointer, true)
1123
- window.addEventListener('resize', onWinChange)
1124
- window.addEventListener('scroll', onWinChange, true)
1125
- })
1126
-
1127
- onUnmounted(() => {
1128
- document.removeEventListener('pointerdown', onDocPointer, true)
1129
- window.removeEventListener('resize', onWinChange)
1130
- window.removeEventListener('scroll', onWinChange, true)
1131
- })
1132
- </script>
1133
-
1134
- <style lang="scss" scoped>
1135
- .vm-color-picker {
1136
- display: flex;
1137
- align-items: center;
1138
- gap: 8px;
1139
- width: 100%;
1140
- min-width: 0;
1141
-
1142
- &.is-swatch-only {
1143
- width: auto;
1144
- gap: 0;
1145
- }
1146
-
1147
- &.is-disabled {
1148
- opacity: 0.6;
1149
- pointer-events: none;
1150
- }
1151
- }
1152
-
1153
- .vm-color-picker__swatch {
1154
- position: relative;
1155
- flex-shrink: 0;
1156
- width: var(--vm-control-height);
1157
- height: var(--vm-control-height);
1158
- padding: 0;
1159
- border: 1px solid var(--border);
1160
- border-radius: var(--vm-control-radius);
1161
- background: var(--muted);
1162
- cursor: pointer;
1163
- overflow: hidden;
1164
-
1165
- outline: none;
1166
-
1167
- &:hover:not(:disabled) {
1168
- border-color: var(--brand, #4e5dff);
1169
- }
1170
-
1171
- /* 弹层触发:打开态不额外描边 */
1172
-
1173
- &:disabled {
1174
- cursor: not-allowed;
1175
- }
1176
- }
1177
-
1178
- .vm-color-picker.is-sm .vm-color-picker__swatch {
1179
- width: 16px;
1180
- height: 16px;
1181
- border-radius: 3px;
1182
- }
1183
-
1184
- .vm-color-picker__swatch-fill {
1185
- display: block;
1186
- width: 100%;
1187
- height: 100%;
1188
- background-image:
1189
- linear-gradient(45deg, #ccc 25%, transparent 25%),
1190
- linear-gradient(-45deg, #ccc 25%, transparent 25%),
1191
- linear-gradient(45deg, transparent 75%, #ccc 75%),
1192
- linear-gradient(-45deg, transparent 75%, #ccc 75%);
1193
- background-size: 8px 8px;
1194
- background-position:
1195
- 0 0,
1196
- 0 4px,
1197
- 4px -4px,
1198
- -4px 0;
1199
- }
1200
-
1201
- .vm-color-picker__input {
1202
- flex: 1;
1203
- min-width: 0;
1204
- height: var(--vm-control-height);
1205
- padding: 0 var(--vm-control-padding-x);
1206
- border: 1px solid var(--border);
1207
- border-radius: var(--vm-control-radius);
1208
- background: var(--muted);
1209
- color: var(--foreground);
1210
- font-size: 13px;
1211
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1212
- outline: none;
1213
-
1214
- /* 弹层触发旁输入:不展示聚焦描边 */
1215
-
1216
- &::placeholder {
1217
- color: var(--muted-foreground);
1218
- }
1219
- }
1220
-
1221
- .vm-color-picker.is-sm .vm-color-picker__input {
1222
- height: 24px;
1223
- padding: 0 6px;
1224
- font-size: 12px;
1225
- border-radius: 4px;
1226
- }
1227
- </style>
1228
-
1229
- <style lang="scss">
1230
- /* Teleport 到 body,不可 scoped */
1231
- .vm-color-picker__panel {
1232
- box-sizing: border-box;
1233
- padding: 12px;
1234
- border: 1px solid var(--border);
1235
- border-radius: 8px;
1236
- background: var(--popover, var(--background, #fff));
1237
- color: var(--foreground);
1238
- box-shadow:
1239
- 0 4px 6px -1px rgb(0 0 0 / 10%),
1240
- 0 10px 24px -4px rgb(0 0 0 / 12%);
1241
- user-select: none;
1242
- overflow: visible;
1243
- }
1244
-
1245
- .vm-color-picker__toolbar {
1246
- display: flex;
1247
- align-items: center;
1248
- gap: 8px;
1249
- margin-bottom: 10px;
1250
- }
1251
-
1252
- .vm-color-picker__toolbar-spacer {
1253
- flex: 1;
1254
- min-width: 0;
1255
- }
1256
-
1257
- .vm-color-picker__close {
1258
- display: inline-flex;
1259
- flex-shrink: 0;
1260
- align-items: center;
1261
- justify-content: center;
1262
- width: 28px;
1263
- height: 28px;
1264
- margin: 0;
1265
- padding: 0;
1266
- border: 0;
1267
- border-radius: 6px;
1268
- background: transparent;
1269
- color: var(--muted-foreground, #9aa0b0);
1270
- cursor: pointer;
1271
-
1272
- &:hover {
1273
- background: var(--muted, #f4f6fc);
1274
- color: var(--foreground);
1275
- }
1276
- }
1277
-
1278
- .vm-color-picker__modes {
1279
- display: grid;
1280
- flex: 1;
1281
- min-width: 0;
1282
- grid-template-columns: 1fr 1fr;
1283
- gap: 4px;
1284
- margin: 0;
1285
- padding: 3px;
1286
- border-radius: 8px;
1287
- background: var(--muted, #f4f6fc);
1288
- }
1289
-
1290
- .vm-color-picker__mode {
1291
- height: 28px;
1292
- border: 0;
1293
- border-radius: 6px;
1294
- background: transparent;
1295
- color: var(--muted-foreground, #9aa0b0);
1296
- font-size: 12px;
1297
- font-weight: 600;
1298
- cursor: pointer;
1299
-
1300
- &.is-on {
1301
- background: var(--card, #fff);
1302
- color: var(--foreground);
1303
- box-shadow: 0 1px 2px rgb(0 0 0 / 6%);
1304
- }
1305
- }
1306
-
1307
- .vm-color-picker__theme {
1308
- display: flex;
1309
- flex-direction: column;
1310
- gap: 4px;
1311
- max-height: 280px;
1312
- overflow: auto;
1313
- margin: 0 -4px;
1314
- padding: 0 4px;
1315
- }
1316
-
1317
- .vm-color-picker__theme-item {
1318
- display: flex;
1319
- align-items: center;
1320
- gap: 10px;
1321
- width: 100%;
1322
- margin: 0;
1323
- padding: 8px;
1324
- border: 1px solid transparent;
1325
- border-radius: 8px;
1326
- background: transparent;
1327
- text-align: left;
1328
- cursor: pointer;
1329
- color: inherit;
1330
-
1331
- &:hover {
1332
- background: color-mix(in srgb, var(--brand, #4e5dff) 8%, transparent);
1333
- }
1334
-
1335
- &.is-on {
1336
- border-color: color-mix(in srgb, var(--brand, #4e5dff) 45%, transparent);
1337
- background: color-mix(in srgb, var(--brand, #4e5dff) 10%, transparent);
1338
- }
1339
- }
1340
-
1341
- .vm-color-picker__theme-swatch {
1342
- flex-shrink: 0;
1343
- width: 22px;
1344
- height: 22px;
1345
- border-radius: 6px;
1346
- border: 1px solid var(--border, #e8ebf5);
1347
- background-image:
1348
- linear-gradient(45deg, #ccc 25%, transparent 25%),
1349
- linear-gradient(-45deg, #ccc 25%, transparent 25%),
1350
- linear-gradient(45deg, transparent 75%, #ccc 75%),
1351
- linear-gradient(-45deg, transparent 75%, #ccc 75%);
1352
- background-size: 8px 8px;
1353
- background-position:
1354
- 0 0,
1355
- 0 4px,
1356
- 4px -4px,
1357
- -4px 0;
1358
- }
1359
-
1360
- .vm-color-picker__theme-meta {
1361
- min-width: 0;
1362
- display: flex;
1363
- flex-direction: column;
1364
- gap: 2px;
1365
- }
1366
-
1367
- .vm-color-picker__theme-label {
1368
- font-size: 13px;
1369
- font-weight: 600;
1370
- line-height: 1.2;
1371
- overflow: hidden;
1372
- text-overflow: ellipsis;
1373
- white-space: nowrap;
1374
- }
1375
-
1376
- .vm-color-picker__theme-key {
1377
- font-size: 11px;
1378
- color: var(--muted-foreground, #9aa0b0);
1379
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1380
- overflow: hidden;
1381
- text-overflow: ellipsis;
1382
- white-space: nowrap;
1383
- }
1384
-
1385
- .vm-color-picker__sv {
1386
- position: relative;
1387
- width: 100%;
1388
- aspect-ratio: 1;
1389
- border-radius: 6px;
1390
- cursor: crosshair;
1391
- touch-action: none;
1392
- overflow: hidden;
1393
- }
1394
-
1395
- .vm-color-picker__sv-cursor {
1396
- position: absolute;
1397
- width: 14px;
1398
- height: 14px;
1399
- margin: -7px 0 0 -7px;
1400
- border: 2px solid #fff;
1401
- border-radius: 50%;
1402
- box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
1403
- pointer-events: none;
1404
- }
1405
-
1406
- .vm-color-picker__sliders {
1407
- display: flex;
1408
- align-items: center;
1409
- gap: 10px;
1410
- margin-top: 12px;
1411
- }
1412
-
1413
- .vm-color-picker__eyedropper {
1414
- flex-shrink: 0;
1415
- display: inline-flex;
1416
- align-items: center;
1417
- justify-content: center;
1418
- width: 28px;
1419
- height: 28px;
1420
- padding: 0;
1421
- border: 1px solid var(--border);
1422
- border-radius: 6px;
1423
- background: var(--muted);
1424
- color: var(--foreground);
1425
- cursor: pointer;
1426
-
1427
- &:hover {
1428
- border-color: var(--brand, #4e5dff);
1429
- color: var(--brand, #4e5dff);
1430
- }
1431
- }
1432
-
1433
- .vm-color-picker__eyedropper-ico {
1434
- display: block;
1435
- }
1436
-
1437
- .vm-color-picker__slider-col {
1438
- flex: 1;
1439
- min-width: 0;
1440
- display: flex;
1441
- flex-direction: column;
1442
- gap: 8px;
1443
- }
1444
-
1445
- .vm-color-picker__hue,
1446
- .vm-color-picker__alpha {
1447
- position: relative;
1448
- height: 12px;
1449
- border-radius: 999px;
1450
- cursor: pointer;
1451
- touch-action: none;
1452
- /* 允许拇指画出轨道高度,但水平用 thumbLeft 钳在内 */
1453
- overflow: visible;
1454
- }
1455
-
1456
- .vm-color-picker__hue {
1457
- background: linear-gradient(
1458
- to right,
1459
- #f00 0%,
1460
- #ff0 17%,
1461
- #0f0 33%,
1462
- #0ff 50%,
1463
- #00f 67%,
1464
- #f0f 83%,
1465
- #f00 100%
1466
- );
1467
- }
1468
-
1469
- .vm-color-picker__alpha {
1470
- background-image:
1471
- linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
1472
- linear-gradient(-45deg, #c8c8c8 25%, transparent 25%),
1473
- linear-gradient(45deg, transparent 75%, #c8c8c8 75%),
1474
- linear-gradient(-45deg, transparent 75%, #c8c8c8 75%);
1475
- background-size: 8px 8px;
1476
- background-position:
1477
- 0 0,
1478
- 0 4px,
1479
- 4px -4px,
1480
- -4px 0;
1481
- background-color: #fff;
1482
- }
1483
-
1484
- .vm-color-picker__alpha-fill {
1485
- position: absolute;
1486
- inset: 0;
1487
- border-radius: inherit;
1488
- pointer-events: none;
1489
- overflow: hidden;
1490
- }
1491
-
1492
- .vm-color-picker__slider-thumb {
1493
- position: absolute;
1494
- top: 50%;
1495
- width: 14px;
1496
- height: 14px;
1497
- transform: translate(-50%, -50%);
1498
- border: 2px solid #fff;
1499
- border-radius: 50%;
1500
- background: transparent;
1501
- box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
1502
- pointer-events: none;
1503
- box-sizing: border-box;
1504
- }
1505
-
1506
- .vm-color-picker__footer {
1507
- position: relative;
1508
- display: flex;
1509
- align-items: center;
1510
- gap: 4px;
1511
- margin-top: 12px;
1512
- }
1513
-
1514
- .vm-color-picker__format-wrap {
1515
- position: relative;
1516
- flex: 0 0 auto;
1517
- max-width: 58px;
1518
- }
1519
-
1520
- .vm-color-picker__format {
1521
- display: inline-flex;
1522
- align-items: center;
1523
- justify-content: space-between;
1524
- gap: 0;
1525
- width: 100%;
1526
- height: 24px;
1527
- padding: 0 4px 0 6px;
1528
- border: 1px solid var(--border);
1529
- border-radius: 6px;
1530
- background: var(--muted);
1531
- color: var(--foreground);
1532
- font-size: 11px;
1533
- line-height: 1;
1534
- cursor: pointer;
1535
-
1536
- &:hover,
1537
- &.is-open {
1538
- border-color: var(--brand, #4e5dff);
1539
- }
1540
-
1541
- svg {
1542
- width: 12px;
1543
- height: 12px;
1544
- flex-shrink: 0;
1545
- opacity: 0.7;
1546
- }
1547
- }
1548
-
1549
- .vm-color-picker__format-menu {
1550
- position: absolute;
1551
- left: 0;
1552
- bottom: calc(100% + 4px);
1553
- z-index: 2;
1554
- min-width: 96px;
1555
- padding: 4px;
1556
- border: 1px solid var(--border);
1557
- border-radius: 8px;
1558
- background: var(--popover, var(--background, #fff));
1559
- box-shadow:
1560
- 0 4px 6px -1px rgb(0 0 0 / 10%),
1561
- 0 10px 20px -4px rgb(0 0 0 / 12%);
1562
- }
1563
-
1564
- .vm-color-picker__format-item {
1565
- display: flex;
1566
- align-items: center;
1567
- gap: 6px;
1568
- width: 100%;
1569
- height: 26px;
1570
- padding: 0 8px;
1571
- border: none;
1572
- border-radius: 6px;
1573
- background: transparent;
1574
- color: var(--foreground);
1575
- font-size: 11px;
1576
- text-align: left;
1577
- cursor: pointer;
1578
-
1579
- &:hover {
1580
- background: color-mix(in srgb, var(--brand, #4e5dff) 12%, transparent);
1581
- }
1582
-
1583
- &.is-active {
1584
- background: var(--brand, #4e5dff);
1585
- color: #fff;
1586
- }
1587
- }
1588
-
1589
- .vm-color-picker__format-check {
1590
- display: inline-flex;
1591
- width: 14px;
1592
- height: 14px;
1593
- flex-shrink: 0;
1594
- align-items: center;
1595
- justify-content: center;
1596
- }
1597
-
1598
- .vm-color-picker__value,
1599
- .vm-color-picker__alpha-pct {
1600
- height: 24px;
1601
- padding: 0 6px;
1602
- border: 1px solid var(--border);
1603
- border-radius: 6px;
1604
- background: var(--muted);
1605
- color: var(--foreground);
1606
- font-size: 11px;
1607
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1608
- outline: none;
1609
-
1610
- &:focus {
1611
- border-color: var(--brand, #4e5dff);
1612
- }
1613
- }
1614
-
1615
- .vm-color-picker__value {
1616
- flex: 1 1 auto;
1617
- min-width: 0;
1618
- }
1619
-
1620
- .vm-color-picker__alpha-pct {
1621
- width: 42px;
1622
- flex: 0 0 42px;
1623
- padding: 0 4px;
1624
- text-align: center;
1625
- }
1626
- </style>
1
+ <template>
2
+ <div
3
+ ref="rootRef"
4
+ class="vm-color-picker"
5
+ :class="{
6
+ 'is-disabled': disabled,
7
+ 'is-sm': size === 'sm',
8
+ 'is-open': open,
9
+ 'is-swatch-only': swatchOnly,
10
+ }"
11
+ >
12
+ <button
13
+ ref="triggerRef"
14
+ type="button"
15
+ class="vm-color-picker__swatch"
16
+ :disabled="disabled"
17
+ :title="disabled ? undefined : crudLabel('pickColor')"
18
+ @click="toggle"
19
+ >
20
+ <span
21
+ class="vm-color-picker__swatch-fill"
22
+ :style="{ background: cssColor }"
23
+ />
24
+ </button>
25
+
26
+ <input
27
+ v-if="!swatchOnly"
28
+ type="text"
29
+ class="vm-color-picker__input"
30
+ :value="inlineDisplay"
31
+ :placeholder="placeholderText"
32
+ :disabled="disabled"
33
+ spellcheck="false"
34
+ autocomplete="off"
35
+ :title="isThemeBound ? themeBoundLabel : undefined"
36
+ @input="onInlineInput"
37
+ @focus="openPanel"
38
+ @blur="onInlineBlur"
39
+ @keydown.esc.stop="onEscEnd"
40
+ />
41
+
42
+ <Teleport to="body">
43
+ <FocusScope
44
+ v-if="open"
45
+ as-child
46
+ loop
47
+ :trapped="true"
48
+ @unmount-auto-focus="onPanelUnmountAutoFocus"
49
+ >
50
+ <div
51
+ ref="panelRef"
52
+ class="vm-color-picker__panel vm-dialog-float"
53
+ :style="panelStyle"
54
+ @pointerdown.stop
55
+ @mousedown.stop
56
+ >
57
+ <div class="vm-color-picker__toolbar">
58
+ <div
59
+ v-if="hasThemeOptions"
60
+ class="vm-color-picker__modes"
61
+ role="tablist"
62
+ >
63
+ <button
64
+ type="button"
65
+ class="vm-color-picker__mode"
66
+ :class="{ 'is-on': panelMode === 'theme' }"
67
+ role="tab"
68
+ @click="panelMode = 'theme'"
69
+ >
70
+ {{ crudLabel('theme') }}
71
+ </button>
72
+ <button
73
+ type="button"
74
+ class="vm-color-picker__mode"
75
+ :class="{ 'is-on': panelMode === 'picker' }"
76
+ role="tab"
77
+ @click="panelMode = 'picker'"
78
+ >
79
+ {{ crudLabel('colorPickerTab') }}
80
+ </button>
81
+ </div>
82
+ <span v-else class="vm-color-picker__toolbar-spacer" />
83
+ <button
84
+ type="button"
85
+ class="vm-color-picker__close"
86
+ :title="crudLabel('dismiss')"
87
+ :aria-label="crudLabel('dismiss')"
88
+ @click="closePanel"
89
+ >
90
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
91
+ <path
92
+ fill="currentColor"
93
+ d="M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"
94
+ />
95
+ </svg>
96
+ </button>
97
+ </div>
98
+
99
+ <div v-if="panelMode === 'theme' && hasThemeOptions" class="vm-color-picker__theme">
100
+ <button
101
+ v-for="opt in themeOptions"
102
+ :key="opt.key"
103
+ type="button"
104
+ class="vm-color-picker__theme-item"
105
+ :class="{ 'is-on': themeBoundKey === opt.key }"
106
+ @click="pickTheme(opt.key)"
107
+ >
108
+ <span
109
+ class="vm-color-picker__theme-swatch"
110
+ :style="{ background: opt.value }"
111
+ />
112
+ <span class="vm-color-picker__theme-meta">
113
+ <span class="vm-color-picker__theme-label">{{ opt.label }}</span>
114
+ <span class="vm-color-picker__theme-key">{{ opt.key }}</span>
115
+ </span>
116
+ </button>
117
+ </div>
118
+
119
+ <template v-else>
120
+ <div
121
+ ref="svRef"
122
+ class="vm-color-picker__sv"
123
+ :style="{ background: svBg }"
124
+ @pointerdown="onSvPointer"
125
+ >
126
+ <span
127
+ class="vm-color-picker__sv-cursor"
128
+ :style="svCursorStyle"
129
+ />
130
+ </div>
131
+
132
+ <div class="vm-color-picker__sliders">
133
+ <button
134
+ v-if="hasEyeDropper"
135
+ type="button"
136
+ class="vm-color-picker__eyedropper"
137
+ :title="crudLabel('eyedropper')"
138
+ @click="pickEyeDropper"
139
+ >
140
+ <!-- 内联 SVG:不依赖 remixicon 字体是否已加载 -->
141
+ <svg
142
+ class="vm-color-picker__eyedropper-ico"
143
+ viewBox="0 0 24 24"
144
+ width="16"
145
+ height="16"
146
+ aria-hidden="true"
147
+ >
148
+ <path
149
+ fill="currentColor"
150
+ d="M19.07 3.66a2.5 2.5 0 0 1 0 3.54l-1.42 1.41 2.12 2.12-1.41 1.42-2.12-2.13-7.07 7.07c-.3.3-.68.48-1.08.54l-3.36.48a.75.75 0 0 1-.84-.84l.48-3.36c.06-.4.24-.78.54-1.08l7.07-7.07-1.41-1.41 1.41-1.42 2.12 2.13 1.42-1.42a2.5 2.5 0 0 1 3.54 0Zm-2.12 2.12.71-.7a1 1 0 0 0-1.42-1.42l-.7.71 1.41 1.41ZM8.48 15.9l-.17.17-.28 1.9 1.9-.28.17-.17 5.66-5.66-1.62-1.62-5.66 5.66Z"
151
+ />
152
+ </svg>
153
+ </button>
154
+ <div class="vm-color-picker__slider-col">
155
+ <div
156
+ ref="hueRef"
157
+ class="vm-color-picker__hue"
158
+ @pointerdown="onHuePointer"
159
+ >
160
+ <span
161
+ class="vm-color-picker__slider-thumb"
162
+ :style="{ left: thumbLeft(hue / 360) }"
163
+ />
164
+ </div>
165
+ <div
166
+ ref="alphaRef"
167
+ class="vm-color-picker__alpha"
168
+ @pointerdown="onAlphaPointer"
169
+ >
170
+ <span
171
+ class="vm-color-picker__alpha-fill"
172
+ :style="{ background: alphaBg }"
173
+ />
174
+ <span
175
+ class="vm-color-picker__slider-thumb"
176
+ :style="{ left: thumbLeft(alpha) }"
177
+ />
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="vm-color-picker__footer">
183
+ <div class="vm-color-picker__format-wrap">
184
+ <button
185
+ type="button"
186
+ class="vm-color-picker__format"
187
+ :class="{ 'is-open': formatOpen }"
188
+ :title="crudLabel('colorFormat')"
189
+ @click.stop="toggleFormatMenu"
190
+ >
191
+ {{ formatLabel }}
192
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
193
+ <path fill="currentColor" d="M12 15.4 6.6 10l1.4-1.4 4 4 4-4L17.4 10 12 15.4Z" />
194
+ </svg>
195
+ </button>
196
+ <div v-if="formatOpen" class="vm-color-picker__format-menu" role="listbox">
197
+ <button
198
+ v-for="opt in FORMAT_OPTS"
199
+ :key="opt.id"
200
+ type="button"
201
+ class="vm-color-picker__format-item"
202
+ :class="{ 'is-active': colorFormat === opt.id }"
203
+ role="option"
204
+ :aria-selected="colorFormat === opt.id"
205
+ @click.stop="setFormat(opt.id)"
206
+ >
207
+ <span class="vm-color-picker__format-check" aria-hidden="true">
208
+ <svg v-if="colorFormat === opt.id" viewBox="0 0 24 24" width="14" height="14">
209
+ <path
210
+ fill="currentColor"
211
+ d="M9.55 17.6 4.9 12.95l1.4-1.4 3.25 3.25 7.15-7.15 1.4 1.4-8.55 8.55Z"
212
+ />
213
+ </svg>
214
+ </span>
215
+ {{ opt.label }}
216
+ </button>
217
+ </div>
218
+ </div>
219
+ <input
220
+ type="text"
221
+ class="vm-color-picker__value"
222
+ :value="panelValue"
223
+ spellcheck="false"
224
+ autocomplete="off"
225
+ @input="onPanelValueInput"
226
+ @blur="onPanelValueBlur"
227
+ @keydown.esc.stop="onEscEnd"
228
+ />
229
+ <input
230
+ type="text"
231
+ class="vm-color-picker__alpha-pct"
232
+ :value="alphaPctText"
233
+ spellcheck="false"
234
+ autocomplete="off"
235
+ @input="onAlphaPctInput"
236
+ @blur="onAlphaPctBlur"
237
+ @keydown.esc.stop="onEscEnd"
238
+ />
239
+ </div>
240
+ </template>
241
+ </div>
242
+ </FocusScope>
243
+ </Teleport>
244
+ </div>
245
+ </template>
246
+
247
+ <script setup lang="ts">
248
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
249
+ import { FocusScope } from 'reka-ui'
250
+ import { POPUP_TRIGGER_GAP } from '../../lib/dialog-float'
251
+ import {
252
+ ADMIN_THEME_COLOR_DEFS,
253
+ resolveCssThemeColorOptions,
254
+ type VmColorThemeOption,
255
+ } from '../../lib/theme-color-options'
256
+ import { crudLabel, getCrudConfig } from '../style'
257
+
258
+ defineOptions({ name: 'vm-color-picker' })
259
+
260
+ export type { VmColorThemeOption } from '../../lib/theme-color-options'
261
+
262
+ const props = withDefaults(
263
+ defineProps<{
264
+ modelValue?: string | null
265
+ disabled?: boolean
266
+ placeholder?: string
267
+ size?: 'default' | 'sm'
268
+ themeOptions?: VmColorThemeOption[]
269
+ /** 只显示色块触发器(工具栏等窄位) */
270
+ swatchOnly?: boolean
271
+ /** 为 false 时不展示主题色(正文 HTML 勿写 CSS 变量) */
272
+ enableTheme?: boolean
273
+ }>(),
274
+ {
275
+ placeholder: '#000000',
276
+ size: 'default',
277
+ themeOptions: () => [],
278
+ swatchOnly: false,
279
+ enableTheme: true,
280
+ },
281
+ )
282
+
283
+ const emit = defineEmits<{
284
+ 'update:modelValue': [v: string]
285
+ change: [v: string]
286
+ }>()
287
+
288
+ type Rgba = { r: number; g: number; b: number; a: number }
289
+ type Hsv = { h: number; s: number; v: number }
290
+ type ColorFormat = 'hex' | 'rgb' | 'css' | 'hsl' | 'hsb'
291
+
292
+ function parseCssVarRef(raw: string): string | null {
293
+ const s = String(raw || '').trim()
294
+ const m = s.match(/^var\(\s*--([a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)$/i)
295
+ return m ? m[1]! : null
296
+ }
297
+
298
+ function toCssVarRef(key: string): string {
299
+ const k = String(key || '')
300
+ .trim()
301
+ .replace(/^--/, '')
302
+ return k ? `var(--${k})` : ''
303
+ }
304
+
305
+ const FORMAT_OPTS: { id: ColorFormat; label: string }[] = [
306
+ { id: 'hex', label: 'Hex' },
307
+ { id: 'rgb', label: 'RGB' },
308
+ { id: 'css', label: 'CSS' },
309
+ { id: 'hsl', label: 'HSL' },
310
+ { id: 'hsb', label: 'HSB' },
311
+ ]
312
+
313
+ const THUMB_PX = 14
314
+
315
+ const rootRef = ref<HTMLElement | null>(null)
316
+ const triggerRef = ref<HTMLElement | null>(null)
317
+ const panelRef = ref<HTMLElement | null>(null)
318
+ const svRef = ref<HTMLElement | null>(null)
319
+ const hueRef = ref<HTMLElement | null>(null)
320
+ const alphaRef = ref<HTMLElement | null>(null)
321
+
322
+ const open = ref(false)
323
+ const suppressOpen = ref(false)
324
+ const panelStyle = ref<Record<string, string>>({})
325
+ const formatOpen = ref(false)
326
+ const colorFormat = ref<ColorFormat>('hex')
327
+ /** 面板:主题变量 | 取色器 */
328
+ const panelMode = ref<'theme' | 'picker'>('picker')
329
+
330
+ const hue = ref(0)
331
+ const sat = ref(0)
332
+ const val = ref(0)
333
+ const alpha = ref(1)
334
+
335
+ /** 行内 Hex(不含 #);面板按当前格式展示 */
336
+ const inlineHex = ref('')
337
+ const panelValue = ref('')
338
+ const alphaPctText = ref('100%')
339
+
340
+ const hasEyeDropper = ref(false)
341
+
342
+ const themeOptions = computed(() => {
343
+ if (props.enableTheme === false) return []
344
+ if (props.themeOptions?.length) return props.themeOptions
345
+ // open 变化时重读 CSS 变量(亮暗切换后预览色同步)
346
+ void open.value
347
+ const fromConfig = getCrudConfig().listThemeColorOptions?.()
348
+ if (fromConfig?.length) return fromConfig
349
+ return resolveCssThemeColorOptions(ADMIN_THEME_COLOR_DEFS)
350
+ })
351
+ const hasThemeOptions = computed(() => themeOptions.value.length > 0)
352
+ const themeBoundKey = computed(() => parseCssVarRef(props.modelValue ?? ''))
353
+ const isThemeBound = computed(() => Boolean(themeBoundKey.value))
354
+ const themeBoundOpt = computed(() => {
355
+ const k = themeBoundKey.value
356
+ if (!k) return null
357
+ return themeOptions.value.find((o) => o.key === k) || null
358
+ })
359
+ const themeBoundLabel = computed(() => {
360
+ const opt = themeBoundOpt.value
361
+ if (opt) return opt.label
362
+ const k = themeBoundKey.value
363
+ return k ? k : ''
364
+ })
365
+ const inlineDisplay = computed(() => {
366
+ if (isThemeBound.value) {
367
+ return String(props.modelValue || '').trim() || themeBoundLabel.value
368
+ }
369
+ return inlineHex.value
370
+ })
371
+
372
+ /** 行内:主题变量 / mustache,不当作 hex 强行加 # */
373
+ function isInlineExpr(raw: string): boolean {
374
+ const s = String(raw || '').trim()
375
+ if (!s) return false
376
+ if (/\{\{[\s\S]*?\}\}/.test(s)) return true
377
+ if (parseCssVarRef(s)) return true
378
+ if (/^var\s*\(/i.test(s)) return true
379
+ return false
380
+ }
381
+
382
+ const placeholderText = computed(() => {
383
+ const p = props.placeholder || '#000000'
384
+ return p.startsWith('#') ? p.slice(1) : p
385
+ })
386
+
387
+ const formatLabel = computed(
388
+ () => FORMAT_OPTS.find((o) => o.id === colorFormat.value)?.label ?? 'Hex',
389
+ )
390
+
391
+ function clamp(n: number, min: number, max: number) {
392
+ return Math.min(max, Math.max(min, n))
393
+ }
394
+
395
+ /** 拇指中心落在轨道内,两端不溢出 */
396
+ function thumbLeft(ratio: number) {
397
+ const t = clamp(ratio, 0, 1)
398
+ return `calc(${t} * (100% - ${THUMB_PX}px) + ${THUMB_PX / 2}px)`
399
+ }
400
+
401
+ function toHex2(n: number) {
402
+ return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0')
403
+ }
404
+
405
+ function rgbToHex(r: number, g: number, b: number, a = 1): string {
406
+ const base = `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`
407
+ if (a >= 1 - 1e-6) return base
408
+ return `${base}${toHex2(a * 255)}`
409
+ }
410
+
411
+ function hsvToRgb(h: number, s: number, v: number): { r: number; g: number; b: number } {
412
+ const hh = ((h % 360) + 360) % 360
413
+ const c = v * s
414
+ const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
415
+ const m = v - c
416
+ let rp = 0
417
+ let gp = 0
418
+ let bp = 0
419
+ if (hh < 60) {
420
+ rp = c
421
+ gp = x
422
+ } else if (hh < 120) {
423
+ rp = x
424
+ gp = c
425
+ } else if (hh < 180) {
426
+ gp = c
427
+ bp = x
428
+ } else if (hh < 240) {
429
+ gp = x
430
+ bp = c
431
+ } else if (hh < 300) {
432
+ rp = x
433
+ bp = c
434
+ } else {
435
+ rp = c
436
+ bp = x
437
+ }
438
+ return {
439
+ r: (rp + m) * 255,
440
+ g: (gp + m) * 255,
441
+ b: (bp + m) * 255,
442
+ }
443
+ }
444
+
445
+ function rgbToHsv(r: number, g: number, b: number): Hsv {
446
+ const rr = r / 255
447
+ const gg = g / 255
448
+ const bb = b / 255
449
+ const max = Math.max(rr, gg, bb)
450
+ const min = Math.min(rr, gg, bb)
451
+ const d = max - min
452
+ let h = 0
453
+ if (d !== 0) {
454
+ if (max === rr) h = ((gg - bb) / d) % 6
455
+ else if (max === gg) h = (bb - rr) / d + 2
456
+ else h = (rr - gg) / d + 4
457
+ h *= 60
458
+ if (h < 0) h += 360
459
+ }
460
+ const s = max === 0 ? 0 : d / max
461
+ return { h, s, v: max }
462
+ }
463
+
464
+ function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
465
+ const rr = r / 255
466
+ const gg = g / 255
467
+ const bb = b / 255
468
+ const max = Math.max(rr, gg, bb)
469
+ const min = Math.min(rr, gg, bb)
470
+ const l = (max + min) / 2
471
+ const d = max - min
472
+ let h = 0
473
+ let s = 0
474
+ if (d !== 0) {
475
+ s = d / (1 - Math.abs(2 * l - 1))
476
+ if (max === rr) h = ((gg - bb) / d) % 6
477
+ else if (max === gg) h = (bb - rr) / d + 2
478
+ else h = (rr - gg) / d + 4
479
+ h *= 60
480
+ if (h < 0) h += 360
481
+ }
482
+ return { h, s, l }
483
+ }
484
+
485
+ function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
486
+ const hh = ((h % 360) + 360) % 360
487
+ const c = (1 - Math.abs(2 * l - 1)) * s
488
+ const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
489
+ const m = l - c / 2
490
+ let rp = 0
491
+ let gp = 0
492
+ let bp = 0
493
+ if (hh < 60) {
494
+ rp = c
495
+ gp = x
496
+ } else if (hh < 120) {
497
+ rp = x
498
+ gp = c
499
+ } else if (hh < 180) {
500
+ gp = c
501
+ bp = x
502
+ } else if (hh < 240) {
503
+ gp = x
504
+ bp = c
505
+ } else if (hh < 300) {
506
+ rp = x
507
+ bp = c
508
+ } else {
509
+ rp = c
510
+ bp = x
511
+ }
512
+ return {
513
+ r: (rp + m) * 255,
514
+ g: (gp + m) * 255,
515
+ b: (bp + m) * 255,
516
+ }
517
+ }
518
+
519
+ /** 解析为 rgba;无效返回 null */
520
+ function parseColor(raw: string): Rgba | null {
521
+ const s = raw.trim()
522
+ if (!s) return null
523
+
524
+ if (s.startsWith('#')) {
525
+ let h = s.slice(1)
526
+ if (/^[0-9a-fA-F]{3}$/.test(h)) {
527
+ h = h
528
+ .split('')
529
+ .map((c) => c + c)
530
+ .join('')
531
+ }
532
+ if (/^[0-9a-fA-F]{6}$/.test(h)) {
533
+ return {
534
+ r: parseInt(h.slice(0, 2), 16),
535
+ g: parseInt(h.slice(2, 4), 16),
536
+ b: parseInt(h.slice(4, 6), 16),
537
+ a: 1,
538
+ }
539
+ }
540
+ if (/^[0-9a-fA-F]{8}$/.test(h)) {
541
+ return {
542
+ r: parseInt(h.slice(0, 2), 16),
543
+ g: parseInt(h.slice(2, 4), 16),
544
+ b: parseInt(h.slice(4, 6), 16),
545
+ a: parseInt(h.slice(6, 8), 16) / 255,
546
+ }
547
+ }
548
+ return null
549
+ }
550
+
551
+ if (/^[0-9a-fA-F]{3}$/.test(s) || /^[0-9a-fA-F]{6}$/.test(s) || /^[0-9a-fA-F]{8}$/.test(s)) {
552
+ return parseColor(`#${s}`)
553
+ }
554
+
555
+ const m = s.match(
556
+ /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i,
557
+ )
558
+ if (m) {
559
+ return {
560
+ r: clamp(Number(m[1]), 0, 255),
561
+ g: clamp(Number(m[2]), 0, 255),
562
+ b: clamp(Number(m[3]), 0, 255),
563
+ a: m[4] != null ? clamp(Number(m[4]), 0, 1) : 1,
564
+ }
565
+ }
566
+
567
+ const hsl = s.match(
568
+ /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/i,
569
+ )
570
+ if (hsl) {
571
+ const rgb = hslToRgb(Number(hsl[1]), Number(hsl[2]) / 100, Number(hsl[3]) / 100)
572
+ return {
573
+ r: rgb.r,
574
+ g: rgb.g,
575
+ b: rgb.b,
576
+ a: hsl[4] != null ? clamp(Number(hsl[4]), 0, 1) : 1,
577
+ }
578
+ }
579
+
580
+ return null
581
+ }
582
+
583
+ /** 按当前格式解析面板中间输入(不改变 alpha,除非 CSS/rgba 自带) */
584
+ function parsePanelValue(raw: string, keepAlpha: number): Rgba | null {
585
+ const s = raw.trim()
586
+ if (!s) return null
587
+ const fmt = colorFormat.value
588
+
589
+ if (fmt === 'hex' || fmt === 'css') {
590
+ const parsed = parseColor(s)
591
+ if (!parsed) return null
592
+ if (fmt === 'hex') parsed.a = keepAlpha
593
+ return parsed
594
+ }
595
+
596
+ if (fmt === 'rgb') {
597
+ const parts = s.split(/[,\s]+/).filter(Boolean)
598
+ if (parts.length < 3) return null
599
+ const r = Number(parts[0])
600
+ const g = Number(parts[1])
601
+ const b = Number(parts[2])
602
+ if (![r, g, b].every(Number.isFinite)) return null
603
+ return { r: clamp(r, 0, 255), g: clamp(g, 0, 255), b: clamp(b, 0, 255), a: keepAlpha }
604
+ }
605
+
606
+ if (fmt === 'hsl') {
607
+ const parts = s.replace(/%/g, '').split(/[,\s]+/).filter(Boolean)
608
+ if (parts.length < 3) return null
609
+ const h = Number(parts[0])
610
+ const satPct = Number(parts[1])
611
+ const lPct = Number(parts[2])
612
+ if (![h, satPct, lPct].every(Number.isFinite)) return null
613
+ const rgb = hslToRgb(h, clamp(satPct, 0, 100) / 100, clamp(lPct, 0, 100) / 100)
614
+ return { ...rgb, a: keepAlpha }
615
+ }
616
+
617
+ if (fmt === 'hsb') {
618
+ const parts = s.replace(/%/g, '').split(/[,\s]+/).filter(Boolean)
619
+ if (parts.length < 3) return null
620
+ const h = Number(parts[0])
621
+ const sPct = Number(parts[1])
622
+ const bPct = Number(parts[2])
623
+ if (![h, sPct, bPct].every(Number.isFinite)) return null
624
+ const rgb = hsvToRgb(h, clamp(sPct, 0, 100) / 100, clamp(bPct, 0, 100) / 100)
625
+ return { ...rgb, a: keepAlpha }
626
+ }
627
+
628
+ return null
629
+ }
630
+
631
+ function currentRgb() {
632
+ return hsvToRgb(hue.value, sat.value, val.value)
633
+ }
634
+
635
+ function currentHex(): string {
636
+ const { r, g, b } = currentRgb()
637
+ return rgbToHex(r, g, b, alpha.value)
638
+ }
639
+
640
+ function formatPanelValue(): string {
641
+ const { r, g, b } = currentRgb()
642
+ const rr = Math.round(r)
643
+ const gg = Math.round(g)
644
+ const bb = Math.round(b)
645
+ const a = alpha.value
646
+ switch (colorFormat.value) {
647
+ case 'rgb':
648
+ return `${rr}, ${gg}, ${bb}`
649
+ case 'css':
650
+ return a < 1 - 1e-6
651
+ ? `rgba(${rr}, ${gg}, ${bb}, ${Math.round(a * 100) / 100})`
652
+ : `#${toHex2(rr)}${toHex2(gg)}${toHex2(bb)}`
653
+ case 'hsl': {
654
+ const hsl = rgbToHsl(rr, gg, bb)
655
+ return `${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%`
656
+ }
657
+ case 'hsb':
658
+ return `${Math.round(hue.value)}, ${Math.round(sat.value * 100)}%, ${Math.round(val.value * 100)}%`
659
+ case 'hex':
660
+ default:
661
+ return `${toHex2(rr)}${toHex2(gg)}${toHex2(bb)}`
662
+ }
663
+ }
664
+
665
+ const cssColor = computed(() => {
666
+ const themePreview = themeBoundOpt.value?.value
667
+ if (themePreview) {
668
+ const parsed = parseColor(themePreview)
669
+ if (parsed) {
670
+ const { r, g, b, a } = parsed
671
+ return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`
672
+ }
673
+ return String(themePreview)
674
+ }
675
+ if (themeBoundKey.value) {
676
+ return `var(--${themeBoundKey.value})`
677
+ }
678
+ const raw = props.modelValue ?? ''
679
+ const parsed = parseColor(raw)
680
+ if (!parsed && !String(raw).trim()) return 'transparent'
681
+ if (!parsed) return 'transparent'
682
+ const { r, g, b, a } = parsed
683
+ return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`
684
+ })
685
+
686
+ function pickTheme(key: string) {
687
+ const ref = toCssVarRef(key)
688
+ if (!ref) return
689
+ commit(ref)
690
+ closePanel()
691
+ }
692
+
693
+ const svBg = computed(() => {
694
+ const { r, g, b } = hsvToRgb(hue.value, 1, 1)
695
+ return `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)}))`
696
+ })
697
+
698
+ const alphaBg = computed(() => {
699
+ const { r, g, b } = currentRgb()
700
+ const c = `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`
701
+ return `linear-gradient(to right, transparent, ${c})`
702
+ })
703
+
704
+ const svCursorStyle = computed(() => ({
705
+ left: `${sat.value * 100}%`,
706
+ top: `${(1 - val.value) * 100}%`,
707
+ }))
708
+
709
+ function syncTextsFromState() {
710
+ const hex = currentHex()
711
+ const bare = hex.startsWith('#') ? hex.slice(1) : hex
712
+ inlineHex.value = bare.slice(0, 6)
713
+ panelValue.value = formatPanelValue()
714
+ alphaPctText.value = `${Math.round(alpha.value * 100)}%`
715
+ }
716
+
717
+ function applyRgba(c: Rgba, emitValue = true) {
718
+ const hsv = rgbToHsv(c.r, c.g, c.b)
719
+ hue.value = hsv.h
720
+ sat.value = hsv.s
721
+ val.value = hsv.v
722
+ alpha.value = clamp(c.a, 0, 1)
723
+ syncTextsFromState()
724
+ if (emitValue) commit(currentHex())
725
+ }
726
+
727
+ /** 3/4 位短 hex:可预览,但行内禁止展开成 6/8 位 */
728
+ function hexBodyOf(raw: string) {
729
+ return String(raw || '')
730
+ .trim()
731
+ .replace(/^#/, '')
732
+ }
733
+
734
+ function isShortHexBody(body: string) {
735
+ return /^[0-9a-fA-F]{3}$/.test(body) || /^[0-9a-fA-F]{4}$/.test(body)
736
+ }
737
+
738
+ function applyHexPreview(body: string) {
739
+ const parsed = parseColor(`#${body}`)
740
+ if (!parsed) return
741
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
742
+ hue.value = hsv.h
743
+ sat.value = hsv.s
744
+ val.value = hsv.v
745
+ if (body.length === 4 || body.length === 8) alpha.value = parsed.a
746
+ }
747
+
748
+ function loadFromModel(raw: string | null | undefined) {
749
+ const s = raw ?? ''
750
+ const varKey = parseCssVarRef(s)
751
+ if (varKey) {
752
+ const opt = themeOptions.value.find((o) => o.key === varKey)
753
+ const preview = opt?.value || ''
754
+ if (preview) {
755
+ const parsed = parseColor(preview)
756
+ if (parsed) {
757
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
758
+ hue.value = hsv.h
759
+ sat.value = hsv.s
760
+ val.value = hsv.v
761
+ alpha.value = parsed.a
762
+ syncTextsFromState()
763
+ return
764
+ }
765
+ }
766
+ inlineHex.value = varKey
767
+ panelValue.value = varKey
768
+ return
769
+ }
770
+ if (!s.trim()) {
771
+ hue.value = 0
772
+ sat.value = 0
773
+ val.value = 0
774
+ alpha.value = 1
775
+ inlineHex.value = ''
776
+ panelValue.value = ''
777
+ alphaPctText.value = '100%'
778
+ return
779
+ }
780
+ const body = hexBodyOf(s)
781
+ // 短 hex / 未满 6 位:保留位数,勿 sync 成 6 位
782
+ if (/^[0-9a-fA-F]+$/.test(body) && body.length !== 6 && body.length !== 8) {
783
+ if (isShortHexBody(body)) applyHexPreview(body)
784
+ inlineHex.value = body
785
+ panelValue.value = body
786
+ alphaPctText.value = `${Math.round(alpha.value * 100)}%`
787
+ return
788
+ }
789
+ const parsed = parseColor(s)
790
+ if (!parsed) {
791
+ inlineHex.value = s.startsWith('#') ? s.slice(1) : s
792
+ return
793
+ }
794
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
795
+ hue.value = hsv.h
796
+ sat.value = hsv.s
797
+ val.value = hsv.v
798
+ alpha.value = parsed.a
799
+ syncTextsFromState()
800
+ }
801
+
802
+ watch(
803
+ () => props.modelValue,
804
+ (v) => {
805
+ if (open.value) {
806
+ const cur = currentHex()
807
+ const parsed = parseColor(v ?? '')
808
+ if (parsed && rgbToHex(parsed.r, parsed.g, parsed.b, parsed.a) === cur) return
809
+ }
810
+ loadFromModel(v)
811
+ },
812
+ { immediate: true },
813
+ )
814
+
815
+ function commit(v: string) {
816
+ emit('update:modelValue', v)
817
+ emit('change', v)
818
+ }
819
+
820
+ function commitFromState() {
821
+ syncTextsFromState()
822
+ commit(currentHex())
823
+ }
824
+
825
+ function onInlineInput(e: Event) {
826
+ const rawInput = (e.target as HTMLInputElement).value
827
+ if (isInlineExpr(rawInput)) {
828
+ inlineHex.value = rawInput
829
+ commit(rawInput.trim())
830
+ return
831
+ }
832
+ const raw = hexBodyOf(rawInput)
833
+ inlineHex.value = raw
834
+ if (!raw) {
835
+ commit('')
836
+ return
837
+ }
838
+ // 纯 hex:仅 6/8 位规范化;3 位等中间态原样写入,禁止 3→6 自动补齐
839
+ if (/^[0-9a-fA-F]+$/.test(raw)) {
840
+ if (raw.length === 6 || raw.length === 8) {
841
+ const parsed = parseColor(`#${raw}`)
842
+ if (parsed) {
843
+ if (raw.length === 6) parsed.a = alpha.value
844
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
845
+ hue.value = hsv.h
846
+ sat.value = hsv.s
847
+ val.value = hsv.v
848
+ if (raw.length === 8) alpha.value = parsed.a
849
+ panelValue.value = formatPanelValue()
850
+ commit(currentHex())
851
+ return
852
+ }
853
+ }
854
+ if (isShortHexBody(raw)) applyHexPreview(raw)
855
+ commit(`#${raw}`)
856
+ return
857
+ }
858
+ const parsed = parseColor(rawInput)
859
+ if (parsed) {
860
+ parsed.a = alpha.value
861
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
862
+ hue.value = hsv.h
863
+ sat.value = hsv.s
864
+ val.value = hsv.v
865
+ panelValue.value = formatPanelValue()
866
+ commit(currentHex())
867
+ return
868
+ }
869
+ commit(raw)
870
+ }
871
+
872
+ function onInlineBlur() {
873
+ const t = inlineHex.value.trim()
874
+ if (!t) {
875
+ commit('')
876
+ hue.value = 0
877
+ sat.value = 0
878
+ val.value = 0
879
+ alpha.value = 1
880
+ inlineHex.value = ''
881
+ panelValue.value = ''
882
+ alphaPctText.value = '100%'
883
+ return
884
+ }
885
+ if (isInlineExpr(t)) {
886
+ commit(t)
887
+ return
888
+ }
889
+ const body = hexBodyOf(t)
890
+ if (/^[0-9a-fA-F]+$/.test(body)) {
891
+ if (body.length === 6 || body.length === 8) {
892
+ const parsed = parseColor(`#${body}`)
893
+ if (parsed) {
894
+ if (body.length === 6) parsed.a = alpha.value
895
+ applyRgba(parsed, true)
896
+ return
897
+ }
898
+ }
899
+ if (isShortHexBody(body)) applyHexPreview(body)
900
+ inlineHex.value = body
901
+ commit(`#${body}`)
902
+ return
903
+ }
904
+ const parsed = parseColor(t)
905
+ if (parsed) {
906
+ parsed.a = alpha.value
907
+ applyRgba(parsed, true)
908
+ return
909
+ }
910
+ loadFromModel(props.modelValue)
911
+ }
912
+
913
+ function onPanelValueInput(e: Event) {
914
+ panelValue.value = (e.target as HTMLInputElement).value
915
+ const parsed = parsePanelValue(panelValue.value, alpha.value)
916
+ if (!parsed) return
917
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
918
+ hue.value = hsv.h
919
+ sat.value = hsv.s
920
+ val.value = hsv.v
921
+ if (colorFormat.value === 'css') alpha.value = parsed.a
922
+ const hex = currentHex()
923
+ inlineHex.value = hex.replace(/^#/, '').slice(0, 6)
924
+ alphaPctText.value = `${Math.round(alpha.value * 100)}%`
925
+ commit(hex)
926
+ }
927
+
928
+ function onPanelValueBlur() {
929
+ const parsed = parsePanelValue(panelValue.value, alpha.value)
930
+ if (parsed) {
931
+ if (colorFormat.value !== 'css') parsed.a = alpha.value
932
+ applyRgba(parsed, true)
933
+ } else {
934
+ syncTextsFromState()
935
+ }
936
+ }
937
+
938
+ function onAlphaPctInput(e: Event) {
939
+ alphaPctText.value = (e.target as HTMLInputElement).value
940
+ const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
941
+ if (!Number.isFinite(n)) return
942
+ alpha.value = clamp(n / 100, 0, 1)
943
+ commitFromState()
944
+ }
945
+
946
+ function onAlphaPctBlur() {
947
+ const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
948
+ if (Number.isFinite(n)) {
949
+ alpha.value = clamp(n / 100, 0, 1)
950
+ }
951
+ syncTextsFromState()
952
+ commit(currentHex())
953
+ }
954
+
955
+ function toggleFormatMenu() {
956
+ formatOpen.value = !formatOpen.value
957
+ }
958
+
959
+ function setFormat(id: ColorFormat) {
960
+ colorFormat.value = id
961
+ formatOpen.value = false
962
+ syncTextsFromState()
963
+ }
964
+
965
+ function ratioFromEvent(el: HTMLElement, e: PointerEvent, axis: 'x' | 'y' | 'both') {
966
+ const r = el.getBoundingClientRect()
967
+ const x = clamp((e.clientX - r.left) / r.width, 0, 1)
968
+ const y = clamp((e.clientY - r.top) / r.height, 0, 1)
969
+ if (axis === 'x') return { x, y: 0 }
970
+ if (axis === 'y') return { x: 0, y }
971
+ return { x, y }
972
+ }
973
+
974
+ function bindDrag(
975
+ el: HTMLElement,
976
+ e: PointerEvent,
977
+ onMove: (ev: PointerEvent) => void,
978
+ ) {
979
+ e.preventDefault()
980
+ el.setPointerCapture(e.pointerId)
981
+ onMove(e)
982
+ const move = (ev: PointerEvent) => onMove(ev)
983
+ const up = (ev: PointerEvent) => {
984
+ el.releasePointerCapture(ev.pointerId)
985
+ el.removeEventListener('pointermove', move)
986
+ el.removeEventListener('pointerup', up)
987
+ el.removeEventListener('pointercancel', up)
988
+ }
989
+ el.addEventListener('pointermove', move)
990
+ el.addEventListener('pointerup', up)
991
+ el.addEventListener('pointercancel', up)
992
+ }
993
+
994
+ function onSvPointer(e: PointerEvent) {
995
+ const el = svRef.value
996
+ if (!el) return
997
+ bindDrag(el, e, (ev) => {
998
+ const { x, y } = ratioFromEvent(el, ev, 'both')
999
+ sat.value = x
1000
+ val.value = 1 - y
1001
+ commitFromState()
1002
+ })
1003
+ }
1004
+
1005
+ function onHuePointer(e: PointerEvent) {
1006
+ const el = hueRef.value
1007
+ if (!el) return
1008
+ bindDrag(el, e, (ev) => {
1009
+ const { x } = ratioFromEvent(el, ev, 'x')
1010
+ hue.value = x * 360
1011
+ commitFromState()
1012
+ })
1013
+ }
1014
+
1015
+ function onAlphaPointer(e: PointerEvent) {
1016
+ const el = alphaRef.value
1017
+ if (!el) return
1018
+ bindDrag(el, e, (ev) => {
1019
+ const { x } = ratioFromEvent(el, ev, 'x')
1020
+ alpha.value = x
1021
+ commitFromState()
1022
+ })
1023
+ }
1024
+
1025
+ async function pickEyeDropper() {
1026
+ const ED = (
1027
+ window as unknown as {
1028
+ EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> }
1029
+ }
1030
+ ).EyeDropper
1031
+ if (!ED) return
1032
+ try {
1033
+ const result = await new ED().open()
1034
+ const parsed = parseColor(result.sRGBHex)
1035
+ if (parsed) {
1036
+ parsed.a = alpha.value
1037
+ applyRgba(parsed, true)
1038
+ }
1039
+ } catch {
1040
+ /* 用户取消 */
1041
+ }
1042
+ }
1043
+
1044
+ function syncPanelPos() {
1045
+ const el = triggerRef.value
1046
+ if (!el) return
1047
+ const r = el.getBoundingClientRect()
1048
+ const width = 240
1049
+ // 贴触发框左侧 + 底部
1050
+ panelStyle.value = {
1051
+ position: 'fixed',
1052
+ top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
1053
+ left: `${r.left}px`,
1054
+ width: `${width}px`,
1055
+ zIndex: '100',
1056
+ }
1057
+ }
1058
+
1059
+ async function openPanel() {
1060
+ if (props.disabled || suppressOpen.value) return
1061
+ loadFromModel(props.modelValue)
1062
+ formatOpen.value = false
1063
+ if (hasThemeOptions.value) {
1064
+ const bound = parseCssVarRef(props.modelValue ?? '')
1065
+ const empty = !(props.modelValue ?? '').trim()
1066
+ panelMode.value = bound || empty ? 'theme' : 'picker'
1067
+ } else {
1068
+ panelMode.value = 'picker'
1069
+ }
1070
+ open.value = true
1071
+ await nextTick()
1072
+ syncPanelPos()
1073
+ }
1074
+
1075
+ function onPanelUnmountAutoFocus(event: Event) {
1076
+ event.preventDefault()
1077
+ }
1078
+
1079
+ function closePanel() {
1080
+ if (!open.value) return
1081
+ suppressOpen.value = true
1082
+ formatOpen.value = false
1083
+ open.value = false
1084
+ nextTick(() => {
1085
+ requestAnimationFrame(() => {
1086
+ suppressOpen.value = false
1087
+ })
1088
+ })
1089
+ }
1090
+
1091
+ /** Esc:收起面板并失焦 */
1092
+ function onEscEnd(e: KeyboardEvent) {
1093
+ closePanel()
1094
+ const el = e.target
1095
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) el.blur()
1096
+ }
1097
+
1098
+ async function toggle() {
1099
+ if (props.disabled) return
1100
+ if (open.value) {
1101
+ closePanel()
1102
+ return
1103
+ }
1104
+ await openPanel()
1105
+ }
1106
+
1107
+ function onDocPointer(e: Event) {
1108
+ if (!open.value) return
1109
+ const t = e.target as Node | null
1110
+ if (!t) return
1111
+ if (rootRef.value?.contains(t)) return
1112
+ if (panelRef.value?.contains(t)) return
1113
+ closePanel()
1114
+ }
1115
+
1116
+ function onWinChange() {
1117
+ if (open.value) syncPanelPos()
1118
+ }
1119
+
1120
+ onMounted(() => {
1121
+ hasEyeDropper.value = typeof window !== 'undefined' && 'EyeDropper' in window
1122
+ document.addEventListener('pointerdown', onDocPointer, true)
1123
+ window.addEventListener('resize', onWinChange)
1124
+ window.addEventListener('scroll', onWinChange, true)
1125
+ })
1126
+
1127
+ onUnmounted(() => {
1128
+ document.removeEventListener('pointerdown', onDocPointer, true)
1129
+ window.removeEventListener('resize', onWinChange)
1130
+ window.removeEventListener('scroll', onWinChange, true)
1131
+ })
1132
+ </script>
1133
+
1134
+ <style lang="scss" scoped>
1135
+ .vm-color-picker {
1136
+ display: flex;
1137
+ align-items: center;
1138
+ gap: 8px;
1139
+ width: 100%;
1140
+ min-width: 0;
1141
+
1142
+ &.is-swatch-only {
1143
+ width: auto;
1144
+ gap: 0;
1145
+ }
1146
+
1147
+ &.is-disabled {
1148
+ opacity: 0.6;
1149
+ pointer-events: none;
1150
+ }
1151
+ }
1152
+
1153
+ .vm-color-picker__swatch {
1154
+ position: relative;
1155
+ flex-shrink: 0;
1156
+ width: var(--vm-control-height);
1157
+ height: var(--vm-control-height);
1158
+ padding: 0;
1159
+ border: 1px solid var(--border);
1160
+ border-radius: var(--vm-control-radius);
1161
+ background: var(--muted);
1162
+ cursor: pointer;
1163
+ overflow: hidden;
1164
+
1165
+ outline: none;
1166
+
1167
+ &:hover:not(:disabled) {
1168
+ border-color: var(--brand, #4e5dff);
1169
+ }
1170
+
1171
+ /* 弹层触发:打开态不额外描边 */
1172
+
1173
+ &:disabled {
1174
+ cursor: not-allowed;
1175
+ }
1176
+ }
1177
+
1178
+ .vm-color-picker.is-sm .vm-color-picker__swatch {
1179
+ width: 16px;
1180
+ height: 16px;
1181
+ border-radius: 3px;
1182
+ }
1183
+
1184
+ .vm-color-picker__swatch-fill {
1185
+ display: block;
1186
+ width: 100%;
1187
+ height: 100%;
1188
+ background-image:
1189
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
1190
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
1191
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
1192
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
1193
+ background-size: 8px 8px;
1194
+ background-position:
1195
+ 0 0,
1196
+ 0 4px,
1197
+ 4px -4px,
1198
+ -4px 0;
1199
+ }
1200
+
1201
+ .vm-color-picker__input {
1202
+ flex: 1;
1203
+ min-width: 0;
1204
+ height: var(--vm-control-height);
1205
+ padding: 0 var(--vm-control-padding-x);
1206
+ border: 1px solid var(--border);
1207
+ border-radius: var(--vm-control-radius);
1208
+ background: var(--muted);
1209
+ color: var(--foreground);
1210
+ font-size: 13px;
1211
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1212
+ outline: none;
1213
+
1214
+ /* 弹层触发旁输入:不展示聚焦描边 */
1215
+
1216
+ &::placeholder {
1217
+ color: var(--muted-foreground);
1218
+ }
1219
+ }
1220
+
1221
+ .vm-color-picker.is-sm .vm-color-picker__input {
1222
+ height: 24px;
1223
+ padding: 0 6px;
1224
+ font-size: 12px;
1225
+ border-radius: 4px;
1226
+ }
1227
+ </style>
1228
+
1229
+ <style lang="scss">
1230
+ /* Teleport 到 body,不可 scoped */
1231
+ .vm-color-picker__panel {
1232
+ box-sizing: border-box;
1233
+ padding: 12px;
1234
+ border: 1px solid var(--border);
1235
+ border-radius: 8px;
1236
+ background: var(--popover, var(--background, #fff));
1237
+ color: var(--foreground);
1238
+ box-shadow:
1239
+ 0 4px 6px -1px rgb(0 0 0 / 10%),
1240
+ 0 10px 24px -4px rgb(0 0 0 / 12%);
1241
+ user-select: none;
1242
+ overflow: visible;
1243
+ }
1244
+
1245
+ .vm-color-picker__toolbar {
1246
+ display: flex;
1247
+ align-items: center;
1248
+ gap: 8px;
1249
+ margin-bottom: 10px;
1250
+ }
1251
+
1252
+ .vm-color-picker__toolbar-spacer {
1253
+ flex: 1;
1254
+ min-width: 0;
1255
+ }
1256
+
1257
+ .vm-color-picker__close {
1258
+ display: inline-flex;
1259
+ flex-shrink: 0;
1260
+ align-items: center;
1261
+ justify-content: center;
1262
+ width: 28px;
1263
+ height: 28px;
1264
+ margin: 0;
1265
+ padding: 0;
1266
+ border: 0;
1267
+ border-radius: 6px;
1268
+ background: transparent;
1269
+ color: var(--muted-foreground, #9aa0b0);
1270
+ cursor: pointer;
1271
+
1272
+ &:hover {
1273
+ background: var(--muted, #f4f6fc);
1274
+ color: var(--foreground);
1275
+ }
1276
+ }
1277
+
1278
+ .vm-color-picker__modes {
1279
+ display: grid;
1280
+ flex: 1;
1281
+ min-width: 0;
1282
+ grid-template-columns: 1fr 1fr;
1283
+ gap: 4px;
1284
+ margin: 0;
1285
+ padding: 3px;
1286
+ border-radius: 8px;
1287
+ background: var(--muted, #f4f6fc);
1288
+ }
1289
+
1290
+ .vm-color-picker__mode {
1291
+ height: 28px;
1292
+ border: 0;
1293
+ border-radius: 6px;
1294
+ background: transparent;
1295
+ color: var(--muted-foreground, #9aa0b0);
1296
+ font-size: 12px;
1297
+ font-weight: 600;
1298
+ cursor: pointer;
1299
+
1300
+ &.is-on {
1301
+ background: var(--card, #fff);
1302
+ color: var(--foreground);
1303
+ box-shadow: 0 1px 2px rgb(0 0 0 / 6%);
1304
+ }
1305
+ }
1306
+
1307
+ .vm-color-picker__theme {
1308
+ display: flex;
1309
+ flex-direction: column;
1310
+ gap: 4px;
1311
+ max-height: 280px;
1312
+ overflow: auto;
1313
+ margin: 0 -4px;
1314
+ padding: 0 4px;
1315
+ }
1316
+
1317
+ .vm-color-picker__theme-item {
1318
+ display: flex;
1319
+ align-items: center;
1320
+ gap: 10px;
1321
+ width: 100%;
1322
+ margin: 0;
1323
+ padding: 8px;
1324
+ border: 1px solid transparent;
1325
+ border-radius: 8px;
1326
+ background: transparent;
1327
+ text-align: left;
1328
+ cursor: pointer;
1329
+ color: inherit;
1330
+
1331
+ &:hover {
1332
+ background: color-mix(in srgb, var(--brand, #4e5dff) 8%, transparent);
1333
+ }
1334
+
1335
+ &.is-on {
1336
+ border-color: color-mix(in srgb, var(--brand, #4e5dff) 45%, transparent);
1337
+ background: color-mix(in srgb, var(--brand, #4e5dff) 10%, transparent);
1338
+ }
1339
+ }
1340
+
1341
+ .vm-color-picker__theme-swatch {
1342
+ flex-shrink: 0;
1343
+ width: 22px;
1344
+ height: 22px;
1345
+ border-radius: 6px;
1346
+ border: 1px solid var(--border, #e8ebf5);
1347
+ background-image:
1348
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
1349
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
1350
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
1351
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
1352
+ background-size: 8px 8px;
1353
+ background-position:
1354
+ 0 0,
1355
+ 0 4px,
1356
+ 4px -4px,
1357
+ -4px 0;
1358
+ }
1359
+
1360
+ .vm-color-picker__theme-meta {
1361
+ min-width: 0;
1362
+ display: flex;
1363
+ flex-direction: column;
1364
+ gap: 2px;
1365
+ }
1366
+
1367
+ .vm-color-picker__theme-label {
1368
+ font-size: 13px;
1369
+ font-weight: 600;
1370
+ line-height: 1.2;
1371
+ overflow: hidden;
1372
+ text-overflow: ellipsis;
1373
+ white-space: nowrap;
1374
+ }
1375
+
1376
+ .vm-color-picker__theme-key {
1377
+ font-size: 11px;
1378
+ color: var(--muted-foreground, #9aa0b0);
1379
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1380
+ overflow: hidden;
1381
+ text-overflow: ellipsis;
1382
+ white-space: nowrap;
1383
+ }
1384
+
1385
+ .vm-color-picker__sv {
1386
+ position: relative;
1387
+ width: 100%;
1388
+ aspect-ratio: 1;
1389
+ border-radius: 6px;
1390
+ cursor: crosshair;
1391
+ touch-action: none;
1392
+ overflow: hidden;
1393
+ }
1394
+
1395
+ .vm-color-picker__sv-cursor {
1396
+ position: absolute;
1397
+ width: 14px;
1398
+ height: 14px;
1399
+ margin: -7px 0 0 -7px;
1400
+ border: 2px solid #fff;
1401
+ border-radius: 50%;
1402
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
1403
+ pointer-events: none;
1404
+ }
1405
+
1406
+ .vm-color-picker__sliders {
1407
+ display: flex;
1408
+ align-items: center;
1409
+ gap: 10px;
1410
+ margin-top: 12px;
1411
+ }
1412
+
1413
+ .vm-color-picker__eyedropper {
1414
+ flex-shrink: 0;
1415
+ display: inline-flex;
1416
+ align-items: center;
1417
+ justify-content: center;
1418
+ width: 28px;
1419
+ height: 28px;
1420
+ padding: 0;
1421
+ border: 1px solid var(--border);
1422
+ border-radius: 6px;
1423
+ background: var(--muted);
1424
+ color: var(--foreground);
1425
+ cursor: pointer;
1426
+
1427
+ &:hover {
1428
+ border-color: var(--brand, #4e5dff);
1429
+ color: var(--brand, #4e5dff);
1430
+ }
1431
+ }
1432
+
1433
+ .vm-color-picker__eyedropper-ico {
1434
+ display: block;
1435
+ }
1436
+
1437
+ .vm-color-picker__slider-col {
1438
+ flex: 1;
1439
+ min-width: 0;
1440
+ display: flex;
1441
+ flex-direction: column;
1442
+ gap: 8px;
1443
+ }
1444
+
1445
+ .vm-color-picker__hue,
1446
+ .vm-color-picker__alpha {
1447
+ position: relative;
1448
+ height: 12px;
1449
+ border-radius: 999px;
1450
+ cursor: pointer;
1451
+ touch-action: none;
1452
+ /* 允许拇指画出轨道高度,但水平用 thumbLeft 钳在内 */
1453
+ overflow: visible;
1454
+ }
1455
+
1456
+ .vm-color-picker__hue {
1457
+ background: linear-gradient(
1458
+ to right,
1459
+ #f00 0%,
1460
+ #ff0 17%,
1461
+ #0f0 33%,
1462
+ #0ff 50%,
1463
+ #00f 67%,
1464
+ #f0f 83%,
1465
+ #f00 100%
1466
+ );
1467
+ }
1468
+
1469
+ .vm-color-picker__alpha {
1470
+ background-image:
1471
+ linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
1472
+ linear-gradient(-45deg, #c8c8c8 25%, transparent 25%),
1473
+ linear-gradient(45deg, transparent 75%, #c8c8c8 75%),
1474
+ linear-gradient(-45deg, transparent 75%, #c8c8c8 75%);
1475
+ background-size: 8px 8px;
1476
+ background-position:
1477
+ 0 0,
1478
+ 0 4px,
1479
+ 4px -4px,
1480
+ -4px 0;
1481
+ background-color: #fff;
1482
+ }
1483
+
1484
+ .vm-color-picker__alpha-fill {
1485
+ position: absolute;
1486
+ inset: 0;
1487
+ border-radius: inherit;
1488
+ pointer-events: none;
1489
+ overflow: hidden;
1490
+ }
1491
+
1492
+ .vm-color-picker__slider-thumb {
1493
+ position: absolute;
1494
+ top: 50%;
1495
+ width: 14px;
1496
+ height: 14px;
1497
+ transform: translate(-50%, -50%);
1498
+ border: 2px solid #fff;
1499
+ border-radius: 50%;
1500
+ background: transparent;
1501
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
1502
+ pointer-events: none;
1503
+ box-sizing: border-box;
1504
+ }
1505
+
1506
+ .vm-color-picker__footer {
1507
+ position: relative;
1508
+ display: flex;
1509
+ align-items: center;
1510
+ gap: 4px;
1511
+ margin-top: 12px;
1512
+ }
1513
+
1514
+ .vm-color-picker__format-wrap {
1515
+ position: relative;
1516
+ flex: 0 0 auto;
1517
+ max-width: 58px;
1518
+ }
1519
+
1520
+ .vm-color-picker__format {
1521
+ display: inline-flex;
1522
+ align-items: center;
1523
+ justify-content: space-between;
1524
+ gap: 0;
1525
+ width: 100%;
1526
+ height: 24px;
1527
+ padding: 0 4px 0 6px;
1528
+ border: 1px solid var(--border);
1529
+ border-radius: 6px;
1530
+ background: var(--muted);
1531
+ color: var(--foreground);
1532
+ font-size: 11px;
1533
+ line-height: 1;
1534
+ cursor: pointer;
1535
+
1536
+ &:hover,
1537
+ &.is-open {
1538
+ border-color: var(--brand, #4e5dff);
1539
+ }
1540
+
1541
+ svg {
1542
+ width: 12px;
1543
+ height: 12px;
1544
+ flex-shrink: 0;
1545
+ opacity: 0.7;
1546
+ }
1547
+ }
1548
+
1549
+ .vm-color-picker__format-menu {
1550
+ position: absolute;
1551
+ left: 0;
1552
+ bottom: calc(100% + 4px);
1553
+ z-index: 2;
1554
+ min-width: 96px;
1555
+ padding: 4px;
1556
+ border: 1px solid var(--border);
1557
+ border-radius: 8px;
1558
+ background: var(--popover, var(--background, #fff));
1559
+ box-shadow:
1560
+ 0 4px 6px -1px rgb(0 0 0 / 10%),
1561
+ 0 10px 20px -4px rgb(0 0 0 / 12%);
1562
+ }
1563
+
1564
+ .vm-color-picker__format-item {
1565
+ display: flex;
1566
+ align-items: center;
1567
+ gap: 6px;
1568
+ width: 100%;
1569
+ height: 26px;
1570
+ padding: 0 8px;
1571
+ border: none;
1572
+ border-radius: 6px;
1573
+ background: transparent;
1574
+ color: var(--foreground);
1575
+ font-size: 11px;
1576
+ text-align: left;
1577
+ cursor: pointer;
1578
+
1579
+ &:hover {
1580
+ background: color-mix(in srgb, var(--brand, #4e5dff) 12%, transparent);
1581
+ }
1582
+
1583
+ &.is-active {
1584
+ background: var(--brand, #4e5dff);
1585
+ color: #fff;
1586
+ }
1587
+ }
1588
+
1589
+ .vm-color-picker__format-check {
1590
+ display: inline-flex;
1591
+ width: 14px;
1592
+ height: 14px;
1593
+ flex-shrink: 0;
1594
+ align-items: center;
1595
+ justify-content: center;
1596
+ }
1597
+
1598
+ .vm-color-picker__value,
1599
+ .vm-color-picker__alpha-pct {
1600
+ height: 24px;
1601
+ padding: 0 6px;
1602
+ border: 1px solid var(--border);
1603
+ border-radius: 6px;
1604
+ background: var(--muted);
1605
+ color: var(--foreground);
1606
+ font-size: 11px;
1607
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1608
+ outline: none;
1609
+
1610
+ &:focus {
1611
+ border-color: var(--brand, #4e5dff);
1612
+ }
1613
+ }
1614
+
1615
+ .vm-color-picker__value {
1616
+ flex: 1 1 auto;
1617
+ min-width: 0;
1618
+ }
1619
+
1620
+ .vm-color-picker__alpha-pct {
1621
+ width: 42px;
1622
+ flex: 0 0 42px;
1623
+ padding: 0 4px;
1624
+ text-align: center;
1625
+ }
1626
+ </style>