Skip to content

Commit

Permalink
Merge pull request #1005 from dnum-mi/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
laruiss authored Jan 6, 2025
2 parents f606c76 + b08d00c commit 988752e
Show file tree
Hide file tree
Showing 34 changed files with 460 additions and 90 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ jobs:
run: pnpm check-exports-ci
- name: Test
run: pnpm test
- name: Install Playwright dependencies
- name: Install Playwright with dependencies
run: pnpx playwright install --with-deps
- name: Install Playwright
run: pnpx playwright install

- name: Build Storybook
run: pnpm build-storybook --quiet
- name: Serve Storybook and run tests
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,10 @@ $RECYCLE.BIN/
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,intellij,windows,macos,linux

stats.html
types/

meta-dts/

# Vitepress
.vitepress/dist
Expand Down
162 changes: 157 additions & 5 deletions docs/guide/icones.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Ci-dessous un exemple :
<<< ../docs-demo/IconesOfficielles.vue [Code de la démo]
:::

##  Utiliser les icônes dans les composants de VueDsfr
## Utiliser les icônes dans les composants de VueDsfr

Plusieurs composants (`DsfrButton`, `DsfrBadge`, `DsfrCallout`...) ont la prop `icon` qui permet d’ajouter une icône.

Expand Down Expand Up @@ -55,16 +55,14 @@ exemple :
</template>
```

Cependant, si le préfix est lui-même sans tiret `-`, alors l’écriture tout en kebab-case est acceptée :
Cependant, si le préfixe est lui-même sans tiret `-`, alors l’écriture tout en kebab-case est acceptée :

```vue
<template>
Nom d’icône accepté : <VIcon name="ri-close-line" />
</template>
```

Ainsi, si vous utilisiez jusqu’ici que des collections remix icon (`ri`) et bootstrap icons (`bi`) vous ne devriez rien avoir à changer.

:::

::: info Les collections disponibles
Expand Down Expand Up @@ -94,11 +92,165 @@ import { VIcon } from '@gouvminint/vue-dsfr'
```

## Éviter les appels réseaux (optionnel - pour les applications internes)

Si vous développez des applications destinées à des agents internes avec potentiellement des accès internet réduits, il
est possible que les appels vers l’API iconify soient bloqués. Vous voudrez donc éviter ces appels réseaux.

Dans ce but, depuis la version [7.3.0](https://github.com/dnum-mi/vue-dsfr/releases/tag/v7.3.0), la dépendance `@iconify/vue`
n’est plus incluse dans la bibliothèque, et doit être installée dans votre application.

Avec cette modification, il est possible d’ajouter des collections d’icônes qui ne feront pas d’appels réseaux.

### TL;DR;

- créer un fichier `scripts/icons.js` dans votre projet avec un contenu de cette forme :
```js
// @ts-check
import { icons as mdiCollection } from '@iconify-json/mdi'
import { icons as riCollection } from '@iconify-json/ri'

/**
* Liste de nom d’icônes **sans** le préfixe de la collection Remix Icons qui sont utilisées dans l’application
* @type {string[]}
*/
const riIconNames = [
'flag-line',
'home-4-line',
'question-mark',
]

/**
* Liste de nom d’icônes **sans** le préfixe de la collection Material Design Icons qui sont utilisées dans l’application
* @type {string[]}
*/
const mdiIconNames = [
'account-heart',
'account-key',
]

/**
* Liste de tuples [collectionDIcônes, tableauDeNomsDIcônesUtiliséesDansLApplication]
* @type {[import('@iconify/vue').IconifyJSON, string[]][]}
*/
export const collectionsToFilter = [
[riCollection, riIconNames],
[mdiCollection, mdiIconNames],
]
```
N.B. : l’exemple ci-dessus montre comment utiliser les icônes `'ri-flag-line'`, `'ri-home-4-line'`, `'ri-question-mark'` de la collection
remix icons (`ri`) et les icônes `'mdi-account-heart'` et `'mdi-account-key'` de la collection Material Design Icons (`mdi`).
- ajouter un script `"icons"` dans le `package.json` de votre application avec cette commande:
`"vue-dsfr-icons -s scripts/icons.js -t src/icon-collections.ts"`
- lancer le script `icons` (e.g. : `npm run icons` si vous utilisez `npm` comme gestionnaire de paquet) à chaque fois
que vous modifiez le fichier `scripts/icons.js`. Pour le fichier `scripts/icons.js`, cela générera le fichier `src/icon-collections.ts` suivant :
```ts
import type { IconifyJSON } from '@iconify/vue'

const collections: IconifyJSON[] = [{ prefix: 'ri', icons: { 'flag-line': { body: '<path fill="currentColor" d="M12.382 3a1 1 0 0 1 .894.553L14 5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-6.382a1 1 0 0 1-.894-.553L12 16H5v6H3V3zm-.618 2H5v9h8.236l1 2H19V7h-6.236z"/>' }, 'home-4-line': { body: '<path fill="currentColor" d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1m-6-2h5V9.157l-6-5.454l-6 5.454V19h5v-6h2z"/>' }, 'question-mark': { body: '<path fill="currentColor" d="M12 19a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3m0-17a6 6 0 0 1 6 6c0 2.165-.753 3.29-2.674 4.923C13.399 14.56 13 15.297 13 17h-2c0-2.474.787-3.695 3.031-5.601C15.548 10.11 16 9.434 16 8a4 4 0 0 0-8 0v1H6V8a6 6 0 0 1 6-6"/>' } }, width: 24, height: 24 }, { prefix: 'mdi', icons: { 'account-heart': { body: '<path fill="currentColor" d="M15 14c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4m0-2a4 4 0 0 0 4-4a4 4 0 0 0-4-4a4 4 0 0 0-4 4a4 4 0 0 0 4 4M5 15l-.6-.5C2.4 12.6 1 11.4 1 9.9c0-1.2 1-2.2 2.2-2.2c.7 0 1.4.3 1.8.8c.4-.5 1.1-.8 1.8-.8C8 7.7 9 8.6 9 9.9c0 1.5-1.4 2.7-3.4 4.6z"/>' }, 'account-key': { body: '<path fill="currentColor" d="M11 10v2H9v2H7v-2H5.8c-.4 1.2-1.5 2-2.8 2c-1.7 0-3-1.3-3-3s1.3-3 3-3c1.3 0 2.4.8 2.8 2zm-8 0c-.6 0-1 .4-1 1s.4 1 1 1s1-.4 1-1s-.4-1-1-1m13 4c2.7 0 8 1.3 8 4v2H8v-2c0-2.7 5.3-4 8-4m0-2c-2.2 0-4-1.8-4-4s1.8-4 4-4s4 1.8 4 4s-1.8 4-4 4"/>' } }, width: 24, height: 24 }]
export default collections
```

ou formatté autrement :

```ts
import type { IconifyJSON } from '@iconify/vue'

const collections: IconifyJSON[] = [{
prefix: 'ri',
icons: {
'flag-line': { body: '<path fill="currentColor" d="M12.382 3a1 1 0 0 1 .894.553L14 5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-6.382a1 1 0 0 1-.894-.553L12 16H5v6H3V3zm-.618 2H5v9h8.236l1 2H19V7h-6.236z"/>' },
'home-4-line': { body: '<path fill="currentColor" d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1m-6-2h5V9.157l-6-5.454l-6 5.454V19h5v-6h2z"/>' },
'question-mark': { body: '<path fill="currentColor" d="M12 19a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3m0-17a6 6 0 0 1 6 6c0 2.165-.753 3.29-2.674 4.923C13.399 14.56 13 15.297 13 17h-2c0-2.474.787-3.695 3.031-5.601C15.548 10.11 16 9.434 16 8a4 4 0 0 0-8 0v1H6V8a6 6 0 0 1 6-6"/>' },
},
width: 24,
height: 24,
}, {
prefix: 'mdi',
icons: {
'account-heart': { body: '<path fill="currentColor" d="M15 14c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4m0-2a4 4 0 0 0 4-4a4 4 0 0 0-4-4a4 4 0 0 0-4 4a4 4 0 0 0 4 4M5 15l-.6-.5C2.4 12.6 1 11.4 1 9.9c0-1.2 1-2.2 2.2-2.2c.7 0 1.4.3 1.8.8c.4-.5 1.1-.8 1.8-.8C8 7.7 9 8.6 9 9.9c0 1.5-1.4 2.7-3.4 4.6z"/>' },
'account-key': { body: '<path fill="currentColor" d="M11 10v2H9v2H7v-2H5.8c-.4 1.2-1.5 2-2.8 2c-1.7 0-3-1.3-3-3s1.3-3 3-3c1.3 0 2.4.8 2.8 2zm-8 0c-.6 0-1 .4-1 1s.4 1 1 1s1-.4 1-1s-.4-1-1-1m13 4c2.7 0 8 1.3 8 4v2H8v-2c0-2.7 5.3-4 8-4m0-2c-2.2 0-4-1.8-4-4s1.8-4 4-4s4 1.8 4 4s-1.8 4-4 4"/>' },
},
width: 24,
height: 24,
}]
export default collections
```
- ajouter les collections dans votre point d’entrée (généralement `src/main.ts`) :
```ts
// (...)
import collections from './icon-collections.js'
// (...)

for (const collection of collections) {
addCollection(collection)
}

// (...)
```

### Plus d’explication pour éviter les appels réseaux

En interne, depuis la version [`6.0.0`](https://github.com/dnum-mi/vue-dsfr/releases/tag/v6.0.0) VueDsfr utilise [iconify](https://iconify.design/docs/icon-components/vue).
Pour comprendre la section précédente, il faut comprendre comment fonctionne iconify.

#### Iconify

::: info

Veuillez consulter la [documentation officielle de @iconify/vue](https://iconify.design/docs/icon-components/vue) pour plus de détails.

:::

Le principe de iconify est de ne pas inclure les icônes dans le bundle et de faire un appel réseau en arrière-plan (XHR ou fetch) pour récupérer les SVG des icônes utilisées à la demande, c’est-à-dire dès qu’un composant qui utilise des icônes iconify est rendu dans le DOM.

Or ces appels API nécessitent pour l’utilisateur de l’application d’avoir accès à internet, car par défaut l’API utilisée pour récupérer les icônes est celle d’iconify, sur internet.

Il est possible d’[héberger soi-même un serveur API](https://iconify.design/docs/api/#hosting-api) et de dire à iconify d’utiliser cette API. C’est néanmoins compliqué.

Il est possible aussi d’utiliser la fonction [`addCollection(collection: IconifyJSON)` exposée par `@iconify/vue`](https://iconify.design/docs/icon-components/vue/add-collection.html#iconify-for-vue-function-addcollection) pour inclure des icônes et faire en sorte qu’[aucun appel réseau ne soit effectué](https://iconify.design/docs/icon-components/vue/add-collection.html#api-provider).

Le plus simple, c’est donc d’utiliser un sous-ensemble d’une collection existante (par exemple `@iconify-json/ri` pour Remix Icons) en créant une nouvelle collection et d’ajouter cette collection. C’est ce que fait la fonction `filterIcons (collection: import('@iconify/vue').IconifyJSON, iconNames: string[])` exposée par `@gouvminint/vue-dsfr/meta`. Vous ne voudrez sans doute pas l’utiliser directement.

Cette fonction `filterIcons()` est utilisée par la fonction `createCustomCollectionFile (sourcePath: string, targetPath: string)` aussi exposée par `@gouvminint/vue-dsfr/meta`, dont voici la partie importante :

```ts
// (...)

const collectionsToFilter = await import(sourcePath).then(({ collectionsToFilter }) => collectionsToFilter)

const collections = collectionsToFilter.map(tuple => filterIcons(...tuple))

const code = `import type { IconifyJSON } from '@iconify/vue'
const collections: IconifyJSON[] = ${JSON.stringify(collections)}
export default collections`

// (...)
```

::: tip

- `sourcePath` est le chemin vers le fichier qui contient le code listé plus dans le fichier `scripts/icons.js`
- `targetPath` est le chemin vers le fichier qui contiendra le code généré et appelé plus haut `src/icon-collections.js`

Libre à vous d’adapter les chemins et les noms de fichiers, veillez simplement à modifier en fonction le script `"icons"` de `package.json` et l’import dans votre fichier d’entrée (souvent `src/main.ts`).

:::

::: tip

Nous vous invitons à regarder les fichiers suivants :

- [`meta/custom-icon-collections-creator.js`](https://github.com/dnum-mi/vue-dsfr/blob/v7.2.0/meta/custom-icon-collections-creator.js)
- [`meta/custom-icon-collections-creator-bin.js`](https://github.com/dnum-mi/vue-dsfr/blob/v7.2.0/meta/custom-icon-collections-creator-bin.js) (celui qui est aliasé en binaire `vue-dsfr-icons` et qui utilise `custom-icon-collections-creator.js`)

:::

## Pour Nuxt 3

Veillez simplement à utiliser la prop `ssr` à `true`.

Plus de détails dans la [documentation officielle de @iconify/vue](https://iconify.design/docs/icon-components/vue/#ssr-attribute).
Plus de détails dans la [documentation officielle de @iconify/vue pour le SSR](https://iconify.design/docs/icon-components/vue/#ssr-attribute).

<script lang="ts" setup>
import IconesOfficielles from '../docs-demo/IconesOfficielles.vue'
Expand Down
10 changes: 10 additions & 0 deletions meta/autoimport-preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Preset Autoimport pour le plugin unplugin-auto-import pour les composables de VueDsfr
*/
export const vueDsfrAutoimportPreset = Object.freeze({
from: '@gouvminint/vue-dsfr',
imports: Object.freeze([
'useScheme',
'useTabs',
]),
})
16 changes: 16 additions & 0 deletions meta/component-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Component resolver pour le plgin unplugin-vue-components pour les composants VueDsfr
*
* @function
* @param {string} componentName - Nom du composant à chercher
*
* @returns {{ name: string, from: string } | undefined} Objet de retour pour le plugin unplugin-vue-components
*/
export const vueDsfrComponentResolver = (componentName) => {
if (componentName.startsWith('Dsfr') || componentName === 'VIcon') {
return {
name: componentName,
from: '@gouvminint/vue-dsfr',
}
}
}
23 changes: 23 additions & 0 deletions meta/custom-icon-collections-creator-bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node

import path from 'node:path'
import process from 'node:process'

import { Command } from 'commander'
import chalk from 'chalk'

import { createCustomCollectionFile } from './custom-icon-collections-creator.js'

const program = new Command()

program
.option('-s, --source <filepath>', 'Chemin vers le fichier de tuples [IconifyJSON, string[]]')
.option('-t, --target <filepath>', 'Chemin vers le fichier destination (src/icons.ts par défaut)')
.parse(process.argv)

const options = program.opts()

if (options.source && options.target) {
createCustomCollectionFile(path.resolve(process.cwd(), options.source), path.resolve(process.cwd(), options.target))
console.log(chalk.green('Les icônes ont été générées')) // eslint-disable-line no-console
}
78 changes: 78 additions & 0 deletions meta/custom-icon-collections-creator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @ts-check
/* eslint-disable no-console */
import childProcess from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import util from 'node:util'

const execPromise = util.promisify(childProcess.exec)

/**
* Filtre les icônes d'une collection en fonction d'une liste de noms.
* @function
*
* @param {string} sourcePath - Fichier source
* @param {string} targetPath - Fichier destination
*
*/
export async function createCustomCollectionFile (sourcePath, targetPath) {
/**
* @type {[import('@iconify/vue').IconifyJSON, string[]][]}
*/
const collectionsToFilter = await import(sourcePath).then(({ collectionsToFilter }) => collectionsToFilter)

const collections = collectionsToFilter.map(tuple => filterIcons(...tuple))

const code = `import type { IconifyJSON } from '@iconify/vue'
const collections: IconifyJSON[] = ${JSON.stringify(collections)}
export default collections`

await fs.writeFile(targetPath, code)

await runShellCommand(`npx eslint ${path.resolve(process.cwd(), targetPath)} --fix`)
}

/**
* Fonctions utilitaires
*/

/**
* Filtre les icônes d'une collection en fonction d'une liste de noms.
* @function
*
* @param {import('@iconify/vue').IconifyJSON} collection - La collection d'icônes.
* @param {string[]} iconNames - La liste des noms d'icônes à conserver.
*
* @returns {import('@iconify/vue').IconifyJSON} - Une nouvelle collection filtrée.
*/
export function filterIcons (collection, iconNames) {
const icons = Object.fromEntries(Object.entries(collection.icons).filter(([key]) => {
return iconNames.includes(key)
}))
const { lastModified, aliases, provider, ...useful } = collection
return {
...useful, // prefix, width, height
icons,
}
}

/**
* Lance une commande shell.
* @function
*
* @param {string} command - La commande shell à lancer
*
* @returns {Promise<undefined>} - Une nouvelle collection filtrée.
*/
export async function runShellCommand (command) {
try {
const { stdout, stderr } = await execPromise(command)
if (stderr) {
console.error('Erreur :', stderr)
}
console.log(stdout)
} catch (error) {
console.error('Erreur d’exécution :', error)
}
}
3 changes: 3 additions & 0 deletions meta/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './autoimport-preset.js'
export * from './component-resolver.js'
export * from './custom-icon-collections-creator.js'
Loading

0 comments on commit 988752e

Please sign in to comment.