@dnax/ui 0.0.43 → 0.0.45
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/QChart.vue +533 -0
- package/components/QDatePicker.vue +95 -14
- package/components/QFooter.vue +16 -2
- package/components/QHeader.vue +16 -2
- package/components/QLayout.vue +138 -0
- package/components/QSidebar.vue +20 -3
- package/components/internal/QDateCalendar.vue +112 -19
- package/index.ts +24 -2
- package/lib/chart.test.ts +991 -0
- package/lib/chart.ts +1340 -0
- package/lib/color.test.ts +81 -0
- package/lib/color.ts +60 -0
- package/lib/datePicker.test.ts +122 -0
- package/lib/datePicker.ts +114 -0
- package/lib/layout.test.ts +125 -0
- package/lib/layout.ts +181 -0
- package/module.ts +0 -18
- package/package.json +2 -10
- package/scripts/generate-exports.ts +22 -0
- package/styles/main.css +320 -132
- package/components/QEditorJs.vue +0 -146
- package/components/QVideo.vue +0 -305
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QDatePicker — sélecteur de date type DatePicker shadcn-vue, API Quasar :
|
|
3
3
|
// <q-date-picker v-model="date" mode="sheet" label="Échéance" clearable outlined />
|
|
4
|
-
// Modes : inline (calendrier en place) |
|
|
5
|
-
|
|
4
|
+
// Modes : inline (calendrier en place) | popover (panneau ancré sous le champ) |
|
|
5
|
+
// modal (centré) | sheet (bottom sheet) | dialog (plein écran).
|
|
6
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
6
7
|
import { Icon } from "@iconify/vue"
|
|
7
8
|
import { icons } from "../lib/icons"
|
|
8
9
|
import { cn } from "../lib/utils"
|
|
9
10
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
11
|
+
import { placePopover, type PopoverPlacement } from "../lib/datePicker"
|
|
10
12
|
import QDateCalendar from "./internal/QDateCalendar.vue"
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
/** Date sélectionnée */
|
|
14
16
|
modelValue?: Date | null
|
|
15
17
|
/** Mode d'affichage */
|
|
16
|
-
mode?: "inline" | "sheet" | "modal" | "dialog"
|
|
18
|
+
mode?: "inline" | "popover" | "sheet" | "modal" | "dialog"
|
|
17
19
|
label?: string
|
|
18
20
|
stackLabel?: boolean
|
|
19
21
|
hint?: string
|
|
@@ -37,10 +39,14 @@ interface Props {
|
|
|
37
39
|
disabledDates?: (d: Date) => boolean
|
|
38
40
|
/** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
|
|
39
41
|
firstDayOfWeek?: number
|
|
40
|
-
/** Largeur du panneau (
|
|
42
|
+
/** Largeur du panneau (popover/sheet/modal) */
|
|
41
43
|
width?: string
|
|
42
|
-
/** Titre du panneau */
|
|
44
|
+
/** Titre du panneau (modes à voile : sheet/modal/dialog) */
|
|
43
45
|
title?: string
|
|
46
|
+
/** Raccourci « Today » sous le calendrier (tous les modes) */
|
|
47
|
+
todayBtn?: boolean
|
|
48
|
+
/** En-tête : libellé cliquable ouvrant le choix du mois (tous les modes) */
|
|
49
|
+
monthDropdown?: boolean
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +64,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
58
64
|
firstDayOfWeek: 1,
|
|
59
65
|
width: "",
|
|
60
66
|
title: "",
|
|
67
|
+
todayBtn: false,
|
|
68
|
+
monthDropdown: false,
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
const emit = defineEmits<{
|
|
@@ -67,14 +75,22 @@ const emit = defineEmits<{
|
|
|
67
75
|
clear: []
|
|
68
76
|
}>()
|
|
69
77
|
|
|
78
|
+
/** Champ déclencheur (tout sauf `inline`) */
|
|
70
79
|
const isPanelMode = computed(() => props.mode !== "inline")
|
|
80
|
+
/** Panneau ancré au champ : ni voile, ni blocage du scroll, ni retour navigateur */
|
|
81
|
+
const isPopover = computed(() => props.mode === "popover")
|
|
82
|
+
/** Modes à voile sombre (sheet/modal/dialog) */
|
|
83
|
+
const isOverlayMode = computed(
|
|
84
|
+
() => props.mode === "sheet" || props.mode === "modal" || props.mode === "dialog",
|
|
85
|
+
)
|
|
71
86
|
|
|
72
87
|
const open = ref(false)
|
|
73
88
|
const rootEl = ref<HTMLElement | null>(null)
|
|
74
89
|
const sheetRef = ref<HTMLElement | null>(null)
|
|
75
90
|
|
|
76
|
-
// « Retour » navigateur → ferme le panneau (sheet/modal/dialog) au lieu de
|
|
77
|
-
|
|
91
|
+
// « Retour » navigateur → ferme le panneau au voile (sheet/modal/dialog) au lieu de
|
|
92
|
+
// naviguer. Le popover, lui, reste un menu : clic extérieur / Échap suffisent.
|
|
93
|
+
const panelOpen = computed(() => isOverlayMode.value && open.value)
|
|
78
94
|
useOverlayBack(panelOpen, () => closePopup(), "QDatePicker")
|
|
79
95
|
|
|
80
96
|
// — Affichage du champ —
|
|
@@ -129,6 +145,51 @@ const sheetStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
129
145
|
props.width ? { width: props.width, maxWidth: props.width } : undefined,
|
|
130
146
|
)
|
|
131
147
|
|
|
148
|
+
// — Popover : panneau ancré au champ, position calculée en `fixed` (téléporté) —
|
|
149
|
+
// Le calcul vit dans un helper pur (`lib/datePicker.ts`) : ici on ne fait que
|
|
150
|
+
// mesurer les rectangles et sérialiser le résultat.
|
|
151
|
+
const popoverPlacement = ref<PopoverPlacement | null>(null)
|
|
152
|
+
// Conserve le dernier emplacement à la fermeture : l'animation de sortie en dépend.
|
|
153
|
+
const popoverStyle = computed<Record<string, string>>(() => {
|
|
154
|
+
const p = popoverPlacement.value
|
|
155
|
+
if (!p) return { visibility: "hidden" }
|
|
156
|
+
return {
|
|
157
|
+
top: p.top === null ? "auto" : `${p.top}px`,
|
|
158
|
+
bottom: p.bottom === null ? "auto" : `${p.bottom}px`,
|
|
159
|
+
left: `${p.left}px`,
|
|
160
|
+
"--q-date-picker-caret": `${p.caret}px`,
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/** Le panneau borne sa hauteur à l'espace visible ; la liste scrolle au-delà. */
|
|
165
|
+
const popoverPanelStyle = computed<Record<string, string | undefined>>(() => ({
|
|
166
|
+
...sheetStyle.value,
|
|
167
|
+
maxHeight: popoverPlacement.value ? `${popoverPlacement.value.maxHeight}px` : undefined,
|
|
168
|
+
}))
|
|
169
|
+
|
|
170
|
+
const positionPopover = () => {
|
|
171
|
+
const el = rootEl.value
|
|
172
|
+
if (!el || typeof window === "undefined") return
|
|
173
|
+
|
|
174
|
+
// Ancre = bas du CHAMP : `.q-field__bottom` réserve ~24px même vide, ce qui
|
|
175
|
+
// éloignerait le panneau. Quand un hint / une erreur est affiché, on prend la
|
|
176
|
+
// racine pour ne pas recouvrir le texte (même règle que le popup de QSelect).
|
|
177
|
+
const control = el.querySelector<HTMLElement>(".q-field__control")
|
|
178
|
+
const hasBottomText = !!el.querySelector(".q-field__hint, .q-field__error")
|
|
179
|
+
const anchor = (!hasBottomText && control ? control : el).getBoundingClientRect()
|
|
180
|
+
|
|
181
|
+
popoverPlacement.value = placePopover({
|
|
182
|
+
anchor: { top: anchor.top, bottom: anchor.bottom, left: anchor.left, width: anchor.width },
|
|
183
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
184
|
+
// Largeur mesurée après rendu (le champ `width` peut être en %, vw…)
|
|
185
|
+
panelWidth: sheetRef.value?.getBoundingClientRect().width,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const onViewportChange = () => {
|
|
190
|
+
if (open.value && isPopover.value) positionPopover()
|
|
191
|
+
}
|
|
192
|
+
|
|
132
193
|
// — Fermetures : clic extérieur (hors panneau téléporté), Échap, scroll lock —
|
|
133
194
|
const onDocMousedown = (e: MouseEvent) => {
|
|
134
195
|
const target = e.target as Node
|
|
@@ -141,10 +202,15 @@ const onDocKeydown = (e: KeyboardEvent) => {
|
|
|
141
202
|
if (e.key === "Escape" && open.value) closePopup()
|
|
142
203
|
}
|
|
143
204
|
|
|
144
|
-
watch(open, (v) => {
|
|
145
|
-
|
|
205
|
+
watch(open, async (v) => {
|
|
206
|
+
// Le voile bloque le scroll de la page ; le popover le laisse vivre.
|
|
207
|
+
if (typeof document !== "undefined" && isOverlayMode.value) {
|
|
146
208
|
document.body.style.overflow = v ? "hidden" : ""
|
|
147
209
|
}
|
|
210
|
+
if (v && isPopover.value) {
|
|
211
|
+
await nextTick()
|
|
212
|
+
positionPopover()
|
|
213
|
+
}
|
|
148
214
|
})
|
|
149
215
|
|
|
150
216
|
onMounted(() => {
|
|
@@ -153,12 +219,17 @@ onMounted(() => {
|
|
|
153
219
|
// sinon l'événement avant `document` → calendrier jamais fermé au clic extérieur.
|
|
154
220
|
document.addEventListener("mousedown", onDocMousedown, true)
|
|
155
221
|
document.addEventListener("keydown", onDocKeydown)
|
|
222
|
+
// Le champ peut bouger (scroll) ou la fenêtre changer de taille : on repositionne
|
|
223
|
+
window.addEventListener("scroll", onViewportChange, true)
|
|
224
|
+
window.addEventListener("resize", onViewportChange)
|
|
156
225
|
}
|
|
157
226
|
})
|
|
158
227
|
onBeforeUnmount(() => {
|
|
159
228
|
if (typeof document !== "undefined") {
|
|
160
229
|
document.removeEventListener("mousedown", onDocMousedown, true)
|
|
161
230
|
document.removeEventListener("keydown", onDocKeydown)
|
|
231
|
+
window.removeEventListener("scroll", onViewportChange, true)
|
|
232
|
+
window.removeEventListener("resize", onViewportChange)
|
|
162
233
|
document.body.style.overflow = ""
|
|
163
234
|
}
|
|
164
235
|
})
|
|
@@ -174,6 +245,8 @@ onBeforeUnmount(() => {
|
|
|
174
245
|
:max-date="maxDate"
|
|
175
246
|
:disabled-dates="disabledDates"
|
|
176
247
|
:first-day-of-week="firstDayOfWeek"
|
|
248
|
+
:today-btn="todayBtn"
|
|
249
|
+
:month-dropdown="monthDropdown"
|
|
177
250
|
@select="onSelect"
|
|
178
251
|
/>
|
|
179
252
|
</div>
|
|
@@ -203,11 +276,15 @@ onBeforeUnmount(() => {
|
|
|
203
276
|
</div>
|
|
204
277
|
|
|
205
278
|
<Teleport to="body">
|
|
206
|
-
<Transition name="q-date-modal">
|
|
279
|
+
<Transition :name="isPopover ? 'q-date-popover' : 'q-date-modal'">
|
|
207
280
|
<div
|
|
208
281
|
v-if="open"
|
|
209
282
|
class="q-date-picker__overlay"
|
|
210
|
-
:class="
|
|
283
|
+
:class="[
|
|
284
|
+
`q-date-picker__overlay--${mode}`,
|
|
285
|
+
isPopover && popoverPlacement?.direction === 'up' && 'q-date-picker__overlay--popover-up',
|
|
286
|
+
]"
|
|
287
|
+
:style="isPopover ? popoverStyle : undefined"
|
|
211
288
|
role="presentation"
|
|
212
289
|
@mousedown.self="closePopup"
|
|
213
290
|
>
|
|
@@ -215,12 +292,14 @@ onBeforeUnmount(() => {
|
|
|
215
292
|
ref="sheetRef"
|
|
216
293
|
class="q-date-picker__sheet"
|
|
217
294
|
:class="`q-date-picker__sheet--${mode}`"
|
|
218
|
-
:style="sheetStyle"
|
|
295
|
+
:style="isPopover ? popoverPanelStyle : sheetStyle"
|
|
219
296
|
role="dialog"
|
|
220
|
-
aria-modal="true"
|
|
297
|
+
:aria-modal="isOverlayMode ? 'true' : undefined"
|
|
221
298
|
:aria-label="sheetTitle"
|
|
222
299
|
>
|
|
223
|
-
|
|
300
|
+
<!-- En-tête (titre + fermeture) réservé aux modes à voile : un popover
|
|
301
|
+
n'a ni titre ni bouton de fermeture, il se ferme au clic extérieur. -->
|
|
302
|
+
<div v-if="isOverlayMode" class="q-date-picker__sheet-header">
|
|
224
303
|
<span class="q-date-picker__sheet-title">{{ sheetTitle }}</span>
|
|
225
304
|
<button
|
|
226
305
|
type="button"
|
|
@@ -238,6 +317,8 @@ onBeforeUnmount(() => {
|
|
|
238
317
|
:max-date="maxDate"
|
|
239
318
|
:disabled-dates="disabledDates"
|
|
240
319
|
:first-day-of-week="firstDayOfWeek"
|
|
320
|
+
:today-btn="todayBtn"
|
|
321
|
+
:month-dropdown="monthDropdown"
|
|
241
322
|
@select="onSelect"
|
|
242
323
|
/>
|
|
243
324
|
</div>
|
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/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 */
|
|
@@ -97,6 +98,13 @@ const toggle = () => setOpen(!open.value)
|
|
|
97
98
|
|
|
98
99
|
provide<SidebarContext>(qSidebarKey, { open, setOpen, toggle })
|
|
99
100
|
|
|
101
|
+
// — Placement dans un QLayout (facultatif) —
|
|
102
|
+
// En mode statique le panneau occupe sa cellule (« l » / « r » du `view`) et devient
|
|
103
|
+
// sticky si la lettre est en majuscule ; en offcanvas il est `fixed` (recouvrant),
|
|
104
|
+
// donc sa place dans la grille ne compte plus.
|
|
105
|
+
const layout = inject(qLayoutKey, null)
|
|
106
|
+
const layoutZone = computed(() => layout?.zones.value[props.side])
|
|
107
|
+
|
|
100
108
|
// « Retour » navigateur → ferme la sidebar (mode offcanvas uniquement)
|
|
101
109
|
const overlayOpen = computed(() => !isStatic.value && open.value)
|
|
102
110
|
useOverlayBack(overlayOpen, () => setOpen(false), "QSidebar")
|
|
@@ -131,7 +139,8 @@ const rootClasses = computed(() =>
|
|
|
131
139
|
isStatic.value ? "q-sidebar--static" : "q-sidebar--offcanvas",
|
|
132
140
|
!isStatic.value && open.value && "q-sidebar--open",
|
|
133
141
|
props.side === "right" && "q-sidebar--right",
|
|
134
|
-
|
|
142
|
+
// sticky : prop explicite, ou imposé par la casse de la lettre dans un QLayout
|
|
143
|
+
(props.sticky || !!layoutZone.value?.fixed) && "q-sidebar--sticky",
|
|
135
144
|
props.bordered && "q-sidebar--bordered",
|
|
136
145
|
props.elevated && "q-sidebar--elevated",
|
|
137
146
|
props.dark && "q-sidebar--dark",
|
|
@@ -143,6 +152,14 @@ const rootStyle = computed<Record<string, string>>(() => {
|
|
|
143
152
|
const style: Record<string, string> = { "--q-sidebar-w": props.width }
|
|
144
153
|
if (props.height) style.height = props.height
|
|
145
154
|
if (props.maxHeight) style.maxHeight = props.maxHeight
|
|
155
|
+
// Cellule de la grille QLayout (mode statique : en offcanvas le panneau est fixed)
|
|
156
|
+
const zone = layoutZone.value
|
|
157
|
+
if (isStatic.value && zone?.area) style.gridArea = zone.area
|
|
158
|
+
// Sticky : ne s'accroche qu'aux barres qui le surplombent (rangée 0 = au-dessus de lui)
|
|
159
|
+
if (isStatic.value && zone?.fixed) {
|
|
160
|
+
if (zone.cells.some(([row]) => row === 0)) style["--q-sidebar-sticky-top"] = "0px"
|
|
161
|
+
if (zone.cells.some(([row]) => row === 2)) style["--q-sidebar-sticky-bottom"] = "0px"
|
|
162
|
+
}
|
|
146
163
|
return style
|
|
147
164
|
})
|
|
148
165
|
|