@dnax/ui 0.0.7 → 0.0.9

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.
@@ -168,11 +168,28 @@ const modeOptions = computed<QSelectModeOptions | undefined>(() => {
168
168
  return undefined
169
169
  })
170
170
 
171
+ // — Options normalisées : les primitives (string/number) deviennent { value, label } —
172
+ const isObjectOption = (o: any): boolean => o !== null && typeof o === "object"
173
+
174
+ const normalizedOptions = computed<any[]>(() =>
175
+ props.options.map((o) => {
176
+ if (isObjectOption(o)) return o
177
+ // Objet normalisé : garde la valeur d'origine (non-énumérable → invisible
178
+ // pour l'affichage, la recherche fuse et les clés de rendu)
179
+ const norm: any = { value: o, label: String(o) }
180
+ Object.defineProperty(norm, "__raw", { value: o, enumerable: false })
181
+ return norm
182
+ }),
183
+ )
184
+
171
185
  // — Valeurs des options —
172
- const getOptionValue = (opt: any): any =>
173
- typeof props.optionValue === "function" ? props.optionValue(opt) : opt?.[props.optionValue]
186
+ const getOptionValue = (opt: any): any => {
187
+ if (!isObjectOption(opt)) return opt
188
+ return typeof props.optionValue === "function" ? props.optionValue(opt) : opt?.[props.optionValue]
189
+ }
174
190
 
175
191
  const getOptionLabel = (opt: any): string => {
192
+ if (!isObjectOption(opt)) return opt === undefined || opt === null ? "" : String(opt)
176
193
  const v = typeof props.optionLabel === "function" ? props.optionLabel(opt) : opt?.[props.optionLabel]
177
194
  return v === undefined || v === null ? "" : String(v)
178
195
  }
@@ -183,7 +200,7 @@ const selectedOptions = computed<any[]>(() => {
183
200
  const list = props.multiple ? (props.modelValue as any[]) : [props.modelValue]
184
201
  return list
185
202
  .map((v) => {
186
- if (props.emitValue) return props.options.find((o) => getOptionValue(o) === v)
203
+ if (props.emitValue) return normalizedOptions.value.find((o) => getOptionValue(o) === v)
187
204
  return v
188
205
  })
189
206
  .filter((o) => o !== undefined)
@@ -194,7 +211,8 @@ const isSelected = (opt: any) =>
194
211
 
195
212
  const select = (opt: any) => {
196
213
  if (props.disable || props.readonly) return
197
- const value = props.emitValue ? getOptionValue(opt) : opt
214
+ // Primitives normalisées : émettre la valeur d'origine (ex. "red"), pas l'objet
215
+ const value = props.emitValue ? getOptionValue(opt) : (opt?.__raw !== undefined ? opt.__raw : opt)
198
216
 
199
217
  if (props.multiple) {
200
218
  if (isSelected(opt)) {
@@ -253,7 +271,7 @@ const searchKeys = computed<string[]>(() => {
253
271
  })
254
272
 
255
273
  const searcher = computed(() =>
256
- createSearcher(props.options, {
274
+ createSearcher(normalizedOptions.value, {
257
275
  keys: searchKeys.value,
258
276
  threshold: typeof props.useSearch === "object" ? (props.useSearch.threshold ?? 0.4) : 0.4,
259
277
  }),
@@ -261,7 +279,7 @@ const searcher = computed(() =>
261
279
 
262
280
  // — Options affichées (recherche floue, filtre simple, ou serveur) —
263
281
  const displayOptions = computed(() => {
264
- let list = props.options
282
+ let list = normalizedOptions.value
265
283
  if (!hasFilter.value) {
266
284
  if (searchEnabled.value && searchKeys.value.length > 0) {
267
285
  list = searcher.value.search(query.value)
@@ -14,7 +14,7 @@ export const qSidebarKey: InjectionKey<SidebarContext> = Symbol("q-sidebar")
14
14
  </script>
15
15
 
16
16
  <script setup lang="ts">
17
- import { computed, onBeforeUnmount, onMounted, provide, ref } from "vue"
17
+ import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from "vue"
18
18
  import type { StyleValue } from "vue"
19
19
  import { cn } from "../lib/utils"
20
20
  import { useOverlayBack } from "../lib/overlayBack"
@@ -133,14 +133,87 @@ const rootStyle = computed<Record<string, string>>(() => {
133
133
  if (props.maxHeight) style.maxHeight = props.maxHeight
134
134
  return style
135
135
  })
136
+
137
+ // — Swipe pour fermer (mode offcanvas, pattern bottom-sheet) —
138
+ const rootEl = ref<HTMLElement | null>(null)
139
+ const dragging = ref(false)
140
+ let startX = 0
141
+ let startY = 0
142
+ let currentDx = 0
143
+ let isHorizontal = false
144
+
145
+ const onPointerDown = (e: PointerEvent) => {
146
+ if (isStatic.value || !open.value || props.disable) return
147
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
148
+ startX = e.clientX
149
+ startY = e.clientY
150
+ currentDx = 0
151
+ isHorizontal = false
152
+ dragging.value = true
153
+ }
154
+
155
+ const onPointerMove = (e: PointerEvent) => {
156
+ if (!dragging.value || !rootEl.value) return
157
+ const dx = e.clientX - startX
158
+ const dy = e.clientY - startY
159
+
160
+ // Ne capture le geste que s'il est majoritairement horizontal (le scroll
161
+ // vertical du contenu continue de fonctionner)
162
+ if (!isHorizontal) {
163
+ if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) {
164
+ isHorizontal = true
165
+ rootEl.value.style.transition = "none"
166
+ }
167
+ else return
168
+ }
169
+
170
+ // Sens : left → glisser vers la gauche (dx négatif) ; right → vers la droite
171
+ const dir = props.side === "right" ? 1 : -1
172
+ const travel = Math.max(0, dir * dx)
173
+ currentDx = travel
174
+ rootEl.value.style.transform = `translateX(${dir === 1 ? travel : -travel}px)`
175
+ }
176
+
177
+ const onPointerUp = () => {
178
+ if (!dragging.value) return
179
+ dragging.value = false
180
+ const el = rootEl.value
181
+ if (!el) return
182
+ el.style.transition = "" // restaure la transition CSS
183
+ const width = el.offsetWidth || parseInt(props.width, 10) || 260
184
+ if (currentDx > Math.min(80, width * 0.3)) {
185
+ // Fermer : l'animation continue depuis la position de swipe jusqu'au bord
186
+ const final = props.side === "right" ? "100%" : "-100%"
187
+ el.style.transform = `translateX(${final})`
188
+ setOpen(false)
189
+ }
190
+ else if (el) {
191
+ el.style.transform = "" // rebond : la transition CSS ramène à l'ouverture
192
+ }
193
+ currentDx = 0
194
+ }
195
+
196
+ // Nettoie le transform inline laissé par le swipe à la réouverture, pour que
197
+ // le panneau reparte de la position CSS (pas de l'endroit où on a relâché)
198
+ watch(open, (v) => {
199
+ const el = rootEl.value
200
+ if (!el) return
201
+ el.style.transition = ""
202
+ if (v) el.style.transform = ""
203
+ })
136
204
  </script>
137
205
 
138
206
  <template>
139
207
  <aside
208
+ ref="rootEl"
140
209
  class="q-sidebar"
141
210
  :class="rootClasses"
142
211
  :style="[rootStyle, props.style]"
143
212
  :aria-hidden="!isStatic && !open ? 'true' : undefined"
213
+ @pointerdown="onPointerDown"
214
+ @pointermove="onPointerMove"
215
+ @pointerup="onPointerUp"
216
+ @pointercancel="onPointerUp"
144
217
  >
145
218
  <slot />
146
219
  </aside>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -859,9 +859,19 @@ html.dark body {
859
859
  }
860
860
 
861
861
  /* Icône empilée au-dessus du label : aérer l'icône du haut du cadre (le tab
862
- n'a que du padding horizontal par défaut) et le label du bas */
863
- .q-tab--stacked-icon {
864
- padding: 8px 16px;
862
+ n'a que du padding horizontal par défaut) et le label du bas.
863
+ Le padding est mis sur le CONTENU (pas le .q-tab qui a height:100% en
864
+ stretch → le padding y serait absorbé). */
865
+ .q-tab--stacked-icon .q-tab__content {
866
+ padding-top: 12px;
867
+ padding-bottom: 12px;
868
+ }
869
+
870
+ /* Fallback : tout tab avec icône et NON inline-label (indépendant de la classe
871
+ JS, au cas où l'icône est rendue via slot) */
872
+ .q-tab:not(.q-tab--inline-label):has(> .q-tab__content > .q-tab__icon) .q-tab__content {
873
+ padding-top: 12px;
874
+ padding-bottom: 12px;
865
875
  }
866
876
 
867
877
  .q-tab__icon {
@@ -3611,6 +3621,8 @@ html.dark body {
3611
3621
  transition:
3612
3622
  transform 0.25s ease,
3613
3623
  visibility 0.25s;
3624
+ /* Scroll vertical natif autorisé, gestes horizontaux (swipe pour fermer) au JS */
3625
+ touch-action: pan-y;
3614
3626
  /* Safe-area : encoche, barre d'accueil et côtés en paysage */
3615
3627
  padding-top: 0;
3616
3628
  padding-top: constant(safe-area-inset-top);