Skip to content

Commit bbb1854

Browse files
committed
fix: isolate material folder icons
1 parent 4c80af5 commit bbb1854

3 files changed

Lines changed: 75 additions & 16 deletions

File tree

src/renderer/components/ui/folder-icon/FolderIcon.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { Variants } from './variants'
33
import { cn } from '@/utils'
44
import { Folder } from 'lucide-vue-next'
5-
import { materialIconInnerSvgClass, resolveFolderIcon } from './icons'
5+
import { resolveFolderIcon } from './icons'
66
import { variants } from './variants'
77
88
interface Props {
@@ -26,12 +26,13 @@ const iconClass = computed(() =>
2626
:class="iconClass"
2727
:data-icon-name="resolvedIcon.name"
2828
/>
29-
<div
30-
v-else-if="resolvedIcon?.svg"
31-
:class="cn(iconClass, materialIconInnerSvgClass)"
29+
<img
30+
v-else-if="resolvedIcon?.src"
31+
:alt="resolvedIcon.name"
32+
:class="cn(iconClass, 'object-contain')"
3233
:data-icon-name="resolvedIcon.name"
33-
v-html="resolvedIcon.svg"
34-
/>
34+
:src="resolvedIcon.src"
35+
>
3536
<Folder
3637
v-else
3738
:class="iconClass"

src/renderer/components/ui/folder-icon/__tests__/icons.test.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
createFolderIconValue,
44
getFilteredFolderIcons,
55
groupFolderIcons,
6-
materialIconInnerSvgClass,
76
parseFolderIconValue,
87
resolveFolderIcon,
98
} from '../icons'
@@ -114,8 +113,36 @@ describe('folder icon filtering', () => {
114113
})
115114

116115
describe('material icon rendering', () => {
117-
it('includes sizing classes for nested svg content', () => {
118-
expect(materialIconInnerSvgClass).toContain('[&_svg]:size-full')
119-
expect(materialIconInnerSvgClass).toContain('[&_svg]:block')
116+
it('resolves material icons to isolated data urls', () => {
117+
const icon = resolveFolderIcon('typescript')
118+
119+
expect(icon?.src).toMatch(/^data:image\/svg\+xml;charset=utf-8,/)
120+
expect(icon?.svg).toContain('<svg')
121+
})
122+
123+
it('prefixes inline svg ids to avoid collisions between icons', () => {
124+
const gitpod = resolveFolderIcon('gitpod')
125+
const vuexStore = resolveFolderIcon('vuex-store')
126+
127+
expect(gitpod?.svg).toContain('id="folder-icon-gitpod-a"')
128+
expect(gitpod?.svg).toContain('clip-path="url(#folder-icon-gitpod-a)"')
129+
expect(gitpod?.svg).not.toContain('clip-path="url(#a)"')
130+
131+
expect(vuexStore?.svg).toContain('id="folder-icon-vuex-store-a"')
132+
expect(vuexStore?.svg).toContain(
133+
'clip-path="url(#folder-icon-vuex-store-a)"',
134+
)
135+
expect(vuexStore?.svg).not.toContain('clip-path="url(#a)"')
136+
})
137+
138+
it('updates href references inside defs when ids are prefixed', () => {
139+
const docker = resolveFolderIcon('folder-docker')
140+
141+
expect(docker?.svg).toContain('id="folder-icon-folder-docker-a"')
142+
expect(docker?.svg).toContain('id="folder-icon-folder-docker-b"')
143+
expect(docker?.svg).toContain('xlink:href="#folder-icon-folder-docker-a"')
144+
expect(docker?.svg).toContain(
145+
'clip-path="url(#folder-icon-folder-docker-b)"',
146+
)
120147
})
121148
})

src/renderer/components/ui/folder-icon/icons.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ const files = import.meta.glob('@/assets/svg/icons/**.svg', {
77
query: '?raw',
88
})
99
const re = /\/([^/]+)\.svg$/
10-
const materialIconInnerSvgClass
11-
= '[&_svg]:block [&_svg]:size-full overflow-hidden'
12-
1310
type FolderIconSource = 'material' | 'lucide'
1411
type FolderIconFilter = 'all' | FolderIconSource
1512

@@ -23,6 +20,7 @@ interface FolderIconOption {
2320
name: string
2421
searchValue: string
2522
source: FolderIconSource
23+
src?: string
2624
svg?: string
2725
value: string
2826
}
@@ -45,7 +43,37 @@ function createFolderIconValue(source: FolderIconSource, name: string) {
4543
return `${source}:${name}`
4644
}
4745

46+
function escapeRegExp(value: string) {
47+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
48+
}
49+
50+
function sanitizeMaterialIconSvg(name: string, svg: string) {
51+
const prefix = `folder-icon-${name.replace(/[^\w-]/g, '-')}-`
52+
const ids = [...svg.matchAll(/\bid="([^"]+)"/g)].map(([, id]) => id)
53+
54+
if (ids.length === 0)
55+
return svg
56+
57+
return ids.reduce((result, id) => {
58+
const uniqueId = `${prefix}${id}`
59+
const escapedId = escapeRegExp(id)
60+
61+
return result
62+
.replace(new RegExp(`\\bid="${escapedId}"`, 'g'), `id="${uniqueId}"`)
63+
.replace(new RegExp(`url\\(#${escapedId}\\)`, 'g'), `url(#${uniqueId})`)
64+
.replace(
65+
new RegExp(`\\b(xlink:href|href)="#${escapedId}"`, 'g'),
66+
`$1="#${uniqueId}"`,
67+
)
68+
}, svg)
69+
}
70+
71+
function createMaterialIconSrc(svg: string) {
72+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
73+
}
74+
4875
const materialIconsSet: Record<string, string> = {}
76+
const materialIconSrcSet: Record<string, string> = {}
4977

5078
const materialIcons: FolderIconOption[] = Object.entries(files)
5179
.flatMap(([path, raw]) => {
@@ -54,15 +82,17 @@ const materialIcons: FolderIconOption[] = Object.entries(files)
5482
if (!name)
5583
return []
5684

57-
const svg = raw as string
85+
const svg = sanitizeMaterialIconSvg(name, raw as string)
5886

5987
materialIconsSet[name] = svg
88+
materialIconSrcSet[name] = createMaterialIconSrc(svg)
6089

6190
return [
6291
{
6392
name,
6493
searchValue: name.toLowerCase(),
6594
source: 'material' as const,
95+
src: materialIconSrcSet[name],
6696
svg,
6797
value: createFolderIconValue('material', name),
6898
},
@@ -135,14 +165,16 @@ function resolveFolderIcon(value?: string | null): FolderIconOption | null {
135165

136166
if (parsedValue.source === 'material') {
137167
const svg = materialIconsSet[parsedValue.name]
168+
const src = materialIconSrcSet[parsedValue.name]
138169

139-
if (!svg)
170+
if (!svg || !src)
140171
return null
141172

142173
return {
143174
name: parsedValue.name,
144175
searchValue: parsedValue.name.toLowerCase(),
145176
source: parsedValue.source,
177+
src,
146178
svg,
147179
value: createFolderIconValue(parsedValue.source, parsedValue.name),
148180
}
@@ -195,7 +227,6 @@ export {
195227
groupFolderIcons,
196228
materialIcons as icons,
197229
materialIconsSet as iconsSet,
198-
materialIconInnerSvgClass,
199230
materialIcons,
200231
materialIconsSet,
201232
parseFolderIconValue,

0 commit comments

Comments
 (0)