@dnax/ui 0.0.46 → 0.0.49

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 (47) hide show
  1. package/components/QBottomSheet.vue +172 -6
  2. package/components/QBtn.vue +3 -4
  3. package/components/QContainer.vue +2 -2
  4. package/components/QImagePicker.vue +28 -1
  5. package/components/QInputChat.vue +859 -0
  6. package/components/QInputCurrency.vue +319 -0
  7. package/components/QItemLabel.vue +99 -0
  8. package/components/QMap.vue +410 -0
  9. package/components/QNumericKeyboard.vue +304 -0
  10. package/components/QPage.vue +26 -6
  11. package/components/QPageContainer.vue +32 -0
  12. package/components/QPopupProxy.vue +378 -0
  13. package/components/QQrcode.vue +132 -0
  14. package/components/QSpreadsheet.vue +395 -69
  15. package/components/QTime.vue +845 -0
  16. package/components/QTimeline.vue +111 -0
  17. package/components/QTimelineEntry.vue +271 -0
  18. package/index.ts +11 -1
  19. package/lib/bottomSheet.test.ts +121 -0
  20. package/lib/bottomSheet.ts +115 -0
  21. package/lib/color.ts +13 -0
  22. package/lib/currency.test.ts +193 -0
  23. package/lib/currency.ts +301 -0
  24. package/lib/fixedLayout.ts +12 -11
  25. package/lib/icons.ts +3 -20
  26. package/lib/map.test.ts +365 -0
  27. package/lib/map.ts +438 -0
  28. package/lib/mapEngine.ts +158 -0
  29. package/lib/mapLeaflet.ts +142 -0
  30. package/lib/mapMaplibre.ts +106 -0
  31. package/lib/mapMaptiler.ts +93 -0
  32. package/lib/numericKeyboard.test.ts +179 -0
  33. package/lib/numericKeyboard.ts +166 -0
  34. package/lib/pagePadding.test.ts +37 -0
  35. package/lib/pagePadding.ts +32 -0
  36. package/lib/qrcode.test.ts +103 -0
  37. package/lib/qrcode.ts +125 -0
  38. package/lib/spreadsheetChanges.test.ts +230 -0
  39. package/lib/spreadsheetChanges.ts +331 -0
  40. package/package.json +5 -17
  41. package/shims.d.ts +19 -0
  42. package/styles/main.css +702 -893
  43. package/components/QTiptap.vue +0 -996
  44. package/components/internal/QTiptapTableView.vue +0 -123
  45. package/components/internal/QTiptapTaskItemView.vue +0 -49
  46. package/lib/tiptap-table.ts +0 -19
  47. package/lib/tiptap-task-list.ts +0 -16
@@ -0,0 +1,304 @@
1
+ <script setup lang="ts">
2
+ // QNumericKeyboard — pavé numérique à l'écran : code PIN, montant, numéro. La valeur est une
3
+ // **chaîne** (elle garde les zéros de tête d'un code et le séparateur tel quel) ; c'est
4
+ // l'application qui l'affiche (points d'un code, montant formaté…).
5
+ //
6
+ // <q-numeric-keyboard v-model="pin" :max-length="6" @complete="submit" />
7
+ // <q-numeric-keyboard v-model="amount" mode="decimal" :max-decimals="2" clearable />
8
+ //
9
+ // Les règles de saisie (longueur, décimales, zéros de tête, effacement) vivent dans
10
+ // `lib/numericKeyboard.ts` (pur, testé).
11
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
12
+ import { Icon } from "@iconify/vue"
13
+ import { icons } from "../lib/icons"
14
+ import {
15
+ applyDigitOrder,
16
+ digitCount,
17
+ keypadLayout,
18
+ pressKey,
19
+ shuffledDigits,
20
+ type KeypadKey,
21
+ type KeypadMode,
22
+ } from "../lib/numericKeyboard"
23
+ import { cn } from "../lib/utils"
24
+
25
+ interface Props {
26
+ /** Valeur saisie (chaîne) */
27
+ modelValue?: string
28
+ /** `numeric` (défaut : code, téléphone) ou `decimal` (montant : touche séparateur) */
29
+ mode?: KeypadMode
30
+ /** Longueur maximale, en **chiffres** (0 = illimité) — 6 pour un code à six chiffres */
31
+ maxLength?: number
32
+ /** Décimales maximales après le séparateur (0 = illimité) */
33
+ maxDecimals?: number
34
+ /** Séparateur décimal affiché — `,` par défaut (français), `.` en anglais */
35
+ decimalSeparator?: string
36
+ /** Touche « tout effacer » (quand une case est libre : mode `numeric`) */
37
+ clearable?: boolean
38
+ /** Nombre de colonnes du pavé */
39
+ columns?: number
40
+ /** Touches plus compactes */
41
+ dense?: boolean
42
+ /** Affiche une rangée de **points** au-dessus du pavé (progression d'un code) ; le point qui
43
+ * se remplit s'anime (respecte `prefers-reduced-motion`). */
44
+ showDots?: boolean
45
+ /** Nombre de points affichés : par défaut `maxLength` ; si les deux sont à 0, un point par
46
+ * chiffre saisi (un champ masqué qui grandit). */
47
+ dots?: number
48
+ /** Taille des points : nombre (**px**) ou valeur CSS (`"14px"`, `"1.2rem"`, `"18px"`).
49
+ * Prioritaire sur `dense` (le défaut reste `--q-nk-dot-size`). */
50
+ dotsSize?: number | string
51
+ /** **Disposition aléatoire** des chiffres (anti-observation) : le mélange est tiré au montage
52
+ * côté client, puis **stable** — il ne change pas à chaque touche. Relancez-le via la méthode
53
+ * `shuffle()` (par ex. après chaque code). Les touches d'édition (`separator`, `C`, `⌫`) ne
54
+ * bougent jamais. */
55
+ random?: boolean
56
+ /** Thème sombre forcé (sinon celui du document) */
57
+ dark?: boolean
58
+ /** **État d'erreur** : les points passent en rouge et `errorMessage` s'affiche sous le pavé.
59
+ * Se remet à zéro tout seul dès que l'on **efface tout** (valeur vide) — l'événement
60
+ * `update:error` permet de le piloter en `v-model:error`. */
61
+ error?: boolean
62
+ /** Message affiché sous le pavé pendant l'erreur (masqué aussitôt l'erreur levée) */
63
+ errorMessage?: string
64
+ disable?: boolean
65
+ /** Libellé accessible du pavé */
66
+ label?: string
67
+ }
68
+
69
+ const props = withDefaults(defineProps<Props>(), {
70
+ modelValue: "",
71
+ mode: "numeric",
72
+ maxLength: 0,
73
+ maxDecimals: 0,
74
+ decimalSeparator: ",",
75
+ clearable: false,
76
+ columns: 3,
77
+ dense: false,
78
+ showDots: false,
79
+ dots: 0,
80
+ random: false,
81
+ dark: false,
82
+ error: false,
83
+ errorMessage: "",
84
+ disable: false,
85
+ label: "Clavier numérique",
86
+ })
87
+
88
+ const emit = defineEmits<{
89
+ "update:modelValue": [value: string]
90
+ /** Touche pressée : un chiffre, `"separator"`, `"backspace"` ou `"clear"` */
91
+ press: [key: string]
92
+ /** La valeur vient d'atteindre `maxLength` (valider un code automatiquement) */
93
+ complete: [value: string]
94
+ /** L'erreur est levée parce que l'on a tout effacé (`v-model:error`) */
95
+ "update:error": [value: boolean]
96
+ }>()
97
+
98
+ const options = computed(() => ({
99
+ mode: props.mode,
100
+ maxLength: props.maxLength,
101
+ maxDecimals: props.maxDecimals,
102
+ decimalSeparator: props.decimalSeparator,
103
+ clearable: props.clearable,
104
+ }))
105
+
106
+ // — Disposition aléatoire (optionnelle) —
107
+ // L'ordre est tiré **côté client** (au montage) : tirer pendant le rendu serveur ferait un écart
108
+ // d'hydratation (le serveur et le client obtiendraient deux dispositions différentes). Tant que
109
+ // l'ordre n'est pas tiré, la disposition canonique est rendue (SSR et premier rendu client
110
+ // identiques), puis le mélange s'applique.
111
+ const digitOrder = ref<string[] | null>(null)
112
+
113
+ /** Retire une nouvelle disposition aléatoire (voir la méthode exposée `shuffle`) */
114
+ const shuffle = () => {
115
+ digitOrder.value = shuffledDigits()
116
+ }
117
+
118
+ onMounted(() => {
119
+ if (props.random) shuffle()
120
+ })
121
+
122
+ watch(
123
+ () => props.random,
124
+ (enabled) => {
125
+ if (enabled) shuffle()
126
+ else digitOrder.value = null
127
+ },
128
+ )
129
+
130
+ const cells = computed(() => {
131
+ const base = keypadLayout(options.value)
132
+ return props.random && digitOrder.value ? applyDigitOrder(base, digitOrder.value) : base
133
+ })
134
+
135
+ // — Points de progression (optionnels) —
136
+ const digitsEntered = computed(() => digitCount(props.modelValue ?? ""))
137
+
138
+ /** Total de points : `dots` explicite, sinon `maxLength`, sinon un point par chiffre saisi */
139
+ const dotTotal = computed(() => {
140
+ if (props.dots > 0) return props.dots
141
+ if (props.maxLength > 0) return props.maxLength
142
+ return digitsEntered.value
143
+ })
144
+
145
+ const dotsFilled = computed(() => Math.min(digitsEntered.value, dotTotal.value))
146
+ const dotIndexes = computed(() => Array.from({ length: dotTotal.value }, (_, i) => i))
147
+
148
+ /** Points qui viennent de se remplir : le temps d'une animation « pop » */
149
+ const pulsingDots = ref<number[]>([])
150
+ let pulseTimer: ReturnType<typeof setTimeout> | undefined
151
+
152
+ watch(dotsFilled, (next, previous) => {
153
+ if (!props.showDots || next <= (previous ?? 0)) return
154
+
155
+ const started: number[] = []
156
+ for (let i = Math.max(0, previous ?? 0); i < next; i++) started.push(i)
157
+ pulsingDots.value = [...new Set([...pulsingDots.value, ...started])]
158
+
159
+ if (pulseTimer) clearTimeout(pulseTimer)
160
+ pulseTimer = setTimeout(() => {
161
+ pulsingDots.value = []
162
+ pulseTimer = undefined
163
+ }, 340)
164
+ })
165
+
166
+ onBeforeUnmount(() => {
167
+ if (pulseTimer) clearTimeout(pulseTimer)
168
+ })
169
+
170
+ /** Annonce lecteur d'écran (« 3 sur 6 ») : la rangée de points elle-même est décorative */
171
+ const dotsAnnouncement = computed(() =>
172
+ props.dots > 0 || props.maxLength > 0
173
+ ? `${dotsFilled.value} sur ${dotTotal.value}`
174
+ : `${dotsFilled.value}`,
175
+ )
176
+
177
+ // — État d'erreur (optionnel) —
178
+ /** Erreur levée localement : l'utilisateur a **tout effacé** depuis l'erreur (état initial) */
179
+ const errorReset = ref(false)
180
+
181
+ // Une **nouvelle** erreur (la prop repasse à vrai) réaffiche points rouges + message
182
+ watch(
183
+ () => props.error,
184
+ (on) => {
185
+ if (on) errorReset.value = false
186
+ },
187
+ )
188
+
189
+ /** Vrai tant que l'erreur doit être montrée (points rouges + message) */
190
+ const hasError = computed(() => props.error && !errorReset.value)
191
+
192
+ /**
193
+ * Remet l'erreur à zéro : masque les points rouges et le message, et prévient le parent
194
+ * (`v-model:error`). Appelé quand l'utilisateur vide complètement le pavé.
195
+ */
196
+ const clearError = () => {
197
+ errorReset.value = true
198
+ emit("update:error", false)
199
+ }
200
+
201
+ // — Taille des points —
202
+ /** `dotsSize` normalisé : nombre → px, chaîne → CSS tel quel (`undefined` = défaut CSS) */
203
+ const dotSize = computed<string | undefined>(() => {
204
+ const v = props.dotsSize
205
+ if (v === undefined || v === null || v === "") return undefined
206
+ return typeof v === "number" ? `${v}px` : String(v)
207
+ })
208
+
209
+ const rootStyle = computed<Record<string, string>>(() => ({
210
+ "--q-nk-columns": String(props.columns),
211
+ ...(dotSize.value ? { "--q-nk-dot-size": dotSize.value } : {}),
212
+ }))
213
+
214
+ const onKey = (cell: KeypadKey) => {
215
+ if (props.disable || cell.kind === "empty") return
216
+
217
+ const current = props.modelValue ?? ""
218
+ const next = pressKey(current, cell.key, options.value)
219
+
220
+ emit("press", cell.key)
221
+
222
+ if (next !== current) {
223
+ emit("update:modelValue", next)
224
+ if (props.maxLength > 0 && digitCount(next) >= props.maxLength) emit("complete", next)
225
+ }
226
+
227
+ // Tout effacé → retour à l'état initial : les points rouges et le message disparaissent.
228
+ // On se fie à la touche (⌫ / C) : un pavé **déjà vide** doit aussi pouvoir lever l'erreur.
229
+ if (props.error && next === "" && (cell.kind === "backspace" || cell.kind === "clear"))
230
+ clearError()
231
+ }
232
+
233
+ /** Insère une touche depuis l'extérieur (raccourci clavier matériel, saisie programmée) */
234
+ const insert = (key: string) => onKey({ key, kind: "digit", label: key })
235
+
236
+ const backspace = () => onKey({ key: "backspace", kind: "backspace", label: "⌫" })
237
+ const clear = () => onKey({ key: "clear", kind: "clear", label: "C" })
238
+
239
+ const rootClasses = computed(() =>
240
+ cn(
241
+ "q-numeric-keyboard",
242
+ props.dense && "q-numeric-keyboard--dense",
243
+ props.dark && "q-numeric-keyboard--dark",
244
+ hasError.value && "q-numeric-keyboard--error",
245
+ props.disable && "q-numeric-keyboard--disabled",
246
+ ),
247
+ )
248
+
249
+ defineExpose({
250
+ /** La valeur courante */
251
+ value: computed(() => props.modelValue ?? ""),
252
+ /** Applique une touche (`"7"`, `"separator"`, `"backspace"`, `"clear"`) */
253
+ press: insert,
254
+ backspace,
255
+ clear,
256
+ /** Retire une nouvelle disposition aléatoire (props `random`) */
257
+ shuffle,
258
+ })
259
+ </script>
260
+
261
+ <template>
262
+ <div
263
+ class="q-numeric-keyboard"
264
+ :class="rootClasses"
265
+ :style="rootStyle"
266
+ role="group"
267
+ :aria-label="label"
268
+ :aria-invalid="hasError ? 'true' : undefined"
269
+ >
270
+ <div v-if="showDots" class="q-numeric-keyboard__dots">
271
+ <span
272
+ v-for="index in dotIndexes"
273
+ :key="index"
274
+ class="q-numeric-keyboard__dot"
275
+ :class="{
276
+ 'q-numeric-keyboard__dot--filled': index < dotsFilled,
277
+ 'q-numeric-keyboard__dot--pop': pulsingDots.includes(index),
278
+ }"
279
+ aria-hidden="true"
280
+ />
281
+ <span class="q-numeric-keyboard__sr" role="status" aria-live="polite">{{ dotsAnnouncement }}</span>
282
+ </div>
283
+
284
+ <template v-for="(cell, index) in cells" :key="`${cell.kind}-${index}`">
285
+ <span v-if="cell.kind === 'empty'" class="q-numeric-keyboard__key q-numeric-keyboard__key--empty" />
286
+ <button
287
+ v-else
288
+ type="button"
289
+ class="q-numeric-keyboard__key"
290
+ :class="`q-numeric-keyboard__key--${cell.kind}`"
291
+ :disabled="disable"
292
+ :aria-label="cell.ariaLabel"
293
+ @click="onKey(cell)"
294
+ >
295
+ <Icon v-if="cell.kind === 'backspace'" :icon="icons.backspace" aria-hidden="true" />
296
+ <template v-else>{{ cell.label }}</template>
297
+ </button>
298
+ </template>
299
+
300
+ <p v-if="hasError && errorMessage" class="q-numeric-keyboard__error" role="alert">
301
+ {{ errorMessage }}
302
+ </p>
303
+ </div>
304
+ </template>
@@ -1,14 +1,23 @@
1
1
  <script setup lang="ts">
2
- // QPage — zone de contenu. Prop `virtual` : active le virtual scroll
3
- // (rendu fenêtré via QVirtualScroll) quand on a beaucoup d'items.
2
+ // QPage — zone de contenu. Prop `padding` : padding uniforme du contenu —
3
+ // présence (modifier) = 14px, valeur CSS (« 12px ») = cette valeur.
4
+ // Prop `virtual` : active le virtual scroll (rendu fenêtré via QVirtualScroll) quand
5
+ // on a beaucoup d'items.
4
6
  // Padding-top/padding-bottom automatiques quand des barres fixed
5
7
  // (q-header / q-back-header avant, q-footer après) entourent la page : le
6
8
  // contenu reste visible, jamais masqué par les barres au scroll.
7
- import { onBeforeUnmount, onMounted, ref } from "vue"
9
+ // La composition du padding (utilisateur + offsets des barres + safe-area) vit dans
10
+ // `styles/main.css` : ce composant ne pose que `--q-page-padding`, l'offset mesuré est
11
+ // publié en variables par `lib/fixedLayout.ts`.
12
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue"
13
+ import type { CSSProperties } from "vue"
8
14
  import { useFixedBarOffset } from "../lib/fixedLayout"
15
+ import { resolvePagePadding } from "../lib/pagePadding"
9
16
  import QVirtualScroll from "./QVirtualScroll.vue"
10
17
 
11
18
  interface Props {
19
+ /** Padding uniforme du contenu : modifier (sans valeur) = 14px, valeur CSS = cette valeur */
20
+ padding?: boolean | string | number
12
21
  /** Active le virtual scroll (rendu fenêtré des items) */
13
22
  virtual?: boolean
14
23
  /** Données à afficher (requis si virtual) */
@@ -29,7 +38,9 @@ interface Props {
29
38
  virtualScrollStickySizeEnd?: number
30
39
  }
31
40
 
32
- defineProps<Props>()
41
+ const props = withDefaults(defineProps<Props>(), {
42
+ padding: false,
43
+ })
33
44
 
34
45
  const rootEl = ref<HTMLElement | null>(null)
35
46
  // ref fonctionnel : sur un élément → l'élément ; sur q-virtual-scroll (composant) → .$el
@@ -38,8 +49,16 @@ const setRoot = (el: unknown) => {
38
49
  rootEl.value = node?.$el ?? node ?? null
39
50
  }
40
51
 
41
- // Offset automatique : padding-top = hauteur des barres fixed précédentes
52
+ // Offset automatique : padding (haut/bas) = hauteur des barres fixed autour de la page
42
53
  useFixedBarOffset(rootEl, "page")
54
+
55
+ // Padding utilisateur → variable CSS lue par `.q-page` (styles/main.css), qui le
56
+ // compose avec les offsets et la safe-area. Inline (et non posé à l'impératif) pour
57
+ // être rendu dès le SSR.
58
+ const rootStyle = computed<CSSProperties | undefined>(() => {
59
+ const padding = resolvePagePadding(props.padding)
60
+ return padding ? ({ "--q-page-padding": padding } as CSSProperties) : undefined
61
+ })
43
62
  </script>
44
63
 
45
64
  <template>
@@ -47,6 +66,7 @@ useFixedBarOffset(rootEl, "page")
47
66
  v-if="virtual"
48
67
  :ref="setRoot"
49
68
  class="q-page"
69
+ :style="rootStyle"
50
70
  :items="items"
51
71
  :item-key="itemKey"
52
72
  :virtual-scroll-slice-size="virtualScrollSliceSize"
@@ -67,7 +87,7 @@ useFixedBarOffset(rootEl, "page")
67
87
  <slot name="after" />
68
88
  </template>
69
89
  </q-virtual-scroll>
70
- <div v-else :ref="setRoot" class="q-page" v-bind="$attrs">
90
+ <div v-else :ref="setRoot" class="q-page" :style="rootStyle" v-bind="$attrs">
71
91
  <slot />
72
92
  </div>
73
93
  </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ // QPageContainer — zone de contenu du QLayout (modèle Quasar) : l'élément qui
3
+ // enveloppe la ou les pages, typiquement le <router-view />.
4
+ //
5
+ // <q-layout view="hHh LpR fFf">
6
+ // <q-header>…</q-header>
7
+ // <q-sidebar side="left" show-if-above>…</q-sidebar>
8
+ // <q-page-container>
9
+ // <q-page padding>…</q-page>
10
+ // </q-page-container>
11
+ // </q-layout>
12
+ //
13
+ // Il occupe la cellule « page » du QLayout (règle `.q-layout > *`) et sert de colonne
14
+ // flex : la page qu'il contient remplit la zone (cf. styles/main.css).
15
+ //
16
+ // Compensation des barres `fixed` : le conteneur ne l'applique que s'il **ne contient
17
+ // pas** de q-page — dans ce cas c'est la page qui s'en charge (règle `:has()` du CSS,
18
+ // donc jamais de double padding). Les hauteurs sont mesurées et publiées en variables
19
+ // par `lib/fixedLayout.ts`, exactement comme pour q-page.
20
+ import { ref } from "vue"
21
+ import { useFixedBarOffset } from "../lib/fixedLayout"
22
+
23
+ const rootEl = ref<HTMLElement | null>(null)
24
+
25
+ useFixedBarOffset(rootEl, "page")
26
+ </script>
27
+
28
+ <template>
29
+ <div ref="rootEl" class="q-page-container" v-bind="$attrs">
30
+ <slot />
31
+ </div>
32
+ </template>