@dnax/ui 0.0.45 → 0.0.48

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.
@@ -39,9 +39,9 @@ interface Props {
39
39
  type?: "button" | "submit" | "reset"
40
40
  /** Rend un lien natif <a> */
41
41
  href?: string
42
- /** Fond transparent, texte coloré */
42
+ /** Fond transparent, texte coloré. Gagne sur `outline` (designs exclusifs) */
43
43
  flat?: boolean
44
- /** Fond transparent + bordure */
44
+ /** Fond transparent + bordure (ignoré si `flat`, cf. Quasar : le design `flat` retire aussi la bordure) */
45
45
  outline?: boolean
46
46
  /** Supprime l'ombre portée */
47
47
  unelevated?: boolean
@@ -110,8 +110,7 @@ const btnClasses = computed(() =>
110
110
  btnVariants({ size: sizeVariant.value }),
111
111
  isTokenColor(props.color) && `q-btn--${props.color}`,
112
112
  isTokenColor(props.textColor) && `q-btn--text-${props.textColor}`,
113
- props.flat && "q-btn--flat",
114
- props.outline && "q-btn--outline",
113
+ props.flat ? "q-btn--flat" : props.outline && "q-btn--outline",
115
114
  props.unelevated && "q-btn--unelevated",
116
115
  props.dense && "q-btn--dense",
117
116
  effectiveRadius.value === true && "q-btn--rounded",
@@ -9,7 +9,7 @@ interface Props {
9
9
  maxWidth?: string | number
10
10
  /** Supprime la largeur max (pleine largeur, padding conservé) */
11
11
  fluid?: boolean
12
- /** Padding horizontal (défaut 16px) */
12
+ /** Padding horizontal (défaut 12px) */
13
13
  padding?: string
14
14
  /** Effet d'arrière-plan : "grid" | "aurora" | "star" | "falling-stars" | "flickering-grid" | "interactive-grid" | "wave" (vagues qui glissent) */
15
15
  backgroundEffect?:
@@ -57,7 +57,7 @@ interface Props {
57
57
  const props = withDefaults(defineProps<Props>(), {
58
58
  maxWidth: "1200px",
59
59
  fluid: false,
60
- padding: "16px",
60
+ padding: "12px",
61
61
  })
62
62
 
63
63
  const IMG_FIT = new Set(["cover", "contain", "fill", "none", "scale-down"])
@@ -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
- /** Décalages en px par rapport à la RACINE (bloc conteneur du popup) — null avant mesure */
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
- popupTop.value = Math.round(anchor.bottom - rect.top + gap)
269
- popupBottom.value = Math.round(rect.bottom - anchor.top + gap)
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 fallback = `calc(100% + ${popupOffset.value}px)`
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" : "0"
290
- style.right = alignEnd ? "0" : "auto"
291
- style.width = width ?? "100%"
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 = "0"
294
- style.right = "0"
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
- const defaultRadius = props.mode === "modal" ? "20px" : "12px"
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
- <div
756
- v-if="open && mode === 'inline'"
757
- ref="popupRef"
758
- class="q-select__popup"
759
- :class="[inlineOptions?.class, popupDirection === 'up' && 'q-select__popup--up']"
760
- :style="[popupStyle, inlineOptions?.style]"
761
- role="listbox"
762
- @mousedown.prevent
763
- >
764
- <div v-if="searchEnabled && !useInput" class="q-select__search" @mousedown.stop>
765
- <Icon :icon="icons.search" class="q-select__search-icon" aria-hidden="true" />
766
- <input
767
- class="q-select__search-input"
768
- :value="query"
769
- :placeholder="effectiveSearchPlaceholder"
770
- @input="onInput"
771
- @keydown="onKeydown"
772
- />
773
- </div>
774
- <template v-if="displayOptions.length">
775
- <div
776
- v-for="(opt, i) in displayOptions"
777
- :key="String(getOptionValue(opt))"
778
- role="option"
779
- :aria-selected="isSelected(opt) ? 'true' : 'false'"
780
- class="q-select__option"
781
- :class="{
782
- 'q-select__option--active': i === activeIndex,
783
- 'q-select__option--selected': isSelected(opt),
784
- }"
785
- @mouseenter="activeIndex = i"
786
- @mousedown.prevent="select(opt)"
787
- >
788
- <slot :opt="opt" :index="i" :selected="isSelected(opt)">
789
- <span class="q-select__option-label">{{ getOptionLabel(opt) }}</span>
790
- </slot>
791
- <Icon :icon="icons.check" v-if="multiple && isSelected(opt)" class="q-select__check" aria-hidden="true" />
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
- </div>
832
+ </Teleport>
798
833
 
799
834
  <!-- Mode modal / sheet / dialog : panneau téléporté -->
800
835
  <Teleport to="body">
@@ -69,6 +69,13 @@ const props = withDefaults(defineProps<Props>(), {
69
69
 
70
70
  const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
71
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
+
72
79
  // — Mode statique (viewport >= breakpoint && show-if-above) —
73
80
  const isAbove = ref(false)
74
81
  let mql: MediaQueryList | null = null
@@ -238,6 +245,7 @@ watch(open, (v) => {
238
245
  class="q-sidebar"
239
246
  :class="rootClasses"
240
247
  :style="[rootStyle, props.style]"
248
+ v-bind="$attrs"
241
249
  :aria-hidden="!isStatic && !open ? 'true' : undefined"
242
250
  @pointerdown="onPointerDown"
243
251
  @pointermove="onPointerMove"
@@ -816,6 +816,7 @@ defineExpose({
816
816
  :disable="!editor.isEditable"
817
817
  aria-label="Font size"
818
818
  class="q-tiptap__font-size"
819
+ :inline-options="{ class: 'q-tiptap__font-size-popup', width: '100px' }"
819
820
  @update:model-value="onFontSizeUpdate"
820
821
  />
821
822
  </div>
@@ -0,0 +1,36 @@
1
+ // Tests unitaires de la résolution du radius (échelle + style CSS).
2
+ import { describe, expect, it } from "bun:test"
3
+ import { RADIUS_VALUES, radiusStyle, radiusValue } from "./useComponentProps"
4
+
5
+ describe("radiusValue", () => {
6
+ it("traduit chaque barreau de l'échelle", () => {
7
+ expect(radiusValue("none")).toBe("0px")
8
+ expect(radiusValue("xs")).toBe("2px")
9
+ expect(radiusValue("sm")).toBe("4px")
10
+ expect(radiusValue("md")).toBe("8px")
11
+ expect(radiusValue("lg")).toBe("16px")
12
+ })
13
+
14
+ it("ne traduit ni la forme native (true) ni l'absence de valeur", () => {
15
+ expect(radiusValue(true)).toBeUndefined()
16
+ expect(radiusValue(false)).toBeUndefined()
17
+ expect(radiusValue(undefined)).toBeUndefined()
18
+ })
19
+
20
+ it("reste aligné sur RADIUS_VALUES", () => {
21
+ for (const [scale, value] of Object.entries(RADIUS_VALUES)) {
22
+ expect(radiusValue(scale as keyof typeof RADIUS_VALUES)).toBe(value)
23
+ }
24
+ })
25
+ })
26
+
27
+ describe("radiusStyle", () => {
28
+ it("pose --q-radius pour une valeur d'échelle", () => {
29
+ expect(radiusStyle("lg")).toEqual({ "--q-radius": "16px" })
30
+ })
31
+
32
+ it("ne pose rien pour true (forme native) ou undefined", () => {
33
+ expect(radiusStyle(true)).toBeUndefined()
34
+ expect(radiusStyle(undefined)).toBeUndefined()
35
+ })
36
+ })
@@ -55,7 +55,13 @@ export function useRadius(
55
55
  return computed(() => toValue(own) ?? (defaults.value.radius as RadiusProp | undefined))
56
56
  }
57
57
 
58
+ /** Valeur CSS concrète d'une échelle de radius (`undefined` si `true` (forme native) ou absent) */
59
+ export function radiusValue(radius: RadiusProp | undefined): string | undefined {
60
+ return isRadiusScale(radius) ? RADIUS_VALUES[radius] : undefined
61
+ }
62
+
58
63
  /** Style `--q-radius` à poser sur la racine du composant pour une valeur d'échelle. */
59
64
  export function radiusStyle(radius: RadiusProp | undefined): Record<string, string> | undefined {
60
- return isRadiusScale(radius) ? { "--q-radius": RADIUS_VALUES[radius] } : undefined
65
+ const value = radiusValue(radius)
66
+ return value ? { "--q-radius": value } : undefined
61
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.45",
3
+ "version": "0.0.48",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -32,18 +32,22 @@ body {
32
32
  /* ===== Échelle des z-index =====
33
33
  Règle : un menu superposé ne doit JAMAIS passer sous un overlay. Les menus
34
34
  téléportés dans <body> sont donc au-dessus de la couche modale.
35
- 1000 popups en flux (`position: absolute` dans leur champ : select,
36
- autocomplete, nav-menu…) → pas concernés par les overlays
35
+ 1000 popups en flux (`position: absolute` dans leur champ : autocomplete,
36
+ nav-menu…) → pas concernés par les overlays
37
37
  1999/2000 sidebar offcanvas + backdrop
38
38
  3000 overlays modaux : dialog, bottom sheet, action sheet,
39
39
  date-picker, country-picker, select sheet
40
40
  3200 menus téléportés dans <body> : panneau QBtnActions, menu
41
41
  contextuel / filtre / suggestions du tableur → --q-z-menu
42
+ 3500 popup inline de QSelect (téléporté dans <body> : il doit passer
43
+ au-dessus d'un overlay quand le champ est dans un dialog/sheet)
44
+ → --q-z-popup
42
45
  4000 palettes tiptap · 7000 loading global · 9999 tooltip ·
43
46
  10000 image preview
44
- Surchargez --q-z-menu si votre application a un overlay plus haut que 3200. */
47
+ Surchargez --q-z-menu ou --q-z-popup si votre application a un overlay plus haut. */
45
48
  :root {
46
49
  --q-z-menu: 3200;
50
+ --q-z-popup: 3500;
47
51
  }
48
52
 
49
53
  /* ===== Safe-area insets (encoche & barre d'accueil) =====
@@ -316,7 +320,6 @@ body {
316
320
  --muted: #1c2128;
317
321
  --border: rgb(255 255 255 / 0.12);
318
322
  --q-table-border: rgb(255 255 255 / 0.12);
319
- --q-table-header-bg: rgb(255 255 255 / 0.04);
320
323
  --q-table-hover-bg: rgb(255 255 255 / 0.06);
321
324
  --q-sidebar-border: rgb(255 255 255 / 0.12);
322
325
  --q-translucent-bg: #0f1115;
@@ -410,7 +413,9 @@ html.dark body {
410
413
  }
411
414
  .dark .q-data-grid {
412
415
  --q-data-grid-border: rgb(255 255 255 / 0.12);
413
- --q-data-grid-header-bg: rgb(255 255 255 / 0.04);
416
+ /* Opaque (cf. QTable : en-tête sticky sur surface composée) */
417
+ --q-data-grid-header-bg: #1f242b;
418
+ --q-data-grid-header-bg: color-mix(in srgb, #fff 4%, var(--card, #161b22));
414
419
  }
415
420
 
416
421
  /* Footer de table (pagination) en dark : range + sélecteur lignes/page */
@@ -3209,7 +3214,10 @@ html.dark body {
3209
3214
  /* ===== QTable ===== */
3210
3215
  .q-table {
3211
3216
  --q-table-border: rgb(0 0 0 / 0.1);
3212
- --q-table-header-bg: rgb(0 0 0 / 0.03);
3217
+ /* En-tête : la MÊME surface que le tableau (blanc en clair, carte en sombre) — il se
3218
+ distingue par sa bordure basse, pas par un fond grisé. Opaque par construction,
3219
+ donc un en-tête collant ne laisse pas voir les lignes qui défilent dessous. */
3220
+ --q-table-header-bg: var(--card, #fff);
3213
3221
  --q-table-hover-bg: rgb(0 0 0 / 0.04);
3214
3222
 
3215
3223
  position: relative;
@@ -3233,7 +3241,8 @@ html.dark body {
3233
3241
 
3234
3242
  .q-table--dark {
3235
3243
  --q-table-border: rgb(255 255 255 / 0.15);
3236
- --q-table-header-bg: rgb(255 255 255 / 0.06);
3244
+ /* = la surface du tableau dans cette variante (cf. `background-color` ci-dessous) */
3245
+ --q-table-header-bg: #1d1d1d;
3237
3246
  --q-table-hover-bg: rgb(255 255 255 / 0.06);
3238
3247
  background-color: #1d1d1d;
3239
3248
  color: #fff;
@@ -4436,11 +4445,15 @@ a.q-breadcrumbs__el:hover {
4436
4445
  transition: background-color 0.15s ease;
4437
4446
  }
4438
4447
 
4439
- .q-sidebar__menu-button:hover {
4448
+ /* Hover : uniquement sur un item NON actif — sans le `:not(...)`, la règle `:hover`
4449
+ (spécificité 0,2,0) écrasait l'état actif `.q-sidebar__menu-button--active`
4450
+ (0,1,0) : au survol, le fond passait au gris et le texte blanc du primaire devenait
4451
+ illisible. Un item actif garde donc son apparence au survol. */
4452
+ .q-sidebar__menu-button:hover:not(.q-sidebar__menu-button--active) {
4440
4453
  background-color: rgb(0 0 0 / 0.05);
4441
4454
  }
4442
4455
 
4443
- .q-sidebar--dark .q-sidebar__menu-button:hover {
4456
+ .q-sidebar--dark .q-sidebar__menu-button:hover:not(.q-sidebar__menu-button--active) {
4444
4457
  background-color: rgb(255 255 255 / 0.08);
4445
4458
  }
4446
4459
 
@@ -4835,12 +4848,16 @@ a.q-breadcrumbs__el:hover {
4835
4848
  transform: rotate(180deg);
4836
4849
  }
4837
4850
 
4851
+ /* Popup inline : téléporté dans <body> et en `position: fixed` — en `position:
4852
+ absolute` il restait **rogné** par le premier ancêtre en `overflow: hidden` (un
4853
+ q-card, par exemple), et un `z-index` ne franchit JAMAIS un clip. Les coordonnées
4854
+ fenêtre (top/bottom/left/width) sont posées en inline par `positionPopup()`, qui
4855
+ le repositionne aussi au scroll et au resize. */
4838
4856
  .q-select__popup {
4839
- position: absolute;
4840
- top: calc(100% + 8px);
4857
+ position: fixed;
4858
+ top: 0;
4841
4859
  left: 0;
4842
- right: 0;
4843
- z-index: 1000;
4860
+ z-index: var(--q-z-popup, 3500);
4844
4861
  max-height: 240px;
4845
4862
  overflow-y: auto;
4846
4863
  padding: 4px 0;
@@ -7294,7 +7311,7 @@ a.q-breadcrumbs__el:hover {
7294
7311
  width: 100%;
7295
7312
  max-width: var(--q-container-max, 1200px);
7296
7313
  margin-inline: auto;
7297
- padding-inline: var(--q-container-pad, 16px);
7314
+ padding-inline: var(--q-container-pad, 12px);
7298
7315
  }
7299
7316
 
7300
7317
  .q-container--fluid {
@@ -8648,7 +8665,9 @@ a.q-breadcrumbs__el:hover {
8648
8665
  /* ===== QDataGrid — grille type tableur (formules, édition, sélection clavier) ===== */
8649
8666
  .q-data-grid {
8650
8667
  --q-data-grid-border: rgb(0 0 0 / 0.1);
8651
- --q-data-grid-header-bg: rgb(0 0 0 / 0.03);
8668
+ /* Opaque (cf. QTable : en-tête sticky sur surface composée) */
8669
+ --q-data-grid-header-bg: #f7f7f7;
8670
+ --q-data-grid-header-bg: color-mix(in srgb, #000 3%, #fff);
8652
8671
 
8653
8672
  position: relative;
8654
8673
  display: flex;
@@ -8667,7 +8686,8 @@ a.q-breadcrumbs__el:hover {
8667
8686
  }
8668
8687
  .q-data-grid--dark {
8669
8688
  --q-data-grid-border: rgb(255 255 255 / 0.15);
8670
- --q-data-grid-header-bg: rgb(255 255 255 / 0.06);
8689
+ --q-data-grid-header-bg: #2b2b2b;
8690
+ --q-data-grid-header-bg: color-mix(in srgb, #fff 6%, #1d1d1d);
8671
8691
  background: #1d1d1d;
8672
8692
  color: #fff;
8673
8693
  }
@@ -9126,11 +9146,14 @@ a.q-breadcrumbs__el:hover {
9126
9146
  width: 17px;
9127
9147
  height: 17px;
9128
9148
  }
9129
- .q-tiptap__tool:hover:not(:disabled) {
9149
+ /* Hover : jamais sur un bouton actif (même règle que QSidebarMenuButton — sans le
9150
+ `:not(...)`, `:hover` (0,2,0) écrasait `.q-tiptap__tool--active` (0,1,0) : le bouton
9151
+ actif perdait son fond et sa couleur primaire au survol). */
9152
+ .q-tiptap__tool:hover:not(:disabled):not(.q-tiptap__tool--active) {
9130
9153
  background-color: rgb(0 0 0 / 0.06);
9131
9154
  color: var(--q-tiptap-text);
9132
9155
  }
9133
- .dark .q-tiptap__tool:hover:not(:disabled) {
9156
+ .dark .q-tiptap__tool:hover:not(:disabled):not(.q-tiptap__tool--active) {
9134
9157
  background-color: rgb(255 255 255 / 0.08);
9135
9158
  }
9136
9159
  .q-tiptap__tool--active {
@@ -9485,11 +9508,12 @@ a.q-breadcrumbs__el:hover {
9485
9508
  width: 15px;
9486
9509
  height: 15px;
9487
9510
  }
9488
- .q-tiptap__bubble-btn:hover:not(:disabled) {
9511
+ /* Idem : un bouton de bulle actif garde son apparence au survol */
9512
+ .q-tiptap__bubble-btn:hover:not(:disabled):not(.q-tiptap__bubble-btn--active) {
9489
9513
  background-color: rgb(0 0 0 / 0.06);
9490
9514
  color: var(--q-tiptap-text);
9491
9515
  }
9492
- .dark .q-tiptap__bubble-btn:hover:not(:disabled) {
9516
+ .dark .q-tiptap__bubble-btn:hover:not(:disabled):not(.q-tiptap__bubble-btn--active) {
9493
9517
  background-color: rgb(255 255 255 / 0.08);
9494
9518
  }
9495
9519
  .q-tiptap__bubble-btn--active {
@@ -9583,11 +9607,13 @@ a.q-breadcrumbs__el:hover {
9583
9607
  width: 14px;
9584
9608
  height: 14px;
9585
9609
  }
9586
- .q-tiptap__font-size .q-select__popup {
9587
- width: 100px;
9610
+ /* Popup inline téléporté dans <body> : impossible de le styler depuis le parent — on
9611
+ passe par la classe du popup (`inline-options.class`) ; la largeur, elle, vient de
9612
+ `inline-options.width` (l'inline posé par positionPopup() prime sur ce CSS). */
9613
+ .q-tiptap__font-size-popup {
9588
9614
  min-width: 100px;
9589
9615
  }
9590
- .q-tiptap__font-size .q-select__option-label {
9616
+ .q-tiptap__font-size-popup .q-select__option-label {
9591
9617
  font-size: 12px;
9592
9618
  }
9593
9619
 
@@ -9777,7 +9803,9 @@ a.q-breadcrumbs__el:hover {
9777
9803
  /* ===== QSpreadsheet (tableur type Excel) ===== */
9778
9804
  .q-spreadsheet {
9779
9805
  --q-spreadsheet-border: rgb(0 0 0 / 0.1);
9780
- --q-spreadsheet-header-bg: rgb(0 0 0 / 0.03);
9806
+ /* Opaque (cf. QTable : entêtes sticky des colonnes/lignes) */
9807
+ --q-spreadsheet-header-bg: #f7f7f7;
9808
+ --q-spreadsheet-header-bg: color-mix(in srgb, #000 3%, #fff);
9781
9809
  --q-spreadsheet-row-alt: rgb(0 0 0 / 0.015);
9782
9810
  --q-spreadsheet-range: rgb(25 118 210 / 0.1);
9783
9811
  --q-spreadsheet-range-brd: var(--primary, #1976d2);
@@ -9806,7 +9834,8 @@ a.q-breadcrumbs__el:hover {
9806
9834
  .q-spreadsheet--dark,
9807
9835
  .dark .q-spreadsheet {
9808
9836
  --q-spreadsheet-border: rgb(255 255 255 / 0.12);
9809
- --q-spreadsheet-header-bg: rgb(255 255 255 / 0.05);
9837
+ --q-spreadsheet-header-bg: #22262d;
9838
+ --q-spreadsheet-header-bg: color-mix(in srgb, #fff 5%, var(--card, #1d1d1d));
9810
9839
  --q-spreadsheet-row-alt: rgb(255 255 255 / 0.02);
9811
9840
  background: var(--card, #1d1d1d);
9812
9841
  color: var(--foreground, #ececef);