@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.
@@ -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) | modal (centré) | sheet (bottom sheet) | dialog (plein écran).
5
- import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
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 (modal/sheet) */
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 naviguer
77
- const panelOpen = computed(() => isPanelMode.value && open.value)
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
- if (typeof document !== "undefined" && isPanelMode.value) {
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="`q-date-picker__overlay--${mode}`"
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
- <div class="q-date-picker__sheet-header">
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>
@@ -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 />
@@ -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>
@@ -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
- props.sticky && "q-sidebar--sticky",
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