@dnax/ui 0.0.48 → 0.0.50

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 (48) hide show
  1. package/components/QBottomSheet.vue +172 -6
  2. package/components/QChart.vue +1 -0
  3. package/components/QDatePicker.vue +1 -1
  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/QSelect.vue +12 -0
  15. package/components/QSpreadsheet.vue +395 -69
  16. package/components/QTime.vue +845 -0
  17. package/components/QTimeline.vue +111 -0
  18. package/components/QTimelineEntry.vue +271 -0
  19. package/index.ts +11 -1
  20. package/lib/bottomSheet.test.ts +121 -0
  21. package/lib/bottomSheet.ts +115 -0
  22. package/lib/color.ts +13 -0
  23. package/lib/currency.test.ts +193 -0
  24. package/lib/currency.ts +301 -0
  25. package/lib/fixedLayout.ts +12 -11
  26. package/lib/icons.ts +3 -20
  27. package/lib/map.test.ts +365 -0
  28. package/lib/map.ts +438 -0
  29. package/lib/mapEngine.ts +158 -0
  30. package/lib/mapLeaflet.ts +142 -0
  31. package/lib/mapMaplibre.ts +106 -0
  32. package/lib/mapMaptiler.ts +93 -0
  33. package/lib/numericKeyboard.test.ts +179 -0
  34. package/lib/numericKeyboard.ts +166 -0
  35. package/lib/pagePadding.test.ts +37 -0
  36. package/lib/pagePadding.ts +32 -0
  37. package/lib/qrcode.test.ts +103 -0
  38. package/lib/qrcode.ts +125 -0
  39. package/lib/spreadsheetChanges.test.ts +230 -0
  40. package/lib/spreadsheetChanges.ts +331 -0
  41. package/package.json +5 -17
  42. package/shims.d.ts +19 -0
  43. package/styles/main.css +689 -884
  44. package/components/QTiptap.vue +0 -996
  45. package/components/internal/QTiptapTableView.vue +0 -123
  46. package/components/internal/QTiptapTaskItemView.vue +0 -49
  47. package/lib/tiptap-table.ts +0 -19
  48. package/lib/tiptap-task-list.ts +0 -16
package/lib/map.ts ADDED
@@ -0,0 +1,438 @@
1
+ // map.ts — `<q-map>` : props → options du SDK JS MapTiler, et conventions des marques.
2
+ //
3
+ // Pur : aucun DOM, aucun import de `@maptiler/sdk` (le composant seul fait le pont, par
4
+ // import dynamique côté client) → tout est testable hors navigateur, comme
5
+ // `lib/chart.ts`. Le SDK est basé sur MapLibre GL JS.
6
+ //
7
+ // <q-map
8
+ // center="[2.35, 48.85]"
9
+ // :zoom="12"
10
+ // :marks="[{ type: 'marker', position: [2.35, 48.85], label: 'Paris' }]"
11
+ // />
12
+ //
13
+ // Référence : https://docs.maptiler.com/sdk-js/ (SDK JS v4)
14
+
15
+ /** Fournisseurs de tuiles supportés.
16
+ * - `maptiler` : SDK JS MapTiler (styles vectoriels, terrain, globe) + clé d'API ;
17
+ * - `maplibre` : **MapLibre GL JS** seul (VectorGL), sans clé — style libre ;
18
+ * - `openstreetmap` : raster OpenStreetMap via le SDK MapTiler, sans clé ;
19
+ * - `leaflet` : moteur Leaflet, raster sans clé (tuiles XYZ quelconques). */
20
+ export type QMapProvider = "maptiler" | "maplibre" | "openstreetmap" | "leaflet"
21
+
22
+ /** Valeurs acceptées par la prop `provider` : les noms canoniques, plus les alias
23
+ * (`osm`, `openstreet`, `map-libre`…) résolus par `providerOf`. */
24
+ export type QMapProviderInput = QMapProvider | "osm" | "openstreet" | "map-libre"
25
+
26
+ export const MAP_PROVIDERS: readonly QMapProvider[] = [
27
+ "maptiler",
28
+ "maplibre",
29
+ "openstreetmap",
30
+ "leaflet",
31
+ ]
32
+
33
+ /** Alias tolérés → fournisseur canonique. */
34
+ export const PROVIDER_ALIASES: Record<string, QMapProvider> = {
35
+ osm: "openstreetmap",
36
+ openstreet: "openstreetmap",
37
+ "open-street-map": "openstreetmap",
38
+ "map-tiler": "maptiler",
39
+ "map-libre": "maplibre",
40
+ "maplibre-gl": "maplibre",
41
+ "leaflet.js": "leaflet",
42
+ "leaflet-js": "leaflet",
43
+ }
44
+
45
+ /** Fournisseur canonique d'une valeur de prop — valeur absente ou inconnue →
46
+ * `maptiler` (le fournisseur par défaut, celui du SDK). */
47
+ export function providerOf(provider?: unknown): QMapProvider {
48
+ if (typeof provider !== "string") return "maptiler"
49
+ const key = provider.trim().toLowerCase()
50
+ if (key === "maptiler" || key === "maplibre" || key === "openstreetmap" || key === "leaflet")
51
+ return key
52
+ return PROVIDER_ALIASES[key] ?? "maptiler"
53
+ }
54
+
55
+ /** La valeur est-elle un fournisseur connu (canonique ou alias) ? `undefined` = défaut,
56
+ * donc connu : sert au composant pour avertir sur une faute de frappe. */
57
+ export function isKnownProvider(provider?: unknown): boolean {
58
+ if (provider === undefined || provider === null || provider === "") return true
59
+ if (typeof provider !== "string") return false
60
+ const key = provider.trim().toLowerCase()
61
+ return (
62
+ key === "maptiler" ||
63
+ key === "maplibre" ||
64
+ key === "openstreetmap" ||
65
+ key === "leaflet" ||
66
+ key in PROVIDER_ALIASES
67
+ )
68
+ }
69
+
70
+ /** Centre par défaut : vue monde. Jamais de géolocalisation IP implicite — le SDK
71
+ * centre la carte sur le visiteur (par IP) quand aucun `center` n'est fourni. */
72
+ export const DEFAULT_CENTER: readonly [number, number] = [0, 20]
73
+
74
+ export const DEFAULT_ZOOM = 1.5
75
+
76
+ /** Hauteur du cadre quand `height` n'est pas fourni (px) */
77
+ export const DEFAULT_MAP_HEIGHT = 320
78
+
79
+ /** Styles nommés acceptés par la prop `style` → clés de `MapStyle` du SDK (« reference
80
+ * styles » du README MapTiler). Toute autre valeur est passée telle quelle au SDK :
81
+ * ID de style (`"outdoor-v2"`) ou URL (`"https://api.maptiler.com/maps/<id>/style.json"`). */
82
+ export const MAP_STYLE_KEYS = [
83
+ "streets",
84
+ "satellite",
85
+ "hybrid",
86
+ "outdoor",
87
+ "winter",
88
+ "dataviz",
89
+ "basic",
90
+ "bright",
91
+ "topo",
92
+ "voyager",
93
+ "toner",
94
+ "ocean",
95
+ "landscape",
96
+ "aquarelle",
97
+ "backdrop",
98
+ "stage",
99
+ "openstreetmap",
100
+ ] as const
101
+
102
+ export type QMapStyleKey = (typeof MAP_STYLE_KEYS)[number]
103
+
104
+ /** Clé de `MapStyle` d'un nom court, ou `undefined` si c'est un ID/URL de style. */
105
+ export function mapStyleKey(style: unknown): QMapStyleKey | undefined {
106
+ if (typeof style !== "string") return undefined
107
+ const key = style.trim().toLowerCase()
108
+ return (MAP_STYLE_KEYS as readonly string[]).includes(key) ? (key as QMapStyleKey) : undefined
109
+ }
110
+
111
+ /** Gabarit de tuiles raster par défaut (OpenStreetMap) — utilisé par
112
+ * `provider="openstreetmap"`, `provider="leaflet"` et le repli sans clé MapTiler. */
113
+ export const OSM_TILES = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
114
+
115
+ /** Attribution par défaut des tuiles OpenStreetMap (obligatoire à l'affichage). */
116
+ export const OSM_ATTRIBUTION = "© OpenStreetMap contributors"
117
+
118
+ /** Style raster (MapLibre / SDK MapTiler) d'une source de tuiles XYZ. */
119
+ export function osmStyle(tiles: string = OSM_TILES, attribution: string = OSM_ATTRIBUTION) {
120
+ return {
121
+ version: 8,
122
+ sources: {
123
+ osm: { type: "raster", tiles: [tiles], tileSize: 256, maxzoom: 19, attribution },
124
+ },
125
+ layers: [{ id: "osm", type: "raster", source: "osm" }],
126
+ }
127
+ }
128
+
129
+ /** Style raster OpenStreetMap par défaut (`provider="openstreetmap"` / repli sans clé).
130
+ * Aucune clé d'API : c'est ce qui permet à la carte de rester visible en démo. */
131
+ export const OSM_STYLE = osmStyle()
132
+
133
+ /** Coordonnées en convention **Leaflet** : `[lat, lng]` (l'inverse de dnax.ui). */
134
+ export const toLeaflet = (position: readonly [number, number]): [number, number] => [
135
+ position[1],
136
+ position[0],
137
+ ]
138
+
139
+ /** Couleur acceptable dans un attribut/motif SVG/HTML (anti-injection dans un `divIcon`).
140
+ * Renvoie `undefined` si la valeur sort du jeu de caractères d'une couleur CSS. */
141
+ export function safeCssColor(value?: unknown): string | undefined {
142
+ if (typeof value !== "string") return undefined
143
+ const color = value.trim()
144
+ return /^[#a-zA-Z0-9(),.%\s\/+-]{1,64}$/.test(color) ? color : undefined
145
+ }
146
+
147
+ /** Longitude/latitude — `[lng, lat]`, `"lng,lat"` (ou `"[lng, lat]"`) ou `{ lng, lat }` */
148
+ export type QMapPosition =
149
+ | [number, number]
150
+ | string
151
+ | { lng?: number | string; lat?: number | string; lon?: number | string }
152
+
153
+ const toNumber = (value: unknown): number | undefined => {
154
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined
155
+ if (typeof value !== "string" || value.trim() === "") return undefined
156
+ const n = Number(value)
157
+ return Number.isFinite(n) ? n : undefined
158
+ }
159
+
160
+ /** Coordonnées valides (lng ∈ [-180, 180], lat ∈ [-90, 90]) — `[lng, lat]`. */
161
+ const coordinates = (lng: unknown, lat: unknown): [number, number] | undefined => {
162
+ const x = toNumber(lng)
163
+ const y = toNumber(lat)
164
+ if (x === undefined || y === undefined) return undefined
165
+ if (x < -180 || x > 180 || y < -90 || y > 90) return undefined
166
+ return [x, y]
167
+ }
168
+
169
+ /** Position `[lng, lat]`, ou `undefined` si la valeur est absente/illisible/hors bornes. */
170
+ export function parsePosition(value?: unknown): [number, number] | undefined {
171
+ if (Array.isArray(value)) return coordinates(value[0], value[1])
172
+
173
+ if (typeof value === "string") {
174
+ // "[2.35, 48.85]", "2.35, 48.85" ou "2.35 48.85"
175
+ const parts = value.replace(/[[\]()]/g, "").split(/[,;\s]+/).filter(Boolean)
176
+ return parts.length >= 2 ? coordinates(parts[0], parts[1]) : undefined
177
+ }
178
+
179
+ if (value && typeof value === "object") {
180
+ const o = value as Record<string, unknown>
181
+ return coordinates(o.lng ?? o.lon, o.lat)
182
+ }
183
+
184
+ return undefined
185
+ }
186
+
187
+ /** Zoom borné à [0, 24]. */
188
+ export function parseZoom(value: unknown): number | undefined {
189
+ const n = toNumber(value)
190
+ return n === undefined ? undefined : Math.min(24, Math.max(0, n))
191
+ }
192
+
193
+ /** Hauteur CSS du cadre : nombre → px, chaîne CSS telle quelle, sinon défaut. */
194
+ export function mapHeight(value: unknown): string {
195
+ const n = toNumber(value)
196
+ if (n !== undefined && n > 0) return `${n}px`
197
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : `${DEFAULT_MAP_HEIGHT}px`
198
+ }
199
+
200
+ // ─── Marques ────────────────────────────────────────────────────────────────────
201
+
202
+ export type QMapMarkType = "marker" | "popup"
203
+
204
+ /** Une marque est un **objet littéral plat** (comme les marks de `<q-chart>`) :
205
+ * `marker` = épingle (+ bulle optionnelle), `popup` = bulle seule. */
206
+ export interface QMapMark {
207
+ /** Type de marque */
208
+ type: QMapMarkType
209
+ /** Position `[lng, lat]` (ou `"lng,lat"`, ou `{ lng, lat }`) */
210
+ position?: QMapPosition
211
+ /** Longitude — alternative à `position` */
212
+ lng?: number | string
213
+ /** Latitude — alternative à `position` */
214
+ lat?: number | string
215
+ /** Texte de la bulle (échappé) — ou libellé du marqueur */
216
+ label?: string
217
+ /** Contenu HTML de la bulle (prioritaire sur `label`, inséré tel quel) */
218
+ html?: string
219
+ /** Couleur du marqueur : token dnax (`primary`, `chart-2`…) ou couleur CSS */
220
+ color?: string
221
+ /** Bulle ouverte au chargement (les `popup` sont ouvertes par défaut) */
222
+ open?: boolean
223
+ /** Décalage de la bulle en px (défaut 18) */
224
+ offset?: number
225
+ /** Épingle déplaçable à la souris */
226
+ draggable?: boolean
227
+ }
228
+
229
+ /** Charge utile de `@pick` : la marque cliquée et sa position. */
230
+ export interface QMapPick {
231
+ mark: QMapMark
232
+ index: number
233
+ position: [number, number]
234
+ lng: number
235
+ lat: number
236
+ }
237
+
238
+ /** Position d'une marque (`position`, sinon `lng`/`lat`). */
239
+ export function markPosition(mark: QMapMark): [number, number] | undefined {
240
+ return parsePosition(mark.position) ?? coordinates(mark.lng, mark.lat)
241
+ }
242
+
243
+ const ESCAPES: Record<string, string> = {
244
+ "&": "&amp;",
245
+ "<": "&lt;",
246
+ ">": "&gt;",
247
+ '"': "&quot;",
248
+ "'": "&#39;",
249
+ }
250
+
251
+ /** Échappe un texte destiné à `innerHTML` (`label` d'une marque). */
252
+ export const escapeHtml = (text: string): string =>
253
+ text.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char)
254
+
255
+ /** Contenu de la bulle : `html` s'il est fourni, sinon `label` échappé, sinon rien. */
256
+ export function popupHtml(mark: QMapMark): string | undefined {
257
+ if (typeof mark.html === "string" && mark.html.trim() !== "") return mark.html
258
+ if (typeof mark.label === "string" && mark.label.trim() !== "") return escapeHtml(mark.label)
259
+ return undefined
260
+ }
261
+
262
+ export interface QMapMarkSpec {
263
+ mark: QMapMark
264
+ index: number
265
+ position: [number, number]
266
+ html?: string
267
+ }
268
+
269
+ /** Marques exploitables, dans l'ordre — une marque sans position valide est ignorée. */
270
+ export function marksOf(marks?: readonly QMapMark[] | null): QMapMarkSpec[] {
271
+ const out: QMapMarkSpec[] = []
272
+ if (!Array.isArray(marks)) return out
273
+
274
+ marks.forEach((mark, index) => {
275
+ if (!mark || typeof mark !== "object") return
276
+ const position = markPosition(mark)
277
+ if (!position) return
278
+ out.push({ mark, index, position, html: popupHtml(mark) })
279
+ })
280
+
281
+ return out
282
+ }
283
+
284
+ // ─── Options du constructeur `Map` ──────────────────────────────────────────────
285
+
286
+ export interface QMapOptionsInput {
287
+ provider?: QMapProviderInput
288
+ apiKey?: string
289
+ /** Style **déjà résolu** (MapStyle du SDK, variante, ID, URL ou objet de style) */
290
+ style?: unknown
291
+ /** Gabarit de tuiles XYZ pour les fournisseurs raster (`openstreetmap`, `leaflet`) */
292
+ tiles?: string
293
+ /** Attribution des tuiles (obligatoire pour OpenStreetMap) */
294
+ attribution?: string
295
+ center?: unknown
296
+ zoom?: unknown
297
+ /** Contrôles du SDK : `true` (position par défaut), `false`, ou un coin
298
+ * (`top-left`, `top-right`, `bottom-left`, `bottom-right`) */
299
+ navigation?: boolean | string
300
+ geolocate?: boolean | string
301
+ scale?: boolean | string
302
+ fullscreen?: boolean | string
303
+ /** Options brutes du SDK — fusionnées en **dernier** */
304
+ options?: Record<string, any>
305
+ }
306
+
307
+ /**
308
+ * Options passées à `new Map(...)` du SDK. `provider="openstreetmap"` impose le style
309
+ * raster OpenStreetMap (aucune clé) ; tout le reste est délégué au SDK, et `options` est
310
+ * fusionné en dernier (règle d'or : toute option brute passe par `options`).
311
+ *
312
+ * `terrain` et `projection` **ne sont pas** ici : le SDK les applique au constructeur
313
+ * avant que le style soit chargé et jette alors « Style is not done loading » (vérifié
314
+ * au navigateur) — `<q-map>` les applique après le `load`, via `enableTerrain()` et
315
+ * `setProjection()`.
316
+ */
317
+ export function mapOptionsOf(input: QMapOptionsInput = {}): Record<string, any> {
318
+ const provider: QMapProvider = providerOf(input.provider)
319
+
320
+ const out: Record<string, any> = {
321
+ center: parsePosition(input.center) ?? DEFAULT_CENTER,
322
+ zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
323
+ style:
324
+ provider === "openstreetmap"
325
+ ? osmStyle(input.tiles ?? OSM_TILES, input.attribution ?? OSM_ATTRIBUTION)
326
+ : input.style,
327
+ }
328
+
329
+ if (provider === "maptiler" && input.apiKey) out.apiKey = input.apiKey
330
+ if (input.navigation !== undefined) out.navigationControl = input.navigation
331
+ if (input.geolocate !== undefined) out.geolocateControl = input.geolocate
332
+ if (input.scale !== undefined) out.scaleControl = input.scale
333
+ if (input.fullscreen !== undefined) out.fullscreenControl = input.fullscreen
334
+
335
+ return { ...out, ...input.options }
336
+ }
337
+
338
+ // ─── Options du moteur Leaflet ──────────────────────────────────────────────────
339
+
340
+ export interface QMapLeafletOptions {
341
+ /** Centre en convention Leaflet : `[lat, lng]` */
342
+ center: [number, number]
343
+ zoom: number
344
+ /** Gabarit de tuiles XYZ (`{z}`/`{x}`/`{y}`) */
345
+ tiles: string
346
+ /** Attribution des tuiles */
347
+ attribution: string
348
+ /** Options passées à `L.map()` (les `options` utilisateur, `tileLayer` exclu) */
349
+ mapOptions: Record<string, any>
350
+ /** Options passées à `L.tileLayer()` (`options.tileLayer`) */
351
+ layerOptions: Record<string, any>
352
+ }
353
+
354
+ /**
355
+ * Options du moteur **Leaflet** : la couche de tuiles et les options de `L.map()`.
356
+ *
357
+ * Convention de coordonnées : **`[lat, lng]`** — Leaflet l'exige, alors que dnax.ui
358
+ * expose `[lng, lat]` partout ailleurs (la conversion est faite ici, une fois).
359
+ * `options` est passé à `L.map()`, sa clé `tileLayer` à `L.tileLayer()`.
360
+ */
361
+ export function leafletOptionsOf(input: QMapOptionsInput = {}): QMapLeafletOptions {
362
+ const { tileLayer, ...mapOptions } = (input.options ?? {}) as Record<string, any>
363
+
364
+ return {
365
+ center: toLeaflet(parsePosition(input.center) ?? DEFAULT_CENTER),
366
+ zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
367
+ tiles: typeof input.tiles === "string" && input.tiles.trim() ? input.tiles.trim() : OSM_TILES,
368
+ attribution:
369
+ typeof input.attribution === "string" && input.attribution.trim()
370
+ ? input.attribution.trim()
371
+ : OSM_ATTRIBUTION,
372
+ mapOptions,
373
+ layerOptions: (tileLayer as Record<string, any>) ?? {},
374
+ }
375
+ }
376
+
377
+ // ─── MapLibre GL JS (styles libres, sans clé) ───────────────────────────────────
378
+
379
+ /** Style de démonstration MapLibre (monde entier, aucune clé) — défaut du fournisseur. */
380
+ export const MAPLIBRE_DEMO_STYLE = "https://demotiles.maplibre.org/style.json"
381
+
382
+ /**
383
+ * Style d'un `<q-map provider="maplibre">` : `demotiles` (défaut), `openstreetmap` (le
384
+ * raster de `osmStyle()`, `tiles`/`attribution` compris), une URL de style, un objet de
385
+ * style complet — ou `streets`, la valeur par défaut du design system, qui n'a pas de
386
+ * sens sans MapTiler et retombe donc sur le style de démo.
387
+ *
388
+ * `known` vaut `false` pour un nom non reconnu : le moteur le signale en console
389
+ * (sinon un `map-style="topo"` afficherait le style de démo en silence).
390
+ */
391
+ export function maplibreStyle(input: QMapOptionsInput = {}): { style: any; known: boolean } {
392
+ const value = input.style
393
+ if (value && typeof value === "object") return { style: value, known: true }
394
+
395
+ const raw = typeof value === "string" ? value.trim() : ""
396
+ const key = raw.toLowerCase()
397
+
398
+ if (key === "" || key === "streets" || key === "demotiles" || key === "demo")
399
+ return { style: MAPLIBRE_DEMO_STYLE, known: true }
400
+
401
+ if (key === "openstreetmap" || key === "osm")
402
+ return {
403
+ style: osmStyle(input.tiles ?? OSM_TILES, input.attribution ?? OSM_ATTRIBUTION),
404
+ known: true,
405
+ }
406
+
407
+ // URL absolue ou relative (le reste est un nom court inconnu)
408
+ if (/^(https?:)?\/\//.test(raw) || raw.startsWith("/") || raw.startsWith("."))
409
+ return { style: raw, known: true }
410
+
411
+ return { style: MAPLIBRE_DEMO_STYLE, known: false }
412
+ }
413
+
414
+ export interface QMapMaplibreOptions {
415
+ /** Options passées à `new Map()` de MapLibre GL JS */
416
+ map: Record<string, any>
417
+ /** Le `map-style` demandé a-t-il été reconnu ? (sinon repli sur le style de démo) */
418
+ styleKnown: boolean
419
+ }
420
+
421
+ /**
422
+ * Options du moteur **MapLibre GL JS** : `center` / `zoom` et le style résolu
423
+ * (`maplibreStyle`). MapLibre n'a ni clé d'API, ni catalogue de styles : c'est le style
424
+ * qui porte les sources de données.
425
+ */
426
+ export function maplibreOptionsOf(input: QMapOptionsInput = {}): QMapMaplibreOptions {
427
+ const { style, known } = maplibreStyle(input)
428
+
429
+ return {
430
+ map: {
431
+ center: parsePosition(input.center) ?? DEFAULT_CENTER,
432
+ zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
433
+ style,
434
+ ...(input.options ?? {}),
435
+ },
436
+ styleKnown: known,
437
+ }
438
+ }
@@ -0,0 +1,158 @@
1
+ // mapEngine — contrat commun aux moteurs de carte de `<q-map>`.
2
+ //
3
+ // `<q-map>` ne connaît que ce contrat : la prop `provider` choisit un moteur, qui traduit
4
+ // centre / zoom / marques / style vers sa bibliothèque (SDK MapTiler, Leaflet) et convertit
5
+ // les conventions de coordonnées (`[lng, lat]` côté dnax.ui). Tout ce qui ne dépend pas du
6
+ // moteur — props, thème, surcouche de chargement, réactivité — reste dans le composant.
7
+ //
8
+ // Les deux moteurs sont importés **à la demande** (`createMapEngine`) : un `<q-map
9
+ // provider="leaflet">` ne charge jamais le SDK MapTiler, `provider="maplibre"` ne charge
10
+ // jamais Leaflet, et inversement.
11
+ import type { QMapMarkSpec, QMapPick, QMapProvider } from "./map"
12
+
13
+ /** Moteur de carte : surface minimale dont `<q-map>` a besoin. */
14
+ export interface MapEngine {
15
+ /** Instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) */
16
+ native: any
17
+ /** Centre `[lng, lat]` + zoom */
18
+ setView(center: [number, number], zoom: number): void
19
+ /** Style de carte : nom court (`streets`, `dataviz`…), ID/URL, ou objet de style —
20
+ * `dark` demande la variante sombre quand le moteur en a une. Sans effet sur les
21
+ * moteurs raster (Leaflet). */
22
+ setStyle(style: unknown, dark?: boolean): void
23
+ /** Couche de tuiles raster — sans effet sur MapTiler, qui passe par `setStyle` */
24
+ setRaster?(tiles: string, attribution?: string): void
25
+ /** Redessine les marques (marqueurs, épingles et bulles) */
26
+ setMarks(marks: QMapMarkSpec[]): void
27
+ /** Relief 3D — sans effet si le moteur ne le gère pas */
28
+ setTerrain(on: boolean): void
29
+ /** Projection — sans effet si le moteur ne la gère pas */
30
+ setProjection(projection: string): void
31
+ /** Recalcule la taille du canvas après un changement de conteneur */
32
+ resize(): void
33
+ /** Détruit la carte et ses marques */
34
+ destroy(): void
35
+ /** Carte utilisable — le callback part tout de suite si elle l'est déjà */
36
+ onReady(callback: (engine: MapEngine) => void): void
37
+ }
38
+
39
+ /** Contexte transmis à un moteur par `<q-map>`. */
40
+ export interface MapEngineInput {
41
+ /** Fournisseur retenu (le composant a déjà normalisé la valeur) */
42
+ provider: QMapProvider
43
+ /** Élément conteneur : le moteur y monte sa carte */
44
+ el: HTMLElement
45
+ /** Options **déjà construites** pour ce moteur (`mapOptionsOf` / `leafletOptionsOf`) */
46
+ options: Record<string, any>
47
+ /** Centre `[lng, lat]` de départ */
48
+ center: [number, number]
49
+ /** Zoom de départ */
50
+ zoom: number
51
+ /** Mode sombre courant (variante `DARK` des styles MapTiler) */
52
+ dark: boolean
53
+ /** Marques de départ (normalisées par `marksOf`) */
54
+ marks: QMapMarkSpec[]
55
+ /** Contrôles demandés (props `navigation`, `geolocate`, `scale`, `fullscreen`) : le SDK
56
+ * MapTiler les reçoit par ses options, les autres moteurs les ajoutent eux-mêmes */
57
+ controls?: {
58
+ navigation?: boolean | string
59
+ geolocate?: boolean | string
60
+ scale?: boolean | string
61
+ fullscreen?: boolean | string
62
+ }
63
+ /** Couleur concrète d'une marque (token du thème → couleur CSS) */
64
+ color: (value?: string) => string | undefined
65
+ /** Une marque a été cliquée */
66
+ pick: (payload: QMapPick) => void
67
+ /** Erreur du moteur — y compris non fatale (tuile manquante…) */
68
+ error: (error: unknown) => void
69
+ }
70
+
71
+ /** Fabrique de moteur : `maptiler` / `openstreetmap` → SDK MapTiler, `maplibre` →
72
+ * MapLibre GL JS seul, `leaflet` → Leaflet. */
73
+ export async function createMapEngine(input: MapEngineInput): Promise<MapEngine> {
74
+ if (input.provider === "leaflet") {
75
+ const { createLeafletEngine } = await import("./mapLeaflet")
76
+ return createLeafletEngine(input)
77
+ }
78
+
79
+ if (input.provider === "maplibre") {
80
+ const { createMaplibreEngine } = await import("./mapMaplibre")
81
+ return createMaplibreEngine(input)
82
+ }
83
+
84
+ const { createMaptilerEngine } = await import("./mapMaptiler")
85
+ return createMaptilerEngine(input)
86
+ }
87
+
88
+ /** Positions de contrôle communes à MapLibre et à Leaflet. */
89
+ export const CONTROL_POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"] as const
90
+
91
+ /** Position de contrôle à partir d'une prop `boolean | string` (`true` → la position par
92
+ * défaut du moteur, une chaîne valide → elle-même). */
93
+ export function controlPosition(
94
+ value: boolean | string | undefined,
95
+ fallback: (typeof CONTROL_POSITIONS)[number],
96
+ ): (typeof CONTROL_POSITIONS)[number] {
97
+ return typeof value === "string" && (CONTROL_POSITIONS as readonly string[]).includes(value)
98
+ ? (value as (typeof CONTROL_POSITIONS)[number])
99
+ : fallback
100
+ }
101
+
102
+ /**
103
+ * Construit les marques DOM d'un moteur **MapLibre** — le SDK MapTiler et `maplibre-gl`
104
+ * exposent les mêmes `Marker` / `Popup` : ce code est partagé par les deux moteurs.
105
+ * Renvoie la fonction de démontage (à appeler avant d'en construire d'autres).
106
+ */
107
+ export function buildMapLibreMarks(
108
+ api: { Marker: any; Popup: any },
109
+ map: any,
110
+ marks: QMapMarkSpec[],
111
+ input: MapEngineInput,
112
+ ): () => void {
113
+ const created: Array<() => void> = []
114
+
115
+ for (const { mark, index, position, html } of marks) {
116
+ const onPick = () => input.pick({ mark, index, position, lng: position[0], lat: position[1] })
117
+ const offset = typeof mark.offset === "number" ? mark.offset : 18
118
+
119
+ if (mark.type === "popup") {
120
+ if (!html) continue // une bulle sans contenu n'a rien à montrer
121
+ const popup = new api.Popup({ offset, className: "q-map__popup", closeOnClick: true })
122
+ .setLngLat(position)
123
+ .setHTML(html)
124
+ if (mark.open !== false) popup.addTo(map)
125
+
126
+ const node = popup.getElement() as HTMLElement | undefined
127
+ node?.addEventListener("click", onPick)
128
+ created.push(() => {
129
+ node?.removeEventListener("click", onPick)
130
+ popup.remove()
131
+ })
132
+ continue
133
+ }
134
+
135
+ // marker
136
+ const markerOptions: Record<string, any> = {}
137
+ const color = input.color(mark.color)
138
+ if (color) markerOptions.color = color
139
+ if (mark.draggable) markerOptions.draggable = true
140
+
141
+ const marker = new api.Marker(markerOptions).setLngLat(position)
142
+ if (html) marker.setPopup(new api.Popup({ offset, className: "q-map__popup" }).setHTML(html))
143
+ marker.addTo(map)
144
+
145
+ const node = marker.getElement() as HTMLElement | undefined
146
+ node?.addEventListener("click", onPick)
147
+ if (mark.open && html) marker.togglePopup()
148
+
149
+ created.push(() => {
150
+ node?.removeEventListener("click", onPick)
151
+ marker.remove()
152
+ })
153
+ }
154
+
155
+ return () => {
156
+ for (const dispose of created) dispose()
157
+ }
158
+ }