@dnax/ui 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
- // QGrid — grille CSS (12 colonnes par défaut) : <q-grid :cols="12" gap="16px">
3
- // Breakpoints configurables via les variables CSS --q-grid-bp-* (:root par défaut).
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
- const style = computed<Record<string, string>>(() => ({
35
- "--q-grid-cols": String(props.cols),
36
- "--q-grid-gap": px(props.gap)!,
37
- ...(props.rowGap !== undefined ? { "--q-grid-row-gap": px(props.rowGap)! } : {}),
38
- ...(props.columnGap !== undefined ? { "--q-grid-column-gap": px(props.columnGap)! } : {}),
39
- alignItems: props.align,
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>
@@ -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
- /** Indicateur en haut au lieu du bas */
62
- switchIndicator?: boolean
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
- switchIndicator: false,
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
- if (props.activeColor) style["--q-tabs-active-color"] = props.activeColor
159
- if (props.activeBgColor) style["--q-tabs-active-bg"] = props.activeBgColor
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
- props.switchIndicator && "q-tabs--switch-indicator",
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"
@@ -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 frères qui le précèdent (le contenu ne se retrouve plus sous le header)
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 détectées par le sélecteur FIXED_BAR_SELECTOR parmi les
11
- // frères du même parent ; la hauteur est observée (ResizeObserver) pour suivre
12
- // les changements de taille (contenu, safe-area).
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
- /** Hauteur cumulée des barres fixed qui précèdent `el` (frères du même parent) */
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 parent = el.parentElement
21
- if (!parent) return 0
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 child of parent.children) {
24
- if (child === el) break
25
- if (child instanceof HTMLElement && child.matches(FIXED_BAR_SELECTOR)) {
26
- total += child.offsetHeight
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
- /** Barres fixed frères de `el` (pour l'observation ResizeObserver) */
33
- function fixedBarSiblings(el: HTMLElement): HTMLElement[] {
34
- const parent = el.parentElement
35
- if (!parent) return []
36
- return Array.from(parent.children).filter(
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
- observe()
102
+ sync()
103
+ // Barres ajoutées/retirées dynamiquement (rendu conditionnel) → re-synchronise
104
+ const root = el.value ? layoutRoot(el.value) : null
105
+ if (root && typeof MutationObserver !== "undefined") {
106
+ mo = new MutationObserver(() => {
107
+ sync()
108
+ apply()
109
+ })
110
+ mo.observe(root, { childList: true, subtree: true })
111
+ }
92
112
  window.addEventListener("resize", apply)
93
113
  })
94
114
  onBeforeUnmount(() => {
95
115
  ro?.disconnect()
116
+ mo?.disconnect()
96
117
  window.removeEventListener("resize", apply)
97
118
  })
98
119
 
package/lib/q.ts CHANGED
@@ -248,7 +248,12 @@ function notify(opts: NotifyOptions): NotifyController {
248
248
  return { dismiss: () => toast.dismiss(id) }
249
249
  }
250
250
 
251
- /** API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading */
251
+ // API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading
252
+ // Conventions Quasar : .open() pour les piles (dialog, bottomSheet, imagePreview),
253
+ // .show() pour notify et loading.
254
+ const notifyPlugin = notify as typeof notify & { show: typeof notify }
255
+ notifyPlugin.show = notify
256
+
252
257
  export const $q = {
253
258
  dialog: {
254
259
  open: openDialog,
@@ -259,7 +264,7 @@ export const $q = {
259
264
  imagePreview: {
260
265
  open: openImagePreview,
261
266
  },
262
- notify,
267
+ notify: notifyPlugin,
263
268
  platform,
264
269
  breakpoints: qBreakpoints,
265
270
  screen,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
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
- /* indicateur en haut (switch-indicator) */
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,