@dnax/ui 0.0.3 → 0.0.5
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/QGrid.vue +63 -10
- package/components/QGridItem.vue +109 -0
- package/components/QPage.vue +2 -2
- package/components/QTabs.vue +23 -7
- package/index.ts +1 -0
- package/lib/fixedLayout.ts +49 -28
- package/lib/q.ts +7 -2
- package/package.json +1 -1
- package/styles/main.css +137 -1
package/components/QGrid.vue
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QGrid — grille CSS (
|
|
3
|
-
//
|
|
2
|
+
// QGrid — grille CSS responsive (layout) ET grille de cellules style Vant.
|
|
3
|
+
// Mode layout (défaut) : <q-grid :cols="12" gap="16px"> + <q-col> enfants.
|
|
4
|
+
// Mode cellules (Vant) : activé par `column-num` → <q-grid-item> enfants
|
|
5
|
+
// (icône + texte), ex. menu d'app : <q-grid :column-num="4" gutter="8" clickable>
|
|
4
6
|
import { computed } from "vue"
|
|
5
7
|
import { useGridBreakpoints } from "../lib/breakpoints"
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
|
-
/** Nombre de colonnes (défaut : 12) */
|
|
10
|
+
/** Nombre de colonnes du layout (défaut : 12) */
|
|
9
11
|
cols?: number
|
|
10
12
|
/** Espacement (string CSS ou nombre px) */
|
|
11
13
|
gap?: string | number
|
|
@@ -18,12 +20,44 @@ interface Props {
|
|
|
18
20
|
colsXl?: number
|
|
19
21
|
/** Alignement vertical des cellules */
|
|
20
22
|
align?: "start" | "center" | "end" | "stretch"
|
|
23
|
+
|
|
24
|
+
// ── Mode cellules (Vant Grid) ──
|
|
25
|
+
/** Nombre de colonnes de cellules — active le mode Vant (défaut : 4) */
|
|
26
|
+
columnNum?: number
|
|
27
|
+
/** Espace entre les cellules (string CSS ou nombre px) */
|
|
28
|
+
gutter?: string | number
|
|
29
|
+
/** Affiche les bordures entre les cellules (défaut : true) */
|
|
30
|
+
border?: boolean
|
|
31
|
+
/** Cellules carrées (aspect-ratio 1) */
|
|
32
|
+
square?: boolean
|
|
33
|
+
/** Centre le contenu des cellules (défaut : true) */
|
|
34
|
+
center?: boolean
|
|
35
|
+
/** Cellules cliquables (effet hover/press) */
|
|
36
|
+
clickable?: boolean
|
|
37
|
+
/** Orientation du contenu : icône au-dessus (vertical) ou à gauche (horizontal) */
|
|
38
|
+
direction?: "vertical" | "horizontal"
|
|
39
|
+
/** Inverse l'ordre icône/texte */
|
|
40
|
+
reverse?: boolean
|
|
41
|
+
/** Taille des icônes des cellules (défaut : 28px) */
|
|
42
|
+
iconSize?: string | number
|
|
43
|
+
/** Couleur des icônes des cellules (token ou hex) */
|
|
44
|
+
iconColor?: string
|
|
21
45
|
}
|
|
22
46
|
|
|
23
47
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
48
|
cols: 12,
|
|
25
49
|
gap: "16px",
|
|
26
50
|
align: "stretch",
|
|
51
|
+
columnNum: 0,
|
|
52
|
+
gutter: 0,
|
|
53
|
+
border: true,
|
|
54
|
+
square: false,
|
|
55
|
+
center: true,
|
|
56
|
+
clickable: false,
|
|
57
|
+
direction: "vertical",
|
|
58
|
+
reverse: false,
|
|
59
|
+
iconSize: "28px",
|
|
60
|
+
iconColor: "",
|
|
27
61
|
})
|
|
28
62
|
|
|
29
63
|
const bp = useGridBreakpoints()
|
|
@@ -31,19 +65,38 @@ const bp = useGridBreakpoints()
|
|
|
31
65
|
const px = (v: string | number | undefined) =>
|
|
32
66
|
v === undefined ? undefined : typeof v === "number" ? `${v}px` : v
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
/** Mode cellules (Vant) : activé dès que column-num est fourni (> 0) */
|
|
69
|
+
const isCellMode = computed(() => props.columnNum > 0)
|
|
70
|
+
|
|
71
|
+
const style = computed<Record<string, string>>(() => {
|
|
72
|
+
const s: Record<string, string> = {}
|
|
73
|
+
if (isCellMode.value) {
|
|
74
|
+
s["--q-grid-cols"] = String(props.columnNum)
|
|
75
|
+
s["--q-grid-gap"] = px(props.gutter)!
|
|
76
|
+
s["--q-grid-item-icon-size"] = px(props.iconSize)!
|
|
77
|
+
if (props.iconColor) s["--q-grid-item-icon-color"] = props.iconColor
|
|
78
|
+
return s
|
|
79
|
+
}
|
|
80
|
+
s["--q-grid-cols"] = String(props.cols)
|
|
81
|
+
s["--q-grid-gap"] = px(props.gap)!
|
|
82
|
+
if (props.rowGap !== undefined) s["--q-grid-row-gap"] = px(props.rowGap)!
|
|
83
|
+
if (props.columnGap !== undefined) s["--q-grid-column-gap"] = px(props.columnGap)!
|
|
84
|
+
s.alignItems = props.align
|
|
85
|
+
return s
|
|
86
|
+
})
|
|
41
87
|
|
|
42
88
|
const classes = computed(() => ({
|
|
43
89
|
...(bp.value.sm && props.colsSm ? { [`q-grid--cols-sm-${props.colsSm}`]: true } : {}),
|
|
44
90
|
...(bp.value.md && props.colsMd ? { [`q-grid--cols-md-${props.colsMd}`]: true } : {}),
|
|
45
91
|
...(bp.value.lg && props.colsLg ? { [`q-grid--cols-lg-${props.colsLg}`]: true } : {}),
|
|
46
92
|
...(bp.value.xl && props.colsXl ? { [`q-grid--cols-xl-${props.colsXl}`]: true } : {}),
|
|
93
|
+
"q-grid--cell": isCellMode.value,
|
|
94
|
+
"q-grid--border": isCellMode.value && props.border,
|
|
95
|
+
"q-grid--square": isCellMode.value && props.square,
|
|
96
|
+
"q-grid--center": isCellMode.value && props.center,
|
|
97
|
+
"q-grid--clickable": isCellMode.value && props.clickable,
|
|
98
|
+
"q-grid--horizontal": isCellMode.value && props.direction === "horizontal",
|
|
99
|
+
"q-grid--reverse": isCellMode.value && props.reverse,
|
|
47
100
|
}))
|
|
48
101
|
</script>
|
|
49
102
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:photo" text="Photos" badge="3" />
|
|
3
|
+
// À utiliser dans <q-grid :column-num="…">. Affiche une icône (ou un slot) + un
|
|
4
|
+
// texte, avec badge / point de notification, effet cliquable et navigation (href).
|
|
5
|
+
import { computed } from "vue"
|
|
6
|
+
import { Icon } from "@iconify/vue"
|
|
7
|
+
import { icons } from "../lib/icons"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Icône Iconify (ex. : "lucide:home") */
|
|
12
|
+
icon?: string
|
|
13
|
+
/** Texte sous l'icône */
|
|
14
|
+
text?: string
|
|
15
|
+
/** Couleur de l'icône (token ou hex) — sinon celle du QGrid (icon-color) */
|
|
16
|
+
iconColor?: string
|
|
17
|
+
/** Badge numérique (masqué si 0 ou absent) */
|
|
18
|
+
badge?: number | string
|
|
19
|
+
/** Point de notification (sans nombre) */
|
|
20
|
+
dot?: boolean
|
|
21
|
+
/** Lien (navigation) — rend la cellule en <a> */
|
|
22
|
+
href?: string
|
|
23
|
+
/** Ouvre le lien dans un nouvel onglet */
|
|
24
|
+
target?: string
|
|
25
|
+
/** Clic désactivé */
|
|
26
|
+
disable?: boolean
|
|
27
|
+
/** Classe additionnelle sur la cellule */
|
|
28
|
+
class?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
32
|
+
icon: "",
|
|
33
|
+
text: "",
|
|
34
|
+
iconColor: "",
|
|
35
|
+
badge: "",
|
|
36
|
+
dot: false,
|
|
37
|
+
href: "",
|
|
38
|
+
target: "",
|
|
39
|
+
disable: false,
|
|
40
|
+
class: "",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{ click: [event: MouseEvent] }>()
|
|
44
|
+
|
|
45
|
+
const hasBadge = computed(
|
|
46
|
+
() =>
|
|
47
|
+
typeof props.badge === "number" ? props.badge > 0 : String(props.badge).length > 0,
|
|
48
|
+
)
|
|
49
|
+
const badgeDisplay = computed(() =>
|
|
50
|
+
typeof props.badge === "number" && props.badge > 99 ? "99+" : String(props.badge),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const iconStyle = computed<Record<string, string>>(() => {
|
|
54
|
+
const style: Record<string, string> = {}
|
|
55
|
+
if (props.iconColor) style.color = props.iconColor
|
|
56
|
+
return style
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const cellClasses = computed(() =>
|
|
60
|
+
cn(
|
|
61
|
+
"q-grid-item",
|
|
62
|
+
props.disable && "q-grid-item--disabled",
|
|
63
|
+
props.dot && "q-grid-item--dot",
|
|
64
|
+
hasBadge.value && "q-grid-item--badge",
|
|
65
|
+
props.href && "q-grid-item--link",
|
|
66
|
+
props.class,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const onClick = (e: MouseEvent) => {
|
|
71
|
+
if (props.disable) {
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
e.stopPropagation()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
emit("click", e)
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<a
|
|
82
|
+
v-if="href"
|
|
83
|
+
:href="href"
|
|
84
|
+
:target="target"
|
|
85
|
+
:class="cellClasses"
|
|
86
|
+
@click="onClick"
|
|
87
|
+
>
|
|
88
|
+
<span v-if="dot" class="q-grid-item__dot" aria-hidden="true" />
|
|
89
|
+
<span v-if="hasBadge" class="q-grid-item__badge">{{ badgeDisplay }}</span>
|
|
90
|
+
|
|
91
|
+
<span class="q-grid-item__content">
|
|
92
|
+
<Icon v-if="icon" :icon="icon" class="q-grid-item__icon" :style="iconStyle" aria-hidden="true" />
|
|
93
|
+
<slot name="icon" v-else />
|
|
94
|
+
<span v-if="text" class="q-grid-item__text">{{ text }}</span>
|
|
95
|
+
<span v-else-if="!icon" class="q-grid-item__text"><slot /></span>
|
|
96
|
+
</span>
|
|
97
|
+
</a>
|
|
98
|
+
<div v-else :class="cellClasses" @click="onClick">
|
|
99
|
+
<span v-if="dot" class="q-grid-item__dot" aria-hidden="true" />
|
|
100
|
+
<span v-if="hasBadge" class="q-grid-item__badge">{{ badgeDisplay }}</span>
|
|
101
|
+
|
|
102
|
+
<span class="q-grid-item__content">
|
|
103
|
+
<Icon v-if="icon" :icon="icon" class="q-grid-item__icon" :style="iconStyle" aria-hidden="true" />
|
|
104
|
+
<slot name="icon" v-else />
|
|
105
|
+
<span v-if="text" class="q-grid-item__text">{{ text }}</span>
|
|
106
|
+
<span v-else-if="!icon" class="q-grid-item__text"><slot /></span>
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
package/components/QPage.vue
CHANGED
|
@@ -44,7 +44,7 @@ useFixedBarOffset(rootEl, "page")
|
|
|
44
44
|
<template>
|
|
45
45
|
<q-virtual-scroll
|
|
46
46
|
v-if="virtual"
|
|
47
|
-
ref="setRoot"
|
|
47
|
+
:ref="setRoot"
|
|
48
48
|
class="q-page"
|
|
49
49
|
:items="items"
|
|
50
50
|
:item-key="itemKey"
|
|
@@ -65,7 +65,7 @@ useFixedBarOffset(rootEl, "page")
|
|
|
65
65
|
<slot name="after" />
|
|
66
66
|
</template>
|
|
67
67
|
</q-virtual-scroll>
|
|
68
|
-
<div v-else ref="setRoot" class="q-page">
|
|
68
|
+
<div v-else :ref="setRoot" class="q-page">
|
|
69
69
|
<slot />
|
|
70
70
|
</div>
|
|
71
71
|
</template>
|
package/components/QTabs.vue
CHANGED
|
@@ -25,6 +25,7 @@ export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
|
|
|
25
25
|
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
|
|
26
26
|
import { cva } from "class-variance-authority"
|
|
27
27
|
import { cn } from "../lib/utils"
|
|
28
|
+
import { colorValue } from "../lib/colors"
|
|
28
29
|
|
|
29
30
|
const tabsVariants = cva("q-tabs", {
|
|
30
31
|
variants: {
|
|
@@ -58,8 +59,11 @@ interface Props {
|
|
|
58
59
|
inlineLabel?: boolean
|
|
59
60
|
/** Pas de majuscules sur les tabs */
|
|
60
61
|
noCaps?: boolean
|
|
61
|
-
/**
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Position de l'indicateur (opt-in — sans la prop, aucun indicateur) :
|
|
64
|
+
* true | "bottom" → en bas (défaut) ; "top" → en haut (pattern iOS/mobile).
|
|
65
|
+
*/
|
|
66
|
+
switchIndicatorPosition?: boolean | "top" | "bottom"
|
|
63
67
|
/** Tabs en colonne (verticales) */
|
|
64
68
|
vertical?: boolean
|
|
65
69
|
/** Ne pas s'étendre (usage dans un QToolbar) */
|
|
@@ -82,7 +86,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
82
86
|
dense: false,
|
|
83
87
|
inlineLabel: false,
|
|
84
88
|
noCaps: false,
|
|
85
|
-
|
|
89
|
+
switchIndicatorPosition: undefined,
|
|
86
90
|
vertical: false,
|
|
87
91
|
shrink: false,
|
|
88
92
|
stretch: false,
|
|
@@ -92,6 +96,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
92
96
|
|
|
93
97
|
const emit = defineEmits<{ "update:modelValue": [value: string | number | null] }>()
|
|
94
98
|
|
|
99
|
+
// Position effective de l'indicateur : "top" | "bottom" (défaut) | "none" (masqué)
|
|
100
|
+
const indicatorPos = computed<"top" | "bottom" | "none">(() => {
|
|
101
|
+
const p = props.switchIndicatorPosition
|
|
102
|
+
if (p === "top") return "top"
|
|
103
|
+
if (p === false || p === undefined || p === null) return "none"
|
|
104
|
+
return "bottom" // true | "" (attribut sans valeur) | "bottom"
|
|
105
|
+
})
|
|
106
|
+
|
|
95
107
|
// shallowRef : on ne veut PAS que Vue dé-unwrappe les Ref contenues dans les enregistrements
|
|
96
108
|
const tabRegs = shallowRef<QTabRegistration[]>([])
|
|
97
109
|
const tick = ref(0)
|
|
@@ -149,14 +161,15 @@ const indicatorStyle = computed<Record<string, string>>(() => {
|
|
|
149
161
|
else {
|
|
150
162
|
style.opacity = "0"
|
|
151
163
|
}
|
|
152
|
-
if (props.indicatorColor) style.backgroundColor = props.indicatorColor
|
|
164
|
+
if (props.indicatorColor) style.backgroundColor = colorValue(props.indicatorColor)
|
|
153
165
|
return style
|
|
154
166
|
})
|
|
155
167
|
|
|
156
168
|
const containerStyle = computed<Record<string, string>>(() => {
|
|
157
169
|
const style: Record<string, string> = {}
|
|
158
|
-
|
|
159
|
-
if (props.
|
|
170
|
+
// Token (primary…) → var(--primary) ; sinon valeur directe (hex, rgb…)
|
|
171
|
+
if (props.activeColor) style["--q-tabs-active-color"] = colorValue(props.activeColor)
|
|
172
|
+
if (props.activeBgColor) style["--q-tabs-active-bg"] = colorValue(props.activeBgColor)
|
|
160
173
|
return style
|
|
161
174
|
})
|
|
162
175
|
|
|
@@ -164,7 +177,10 @@ const tabsClasses = computed(() =>
|
|
|
164
177
|
cn(
|
|
165
178
|
tabsVariants({ align: props.align, dense: props.dense }),
|
|
166
179
|
props.vertical && "q-tabs--vertical",
|
|
167
|
-
|
|
180
|
+
// Indicateur opt-in : bottom (défaut) | top | aucun
|
|
181
|
+
indicatorPos.value === "none"
|
|
182
|
+
? "q-tabs--no-indicator"
|
|
183
|
+
: indicatorPos.value === "top" && "q-tabs--switch-indicator",
|
|
168
184
|
props.shrink && "q-tabs--shrink",
|
|
169
185
|
props.stretch && "q-tabs--stretch",
|
|
170
186
|
props.animated && "q-tabs--animated",
|
package/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { default as QFilePicker } from "./components/QFilePicker.vue"
|
|
|
49
49
|
export { default as QFooter } from "./components/QFooter.vue"
|
|
50
50
|
export { default as QGallery } from "./components/QGallery.vue"
|
|
51
51
|
export { default as QGrid } from "./components/QGrid.vue"
|
|
52
|
+
export { default as QGridItem } from "./components/QGridItem.vue"
|
|
52
53
|
export { default as QHeader } from "./components/QHeader.vue"
|
|
53
54
|
export { default as QIcon } from "./components/QIcon.vue"
|
|
54
55
|
export { default as QImagePicker } from "./components/QImagePicker.vue"
|
package/lib/fixedLayout.ts
CHANGED
|
@@ -4,38 +4,48 @@
|
|
|
4
4
|
// Problème : les barres fixed sortent du flux (position: fixed) → le contenu de
|
|
5
5
|
// q-page passe dessous. Ce module :
|
|
6
6
|
// - q-page (mode "page") reçoit un padding-top = hauteur cumulée des barres
|
|
7
|
-
// fixed
|
|
7
|
+
// fixed qui le précèdent (le contenu ne se retrouve plus sous le header)
|
|
8
8
|
// - chaque barre fixed suivante (mode "bar") s'empile sous la précédente (top)
|
|
9
9
|
//
|
|
10
|
-
// Les barres fixed sont
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// Les barres fixed sont cherchées dans TOUT le sous-arbre de l'enveloppe q-app
|
|
11
|
+
// (pas seulement parmi les frères directs) et ordonnées par position dans le
|
|
12
|
+
// document : l'imbrication intermédiaire (wrapper, rendu conditionnel…) est
|
|
13
|
+
// gérée. Les hauteurs sont observées (ResizeObserver) et l'ajout/suppression de
|
|
14
|
+
// barres aussi (MutationObserver sur la racine).
|
|
13
15
|
import { onBeforeUnmount, onMounted, watch } from "vue"
|
|
14
16
|
import type { Ref } from "vue"
|
|
15
17
|
|
|
16
18
|
export const FIXED_BAR_SELECTOR = ".q-header--fixed, .q-back-header--fixed"
|
|
17
19
|
|
|
18
|
-
/**
|
|
20
|
+
/** Racine du layout : le .q-app le plus proche, sinon le parent direct. */
|
|
21
|
+
function layoutRoot(el: HTMLElement): HTMLElement | null {
|
|
22
|
+
return el.closest(".q-app") ?? el.parentElement
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hauteur cumulée des barres fixed qui précèdent `el` dans l'ordre du document,
|
|
27
|
+
* sous la même enveloppe q-app. `el` lui-même (cas mode "bar") est exclu.
|
|
28
|
+
*/
|
|
19
29
|
export function fixedBarsHeightBefore(el: HTMLElement): number {
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
30
|
+
const root = layoutRoot(el)
|
|
31
|
+
if (!root) return 0
|
|
32
|
+
const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_BAR_SELECTOR))
|
|
22
33
|
let total = 0
|
|
23
|
-
for (const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
for (const bar of bars) {
|
|
35
|
+
if (bar === el) continue
|
|
36
|
+
// DOCUMENT_POSITION_FOLLOWING = la barre est AVANT el dans le document
|
|
37
|
+
if (bar.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
38
|
+
total += bar.offsetHeight
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
return total
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
/**
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
return Array.from(
|
|
37
|
-
(c): c is HTMLElement => c instanceof HTMLElement && c.matches(FIXED_BAR_SELECTOR),
|
|
38
|
-
)
|
|
44
|
+
/** Toutes les barres fixed sous l'enveloppe q-app (pour l'observation). */
|
|
45
|
+
function fixedBarsInRoot(el: HTMLElement): HTMLElement[] {
|
|
46
|
+
const root = layoutRoot(el)
|
|
47
|
+
if (!root) return []
|
|
48
|
+
return Array.from(root.querySelectorAll<HTMLElement>(FIXED_BAR_SELECTOR))
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/**
|
|
@@ -51,6 +61,16 @@ export function useFixedBarOffset(
|
|
|
51
61
|
enabled: () => boolean = () => true,
|
|
52
62
|
) {
|
|
53
63
|
let ro: ResizeObserver | null = null
|
|
64
|
+
let mo: MutationObserver | null = null
|
|
65
|
+
|
|
66
|
+
const sync = () => {
|
|
67
|
+
const node = el.value
|
|
68
|
+
if (!node || typeof ResizeObserver === "undefined") return
|
|
69
|
+
const bars = fixedBarsInRoot(node)
|
|
70
|
+
ro?.disconnect()
|
|
71
|
+
ro = new ResizeObserver(apply)
|
|
72
|
+
bars.forEach((b) => ro!.observe(b))
|
|
73
|
+
}
|
|
54
74
|
|
|
55
75
|
const apply = () => {
|
|
56
76
|
const node = el.value
|
|
@@ -77,22 +97,23 @@ export function useFixedBarOffset(
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
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
100
|
onMounted(() => {
|
|
90
101
|
apply()
|
|
91
|
-
|
|
102
|
+
sync()
|
|
103
|
+
// Barres ajoutées/retirées dynamiquement (rendu conditionnel) → re-synchronise
|
|
104
|
+
const root = el.value ? layoutRoot(el.value) : null
|
|
105
|
+
if (root && typeof MutationObserver !== "undefined") {
|
|
106
|
+
mo = new MutationObserver(() => {
|
|
107
|
+
sync()
|
|
108
|
+
apply()
|
|
109
|
+
})
|
|
110
|
+
mo.observe(root, { childList: true, subtree: true })
|
|
111
|
+
}
|
|
92
112
|
window.addEventListener("resize", apply)
|
|
93
113
|
})
|
|
94
114
|
onBeforeUnmount(() => {
|
|
95
115
|
ro?.disconnect()
|
|
116
|
+
mo?.disconnect()
|
|
96
117
|
window.removeEventListener("resize", apply)
|
|
97
118
|
})
|
|
98
119
|
|
package/lib/q.ts
CHANGED
|
@@ -248,7 +248,12 @@ function notify(opts: NotifyOptions): NotifyController {
|
|
|
248
248
|
return { dismiss: () => toast.dismiss(id) }
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
// API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading
|
|
252
|
+
// Conventions Quasar : .open() pour les piles (dialog, bottomSheet, imagePreview),
|
|
253
|
+
// .show() pour notify et loading.
|
|
254
|
+
const notifyPlugin = notify as typeof notify & { show: typeof notify }
|
|
255
|
+
notifyPlugin.show = notify
|
|
256
|
+
|
|
252
257
|
export const $q = {
|
|
253
258
|
dialog: {
|
|
254
259
|
open: openDialog,
|
|
@@ -259,7 +264,7 @@ export const $q = {
|
|
|
259
264
|
imagePreview: {
|
|
260
265
|
open: openImagePreview,
|
|
261
266
|
},
|
|
262
|
-
notify,
|
|
267
|
+
notify: notifyPlugin,
|
|
263
268
|
platform,
|
|
264
269
|
breakpoints: qBreakpoints,
|
|
265
270
|
screen,
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -656,6 +656,12 @@ html.dark body {
|
|
|
656
656
|
width: 100%;
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
+
/* stretch → chaque tab prend une largeur équitable (comme justify), même si
|
|
660
|
+
align est left/center/right (placé après pour primer sur flex: 0 0 auto) */
|
|
661
|
+
.q-tabs--stretch .q-tab {
|
|
662
|
+
flex: 1 1 auto;
|
|
663
|
+
}
|
|
664
|
+
|
|
659
665
|
.q-tabs--shrink {
|
|
660
666
|
flex: 0 1 auto;
|
|
661
667
|
width: auto;
|
|
@@ -679,7 +685,12 @@ html.dark body {
|
|
|
679
685
|
height: auto;
|
|
680
686
|
}
|
|
681
687
|
|
|
682
|
-
/*
|
|
688
|
+
/* Indicateur : masqué par défaut (opt-in via switch-indicator-position) */
|
|
689
|
+
.q-tabs--no-indicator .q-tabs__indicator {
|
|
690
|
+
display: none;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* indicateur en haut (switch-indicator-position) */
|
|
683
694
|
.q-tabs--switch-indicator .q-tabs__indicator {
|
|
684
695
|
top: 0;
|
|
685
696
|
bottom: auto;
|
|
@@ -7184,6 +7195,131 @@ html.dark body {
|
|
|
7184
7195
|
.q-grid--cols-xl-11 { --q-grid-cols: 11; }
|
|
7185
7196
|
.q-grid--cols-xl-12 { --q-grid-cols: 12; }
|
|
7186
7197
|
|
|
7198
|
+
/* ===== QGrid — mode cellules (Vant Grid) =====
|
|
7199
|
+
Activé par :column-num — cellules QGridItem en grille égale, avec icône + texte. */
|
|
7200
|
+
.q-grid--cell {
|
|
7201
|
+
--q-grid-item-icon-size: 28px;
|
|
7202
|
+
--q-grid-item-icon-color: inherit;
|
|
7203
|
+
--q-grid-item-min-h: 84px;
|
|
7204
|
+
row-gap: var(--q-grid-gap, 0);
|
|
7205
|
+
column-gap: var(--q-grid-gap, 0);
|
|
7206
|
+
}
|
|
7207
|
+
.q-grid--cell > .q-grid-item {
|
|
7208
|
+
min-width: 0;
|
|
7209
|
+
}
|
|
7210
|
+
.q-grid--border {
|
|
7211
|
+
border-top: 1px solid rgb(0 0 0 / 0.08);
|
|
7212
|
+
border-left: 1px solid rgb(0 0 0 / 0.08);
|
|
7213
|
+
overflow: hidden;
|
|
7214
|
+
}
|
|
7215
|
+
.dark .q-grid--border {
|
|
7216
|
+
border-color: var(--border);
|
|
7217
|
+
}
|
|
7218
|
+
.q-grid--square > .q-grid-item {
|
|
7219
|
+
aspect-ratio: 1;
|
|
7220
|
+
}
|
|
7221
|
+
.q-grid--clickable > .q-grid-item {
|
|
7222
|
+
cursor: pointer;
|
|
7223
|
+
}
|
|
7224
|
+
.q-grid--clickable > .q-grid-item:hover {
|
|
7225
|
+
background-color: rgb(0 0 0 / 0.04);
|
|
7226
|
+
}
|
|
7227
|
+
|
|
7228
|
+
/* — Cellule — */
|
|
7229
|
+
.q-grid-item {
|
|
7230
|
+
position: relative;
|
|
7231
|
+
display: flex;
|
|
7232
|
+
align-items: center;
|
|
7233
|
+
justify-content: center;
|
|
7234
|
+
min-height: var(--q-grid-item-min-h);
|
|
7235
|
+
padding: 12px 8px;
|
|
7236
|
+
background: transparent;
|
|
7237
|
+
border-right: 1px solid rgb(0 0 0 / 0.08);
|
|
7238
|
+
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
7239
|
+
color: inherit;
|
|
7240
|
+
text-decoration: none;
|
|
7241
|
+
}
|
|
7242
|
+
.dark .q-grid-item {
|
|
7243
|
+
border-color: var(--border);
|
|
7244
|
+
}
|
|
7245
|
+
.q-grid-item--link {
|
|
7246
|
+
cursor: pointer;
|
|
7247
|
+
}
|
|
7248
|
+
.q-grid-item--link:hover {
|
|
7249
|
+
background-color: rgb(0 0 0 / 0.04);
|
|
7250
|
+
}
|
|
7251
|
+
.q-grid-item--disabled {
|
|
7252
|
+
opacity: 0.5;
|
|
7253
|
+
cursor: not-allowed;
|
|
7254
|
+
pointer-events: none;
|
|
7255
|
+
}
|
|
7256
|
+
|
|
7257
|
+
/* Contenu : vertical (icône au-dessus) ou horizontal (icône à gauche) */
|
|
7258
|
+
.q-grid-item__content {
|
|
7259
|
+
display: flex;
|
|
7260
|
+
flex-direction: column;
|
|
7261
|
+
align-items: center;
|
|
7262
|
+
justify-content: center;
|
|
7263
|
+
gap: 6px;
|
|
7264
|
+
text-align: center;
|
|
7265
|
+
min-width: 0;
|
|
7266
|
+
}
|
|
7267
|
+
.q-grid--horizontal .q-grid-item__content {
|
|
7268
|
+
flex-direction: row;
|
|
7269
|
+
gap: 8px;
|
|
7270
|
+
text-align: left;
|
|
7271
|
+
}
|
|
7272
|
+
.q-grid--reverse .q-grid-item__content {
|
|
7273
|
+
flex-direction: column-reverse;
|
|
7274
|
+
}
|
|
7275
|
+
.q-grid--horizontal.q-grid--reverse .q-grid-item__content {
|
|
7276
|
+
flex-direction: row-reverse;
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7279
|
+
.q-grid-item__icon {
|
|
7280
|
+
width: var(--q-grid-item-icon-size);
|
|
7281
|
+
height: var(--q-grid-item-icon-size);
|
|
7282
|
+
color: var(--q-grid-item-icon-color);
|
|
7283
|
+
flex-shrink: 0;
|
|
7284
|
+
}
|
|
7285
|
+
.q-grid-item__text {
|
|
7286
|
+
font-size: 12px;
|
|
7287
|
+
line-height: 1.35;
|
|
7288
|
+
color: inherit;
|
|
7289
|
+
overflow: hidden;
|
|
7290
|
+
display: -webkit-box;
|
|
7291
|
+
-webkit-line-clamp: 2;
|
|
7292
|
+
-webkit-box-orient: vertical;
|
|
7293
|
+
line-clamp: 2;
|
|
7294
|
+
}
|
|
7295
|
+
|
|
7296
|
+
/* Badge numérique + point de notification */
|
|
7297
|
+
.q-grid-item__badge {
|
|
7298
|
+
position: absolute;
|
|
7299
|
+
top: 6px;
|
|
7300
|
+
right: 6px;
|
|
7301
|
+
min-width: 16px;
|
|
7302
|
+
height: 16px;
|
|
7303
|
+
padding: 0 5px;
|
|
7304
|
+
border-radius: 999px;
|
|
7305
|
+
background-color: var(--negative);
|
|
7306
|
+
color: #fff;
|
|
7307
|
+
font-size: 10px;
|
|
7308
|
+
font-weight: 700;
|
|
7309
|
+
display: flex;
|
|
7310
|
+
align-items: center;
|
|
7311
|
+
justify-content: center;
|
|
7312
|
+
}
|
|
7313
|
+
.q-grid-item__dot {
|
|
7314
|
+
position: absolute;
|
|
7315
|
+
top: 8px;
|
|
7316
|
+
right: 8px;
|
|
7317
|
+
width: 8px;
|
|
7318
|
+
height: 8px;
|
|
7319
|
+
border-radius: 50%;
|
|
7320
|
+
background-color: var(--negative);
|
|
7321
|
+
}
|
|
7322
|
+
|
|
7187
7323
|
/* ===== QIntersection — transitions d'apparition (reveal au scroll) ===== */
|
|
7188
7324
|
.q-intersection-fade-enter-active,
|
|
7189
7325
|
.q-intersection-fade-leave-active,
|