@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.
- package/components/QSelect.vue +24 -6
- package/components/QSidebar.vue +74 -1
- package/package.json +1 -1
- package/styles/main.css +15 -3
package/components/QSelect.vue
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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)
|
package/components/QSidebar.vue
CHANGED
|
@@ -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
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
|
|
864
|
-
|
|
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);
|