@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.
@@ -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="radius"
144
+ :radius="effectiveRadius"
138
145
  :flat="flat"
139
146
  :outline="outline"
140
147
  :unelevated="unelevated"
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
40
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
41
41
  </script>
42
42
 
43
43
  <template>
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
153
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
154
154
 
155
155
  const contentClasses = computed(() =>
156
156
  cn(
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- // QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:photo" text="Photos" badge="3" />
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"
@@ -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"
@@ -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
 
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
47
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
48
48
 
49
49
  const slots = useSlots()
50
50
 
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
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 { RADIUS_VALUES, radiusStyle, radiusValue } from "./useComponentProps"
9
+ import {
10
+ RADIUS_VALUES,
11
+ isRadiusScale,
12
+ radiusStyle,
13
+ radiusStyleNative,
14
+ radiusValue,
15
+ } from "./useComponentProps"
4
16
 
5
17
  describe("radiusValue", () => {
6
18
  it("traduit chaque barreau de l'échelle", () => {
@@ -33,4 +45,46 @@ describe("radiusStyle", () => {
33
45
  expect(radiusStyle(true)).toBeUndefined()
34
46
  expect(radiusStyle(undefined)).toBeUndefined()
35
47
  })
48
+
49
+ it("gère aussi none (carre) et l'échelle basse", () => {
50
+ expect(radiusStyle("none")).toEqual({ "--q-radius": "0px" })
51
+ expect(radiusStyle("sm")).toEqual({ "--q-radius": "4px" })
52
+ })
53
+ })
54
+
55
+ describe("isRadiusScale", () => {
56
+ it("reconnaît les échelles et rejette le reste", () => {
57
+ expect(isRadiusScale("md")).toBe(true)
58
+ expect(isRadiusScale("none")).toBe(true)
59
+ expect(isRadiusScale("12px")).toBe(false)
60
+ expect(isRadiusScale(true)).toBe(false)
61
+ expect(isRadiusScale(undefined)).toBe(false)
62
+ })
63
+ })
64
+
65
+ describe("RADIUS_VALUES", () => {
66
+ it("couvre toute l'échelle, croissante", () => {
67
+ expect(Object.keys(RADIUS_VALUES)).toEqual(["none", "xs", "sm", "md", "lg"])
68
+ expect(RADIUS_VALUES.none).toBe("0px")
69
+ expect(RADIUS_VALUES.lg).toBe("16px")
70
+ })
71
+ })
72
+
73
+ describe("radiusStyleNative (composants « true = échelle »)", () => {
74
+ it("convertit `true` en md par défaut", () => {
75
+ expect(radiusStyleNative(true)).toEqual({ "--q-radius": "8px" })
76
+ })
77
+
78
+ it("accepte une autre échelle par défaut", () => {
79
+ expect(radiusStyleNative(true, "lg")).toEqual({ "--q-radius": "16px" })
80
+ })
81
+
82
+ it("transmet une échelle telle quelle", () => {
83
+ expect(radiusStyleNative("sm")).toEqual({ "--q-radius": "4px" })
84
+ expect(radiusStyleNative("none")).toEqual({ "--q-radius": "0px" })
85
+ })
86
+
87
+ it("laisse undefined inchangé", () => {
88
+ expect(radiusStyleNative(undefined)).toBeUndefined()
89
+ })
36
90
  })
@@ -65,3 +65,18 @@ export function radiusStyle(radius: RadiusProp | undefined): Record<string, stri
65
65
  const value = radiusValue(radius)
66
66
  return value ? { "--q-radius": value } : undefined
67
67
  }
68
+
69
+ /**
70
+ * Style `--q-radius` des composants dont le booléen `radius` vaut une **échelle fixe**
71
+ * (documenté « true = md ») : `true` → `scale`.
72
+ *
73
+ * Les composants à forme native « pilule » (champs, boutons) ne passent **pas** par ici :
74
+ * ils posent leur propre classe (`.q-field--rounded`, `.q-btn--rounded`) — pour eux,
75
+ * `true` ne doit surtout pas devenir un `--q-radius` global qui fuirait aux enfants.
76
+ */
77
+ export function radiusStyleNative(
78
+ radius: RadiusProp | undefined,
79
+ scale: RadiusScale = "md",
80
+ ): Record<string, string> | undefined {
81
+ return radiusStyle(radius === true ? scale : radius)
82
+ }
package/module.ts CHANGED
@@ -4,12 +4,39 @@
4
4
  *
5
5
  * - Auto-importe les composants Q* (balises kebab-case : <q-btn>, <q-header>…)
6
6
  * - Ajoute styles/main.css (safe-area, tokens, styles de tous les composants)
7
+ * - **Embarque les icônes** : enregistre un sous-ensemble `lucide` auprès d'Iconify
8
+ * (`addCollection`) — design system + (par défaut) les icônes `lucide:…` trouvées
9
+ * dans les sources de l'app. Aucune requête vers l'API Iconify pour ces noms, et les
10
+ * `<path>` sont présents dès le SSR.
7
11
  *
8
12
  * Options (clé `dnaxUi`) :
9
- * components: false — désactive l'auto-import des composants
10
- * css: false — désactive l'ajout de styles/main.css
13
+ * components: false — désactive l'auto-import des composants
14
+ * css: false — désactive l'ajout de styles/main.css
15
+ * icons: false — n'embarque aucune icône (API Iconify à la demande)
16
+ * icons: { scan: false } — n'embarque que le design system
17
+ * icons: { dirs: ["app"] } — limite le scan (chemins relatifs à rootDir)
11
18
  */
12
- import { addComponentsDir, addPluginTemplate, createResolver, defineNuxtModule } from "@nuxt/kit"
19
+ import { addComponentsDir, addPluginTemplate, createResolver, defineNuxtModule, updateTemplates } from "@nuxt/kit"
20
+ import { readFileSync } from "node:fs"
21
+ import { createRequire } from "node:module"
22
+ import { join, resolve } from "node:path"
23
+ import { lucideIconSet } from "./lib/iconSet"
24
+ import {
25
+ buildIconSet,
26
+ mergeIconSets,
27
+ scrapeIconNames,
28
+ type IconifySourceCollection,
29
+ } from "./lib/iconSetScrape"
30
+
31
+ export interface DnaxUiIconsOptions {
32
+ /**
33
+ * Scanne les sources de l'app au build pour embarquer aussi **ses** icônes
34
+ * `lucide:` (défaut : true)
35
+ */
36
+ scan?: boolean
37
+ /** Dossiers à scanner, résolus depuis `rootDir` (défaut : tout `rootDir`) */
38
+ dirs?: string[]
39
+ }
13
40
 
14
41
  export interface DnaxUiModuleOptions {
15
42
  /** Auto-importe les composants Q* (défaut : true) */
@@ -18,6 +45,33 @@ export interface DnaxUiModuleOptions {
18
45
  css?: boolean
19
46
  /** Installe le handler « retour » : ferme l'overlay ouvert au lieu de naviguer (défaut : true) */
20
47
  overlayBack?: boolean
48
+ /**
49
+ * Icônes : `false` = rien d'embarqué (API Iconify à la demande) ; objet = réglages
50
+ * du scan. Par défaut : sous-ensemble du design system **+** scan des sources.
51
+ */
52
+ icons?: boolean | DnaxUiIconsOptions
53
+ }
54
+
55
+ /** Collection Iconify lue pour résoudre les noms (`@iconify-json/lucide`). */
56
+ const LUCIDE_COLLECTION = "@iconify-json/lucide/icons.json"
57
+
58
+ /**
59
+ * Charge la collection `lucide`. Elle vit dans les dépendances de `@dnax/ui` ; on tente
60
+ * la résolution depuis ce module, puis depuis l'app hôte (hoisting).
61
+ */
62
+ const loadLucideCollection = (rootDir: string): IconifySourceCollection | null => {
63
+ const resolvers = [
64
+ () => createRequire(import.meta.url).resolve(LUCIDE_COLLECTION),
65
+ () => createRequire(join(rootDir, "package.json")).resolve(LUCIDE_COLLECTION),
66
+ ]
67
+ for (const resolveSpec of resolvers) {
68
+ try {
69
+ return JSON.parse(readFileSync(resolveSpec(), "utf8")) as IconifySourceCollection
70
+ } catch {
71
+ // on passe au résolveur suivant
72
+ }
73
+ }
74
+ return null
21
75
  }
22
76
 
23
77
  export default defineNuxtModule<DnaxUiModuleOptions>({
@@ -30,12 +84,13 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
30
84
  components: true,
31
85
  css: true,
32
86
  overlayBack: true,
87
+ icons: { scan: true },
33
88
  },
34
89
  setup(options, nuxt) {
35
- const { resolve } = createResolver(import.meta.url)
90
+ const { resolve: resolvePath } = createResolver(import.meta.url)
36
91
 
37
92
  if (options.css) {
38
- const cssPath = resolve("./styles/main.css")
93
+ const cssPath = resolvePath("./styles/main.css")
39
94
  if (!nuxt.options.css.includes(cssPath)) {
40
95
  nuxt.options.css.push(cssPath)
41
96
  }
@@ -43,13 +98,76 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
43
98
 
44
99
  if (options.components) {
45
100
  addComponentsDir({
46
- path: resolve("./components"),
101
+ path: resolvePath("./components"),
47
102
  prefix: "", // pas de préfixe : QBtn.vue → <q-btn>
48
103
  pathPrefix: false, // pas de préfixe par sous-dossier
49
104
  ignore: ["**/internal/**"], // composants internes : non auto-importés
50
105
  })
51
106
  }
52
107
 
108
+ // — Icônes embarquées ---------------------------------------------------------
109
+ // Plugin **universel** : côté serveur aussi, sinon le SSR ne rend que des coquilles
110
+ // `<svg class="iconify">`, les `<path>` n'arrivant qu'après le fetch client (et
111
+ // jamais sans réseau). `addCollection` est purement local et **fusionne**.
112
+ if (options.icons !== false) {
113
+ const iconsOptions: DnaxUiIconsOptions =
114
+ typeof options.icons === "object" ? options.icons : {}
115
+ const scan = iconsOptions.scan !== false
116
+
117
+ const dirs = iconsOptions.dirs?.length
118
+ ? iconsOptions.dirs.map((dir) => resolve(nuxt.options.rootDir, dir))
119
+ : [nuxt.options.rootDir]
120
+
121
+ const collection = scan ? loadLucideCollection(nuxt.options.rootDir) : null
122
+ if (scan && !collection) {
123
+ console.warn(
124
+ "[@dnax/ui] `@iconify-json/lucide` introuvable : seules les icônes du design system sont embarquées. " +
125
+ "Installez-le (`bun add -D @iconify-json/lucide`) pour scanner votre app.",
126
+ )
127
+ }
128
+
129
+ addPluginTemplate({
130
+ filename: "dnax-ui-icons.mjs",
131
+ getContents: () => {
132
+ let set = lucideIconSet
133
+ let scanned = false
134
+ if (collection) {
135
+ const { set: appSet, missing } = buildIconSet(collection, scrapeIconNames(dirs))
136
+ set = mergeIconSets(lucideIconSet, appSet)
137
+ scanned = true
138
+ if (nuxt.options.dev && missing.length) {
139
+ console.warn(
140
+ `[@dnax/ui] icône(s) \`lucide:\` absente(s) de la collection (faute de frappe ?) : ${missing.join(", ")}`,
141
+ )
142
+ }
143
+ }
144
+ return `
145
+ import { defineNuxtPlugin } from "#imports"
146
+ import { registerIconSet } from "@dnax/ui/runtime"
147
+
148
+ // Sous-ensemble \`lucide\` embarqué (${Object.keys(set.icons).length} icônes) :
149
+ // design system${scanned ? " + sources de l'app" : ""}. Aucune requête vers l'API Iconify
150
+ // pour ces noms ; les autres retombent dessus (progressive enhancement).
151
+ const ICON_SET = ${JSON.stringify(set)}
152
+
153
+ export default defineNuxtPlugin(() => {
154
+ registerIconSet(ICON_SET)
155
+ })
156
+ `
157
+ },
158
+ })
159
+
160
+ // Dev : une icône ajoutée dans les sources régénère le module virtuel — sans ça
161
+ // il faudrait redémarrer le serveur pour l'embarquer (en attendant, l'icône
162
+ // s'affiche via l'API : dégradation, pas rupture).
163
+ if (scan) {
164
+ nuxt.hook("builder:watch", async (_event, watchedPath) => {
165
+ if (!/\.(vue|ts|js|mjs|md|mdx)$/.test(String(watchedPath).replace(/\\/g, "/"))) return
166
+ await updateTemplates({ filter: (template) => template.filename === "dnax-ui-icons.mjs" })
167
+ })
168
+ }
169
+ }
170
+
53
171
  // Dépendances CJS sans export ESM `default` explicite : esbuild doit les pré-bundler
54
172
  // (interop) sinon, servies brutes, le navigateur échoue sur « does not provide an
55
173
  // export named 'default' ». `leaflet` (lib/mapLeaflet.ts) est dans ce cas ; le code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.73",
3
+ "version": "0.0.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
- * Regenerates `index.ts` with one explicit export per component.
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 exports = files
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
- // Exports manuels (composables / helpers) — conservés à chaque régénération
28
- const manualExports = `
29
- export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
30
- export type {
31
- DialogOptions,
32
- DialogController,
33
- BottomSheetOptions,
34
- BottomSheetController,
35
- BottomSheetPluginContext,
36
- NotifyOptions,
37
- NotifyController,
38
- } from "./lib/q"
39
- export { platform, qBreakpoints } from "./lib/platform"
40
- export type { QPlatform } from "./lib/platform"
41
- export { screen } from "./lib/screen"
42
- export type { QScreen } from "./lib/screen"
43
- export { loading } from "./lib/loading"
44
- export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
45
- export { localStorage, sessionStorage } from "./lib/storage"
46
- export type { QWebStorage } from "./lib/storage"
47
- export {
48
- registerOverlay,
49
- unregisterOverlay,
50
- hasOpenOverlays,
51
- closeTopmostOverlay,
52
- useOverlayBack,
53
- installOverlayBackHandler,
54
- } from "./lib/overlayBack"
55
- export type { OverlayHandle } from "./lib/overlayBack"
56
- export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
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
- await Bun.write(indexFile, `${exports}\n${manualExports}`)
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);