11<script setup lang="ts">
2- import UiInput from ' ~/renderer/components/ui/input/Input.vue'
2+ import {
3+ type FolderIconFilter ,
4+ type FolderIconSource ,
5+ getFilteredFolderIcons ,
6+ groupFolderIcons ,
7+ } from ' @/components/ui/folder-icon/icons'
8+ import { i18n } from ' @/electron'
39import { useFolders } from ' ~/renderer/composables'
4- import { icons , iconsSet } from ' ./icons'
510
611interface Props {
712 nodeId: number
@@ -13,20 +18,28 @@ const props = defineProps<Props>()
1318const { updateFolder, getFolders } = useFolders ()
1419
1520const search = ref (' ' )
21+ const filter = ref <FolderIconFilter >(' material' )
1622
1723const containerRef = useTemplateRef (' containerRef' )
24+ const listRef = useTemplateRef (' listRef' )
1825
19- const iconsBySearch = computed (() => {
20- if (search .value === ' ' ) {
21- return icons
22- }
23- return icons .filter (i => i .name ?.includes (search .value .toLowerCase ()))
24- })
26+ const filterOptions = computed <
27+ Array < { label : string , value : FolderIconFilter }>
28+ > (() => [
29+ { label: i18n .t (' folder.iconPicker.filters.material' ), value: ' material' },
30+ { label: i18n .t (' folder.iconPicker.filters.lucide' ), value: ' lucide' },
31+ ])
32+
33+ const visibleIcons = computed (() =>
34+ getFilteredFolderIcons (search .value , filter .value ),
35+ )
36+
37+ const iconSections = computed (() => groupFolderIcons (visibleIcons .value ))
2538
2639const selectedIndex = ref (- 1 )
2740
2841function onKeydown(e : KeyboardEvent ) {
29- const len = iconsBySearch .value .length
42+ const len = visibleIcons .value .length
3043
3144 if (e .key === ' ArrowDown' ) {
3245 e .preventDefault ()
@@ -42,19 +55,28 @@ function onKeydown(e: KeyboardEvent) {
4255 }
4356 else if (e .key === ' Enter' ) {
4457 e .preventDefault ()
45- onSet (iconsBySearch .value [selectedIndex .value ].name ! )
58+
59+ const icon = visibleIcons .value [selectedIndex .value ]
60+
61+ if (icon ) {
62+ onSet (icon .value )
63+ }
4664 }
4765}
4866
49- async function onSet(name : string ) {
67+ function getSectionTitle(source : FolderIconSource ) {
68+ return i18n .t (` folder.iconPicker.filters.${source } ` )
69+ }
70+
71+ async function onSet(value : string ) {
5072 if (! props .nodeId )
5173 return
5274
5375 if (props .onSetIcon ) {
54- await props .onSetIcon (props .nodeId , name )
76+ await props .onSetIcon (props .nodeId , value )
5577 }
5678 else {
57- await updateFolder (props .nodeId , { icon: name })
79+ await updateFolder (props .nodeId , { icon: value })
5880 await getFolders ()
5981 }
6082
@@ -63,17 +85,21 @@ async function onSet(name: string) {
6385 )
6486}
6587
66- watch (
67- () => search .value ,
68- () => {
69- selectedIndex .value = - 1
70- },
71- )
88+ watch (search , () => {
89+ selectedIndex .value = - 1
90+ })
91+
92+ watch (filter , () => {
93+ selectedIndex .value = - 1
94+ nextTick (() => {
95+ listRef .value ?.scrollTo ({ top: 0 })
96+ })
97+ })
7298
7399watch (
74- () => iconsBySearch . value ,
100+ visibleIcons ,
75101 () => {
76- if (selectedIndex .value >= iconsBySearch .value .length ) {
102+ if (selectedIndex .value >= visibleIcons .value .length ) {
77103 selectedIndex .value = - 1
78104 }
79105 },
@@ -96,29 +122,82 @@ watch(selectedIndex, () => {
96122 ref =" containerRef"
97123 class =" space-y-5"
98124 >
99- <div >
125+ <div class = " space-y-3 " >
100126 <UiInput
101127 v-model =" search"
102- placeholder =" Search... "
128+ : placeholder =" i18n.t('folder.iconPicker.searchPlaceholder') "
103129 @keydown =" onKeydown"
104130 />
131+ <UiShadcnTabs
132+ v-model =" filter"
133+ class =" gap-0"
134+ >
135+ <UiShadcnTabsList >
136+ <UiShadcnTabsTrigger
137+ v-for =" item in filterOptions"
138+ :key =" item.value"
139+ :value =" item.value"
140+ >
141+ {{ item.label }}
142+ </UiShadcnTabsTrigger >
143+ </UiShadcnTabsList >
144+ </UiShadcnTabs >
105145 </div >
106- <div class =" scrollbar max-h-[200px] overflow-y-auto" >
107- <div class =" grid auto-rows-[36px] grid-cols-8 gap-2" >
146+ <div
147+ ref =" listRef"
148+ class =" scrollbar max-h-[280px] overflow-y-auto"
149+ >
150+ <div
151+ v-if =" iconSections.length"
152+ class =" space-y-4"
153+ >
108154 <div
109- v-for =" (icon, index) in iconsBySearch"
110- :id =" `icon-${index}`"
111- :key =" icon.name"
112- class =" user-select-none flex items-center justify-center rounded-md"
113- :class =" index === selectedIndex ? 'bg-muted' : 'hover:bg-muted'"
114- @click =" onSet(icon.name!)"
155+ v-for =" section in iconSections"
156+ :key =" section.source"
157+ class =" space-y-2"
115158 >
116- <span
117- class =" *:size-5"
118- v-html =" iconsSet[icon.name!]"
119- />
159+ <UiText
160+ as =" div"
161+ variant =" xs"
162+ weight =" medium"
163+ muted
164+ uppercase
165+ class =" px-1 tracking-[0.08em]"
166+ >
167+ {{ getSectionTitle(section.source) }}
168+ </UiText >
169+ <div class =" grid auto-rows-[36px] grid-cols-8 gap-2" >
170+ <div
171+ v-for =" icon in section.items"
172+ :id =" `icon-${visibleIcons.findIndex((item) => item.value === icon.value)}`"
173+ :key =" icon.value"
174+ class =" user-select-none flex items-center justify-center rounded-md"
175+ :class ="
176+ visibleIcons[selectedIndex]?.value === icon.value
177+ ? 'bg-muted'
178+ : 'hover:bg-muted'
179+ "
180+ @click =" onSet(icon.value)"
181+ >
182+ <UiFolderIcon
183+ :name =" icon.value"
184+ class =" size-5"
185+ />
186+ </div >
187+ </div >
120188 </div >
121189 </div >
190+ <div
191+ v-else
192+ class =" flex min-h-24 items-center justify-center px-4 text-center"
193+ >
194+ <UiText
195+ variant =" sm"
196+ muted
197+ >
198+ {{ i18n.t("folder.iconPicker.emptyResults") }}
199+ </UiText >
200+ </div >
122201 </div >
123202 </div >
124203</template >
0 commit comments