@dnax/ui 0.0.28 → 0.0.30
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/QBar.vue +42 -0
- package/components/QBoard.vue +285 -0
- package/components/QConfigProvider.vue +9 -5
- package/components/QInteract.vue +387 -0
- package/components/QSpreadsheet.vue +4662 -0
- package/components/QVideo.vue +17 -1
- package/index.ts +6 -0
- package/lib/config.ts +6 -0
- package/lib/formula.test.ts +206 -0
- package/lib/formula.ts +292 -0
- package/lib/icons.ts +17 -0
- package/lib/intersection.ts +118 -0
- package/lib/spreadsheet.test.ts +92 -0
- package/lib/spreadsheet.ts +175 -0
- package/lib/useComponentProps.ts +7 -0
- package/module.ts +2 -0
- package/package.json +3 -1
- package/scripts/generate-exports.ts +2 -0
- package/shims.d.ts +6 -0
- package/styles/main.css +1344 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBar — barre fine façon Quasar : ligne compacte en haut d'une fenêtre /
|
|
3
|
+
// d'une app (contrôles fenêtre, menus, statut…). Utile pour Electron
|
|
4
|
+
// frameless, entêtes de dialogs desktop, etc. API : <q-bar dense dark>…
|
|
5
|
+
import type { Component } from "vue"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Compact (hauteur réduite) */
|
|
9
|
+
dense?: boolean
|
|
10
|
+
/** Force le style sombre (fond foncé, texte clair) */
|
|
11
|
+
dark?: boolean
|
|
12
|
+
/** Balise rendue (défaut : div) */
|
|
13
|
+
as?: Component | string
|
|
14
|
+
/** Rôle ARIA (défaut : toolbar) — passer "" pour aucun */
|
|
15
|
+
role?: string
|
|
16
|
+
/** aria-label du toolbar */
|
|
17
|
+
label?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
dense: false,
|
|
22
|
+
dark: false,
|
|
23
|
+
as: "div",
|
|
24
|
+
role: "toolbar",
|
|
25
|
+
label: "Bar",
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<component
|
|
31
|
+
:is="props.as"
|
|
32
|
+
class="q-bar"
|
|
33
|
+
:class="{
|
|
34
|
+
'q-bar--dense': dense,
|
|
35
|
+
'q-bar--dark': dark,
|
|
36
|
+
}"
|
|
37
|
+
:role="role || undefined"
|
|
38
|
+
:aria-label="role === 'toolbar' ? label : undefined"
|
|
39
|
+
>
|
|
40
|
+
<slot />
|
|
41
|
+
</component>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QBoard — tableau de bord type Power BI : une grille de tuiles en arrière-plan
|
|
3
|
+
// (colonnes × rangées) et, À L'INTÉRIEUR, QInteract qui gère le drag & resize
|
|
4
|
+
// des widgets. Les items sont exprimés en TUILLES :
|
|
5
|
+
// { id, column, row, spanColumns, spanRows, … }
|
|
6
|
+
// QBoard traduit ces tuiles en pixels pour QInteract et reconvertit les
|
|
7
|
+
// positions déplacées / redimensionnées en tuiles (v-model:items).
|
|
8
|
+
export interface QBoardItem {
|
|
9
|
+
/** Identifiant unique */
|
|
10
|
+
id: string | number
|
|
11
|
+
/** Colonne de départ (0-based) */
|
|
12
|
+
column: number
|
|
13
|
+
/** Rangée de départ (0-based) */
|
|
14
|
+
row: number
|
|
15
|
+
/** Nombre de colonnes occupées */
|
|
16
|
+
spanColumns: number
|
|
17
|
+
/** Nombre de rangées occupées */
|
|
18
|
+
spanRows: number
|
|
19
|
+
/** Données métier libres (title, type de widget, …) */
|
|
20
|
+
[key: string]: any
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
26
|
+
import QInteract from "./QInteract.vue"
|
|
27
|
+
import type { QInteractItem } from "./QInteract.vue"
|
|
28
|
+
import { cn } from "../lib/utils"
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
/** Tuiles (v-model:items) : [{ id, column, row, spanColumns, spanRows, … }] */
|
|
32
|
+
items?: QBoardItem[]
|
|
33
|
+
/** Tuile sélectionnée (v-model:selected) */
|
|
34
|
+
selected?: string | number | null
|
|
35
|
+
/** Nombre de colonnes de la grille (défaut 12) */
|
|
36
|
+
columns?: number
|
|
37
|
+
/** Hauteur d'une rangée en px (défaut 64) */
|
|
38
|
+
rowHeight?: number
|
|
39
|
+
/** Nombre de rangées visibles (défaut : calculé depuis les tuiles) */
|
|
40
|
+
rows?: number
|
|
41
|
+
/** Hauteur du conteneur (px ou CSS) — sinon auto depuis rows */
|
|
42
|
+
height?: string | number
|
|
43
|
+
/** Marge intérieure (px) */
|
|
44
|
+
padding?: number
|
|
45
|
+
/** Espacement entre les tuiles (px, défaut 6) */
|
|
46
|
+
gap?: number
|
|
47
|
+
/** Autorise drag & resize (défaut true) */
|
|
48
|
+
interactive?: boolean
|
|
49
|
+
/** Lecture seule : drag & resize désactivés, poignées masquées */
|
|
50
|
+
readonly?: boolean
|
|
51
|
+
/** Drag limité à la poignée haute */
|
|
52
|
+
handle?: boolean
|
|
53
|
+
/** Empêche le chevauchement des tuiles (défaut true) */
|
|
54
|
+
collision?: boolean
|
|
55
|
+
/** Affiche la numérotation des tuiles de fond */
|
|
56
|
+
showLabels?: boolean
|
|
57
|
+
dense?: boolean
|
|
58
|
+
dark?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
62
|
+
items: () => [],
|
|
63
|
+
selected: null,
|
|
64
|
+
columns: 12,
|
|
65
|
+
rowHeight: 64,
|
|
66
|
+
rows: 0,
|
|
67
|
+
height: undefined,
|
|
68
|
+
padding: 10,
|
|
69
|
+
gap: 6,
|
|
70
|
+
interactive: true,
|
|
71
|
+
readonly: false,
|
|
72
|
+
handle: false,
|
|
73
|
+
collision: true,
|
|
74
|
+
showLabels: false,
|
|
75
|
+
dense: false,
|
|
76
|
+
dark: false,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const emit = defineEmits<{
|
|
80
|
+
"update:items": [value: QBoardItem[]]
|
|
81
|
+
"update:selected": [value: string | number | null]
|
|
82
|
+
select: [item: QBoardItem]
|
|
83
|
+
"drag-end": [item: QBoardItem]
|
|
84
|
+
"resize-end": [item: QBoardItem]
|
|
85
|
+
}>()
|
|
86
|
+
|
|
87
|
+
const grid = ref<QBoardItem[]>([])
|
|
88
|
+
watch(
|
|
89
|
+
() => props.items,
|
|
90
|
+
(v) => {
|
|
91
|
+
if (v && v !== grid.value) grid.value = v.map((it) => ({ ...it }))
|
|
92
|
+
},
|
|
93
|
+
{ immediate: true },
|
|
94
|
+
)
|
|
95
|
+
const push = (next: QBoardItem[]) => {
|
|
96
|
+
grid.value = next
|
|
97
|
+
emit("update:items", next)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sel = ref<string | number | null>(props.selected)
|
|
101
|
+
watch(
|
|
102
|
+
() => props.selected,
|
|
103
|
+
(v) => {
|
|
104
|
+
sel.value = v
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// ─── Largeur réelle (Responsive : la cellule s'adapte à la largeur) ───
|
|
109
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
110
|
+
const widthPx = ref(0)
|
|
111
|
+
let ro: ResizeObserver | null = null
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
if (rootEl.value) widthPx.value = rootEl.value.clientWidth
|
|
114
|
+
if (typeof ResizeObserver !== "undefined" && rootEl.value) {
|
|
115
|
+
ro = new ResizeObserver((entries) => {
|
|
116
|
+
const w = entries[0]?.contentRect.width ?? 0
|
|
117
|
+
if (w !== widthPx.value) widthPx.value = w
|
|
118
|
+
})
|
|
119
|
+
ro.observe(rootEl.value)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
onBeforeUnmount(() => ro?.disconnect())
|
|
123
|
+
|
|
124
|
+
const innerWidth = computed(() => Math.max(1, widthPx.value - props.padding * 2))
|
|
125
|
+
const cellW = computed(() => innerWidth.value / Math.max(1, props.columns))
|
|
126
|
+
|
|
127
|
+
// ─── Hauteur ───
|
|
128
|
+
const usedRows = computed(() => {
|
|
129
|
+
if (props.rows > 0) return props.rows
|
|
130
|
+
let max = 1
|
|
131
|
+
for (const it of grid.value) max = Math.max(max, it.row + (it.spanRows || 1))
|
|
132
|
+
return max
|
|
133
|
+
})
|
|
134
|
+
const heightStyle = computed<Record<string, string>>(() =>
|
|
135
|
+
props.height !== undefined
|
|
136
|
+
? { height: typeof props.height === "number" ? props.height + "px" : props.height }
|
|
137
|
+
: { height: usedRows.value * props.rowHeight + props.padding * 2 + "px" },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// ─── Tuiles → pixels (pour QInteract) ───
|
|
141
|
+
const pxItems = ref<QInteractItem[]>([])
|
|
142
|
+
const clampCol = (c: number) => Math.max(0, Math.min(props.columns - 1, c))
|
|
143
|
+
const buildPx = () => {
|
|
144
|
+
pxItems.value = grid.value.map((it) => {
|
|
145
|
+
const col = Math.min(it.column, props.columns - 1)
|
|
146
|
+
return {
|
|
147
|
+
...it,
|
|
148
|
+
x: Math.round(col * cellW.value) + props.padding,
|
|
149
|
+
y: Math.round(it.row * props.rowHeight) + props.padding,
|
|
150
|
+
w: Math.round((it.spanColumns || 1) * cellW.value),
|
|
151
|
+
h: Math.round((it.spanRows || 1) * props.rowHeight),
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
watch([grid, cellW, () => props.rowHeight, () => props.padding], buildPx, { immediate: true })
|
|
156
|
+
watch(innerWidth, () => nextTickPx())
|
|
157
|
+
|
|
158
|
+
let pending = false
|
|
159
|
+
const nextTickPx = () => {
|
|
160
|
+
if (pending) return
|
|
161
|
+
pending = true
|
|
162
|
+
nextTick(() => {
|
|
163
|
+
pending = false
|
|
164
|
+
buildPx()
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── QInteract (pixels) → tuiles ───
|
|
169
|
+
const toGridItem = (px: QInteractItem): QBoardItem => {
|
|
170
|
+
const cw = cellW.value || 1
|
|
171
|
+
const column = clampCol(Math.round((px.x - props.padding) / cw))
|
|
172
|
+
const spanColumns = Math.max(1, Math.round(px.w / cw))
|
|
173
|
+
const row = Math.max(0, Math.round((px.y - props.padding) / props.rowHeight))
|
|
174
|
+
const spanRows = Math.max(1, Math.round(px.h / props.rowHeight))
|
|
175
|
+
return { ...px, column, row, spanColumns, spanRows } as QBoardItem
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const onInteractItems = (pxList: QInteractItem[]) => {
|
|
179
|
+
const next = pxList.map(toGridItem)
|
|
180
|
+
// recentre les tuiles si elles débordent de la grille
|
|
181
|
+
for (const it of next) {
|
|
182
|
+
it.column = clampCol(Math.min(it.column, props.columns - (it.spanColumns || 1)))
|
|
183
|
+
}
|
|
184
|
+
push(next)
|
|
185
|
+
buildPx()
|
|
186
|
+
}
|
|
187
|
+
const onSelect = (px: QInteractItem) => {
|
|
188
|
+
const g = toGridItem(px)
|
|
189
|
+
sel.value = g.id
|
|
190
|
+
emit("update:selected", g.id)
|
|
191
|
+
emit("select", g)
|
|
192
|
+
}
|
|
193
|
+
const forwardDragEnd = (px: QInteractItem) => {
|
|
194
|
+
emit("drag-end", toGridItem(px))
|
|
195
|
+
}
|
|
196
|
+
const forwardResizeEnd = (px: QInteractItem) => {
|
|
197
|
+
emit("resize-end", toGridItem(px))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const gridToUser = (g: QBoardItem) => g
|
|
201
|
+
const userOf = (px: QInteractItem) => toGridItem(px)
|
|
202
|
+
|
|
203
|
+
const gridStyle = computed<Record<string, string>>(() => ({
|
|
204
|
+
backgroundSize: Math.max(1, cellW.value) + "px " + props.rowHeight + "px",
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
const rootClasses = computed(() =>
|
|
208
|
+
cn("q-board", props.dark && "q-board--dark", props.dense && "q-board--dense"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// ─── API ───
|
|
212
|
+
const addItem = (item: QBoardItem) => {
|
|
213
|
+
push([...grid.value, { ...item }])
|
|
214
|
+
sel.value = item.id
|
|
215
|
+
emit("update:selected", item.id)
|
|
216
|
+
}
|
|
217
|
+
const removeItem = (id: string | number) => {
|
|
218
|
+
push(grid.value.filter((it) => String(it.id) !== String(id)))
|
|
219
|
+
if (String(sel.value) === String(id)) sel.value = null
|
|
220
|
+
}
|
|
221
|
+
const clear = () => push([])
|
|
222
|
+
const layout = () => grid.value.map((it) => ({ ...it }))
|
|
223
|
+
|
|
224
|
+
defineExpose({
|
|
225
|
+
addItem,
|
|
226
|
+
removeItem,
|
|
227
|
+
clear,
|
|
228
|
+
layout,
|
|
229
|
+
getItems: () => grid.value.map((it) => ({ ...it })),
|
|
230
|
+
columns: () => props.columns,
|
|
231
|
+
rows: () => usedRows.value,
|
|
232
|
+
})
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<div ref="rootEl" class="q-board" :class="rootClasses" :style="heightStyle">
|
|
237
|
+
<!-- ═══════ Grille de tuiles en arrière-plan ═══════ -->
|
|
238
|
+
<div class="q-board__grid" :style="gridStyle" aria-hidden="true">
|
|
239
|
+
<template v-if="showLabels">
|
|
240
|
+
<span
|
|
241
|
+
v-for="c in columns"
|
|
242
|
+
:key="'c' + c"
|
|
243
|
+
class="q-board__cell-label q-board__cell-label--col"
|
|
244
|
+
:style="{ left: props.padding + (c - 1) * cellW + 'px', width: cellW + 'px' }"
|
|
245
|
+
>{{ c }}</span>
|
|
246
|
+
<span
|
|
247
|
+
v-for="r in usedRows"
|
|
248
|
+
:key="'r' + r"
|
|
249
|
+
class="q-board__cell-label q-board__cell-label--row"
|
|
250
|
+
:style="{ top: props.padding + (r - 1) * rowHeight + 'px', height: rowHeight + 'px' }"
|
|
251
|
+
>{{ r }}</span>
|
|
252
|
+
</template>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- ═══════ QInteract à l'intérieur (drag & resize) ═══════ -->
|
|
256
|
+
<QInteract
|
|
257
|
+
class="q-board__interact"
|
|
258
|
+
:items="pxItems"
|
|
259
|
+
:selected="sel"
|
|
260
|
+
:interactive="interactive"
|
|
261
|
+
:readonly="readonly"
|
|
262
|
+
:handle="handle"
|
|
263
|
+
:snap="1"
|
|
264
|
+
:collision="collision"
|
|
265
|
+
:padding="props.padding"
|
|
266
|
+
:gap="gap"
|
|
267
|
+
:min-width="cellW"
|
|
268
|
+
:min-height="rowHeight"
|
|
269
|
+
:dense="dense"
|
|
270
|
+
@update:items="onInteractItems"
|
|
271
|
+
@update:selected="(v) => (sel = v)"
|
|
272
|
+
@select="onSelect"
|
|
273
|
+
@drag-end="forwardDragEnd"
|
|
274
|
+
@resize-end="forwardResizeEnd"
|
|
275
|
+
>
|
|
276
|
+
<template #item="{ item }">
|
|
277
|
+
<slot name="item" :item="userOf(item)" />
|
|
278
|
+
</template>
|
|
279
|
+
<template #handle="{ item }">
|
|
280
|
+
<slot name="handle" :item="userOf(item)" />
|
|
281
|
+
</template>
|
|
282
|
+
<slot name="empty" />
|
|
283
|
+
</QInteract>
|
|
284
|
+
</div>
|
|
285
|
+
</template>
|
|
@@ -12,7 +12,7 @@ import QLoadingProvider from "./QLoadingProvider.vue"
|
|
|
12
12
|
import QBottomSheetProvider from "./QBottomSheetProvider.vue"
|
|
13
13
|
import QImagePreviewProvider from "./QImagePreviewProvider.vue"
|
|
14
14
|
import { qConfigKey, qProvidersKey } from "../lib/config"
|
|
15
|
-
import type { QConfigContext, QTheme, ThemeMode } from "../lib/config"
|
|
15
|
+
import type { QAppLang, QConfigContext, QTheme, ThemeMode } from "../lib/config"
|
|
16
16
|
import { isRadiusScale, RADIUS_VALUES } from "../lib/useComponentProps"
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
@@ -23,6 +23,8 @@ interface Props {
|
|
|
23
23
|
* :theme="{ mode: 'dark', colors: { primary: '#ff0000' }, componentProps: { QBtn: { radius: 'md' } } }"
|
|
24
24
|
*/
|
|
25
25
|
theme?: QTheme | ThemeMode
|
|
26
|
+
/** Langue appliquée aux composants qui la supportent ("en" | "fr") */
|
|
27
|
+
lang?: QAppLang
|
|
26
28
|
/** Rend un div conteneur ; sinon fournit le thème sans élément DOM */
|
|
27
29
|
render?: boolean
|
|
28
30
|
/** Classe(s) additionnelle(s) sur le conteneur */
|
|
@@ -33,6 +35,7 @@ interface Props {
|
|
|
33
35
|
|
|
34
36
|
const props = withDefaults(defineProps<Props>(), {
|
|
35
37
|
theme: () => ({}),
|
|
38
|
+
lang: undefined,
|
|
36
39
|
render: true,
|
|
37
40
|
class: "",
|
|
38
41
|
style: undefined,
|
|
@@ -67,9 +70,13 @@ const mergedTheme = computed<QTheme>(() => {
|
|
|
67
70
|
mode: self.mode ?? parentTheme?.mode ?? "system",
|
|
68
71
|
colors: { ...parentTheme?.colors, ...self.colors },
|
|
69
72
|
componentProps: mergeComponentProps(parentTheme?.componentProps, self.componentProps),
|
|
73
|
+
lang: props.lang ?? self.lang ?? parentTheme?.lang ?? "en",
|
|
70
74
|
}
|
|
71
75
|
})
|
|
72
76
|
|
|
77
|
+
/** Langue effective : prop > theme.lang > parent */
|
|
78
|
+
const lang = computed<QAppLang>(() => mergedTheme.value.lang ?? "en")
|
|
79
|
+
|
|
73
80
|
// — Mode clair/sombre —
|
|
74
81
|
const systemDark = ref(false)
|
|
75
82
|
let mql: MediaQueryList | null = null
|
|
@@ -100,9 +107,6 @@ const isDark = computed<boolean>(() => {
|
|
|
100
107
|
if (mode === "light") return false
|
|
101
108
|
return systemDark.value
|
|
102
109
|
})
|
|
103
|
-
|
|
104
|
-
// Applique .dark sur le conteneur ET sur <html> (les overlays téléportés au body
|
|
105
|
-
// — dialog, sheets — doivent aussi passer en sombre)
|
|
106
110
|
watch(
|
|
107
111
|
isDark,
|
|
108
112
|
(dark) => {
|
|
@@ -113,7 +117,7 @@ watch(
|
|
|
113
117
|
{ immediate: true },
|
|
114
118
|
)
|
|
115
119
|
|
|
116
|
-
provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark })
|
|
120
|
+
provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark, lang })
|
|
117
121
|
|
|
118
122
|
// Providers intégrés ($q.dialog + $q.notify) : rendus UNE fois par le
|
|
119
123
|
// QConfigProvider le plus externe (les imbriqués ne re-rendent pas → pas de doublons)
|