@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
@@ -0,0 +1,491 @@
1
+ <script lang="ts">
2
+ // QDataGrid — grille type tableur (inspirée de MUI X Data Grid) :
3
+ // sélection de cellule au clavier (flèches, Tab), édition en place (Entrée/F2,
4
+ // double-clic, ou frappe directe), et formules style spreadsheet sur les colonnes
5
+ // allow-formulas : =price * quantity, =SUM(E1:E3), =E4 * $B$5 …
6
+ export interface QDataGridColumn {
7
+ /** Clé de la propriété dans la ligne */
8
+ name: string
9
+ label?: string
10
+ type?: "string" | "number" | "boolean"
11
+ /** Largeur de colonne (nombre px ou chaîne CSS) */
12
+ width?: number | string
13
+ align?: "left" | "center" | "right"
14
+ /** La cellule est éditable (Entrée / double-clic / frappe) */
15
+ editable?: boolean
16
+ /** Les valeurs "=…" sont évaluées comme formules (sinon chaîne littérale) */
17
+ allowFormulas?: boolean
18
+ /** Formatteur de la valeur affichée (après évaluation) */
19
+ format?: (val: any) => any
20
+ headerClass?: string
21
+ headerStyle?: string
22
+ }
23
+
24
+ export interface QDataGridCell {
25
+ row: number
26
+ column: string
27
+ }
28
+ </script>
29
+
30
+ <script setup lang="ts">
31
+ import { computed, nextTick, ref, watch } from "vue"
32
+ import { cn } from "../lib/utils"
33
+ import { evaluateFormula, FormulaError, isError } from "../lib/formula"
34
+ import type { FormulaValue } from "../lib/formula"
35
+ import { radiusStyle, useRadius } from "../lib/useComponentProps"
36
+ import type { RadiusProp } from "../lib/useComponentProps"
37
+
38
+ interface Props {
39
+ /** Lignes (v-model:rows) */
40
+ rows?: Record<string, any>[]
41
+ /** Colonnes ({ name, label, type, width, align, editable, allowFormulas, format, headerClass, headerStyle }) */
42
+ columns?: QDataGridColumn[]
43
+ /** Hauteur du conteneur scrollable (nombre px ou chaîne CSS, défaut "320px") */
44
+ height?: string | number
45
+ /** Hauteur réduite */
46
+ dense?: boolean
47
+ /** Sans ombre */
48
+ flat?: boolean
49
+ /** Bordure */
50
+ bordered?: boolean
51
+ /** Séparateurs : horizontal | vertical | cell | none */
52
+ separator?: "horizontal" | "vertical" | "cell" | "none"
53
+ dark?: boolean
54
+ /** Désactive l'édition (lecture seule) */
55
+ readonly?: boolean
56
+ /** Cellules sélectionnées (v-model:selected) */
57
+ selected?: QDataGridCell[]
58
+ /** Classe CSS appliquée à toutes les cellules d'en-tête */
59
+ headerClass?: string
60
+ /** Style CSS appliqué à toutes les cellules d'en-tête */
61
+ headerStyle?: string
62
+ /** Coins arrondis (échelle xs|sm|md|lg ou none) — défaut : composantProps */
63
+ radius?: RadiusProp
64
+ }
65
+
66
+ const props = withDefaults(defineProps<Props>(), {
67
+ rows: () => [],
68
+ columns: () => [],
69
+ height: "320px",
70
+ dense: false,
71
+ flat: false,
72
+ bordered: false,
73
+ separator: "horizontal",
74
+ dark: false,
75
+ readonly: false,
76
+ selected: undefined,
77
+ headerClass: "",
78
+ headerStyle: "",
79
+ })
80
+
81
+ const emit = defineEmits<{
82
+ "update:rows": [value: Record<string, any>[]]
83
+ "update:selected": [value: QDataGridCell[]]
84
+ "cell-click": [payload: { row: number; column: string; value: any }]
85
+ "cell-change": [payload: { row: number; column: string; oldValue: any; newValue: any; rows: Record<string, any>[] }]
86
+ "cell-edit-start": [payload: { row: number; column: string }]
87
+ "cell-edit-end": [payload: { row: number; column: string; canceled: boolean }]
88
+ }>()
89
+
90
+ // ─── Données internes (copie éditable des rows) ───
91
+ const state = ref<Record<string, any>[]>([])
92
+ watch(
93
+ () => props.rows,
94
+ (v) => {
95
+ if (v && v !== state.value) state.value = v.map((r) => ({ ...r }))
96
+ },
97
+ { immediate: true },
98
+ )
99
+
100
+ // ─── Sélection ───
101
+ const sel = ref<QDataGridCell[]>([])
102
+ watch(
103
+ () => props.selected,
104
+ (v) => {
105
+ if (v) sel.value = v
106
+ },
107
+ { immediate: true, deep: true },
108
+ )
109
+
110
+ const isSelected = (row: number, column: string) =>
111
+ sel.value.some((c) => c.row === row && c.column === column)
112
+
113
+ const select = (row: number, column: string) => {
114
+ sel.value = [{ row, column }]
115
+ emit("update:selected", sel.value)
116
+ nextTick(focusCell)
117
+ }
118
+
119
+ const moveSelection = (dr: number, dc: number) => {
120
+ if (!sel.value.length || !props.columns.length) return
121
+ const { row, column } = sel.value[0]!
122
+ const ci = props.columns.findIndex((c) => c.name === column)
123
+ const nr = Math.max(0, Math.min(state.value.length - 1, row + dr))
124
+ const nc = Math.max(0, Math.min(props.columns.length - 1, ci + dc))
125
+ select(nr, props.columns[nc]!.name)
126
+ }
127
+
128
+ const escAttr = (s: string) => String(s).replace(/"/g, '\\"')
129
+ const focusCell = () => {
130
+ if (!sel.value.length) return
131
+ const { row, column } = sel.value[0]!
132
+ const td = scrollEl.value?.querySelector<HTMLElement>(
133
+ `td[data-row="${row}"][data-col="${escAttr(column)}"]`,
134
+ )
135
+ td?.focus()
136
+ }
137
+
138
+ // ─── Évaluation des cellules (formules + graphe de dépendances) ───
139
+ const colIndex = (name: string) =>
140
+ props.columns.findIndex((c) => c.name.toLowerCase() === name.toLowerCase())
141
+
142
+ const evaluated = computed<FormulaValue[][]>(() => {
143
+ const memo = new Map<string, FormulaValue>()
144
+ const visiting = new Set<string>()
145
+
146
+ const get = (r: number, c: number): FormulaValue => {
147
+ const key = r + ":" + c
148
+ if (memo.has(key)) return memo.get(key)!
149
+ if (visiting.has(key)) return new FormulaError("#CYCLE!")
150
+ const col = props.columns[c]
151
+ const row = state.value[r]
152
+ if (!col || !row) return new FormulaError("#REF!")
153
+ const raw = row[col.name]
154
+ let val: FormulaValue = raw ?? null
155
+ if (typeof raw === "string" && raw.startsWith("=") && col.allowFormulas) {
156
+ visiting.add(key)
157
+ val = evaluateFormula(raw, {
158
+ row: r,
159
+ resolveField: (name) => {
160
+ const i = colIndex(name)
161
+ return i === -1 ? undefined : get(r, i)
162
+ },
163
+ resolveCell: (rr, cc) => get(rr, cc),
164
+ })
165
+ visiting.delete(key)
166
+ }
167
+ memo.set(key, val)
168
+ return val
169
+ }
170
+
171
+ return state.value.map((_, r) => props.columns.map((_, c) => get(r, c)))
172
+ })
173
+
174
+ const cellValue = (r: number, c: number) => evaluated.value[r]?.[c]
175
+ const cellText = (r: number, c: number): string => {
176
+ const v = cellValue(r, c)
177
+ const col = props.columns[c]
178
+ if (isError(v)) return v.toString()
179
+ let out = v
180
+ if (col?.format) out = col.format(v)
181
+ if (out === null || out === undefined || out === "") return ""
182
+ return String(out)
183
+ }
184
+
185
+ const cellTitle = (r: number, c: number): string | undefined => {
186
+ const col = props.columns[c]
187
+ if (!col) return undefined
188
+ const raw = state.value[r]?.[col.name]
189
+ return typeof raw === "string" && raw.startsWith("=") && col.allowFormulas ? raw : undefined
190
+ }
191
+
192
+ // ─── Édition ───
193
+ const editing = ref<{ row: number; column: string } | null>(null)
194
+ const draft = ref("")
195
+ const scrollEl = ref<HTMLElement | null>(null)
196
+ const editorEl = ref<HTMLElement | null>(null)
197
+ const editInputEl = ref<HTMLInputElement | null>(null)
198
+ const editAreaEl = ref<HTMLTextAreaElement | null>(null)
199
+ const editorStyle = ref<Record<string, string>>({})
200
+
201
+ const columnOf = (name: string) => props.columns.find((c) => c.name === name)
202
+ const isFormulaDraft = computed(() => {
203
+ const col = editing.value ? columnOf(editing.value.column) : undefined
204
+ return !!col?.allowFormulas && draft.value.startsWith("=")
205
+ })
206
+ const draftLines = computed(() => Math.min(5, Math.max(1, draft.value.split("\n").length)))
207
+ const editorInputType = computed(() => {
208
+ const col = editing.value ? columnOf(editing.value.column) : undefined
209
+ return col?.type === "number" ? "number" : "text"
210
+ })
211
+
212
+ const startEdit = (row: number, column: string, initial?: string) => {
213
+ if (props.readonly) return
214
+ const col = columnOf(column)
215
+ if (!col?.editable) return
216
+ const raw = state.value[row]?.[column]
217
+ draft.value =
218
+ initial ??
219
+ (typeof raw === "string" && raw.startsWith("=") ? raw : String(raw ?? ""))
220
+ editing.value = { row, column }
221
+ emit("cell-edit-start", { row, column })
222
+ nextTick(positionEditor)
223
+ }
224
+
225
+ const positionEditor = () => {
226
+ if (!editing.value || !scrollEl.value) return
227
+ const td = scrollEl.value.querySelector<HTMLElement>(
228
+ `td[data-row="${editing.value.row}"][data-col="${escAttr(editing.value.column)}"]`,
229
+ )
230
+ if (!td) return
231
+ const sc = scrollEl.value.getBoundingClientRect()
232
+ const tr = td.getBoundingClientRect()
233
+ // L'éditeur de formule s'élargit avec le draft (monospace ≈ 7.6px/car),
234
+ // sans dépasser le bord droit du conteneur.
235
+ const formulaW = isFormulaDraft.value
236
+ ? Math.min(480, Math.max(180, draft.value.length * 7.6 + 30))
237
+ : 0
238
+ const left = tr.left - sc.left + scrollEl.value.scrollLeft
239
+ const maxW = sc.width - (tr.left - sc.left) - 4
240
+ editorStyle.value = {
241
+ left: left + "px",
242
+ top: tr.top - sc.top + scrollEl.value.scrollTop + "px",
243
+ width: Math.min(maxW, Math.max(tr.width, formulaW)) + "px",
244
+ minHeight: tr.height + "px",
245
+ }
246
+ nextTick(() => {
247
+ ;(isFormulaDraft.value ? editAreaEl.value : editInputEl.value)?.focus()
248
+ })
249
+ }
250
+
251
+ const cancelEdit = () => {
252
+ if (!editing.value) return
253
+ const cell = { ...editing.value }
254
+ editing.value = null
255
+ emit("cell-edit-end", { ...cell, canceled: true })
256
+ nextTick(focusCell)
257
+ }
258
+
259
+ const commitEdit = () => {
260
+ if (!editing.value) return
261
+ const { row, column } = editing.value
262
+ const col = columnOf(column)
263
+ const old = state.value[row]?.[column]
264
+ const text = draft.value
265
+ let next: any
266
+ if (text.trim() === "") next = null
267
+ else if (text.startsWith("=") && col?.allowFormulas) next = text
268
+ else if (col?.type === "number") {
269
+ const n = Number(text)
270
+ next = Number.isNaN(n) ? old : n
271
+ } else if (col?.type === "boolean") {
272
+ next = text === "true" ? true : text === "false" ? false : old
273
+ } else next = text
274
+
275
+ editing.value = null
276
+ emit("cell-edit-end", { row, column, canceled: false })
277
+ nextTick(focusCell)
278
+
279
+ if (next !== old) {
280
+ const nextRow = { ...state.value[row], [column]: next }
281
+ const nextRows = [...state.value]
282
+ nextRows[row] = nextRow
283
+ state.value = nextRows
284
+ emit("update:rows", nextRows)
285
+ emit("cell-change", { row, column, oldValue: old, newValue: next, rows: nextRows })
286
+ }
287
+ }
288
+
289
+ const clearCell = (row: number, column: string) => {
290
+ const col = columnOf(column)
291
+ if (!col?.editable || props.readonly) return
292
+ const old = state.value[row]?.[column]
293
+ if (old === null || old === undefined || old === "") return
294
+ const nextRow = { ...state.value[row], [column]: null }
295
+ const nextRows = [...state.value]
296
+ nextRows[row] = nextRow
297
+ state.value = nextRows
298
+ emit("update:rows", nextRows)
299
+ emit("cell-change", { row, column, oldValue: old, newValue: null, rows: nextRows })
300
+ }
301
+
302
+ // ─── Clavier ───
303
+ const onKeydown = (e: KeyboardEvent) => {
304
+ if (editing.value || e.defaultPrevented) return
305
+ if (!sel.value.length) return
306
+ const { row, column } = sel.value[0]!
307
+ const col = columnOf(column)
308
+ const colIdx = props.columns.findIndex((c) => c.name === column)
309
+
310
+ if (e.key === "ArrowUp") return moveSelection(-1, 0)
311
+ if (e.key === "ArrowDown") return moveSelection(1, 0)
312
+ if (e.key === "ArrowLeft") return moveSelection(0, -1)
313
+ if (e.key === "ArrowRight") return moveSelection(0, 1)
314
+ if (e.key === "Enter" || e.key === "F2") {
315
+ e.preventDefault()
316
+ if (col?.editable && !props.readonly) startEdit(row, column)
317
+ return
318
+ }
319
+ if (e.key === "Tab") {
320
+ e.preventDefault()
321
+ moveSelection(0, 1)
322
+ return
323
+ }
324
+ if (e.key === "Delete" || e.key === "Backspace") {
325
+ e.preventDefault()
326
+ clearCell(row, column)
327
+ return
328
+ }
329
+ if ((e.key === "c" || e.key === "C") && (e.ctrlKey || e.metaKey)) {
330
+ e.preventDefault()
331
+ copySelected()
332
+ return
333
+ }
334
+ // Frappe directe : démarre l'édition
335
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
336
+ if (col?.editable && !props.readonly) {
337
+ e.preventDefault()
338
+ startEdit(row, column, e.key)
339
+ }
340
+ }
341
+ }
342
+
343
+ const onEditKeydown = (e: KeyboardEvent) => {
344
+ if (e.key === "Enter") {
345
+ if (isFormulaDraft.value && e.shiftKey) return // retour ligne dans la formule
346
+ e.preventDefault()
347
+ commitEdit()
348
+ moveSelection(1, 0) // Entrée commit puis descend (convention tableur)
349
+ } else if (e.key === "Escape") {
350
+ e.preventDefault()
351
+ cancelEdit()
352
+ } else if (e.key === "Tab") {
353
+ e.preventDefault()
354
+ commitEdit()
355
+ moveSelection(0, 1)
356
+ }
357
+ }
358
+
359
+ // Quand le draft devient une formule (=…), l'éditeur bascule input → textarea :
360
+ // le nouveau champ doit reprendre le focus.
361
+ watch(isFormulaDraft, () => {
362
+ if (!editing.value) return
363
+ nextTick(() => {
364
+ ;(isFormulaDraft.value ? editAreaEl.value : editInputEl.value)?.focus()
365
+ })
366
+ })
367
+
368
+ const copySelected = async () => {
369
+ if (!sel.value.length) return
370
+ const { row, column } = sel.value[0]!
371
+ const ci = props.columns.findIndex((c) => c.name === column)
372
+ const text = cellText(row, ci)
373
+ try {
374
+ await navigator.clipboard.writeText(text)
375
+ } catch {
376
+ /* presse-papiers indisponible */
377
+ }
378
+ }
379
+
380
+ const onCellClick = (row: number, column: string) => {
381
+ select(row, column)
382
+ emit("cell-click", { row, column, value: state.value[row]?.[column] })
383
+ }
384
+
385
+ const onScroll = () => {
386
+ if (editing.value) positionEditor()
387
+ }
388
+
389
+ // ─── Styles ───
390
+ const effectiveRadius = useRadius("QDataGrid", () => props.radius)
391
+
392
+ const rootClasses = computed(() =>
393
+ cn(
394
+ "q-data-grid",
395
+ props.flat && "q-data-grid--flat",
396
+ props.bordered && "q-data-grid--bordered",
397
+ props.dark && "q-data-grid--dark",
398
+ props.dense && "q-data-grid--dense",
399
+ `q-data-grid--separator-${props.separator}`,
400
+ ),
401
+ )
402
+
403
+ const cellClasses = (r: number, col: QDataGridColumn, c: number) =>
404
+ cn(
405
+ "q-data-grid__cell",
406
+ col.type === "number" && "q-data-grid__cell--number",
407
+ col.align === "center" && "q-table__cell--align-center",
408
+ col.align === "right" && "q-table__cell--align-right",
409
+ isError(cellValue(r, c)) && "q-data-grid__cell--error",
410
+ isSelected(r, col.name) && "q-data-grid__cell--selected",
411
+ editing.value?.row === r && editing.value?.column === col.name && "q-data-grid__cell--editing",
412
+ )
413
+
414
+ const colWidth = (col: QDataGridColumn) =>
415
+ col.width === undefined ? undefined : typeof col.width === "number" ? col.width + "px" : col.width
416
+
417
+ const heightCss = computed(() =>
418
+ typeof props.height === "number" ? props.height + "px" : props.height,
419
+ )
420
+ </script>
421
+
422
+ <template>
423
+ <div class="q-data-grid" :class="rootClasses" :style="radiusStyle(effectiveRadius)">
424
+ <div
425
+ ref="scrollEl"
426
+ class="q-data-grid__scroll"
427
+ :style="{ height: heightCss }"
428
+ @scroll="onScroll"
429
+ @keydown="onKeydown"
430
+ >
431
+ <table class="q-table__table q-data-grid__table">
432
+ <thead>
433
+ <tr>
434
+ <th
435
+ v-for="col in columns"
436
+ :key="col.name"
437
+ :class="[col.headerClass, headerClass]"
438
+ :style="[{ width: colWidth(col) }, col.headerStyle, headerStyle]"
439
+ >
440
+ {{ col.label ?? col.name }}
441
+ </th>
442
+ </tr>
443
+ </thead>
444
+ <tbody>
445
+ <tr v-for="(row, r) in state" :key="r">
446
+ <td
447
+ v-for="(col, c) in columns"
448
+ :key="col.name"
449
+ :data-row="r"
450
+ :data-col="col.name"
451
+ :tabindex="isSelected(r, col.name) ? 0 : -1"
452
+ :class="cellClasses(r, col, c)"
453
+ :title="cellTitle(r, c)"
454
+ @click="onCellClick(r, col.name)"
455
+ @dblclick="startEdit(r, col.name)"
456
+ >
457
+ {{ cellText(r, c) }}
458
+ </td>
459
+ </tr>
460
+ </tbody>
461
+ </table>
462
+
463
+ <div
464
+ v-if="editing"
465
+ ref="editorEl"
466
+ class="q-data-grid__editor"
467
+ :class="{ 'q-data-grid__editor--formula': isFormulaDraft }"
468
+ :style="editorStyle"
469
+ @mousedown.stop
470
+ >
471
+ <textarea
472
+ v-if="isFormulaDraft"
473
+ ref="editAreaEl"
474
+ v-model="draft"
475
+ :rows="draftLines"
476
+ spellcheck="false"
477
+ @keydown.stop="onEditKeydown"
478
+ @blur="commitEdit"
479
+ />
480
+ <input
481
+ v-else
482
+ ref="editInputEl"
483
+ v-model="draft"
484
+ :type="editorInputType"
485
+ @keydown.stop="onEditKeydown"
486
+ @blur="commitEdit"
487
+ />
488
+ </div>
489
+ </div>
490
+ </div>
491
+ </template>