@dnax/ui 0.0.73 → 0.0.75
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/QGridItem.vue +1 -1
- package/components/QImg.vue +3 -1
- package/components/QInputOtp.vue +2 -0
- package/components/QSyntax.vue +2 -2
- package/components/QTable.vue +2 -2
- package/index.ts +4 -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/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 +12 -0
|
@@ -31,6 +31,7 @@ export interface DropdownItem {
|
|
|
31
31
|
import { computed } from "vue"
|
|
32
32
|
import type { StyleValue } from "vue"
|
|
33
33
|
import QBtnActions, { type DropdownPosition } from "./QBtnActions.vue"
|
|
34
|
+
import { useRadius } from "../lib/useComponentProps"
|
|
34
35
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
35
36
|
|
|
36
37
|
interface Props {
|
|
@@ -115,6 +116,12 @@ const mappedActions = computed(() =>
|
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
const onSelectAction = (value: any) => emit("select", value)
|
|
119
|
+
|
|
120
|
+
// radius : prop explicite > composantProps.QBtnDropdown > composantProps.default.
|
|
121
|
+
// On transmet la valeur **résolue** (et non la prop brute) pour que
|
|
122
|
+
// `componentProps.QBtnDropdown.radius` ne soit pas ignoré au profit de celui du
|
|
123
|
+
// moteur QBtnActions.
|
|
124
|
+
const effectiveRadius = useRadius("QBtnDropdown", () => props.radius)
|
|
118
125
|
</script>
|
|
119
126
|
|
|
120
127
|
<template>
|
|
@@ -134,7 +141,7 @@ const onSelectAction = (value: any) => emit("select", value)
|
|
|
134
141
|
:color="color"
|
|
135
142
|
:text-color="textColor"
|
|
136
143
|
:size="size"
|
|
137
|
-
:radius="
|
|
144
|
+
:radius="effectiveRadius"
|
|
138
145
|
:flat="flat"
|
|
139
146
|
:outline="outline"
|
|
140
147
|
:unelevated="unelevated"
|
package/components/QCard.vue
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Pas d'ombre par défaut ; elevated l'ajoute, flat la retire explicitement.
|
|
4
4
|
// Slots : default, media (zone image en haut), actions (zone actions en bas)
|
|
5
5
|
import { computed } from "vue"
|
|
6
|
-
import {
|
|
6
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
7
7
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
@@ -35,9 +35,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
35
35
|
hover: false,
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
// radius : prop explicite > composantProps.QCard.radius ; échelle → --q-radius
|
|
38
|
+
// radius : prop explicite > composantProps.QCard.radius ; échelle → --q-radius (true = md)
|
|
39
39
|
const effectiveRadius = useRadius("QCard", () => props.radius)
|
|
40
|
-
const roundedStyle = computed(() =>
|
|
40
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
41
41
|
</script>
|
|
42
42
|
|
|
43
43
|
<template>
|
package/components/QDialog.vue
CHANGED
|
@@ -14,7 +14,7 @@ export const qDialogKey: InjectionKey<DialogContext> = Symbol("q-dialog")
|
|
|
14
14
|
import { computed, onBeforeUnmount, onMounted, provide, watch } from "vue"
|
|
15
15
|
import type { StyleValue } from "vue"
|
|
16
16
|
import { cn } from "../lib/utils"
|
|
17
|
-
import {
|
|
17
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
18
18
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
19
19
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
20
20
|
import { markOverlayClose } from "../lib/closeOverlay"
|
|
@@ -150,7 +150,7 @@ const overlayClasses = computed(() => cn("q-dialog__overlay", `q-dialog__overlay
|
|
|
150
150
|
|
|
151
151
|
// radius : prop explicite > composantProps.QDialog.radius ; échelle → --q-radius
|
|
152
152
|
const effectiveRadius = useRadius("QDialog", () => props.radius)
|
|
153
|
-
const roundedStyle = computed(() =>
|
|
153
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
154
154
|
|
|
155
155
|
const contentClasses = computed(() =>
|
|
156
156
|
cn(
|
package/components/QGridItem.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:
|
|
2
|
+
// QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:image" text="Photos" badge="3" />
|
|
3
3
|
// À utiliser dans <q-grid :column-num="…">. Affiche une icône (ou un slot) + un
|
|
4
4
|
// texte, avec badge / point de notification, effet cliquable et navigation (href).
|
|
5
5
|
import { computed } from "vue"
|
package/components/QImg.vue
CHANGED
|
@@ -77,8 +77,10 @@ const imageClasses = computed(() =>
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
// radius : prop explicite > composantProps.QImg > composantProps.default → --q-radius
|
|
80
|
+
// `true` = pilule (forme native documentée) → classe dédiée, pas un `--q-radius`
|
|
80
81
|
const effectiveRadius = useRadius("QImg", () => props.radius)
|
|
81
82
|
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
83
|
+
const roundedClass = computed(() => (effectiveRadius.value === true ? "q-img--rounded" : undefined))
|
|
82
84
|
|
|
83
85
|
const imageStyle = computed(() => `${props.imgStyle}; object-position: ${props.position}`)
|
|
84
86
|
|
|
@@ -101,7 +103,7 @@ const showLoading = computed(() => props.loading || imgLoading.value)
|
|
|
101
103
|
</script>
|
|
102
104
|
|
|
103
105
|
<template>
|
|
104
|
-
<div class="q-img" :style="[containerStyle, roundedStyle]">
|
|
106
|
+
<div class="q-img" :class="roundedClass" :style="[containerStyle, roundedStyle]">
|
|
105
107
|
<!-- Placeholder basse résolution pendant le chargement -->
|
|
106
108
|
<img
|
|
107
109
|
v-if="placeholderSrc && imgLoading && !imgError"
|
package/components/QInputOtp.vue
CHANGED
|
@@ -190,6 +190,8 @@ const rootClasses = computed(() =>
|
|
|
190
190
|
props.dense && "q-field--dense",
|
|
191
191
|
props.error && "q-field--error",
|
|
192
192
|
props.disable && "q-field--disabled",
|
|
193
|
+
// `radius: true` = pilule (forme native documentée)
|
|
194
|
+
effectiveRadius.value === true && "q-input-otp--rounded",
|
|
193
195
|
),
|
|
194
196
|
)
|
|
195
197
|
|
package/components/QSyntax.vue
CHANGED
|
@@ -8,7 +8,7 @@ import { createHighlighter, type Highlighter } from "shiki"
|
|
|
8
8
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
|
|
9
9
|
import { Icon } from "@iconify/vue"
|
|
10
10
|
import { icons } from "../lib/icons"
|
|
11
|
-
import {
|
|
11
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
12
12
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
13
13
|
|
|
14
14
|
const DEFAULT_THEME = "github-dark-default"
|
|
@@ -44,7 +44,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
44
44
|
|
|
45
45
|
// radius : prop explicite > composantProps.QSyntax.radius > composantProps.default.radius
|
|
46
46
|
const effectiveRadius = useRadius("QSyntax", () => props.radius)
|
|
47
|
-
const roundedStyle = computed(() =>
|
|
47
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
48
48
|
|
|
49
49
|
const slots = useSlots()
|
|
50
50
|
|
package/components/QTable.vue
CHANGED
|
@@ -33,7 +33,7 @@ import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch }
|
|
|
33
33
|
import { Icon } from "@iconify/vue"
|
|
34
34
|
import { icons } from "../lib/icons"
|
|
35
35
|
import { cn } from "../lib/utils"
|
|
36
|
-
import {
|
|
36
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
37
37
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
38
38
|
import QSelect from "./QSelect.vue"
|
|
39
39
|
|
|
@@ -437,7 +437,7 @@ const tableClasses = computed(() =>
|
|
|
437
437
|
|
|
438
438
|
// radius : prop explicite > composantProps.QTable.radius > composantProps.default.radius
|
|
439
439
|
const effectiveRadius = useRadius("QTable", () => props.radius)
|
|
440
|
-
const roundedStyle = computed(() =>
|
|
440
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
441
441
|
|
|
442
442
|
const sortThClass = computed(() => "q-table__sort")
|
|
443
443
|
|
package/index.ts
CHANGED
|
@@ -208,6 +208,10 @@ export { textareaCap, textareaMetrics, textareaSizing } from "./lib/textarea"
|
|
|
208
208
|
export type { TextareaMetrics, TextareaSizing, TextareaComputedStyle } from "./lib/textarea"
|
|
209
209
|
export { useTextareaAutosize } from "./lib/useTextareaAutosize"
|
|
210
210
|
export type { TextareaAutosize, UseTextareaAutosizeOptions } from "./lib/useTextareaAutosize"
|
|
211
|
+
export { iconsRegistered, registerIconSet, registerIcons } from "./lib/registerIcons"
|
|
212
|
+
export { lucideIconSet } from "./lib/iconSet"
|
|
213
|
+
export { icons } from "./lib/icons"
|
|
214
|
+
export type { IconSetJson } from "./lib/icons"
|
|
211
215
|
export { chartToECharts, channelValue, pickFromEvent, tableCsv, tableModel, textValue, tokenCss } from "./lib/chart"
|
|
212
216
|
export type {
|
|
213
217
|
QChartAxis,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Icônes embarquées — garde-fou contre la dérive du fichier généré.
|
|
2
|
+
//
|
|
3
|
+
// `lib/iconSet.ts` est produit par `bun scripts/gen-icon-set.ts`. Si quelqu'un ajoute
|
|
4
|
+
// un `lucide:…` dans un composant sans relancer le script, l'icône repasse
|
|
5
|
+
// silencieusement par l'API Iconify (donc : absente du SSR et du mode hors ligne).
|
|
6
|
+
// Ces tests échouent dans ce cas, sans navigateur ni réseau.
|
|
7
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import { describe, expect, it } from "bun:test"
|
|
10
|
+
import { icons } from "./icons"
|
|
11
|
+
import { lucideIconSet } from "./iconSet"
|
|
12
|
+
|
|
13
|
+
const ICON_NAME_RE = /\blucide:[a-z0-9]+(?:-[a-z0-9]+)*/g
|
|
14
|
+
const componentsDir = join(import.meta.dir, "..", "components")
|
|
15
|
+
|
|
16
|
+
const has = (name: string): boolean =>
|
|
17
|
+
name in lucideIconSet.icons || Boolean(lucideIconSet.aliases && name in lucideIconSet.aliases)
|
|
18
|
+
|
|
19
|
+
const strip = (value: string): string => value.slice("lucide:".length)
|
|
20
|
+
|
|
21
|
+
describe("lucideIconSet (généré)", () => {
|
|
22
|
+
it("est une collection lucide valide", () => {
|
|
23
|
+
expect(lucideIconSet.prefix).toBe("lucide")
|
|
24
|
+
expect(lucideIconSet.width).toBe(24)
|
|
25
|
+
expect(lucideIconSet.height).toBe(24)
|
|
26
|
+
expect(Object.keys(lucideIconSet.icons).length).toBeGreaterThan(0)
|
|
27
|
+
for (const [name, icon] of Object.entries(lucideIconSet.icons)) {
|
|
28
|
+
expect(name).not.toContain(":")
|
|
29
|
+
expect(icon.body.length).toBeGreaterThan(0)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("contient toutes les icônes de `lib/icons.ts`", () => {
|
|
34
|
+
const missing = Object.values(icons)
|
|
35
|
+
.map(strip)
|
|
36
|
+
.filter((name) => !has(name))
|
|
37
|
+
expect(missing).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("contient tous les `lucide:…` référencés dans les composants", () => {
|
|
41
|
+
const files = readdirSync(componentsDir, { recursive: true })
|
|
42
|
+
.map(String)
|
|
43
|
+
.filter((path) => path.endsWith(".vue"))
|
|
44
|
+
.map((path) => join(componentsDir, path))
|
|
45
|
+
|
|
46
|
+
const missing = new Set<string>()
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
for (const match of readFileSync(file, "utf8").matchAll(ICON_NAME_RE)) {
|
|
49
|
+
const name = strip(match[0])
|
|
50
|
+
if (!has(name)) missing.add(name)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
expect([...missing]).toEqual([])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("embarque la parente de chaque alias", () => {
|
|
57
|
+
for (const alias of Object.values(lucideIconSet.aliases ?? {})) {
|
|
58
|
+
expect(alias.parent in lucideIconSet.icons).toBe(true)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
package/lib/iconSet.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ⚠️ FICHIER GÉNÉRÉ — ne pas éditer à la main.
|
|
2
|
+
// Commande : bun scripts/gen-icon-set.ts --out ./lib/iconSet.ts --src ./components --src ./lib
|
|
3
|
+
//
|
|
4
|
+
// Sous-ensemble de la collection `lucide` : 68 icônes + 4 alias, relevées dans
|
|
5
|
+
// `./components`, `./lib`. Embarqué dans le bundle, il est enregistré auprès d'Iconify par
|
|
6
|
+
// `registerIcons()` (ou par le module Nuxt `@dnax/ui`) via `addCollection()` :
|
|
7
|
+
// ces icônes ne déclenchent **aucune** requête vers l'API Iconify et sont rendues
|
|
8
|
+
// dès le SSR. Les noms absents de ce fichier continuent de passer par l'API.
|
|
9
|
+
import type { IconSetJson } from "./icons"
|
|
10
|
+
|
|
11
|
+
export const lucideIconSet: IconSetJson = {
|
|
12
|
+
prefix: "lucide",
|
|
13
|
+
width: 24,
|
|
14
|
+
height: 24,
|
|
15
|
+
icons: {
|
|
16
|
+
"align-left": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 12H3m14 6H3M21 6H3\"/>" },
|
|
17
|
+
"arrow-down": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 5v14m7-7l-7 7l-7-7\"/>" },
|
|
18
|
+
"arrow-down-a-z": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m3 16l4 4l4-4m-4 4V4m13 4h-5m0 2V6.5a2.5 2.5 0 0 1 5 0V10m-5 4h5l-5 6h5\"/>" },
|
|
19
|
+
"arrow-down-wide-narrow": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m3 16l4 4l4-4m-4 4V4m4 0h10M11 8h7m-7 4h4\"/>" },
|
|
20
|
+
"arrow-up": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 12l7-7l7 7m-7 7V5\"/>" },
|
|
21
|
+
"arrow-up-a-z": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m3 8l4-4l4 4M7 4v16M20 8h-5m0 2V6.5a2.5 2.5 0 0 1 5 0V10m-5 4h5l-5 6h5\"/>" },
|
|
22
|
+
"arrow-up-down": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m21 16l-4 4l-4-4m4 4V4M3 8l4-4l4 4M7 4v16\"/>" },
|
|
23
|
+
"arrow-up-narrow-wide": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m3 8l4-4l4 4M7 4v16m4-8h4m-4 4h7m-7 4h10\"/>" },
|
|
24
|
+
"calendar-days": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M8 2v3m8-3v3\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18M8 13h.01M12 13h.01M16 13h.01M8 17h.01M12 17h.01M16 17h.01\"/></g>" },
|
|
25
|
+
"check": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 6L9 17l-5-5\"/>" },
|
|
26
|
+
"chevron-down": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"/>" },
|
|
27
|
+
"chevron-left": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m15 18l-6-6l6-6\"/>" },
|
|
28
|
+
"chevron-right": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m9 18l6-6l-6-6\"/>" },
|
|
29
|
+
"chevron-up": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m18 15l-6-6l-6 6\"/>" },
|
|
30
|
+
"chevrons-left": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m11 17l-5-5l5-5m7 10l-5-5l5-5\"/>" },
|
|
31
|
+
"chevrons-right": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 17l5-5l-5-5m7 10l5-5l-5-5\"/>" },
|
|
32
|
+
"clipboard-paste": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M11 14h10M16 4h2a2 2 0 0 1 2 2v1.344M17 18l4-4l-4-4\"/><path d=\"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113\"/><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/></g>" },
|
|
33
|
+
"clock": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></g>" },
|
|
34
|
+
"columns-3": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M9 3v18m6-18v18\"/></g>" },
|
|
35
|
+
"copy": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></g>" },
|
|
36
|
+
"delete": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm2 4l6 6m0-6l-6 6\"/>" },
|
|
37
|
+
"download": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"m7 10l5 5l5-5\"/></g>" },
|
|
38
|
+
"ellipsis": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></g>" },
|
|
39
|
+
"ellipsis-vertical": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/></g>" },
|
|
40
|
+
"eraser": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21m-7.752-9.91l8.828 8.828\"/>" },
|
|
41
|
+
"eye": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696a10.75 10.75 0 0 1 19.876 0a1 1 0 0 1 0 .696a10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></g>" },
|
|
42
|
+
"eye-off": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575a1 1 0 0 1 0 .696a10.8 10.8 0 0 1-1.444 2.49m-6.41-.679a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151a1 1 0 0 1 0-.696a10.75 10.75 0 0 1 4.446-5.143M2 2l20 20\"/></g>" },
|
|
43
|
+
"file": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/></g>" },
|
|
44
|
+
"file-archive": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5M8 12v-1m0 7v-2m0-9V6\"/><circle cx=\"8\" cy=\"20\" r=\"2\"/></g>" },
|
|
45
|
+
"file-audio": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M17.5 22h.5a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M2 19a2 2 0 1 1 4 0v1a2 2 0 1 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 1 1-4 0v-1a2 2 0 1 1 4 0\"/></g>" },
|
|
46
|
+
"file-code": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5m-10 4.5L8 15l2 2.5m4-5l2 2.5l-2 2.5\"/></g>" },
|
|
47
|
+
"file-image": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/><circle cx=\"10\" cy=\"12\" r=\"2\"/><path d=\"m20 17l-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22\"/></g>" },
|
|
48
|
+
"file-play": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5m-4.967 5.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z\"/></g>" },
|
|
49
|
+
"file-plus": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5M9 15h6m-3 3v-6\"/></g>" },
|
|
50
|
+
"file-spreadsheet": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5M8 13h2m4 0h2m-8 4h2m4 0h2\"/></g>" },
|
|
51
|
+
"file-text": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8\"/></g>" },
|
|
52
|
+
"filter": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M22 3H2l8 9.46V19l4 2v-8.54z\"/>" },
|
|
53
|
+
"globe": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 2a14.5 14.5 0 0 0 0 20a14.5 14.5 0 0 0 0-20M2 12h20\"/></g>" },
|
|
54
|
+
"grip-vertical": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"9\" cy=\"12\" r=\"1\"/><circle cx=\"9\" cy=\"5\" r=\"1\"/><circle cx=\"9\" cy=\"19\" r=\"1\"/><circle cx=\"15\" cy=\"12\" r=\"1\"/><circle cx=\"15\" cy=\"5\" r=\"1\"/><circle cx=\"15\" cy=\"19\" r=\"1\"/></g>" },
|
|
55
|
+
"highlighter": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m9 11l-6 6v3h9l3-3\"/><path d=\"m22 12l-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4\"/></g>" },
|
|
56
|
+
"house": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/></g>" },
|
|
57
|
+
"image": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></g>" },
|
|
58
|
+
"image-plus": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M16 5h6m-3-3v6m2 3.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5\"/><path d=\"m21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/></g>" },
|
|
59
|
+
"info": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4m0-4h.01\"/></g>" },
|
|
60
|
+
"italic": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 4h-9m4 16H5M15 4L9 20\"/>" },
|
|
61
|
+
"letter-b": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/>" },
|
|
62
|
+
"loader-circle": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 12a9 9 0 1 1-6.219-8.56\"/>" },
|
|
63
|
+
"menu": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"/>" },
|
|
64
|
+
"minus": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14\"/>" },
|
|
65
|
+
"paperclip": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m16 6l-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551\"/>" },
|
|
66
|
+
"pencil": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497zM15 5l4 4\"/>" },
|
|
67
|
+
"plus": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7v14\"/>" },
|
|
68
|
+
"redo-2": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m15 14l5-5l-5-5\"/><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\"/></g>" },
|
|
69
|
+
"refresh-cw": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M3 12a9 9 0 0 1 9-9a9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5m5 4a9 9 0 0 1-9 9a9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></g>" },
|
|
70
|
+
"rows-3": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M21 9H3m18 6H3\"/></g>" },
|
|
71
|
+
"scissors": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"6\" cy=\"6\" r=\"3\"/><path d=\"M8.12 8.12L12 12m8-8L8.12 15.88\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M14.8 14.8L20 20\"/></g>" },
|
|
72
|
+
"search": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m21 21l-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></g>" },
|
|
73
|
+
"send": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11zm7.318-19.539l-10.94 10.939\"/>" },
|
|
74
|
+
"settings": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0a2.34 2.34 0 0 0 3.319 1.915a2.34 2.34 0 0 1 2.33 4.033a2.34 2.34 0 0 0 0 3.831a2.34 2.34 0 0 1-2.33 4.033a2.34 2.34 0 0 0-3.319 1.915a2.34 2.34 0 0 1-4.659 0a2.34 2.34 0 0 0-3.32-1.915a2.34 2.34 0 0 1-2.33-4.033a2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></g>" },
|
|
75
|
+
"share": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 2v13m4-9l-4-4l-4 4m-4 6v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\"/>" },
|
|
76
|
+
"square-minus": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M8 12h8\"/></g>" },
|
|
77
|
+
"square-plus": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M8 12h8m-4-4v8\"/></g>" },
|
|
78
|
+
"star": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.12 2.12 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16z\"/>" },
|
|
79
|
+
"table-cells-merge": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 21v-6m0-6V3M3 15h18M3 9h18\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/></g>" },
|
|
80
|
+
"trash": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/>" },
|
|
81
|
+
"undo-2": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M9 14L4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\"/></g>" },
|
|
82
|
+
"user": { body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></g>" },
|
|
83
|
+
"x": { body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/>" },
|
|
84
|
+
},
|
|
85
|
+
aliases: {
|
|
86
|
+
"bold": { parent: "letter-b" },
|
|
87
|
+
"file-video": { parent: "file-play" },
|
|
88
|
+
"home": { parent: "house" },
|
|
89
|
+
"trash-2": { parent: "trash" },
|
|
90
|
+
},
|
|
91
|
+
}
|
|
@@ -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,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.75",
|
|
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"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Génère un **sous-ensemble** de la collection `lucide` contenant uniquement les
|
|
3
|
+
* icônes réellement référencées dans le code, sous forme d'un module TS prêt à être
|
|
4
|
+
* embarqué dans le bundle.
|
|
5
|
+
*
|
|
6
|
+
* C'est ce qui produit `lib/iconSet.ts` (icônes du design system, figées et testées).
|
|
7
|
+
* Le module Nuxt fait la même chose **à la volée** pour les sources de l'app hôte
|
|
8
|
+
* (option `icons.scan`) ; ce script sert quand on veut un fichier committé, ou sans Nuxt.
|
|
9
|
+
*
|
|
10
|
+
* Usage :
|
|
11
|
+
* # design system (composants + lib) → packages/ui/lib/iconSet.ts
|
|
12
|
+
* bun scripts/gen-icon-set.ts --out ./lib/iconSet.ts --src ./components --src ./lib
|
|
13
|
+
*
|
|
14
|
+
* # app / site de docs → ./app/iconSet.ts
|
|
15
|
+
* bun scripts/gen-icon-set.ts --out ./app/iconSet.ts --src ./app --src ./content \
|
|
16
|
+
* --type-import @dnax/ui
|
|
17
|
+
*
|
|
18
|
+
* `--src` est récursif (`.vue`, `.ts`, `.js`, `.mjs`, `.md`), les dossiers
|
|
19
|
+
* `node_modules` / `.nuxt` / `.output` / `dist` / `public` sont ignorés. Un nom absent
|
|
20
|
+
* de la collection est signalé (faute de frappe le plus souvent).
|
|
21
|
+
*/
|
|
22
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
23
|
+
import { dirname, resolve } from "node:path"
|
|
24
|
+
import {
|
|
25
|
+
buildIconSet,
|
|
26
|
+
scrapeIconNames,
|
|
27
|
+
type IconifySourceCollection,
|
|
28
|
+
} from "../lib/iconSetScrape"
|
|
29
|
+
|
|
30
|
+
// — Arguments ------------------------------------------------------------------
|
|
31
|
+
let outFile = ""
|
|
32
|
+
let typeImport = "./icons"
|
|
33
|
+
const sources: string[] = []
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2)
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i]!
|
|
38
|
+
if (arg === "--out") outFile = args[++i] ?? ""
|
|
39
|
+
else if (arg === "--src") sources.push(args[++i] ?? "")
|
|
40
|
+
else if (arg === "--type-import") typeImport = args[++i] ?? "./icons"
|
|
41
|
+
else {
|
|
42
|
+
console.error(`Argument inconnu : ${arg}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!outFile || sources.length === 0) {
|
|
48
|
+
console.error(
|
|
49
|
+
"Usage : bun scripts/gen-icon-set.ts --out <fichier.ts> --src <dossier> [--src <dossier>…] [--type-import <specifier>]",
|
|
50
|
+
)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// — Relevé + résolution --------------------------------------------------------
|
|
55
|
+
const resolvedSources = sources.map((src) => resolve(process.cwd(), src))
|
|
56
|
+
const names = scrapeIconNames(resolvedSources)
|
|
57
|
+
|
|
58
|
+
if (names.length === 0) {
|
|
59
|
+
console.error("Aucune icône `lucide:` trouvée dans les sources fournies.")
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const collectionUrl = import.meta.resolve("@iconify-json/lucide/icons.json")
|
|
64
|
+
const collection = JSON.parse(await readFile(new URL(collectionUrl), "utf8")) as IconifySourceCollection
|
|
65
|
+
const { set, missing } = buildIconSet(collection, names)
|
|
66
|
+
|
|
67
|
+
// — Écriture -------------------------------------------------------------------
|
|
68
|
+
const iconNames = Object.keys(set.icons).sort()
|
|
69
|
+
const aliasNames = Object.keys(set.aliases ?? {}).sort()
|
|
70
|
+
|
|
71
|
+
const iconLines = iconNames
|
|
72
|
+
.map((name) => ` ${JSON.stringify(name)}: { body: ${JSON.stringify(set.icons[name]!.body)} },`)
|
|
73
|
+
.join("\n")
|
|
74
|
+
const aliasLines = aliasNames
|
|
75
|
+
.map(
|
|
76
|
+
(name) =>
|
|
77
|
+
` ${JSON.stringify(name)}: { parent: ${JSON.stringify(set.aliases![name]!.parent)} },`,
|
|
78
|
+
)
|
|
79
|
+
.join("\n")
|
|
80
|
+
|
|
81
|
+
const sourceLabel = sources.map((src) => `\`${src}\``).join(", ")
|
|
82
|
+
const header = `// ⚠️ FICHIER GÉNÉRÉ — ne pas éditer à la main.
|
|
83
|
+
// Commande : bun scripts/gen-icon-set.ts --out ${outFile} --src ${sources.join(" --src ")}${typeImport === "./icons" ? "" : ` --type-import ${typeImport}`}
|
|
84
|
+
//
|
|
85
|
+
// Sous-ensemble de la collection \`lucide\` : ${iconNames.length} icônes${aliasNames.length ? ` + ${aliasNames.length} alias` : ""}, relevées dans
|
|
86
|
+
// ${sourceLabel}. Embarqué dans le bundle, il est enregistré auprès d'Iconify par
|
|
87
|
+
// \`registerIcons()\` (ou par le module Nuxt \`@dnax/ui\`) via \`addCollection()\` :
|
|
88
|
+
// ces icônes ne déclenchent **aucune** requête vers l'API Iconify et sont rendues
|
|
89
|
+
// dès le SSR. Les noms absents de ce fichier continuent de passer par l'API.
|
|
90
|
+
import type { IconSetJson } from "${typeImport}"
|
|
91
|
+
|
|
92
|
+
export const lucideIconSet: IconSetJson = {
|
|
93
|
+
prefix: "lucide",
|
|
94
|
+
width: ${set.width ?? 24},
|
|
95
|
+
height: ${set.height ?? 24},
|
|
96
|
+
icons: {
|
|
97
|
+
${iconLines}
|
|
98
|
+
},
|
|
99
|
+
${aliasLines ? ` aliases: {\n${aliasLines}\n },\n` : ""}}
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
const outPath = resolve(process.cwd(), outFile)
|
|
103
|
+
await mkdir(dirname(outPath), { recursive: true })
|
|
104
|
+
await writeFile(outPath, header)
|
|
105
|
+
|
|
106
|
+
const kb = (Buffer.byteLength(header) / 1024).toFixed(1)
|
|
107
|
+
console.log(`✓ ${iconNames.length} icône(s) + ${aliasNames.length} alias → ${outFile} (${kb} Ko)`)
|
|
108
|
+
if (missing.length) {
|
|
109
|
+
console.warn(`⚠️ ${missing.length} nom(s) introuvable(s) dans lucide : ${missing.join(", ")}`)
|
|
110
|
+
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Regénère `index.ts` : une ligne d'export par composant, puis le **bloc manuel**
|
|
3
|
+
* (composables, helpers, types de `lib/`) recopié **tel quel**.
|
|
3
4
|
* Run with: bun scripts/generate-exports.ts
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ Ne jamais figer le bloc manuel dans ce script. Une version précédente le
|
|
7
|
+
* contenait en dur ; il avait pris du retard sur `index.ts` et une exécution a
|
|
8
|
+
* silencieusement supprimé `network`, `wakeLock`, `chartFormat`, `chartStats`,
|
|
9
|
+
* `chartTable`… (cf. `.memory/warnings.md`). Ici le bloc manuel est **relu** depuis
|
|
10
|
+
* `index.ts` : tout ce qui suit le dernier export de composant est conservé, donc le
|
|
11
|
+
* script est idempotent et ne peut plus rien perdre.
|
|
4
12
|
*/
|
|
5
|
-
import { readdir } from "node:fs/promises"
|
|
13
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
6
14
|
|
|
7
15
|
const componentsDir = new URL("../components/", import.meta.url)
|
|
8
16
|
const indexFile = new URL("../index.ts", import.meta.url)
|
|
9
17
|
|
|
18
|
+
/** Ligne produite pour un composant : `export { default as QX } from "./components/QX.vue"` */
|
|
19
|
+
const COMPONENT_EXPORT_LINE = /^export \{ default as [\w$]+ \} from "\.\/components\/[\w$]+\.vue"$/
|
|
20
|
+
|
|
10
21
|
const toPascalCase = (name: string) =>
|
|
11
22
|
name
|
|
12
23
|
.split(/[-_]/)
|
|
@@ -17,78 +28,44 @@ const files = (await readdir(componentsDir))
|
|
|
17
28
|
.filter((file) => file.endsWith(".vue") && !file.startsWith("_"))
|
|
18
29
|
.sort()
|
|
19
30
|
|
|
20
|
-
const
|
|
31
|
+
const componentExports = files
|
|
21
32
|
.map(
|
|
22
33
|
(file) =>
|
|
23
34
|
`export { default as ${toPascalCase(file.replace(/\.vue$/, ""))} } from "./components/${file}"`,
|
|
24
35
|
)
|
|
25
36
|
.join("\n")
|
|
26
37
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export { placePopover } from "./lib/datePicker"
|
|
58
|
-
export type {
|
|
59
|
-
PlacePopoverOptions,
|
|
60
|
-
PopoverAnchor,
|
|
61
|
-
PopoverPlacement,
|
|
62
|
-
PopoverViewport,
|
|
63
|
-
} from "./lib/datePicker"
|
|
64
|
-
export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
|
|
65
|
-
export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
|
|
66
|
-
export { vRipple } from "./lib/ripple"
|
|
67
|
-
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
68
|
-
export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
|
|
69
|
-
export type {
|
|
70
|
-
QChartAxis,
|
|
71
|
-
QChartChannel,
|
|
72
|
-
QChartColumn,
|
|
73
|
-
QChartConfig,
|
|
74
|
-
QChartLegend,
|
|
75
|
-
QChartMark,
|
|
76
|
-
QChartMarkType,
|
|
77
|
-
QChartOrientation,
|
|
78
|
-
QChartTable,
|
|
79
|
-
QChartPick,
|
|
80
|
-
} from "./lib/chart"
|
|
81
|
-
export { vTouchPan } from "./lib/touchPan"
|
|
82
|
-
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
83
|
-
export { vTouchHold } from "./lib/touchHold"
|
|
84
|
-
export type { TouchHoldDetails, TouchHoldHandler } from "./lib/touchHold"
|
|
85
|
-
export { vTouchSwipe } from "./lib/touchSwipe"
|
|
86
|
-
export type { TouchSwipeDetails, TouchSwipeHandler, SwipeDirection } from "./lib/touchSwipe"
|
|
87
|
-
export { vTouchRepeat } from "./lib/touchRepeat"
|
|
88
|
-
export type { TouchRepeatDetails, TouchRepeatHandler } from "./lib/touchRepeat"
|
|
89
|
-
export { vIntersection } from "./lib/intersection"
|
|
90
|
-
export type { IntersectionValue } from "./lib/intersection"
|
|
91
|
-
`
|
|
38
|
+
// — Bloc manuel : relu dans l'`index.ts` courant -------------------------------
|
|
39
|
+
let current = ""
|
|
40
|
+
try {
|
|
41
|
+
current = await readFile(indexFile, "utf8")
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"packages/ui/index.ts est introuvable : le bloc manuel (exports de `lib/`) ne peut pas être conservé. " +
|
|
45
|
+
"Recréez le fichier avec ses exports manuels avant de lancer ce script.",
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = current.split("\n")
|
|
50
|
+
let lastComponentLine = -1
|
|
51
|
+
lines.forEach((line, i) => {
|
|
52
|
+
if (COMPONENT_EXPORT_LINE.test(line)) lastComponentLine = i
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Fichier sans aucun export de composant (création manuelle) : tout est conservé.
|
|
56
|
+
const manualExports = (lastComponentLine === -1 ? current : lines.slice(lastComponentLine + 1).join("\n"))
|
|
57
|
+
.trim()
|
|
58
|
+
|
|
59
|
+
if (lastComponentLine === -1 && /\bexport\b/.test(manualExports)) {
|
|
60
|
+
console.warn(
|
|
61
|
+
"⚠️ Aucun export de composant reconnu dans index.ts : tout son contenu est traité comme bloc manuel.",
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const output = manualExports ? `${componentExports}\n\n${manualExports}\n` : `${componentExports}\n`
|
|
66
|
+
|
|
67
|
+
await Bun.write(indexFile, output)
|
|
92
68
|
|
|
93
|
-
|
|
69
|
+
const kept = manualExports ? manualExports.split("\n").length : 0
|
|
94
70
|
console.log(`✓ ${files.length} composant(s) exporté(s) dans index.ts`)
|
|
71
|
+
console.log(`✓ ${kept} ligne(s) d'exports manuels conservées (bloc relu depuis index.ts)`)
|
package/styles/main.css
CHANGED
|
@@ -2972,6 +2972,11 @@ html.dark body {
|
|
|
2972
2972
|
border-radius: var(--q-radius);
|
|
2973
2973
|
}
|
|
2974
2974
|
|
|
2975
|
+
/* `radius` (booléen) : forme pilule, cf. JSDoc de la prop. */
|
|
2976
|
+
.q-img--rounded {
|
|
2977
|
+
border-radius: 9999px;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2975
2980
|
.q-img__image {
|
|
2976
2981
|
position: absolute;
|
|
2977
2982
|
inset: 0;
|
|
@@ -9254,6 +9259,13 @@ a.q-breadcrumbs__el:hover {
|
|
|
9254
9259
|
.q-input-otp__box--error {
|
|
9255
9260
|
border-color: var(--negative);
|
|
9256
9261
|
}
|
|
9262
|
+
|
|
9263
|
+
/* `radius` (booléen) : forme pilule — même intention que `.q-field--rounded` (20px).
|
|
9264
|
+
Les cases font 48px : 20px les rend nettement arrondies sans les transformer en
|
|
9265
|
+
cercles, ce qui laisserait des trous entre les caractères. */
|
|
9266
|
+
.q-input-otp--rounded .q-input-otp__box {
|
|
9267
|
+
border-radius: 20px;
|
|
9268
|
+
}
|
|
9257
9269
|
.q-input-otp__box--error:focus {
|
|
9258
9270
|
border-color: var(--negative);
|
|
9259
9271
|
box-shadow: 0 0 0 1px var(--negative);
|