@dnax/ui 0.0.1 → 0.0.3
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/QBackHeader.vue +8 -2
- package/components/QHeader.vue +10 -1
- package/components/QPage.vue +16 -1
- package/components/QSelect.vue +15 -2
- package/lib/fixedLayout.ts +106 -0
- package/package.json +1 -1
- package/styles/main.css +42 -4
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QBackHeader — barre de retour : <q-back-header title="Détails" fixed @back="router.back()" />
|
|
3
3
|
// Bouton retour + titre + actions à droite ; safe-area top intégrée.
|
|
4
|
-
import { computed } from "vue"
|
|
4
|
+
import { computed, ref } from "vue"
|
|
5
5
|
import { Icon } from "@iconify/vue"
|
|
6
6
|
import { icons } from "../lib/icons"
|
|
7
7
|
import { cn } from "../lib/utils"
|
|
8
|
+
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
/** Titre de la page */
|
|
@@ -32,6 +33,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
32
33
|
|
|
33
34
|
const emit = defineEmits<{ back: [] }>()
|
|
34
35
|
|
|
36
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
37
|
+
|
|
38
|
+
// Empilement : top = hauteur cumulée des barres fixed précédentes (si fixed)
|
|
39
|
+
useFixedBarOffset(rootEl, "bar", () => props.fixed)
|
|
40
|
+
|
|
35
41
|
const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
36
42
|
props.translucent === true || typeof props.translucent === "number"
|
|
37
43
|
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
@@ -49,7 +55,7 @@ const headerClasses = computed(() =>
|
|
|
49
55
|
</script>
|
|
50
56
|
|
|
51
57
|
<template>
|
|
52
|
-
<header class="q-back-header" :class="headerClasses" :style="translucentStyle">
|
|
58
|
+
<header ref="rootEl" class="q-back-header" :class="headerClasses" :style="translucentStyle">
|
|
53
59
|
<button
|
|
54
60
|
v-if="showBack"
|
|
55
61
|
type="button"
|
package/components/QHeader.vue
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QHeader — barre haute. fixed = position: fixed en haut (opt-in).
|
|
3
3
|
// Safe-area top/left/right appliquées toujours (styles/main.css).
|
|
4
|
-
|
|
4
|
+
// Quand plusieurs barres fixed sont empilées (q-back-header + q-header), chaque
|
|
5
|
+
// barre se décale sous la précédente automatiquement.
|
|
6
|
+
import { computed, ref } from "vue"
|
|
7
|
+
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
5
8
|
|
|
6
9
|
interface Props {
|
|
7
10
|
/** Fixe la barre en haut de l'écran (sort du flux) */
|
|
@@ -16,6 +19,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
16
19
|
fixed: false,
|
|
17
20
|
})
|
|
18
21
|
|
|
22
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
23
|
+
|
|
24
|
+
// Empilement : top = hauteur cumulée des barres fixed précédentes (si fixed)
|
|
25
|
+
useFixedBarOffset(rootEl, "bar", () => props.fixed)
|
|
26
|
+
|
|
19
27
|
const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
20
28
|
props.translucent === true || typeof props.translucent === "number"
|
|
21
29
|
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
@@ -25,6 +33,7 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
25
33
|
|
|
26
34
|
<template>
|
|
27
35
|
<div
|
|
36
|
+
ref="rootEl"
|
|
28
37
|
class="q-header"
|
|
29
38
|
:class="{
|
|
30
39
|
'q-header--fixed': fixed,
|
package/components/QPage.vue
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QPage — zone de contenu. Prop `virtual` : active le virtual scroll
|
|
3
3
|
// (rendu fenêtré via QVirtualScroll) quand on a beaucoup d'items.
|
|
4
|
+
// Padding-top automatique quand des barres fixed (q-header / q-back-header)
|
|
5
|
+
// précèdent la page : le contenu reste sous les barres (jamais masqué).
|
|
6
|
+
import { onBeforeUnmount, onMounted, ref } from "vue"
|
|
7
|
+
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
4
8
|
import QVirtualScroll from "./QVirtualScroll.vue"
|
|
5
9
|
|
|
6
10
|
interface Props {
|
|
@@ -25,11 +29,22 @@ interface Props {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
defineProps<Props>()
|
|
32
|
+
|
|
33
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
34
|
+
// ref fonctionnel : sur un élément → l'élément ; sur q-virtual-scroll (composant) → .$el
|
|
35
|
+
const setRoot = (el: unknown) => {
|
|
36
|
+
const node = el as (HTMLElement & { $el?: HTMLElement }) | null
|
|
37
|
+
rootEl.value = node?.$el ?? node ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Offset automatique : padding-top = hauteur des barres fixed précédentes
|
|
41
|
+
useFixedBarOffset(rootEl, "page")
|
|
28
42
|
</script>
|
|
29
43
|
|
|
30
44
|
<template>
|
|
31
45
|
<q-virtual-scroll
|
|
32
46
|
v-if="virtual"
|
|
47
|
+
ref="setRoot"
|
|
33
48
|
class="q-page"
|
|
34
49
|
:items="items"
|
|
35
50
|
:item-key="itemKey"
|
|
@@ -50,7 +65,7 @@ defineProps<Props>()
|
|
|
50
65
|
<slot name="after" />
|
|
51
66
|
</template>
|
|
52
67
|
</q-virtual-scroll>
|
|
53
|
-
<div v-else class="q-page">
|
|
68
|
+
<div v-else ref="setRoot" class="q-page">
|
|
54
69
|
<slot />
|
|
55
70
|
</div>
|
|
56
71
|
</template>
|
package/components/QSelect.vue
CHANGED
|
@@ -300,6 +300,12 @@ const inputDisplay = computed(() => {
|
|
|
300
300
|
return selectedOptions.value[0] ? getOptionLabel(selectedOptions.value[0]) : ""
|
|
301
301
|
})
|
|
302
302
|
|
|
303
|
+
// En mode multiple + use-chips, le texte du display est remplacé par les chips ;
|
|
304
|
+
// on ne garde le display que pour le placeholder quand la liste est vide.
|
|
305
|
+
const showDisplay = computed(
|
|
306
|
+
() => !(props.multiple && props.useChips) || selectedOptions.value.length === 0,
|
|
307
|
+
)
|
|
308
|
+
|
|
303
309
|
const floatActive = computed(
|
|
304
310
|
() => focused.value || query.value !== "" || selectedOptions.value.length > 0 || props.stackLabel,
|
|
305
311
|
)
|
|
@@ -497,7 +503,14 @@ const sheetClass = computed(() => [`q-select__sheet--${props.mode}`, modeOptions
|
|
|
497
503
|
<div class="q-field__control" @click="onControlClick">
|
|
498
504
|
<slot name="prepend" />
|
|
499
505
|
|
|
500
|
-
|
|
506
|
+
<!-- Chips : uniquement en multiple + use-chips (comme Quasar — un select
|
|
507
|
+
simple avec use-chips affiche juste le texte du display) -->
|
|
508
|
+
<span
|
|
509
|
+
v-for="(opt, i) in selectedOptions"
|
|
510
|
+
v-if="multiple && useChips"
|
|
511
|
+
:key="`${String(getOptionValue(opt))}-${i}`"
|
|
512
|
+
class="q-select__chip"
|
|
513
|
+
>
|
|
501
514
|
<span class="q-select__chip-label">{{ getOptionLabel(opt) }}</span>
|
|
502
515
|
<button
|
|
503
516
|
v-if="!disable && !readonly"
|
|
@@ -527,7 +540,7 @@ const sheetClass = computed(() => [`q-select__sheet--${props.mode}`, modeOptions
|
|
|
527
540
|
@keydown="onKeydown"
|
|
528
541
|
/>
|
|
529
542
|
<span
|
|
530
|
-
v-else
|
|
543
|
+
v-else-if="showDisplay"
|
|
531
544
|
class="q-select__display"
|
|
532
545
|
:class="{ 'q-select__display--placeholder': !displayText }"
|
|
533
546
|
>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// fixedLayout — offset automatique des barres fixed (q-header, q-back-header)
|
|
2
|
+
// et du q-page qui les suit.
|
|
3
|
+
//
|
|
4
|
+
// Problème : les barres fixed sortent du flux (position: fixed) → le contenu de
|
|
5
|
+
// q-page passe dessous. Ce module :
|
|
6
|
+
// - q-page (mode "page") reçoit un padding-top = hauteur cumulée des barres
|
|
7
|
+
// fixed frères qui le précèdent (le contenu ne se retrouve plus sous le header)
|
|
8
|
+
// - chaque barre fixed suivante (mode "bar") s'empile sous la précédente (top)
|
|
9
|
+
//
|
|
10
|
+
// Les barres fixed sont détectées par le sélecteur FIXED_BAR_SELECTOR parmi les
|
|
11
|
+
// frères du même parent ; la hauteur est observée (ResizeObserver) pour suivre
|
|
12
|
+
// les changements de taille (contenu, safe-area).
|
|
13
|
+
import { onBeforeUnmount, onMounted, watch } from "vue"
|
|
14
|
+
import type { Ref } from "vue"
|
|
15
|
+
|
|
16
|
+
export const FIXED_BAR_SELECTOR = ".q-header--fixed, .q-back-header--fixed"
|
|
17
|
+
|
|
18
|
+
/** Hauteur cumulée des barres fixed qui précèdent `el` (frères du même parent) */
|
|
19
|
+
export function fixedBarsHeightBefore(el: HTMLElement): number {
|
|
20
|
+
const parent = el.parentElement
|
|
21
|
+
if (!parent) return 0
|
|
22
|
+
let total = 0
|
|
23
|
+
for (const child of parent.children) {
|
|
24
|
+
if (child === el) break
|
|
25
|
+
if (child instanceof HTMLElement && child.matches(FIXED_BAR_SELECTOR)) {
|
|
26
|
+
total += child.offsetHeight
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return total
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Barres fixed frères de `el` (pour l'observation ResizeObserver) */
|
|
33
|
+
function fixedBarSiblings(el: HTMLElement): HTMLElement[] {
|
|
34
|
+
const parent = el.parentElement
|
|
35
|
+
if (!parent) return []
|
|
36
|
+
return Array.from(parent.children).filter(
|
|
37
|
+
(c): c is HTMLElement => c instanceof HTMLElement && c.matches(FIXED_BAR_SELECTOR),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Applique l'offset lié aux barres fixed qui précèdent `el` :
|
|
43
|
+
* - mode "page" : padding-top = hauteur cumulée (q-page) + variable --q-page-offset
|
|
44
|
+
* - mode "bar" : top = hauteur cumulée (empilement des barres fixed)
|
|
45
|
+
*
|
|
46
|
+
* `enabled` permet de n'activer que dans certaines conditions (ex. props.fixed).
|
|
47
|
+
*/
|
|
48
|
+
export function useFixedBarOffset(
|
|
49
|
+
el: Ref<HTMLElement | null>,
|
|
50
|
+
mode: "page" | "bar",
|
|
51
|
+
enabled: () => boolean = () => true,
|
|
52
|
+
) {
|
|
53
|
+
let ro: ResizeObserver | null = null
|
|
54
|
+
|
|
55
|
+
const apply = () => {
|
|
56
|
+
const node = el.value
|
|
57
|
+
if (!node || !enabled()) return
|
|
58
|
+
const h = fixedBarsHeightBefore(node)
|
|
59
|
+
if (mode === "page") {
|
|
60
|
+
node.style.setProperty("--q-page-offset", h ? `${h}px` : "0px")
|
|
61
|
+
node.style.paddingTop = h ? `${h}px` : ""
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
node.style.top = h ? `${h}px` : ""
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const clear = () => {
|
|
69
|
+
const node = el.value
|
|
70
|
+
if (!node) return
|
|
71
|
+
if (mode === "page") {
|
|
72
|
+
node.style.removeProperty("--q-page-offset")
|
|
73
|
+
node.style.paddingTop = ""
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
node.style.top = ""
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const observe = () => {
|
|
81
|
+
const node = el.value
|
|
82
|
+
if (!node || typeof ResizeObserver === "undefined") return
|
|
83
|
+
const bars = fixedBarSiblings(node)
|
|
84
|
+
if (!bars.length) return
|
|
85
|
+
ro = new ResizeObserver(apply)
|
|
86
|
+
bars.forEach((b) => ro!.observe(b))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onMounted(() => {
|
|
90
|
+
apply()
|
|
91
|
+
observe()
|
|
92
|
+
window.addEventListener("resize", apply)
|
|
93
|
+
})
|
|
94
|
+
onBeforeUnmount(() => {
|
|
95
|
+
ro?.disconnect()
|
|
96
|
+
window.removeEventListener("resize", apply)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Ré-applique/efface quand la condition change (ex. bascule fixed)
|
|
100
|
+
watch(enabled, (v) => {
|
|
101
|
+
if (v) apply()
|
|
102
|
+
else clear()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return apply
|
|
106
|
+
}
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/* ===== Reset global (Preflight Tailwind) =====
|
|
2
|
+
box-sizing: border-box partout : width:100% + padding (q-container, q-btn
|
|
3
|
+
stretch, q-toolbar…) ne débordent plus du viewport (scroll horizontal).
|
|
4
|
+
Sans ce reset, les composants en content-box débordaient. */
|
|
5
|
+
*,
|
|
6
|
+
::before,
|
|
7
|
+
::after {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
-webkit-text-size-adjust: 100%;
|
|
13
|
+
tab-size: 4;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
line-height: inherit;
|
|
19
|
+
}
|
|
20
|
+
|
|
1
21
|
/* ===== Tokens globaux ===== */
|
|
2
22
|
/* Rayon de base des composants. Surchargé localement par la prop `rounded`
|
|
3
23
|
(échelle none|xs|sm|md|lg via --q-radius) ou les composantProps du thème. */
|
|
@@ -580,6 +600,7 @@ html.dark body {
|
|
|
580
600
|
|
|
581
601
|
position: relative;
|
|
582
602
|
display: flex;
|
|
603
|
+
width: 100%;
|
|
583
604
|
min-height: var(--q-tabs-h);
|
|
584
605
|
}
|
|
585
606
|
|
|
@@ -611,16 +632,33 @@ html.dark body {
|
|
|
611
632
|
flex: 1 1 auto;
|
|
612
633
|
}
|
|
613
634
|
|
|
614
|
-
|
|
615
|
-
|
|
635
|
+
/* align non-justify : les tabs gardent leur taille naturelle (sinon flex:1
|
|
636
|
+
les étire toujours → center/right n'auraient aucun effet) */
|
|
637
|
+
.q-tabs--left .q-tab,
|
|
638
|
+
.q-tabs--center .q-tab,
|
|
639
|
+
.q-tabs--right .q-tab {
|
|
640
|
+
flex: 0 0 auto;
|
|
616
641
|
}
|
|
617
642
|
|
|
618
|
-
.q-tabs--
|
|
619
|
-
|
|
643
|
+
.q-tabs--dense .q-tab {
|
|
644
|
+
padding: 0 12px;
|
|
620
645
|
}
|
|
621
646
|
|
|
622
647
|
.q-tabs--stretch {
|
|
623
648
|
align-self: stretch;
|
|
649
|
+
flex: 1 1 auto;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Dans un parent flex (QToolbar…) sans align-items: stretch, la barre ne prend
|
|
653
|
+
que la largeur de son contenu → center/right sans effet. stretch force la
|
|
654
|
+
largeur disponible ET l'étirement sur l'axe croisé. */
|
|
655
|
+
.q-tabs--stretch.q-tabs {
|
|
656
|
+
width: 100%;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.q-tabs--shrink {
|
|
660
|
+
flex: 0 1 auto;
|
|
661
|
+
width: auto;
|
|
624
662
|
}
|
|
625
663
|
|
|
626
664
|
.q-tabs--vertical {
|