@dnax/ui 0.0.72 → 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/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.72",
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
@@ -439,6 +439,7 @@ html.dark body {
439
439
  }
440
440
 
441
441
  .dark .q-input .q-field__native::placeholder,
442
+ .dark .q-input-textarea .q-field__native::placeholder,
442
443
  .dark .q-input-tag__native::placeholder,
443
444
  .dark .q-input-otp__box::placeholder,
444
445
  .dark .q-select__search-input::placeholder,
@@ -2358,6 +2359,61 @@ html.dark body {
2358
2359
  margin-left: auto;
2359
2360
  }
2360
2361
 
2362
+ /* ===== QInputTextarea (champ multiligne) ===== */
2363
+ .q-input-textarea {
2364
+ display: flex;
2365
+ flex-direction: column;
2366
+ min-width: 0;
2367
+ font-size: 14px;
2368
+ }
2369
+
2370
+ /* Le contenu est ancré **en haut** du contrôle (un champ simple centre sa valeur) :
2371
+ `rows` doit correspondre à ce qu'on voit, et les icônes / affixes restent sur la
2372
+ première ligne quand le champ grandit. 10px de padding = 40px de contrôle pour
2373
+ 1 ligne de 20px, soit la hauteur exacte du champ simple. */
2374
+ .q-input-textarea .q-field__control {
2375
+ align-items: flex-start;
2376
+ padding: 10px 12px;
2377
+ }
2378
+
2379
+ .q-input-textarea .q-field__native {
2380
+ flex: 1;
2381
+ width: 100%;
2382
+ min-width: 0;
2383
+ height: auto;
2384
+ padding: 0;
2385
+ border: none;
2386
+ outline: none;
2387
+ background: transparent;
2388
+ color: inherit;
2389
+ font: inherit;
2390
+ /* `rows` × line-height = hauteur visible ; l'auto-extension mesure la même valeur */
2391
+ line-height: 20px;
2392
+ resize: none;
2393
+ overflow-y: hidden;
2394
+ }
2395
+
2396
+ .q-input-textarea .q-field__native::placeholder {
2397
+ color: rgb(0 0 0 / 0.45);
2398
+ }
2399
+
2400
+ /* Affixes, icônes et bouton d'effacement : sur la première ligne */
2401
+ .q-input-textarea .q-field__icon,
2402
+ .q-input-textarea .q-field__prefix,
2403
+ .q-input-textarea .q-field__suffix,
2404
+ .q-input-textarea .q-field__clear {
2405
+ align-self: flex-start;
2406
+ line-height: 20px;
2407
+ }
2408
+
2409
+ .q-field--dense.q-input-textarea .q-field__control {
2410
+ padding: 7px 10px;
2411
+ }
2412
+
2413
+ .q-field--dense.q-input-textarea .q-field__native {
2414
+ line-height: 18px;
2415
+ }
2416
+
2361
2417
  /* ===== QInputPassword (toggle œil) ===== */
2362
2418
  .q-input-password__toggle {
2363
2419
  display: inline-flex;
@@ -2916,6 +2972,11 @@ html.dark body {
2916
2972
  border-radius: var(--q-radius);
2917
2973
  }
2918
2974
 
2975
+ /* `radius` (booléen) : forme pilule, cf. JSDoc de la prop. */
2976
+ .q-img--rounded {
2977
+ border-radius: 9999px;
2978
+ }
2979
+
2919
2980
  .q-img__image {
2920
2981
  position: absolute;
2921
2982
  inset: 0;
@@ -9198,6 +9259,13 @@ a.q-breadcrumbs__el:hover {
9198
9259
  .q-input-otp__box--error {
9199
9260
  border-color: var(--negative);
9200
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
+ }
9201
9269
  .q-input-otp__box--error:focus {
9202
9270
  border-color: var(--negative);
9203
9271
  box-shadow: 0 0 0 1px var(--negative);