@dnax/ui 0.0.61 → 0.0.63
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/QBottomSheetHeader.vue +12 -1
- package/components/QCollapse.vue +7 -1
- package/components/QDialogHeader.vue +12 -1
- package/components/QFooter.vue +26 -5
- package/components/QHeader.vue +5 -3
- package/components/QSelect.vue +13 -4
- package/lib/layout.test.ts +26 -1
- package/lib/layout.ts +21 -0
- package/lib/select.test.ts +15 -1
- package/lib/select.ts +15 -0
- package/lib/styleTokens.test.ts +165 -0
- package/package.json +1 -1
- package/styles/main.css +105 -29
|
@@ -56,6 +56,14 @@ const hasCustomContent = computed(() => {
|
|
|
56
56
|
return slotHasContent(slots.default?.() ?? [])
|
|
57
57
|
})
|
|
58
58
|
const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
59
|
+
|
|
60
|
+
// Une **description** (prop ou slot) rend le bloc titre+description plus haut : la toolbar
|
|
61
|
+
// (min-height 50px, `align-items: center`) collait le titre au bord haut. Le titre reçoit
|
|
62
|
+
// alors un peu de `padding-top` (cf. `.q-bottom-sheet__header-text--with-description`).
|
|
63
|
+
const hasDescription = computed(() => {
|
|
64
|
+
void refresh.value
|
|
65
|
+
return !!props.description || slotHasContent(slots.description?.() ?? [])
|
|
66
|
+
})
|
|
59
67
|
</script>
|
|
60
68
|
|
|
61
69
|
<template>
|
|
@@ -67,7 +75,10 @@ const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
|
67
75
|
<slot v-if="useCustom" />
|
|
68
76
|
|
|
69
77
|
<q-toolbar v-else>
|
|
70
|
-
<div
|
|
78
|
+
<div
|
|
79
|
+
class="q-bottom-sheet__header-text"
|
|
80
|
+
:class="{ 'q-bottom-sheet__header-text--with-description': hasDescription }"
|
|
81
|
+
>
|
|
71
82
|
<h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
|
|
72
83
|
<slot name="title" />
|
|
73
84
|
<p v-if="description" class="q-bottom-sheet__description">{{ description }}</p>
|
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"
|
|
@@ -64,6 +64,14 @@ const hasCustomContent = computed(() => {
|
|
|
64
64
|
return slotHasContent(slots.default?.() ?? [])
|
|
65
65
|
})
|
|
66
66
|
const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
67
|
+
|
|
68
|
+
// Une **description** (prop ou slot) rend le bloc titre+description plus haut : la toolbar
|
|
69
|
+
// (min-height 50px, `align-items: center`) collait le titre au bord haut. Le titre reçoit
|
|
70
|
+
// alors un peu de `padding-top` (cf. `.q-dialog__header-text--with-description`).
|
|
71
|
+
const hasDescription = computed(() => {
|
|
72
|
+
void refresh.value
|
|
73
|
+
return !!props.description || slotHasContent(slots.description?.() ?? [])
|
|
74
|
+
})
|
|
67
75
|
</script>
|
|
68
76
|
|
|
69
77
|
<template>
|
|
@@ -77,7 +85,10 @@ const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
|
77
85
|
|
|
78
86
|
<!-- Mode standard : toolbar embarqué (title / description / close) -->
|
|
79
87
|
<q-toolbar v-else>
|
|
80
|
-
<div
|
|
88
|
+
<div
|
|
89
|
+
class="q-dialog__header-text"
|
|
90
|
+
:class="{ 'q-dialog__header-text--with-description': hasDescription }"
|
|
91
|
+
>
|
|
81
92
|
<h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
|
|
82
93
|
<slot name="title" />
|
|
83
94
|
<p v-if="description" class="q-dialog__description">{{ description }}</p>
|
package/components/QFooter.vue
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QFooter — barre basse (safe-area bottom appliquée par styles/main.css : .q-app .q-footer)
|
|
3
|
-
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue"
|
|
3
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue"
|
|
4
4
|
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
5
|
-
import { qLayoutKey } from "../lib/layout"
|
|
5
|
+
import { barPin, qLayoutKey } from "../lib/layout"
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
/** Fixe la barre en bas de l'écran (sort du flux) */
|
|
9
9
|
fixed?: boolean
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Colle la barre **en fin de page** (rangée basse du grid). Dans le modèle scroll-document,
|
|
12
|
+
* la footer étant la **dernière rangée**, `F`/`f` sont **équivalents** et ce `sticky` n'a
|
|
13
|
+
* **pas d'effet visible**. Pour **épingler** la barre à l'écran, utiliser `fixed`.
|
|
14
|
+
*/
|
|
11
15
|
sticky?: boolean
|
|
12
16
|
/** Ombre portée vers le haut */
|
|
13
17
|
elevated?: boolean
|
|
@@ -38,11 +42,28 @@ const rootEl = ref<HTMLElement | null>(null)
|
|
|
38
42
|
// par la casse de la lettre (F).
|
|
39
43
|
const layout = inject(qLayoutKey, null)
|
|
40
44
|
const zone = computed(() => layout?.zones.value.footer)
|
|
41
|
-
|
|
45
|
+
// Épinglage vs collage : contrat centralisé dans `barPin` (la lettre `F` ne fait que « coller »,
|
|
46
|
+
// sans effet visible sur la dernière rangée).
|
|
47
|
+
const pin = computed(() => barPin(props, !!zone.value?.fixed))
|
|
48
|
+
const isSticky = computed(() => pin.value.sticky)
|
|
42
49
|
const layoutStyle = computed<Record<string, string | undefined>>(() => ({
|
|
43
50
|
gridArea: zone.value?.area,
|
|
44
51
|
}))
|
|
45
52
|
|
|
53
|
+
// Un `view` qui contient `F` (majuscule) laisse croire à un épinglage : la classe sticky est
|
|
54
|
+
// posée mais reste **sans effet visible**. On le dit une fois plutôt que de le faire taire.
|
|
55
|
+
let warnedNoopSticky = false
|
|
56
|
+
watchEffect(() => {
|
|
57
|
+
if (warnedNoopSticky) return
|
|
58
|
+
if (zone.value?.fixed && !props.fixed) {
|
|
59
|
+
warnedNoopSticky = true
|
|
60
|
+
console.warn(
|
|
61
|
+
'[q-footer] `view` "…F…" rend la footer *sticky*, sans effet visible ' +
|
|
62
|
+
'(dernière rangée du grid + scroll document). Pour épingler : <q-footer fixed>.',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
46
67
|
// Empilement : bottom = hauteur cumulée des footers fixed suivants (si fixed)
|
|
47
68
|
useFixedBarOffset(rootEl, "bar-bottom", () => props.fixed)
|
|
48
69
|
|
|
@@ -80,7 +101,7 @@ onBeforeUnmount(() => {
|
|
|
80
101
|
ref="rootEl"
|
|
81
102
|
class="q-footer"
|
|
82
103
|
:class="{
|
|
83
|
-
'q-footer--fixed': fixed,
|
|
104
|
+
'q-footer--fixed': pin.fixed,
|
|
84
105
|
'q-footer--sticky': isSticky,
|
|
85
106
|
'q-footer--elevated': props.elevated,
|
|
86
107
|
'q-footer--bordered': props.bordered,
|
package/components/QHeader.vue
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// barre se décale sous la précédente automatiquement.
|
|
6
6
|
import { computed, inject, ref } from "vue"
|
|
7
7
|
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
8
|
-
import { qLayoutKey } from "../lib/layout"
|
|
8
|
+
import { barPin, qLayoutKey } from "../lib/layout"
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
/** Fixe la barre en haut de l'écran (sort du flux) */
|
|
@@ -29,7 +29,9 @@ const rootEl = ref<HTMLElement | null>(null)
|
|
|
29
29
|
// par la casse de la lettre (H) — la barre reste alors close dans sa cellule.
|
|
30
30
|
const layout = inject(qLayoutKey, null)
|
|
31
31
|
const zone = computed(() => layout?.zones.value.header)
|
|
32
|
-
|
|
32
|
+
// Même contrat que la footer : seule la prop `fixed` épingle ; la casse `H`/`F` « colle ».
|
|
33
|
+
const pin = computed(() => barPin(props, !!zone.value?.fixed))
|
|
34
|
+
const isSticky = computed(() => pin.value.sticky)
|
|
33
35
|
const layoutStyle = computed<Record<string, string | undefined>>(() => ({
|
|
34
36
|
gridArea: zone.value?.area,
|
|
35
37
|
}))
|
|
@@ -49,7 +51,7 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
49
51
|
ref="rootEl"
|
|
50
52
|
class="q-header"
|
|
51
53
|
:class="{
|
|
52
|
-
'q-header--fixed': fixed,
|
|
54
|
+
'q-header--fixed': pin.fixed,
|
|
53
55
|
'q-header--sticky': isSticky,
|
|
54
56
|
'q-header--translucent': translucent === true || typeof translucent === 'number',
|
|
55
57
|
'q-header--glass': glass,
|
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/lib/layout.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Tests unitaires de l'analyse du prop `view` de QLayout (matrice 3×3).
|
|
2
2
|
import { describe, expect, it } from "bun:test"
|
|
3
|
-
import { DEFAULT_LAYOUT_VIEW, parseView } from "./layout"
|
|
3
|
+
import { barPin, DEFAULT_LAYOUT_VIEW, parseView } from "./layout"
|
|
4
4
|
|
|
5
5
|
describe("parseView", () => {
|
|
6
6
|
it("applique la vue par défaut sans argument", () => {
|
|
@@ -123,3 +123,28 @@ describe("parseView", () => {
|
|
|
123
123
|
expect(v.gridTemplateAreas).toBe('"h h h" "l p r" "f f f"')
|
|
124
124
|
})
|
|
125
125
|
})
|
|
126
|
+
|
|
127
|
+
describe("barPin — contrat header / footer", () => {
|
|
128
|
+
it("seule la prop `fixed` épingle (position: fixed)", () => {
|
|
129
|
+
expect(barPin({}, false)).toEqual({ fixed: false, sticky: false })
|
|
130
|
+
expect(barPin({ fixed: true }, false)).toEqual({ fixed: true, sticky: false })
|
|
131
|
+
expect(barPin({ fixed: false }, true)).toEqual({ fixed: false, sticky: true })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("la prop `sticky` colle (position: sticky)", () => {
|
|
135
|
+
expect(barPin({ sticky: true }, false)).toEqual({ fixed: false, sticky: true })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("la casse F/H de la zone colle, mais n'épingle JAMAIS", () => {
|
|
139
|
+
// `view="hHh LpR fFf"` : la zone footer est « fixed » au sens du parsing…
|
|
140
|
+
const zoneFixed = parseView("hHh LpR fFf").zones.footer.fixed
|
|
141
|
+
expect(zoneFixed).toBe(true)
|
|
142
|
+
// …et pourtant la barre ne doit PAS devenir `position: fixed` : seulement `sticky`
|
|
143
|
+
// (sans effet visible sur la dernière rangée du grid).
|
|
144
|
+
expect(barPin({}, zoneFixed)).toEqual({ fixed: false, sticky: true })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("`fixed` l'emporte : épinglé et collé sont indépendants", () => {
|
|
148
|
+
expect(barPin({ fixed: true, sticky: true }, true)).toEqual({ fixed: true, sticky: true })
|
|
149
|
+
})
|
|
150
|
+
})
|
package/lib/layout.ts
CHANGED
|
@@ -179,3 +179,24 @@ export function parseView(view?: string): LayoutView {
|
|
|
179
179
|
}
|
|
180
180
|
return parsed
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Position d'une **barre** (`q-header` / `q-footer`) dans le modèle scroll-document :
|
|
185
|
+
* - **`fixed`** — *épinglée* (`position: fixed`), hors flux : **décidée par la seule prop
|
|
186
|
+
* `fixed`** ;
|
|
187
|
+
* - **`sticky`** — *collée* (`position: sticky`) : la prop `sticky` **ou** la casse `F`/`H`
|
|
188
|
+
* de la zone.
|
|
189
|
+
*
|
|
190
|
+
* ⚠️ **Contrat (ne pas inverser)** : pour la **footer**, la lettre `F` produit bien
|
|
191
|
+
* `sticky`, mais `position: sticky; bottom: 0` sur la **dernière rangée** du grid est
|
|
192
|
+
* **sans effet visible** (la barre tombe déjà en fin de page) — contrairement à `H` sur le
|
|
193
|
+
* header, qui reste collé en haut. Un `F` **minuscule ou majuscule** est donc équivalent.
|
|
194
|
+
* `zoneFixed` ne doit **jamais** être traduit en `fixed` : ce serait un changement de
|
|
195
|
+
* sémantique *breaking* (le contenu passerait sous la barre). Seul `fixed` épingle.
|
|
196
|
+
*/
|
|
197
|
+
export function barPin(
|
|
198
|
+
props: { fixed?: boolean; sticky?: boolean },
|
|
199
|
+
zoneFixed: boolean,
|
|
200
|
+
): { fixed: boolean; sticky: boolean } {
|
|
201
|
+
return { fixed: props.fixed === true, sticky: props.sticky === true || zoneFixed }
|
|
202
|
+
}
|
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
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Contrat des **styles** (`styles/main.css` et les composants) :
|
|
2
|
+
// 1. tokens de thème (couche `@layer dnax-tokens`) — voir plus bas ;
|
|
3
|
+
// 2. classes de style **partagées** entre un composant et la feuille : une divergence des deux
|
|
4
|
+
// côtés (classe renommée d'un seul côté) est un échec **silencieux** — aucune erreur, juste
|
|
5
|
+
// un style qui ne s'applique plus.
|
|
6
|
+
//
|
|
7
|
+
// Test de contenu de fichier : la seule vérification possible sans navigateur (règle projet).
|
|
8
|
+
import { describe, expect, it } from "bun:test"
|
|
9
|
+
|
|
10
|
+
const css = await Bun.file(new URL("../styles/main.css", import.meta.url)).text()
|
|
11
|
+
|
|
12
|
+
/** Contenu d'un bloc `selector { … }` (accolades équilibrées), à partir de `from`. */
|
|
13
|
+
function blockAfter(source: string, selector: string, from = 0): string | undefined {
|
|
14
|
+
const at = source.indexOf(selector, from)
|
|
15
|
+
if (at === -1) return undefined
|
|
16
|
+
const open = source.indexOf("{", at)
|
|
17
|
+
if (open === -1) return undefined
|
|
18
|
+
let depth = 0
|
|
19
|
+
for (let i = open; i < source.length; i++) {
|
|
20
|
+
if (source[i] === "{") depth++
|
|
21
|
+
else if (source[i] === "}") {
|
|
22
|
+
depth--
|
|
23
|
+
if (depth === 0) return source.slice(open + 1, i)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Propriétés personnalisées (`--x:`) déclarées dans un bloc. */
|
|
30
|
+
function tokensOf(block: string): Set<string> {
|
|
31
|
+
const out = new Set<string>()
|
|
32
|
+
for (const m of block.matchAll(/(--[\w-]+)\s*:/g)) out.add(m[1]!)
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tokensLayerAt = css.indexOf("@layer dnax-tokens")
|
|
37
|
+
const lightBlock = blockAfter(css, ":root", tokensLayerAt)
|
|
38
|
+
const darkBlock = blockAfter(css, ".dark", tokensLayerAt)
|
|
39
|
+
|
|
40
|
+
/** Tokens sémantiques (surfaces + texte) — doivent exister dans les deux thèmes. */
|
|
41
|
+
const SEMANTIC = ["--background", "--foreground", "--card", "--muted", "--muted-foreground", "--border"]
|
|
42
|
+
|
|
43
|
+
describe("tokens de thème (clair / sombre)", () => {
|
|
44
|
+
it("la couche @layer dnax-tokens contient les deux thèmes", () => {
|
|
45
|
+
expect(tokensLayerAt).toBeGreaterThan(-1)
|
|
46
|
+
expect(lightBlock).toBeDefined()
|
|
47
|
+
expect(darkBlock).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("clair et sombre définissent les mêmes tokens sémantiques", () => {
|
|
51
|
+
const light = tokensOf(lightBlock!)
|
|
52
|
+
const dark = tokensOf(darkBlock!)
|
|
53
|
+
for (const token of SEMANTIC) {
|
|
54
|
+
expect({ token, inLight: light.has(token) }).toEqual({ token, inLight: true })
|
|
55
|
+
expect({ token, inDark: dark.has(token) }).toEqual({ token, inDark: true })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("tout `var(--muted-foreground…)` a un token défini (pas seulement un fallback)", () => {
|
|
60
|
+
// Le token est référencé ailleurs dans la feuille : il doit exister en clair ET en sombre.
|
|
61
|
+
expect(css.includes("var(--muted-foreground")).toBe(true)
|
|
62
|
+
expect(tokensOf(lightBlock!)).toContain("--muted-foreground")
|
|
63
|
+
expect(tokensOf(darkBlock!)).toContain("--muted-foreground")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("les descriptions de dialog et de bottom sheet lisent le token (plus de noir en dur)", () => {
|
|
67
|
+
for (const selector of [".q-dialog__description", ".q-bottom-sheet__description"]) {
|
|
68
|
+
const block = blockAfter(css, selector)
|
|
69
|
+
expect({ selector, defined: block !== undefined }).toEqual({ selector, defined: true })
|
|
70
|
+
expect({ selector, usesToken: block!.includes("var(--muted-foreground") }).toEqual({
|
|
71
|
+
selector,
|
|
72
|
+
usesToken: true,
|
|
73
|
+
})
|
|
74
|
+
expect({ selector, hardcodedBlack: block!.includes("color: rgb(0 0 0 / 0.6)") }).toEqual({
|
|
75
|
+
selector,
|
|
76
|
+
hardcodedBlack: false,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("plus aucune couleur de texte secondaire en noir codé en dur", () => {
|
|
82
|
+
// Le token clair vaut exactement `rgb(0 0 0 / 0.6)` → la migration est transparente en clair
|
|
83
|
+
// et corrige le sombre. On interdit donc la forme littérale (le fallback du token reste permis :
|
|
84
|
+
// il s'écrit `var(--muted-foreground, rgb(0 0 0 / 0.6))`).
|
|
85
|
+
// ⚠️ Lookbehind : `background-color: …` **contient** la sous-chaîne `color: …`.
|
|
86
|
+
expect(/(?<!-)color: rgb\(0 0 0 \/ 0\.6\)/.test(css)).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("classes de style (composant ↔ CSS)", () => {
|
|
91
|
+
/** `[fichier du composant, classe, sélecteur CSS attendu]` */
|
|
92
|
+
const CONTRACTS: [string, string, string][] = [
|
|
93
|
+
[
|
|
94
|
+
"components/QDialogHeader.vue",
|
|
95
|
+
"q-dialog__header-text--with-description",
|
|
96
|
+
".q-dialog__header-text--with-description .q-dialog__title",
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"components/QBottomSheetHeader.vue",
|
|
100
|
+
"q-bottom-sheet__header-text--with-description",
|
|
101
|
+
".q-bottom-sheet__header-text--with-description .q-bottom-sheet__title",
|
|
102
|
+
],
|
|
103
|
+
["components/QCollapse.vue", "q-collapse--guide", ".q-collapse--guide .q-collapse__inner"],
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for (const [file, className, selector] of CONTRACTS) {
|
|
107
|
+
it(`« ${className} » est posée par le composant ET stylée par le CSS`, async () => {
|
|
108
|
+
const vue = await Bun.file(new URL(`../${file}`, import.meta.url)).text()
|
|
109
|
+
expect({ file, inComponent: vue.includes(className) }).toEqual({ file, inComponent: true })
|
|
110
|
+
expect({ selector, inCss: css.includes(selector) }).toEqual({ selector, inCss: true })
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it("une description ajoute aussi de l'air AVANT le séparateur (padding-bottom)", () => {
|
|
115
|
+
// Le séparateur est le `border-bottom` de l'en-tête : sans ce padding, le trait colle
|
|
116
|
+
// au texte de la description.
|
|
117
|
+
for (const selector of [
|
|
118
|
+
".q-dialog__header-text--with-description",
|
|
119
|
+
".q-bottom-sheet__header-text--with-description",
|
|
120
|
+
]) {
|
|
121
|
+
const block = blockAfter(css, selector)
|
|
122
|
+
expect({ selector, defined: block !== undefined }).toEqual({ selector, defined: true })
|
|
123
|
+
expect({ selector, padded: block!.includes("padding-bottom") }).toEqual({
|
|
124
|
+
selector,
|
|
125
|
+
padded: true,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("mode sombre des contrôles", () => {
|
|
132
|
+
it("la bordure noire du checkbox / radio décoché est corrigée en sombre", () => {
|
|
133
|
+
// Base claire : bordure codée en dur (le variant `--dark` du composant la corrige, mais
|
|
134
|
+
// il faut aussi le **mode sombre du thème**) — sinon le contrôle est invisible en dark.
|
|
135
|
+
expect(css.includes("border: 2px solid rgb(0 0 0 / 0.45)")).toBe(true)
|
|
136
|
+
for (const selector of [".dark .q-checkbox__box", ".dark .q-radio__circle"]) {
|
|
137
|
+
expect({ selector, present: css.includes(selector) }).toEqual({ selector, present: true })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe("en-têtes de panneau : safe-area", () => {
|
|
143
|
+
// La safe-area doit **s'ajouter** au padding de base, jamais le remplacer : sinon, sans
|
|
144
|
+
// encoche (`env()` = 0), le titre et le × collent au bord haut.
|
|
145
|
+
const PANEL_HEADERS = [
|
|
146
|
+
".q-select__sheet--dialog .q-select__sheet-header",
|
|
147
|
+
".q-date-picker__sheet--dialog .q-date-picker__sheet-header",
|
|
148
|
+
".q-country-picker__sheet--dialog .q-country-picker__sheet-header",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
for (const selector of PANEL_HEADERS) {
|
|
152
|
+
it(`« ${selector} » ajoute la safe-area à un padding de base`, () => {
|
|
153
|
+
const block = blockAfter(css, selector)
|
|
154
|
+
expect({ selector, defined: block !== undefined }).toEqual({ selector, defined: true })
|
|
155
|
+
expect({ selector, adds: block!.includes("calc(env(safe-area-inset-top) + 16px)") }).toEqual({
|
|
156
|
+
selector,
|
|
157
|
+
adds: true,
|
|
158
|
+
})
|
|
159
|
+
expect({ selector, bare: block!.includes("padding-top: env(safe-area-inset-top);") }).toEqual({
|
|
160
|
+
selector,
|
|
161
|
+
bare: false,
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
})
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -155,14 +155,18 @@ body {
|
|
|
155
155
|
|
|
156
156
|
/* Fallback sans mesure JS (SSR, barres pas encore observées) : 50px + safe-area.
|
|
157
157
|
Écrit dans la **variable** d'offset — et non en padding — pour que la hauteur
|
|
158
|
-
mesurée (style inline, lib/fixedLayout.ts) la remplace dès l'hydratation.
|
|
159
|
-
.q-app
|
|
158
|
+
mesurée (style inline, lib/fixedLayout.ts) la remplace dès l'hydratation.
|
|
159
|
+
`:is(.q-app, .q-config-provider, .q-layout)` couvre les trois racines possibles :
|
|
160
|
+
l'installation **recommandée** (QConfigProvider, avec ou sans QLayout) n'utilise pas
|
|
161
|
+
forcément `.q-app`. Le chemin **mesuré** (`fixedLayout.ts`) retombe déjà sur
|
|
162
|
+
`.q-app` sinon le parent direct — seul ce fallback CSS était trop restrictif. */
|
|
163
|
+
:is(.q-app, .q-config-provider, .q-layout):has(.q-header--fixed) :is(.q-page, .q-page-container) {
|
|
160
164
|
--q-page-offset: 50px;
|
|
161
165
|
--q-page-offset: calc(constant(safe-area-inset-top) + 50px);
|
|
162
166
|
--q-page-offset: calc(env(safe-area-inset-top) + 50px);
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
.q-app:has(.q-footer--fixed) :is(.q-page, .q-page-container) {
|
|
169
|
+
:is(.q-app, .q-config-provider, .q-layout):has(.q-footer--fixed) :is(.q-page, .q-page-container) {
|
|
166
170
|
--q-page-offset-bottom: 50px;
|
|
167
171
|
--q-page-offset-bottom: calc(constant(safe-area-inset-bottom) + 50px);
|
|
168
172
|
--q-page-offset-bottom: calc(env(safe-area-inset-bottom) + 50px);
|
|
@@ -342,6 +346,9 @@ body {
|
|
|
342
346
|
--foreground: #1d1d1d;
|
|
343
347
|
--card: #ffffff;
|
|
344
348
|
--muted: #f4f6f9;
|
|
349
|
+
/* Texte secondaire (descriptions, préfixes, curseurs…) — **défini ici ET en .dark** :
|
|
350
|
+
tout `var(--muted-foreground, …)` sans définition tombait sur son fallback sombre. */
|
|
351
|
+
--muted-foreground: rgb(0 0 0 / 0.6);
|
|
345
352
|
--border: rgb(0 0 0 / 0.12);
|
|
346
353
|
|
|
347
354
|
/* Palette des séries de <q-chart> (convention `--chart-N`, reprise de shadcn) :
|
|
@@ -362,6 +369,7 @@ body {
|
|
|
362
369
|
--foreground: #e6edf3;
|
|
363
370
|
--card: #161b22;
|
|
364
371
|
--muted: #1c2128;
|
|
372
|
+
--muted-foreground: rgb(255 255 255 / 0.62);
|
|
365
373
|
--border: rgb(255 255 255 / 0.12);
|
|
366
374
|
--q-table-border: rgb(255 255 255 / 0.12);
|
|
367
375
|
--q-table-hover-bg: rgb(255 255 255 / 0.06);
|
|
@@ -507,6 +515,11 @@ html.dark body {
|
|
|
507
515
|
border-color: var(--border);
|
|
508
516
|
}
|
|
509
517
|
|
|
518
|
+
/* Séparateur de fil d'Ariane : le noir translucide est invisible en sombre */
|
|
519
|
+
.dark .q-breadcrumbs {
|
|
520
|
+
--q-breadcrumbs-sep-color: rgb(255 255 255 / 0.42);
|
|
521
|
+
}
|
|
522
|
+
|
|
510
523
|
/* ===== QBtn ===== */
|
|
511
524
|
.q-btn {
|
|
512
525
|
--q-btn-bg: var(--primary);
|
|
@@ -2136,12 +2149,12 @@ html.dark body {
|
|
|
2136
2149
|
display: block;
|
|
2137
2150
|
margin-bottom: 2px;
|
|
2138
2151
|
font-size: 12px;
|
|
2139
|
-
color: rgb(0 0 0 / 0.6);
|
|
2152
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
2140
2153
|
}
|
|
2141
2154
|
|
|
2142
2155
|
.q-field__prefix,
|
|
2143
2156
|
.q-field__suffix {
|
|
2144
|
-
color: rgb(0 0 0 / 0.6);
|
|
2157
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
2145
2158
|
white-space: nowrap;
|
|
2146
2159
|
}
|
|
2147
2160
|
|
|
@@ -2335,7 +2348,7 @@ html.dark body {
|
|
|
2335
2348
|
font-size: 12px;
|
|
2336
2349
|
line-height: 1.2;
|
|
2337
2350
|
text-align: center;
|
|
2338
|
-
color: rgb(0 0 0 / 0.6);
|
|
2351
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
2339
2352
|
}
|
|
2340
2353
|
|
|
2341
2354
|
.q-image-picker__input {
|
|
@@ -2471,7 +2484,7 @@ html.dark body {
|
|
|
2471
2484
|
|
|
2472
2485
|
.q-file-picker__add-label {
|
|
2473
2486
|
font-size: 13px;
|
|
2474
|
-
color: rgb(0 0 0 / 0.6);
|
|
2487
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
2475
2488
|
}
|
|
2476
2489
|
|
|
2477
2490
|
.q-file-picker__input {
|
|
@@ -3453,7 +3466,7 @@ html.dark body {
|
|
|
3453
3466
|
display: flex;
|
|
3454
3467
|
align-items: center;
|
|
3455
3468
|
gap: 6px;
|
|
3456
|
-
color: rgb(0 0 0 / 0.6);
|
|
3469
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
3457
3470
|
}
|
|
3458
3471
|
|
|
3459
3472
|
.q-table--dark .q-table__rows-select {
|
|
@@ -3596,6 +3609,15 @@ html.dark body {
|
|
|
3596
3609
|
padding: 0 16px;
|
|
3597
3610
|
}
|
|
3598
3611
|
|
|
3612
|
+
/* Guide vertical (« tirets longs ») à gauche du contenu : trait aligné sous l'**icône** du
|
|
3613
|
+
header (padding 16px + icône 18px / 2) et contenu décalé d'autant. Suit le thème via
|
|
3614
|
+
`--border` (invisible nulle part : clair comme sombre). */
|
|
3615
|
+
.q-collapse--guide .q-collapse__inner {
|
|
3616
|
+
margin-left: 25px;
|
|
3617
|
+
padding-left: 12px;
|
|
3618
|
+
border-left: 1px solid var(--border);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3599
3621
|
/* ===== QCheckbox / QRadio ===== */
|
|
3600
3622
|
.q-checkbox,
|
|
3601
3623
|
.q-radio {
|
|
@@ -3675,8 +3697,14 @@ html.dark body {
|
|
|
3675
3697
|
line-height: 1.4;
|
|
3676
3698
|
}
|
|
3677
3699
|
|
|
3700
|
+
/* Bordure du contrôle (état décoché) : noir 45 % en clair → **illisible en sombre**.
|
|
3701
|
+
Couvert par la variante `--dark` du composant (surface sombre explicite) ET par le
|
|
3702
|
+
**mode sombre du thème** (`.dark`), avec la même valeur.
|
|
3703
|
+
L'état coché/indéterminé est stylé en inline (couleur de la prop) — non concerné. */
|
|
3678
3704
|
.q-checkbox--dark .q-checkbox__box,
|
|
3679
|
-
.q-radio--dark .q-radio__circle
|
|
3705
|
+
.q-radio--dark .q-radio__circle,
|
|
3706
|
+
.dark .q-checkbox__box,
|
|
3707
|
+
.dark .q-radio__circle {
|
|
3680
3708
|
border-color: rgb(255 255 255 / 0.6);
|
|
3681
3709
|
}
|
|
3682
3710
|
|
|
@@ -3701,7 +3729,7 @@ html.dark body {
|
|
|
3701
3729
|
width: 18px;
|
|
3702
3730
|
height: 18px;
|
|
3703
3731
|
flex-shrink: 0;
|
|
3704
|
-
color: rgb(0 0 0 / 0.6);
|
|
3732
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
3705
3733
|
transition: transform 0.2s ease;
|
|
3706
3734
|
}
|
|
3707
3735
|
|
|
@@ -3836,7 +3864,7 @@ html.dark body {
|
|
|
3836
3864
|
align-items: center;
|
|
3837
3865
|
justify-content: space-between;
|
|
3838
3866
|
gap: 8px;
|
|
3839
|
-
padding:
|
|
3867
|
+
padding: 16px 16px 14px;
|
|
3840
3868
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
3841
3869
|
font-weight: 600;
|
|
3842
3870
|
font-size: 15px;
|
|
@@ -4164,7 +4192,7 @@ html.dark body {
|
|
|
4164
4192
|
/* ===== QBreadcrumbs ===== */
|
|
4165
4193
|
.q-breadcrumbs {
|
|
4166
4194
|
--q-breadcrumbs-gutter: 8px;
|
|
4167
|
-
--q-breadcrumbs-color: rgb(0 0 0 / 0.6);
|
|
4195
|
+
--q-breadcrumbs-color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
4168
4196
|
--q-breadcrumbs-sep-color: rgb(0 0 0 / 0.42);
|
|
4169
4197
|
--q-breadcrumbs-active-color: var(--primary);
|
|
4170
4198
|
|
|
@@ -4888,7 +4916,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
4888
4916
|
width: 18px;
|
|
4889
4917
|
height: 18px;
|
|
4890
4918
|
flex-shrink: 0;
|
|
4891
|
-
color: rgb(0 0 0 / 0.6);
|
|
4919
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
4892
4920
|
transition: transform 0.2s ease;
|
|
4893
4921
|
}
|
|
4894
4922
|
|
|
@@ -5088,7 +5116,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
5088
5116
|
align-items: center;
|
|
5089
5117
|
justify-content: space-between;
|
|
5090
5118
|
gap: 8px;
|
|
5091
|
-
|
|
5119
|
+
/* Un peu plus d'air en haut (titre + bouton de fermeture) : le panneau a un coin
|
|
5120
|
+
arrondi, 14px des deux côtés collait le titre au bord. Bas inchangé. */
|
|
5121
|
+
padding: 16px 16px 14px;
|
|
5092
5122
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5093
5123
|
font-weight: 600;
|
|
5094
5124
|
font-size: 15px;
|
|
@@ -5324,6 +5354,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
5324
5354
|
min-width: 0;
|
|
5325
5355
|
}
|
|
5326
5356
|
|
|
5357
|
+
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
5358
|
+
un peu d'air au-dessus du titre, **et un peu avant le séparateur** (sinon le trait colle
|
|
5359
|
+
au texte de la description). */
|
|
5360
|
+
.q-bottom-sheet__header-text--with-description {
|
|
5361
|
+
padding-bottom: 6px;
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
.q-bottom-sheet__header-text--with-description .q-bottom-sheet__title {
|
|
5365
|
+
padding-top: 4px;
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5327
5368
|
.q-bottom-sheet__title {
|
|
5328
5369
|
margin: 0;
|
|
5329
5370
|
font-size: 17px;
|
|
@@ -5334,7 +5375,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
5334
5375
|
.q-bottom-sheet__description {
|
|
5335
5376
|
margin: 4px 0 0;
|
|
5336
5377
|
font-size: 14px;
|
|
5337
|
-
color: rgb(0 0 0 / 0.6);
|
|
5378
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
5338
5379
|
line-height: 1.4;
|
|
5339
5380
|
}
|
|
5340
5381
|
|
|
@@ -5382,10 +5423,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
5382
5423
|
}
|
|
5383
5424
|
|
|
5384
5425
|
.q-bottom-sheet__panel--dark .q-bottom-sheet__header,
|
|
5385
|
-
.q-bottom-sheet__panel--dark .q-bottom-sheet__footer
|
|
5426
|
+
.q-bottom-sheet__panel--dark .q-bottom-sheet__footer,
|
|
5427
|
+
/* Thème sombre : le noir 8 % codé en dur est invisible (même classe que le dialog). */
|
|
5428
|
+
.dark .q-bottom-sheet__header,
|
|
5429
|
+
.dark .q-bottom-sheet__footer {
|
|
5386
5430
|
border-color: rgb(255 255 255 / 0.12);
|
|
5387
5431
|
}
|
|
5388
5432
|
|
|
5433
|
+
.dark .q-bottom-sheet__close:hover {
|
|
5434
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5389
5437
|
/* — transitions (slide vertical type vaul) — durées configurables via
|
|
5390
5438
|
transition-duration (vars --q-bs-duration-enter/leave) */
|
|
5391
5439
|
.q-bs-enter-active,
|
|
@@ -5655,7 +5703,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
5655
5703
|
width: 18px;
|
|
5656
5704
|
height: 18px;
|
|
5657
5705
|
flex-shrink: 0;
|
|
5658
|
-
color: rgb(0 0 0 / 0.6);
|
|
5706
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
5659
5707
|
}
|
|
5660
5708
|
|
|
5661
5709
|
.q-date-picker__overlay {
|
|
@@ -5708,9 +5756,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
5708
5756
|
}
|
|
5709
5757
|
|
|
5710
5758
|
.q-date-picker__sheet--dialog .q-date-picker__sheet-header {
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
padding-top:
|
|
5759
|
+
/* La safe-area **s'ajoute** au padding de base — sinon le titre et le × collent au bord
|
|
5760
|
+
haut (0 px) sur tout appareil sans encoche (`env()` vaut 0). */
|
|
5761
|
+
padding-top: 16px;
|
|
5762
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
5763
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
5714
5764
|
}
|
|
5715
5765
|
|
|
5716
5766
|
.q-date-picker__sheet-header {
|
|
@@ -5718,7 +5768,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
5718
5768
|
align-items: center;
|
|
5719
5769
|
justify-content: space-between;
|
|
5720
5770
|
gap: 8px;
|
|
5721
|
-
padding:
|
|
5771
|
+
padding: 16px 16px 14px;
|
|
5722
5772
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5723
5773
|
font-weight: 600;
|
|
5724
5774
|
font-size: 15px;
|
|
@@ -6186,16 +6236,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
6186
6236
|
box-shadow: none;
|
|
6187
6237
|
}
|
|
6188
6238
|
.q-country-picker__sheet--dialog .q-country-picker__sheet-header {
|
|
6189
|
-
padding-
|
|
6190
|
-
padding-top:
|
|
6191
|
-
padding-top:
|
|
6239
|
+
/* Safe-area **ajoutée** au padding de base (cf. date-picker). */
|
|
6240
|
+
padding-top: 16px;
|
|
6241
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
6242
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
6192
6243
|
}
|
|
6193
6244
|
.q-country-picker__sheet-header {
|
|
6194
6245
|
display: flex;
|
|
6195
6246
|
align-items: center;
|
|
6196
6247
|
justify-content: space-between;
|
|
6197
6248
|
gap: 8px;
|
|
6198
|
-
padding:
|
|
6249
|
+
padding: 16px 16px 14px;
|
|
6199
6250
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
6200
6251
|
font-weight: 600;
|
|
6201
6252
|
font-size: 15px;
|
|
@@ -6946,6 +6997,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
6946
6997
|
min-width: 0;
|
|
6947
6998
|
}
|
|
6948
6999
|
|
|
7000
|
+
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
7001
|
+
un peu d'air au-dessus du titre, **et un peu avant le séparateur** (sinon le trait colle
|
|
7002
|
+
au texte de la description). */
|
|
7003
|
+
.q-dialog__header-text--with-description {
|
|
7004
|
+
padding-bottom: 6px;
|
|
7005
|
+
}
|
|
7006
|
+
|
|
7007
|
+
.q-dialog__header-text--with-description .q-dialog__title {
|
|
7008
|
+
padding-top: 4px;
|
|
7009
|
+
}
|
|
7010
|
+
|
|
6949
7011
|
.q-dialog__title {
|
|
6950
7012
|
margin: 0;
|
|
6951
7013
|
font-size: 17px;
|
|
@@ -6956,7 +7018,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
6956
7018
|
.q-dialog__description {
|
|
6957
7019
|
margin: 4px 0 0;
|
|
6958
7020
|
font-size: 14px;
|
|
6959
|
-
color: rgb(0 0 0 / 0.6);
|
|
7021
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
6960
7022
|
line-height: 1.4;
|
|
6961
7023
|
}
|
|
6962
7024
|
|
|
@@ -6979,6 +7041,18 @@ a.q-breadcrumbs__el:hover {
|
|
|
6979
7041
|
background-color: rgb(0 0 0 / 0.06);
|
|
6980
7042
|
}
|
|
6981
7043
|
|
|
7044
|
+
/* Mode sombre du dialog : bordure des zones sticky et survol du × — le noir
|
|
7045
|
+
codé en dur (bordure, hover) y est invisible. Convention du paquet :
|
|
7046
|
+
surfaces/hovers sombres en blanc translucide (cf. `.dark .q-btn-actions__item:hover`). */
|
|
7047
|
+
.dark .q-dialog__header,
|
|
7048
|
+
.dark .q-dialog__footer {
|
|
7049
|
+
border-color: var(--border);
|
|
7050
|
+
}
|
|
7051
|
+
|
|
7052
|
+
.dark .q-dialog__close:hover {
|
|
7053
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
7054
|
+
}
|
|
7055
|
+
|
|
6982
7056
|
.q-dialog__close svg {
|
|
6983
7057
|
width: 16px;
|
|
6984
7058
|
height: 16px;
|
|
@@ -8103,9 +8177,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
8103
8177
|
En mode sheet avec recherche en bas, c'est le champ qui gère l'inset ;
|
|
8104
8178
|
sinon (liste seule) c'est la liste. */
|
|
8105
8179
|
.q-select__sheet--dialog .q-select__sheet-header {
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
padding-top:
|
|
8180
|
+
/* Safe-area **ajoutée** au padding de base — en mode plein écran le titre et le ×
|
|
8181
|
+
sont au bord haut ; sans encoche `env()` vaut 0, d'où le padding de repli. */
|
|
8182
|
+
padding-top: 16px;
|
|
8183
|
+
padding-top: calc(constant(safe-area-inset-top) + 16px);
|
|
8184
|
+
padding-top: calc(env(safe-area-inset-top) + 16px);
|
|
8109
8185
|
}
|
|
8110
8186
|
|
|
8111
8187
|
.q-select__sheet--dialog .q-select__sheet-list,
|