@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.
- package/components/QBtnDropdown.vue +8 -1
- package/components/QCard.vue +3 -3
- package/components/QDialog.vue +2 -2
- package/components/QFilePicker.vue +22 -65
- package/components/QGridItem.vue +1 -1
- package/components/QImg.vue +3 -1
- package/components/QInputOtp.vue +2 -0
- package/components/QSpreadsheet.vue +593 -65
- package/components/QSyntax.vue +2 -2
- package/components/QTable.vue +2 -2
- package/index.ts +18 -0
- package/lib/cellSize.test.ts +52 -0
- package/lib/cellSize.ts +43 -0
- package/lib/fileList.test.ts +119 -0
- package/lib/fileList.ts +159 -0
- package/lib/iconSet.test.ts +61 -0
- package/lib/iconSet.ts +91 -0
- package/lib/iconSetScrape.ts +139 -0
- package/lib/icons.ts +16 -0
- package/lib/pagination.test.ts +77 -0
- package/lib/pagination.ts +50 -0
- package/lib/registerIcons.ts +37 -0
- package/lib/useComponentProps.test.ts +55 -1
- package/lib/useComponentProps.ts +15 -0
- package/module.ts +124 -6
- package/package.json +4 -2
- package/runtime.ts +5 -0
- package/scripts/gen-icon-set.ts +110 -0
- package/scripts/generate-exports.ts +46 -69
- package/styles/main.css +191 -1
|
@@ -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 {
|
|
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
|
})
|
package/lib/useComponentProps.ts
CHANGED
|
@@ -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
|
|
10
|
-
* css: false
|
|
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 =
|
|
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:
|
|
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.
|
|
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"
|