@betterstart/cli 0.1.2 → 0.1.4

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 (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -354
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -260
  6. package/dist/index.js +4 -11373
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-G4KI4DVB.js +0 -179
  215. package/dist/chunk-G4KI4DVB.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,435 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@cms/utils/cn'
4
+ import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual'
5
+ import Fuse from 'fuse.js'
6
+ import type { LucideIcon, LucideProps } from 'lucide-react'
7
+ import { DynamicIcon, dynamicIconImports, type IconName } from 'lucide-react/dynamic'
8
+ import * as React from 'react'
9
+ import { useCallback, useEffect, useMemo, useState } from 'react'
10
+ import { useDebounceValue } from 'usehooks-ts'
11
+ import { Button } from './button'
12
+ import type { iconsData } from './icons-data'
13
+ import { Input } from './input'
14
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
15
+ import { Skeleton } from './skeleton'
16
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
17
+
18
+ export type IconData = (typeof iconsData)[number]
19
+
20
+ interface IconPickerProps
21
+ extends Omit<React.ComponentPropsWithoutRef<typeof PopoverTrigger>, 'onSelect' | 'onOpenChange'> {
22
+ value?: IconName
23
+ defaultValue?: IconName
24
+ onValueChange?: (value: IconName) => void
25
+ open?: boolean
26
+ defaultOpen?: boolean
27
+ onOpenChange?: (open: boolean) => void
28
+ searchable?: boolean
29
+ searchPlaceholder?: string
30
+ triggerPlaceholder?: string
31
+ iconsList?: IconData[]
32
+ categorized?: boolean
33
+ modal?: boolean
34
+ }
35
+
36
+ const IconRenderer = React.memo(({ name }: { name: IconName }) => {
37
+ return <Icon name={name} />
38
+ })
39
+ IconRenderer.displayName = 'IconRenderer'
40
+
41
+ const IconsColumnSkeleton = () => {
42
+ return (
43
+ <div className="flex flex-col gap-2 w-full">
44
+ <Skeleton className="h-4 w-1/2 rounded-md corner-squircle" />
45
+ <div className="grid grid-cols-5 gap-2 w-full">
46
+ {Array.from({ length: 40 }).map((_, i) => (
47
+ <Skeleton key={i} className="h-10 w-10 rounded-md corner-squircle" />
48
+ ))}
49
+ </div>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ const useIconsData = () => {
55
+ const [icons, setIcons] = useState<IconData[]>([])
56
+ const [isLoading, setIsLoading] = useState(true)
57
+
58
+ useEffect(() => {
59
+ let isMounted = true
60
+
61
+ const loadIcons = async () => {
62
+ setIsLoading(true)
63
+
64
+ const { iconsData } = await import('./icons-data')
65
+ if (isMounted) {
66
+ setIcons(
67
+ iconsData.filter((icon: IconData) => {
68
+ return icon.name in dynamicIconImports
69
+ })
70
+ )
71
+ setIsLoading(false)
72
+ }
73
+ }
74
+
75
+ loadIcons()
76
+
77
+ return () => {
78
+ isMounted = false
79
+ }
80
+ }, [])
81
+
82
+ return { icons, isLoading }
83
+ }
84
+
85
+ const IconPicker = React.forwardRef<React.ComponentRef<typeof PopoverTrigger>, IconPickerProps>(
86
+ (
87
+ {
88
+ value,
89
+ defaultValue,
90
+ onValueChange,
91
+ open,
92
+ defaultOpen,
93
+ onOpenChange,
94
+ children,
95
+ searchable = true,
96
+ searchPlaceholder = 'Search for an icon...',
97
+ triggerPlaceholder = 'Select an Icon',
98
+ iconsList,
99
+ categorized = true,
100
+ modal = false,
101
+ ...props
102
+ },
103
+ ref
104
+ ) => {
105
+ const [selectedIcon, setSelectedIcon] = useState<IconName | undefined>(defaultValue)
106
+ const [isOpen, setIsOpen] = useState(defaultOpen || false)
107
+ const [search, setSearch] = useDebounceValue('', 100)
108
+ const [isPopoverVisible, setIsPopoverVisible] = useState(false)
109
+ const { icons } = useIconsData()
110
+ const [isLoading, setIsLoading] = useState(true)
111
+
112
+ const iconsToUse = useMemo(() => iconsList || icons, [iconsList, icons])
113
+
114
+ const fuseInstance = useMemo(() => {
115
+ return new Fuse(iconsToUse, {
116
+ keys: ['name', 'tags', 'categories'],
117
+ threshold: 0.3,
118
+ ignoreLocation: true,
119
+ includeScore: true
120
+ })
121
+ }, [iconsToUse])
122
+
123
+ const filteredIcons = useMemo(() => {
124
+ if (search.trim() === '') {
125
+ return iconsToUse
126
+ }
127
+
128
+ const results = fuseInstance.search(search.toLowerCase().trim())
129
+ return results.map((result) => result.item)
130
+ }, [search, iconsToUse, fuseInstance])
131
+
132
+ const categorizedIcons = useMemo(() => {
133
+ if (!categorized || search.trim() !== '') {
134
+ return [{ name: 'All Icons', icons: filteredIcons }]
135
+ }
136
+
137
+ const categories = new Map<string, IconData[]>()
138
+
139
+ filteredIcons.forEach((icon) => {
140
+ if (icon.categories && icon.categories.length > 0) {
141
+ icon.categories.forEach((category) => {
142
+ if (!categories.has(category)) {
143
+ categories.set(category, [])
144
+ }
145
+ categories.get(category)!.push(icon)
146
+ })
147
+ } else {
148
+ const category = 'Other'
149
+ if (!categories.has(category)) {
150
+ categories.set(category, [])
151
+ }
152
+ categories.get(category)!.push(icon)
153
+ }
154
+ })
155
+
156
+ return Array.from(categories.entries())
157
+ .map(([name, icons]) => ({ name, icons }))
158
+ .sort((a, b) => a.name.localeCompare(b.name))
159
+ }, [filteredIcons, categorized, search])
160
+
161
+ const virtualItems = useMemo(() => {
162
+ const items: Array<{
163
+ type: 'category' | 'row'
164
+ categoryIndex: number
165
+ rowIndex?: number
166
+ icons?: IconData[]
167
+ }> = []
168
+
169
+ categorizedIcons.forEach((category, categoryIndex) => {
170
+ items.push({ type: 'category', categoryIndex })
171
+
172
+ const rows = []
173
+ for (let i = 0; i < category.icons.length; i += 5) {
174
+ rows.push(category.icons.slice(i, i + 5))
175
+ }
176
+
177
+ rows.forEach((rowIcons, rowIndex) => {
178
+ items.push({
179
+ type: 'row',
180
+ categoryIndex,
181
+ rowIndex,
182
+ icons: rowIcons
183
+ })
184
+ })
185
+ })
186
+
187
+ return items
188
+ }, [categorizedIcons])
189
+
190
+ const categoryIndices = useMemo(() => {
191
+ const indices: Record<string, number> = {}
192
+
193
+ virtualItems.forEach((item, index) => {
194
+ if (item.type === 'category') {
195
+ indices[categorizedIcons[item.categoryIndex].name] = index
196
+ }
197
+ })
198
+
199
+ return indices
200
+ }, [virtualItems, categorizedIcons])
201
+
202
+ const parentRef = React.useRef<HTMLDivElement>(null)
203
+
204
+ const virtualizer = useVirtualizer({
205
+ count: virtualItems.length,
206
+ getScrollElement: () => parentRef.current,
207
+ estimateSize: (index) => (virtualItems[index].type === 'category' ? 25 : 40),
208
+ paddingEnd: 2,
209
+ gap: 10,
210
+ overscan: 5
211
+ })
212
+
213
+ const handleValueChange = useCallback(
214
+ (icon: IconName) => {
215
+ if (value === undefined) {
216
+ setSelectedIcon(icon)
217
+ }
218
+ onValueChange?.(icon)
219
+ },
220
+ [value, onValueChange]
221
+ )
222
+
223
+ const handleOpenChange = useCallback(
224
+ (newOpen: boolean) => {
225
+ setSearch('')
226
+ if (open === undefined) {
227
+ setIsOpen(newOpen)
228
+ }
229
+ onOpenChange?.(newOpen)
230
+
231
+ setIsPopoverVisible(newOpen)
232
+
233
+ if (newOpen) {
234
+ setTimeout(() => {
235
+ virtualizer.measure()
236
+ setIsLoading(false)
237
+ }, 1)
238
+ }
239
+ },
240
+ [open, onOpenChange, virtualizer, setSearch]
241
+ )
242
+
243
+ const handleIconClick = useCallback(
244
+ (iconName: IconName) => {
245
+ handleValueChange(iconName)
246
+ setIsOpen(false)
247
+ setSearch('')
248
+ },
249
+ [handleValueChange, setSearch]
250
+ )
251
+
252
+ const handleSearchChange = useCallback(
253
+ (e: React.ChangeEvent<HTMLInputElement>) => {
254
+ setSearch(e.target.value)
255
+
256
+ if (parentRef.current) {
257
+ parentRef.current.scrollTop = 0
258
+ }
259
+
260
+ virtualizer.scrollToOffset(0)
261
+ },
262
+ [virtualizer, setSearch]
263
+ )
264
+
265
+ const scrollToCategory = useCallback(
266
+ (categoryName: string) => {
267
+ const categoryIndex = categoryIndices[categoryName]
268
+
269
+ if (categoryIndex !== undefined && virtualizer) {
270
+ virtualizer.scrollToIndex(categoryIndex, {
271
+ align: 'start',
272
+ behavior: 'smooth'
273
+ })
274
+ }
275
+ },
276
+ [categoryIndices, virtualizer]
277
+ )
278
+
279
+ const categoryButtons = useMemo(() => {
280
+ if (!categorized || search.trim() !== '') return null
281
+
282
+ return categorizedIcons.map((category) => (
283
+ <Button
284
+ key={category.name}
285
+ variant={'outline'}
286
+ size="sm"
287
+ className="text-xs"
288
+ onClick={(e) => {
289
+ e.stopPropagation()
290
+ scrollToCategory(category.name)
291
+ }}
292
+ >
293
+ {category.name.charAt(0).toUpperCase() + category.name.slice(1)}
294
+ </Button>
295
+ ))
296
+ }, [categorizedIcons, scrollToCategory, categorized, search])
297
+
298
+ const renderIcon = useCallback(
299
+ (icon: IconData) => (
300
+ <TooltipProvider key={icon.name}>
301
+ <Tooltip>
302
+ <TooltipTrigger
303
+ className={cn(
304
+ 'p-2 rounded-md corner-squircle border hover:bg-foreground/10 transition',
305
+ 'flex items-center justify-center'
306
+ )}
307
+ onClick={() => handleIconClick(icon.name as IconName)}
308
+ >
309
+ <IconRenderer name={icon.name as IconName} />
310
+ </TooltipTrigger>
311
+ <TooltipContent>
312
+ <p>{icon.name}</p>
313
+ </TooltipContent>
314
+ </Tooltip>
315
+ </TooltipProvider>
316
+ ),
317
+ [handleIconClick]
318
+ )
319
+
320
+ const renderVirtualContent = useCallback(() => {
321
+ if (filteredIcons.length === 0) {
322
+ return <div className="text-center text-muted-foreground">No icon found</div>
323
+ }
324
+
325
+ return (
326
+ <div
327
+ className="relative w-full overscroll-contain"
328
+ style={{
329
+ height: `${virtualizer.getTotalSize()}px`
330
+ }}
331
+ >
332
+ {virtualizer.getVirtualItems().map((virtualItem: VirtualItem) => {
333
+ const item = virtualItems[virtualItem.index]
334
+
335
+ if (!item) return null
336
+
337
+ const itemStyle = {
338
+ position: 'absolute' as const,
339
+ top: 0,
340
+ left: 0,
341
+ width: '100%',
342
+ height: `${virtualItem.size}px`,
343
+ transform: `translateY(${virtualItem.start}px)`
344
+ }
345
+
346
+ if (item.type === 'category') {
347
+ return (
348
+ <div key={virtualItem.key} style={itemStyle} className="top-0 bg-background z-10">
349
+ <h3 className="font-medium text-sm capitalize">
350
+ {categorizedIcons[item.categoryIndex].name}
351
+ </h3>
352
+ <div className="h-[1px] bg-foreground/10 w-full" />
353
+ </div>
354
+ )
355
+ }
356
+
357
+ return (
358
+ <div key={virtualItem.key} data-index={virtualItem.index} style={itemStyle}>
359
+ <div className="grid grid-cols-5 gap-2 w-full">{item.icons!.map(renderIcon)}</div>
360
+ </div>
361
+ )
362
+ })}
363
+ </div>
364
+ )
365
+ }, [virtualizer, virtualItems, categorizedIcons, filteredIcons, renderIcon])
366
+
367
+ React.useEffect(() => {
368
+ if (isPopoverVisible) {
369
+ setIsLoading(true)
370
+ const timer = setTimeout(() => {
371
+ setIsLoading(false)
372
+ virtualizer.measure()
373
+ }, 10)
374
+
375
+ const resizeObserver = new ResizeObserver(() => {
376
+ virtualizer.measure()
377
+ })
378
+
379
+ if (parentRef.current) {
380
+ resizeObserver.observe(parentRef.current)
381
+ }
382
+
383
+ return () => {
384
+ clearTimeout(timer)
385
+ resizeObserver.disconnect()
386
+ }
387
+ }
388
+ }, [isPopoverVisible, virtualizer])
389
+
390
+ return (
391
+ <Popover open={open ?? isOpen} onOpenChange={handleOpenChange} modal={modal}>
392
+ <PopoverTrigger ref={ref} asChild {...props}>
393
+ {children || (
394
+ <Button variant="outline" size="lg">
395
+ {value || selectedIcon ? (
396
+ <Icon name={(value || selectedIcon)!} />
397
+ ) : (
398
+ triggerPlaceholder
399
+ )}
400
+ </Button>
401
+ )}
402
+ </PopoverTrigger>
403
+ <PopoverContent className="w-64 p-2">
404
+ {searchable && (
405
+ <Input placeholder={searchPlaceholder} onChange={handleSearchChange} className="mb-2" />
406
+ )}
407
+ {categorized && search.trim() === '' && (
408
+ <div className="flex flex-row gap-1 mt-2 overflow-x-auto pb-2">{categoryButtons}</div>
409
+ )}
410
+ <div
411
+ ref={parentRef}
412
+ className="max-h-60 overflow-auto"
413
+ style={{ scrollbarWidth: 'thin' }}
414
+ >
415
+ {isLoading ? <IconsColumnSkeleton /> : renderVirtualContent()}
416
+ </div>
417
+ </PopoverContent>
418
+ </Popover>
419
+ )
420
+ }
421
+ )
422
+ IconPicker.displayName = 'IconPicker'
423
+
424
+ interface IconProps extends Omit<LucideProps, 'ref'> {
425
+ name: IconName
426
+ }
427
+
428
+ const Icon = React.forwardRef<React.ComponentRef<LucideIcon>, IconProps>(
429
+ ({ name, ...props }, ref) => {
430
+ return <DynamicIcon name={name} {...props} ref={ref} />
431
+ }
432
+ )
433
+ Icon.displayName = 'Icon'
434
+
435
+ export { IconPicker, Icon, type IconName }
@@ -0,0 +1,6 @@
1
+ import { dynamicIconImports } from 'lucide-react/dynamic'
2
+
3
+ export const iconsData = Object.keys(dynamicIconImports).map((name) => ({
4
+ name,
5
+ categories: [] as string[]
6
+ }))