@dnax/ui 0.0.4 → 0.0.6
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/QNotifyProvider.vue +1 -0
- package/components/QTabs.vue +23 -7
- package/index.ts +1 -0
- package/lib/fixedLayout.ts +49 -28
- package/lib/q.ts +16 -6
- 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>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
|
|
4
4
|
// mais utilisable aussi en autonome : <q-notify-provider />.
|
|
5
5
|
import { Toaster } from "vue-sonner"
|
|
6
|
+
import "vue-sonner/style.css" // styles du Toaster (sinon les toasts sont rendus mais invisibles)
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
/** Position des toasts (défaut : bottom-right) */
|
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
|
@@ -189,8 +189,9 @@ export interface NotifyOptions {
|
|
|
189
189
|
icon?: string
|
|
190
190
|
/** Couleur de fond : token (primary, negative…) ou hex */
|
|
191
191
|
color?: string
|
|
192
|
-
/** Type → couleur riche sonner
|
|
193
|
-
|
|
192
|
+
/** Type → couleur riche sonner. Tokens Quasar acceptés : positive = success,
|
|
193
|
+
* negative = error, warning, info (success/error aussi acceptés). */
|
|
194
|
+
type?: "default" | "success" | "error" | "warning" | "info" | "positive" | "negative"
|
|
194
195
|
/** Position (défaut : bottom-right, celle du QNotifyProvider) */
|
|
195
196
|
position?: ExternalToast["position"]
|
|
196
197
|
/** Durée d'affichage en ms (défaut : 4000) */
|
|
@@ -214,9 +215,13 @@ function notify(opts: NotifyOptions): NotifyController {
|
|
|
214
215
|
icon: icon ? h(icon) : undefined,
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
// Tokens Quasar → types sonner : positive = success, negative = error
|
|
219
|
+
const sonnerType =
|
|
220
|
+
type === "positive" ? "success" : type === "negative" ? "error" : type
|
|
221
|
+
|
|
217
222
|
let id: string | number | undefined
|
|
218
223
|
|
|
219
|
-
if (color &&
|
|
224
|
+
if (color && sonnerType === "default") {
|
|
220
225
|
// Couleur Quasar custom → rendu custom (QNotifyToast)
|
|
221
226
|
id = toast.custom(QNotifyToast, {
|
|
222
227
|
...data,
|
|
@@ -239,7 +244,7 @@ function notify(opts: NotifyOptions): NotifyController {
|
|
|
239
244
|
info: toast.info,
|
|
240
245
|
default: toast,
|
|
241
246
|
} as const
|
|
242
|
-
id = toasts[
|
|
247
|
+
id = toasts[sonnerType](message, {
|
|
243
248
|
...data,
|
|
244
249
|
action: action ? { label: action.label, onClick: () => action.handler() } : undefined,
|
|
245
250
|
})
|
|
@@ -248,7 +253,12 @@ function notify(opts: NotifyOptions): NotifyController {
|
|
|
248
253
|
return { dismiss: () => toast.dismiss(id) }
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
// API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading
|
|
257
|
+
// Conventions Quasar : .open() pour les piles (dialog, bottomSheet, imagePreview),
|
|
258
|
+
// .show() pour notify et loading.
|
|
259
|
+
const notifyPlugin = notify as typeof notify & { show: typeof notify }
|
|
260
|
+
notifyPlugin.show = notify
|
|
261
|
+
|
|
252
262
|
export const $q = {
|
|
253
263
|
dialog: {
|
|
254
264
|
open: openDialog,
|
|
@@ -259,7 +269,7 @@ export const $q = {
|
|
|
259
269
|
imagePreview: {
|
|
260
270
|
open: openImagePreview,
|
|
261
271
|
},
|
|
262
|
-
notify,
|
|
272
|
+
notify: notifyPlugin,
|
|
263
273
|
platform,
|
|
264
274
|
breakpoints: qBreakpoints,
|
|
265
275
|
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,
|