Skip to content

Commit 4f307fa

Browse files
committed
feat(NcIcon): add a generic component for icons
Signed-off-by: Grigorii K. Shartsev <[email protected]>
1 parent 74bfe86 commit 4f307fa

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

src/components/NcIcon/NcIcon.vue

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<docs>
7+
Universal component to render any icon. It can render:
8+
9+
- A Vue component icon like `vue-material-design-icons/Account.vue`
10+
- An SVG icon like `@mdi/svg/svg/account.svg`
11+
- A path icon like `@mdi/js/account.js`
12+
- An URL icon like `https://path/to/icon.svg` or `data:image/svg+xml;base64,...`
13+
- A class icon like `icon-account` (not recommended)
14+
15+
Using this component, you can develop a component with a single `icon`` prop and `#icon` slot to support all types of icons.
16+
17+
```vue
18+
<script>
19+
import IconAccountComp from 'vue-material-design-icons/Account.vue'
20+
import IconAccountSvg from '@mdi/svg/svg/account.svg?raw'
21+
import { mdiAccount } from '@mdi/js'
22+
23+
export default {
24+
setup() {
25+
return { IconAccountComp, IconAccountSvg, mdiAccount }
26+
},
27+
}
28+
</script>
29+
30+
<template>
31+
<div>
32+
<NcIcon :icon="IconAccountComp" />
33+
<NcIcon :icon="IconAccountSvg" />
34+
<NcIcon :icon="mdiAccount" />
35+
<NcIcon icon="icon-user-white" />
36+
</div>
37+
</template>
38+
```
39+
</docs>
40+
41+
<script setup>
42+
import { computed } from 'vue'
43+
import { useIcon } from './useIcon.ts'
44+
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
45+
46+
const props = defineProps({
47+
icon: { type: [Object, String], required: true },
48+
size: { type: [Number, String], default: 20 },
49+
inline: { type: Boolean, default: false },
50+
})
51+
52+
const normalizedIcon = useIcon(() => props.icon)
53+
const normalizedSize = computed(() => typeof props.size === 'number' ? `${props.size}px` : props.size)
54+
</script>
55+
56+
<template>
57+
<component :is="icon" v-if="normalizedIcon.type === 'component'" :size="normalizedSize" />
58+
<NcIconSvgWrapper v-else-if="normalizedIcon.type === 'svg'" :svg="normalizedIcon.icon" :inline="inline" />
59+
<NcIconSvgWrapper v-else-if="normalizedIcon.type === 'path'" :path="normalizedIcon.icon" :inline="inline" />
60+
<span v-else-if="normalizedIcon.type === 'class'"
61+
class="icon"
62+
:class="[normalizedIcon.icon, { inline }]"
63+
:style="'--icon-size: ' + normalizedSize"
64+
aria-hidden="true" />
65+
<span v-else-if="normalizedIcon.type === 'url'"
66+
class="icon icon-url"
67+
:class="{ inline }"
68+
:style="`background-image: url('${normalizedIcon.icon}')`"
69+
aria-hidden="true" />
70+
</template>
71+
72+
<style scoped>
73+
.icon {
74+
display: flex;
75+
justify-content: center;
76+
align-items: center;
77+
min-width: var(--default-clickable-area);
78+
min-height: var(--default-clickable-area);
79+
height: var(--icon-size);
80+
width: var(--icon-size);
81+
82+
&.inline {
83+
display: inline-flex;
84+
min-width: fit-content;
85+
min-height: fit-content;
86+
vertical-align: text-bottom;
87+
}
88+
}
89+
</style>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Component } from 'vue'
7+
8+
export type IconComponent = Component
9+
export type IconPath = `M${number}${string}`
10+
export type IconSvg = `<svg${string}>${string}</svg>`
11+
export type IconUrl = `http://${string}` | `https://${string}` | `data:${string}`
12+
export type IconClass = `icon-${string}`
13+
export type IconGeneral = IconComponent | IconPath | IconClass | IconSvg | IconUrl
14+
15+
export type IconNormalized = { type: 'component', icon: IconComponent }
16+
| { type: 'path', icon: IconPath }
17+
| { type: 'svg', icon: IconSvg }
18+
| { type: 'url', icon: IconUrl }
19+
| { type: 'class', icon: IconClass }
20+
| { type: 'unknown', icon: IconGeneral }
21+
22+
/**
23+
*
24+
* @param icon - Icon in any supported format
25+
*/
26+
export function normalizeIcon(icon: IconGeneral): IconNormalized {
27+
if (typeof icon === 'object' || typeof icon === 'function') {
28+
return { type: 'component', icon: icon as IconComponent }
29+
}
30+
if (icon.startsWith('<svg')) {
31+
return { type: 'svg', icon: icon as IconSvg }
32+
}
33+
if (icon.startsWith('M')) {
34+
return { type: 'path', icon: icon as IconPath }
35+
}
36+
if (icon.startsWith('http://') || icon.startsWith('https://') || icon.startsWith('data:')) {
37+
return { type: 'url', icon: icon as IconUrl }
38+
}
39+
if (icon.startsWith('icon-')) {
40+
return { type: 'class', icon: icon as IconClass }
41+
}
42+
return { type: 'unknown', icon: icon as IconGeneral }
43+
}

src/components/NcIcon/useIcon.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { ComputedRef } from 'vue'
7+
import type { MaybeRefOrGetter } from '@vueuse/core'
8+
import type { IconGeneral, IconNormalized } from './normalizeIcon.ts'
9+
import { computed } from 'vue'
10+
import { toValue } from '@vueuse/core'
11+
import { normalizeIcon } from './normalizeIcon.ts'
12+
13+
/**
14+
* Reactive normalizeIcon
15+
* @param icon - Icon in any supported format
16+
*/
17+
export function useIcon(icon: MaybeRefOrGetter<IconGeneral>): ComputedRef<IconNormalized> {
18+
return computed(() => normalizeIcon(toValue(icon)))
19+
}

0 commit comments

Comments
 (0)