@dnax/ui 0.0.48 → 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 (45) hide show
  1. package/components/QBottomSheet.vue +172 -6
  2. package/components/QImagePicker.vue +28 -1
  3. package/components/QInputChat.vue +859 -0
  4. package/components/QInputCurrency.vue +319 -0
  5. package/components/QItemLabel.vue +99 -0
  6. package/components/QMap.vue +410 -0
  7. package/components/QNumericKeyboard.vue +304 -0
  8. package/components/QPage.vue +26 -6
  9. package/components/QPageContainer.vue +32 -0
  10. package/components/QPopupProxy.vue +378 -0
  11. package/components/QQrcode.vue +132 -0
  12. package/components/QSpreadsheet.vue +395 -69
  13. package/components/QTime.vue +845 -0
  14. package/components/QTimeline.vue +111 -0
  15. package/components/QTimelineEntry.vue +271 -0
  16. package/index.ts +11 -1
  17. package/lib/bottomSheet.test.ts +121 -0
  18. package/lib/bottomSheet.ts +115 -0
  19. package/lib/color.ts +13 -0
  20. package/lib/currency.test.ts +193 -0
  21. package/lib/currency.ts +301 -0
  22. package/lib/fixedLayout.ts +12 -11
  23. package/lib/icons.ts +3 -20
  24. package/lib/map.test.ts +365 -0
  25. package/lib/map.ts +438 -0
  26. package/lib/mapEngine.ts +158 -0
  27. package/lib/mapLeaflet.ts +142 -0
  28. package/lib/mapMaplibre.ts +106 -0
  29. package/lib/mapMaptiler.ts +93 -0
  30. package/lib/numericKeyboard.test.ts +179 -0
  31. package/lib/numericKeyboard.ts +166 -0
  32. package/lib/pagePadding.test.ts +37 -0
  33. package/lib/pagePadding.ts +32 -0
  34. package/lib/qrcode.test.ts +103 -0
  35. package/lib/qrcode.ts +125 -0
  36. package/lib/spreadsheetChanges.test.ts +230 -0
  37. package/lib/spreadsheetChanges.ts +331 -0
  38. package/package.json +5 -17
  39. package/shims.d.ts +19 -0
  40. package/styles/main.css +689 -884
  41. package/components/QTiptap.vue +0 -996
  42. package/components/internal/QTiptapTableView.vue +0 -123
  43. package/components/internal/QTiptapTaskItemView.vue +0 -49
  44. package/lib/tiptap-table.ts +0 -19
  45. package/lib/tiptap-task-list.ts +0 -16
@@ -0,0 +1,410 @@
1
+ <script setup lang="ts">
2
+ // QMap — carte interactive, plusieurs fournisseurs (moteurs dans `lib/mapEngine.ts`).
3
+ //
4
+ // <q-map center="[2.35, 48.85]" :zoom="12" :marks="[{ type: 'marker', position: [2.35, 48.85], label: 'Paris' }]" />
5
+ // <q-map provider="leaflet" tiles="https://mon-serveur/{z}/{x}/{y}.png" />
6
+ //
7
+ // - **Client seulement** : le moteur (SDK JS MapTiler ou Leaflet) est importé
8
+ // dynamiquement au montage → ni WebGL ni DOM au prerender SSR.
9
+ // - **Fournisseurs** : `maptiler` (défaut, clé requise), `maplibre` (MapLibre GL JS seul,
10
+ // style libre sans clé), `openstreetmap` (raster sans clé, via le SDK MapTiler),
11
+ // `leaflet` (moteur Leaflet, raster sans clé, tuiles XYZ quelconques). Sans clé
12
+ // MapTiler, `maptiler` retombe sur le raster OpenStreetMap avec un avertissement — le
13
+ // cadre n'est jamais vide.
14
+ // - `marks` : marques en objets littéraux plats (`marker`, `popup`), comme les marks de
15
+ // `<q-chart>` — voir `lib/map.ts`.
16
+ // - Thème : la variante sombre du style suit la classe `.dark` du document (styles
17
+ // vectoriels) ; les bulles et les contrôles des deux moteurs sont peints avec les tokens
18
+ // (`styles/main.css`).
19
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
20
+ import type { CSSProperties } from "vue"
21
+ import { COLOR_TOKENS } from "../lib/chart"
22
+ import { normalizeCssColor, probeContext } from "../lib/color"
23
+ import {
24
+ DEFAULT_CENTER,
25
+ DEFAULT_ZOOM,
26
+ isKnownProvider,
27
+ leafletOptionsOf,
28
+ mapHeight,
29
+ maplibreOptionsOf,
30
+ mapOptionsOf,
31
+ marksOf,
32
+ parsePosition,
33
+ parseZoom,
34
+ providerOf,
35
+ type QMapMark,
36
+ type QMapPick,
37
+ type QMapPosition,
38
+ type QMapProvider,
39
+ } from "../lib/map"
40
+ import { createMapEngine, type MapEngine } from "../lib/mapEngine"
41
+ import { useComponentProps } from "../lib/useComponentProps"
42
+
43
+ interface Props {
44
+ /** Fournisseur : `maptiler` (défaut), `maplibre`, `openstreetmap` ou `leaflet` —
45
+ * `osm` et `openstreet` sont acceptés comme alias d'`openstreetmap` */
46
+ provider?: "maptiler" | "maplibre" | "openstreetmap" | "leaflet" | "osm"
47
+ /** Clé d'API MapTiler (repli : `componentProps.QMap.apiKey` du QConfigProvider).
48
+ * Sans effet avec `leaflet`. */
49
+ apiKey?: string
50
+ /** Style de carte : nom court (`streets`, `satellite`, `outdoor`, `dataviz`…), ID ou URL
51
+ * de style. Avec `provider="maplibre"` : `demotiles` (défaut), `openstreetmap`, une URL
52
+ * de style ou un objet de style. Sans effet avec `leaflet` (tuiles raster).
53
+ * Nommé `map-style` (et non `style`) pour ne pas entrer en collision avec l'attribut
54
+ * CSS `style` de Vue. */
55
+ mapStyle?: string
56
+ /** Gabarit de tuiles XYZ des fournisseurs raster (`openstreetmap`, `leaflet` — et
57
+ * `maplibre` avec `map-style="openstreetmap"`) — défaut : les tuiles OpenStreetMap */
58
+ tiles?: string
59
+ /** Attribution des tuiles (obligatoire pour OpenStreetMap) */
60
+ attribution?: string
61
+ /** Centre de la carte : `[lng, lat]`, `"lng,lat"` ou `{ lng, lat }` — Leaflet reçoit la
62
+ * même valeur (la conversion `[lat, lng]` est faite par le moteur) */
63
+ center?: QMapPosition
64
+ /** Zoom (0–24) */
65
+ zoom?: number | string
66
+ /** Hauteur du cadre : nombre → px, ou longueur CSS (`"40vh"`) */
67
+ height?: number | string
68
+ /** Marques : marqueurs (bulle optionnelle) et bulles seules */
69
+ marks?: QMapMark[]
70
+ /** Contrôles : `true` (position par défaut), `false`, ou un coin (`top-right`…).
71
+ * Ignorés par `leaflet` (contrôles natifs de Leaflet). */
72
+ navigation?: boolean | string
73
+ geolocate?: boolean | string
74
+ scale?: boolean | string
75
+ fullscreen?: boolean | string
76
+ /** Relief 3D — styles MapTiler uniquement (`maplibre` : passez une source DEM via
77
+ * `options.terrain`) */
78
+ terrain?: boolean
79
+ /** Projection : `mercator` (défaut) ou `globe` — MapTiler et MapLibre */
80
+ projection?: "mercator" | "globe"
81
+ /** Force la variante sombre du style (sinon suivi du mode clair/sombre du document) */
82
+ dark?: boolean
83
+ /** Libellé accessible du cadre */
84
+ label?: string
85
+ /** Options brutes du moteur — fusionnées en dernier. Pour Leaflet : options de `L.map()`,
86
+ * `options.tileLayer` allant à `L.tileLayer()`. */
87
+ options?: Record<string, any>
88
+ }
89
+
90
+ const props = withDefaults(defineProps<Props>(), {
91
+ provider: "maptiler",
92
+ apiKey: "",
93
+ mapStyle: "streets",
94
+ tiles: "",
95
+ attribution: "",
96
+ label: "Carte",
97
+ })
98
+
99
+ const emit = defineEmits<{
100
+ /** Carte prête — reçoit l'instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) */
101
+ ready: [map: any]
102
+ /** Une marque a été cliquée */
103
+ pick: [payload: QMapPick]
104
+ /** Erreur du moteur : échec de construction (surcouche « Carte indisponible ») ou
105
+ * erreur signalée ensuite (tuile, style, réseau) — seule la première est émise */
106
+ error: [error: unknown]
107
+ }>()
108
+
109
+ /** Valeurs globales du composant (`componentProps.QMap` du QConfigProvider) */
110
+ const defaults = useComponentProps("QMap")
111
+
112
+ const el = ref<HTMLElement | null>(null)
113
+ const engine = shallowRef<MapEngine | null>(null)
114
+ const loading = ref(true)
115
+ const failure = ref<string | null>(null)
116
+
117
+ // ─── Thème clair / sombre ──────────────────────────────────────────────────────
118
+
119
+ const documentDark = ref(false)
120
+ let themeObserver: MutationObserver | null = null
121
+
122
+ const isDocumentDark = () =>
123
+ typeof document !== "undefined" && document.documentElement.classList.contains("dark")
124
+
125
+ /** `dark` explicite, sinon le mode du document (classe `.dark` sur `<html>`) */
126
+ const isDark = computed(() => props.dark ?? documentDark.value)
127
+
128
+ // ─── Valeurs résolues ──────────────────────────────────────────────────────────
129
+
130
+ const center = computed(() => parsePosition(props.center) ?? ([...DEFAULT_CENTER] as [number, number]))
131
+ const zoom = computed(() => parseZoom(props.zoom) ?? DEFAULT_ZOOM)
132
+ const frameStyle = computed<CSSProperties>(() => ({ height: mapHeight(props.height) }))
133
+ const apiKey = computed(() => props.apiKey || String(defaults.value.apiKey ?? ""))
134
+
135
+ /** Fournisseur effectif : `maptiler` sans clé retombe sur le raster OpenStreetMap. */
136
+ const provider = computed<QMapProvider>(() => {
137
+ const requested = providerOf(props.provider)
138
+ return requested === "maptiler" && !apiKey.value ? "openstreetmap" : requested
139
+ })
140
+
141
+ /** Marques normalisées (positions résolues, marques invalides écartées) */
142
+ const markSpecs = computed(() => marksOf(props.marks))
143
+
144
+ /** Options passées au moteur, selon le fournisseur. Pour MapTiler, `style` reste la
145
+ * valeur brute de la prop : c'est le moteur qui résout les noms courts (`MapStyle`). */
146
+ const engineOptions = computed<Record<string, any>>(() => {
147
+ const shared = {
148
+ center: center.value,
149
+ zoom: zoom.value,
150
+ tiles: props.tiles,
151
+ attribution: props.attribution,
152
+ options: props.options,
153
+ }
154
+
155
+ if (provider.value === "leaflet") return leafletOptionsOf(shared)
156
+ if (provider.value === "maplibre") return maplibreOptionsOf({ ...shared, style: props.mapStyle })
157
+
158
+ return mapOptionsOf({
159
+ ...shared,
160
+ provider: provider.value,
161
+ apiKey: apiKey.value,
162
+ style: props.mapStyle,
163
+ navigation: props.navigation,
164
+ geolocate: props.geolocate,
165
+ scale: props.scale,
166
+ fullscreen: props.fullscreen,
167
+ })
168
+ })
169
+
170
+ /** Style à appliquer : la prop pour MapTiler (nom court, ID ou URL) et MapLibre (le
171
+ * moteur résout `demotiles` / `openstreetmap` / URL), le style raster construit pour
172
+ * `openstreetmap`, `undefined` pour Leaflet (pas de notion de style). */
173
+ const styleForProvider = computed<any>(() =>
174
+ provider.value === "leaflet"
175
+ ? undefined
176
+ : provider.value === "openstreetmap"
177
+ ? engineOptions.value.style
178
+ : props.mapStyle,
179
+ )
180
+
181
+ // ─── Couleurs des marques ──────────────────────────────────────────────────────
182
+
183
+ /** Token dnax (`primary`, `chart-2`…) lu sur le thème, ou couleur CSS — normalisée
184
+ * (`lib/color.ts`) car elle finit dans un attribut SVG/markup (cf. `.q-map` de main.css). */
185
+ const colorOf = (color?: string): string | undefined => {
186
+ if (typeof color !== "string" || color.trim() === "") return undefined
187
+ const value = color.trim()
188
+ const node = el.value
189
+ const isToken = !!node && (COLOR_TOKENS as readonly string[]).includes(value)
190
+ const raw = isToken ? getComputedStyle(node).getPropertyValue(`--${value}`).trim() : value
191
+ if (!raw) return undefined
192
+ return normalizeCssColor(raw, probeContext()) ?? raw
193
+ }
194
+
195
+ // ─── Cycle de vie ─────────────────────────────────────────────────────────────
196
+
197
+ let resizeObserver: ResizeObserver | null = null
198
+ /** Style (et mode sombre) appliqué par la dernière fois — évite un `setStyle` au montage */
199
+ let appliedStyle: unknown = null
200
+ let appliedDark: boolean | undefined
201
+ /** La carte a fini de charger (moteur prêt) */
202
+ let isReady = false
203
+ /** Minuteur de repli : ne jamais laisser la surcouche « Chargement… » indéfiniment */
204
+ let readyTimer: ReturnType<typeof setTimeout> | null = null
205
+ /** Erreurs signalées par le moteur (la 1re est journalisée et émise, les suivantes tues) */
206
+ let errorCount = 0
207
+
208
+ /** Délai au bout duquel on retire la surcouche même sans `ready` : les tuiles peuvent
209
+ * échouer (réseau, quota) sans que la carte soit inutilisable. */
210
+ const READY_TIMEOUT = 8000
211
+
212
+ const messageOf = (event: any): string => {
213
+ const error = event?.error ?? event
214
+ const message = error?.message ?? (typeof error === "string" ? error : "")
215
+ return message ? `Carte indisponible — ${message}` : "Carte indisponible"
216
+ }
217
+
218
+ const fail = (error: unknown) => {
219
+ console.warn("[q-map]", error)
220
+ loading.value = false
221
+ failure.value = messageOf(error)
222
+ emit("error", error)
223
+ }
224
+
225
+ /** Erreur signalée **par le moteur** une fois la carte lancée (tuile, style, réseau…) :
226
+ * la carte reste utilisable, on n'affiche donc pas d'erreur plein cadre — seulement le
227
+ * premier avertissement et l'événement `error`. Les échecs de construction, eux,
228
+ * passent par `fail()` (surcouche « Carte indisponible »). */
229
+ const noteError = (error: unknown) => {
230
+ if (errorCount++ > 0) return
231
+ console.warn("[q-map]", error)
232
+ emit("error", error)
233
+ }
234
+
235
+ /** Fournisseurs dont la carte est un raster : ils ignorent `map-style` (les tuiles
236
+ * passent par `tiles`), alors que `maptiler` et `maplibre` portent des styles. */
237
+ const RASTER_PROVIDERS: readonly QMapProvider[] = ["openstreetmap", "leaflet"]
238
+
239
+ /** Relief / projection : des méthodes du moteur, à appliquer une fois la carte chargée
240
+ * (au constructeur, MapTiler jette « Style is not done loading » sur `terrain`, et
241
+ * MapLibre attend un style chargé) puis à chaque changement. Chaque moteur ne fait que
242
+ * ce qu'il sait faire : `openstreetmap` et `leaflet` sont des no-op. */
243
+ const applyTerrainProjection = (target: MapEngine) => {
244
+ if (props.terrain) target.setTerrain(true)
245
+ if (props.projection) target.setProjection(props.projection)
246
+ }
247
+
248
+ onMounted(async () => {
249
+ documentDark.value = isDocumentDark()
250
+ if (typeof MutationObserver !== "undefined") {
251
+ themeObserver = new MutationObserver(() => {
252
+ documentDark.value = isDocumentDark()
253
+ })
254
+ themeObserver.observe(document.documentElement, {
255
+ attributes: true,
256
+ attributeFilter: ["class", "data-theme"],
257
+ })
258
+ }
259
+
260
+ const node = el.value
261
+ if (!node) return
262
+
263
+ if (props.provider === "maptiler" && !apiKey.value) {
264
+ console.warn(
265
+ "[q-map] aucune clé d'API MapTiler (prop `api-key` ou componentProps.QMap.apiKey) : " +
266
+ 'repli sur le raster OpenStreetMap (provider="openstreetmap")',
267
+ )
268
+ }
269
+ if (!isKnownProvider(props.provider)) {
270
+ console.warn(
271
+ `[q-map] fournisseur \`${props.provider}\` inconnu : repli sur \`maptiler\` ` +
272
+ '(valeurs : "maptiler", "openstreetmap", "leaflet" — alias "osm", "openstreet")',
273
+ )
274
+ }
275
+ if (RASTER_PROVIDERS.includes(provider.value) && props.mapStyle !== "streets") {
276
+ console.warn(
277
+ `[q-map] \`map-style\` est sans effet avec provider="${props.provider}" (tuiles raster) : ` +
278
+ "utilisez la prop `tiles` pour choisir la source",
279
+ )
280
+ }
281
+
282
+ try {
283
+ const next = await createMapEngine({
284
+ provider: provider.value,
285
+ el: node,
286
+ options: engineOptions.value,
287
+ center: center.value,
288
+ zoom: zoom.value,
289
+ marks: markSpecs.value,
290
+ dark: isDark.value,
291
+ controls: {
292
+ navigation: props.navigation,
293
+ geolocate: props.geolocate,
294
+ scale: props.scale,
295
+ fullscreen: props.fullscreen,
296
+ },
297
+ color: colorOf,
298
+ pick: (payload) => emit("pick", payload),
299
+ error: noteError,
300
+ })
301
+
302
+ if (!el.value) {
303
+ next.destroy() // démonté pendant le chargement du moteur
304
+ return
305
+ }
306
+
307
+ engine.value = next
308
+ appliedStyle = styleForProvider.value
309
+ appliedDark = isDark.value
310
+
311
+ next.onReady((ready) => {
312
+ isReady = true
313
+ loading.value = false
314
+ if (readyTimer) clearTimeout(readyTimer)
315
+ readyTimer = null
316
+ applyTerrainProjection(ready)
317
+ emit("ready", ready.native)
318
+ })
319
+
320
+ // Repli : une carte dont les tuiles n'arrivent pas ne doit pas rester sous la
321
+ // surcouche — l'utilisateur voit la carte (fond neutre) et la console explique.
322
+ readyTimer = setTimeout(() => {
323
+ readyTimer = null
324
+ if (!isReady) {
325
+ console.warn("[q-map] la carte n'a pas signalé son chargement — surcouche retirée")
326
+ loading.value = false
327
+ }
328
+ }, READY_TIMEOUT)
329
+
330
+ if (typeof ResizeObserver !== "undefined") {
331
+ resizeObserver = new ResizeObserver(() => engine.value?.resize())
332
+ resizeObserver.observe(node)
333
+ }
334
+ } catch (error) {
335
+ fail(error)
336
+ }
337
+ })
338
+
339
+ onBeforeUnmount(() => {
340
+ themeObserver?.disconnect()
341
+ themeObserver = null
342
+ resizeObserver?.disconnect()
343
+ resizeObserver = null
344
+ if (readyTimer) clearTimeout(readyTimer)
345
+ readyTimer = null
346
+ isReady = false
347
+ engine.value?.destroy()
348
+ engine.value = null
349
+ })
350
+
351
+ // ─── Réactivité ───────────────────────────────────────────────────────────────
352
+
353
+ // Style (et bascule clair/sombre : la variante `.DARK` est résolue par le moteur)
354
+ watch([styleForProvider, isDark], ([style, dark]) => {
355
+ if (!isReady || style === undefined) return
356
+ if (style === appliedStyle && dark === appliedDark) return
357
+ appliedStyle = style
358
+ appliedDark = dark
359
+ engine.value?.setStyle(style, dark)
360
+ })
361
+
362
+ // Tuiles raster (Leaflet) — `mapOptionsOf` gère déjà le cas d'`openstreetmap`.
363
+ watch([() => props.tiles, () => props.attribution], () => {
364
+ const target = engine.value
365
+ if (!isReady || provider.value !== "leaflet" || !target?.setRaster) return
366
+ const next = leafletOptionsOf({ tiles: props.tiles, attribution: props.attribution })
367
+ target.setRaster(next.tiles, next.attribution)
368
+ })
369
+
370
+ watch([center, zoom], ([nextCenter, nextZoom]) => {
371
+ if (isReady) engine.value?.setView(nextCenter, nextZoom)
372
+ })
373
+
374
+ watch(markSpecs, (specs) => engine.value?.setMarks(specs), { deep: true })
375
+
376
+ watch(
377
+ () => props.terrain,
378
+ (value) => {
379
+ const target = engine.value
380
+ if (!target || !isReady || typeof value !== "boolean") return
381
+ target.setTerrain(value)
382
+ },
383
+ )
384
+
385
+ watch(
386
+ () => props.projection,
387
+ (value) => {
388
+ const target = engine.value
389
+ if (!target || !isReady || !value) return
390
+ target.setProjection(value)
391
+ },
392
+ )
393
+
394
+ defineExpose({
395
+ /** Instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) — API complète du moteur */
396
+ map: computed(() => engine.value?.native ?? null),
397
+ /** Recalcule la taille du canvas (cadre qui vient d'apparaître) */
398
+ refresh: () => engine.value?.resize(),
399
+ })
400
+ </script>
401
+
402
+ <template>
403
+ <div class="q-map" :style="frameStyle" v-bind="$attrs">
404
+ <div ref="el" class="q-map__canvas" role="application" :aria-label="label" />
405
+
406
+ <div v-if="loading || failure" class="q-map__overlay">
407
+ <p class="q-map__note">{{ failure ?? "Chargement de la carte…" }}</p>
408
+ </div>
409
+ </div>
410
+ </template>