@dnax/ui 0.0.28 → 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,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)