@dnax/ui 0.0.76 → 0.0.77

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.
@@ -234,7 +234,8 @@ import {
234
234
  } from "../lib/spreadsheet"
235
235
  import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
236
236
  import { pageInfo } from "../lib/pagination"
237
- import { cellFontFor, cellSizePx, cellWidthFor } from "../lib/cellSize"
237
+ import { fxTextFor, searchTextFor } from "../lib/spreadsheetDisplay"
238
+ import { cellFontFor, cellSizePx, cellWidthFor, headerMetricsFor } from "../lib/cellSize"
238
239
  import type { CellSizePreset } from "../lib/cellSize"
239
240
  import { attachmentsOf as fileAttachmentsOf, fileIconKind, formatFileSize, isAttachment, selectFiles } from "../lib/fileList"
240
241
  import type { FileIconKind } from "../lib/fileList"
@@ -672,11 +673,19 @@ const emit = defineEmits<{
672
673
  const cellHeightPx = computed(() => props.rowHeight ?? cellSizePx(props.sizeCell))
673
674
  const cellWidthPx = computed(() => props.defaultColWidth ?? cellWidthFor(cellHeightPx.value))
674
675
  const cellFontPx = computed(() => cellFontFor(cellHeightPx.value))
676
+ /** En-têtes accordés aux cellules (hauteur, gouttière, polices) */
677
+ const headerMetrics = computed(() => headerMetricsFor(cellHeightPx.value))
675
678
 
676
679
  /** Variables CSS de la grille (police des cellules + hauteur de repli) */
677
680
  const cellSizeStyle = computed(() => ({
678
681
  "--q-spreadsheet-font": `${cellFontPx.value}px`,
679
682
  "--q-spreadsheet-row-height": `${cellHeightPx.value}px`,
683
+ // En-têtes : mêmes proportions que les cellules
684
+ "--q-spreadsheet-head-height": `${headerMetrics.value.height}px`,
685
+ "--q-spreadsheet-head-font": `${headerMetrics.value.labelFont}px`,
686
+ "--q-spreadsheet-head-letter-font": `${headerMetrics.value.letterFont}px`,
687
+ "--q-spreadsheet-row-font": `${headerMetrics.value.rowFont}px`,
688
+ "--q-spreadsheet-gutter-width": `${headerMetrics.value.gutter}px`,
680
689
  }))
681
690
 
682
691
  // ─── Clés de ligne (`_key`) ───
@@ -913,12 +922,14 @@ const choiceText = (col: QSpreadsheetColumn | undefined, raw: any): string => {
913
922
 
914
923
  // ─── Sélection (ancre + plage) ───
915
924
  const sel = ref<QSpreadsheetSelection | null>(null)
925
+ // `immediate` : une cellule **présélectionnée** (`:selected`) doit être active dès le premier
926
+ // rendu (SSR compris) — sans ça, elle était ignorée jusqu'au premier changement de prop.
916
927
  watch(
917
928
  () => props.selected,
918
929
  (v) => {
919
930
  if (v !== sel.value) sel.value = v ? { ...v } : null
920
931
  },
921
- { deep: true },
932
+ { deep: true, immediate: true },
922
933
  )
923
934
  const setSel = (s: QSpreadsheetSelection | null) => {
924
935
  sel.value = s
@@ -2339,13 +2350,17 @@ const fxInputEl = ref<HTMLInputElement | null>(null)
2339
2350
 
2340
2351
  const syncFx = () => {
2341
2352
  const s = sel.value
2353
+ const col = s ? colOf(s.column) : undefined
2342
2354
  const raw =
2343
2355
  s && s.row >= 0 && s.row < state.value.length
2344
2356
  ? state.value[s.row]?.[s.column]
2345
2357
  : undefined
2346
- fxDraft.value = raw === null || raw === undefined ? "" : String(raw)
2358
+ // Fichier joint : rien à montrer en fx (`String(raw)` affichait « [object Object]… »)
2359
+ fxDraft.value = fxTextFor(raw, col?.type)
2347
2360
  }
2348
- watch([sel, state], syncFx)
2361
+ // `immediate` : avec une cellule présélectionnée (`selected`), la barre fx affiche la
2362
+ // valeur dès le premier rendu (et en SSR) au lieu de rester vide jusqu'au premier clic.
2363
+ watch([sel, state], syncFx, { immediate: true })
2349
2364
 
2350
2365
  const fxCanEdit = computed(() => {
2351
2366
  if (!sel.value) return false
@@ -2356,10 +2371,17 @@ const fxCanEdit = computed(() => {
2356
2371
  !!col &&
2357
2372
  col.type !== "boolean" &&
2358
2373
  col.type !== "select" &&
2359
- col.type !== "multiselect"
2374
+ col.type !== "multiselect" &&
2375
+ col.type !== "attachment"
2360
2376
  )
2361
2377
  })
2362
2378
 
2379
+ /** Placeholder de la barre fx : vide pour un fichier joint (rien à éditer ni à montrer) */
2380
+ const fxPlaceholderText = computed(() => {
2381
+ if (sel.value && colOf(sel.value.column)?.type === "attachment") return ""
2382
+ return fxCanEdit.value ? t("fxPlaceholder") : t("fxNoSel")
2383
+ })
2384
+
2363
2385
  /**
2364
2386
  * Aperçu **live** du résultat de la formule en cours de saisie dans la barre fx.
2365
2387
  * Évaluée dans le contexte de la ligne sélectionnée (références par nom de colonne
@@ -2833,8 +2855,8 @@ const clearFormatSelection = () => {
2833
2855
  // ════════ Freeze panes (gel lignes / colonnes) ════════
2834
2856
  const frozenRowsN = computed(() => Math.max(0, Math.min(props.frozenRows ?? 0, state.value.length)))
2835
2857
  const frozenColsN = computed(() => Math.max(0, Math.min(props.frozenCols ?? 0, cols.value.length)))
2836
- const gutterW = computed(() => (props.showRowNumbers ? 34 : 0))
2837
- const headH = computed(() => (props.showColumnHeaders ? 28 : 0))
2858
+ const gutterW = computed(() => (props.showRowNumbers ? headerMetrics.value.gutter : 0))
2859
+ const headH = computed(() => (props.showColumnHeaders ? headerMetrics.value.height : 0))
2838
2860
  const colWNum = (ci: number) => {
2839
2861
  const c = cols.value[ci]
2840
2862
  if (!c) return cellWidthPx.value
@@ -4192,7 +4214,8 @@ const findTextOf = (r: number, ci: number): string => {
4192
4214
  const raw = state.value[r]?.[col.name]
4193
4215
  // `label` peut être un nombre : findScan fait `.toLowerCase()` sur le retour
4194
4216
  if (col.type === "select" || col.type === "multiselect") return choiceText(col, raw)
4195
- return raw === null || raw === undefined ? "" : String(raw)
4217
+ // Fichier joint → noms de fichiers ; jamais « [object Object] » (cf. lib/spreadsheetDisplay)
4218
+ return searchTextFor(raw, col.type)
4196
4219
  }
4197
4220
  const findScan = () => {
4198
4221
  const q = findQuery.value
@@ -5171,7 +5194,7 @@ defineExpose({
5171
5194
  class="q-spreadsheet__fx-input"
5172
5195
  :value="fxDraft"
5173
5196
  :disabled="readonly || disable"
5174
- :placeholder="fxCanEdit ? t('fxPlaceholder') : t('fxNoSel')"
5197
+ :placeholder="fxPlaceholderText"
5175
5198
  spellcheck="false"
5176
5199
  @input="onFxInput"
5177
5200
  @keyup="syncFxCaret"
@@ -2,7 +2,7 @@
2
2
  // Le point sensible est la **non-régression** : `md` doit reproduire exactement les valeurs
3
3
  // historiques (32 px de haut, 160 px de large, 13 px de police).
4
4
  import { describe, expect, it } from "bun:test"
5
- import { CELL_SIZE_MAX, CELL_SIZE_MIN, CELL_SIZE_PX, cellFontFor, cellSizePx, cellWidthFor } from "./cellSize"
5
+ import { CELL_SIZE_MAX, CELL_SIZE_MIN, CELL_SIZE_PX, cellFontFor, cellSizePx, cellWidthFor, headerMetricsFor } from "./cellSize"
6
6
 
7
7
  describe("cellSizePx", () => {
8
8
  it("md est le défaut historique (32 px)", () => {
@@ -50,3 +50,41 @@ describe("cellFontFor", () => {
50
50
  expect(cellFontFor(120)).toBe(20)
51
51
  })
52
52
  })
53
+
54
+ describe("headerMetricsFor", () => {
55
+ it("md reproduit exactement l'historique (aucune régression)", () => {
56
+ expect(headerMetricsFor(32)).toEqual({
57
+ height: 28,
58
+ gutter: 34,
59
+ labelFont: 11.5,
60
+ letterFont: 9.5,
61
+ rowFont: 11,
62
+ })
63
+ })
64
+
65
+ it("sm réduit l'en-tête", () => {
66
+ const m = headerMetricsFor(28)
67
+ expect(m.height).toBe(25)
68
+ expect(m.gutter).toBe(28)
69
+ expect(m.labelFont).toBe(9.5)
70
+ })
71
+
72
+ it("lg et 66 agrandissent l'en-tête", () => {
73
+ expect(headerMetricsFor(40)).toEqual({
74
+ height: 35,
75
+ gutter: 41,
76
+ labelFont: 14,
77
+ letterFont: 12,
78
+ rowFont: 14,
79
+ })
80
+ const big = headerMetricsFor(66)
81
+ expect(big.height).toBe(56) // plafonné
82
+ expect(big.gutter).toBe(47)
83
+ expect(big.labelFont).toBe(16)
84
+ })
85
+
86
+ it("la gouttière s'élargit avec la police (numéros à 4-5 chiffres)", () => {
87
+ expect(headerMetricsFor(48).gutter).toBeGreaterThan(headerMetricsFor(32).gutter)
88
+ expect(headerMetricsFor(120).gutter).toBeLessThanOrEqual(56)
89
+ })
90
+ })
package/lib/cellSize.ts CHANGED
@@ -41,3 +41,39 @@ export const cellWidthFor = (heightPx: number): number => Math.round(heightPx *
41
41
  */
42
42
  export const cellFontFor = (heightPx: number): number =>
43
43
  Math.min(20, Math.max(11, Math.round(heightPx * 0.406)))
44
+
45
+ /** Métriques de la ligne d'en-tête (noms de colonnes + numéros de ligne). */
46
+ export interface HeaderMetrics {
47
+ /** Hauteur de la ligne d'en-tête, en px */
48
+ height: number
49
+ /** Largeur de la gouttière des numéros de ligne, en px */
50
+ gutter: number
51
+ /** Police du libellé de colonne, en px */
52
+ labelFont: number
53
+ /** Police de la lettre de colonne (A, B, C…), en px */
54
+ letterFont: number
55
+ /** Police des numéros de ligne, en px */
56
+ rowFont: number
57
+ }
58
+
59
+ const clamp = (value: number, min: number, max: number): number =>
60
+ Math.min(max, Math.max(min, value))
61
+
62
+ /**
63
+ * En-têtes **accordés à la taille des cellules** : une cellule de 66 px avec un en-tête de
64
+ * 28 px (et une police de 11 px) jurerait. À `md` (32 px), toutes les valeurs retombent
65
+ * **exactement** sur l'historique (28 px de haut, gouttière 34, libellé 11.5, lettre 9.5,
66
+ * numéro 11) — donc aucune régression sans `sizeCell`.
67
+ */
68
+ export const headerMetricsFor = (cellHeightPx: number): HeaderMetrics => {
69
+ const cellFont = cellFontFor(cellHeightPx)
70
+ // Arrondi au demi-px : `md` tombe pile sur 11.5 (valeur historique)
71
+ const labelFont = clamp(Math.round(cellFont * 1.77) / 2, 9.5, 16)
72
+ return {
73
+ height: clamp(Math.round(cellHeightPx * 0.875), 24, 56),
74
+ gutter: clamp(Math.round(labelFont * 2.96), 28, 56),
75
+ labelFont,
76
+ letterFont: clamp(labelFont - 2, 8, 13),
77
+ rowFont: clamp(Math.round(cellFont * 0.846), 9, 15),
78
+ }
79
+ }
@@ -0,0 +1,49 @@
1
+ // Textes « plats » d'une cellule : barre de formule (fx) et recherche.
2
+ // Le cas d'origine : une cellule `attachment` (tableau d'objets) affichait
3
+ // « [object Object],[object Object] » dans fx.
4
+ import { describe, expect, it } from "bun:test"
5
+ import { fxTextFor, searchTextFor } from "./spreadsheetDisplay"
6
+
7
+ const files = [
8
+ { id: "a", name: "login.png", size: 12, type: "image/png" },
9
+ { id: "b", name: "dashboard.png", size: 34, type: "image/png" },
10
+ ]
11
+
12
+ describe("fxTextFor", () => {
13
+ it("n'affiche rien pour un fichier joint", () => {
14
+ expect(fxTextFor(files, "attachment")).toBe("")
15
+ expect(fxTextFor([], "attachment")).toBe("")
16
+ expect(fxTextFor(null, "attachment")).toBe("")
17
+ })
18
+
19
+ it("ne laisse jamais fuiter [object Object]", () => {
20
+ expect(fxTextFor({ name: "x" })).toBe("")
21
+ expect(fxTextFor([{ a: 1 }, { b: 2 }])).toBe("")
22
+ })
23
+
24
+ it("garde le texte des valeurs simples", () => {
25
+ expect(fxTextFor("Ada")).toBe("Ada")
26
+ expect(fxTextFor(18.5)).toBe("18.5")
27
+ expect(fxTextFor(true)).toBe("true")
28
+ expect(fxTextFor("=A1*2")).toBe("=A1*2")
29
+ expect(fxTextFor(null)).toBe("")
30
+ expect(fxTextFor(undefined)).toBe("")
31
+ })
32
+
33
+ it("joint proprement un tableau scalaire (multiselect)", () => {
34
+ expect(fxTextFor(["tea", "beer"])).toBe("tea, beer")
35
+ expect(fxTextFor(["tea", null, "beer"])).toBe("tea, beer")
36
+ })
37
+ })
38
+
39
+ describe("searchTextFor", () => {
40
+ it("indexe les noms de fichiers", () => {
41
+ expect(searchTextFor(files, "attachment")).toBe("login.png, dashboard.png")
42
+ })
43
+
44
+ it("retombe sur le texte simple", () => {
45
+ expect(searchTextFor("rapport.pdf", "string")).toBe("rapport.pdf")
46
+ expect(searchTextFor(null, "string")).toBe("")
47
+ expect(searchTextFor({ a: 1 })).toBe("")
48
+ })
49
+ })
@@ -0,0 +1,42 @@
1
+ // Texte d'une valeur de cellule pour les surfaces **textuelles** du tableur :
2
+ // barre de formule (`fx`) et moteur de recherche (Find).
3
+ //
4
+ // Piège visé : `String(valeur)` sur un objet/tableau produit « [object Object] ». Les types
5
+ // à valeur structurée (`attachment`, et tout futur type objet) doivent donc passer par ici
6
+ // au lieu d'un `String()` sauvage.
7
+ import { attachmentsOf } from "./fileList"
8
+
9
+ /** Valeur affichable en texte : `null`/objet → `""`, tableau → éléments joints. */
10
+ const plain = (value: unknown): string => {
11
+ if (value === null || value === undefined) return ""
12
+ if (Array.isArray(value)) {
13
+ return value
14
+ .map((entry) => (entry === null || entry === undefined || typeof entry === "object" ? "" : String(entry)))
15
+ .filter((text) => text !== "")
16
+ .join(", ")
17
+ }
18
+ if (typeof value === "object") return ""
19
+ return String(value)
20
+ }
21
+
22
+ /**
23
+ * Contenu de la **barre de formule** pour une cellule.
24
+ * `attachment` → `""` (la valeur est un tableau d'objets : rien de pertinent à éditer).
25
+ */
26
+ export const fxTextFor = (value: unknown, type?: string): string => {
27
+ if (type === "attachment") return ""
28
+ return plain(value)
29
+ }
30
+
31
+ /**
32
+ * Texte indexé par la **recherche** (Find & Replace).
33
+ * `attachment` → les **noms de fichiers**, pour pouvoir chercher par nom.
34
+ */
35
+ export const searchTextFor = (value: unknown, type?: string): string => {
36
+ if (type === "attachment") {
37
+ return attachmentsOf(value)
38
+ .map((att) => att.name)
39
+ .join(", ")
40
+ }
41
+ return plain(value)
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.76",
3
+ "version": "0.0.77",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -9690,9 +9690,9 @@ a.q-breadcrumbs__el:hover {
9690
9690
  top: 0;
9691
9691
  left: 0;
9692
9692
  z-index: 6;
9693
- width: 34px;
9694
- min-width: 34px;
9695
- height: 28px;
9693
+ width: var(--q-spreadsheet-gutter-width, 34px);
9694
+ min-width: var(--q-spreadsheet-gutter-width, 34px);
9695
+ height: var(--q-spreadsheet-head-height, 28px);
9696
9696
  border-right: 1px solid var(--q-spreadsheet-border);
9697
9697
  border-bottom: 1px solid var(--q-spreadsheet-border);
9698
9698
  text-align: center;
@@ -9702,7 +9702,7 @@ a.q-breadcrumbs__el:hover {
9702
9702
  .q-spreadsheet__colhead {
9703
9703
  top: 0;
9704
9704
  z-index: 4;
9705
- height: 28px;
9705
+ height: var(--q-spreadsheet-head-height, 28px);
9706
9706
  padding: 0 10px 0 6px;
9707
9707
  border-right: 1px solid var(--q-spreadsheet-border);
9708
9708
  border-bottom: 1px solid var(--q-spreadsheet-border);
@@ -9723,7 +9723,7 @@ a.q-breadcrumbs__el:hover {
9723
9723
  padding-right: 6px;
9724
9724
  }
9725
9725
  .q-spreadsheet__colhead-letter {
9726
- font-size: 9.5px;
9726
+ font-size: var(--q-spreadsheet-head-letter-font, 9.5px);
9727
9727
  font-weight: 700;
9728
9728
  line-height: 1.1;
9729
9729
  letter-spacing: 0.04em;
@@ -9731,7 +9731,7 @@ a.q-breadcrumbs__el:hover {
9731
9731
  color: var(--primary, #1976d2);
9732
9732
  }
9733
9733
  .q-spreadsheet__colhead-label {
9734
- font-size: 11.5px;
9734
+ font-size: var(--q-spreadsheet-head-font, 11.5px);
9735
9735
  line-height: 1.25;
9736
9736
  overflow: hidden;
9737
9737
  text-overflow: ellipsis;
@@ -9739,11 +9739,11 @@ a.q-breadcrumbs__el:hover {
9739
9739
  .q-spreadsheet__rownum {
9740
9740
  left: 0;
9741
9741
  z-index: 3;
9742
- width: 34px;
9743
- min-width: 34px;
9742
+ width: var(--q-spreadsheet-gutter-width, 34px);
9743
+ min-width: var(--q-spreadsheet-gutter-width, 34px);
9744
9744
  border-right: 1px solid var(--q-spreadsheet-border);
9745
9745
  border-bottom: 1px solid var(--q-spreadsheet-border);
9746
- font-size: 11px;
9746
+ font-size: var(--q-spreadsheet-row-font, 11px);
9747
9747
  font-weight: 500;
9748
9748
  text-align: center;
9749
9749
  vertical-align: middle;