@dnax/ui 0.0.62 → 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/QCollapse.vue +7 -1
- package/components/QSelect.vue +13 -4
- package/lib/select.test.ts +15 -1
- package/lib/select.ts +15 -0
- package/lib/styleTokens.test.ts +42 -0
- package/package.json +1 -1
- package/styles/main.css +49 -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/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,7 @@ 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"],
|
|
103
104
|
]
|
|
104
105
|
|
|
105
106
|
for (const [file, className, selector] of CONTRACTS) {
|
|
@@ -109,6 +110,22 @@ describe("classes de style (composant ↔ CSS)", () => {
|
|
|
109
110
|
expect({ selector, inCss: css.includes(selector) }).toEqual({ selector, inCss: true })
|
|
110
111
|
})
|
|
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
|
+
})
|
|
112
129
|
})
|
|
113
130
|
|
|
114
131
|
describe("mode sombre des contrôles", () => {
|
|
@@ -121,3 +138,28 @@ describe("mode sombre des contrôles", () => {
|
|
|
121
138
|
}
|
|
122
139
|
})
|
|
123
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
|
@@ -3609,6 +3609,15 @@ html.dark body {
|
|
|
3609
3609
|
padding: 0 16px;
|
|
3610
3610
|
}
|
|
3611
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
|
+
|
|
3612
3621
|
/* ===== QCheckbox / QRadio ===== */
|
|
3613
3622
|
.q-checkbox,
|
|
3614
3623
|
.q-radio {
|
|
@@ -3855,7 +3864,7 @@ html.dark body {
|
|
|
3855
3864
|
align-items: center;
|
|
3856
3865
|
justify-content: space-between;
|
|
3857
3866
|
gap: 8px;
|
|
3858
|
-
padding:
|
|
3867
|
+
padding: 16px 16px 14px;
|
|
3859
3868
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
3860
3869
|
font-weight: 600;
|
|
3861
3870
|
font-size: 15px;
|
|
@@ -5107,7 +5116,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
5107
5116
|
align-items: center;
|
|
5108
5117
|
justify-content: space-between;
|
|
5109
5118
|
gap: 8px;
|
|
5110
|
-
|
|
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;
|
|
5111
5122
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5112
5123
|
font-weight: 600;
|
|
5113
5124
|
font-size: 15px;
|
|
@@ -5344,7 +5355,12 @@ a.q-breadcrumbs__el:hover {
|
|
|
5344
5355
|
}
|
|
5345
5356
|
|
|
5346
5357
|
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
5347
|
-
un peu d'air au-dessus du titre
|
|
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
|
+
|
|
5348
5364
|
.q-bottom-sheet__header-text--with-description .q-bottom-sheet__title {
|
|
5349
5365
|
padding-top: 4px;
|
|
5350
5366
|
}
|
|
@@ -5407,10 +5423,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
5407
5423
|
}
|
|
5408
5424
|
|
|
5409
5425
|
.q-bottom-sheet__panel--dark .q-bottom-sheet__header,
|
|
5410
|
-
.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 {
|
|
5411
5430
|
border-color: rgb(255 255 255 / 0.12);
|
|
5412
5431
|
}
|
|
5413
5432
|
|
|
5433
|
+
.dark .q-bottom-sheet__close:hover {
|
|
5434
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5414
5437
|
/* — transitions (slide vertical type vaul) — durées configurables via
|
|
5415
5438
|
transition-duration (vars --q-bs-duration-enter/leave) */
|
|
5416
5439
|
.q-bs-enter-active,
|
|
@@ -5733,9 +5756,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
5733
5756
|
}
|
|
5734
5757
|
|
|
5735
5758
|
.q-date-picker__sheet--dialog .q-date-picker__sheet-header {
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
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);
|
|
5739
5764
|
}
|
|
5740
5765
|
|
|
5741
5766
|
.q-date-picker__sheet-header {
|
|
@@ -5743,7 +5768,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
5743
5768
|
align-items: center;
|
|
5744
5769
|
justify-content: space-between;
|
|
5745
5770
|
gap: 8px;
|
|
5746
|
-
padding:
|
|
5771
|
+
padding: 16px 16px 14px;
|
|
5747
5772
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5748
5773
|
font-weight: 600;
|
|
5749
5774
|
font-size: 15px;
|
|
@@ -6211,16 +6236,17 @@ a.q-breadcrumbs__el:hover {
|
|
|
6211
6236
|
box-shadow: none;
|
|
6212
6237
|
}
|
|
6213
6238
|
.q-country-picker__sheet--dialog .q-country-picker__sheet-header {
|
|
6214
|
-
padding-
|
|
6215
|
-
padding-top:
|
|
6216
|
-
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);
|
|
6217
6243
|
}
|
|
6218
6244
|
.q-country-picker__sheet-header {
|
|
6219
6245
|
display: flex;
|
|
6220
6246
|
align-items: center;
|
|
6221
6247
|
justify-content: space-between;
|
|
6222
6248
|
gap: 8px;
|
|
6223
|
-
padding:
|
|
6249
|
+
padding: 16px 16px 14px;
|
|
6224
6250
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
6225
6251
|
font-weight: 600;
|
|
6226
6252
|
font-size: 15px;
|
|
@@ -6972,7 +6998,12 @@ a.q-breadcrumbs__el:hover {
|
|
|
6972
6998
|
}
|
|
6973
6999
|
|
|
6974
7000
|
/* Description présente → le bloc titre+description est plus haut que la toolbar centrée :
|
|
6975
|
-
un peu d'air au-dessus du titre
|
|
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
|
+
|
|
6976
7007
|
.q-dialog__header-text--with-description .q-dialog__title {
|
|
6977
7008
|
padding-top: 4px;
|
|
6978
7009
|
}
|
|
@@ -8146,9 +8177,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
8146
8177
|
En mode sheet avec recherche en bas, c'est le champ qui gère l'inset ;
|
|
8147
8178
|
sinon (liste seule) c'est la liste. */
|
|
8148
8179
|
.q-select__sheet--dialog .q-select__sheet-header {
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
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);
|
|
8152
8185
|
}
|
|
8153
8186
|
|
|
8154
8187
|
.q-select__sheet--dialog .q-select__sheet-list,
|