@dnax/ui 0.0.44 → 0.0.46
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/QApp.vue +13 -6
- package/components/QFooter.vue +16 -2
- package/components/QHeader.vue +16 -2
- package/components/QLayout.vue +138 -0
- package/components/QSelect.vue +91 -56
- package/components/QSidebar.vue +28 -3
- package/components/QTiptap.vue +1 -0
- package/index.ts +3 -1
- package/lib/layout.test.ts +125 -0
- package/lib/layout.ts +181 -0
- package/lib/useComponentProps.test.ts +36 -0
- package/lib/useComponentProps.ts +7 -1
- package/module.ts +0 -18
- package/package.json +1 -2
- package/scripts/generate-exports.ts +9 -0
- package/styles/main.css +112 -20
- package/components/QVideo.vue +0 -305
package/components/QApp.vue
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QApp — conteneur racine de l'application : une div + le slot, en colonne flex sur
|
|
3
|
+
// toute la largeur et une hauteur d'écran. C'est le pendant **pur CSS** de
|
|
4
|
+
// `flex h-screen w-full` (cf. `styles/main.css` : `display: flex; flex-direction: column;
|
|
5
|
+
// width: 100%; height: 100dvh`) — les classes sont aussi posées sur la div pour les
|
|
6
|
+
// apps Tailwind.
|
|
7
|
+
// Les barres (`fixed` ou non) et leurs offsets sont résolus dans son sous-arbre par
|
|
8
|
+
// `lib/fixedLayout.ts`, d'où le `<slot />` qui doit rester son unique enfant.
|
|
9
|
+
</script>
|
|
10
|
+
|
|
1
11
|
<template>
|
|
2
|
-
<div class="q-app">
|
|
3
|
-
|
|
4
|
-
</div>
|
|
12
|
+
<div class="q-app flex h-screen w-full" v-bind="$attrs">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
5
15
|
</template>
|
|
6
|
-
<script setup>
|
|
7
|
-
|
|
8
|
-
</script>
|
package/components/QFooter.vue
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QFooter — barre basse (safe-area bottom appliquée par styles/main.css : .q-app .q-footer)
|
|
3
|
-
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
3
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue"
|
|
4
4
|
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
5
|
+
import { qLayoutKey } from "../lib/layout"
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
/** Fixe la barre en bas de l'écran (sort du flux) */
|
|
8
9
|
fixed?: boolean
|
|
10
|
+
/** Colle la barre en bas pendant le scroll (automatique dans un QLayout avec une lettre majuscule) */
|
|
11
|
+
sticky?: boolean
|
|
9
12
|
/** Ombre portée vers le haut */
|
|
10
13
|
elevated?: boolean
|
|
11
14
|
/** Masqué au scroll vers le bas, réaffiché au scroll vers le haut */
|
|
@@ -22,6 +25,7 @@ interface Props {
|
|
|
22
25
|
|
|
23
26
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
27
|
fixed: false,
|
|
28
|
+
sticky: false,
|
|
25
29
|
elevated: false,
|
|
26
30
|
reveal: false,
|
|
27
31
|
bordered: false,
|
|
@@ -30,6 +34,15 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
30
34
|
|
|
31
35
|
const rootEl = ref<HTMLElement | null>(null)
|
|
32
36
|
|
|
37
|
+
// Placement dans un QLayout (facultatif) : cellule « f » du `view`, et sticky imposé
|
|
38
|
+
// par la casse de la lettre (F).
|
|
39
|
+
const layout = inject(qLayoutKey, null)
|
|
40
|
+
const zone = computed(() => layout?.zones.value.footer)
|
|
41
|
+
const isSticky = computed(() => props.sticky || !!zone.value?.fixed)
|
|
42
|
+
const layoutStyle = computed<Record<string, string | undefined>>(() => ({
|
|
43
|
+
gridArea: zone.value?.area,
|
|
44
|
+
}))
|
|
45
|
+
|
|
33
46
|
// Empilement : bottom = hauteur cumulée des footers fixed suivants (si fixed)
|
|
34
47
|
useFixedBarOffset(rootEl, "bar-bottom", () => props.fixed)
|
|
35
48
|
|
|
@@ -68,6 +81,7 @@ onBeforeUnmount(() => {
|
|
|
68
81
|
class="q-footer"
|
|
69
82
|
:class="{
|
|
70
83
|
'q-footer--fixed': fixed,
|
|
84
|
+
'q-footer--sticky': isSticky,
|
|
71
85
|
'q-footer--elevated': props.elevated,
|
|
72
86
|
'q-footer--bordered': props.bordered,
|
|
73
87
|
'q-footer--reveal': props.reveal,
|
|
@@ -76,7 +90,7 @@ onBeforeUnmount(() => {
|
|
|
76
90
|
'q-footer--glass': props.glass,
|
|
77
91
|
'q-footer--no-padding': props.noPadding,
|
|
78
92
|
}"
|
|
79
|
-
:style="translucentStyle"
|
|
93
|
+
:style="[translucentStyle, layoutStyle]"
|
|
80
94
|
v-bind="$attrs"
|
|
81
95
|
>
|
|
82
96
|
<slot />
|
package/components/QHeader.vue
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
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
5
|
// barre se décale sous la précédente automatiquement.
|
|
6
|
-
import { computed, ref } from "vue"
|
|
6
|
+
import { computed, inject, ref } from "vue"
|
|
7
7
|
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
8
|
+
import { qLayoutKey } from "../lib/layout"
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
/** Fixe la barre en haut de l'écran (sort du flux) */
|
|
11
12
|
fixed?: boolean
|
|
13
|
+
/** Colle la barre en haut pendant le scroll du document (automatique dans un QLayout avec une lettre majuscule) */
|
|
14
|
+
sticky?: boolean
|
|
12
15
|
/** Fond translucide (frosted glass) : true = 70%, ou valeur % ("70") */
|
|
13
16
|
translucent?: boolean | number
|
|
14
17
|
/** Glassmorphism marqué : fond très translucide + flou fort + bordure claire */
|
|
@@ -17,10 +20,20 @@ interface Props {
|
|
|
17
20
|
|
|
18
21
|
const props = withDefaults(defineProps<Props>(), {
|
|
19
22
|
fixed: false,
|
|
23
|
+
sticky: false,
|
|
20
24
|
})
|
|
21
25
|
|
|
22
26
|
const rootEl = ref<HTMLElement | null>(null)
|
|
23
27
|
|
|
28
|
+
// Placement dans un QLayout (facultatif) : cellule « h » du `view`, et sticky imposé
|
|
29
|
+
// par la casse de la lettre (H) — la barre reste alors close dans sa cellule.
|
|
30
|
+
const layout = inject(qLayoutKey, null)
|
|
31
|
+
const zone = computed(() => layout?.zones.value.header)
|
|
32
|
+
const isSticky = computed(() => props.sticky || !!zone.value?.fixed)
|
|
33
|
+
const layoutStyle = computed<Record<string, string | undefined>>(() => ({
|
|
34
|
+
gridArea: zone.value?.area,
|
|
35
|
+
}))
|
|
36
|
+
|
|
24
37
|
// Empilement : top = hauteur cumulée des barres fixed précédentes (si fixed)
|
|
25
38
|
useFixedBarOffset(rootEl, "bar", () => props.fixed)
|
|
26
39
|
|
|
@@ -37,10 +50,11 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
37
50
|
class="q-header"
|
|
38
51
|
:class="{
|
|
39
52
|
'q-header--fixed': fixed,
|
|
53
|
+
'q-header--sticky': isSticky,
|
|
40
54
|
'q-header--translucent': translucent === true || typeof translucent === 'number',
|
|
41
55
|
'q-header--glass': glass,
|
|
42
56
|
}"
|
|
43
|
-
:style="translucentStyle"
|
|
57
|
+
:style="[translucentStyle, layoutStyle]"
|
|
44
58
|
v-bind="$attrs"
|
|
45
59
|
>
|
|
46
60
|
<slot />
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QLayout — matrice 3×3 pilotée par le prop `view` (modèle Quasar). Les enfants
|
|
3
|
+
// **directs** se placent eux-mêmes : q-header en cellule « h », q-footer en « f »,
|
|
4
|
+
// q-sidebar en « l » / « r », et tout le reste — donc le contenu — en cellule « p ».
|
|
5
|
+
//
|
|
6
|
+
// <q-layout view="hHh LpR fFf">
|
|
7
|
+
// <q-header>…</q-header>
|
|
8
|
+
// <q-sidebar side="left" show-if-above>…</q-sidebar>
|
|
9
|
+
// <q-page>…</q-page> ← un SEUL enfant de contenu
|
|
10
|
+
// <q-footer>…</q-footer>
|
|
11
|
+
// </q-layout>
|
|
12
|
+
//
|
|
13
|
+
// Majuscule = la zone reste en place au scroll (`position: sticky`), minuscule =
|
|
14
|
+
// elle défile avec la page. L'analyse de `view` vit dans `lib/layout.ts` (pure) ;
|
|
15
|
+
// ici : le rendu de la grille et la mesure.
|
|
16
|
+
//
|
|
17
|
+
// Deux différences assumées avec Quasar : le scroll reste celui du **document**
|
|
18
|
+
// (pas de scroll interne, donc `QFooter reveal`, `QSidebar` statique et les
|
|
19
|
+
// safe-areas continuent de fonctionner), et un header sticky est **clos dans sa
|
|
20
|
+
// cellule** — il ne peut pas passer au-dessus du drawer voisin.
|
|
21
|
+
//
|
|
22
|
+
// Les hauteurs des barres sont mesurées en continu et publiées en variables CSS
|
|
23
|
+
// (`--q-layout-header-h` / `--q-layout-footer-h` / `--q-layout-viewport-h`) : les
|
|
24
|
+
// drawers sticky s'y accrochent sans glisser sous le header, et la hauteur de
|
|
25
|
+
// référence suit le mode (fenêtre, ou conteneur en mode `container`).
|
|
26
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from "vue"
|
|
27
|
+
import type { StyleValue } from "vue"
|
|
28
|
+
import { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "../lib/layout"
|
|
29
|
+
import { cn } from "../lib/utils"
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
/** Matrice 3×3 : 3 groupes de 3 lettres (l, h, r, f, p, ou .) + casse = la zone reste en place */
|
|
33
|
+
view?: string
|
|
34
|
+
/** Layout conteneur (comme Quasar) : ne gère pas la fenêtre, une hauteur explicite est requise */
|
|
35
|
+
container?: boolean
|
|
36
|
+
/** Classe(s) additionnelle(s) */
|
|
37
|
+
class?: string
|
|
38
|
+
/** Styles CSS additionnels */
|
|
39
|
+
style?: StyleValue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
43
|
+
view: DEFAULT_LAYOUT_VIEW,
|
|
44
|
+
container: false,
|
|
45
|
+
class: "",
|
|
46
|
+
style: undefined,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const parsed = computed(() => parseView(props.view))
|
|
50
|
+
|
|
51
|
+
// Une `view` invalide n'est jamais fatale : la vue par défaut s'applique et, en dev,
|
|
52
|
+
// la raison est affichée (l'analyse est pure, le composant ne fait que rapporter).
|
|
53
|
+
watch(
|
|
54
|
+
parsed,
|
|
55
|
+
(v) => {
|
|
56
|
+
if (v.error) console.warn(`[q-layout] ${v.error} — vue par défaut appliquée`)
|
|
57
|
+
},
|
|
58
|
+
{ immediate: true },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Les enfants (q-header, q-footer, q-sidebar) lisent leur cellule ici.
|
|
62
|
+
provide(qLayoutKey, { zones: computed(() => parsed.value.zones), container: toRef(props, "container") })
|
|
63
|
+
|
|
64
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
65
|
+
|
|
66
|
+
// — Mesure des barres : hauteur cumulée des headers/footers enfants directs —
|
|
67
|
+
let ro: ResizeObserver | null = null
|
|
68
|
+
let mo: MutationObserver | null = null
|
|
69
|
+
|
|
70
|
+
const bars = (): HTMLElement[] =>
|
|
71
|
+
Array.from(rootEl.value?.children ?? []).filter(
|
|
72
|
+
(el): el is HTMLElement =>
|
|
73
|
+
el instanceof HTMLElement && el.matches(".q-header, .q-back-header, .q-footer"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const measure = () => {
|
|
77
|
+
const root = rootEl.value
|
|
78
|
+
if (!root || typeof window === "undefined") return
|
|
79
|
+
let header = 0
|
|
80
|
+
let footer = 0
|
|
81
|
+
for (const bar of bars()) {
|
|
82
|
+
if (bar.matches(".q-footer")) footer += bar.offsetHeight
|
|
83
|
+
else header += bar.offsetHeight
|
|
84
|
+
}
|
|
85
|
+
// Hauteur de référence des zones sticky : la fenêtre, ou le conteneur (mode `container`)
|
|
86
|
+
const viewport = props.container ? root.clientHeight : window.innerHeight
|
|
87
|
+
root.style.setProperty("--q-layout-header-h", `${header}px`)
|
|
88
|
+
root.style.setProperty("--q-layout-footer-h", `${footer}px`)
|
|
89
|
+
root.style.setProperty("--q-layout-viewport-h", `${viewport}px`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Les barres peuvent être rendues conditionnellement (v-if) → on ré-observe.
|
|
93
|
+
const observe = () => {
|
|
94
|
+
ro?.disconnect()
|
|
95
|
+
if (typeof ResizeObserver === "undefined") return
|
|
96
|
+
ro = new ResizeObserver(measure)
|
|
97
|
+
bars().forEach((bar) => ro!.observe(bar))
|
|
98
|
+
// En mode conteneur la hauteur de référence est celle du conteneur : on la suit
|
|
99
|
+
if (props.container && rootEl.value) ro.observe(rootEl.value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onMounted(() => {
|
|
103
|
+
measure()
|
|
104
|
+
observe()
|
|
105
|
+
if (typeof MutationObserver !== "undefined" && rootEl.value) {
|
|
106
|
+
mo = new MutationObserver(() => {
|
|
107
|
+
observe()
|
|
108
|
+
measure()
|
|
109
|
+
})
|
|
110
|
+
mo.observe(rootEl.value, { childList: true })
|
|
111
|
+
}
|
|
112
|
+
window.addEventListener("resize", measure)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
onBeforeUnmount(() => {
|
|
116
|
+
ro?.disconnect()
|
|
117
|
+
mo?.disconnect()
|
|
118
|
+
window.removeEventListener("resize", measure)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Cellules de la grille (`grid-template-areas` + cellule par défaut du contenu).
|
|
122
|
+
const rootStyle = computed<Record<string, string>>(() => ({
|
|
123
|
+
"--q-layout-areas": parsed.value.gridTemplateAreas,
|
|
124
|
+
"--q-layout-page-area": parsed.value.zones.page.area ?? "p",
|
|
125
|
+
}))
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<div
|
|
130
|
+
ref="rootEl"
|
|
131
|
+
class="q-layout"
|
|
132
|
+
:class="cn(container && 'q-layout--container', props.class)"
|
|
133
|
+
:style="[rootStyle, props.style]"
|
|
134
|
+
v-bind="$attrs"
|
|
135
|
+
>
|
|
136
|
+
<slot />
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
package/components/QSelect.vue
CHANGED
|
@@ -56,7 +56,7 @@ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref
|
|
|
56
56
|
import { Icon } from "@iconify/vue"
|
|
57
57
|
import { icons } from "../lib/icons"
|
|
58
58
|
import { cn } from "../lib/utils"
|
|
59
|
-
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
59
|
+
import { radiusStyle, radiusValue, useRadius } from "../lib/useComponentProps"
|
|
60
60
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
61
61
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
62
62
|
import { createSearcher } from "../lib/search"
|
|
@@ -184,6 +184,11 @@ defineSlots<{
|
|
|
184
184
|
hint?: () => any
|
|
185
185
|
}>()
|
|
186
186
|
|
|
187
|
+
// Multi-racines (le champ + les <Teleport> du popup et du panneau) : Vue ne peut pas
|
|
188
|
+
// hériter les attributs tout seul (avertissement « fragment or text or teleport root
|
|
189
|
+
// nodes »). Ils sont posés explicitement sur le champ (racine principale).
|
|
190
|
+
defineOptions({ inheritAttrs: false })
|
|
191
|
+
|
|
187
192
|
const instance = getCurrentInstance()
|
|
188
193
|
const hasFilter = computed(() => !!instance?.vnode.props?.onFilter)
|
|
189
194
|
|
|
@@ -223,9 +228,11 @@ const VIEWPORT_MARGIN = 8
|
|
|
223
228
|
|
|
224
229
|
const popupDirection = ref<"down" | "up">("down")
|
|
225
230
|
const popupMaxHeight = ref(POPUP_MAX_HEIGHT)
|
|
226
|
-
/**
|
|
231
|
+
/** Coordonnées **fenêtre** : le popup est téléporté dans <body> (`position: fixed`) — null avant mesure */
|
|
227
232
|
const popupTop = ref<number | null>(null)
|
|
228
233
|
const popupBottom = ref<number | null>(null)
|
|
234
|
+
/** Rect du champ (coordonnées fenêtre) : ancre horizontale du popup */
|
|
235
|
+
const popupAnchor = ref<{ left: number; right: number; width: number } | null>(null)
|
|
229
236
|
|
|
230
237
|
/** Écart champ ↔ popup : `inline-options.offset` prioritaire sur la prop `offset` */
|
|
231
238
|
const popupOffset = computed(() => modeOptions.value?.offset ?? props.offset)
|
|
@@ -265,17 +272,32 @@ const positionPopup = () => {
|
|
|
265
272
|
|
|
266
273
|
popupDirection.value = down ? "down" : "up"
|
|
267
274
|
popupMaxHeight.value = Math.max(0, Math.min(POPUP_MAX_HEIGHT, available - gap))
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
// Tout en coordonnées fenêtre : le popup est `fixed` dans <body>, il n'est donc
|
|
276
|
+
// plus positionné par rapport à la racine du composant. Horizontalement on garde la
|
|
277
|
+
// boîte du CHAMP (c'était `left: 0; right: 0` sur la racine), verticalement l'ancre.
|
|
278
|
+
popupAnchor.value = { left: rect.left, right: rect.right, width: rect.width }
|
|
279
|
+
popupTop.value = Math.round(anchor.bottom + gap)
|
|
280
|
+
popupBottom.value = Math.round(window.innerHeight - anchor.top + gap)
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
const popupStyle = computed<Record<string, string>>(() => {
|
|
273
284
|
const up = popupDirection.value === "up"
|
|
274
|
-
const
|
|
285
|
+
const anchor = popupAnchor.value
|
|
275
286
|
const style: Record<string, string> = {
|
|
276
|
-
top: up ? "auto" : popupTop.value === null ? fallback : `${popupTop.value}px`,
|
|
277
|
-
bottom: up ? (popupBottom.value === null ? fallback : `${popupBottom.value}px`) : "auto",
|
|
278
287
|
maxHeight: `${Math.round(popupMaxHeight.value)}px`,
|
|
288
|
+
// Arrondi : l'échelle de radius du composant (composantProps / prop), sinon la
|
|
289
|
+
// variable globale. Le popup est TÉLÉPORTÉ dans <body> : il n'hérite plus de
|
|
290
|
+
// `--q-radius` posé sur le champ, d'où la valeur matérialisée ici.
|
|
291
|
+
borderRadius: radiusValue(effectiveFieldRadius.value) ?? "var(--q-radius, 4px)",
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Vertical : sous le champ, ou au-dessus (bascule / placement forcé)
|
|
295
|
+
if (up) {
|
|
296
|
+
style.bottom = popupBottom.value === null ? "0px" : `${popupBottom.value}px`
|
|
297
|
+
style.top = "auto"
|
|
298
|
+
} else {
|
|
299
|
+
style.top = popupTop.value === null ? "0px" : `${popupTop.value}px`
|
|
300
|
+
style.bottom = "auto"
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
// Ancre horizontale : par défaut le popup couvre la largeur du champ. Les suffixes
|
|
@@ -284,14 +306,19 @@ const popupStyle = computed<Record<string, string>>(() => {
|
|
|
284
306
|
const placement = popupPlacement.value
|
|
285
307
|
const width = modeOptions.value?.width
|
|
286
308
|
const anchored = placement !== "auto" && placement.includes("-")
|
|
309
|
+
const left = anchor?.left ?? 0
|
|
310
|
+
const right =
|
|
311
|
+
anchor && typeof window !== "undefined" ? window.innerWidth - anchor.right : 0
|
|
312
|
+
|
|
287
313
|
if (anchored || width) {
|
|
288
314
|
const alignEnd = placement.endsWith("-end")
|
|
289
|
-
style.left = alignEnd ? "auto" :
|
|
290
|
-
style.right = alignEnd ?
|
|
291
|
-
style.width = width ?? "
|
|
315
|
+
style.left = alignEnd ? "auto" : `${left}px`
|
|
316
|
+
style.right = alignEnd ? `${right}px` : "auto"
|
|
317
|
+
style.width = width ?? "auto"
|
|
292
318
|
} else {
|
|
293
|
-
style.left =
|
|
294
|
-
style.right = "
|
|
319
|
+
style.left = `${left}px`
|
|
320
|
+
style.right = "auto"
|
|
321
|
+
style.width = anchor ? `${anchor.width}px` : "auto"
|
|
295
322
|
}
|
|
296
323
|
|
|
297
324
|
return style
|
|
@@ -575,6 +602,8 @@ const onDocMousedown = (e: MouseEvent) => {
|
|
|
575
602
|
const target = e.target as Node
|
|
576
603
|
if (rootEl.value?.contains(target)) return
|
|
577
604
|
if (sheetRef.value?.contains(target)) return
|
|
605
|
+
// Popup inline : téléporté dans <body>, donc HORS de la racine du composant
|
|
606
|
+
if (popupRef.value?.contains(target)) return
|
|
578
607
|
closePopup()
|
|
579
608
|
}
|
|
580
609
|
|
|
@@ -643,7 +672,10 @@ const sheetRadiusStyle = computed<Record<string, string> | undefined>(() => {
|
|
|
643
672
|
const rounded = modeOptions.value?.rounded ?? props.rounded
|
|
644
673
|
if (rounded === false) return { borderRadius: "0" }
|
|
645
674
|
|
|
646
|
-
|
|
675
|
+
// Défaut : l'échelle de radius du composant (composantProps), sinon le défaut
|
|
676
|
+
// historique du mode — le panneau est téléporté, il ne peut pas lire `--q-radius`.
|
|
677
|
+
const defaultRadius =
|
|
678
|
+
radiusValue(effectiveFieldRadius.value) ?? (props.mode === "modal" ? "20px" : "12px")
|
|
647
679
|
const value = typeof rounded === "string" ? rounded : defaultRadius
|
|
648
680
|
const asPx = (v: string | number) => (typeof v === "number" ? `${v}px` : v)
|
|
649
681
|
|
|
@@ -672,7 +704,7 @@ const sheetClass = computed(() => [`q-select__sheet--${props.mode}`, modeOptions
|
|
|
672
704
|
</script>
|
|
673
705
|
|
|
674
706
|
<template>
|
|
675
|
-
<div ref="rootEl" class="q-select" :class="fieldClasses" :style="fieldRadiusStyle">
|
|
707
|
+
<div ref="rootEl" class="q-select" :class="fieldClasses" :style="fieldRadiusStyle" v-bind="$attrs">
|
|
676
708
|
<label v-if="label" class="q-field__label-stack">{{ label }}</label>
|
|
677
709
|
<div class="q-field__control" @click="onControlClick">
|
|
678
710
|
<slot name="prepend">
|
|
@@ -751,50 +783,53 @@ const sheetClass = computed(() => [`q-select__sheet--${props.mode}`, modeOptions
|
|
|
751
783
|
</div>
|
|
752
784
|
</div>
|
|
753
785
|
|
|
754
|
-
<!-- Popup inline (mode inline)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
<
|
|
767
|
-
class="q-select__search-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
786
|
+
<!-- Popup inline (mode inline) : téléporté dans <body> + `position: fixed`,
|
|
787
|
+
pour échapper au `overflow: hidden` d'un q-card (cf. positionPopup) -->
|
|
788
|
+
<Teleport to="body">
|
|
789
|
+
<div
|
|
790
|
+
v-if="open && mode === 'inline'"
|
|
791
|
+
ref="popupRef"
|
|
792
|
+
class="q-select__popup"
|
|
793
|
+
:class="[inlineOptions?.class, popupDirection === 'up' && 'q-select__popup--up']"
|
|
794
|
+
:style="[popupStyle, inlineOptions?.style]"
|
|
795
|
+
role="listbox"
|
|
796
|
+
@mousedown.prevent
|
|
797
|
+
>
|
|
798
|
+
<div v-if="searchEnabled && !useInput" class="q-select__search" @mousedown.stop>
|
|
799
|
+
<Icon :icon="icons.search" class="q-select__search-icon" aria-hidden="true" />
|
|
800
|
+
<input
|
|
801
|
+
class="q-select__search-input"
|
|
802
|
+
:value="query"
|
|
803
|
+
:placeholder="effectiveSearchPlaceholder"
|
|
804
|
+
@input="onInput"
|
|
805
|
+
@keydown="onKeydown"
|
|
806
|
+
/>
|
|
807
|
+
</div>
|
|
808
|
+
<template v-if="displayOptions.length">
|
|
809
|
+
<div
|
|
810
|
+
v-for="(opt, i) in displayOptions"
|
|
811
|
+
:key="String(getOptionValue(opt))"
|
|
812
|
+
role="option"
|
|
813
|
+
:aria-selected="isSelected(opt) ? 'true' : 'false'"
|
|
814
|
+
class="q-select__option"
|
|
815
|
+
:class="{
|
|
816
|
+
'q-select__option--active': i === activeIndex,
|
|
817
|
+
'q-select__option--selected': isSelected(opt),
|
|
818
|
+
}"
|
|
819
|
+
@mouseenter="activeIndex = i"
|
|
820
|
+
@mousedown.prevent="select(opt)"
|
|
821
|
+
>
|
|
822
|
+
<slot :opt="opt" :index="i" :selected="isSelected(opt)">
|
|
823
|
+
<span class="q-select__option-label">{{ getOptionLabel(opt) }}</span>
|
|
824
|
+
</slot>
|
|
825
|
+
<Icon :icon="icons.check" v-if="multiple && isSelected(opt)" class="q-select__check" aria-hidden="true" />
|
|
826
|
+
</div>
|
|
827
|
+
</template>
|
|
828
|
+
<div v-else class="q-select__empty">
|
|
829
|
+
<slot name="noOption">{{ noOptionsLabel }}</slot>
|
|
792
830
|
</div>
|
|
793
|
-
</template>
|
|
794
|
-
<div v-else class="q-select__empty">
|
|
795
|
-
<slot name="noOption">{{ noOptionsLabel }}</slot>
|
|
796
831
|
</div>
|
|
797
|
-
</
|
|
832
|
+
</Teleport>
|
|
798
833
|
|
|
799
834
|
<!-- Mode modal / sheet / dialog : panneau téléporté -->
|
|
800
835
|
<Teleport to="body">
|
package/components/QSidebar.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// QSidebar — panneau latéral type Sidebar shadcn-vue, API QDrawer Quasar :
|
|
3
|
-
// <q-sidebar v-model="open" side="left" width="260px" bordered show-if-above breakpoint="1023">
|
|
3
|
+
// <q-sidebar v-model="open" side="left" width="260px" bordered show-if-above :breakpoint="1023">
|
|
4
4
|
// Fournit le contexte (toggle) aux QSidebarTrigger enfants.
|
|
5
5
|
import type { InjectionKey, Ref } from "vue"
|
|
6
6
|
|
|
@@ -14,10 +14,11 @@ 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, watch } from "vue"
|
|
17
|
+
import { computed, inject, 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"
|
|
21
|
+
import { qLayoutKey } from "../lib/layout"
|
|
21
22
|
|
|
22
23
|
interface Props {
|
|
23
24
|
/** Ouvert (v-model) — mode offcanvas ; ignoré en mode statique */
|
|
@@ -68,6 +69,13 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
68
69
|
|
|
69
70
|
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
|
|
70
71
|
|
|
72
|
+
// Multi-racines (<aside> + backdrop) : Vue ne peut pas hériter les attributs tout seul
|
|
73
|
+
// (avertissement « could not be automatically inherited because component renders
|
|
74
|
+
// fragment … »). On les applique explicitement sur le panneau, la seule racine qui
|
|
75
|
+
// compte : `id`, `data-*`, `aria-label`… arrivent donc sur l'<aside> (l'accessibilité
|
|
76
|
+
// et les sélecteurs de test sont préservés). `class` / `style` restent des props.
|
|
77
|
+
defineOptions({ inheritAttrs: false })
|
|
78
|
+
|
|
71
79
|
// — Mode statique (viewport >= breakpoint && show-if-above) —
|
|
72
80
|
const isAbove = ref(false)
|
|
73
81
|
let mql: MediaQueryList | null = null
|
|
@@ -97,6 +105,13 @@ const toggle = () => setOpen(!open.value)
|
|
|
97
105
|
|
|
98
106
|
provide<SidebarContext>(qSidebarKey, { open, setOpen, toggle })
|
|
99
107
|
|
|
108
|
+
// — Placement dans un QLayout (facultatif) —
|
|
109
|
+
// En mode statique le panneau occupe sa cellule (« l » / « r » du `view`) et devient
|
|
110
|
+
// sticky si la lettre est en majuscule ; en offcanvas il est `fixed` (recouvrant),
|
|
111
|
+
// donc sa place dans la grille ne compte plus.
|
|
112
|
+
const layout = inject(qLayoutKey, null)
|
|
113
|
+
const layoutZone = computed(() => layout?.zones.value[props.side])
|
|
114
|
+
|
|
100
115
|
// « Retour » navigateur → ferme la sidebar (mode offcanvas uniquement)
|
|
101
116
|
const overlayOpen = computed(() => !isStatic.value && open.value)
|
|
102
117
|
useOverlayBack(overlayOpen, () => setOpen(false), "QSidebar")
|
|
@@ -131,7 +146,8 @@ const rootClasses = computed(() =>
|
|
|
131
146
|
isStatic.value ? "q-sidebar--static" : "q-sidebar--offcanvas",
|
|
132
147
|
!isStatic.value && open.value && "q-sidebar--open",
|
|
133
148
|
props.side === "right" && "q-sidebar--right",
|
|
134
|
-
|
|
149
|
+
// sticky : prop explicite, ou imposé par la casse de la lettre dans un QLayout
|
|
150
|
+
(props.sticky || !!layoutZone.value?.fixed) && "q-sidebar--sticky",
|
|
135
151
|
props.bordered && "q-sidebar--bordered",
|
|
136
152
|
props.elevated && "q-sidebar--elevated",
|
|
137
153
|
props.dark && "q-sidebar--dark",
|
|
@@ -143,6 +159,14 @@ const rootStyle = computed<Record<string, string>>(() => {
|
|
|
143
159
|
const style: Record<string, string> = { "--q-sidebar-w": props.width }
|
|
144
160
|
if (props.height) style.height = props.height
|
|
145
161
|
if (props.maxHeight) style.maxHeight = props.maxHeight
|
|
162
|
+
// Cellule de la grille QLayout (mode statique : en offcanvas le panneau est fixed)
|
|
163
|
+
const zone = layoutZone.value
|
|
164
|
+
if (isStatic.value && zone?.area) style.gridArea = zone.area
|
|
165
|
+
// Sticky : ne s'accroche qu'aux barres qui le surplombent (rangée 0 = au-dessus de lui)
|
|
166
|
+
if (isStatic.value && zone?.fixed) {
|
|
167
|
+
if (zone.cells.some(([row]) => row === 0)) style["--q-sidebar-sticky-top"] = "0px"
|
|
168
|
+
if (zone.cells.some(([row]) => row === 2)) style["--q-sidebar-sticky-bottom"] = "0px"
|
|
169
|
+
}
|
|
146
170
|
return style
|
|
147
171
|
})
|
|
148
172
|
|
|
@@ -221,6 +245,7 @@ watch(open, (v) => {
|
|
|
221
245
|
class="q-sidebar"
|
|
222
246
|
:class="rootClasses"
|
|
223
247
|
:style="[rootStyle, props.style]"
|
|
248
|
+
v-bind="$attrs"
|
|
224
249
|
:aria-hidden="!isStatic && !open ? 'true' : undefined"
|
|
225
250
|
@pointerdown="onPointerDown"
|
|
226
251
|
@pointermove="onPointerMove"
|
package/components/QTiptap.vue
CHANGED
package/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export { default as QInteract } from "./components/QInteract.vue"
|
|
|
74
74
|
export { default as QIntersection } from "./components/QIntersection.vue"
|
|
75
75
|
export { default as QItem } from "./components/QItem.vue"
|
|
76
76
|
export { default as QItemSection } from "./components/QItemSection.vue"
|
|
77
|
+
export { default as QLayout } from "./components/QLayout.vue"
|
|
77
78
|
export { default as QLinearProgress } from "./components/QLinearProgress.vue"
|
|
78
79
|
export { default as QList } from "./components/QList.vue"
|
|
79
80
|
export { default as QLoading } from "./components/QLoading.vue"
|
|
@@ -135,7 +136,6 @@ export { default as QTiptap } from "./components/QTiptap.vue"
|
|
|
135
136
|
export { default as QToolbar } from "./components/QToolbar.vue"
|
|
136
137
|
export { default as QTooltip } from "./components/QTooltip.vue"
|
|
137
138
|
export { default as QUploader } from "./components/QUploader.vue"
|
|
138
|
-
export { default as QVideo } from "./components/QVideo.vue"
|
|
139
139
|
export { default as QVirtualScroll } from "./components/QVirtualScroll.vue"
|
|
140
140
|
|
|
141
141
|
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
|
|
@@ -173,6 +173,8 @@ export type {
|
|
|
173
173
|
PopoverPlacement,
|
|
174
174
|
PopoverViewport,
|
|
175
175
|
} from "./lib/datePicker"
|
|
176
|
+
export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
|
|
177
|
+
export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
|
|
176
178
|
export { vRipple } from "./lib/ripple"
|
|
177
179
|
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
178
180
|
export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
|