@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.
- package/components/QAutocomplete.vue +11 -5
- package/components/QConfigProvider.vue +40 -29
- package/components/QSelect.vue +10 -4
- package/components/QSpreadsheet.vue +50 -64
- package/lib/config.ts +8 -0
- package/lib/qrcode.ts +26 -9
- package/lib/select.test.ts +42 -0
- package/lib/select.ts +30 -0
- package/lib/spreadsheetValidation.test.ts +106 -0
- package/lib/spreadsheetValidation.ts +102 -0
- package/lib/themeVars.test.ts +42 -0
- package/lib/themeVars.ts +32 -14
- package/module.ts +11 -0
- package/package.json +4 -3
- package/runtime.ts +48 -0
- package/shims.d.ts +6 -12
- package/styles/main.css +9 -5
|
@@ -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 =
|
|
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
|
|
247
|
+
if (hasFilter.value) return optionList.value
|
|
242
248
|
const q = query.value.trim().toLowerCase()
|
|
243
|
-
if (!q) return
|
|
244
|
-
return
|
|
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 =
|
|
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 {
|
|
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
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
package/components/QSelect.vue
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
|
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,
|
|
56
|
-
validation?:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
|
43
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
"./
|
|
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
|
-
|
|
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
|
|