@dnax/ui 0.0.1

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.
Files changed (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. package/tsconfig.json +33 -0
package/lib/formula.ts ADDED
@@ -0,0 +1,630 @@
1
+ // Moteur de formules style tableur pour QDataGrid.
2
+ // Syntaxe : une valeur de cellule qui commence par "=" est une formule :
3
+ // =price * quantity → références par nom de colonne (ligne courante)
4
+ // =SUM(E1:E3) → plages A1, fonctions intégrées
5
+ // =E4 * $B$5 → références A1 (absolues avec $)
6
+ // =IF(A1 > 0, "ok", "ko") → fonctions, comparaisons
7
+ // Opérateurs : + - * / ^ % & = <> < > <= >= · parenthèses · unaires +/- · chaînes "..."/'...'
8
+ // Erreurs : #DIV/0! #NAME? #VALUE! #REF! #N/A #CYCLE! #ERROR! (rendues en rouge par le grid).
9
+
10
+ export class FormulaError {
11
+ readonly code: string
12
+ constructor(code: string) {
13
+ this.code = code
14
+ }
15
+ toString() {
16
+ return this.code
17
+ }
18
+ }
19
+
20
+ export const isError = (v: unknown): v is FormulaError => v instanceof FormulaError
21
+
22
+ export type FormulaValue = number | string | boolean | null | FormulaError
23
+ type EvalValue = FormulaValue | FormulaValue[]
24
+
25
+ export interface FormulaContext {
26
+ /** Ligne (0-based) de la cellule qui contient la formule — pour les références par champ */
27
+ row: number
28
+ /** Résout une référence par nom de colonne (dans ctx.row). undefined = champ inconnu */
29
+ resolveField: (name: string) => FormulaValue | undefined
30
+ /** Résout une référence positionnelle A1 (row/col 0-based). undefined = hors grille */
31
+ resolveCell: (row: number, col: number) => FormulaValue | undefined
32
+ }
33
+
34
+ // ─── AST ───
35
+ interface RefToken {
36
+ raw: string
37
+ field?: string // nom de colonne (identifiant simple)
38
+ col?: number // 0-based si A1
39
+ row?: number // 0-based si A1
40
+ colAbs?: boolean
41
+ rowAbs?: boolean
42
+ either?: boolean // ressemble à A1 (lettres+chiffres) mais peut être un champ (ex. price1)
43
+ }
44
+
45
+ type Node =
46
+ | { kind: "num"; value: number }
47
+ | { kind: "str"; value: string }
48
+ | { kind: "ref"; ref: RefToken }
49
+ | { kind: "range"; start: RefToken; end: RefToken }
50
+ | { kind: "call"; name: string; args: Node[] }
51
+ | { kind: "unary"; op: "+" | "-"; arg: Node }
52
+ | { kind: "postfix"; op: "%"; arg: Node }
53
+ | { kind: "binary"; op: string; left: Node; right: Node }
54
+
55
+ // ─── Parser (descente récursive, précédence) ───
56
+ class Parser {
57
+ private pos = 0
58
+
59
+ constructor(private src: string) {}
60
+
61
+ private peek() {
62
+ return this.src[this.pos]
63
+ }
64
+ eof() {
65
+ return this.pos >= this.src.length
66
+ }
67
+ private next() {
68
+ return this.src[this.pos++]
69
+ }
70
+ private skipWs() {
71
+ while (!this.eof() && /\s/.test(this.peek()!)) this.pos++
72
+ }
73
+ private expect(char: string) {
74
+ this.skipWs()
75
+ if (this.peek() !== char) throw new Error("expected " + char)
76
+ this.pos++
77
+ }
78
+
79
+ /** parse une expression complète (niveau comparaison) */
80
+ parseExpr(): Node {
81
+ this.skipWs()
82
+ return this.parseComparison()
83
+ }
84
+
85
+ private parseComparison(): Node {
86
+ let left = this.parseAdditive()
87
+ for (;;) {
88
+ this.skipWs()
89
+ let op: string | null = null
90
+ if (this.peek() === "=") op = "="
91
+ else if (this.peek() === "<") {
92
+ if (this.src[this.pos + 1] === ">") {
93
+ op = "<>"
94
+ this.pos++
95
+ } else if (this.src[this.pos + 1] === "=") {
96
+ op = "<="
97
+ this.pos++
98
+ } else op = "<"
99
+ } else if (this.peek() === ">") {
100
+ if (this.src[this.pos + 1] === "=") {
101
+ op = ">="
102
+ this.pos++
103
+ } else op = ">"
104
+ }
105
+ if (!op) return left
106
+ this.pos++
107
+ const right = this.parseAdditive()
108
+ left = { kind: "binary", op, left, right }
109
+ }
110
+ }
111
+
112
+ private parseAdditive(): Node {
113
+ let left = this.parseMultiplicative()
114
+ for (;;) {
115
+ this.skipWs()
116
+ const op = this.peek()
117
+ if (op !== "+" && op !== "-" && op !== "&") return left
118
+ this.pos++
119
+ const right = this.parseMultiplicative()
120
+ left = { kind: "binary", op, left, right }
121
+ }
122
+ }
123
+
124
+ private parseMultiplicative(): Node {
125
+ let left = this.parsePower()
126
+ for (;;) {
127
+ this.skipWs()
128
+ const op = this.peek()
129
+ if (op !== "*" && op !== "/") return left
130
+ this.pos++
131
+ const right = this.parsePower()
132
+ left = { kind: "binary", op, left, right }
133
+ }
134
+ }
135
+
136
+ private parsePower(): Node {
137
+ const left = this.parseUnary()
138
+ this.skipWs()
139
+ if (this.peek() === "^") {
140
+ this.pos++
141
+ const right = this.parsePower() // associatif à droite
142
+ return { kind: "binary", op: "^", left, right }
143
+ }
144
+ return left
145
+ }
146
+
147
+ private parseUnary(): Node {
148
+ this.skipWs()
149
+ const op = this.peek()
150
+ if (op === "+" || op === "-") {
151
+ this.pos++
152
+ return { kind: "unary", op, arg: this.parseUnary() }
153
+ }
154
+ return this.parsePostfix()
155
+ }
156
+
157
+ private parsePostfix(): Node {
158
+ let node = this.parsePrimary()
159
+ for (;;) {
160
+ this.skipWs()
161
+ if (this.peek() === "%") {
162
+ this.pos++
163
+ node = { kind: "postfix", op: "%", arg: node }
164
+ } else return node
165
+ }
166
+ }
167
+
168
+ private parsePrimary(): Node {
169
+ this.skipWs()
170
+ const ch = this.peek()
171
+ if (ch === "(") {
172
+ this.pos++
173
+ const node = this.parseExpr()
174
+ this.expect(")")
175
+ return node
176
+ }
177
+ if (ch === '"' || ch === "'") return { kind: "str", value: this.parseString(ch) }
178
+ if (ch && /[0-9.]/.test(ch)) return this.parseNumber()
179
+ if (ch && /[A-Za-z_$]/.test(ch)) return this.parseRefOrCall()
180
+ throw new Error("unexpected char")
181
+ }
182
+
183
+ private parseString(quote: string): string {
184
+ this.pos++ // quote d'ouverture
185
+ let out = ""
186
+ while (!this.eof()) {
187
+ const c = this.next()
188
+ if (c === quote) {
189
+ if (this.peek() === quote) {
190
+ this.pos++ // échappement ""
191
+ out += quote
192
+ } else return out
193
+ } else out += c
194
+ }
195
+ throw new Error("unterminated string")
196
+ }
197
+
198
+ private parseNumber(): Node {
199
+ let s = ""
200
+ while (!this.eof() && /[0-9.]/.test(this.peek()!)) s += this.next()
201
+ const value = parseFloat(s)
202
+ return { kind: "num", value: Number.isNaN(value) ? 0 : value }
203
+ }
204
+
205
+ private parseRefOrCall(): Node {
206
+ // [$]lettres[$]chiffres → A1 ; identifiant( → appel ; sinon champ
207
+ let colAbs = false
208
+ if (this.peek() === "$") {
209
+ colAbs = true
210
+ this.pos++
211
+ }
212
+ let letters = ""
213
+ while (!this.eof() && /[A-Za-z]/.test(this.peek()!)) letters += this.next()
214
+ if (letters.length === 0) throw new Error("bad ref")
215
+
216
+ // Appel de fonction : identifiant suivi de "("
217
+ if (this.peek() === "(") {
218
+ if (colAbs) throw new Error("bad call")
219
+ this.pos++
220
+ const args: Node[] = []
221
+ this.skipWs()
222
+ if (this.peek() !== ")") {
223
+ for (;;) {
224
+ args.push(this.parseExpr())
225
+ this.skipWs()
226
+ if (this.peek() === ",") {
227
+ this.pos++
228
+ continue
229
+ }
230
+ break
231
+ }
232
+ }
233
+ this.expect(")")
234
+ return { kind: "call", name: letters, args }
235
+ }
236
+
237
+ let rowAbs = false
238
+ if (this.peek() === "$") {
239
+ rowAbs = true
240
+ this.pos++
241
+ }
242
+ let digits = ""
243
+ while (!this.eof() && /[0-9]/.test(this.peek()!)) digits += this.next()
244
+
245
+ let token: RefToken
246
+ if (digits.length > 0 && letters.length <= 3) {
247
+ token = {
248
+ raw: (colAbs ? "$" : "") + letters + (rowAbs ? "$" : "") + digits,
249
+ col: lettersToCol(letters),
250
+ row: parseInt(digits, 10) - 1,
251
+ colAbs,
252
+ rowAbs,
253
+ either: !colAbs && !rowAbs,
254
+ }
255
+ } else {
256
+ token = { raw: (colAbs ? "$" : "") + letters, field: letters }
257
+ }
258
+
259
+ // Plage A1:A3 (uniquement par positions)
260
+ this.skipWs()
261
+ if (this.peek() === ":") {
262
+ this.pos++
263
+ const end = this.parseRefOnly()
264
+ return { kind: "range", start: token, end }
265
+ }
266
+ return { kind: "ref", ref: token }
267
+ }
268
+
269
+ /** Lit un token de référence seul (extrémité de plage) */
270
+ private parseRefOnly(): RefToken {
271
+ this.skipWs()
272
+ let colAbs = false
273
+ if (this.peek() === "$") {
274
+ colAbs = true
275
+ this.pos++
276
+ }
277
+ let letters = ""
278
+ while (!this.eof() && /[A-Za-z]/.test(this.peek()!)) letters += this.next()
279
+ let rowAbs = false
280
+ if (this.peek() === "$") {
281
+ rowAbs = true
282
+ this.pos++
283
+ }
284
+ let digits = ""
285
+ while (!this.eof() && /[0-9]/.test(this.peek()!)) digits += this.next()
286
+ if (digits.length === 0 || letters.length === 0 || letters.length > 3)
287
+ throw new Error("bad range")
288
+ return {
289
+ raw: (colAbs ? "$" : "") + letters + (rowAbs ? "$" : "") + digits,
290
+ col: lettersToCol(letters),
291
+ row: parseInt(digits, 10) - 1,
292
+ colAbs,
293
+ rowAbs,
294
+ }
295
+ }
296
+ }
297
+
298
+ const lettersToCol = (letters: string): number => {
299
+ let n = 0
300
+ for (const ch of letters.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64)
301
+ return n - 1
302
+ }
303
+
304
+ // ─── Évaluation ───
305
+ const err = (code: string): FormulaError => new FormulaError(code)
306
+
307
+ const toNum = (v: FormulaValue): number | FormulaError => {
308
+ if (v === null || v === undefined) return 0
309
+ if (typeof v === "boolean") return v ? 1 : 0
310
+ if (typeof v === "number") return v
311
+ const t = (v as string).trim()
312
+ if (t === "") return 0
313
+ const n = Number(t)
314
+ return Number.isNaN(n) ? err("#VALUE!") : n
315
+ }
316
+
317
+ const toStr = (v: FormulaValue): string => {
318
+ if (v === null || v === undefined) return ""
319
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE"
320
+ return String(v)
321
+ }
322
+
323
+ const truthy = (v: FormulaValue): boolean => {
324
+ if (v === null || v === undefined) return false
325
+ if (typeof v === "boolean") return v
326
+ if (typeof v === "number") return v !== 0
327
+ return v !== ""
328
+ }
329
+
330
+ // collect=false ne produit jamais de tableau (les plages renvoient #VALUE!) —
331
+ // ce garde sert uniquement au typage.
332
+ const toScalar = (v: EvalValue): FormulaValue => (Array.isArray(v) ? err("#VALUE!") : v)
333
+
334
+ const resolveRef = (ref: RefToken, ctx: FormulaContext): FormulaValue => {
335
+ if (ref.field !== undefined) {
336
+ const v = ctx.resolveField(ref.field)
337
+ return v === undefined ? err("#NAME?") : v
338
+ }
339
+ if (ref.either) {
340
+ const f = ctx.resolveField(ref.raw)
341
+ if (f !== undefined) return f
342
+ }
343
+ const v = ctx.resolveCell(ref.row!, ref.col!)
344
+ return v === undefined ? err("#REF!") : v
345
+ }
346
+
347
+ const resolveRefPos = (ref: RefToken): [number, number] | null => {
348
+ if (ref.field !== undefined) return null // pas de plage par champ
349
+ return [ref.row!, ref.col!]
350
+ }
351
+
352
+ function evalNode(node: Node, ctx: FormulaContext, collect: boolean): EvalValue {
353
+ switch (node.kind) {
354
+ case "num":
355
+ return node.value
356
+ case "str":
357
+ return node.value
358
+ case "ref":
359
+ return resolveRef(node.ref, ctx)
360
+ case "range": {
361
+ if (!collect) return err("#VALUE!")
362
+ const a = resolveRefPos(node.start)
363
+ const b = resolveRefPos(node.end)
364
+ if (!a || !b) return err("#REF!")
365
+ const out: FormulaValue[] = []
366
+ for (let r = Math.min(a[0], b[0]); r <= Math.max(a[0], b[0]); r++)
367
+ for (let c = Math.min(a[1], b[1]); c <= Math.max(a[1], b[1]); c++)
368
+ out.push(ctx.resolveCell(r, c) ?? null)
369
+ return out
370
+ }
371
+ case "unary": {
372
+ const v = evalNode(node.arg, ctx, false)
373
+ if (isError(v)) return v
374
+ const n = toNum(v as FormulaValue)
375
+ return isError(n) ? n : node.op === "-" ? -n : n
376
+ }
377
+ case "postfix": {
378
+ const v = evalNode(node.arg, ctx, false)
379
+ if (isError(v)) return v
380
+ const n = toNum(v as FormulaValue)
381
+ return isError(n) ? n : n / 100
382
+ }
383
+ case "binary":
384
+ return evalBinary(node, ctx)
385
+ case "call":
386
+ return evalCall(node, ctx)
387
+ }
388
+ }
389
+
390
+ function evalBinary(node: Extract<Node, { kind: "binary" }>, ctx: FormulaContext): FormulaValue {
391
+ if (node.op === "&") {
392
+ const a = toScalar(evalNode(node.left, ctx, false))
393
+ const b = toScalar(evalNode(node.right, ctx, false))
394
+ if (isError(a)) return a
395
+ if (isError(b)) return b
396
+ return toStr(a) + toStr(b)
397
+ }
398
+ const a = toScalar(evalNode(node.left, ctx, false))
399
+ const b = toScalar(evalNode(node.right, ctx, false))
400
+ if (isError(a)) return a
401
+ if (isError(b)) return b
402
+
403
+ switch (node.op) {
404
+ case "+":
405
+ case "-":
406
+ case "*":
407
+ case "/":
408
+ case "^": {
409
+ const na = toNum(a as FormulaValue)
410
+ const nb = toNum(b as FormulaValue)
411
+ if (isError(na)) return na
412
+ if (isError(nb)) return nb
413
+ switch (node.op) {
414
+ case "+":
415
+ return na + nb
416
+ case "-":
417
+ return na - nb
418
+ case "*":
419
+ return na * nb
420
+ case "/":
421
+ return nb === 0 ? err("#DIV/0!") : na / nb
422
+ case "^":
423
+ return Math.pow(na, nb)
424
+ }
425
+ break
426
+ }
427
+ case "=":
428
+ case "<>":
429
+ case "<":
430
+ case ">":
431
+ case "<=":
432
+ case ">=": {
433
+ const cmp = compare(a as FormulaValue, b as FormulaValue)
434
+ if (cmp === "err") return err("#VALUE!")
435
+ switch (node.op) {
436
+ case "=":
437
+ return cmp === 0
438
+ case "<>":
439
+ return cmp !== 0
440
+ case "<":
441
+ return cmp < 0
442
+ case ">":
443
+ return cmp > 0
444
+ case "<=":
445
+ return cmp <= 0
446
+ case ">=":
447
+ return cmp >= 0
448
+ }
449
+ break
450
+ }
451
+ }
452
+ return err("#ERROR!")
453
+ }
454
+
455
+ function compare(a: FormulaValue, b: FormulaValue): number | "err" {
456
+ if (typeof a === "boolean") a = a ? 1 : 0
457
+ if (typeof b === "boolean") b = b ? 1 : 0
458
+ if (typeof a === "string" && typeof b === "string") {
459
+ return a === b ? 0 : a < b ? -1 : 1
460
+ }
461
+ if (a === null) a = 0
462
+ if (b === null) b = 0
463
+ if (typeof a === "number" && typeof b === "number") {
464
+ return a === b ? 0 : a < b ? -1 : 1
465
+ }
466
+ const na = toNum(a)
467
+ const nb = toNum(b)
468
+ if (!isError(na) && !isError(nb)) {
469
+ return na === nb ? 0 : na < nb ? -1 : 1
470
+ }
471
+ const sa = toStr(a)
472
+ const sb = toStr(b)
473
+ return sa === sb ? 0 : sa < sb ? -1 : 1
474
+ }
475
+
476
+ function evalCall(node: Extract<Node, { kind: "call" }>, ctx: FormulaContext): FormulaValue {
477
+ const name = node.name.toLowerCase()
478
+
479
+ // Appels paresseux (évalués conditionnellement)
480
+ if (name === "if" && node.args.length >= 2) {
481
+ const cond = toScalar(evalNode(node.args[0]!, ctx, false))
482
+ if (isError(cond)) return cond
483
+ return truthy(cond)
484
+ ? toScalar(evalNode(node.args[1]!, ctx, false))
485
+ : node.args[2] !== undefined
486
+ ? toScalar(evalNode(node.args[2], ctx, false))
487
+ : false
488
+ }
489
+ if (name === "iferror" && node.args.length >= 1) {
490
+ const v = toScalar(evalNode(node.args[0]!, ctx, false))
491
+ if (isError(v))
492
+ return node.args[1] !== undefined ? toScalar(evalNode(node.args[1], ctx, false)) : null
493
+ return v
494
+ }
495
+ if (name === "and" || name === "or") {
496
+ if (node.args.length === 0) return name === "and"
497
+ for (const arg of node.args) {
498
+ const v = toScalar(evalNode(arg, ctx, false))
499
+ if (isError(v)) return v
500
+ const t = truthy(v)
501
+ if (name === "and" && !t) return false
502
+ if (name === "or" && t) return true
503
+ }
504
+ return name === "and"
505
+ }
506
+
507
+ const fn = FUNCTIONS[name]
508
+ if (!fn) return err("#NAME?")
509
+
510
+ const raw = node.args.map((a) => evalNode(a, ctx, true))
511
+ const hadArray = raw.some(Array.isArray)
512
+ const flat = raw.flatMap((v) => (Array.isArray(v) ? (v as FormulaValue[]) : [v as FormulaValue]))
513
+ return fn(flat, hadArray)
514
+ }
515
+
516
+ type GridFunction = (args: FormulaValue[], hadArray: boolean) => FormulaValue
517
+
518
+ const scalar = (args: FormulaValue[], hadArray: boolean, fn: (v: FormulaValue) => FormulaValue): FormulaValue => {
519
+ if (hadArray) return err("#VALUE!")
520
+ return fn(args[0] ?? null)
521
+ }
522
+
523
+ const numbers = (args: FormulaValue[]): number[] =>
524
+ args.filter((v): v is number => typeof v === "number")
525
+
526
+ const FUNCTIONS: Record<string, GridFunction> = {
527
+ sum: (args) => numbers(args).reduce((acc, n) => acc + n, 0),
528
+ average: (args) => {
529
+ const n = numbers(args)
530
+ return n.length ? n.reduce((acc, x) => acc + x, 0) / n.length : err("#DIV/0!")
531
+ },
532
+ avg: (args) => FUNCTIONS.average!(args, false),
533
+ min: (args) => {
534
+ const n = numbers(args)
535
+ return n.length ? Math.min(...n) : 0
536
+ },
537
+ max: (args) => {
538
+ const n = numbers(args)
539
+ return n.length ? Math.max(...n) : 0
540
+ },
541
+ count: (args) => numbers(args).length,
542
+ counta: (args) => args.filter((v) => v !== null && v !== undefined).length,
543
+ abs: (args, h) => scalar(args, h, (v) => {
544
+ const n = toNum(v)
545
+ return isError(n) ? n : Math.abs(n)
546
+ }),
547
+ round: (args, h) => scalar(args, h, (v) => {
548
+ const n = toNum(v)
549
+ if (isError(n)) return n
550
+ const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
551
+ const f = Math.pow(10, d)
552
+ return Math.round(n * f) / f
553
+ }),
554
+ roundup: (args, h) => scalar(args, h, (v) => {
555
+ const n = toNum(v)
556
+ if (isError(n)) return n
557
+ const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
558
+ const f = Math.pow(10, d)
559
+ return (n < 0 ? -1 : 1) * Math.ceil(Math.abs(n) * f) / f
560
+ }),
561
+ rounddown: (args, h) => scalar(args, h, (v) => {
562
+ const n = toNum(v)
563
+ if (isError(n)) return n
564
+ const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
565
+ const f = Math.pow(10, d)
566
+ return (n < 0 ? -1 : 1) * Math.floor(Math.abs(n) * f) / f
567
+ }),
568
+ floor: (args, h) => scalar(args, h, (v) => {
569
+ const n = toNum(v)
570
+ return isError(n) ? n : Math.floor(n)
571
+ }),
572
+ ceiling: (args, h) => scalar(args, h, (v) => {
573
+ const n = toNum(v)
574
+ return isError(n) ? n : Math.ceil(n)
575
+ }),
576
+ ceil: (args, h) => FUNCTIONS.ceiling!(args, h),
577
+ int: (args, h) => scalar(args, h, (v) => {
578
+ const n = toNum(v)
579
+ return isError(n) ? n : Math.trunc(n)
580
+ }),
581
+ mod: (args, h) => scalar(args, h, (v) => {
582
+ const n = toNum(v)
583
+ if (isError(n)) return n
584
+ const m = toNum(args[1] ?? null)
585
+ if (isError(m)) return m
586
+ return m === 0 ? err("#DIV/0!") : n % m
587
+ }),
588
+ sqrt: (args, h) => scalar(args, h, (v) => {
589
+ const n = toNum(v)
590
+ return isError(n) ? n : n < 0 ? err("#VALUE!") : Math.sqrt(n)
591
+ }),
592
+ power: (args, h) => scalar(args, h, (v) => {
593
+ const n = toNum(v)
594
+ if (isError(n)) return n
595
+ const e = toNum(args[1] ?? null)
596
+ return isError(e) ? e : Math.pow(n, e)
597
+ }),
598
+ pow: (args, h) => FUNCTIONS.power!(args, h),
599
+ pi: () => Math.PI,
600
+ not: (args, h) => scalar(args, h, (v) => !truthy(v)),
601
+ concat: (args) => args.map(toStr).join(""),
602
+ upper: (args, h) => scalar(args, h, (v) => toStr(v).toUpperCase()),
603
+ lower: (args, h) => scalar(args, h, (v) => toStr(v).toLowerCase()),
604
+ trim: (args, h) => scalar(args, h, (v) => toStr(v).trim()),
605
+ len: (args, h) => scalar(args, h, (v) => toStr(v).length),
606
+ isblank: (args, h) => scalar(args, h, (v) => v === null || v === undefined || v === ""),
607
+ isnumber: (args, h) => scalar(args, h, (v) => typeof v === "number"),
608
+ istext: (args, h) => scalar(args, h, (v) => typeof v === "string"),
609
+ value: (args, h) => scalar(args, h, (v) => toNum(v)),
610
+ n: (args, h) => scalar(args, h, (v) => {
611
+ if (v === null || v === undefined) return 0
612
+ if (typeof v === "boolean") return v ? 1 : 0
613
+ if (typeof v === "number") return v
614
+ return toNum(v)
615
+ }),
616
+ }
617
+
618
+ /** Évalue une formule ("=…" optionnel). Retourne une valeur ou une FormulaError. */
619
+ export function evaluateFormula(source: string, ctx: FormulaContext): FormulaValue {
620
+ const src = source.startsWith("=") ? source.slice(1) : source
621
+ try {
622
+ const parser = new Parser(src)
623
+ const node = parser.parseExpr()
624
+ if (!parser.eof()) return err("#ERROR!")
625
+ const v = evalNode(node, ctx, false)
626
+ return Array.isArray(v) ? err("#VALUE!") : v
627
+ } catch {
628
+ return err("#ERROR!")
629
+ }
630
+ }
package/lib/icons.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Icônes par défaut du design system — noms Iconify (famille lucide:),
2
+ // rendus par le composant <Icon> de @iconify/vue (https://iconify.design/docs/icon-components/vue/).
3
+ // Source unique des icônes internes : un composant peut exposer une prop
4
+ // d'icône publique (string) qui surcharge ces défauts.
5
+ export const icons = {
6
+ arrowDown: "lucide:arrow-down",
7
+ arrowUp: "lucide:arrow-up",
8
+ arrowUpDown: "lucide:arrow-up-down",
9
+ calendarDays: "lucide:calendar-days",
10
+ check: "lucide:check",
11
+ chevronDown: "lucide:chevron-down",
12
+ chevronLeft: "lucide:chevron-left",
13
+ chevronRight: "lucide:chevron-right",
14
+ chevronUp: "lucide:chevron-up",
15
+ chevronsLeft: "lucide:chevrons-left",
16
+ chevronsRight: "lucide:chevrons-right",
17
+ copy: "lucide:copy",
18
+ eye: "lucide:eye",
19
+ eyeOff: "lucide:eye-off",
20
+ file: "lucide:file",
21
+ fileArchive: "lucide:file-archive",
22
+ fileAudio: "lucide:file-audio",
23
+ fileCode: "lucide:file-code",
24
+ fileImage: "lucide:file-image",
25
+ filePlus: "lucide:file-plus",
26
+ fileSpreadsheet: "lucide:file-spreadsheet",
27
+ fileText: "lucide:file-text",
28
+ fileVideo: "lucide:file-video",
29
+ gripVertical: "lucide:grip-vertical",
30
+ imagePlus: "lucide:image-plus",
31
+ menu: "lucide:menu",
32
+ minus: "lucide:minus",
33
+ plus: "lucide:plus",
34
+ refreshCw: "lucide:refresh-cw",
35
+ search: "lucide:search",
36
+ star: "lucide:star",
37
+ x: "lucide:x",
38
+ } as const