@dnax/ui 0.0.62 → 0.0.64
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/QCollapse.vue +7 -1
- package/components/QSelect.vue +13 -4
- package/components/QTabs.vue +70 -2
- package/lib/select.test.ts +15 -1
- package/lib/select.ts +15 -0
- package/lib/styleTokens.test.ts +43 -0
- package/lib/tabs.test.ts +35 -0
- package/lib/tabs.ts +38 -0
- package/package.json +1 -1
- package/styles/main.css +109 -16
package/components/QCollapse.vue
CHANGED
|
@@ -23,6 +23,11 @@ interface Props {
|
|
|
23
23
|
iconRight?: string
|
|
24
24
|
/** Icône Iconify du chevron (défaut : lucide:chevron-down) */
|
|
25
25
|
expandIcon?: string
|
|
26
|
+
/**
|
|
27
|
+
* **Guide vertical** à gauche du contenu (« tirets longs »), aligné sous l'icône du header —
|
|
28
|
+
* pour une arborescence (groupe + enfants). Suit le thème (`--border`).
|
|
29
|
+
*/
|
|
30
|
+
guide?: boolean
|
|
26
31
|
headerClass?: string
|
|
27
32
|
/** Hauteur réduite */
|
|
28
33
|
dense?: boolean
|
|
@@ -32,6 +37,7 @@ interface Props {
|
|
|
32
37
|
|
|
33
38
|
const props = withDefaults(defineProps<Props>(), {
|
|
34
39
|
defaultOpened: false,
|
|
40
|
+
guide: false,
|
|
35
41
|
dense: false,
|
|
36
42
|
disable: false,
|
|
37
43
|
headerClass: "",
|
|
@@ -98,7 +104,7 @@ onMounted(measure)
|
|
|
98
104
|
</script>
|
|
99
105
|
|
|
100
106
|
<template>
|
|
101
|
-
<div class="q-collapse">
|
|
107
|
+
<div class="q-collapse" :class="{ 'q-collapse--guide': guide }">
|
|
102
108
|
<button
|
|
103
109
|
type="button"
|
|
104
110
|
class="q-collapse__header"
|
package/components/QSelect.vue
CHANGED
|
@@ -60,7 +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
|
+
import { optionsOf, selectionOf, shouldClearSearchOnClose } from "../lib/select"
|
|
64
64
|
|
|
65
65
|
interface Props {
|
|
66
66
|
/** Valeur sélectionnée : option, valeur (emit-value), ou tableau (multiple) */
|
|
@@ -112,6 +112,12 @@ interface Props {
|
|
|
112
112
|
useSearch?: boolean | UseSearchOptions
|
|
113
113
|
/** Placeholder du champ de recherche (useSearch) */
|
|
114
114
|
searchPlaceholder?: string
|
|
115
|
+
/**
|
|
116
|
+
* Efface la recherche saisie à la **fermeture sans sélection** (clic extérieur, backdrop,
|
|
117
|
+
* Échap, ×). **Désactivé par défaut** : la recherche est conservée et réaffichée à la
|
|
118
|
+
* réouverture. Même activé, elle n'est effacée qu'en l'absence de valeur sélectionnée.
|
|
119
|
+
*/
|
|
120
|
+
clearSearchOnClose?: boolean
|
|
115
121
|
/** Mode d'ouverture : inline (dropdown) | modal (centré) | sheet (bottom sheet) | dialog (plein écran) */
|
|
116
122
|
mode?: "inline" | "modal" | "sheet" | "dialog"
|
|
117
123
|
/** Largeur du panneau (modal/sheet) : "300px", "80%"… (par défaut : modal 480px / 90% si écran < 768px, sheet 640px) */
|
|
@@ -164,6 +170,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
164
170
|
mode: "inline",
|
|
165
171
|
offset: 8,
|
|
166
172
|
position: "auto",
|
|
173
|
+
// Comportement de "reset" de la recherche à la fermeture : **désactivé par défaut**
|
|
174
|
+
clearSearchOnClose: false,
|
|
167
175
|
})
|
|
168
176
|
|
|
169
177
|
const emit = defineEmits<{
|
|
@@ -532,9 +540,10 @@ const openPopup = () => {
|
|
|
532
540
|
|
|
533
541
|
const closePopup = () => {
|
|
534
542
|
open.value = false
|
|
535
|
-
// Fermeture sans sélection (clic extérieur, backdrop, Échap, ×) :
|
|
536
|
-
//
|
|
537
|
-
|
|
543
|
+
// Fermeture sans sélection (clic extérieur, backdrop, Échap, ×) : la recherche est
|
|
544
|
+
// **conservée par défaut** (opt-in via `clearSearchOnClose`) — le filtre ne "disparaît"
|
|
545
|
+
// donc plus tout seul à la réouverture (cf. `shouldClearSearchOnClose`).
|
|
546
|
+
if (shouldClearSearchOnClose(props.clearSearchOnClose, query.value, hasValue.value)) {
|
|
538
547
|
query.value = ""
|
|
539
548
|
emit("update:inputValue", "")
|
|
540
549
|
}
|
package/components/QTabs.vue
CHANGED
|
@@ -25,9 +25,12 @@ export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
|
|
|
25
25
|
|
|
26
26
|
<script setup lang="ts">
|
|
27
27
|
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
|
|
28
|
+
import { Icon } from "@iconify/vue"
|
|
28
29
|
import { cva } from "class-variance-authority"
|
|
29
30
|
import { cn } from "../lib/utils"
|
|
30
31
|
import { colorValue } from "../lib/colors"
|
|
32
|
+
import { icons } from "../lib/icons"
|
|
33
|
+
import { tabArrowState, tabArrowStep } from "../lib/tabs"
|
|
31
34
|
|
|
32
35
|
const tabsVariants = cva("q-tabs", {
|
|
33
36
|
variants: {
|
|
@@ -70,6 +73,12 @@ interface Props {
|
|
|
70
73
|
switchIndicatorPosition?: boolean | "top" | "bottom"
|
|
71
74
|
/** Tabs en colonne (verticales) */
|
|
72
75
|
vertical?: boolean
|
|
76
|
+
/**
|
|
77
|
+
* **Flèches de défilement** quand les tabs débordent (comportement Quasar, défaut `true`) :
|
|
78
|
+
* le contenu devient défilable et deux boutons apparaissent aux extrémités débordantes.
|
|
79
|
+
* `false` → aucun défilement (les tabs débordent, comme avant).
|
|
80
|
+
*/
|
|
81
|
+
showArrows?: boolean
|
|
73
82
|
/** Ne pas s'étendre (usage dans un QToolbar) */
|
|
74
83
|
shrink?: boolean
|
|
75
84
|
/** S'étendre sur l'axe croisé du parent flex */
|
|
@@ -106,6 +115,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
115
|
noCaps: false,
|
|
107
116
|
switchIndicatorPosition: undefined,
|
|
108
117
|
vertical: false,
|
|
118
|
+
showArrows: true,
|
|
109
119
|
shrink: false,
|
|
110
120
|
stretch: false,
|
|
111
121
|
contentClass: "",
|
|
@@ -148,10 +158,36 @@ const tabRegs = shallowRef<QTabRegistration[]>([])
|
|
|
148
158
|
const tick = ref(0)
|
|
149
159
|
const rootEl = ref<HTMLElement | null>(null)
|
|
150
160
|
|
|
151
|
-
//
|
|
161
|
+
// — Flèches de défilement (`show-arrows`, comportement Quasar) —
|
|
162
|
+
const contentEl = ref<HTMLElement | null>(null)
|
|
163
|
+
const canScrollStart = ref(false)
|
|
164
|
+
const canScrollEnd = ref(false)
|
|
165
|
+
let contentObserver: ResizeObserver | null = null
|
|
166
|
+
|
|
167
|
+
/** Met à jour la visibilité des flèches depuis les mesures de défilement (axe courant). */
|
|
168
|
+
const updateArrows = () => {
|
|
169
|
+
const el = contentEl.value
|
|
170
|
+
if (!el) return
|
|
171
|
+
const state = props.vertical
|
|
172
|
+
? tabArrowState(el.scrollTop, el.clientHeight, el.scrollHeight)
|
|
173
|
+
: tabArrowState(el.scrollLeft, el.clientWidth, el.scrollWidth)
|
|
174
|
+
canScrollStart.value = state.canStart
|
|
175
|
+
canScrollEnd.value = state.canEnd
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Fait défiler d'un cran (≈ 60 % de la zone visible) — fluide. */
|
|
179
|
+
const scrollArrows = (dir: -1 | 1) => {
|
|
180
|
+
const el = contentEl.value
|
|
181
|
+
if (!el) return
|
|
182
|
+
const delta = tabArrowStep(props.vertical ? el.clientHeight : el.clientWidth) * dir
|
|
183
|
+
el.scrollBy(props.vertical ? { top: delta, behavior: "smooth" } : { left: delta, behavior: "smooth" })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Recalcule la position de l'indicateur (et les flèches) après chaque changement de DOM
|
|
152
187
|
const measure = async () => {
|
|
153
188
|
await nextTick()
|
|
154
189
|
tick.value++
|
|
190
|
+
updateArrows()
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
// collapse-inactive : le label s'étend/retombe via une transition max-width
|
|
@@ -169,10 +205,18 @@ onMounted(() => {
|
|
|
169
205
|
measure()
|
|
170
206
|
if (typeof window !== "undefined") window.addEventListener("resize", measure)
|
|
171
207
|
rootEl.value?.addEventListener("transitionend", onCollapseEnd)
|
|
208
|
+
// Le conteneur peut changer de largeur sans resize fenêtre (sidebar, flex…) : les
|
|
209
|
+
// flèches doivent suivre le débordement réel.
|
|
210
|
+
if (typeof ResizeObserver !== "undefined" && contentEl.value) {
|
|
211
|
+
contentObserver = new ResizeObserver(updateArrows)
|
|
212
|
+
contentObserver.observe(contentEl.value)
|
|
213
|
+
}
|
|
172
214
|
})
|
|
173
215
|
onBeforeUnmount(() => {
|
|
174
216
|
if (typeof window !== "undefined") window.removeEventListener("resize", measure)
|
|
175
217
|
rootEl.value?.removeEventListener("transitionend", onCollapseEnd)
|
|
218
|
+
contentObserver?.disconnect()
|
|
219
|
+
contentObserver = null
|
|
176
220
|
})
|
|
177
221
|
|
|
178
222
|
const context: QTabContext = {
|
|
@@ -240,6 +284,7 @@ const tabsClasses = computed(() =>
|
|
|
240
284
|
props.animated && "q-tabs--animated",
|
|
241
285
|
props.animated && `q-tabs--anim-${props.transition}`,
|
|
242
286
|
props.collapseInactive && "q-tabs--collapse-inactive",
|
|
287
|
+
props.showArrows && "q-tabs--arrows",
|
|
243
288
|
),
|
|
244
289
|
)
|
|
245
290
|
</script>
|
|
@@ -253,9 +298,32 @@ const tabsClasses = computed(() =>
|
|
|
253
298
|
role="tablist"
|
|
254
299
|
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
|
|
255
300
|
>
|
|
256
|
-
<
|
|
301
|
+
<button
|
|
302
|
+
v-if="showArrows && canScrollStart"
|
|
303
|
+
type="button"
|
|
304
|
+
class="q-tabs__arrow q-tabs__arrow--start"
|
|
305
|
+
:aria-label="vertical ? 'Defiler les onglets vers le haut' : 'Defiler les onglets vers la gauche'"
|
|
306
|
+
@click="scrollArrows(-1)"
|
|
307
|
+
>
|
|
308
|
+
<Icon :icon="vertical ? icons.chevronUp : icons.chevronLeft" aria-hidden="true" />
|
|
309
|
+
</button>
|
|
310
|
+
<div
|
|
311
|
+
ref="contentEl"
|
|
312
|
+
class="q-tabs__content"
|
|
313
|
+
:class="contentClass"
|
|
314
|
+
@scroll="updateArrows"
|
|
315
|
+
>
|
|
257
316
|
<slot />
|
|
258
317
|
<div class="q-tabs__indicator" :style="indicatorStyle" aria-hidden="true" />
|
|
259
318
|
</div>
|
|
319
|
+
<button
|
|
320
|
+
v-if="showArrows && canScrollEnd"
|
|
321
|
+
type="button"
|
|
322
|
+
class="q-tabs__arrow q-tabs__arrow--end"
|
|
323
|
+
:aria-label="vertical ? 'Defiler les onglets vers le bas' : 'Defiler les onglets vers la droite'"
|
|
324
|
+
@click="scrollArrows(1)"
|
|
325
|
+
>
|
|
326
|
+
<Icon :icon="vertical ? icons.chevronDown : icons.chevronRight" aria-hidden="true" />
|
|
327
|
+
</button>
|
|
260
328
|
</div>
|
|
261
329
|
</template>
|
package/lib/select.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Régression : `multiple` + modèle scalaire (`false`) faisait jeter `list.map` et
|
|
3
3
|
// empêchait le popup entier de se rendre.
|
|
4
4
|
import { describe, expect, it } from "bun:test"
|
|
5
|
-
import { optionsOf, selectionOf } from "./select"
|
|
5
|
+
import { optionsOf, selectionOf, shouldClearSearchOnClose } from "./select"
|
|
6
6
|
|
|
7
7
|
describe("optionsOf", () => {
|
|
8
8
|
it("laisse passer un tableau (même vide)", () => {
|
|
@@ -40,3 +40,17 @@ describe("selectionOf", () => {
|
|
|
40
40
|
expect(selectionOf(undefined, false)).toEqual([])
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
|
+
|
|
44
|
+
describe("shouldClearSearchOnClose", () => {
|
|
45
|
+
it("désactivé (défaut) → la recherche est CONSERVÉE", () => {
|
|
46
|
+
expect(shouldClearSearchOnClose(false, "ada", false)).toBe(false)
|
|
47
|
+
expect(shouldClearSearchOnClose(false, "ada", true)).toBe(false)
|
|
48
|
+
expect(shouldClearSearchOnClose(false, "", false)).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("activé → efface seulement si une recherche est saisie et aucune valeur choisie", () => {
|
|
52
|
+
expect(shouldClearSearchOnClose(true, "ada", false)).toBe(true)
|
|
53
|
+
expect(shouldClearSearchOnClose(true, "", false)).toBe(false) // rien à effacer
|
|
54
|
+
expect(shouldClearSearchOnClose(true, "ada", true)).toBe(false) // valeur choisie → label
|
|
55
|
+
})
|
|
56
|
+
})
|
package/lib/select.ts
CHANGED
|
@@ -28,3 +28,18 @@ export const selectionOf = (value: unknown, multiple: boolean): any[] => {
|
|
|
28
28
|
if (multiple) return Array.isArray(value) ? value : []
|
|
29
29
|
return [value]
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* La recherche saisie doit-elle être **effacée** à la fermeture du panneau (clic extérieur,
|
|
34
|
+
* backdrop, Échap, ×) ?
|
|
35
|
+
*
|
|
36
|
+
* **Opt-in** (prop `clearSearchOnClose`, désactivée par défaut) : par défaut la recherche est
|
|
37
|
+
* **conservée** — un `inputValue` vidé à la fermeture n'est pas toujours souhaité (formulaire
|
|
38
|
+
* qui reprend la saisie, filtre persistant…). Même sous opt-in, on n'efface que s'il n'y a **pas**
|
|
39
|
+
* de valeur : sinon le champ d'affichage reprendrait le label et la recherche n'aurait plus cours.
|
|
40
|
+
*/
|
|
41
|
+
export const shouldClearSearchOnClose = (
|
|
42
|
+
enabled: boolean,
|
|
43
|
+
query: string,
|
|
44
|
+
hasValue: boolean,
|
|
45
|
+
): boolean => enabled && query !== "" && !hasValue
|
package/lib/styleTokens.test.ts
CHANGED
|
@@ -100,6 +100,8 @@ describe("classes de style (composant ↔ CSS)", () => {
|
|
|
100
100
|
"q-bottom-sheet__header-text--with-description",
|
|
101
101
|
".q-bottom-sheet__header-text--with-description .q-bottom-sheet__title",
|
|
102
102
|
],
|
|
103
|
+
["components/QCollapse.vue", "q-collapse--guide", ".q-collapse--guide .q-collapse__inner"],
|
|
104
|
+
["components/QTabs.vue", "q-tabs--arrows", ".q-tabs--arrows .q-tabs__content"],
|
|
103
105
|
]
|
|
104
106
|
|
|
105
107
|
for (const [file, className, selector] of CONTRACTS) {
|
|
@@ -109,6 +111,22 @@ describe("classes de style (composant ↔ CSS)", () => {
|
|
|
109
111
|
expect({ selector, inCss: css.includes(selector) }).toEqual({ selector, inCss: true })
|
|
110
112
|
})
|
|
111
113
|
}
|
|
114
|
+
|
|
115
|
+
it("une description ajoute aussi de l'air AVANT le séparateur (padding-bottom)", () => {
|
|
116
|
+
// Le séparateur est le `border-bottom` de l'en-tête : sans ce padding, le trait colle
|
|
117
|
+
// au texte de la description.
|
|
118
|
+
for (const selector of [
|
|
119
|
+
".q-dialog__header-text--with-description",
|
|
120
|
+
".q-bottom-sheet__header-text--with-description",
|
|
121
|
+
]) {
|
|
122
|
+
const block = blockAfter(css, selector)
|
|
123
|
+
expect({ selector, defined: block !== undefined }).toEqual({ selector, defined: true })
|
|
124
|
+
expect({ selector, padded: block!.includes("padding-bottom") }).toEqual({
|
|
125
|
+
selector,
|
|
126
|
+
padded: true,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
|
112
130
|
})
|
|
113
131
|
|
|
114
132
|
describe("mode sombre des contrôles", () => {
|
|
@@ -121,3 +139,28 @@ describe("mode sombre des contrôles", () => {
|
|
|
121
139
|
}
|
|
122
140
|
})
|
|
123
141
|
})
|
|
142
|
+
|
|
143
|
+
describe("en-têtes de panneau : safe-area", () => {
|
|
144
|
+
// La safe-area doit **s'ajouter** au padding de base, jamais le remplacer : sinon, sans
|
|
145
|
+
// encoche (`env()` = 0), le titre et le × collent au bord haut.
|
|
146
|
+
const PANEL_HEADERS = [
|
|
147
|
+
".q-select__sheet--dialog .q-select__sheet-header",
|
|
148
|
+
".q-date-picker__sheet--dialog .q-date-picker__sheet-header",
|
|
149
|
+
".q-country-picker__sheet--dialog .q-country-picker__sheet-header",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
for (const selector of PANEL_HEADERS) {
|
|
153
|
+
it(`« ${selector} » ajoute la safe-area à un padding de base`, () => {
|
|
154
|
+
const block = blockAfter(css, selector)
|
|
155
|
+
expect({ selector, defined: block !== undefined }).toEqual({ selector, defined: true })
|
|
156
|
+
expect({ selector, adds: block!.includes("calc(env(safe-area-inset-top) + 16px)") }).toEqual({
|
|
157
|
+
selector,
|
|
158
|
+
adds: true,
|
|
159
|
+
})
|
|
160
|
+
expect({ selector, bare: block!.includes("padding-top: env(safe-area-inset-top);") }).toEqual({
|
|
161
|
+
selector,
|
|
162
|
+
bare: false,
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
})
|
package/lib/tabs.test.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tests unitaires des flèches de défilement de QTabs (`lib/tabs.ts`).
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import { tabArrowState, tabArrowStep } from "./tabs"
|
|
4
|
+
|
|
5
|
+
describe("tabArrowState", () => {
|
|
6
|
+
it("pas de débordement → aucune flèche", () => {
|
|
7
|
+
expect(tabArrowState(0, 400, 400)).toEqual({ canStart: false, canEnd: false })
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("début → seule la flèche de fin", () => {
|
|
11
|
+
expect(tabArrowState(0, 400, 900)).toEqual({ canStart: false, canEnd: true })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("milieu → les deux flèches", () => {
|
|
15
|
+
expect(tabArrowState(200, 400, 900)).toEqual({ canStart: true, canEnd: true })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("fin → seule la flèche de début", () => {
|
|
19
|
+
expect(tabArrowState(500, 400, 900)).toEqual({ canStart: true, canEnd: false })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("tolère l'arrondi des sous-pixels (1 px)", () => {
|
|
23
|
+
// Collé au début malgré un `scrollLeft` de 1 (arrondi)
|
|
24
|
+
expect(tabArrowState(1, 400, 900)).toEqual({ canStart: false, canEnd: true })
|
|
25
|
+
// Collé à la fin malgré un `scrollLeft` à 1 px du max
|
|
26
|
+
expect(tabArrowState(499, 400, 900)).toEqual({ canStart: true, canEnd: false })
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe("tabArrowStep", () => {
|
|
31
|
+
it("avance d'environ 60 % de la zone visible, avec un plancher de 80 px", () => {
|
|
32
|
+
expect(tabArrowStep(400)).toBe(240)
|
|
33
|
+
expect(tabArrowStep(100)).toBe(80) // 60 < 80 → plancher
|
|
34
|
+
})
|
|
35
|
+
})
|
package/lib/tabs.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Flèches de défilement de QTabs (`show-arrows`, comportement Quasar) : décide si les boutons
|
|
2
|
+
// de début / fin doivent apparaître, selon la position de défilement et les mesures.
|
|
3
|
+
//
|
|
4
|
+
// Logique **pure**, testable hors navigateur. Les navigateurs arrondissent `scrollLeft` /
|
|
5
|
+
// `scrollTop` (sous-pixels, zoom) : on tolère 1 px pour ne pas faire clignoter une flèche.
|
|
6
|
+
|
|
7
|
+
const EPSILON = 1
|
|
8
|
+
|
|
9
|
+
export interface QTabArrowState {
|
|
10
|
+
/** On peut défiler vers le **début** (gauche / haut) */
|
|
11
|
+
canStart: boolean
|
|
12
|
+
/** On peut défiler vers la **fin** (droite / bas) */
|
|
13
|
+
canEnd: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* État des flèches pour un axe donné.
|
|
18
|
+
*
|
|
19
|
+
* @param position `scrollLeft` / `scrollTop`
|
|
20
|
+
* @param clientSize `clientWidth` / `clientHeight` (partie visible)
|
|
21
|
+
* @param scrollSize `scrollWidth` / `scrollHeight` (contenu total)
|
|
22
|
+
*/
|
|
23
|
+
export function tabArrowState(
|
|
24
|
+
position: number,
|
|
25
|
+
clientSize: number,
|
|
26
|
+
scrollSize: number,
|
|
27
|
+
): QTabArrowState {
|
|
28
|
+
const overflow = scrollSize - clientSize
|
|
29
|
+
return {
|
|
30
|
+
canStart: position > EPSILON,
|
|
31
|
+
canEnd: overflow > EPSILON && position < overflow - EPSILON,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Déplacement d'un clic sur une flèche : ~60 % de la zone visible (au moins 80 px). */
|
|
36
|
+
export function tabArrowStep(clientSize: number): number {
|
|
37
|
+
return Math.max(80, Math.round(clientSize * 0.6))
|
|
38
|
+
}
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -831,6 +831,62 @@ html.dark body {
|
|
|
831
831
|
align-items: stretch;
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
+
/* Flèches de défilement (`show-arrows`, comportement Quasar) : le contenu devient
|
|
835
|
+
défilable (barre de défilement masquée, le défilement est piloté par les flèches). */
|
|
836
|
+
.q-tabs--arrows .q-tabs__content {
|
|
837
|
+
overflow-x: auto;
|
|
838
|
+
overflow-y: hidden;
|
|
839
|
+
scrollbar-width: none;
|
|
840
|
+
-ms-overflow-style: none;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.q-tabs--arrows .q-tabs__content::-webkit-scrollbar {
|
|
844
|
+
display: none;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.q-tabs--vertical.q-tabs--arrows .q-tabs__content {
|
|
848
|
+
overflow-x: hidden;
|
|
849
|
+
overflow-y: auto;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.q-tabs__arrow {
|
|
853
|
+
flex: 0 0 auto;
|
|
854
|
+
display: inline-flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
justify-content: center;
|
|
857
|
+
width: 28px;
|
|
858
|
+
padding: 0;
|
|
859
|
+
border: none;
|
|
860
|
+
border-radius: 0;
|
|
861
|
+
background: transparent;
|
|
862
|
+
color: inherit;
|
|
863
|
+
opacity: 0.6;
|
|
864
|
+
cursor: pointer;
|
|
865
|
+
transition:
|
|
866
|
+
opacity 0.15s ease,
|
|
867
|
+
background-color 0.15s ease;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.q-tabs__arrow:hover {
|
|
871
|
+
opacity: 1;
|
|
872
|
+
background-color: rgb(0 0 0 / 0.04);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* Thème sombre : le hover noir 4 % est invisible (cf. convention des hovers sombres). */
|
|
876
|
+
.dark .q-tabs__arrow:hover {
|
|
877
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.q-tabs__arrow svg {
|
|
881
|
+
width: 16px;
|
|
882
|
+
height: 16px;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.q-tabs--vertical .q-tabs__arrow {
|
|
886
|
+
width: 100%;
|
|
887
|
+
height: 24px;
|
|
888
|
+
}
|
|
889
|
+
|
|
834
890
|
.q-tabs--dense {
|
|
835
891
|
--q-tabs-h: 32px;
|
|
836
892
|
}
|
|
@@ -841,10 +897,14 @@ html.dark body {
|
|
|
841
897
|
|
|
842
898
|
.q-tabs--center .q-tabs__content {
|
|
843
899
|
justify-content: center;
|
|
900
|
+
/* `safe` : en cas de débordement, retombe sur `flex-start` au lieu de rendre le
|
|
901
|
+
début inaccessible au défilement (bug classique du flex centré + overflow). */
|
|
902
|
+
justify-content: safe center;
|
|
844
903
|
}
|
|
845
904
|
|
|
846
905
|
.q-tabs--right .q-tabs__content {
|
|
847
906
|
justify-content: flex-end;
|
|
907
|
+
justify-content: safe flex-end;
|
|
848
908
|
}
|
|
849
909
|
|
|
850
910
|
.q-tabs--justify .q-tab {
|
|
@@ -3609,6 +3669,15 @@ html.dark body {
|
|
|
3609
3669
|
padding: 0 16px;
|
|
3610
3670
|
}
|
|
3611
3671
|
|
|
3672
|
+
/* Guide vertical (« tirets longs ») à gauche du contenu : trait aligné sous l'**icône** du
|
|
3673
|
+
header (padding 16px + icône 18px / 2) et contenu décalé d'autant. Suit le thème via
|
|
3674
|
+
`--border` (invisible nulle part : clair comme sombre). */
|
|
3675
|
+
.q-collapse--guide .q-collapse__inner {
|
|
3676
|
+
margin-left: 25px;
|
|
3677
|
+
padding-left: 12px;
|
|
3678
|
+
border-left: 1px solid var(--border);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3612
3681
|
/* ===== QCheckbox / QRadio ===== */
|
|
3613
3682
|
.q-checkbox,
|
|
3614
3683
|
.q-radio {
|
|
@@ -3855,7 +3924,7 @@ html.dark body {
|
|
|
3855
3924
|
align-items: center;
|
|
3856
3925
|
justify-content: space-between;
|
|
3857
3926
|
gap: 8px;
|
|
3858
|
-
padding:
|
|
3927
|
+
padding: 16px 16px 14px;
|
|
3859
3928
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
3860
3929
|
font-weight: 600;
|
|
3861
3930
|
font-size: 15px;
|
|
@@ -5107,7 +5176,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
5107
5176
|
align-items: center;
|
|
5108
5177
|
justify-content: space-between;
|
|
5109
5178
|
gap: 8px;
|
|
5110
|
-
|
|
5179
|
+
/* Un peu plus d'air en haut (titre + bouton de fermeture) : le panneau a un coin
|
|
5180
|
+
arrondi, 14px des deux côtés collait le titre au bord. Bas inchangé. */
|
|
5181
|
+
padding: 16px 16px 14px;
|
|
5111
5182
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5112
5183
|
font-weight: 600;
|
|
5113
5184
|
font-size: 15px;
|
|
@@ -5344,7 +5415,12 @@ a.q-breadcrumbs__el:hover {
|
|
|
5344
5415
|
}
|
|
5345
5416
|
|
|
5346
5417
|
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
5347
|
-
un peu d'air au-dessus du titre
|
|
5418
|
+
un peu d'air au-dessus du titre, **et un peu avant le séparateur** (sinon le trait colle
|
|
5419
|
+
au texte de la description). */
|
|
5420
|
+
.q-bottom-sheet__header-text--with-description {
|
|
5421
|
+
padding-bottom: 6px;
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5348
5424
|
.q-bottom-sheet__header-text--with-description .q-bottom-sheet__title {
|
|
5349
5425
|
padding-top: 4px;
|
|
5350
5426
|
}
|
|
@@ -5407,10 +5483,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
5407
5483
|
}
|
|
5408
5484
|
|
|
5409
5485
|
.q-bottom-sheet__panel--dark .q-bottom-sheet__header,
|
|
5410
|
-
.q-bottom-sheet__panel--dark .q-bottom-sheet__footer
|
|
5486
|
+
.q-bottom-sheet__panel--dark .q-bottom-sheet__footer,
|
|
5487
|
+
/* Thème sombre : le noir 8 % codé en dur est invisible (même classe que le dialog). */
|
|
5488
|
+
.dark .q-bottom-sheet__header,
|
|
5489
|
+
.dark .q-bottom-sheet__footer {
|
|
5411
5490
|
border-color: rgb(255 255 255 / 0.12);
|
|
5412
5491
|
}
|
|
5413
5492
|
|
|
5493
|
+
.dark .q-bottom-sheet__close:hover {
|
|
5494
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5414
5497
|
/* — transitions (slide vertical type vaul) — durées configurables via
|
|
5415
5498
|
transition-duration (vars --q-bs-duration-enter/leave) */
|
|
5416
5499
|
.q-bs-enter-active,
|
|
@@ -5733,9 +5816,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
5733
5816
|
}
|
|
5734
5817
|
|
|
5735
5818
|
.q-date-picker__sheet--dialog .q-date-picker__sheet-header {
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
padding-top:
|
|
5819
|
+
/* La safe-area **s'ajoute** au padding de base — sinon le titre et le × collent au bord
|
|
5820
|
+
haut (0 px) sur tout appareil sans encoche (`env()` vaut 0). */
|
|
5821
|
+
padding-top: 16px;
|
|
5822
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
5823
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
5739
5824
|
}
|
|
5740
5825
|
|
|
5741
5826
|
.q-date-picker__sheet-header {
|
|
@@ -5743,7 +5828,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
5743
5828
|
align-items: center;
|
|
5744
5829
|
justify-content: space-between;
|
|
5745
5830
|
gap: 8px;
|
|
5746
|
-
padding:
|
|
5831
|
+
padding: 16px 16px 14px;
|
|
5747
5832
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5748
5833
|
font-weight: 600;
|
|
5749
5834
|
font-size: 15px;
|
|
@@ -6211,16 +6296,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
6211
6296
|
box-shadow: none;
|
|
6212
6297
|
}
|
|
6213
6298
|
.q-country-picker__sheet--dialog .q-country-picker__sheet-header {
|
|
6214
|
-
padding-
|
|
6215
|
-
padding-top:
|
|
6216
|
-
padding-top:
|
|
6299
|
+
/* Safe-area **ajoutée** au padding de base (cf. date-picker). */
|
|
6300
|
+
padding-top: 16px;
|
|
6301
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
6302
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
6217
6303
|
}
|
|
6218
6304
|
.q-country-picker__sheet-header {
|
|
6219
6305
|
display: flex;
|
|
6220
6306
|
align-items: center;
|
|
6221
6307
|
justify-content: space-between;
|
|
6222
6308
|
gap: 8px;
|
|
6223
|
-
padding:
|
|
6309
|
+
padding: 16px 16px 14px;
|
|
6224
6310
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
6225
6311
|
font-weight: 600;
|
|
6226
6312
|
font-size: 15px;
|
|
@@ -6972,7 +7058,12 @@ a.q-breadcrumbs__el:hover {
|
|
|
6972
7058
|
}
|
|
6973
7059
|
|
|
6974
7060
|
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
6975
|
-
un peu d'air au-dessus du titre
|
|
7061
|
+
un peu d'air au-dessus du titre, **et un peu avant le séparateur** (sinon le trait colle
|
|
7062
|
+
au texte de la description). */
|
|
7063
|
+
.q-dialog__header-text--with-description {
|
|
7064
|
+
padding-bottom: 6px;
|
|
7065
|
+
}
|
|
7066
|
+
|
|
6976
7067
|
.q-dialog__header-text--with-description .q-dialog__title {
|
|
6977
7068
|
padding-top: 4px;
|
|
6978
7069
|
}
|
|
@@ -8146,9 +8237,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
8146
8237
|
En mode sheet avec recherche en bas, c'est le champ qui gère l'inset ;
|
|
8147
8238
|
sinon (liste seule) c'est la liste. */
|
|
8148
8239
|
.q-select__sheet--dialog .q-select__sheet-header {
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
padding-top:
|
|
8240
|
+
/* Safe-area **ajoutée** au padding de base — en mode plein écran le titre et le ×
|
|
8241
|
+
sont au bord haut ; sans encoche `env()` vaut 0, d'où le padding de repli. */
|
|
8242
|
+
padding-top: 16px;
|
|
8243
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
8244
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
8152
8245
|
}
|
|
8153
8246
|
|
|
8154
8247
|
.q-select__sheet--dialog .q-select__sheet-list,
|