@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,319 @@
1
+ <script setup lang="ts">
2
+ // QInputCurrency — champ montant : la saisie est formatée à la volée (groupement des
3
+ // milliers, décimales et séparateur de la locale) et le `v-model` porte un **nombre**
4
+ // (`number | null`), jamais la chaîne affichée.
5
+ //
6
+ // <q-input-currency v-model="price" label="Prix" currency="EUR" outlined dense />
7
+ // <q-input-currency v-model="amount" currency="USD" locale="en-US" :min="0" />
8
+ //
9
+ // Le symbole de la devise est un **décor** : sa place et son espacement viennent d'`Intl`
10
+ // (« 1 234,56 € » en français, « $1,234.56 » en anglais), la valeur reste éditable. Les
11
+ // bornes (`min` / `max`) et l'arrondi de la devise s'appliquent à la sortie du champ ; les
12
+ // flèches ↑ / ↓ avancent du pas (`step`, ×10 avec Maj).
13
+ // Tout le calcul vit dans `lib/currency.ts` (pur, testé).
14
+ import { computed, ref, watch } from "vue"
15
+ import { Icon } from "@iconify/vue"
16
+ import {
17
+ applyLimits,
18
+ caretForDigitCount,
19
+ currencyFormat,
20
+ digitCountBefore,
21
+ draftFromValue,
22
+ draftValue,
23
+ formatAmount,
24
+ formatDraft,
25
+ localeOf,
26
+ parseAmount,
27
+ stepValue,
28
+ type AmountDraft,
29
+ } from "../lib/currency"
30
+ import { icons } from "../lib/icons"
31
+ import { cn } from "../lib/utils"
32
+ import { radiusStyle, useConfigLang, useRadius } from "../lib/useComponentProps"
33
+ import type { RadiusProp } from "../lib/useComponentProps"
34
+
35
+ interface Props {
36
+ /** Montant — `null` quand le champ est vide */
37
+ modelValue?: number | null
38
+ /** Code ISO 4217 (défaut `"EUR"`) — `""` pour un montant sans devise */
39
+ currency?: string
40
+ /** Locale BCP-47 (défaut : la langue de QConfigProvider — `fr` → `fr-FR`, `en` → `en-US`) */
41
+ locale?: string
42
+ /** Décimales (défaut : celles de la devise — 2 pour EUR, 0 pour JPY) */
43
+ decimals?: number
44
+ /** Affichage de la devise : symbole (défaut), code ISO ou nom localisé */
45
+ currencyDisplay?: "symbol" | "code" | "name"
46
+ /** Autorise un montant négatif */
47
+ allowNegative?: boolean
48
+ /** Borne basse, appliquée à la sortie du champ */
49
+ min?: number
50
+ /** Borne haute, appliquée à la sortie du champ */
51
+ max?: number
52
+ /** Pas des flèches ↑ / ↓ (×10 avec Maj) */
53
+ step?: number
54
+ /** Label affiché au-dessus du champ */
55
+ label?: string
56
+ /** Aide affichée sous le champ */
57
+ hint?: string
58
+ /** État d'erreur */
59
+ error?: boolean
60
+ /** Message d'erreur (affiché quand error) */
61
+ errorMessage?: string
62
+ /** Bouton d'effacement (remet `null`) */
63
+ clearable?: boolean
64
+ /** Bordure visible */
65
+ outlined?: boolean
66
+ /** Fond gris, soulignement */
67
+ filled?: boolean
68
+ /** Aucune bordure */
69
+ borderless?: boolean
70
+ /** Coins arrondis : true = pilule, ou échelle xs|sm|md|lg */
71
+ radius?: RadiusProp
72
+ /** Hauteur réduite */
73
+ dense?: boolean
74
+ disable?: boolean
75
+ readonly?: boolean
76
+ placeholder?: string
77
+ /** Icône Iconify à gauche (remplacée par le slot #prepend) */
78
+ iconLeft?: string
79
+ /** Icône Iconify à droite (remplacée par le slot #append) */
80
+ iconRight?: string
81
+ /** Nom du champ : un input caché porte la valeur brute pour l'envoi de formulaire */
82
+ name?: string
83
+ /** Id de l'input natif (pour un `<label for>`) */
84
+ id?: string
85
+ }
86
+
87
+ const props = withDefaults(defineProps<Props>(), {
88
+ modelValue: null,
89
+ currency: "EUR",
90
+ locale: "",
91
+ currencyDisplay: "symbol",
92
+ allowNegative: false,
93
+ step: 1,
94
+ error: false,
95
+ clearable: false,
96
+ outlined: false,
97
+ filled: false,
98
+ borderless: false,
99
+ dense: false,
100
+ disable: false,
101
+ readonly: false,
102
+ })
103
+
104
+ const emit = defineEmits<{
105
+ "update:modelValue": [value: number | null]
106
+ focus: [event: FocusEvent]
107
+ blur: [event: FocusEvent]
108
+ clear: []
109
+ }>()
110
+
111
+ const lang = useConfigLang()
112
+ const effectiveRadius = useRadius("QInputCurrency", () => props.radius)
113
+ const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
114
+
115
+ /** Réglages de formatage : locale, devise, décimales, bornes */
116
+ const options = computed(() => ({
117
+ locale: props.locale || localeOf(lang.value),
118
+ currency: props.currency,
119
+ decimals: props.decimals,
120
+ allowNegative: props.allowNegative,
121
+ currencyDisplay: props.currencyDisplay,
122
+ min: props.min,
123
+ max: props.max,
124
+ }))
125
+
126
+ const format = computed(() => currencyFormat(options.value))
127
+
128
+ const focused = ref(false)
129
+ const nativeEl = ref<HTMLInputElement | null>(null)
130
+ /** État de la frappe (indépendant de la locale) */
131
+ const draft = ref<AmountDraft>({ negative: false, integer: "", fraction: "", hasSeparator: false })
132
+
133
+ // Le modèle fait foi : au montage et à chaque changement venu de l'extérieur, on repart de
134
+ // la valeur — sauf quand l'utilisateur est en train de taper (on ne l'interrompt pas).
135
+ watch(
136
+ () => props.modelValue,
137
+ (value) => {
138
+ if (focused.value) return
139
+ if (draftValue(draft.value) === value) return
140
+ draft.value = draftFromValue(value, options.value)
141
+ },
142
+ { immediate: true },
143
+ )
144
+
145
+ // La devise ou la locale changent : l'affichage se recalcule et la valeur est ramenée aux
146
+ // nouvelles décimales (un montant en JPY n'a pas de centimes) — le modèle reste cohérent
147
+ // avec ce que le champ affiche.
148
+ watch(format, () => {
149
+ const limited = applyLimits(props.modelValue, options.value)
150
+ if (limited !== props.modelValue) emit("update:modelValue", limited)
151
+ if (focused.value) return
152
+ draft.value = draftFromValue(limited, options.value)
153
+ })
154
+
155
+ const displayValue = computed(() =>
156
+ focused.value ? formatDraft(draft.value, options.value) : formatAmount(props.modelValue, options.value),
157
+ )
158
+
159
+ const showClear = computed(
160
+ () =>
161
+ props.clearable &&
162
+ !props.disable &&
163
+ !props.readonly &&
164
+ (props.modelValue !== null || draft.value.integer !== "" || draft.value.fraction !== ""),
165
+ )
166
+
167
+ const fieldClasses = computed(() =>
168
+ cn(
169
+ "q-input",
170
+ props.outlined && "q-field--outlined",
171
+ props.filled && "q-field--filled",
172
+ props.borderless && "q-field--borderless",
173
+ effectiveRadius.value === true && "q-field--rounded",
174
+ props.dense && "q-field--dense",
175
+ props.error && "q-field--error",
176
+ props.disable && "q-field--disabled",
177
+ ),
178
+ )
179
+
180
+ const inputAttrs = computed(() => ({
181
+ type: "text",
182
+ inputmode: "decimal" as const,
183
+ autocomplete: "off",
184
+ id: props.id || undefined,
185
+ placeholder: props.placeholder,
186
+ disabled: props.disable || undefined,
187
+ readonly: props.readonly || undefined,
188
+ "aria-invalid": props.error || undefined,
189
+ }))
190
+
191
+ /** Écrit la valeur formatée dans l'input et replace le curseur après les mêmes chiffres */
192
+ const writeDisplay = (digitsBefore: number) => {
193
+ const el = nativeEl.value
194
+ if (!el) return
195
+ const display = formatDraft(draft.value, options.value)
196
+ el.value = display
197
+ const caret = caretForDigitCount(display, digitsBefore)
198
+ el.setSelectionRange(caret, caret)
199
+ }
200
+
201
+ const onInput = (event: Event) => {
202
+ const el = event.target as HTMLInputElement
203
+ const caret = el.selectionStart ?? el.value.length
204
+ const digitsBefore = digitCountBefore(el.value, caret)
205
+
206
+ draft.value = parseAmount(el.value, options.value)
207
+ const value = draftValue(draft.value)
208
+ if (value !== props.modelValue) emit("update:modelValue", value)
209
+
210
+ // La saisie est reformatée immédiatement (même sans `v-model` : le champ ne peut pas
211
+ // contenir de texte non formaté)
212
+ writeDisplay(digitsBefore)
213
+ }
214
+
215
+ const onFocus = (event: FocusEvent) => {
216
+ focused.value = true
217
+ draft.value = parseAmount(
218
+ props.modelValue === null
219
+ ? formatDraft(draft.value, options.value)
220
+ : formatAmount(props.modelValue, options.value),
221
+ options.value,
222
+ )
223
+ emit("focus", event)
224
+ }
225
+
226
+ const onBlur = (event: FocusEvent) => {
227
+ focused.value = false
228
+ const limited = applyLimits(draftValue(draft.value), options.value)
229
+ draft.value = draftFromValue(limited, options.value)
230
+ if (limited !== props.modelValue) emit("update:modelValue", limited)
231
+ emit("blur", event)
232
+ }
233
+
234
+ const onKeydown = (event: KeyboardEvent) => {
235
+ if (props.disable || props.readonly) return
236
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return
237
+
238
+ event.preventDefault()
239
+ const next = stepValue(
240
+ draftValue(draft.value),
241
+ props.step,
242
+ event.key === "ArrowUp" ? 1 : -1,
243
+ event.shiftKey,
244
+ options.value,
245
+ )
246
+ draft.value = draftFromValue(next, options.value)
247
+ // Curseur en fin de saisie après le pas (tous les chiffres du brouillon)
248
+ writeDisplay(draft.value.integer.length + draft.value.fraction.length)
249
+ if (next !== props.modelValue) emit("update:modelValue", next)
250
+ }
251
+
252
+ const onClear = () => {
253
+ draft.value = { negative: false, integer: "", fraction: "", hasSeparator: false }
254
+ emit("update:modelValue", null)
255
+ emit("clear")
256
+ nativeEl.value?.focus()
257
+ }
258
+
259
+ /** Décale le groupe et l'espace : « 1 234,56 € » ou « $1,234.56 » */
260
+ const symbolBefore = computed(() => format.value.symbol !== "" && format.value.position === "before")
261
+ const symbolAfter = computed(() => format.value.symbol !== "" && format.value.position === "after")
262
+ </script>
263
+
264
+ <template>
265
+ <div class="q-input q-input-currency" :class="fieldClasses" :style="roundedStyle">
266
+ <label v-if="label" class="q-field__label-stack" :for="id">{{ label }}</label>
267
+
268
+ <div class="q-field__control">
269
+ <slot name="prepend">
270
+ <Icon v-if="iconLeft" :icon="iconLeft" class="q-field__icon" aria-hidden="true" />
271
+ </slot>
272
+
273
+ <span v-if="symbolBefore" class="q-field__currency q-field__currency--before">{{
274
+ format.symbol
275
+ }}</span>
276
+
277
+ <input
278
+ ref="nativeEl"
279
+ class="q-field__native"
280
+ :value="displayValue"
281
+ v-bind="inputAttrs"
282
+ @input="onInput"
283
+ @focus="onFocus"
284
+ @blur="onBlur"
285
+ @keydown="onKeydown"
286
+ />
287
+
288
+ <span v-if="symbolAfter" class="q-field__currency q-field__currency--after">{{
289
+ format.symbol
290
+ }}</span>
291
+
292
+ <button
293
+ v-if="showClear"
294
+ class="q-field__clear"
295
+ type="button"
296
+ aria-label="Effacer"
297
+ @click="onClear"
298
+ >
299
+ <Icon :icon="icons.x" aria-hidden="true" />
300
+ </button>
301
+
302
+ <slot name="append">
303
+ <Icon v-if="iconRight" :icon="iconRight" class="q-field__icon" aria-hidden="true" />
304
+ </slot>
305
+ </div>
306
+
307
+ <div class="q-field__bottom">
308
+ <div v-if="error" class="q-field__error">
309
+ <slot name="error">{{ errorMessage }}</slot>
310
+ </div>
311
+ <div v-else-if="hint || $slots.hint" class="q-field__hint">
312
+ <slot name="hint">{{ hint }}</slot>
313
+ </div>
314
+ </div>
315
+
316
+ <!-- Valeur brute : un formulaire classique envoie un nombre, pas un montant formaté -->
317
+ <input v-if="name" type="hidden" :name="name" :value="modelValue === null ? '' : String(modelValue)" />
318
+ </div>
319
+ </template>
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ // QItemLabel — API Quasar : <q-item-label overline caption header :lines="2" color="primary">
3
+ // Libellé de texte d'un QItem : variantes overline / caption / header,
4
+ // troncature multi-lignes (lines) et couleur (token ou hex).
5
+ import { cn } from "../lib/utils"
6
+ import { colorValue } from "../lib/colors"
7
+
8
+ interface Props {
9
+ /** Petites majuscules, lettrage espacé (étiquette au-dessus) */
10
+ overline?: boolean
11
+ /** Texte secondaire (couleur atténuée) */
12
+ caption?: boolean
13
+ /** Titre d'en-tête */
14
+ header?: boolean
15
+ /** Tronque après N lignes (`-webkit-line-clamp`) — 0 = pas de troncature */
16
+ lines?: number
17
+ /** Couleur du texte (token ou hex) */
18
+ color?: string
19
+ /** Élément rendu (div par défaut) */
20
+ tag?: string
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ overline: false,
25
+ caption: false,
26
+ header: false,
27
+ lines: 0,
28
+ color: "",
29
+ tag: "div",
30
+ })
31
+
32
+ const labelClasses = cn(
33
+ "q-item__label",
34
+ props.caption && "q-item__label--caption",
35
+ props.overline && "q-item__label--overline",
36
+ props.header && "q-item__label--header",
37
+ props.lines > 0 && "q-item__label--clamp",
38
+ )
39
+
40
+ const labelStyle: Record<string, string> = {
41
+ ...(props.color ? { color: colorValue(props.color) } : {}),
42
+ ...(props.lines > 0 ? { "--q-item-label-lines": String(props.lines) } : {}),
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <component
48
+ :is="tag"
49
+ class="q-item__label"
50
+ :class="labelClasses"
51
+ :style="labelStyle"
52
+ v-bind="$attrs"
53
+ >
54
+ <slot />
55
+ </component>
56
+ </template>
57
+
58
+ <style scoped>
59
+ .q-item__label {
60
+ font-size: 14px;
61
+ line-height: 1.35;
62
+ color: var(--foreground);
63
+ }
64
+
65
+ /* ─── caption : texte secondaire atténué ─── */
66
+ .q-item__label--caption {
67
+ font-size: 12.5px;
68
+ color: var(--muted-foreground);
69
+ }
70
+
71
+ /* ─── overline : petites majuscules, lettrage espacé ─── */
72
+ .q-item__label--overline {
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.1em;
75
+ font-size: 10px;
76
+ font-weight: 600;
77
+ line-height: 1.2;
78
+ color: var(--muted-foreground);
79
+ }
80
+
81
+ /* ─── header : titre de section ─── */
82
+ .q-item__label--header {
83
+ font-size: 13px;
84
+ font-weight: 600;
85
+ letter-spacing: 0.01em;
86
+ color: var(--muted-foreground);
87
+ }
88
+
89
+ /* ─── troncature multi-lignes avec … (line-clamp) ─── */
90
+ .q-item__label--clamp {
91
+ display: -webkit-box;
92
+ -webkit-box-orient: vertical;
93
+ -webkit-line-clamp: var(--q-item-label-lines, 1);
94
+ line-clamp: var(--q-item-label-lines, 1);
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ word-break: break-word;
98
+ }
99
+ </style>