@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.
@@ -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
  }
@@ -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,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.62",
3
+ "version": "0.0.63",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
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: 14px 16px;
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
- padding: 14px 16px;
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 évite qu'il colle au bord haut. */
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
- padding-top: 0;
5737
- padding-top: constant(safe-area-inset-top);
5738
- padding-top: env(safe-area-inset-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: 14px 16px;
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-top: 0;
6215
- padding-top: constant(safe-area-inset-top);
6216
- padding-top: env(safe-area-inset-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: 14px 16px;
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 évite qu'il colle au bord haut. */
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
- padding-top: 0;
8150
- padding-top: constant(safe-area-inset-top);
8151
- padding-top: env(safe-area-inset-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,