@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.
@@ -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"
@@ -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, ×) : on abandonne la
536
- // recherche saisie pour ne pas laisser un filtre fantôme au prochain affichage.
537
- if (query.value !== "" && !hasValue.value) {
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
  }
@@ -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
- // Recalcule la position de l'indicateur après chaque changement de DOM
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
- <div class="q-tabs__content" :class="contentClass">
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>
@@ -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
@@ -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
+ })
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
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: 14px 16px;
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
- padding: 14px 16px;
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 évite qu'il colle au bord haut. */
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
- padding-top: 0;
5737
- padding-top: constant(safe-area-inset-top);
5738
- padding-top: env(safe-area-inset-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: 14px 16px;
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-top: 0;
6215
- padding-top: constant(safe-area-inset-top);
6216
- padding-top: env(safe-area-inset-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: 14px 16px;
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 évite qu'il colle au bord haut. */
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
- padding-top: 0;
8150
- padding-top: constant(safe-area-inset-top);
8151
- padding-top: env(safe-area-inset-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,