Skip to content

Commit 01fc5ae

Browse files
feat: add lucide folder icons (#740)
* feat: add lucide support for folder icons * fix: reset icon picker scroll on tab switch
1 parent 8da2323 commit 01fc5ae

File tree

7 files changed

+475
-69
lines changed

7 files changed

+475
-69
lines changed

src/main/i18n/locales/en_US/ui.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,16 @@
110110
}
111111
},
112112
"folder": {
113-
"untitled": "Untitled folder"
113+
"untitled": "Untitled folder",
114+
"iconPicker": {
115+
"searchPlaceholder": "Search icons...",
116+
"emptyResults": "No icons found",
117+
"filters": {
118+
"all": "All",
119+
"material": "Material",
120+
"lucide": "Lucide"
121+
}
122+
}
114123
},
115124
"button": {
116125
"back": "Back",

src/main/i18n/locales/ru_RU/ui.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,16 @@
110110
}
111111
},
112112
"folder": {
113-
"untitled": "Папка без названия"
113+
"untitled": "Папка без названия",
114+
"iconPicker": {
115+
"searchPlaceholder": "Поиск иконок...",
116+
"emptyResults": "Иконки не найдены",
117+
"filters": {
118+
"all": "Все",
119+
"material": "Material",
120+
"lucide": "Lucide"
121+
}
122+
}
114123
},
115124
"button": {
116125
"back": "Назад",
Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
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'
39
import { useFolders } from '~/renderer/composables'
4-
import { icons, iconsSet } from './icons'
510
611
interface Props {
712
nodeId: number
@@ -13,20 +18,28 @@ const props = defineProps<Props>()
1318
const { updateFolder, getFolders } = useFolders()
1419
1520
const search = ref('')
21+
const filter = ref<FolderIconFilter>('material')
1622
1723
const 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
2639
const selectedIndex = ref(-1)
2740
2841
function 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
7399
watch(
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>

src/renderer/components/sidebar/folders/custom-icons/icons.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script setup lang="ts">
22
import type { Variants } from './variants'
33
import { cn } from '@/utils'
4-
import { iconsSet } from './icons'
4+
import { Folder } from 'lucide-vue-next'
5+
import { materialIconInnerSvgClass, resolveFolderIcon } from './icons'
56
import { variants } from './variants'
67
78
interface Props {
@@ -11,12 +12,29 @@ interface Props {
1112
}
1213
1314
const props = defineProps<Props>()
15+
16+
const resolvedIcon = computed(() => resolveFolderIcon(props.name))
17+
const iconClass = computed(() =>
18+
cn(variants({ size: props.size }), props.class),
19+
)
1420
</script>
1521

1622
<template>
23+
<component
24+
:is="resolvedIcon.component"
25+
v-if="resolvedIcon?.component"
26+
:class="iconClass"
27+
:data-icon-name="resolvedIcon.name"
28+
/>
1729
<div
18-
:class="cn(variants({ size }), props.class)"
30+
v-else-if="resolvedIcon?.svg"
31+
:class="cn(iconClass, materialIconInnerSvgClass)"
32+
:data-icon-name="resolvedIcon.name"
33+
v-html="resolvedIcon.svg"
34+
/>
35+
<Folder
36+
v-else
37+
:class="iconClass"
1938
:data-icon-name="name"
20-
v-html="iconsSet[name]"
2139
/>
2240
</template>

0 commit comments

Comments
 (0)