@dnax/ui 0.0.66 → 0.0.69
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.
- package/components/QChart.vue +329 -19
- package/components/QSpreadsheet.vue +194 -37
- package/index.ts +11 -1
- package/lib/chart.axis.test.ts +1 -1
- package/lib/chart.bi.test.ts +839 -0
- package/lib/chart.modules.test.ts +232 -0
- package/lib/chart.test.ts +13 -4
- package/lib/chart.ts +1400 -80
- package/lib/chartFormat.ts +222 -0
- package/lib/chartStats.ts +178 -0
- package/lib/formula.test.ts +8 -0
- package/lib/formula.ts +4 -2
- package/lib/history.test.ts +78 -0
- package/lib/history.ts +72 -0
- package/lib/spreadsheet.test.ts +110 -0
- package/lib/spreadsheet.ts +204 -5
- package/package.json +1 -1
- package/styles/main.css +65 -0
package/lib/spreadsheet.test.ts
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
isBlankValue,
|
|
7
7
|
isoAddDays,
|
|
8
8
|
shiftFormulaRefs,
|
|
9
|
+
functionQueryAt,
|
|
10
|
+
rankSuggestions,
|
|
11
|
+
functionCallAt,
|
|
12
|
+
signatureArgs,
|
|
9
13
|
csvEscape,
|
|
10
14
|
csvSplitLine,
|
|
11
15
|
parseCsv,
|
|
@@ -44,6 +48,112 @@ describe("shiftFormulaRefs (autofill)", () => {
|
|
|
44
48
|
it("laisse les valeurs sans lettres intactes", () => {
|
|
45
49
|
expect(shiftFormulaRefs("=1+2", 1, 1)).toBe("=1+2")
|
|
46
50
|
})
|
|
51
|
+
it("ne touche pas aux littéraux de chaîne", () => {
|
|
52
|
+
expect(shiftFormulaRefs('=IF(A1>0,"A1","x")', 1, 0)).toBe('=IF(A2>0,"A1","x")')
|
|
53
|
+
expect(shiftFormulaRefs("=CONCAT('B2',A1)", 0, 1)).toBe("=CONCAT('B2',B1)")
|
|
54
|
+
})
|
|
55
|
+
it("ne confond pas un identifiant avec une référence", () => {
|
|
56
|
+
expect(shiftFormulaRefs("=LOG10(A1)", 1, 0)).toBe("=LOG10(A2)")
|
|
57
|
+
expect(shiftFormulaRefs("=price1+A1", 1, 0)).toBe("=price1+A2")
|
|
58
|
+
expect(shiftFormulaRefs("=A1:price2", 1, 0)).toBe("=A2:price2")
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("functionQueryAt (autocomplétion fx)", () => {
|
|
63
|
+
it("rend l'identifiant en cours de frappe, en minuscules", () => {
|
|
64
|
+
expect(functionQueryAt("=SU")).toBe("su")
|
|
65
|
+
expect(functionQueryAt("=sum(")).toBe("")
|
|
66
|
+
expect(functionQueryAt("=SUM(A1)+IF")).toBe("if")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("ne suggère rien hors d'une formule ou dans une chaîne", () => {
|
|
70
|
+
expect(functionQueryAt("SUM")).toBe("")
|
|
71
|
+
expect(functionQueryAt('="hello wor')).toBe("")
|
|
72
|
+
expect(functionQueryAt("='hello wor")).toBe("")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("ne suggère pas au milieu d'une référence ou d'une plage", () => {
|
|
76
|
+
expect(functionQueryAt("=A1", 2)).toBe("")
|
|
77
|
+
expect(functionQueryAt("=A1:B")).toBe("")
|
|
78
|
+
expect(functionQueryAt("=$AB")).toBe("")
|
|
79
|
+
expect(functionQueryAt("=A1+B")).toBe("b")
|
|
80
|
+
expect(functionQueryAt("=SUM(A")).toBe("a")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("suit le curseur, pas seulement la fin du texte", () => {
|
|
84
|
+
expect(functionQueryAt("=SU+1", 3)).toBe("su")
|
|
85
|
+
expect(functionQueryAt("=SU+1", 5)).toBe("")
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe("rankSuggestions (autocomplétion)", () => {
|
|
90
|
+
const items = [
|
|
91
|
+
{ name: "SUM" },
|
|
92
|
+
{ name: "SUMIF" },
|
|
93
|
+
{ name: "SUMPRODUCT" },
|
|
94
|
+
{ name: "MID" },
|
|
95
|
+
{ name: "MEDIAN" },
|
|
96
|
+
{ name: "IF" },
|
|
97
|
+
{ name: "IFS" },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
it("classe égalité > préfixe, puis nom le plus court", () => {
|
|
101
|
+
expect(rankSuggestions(items, "sum").map((i) => i.name)).toEqual([
|
|
102
|
+
"SUM",
|
|
103
|
+
"SUMIF",
|
|
104
|
+
"SUMPRODUCT",
|
|
105
|
+
])
|
|
106
|
+
expect(rankSuggestions(items, "if").map((i) => i.name)).toEqual(["IF", "IFS", "SUMIF"])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("trouve une sous-chaîne", () => {
|
|
110
|
+
expect(rankSuggestions(items, "edian").map((i) => i.name)).toEqual(["MEDIAN"])
|
|
111
|
+
expect(rankSuggestions(items, "ia").map((i) => i.name)).toEqual(["MEDIAN"])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("trouve une sous-séquence (fuzzy)", () => {
|
|
115
|
+
expect(rankSuggestions(items, "sm").map((i) => i.name)).toEqual([
|
|
116
|
+
"SUM",
|
|
117
|
+
"SUMIF",
|
|
118
|
+
"SUMPRODUCT",
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("respecte la limite et ignore une requête vide", () => {
|
|
123
|
+
expect(rankSuggestions(items, "s", 1)).toHaveLength(1)
|
|
124
|
+
expect(rankSuggestions(items, "")).toEqual([])
|
|
125
|
+
expect(rankSuggestions(items, "zzz")).toEqual([])
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("functionCallAt / signatureArgs (aide à l'argument)", () => {
|
|
130
|
+
const known = ["SUM", "IF", "MAX"]
|
|
131
|
+
|
|
132
|
+
it("détecte la fonction et l'index de l'argument courant", () => {
|
|
133
|
+
expect(functionCallAt("=SUM(", 5, known)).toEqual({ name: "sum", index: 0 })
|
|
134
|
+
expect(functionCallAt("=SUM(A1,", 8, known)).toEqual({ name: "sum", index: 1 })
|
|
135
|
+
expect(functionCallAt("=IF(A1>0,", 9, known)).toEqual({ name: "if", index: 1 })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("prend la fonction la plus interne", () => {
|
|
139
|
+
expect(functionCallAt("=SUM(IF(A1>0,", 13, known)).toEqual({ name: "if", index: 1 })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("ignore les virgules des chaînes et les appels fermés", () => {
|
|
143
|
+
expect(functionCallAt('=IF(A1,"x,y",', 7, known)).toEqual({ name: "if", index: 1 })
|
|
144
|
+
expect(functionCallAt('=IF(A1,"x,y",', 13, known)).toEqual({ name: "if", index: 2 })
|
|
145
|
+
expect(functionCallAt("=A1+B1", 6, known)).toBeNull()
|
|
146
|
+
expect(functionCallAt("=SUM(A1)", 9, known)).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("signatureArgs découpe les paramètres", () => {
|
|
150
|
+
expect(signatureArgs("range, criteria, [sum_range]")).toEqual([
|
|
151
|
+
"range",
|
|
152
|
+
"criteria",
|
|
153
|
+
"[sum_range]",
|
|
154
|
+
])
|
|
155
|
+
expect(signatureArgs("")).toEqual([])
|
|
156
|
+
})
|
|
47
157
|
})
|
|
48
158
|
|
|
49
159
|
describe("dates ISO", () => {
|
package/lib/spreadsheet.ts
CHANGED
|
@@ -26,14 +26,213 @@ export const isBlankValue = (v: any): boolean =>
|
|
|
26
26
|
/**
|
|
27
27
|
* Décale les références A1 non absolues d'une formule de (dr, dc) —
|
|
28
28
|
* plages incluses, `$` préservés (copie / autofill).
|
|
29
|
+
*
|
|
30
|
+
* Les **littéraux de chaîne** sont laissés intacts (`"A1"` reste `"A1"`) et les
|
|
31
|
+
* **identifiants** contenant des chiffres ne sont pas pris pour des références
|
|
32
|
+
* (`LOG10(...)`, `price1`…).
|
|
29
33
|
*/
|
|
30
34
|
export const shiftFormulaRefs = (src: string, dr: number, dc: number): string =>
|
|
31
|
-
src
|
|
35
|
+
splitFormulaSegments(src)
|
|
36
|
+
.map((segment) => (segment.code ? shiftCodeRefs(segment.text, dr, dc) : segment.text))
|
|
37
|
+
.join("")
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Requête de suggestion de **fonction** à la position du curseur.
|
|
41
|
+
*
|
|
42
|
+
* Ne suggère que si le curseur est en train de taper un identifiant de fonction :
|
|
43
|
+
* hors littéral de chaîne, commençant un token (`=`, `(`, `,`, opérateur, espace…)
|
|
44
|
+
* et **pas** au milieu d'une référence de cellule (`A` devant `1`). Retourne `""`
|
|
45
|
+
* quand il n'y a rien à suggérer (ex. `=SUM(`).
|
|
46
|
+
*/
|
|
47
|
+
export function functionQueryAt(text: string, caret: number = text.length): string {
|
|
48
|
+
if (!text.trim().startsWith("=")) return ""
|
|
49
|
+
const end = Math.max(0, Math.min(caret, text.length))
|
|
50
|
+
|
|
51
|
+
// Curseur dans une chaîne littérale → aucune suggestion
|
|
52
|
+
let quote = ""
|
|
53
|
+
for (let i = 0; i < end; i++) {
|
|
54
|
+
const ch = text[i]!
|
|
55
|
+
if (quote) {
|
|
56
|
+
if (ch === quote) {
|
|
57
|
+
if (text[i + 1] === quote) {
|
|
58
|
+
i++
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
quote = ""
|
|
62
|
+
}
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (ch === '"' || ch === "'") quote = ch
|
|
66
|
+
}
|
|
67
|
+
if (quote) return ""
|
|
68
|
+
|
|
69
|
+
let start = end
|
|
70
|
+
while (start > 0 && /[A-Za-z]/.test(text[start - 1]!)) start--
|
|
71
|
+
if (start === end) return ""
|
|
72
|
+
// « A » devant « 1 » : référence de cellule en cours de frappe, pas une fonction
|
|
73
|
+
if (/[0-9]/.test(text[end] ?? "")) return ""
|
|
74
|
+
// Doit commencer un token (évite `A1:B`, `$AB`, `sheet!SUM`…)
|
|
75
|
+
const before = start > 0 ? text[start - 1]! : ""
|
|
76
|
+
if (before && !/[=(,;\s+\-*/^%&<>]/.test(before)) return ""
|
|
77
|
+
return text.slice(start, end).toLowerCase()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Pose d'une sous-séquence : les lettres de `query` apparaissent dans l'ordre. */
|
|
81
|
+
const isSubsequence = (query: string, name: string): boolean => {
|
|
82
|
+
let i = 0
|
|
83
|
+
for (const ch of name) {
|
|
84
|
+
if (ch === query[i]) i++
|
|
85
|
+
if (i === query.length) return true
|
|
86
|
+
}
|
|
87
|
+
return query.length === 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Classe des suggestions par pertinence :
|
|
92
|
+
* égalité > préfixe > sous-chaîne > sous-séquence, puis nom le plus court,
|
|
93
|
+
* puis ordre d'origine. `query` vide → aucun résultat.
|
|
94
|
+
*/
|
|
95
|
+
export function rankSuggestions<T extends { name: string }>(
|
|
96
|
+
items: T[],
|
|
97
|
+
query: string,
|
|
98
|
+
limit = 8,
|
|
99
|
+
): T[] {
|
|
100
|
+
const q = query.trim().toLowerCase()
|
|
101
|
+
if (!q) return []
|
|
102
|
+
return items
|
|
103
|
+
.map((item, index) => {
|
|
104
|
+
const name = item.name.toLowerCase()
|
|
105
|
+
const score = name === q
|
|
106
|
+
? 0
|
|
107
|
+
: name.startsWith(q)
|
|
108
|
+
? 1
|
|
109
|
+
: name.includes(q)
|
|
110
|
+
? 2
|
|
111
|
+
: isSubsequence(q, name)
|
|
112
|
+
? 3
|
|
113
|
+
: -1
|
|
114
|
+
return { item, index, score, length: name.length }
|
|
115
|
+
})
|
|
116
|
+
.filter((entry) => entry.score >= 0)
|
|
117
|
+
.sort((a, b) => a.score - b.score || a.length - b.length || a.index - b.index)
|
|
118
|
+
.slice(0, limit)
|
|
119
|
+
.map((entry) => entry.item)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Arguments d'une signature (`"range, criteria, [sum_range]"` → 3 entrées). */
|
|
123
|
+
export function signatureArgs(sig: string): string[] {
|
|
124
|
+
return sig
|
|
125
|
+
.split(",")
|
|
126
|
+
.map((part) => part.trim())
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface FunctionCallAtResult {
|
|
131
|
+
/** Nom de la fonction (minuscules) */
|
|
132
|
+
name: string
|
|
133
|
+
/** Index de l'argument courant (0-based) */
|
|
134
|
+
index: number
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fonction (parmi `known`) dans les parenthèses de laquelle se trouve le curseur.
|
|
139
|
+
* Gère l'imbrication, les littéraux et les virgules de chaque niveau.
|
|
140
|
+
*/
|
|
141
|
+
export function functionCallAt(
|
|
142
|
+
text: string,
|
|
143
|
+
caret: number,
|
|
144
|
+
known: Iterable<string>,
|
|
145
|
+
): FunctionCallAtResult | null {
|
|
146
|
+
const names = new Set([...known].map((name) => name.toLowerCase()))
|
|
147
|
+
const end = Math.max(0, Math.min(caret, text.length))
|
|
148
|
+
const stack: { name: string; index: number }[] = []
|
|
149
|
+
let quote = ""
|
|
150
|
+
for (let i = 0; i < end; i++) {
|
|
151
|
+
const ch = text[i]!
|
|
152
|
+
if (quote) {
|
|
153
|
+
if (ch === quote) {
|
|
154
|
+
if (text[i + 1] === quote) {
|
|
155
|
+
i++
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
quote = ""
|
|
159
|
+
}
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (ch === '"' || ch === "'") {
|
|
163
|
+
quote = ch
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (ch === "(") {
|
|
167
|
+
let j = i - 1
|
|
168
|
+
while (j >= 0 && /[A-Za-z0-9_]/.test(text[j]!)) j--
|
|
169
|
+
stack.push({ name: text.slice(j + 1, i).toLowerCase(), index: 0 })
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (ch === ")") {
|
|
173
|
+
stack.pop()
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (ch === "," && stack.length) stack[stack.length - 1]!.index++
|
|
177
|
+
}
|
|
178
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
179
|
+
const frame = stack[k]!
|
|
180
|
+
if (names.has(frame.name)) return { name: frame.name, index: frame.index }
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Segments d'une formule : `code: false` = littéral de chaîne (guillemets simples/doubles). */
|
|
186
|
+
const splitFormulaSegments = (src: string): { code: boolean; text: string }[] => {
|
|
187
|
+
const out: { code: boolean; text: string }[] = []
|
|
188
|
+
let code = ""
|
|
189
|
+
let i = 0
|
|
190
|
+
while (i < src.length) {
|
|
191
|
+
const ch = src[i]!
|
|
192
|
+
if (ch !== '"' && ch !== "'") {
|
|
193
|
+
code += ch
|
|
194
|
+
i++
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (code) {
|
|
198
|
+
out.push({ code: true, text: code })
|
|
199
|
+
code = ""
|
|
200
|
+
}
|
|
201
|
+
let literal = ch
|
|
202
|
+
i++
|
|
203
|
+
while (i < src.length) {
|
|
204
|
+
const c = src[i]!
|
|
205
|
+
literal += c
|
|
206
|
+
i++
|
|
207
|
+
if (c !== ch) continue
|
|
208
|
+
// Guillemet doublé = caractère littéral (cf. parseString du moteur)
|
|
209
|
+
if (src[i] === ch) {
|
|
210
|
+
literal += src[i]
|
|
211
|
+
i++
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
out.push({ code: false, text: literal })
|
|
217
|
+
}
|
|
218
|
+
if (code) out.push({ code: true, text: code })
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Décalage des références dans un segment de code (jamais dans une chaîne). */
|
|
223
|
+
const shiftCodeRefs = (code: string, dr: number, dc: number): string =>
|
|
224
|
+
code.replace(
|
|
32
225
|
/(\$?)([A-Za-z]{1,3})(\$?)(\d+)/g,
|
|
33
|
-
(
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
226
|
+
(match: string, ca: string, letters: string, ra: string, digits: string, offset: number) => {
|
|
227
|
+
// Borne gauche : la ref doit commencer un token (pas « LOG10 », « price1 »…)
|
|
228
|
+
const before = offset > 0 ? code[offset - 1]! : ""
|
|
229
|
+
if (before && /[A-Za-z0-9_.]/.test(before)) return match
|
|
230
|
+
// Borne droite : pas de suite alphanumérique, pas un appel de fonction
|
|
231
|
+
const after = code[offset + match.length] ?? ""
|
|
232
|
+
if (after && /[A-Za-z0-9_(]/.test(after)) return match
|
|
233
|
+
const col = colFromLetters(letters) + (ca === "$" ? 0 : dc)
|
|
234
|
+
const row = Number(digits) - 1 + (ra === "$" ? 0 : dr)
|
|
235
|
+
if (col < 0 || row < 0) return match
|
|
37
236
|
return ca + colLetter(col) + ra + (row + 1)
|
|
38
237
|
},
|
|
39
238
|
)
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -9934,6 +9934,67 @@ a.q-breadcrumbs__el:hover {
|
|
|
9934
9934
|
opacity: 0.55;
|
|
9935
9935
|
}
|
|
9936
9936
|
|
|
9937
|
+
/* Aperçu live du résultat (barre fx) — discret, monospace, rouge si erreur */
|
|
9938
|
+
.q-spreadsheet__fx-preview {
|
|
9939
|
+
flex: 0 1 auto;
|
|
9940
|
+
min-width: 0;
|
|
9941
|
+
max-width: 40%;
|
|
9942
|
+
overflow: hidden;
|
|
9943
|
+
text-overflow: ellipsis;
|
|
9944
|
+
white-space: nowrap;
|
|
9945
|
+
padding-left: 8px;
|
|
9946
|
+
border-left: 1px solid var(--q-spreadsheet-border);
|
|
9947
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
9948
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
9949
|
+
font-size: 12px;
|
|
9950
|
+
}
|
|
9951
|
+
.q-spreadsheet__fx-preview-eq {
|
|
9952
|
+
margin-right: 4px;
|
|
9953
|
+
opacity: 0.6;
|
|
9954
|
+
}
|
|
9955
|
+
.q-spreadsheet__fx-preview--error {
|
|
9956
|
+
color: var(--negative, #c10015);
|
|
9957
|
+
font-weight: 600;
|
|
9958
|
+
}
|
|
9959
|
+
.dark .q-spreadsheet__fx-preview,
|
|
9960
|
+
.q-spreadsheet--dark .q-spreadsheet__fx-preview {
|
|
9961
|
+
color: rgb(255 255 255 / 0.6);
|
|
9962
|
+
}
|
|
9963
|
+
|
|
9964
|
+
/* Aide à l'argument : signature de la fonction au curseur */
|
|
9965
|
+
.q-spreadsheet__fxhint {
|
|
9966
|
+
flex: 0 1 auto;
|
|
9967
|
+
min-width: 0;
|
|
9968
|
+
overflow: hidden;
|
|
9969
|
+
text-overflow: ellipsis;
|
|
9970
|
+
white-space: nowrap;
|
|
9971
|
+
color: var(--muted-foreground, rgb(0 0 0 / 0.6));
|
|
9972
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
9973
|
+
font-size: 12px;
|
|
9974
|
+
}
|
|
9975
|
+
.q-spreadsheet__fxhint strong {
|
|
9976
|
+
color: var(--foreground, #1d1d1d);
|
|
9977
|
+
}
|
|
9978
|
+
.q-spreadsheet__fxhint-arg--on {
|
|
9979
|
+
color: var(--primary, #1976d2);
|
|
9980
|
+
font-weight: 700;
|
|
9981
|
+
}
|
|
9982
|
+
/* Dans l'éditeur de cellule : sous le textarea, séparateur discret */
|
|
9983
|
+
.q-spreadsheet__fxhint--editor {
|
|
9984
|
+
flex: 0 0 auto;
|
|
9985
|
+
padding: 3px 8px 5px;
|
|
9986
|
+
border-top: 1px dashed var(--q-spreadsheet-border);
|
|
9987
|
+
white-space: normal;
|
|
9988
|
+
}
|
|
9989
|
+
.dark .q-spreadsheet__fxhint,
|
|
9990
|
+
.q-spreadsheet--dark .q-spreadsheet__fxhint {
|
|
9991
|
+
color: rgb(255 255 255 / 0.6);
|
|
9992
|
+
}
|
|
9993
|
+
.dark .q-spreadsheet__fxhint strong,
|
|
9994
|
+
.q-spreadsheet--dark .q-spreadsheet__fxhint strong {
|
|
9995
|
+
color: #fff;
|
|
9996
|
+
}
|
|
9997
|
+
|
|
9937
9998
|
/* ─── Éditeur formule (textarea monospace) ─── */
|
|
9938
9999
|
.q-spreadsheet__editor--formula {
|
|
9939
10000
|
align-items: stretch;
|
|
@@ -10324,6 +10385,10 @@ a.q-breadcrumbs__el:hover {
|
|
|
10324
10385
|
color: #8b93a1;
|
|
10325
10386
|
font-size: 11.5px;
|
|
10326
10387
|
}
|
|
10388
|
+
.q-spreadsheet__fnsug-kind {
|
|
10389
|
+
font-style: italic;
|
|
10390
|
+
opacity: 0.85;
|
|
10391
|
+
}
|
|
10327
10392
|
.q-spreadsheet__fnsug-item:hover,
|
|
10328
10393
|
.q-spreadsheet__fnsug-item--on {
|
|
10329
10394
|
background: rgb(25 118 210 / 0.1);
|