@dnax/ui 0.0.27 → 0.0.29

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.
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ // QTiptapTableView — NodeView Vue du nœud « table » : rend un vrai <table>
3
+ // avec le contentDOM ProseMirror dans le <tbody>. Les rangées/cellules restent
4
+ // du contenu ProseMirror classique (sélection, merge/split, clavier).
5
+ // Ajouts maison : colgroup + poignées de redimensionnement de colonnes
6
+ // (drag), appliqué localement via des largeurs en % (non persistées dans le
7
+ // document — pour persister il faudrait écrire les attrs colwidth des cellules).
8
+ import { computed, onBeforeUnmount, ref, watch } from "vue"
9
+ import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
10
+
11
+ const props = defineProps(nodeViewProps)
12
+
13
+ // Nombre de colonnes = largeur de la première rangée (les suivantes suivent
14
+ // les rowspans/colspans du schéma).
15
+ const cols = computed(() => {
16
+ const first = props.node?.firstChild
17
+ return first ? first.childCount : 0
18
+ })
19
+
20
+ // Largeurs en % (initialement égales) ; recalculees quand le nombre de
21
+ // colonnes change (ajout/suppression).
22
+ const widths = ref<number[]>([])
23
+ watch(
24
+ cols,
25
+ (count) => {
26
+ if (count === widths.value.length) return
27
+ const each = count > 0 ? 100 / count : 0
28
+ widths.value = Array.from({ length: count }, () => each)
29
+ },
30
+ { immediate: true },
31
+ )
32
+
33
+ // — Drag de redimensionnement (poignées entre colonnes) —
34
+ const tableEl = ref<HTMLElement | null>(null)
35
+ let dragging = false
36
+ let boundary = 0
37
+ let startX = 0
38
+ let leftStart = 0
39
+ let rightStart = 0
40
+ let tableWidth = 0
41
+
42
+ const onMove = (event: PointerEvent) => {
43
+ if (!dragging) return
44
+ const delta = event.clientX - startX
45
+ const minPx = 48
46
+ let leftPx = leftStart + delta
47
+ let rightPx = rightStart - delta
48
+ if (leftPx < minPx) {
49
+ rightPx -= minPx - leftPx
50
+ leftPx = minPx
51
+ }
52
+ if (rightPx < minPx) {
53
+ leftPx -= minPx - rightPx
54
+ rightPx = minPx
55
+ }
56
+ if (leftPx < minPx || rightPx < minPx) return
57
+ const total = tableWidth || 1
58
+ const next = [...widths.value]
59
+ next[boundary - 1] = (leftPx / total) * 100
60
+ next[boundary] = (rightPx / total) * 100
61
+ widths.value = next
62
+ }
63
+
64
+ const onUp = () => {
65
+ dragging = false
66
+ window.removeEventListener("pointermove", onMove)
67
+ window.removeEventListener("pointerup", onUp)
68
+ }
69
+
70
+ const startResize = (event: PointerEvent, boundaryIndex: number) => {
71
+ if (!props.editor.isEditable) return
72
+ event.preventDefault()
73
+ const table = tableEl.value
74
+ if (!table) return
75
+ const row = table.querySelector("tr")
76
+ if (!row) return
77
+ const leftCell = row.children[boundaryIndex - 1] as HTMLElement | undefined
78
+ const rightCell = row.children[boundaryIndex] as HTMLElement | undefined
79
+ if (!leftCell || !rightCell) return
80
+ tableWidth = table.getBoundingClientRect().width || 1
81
+ leftStart = leftCell.getBoundingClientRect().width
82
+ rightStart = rightCell.getBoundingClientRect().width
83
+ startX = event.clientX
84
+ boundary = boundaryIndex
85
+ dragging = true
86
+ window.addEventListener("pointermove", onMove)
87
+ window.addEventListener("pointerup", onUp)
88
+ }
89
+
90
+ onBeforeUnmount(() => {
91
+ window.removeEventListener("pointermove", onMove)
92
+ window.removeEventListener("pointerup", onUp)
93
+ })
94
+
95
+ // Position d'une poignée (bord droit de la colonne index) en %
96
+ const boundaryLeft = (index: number) => {
97
+ const acc = widths.value.slice(0, index).reduce((sum, w) => sum + w, 0)
98
+ return Math.min(acc, 99.5)
99
+ }
100
+ </script>
101
+
102
+ <template>
103
+ <node-view-wrapper as="div" class="q-tiptap__table-wrapper">
104
+ <table ref="tableEl" class="q-tiptap__table" :class="{ 'q-tiptap__table--resizable': editor.isEditable }">
105
+ <colgroup v-if="widths.length">
106
+ <col v-for="(width, i) in widths" :key="i" :style="{ width: width + '%' }" />
107
+ </colgroup>
108
+ <node-view-content as="tbody" />
109
+ </table>
110
+
111
+ <!-- Poignées de redimensionnement entre les colonnes (éditable seulement) -->
112
+ <span
113
+ v-for="index in Math.max(cols - 1, 0)"
114
+ :key="'handle-' + index"
115
+ class="q-tiptap__resize-handle"
116
+ :style="{ left: boundaryLeft(index) + '%' }"
117
+ role="separator"
118
+ aria-orientation="vertical"
119
+ :aria-label="'Resize column ' + index"
120
+ @pointerdown.prevent.stop="startResize($event, index)"
121
+ />
122
+ </node-view-wrapper>
123
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ // QTiptapTaskItemView — NodeView Vue du nœud « taskItem » : rend une vraie
3
+ // <q-checkbox> dnax.ui (au lieu du <input> natif du DOM par défaut).
4
+ // Le nœud est fourni par l'extension officielle TaskItem (schéma, keymap
5
+ // Enter/Tab, input rules "[ ]" / "[x]") dont on ne remplace que addNodeView.
6
+ //
7
+ // ⚠ NodeView Vue : la RACINE DOIT être <node-view-wrapper> (erreur Tiptap
8
+ // « Please use the NodeViewWrapper component » sinon). En v3 ce wrapper ne
9
+ // propage pas les attrs → data-checked/class cochée portés par le div interne
10
+ // qui contient <node-view-content> (le contentDOM peut être imbriqué).
11
+ import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
12
+ import QCheckbox from "../QCheckbox.vue"
13
+
14
+ // nodeViewProps : descripteurs runtime officiels (editor, node, updateAttributes,
15
+ // getPos…) — pattern documenté pour les NodeViews Vue dans Tiptap.
16
+ const props = defineProps(nodeViewProps)
17
+
18
+ const checked = () => !!props.node.attrs.checked
19
+ const isEditable = () => props.editor.isEditable
20
+
21
+ const onToggle = (value: unknown) => {
22
+ if (!isEditable()) return
23
+ if (typeof props.getPos !== "function") return
24
+ props.editor.chain().focus().command(({ tr }) => {
25
+ const position = props.getPos()
26
+ if (typeof position !== "number") return false
27
+ const current = tr.doc.nodeAt(position)
28
+ tr.setNodeMarkup(position, undefined, { ...current?.attrs, checked: Boolean(value) })
29
+ return true
30
+ }).run()
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <node-view-wrapper as="li">
36
+ <label class="q-tiptap__task-check" contenteditable="false" @mousedown.prevent>
37
+ <q-checkbox
38
+ :model-value="checked()"
39
+ dense
40
+ :disable="!isEditable()"
41
+ aria-label="Task done"
42
+ @update:model-value="onToggle"
43
+ />
44
+ </label>
45
+ <div class="q-tiptap__task-content" :class="{ 'q-tiptap__task-content--checked': checked() }">
46
+ <node-view-content />
47
+ </div>
48
+ </node-view-wrapper>
49
+ </template>
package/index.ts CHANGED
@@ -9,6 +9,8 @@ export { default as QAvatar } from "./components/QAvatar.vue"
9
9
  export { default as QBackHeader } from "./components/QBackHeader.vue"
10
10
  export { default as QBackTop } from "./components/QBackTop.vue"
11
11
  export { default as QBadge } from "./components/QBadge.vue"
12
+ export { default as QBar } from "./components/QBar.vue"
13
+ export { default as QBoard } from "./components/QBoard.vue"
12
14
  export { default as QBottomSheet } from "./components/QBottomSheet.vue"
13
15
  export { default as QBottomSheetFooter } from "./components/QBottomSheetFooter.vue"
14
16
  export { default as QBottomSheetHeader } from "./components/QBottomSheetHeader.vue"
@@ -44,6 +46,7 @@ export { default as QDialogFooter } from "./components/QDialogFooter.vue"
44
46
  export { default as QDialogHeader } from "./components/QDialogHeader.vue"
45
47
  export { default as QDialogProvider } from "./components/QDialogProvider.vue"
46
48
  export { default as QDialogTrigger } from "./components/QDialogTrigger.vue"
49
+ export { default as QEditorJs } from "./components/QEditorJs.vue"
47
50
  export { default as QFab } from "./components/QFab.vue"
48
51
  export { default as QFabAction } from "./components/QFabAction.vue"
49
52
  export { default as QFilePicker } from "./components/QFilePicker.vue"
@@ -63,6 +66,7 @@ export { default as QInput } from "./components/QInput.vue"
63
66
  export { default as QInputOtp } from "./components/QInputOtp.vue"
64
67
  export { default as QInputPassword } from "./components/QInputPassword.vue"
65
68
  export { default as QInputTag } from "./components/QInputTag.vue"
69
+ export { default as QInteract } from "./components/QInteract.vue"
66
70
  export { default as QIntersection } from "./components/QIntersection.vue"
67
71
  export { default as QItem } from "./components/QItem.vue"
68
72
  export { default as QItemSection } from "./components/QItemSection.vue"
@@ -110,6 +114,7 @@ export { default as QSlider } from "./components/QSlider.vue"
110
114
  export { default as QSpace } from "./components/QSpace.vue"
111
115
  export { default as QSpinner } from "./components/QSpinner.vue"
112
116
  export { default as QSplitter } from "./components/QSplitter.vue"
117
+ export { default as QSpreadsheet } from "./components/QSpreadsheet.vue"
113
118
  export { default as QSticky } from "./components/QSticky.vue"
114
119
  export { default as QSwipeCell } from "./components/QSwipeCell.vue"
115
120
  export { default as QSwiper } from "./components/QSwiper.vue"
@@ -122,6 +127,7 @@ export { default as QTable } from "./components/QTable.vue"
122
127
  export { default as QTabs } from "./components/QTabs.vue"
123
128
  export { default as QText } from "./components/QText.vue"
124
129
  export { default as QTextCaption } from "./components/QTextCaption.vue"
130
+ export { default as QTiptap } from "./components/QTiptap.vue"
125
131
  export { default as QToolbar } from "./components/QToolbar.vue"
126
132
  export { default as QTooltip } from "./components/QTooltip.vue"
127
133
  export { default as QUploader } from "./components/QUploader.vue"
@@ -144,6 +150,8 @@ export { screen } from "./lib/screen"
144
150
  export type { QScreen } from "./lib/screen"
145
151
  export { loading } from "./lib/loading"
146
152
  export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
153
+ export { localStorage, sessionStorage } from "./lib/storage"
154
+ export type { QWebStorage } from "./lib/storage"
147
155
  export {
148
156
  registerOverlay,
149
157
  unregisterOverlay,
@@ -154,3 +162,13 @@ export {
154
162
  } from "./lib/overlayBack"
155
163
  export type { OverlayHandle } from "./lib/overlayBack"
156
164
  export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
165
+ export { vTouchPan } from "./lib/touchPan"
166
+ export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
167
+ export { vTouchHold } from "./lib/touchHold"
168
+ export type { TouchHoldDetails, TouchHoldHandler } from "./lib/touchHold"
169
+ export { vTouchSwipe } from "./lib/touchSwipe"
170
+ export type { TouchSwipeDetails, TouchSwipeHandler, SwipeDirection } from "./lib/touchSwipe"
171
+ export { vTouchRepeat } from "./lib/touchRepeat"
172
+ export type { TouchRepeatDetails, TouchRepeatHandler } from "./lib/touchRepeat"
173
+ export { vIntersection } from "./lib/intersection"
174
+ export type { IntersectionValue } from "./lib/intersection"
package/lib/config.ts CHANGED
@@ -14,10 +14,14 @@ export interface QThemeColors {
14
14
  warning?: string
15
15
  }
16
16
 
17
+ export type QAppLang = "en" | "fr"
18
+
17
19
  export interface QTheme {
18
20
  /** Mode clair/sombre : light | dark | system (défaut : system = préférence OS) */
19
21
  mode?: ThemeMode
20
22
  colors?: QThemeColors
23
+ /** Langue par défaut appliquée aux composants qui la supportent */
24
+ lang?: QAppLang
21
25
  /** Props par défaut par composant (clé = nom du composant, ex. "QBtn") */
22
26
  componentProps?: Record<string, Record<string, unknown>>
23
27
  }
@@ -26,6 +30,8 @@ export interface QConfigContext {
26
30
  theme: Ref<QTheme>
27
31
  /** true si le mode effectif est sombre (résout "system") */
28
32
  isDark: Readonly<Ref<boolean>>
33
+ /** Langue de l'application ("en" par défaut) */
34
+ lang: Readonly<Ref<QAppLang>>
29
35
  }
30
36
 
31
37
  export const qConfigKey: InjectionKey<QConfigContext> = Symbol("q-config")
@@ -0,0 +1,206 @@
1
+ // Tests unitaires du moteur de formules A1 (bun test).
2
+ import { describe, expect, it } from "bun:test"
3
+ import { evaluateFormula, isError } from "./formula"
4
+ import type { FormulaValue } from "./formula"
5
+
6
+ type Grid = (string | number | boolean | null)[][]
7
+
8
+ function harness(grid: Grid, formulaRow = 0) {
9
+ return (f: string): FormulaValue =>
10
+ evaluateFormula(f, {
11
+ row: formulaRow,
12
+ resolveField: () => undefined,
13
+ resolveCell: (r, c) => {
14
+ const row = grid[r]
15
+ return row && c >= 0 && c < row.length
16
+ ? (row[c] as FormulaValue | undefined)
17
+ : undefined
18
+ },
19
+ })
20
+ }
21
+
22
+ describe("opérateurs & références", () => {
23
+ const grid: Grid = [
24
+ [1, 2, 3],
25
+ [10, 20, 30],
26
+ ]
27
+ const e = harness(grid)
28
+ it("arithmétique et parenthèses", () => {
29
+ expect(e("=1+2*3")).toBe(7)
30
+ expect(e("=(1+2)*3")).toBe(9)
31
+ expect(e("=2^3")).toBe(8)
32
+ expect(e("=10%")).toBe(0.1)
33
+ expect(e("=200*10%")).toBe(20)
34
+ })
35
+ it("références A1 / plages / concat", () => {
36
+ expect(e("=A1+B1")).toBe(3)
37
+ expect(e("=A2*C1")).toBe(30)
38
+ expect(e('="x"&A1')).toBe("x1")
39
+ })
40
+ it("comparaisons", () => {
41
+ expect(e("=B2>B1")).toBe(true)
42
+ expect(e('=A1="1"')).toBe(true)
43
+ })
44
+ it("référence hors grille → #REF!", () => {
45
+ expect(String(e("=Z99"))).toBe("#REF!")
46
+ })
47
+ it("division par zéro → #DIV/0!", () => {
48
+ expect(String(e("=1/0"))).toBe("#DIV/0!")
49
+ })
50
+ })
51
+
52
+ describe("fonctions agrégation", () => {
53
+ const grid: Grid = [
54
+ [5, "x", 10],
55
+ [null, 2, 3],
56
+ ]
57
+ const e = harness(grid)
58
+ it("SUM / AVERAGE / MIN / MAX sur plages", () => {
59
+ expect(e("=SUM(A1:C2)")).toBe(20)
60
+ expect(e("=AVERAGE(A1,C1,C2)")).toBe(6)
61
+ expect(e("=MIN(A1:C2)")).toBe(2)
62
+ expect(e("=MAX(A1:C2)")).toBe(10)
63
+ })
64
+ it("COUNT compte les nombres, COUNTA les non-vides", () => {
65
+ expect(e("=COUNT(A1:C2)")).toBe(4)
66
+ expect(e("=COUNTA(A1:C2)")).toBe(5)
67
+ })
68
+ })
69
+
70
+ describe("fonctions logiques (lazy)", () => {
71
+ const e = harness([[]])
72
+ it("IF n'évalue que la branche prise", () => {
73
+ expect(e("=IF(1,1,1/0)")).toBe(1)
74
+ expect(e('=IF(0,1,"no")')).toBe("no")
75
+ })
76
+ it("IFS premier vrai", () => {
77
+ expect(e('=IFS(1>2,"a",1<2,"b")')).toBe("b")
78
+ expect(String(e("=IFS(0,1)"))).toBe("#N/A")
79
+ })
80
+ it("SWITCH avec défaut", () => {
81
+ expect(e('=SWITCH(2,1,"un",2,"deux","autre")')).toBe("deux")
82
+ expect(e('=SWITCH(9,1,"un","autre")')).toBe("autre")
83
+ })
84
+ it("IFERROR / IFNA", () => {
85
+ expect(e('=IFERROR(1/0,"ko")')).toBe("ko")
86
+ expect(e('=IFERROR(2,"ko")')).toBe(2)
87
+ })
88
+ it("AND / OR / NOT", () => {
89
+ expect(e("=AND(1,1>0)")).toBe(true)
90
+ expect(e("=OR(0,1=1)")).toBe(true)
91
+ expect(e("=NOT(0)")).toBe(true)
92
+ })
93
+ })
94
+
95
+ describe("VLOOKUP", () => {
96
+ const grid: Grid = [
97
+ ["A", 1, "x"],
98
+ ["B", 2, "y"],
99
+ ["A", 3, "z"],
100
+ ]
101
+ const e = harness(grid)
102
+ it("recherche exacte dans la 1re colonne", () => {
103
+ expect(e('=VLOOKUP("B",A1:C3,2)')).toBe(2)
104
+ expect(e('=VLOOKUP("A",A1:C3,3)')).toBe("x")
105
+ })
106
+ it("absence → #N/A, colonne hors plage → #REF!", () => {
107
+ expect(String(e('=VLOOKUP("Z",A1:C3,2)'))).toBe("#N/A")
108
+ expect(String(e('=VLOOKUP("A",A1:C3,5)'))).toBe("#REF!")
109
+ })
110
+ it("pas une plage → #VALUE!", () => {
111
+ expect(String(e('=VLOOKUP("A",A1,2)'))).toBe("#VALUE!")
112
+ })
113
+ })
114
+
115
+ describe("XLOOKUP + dates avancées", () => {
116
+ const grid: Grid = [
117
+ ["A", 1, "x"],
118
+ ["B", 2, "y"],
119
+ ["A", 3, "z"],
120
+ ]
121
+ const e = harness(grid)
122
+ it("XLOOKUP retourne la cellule correspondante du range retour", () => {
123
+ expect(e('=XLOOKUP("B",A1:A3,B1:B3)')).toBe(2)
124
+ expect(e('=XLOOKUP("A",A1:A3,C1:C3)')).toBe("x")
125
+ })
126
+ it("XLOOKUP fallback / #N/A", () => {
127
+ expect(String(e('=XLOOKUP("Z",A1:A3,B1:B3,"?")'))).toBe("?")
128
+ expect(String(e('=XLOOKUP("Z",A1:A3,B1:B3)'))).toBe("#N/A")
129
+ })
130
+ it("WEEKDAY / EOMONTH / DATEDIF / TEXT", () => {
131
+ const d = harness([[]])
132
+ // 2026-09-07 = lundi
133
+ expect(d('=WEEKDAY("2026-09-07",2)')).toBe(1)
134
+ expect(d('=EOMONTH("2026-01-15",0)')).toBe("2026-01-31")
135
+ expect(d('=DATEDIF("2026-01-01","2026-02-10","D")')).toBe(40)
136
+ expect(d('=TEXT(1.5,"0.00")')).toBe("1.50")
137
+ expect(d('=TEXT(0.12,"0%")')).toBe("12%")
138
+ expect(d('=TEXT("2026-09-07","YYYY-MM-DD")')).toBe("2026-09-07")
139
+ })
140
+ })
141
+
142
+ describe("fonctions texte", () => {
143
+ const e = harness([[]])
144
+ it("LEFT / RIGHT / MID", () => {
145
+ expect(e('=LEFT("Hello",2)')).toBe("He")
146
+ expect(e('=RIGHT("Hello",2)')).toBe("lo")
147
+ expect(e('=MID("Hello",2,3)')).toBe("ell")
148
+ })
149
+ it("FIND / SUBSTITUTE / REPLACE / CONCAT", () => {
150
+ expect(e('=FIND("llo","Hello")')).toBe(3)
151
+ expect(e('=SUBSTITUTE("a-b-a","a","x",2)')).toBe("a-b-x")
152
+ expect(e('=SUBSTITUTE("a-b-a","a","x")')).toBe("x-b-x")
153
+ expect(e('=REPLACE("Hello",1,2,"XY")')).toBe("XYllo")
154
+ expect(e('=CONCATENATE("a",1)')).toBe("a1")
155
+ })
156
+ it("UPPER / LOWER / TRIM / LEN", () => {
157
+ expect(e('=UPPER("aB")')).toBe("AB")
158
+ expect(e('=TRIM(" x ")')).toBe("x")
159
+ expect(e('=LEN("abc")')).toBe(3)
160
+ })
161
+ })
162
+
163
+ describe("fonctions dates", () => {
164
+ const e = harness([[]])
165
+ it("DATE construit en UTC", () => {
166
+ expect(e("=DATE(2026,9,7)")).toBe("2026-09-07")
167
+ })
168
+ it("YEAR / MONTH / DAY", () => {
169
+ expect(e('=YEAR("2026-09-07")')).toBe(2026)
170
+ expect(e('=MONTH("2026-09-07")')).toBe(9)
171
+ expect(e('=DAY("2026-09-07")')).toBe(7)
172
+ })
173
+ it("EDATE", () => {
174
+ expect(e('=EDATE("2026-01-15",1)')).toBe("2026-02-15")
175
+ })
176
+ it("TODAY / NOW / RAND sont valides", () => {
177
+ const t = e("=TODAY()")
178
+ expect(typeof t).toBe("string")
179
+ expect(String(t)).toMatch(/^\d{4}-\d{2}-\d{2}$/)
180
+ const r = e("=RAND()")
181
+ expect(typeof r).toBe("number")
182
+ expect(Number(r)).toBeGreaterThanOrEqual(0)
183
+ expect(Number(r)).toBeLessThan(1)
184
+ })
185
+ })
186
+
187
+ describe("math & erreurs de type", () => {
188
+ const e = harness([[]])
189
+ it("ABS / ROUND / INT / MOD / SQRT / POWER", () => {
190
+ expect(e("=ABS(-4)")).toBe(4)
191
+ expect(e("=ROUND(1.234,2)")).toBe(1.23)
192
+ expect(e("=INT(-3.7)")).toBe(-3)
193
+ expect(e("=MOD(7,3)")).toBe(1)
194
+ expect(e("=SQRT(16)")).toBe(4)
195
+ expect(e("=POWER(2,3)")).toBe(8)
196
+ })
197
+ it("#NAME? pour une fonction inconnue", () => {
198
+ expect(String(e("=NOPE(1)"))).toBe("#NAME?")
199
+ })
200
+ it("#VALUE! pour du texte en numéro", () => {
201
+ expect(String(e('=1+"a"'))).toBe("#VALUE!")
202
+ })
203
+ it("isError détecte FormulaError", () => {
204
+ expect(isError(e("=1/0"))).toBe(true)
205
+ })
206
+ })