@dnax/ui 0.0.36 → 0.0.38
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/QBtnActions.vue +40 -4
- package/components/QBtnDropdown.vue +18 -1
- package/components/QSelect.vue +155 -3
- package/package.json +1 -1
- package/styles/main.css +38 -1
|
@@ -44,11 +44,13 @@ export type DropdownPosition =
|
|
|
44
44
|
|
|
45
45
|
<script setup lang="ts">
|
|
46
46
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
|
|
47
|
+
import type { StyleValue } from "vue"
|
|
47
48
|
import { Icon } from "@iconify/vue"
|
|
48
49
|
import { colorValue } from "../lib/colors"
|
|
49
50
|
import { icons } from "../lib/icons"
|
|
50
51
|
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
51
52
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
53
|
+
import { cn } from "../lib/utils"
|
|
52
54
|
import QBtn from "./QBtn.vue"
|
|
53
55
|
|
|
54
56
|
interface Props {
|
|
@@ -71,8 +73,23 @@ interface Props {
|
|
|
71
73
|
position?: DropdownPosition
|
|
72
74
|
/** Distance entre le panneau et le déclencheur, en px (défaut : 4) */
|
|
73
75
|
offset?: number
|
|
74
|
-
/** Largeur minimale du menu (ex. "200px") */
|
|
76
|
+
/** Largeur minimale du menu (ex. "200px") — sert aussi de plancher quand `fit` est actif */
|
|
75
77
|
menuWidth?: string
|
|
78
|
+
/**
|
|
79
|
+
* Le panneau fait AU MOINS la largeur du déclencheur (défaut : true).
|
|
80
|
+
* Sémantique Quasar `QMenu.fit` : c'est un plancher, pas une largeur exacte —
|
|
81
|
+
* un item plus large (libellé long) élargit encore le panneau.
|
|
82
|
+
*/
|
|
83
|
+
fit?: boolean
|
|
84
|
+
/** Classes attribuées au panneau téléporté (en plus de `q-btn-actions__panel`) */
|
|
85
|
+
contentClass?: string
|
|
86
|
+
/**
|
|
87
|
+
* Style(s) attribués au panneau téléporté — string, objet ou tableau (comme la
|
|
88
|
+
* prop `style` de Vue).
|
|
89
|
+
* PRIORITAIRE sur le style calculé (placement, min-width) : c'est la trappe de
|
|
90
|
+
* sortie pour forcer une largeur exacte ou une couleur de fond.
|
|
91
|
+
*/
|
|
92
|
+
contentStyle?: StyleValue
|
|
76
93
|
// — API déclencheur (transmise à QBtn) —
|
|
77
94
|
color?: string
|
|
78
95
|
textColor?: string
|
|
@@ -99,6 +116,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
99
116
|
align: "right",
|
|
100
117
|
offset: 4,
|
|
101
118
|
menuWidth: "180px",
|
|
119
|
+
fit: true,
|
|
120
|
+
contentClass: "",
|
|
121
|
+
contentStyle: "",
|
|
102
122
|
size: "md",
|
|
103
123
|
color: "primary",
|
|
104
124
|
flat: false,
|
|
@@ -124,6 +144,8 @@ const rootEl = ref<HTMLElement | null>(null)
|
|
|
124
144
|
const panelEl = ref<HTMLElement | null>(null)
|
|
125
145
|
/** Position fixed du panneau (téléporté dans <body>) */
|
|
126
146
|
const panelPos = ref({ top: 0, left: 0, transform: "" })
|
|
147
|
+
/** Largeur mesurée du déclencheur — plancher du panneau quand `fit` est actif */
|
|
148
|
+
const triggerWidth = ref(0)
|
|
127
149
|
|
|
128
150
|
const canOpen = computed(() => !props.disable && !props.loading && props.actions.length > 0)
|
|
129
151
|
|
|
@@ -141,14 +163,27 @@ const triggerClasses = computed(() => [
|
|
|
141
163
|
const effectiveRadius = useRadius("QBtnActions", () => props.radius)
|
|
142
164
|
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
143
165
|
|
|
166
|
+
// `fit` : min-width = max(menu-width, largeur du déclencheur). `max()` est calculé
|
|
167
|
+
// par le CSS → `menu-width` peut rester une longueur quelconque ("16rem", "40%").
|
|
168
|
+
const panelMinWidth = computed(() => {
|
|
169
|
+
const base = props.menuWidth
|
|
170
|
+
if (!props.fit || !triggerWidth.value) return base
|
|
171
|
+
const fitted = `${Math.round(triggerWidth.value)}px`
|
|
172
|
+
return base ? `max(${base}, ${fitted})` : fitted
|
|
173
|
+
})
|
|
174
|
+
|
|
144
175
|
const panelStyle = computed<Record<string, string>>(() => ({
|
|
145
176
|
top: `${panelPos.value.top}px`,
|
|
146
177
|
left: `${panelPos.value.left}px`,
|
|
147
178
|
transform: panelPos.value.transform,
|
|
148
|
-
minWidth:
|
|
179
|
+
minWidth: panelMinWidth.value,
|
|
149
180
|
...(roundedStyle.value ?? {}),
|
|
150
181
|
}))
|
|
151
182
|
|
|
183
|
+
// Le panneau est téléporté dans <body> : `content-class` / `content-style` sont
|
|
184
|
+
// donc la seule façon de le styler depuis le composant consommateur.
|
|
185
|
+
const panelClasses = computed(() => cn("q-btn-actions__panel", props.contentClass))
|
|
186
|
+
|
|
152
187
|
// `position` explicite, sinon alias `align` (compat) → bottom-start / bottom-end
|
|
153
188
|
const placement = computed<DropdownPosition>(
|
|
154
189
|
() => props.position ?? (props.align === "left" ? "bottom-start" : "bottom-end"),
|
|
@@ -162,6 +197,7 @@ const placePanel = () => {
|
|
|
162
197
|
const el = rootEl.value
|
|
163
198
|
if (!el || typeof document === "undefined") return
|
|
164
199
|
const rect = el.getBoundingClientRect()
|
|
200
|
+
triggerWidth.value = rect.width
|
|
165
201
|
const [main, alt] = placement.value.split("-") as ["bottom" | "top" | "left" | "right", "start" | "end" | undefined]
|
|
166
202
|
const off = props.offset
|
|
167
203
|
const midX = (rect.left + rect.right) / 2
|
|
@@ -355,10 +391,10 @@ onBeforeUnmount(() => {
|
|
|
355
391
|
<div
|
|
356
392
|
v-if="open"
|
|
357
393
|
ref="panelEl"
|
|
358
|
-
class="
|
|
394
|
+
:class="panelClasses"
|
|
359
395
|
role="menu"
|
|
360
396
|
:aria-label="label ?? 'Actions'"
|
|
361
|
-
:style="panelStyle"
|
|
397
|
+
:style="[panelStyle, contentStyle]"
|
|
362
398
|
@keydown="onKeydown"
|
|
363
399
|
>
|
|
364
400
|
<template v-for="row in menuRows" :key="row.key">
|
|
@@ -29,6 +29,7 @@ export interface DropdownItem {
|
|
|
29
29
|
|
|
30
30
|
<script setup lang="ts">
|
|
31
31
|
import { computed } from "vue"
|
|
32
|
+
import type { StyleValue } from "vue"
|
|
32
33
|
import QBtnActions, { type DropdownPosition } from "./QBtnActions.vue"
|
|
33
34
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
34
35
|
|
|
@@ -52,8 +53,21 @@ interface Props {
|
|
|
52
53
|
position?: DropdownPosition
|
|
53
54
|
/** Distance entre le panneau et le déclencheur, en px (défaut : 4) */
|
|
54
55
|
offset?: number
|
|
55
|
-
/** Largeur minimale du menu (ex. "220px") */
|
|
56
|
+
/** Largeur minimale du menu (ex. "220px") — sert aussi de plancher quand `fit` est actif */
|
|
56
57
|
menuWidth?: string
|
|
58
|
+
/**
|
|
59
|
+
* Le panneau fait AU MOINS la largeur du déclencheur (défaut : true).
|
|
60
|
+
* Sémantique Quasar `QMenu.fit` : c'est un plancher, pas une largeur exacte —
|
|
61
|
+
* un item plus large (libellé long) élargit encore le panneau.
|
|
62
|
+
*/
|
|
63
|
+
fit?: boolean
|
|
64
|
+
/** Classes attribuées au panneau du menu (téléporté dans `<body>`) */
|
|
65
|
+
contentClass?: string
|
|
66
|
+
/**
|
|
67
|
+
* Style(s) attribués au panneau du menu — string, objet ou tableau (comme la
|
|
68
|
+
* prop `style` de Vue). PRIORITAIRE sur le style calculé (placement, min-width).
|
|
69
|
+
*/
|
|
70
|
+
contentStyle?: StyleValue
|
|
57
71
|
// — API déclencheur (transmise à QBtn) —
|
|
58
72
|
color?: string
|
|
59
73
|
textColor?: string
|
|
@@ -114,6 +128,9 @@ const onSelectAction = (value: any) => emit("select", value)
|
|
|
114
128
|
:position="position"
|
|
115
129
|
:offset="offset"
|
|
116
130
|
:menu-width="menuWidth"
|
|
131
|
+
:fit="fit"
|
|
132
|
+
:content-class="contentClass"
|
|
133
|
+
:content-style="contentStyle"
|
|
117
134
|
:color="color"
|
|
118
135
|
:text-color="textColor"
|
|
119
136
|
:size="size"
|
package/components/QSelect.vue
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// QSelect — options spécifiques par mode d'ouverture.
|
|
3
3
|
// sheetOptions / modalOptions / inlineOptions : style, classes, hauteur, arrondi, placeholder de recherche.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Placement du popup `inline` (même vocabulaire que `DropdownPosition` de QBtnActions) :
|
|
7
|
+
*
|
|
8
|
+
* - `"auto"` — suit la place disponible : sous le champ, bascule au-dessus s'il n'y
|
|
9
|
+
* a pas la place. Le popup fait la largeur du champ.
|
|
10
|
+
* - `"top"` / `"bottom"` — force le côté (pas de bascule). Le popup fait la largeur
|
|
11
|
+
* du champ.
|
|
12
|
+
* - suffixe `"-start"` / `"-end"` — ancre le popup sur le bord GAUCHE / DROIT du
|
|
13
|
+
* champ ; il prend alors sa largeur naturelle (ou `inline-options.width`), donc il
|
|
14
|
+
* peut être plus étroit ou plus large que le champ.
|
|
15
|
+
*
|
|
16
|
+
* Les placements latéraux de `DropdownPosition` (`left`, `right`) ne sont pas repris :
|
|
17
|
+
* un popup de sélection s'ouvre toujours au-dessus ou au-dessous de son champ.
|
|
18
|
+
*/
|
|
19
|
+
export type SelectPopupPosition =
|
|
20
|
+
| "auto"
|
|
21
|
+
| "bottom" | "bottom-start" | "bottom-end"
|
|
22
|
+
| "top" | "top-start" | "top-end"
|
|
23
|
+
|
|
4
24
|
export interface QSelectModeOptions {
|
|
5
25
|
/** Largeur du panneau/popup (remplace width) */
|
|
6
26
|
width?: string
|
|
@@ -10,6 +30,10 @@ export interface QSelectModeOptions {
|
|
|
10
30
|
class?: string
|
|
11
31
|
/** Hauteur de la liste scrollable (remplace height) */
|
|
12
32
|
height?: string
|
|
33
|
+
/** Écart champ ↔ popup (mode inline) — remplace la prop `offset` */
|
|
34
|
+
offset?: number
|
|
35
|
+
/** Placement du popup (mode inline) — remplace la prop `position` */
|
|
36
|
+
position?: SelectPopupPosition
|
|
13
37
|
/** Arrondi du panneau (remplace rounded) */
|
|
14
38
|
rounded?: boolean | string | { tl?: string | number; tr?: string | number; br?: string | number; bl?: string | number }
|
|
15
39
|
/** Placeholder du champ de recherche pour ce mode */
|
|
@@ -95,6 +119,18 @@ interface Props {
|
|
|
95
119
|
height?: string
|
|
96
120
|
/** Arrondi du panneau : true (défaut du mode) | false (carré) | valeur CSS | objet { tl, tr, br, bl } */
|
|
97
121
|
rounded?: boolean | string | { tl?: string | number; tr?: string | number; br?: string | number; bl?: string | number }
|
|
122
|
+
/**
|
|
123
|
+
* Écart entre le champ et le popup inline, en px (défaut : 8).
|
|
124
|
+
* Réduit automatiquement (jusqu'à 0) si la place manque sous/au-dessus du champ.
|
|
125
|
+
* Surchargeable par mode : `inline-options="{ offset: 8 }"`.
|
|
126
|
+
*/
|
|
127
|
+
offset?: number
|
|
128
|
+
/**
|
|
129
|
+
* Placement du popup inline — voir `SelectPopupPosition`. Défaut : `"auto"`
|
|
130
|
+
* (bascule automatique selon la place disponible).
|
|
131
|
+
* Surchargeable par mode : `inline-options="{ position: 'top' }"`.
|
|
132
|
+
*/
|
|
133
|
+
position?: SelectPopupPosition
|
|
98
134
|
/** Options spécifiques au mode sheet (style, class, height, rounded, width, searchPlaceholder) */
|
|
99
135
|
sheetOptions?: QSelectModeOptions
|
|
100
136
|
/** Options spécifiques au mode modal */
|
|
@@ -125,6 +161,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
125
161
|
loading: false,
|
|
126
162
|
noOptionsLabel: "No options to display",
|
|
127
163
|
mode: "inline",
|
|
164
|
+
offset: 8,
|
|
165
|
+
position: "auto",
|
|
128
166
|
})
|
|
129
167
|
|
|
130
168
|
const emit = defineEmits<{
|
|
@@ -170,6 +208,104 @@ const modeOptions = computed<QSelectModeOptions | undefined>(() => {
|
|
|
170
208
|
return undefined
|
|
171
209
|
})
|
|
172
210
|
|
|
211
|
+
// — Popup inline : direction, écart et hauteur max calculés selon l'espace visible —
|
|
212
|
+
// Ouvre vers le bas ; s'il n'y a pas la place (POPUP_MIN_SPACE) et qu'au-dessus il y
|
|
213
|
+
// a davantage de place, il bascule au-dessus du champ. L'écart avec le champ
|
|
214
|
+
// (`offset`, 8px par défaut) se réduit quand la place manque (jusqu'à 0) et la
|
|
215
|
+
// hauteur max est bornée à l'espace disponible : le popup ne dépasse jamais le bord
|
|
216
|
+
// de la fenêtre.
|
|
217
|
+
/** Espace vertical visé pour la liste : en dessous, on cherche de la place ailleurs */
|
|
218
|
+
const POPUP_MIN_SPACE = 96
|
|
219
|
+
/** Plafond historique de la liste scrollable */
|
|
220
|
+
const POPUP_MAX_HEIGHT = 240
|
|
221
|
+
/** Marge conservée avec les bords de la fenêtre */
|
|
222
|
+
const VIEWPORT_MARGIN = 8
|
|
223
|
+
|
|
224
|
+
const popupDirection = ref<"down" | "up">("down")
|
|
225
|
+
const popupMaxHeight = ref(POPUP_MAX_HEIGHT)
|
|
226
|
+
/** Décalages en px par rapport à la RACINE (bloc conteneur du popup) — null avant mesure */
|
|
227
|
+
const popupTop = ref<number | null>(null)
|
|
228
|
+
const popupBottom = ref<number | null>(null)
|
|
229
|
+
|
|
230
|
+
/** Écart champ ↔ popup : `inline-options.offset` prioritaire sur la prop `offset` */
|
|
231
|
+
const popupOffset = computed(() => modeOptions.value?.offset ?? props.offset)
|
|
232
|
+
|
|
233
|
+
/** Placement demandé : `inline-options.position` prioritaire sur la prop `position` */
|
|
234
|
+
const popupPlacement = computed<SelectPopupPosition>(
|
|
235
|
+
() => modeOptions.value?.position ?? props.position,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const positionPopup = () => {
|
|
239
|
+
const el = rootEl.value
|
|
240
|
+
if (!el || typeof window === "undefined") return
|
|
241
|
+
const rect = el.getBoundingClientRect()
|
|
242
|
+
|
|
243
|
+
// Ancre = bas du CHAMP, pas bas de la racine : `.q-field__bottom` réserve
|
|
244
|
+
// ~24px (min-height 20px + padding-top 4px) même quand il est vide, ce qui
|
|
245
|
+
// éloignait le popup du champ. Quand un hint / une erreur est affiché, on
|
|
246
|
+
// garde le bas de la racine pour ne pas recouvrir le texte.
|
|
247
|
+
const control = el.querySelector<HTMLElement>(".q-field__control")
|
|
248
|
+
const hasBottomText = !!el.querySelector(".q-field__hint, .q-field__error")
|
|
249
|
+
const anchor = (!hasBottomText && control ? control : el).getBoundingClientRect()
|
|
250
|
+
|
|
251
|
+
const below = window.innerHeight - anchor.bottom - VIEWPORT_MARGIN
|
|
252
|
+
const above = anchor.top - VIEWPORT_MARGIN
|
|
253
|
+
|
|
254
|
+
// `auto` : sous le champ, bascule au-dessus s'il n'y a pas la place (et qu'il y en
|
|
255
|
+
// a plus au-dessus). `top` / `bottom` forcent le côté, sans bascule.
|
|
256
|
+
const placement = popupPlacement.value
|
|
257
|
+
const down =
|
|
258
|
+
placement === "auto"
|
|
259
|
+
? below >= POPUP_MIN_SPACE || below >= above
|
|
260
|
+
: !placement.startsWith("top")
|
|
261
|
+
const available = Math.max(down ? below : above, 0)
|
|
262
|
+
|
|
263
|
+
// Écart réduit quand la place manque, pour laisser le maximum à la liste
|
|
264
|
+
const gap = Math.max(0, Math.min(popupOffset.value, available - POPUP_MIN_SPACE))
|
|
265
|
+
|
|
266
|
+
popupDirection.value = down ? "down" : "up"
|
|
267
|
+
popupMaxHeight.value = Math.max(0, Math.min(POPUP_MAX_HEIGHT, available - gap))
|
|
268
|
+
popupTop.value = Math.round(anchor.bottom - rect.top + gap)
|
|
269
|
+
popupBottom.value = Math.round(rect.bottom - anchor.top + gap)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const popupStyle = computed<Record<string, string>>(() => {
|
|
273
|
+
const up = popupDirection.value === "up"
|
|
274
|
+
const fallback = `calc(100% + ${popupOffset.value}px)`
|
|
275
|
+
const style: Record<string, string> = {
|
|
276
|
+
top: up ? "auto" : popupTop.value === null ? fallback : `${popupTop.value}px`,
|
|
277
|
+
bottom: up ? (popupBottom.value === null ? fallback : `${popupBottom.value}px`) : "auto",
|
|
278
|
+
maxHeight: `${Math.round(popupMaxHeight.value)}px`,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Ancre horizontale : par défaut le popup couvre la largeur du champ. Les suffixes
|
|
282
|
+
// `-start` / `-end` (ou un `inline-options.width` explicite) l'ancrent sur un bord —
|
|
283
|
+
// l'effet est visible dès que le popup n'a pas la largeur du champ.
|
|
284
|
+
const placement = popupPlacement.value
|
|
285
|
+
const width = modeOptions.value?.width
|
|
286
|
+
const anchored = placement !== "auto" && placement.includes("-")
|
|
287
|
+
if (anchored || width) {
|
|
288
|
+
const alignEnd = placement.endsWith("-end")
|
|
289
|
+
style.left = alignEnd ? "auto" : "0"
|
|
290
|
+
style.right = alignEnd ? "0" : "auto"
|
|
291
|
+
style.width = width ?? "100%"
|
|
292
|
+
} else {
|
|
293
|
+
style.left = "0"
|
|
294
|
+
style.right = "0"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return style
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Le champ peut bouger (scroll) ou la fenêtre changer de taille : on recalcule
|
|
301
|
+
const onPopupViewportChange = () => {
|
|
302
|
+
if (open.value && props.mode === "inline") positionPopup()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Un changement de `position` / `offset` (prop ou options de mode) pendant que le
|
|
306
|
+
// popup est ouvert doit le replacer tout de suite (ex. sélecteur de placement).
|
|
307
|
+
watch([popupPlacement, popupOffset], onPopupViewportChange)
|
|
308
|
+
|
|
173
309
|
// — Options normalisées : les primitives (string/number) deviennent { value, label } —
|
|
174
310
|
const isObjectOption = (o: any): boolean => o !== null && typeof o === "object"
|
|
175
311
|
|
|
@@ -446,11 +582,23 @@ const onDocKeydown = (e: KeyboardEvent) => {
|
|
|
446
582
|
if (e.key === "Escape" && open.value && props.mode !== "inline") closePopup()
|
|
447
583
|
}
|
|
448
584
|
|
|
449
|
-
// Verrouille le scroll du body quand un panneau modal est ouvert
|
|
585
|
+
// Verrouille le scroll du body quand un panneau modal est ouvert ;
|
|
586
|
+
// en mode inline, (re)calcule la direction/écart/hauteur du popup à l'ouverture
|
|
587
|
+
// et suit les changements de viewport tant qu'il est ouvert.
|
|
450
588
|
watch(open, (v) => {
|
|
451
589
|
if (typeof document !== "undefined" && props.mode !== "inline") {
|
|
452
590
|
document.body.style.overflow = v ? "hidden" : ""
|
|
453
591
|
}
|
|
592
|
+
|
|
593
|
+
if (typeof window === "undefined") return
|
|
594
|
+
if (v && props.mode === "inline") {
|
|
595
|
+
positionPopup()
|
|
596
|
+
window.addEventListener("resize", onPopupViewportChange)
|
|
597
|
+
window.addEventListener("scroll", onPopupViewportChange, true)
|
|
598
|
+
} else {
|
|
599
|
+
window.removeEventListener("resize", onPopupViewportChange)
|
|
600
|
+
window.removeEventListener("scroll", onPopupViewportChange, true)
|
|
601
|
+
}
|
|
454
602
|
})
|
|
455
603
|
|
|
456
604
|
onMounted(() => {
|
|
@@ -465,6 +613,10 @@ onBeforeUnmount(() => {
|
|
|
465
613
|
document.removeEventListener("keydown", onDocKeydown)
|
|
466
614
|
document.body.style.overflow = ""
|
|
467
615
|
}
|
|
616
|
+
if (typeof window !== "undefined") {
|
|
617
|
+
window.removeEventListener("resize", onPopupViewportChange)
|
|
618
|
+
window.removeEventListener("scroll", onPopupViewportChange, true)
|
|
619
|
+
}
|
|
468
620
|
})
|
|
469
621
|
|
|
470
622
|
// — Styles du panneau (props globales fusionnées avec les options du mode) —
|
|
@@ -602,8 +754,8 @@ const sheetClass = computed(() => [`q-select__sheet--${props.mode}`, modeOptions
|
|
|
602
754
|
v-if="open && mode === 'inline'"
|
|
603
755
|
ref="popupRef"
|
|
604
756
|
class="q-select__popup"
|
|
605
|
-
:class="inlineOptions?.class"
|
|
606
|
-
:style="inlineOptions?.style"
|
|
757
|
+
:class="[inlineOptions?.class, popupDirection === 'up' && 'q-select__popup--up']"
|
|
758
|
+
:style="[popupStyle, inlineOptions?.style]"
|
|
607
759
|
role="listbox"
|
|
608
760
|
@mousedown.prevent
|
|
609
761
|
>
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -264,6 +264,25 @@ html.dark body {
|
|
|
264
264
|
color: rgb(255 255 255 / 0.65);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/* Icônes de champ en dark : icône gauche + chevrons (select, autocomplete) +
|
|
268
|
+
loupe de recherche des listes déroulantes. Ces icônes ont une couleur NOIRE
|
|
269
|
+
codée en dur (`rgb(0 0 0 / …)` sur `.q-field__icon`, `.q-select__arrow`,
|
|
270
|
+
`.q-autocomplete__arrow`, `.q-select__search-icon`) : sans cette surcharge
|
|
271
|
+
elles restaient noires sur fond sombre → illisibles. */
|
|
272
|
+
.dark .q-field__icon,
|
|
273
|
+
.dark .q-select__arrow,
|
|
274
|
+
.dark .q-autocomplete__arrow,
|
|
275
|
+
.dark .q-select__search-icon,
|
|
276
|
+
.dark .q-autocomplete__search-icon {
|
|
277
|
+
color: var(--foreground);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* État vide des listes déroulantes en dark (texte noir codé en dur) */
|
|
281
|
+
.dark .q-select__empty,
|
|
282
|
+
.dark .q-autocomplete__empty {
|
|
283
|
+
color: rgb(255 255 255 / 0.65);
|
|
284
|
+
}
|
|
285
|
+
|
|
267
286
|
.dark .q-input .q-field__native::placeholder,
|
|
268
287
|
.dark .q-input-tag__native::placeholder,
|
|
269
288
|
.dark .q-input-otp__box::placeholder,
|
|
@@ -4709,7 +4728,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
4709
4728
|
|
|
4710
4729
|
.q-select__popup {
|
|
4711
4730
|
position: absolute;
|
|
4712
|
-
top: calc(100% +
|
|
4731
|
+
top: calc(100% + 8px);
|
|
4713
4732
|
left: 0;
|
|
4714
4733
|
right: 0;
|
|
4715
4734
|
z-index: 1000;
|
|
@@ -4723,6 +4742,24 @@ a.q-breadcrumbs__el:hover {
|
|
|
4723
4742
|
animation: q-popup-in 0.12s ease;
|
|
4724
4743
|
}
|
|
4725
4744
|
|
|
4745
|
+
/* Bascule au-dessus du champ (pas assez de place en dessous) : l'animation
|
|
4746
|
+
vient d'en bas. `top`/`bottom`/`max-height` sont posés en inline par le script
|
|
4747
|
+
(`positionPopup()`), selon l'espace visible. */
|
|
4748
|
+
.q-select__popup--up {
|
|
4749
|
+
animation-name: q-popup-in-up;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
@keyframes q-popup-in-up {
|
|
4753
|
+
from {
|
|
4754
|
+
opacity: 0;
|
|
4755
|
+
transform: translateY(4px);
|
|
4756
|
+
}
|
|
4757
|
+
to {
|
|
4758
|
+
opacity: 1;
|
|
4759
|
+
transform: none;
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4726
4763
|
/* — champ de recherche séparé (useSearch), affiché avant la liste — */
|
|
4727
4764
|
.q-select__search {
|
|
4728
4765
|
display: flex;
|