@dnax/ui 0.0.48 → 0.0.50

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.
Files changed (48) hide show
  1. package/components/QBottomSheet.vue +172 -6
  2. package/components/QChart.vue +1 -0
  3. package/components/QDatePicker.vue +1 -1
  4. package/components/QImagePicker.vue +28 -1
  5. package/components/QInputChat.vue +859 -0
  6. package/components/QInputCurrency.vue +319 -0
  7. package/components/QItemLabel.vue +99 -0
  8. package/components/QMap.vue +410 -0
  9. package/components/QNumericKeyboard.vue +304 -0
  10. package/components/QPage.vue +26 -6
  11. package/components/QPageContainer.vue +32 -0
  12. package/components/QPopupProxy.vue +378 -0
  13. package/components/QQrcode.vue +132 -0
  14. package/components/QSelect.vue +12 -0
  15. package/components/QSpreadsheet.vue +395 -69
  16. package/components/QTime.vue +845 -0
  17. package/components/QTimeline.vue +111 -0
  18. package/components/QTimelineEntry.vue +271 -0
  19. package/index.ts +11 -1
  20. package/lib/bottomSheet.test.ts +121 -0
  21. package/lib/bottomSheet.ts +115 -0
  22. package/lib/color.ts +13 -0
  23. package/lib/currency.test.ts +193 -0
  24. package/lib/currency.ts +301 -0
  25. package/lib/fixedLayout.ts +12 -11
  26. package/lib/icons.ts +3 -20
  27. package/lib/map.test.ts +365 -0
  28. package/lib/map.ts +438 -0
  29. package/lib/mapEngine.ts +158 -0
  30. package/lib/mapLeaflet.ts +142 -0
  31. package/lib/mapMaplibre.ts +106 -0
  32. package/lib/mapMaptiler.ts +93 -0
  33. package/lib/numericKeyboard.test.ts +179 -0
  34. package/lib/numericKeyboard.ts +166 -0
  35. package/lib/pagePadding.test.ts +37 -0
  36. package/lib/pagePadding.ts +32 -0
  37. package/lib/qrcode.test.ts +103 -0
  38. package/lib/qrcode.ts +125 -0
  39. package/lib/spreadsheetChanges.test.ts +230 -0
  40. package/lib/spreadsheetChanges.ts +331 -0
  41. package/package.json +5 -17
  42. package/shims.d.ts +19 -0
  43. package/styles/main.css +689 -884
  44. package/components/QTiptap.vue +0 -996
  45. package/components/internal/QTiptapTableView.vue +0 -123
  46. package/components/internal/QTiptapTaskItemView.vue +0 -49
  47. package/lib/tiptap-table.ts +0 -19
  48. package/lib/tiptap-task-list.ts +0 -16
@@ -0,0 +1,378 @@
1
+ <script lang="ts">
2
+ // QPopupProxy — proxy de popup façon Quasar : le contenu du slot par défaut est
3
+ // rendu dans un PANNEAU ancré à l'élément parent sur grand écran, et dans un
4
+ // DIALOGUE centré sur écran étroit — même contenu, même API dans les deux cas.
5
+ //
6
+ // <q-btn label="Actions" @click="proxy?.show()">
7
+ // <q-popup-proxy ref="proxy" position="bottom-end">
8
+ // <q-list>…</q-list>
9
+ // </q-popup-proxy>
10
+ // </q-btn>
11
+ //
12
+ // Comme QTooltip, le proxy n'a PAS de déclencheur propre : il rend une ancre
13
+ // invisible DANS la cible (display:none) et retrouve ainsi son élément parent —
14
+ // même après le Teleport du panneau vers <body> (qui couperait le lien DOM).
15
+
16
+ /**
17
+ * Placement du panneau autour du parent : côté principal + alignement sur l'axe
18
+ * perpendiculaire — mêmes valeurs que QBtnActions / QBtnDropdown.
19
+ */
20
+ export type PopupProxyPosition =
21
+ | "bottom-start" | "bottom-end" | "bottom"
22
+ | "top-start" | "top-end" | "top"
23
+ | "left-start" | "left-end" | "left"
24
+ | "right-start" | "right-end" | "right"
25
+ </script>
26
+
27
+ <script setup lang="ts">
28
+ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
29
+ import type { StyleValue } from "vue"
30
+ import { cn } from "../lib/utils"
31
+ import QDialog from "./QDialog.vue"
32
+
33
+ interface Props {
34
+ /** Ouvert (v-model) ; sinon état interne piloté par show() / hide() / toggle() */
35
+ modelValue?: boolean
36
+ /** Largeur de fenêtre (px) EN DESSOUS de laquelle le panneau devient un dialogue — défaut 599 */
37
+ breakpoint?: number
38
+ /** Placement du panneau autour du parent — défaut "bottom-end" */
39
+ position?: PopupProxyPosition
40
+ /** Distance entre le panneau et le parent, en px — défaut 4 */
41
+ offset?: number
42
+ /** Le clic extérieur / Échap ne ferment pas le popup */
43
+ persistent?: boolean
44
+ /** Classes attribuées au panneau téléporté (en plus de `q-popup-proxy__panel`) */
45
+ contentClass?: string
46
+ /** Style(s) du panneau téléporté — PRIORITAIRE sur le style calculé (placement) */
47
+ contentStyle?: StyleValue
48
+ /** Thème sombre */
49
+ dark?: boolean
50
+ }
51
+
52
+ const props = withDefaults(defineProps<Props>(), {
53
+ modelValue: false,
54
+ breakpoint: 599,
55
+ position: "bottom-end",
56
+ offset: 4,
57
+ persistent: false,
58
+ contentClass: "",
59
+ contentStyle: "",
60
+ dark: false,
61
+ })
62
+
63
+ const emit = defineEmits<{
64
+ "update:modelValue": [value: boolean]
65
+ show: []
66
+ hide: []
67
+ }>()
68
+
69
+ // Multi-racines (ancre + Teleport ou dialogue) : Vue ne peut pas hériter les
70
+ // attributs tout seul. Ils sont posés sur l'ancre, seule racine toujours présente.
71
+ defineOptions({ inheritAttrs: false })
72
+
73
+ // — Ouverture contrôlée / interne —
74
+ // Détection du v-model par les PROPS RÉELLEMENT PASSÉES (vnode.props) : une prop
75
+ // Boolean non fournie vaut false (pas undefined) → `modelValue !== undefined`
76
+ // serait TOUJOURS contrôlé et le proxy ne s'ouvrirait jamais sans v-model.
77
+ const instance = getCurrentInstance()
78
+ const controlled = computed(() => {
79
+ const p = instance?.vnode.props
80
+ return !!(p && ("modelValue" in p || "onUpdate:modelValue" in p))
81
+ })
82
+
83
+ const internalOpen = ref(false)
84
+ const isOpen = computed(() => (controlled.value ? props.modelValue : internalOpen.value))
85
+
86
+ const setOpen = (v: boolean) => {
87
+ if (controlled.value) emit("update:modelValue", v)
88
+ else internalOpen.value = v
89
+ }
90
+
91
+ // — Bascule large / étroit —
92
+ // Valeur identique serveur/client (1024) puis mise à jour au montage : aucun
93
+ // mismatch d'hydratation (le popup est de toute façon fermé au prerender).
94
+ const windowWidth = ref(1024)
95
+ const isWide = computed(() => windowWidth.value >= props.breakpoint)
96
+
97
+ // — Ancre & éléments —
98
+ const anchorEl = ref<HTMLElement | null>(null)
99
+ const parentEl = ref<HTMLElement | null>(null)
100
+ const panelEl = ref<HTMLElement | null>(null)
101
+ const dialogEl = ref<HTMLElement | null>(null)
102
+
103
+ // ═════════ Positionnement (fixed, téléporté) ═════════
104
+ // Placement par « point d'ancrage » (top/left fixed) + décalage par translation en
105
+ // % de la taille du panneau (−100% = bord opposé, −50% = centré) — même logique que
106
+ // QBtnActions, aucun calcul de largeur/hauteur nécessaire. Le clamp n'intervient
107
+ // qu'une fois le panneau mesuré : il décale alors le point d'ancrage en px pour
108
+ // garder le panneau dans le viewport.
109
+ const PAD = 8
110
+ const panelPos = ref({ top: 0, left: 0, transform: "none" })
111
+
112
+ const placePanel = () => {
113
+ const parent = parentEl.value
114
+ if (!parent || typeof document === "undefined") return
115
+ const rect = parent.getBoundingClientRect()
116
+ const [main, alt] = props.position.split("-") as [
117
+ "bottom" | "top" | "left" | "right",
118
+ "start" | "end" | undefined,
119
+ ]
120
+ const off = props.offset
121
+ const midX = (rect.left + rect.right) / 2
122
+ const midY = (rect.top + rect.bottom) / 2
123
+
124
+ let top = 0
125
+ let left = 0
126
+ let tx = "0%"
127
+ let ty = "0%"
128
+
129
+ if (main === "bottom" || main === "top") {
130
+ // Axe horizontal : bord gauche (start), bord droit (end) ou centre
131
+ if (alt === "start") left = rect.left
132
+ else if (alt === "end") { left = rect.right; tx = "-100%" }
133
+ else { left = midX; tx = "-50%" }
134
+ if (main === "bottom") top = rect.bottom + off
135
+ else { top = rect.top - off; ty = "-100%" }
136
+ }
137
+ else {
138
+ // Axe vertical : haut (start), bas (end) ou centre
139
+ if (alt === "start") top = rect.top
140
+ else if (alt === "end") { top = rect.bottom; ty = "-100%" }
141
+ else { top = midY; ty = "-50%" }
142
+ if (main === "right") left = rect.right + off
143
+ else { left = rect.left - off; tx = "-100%" }
144
+ }
145
+
146
+ // Clamp viewport : seulement si le panneau est déjà rendu (taille connue)
147
+ const panel = panelEl.value
148
+ if (panel) {
149
+ const pw = panel.offsetWidth
150
+ const ph = panel.offsetHeight
151
+ const vw = window.innerWidth
152
+ const vh = window.innerHeight
153
+ const dx = tx === "-100%" ? -pw : tx === "-50%" ? -pw / 2 : 0
154
+ const dy = ty === "-100%" ? -ph : ty === "-50%" ? -ph / 2 : 0
155
+ const clampedX = Math.min(Math.max(left + dx, PAD), Math.max(PAD, vw - pw - PAD))
156
+ const clampedY = Math.min(Math.max(top + dy, PAD), Math.max(PAD, vh - ph - PAD))
157
+ left += clampedX - (left + dx)
158
+ top += clampedY - (top + dy)
159
+ }
160
+
161
+ panelPos.value = {
162
+ top,
163
+ left,
164
+ transform: tx === "0%" && ty === "0%" ? "none" : `translate(${tx}, ${ty})`,
165
+ }
166
+ }
167
+
168
+ const panelStyle = computed<StyleValue>(() => ({
169
+ top: `${panelPos.value.top}px`,
170
+ left: `${panelPos.value.left}px`,
171
+ transform: panelPos.value.transform,
172
+ }))
173
+
174
+ // — Suivi (resize / scroll) tant que le panneau ancré est ouvert —
175
+ let tracking = false
176
+ const startTracking = () => {
177
+ if (tracking || typeof window === "undefined") return
178
+ tracking = true
179
+ window.addEventListener("resize", placePanel)
180
+ window.addEventListener("scroll", placePanel, true)
181
+ }
182
+ const stopTracking = () => {
183
+ if (!tracking || typeof window === "undefined") return
184
+ tracking = false
185
+ window.removeEventListener("resize", placePanel)
186
+ window.removeEventListener("scroll", placePanel, true)
187
+ }
188
+
189
+ // — Focus : premier élément focusable à l'ouverture, restauration à la fermeture —
190
+ const FOCUSABLE =
191
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
192
+ let lastFocused: HTMLElement | null = null
193
+
194
+ const focusFirst = (container: HTMLElement | null) => {
195
+ if (!container) return
196
+ const el = container.querySelector<HTMLElement>(FOCUSABLE)
197
+ ;(el ?? container).focus({ preventScroll: true })
198
+ }
199
+
200
+ const restoreFocus = () => {
201
+ const el = lastFocused
202
+ lastFocused = null
203
+ if (el && typeof document !== "undefined" && document.contains(el)) el.focus({ preventScroll: true })
204
+ }
205
+
206
+ // — Cycle ouverture / fermeture (commun aux deux rendus) —
207
+ const onOpened = async () => {
208
+ if (typeof document === "undefined") return
209
+ lastFocused = (document.activeElement as HTMLElement | null) ?? null
210
+ await nextTick()
211
+ if (isWide.value) {
212
+ startTracking()
213
+ placePanel()
214
+ await nextTick() // 2ᵉ passage : taille du panneau connue → clamp viewport
215
+ placePanel()
216
+ focusFirst(panelEl.value)
217
+ }
218
+ else {
219
+ focusFirst(dialogEl.value)
220
+ }
221
+ emit("show")
222
+ }
223
+
224
+ const onClosed = () => {
225
+ stopTracking()
226
+ emit("hide")
227
+ restoreFocus()
228
+ }
229
+
230
+ watch(isOpen, (v, old) => {
231
+ if (v) onOpened()
232
+ else if (old) onClosed()
233
+ })
234
+
235
+ // Fenêtre redimensionnée pendant l'ouverture : bascule de rendu + repositionnement
236
+ watch(isWide, async (wide) => {
237
+ if (!isOpen.value) return
238
+ if (wide) {
239
+ await nextTick()
240
+ startTracking()
241
+ placePanel()
242
+ await nextTick()
243
+ placePanel()
244
+ focusFirst(panelEl.value)
245
+ }
246
+ else {
247
+ stopTracking()
248
+ await nextTick()
249
+ focusFirst(dialogEl.value)
250
+ }
251
+ })
252
+
253
+ const onWindowResize = () => {
254
+ windowWidth.value = window.innerWidth
255
+ }
256
+
257
+ // — Fermeture : clic extérieur (phase de CAPTURE) et Échap — sauf `persistent` —
258
+ // Le rendu étroit est un QDialog : il gère lui-même backdrop / Échap (via persistent).
259
+ const onDocMousedown = (e: MouseEvent) => {
260
+ if (!isOpen.value || !isWide.value || props.persistent) return
261
+ const target = e.target as Node
262
+ if (parentEl.value?.contains(target) || panelEl.value?.contains(target)) return
263
+ setOpen(false)
264
+ }
265
+
266
+ const onDocKeydown = (e: KeyboardEvent) => {
267
+ if (!isOpen.value || !isWide.value || props.persistent) return
268
+ if (e.key !== "Escape") return
269
+ e.preventDefault()
270
+ setOpen(false)
271
+ }
272
+
273
+ // — Méthodes publiques —
274
+ const show = () => setOpen(true)
275
+ const hide = () => setOpen(false)
276
+ const toggle = () => (isOpen.value ? hide() : show())
277
+
278
+ defineExpose({ show, hide, toggle })
279
+
280
+ onMounted(() => {
281
+ // L'ancre (span invisible rendu DANS la cible) donne l'élément parent, même
282
+ // après le Teleport du panneau vers <body>.
283
+ parentEl.value = anchorEl.value?.parentElement ?? null
284
+ if (typeof window !== "undefined") {
285
+ windowWidth.value = window.innerWidth
286
+ window.addEventListener("resize", onWindowResize)
287
+ }
288
+ if (typeof document !== "undefined") {
289
+ // Phase de CAPTURE : sinon un `@mousedown.stop` parent (contenu de QDialog,
290
+ // QDataGrid…) bloque l'événement avant `document` → popup jamais fermé.
291
+ document.addEventListener("mousedown", onDocMousedown, true)
292
+ document.addEventListener("keydown", onDocKeydown)
293
+ }
294
+ if (isOpen.value) onOpened()
295
+ })
296
+
297
+ onBeforeUnmount(() => {
298
+ stopTracking()
299
+ if (typeof window !== "undefined") window.removeEventListener("resize", onWindowResize)
300
+ if (typeof document !== "undefined") {
301
+ document.removeEventListener("mousedown", onDocMousedown, true)
302
+ document.removeEventListener("keydown", onDocKeydown)
303
+ }
304
+ })
305
+ </script>
306
+
307
+ <template>
308
+ <!-- Ancre invisible DANS la cible : référence pour retrouver l'élément parent. -->
309
+ <span
310
+ ref="anchorEl"
311
+ class="q-popup-proxy__anchor"
312
+ aria-hidden="true"
313
+ style="display: none"
314
+ v-bind="$attrs"
315
+ />
316
+
317
+ <!-- Grand écran (≥ breakpoint) : panneau ancré au parent, téléporté dans <body> -->
318
+ <Teleport v-if="isWide" to="body">
319
+ <Transition name="q-popup">
320
+ <div
321
+ v-if="isOpen"
322
+ ref="panelEl"
323
+ class="q-popup-proxy__panel"
324
+ :class="[dark && 'dark', contentClass]"
325
+ :style="[panelStyle, contentStyle]"
326
+ role="dialog"
327
+ tabindex="-1"
328
+ >
329
+ <slot />
330
+ </div>
331
+ </Transition>
332
+ </Teleport>
333
+
334
+ <!-- Écran étroit : même contenu rendu dans un QDialog (transition du dialog) -->
335
+ <QDialog
336
+ v-else
337
+ :model-value="isOpen"
338
+ :persistent="persistent"
339
+ :content-class="cn('q-popup-proxy__dialog-content', contentClass)"
340
+ :content-style="contentStyle"
341
+ @update:model-value="setOpen"
342
+ >
343
+ <div ref="dialogEl" class="q-popup-proxy__dialog">
344
+ <slot />
345
+ </div>
346
+ </QDialog>
347
+ </template>
348
+
349
+ <style scoped>
350
+ /* Panneau ancré (grand écran) — téléporté dans <body>, donc position: fixed et
351
+ couche « menus téléportés » (--q-z-menu), au-dessus des overlays modaux. */
352
+ .q-popup-proxy__panel {
353
+ position: fixed;
354
+ z-index: var(--q-z-menu, 3200);
355
+ min-width: 160px;
356
+ padding: 4px;
357
+ background-color: var(--card, #fff);
358
+ color: var(--foreground, #1d1d1d);
359
+ border: 1px solid var(--border, rgb(0 0 0 / 0.12));
360
+ border-radius: var(--radius, var(--q-radius, 6px));
361
+ box-shadow: 0 4px 16px rgb(0 0 0 / 0.15);
362
+ outline: none;
363
+ }
364
+
365
+ /* Mode sombre : le panneau est téléporté hors du conteneur du provider → on couvre
366
+ à la fois l'ancêtre `.dark` (html) et la classe `dark` posée par la prop. */
367
+ .dark .q-popup-proxy__panel,
368
+ .q-popup-proxy__panel.dark {
369
+ background-color: var(--card, #161b22);
370
+ color: var(--foreground, #e6edf3);
371
+ border-color: var(--border, rgb(255 255 255 / 0.12));
372
+ }
373
+
374
+ /* Contenu du dialogue (écran étroit) — surface/ombre fournies par QDialog. */
375
+ .q-popup-proxy__dialog {
376
+ color: var(--foreground);
377
+ }
378
+ </style>
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ // QQrcode — code QR : la matrice est encodée par la dépendance `qrcode` (déjà présente) et
3
+ // tracée par dnax.ui en **SVG** — un seul chemin, net à toutes les tailles, thémable et
4
+ // rendu côté serveur (l'encodage est du JS pur).
5
+ //
6
+ // <q-qrcode value="https://dnax.io" />
7
+ // <q-qrcode :value="url" :size="220" :margin="2" color="var(--foreground)" />
8
+ // <q-qrcode value="…" ecc="H" background="transparent" />
9
+ //
10
+ // Le composant expose `svg()` (SVG sérialisé : téléchargement, presse-papier) et
11
+ // `toDataURL()` (PNG, côté client). Le tracé vit dans `lib/qrcode.ts` (pur, testé).
12
+ import { computed, watchEffect } from "vue"
13
+ import { isDark, encodeQr, qrPath, qrSize, qrSvg, qrTotalSize, qrViewBox, type QrEcc } from "../lib/qrcode"
14
+
15
+ interface Props {
16
+ /** Contenu encodé (URL, texte, vCard, wifi…) — vide : rien n'est rendu */
17
+ value?: string
18
+ /** Côté du rendu : nombre (px) ou longueur CSS */
19
+ size?: number | string
20
+ /** Correction d'erreur : `L` (~7 %) → `H` (~30 %, pour un QR abîmé ou un logo au centre) */
21
+ ecc?: QrEcc
22
+ /** Zone de silence en modules (défaut 4 : c'est ce qu'attend un lecteur ; 0 pour un QR
23
+ * collé à son cadre — à ne faire que si le fond autour est clair et uni) */
24
+ margin?: number
25
+ /** Couleur des modules (couleur CSS quelconque, `var(--token)` comprise) */
26
+ color?: string
27
+ /** Couleur du fond — `"transparent"` laisse voir le fond du parent */
28
+ background?: string
29
+ /** Étiquette accessible du SVG (`role="img"`) */
30
+ label?: string
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ value: "",
35
+ size: 160,
36
+ ecc: "M",
37
+ margin: 4,
38
+ color: "#000",
39
+ background: "#fff",
40
+ label: "Code QR",
41
+ })
42
+
43
+ /** Matrice de modules — `undefined` si la valeur ne tient pas dans un QR */
44
+ const matrix = computed(() => encodeQr(props.value, { ecc: props.ecc }))
45
+
46
+ watchEffect(() => {
47
+ if (props.value && !matrix.value) {
48
+ console.warn(
49
+ `[q-qrcode] valeur non encodable (trop longue pour ecc="${props.ecc}") : rien n'est rendu`,
50
+ )
51
+ }
52
+ })
53
+
54
+ const total = computed(() => (matrix.value ? qrTotalSize(matrix.value, props.margin) : 0))
55
+ const viewBox = computed(() => (matrix.value ? qrViewBox(matrix.value, props.margin) : "0 0 0 0"))
56
+ const path = computed(() => (matrix.value ? qrPath(matrix.value, props.margin) : ""))
57
+ const cssSize = computed(() => qrSize(props.size))
58
+ const transparent = computed(() => props.background === "transparent")
59
+
60
+ /** SVG sérialisé (téléchargement, presse-papier, `<img src="data:image/svg+xml…">`) */
61
+ const svg = (): string =>
62
+ matrix.value
63
+ ? qrSvg(matrix.value, {
64
+ margin: props.margin,
65
+ color: props.color,
66
+ background: props.background,
67
+ label: props.label,
68
+ })
69
+ : ""
70
+
71
+ /** Couleur résolue en valeur calculée (le canvas ne comprend ni `var()` ni `currentColor`) */
72
+ const resolveColor = (value: string, fallback: string): string => {
73
+ if (typeof document === "undefined") return fallback
74
+ const probe = document.createElement("span")
75
+ probe.style.color = value
76
+ probe.style.display = "none"
77
+ document.body.appendChild(probe)
78
+ const resolved = getComputedStyle(probe).color
79
+ probe.remove()
80
+ return resolved || fallback
81
+ }
82
+
83
+ /** PNG (data URL) dessiné depuis la matrice — navigateur uniquement, `null` côté serveur */
84
+ const toDataURL = (options: { pixelSize?: number; margin?: number } = {}): string | null => {
85
+ const m = matrix.value
86
+ if (!m || typeof document === "undefined") return null
87
+
88
+ const pixel = Math.max(1, Math.floor(options.pixelSize ?? 8))
89
+ const margin = Math.max(0, Math.floor(options.margin ?? props.margin))
90
+ const side = qrTotalSize(m, margin)
91
+
92
+ const canvas = document.createElement("canvas")
93
+ canvas.width = canvas.height = side * pixel
94
+ const ctx = canvas.getContext("2d")
95
+ if (!ctx) return null
96
+
97
+ ctx.fillStyle = resolveColor(props.background, "transparent")
98
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
99
+ ctx.fillStyle = resolveColor(props.color, "#000")
100
+
101
+ for (let y = 0; y < m.size; y++) {
102
+ for (let x = 0; x < m.size; x++) {
103
+ if (isDark(m, x, y)) ctx.fillRect((x + margin) * pixel, (y + margin) * pixel, pixel, pixel)
104
+ }
105
+ }
106
+
107
+ return canvas.toDataURL("image/png")
108
+ }
109
+
110
+ defineExpose({
111
+ /** SVG complet du code courant ("" si la valeur n'est pas encodable) */
112
+ svg,
113
+ /** PNG en `data:` URL (nécessite un navigateur) */
114
+ toDataURL,
115
+ })
116
+ </script>
117
+
118
+ <template>
119
+ <svg
120
+ v-if="matrix"
121
+ class="q-qrcode"
122
+ :viewBox="viewBox"
123
+ :width="cssSize"
124
+ :height="cssSize"
125
+ role="img"
126
+ :aria-label="label"
127
+ v-bind="$attrs"
128
+ >
129
+ <rect v-if="!transparent" :width="total" :height="total" :fill="background" />
130
+ <path :d="path" :fill="color" shape-rendering="crispEdges" />
131
+ </svg>
132
+ </template>
@@ -397,6 +397,12 @@ const select = (opt: any) => {
397
397
  else {
398
398
  if (!isSelected(opt)) emit("update:modelValue", value)
399
399
  closePopup()
400
+ // Après une sélection (simple), on efface la recherche pour laisser place à
401
+ // l'élément choisi : le champ d'affichage reprend le label, plus le filtre.
402
+ if (query.value !== "") {
403
+ query.value = ""
404
+ emit("update:inputValue", "")
405
+ }
400
406
  }
401
407
  }
402
408
 
@@ -520,6 +526,12 @@ const openPopup = () => {
520
526
 
521
527
  const closePopup = () => {
522
528
  open.value = false
529
+ // Fermeture sans sélection (clic extérieur, backdrop, Échap, ×) : on abandonne la
530
+ // recherche saisie pour ne pas laisser un filtre fantôme au prochain affichage.
531
+ if (query.value !== "" && !hasValue.value) {
532
+ query.value = ""
533
+ emit("update:inputValue", "")
534
+ }
523
535
  }
524
536
 
525
537
  const togglePopup = () => (open.value ? closePopup() : openPopup())