Skip to content

Commit fde18d4

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

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

src/components/NcIcon/NcIcon.vue

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