@dnax/ui 0.0.53 → 0.0.55

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.
@@ -9,6 +9,7 @@ import { cn } from "../lib/utils"
9
9
  import { radiusStyle, useRadius } from "../lib/useComponentProps"
10
10
  import type { RadiusProp } from "../lib/useComponentProps"
11
11
  import { useOverlayBack } from "../lib/overlayBack"
12
+ import { optionsOf } from "../lib/select"
12
13
 
13
14
  interface Props {
14
15
  /** Valeur sélectionnée */
@@ -230,18 +231,23 @@ const getLabel = (opt: any): string => {
230
231
  return v === undefined || v === null ? "" : String(v)
231
232
  }
232
233
 
234
+ // `options` peut arriver `false` (v-if/&&, config non chargée) : sans cette coercition,
235
+ // `.find` / `.filter` / `.some` jetteraient et le popup cesserait de se rendre
236
+ // (même classe de bug que `q-select multiple` — cf. `.memory/knowledges.md`).
237
+ const optionList = computed<any[]>(() => optionsOf(props.options))
238
+
233
239
  const selectedLabel = computed(() => {
234
240
  if (props.modelValue === undefined || props.modelValue === null) return ""
235
- const opt = props.options.find((o) => getValue(o) === props.modelValue)
241
+ const opt = optionList.value.find((o) => getValue(o) === props.modelValue)
236
242
  return opt ? getLabel(opt) : String(props.modelValue)
237
243
  })
238
244
 
239
245
  // — Filtre client (sauté en mode serveur @filter) —
240
246
  const filtered = computed(() => {
241
- if (hasFilter.value) return props.options
247
+ if (hasFilter.value) return optionList.value
242
248
  const q = query.value.trim().toLowerCase()
243
- if (!q) return props.options
244
- return props.options.filter((o) => getLabel(o).toLowerCase().includes(q))
249
+ if (!q) return optionList.value
250
+ return optionList.value.filter((o) => getLabel(o).toLowerCase().includes(q))
245
251
  })
246
252
 
247
253
  // — Affichage du champ —
@@ -323,7 +329,7 @@ const commit = () => {
323
329
  const q = query.value.trim()
324
330
  if (!q) return
325
331
  const label = q.toLowerCase()
326
- const matches = props.options.some((o) => getLabel(o).toLowerCase() === label)
332
+ const matches = optionList.value.some((o) => getLabel(o).toLowerCase() === label)
327
333
  if (!matches) {
328
334
  query.value = ""
329
335
  emit("update:inputValue", "")
@@ -13,7 +13,7 @@ import QBottomSheetProvider from "./QBottomSheetProvider.vue"
13
13
  import QImagePreviewProvider from "./QImagePreviewProvider.vue"
14
14
  import { qConfigKey, qProvidersKey } from "../lib/config"
15
15
  import type { QAppLang, QConfigContext, QTheme, ThemeMode } from "../lib/config"
16
- import { isRadiusScale, RADIUS_VALUES } from "../lib/useComponentProps"
16
+ import { themeVars } from "../lib/themeVars"
17
17
 
18
18
  interface Props {
19
19
  /**
@@ -119,40 +119,51 @@ watch(
119
119
 
120
120
  provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark, lang })
121
121
 
122
+ // — Dev : `componentProps` n'est PAS un spread de props — une clé non lue est ignorée
123
+ // silencieusement (cf. `.memory/knowledges.md`). On le signale, une fois par clé.
124
+ /** Clés de `componentProps.<Nom>` réellement lues, en plus du `radius` générique. */
125
+ const EXTRA_COMPONENT_PROP_KEYS: Record<string, readonly string[]> = { QMap: ["apiKey"] }
126
+
127
+ const IS_DEV = !!((import.meta as any).env?.DEV ?? (import.meta as any).dev)
128
+ if (IS_DEV) {
129
+ const warned = new Set<string>()
130
+ watch(
131
+ mergedTheme,
132
+ (theme) => {
133
+ if (typeof document === "undefined") return // pas de bruit au SSR/build
134
+ for (const [name, entry] of Object.entries(theme.componentProps ?? {})) {
135
+ if (!entry || typeof entry !== "object") continue
136
+ const allowed = new Set<string>(["radius", ...(EXTRA_COMPONENT_PROP_KEYS[name] ?? [])])
137
+ const ignored = Object.keys(entry).filter((key) => !allowed.has(key))
138
+ if (!ignored.length) continue
139
+ const signature = `${name}:${ignored.join(",")}`
140
+ if (warned.has(signature)) continue
141
+ warned.add(signature)
142
+ console.warn(
143
+ `[dnax/ui] componentProps.${name} : clé(s) ignorée(s) → ${ignored.map((k) => `"${k}"`).join(", ")}.\n` +
144
+ ` Ces clés sont lues pour ${name} : ${[...allowed].map((k) => `"${k}"`).join(", ")}.\n` +
145
+ ` Pour une couleur/un fond, passez par theme.vars (ex. { "--q-field-bg": "#eef7ee" }) ou du CSS.`,
146
+ )
147
+ }
148
+ },
149
+ { immediate: true, deep: true },
150
+ )
151
+ }
152
+
122
153
  // Providers intégrés ($q.dialog + $q.notify) : rendus UNE fois par le
123
154
  // QConfigProvider le plus externe (les imbriqués ne re-rendent pas → pas de doublons)
124
155
  const hasProviders = inject(qProvidersKey, false)
125
156
  provide(qProvidersKey, true)
126
157
  const isProvidersRoot = computed(() => !hasProviders)
127
158
 
128
- /** Calcule un foreground lisible (blanc ou sombre) pour un fond donné. */
129
- const textColorFor = (bg: string): string => {
130
- let hex = bg.trim().replace(/^#/, "")
131
- if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("")
132
- if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff"
133
- const r = parseInt(hex.slice(0, 2), 16)
134
- const g = parseInt(hex.slice(2, 4), 16)
135
- const b = parseInt(hex.slice(4, 6), 16)
136
- const luminance = 0.299 * r + 0.587 * g + 0.114 * b
137
- return luminance > 150 ? "#1d1d1d" : "#ffffff"
138
- }
139
-
140
- const themeStyle = computed<Record<string, string>>(() => {
141
- const style: Record<string, string> = {
142
- "color-scheme": isDark.value ? "dark" : "light",
143
- }
144
- for (const [token, value] of Object.entries(mergedTheme.value.colors ?? {})) {
145
- if (!value) continue
146
- style[`--${token}`] = value
147
- style[`--${token}-foreground`] = textColorFor(value)
148
- }
149
- // Arrondi global : composantProps.default.radius → --q-radius hérité par TOUS
150
- // les composants dont le CSS utilise var(--q-radius). Une prop/override
151
- // spécifique (useRadius) pose son propre --q-radius inline et prime.
152
- const global = mergedTheme.value.componentProps?.default?.radius
153
- if (isRadiusScale(global)) style["--q-radius"] = RADIUS_VALUES[global]
154
- return style
155
- })
159
+ /**
160
+ * Variables CSS du thème : `color-scheme` + couleurs, `--q-radius` et `vars` libres
161
+ * (logique partagée avec les providers d'overlays téléportés, cf. `themeVars`).
162
+ */
163
+ const themeStyle = computed<Record<string, string>>(() => ({
164
+ "color-scheme": isDark.value ? "dark" : "light",
165
+ ...themeVars(mergedTheme.value),
166
+ }))
156
167
 
157
168
  // Thème GLOBAL (provider racine) : pose aussi les variables sur <html> — les
158
169
  // overlays téléportés au body (dialogs $q.dialog, …) perdent l'héritage du div
@@ -60,6 +60,7 @@ import { radiusStyle, radiusValue, useRadius } from "../lib/useComponentProps"
60
60
  import type { RadiusProp } from "../lib/useComponentProps"
61
61
  import { useOverlayBack } from "../lib/overlayBack"
62
62
  import { createSearcher } from "../lib/search"
63
+ import { optionsOf, selectionOf } from "../lib/select"
63
64
 
64
65
  interface Props {
65
66
  /** Valeur sélectionnée : option, valeur (emit-value), ou tableau (multiple) */
@@ -337,7 +338,9 @@ watch([popupPlacement, popupOffset], onPopupViewportChange)
337
338
  const isObjectOption = (o: any): boolean => o !== null && typeof o === "object"
338
339
 
339
340
  const normalizedOptions = computed<any[]>(() =>
340
- props.options.map((o) => {
341
+ // `options` peut arriver `false` (v-if/&&, config non chargée) : sans garde,
342
+ // `props.options.map` jetait et l'option par défaut ne s'affichait plus.
343
+ optionsOf(props.options).map((o) => {
341
344
  if (isObjectOption(o)) return o
342
345
  // Objet normalisé : garde la valeur d'origine (non-énumérable → invisible
343
346
  // pour l'affichage, la recherche fuse et les clés de rendu)
@@ -361,8 +364,9 @@ const getOptionLabel = (opt: any): string => {
361
364
 
362
365
  // — Sélection (dérivée du modèle) —
363
366
  const selectedOptions = computed<any[]>(() => {
364
- if (props.modelValue === undefined || props.modelValue === null) return []
365
- const list = props.multiple ? (props.modelValue as any[]) : [props.modelValue]
367
+ // En `multiple`, un modèle scalaire (ex. `false`) ne doit PAS faire jeter `list.map`
368
+ // (tout le popup cessait de se rendre) : `selectionOf` le traite comme vide.
369
+ const list = selectionOf(props.modelValue, props.multiple)
366
370
  return list
367
371
  .map((v) => {
368
372
  if (props.emitValue) return normalizedOptions.value.find((o) => getOptionValue(o) === v)
@@ -417,7 +421,9 @@ const clear = () => {
417
421
  }
418
422
 
419
423
  const hasValue = computed(() =>
420
- props.multiple ? (props.modelValue as any[])?.length > 0 : props.modelValue !== undefined && props.modelValue !== null,
424
+ props.multiple
425
+ ? Array.isArray(props.modelValue) && props.modelValue.length > 0
426
+ : props.modelValue !== undefined && props.modelValue !== null,
421
427
  )
422
428
 
423
429
  // — Recherche floue (fuse.js) —
@@ -8,6 +8,16 @@
8
8
  // Suivi des modifications : `v-model:dirty` (état) + `v-model:changes` (delta lignes / feuilles
9
9
  // ajoutées, modifiées, supprimées), calculé en comparant le document à une **référence**
10
10
  // (`lib/spreadsheetChanges.ts`).
11
+ import type {
12
+ QSpreadsheetValidation,
13
+ QSpreadsheetValidationRule,
14
+ } from "../lib/spreadsheetValidation"
15
+
16
+ export type {
17
+ QSpreadsheetValidation,
18
+ QSpreadsheetValidationRule,
19
+ } from "../lib/spreadsheetValidation"
20
+
11
21
  export type QSpreadsheetCellType =
12
22
  | "string"
13
23
  | "text"
@@ -52,8 +62,8 @@ export interface QSpreadsheetColumn {
52
62
  options?: QSpreadsheetCellOption[]
53
63
  /** Pour type "select" / "multiselect" : rend les options actives en badge coloré (sinon texte) */
54
64
  chip?: boolean
55
- /** Validation à la saisie : { min, max, integer?, pattern?, message? } */
56
- validation?: QSpreadsheetValidation
65
+ /** Validation à la saisie : objet `{ min, max, … }` ou expression ArkType (`"number < 4"`) */
66
+ validation?: QSpreadsheetValidationRule
57
67
  /** Formatteur de la valeur affichée */
58
68
  format?: (val: any, row: Record<string, any>) => any
59
69
  /** Classe CSS de la cellule (défaut : ) */
@@ -64,24 +74,9 @@ export interface QSpreadsheetColumn {
64
74
  headerStyle?: string
65
75
  }
66
76
 
67
- export interface QSpreadsheetValidation {
68
- /** Valeur minimale (types numériques / entiers) */
69
- min?: number
70
- /** Valeur maximale (types numériques / entiers) */
71
- max?: number
72
- /** Exige un entier (si présent, valide les entiers) */
73
- integer?: boolean
74
- /** Expression régulière validée contre le texte */
75
- pattern?: string
76
- /** Champ obligatoire (vide refusé) */
77
- required?: boolean
78
- /** Valeurs autorisées (liste) */
79
- list?: any[]
80
- /** Message d'erreur affiché (sinon message générique) */
81
- message?: string
82
- /** Schéma ArkType (string) — ex. "number.integer & >= 0 & <= 100" (optionnel) */
83
- schema?: string
84
- }
77
+ // `QSpreadsheetValidation` et `QSpreadsheetValidationRule` vivent dans
78
+ // `lib/spreadsheetValidation.ts` (source unique, testée hors navigateur) et sont
79
+ // ré-exportées en tête de ce bloc.
85
80
 
86
81
  /** Validation par plage de cellules (rows/cols 0-based) */
87
82
  export interface QSpreadsheetRangeValidator {
@@ -89,7 +84,7 @@ export interface QSpreadsheetRangeValidator {
89
84
  c0: number
90
85
  r1: number
91
86
  c1: number
92
- validation: QSpreadsheetValidation
87
+ validation: QSpreadsheetValidationRule
93
88
  }
94
89
 
95
90
  export interface QSpreadsheetSelection {
@@ -161,6 +156,7 @@ import {
161
156
  deriveCols,
162
157
  parseCsv,
163
158
  } from "../lib/spreadsheet"
159
+ import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
164
160
  import {
165
161
  DEFAULT_ROW_KEY,
166
162
  diffDocuments,
@@ -1676,7 +1672,7 @@ const toggleMultiOption = (opt: QSpreadsheetCellOption) => {
1676
1672
  const next = multiSelected(opt)
1677
1673
  ? current.filter((v) => v !== opt.value)
1678
1674
  : [...current, opt.value]
1679
- validateAndSet(row, column, old, next)
1675
+ void validateAndSet(row, column, old, next)
1680
1676
  }
1681
1677
 
1682
1678
  // ─── Barre de formule (fx) ───
@@ -3854,60 +3850,50 @@ const condStyleOf = (r: number, c: number, name: string) => {
3854
3850
  return Object.keys(st).length ? st : undefined
3855
3851
  }
3856
3852
 
3857
- // ════════ Validation de colonne (min / max / entier / pattern) ════════
3853
+ // ════════ Validation de colonne (min / max / entier / pattern / ArkType) ════════
3858
3854
  const valErrors = ref<Record<string, string>>({})
3859
3855
  const valKey = (r: number, name: string) => r + ":" + name
3856
+
3857
+ // ArkType : import **dynamique** (jamais dans le graphe du barrel) mémoïsé au niveau
3858
+ // module — une seule promesse pour toute la session, et non une par édition de cellule.
3859
+ let arkModule: Promise<any | null> | null = null
3860
+ const loadArk = (): Promise<any | null> => {
3861
+ arkModule ??= import("arktype")
3862
+ .then((m) => m as any)
3863
+ .catch(() => null)
3864
+ return arkModule
3865
+ }
3866
+
3867
+ /**
3868
+ * Vérifie une expression ArkType. Une expression **invalide** (faute de syntaxe) ne doit
3869
+ * JAMAIS jeter : l'exception remonterait en rejet non géré et la cellule ne serait ni
3870
+ * validée ni commitée, sans aucun message (cf. `.memory/knowledges.md`).
3871
+ */
3872
+ const arkCheck: ArkCheck = async (schema, value) => {
3873
+ const m = await loadArk()
3874
+ if (!m) return "ArkType unavailable"
3875
+ try {
3876
+ const problems = m.type(schema)(value)
3877
+ const p = Array.isArray(problems) ? problems[0] : undefined
3878
+ return p?.message ?? null
3879
+ } catch (e) {
3880
+ return `Invalid ArkType schema: ${(e as Error)?.message ?? String(e)}`
3881
+ }
3882
+ }
3883
+
3884
+ /** Validation d'une cellule : règles de la colonne, puis celles des plages qui la couvrent. */
3860
3885
  const guardValidation = async (
3861
3886
  row: number,
3862
3887
  col: QSpreadsheetColumn | undefined,
3863
3888
  value: any,
3864
3889
  ): Promise<string | null> => {
3865
- let ark: Promise<{ type: (s: string) => (v: unknown) => { message?: string }[] } | null> | null = null
3866
- const arkCheck = async (schema: string): Promise<string | null> => {
3867
- if (!ark)
3868
- ark = import("arktype")
3869
- .then((m) => m as any)
3870
- .catch(() => null)
3871
- const m = await ark
3872
- if (!m) return "ArkType unavailable"
3873
- const problems = m.type(schema)(value)
3874
- const p = Array.isArray(problems) ? problems[0] : undefined
3875
- return p?.message ?? null
3876
- }
3877
- const check = async (v: QSpreadsheetValidation | undefined): Promise<string | null> => {
3878
- if (!v) return null
3879
- const multi = Array.isArray(value)
3880
- const blank =
3881
- value === null || value === undefined || value === "" || (multi && value.length === 0)
3882
- if (v.required && blank) return v.message ?? "Required"
3883
- if (blank) return null // champ vide autorisé (sauf required)
3884
- if (v.list) {
3885
- // Choix multiples : chacune des valeurs doit être dans la liste
3886
- const values = multi ? value : [value]
3887
- if (values.some((x) => !v.list!.some((y) => String(y) === String(x))))
3888
- return v.message ?? "Not in the allowed list"
3889
- }
3890
- if (v.integer && typeof value === "number" && !Number.isInteger(value))
3891
- return v.message ?? "Integer required"
3892
- if (typeof value === "number") {
3893
- if (v.min !== undefined && value < v.min) return v.message ?? "Value below minimum (" + v.min + ")"
3894
- if (v.max !== undefined && value > v.max) return v.message ?? "Value above maximum (" + v.max + ")"
3895
- }
3896
- if (v.pattern && typeof value === "string" && !new RegExp(v.pattern).test(value))
3897
- return v.message ?? "Value does not match the required format"
3898
- if (v.schema) {
3899
- const e = await arkCheck(v.schema)
3900
- if (e) return v.message ?? e
3901
- }
3902
- return null
3903
- }
3904
- const colErr = await check(col?.validation)
3890
+ const colErr = await checkValidation(col?.validation, value, arkCheck)
3905
3891
  if (colErr) return colErr
3906
3892
  if (props.validators?.length && col) {
3907
3893
  const ci = colIndex(col.name)
3908
3894
  for (const rv of props.validators) {
3909
3895
  if (row >= rv.r0 && row <= rv.r1 && ci >= rv.c0 && ci <= rv.c1) {
3910
- const e = await check(rv.validation)
3896
+ const e = await checkValidation(rv.validation, value, arkCheck)
3911
3897
  if (e) return e
3912
3898
  }
3913
3899
  }
package/lib/config.ts CHANGED
@@ -24,6 +24,14 @@ export interface QTheme {
24
24
  lang?: QAppLang
25
25
  /** Props par défaut par composant (clé = nom du composant, ex. "QBtn") */
26
26
  componentProps?: Record<string, Record<string, unknown>>
27
+ /**
28
+ * Variables CSS libres posées avec le thème, puis héritées par tout le sous-arbre
29
+ * (overlays téléportés compris : dialogs, bottom sheets). Clé avec ou sans `--`
30
+ * (ex. `{ "q-field-bg": "#eef7ee" }`). Appliquées **en dernier**, elles peuvent donc
31
+ * surcharger les tokens de `colors`. Utile pour ce qu'aucun token ne couvre
32
+ * (ex. le fond des champs, cf. `--q-field-bg`).
33
+ */
34
+ vars?: Record<string, string>
27
35
  }
28
36
 
29
37
  export interface QConfigContext {
package/lib/qrcode.ts CHANGED
@@ -1,11 +1,13 @@
1
- // qrcode — encodage QR et tracé SVG. Le moteur d'encodage est la dépendance `qrcode`
2
- // (déjà présente, sans types — cf. `shims.d.ts`) : elle fournit la matrice de modules, ce
3
- // module la traduit en **chemin SVG** (une suite de rectangles horizontaux, bien plus
4
- // compact qu'un carré par module) — d'où un rendu net à toutes les tailles, thémable et
5
- // rendable en SSR.
1
+ // qrcode — encodage QR et tracé SVG. Le moteur d'encodage est `uqr` (ESM pur, sans
2
+ // dépendance) : il fournit la matrice de modules, ce module la traduit en **chemin SVG**
3
+ // (une suite de rectangles horizontaux, bien plus compact qu'un carré par module) — d'où
4
+ // un rendu net à toutes les tailles, thémable et rendable en SSR.
5
+ //
6
+ // `uqr` est **ESM** (contrairement à l'ancien `qrcode`, CJS sans `default`, qui cassait le
7
+ // bundle client — cf. `.memory/warnings.md`) : aucun interop esbuild, aucun `optimizeDeps`.
6
8
  //
7
9
  // Pur (aucun DOM) → testable hors navigateur, comme `lib/pagePadding.ts`.
8
- import QRCode from "qrcode"
10
+ import { encode } from "uqr"
9
11
 
10
12
  /** Niveau de correction d'erreur : L (~7 %) → H (~30 %) */
11
13
  export type QrEcc = "L" | "M" | "Q" | "H"
@@ -39,10 +41,25 @@ export function encodeQr(value: string, options: QrEncodeOptions = {}): QrMatrix
39
41
  if (typeof value !== "string" || value === "") return undefined
40
42
 
41
43
  try {
42
- const settings: Record<string, unknown> = { errorCorrectionLevel: options.ecc ?? "M" }
43
- if (typeof options.version === "number") settings.version = options.version
44
+ const { size, data } = encode(value, {
45
+ ecc: options.ecc ?? "M",
46
+ // La zone de silence est gérée par dnax.ui (`margin`) : on demande la matrice nue.
47
+ border: 0,
48
+ // `uqr` raisonne en bornes de version, pas en version forcée.
49
+ ...(typeof options.version === "number"
50
+ ? { minVersion: options.version, maxVersion: options.version }
51
+ : {}),
52
+ })
53
+
54
+ // `uqr` renvoie un `boolean[][]` ; on l'aplatit au contrat `data[y * size + x]`
55
+ // (1 = module sombre) attendu par `isDark` et le tracé.
56
+ const flat = new Uint8Array(size * size)
57
+ let i = 0
58
+ for (const row of data) {
59
+ for (let x = 0; x < size; x++) flat[i++] = row[x] ? 1 : 0
60
+ }
44
61
 
45
- return QRCode.create(value, settings).modules
62
+ return { size, data: flat }
46
63
  } catch {
47
64
  return undefined
48
65
  }
@@ -0,0 +1,42 @@
1
+ // Tests unitaires des coercitions défensives de QSelect (`optionsOf` / `selectionOf`).
2
+ // Régression : `multiple` + modèle scalaire (`false`) faisait jeter `list.map` et
3
+ // empêchait le popup entier de se rendre.
4
+ import { describe, expect, it } from "bun:test"
5
+ import { optionsOf, selectionOf } from "./select"
6
+
7
+ describe("optionsOf", () => {
8
+ it("laisse passer un tableau (même vide)", () => {
9
+ expect(optionsOf(["a", "b"])).toEqual(["a", "b"])
10
+ expect(optionsOf([])).toEqual([])
11
+ })
12
+
13
+ it("traite toute valeur non-tableau comme vide (au lieu de planter)", () => {
14
+ expect(optionsOf(false)).toEqual([])
15
+ expect(optionsOf(undefined)).toEqual([])
16
+ expect(optionsOf(null)).toEqual([])
17
+ expect(optionsOf("oops")).toEqual([])
18
+ expect(optionsOf({ value: 1 })).toEqual([])
19
+ })
20
+ })
21
+
22
+ describe("selectionOf", () => {
23
+ it("multiple : accepte un tableau", () => {
24
+ expect(selectionOf(["a", "b"], true)).toEqual(["a", "b"])
25
+ expect(selectionOf([], true)).toEqual([])
26
+ })
27
+
28
+ it("multiple + scalaire → rien de sélectionné (le bug d'origine : `false`)", () => {
29
+ expect(selectionOf(false, true)).toEqual([])
30
+ expect(selectionOf(true, true)).toEqual([])
31
+ expect(selectionOf("a", true)).toEqual([])
32
+ expect(selectionOf(0, true)).toEqual([])
33
+ })
34
+
35
+ it("simple : emballe la valeur, sauf null/undefined", () => {
36
+ expect(selectionOf("a", false)).toEqual(["a"])
37
+ expect(selectionOf(0, false)).toEqual([0])
38
+ expect(selectionOf(false, false)).toEqual([false])
39
+ expect(selectionOf(null, false)).toEqual([])
40
+ expect(selectionOf(undefined, false)).toEqual([])
41
+ })
42
+ })
package/lib/select.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Coercitions défensives des composants de sélection (QSelect, QAutocomplete) (`lib/select.ts`).
2
+ //
3
+ // Un `v-model` ou des `options` mal formés ne doivent **jamais** casser le rendu : un
4
+ // `list.map is not a function` dans un `computed` du template tue tout le popup, sans
5
+ // message utile. Cas réel : `<q-select multiple v-model="x">` avec `x` initialisé à
6
+ // `false` (défaut d'un `ref` booléen, retour d'un `&&`, config non chargée) — le garde
7
+ // `=== null`/`=== undefined` ne l'attrape pas, puis `false.map(...)` jette. Idem
8
+ // `:options` arrivant `false` (`props.options.map` / `.filter` / `.find`).
9
+ //
10
+ // Pur (aucun DOM) → testable hors navigateur, comme `lib/pagePadding.ts`.
11
+
12
+ /**
13
+ * Tableau des options. `options` doit être un tableau : toute autre valeur (ex. `false`,
14
+ * `0`, un objet isolé) est traitée comme **vide** plutôt que de faire planter
15
+ * `normalizedOptions`.
16
+ */
17
+ export const optionsOf = (value: unknown): any[] => (Array.isArray(value) ? value : [])
18
+
19
+ /**
20
+ * Tableau de sélection dérivé du `v-model` :
21
+ * - `null` / `undefined` → rien ;
22
+ * - `multiple` → le modèle **doit** être un tableau ; une valeur scalaire (ex. `false`)
23
+ * vaut « rien de sélectionné » (et non une sélection de cette valeur) ;
24
+ * - mode simple → on emballe la valeur.
25
+ */
26
+ export const selectionOf = (value: unknown, multiple: boolean): any[] => {
27
+ if (value === undefined || value === null) return []
28
+ if (multiple) return Array.isArray(value) ? value : []
29
+ return [value]
30
+ }
@@ -0,0 +1,106 @@
1
+ // Tests unitaires de la validation de colonne de QSpreadsheet (`lib/spreadsheetValidation.ts`).
2
+ // L'accès à ArkType est **injecté** : on teste l'ordonnancement des règles sans bundler
3
+ // ni navigateur, et on vérifie qu'une expression est bien transmise telle quelle.
4
+ import { describe, expect, it } from "bun:test"
5
+ import { checkValidation, normalizeValidation, type ArkCheck } from "./spreadsheetValidation"
6
+
7
+ /** Faux ArkType : échoue pour toute valeur >= 4 (comme une règle `"number < 4"`). */
8
+ const fakeArk: ArkCheck = async (schema, value) =>
9
+ schema === "number < 4" && typeof value === "number" && value >= 4 ? "must be < 4" : null
10
+
11
+ const noArk: ArkCheck = async () => {
12
+ throw new Error("arkCheck ne devait pas être appelé")
13
+ }
14
+
15
+ describe("normalizeValidation", () => {
16
+ it("une chaîne devient un schéma ArkType", () => {
17
+ expect(normalizeValidation("number < 4")).toEqual({ schema: "number < 4" })
18
+ })
19
+
20
+ it("un objet est laissé tel quel", () => {
21
+ const rule = { min: 0, max: 10 }
22
+ expect(normalizeValidation(rule)).toBe(rule)
23
+ })
24
+
25
+ it("undefined → undefined", () => {
26
+ expect(normalizeValidation(undefined)).toBeUndefined()
27
+ })
28
+ })
29
+
30
+ describe("checkValidation — expression directe (« number < 4 »)", () => {
31
+ it("transmet l'expression à ArkType et remonte son message", async () => {
32
+ expect(await checkValidation("number < 4", 3, fakeArk)).toBeNull()
33
+ expect(await checkValidation("number < 4", 4, fakeArk)).toBe("must be < 4")
34
+ })
35
+
36
+ it("`message` surcharge le message d'ArkType", async () => {
37
+ const rule = { schema: "number < 4", message: "Trop grand" }
38
+ expect(await checkValidation(rule, 9, fakeArk)).toBe("Trop grand")
39
+ })
40
+
41
+ it("une valeur vide passe (comme la forme objet)", async () => {
42
+ expect(await checkValidation("number < 4", null, noArk)).toBeNull()
43
+ expect(await checkValidation("number < 4", "", noArk)).toBeNull()
44
+ })
45
+
46
+ it("`required` refuse le vide **avant** d'appeler ArkType", async () => {
47
+ expect(await checkValidation({ required: true, schema: "number < 4" }, null, noArk)).toBe(
48
+ "Required",
49
+ )
50
+ })
51
+ })
52
+
53
+ describe("checkValidation — règle objet", () => {
54
+ it("sans règle → null", async () => {
55
+ expect(await checkValidation(undefined, 42, noArk)).toBeNull()
56
+ })
57
+
58
+ it("required + message", async () => {
59
+ expect(await checkValidation({ required: true }, "", noArk)).toBe("Required")
60
+ expect(await checkValidation({ required: true, message: "Obligatoire" }, [], noArk)).toBe(
61
+ "Obligatoire",
62
+ )
63
+ })
64
+
65
+ it("list : une valeur (mode simple)", async () => {
66
+ const rule = { list: ["a", "b"] }
67
+ expect(await checkValidation(rule, "a", noArk)).toBeNull()
68
+ expect(await checkValidation(rule, "z", noArk)).toBe("Not in the allowed list")
69
+ })
70
+
71
+ it("list : chaque valeur (multiselect)", async () => {
72
+ const rule = { list: [1, 2, 3] }
73
+ expect(await checkValidation(rule, [1, 3], noArk)).toBeNull()
74
+ expect(await checkValidation(rule, [1, 9], noArk)).toBe("Not in the allowed list")
75
+ })
76
+
77
+ it("list : compare en texte (1 ≡ « 1 »)", async () => {
78
+ expect(await checkValidation({ list: ["1", "2"] }, 1, noArk)).toBeNull()
79
+ })
80
+
81
+ it("integer / min / max", async () => {
82
+ expect(await checkValidation({ integer: true }, 2.5, noArk)).toBe("Integer required")
83
+ expect(await checkValidation({ min: 0 }, -1, noArk)).toBe("Value below minimum (0)")
84
+ expect(await checkValidation({ max: 100 }, 150, noArk)).toBe("Value above maximum (100)")
85
+ expect(await checkValidation({ min: 0, max: 100 }, 42, noArk)).toBeNull()
86
+ })
87
+
88
+ it("min/max ne s'appliquent qu'aux nombres", async () => {
89
+ expect(await checkValidation({ min: 0 }, "abc", noArk)).toBeNull()
90
+ })
91
+
92
+ it("pattern (texte seulement)", async () => {
93
+ const rule = { pattern: "^\\d{4}$" }
94
+ expect(await checkValidation(rule, "2026", noArk)).toBeNull()
95
+ expect(await checkValidation(rule, "26", noArk)).toBe(
96
+ "Value does not match the required format",
97
+ )
98
+ expect(await checkValidation(rule, 2026 as any, noArk)).toBeNull() // non-string : ignoré
99
+ })
100
+
101
+ it("le schéma est évalué en dernier (min échoue avant)", async () => {
102
+ const rule = { min: 0, schema: "number < 4" }
103
+ expect(await checkValidation(rule, -1, fakeArk)).toBe("Value below minimum (0)")
104
+ expect(await checkValidation(rule, 9, fakeArk)).toBe("must be < 4")
105
+ })
106
+ })
@@ -0,0 +1,102 @@
1
+ // Validation de colonne de QSpreadsheet (`<q-spreadsheet>`).
2
+ //
3
+ // Vit dans `lib/` — et non dans le SFC — pour être **testable sans navigateur** : c'est
4
+ // exactement le code du composant, seul l'accès à ArkType est **injecté** (`ArkCheck`).
5
+ //
6
+ // Deux écritures possibles pour une colonne (ou une plage) :
7
+ //
8
+ // { name: "score", validation: { min: 0, max: 100, integer: true } } // objet
9
+ // { name: "priority", validation: "number < 4" } // expression ArkType
10
+ //
11
+ // Une **chaîne** est une expression ArkType : c'est le raccourci de `{ schema: "…" }`.
12
+ // Les deux formes se combinent (`{ required: true, schema: "number < 4" }`) et `message`
13
+ // surcharge le message de la règle qui échoue.
14
+
15
+ /** Règles de validation d'une colonne (ou d'une plage de cellules). */
16
+ export interface QSpreadsheetValidation {
17
+ /** Valeur minimale (types numériques / entiers) */
18
+ min?: number
19
+ /** Valeur maximale (types numériques / entiers) */
20
+ max?: number
21
+ /** Exige un entier (si présent, valide les entiers) */
22
+ integer?: boolean
23
+ /** Expression régulière validée contre le texte */
24
+ pattern?: string
25
+ /** Champ obligatoire (vide refusé) */
26
+ required?: boolean
27
+ /** Valeurs autorisées (liste) */
28
+ list?: any[]
29
+ /** Message d'erreur affiché (sinon message générique) */
30
+ message?: string
31
+ /**
32
+ * Expression **ArkType** — ex. `"number < 4"`,
33
+ * `"number.integer & number >= 0 & number <= 100"`, `"string.email"`.
34
+ * Équivalent d'une règle écrite directement en chaîne.
35
+ */
36
+ schema?: string
37
+ }
38
+
39
+ /**
40
+ * Règle de validation : objet `{ min, max, … }` **ou** expression ArkType écrite
41
+ * directement (`validation: "number < 4"`).
42
+ */
43
+ export type QSpreadsheetValidationRule = QSpreadsheetValidation | string
44
+
45
+ /**
46
+ * Vérification d'un schéma ArkType, **injectée** par le composant (import dynamique +
47
+ * mémoïsation + `try/catch`) : renvoie un message d'erreur, ou `null` si la valeur passe.
48
+ */
49
+ export type ArkCheck = (schema: string, value: unknown) => Promise<string | null>
50
+
51
+ /** Normalise une règle : une chaîne devient un schéma ArkType (`{ schema }`). */
52
+ export const normalizeValidation = (
53
+ rule: QSpreadsheetValidationRule | undefined,
54
+ ): QSpreadsheetValidation | undefined => (typeof rule === "string" ? { schema: rule } : rule)
55
+
56
+ /**
57
+ * Valide une valeur contre une règle. Renvoie le message d'erreur, ou `null`.
58
+ *
59
+ * Ordre : `required` → vide toléré → `list` → `integer` → `min` / `max` → `pattern` →
60
+ * `schema` (ArkType). Une valeur **vide** passe toutes les règles sauf `required`.
61
+ * En `multiselect` (valeur = tableau), `list` s'applique **à chaque valeur** et un tableau
62
+ * vide compte comme vide, donc `required` le refuse.
63
+ */
64
+ export async function checkValidation(
65
+ rule: QSpreadsheetValidationRule | undefined,
66
+ value: any,
67
+ arkCheck: ArkCheck,
68
+ ): Promise<string | null> {
69
+ const v = normalizeValidation(rule)
70
+ if (!v) return null
71
+
72
+ const multi = Array.isArray(value)
73
+ const blank =
74
+ value === null || value === undefined || value === "" || (multi && value.length === 0)
75
+ if (v.required && blank) return v.message ?? "Required"
76
+ if (blank) return null // champ vide autorisé (sauf required)
77
+
78
+ if (v.list) {
79
+ // Choix multiples : chacune des valeurs doit être dans la liste
80
+ const values = multi ? value : [value]
81
+ if (values.some((x) => !v.list!.some((y) => String(y) === String(x))))
82
+ return v.message ?? "Not in the allowed list"
83
+ }
84
+
85
+ if (v.integer && typeof value === "number" && !Number.isInteger(value))
86
+ return v.message ?? "Integer required"
87
+
88
+ if (typeof value === "number") {
89
+ if (v.min !== undefined && value < v.min) return v.message ?? `Value below minimum (${v.min})`
90
+ if (v.max !== undefined && value > v.max) return v.message ?? `Value above maximum (${v.max})`
91
+ }
92
+
93
+ if (v.pattern && typeof value === "string" && !new RegExp(v.pattern).test(value))
94
+ return v.message ?? "Value does not match the required format"
95
+
96
+ if (v.schema) {
97
+ const e = await arkCheck(v.schema, value)
98
+ if (e) return v.message ?? e
99
+ }
100
+
101
+ return null
102
+ }
@@ -0,0 +1,42 @@
1
+ // Tests unitaires des variables CSS du thème (`themeVars`) — partagées par
2
+ // QConfigProvider et les providers d'overlays téléportés.
3
+ import { describe, expect, it } from "bun:test"
4
+ import { themeVars } from "./themeVars"
5
+
6
+ describe("themeVars", () => {
7
+ it("expose les couleurs en `--token` + `--token-foreground`", () => {
8
+ const style = themeVars({ colors: { primary: "#1976d2" } })
9
+ expect(style["--primary"]).toBe("#1976d2")
10
+ // foreground lisible calculé (clair sur fond sombre)
11
+ expect(style["--primary-foreground"]).toBe("#ffffff")
12
+ })
13
+
14
+ it("traduit `componentProps.default.radius` en `--q-radius`", () => {
15
+ expect(themeVars({ componentProps: { default: { radius: "md" } } })["--q-radius"]).toBe("8px")
16
+ })
17
+
18
+ it("ignore un `default.radius` non-échelle (`true` = forme native)", () => {
19
+ expect(themeVars({ componentProps: { default: { radius: true } } })["--q-radius"]).toBeUndefined()
20
+ })
21
+
22
+ it("pose les `vars` libres, avec ou sans préfixe `--`", () => {
23
+ const style = themeVars({ vars: { "--q-field-bg": "#eef7ee", "q-field-bg-filled": "red" } })
24
+ expect(style["--q-field-bg"]).toBe("#eef7ee")
25
+ expect(style["--q-field-bg-filled"]).toBe("red")
26
+ })
27
+
28
+ it("donne le dernier mot aux `vars` sur les tokens de `colors`", () => {
29
+ const style = themeVars({ colors: { primary: "#111" }, vars: { "--primary": "#222" } })
30
+ expect(style["--primary"]).toBe("#222")
31
+ })
32
+
33
+ it("ignore les valeurs vides", () => {
34
+ const style = themeVars({ colors: { primary: "" }, vars: { "--x": "" } })
35
+ expect(style["--primary"]).toBeUndefined()
36
+ expect(style["--x"]).toBeUndefined()
37
+ })
38
+
39
+ it("renvoie un objet vide pour un thème vide", () => {
40
+ expect(themeVars({})).toEqual({})
41
+ })
42
+ })
package/lib/themeVars.ts CHANGED
@@ -1,22 +1,40 @@
1
- // themeVarsStyle — variables CSS du thème (couleurs + radius) pour les contenus
2
- // téléportés (dialogs, bottom sheets) qui sortent du div .q-config-provider.
1
+ // themeVars — variables CSS du thème (couleurs, radius global, `vars` libres) pour les
2
+ // contenus téléportés (dialogs, bottom sheets) qui sortent du div .q-config-provider.
3
3
  import { computed } from "vue"
4
4
  import type { ComputedRef } from "vue"
5
5
  import { contrastText } from "./colors"
6
6
  import { isRadiusScale, RADIUS_VALUES } from "./useComponentProps"
7
7
  import type { QTheme } from "./config"
8
8
 
9
- /** Computed des variables --token / --token-foreground / --q-radius du thème. */
9
+ /**
10
+ * Variables CSS d'un thème : `--<token>` (+ `--<token>-foreground` calculé) pour
11
+ * `colors`, `--q-radius` pour `componentProps.default.radius`, puis les `vars` libres
12
+ * **en dernier** (elles ont le dernier mot). Pur → testable hors navigateur.
13
+ */
14
+ export function themeVars(theme: QTheme): Record<string, string> {
15
+ const style: Record<string, string> = {}
16
+
17
+ for (const [token, value] of Object.entries(theme.colors ?? {})) {
18
+ if (!value) continue
19
+ style[`--${token}`] = value
20
+ style[`--${token}-foreground`] = contrastText(value)
21
+ }
22
+
23
+ // Arrondi global : hérité par TOUS les composants dont le CSS lit var(--q-radius).
24
+ // Une prop/override spécifique (`useRadius`) pose son propre --q-radius inline et prime.
25
+ const global = theme.componentProps?.default?.radius
26
+ if (isRadiusScale(global)) style["--q-radius"] = RADIUS_VALUES[global]
27
+
28
+ // Échappatoire : toute variable CSS, avec ou sans préfixe `--`.
29
+ for (const [name, value] of Object.entries(theme.vars ?? {})) {
30
+ if (!value) continue
31
+ style[name.startsWith("--") ? name : `--${name}`] = value
32
+ }
33
+
34
+ return style
35
+ }
36
+
37
+ /** Computed des variables du thème (cf. `themeVars`). */
10
38
  export function themeVarsStyle(theme: ComputedRef<QTheme>): ComputedRef<Record<string, string>> {
11
- return computed(() => {
12
- const style: Record<string, string> = {}
13
- for (const [token, value] of Object.entries(theme.value.colors ?? {})) {
14
- if (!value) continue
15
- style[`--${token}`] = value
16
- style[`--${token}-foreground`] = contrastText(value)
17
- }
18
- const global = theme.value.componentProps?.default?.radius
19
- if (isRadiusScale(global)) style["--q-radius"] = RADIUS_VALUES[global]
20
- return style
21
- })
39
+ return computed(() => themeVars(theme.value))
22
40
  }
package/module.ts CHANGED
@@ -50,6 +50,17 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
50
50
  })
51
51
  }
52
52
 
53
+ // Dépendances CJS sans export ESM `default` explicite : esbuild doit les pré-bundler
54
+ // (interop) sinon, servies brutes, le navigateur échoue sur « does not provide an
55
+ // export named 'default' ». `leaflet` (lib/mapLeaflet.ts) est dans ce cas ; le code
56
+ // tolère déjà les deux formes (`default ?? module`), ceci est une ceinture-bretelles
57
+ // côté hôte. Ne pas pointer une sous-entrée sur `index.ts` (cf. warnings en mémoire).
58
+ nuxt.hook("vite:extendConfig", (config) => {
59
+ const optimizeDeps = ((config as any).optimizeDeps ||= {})
60
+ const include: string[] = (optimizeDeps.include ||= [])
61
+ if (!include.includes("leaflet")) include.push("leaflet")
62
+ })
63
+
53
64
  // « Retour » navigateur → ferme l'overlay ouvert (dialog, sheet, sidebar…)
54
65
  // addPluginTemplate : le template est compilé dans l'app (où #imports existe),
55
66
  // évitant une dépendance `nuxt` dans ce package pour les types du plugin.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./index.ts",
9
- "./runtime": "./index.ts",
9
+ "./registry": "./index.ts",
10
+ "./runtime": "./runtime.ts",
10
11
  "./module": "./module.ts",
11
12
  "./styles/main.css": "./styles/main.css"
12
13
  },
@@ -36,11 +37,11 @@
36
37
  "leaflet": "^1.9.4",
37
38
  "maplibre-gl": "^5.24.0",
38
39
  "moment": "^2.30.1",
39
- "qrcode": "^1.5.4",
40
40
  "shiki": "^3.0.0",
41
41
  "swiper": "^14.2.0",
42
42
  "tailwind-merge": "^3.6.0",
43
43
  "tw-animate-css": "^1.4.0",
44
+ "uqr": "^0.1.3",
44
45
  "uuid": "^14.0.2",
45
46
  "vue-router": "^5.3.0",
46
47
  "vue-sonner": "^2.0.9"
package/runtime.ts ADDED
@@ -0,0 +1,48 @@
1
+ // runtime.ts — **surface runtime** consommée par les plugins (module Nuxt + plugins de
2
+ // l'app hôte). Elle contient, et rien de plus :
3
+ // - l'API directive / overlay (`installOverlayBackHandler`, `v-close`, `v-ripple`,
4
+ // `v-touch-*`, `v-intersection`) ;
5
+ // - l'API impérative `$q` (composables `usePlugin` / `useDialogPluginComponent`…) ;
6
+ // - les **providers** de `$q` (dialog, bottom sheet, notify, loading, image preview),
7
+ // montés au boot par l'app hôte.
8
+ //
9
+ // ⚠️ Ne JAMAIS réexporter `index.ts` ici. Le barrel importe **tous** les composants et,
10
+ // avec eux, leurs dépendances lourdes (echarts, @maptiler/sdk, swiper, shiki, uqr…) : une
11
+ // sous-entrée qui pointe sur `index.ts` les évalue au boot, même si l'app n'utilise aucun
12
+ // de ces composants. C'est tout l'intérêt de ce fichier — voir `.memory/warnings.md`.
13
+ //
14
+ // Pour *résoudre un composant par son nom* (tableaux d'API, docs), importer le barrel
15
+ // `@dnax/ui` — pas cette entrée.
16
+ export {
17
+ installOverlayBackHandler,
18
+ registerOverlay,
19
+ unregisterOverlay,
20
+ hasOpenOverlays,
21
+ closeTopmostOverlay,
22
+ useOverlayBack,
23
+ } from "./lib/overlayBack"
24
+ export type { OverlayHandle } from "./lib/overlayBack"
25
+ export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
26
+ export { vRipple } from "./lib/ripple"
27
+ export { vTouchPan } from "./lib/touchPan"
28
+ export { vTouchHold } from "./lib/touchHold"
29
+ export { vTouchSwipe } from "./lib/touchSwipe"
30
+ export { vTouchRepeat } from "./lib/touchRepeat"
31
+ export { vIntersection } from "./lib/intersection"
32
+
33
+ // — API impérative `$q` (composables) —
34
+ export {
35
+ $q,
36
+ usePlugin,
37
+ useQ,
38
+ QPlugin,
39
+ useDialogPluginComponent,
40
+ useBottomSheetPluginComponent,
41
+ } from "./lib/q"
42
+
43
+ // — Providers de `$q` (montés au boot par l'app hôte) —
44
+ export { default as QDialogProvider } from "./components/QDialogProvider.vue"
45
+ export { default as QBottomSheetProvider } from "./components/QBottomSheetProvider.vue"
46
+ export { default as QNotifyProvider } from "./components/QNotifyProvider.vue"
47
+ export { default as QLoadingProvider } from "./components/QLoadingProvider.vue"
48
+ export { default as QImagePreviewProvider } from "./components/QImagePreviewProvider.vue"
package/shims.d.ts CHANGED
@@ -7,19 +7,13 @@ declare module "*.css?raw" {
7
7
 
8
8
  // Leaflet ne publie pas de types (ils vivent dans `@types/leaflet`, un paquet séparé) :
9
9
  // le moteur `lib/mapLeaflet.ts` le manipule donc en `any`, comme le SDK MapTiler.
10
+ //
11
+ // ⚠️ Ce `default` est une commodité de **typage** : au runtime `leaflet` est un UMD, sans
12
+ // export ESM `default` garanti (selon la pré-bundlisation Vite). C'est pourquoi
13
+ // `lib/mapLeaflet.ts` importe en **dynamique** puis prend `leaflet.default ?? leaflet` —
14
+ // jamais un `import L from "leaflet"` statique (même piège que l'ancien `qrcode`, cf.
15
+ // `.memory/warnings.md`).
10
16
  declare module "leaflet" {
11
17
  const leaflet: any
12
18
  export default leaflet
13
19
  }
14
-
15
- // Même situation pour `qrcode` (=`@types/qrcode` séparé) : seul `create()` est utilisé
16
- // (matrice de modules) — le rendu est fait par dnax.ui (`lib/qrcode.ts`).
17
- declare module "qrcode" {
18
- const qrcode: {
19
- create: (
20
- text: string,
21
- options?: { errorCorrectionLevel?: "L" | "M" | "Q" | "H"; version?: number },
22
- ) => { modules: { size: number; data: ArrayLike<number> } }
23
- }
24
- export default qrcode
25
- }
package/styles/main.css CHANGED
@@ -400,7 +400,7 @@ html.dark body {
400
400
  }
401
401
 
402
402
  .dark .q-field__control {
403
- background-color: var(--muted);
403
+ background-color: var(--q-field-bg, var(--muted));
404
404
  border-color: var(--border);
405
405
  color: var(--foreground);
406
406
  }
@@ -2021,7 +2021,11 @@ html.dark body {
2021
2021
  gap: 6px;
2022
2022
  min-height: 40px;
2023
2023
  padding: 0 12px;
2024
- background-color: #fff;
2024
+ /* Fond du champ, partagé par TOUS les champs (QInput, QSelect, QAutocomplete,
2025
+ QDatePicker, QCountryPicker, QInputTag, QInputOtp…). Surchargeable globalement via
2026
+ `--q-field-bg` (héritée par le sous-arbre) — `theme.vars: { "--q-field-bg": "…" }` —
2027
+ ou localement par CSS. La variante `filled` lit `--q-field-bg-filled`. */
2028
+ background-color: var(--q-field-bg, #fff);
2025
2029
  border: 1px solid rgb(0 0 0 / 0.22);
2026
2030
  border-radius: var(--q-radius);
2027
2031
  transition:
@@ -2031,18 +2035,18 @@ html.dark body {
2031
2035
  }
2032
2036
 
2033
2037
  .q-field--outlined .q-field__control {
2034
- background-color: transparent;
2038
+ background-color: var(--q-field-bg, transparent);
2035
2039
  }
2036
2040
 
2037
2041
  .q-field--filled .q-field__control {
2038
- background-color: rgb(0 0 0 / 0.05);
2042
+ background-color: var(--q-field-bg-filled, rgb(0 0 0 / 0.05));
2039
2043
  border-color: transparent;
2040
2044
  border-bottom: 1px solid rgb(0 0 0 / 0.3);
2041
2045
  border-radius: var(--q-radius) var(--q-radius) 0 0;
2042
2046
  }
2043
2047
 
2044
2048
  .q-field--borderless .q-field__control {
2045
- background-color: transparent;
2049
+ background-color: var(--q-field-bg, transparent);
2046
2050
  border-color: transparent;
2047
2051
  }
2048
2052