@dnax/ui 0.0.37 → 0.0.39

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,6 +1,26 @@
1
1
  <script lang="ts">
2
2
  // QSelect — options spécifiques par mode d'ouverture.
3
3
  // sheetOptions / modalOptions / inlineOptions : style, classes, hauteur, arrondi, placeholder de recherche.
4
+
5
+ /**
6
+ * Placement du popup `inline` (même vocabulaire que `DropdownPosition` de QBtnActions) :
7
+ *
8
+ * - `"auto"` — suit la place disponible : sous le champ, bascule au-dessus s'il n'y
9
+ * a pas la place. Le popup fait la largeur du champ.
10
+ * - `"top"` / `"bottom"` — force le côté (pas de bascule). Le popup fait la largeur
11
+ * du champ.
12
+ * - suffixe `"-start"` / `"-end"` — ancre le popup sur le bord GAUCHE / DROIT du
13
+ * champ ; il prend alors sa largeur naturelle (ou `inline-options.width`), donc il
14
+ * peut être plus étroit ou plus large que le champ.
15
+ *
16
+ * Les placements latéraux de `DropdownPosition` (`left`, `right`) ne sont pas repris :
17
+ * un popup de sélection s'ouvre toujours au-dessus ou au-dessous de son champ.
18
+ */
19
+ export type SelectPopupPosition =
20
+ | "auto"
21
+ | "bottom" | "bottom-start" | "bottom-end"
22
+ | "top" | "top-start" | "top-end"
23
+
4
24
  export interface QSelectModeOptions {
5
25
  /** Largeur du panneau/popup (remplace width) */
6
26
  width?: string
@@ -10,6 +30,10 @@ export interface QSelectModeOptions {
10
30
  class?: string
11
31
  /** Hauteur de la liste scrollable (remplace height) */
12
32
  height?: string
33
+ /** Écart champ ↔ popup (mode inline) — remplace la prop `offset` */
34
+ offset?: number
35
+ /** Placement du popup (mode inline) — remplace la prop `position` */
36
+ position?: SelectPopupPosition
13
37
  /** Arrondi du panneau (remplace rounded) */
14
38
  rounded?: boolean | string | { tl?: string | number; tr?: string | number; br?: string | number; bl?: string | number }
15
39
  /** Placeholder du champ de recherche pour ce mode */
@@ -95,6 +119,18 @@ interface Props {
95
119
  height?: string
96
120
  /** Arrondi du panneau : true (défaut du mode) | false (carré) | valeur CSS | objet { tl, tr, br, bl } */
97
121
  rounded?: boolean | string | { tl?: string | number; tr?: string | number; br?: string | number; bl?: string | number }
122
+ /**
123
+ * Écart entre le champ et le popup inline, en px (défaut : 8).
124
+ * Réduit automatiquement (jusqu'à 0) si la place manque sous/au-dessus du champ.
125
+ * Surchargeable par mode : `inline-options="{ offset: 8 }"`.
126
+ */
127
+ offset?: number
128
+ /**
129
+ * Placement du popup inline — voir `SelectPopupPosition`. Défaut : `"auto"`
130
+ * (bascule automatique selon la place disponible).
131
+ * Surchargeable par mode : `inline-options="{ position: 'top' }"`.
132
+ */
133
+ position?: SelectPopupPosition
98
134
  /** Options spécifiques au mode sheet (style, class, height, rounded, width, searchPlaceholder) */
99
135
  sheetOptions?: QSelectModeOptions
100
136
  /** Options spécifiques au mode modal */
@@ -125,6 +161,8 @@ const props = withDefaults(defineProps<Props>(), {
125
161
  loading: false,
126
162
  noOptionsLabel: "No options to display",
127
163
  mode: "inline",
164
+ offset: 8,
165
+ position: "auto",
128
166
  })
129
167
 
130
168
  const emit = defineEmits<{
@@ -172,10 +210,10 @@ const modeOptions = computed<QSelectModeOptions | undefined>(() => {
172
210
 
173
211
  // — Popup inline : direction, écart et hauteur max calculés selon l'espace visible —
174
212
  // Ouvre vers le bas ; s'il n'y a pas la place (POPUP_MIN_SPACE) et qu'au-dessus il y
175
- // en a davantage, il bascule au-dessus du champ. L'écart avec le champ se réduit
176
- // quand la place manque (jusqu'à 0) et la hauteur max est bornée à l'espace
177
- // disponible : le popup ne dépasse jamais le bord de la fenêtre.
178
- const POPUP_GAP = 4
213
+ // a davantage de place, il bascule au-dessus du champ. L'écart avec le champ
214
+ // (`offset`, 8px par défaut) se réduit quand la place manque (jusqu'à 0) et la
215
+ // hauteur max est bornée à l'espace disponible : le popup ne dépasse jamais le bord
216
+ // de la fenêtre.
179
217
  /** Espace vertical visé pour la liste : en dessous, on cherche de la place ailleurs */
180
218
  const POPUP_MIN_SPACE = 96
181
219
  /** Plafond historique de la liste scrollable */
@@ -184,34 +222,79 @@ const POPUP_MAX_HEIGHT = 240
184
222
  const VIEWPORT_MARGIN = 8
185
223
 
186
224
  const popupDirection = ref<"down" | "up">("down")
187
- const popupGap = ref(POPUP_GAP)
188
225
  const popupMaxHeight = ref(POPUP_MAX_HEIGHT)
226
+ /** Décalages en px par rapport à la RACINE (bloc conteneur du popup) — null avant mesure */
227
+ const popupTop = ref<number | null>(null)
228
+ const popupBottom = ref<number | null>(null)
229
+
230
+ /** Écart champ ↔ popup : `inline-options.offset` prioritaire sur la prop `offset` */
231
+ const popupOffset = computed(() => modeOptions.value?.offset ?? props.offset)
232
+
233
+ /** Placement demandé : `inline-options.position` prioritaire sur la prop `position` */
234
+ const popupPlacement = computed<SelectPopupPosition>(
235
+ () => modeOptions.value?.position ?? props.position,
236
+ )
189
237
 
190
238
  const positionPopup = () => {
191
239
  const el = rootEl.value
192
240
  if (!el || typeof window === "undefined") return
193
241
  const rect = el.getBoundingClientRect()
194
- const below = window.innerHeight - rect.bottom - VIEWPORT_MARGIN
195
- const above = rect.top - VIEWPORT_MARGIN
196
242
 
197
- const down = below >= POPUP_MIN_SPACE || below >= above
243
+ // Ancre = bas du CHAMP, pas bas de la racine : `.q-field__bottom` réserve
244
+ // ~24px (min-height 20px + padding-top 4px) même quand il est vide, ce qui
245
+ // éloignait le popup du champ. Quand un hint / une erreur est affiché, on
246
+ // garde le bas de la racine pour ne pas recouvrir le texte.
247
+ const control = el.querySelector<HTMLElement>(".q-field__control")
248
+ const hasBottomText = !!el.querySelector(".q-field__hint, .q-field__error")
249
+ const anchor = (!hasBottomText && control ? control : el).getBoundingClientRect()
250
+
251
+ const below = window.innerHeight - anchor.bottom - VIEWPORT_MARGIN
252
+ const above = anchor.top - VIEWPORT_MARGIN
253
+
254
+ // `auto` : sous le champ, bascule au-dessus s'il n'y a pas la place (et qu'il y en
255
+ // a plus au-dessus). `top` / `bottom` forcent le côté, sans bascule.
256
+ const placement = popupPlacement.value
257
+ const down =
258
+ placement === "auto"
259
+ ? below >= POPUP_MIN_SPACE || below >= above
260
+ : !placement.startsWith("top")
198
261
  const available = Math.max(down ? below : above, 0)
199
262
 
200
263
  // Écart réduit quand la place manque, pour laisser le maximum à la liste
201
- const gap = Math.max(0, Math.min(POPUP_GAP, available - POPUP_MIN_SPACE))
264
+ const gap = Math.max(0, Math.min(popupOffset.value, available - POPUP_MIN_SPACE))
202
265
 
203
266
  popupDirection.value = down ? "down" : "up"
204
- popupGap.value = gap
205
267
  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)
206
270
  }
207
271
 
208
272
  const popupStyle = computed<Record<string, string>>(() => {
209
- const edge = `calc(100% + ${popupGap.value}px)`
210
- return {
211
- top: popupDirection.value === "down" ? edge : "auto",
212
- bottom: popupDirection.value === "up" ? edge : "auto",
273
+ const up = popupDirection.value === "up"
274
+ const fallback = `calc(100% + ${popupOffset.value}px)`
275
+ 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",
213
278
  maxHeight: `${Math.round(popupMaxHeight.value)}px`,
214
279
  }
280
+
281
+ // Ancre horizontale : par défaut le popup couvre la largeur du champ. Les suffixes
282
+ // `-start` / `-end` (ou un `inline-options.width` explicite) l'ancrent sur un bord —
283
+ // l'effet est visible dès que le popup n'a pas la largeur du champ.
284
+ const placement = popupPlacement.value
285
+ const width = modeOptions.value?.width
286
+ const anchored = placement !== "auto" && placement.includes("-")
287
+ if (anchored || width) {
288
+ const alignEnd = placement.endsWith("-end")
289
+ style.left = alignEnd ? "auto" : "0"
290
+ style.right = alignEnd ? "0" : "auto"
291
+ style.width = width ?? "100%"
292
+ } else {
293
+ style.left = "0"
294
+ style.right = "0"
295
+ }
296
+
297
+ return style
215
298
  })
216
299
 
217
300
  // Le champ peut bouger (scroll) ou la fenêtre changer de taille : on recalcule
@@ -219,6 +302,10 @@ const onPopupViewportChange = () => {
219
302
  if (open.value && props.mode === "inline") positionPopup()
220
303
  }
221
304
 
305
+ // Un changement de `position` / `offset` (prop ou options de mode) pendant que le
306
+ // popup est ouvert doit le replacer tout de suite (ex. sélecteur de placement).
307
+ watch([popupPlacement, popupOffset], onPopupViewportChange)
308
+
222
309
  // — Options normalisées : les primitives (string/number) deviennent { value, label } —
223
310
  const isObjectOption = (o: any): boolean => o !== null && typeof o === "object"
224
311
 
@@ -4118,7 +4118,12 @@ defineExpose({
4118
4118
  @scroll="onViewportScroll"
4119
4119
  @contextmenu.prevent
4120
4120
  >
4121
- <table class="q-spreadsheet__table" cellspacing="0" :style="zoomTableStyle">
4121
+ <table
4122
+ class="q-spreadsheet__table"
4123
+ :class="{ 'q-spreadsheet__table--empty': cols.length === 0 }"
4124
+ cellspacing="0"
4125
+ :style="zoomTableStyle"
4126
+ >
4122
4127
  <thead v-if="showColumnHeaders">
4123
4128
  <tr class="q-spreadsheet__head-row">
4124
4129
  <th
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -264,6 +264,25 @@ html.dark body {
264
264
  color: rgb(255 255 255 / 0.65);
265
265
  }
266
266
 
267
+ /* Icônes de champ en dark : icône gauche + chevrons (select, autocomplete) +
268
+ loupe de recherche des listes déroulantes. Ces icônes ont une couleur NOIRE
269
+ codée en dur (`rgb(0 0 0 / …)` sur `.q-field__icon`, `.q-select__arrow`,
270
+ `.q-autocomplete__arrow`, `.q-select__search-icon`) : sans cette surcharge
271
+ elles restaient noires sur fond sombre → illisibles. */
272
+ .dark .q-field__icon,
273
+ .dark .q-select__arrow,
274
+ .dark .q-autocomplete__arrow,
275
+ .dark .q-select__search-icon,
276
+ .dark .q-autocomplete__search-icon {
277
+ color: var(--foreground);
278
+ }
279
+
280
+ /* État vide des listes déroulantes en dark (texte noir codé en dur) */
281
+ .dark .q-select__empty,
282
+ .dark .q-autocomplete__empty {
283
+ color: rgb(255 255 255 / 0.65);
284
+ }
285
+
267
286
  .dark .q-input .q-field__native::placeholder,
268
287
  .dark .q-input-tag__native::placeholder,
269
288
  .dark .q-input-otp__box::placeholder,
@@ -4709,7 +4728,7 @@ a.q-breadcrumbs__el:hover {
4709
4728
 
4710
4729
  .q-select__popup {
4711
4730
  position: absolute;
4712
- top: calc(100% + 4px);
4731
+ top: calc(100% + 8px);
4713
4732
  left: 0;
4714
4733
  right: 0;
4715
4734
  z-index: 1000;
@@ -9596,14 +9615,24 @@ a.q-breadcrumbs__el:hover {
9596
9615
  outline: none;
9597
9616
  }
9598
9617
  .q-spreadsheet__table {
9618
+ /* PAS de `min-width: 100%` : avec `table-layout: fixed`, un tableau étiré au-delà
9619
+ de la somme de ses colonnes RÉPARTIT l'espace libre sur toutes les colonnes
9620
+ (proportionnellement) — la gouttière des numéros de ligne (34px) gonflait donc
9621
+ jusqu'à ~70px sur une grille étroite, et les largeurs configurées de colonnes
9622
+ étaient ignorées. Ici le tableau fait exactement la somme des colonnes ;
9623
+ l'espace restant à droite reste vide (comportement tableur). */
9599
9624
  width: max-content;
9600
- min-width: 100%;
9601
9625
  border-collapse: separate;
9602
9626
  border-spacing: 0;
9603
9627
  table-layout: fixed;
9604
9628
  font-size: 13px;
9605
9629
  }
9606
9630
 
9631
+ /* Grille sans aucune colonne : un seul message → il doit occuper toute la largeur */
9632
+ .q-spreadsheet__table--empty {
9633
+ min-width: 100%;
9634
+ }
9635
+
9607
9636
  /* ─── En-têtes ─── */
9608
9637
  .q-spreadsheet__corner,
9609
9638
  .q-spreadsheet__colhead,