@dnax/ui 0.0.73 → 0.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ // Extraction d'icônes pour l'embarquement — **Node uniquement (build)**.
2
+ //
3
+ // Utilisé par :
4
+ // - `module.ts` (module Nuxt) : scanne les sources de l'app hôte au build ;
5
+ // - `scripts/gen-icon-set.ts` : produit `lib/iconSet.ts` (sous-ensemble figé) ;
6
+ // - `lib/iconSet.test.ts` : garde-fou contre la dérive du fichier généré.
7
+ //
8
+ // ⚠️ Ne pas réexporter depuis `index.ts` : ce fichier importe `node:fs`, il n'a rien à
9
+ // faire dans un bundle client.
10
+ import { readdirSync, readFileSync } from "node:fs"
11
+ import { join } from "node:path"
12
+ import type { IconSetJson } from "./icons"
13
+
14
+ /** `lucide:arrow-down`, `lucide:table-cells-merge`… */
15
+ export const ICON_NAME_RE = /\blucide:[a-z0-9]+(?:-[a-z0-9]+)*/g
16
+
17
+ const PREFIX = "lucide:"
18
+
19
+ const TEXT_FILE_RE = /\.(vue|ts|js|mjs|md|mdx)$/
20
+
21
+ /** Dossiers techniques jamais scannés (le reste de l'arbre est parcouru). */
22
+ export const SKIP_DIRS = new Set([
23
+ "node_modules",
24
+ ".nuxt",
25
+ ".output",
26
+ ".data",
27
+ ".git",
28
+ "dist",
29
+ "coverage",
30
+ "public",
31
+ ])
32
+
33
+ /** Noms d'icônes cités dans un texte, **sans** le préfixe (`arrow-down`). */
34
+ export const iconNamesIn = (text: string): string[] =>
35
+ [...text.matchAll(ICON_NAME_RE)].map((match) => match[0].slice(PREFIX.length))
36
+
37
+ /** Sous-ensemble de `IconifyJSON` (collection officielle) dont on a besoin. */
38
+ export interface IconifySourceCollection {
39
+ prefix: string
40
+ width?: number
41
+ height?: number
42
+ icons: Record<string, { body: string; [key: string]: unknown }>
43
+ aliases?: Record<string, { parent: string; [key: string]: unknown }>
44
+ }
45
+
46
+ export interface ScrapeOptions {
47
+ /** Dossiers supplémentaires à ignorer (noms de base, pas des chemins) */
48
+ skipDirs?: Iterable<string>
49
+ }
50
+
51
+ /** Fichiers texte (`.vue`, `.ts`, `.js`, `.mjs`, `.md`…) sous `dir`, récursivement. */
52
+ export const textFilesUnder = (dir: string, options: ScrapeOptions = {}): string[] => {
53
+ const skip = new Set([...SKIP_DIRS, ...(options.skipDirs ?? [])])
54
+ const found: string[] = []
55
+
56
+ const walk = (current: string): void => {
57
+ let entries
58
+ try {
59
+ entries = readdirSync(current, { withFileTypes: true })
60
+ } catch {
61
+ return // dossier absent ou illisible : on ignore
62
+ }
63
+ for (const entry of entries) {
64
+ const path = join(current, entry.name)
65
+ if (entry.isDirectory()) {
66
+ if (!skip.has(entry.name)) walk(path)
67
+ continue
68
+ }
69
+ // Les tests citent des noms d'icônes pour vérifier : rien à embarquer.
70
+ if (!TEXT_FILE_RE.test(entry.name) || entry.name.endsWith(".test.ts")) continue
71
+ found.push(path)
72
+ }
73
+ }
74
+
75
+ walk(dir)
76
+ return found
77
+ }
78
+
79
+ /** Noms d'icônes relevés dans un ensemble de dossiers (dédupliqués, triés). */
80
+ export const scrapeIconNames = (dirs: string[], options: ScrapeOptions = {}): string[] => {
81
+ const names = new Set<string>()
82
+ for (const dir of dirs) {
83
+ for (const file of textFilesUnder(dir, options)) {
84
+ for (const name of iconNamesIn(readFileSync(file, "utf8"))) names.add(name)
85
+ }
86
+ }
87
+ return [...names].sort()
88
+ }
89
+
90
+ /** Construit une collection (alias résolus, parente incluse) à partir de noms nus. */
91
+ export const buildIconSet = (
92
+ source: IconifySourceCollection,
93
+ names: Iterable<string>,
94
+ ): { set: IconSetJson; missing: string[] } => {
95
+ const icons: IconSetJson["icons"] = {}
96
+ const aliases: NonNullable<IconSetJson["aliases"]> = {}
97
+ const missing: string[] = []
98
+
99
+ const add = (name: string): void => {
100
+ if (name in icons || name in aliases) return
101
+ const icon = source.icons[name]
102
+ if (icon) {
103
+ icons[name] = { body: icon.body }
104
+ return
105
+ }
106
+ const alias = source.aliases?.[name]
107
+ if (alias) {
108
+ aliases[name] = { parent: alias.parent }
109
+ // Un alias ne rend rien sans sa parente : on l'embarque aussi.
110
+ add(alias.parent)
111
+ return
112
+ }
113
+ missing.push(name)
114
+ }
115
+
116
+ for (const name of [...names].sort()) add(name)
117
+
118
+ const set: IconSetJson = {
119
+ prefix: source.prefix,
120
+ width: source.width ?? 24,
121
+ height: source.height ?? 24,
122
+ icons,
123
+ }
124
+ if (Object.keys(aliases).length) set.aliases = aliases
125
+ return { set, missing }
126
+ }
127
+
128
+ /** Fusionne deux collections (les entrées de `extra` complètent `base`). */
129
+ export const mergeIconSets = (base: IconSetJson, extra: IconSetJson): IconSetJson => {
130
+ const aliases = { ...base.aliases, ...extra.aliases }
131
+ const merged: IconSetJson = {
132
+ prefix: base.prefix,
133
+ width: base.width ?? extra.width,
134
+ height: base.height ?? extra.height,
135
+ icons: { ...base.icons, ...extra.icons },
136
+ }
137
+ if (Object.keys(aliases).length) merged.aliases = aliases
138
+ return merged
139
+ }
package/lib/icons.ts CHANGED
@@ -64,3 +64,19 @@ export const icons = {
64
64
  highlighter: "lucide:highlighter",
65
65
  tableCellsMerge: "lucide:table-cells-merge",
66
66
  } as const
67
+
68
+ /**
69
+ * Collection Iconify minimale : le sous-ensemble d'une collection officielle
70
+ * (`lucide` ici) que l'on peut **embarquer dans le bundle** pour éviter l'appel
71
+ * réseau de `@iconify/vue`. Voir `lib/iconSet.ts` (généré) et `lib/registerIcons.ts`.
72
+ */
73
+ export interface IconSetJson {
74
+ /** Préfixe de collection, sans `:` — ex. `"lucide"` */
75
+ prefix: string
76
+ width?: number
77
+ height?: number
78
+ /** Icônes : nom (sans préfixe) → corps SVG */
79
+ icons: Record<string, { body: string }>
80
+ /** Alias (lucide en a quelques-uns : `activity-square` → `square-activity`) */
81
+ aliases?: Record<string, { parent: string }>
82
+ }
@@ -0,0 +1,77 @@
1
+ // Pagination distante : bornes, nombre de pages, plage globale des lignes.
2
+ // Les cas qui cassent en pratique : total qui rétrécit (page hors bornes), dernière page
3
+ // partielle, `pageSize` désactivé, total nul.
4
+ import { describe, expect, it } from "bun:test"
5
+ import { pageInfo } from "./pagination"
6
+
7
+ describe("pageInfo", () => {
8
+ it("calcule la première page", () => {
9
+ expect(pageInfo({ page: 1, pageSize: 400, total: 1200 })).toEqual({
10
+ page: 1,
11
+ pageCount: 3,
12
+ offset: 0,
13
+ from: 1,
14
+ to: 400,
15
+ })
16
+ })
17
+
18
+ it("décale la plage sur les pages suivantes", () => {
19
+ expect(pageInfo({ page: 3, pageSize: 400, total: 1200 })).toEqual({
20
+ page: 3,
21
+ pageCount: 3,
22
+ offset: 800,
23
+ from: 801,
24
+ to: 1200,
25
+ })
26
+ })
27
+
28
+ it("gère une dernière page partielle", () => {
29
+ expect(pageInfo({ page: 2, pageSize: 400, total: 500 })).toEqual({
30
+ page: 2,
31
+ pageCount: 2,
32
+ offset: 400,
33
+ from: 401,
34
+ to: 500,
35
+ })
36
+ })
37
+
38
+ it("tient compte des lignes réellement chargées", () => {
39
+ // La page 2 n'a reçu que 80 lignes (chargement partiel) : la plage le reflète
40
+ expect(pageInfo({ page: 2, pageSize: 400, total: 1200, loaded: 80 })).toMatchObject({
41
+ from: 401,
42
+ to: 480,
43
+ })
44
+ })
45
+
46
+ it("borne une page trop haute ou trop basse", () => {
47
+ expect(pageInfo({ page: 99, pageSize: 400, total: 1200 }).page).toBe(3)
48
+ expect(pageInfo({ page: 0, pageSize: 400, total: 1200 }).page).toBe(1)
49
+ expect(pageInfo({ page: -5, pageSize: 400, total: 1200 }).page).toBe(1)
50
+ expect(pageInfo({ page: Number.NaN, pageSize: 400, total: 1200 }).page).toBe(1)
51
+ })
52
+
53
+ it("sans pagination : une seule page, tout est affiché", () => {
54
+ expect(pageInfo({ page: 1, pageSize: 0, total: 250 })).toEqual({
55
+ page: 1,
56
+ pageCount: 1,
57
+ offset: 0,
58
+ from: 1,
59
+ to: 250,
60
+ })
61
+ })
62
+
63
+ it("total nul : from/to à 0", () => {
64
+ expect(pageInfo({ page: 1, pageSize: 400, total: 0 })).toEqual({
65
+ page: 1,
66
+ pageCount: 1,
67
+ offset: 0,
68
+ from: 0,
69
+ to: 0,
70
+ })
71
+ })
72
+
73
+ it("ne dépasse jamais le total", () => {
74
+ // `loaded` plus grand que le quota (données incohérentes) → plage bornée au total
75
+ expect(pageInfo({ page: 1, pageSize: 400, total: 10, loaded: 400 }).to).toBe(10)
76
+ })
77
+ })
@@ -0,0 +1,50 @@
1
+ // Pagination « distante » d'une grille — calculs **purs** (aucun DOM, aucun état).
2
+ //
3
+ // Le tableau ne découpe rien lui-même : `rows` contient **la page courante**, chargée par
4
+ // l'app. Ce module borne la page, calcule le nombre de pages et la plage globale des lignes
5
+ // affichées (numérotation des en-têtes de ligne, « lignes 401–800 sur 12 000 »).
6
+
7
+ export interface PageInfo {
8
+ /** Page courante, bornée à `[1, pageCount]` */
9
+ page: number
10
+ /** Nombre de pages (toujours ≥ 1) */
11
+ pageCount: number
12
+ /** Décalage global de la première ligne affichée (0-based) */
13
+ offset: number
14
+ /** Numéro global (1-based) de la première ligne affichée — `0` si le total est nul */
15
+ from: number
16
+ /** Numéro global (1-based) de la dernière ligne affichée — `0` si le total est nul */
17
+ to: number
18
+ }
19
+
20
+ export interface PageInfoOptions {
21
+ /** Page demandée (1-based) — bornée au résultat */
22
+ page: number
23
+ /** Lignes par page (0 = pas de pagination) */
24
+ pageSize: number
25
+ /** Total de lignes, toutes pages confondues */
26
+ total: number
27
+ /** Lignes réellement fournies pour cette page (défaut : `pageSize`, ou `total` sans pagination) */
28
+ loaded?: number
29
+ }
30
+
31
+ /**
32
+ * Calcule l'état de pagination. Une page hors bornes est **ramenée** dans `[1, pageCount]`
33
+ * (un total qui rétrécit après un filtre ne laisse pas la vue sur une page vide), et une
34
+ * dernière page peut être partielle (`loaded` < `pageSize`).
35
+ */
36
+ export const pageInfo = (options: PageInfoOptions): PageInfo => {
37
+ const size = Math.max(0, Math.trunc(options.pageSize || 0))
38
+ const total = Math.max(0, Math.trunc(options.total || 0))
39
+ const pageCount = size > 0 ? Math.max(1, Math.ceil(total / size)) : 1
40
+
41
+ const raw = Number.isFinite(options.page) ? Math.trunc(options.page) : 1
42
+ const page = Math.min(Math.max(1, raw), pageCount)
43
+ const offset = size > 0 ? (page - 1) * size : 0
44
+
45
+ const loaded = Math.max(0, Math.trunc(options.loaded ?? (size > 0 ? size : total)))
46
+ const from = total === 0 ? 0 : Math.min(offset + 1, total)
47
+ const to = total === 0 ? 0 : Math.min(offset + loaded, total)
48
+
49
+ return { page, pageCount, offset, from, to }
50
+ }
@@ -0,0 +1,37 @@
1
+ // registerIcons — embarque les icônes du design system dans le bundle.
2
+ //
3
+ // Par défaut, `@iconify/vue` va chercher chaque icône **à la demande** sur
4
+ // `api.iconify.design`. En SSR cela ne rend que la coquille
5
+ // `<svg class="iconify iconify--lucide">` (les `<path>` arrivent après le fetch
6
+ // client) et, sans réseau, **aucune** icône n'apparaît. Enregistrer une collection
7
+ // locale (`addCollection`) supprime la requête pour toutes les icônes de
8
+ // `lib/iconSet.ts` (fichier généré par `scripts/gen-icon-set.ts`).
9
+ //
10
+ // - Appelé automatiquement par le module Nuxt `@dnax/ui` (option `icons`, défaut `true`).
11
+ // - À appeler soi-même quand le module n'est pas utilisé (Vite/Nuxt manuel).
12
+ // - Idempotent, et `addCollection` **fusionne** avec une collection déjà enregistrée
13
+ // sous le même préfixe : plusieurs appels (design system + extras d'une app) sont
14
+ // sans danger.
15
+ import { addCollection } from "@iconify/vue"
16
+ import { lucideIconSet } from "./iconSet"
17
+ import type { IconSetJson } from "./icons"
18
+
19
+ let registered = false
20
+
21
+ /** Enregistre les icônes du design system auprès d'Iconify (idempotent). */
22
+ export const registerIcons = (): void => {
23
+ if (registered) return
24
+ registered = true
25
+ addCollection(lucideIconSet)
26
+ }
27
+
28
+ /** `true` si `registerIcons()` a déjà été appelé. */
29
+ export const iconsRegistered = (): boolean => registered
30
+
31
+ /**
32
+ * Enregistre une collection Iconify **supplémentaire** (icônes propres à une app, à un
33
+ * site de docs…). Fusionne avec la collection déjà chargée — même préfixe compris.
34
+ */
35
+ export const registerIconSet = (set: IconSetJson): void => {
36
+ addCollection(set)
37
+ }
@@ -1,6 +1,18 @@
1
1
  // Tests unitaires de la résolution du radius (échelle + style CSS).
2
+ //
3
+ // Ajout (2026-11-01) : `radius: true` était documenté « true = md » sur QCard / QDialog /
4
+ // QSyntax / QTable sans poser le moindre style → surface carrée, silencieusement.
5
+ // `radiusStyleNative` comble ce trou ; `radiusStyle` reste volontairement inchangé
6
+ // (`true` → undefined) pour ne pas injecter un `--q-radius` global qui fuirait aux
7
+ // enfants des champs/boutons (cf. `.memory/decisions.md`).
2
8
  import { describe, expect, it } from "bun:test"
3
- import { RADIUS_VALUES, radiusStyle, radiusValue } from "./useComponentProps"
9
+ import {
10
+ RADIUS_VALUES,
11
+ isRadiusScale,
12
+ radiusStyle,
13
+ radiusStyleNative,
14
+ radiusValue,
15
+ } from "./useComponentProps"
4
16
 
5
17
  describe("radiusValue", () => {
6
18
  it("traduit chaque barreau de l'échelle", () => {
@@ -33,4 +45,46 @@ describe("radiusStyle", () => {
33
45
  expect(radiusStyle(true)).toBeUndefined()
34
46
  expect(radiusStyle(undefined)).toBeUndefined()
35
47
  })
48
+
49
+ it("gère aussi none (carre) et l'échelle basse", () => {
50
+ expect(radiusStyle("none")).toEqual({ "--q-radius": "0px" })
51
+ expect(radiusStyle("sm")).toEqual({ "--q-radius": "4px" })
52
+ })
53
+ })
54
+
55
+ describe("isRadiusScale", () => {
56
+ it("reconnaît les échelles et rejette le reste", () => {
57
+ expect(isRadiusScale("md")).toBe(true)
58
+ expect(isRadiusScale("none")).toBe(true)
59
+ expect(isRadiusScale("12px")).toBe(false)
60
+ expect(isRadiusScale(true)).toBe(false)
61
+ expect(isRadiusScale(undefined)).toBe(false)
62
+ })
63
+ })
64
+
65
+ describe("RADIUS_VALUES", () => {
66
+ it("couvre toute l'échelle, croissante", () => {
67
+ expect(Object.keys(RADIUS_VALUES)).toEqual(["none", "xs", "sm", "md", "lg"])
68
+ expect(RADIUS_VALUES.none).toBe("0px")
69
+ expect(RADIUS_VALUES.lg).toBe("16px")
70
+ })
71
+ })
72
+
73
+ describe("radiusStyleNative (composants « true = échelle »)", () => {
74
+ it("convertit `true` en md par défaut", () => {
75
+ expect(radiusStyleNative(true)).toEqual({ "--q-radius": "8px" })
76
+ })
77
+
78
+ it("accepte une autre échelle par défaut", () => {
79
+ expect(radiusStyleNative(true, "lg")).toEqual({ "--q-radius": "16px" })
80
+ })
81
+
82
+ it("transmet une échelle telle quelle", () => {
83
+ expect(radiusStyleNative("sm")).toEqual({ "--q-radius": "4px" })
84
+ expect(radiusStyleNative("none")).toEqual({ "--q-radius": "0px" })
85
+ })
86
+
87
+ it("laisse undefined inchangé", () => {
88
+ expect(radiusStyleNative(undefined)).toBeUndefined()
89
+ })
36
90
  })
@@ -65,3 +65,18 @@ export function radiusStyle(radius: RadiusProp | undefined): Record<string, stri
65
65
  const value = radiusValue(radius)
66
66
  return value ? { "--q-radius": value } : undefined
67
67
  }
68
+
69
+ /**
70
+ * Style `--q-radius` des composants dont le booléen `radius` vaut une **échelle fixe**
71
+ * (documenté « true = md ») : `true` → `scale`.
72
+ *
73
+ * Les composants à forme native « pilule » (champs, boutons) ne passent **pas** par ici :
74
+ * ils posent leur propre classe (`.q-field--rounded`, `.q-btn--rounded`) — pour eux,
75
+ * `true` ne doit surtout pas devenir un `--q-radius` global qui fuirait aux enfants.
76
+ */
77
+ export function radiusStyleNative(
78
+ radius: RadiusProp | undefined,
79
+ scale: RadiusScale = "md",
80
+ ): Record<string, string> | undefined {
81
+ return radiusStyle(radius === true ? scale : radius)
82
+ }
package/module.ts CHANGED
@@ -4,12 +4,39 @@
4
4
  *
5
5
  * - Auto-importe les composants Q* (balises kebab-case : <q-btn>, <q-header>…)
6
6
  * - Ajoute styles/main.css (safe-area, tokens, styles de tous les composants)
7
+ * - **Embarque les icônes** : enregistre un sous-ensemble `lucide` auprès d'Iconify
8
+ * (`addCollection`) — design system + (par défaut) les icônes `lucide:…` trouvées
9
+ * dans les sources de l'app. Aucune requête vers l'API Iconify pour ces noms, et les
10
+ * `<path>` sont présents dès le SSR.
7
11
  *
8
12
  * Options (clé `dnaxUi`) :
9
- * components: false — désactive l'auto-import des composants
10
- * css: false — désactive l'ajout de styles/main.css
13
+ * components: false — désactive l'auto-import des composants
14
+ * css: false — désactive l'ajout de styles/main.css
15
+ * icons: false — n'embarque aucune icône (API Iconify à la demande)
16
+ * icons: { scan: false } — n'embarque que le design system
17
+ * icons: { dirs: ["app"] } — limite le scan (chemins relatifs à rootDir)
11
18
  */
12
- import { addComponentsDir, addPluginTemplate, createResolver, defineNuxtModule } from "@nuxt/kit"
19
+ import { addComponentsDir, addPluginTemplate, createResolver, defineNuxtModule, updateTemplates } from "@nuxt/kit"
20
+ import { readFileSync } from "node:fs"
21
+ import { createRequire } from "node:module"
22
+ import { join, resolve } from "node:path"
23
+ import { lucideIconSet } from "./lib/iconSet"
24
+ import {
25
+ buildIconSet,
26
+ mergeIconSets,
27
+ scrapeIconNames,
28
+ type IconifySourceCollection,
29
+ } from "./lib/iconSetScrape"
30
+
31
+ export interface DnaxUiIconsOptions {
32
+ /**
33
+ * Scanne les sources de l'app au build pour embarquer aussi **ses** icônes
34
+ * `lucide:` (défaut : true)
35
+ */
36
+ scan?: boolean
37
+ /** Dossiers à scanner, résolus depuis `rootDir` (défaut : tout `rootDir`) */
38
+ dirs?: string[]
39
+ }
13
40
 
14
41
  export interface DnaxUiModuleOptions {
15
42
  /** Auto-importe les composants Q* (défaut : true) */
@@ -18,6 +45,33 @@ export interface DnaxUiModuleOptions {
18
45
  css?: boolean
19
46
  /** Installe le handler « retour » : ferme l'overlay ouvert au lieu de naviguer (défaut : true) */
20
47
  overlayBack?: boolean
48
+ /**
49
+ * Icônes : `false` = rien d'embarqué (API Iconify à la demande) ; objet = réglages
50
+ * du scan. Par défaut : sous-ensemble du design system **+** scan des sources.
51
+ */
52
+ icons?: boolean | DnaxUiIconsOptions
53
+ }
54
+
55
+ /** Collection Iconify lue pour résoudre les noms (`@iconify-json/lucide`). */
56
+ const LUCIDE_COLLECTION = "@iconify-json/lucide/icons.json"
57
+
58
+ /**
59
+ * Charge la collection `lucide`. Elle vit dans les dépendances de `@dnax/ui` ; on tente
60
+ * la résolution depuis ce module, puis depuis l'app hôte (hoisting).
61
+ */
62
+ const loadLucideCollection = (rootDir: string): IconifySourceCollection | null => {
63
+ const resolvers = [
64
+ () => createRequire(import.meta.url).resolve(LUCIDE_COLLECTION),
65
+ () => createRequire(join(rootDir, "package.json")).resolve(LUCIDE_COLLECTION),
66
+ ]
67
+ for (const resolveSpec of resolvers) {
68
+ try {
69
+ return JSON.parse(readFileSync(resolveSpec(), "utf8")) as IconifySourceCollection
70
+ } catch {
71
+ // on passe au résolveur suivant
72
+ }
73
+ }
74
+ return null
21
75
  }
22
76
 
23
77
  export default defineNuxtModule<DnaxUiModuleOptions>({
@@ -30,12 +84,13 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
30
84
  components: true,
31
85
  css: true,
32
86
  overlayBack: true,
87
+ icons: { scan: true },
33
88
  },
34
89
  setup(options, nuxt) {
35
- const { resolve } = createResolver(import.meta.url)
90
+ const { resolve: resolvePath } = createResolver(import.meta.url)
36
91
 
37
92
  if (options.css) {
38
- const cssPath = resolve("./styles/main.css")
93
+ const cssPath = resolvePath("./styles/main.css")
39
94
  if (!nuxt.options.css.includes(cssPath)) {
40
95
  nuxt.options.css.push(cssPath)
41
96
  }
@@ -43,13 +98,76 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
43
98
 
44
99
  if (options.components) {
45
100
  addComponentsDir({
46
- path: resolve("./components"),
101
+ path: resolvePath("./components"),
47
102
  prefix: "", // pas de préfixe : QBtn.vue → <q-btn>
48
103
  pathPrefix: false, // pas de préfixe par sous-dossier
49
104
  ignore: ["**/internal/**"], // composants internes : non auto-importés
50
105
  })
51
106
  }
52
107
 
108
+ // — Icônes embarquées ---------------------------------------------------------
109
+ // Plugin **universel** : côté serveur aussi, sinon le SSR ne rend que des coquilles
110
+ // `<svg class="iconify">`, les `<path>` n'arrivant qu'après le fetch client (et
111
+ // jamais sans réseau). `addCollection` est purement local et **fusionne**.
112
+ if (options.icons !== false) {
113
+ const iconsOptions: DnaxUiIconsOptions =
114
+ typeof options.icons === "object" ? options.icons : {}
115
+ const scan = iconsOptions.scan !== false
116
+
117
+ const dirs = iconsOptions.dirs?.length
118
+ ? iconsOptions.dirs.map((dir) => resolve(nuxt.options.rootDir, dir))
119
+ : [nuxt.options.rootDir]
120
+
121
+ const collection = scan ? loadLucideCollection(nuxt.options.rootDir) : null
122
+ if (scan && !collection) {
123
+ console.warn(
124
+ "[@dnax/ui] `@iconify-json/lucide` introuvable : seules les icônes du design system sont embarquées. " +
125
+ "Installez-le (`bun add -D @iconify-json/lucide`) pour scanner votre app.",
126
+ )
127
+ }
128
+
129
+ addPluginTemplate({
130
+ filename: "dnax-ui-icons.mjs",
131
+ getContents: () => {
132
+ let set = lucideIconSet
133
+ let scanned = false
134
+ if (collection) {
135
+ const { set: appSet, missing } = buildIconSet(collection, scrapeIconNames(dirs))
136
+ set = mergeIconSets(lucideIconSet, appSet)
137
+ scanned = true
138
+ if (nuxt.options.dev && missing.length) {
139
+ console.warn(
140
+ `[@dnax/ui] icône(s) \`lucide:\` absente(s) de la collection (faute de frappe ?) : ${missing.join(", ")}`,
141
+ )
142
+ }
143
+ }
144
+ return `
145
+ import { defineNuxtPlugin } from "#imports"
146
+ import { registerIconSet } from "@dnax/ui/runtime"
147
+
148
+ // Sous-ensemble \`lucide\` embarqué (${Object.keys(set.icons).length} icônes) :
149
+ // design system${scanned ? " + sources de l'app" : ""}. Aucune requête vers l'API Iconify
150
+ // pour ces noms ; les autres retombent dessus (progressive enhancement).
151
+ const ICON_SET = ${JSON.stringify(set)}
152
+
153
+ export default defineNuxtPlugin(() => {
154
+ registerIconSet(ICON_SET)
155
+ })
156
+ `
157
+ },
158
+ })
159
+
160
+ // Dev : une icône ajoutée dans les sources régénère le module virtuel — sans ça
161
+ // il faudrait redémarrer le serveur pour l'embarquer (en attendant, l'icône
162
+ // s'affiche via l'API : dégradation, pas rupture).
163
+ if (scan) {
164
+ nuxt.hook("builder:watch", async (_event, watchedPath) => {
165
+ if (!/\.(vue|ts|js|mjs|md|mdx)$/.test(String(watchedPath).replace(/\\/g, "/"))) return
166
+ await updateTemplates({ filter: (template) => template.filename === "dnax-ui-icons.mjs" })
167
+ })
168
+ }
169
+ }
170
+
53
171
  // Dépendances CJS sans export ESM `default` explicite : esbuild doit les pré-bundler
54
172
  // (interop) sinon, servies brutes, le navigateur échoue sur « does not provide an
55
173
  // export named 'default' ». `leaflet` (lib/mapLeaflet.ts) est dans ce cas ; le code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.73",
3
+ "version": "0.0.76",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
@@ -12,7 +12,8 @@
12
12
  "./styles/main.css": "./styles/main.css"
13
13
  },
14
14
  "scripts": {
15
- "generate": "bun scripts/generate-exports.ts"
15
+ "generate": "bun scripts/generate-exports.ts",
16
+ "gen:icons": "bun scripts/gen-icon-set.ts --out ./lib/iconSet.ts --src ./components --src ./lib"
16
17
  },
17
18
  "private": false,
18
19
  "devDependencies": {
@@ -23,6 +24,7 @@
23
24
  "vue": "^3.5.41"
24
25
  },
25
26
  "dependencies": {
27
+ "@iconify-json/lucide": "^1.2.150",
26
28
  "@iconify/vue": "^5.0.1",
27
29
  "@maptiler/sdk": "^4.1.0",
28
30
  "@nuxt/kit": "4.5.2",
package/runtime.ts CHANGED
@@ -46,6 +46,11 @@ export type { QNetwork, NetworkOptions } from "./lib/network"
46
46
  // `$q.wakeLock` (écran allumé) — Screen Wake Lock API.
47
47
  export { wakeLock, createWakeLock } from "./lib/wakeLock"
48
48
  export type { QWakeLock, WakeLockOptions } from "./lib/wakeLock"
49
+ // Icônes du design system embarquées dans le bundle (`lib/iconSet.ts`, généré) :
50
+ // appelé par le module Nuxt, à appeler soi-même sans module. Évite l'appel runtime
51
+ // à l'API Iconify et rend les icônes dès le SSR.
52
+ export { registerIcons, iconsRegistered } from "./lib/registerIcons"
53
+ export { registerIconSet } from "./lib/registerIcons"
49
54
 
50
55
  // — Providers de `$q` (montés au boot par l'app hôte) —
51
56
  export { default as QDialogProvider } from "./components/QDialogProvider.vue"