Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SVG Sprite Module for icons #1808

Merged
merged 13 commits into from Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 66 additions & 0 deletions documentation/guides/frontend/icons.md
@@ -0,0 +1,66 @@
# SVG Icon Sprites in Openverse Frontend

We use SVG for icons in Openverse frontend because it allows for high-quality
graphics, easy scalability without losing quality, and small file sizes. To
reduce the number of HTTP requests for icons and reduce load times, we combine
all of them into a single file called SVG sprite. An SVG sprite is a single SVG
file that contains multiple SVG symbols. Each symbol can be accessed using its
unique ID.

This guide explains how to use SVG sprites in Openverse Frontend and how to add
a new SVG file to the sprite.

## Using SVG Sprites in Openverse Frontend

You can use SVG sprites through the `VIcon` component. To use it, first, import
it into your component:

```js
import { VIcon } from "~/components/VIcon"
```

Then, add the VIcon component to your template, passing the name of the icon as
a prop:

```html
<template>
<VIcon name="arrow-up" />
</template>
```

The name prop is used to identify the icon within the SVG sprite. You can find
the names of the available icons in the `frontend/src/assets/icons/sprites.json`
file. The default icons listed in the object with the name `icons` use the name
as is, without prefixes. To use the license icons, you would need to add
`license/` as a prefix to the name. For example, to use an `external-link` icon,
you would use the name `external-link`, and for the `cc-by` icon, you would use
the name `license/cc-by`.

You can also view a list of all available icons if you go to
`http://localhost:8443/_icons`. You can copy the icon name (with the prefix, if
obulat marked this conversation as resolved.
Show resolved Hide resolved
necessary) by clicking on the icon. This page is only available in development
environment and is not available in production.

## Adding New Icons to the Sprite

Openverse uses
[svg-sprite-module](https://github.com/nuxt-community/svg-sprite-module) to
automatically generate the SVG sprite when you add an SVG file to
`frontned/src/assets/icons/raw/`. It runs when you run `just frontend/run dev`
or `just frontend/run build`. The module also watches the content of the
`frontend/src/assets/icons/raw` directory, and re-generates the sprite if you
add an icon there, so you don't need to re-run the app.

So, to add a new icon to the SVG sprite, you only need to create an SVG file
containing the icon in `frontend/src/assets/icons/raw/` directory. The name of
the file without the `.svg` extension will be used in the component. There are
several requirements for the file:

- It should contain the SVG code for the icon, with a `path` with `id` icon that
wraps the inner contents.
- The color of the icon should be `currentColor`. This allows the icon to
inherit the color of the parent element. Also, remove white backgrounds from
the icon.
- It should have a `viewBox` attribute with the dimensions of the icon.
Openverse uses 24x24 icons. If you are exporting the icon from Figma, resize
the containing frame to 24x24 before exporting.
10 changes: 9 additions & 1 deletion frontend/.storybook/main.js
@@ -1,6 +1,6 @@
const { nuxifyStorybook } = require("../.nuxt-storybook/storybook/main")

module.exports = nuxifyStorybook({
const storybook = nuxifyStorybook({
webpackFinal(config) {
// extend config here

Expand All @@ -13,3 +13,11 @@ module.exports = nuxifyStorybook({
// Add your addons here
],
})

const generatedIconsStory = storybook.stories.indexOf(
"@nuxtjs/svg-sprite/stories/*.stories.js"
)
storybook.stories[generatedIconsStory] =
"../node_modules/@nuxtjs/svg-sprite/stories/*.stories.js"

module.exports = storybook
5 changes: 5 additions & 0 deletions frontend/nuxt.config.ts
Expand Up @@ -155,6 +155,7 @@ const config: NuxtConfig = {
"@nuxtjs/composition-api/module",
"@nuxtjs/style-resources",
"@nuxtjs/svg",
"@nuxtjs/svg-sprite",
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
"@nuxtjs/eslint-module",
"@pinia/nuxt",
],
Expand All @@ -174,6 +175,10 @@ const config: NuxtConfig = {
{ path: "/healthcheck", handler: "~/server-middleware/healthcheck.js" },
{ path: "/robots.txt", handler: "~/server-middleware/robots.js" },
],
svgSprite: {
input: "~/assets/svg/raw",
output: "~/assets/svg/sprite",
},
i18n: {
baseUrl: "https://openverse.org",
locales: openverseLocales,
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Expand Up @@ -68,6 +68,7 @@
"@nuxtjs/sentry": "^7.1.4",
"@nuxtjs/sitemap": "^2.4.0",
"@nuxtjs/svg": "^0.4.0",
"@nuxtjs/svg-sprite": "0.5.2",
"@pinia/nuxt": "0.2.1",
"@popperjs/core": "^2.11.2",
"@tailwindcss/typography": "^0.5.9",
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/assets/icons/audio-wave.svg

This file was deleted.

6 changes: 0 additions & 6 deletions frontend/src/assets/icons/image.svg

This file was deleted.

1 change: 0 additions & 1 deletion frontend/src/assets/icons/source.svg

This file was deleted.

5 changes: 5 additions & 0 deletions frontend/src/assets/svg/raw/licenses/share.svg
5 changes: 5 additions & 0 deletions frontend/src/assets/svg/raw/radiomark.svg
1 change: 1 addition & 0 deletions frontend/src/assets/svg/raw/source.svg
12 changes: 4 additions & 8 deletions frontend/src/components/VAudioTrack/VPlayPause.vue
Expand Up @@ -3,7 +3,7 @@
v-bind="$attrs"
:tabindex="isTabbable ? 0 : -1"
class="play-pause flex-shrink-0 border-dark-charcoal bg-dark-charcoal text-white focus-visible:border-pink focus-visible:shadow-ring focus-visible:outline-none active:shadow-ring disabled:opacity-70"
:icon-props="icon === undefined ? undefined : { iconPath: icon }"
:icon-props="icon === undefined ? undefined : { name: icon }"
:label="$t(label)"
:button-props="buttonProps"
@click.stop.prevent="handleClick"
Expand Down Expand Up @@ -38,14 +38,10 @@ import type { ButtonConnections, ButtonVariant } from "~/types/button"

import VIconButton from "~/components/VIconButton/VIconButton.vue"

import playIcon from "~/assets/icons/play.svg"
import pauseIcon from "~/assets/icons/pause.svg"
import replayIcon from "~/assets/icons/replay.svg"

const statusIconMap = {
playing: pauseIcon,
paused: playIcon,
played: replayIcon,
playing: "pause",
paused: "play",
played: "replay",
loading: undefined,
} as const

Expand Down
7 changes: 1 addition & 6 deletions frontend/src/components/VBackToSearchResultsLink.vue
Expand Up @@ -4,7 +4,7 @@
class="time inline-flex flex-row items-center gap-2 rounded-sm p-2 pe-3 text-xs font-semibold text-dark-charcoal-70 hover:text-dark-charcoal"
v-bind="$attrs"
>
<VIcon :icon-path="chevronIcon" :rtl-flip="true" />
<VIcon name="chevron-left" :rtl-flip="true" />
{{ $t("single-result.back") }}
</VLink>
</template>
Expand All @@ -15,8 +15,6 @@ import { defineComponent } from "vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import VLink from "~/components/VLink.vue"

import chevronIcon from "~/assets/icons/chevron-left.svg"

/**
* This link takes the user from a single result back to the list of all
* results. It only appears if the user navigated from the search results.
Expand All @@ -27,8 +25,5 @@ export default defineComponent({
VLink,
},
inheritAttrs: false,
setup() {
return { chevronIcon }
},
})
</script>
5 changes: 1 addition & 4 deletions frontend/src/components/VCheckbox/VCheckbox.vue
Expand Up @@ -48,7 +48,7 @@
v-else
v-show="localCheckedState"
class="absolute inset-0 transform text-white"
:icon-path="checkIcon"
name="check"
:size="5"
/>
</span>
Expand All @@ -64,8 +64,6 @@ import { defineEvent } from "~/types/emits"

import VIcon from "~/components/VIcon/VIcon.vue"

import checkIcon from "~/assets/icons/check.svg"

type CheckboxAttrs = {
name: string
value: string
Expand Down Expand Up @@ -182,7 +180,6 @@ export default defineComponent({
})
}
return {
checkIcon,
localCheckedState,
labelClasses,
inputAttrs,
Expand Down
47 changes: 26 additions & 21 deletions frontend/src/components/VCloseButton.vue
@@ -1,14 +1,7 @@
<template>
<VIconButton
:button-props="{
variant:
variant === 'black'
? 'plain--avoid'
: variant === 'filled-white-light'
? 'filled-white'
: variant,
}"
:icon-props="{ iconPath }"
:button-props="buttonProps"
:icon-props="iconProps"
:borderless="true"
:size="size"
:class="{
Expand All @@ -24,12 +17,19 @@
<script lang="ts">
import { computed, defineComponent, PropType } from "vue"

import type { ButtonVariant } from "~/types/button"

import VIconButton from "~/components/VIconButton/VIconButton.vue"

import type { TranslateResult } from "vue-i18n"

import closeIcon from "~/assets/icons/close.svg"
import closeIconSmall from "~/assets/icons/close-small.svg"
type CloseButtonVariant =
| "filled-white"
| "filled-white-light"
| "filled-transparent"
| "filled-dark"
| "black"
| "plain--avoid"

/**
* The square icon button with a cross in it. Used to close popovers,
Expand All @@ -54,13 +54,7 @@ export default defineComponent({
* @default "filled-white-light"
*/
variant: {
type: String as PropType<
| "filled-white"
| "filled-white-light"
| "filled-transparent"
| "filled-dark"
| "black"
>,
type: String as PropType<CloseButtonVariant>,
default: "filled-white-light",
},
/**
Expand All @@ -81,12 +75,23 @@ export default defineComponent({
},
},
setup(props) {
const iconPath = computed<string>(() => {
return props.iconSize === "small" ? closeIconSmall : closeIcon
const iconProps = computed(() => {
return { name: props.iconSize === "small" ? "close-small" : "close" }
})

const buttonProps = computed<{ variant: ButtonVariant }>(() => {
const variant =
props.variant === "black"
? "plain--avoid"
: props.variant === "filled-white-light"
? "filled-white"
: props.variant
return { variant }
})

return {
iconPath,
buttonProps,
iconProps,
}
},
})
Expand Down
18 changes: 3 additions & 15 deletions frontend/src/components/VContentLink/VContentLink.vue
Expand Up @@ -11,7 +11,7 @@
@keydown.native.shift.tab.exact="$emit('shift-tab', $event)"
>
<div class="flex flex-col items-start md:flex-row md:items-center">
<VIcon :icon-path="iconPath" />
<VIcon :name="mediaType" />
<p class="hidden pt-1 font-semibold md:block md:ps-2 md:pt-0 md:text-2xl">
{{ $t(`search-type.see-${mediaType}`) }}
</p>
Expand All @@ -27,24 +27,15 @@
import { computed, defineComponent, PropType } from "vue"

import { useI18nResultsCount } from "~/composables/use-i18n-utilities"
import { AUDIO, IMAGE, SupportedMediaType } from "~/constants/media"
import type { SupportedMediaType } from "~/constants/media"

import { defineEvent } from "~/types/emits"

import VIcon from "~/components/VIcon/VIcon.vue"
import VLink from "~/components/VLink.vue"

import audioIcon from "~/assets/icons/audio-wave.svg"
import imageIcon from "~/assets/icons/image.svg"

const iconMapping = {
[AUDIO]: audioIcon,
[IMAGE]: imageIcon,
}

export default defineComponent({
name: "VContentLink",
components: { VIcon, VLink },
components: { VLink },
props: {
/**
* One of the media types supported.
Expand Down Expand Up @@ -72,14 +63,11 @@ export default defineComponent({
"shift-tab": defineEvent<[KeyboardEvent]>(),
},
setup(props) {
const iconPath = computed(() => iconMapping[props.mediaType])
const { getI18nCount } = useI18nResultsCount()
const hasResults = computed(() => props.resultsCount > 0)
const resultsCountLabel = computed(() => getI18nCount(props.resultsCount))

return {
iconPath,
imageIcon,
resultsCountLabel,
hasResults,
}
Expand Down
Expand Up @@ -9,7 +9,7 @@
<span class="hidden text-sr md:inline md:text-base">{{
$t("media-details.content-report.long")
}}</span>
<VIcon :icon-path="icons.flag" class="ms-2" />
<VIcon name="flag" class="ms-2" />
</VButton>
</template>

Expand All @@ -19,18 +19,9 @@ import { defineComponent } from "vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import VButton from "~/components/VButton.vue"

import flagIcon from "~/assets/icons/flag.svg"

export default defineComponent({
name: "VContentReportButton",
components: { VButton, VIcon },
setup() {
return {
icons: {
flag: flagIcon,
},
}
},
})
</script>

Expand Down