poe-svelte-ui-lib 1.37.3 → 1.38.1

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 (46) hide show
  1. package/dist/CommonSnippets.svelte +0 -4
  2. package/dist/WidgetMap/FitIcon.svelte +12 -0
  3. package/dist/WidgetMap/FitIcon.svelte.d.ts +18 -0
  4. package/dist/WidgetMap/MapIcon.svelte +12 -0
  5. package/dist/WidgetMap/MapIcon.svelte.d.ts +18 -0
  6. package/dist/WidgetMap/README.md +95 -0
  7. package/dist/WidgetMap/WidgetMap.svelte +386 -0
  8. package/dist/WidgetMap/WidgetMap.svelte.d.ts +4 -0
  9. package/dist/WidgetMap/WidgetMapProps.svelte +78 -0
  10. package/dist/WidgetMap/WidgetMapProps.svelte.d.ts +14 -0
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +2 -2
  13. package/dist/locales/translations.js +6 -5
  14. package/dist/types.d.ts +31 -25
  15. package/package.json +1 -1
  16. package/dist/Map/Map.svelte +0 -403
  17. package/dist/Map/Map.svelte.d.ts +0 -4
  18. package/dist/Map/MapProps.svelte +0 -115
  19. package/dist/Map/MapProps.svelte.d.ts +0 -16
  20. package/dist/Map/README.md +0 -149
  21. /package/dist/{Map → WidgetMap}/mapWrapper/Line.svelte +0 -0
  22. /package/dist/{Map → WidgetMap}/mapWrapper/Line.svelte.d.ts +0 -0
  23. /package/dist/{Map → WidgetMap}/mapWrapper/MapLibre.svelte +0 -0
  24. /package/dist/{Map → WidgetMap}/mapWrapper/MapLibre.svelte.d.ts +0 -0
  25. /package/dist/{Map → WidgetMap}/mapWrapper/Marker.svelte +0 -0
  26. /package/dist/{Map → WidgetMap}/mapWrapper/Marker.svelte.d.ts +0 -0
  27. /package/dist/{Map → WidgetMap}/mapWrapper/Popup.svelte +0 -0
  28. /package/dist/{Map → WidgetMap}/mapWrapper/Popup.svelte.d.ts +0 -0
  29. /package/dist/{Map → WidgetMap}/mapWrapper/contexts.svelte.d.ts +0 -0
  30. /package/dist/{Map → WidgetMap}/mapWrapper/contexts.svelte.js +0 -0
  31. /package/dist/{Map → WidgetMap}/mapWrapper/controls/CustomControl.svelte +0 -0
  32. /package/dist/{Map → WidgetMap}/mapWrapper/controls/CustomControl.svelte.d.ts +0 -0
  33. /package/dist/{Map → WidgetMap}/mapWrapper/controls/FullScreenControl.svelte +0 -0
  34. /package/dist/{Map → WidgetMap}/mapWrapper/controls/FullScreenControl.svelte.d.ts +0 -0
  35. /package/dist/{Map → WidgetMap}/mapWrapper/controls/GeolocateControl.svelte +0 -0
  36. /package/dist/{Map → WidgetMap}/mapWrapper/controls/GeolocateControl.svelte.d.ts +0 -0
  37. /package/dist/{Map → WidgetMap}/mapWrapper/controls/NavigationControl.svelte +0 -0
  38. /package/dist/{Map → WidgetMap}/mapWrapper/controls/NavigationControl.svelte.d.ts +0 -0
  39. /package/dist/{Map → WidgetMap}/mapWrapper/controls/ScaleControl.svelte +0 -0
  40. /package/dist/{Map → WidgetMap}/mapWrapper/controls/ScaleControl.svelte.d.ts +0 -0
  41. /package/dist/{Map → WidgetMap}/mapWrapper/index.d.ts +0 -0
  42. /package/dist/{Map → WidgetMap}/mapWrapper/index.js +0 -0
  43. /package/dist/{Map → WidgetMap}/mapWrapper/types.d.ts +0 -0
  44. /package/dist/{Map → WidgetMap}/mapWrapper/types.js +0 -0
  45. /package/dist/{Map → WidgetMap}/mapWrapper/utils.d.ts +0 -0
  46. /package/dist/{Map → WidgetMap}/mapWrapper/utils.js +0 -0
@@ -65,7 +65,6 @@
65
65
  component.type === "ProgressBar" ||
66
66
  component.type === "TextField" ||
67
67
  component.type === "Graph" ||
68
- component.type === "Map" ||
69
68
  component.type === "PeriodChart"
70
69
  ? $optionsStore.ACCESS_OPTION.filter((o) => o.value !== "viewOnly")
71
70
  : $optionsStore.ACCESS_OPTION}
@@ -101,7 +100,6 @@
101
100
  | UI.ITableProps<object>
102
101
  | UI.IFileAttachProps
103
102
  | UI.IJoystickProps
104
- | UI.IMapProps
105
103
  ).label?.name}
106
104
  onUpdate={(value) => updateProperty("label.name", value as string, component, onPropertyChange)}
107
105
  />
@@ -128,7 +126,6 @@
128
126
  | UI.ITableProps<object>
129
127
  | UI.IFileAttachProps
130
128
  | UI.IJoystickProps
131
- | UI.IMapProps
132
129
  ).label?.class,
133
130
  (option as UI.IOption<string>).value,
134
131
  ),
@@ -152,7 +149,6 @@
152
149
  | UI.ITableProps<object>
153
150
  | UI.IFileAttachProps
154
151
  | UI.IJoystickProps
155
- | UI.IMapProps
156
152
  ).label?.class}
157
153
  onUpdate={(value) => updateProperty("label.class", value as string, component, onPropertyChange)}
158
154
  />
@@ -0,0 +1,12 @@
1
+ <script lang="ts"></script>
2
+
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
4
+ ><path
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="2"
10
+ d="M4 9V5a1 1 0 0 1 1-1h4m6 0h4a1 1 0 0 1 1 1v4m0 6v4a1 1 0 0 1-1 1h-4m-6 0H5a1 1 0 0 1-1-1v-4m5-3a3 3 0 1 0 6 0a3 3 0 1 0-6 0"
11
+ /></svg
12
+ >
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const FitIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type FitIcon = InstanceType<typeof FitIcon>;
18
+ export default FitIcon;
@@ -0,0 +1,12 @@
1
+ <script lang="ts"></script>
2
+
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
4
+ ><path
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="2"
10
+ d="M9 4 3 6v14l6-2 6 2 6-2V4l-6 2zm0 0v14m6-12v14"
11
+ /></svg
12
+ >
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const MapIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type MapIcon = InstanceType<typeof MapIcon>;
18
+ export default MapIcon;
@@ -0,0 +1,95 @@
1
+ # WidgetMap Component
2
+
3
+ ## Описание
4
+
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки); высота карты задаётся пропом `height` (px), в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает шапку.
6
+
7
+ Смарт-виджет «Карта» — положение бортов на карте MapLibre с траекториями. Каждый борт — одна **группа полей**
8
+ устройства (префикс, например `MAV`); виджет показывает сразу несколько групп, например `MAV, GNSS` — автопилот
9
+ и собственный GNSS-приёмник модуля. Только отображение: ничего не отправляет и не знает про DeviceStore/WebSocket.
10
+ Заменяет удалённый компонент `Map`.
11
+
12
+ ## Как работать с картой
13
+
14
+ 1. **Устройство** шлёт поля группы в поток телеметрии (`Telemetry_SendObject`, ключ `Data`) — облако и WebApp
15
+ раскладывают их в переменные `<ModSN>|<группа>.<ключ>`. Координаты — раз в 0.5–1 с, имя и серийный номер
16
+ можно в том же пакете (строки передаются только при отправке, отдельной частоты не нужно).
17
+ 2. **В конструкторе** добавьте «Карту» из смарт-виджетов и перечислите группы через запятую (`MAV` или
18
+ `MAV, GNSS`). Переменные привязывать не нужно — виджет сам берёт ключи из таблицы ниже.
19
+ 3. **Оператор** поверх карты включает траекторию, выбирает число её точек и таймаут маркеров. Кнопка
20
+ **«Все борта»** в шапке виджета вписывает все видимые борта в экран (один борт — приближение к нему).
21
+ Сама карта вид не меняет — бортов может быть много.
22
+ 4. **Клик по маркеру** открывает карточку борта: имя, группа, координаты, высота, курс, спутники, серийный
23
+ номер; кнопки «Копировать» (всё в буфер обмена) и «Скрыть» (до следующего обновления борта).
24
+
25
+ ```c
26
+ /* Прошивка: группа MAV — те же ключи читает и WidgetPFD, одна отправка кормит оба виджета */
27
+ const telemetry_kv_t mav[] = {
28
+ TLM_F("MAV.Lat", lat_deg), TLM_F("MAV.Lon", lon_deg), TLM_F("MAV.Alt", alt_m),
29
+ TLM_F("MAV.Heading", hdg_deg), TLM_F("MAV.Sat", (float) sats),
30
+ TLM_S("MAV.DevName", "Борт 1"), TLM_S("MAV.DevSN", g_mcfg.DevSN),
31
+ };
32
+ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", mav, sizeof(mav) / sizeof(mav[0]));
33
+ ```
34
+
35
+ ## Ключи группы
36
+
37
+ Имена общие с другими виджетами: `Lat`/`Lon`/`Alt`/`Heading`/`Sat` — как у WidgetPFD, `DevName`/`DevSN` — как
38
+ у WidgetDeviceInfo. Поэтому одни и те же данные отображаются сразу в нескольких виджетах.
39
+
40
+ | Ключ | Тип | Обязателен | Что |
41
+ | --------- | ------ | ---------- | ------------------------------------------------------------------------------- |
42
+ | `Lat` | число | да | широта, ° (WGS-84); `Lat` и `Lon` = 0 или нечисло — нет фикса, борт не рисуется |
43
+ | `Lon` | число | да | долгота, ° |
44
+ | `Alt` | число | нет | высота, м — в карточке борта |
45
+ | `Heading` | число | нет | курс, 0..360° — поворот маркера |
46
+ | `Sat` | число | нет | число спутников — в карточке |
47
+ | `DevName` | строка | нет | имя борта — подпись маркера и заголовок карточки; без него — имя группы |
48
+ | `DevSN` | строка | нет | серийный номер — в карточке и при копировании |
49
+
50
+ Имена ключей можно переопределить пропом `keys` (например `{ Lat: "NavLat" }`), если прошивка шлёт их иначе.
51
+
52
+ ## Поведение
53
+
54
+ - **Свежесть.** Борт зелёный, пока его значения менялись за таймаут; без изменений — красный; ещё через таймаут
55
+ скрывается до новых данных.
56
+ - **Траектория** — точка добавляется только при смене координат (курс и высота могут обновляться чаще позиции).
57
+ - **Шапка** — сколько бортов на связи из заданных: зелёный — все, серый — часть, красный — ни одного.
58
+ - Карта светлая/тёмная по теме приложения (событие `ThemeChange`).
59
+
60
+ ## Пропсы
61
+
62
+ | Название | Тип | По умолчанию | Описание |
63
+ | ---------------- | ----------------------------------------------- | ------------------- | --------------------------------------------------------------- |
64
+ | `wrapperClass` | `string` | `""` | CSS-классы обёртки |
65
+ | `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-blue"` |
66
+ | `label` | `{ name?: string }` | `{ name: "Карта" }` | Заголовок карточки |
67
+ | `groups` | `string[]` | `["MAV"]` | Префиксы групп полей — по борту на группу |
68
+ | `value` | `Record<string, Partial<IWidgetMapData>>` | `undefined` | Значения по группам: `{ MAV: { Lat, Lon, ... }, GNSS: {...} }` |
69
+ | `keys` | `Partial<Record<keyof IWidgetMapData, string>>` | `undefined` | Переопределение имён ключей (применяет GUIPreview) |
70
+ | `height` | `number` | `400` | Высота карты, px |
71
+ | `markerIcon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка маркера (SVG-строка или компонент), по умолчанию стрелка |
72
+ | `trackEnabled` | `boolean` | `false` | Начальное состояние «Траектория» (`$bindable`) |
73
+ | `trackLength` | `number` | `1000` | Начальное число точек траектории (`$bindable`) |
74
+ | `markerTimeout` | `number` | `30000` | Начальный таймаут маркеров, мс (`$bindable`) |
75
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
76
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
77
+
78
+ ## Пример
79
+
80
+ ```svelte
81
+ <UI.WidgetMap groups={["MAV", "GNSS"]} value={{ MAV: mav, GNSS: gnss }} height={400} trackEnabled />
82
+ ```
83
+
84
+ В DevCloud и WebApp `value` собирает GUIPreview: для каждой группы читает `<ModSN>|<группа>.<ключ>` по таблице выше.
85
+
86
+ ## Конструктор свойств (WidgetMapProps.svelte)
87
+
88
+ Группы панели: общие (заголовок, группы полей через запятую, мастер-цвет), оформление (высота карты, иконка
89
+ маркера), по умолчанию (траектория и её длина — оператор меняет их на карте).
90
+
91
+ ## Внутренняя архитектура
92
+
93
+ `mapWrapper/` — тонкие Svelte-обёртки над MapLibre GL (`MapLibre`, `Marker`, `Popup`, `Line`, контролы,
94
+ `CustomControl` для своих элементов поверх карты). MapLibre подгружается при монтировании; без интернета
95
+ показывается заглушка «Карта не загружена».
@@ -0,0 +1,386 @@
1
+ <!-- $lib/WidgetMap/WidgetMap.svelte — смарт-виджет «Карта»: борта из групп полей устройства (по умолчанию MAV) с полями
2
+ Lat/Lon/Alt/Heading/Sat/DevName/DevSN (см. IWidgetMapData), каждая группа — свой маркер и своя траектория. Только отображение:
3
+ ничего не отправляет и не знает про DeviceStore/WebSocket. Траекторию, длину трека и таймаут маркеров оператор
4
+ переключает прямо поверх карты, «Все борта» — кнопка в шапке; вид карта сама не меняет — бортов может быть много. -->
5
+ <script lang="ts">
6
+ import { onMount, untrack } from "svelte"
7
+ import { slide } from "svelte/transition"
8
+ import { twMerge } from "tailwind-merge"
9
+ import { T } from "../locales/i18n"
10
+ import type { IWidgetMapProps } from "../types"
11
+ import CopiedChip from "../CopiedChip.svelte"
12
+ import WidgetHeader from "../WidgetHeader.svelte"
13
+ import { widgetAccentStyle } from "../widgetAccent"
14
+ import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
15
+ import MapIcon from "./MapIcon.svelte"
16
+ import FitIcon from "./FitIcon.svelte"
17
+ import * as UI from ".."
18
+ import { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, Line, CustomControl } from "./mapWrapper"
19
+
20
+ let {
21
+ wrapperClass = "",
22
+ componentClass = "",
23
+ label = { name: "Карта" },
24
+ groups = ["MAV"],
25
+ value,
26
+ height = 400,
27
+ markerIcon,
28
+ trackEnabled = $bindable(false),
29
+ trackLength = $bindable(1000),
30
+ markerTimeout = $bindable(30_000),
31
+ collapsed = $bindable(false),
32
+ persistKey,
33
+ }: IWidgetMapProps = $props()
34
+
35
+ let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
36
+
37
+ /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
38
+ let hasMergedPersisted = false
39
+ $effect(() => {
40
+ if (hasMergedPersisted) return
41
+ hasMergedPersisted = true
42
+ const persisted = readPersistedCollapsed(persistKey, collapsed)
43
+ if (persisted !== collapsed) collapsed = persisted
44
+ })
45
+ $effect(() => writePersistedCollapsed(persistKey, collapsed))
46
+
47
+ interface Board {
48
+ group: string
49
+ lat: number
50
+ lon: number
51
+ alt: number
52
+ heading: number
53
+ sat: number
54
+ name: string
55
+ sn: string
56
+ track: [number, number][]
57
+ lastChange: number
58
+ hiddenAt: number
59
+ }
60
+
61
+ const num = (v: unknown, fallback = 0) => (typeof v === "number" && Number.isFinite(v) ? v : fallback)
62
+ /* (0, 0) и нечисловые координаты — признак отсутствия фикса */
63
+ const hasFix = (lat: unknown, lon: unknown) => Number.isFinite(lat) && Number.isFinite(lon) && (lat !== 0 || lon !== 0)
64
+
65
+ let groupList = $derived(groups.map((g) => g.trim()).filter(Boolean))
66
+ let boards: Board[] = $state([])
67
+ let now = $state(Date.now())
68
+ let mapInstance: any = $state()
69
+ let isCopied = $state(false)
70
+ let isDarkMode = $state(false)
71
+
72
+ /* Борт обновляется только при реальной смене значений — свежесть считается от последнего изменения, а не от перерисовки */
73
+ $effect(() => {
74
+ for (const group of groupList) {
75
+ const v = value?.[group]
76
+ if (!v || !hasFix(v.Lat, v.Lon)) continue
77
+ const lat = v.Lat as number
78
+ const lon = v.Lon as number
79
+ const alt = num(v.Alt)
80
+ const heading = num(v.Heading)
81
+ const sat = num(v.Sat)
82
+ const name = typeof v.DevName === "string" && v.DevName ? v.DevName : group
83
+ const sn = typeof v.DevSN === "string" ? v.DevSN : ""
84
+ untrack(() => {
85
+ const idx = boards.findIndex((b) => b.group === group)
86
+ const prev = idx === -1 ? null : boards[idx]
87
+ if (
88
+ prev &&
89
+ prev.lat === lat &&
90
+ prev.lon === lon &&
91
+ prev.alt === alt &&
92
+ prev.heading === heading &&
93
+ prev.sat === sat &&
94
+ prev.name === name &&
95
+ prev.sn === sn
96
+ )
97
+ return
98
+ const moved = !prev || prev.lat !== lat || prev.lon !== lon
99
+ const track = moved ? [...(prev?.track ?? []), [lon, lat] as [number, number]].slice(-trackLength) : prev!.track
100
+ const next: Board = { group, lat, lon, alt, heading, sat, name, sn, track, lastChange: Date.now(), hiddenAt: prev?.hiddenAt ?? 0 }
101
+ if (idx === -1) boards.push(next)
102
+ else boards[idx] = next
103
+ })
104
+ }
105
+ })
106
+
107
+ /* Подрезает накопленные траектории при уменьшении trackLength */
108
+ $effect(() => {
109
+ const limit = trackLength
110
+ untrack(() => boards.forEach((b) => b.track.length > limit && (b.track = b.track.slice(-limit))))
111
+ })
112
+
113
+ $effect(() => {
114
+ const timer = setInterval(() => (now = Date.now()), 1000)
115
+ return () => clearInterval(timer)
116
+ })
117
+
118
+ /* Свежий — менялся за markerTimeout; устаревший (красный) — ещё markerTimeout; дальше скрыт до новых данных */
119
+ let visibleBoards = $derived(boards.filter((b) => groupList.includes(b.group) && b.lastChange > b.hiddenAt && now - b.lastChange < markerTimeout * 2))
120
+ const isFresh = (b: Board) => now - b.lastChange < markerTimeout
121
+ let freshCount = $derived(visibleBoards.filter(isFresh).length)
122
+
123
+ /* Вписывает в экран все видимые борта, один борт — приближение к нему */
124
+ const fitAllBoards = () => {
125
+ if (!mapInstance || visibleBoards.length === 0) return
126
+ if (visibleBoards.length === 1) return mapInstance.flyTo({ center: [visibleBoards[0].lon, visibleBoards[0].lat], zoom: 16 })
127
+ const lons = visibleBoards.map((b) => b.lon)
128
+ const lats = visibleBoards.map((b) => b.lat)
129
+ mapInstance.fitBounds(
130
+ [
131
+ [Math.min(...lons), Math.min(...lats)],
132
+ [Math.max(...lons), Math.max(...lats)],
133
+ ],
134
+ { padding: 60, maxZoom: 16 },
135
+ )
136
+ }
137
+
138
+ const hideBoard = (group: string) => {
139
+ const b = boards.find((x) => x.group === group)
140
+ if (b) b.hiddenAt = b.lastChange
141
+ }
142
+
143
+ const copyBoard = (b: Board) => {
144
+ navigator.clipboard.writeText(
145
+ `DevName: ${b.name}\nDevSN: ${b.sn}\nGroup: ${b.group}\nLat: ${b.lat.toFixed(6)} | Lon: ${b.lon.toFixed(6)} | Alt: ${b.alt.toFixed(1)}\nHeading: ${b.heading.toFixed(1)} | Satellites: ${b.sat}`,
146
+ )
147
+ isCopied = true
148
+ setTimeout(() => (isCopied = false), 1000)
149
+ }
150
+
151
+ const handleThemeChange = (event: CustomEvent) => {
152
+ isDarkMode = !event.detail.currentTheme
153
+ }
154
+ onMount(() => {
155
+ isDarkMode = localStorage.getItem("AppTheme") !== "light"
156
+ window.addEventListener("ThemeChange", handleThemeChange as EventListener)
157
+ return () => window.removeEventListener("ThemeChange", handleThemeChange as EventListener)
158
+ })
159
+
160
+ const trackLengthOptions = [100, 500, 1000, 5000, 10000, 50000]
161
+ const timeoutOptions: { label: string; value: number }[] = [
162
+ { label: "30 sec", value: 30_000 },
163
+ { label: "1 min", value: 60_000 },
164
+ { label: "3 min", value: 180_000 },
165
+ { label: "5 min", value: 300_000 },
166
+ { label: "10 min", value: 600_000 },
167
+ { label: "30 min", value: 1_800_000 },
168
+ { label: "1 h", value: 3_600_000 },
169
+ ]
170
+ </script>
171
+
172
+ <div
173
+ class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
174
+ style={accentStyle}
175
+ >
176
+ <WidgetHeader icon={MapIcon} label={label?.name ?? "Карта"} bind:collapsed>
177
+ {#snippet right()}
178
+ <div class="flex items-center gap-3">
179
+ <UI.Button
180
+ wrapperClass="w-auto"
181
+ componentClass="bg-transparent px-3"
182
+ content={{ icon: FitIcon, name: $T("constructor.props.map.fitAll") }}
183
+ disabled={collapsed || visibleBoards.length === 0}
184
+ onClick={fitAllBoards}
185
+ />
186
+ <span
187
+ class="text-[12px] font-semibold tabular-nums {freshCount === 0
188
+ ? 'text-(--red-color)'
189
+ : freshCount < groupList.length
190
+ ? 'text-(--muted-color)'
191
+ : 'text-(--green-color)'}"
192
+ title={$T("constructor.props.map.boards")}
193
+ >
194
+ {freshCount} / {groupList.length}
195
+ </span>
196
+ </div>
197
+ {/snippet}
198
+ </WidgetHeader>
199
+
200
+ {#if !collapsed}
201
+ <div transition:slide={{ duration: 150 }} style="height: {height}px">
202
+ <MapLibre
203
+ bind:map={mapInstance}
204
+ class="h-full w-full overflow-hidden rounded-[14px] border border-(--hairline-color)"
205
+ style={isDarkMode ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
206
+ zoom={1.5}
207
+ center={{ lat: 30, lng: 0 }}
208
+ >
209
+ <NavigationControl />
210
+ <ScaleControl />
211
+ <GeolocateControl />
212
+ <FullScreenControl />
213
+
214
+ <!-- Панель оператора поверх карты -->
215
+ <CustomControl position="top-left">
216
+ <div class="flex flex-wrap items-center gap-3 rounded-lg px-2 py-1 text-black">
217
+ <label class="flex items-center gap-1.5 text-sm font-medium">
218
+ <input type="checkbox" bind:checked={trackEnabled} />
219
+ {$T("constructor.props.map.track")}
220
+ </label>
221
+ {#if trackEnabled}
222
+ <div class="flex items-center gap-2">
223
+ <span class="text-sm font-medium">{$T("constructor.props.map.trackLength")}</span>
224
+ <select class="rounded px-2 py-1 text-sm" bind:value={trackLength}>
225
+ {#each trackLengthOptions as opt}
226
+ <option value={opt}>{opt}</option>
227
+ {/each}
228
+ </select>
229
+ </div>
230
+ {/if}
231
+ <div class="flex items-center gap-2">
232
+ <span class="text-sm font-medium">{$T("constructor.props.map.timeout")}</span>
233
+ <select class="rounded px-2 py-1 text-sm" bind:value={markerTimeout}>
234
+ {#each timeoutOptions as opt}
235
+ <option value={opt.value}>{opt.label}</option>
236
+ {/each}
237
+ </select>
238
+ </div>
239
+ </div>
240
+ </CustomControl>
241
+
242
+ {#if trackEnabled}
243
+ {#each visibleBoards as board (board.group)}
244
+ {#if board.track.length >= 2}
245
+ <Line id={`track-${board.group}`} coordinates={board.track} color={isFresh(board) ? "#22c55e" : "#ef4444"} />
246
+ {/if}
247
+ {/each}
248
+ {/if}
249
+
250
+ {#each visibleBoards as board (board.group)}
251
+ <Marker lnglat={{ lng: board.lon, lat: board.lat }}>
252
+ {#snippet content()}
253
+ <div class="flex flex-col items-center justify-center leading-none">
254
+ <div
255
+ class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
256
+ {isFresh(board) ? 'text-(--green-color)' : 'text-(--red-color)'}"
257
+ style="rotate: {board.heading}deg;"
258
+ >
259
+ {#if markerIcon}
260
+ {#if typeof markerIcon === "string"}
261
+ {@html markerIcon}
262
+ {:else}
263
+ {@const IconComponent = markerIcon}
264
+ <IconComponent />
265
+ {/if}
266
+ {:else}
267
+ <div class="rotate-270">
268
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
269
+ <path
270
+ fill="none"
271
+ stroke="currentColor"
272
+ stroke-linecap="round"
273
+ stroke-linejoin="round"
274
+ stroke-width="1.5"
275
+ d="M14.76 12H6.832m0 0c0-.275-.057-.55-.17-.808L4.285 5.814c-.76-1.72 1.058-3.442 2.734-2.591L20.8 10.217c1.46.74 1.46 2.826 0 3.566L7.02 20.777c-1.677.851-3.495-.872-2.735-2.591l2.375-5.378A2 2 0 0 0 6.83 12"
276
+ /></svg
277
+ >
278
+ </div>
279
+ {/if}
280
+ </div>
281
+ <p class="font-bold">{board.name}</p>
282
+ </div>
283
+ {/snippet}
284
+
285
+ <Popup closeButton={false} class="map-popup text-left">
286
+ <div class="flex flex-col gap-1.5">
287
+ <div class="flex items-center gap-1.5">
288
+ <span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
289
+ <h6 class="text-sm leading-none font-bold">{board.name}</h6>
290
+ {#if board.name !== board.group}<span class="text-[0.6rem] font-semibold uppercase opacity-60">{board.group}</span>{/if}
291
+ </div>
292
+
293
+ <div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
294
+ <div>
295
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
296
+ <p class="font-medium">{board.lat.toFixed(6)}, {board.lon.toFixed(6)}</p>
297
+ </div>
298
+ <div>
299
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
300
+ <p class="font-medium">{board.alt.toFixed(1)} m</p>
301
+ </div>
302
+ <div>
303
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
304
+ <p class="font-medium">{board.heading.toFixed(1)}°</p>
305
+ </div>
306
+ <div>
307
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
308
+ <p class="font-medium">{board.sat}</p>
309
+ </div>
310
+ </div>
311
+
312
+ {#if board.sn}<p class="text-center font-mono text-[0.6rem] tracking-wide uppercase">{board.sn}</p>{/if}
313
+
314
+ <div class="flex items-center justify-between border-t border-(--hairline-color) pt-1">
315
+ <div class="relative flex items-center">
316
+ <button
317
+ class="flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-1 py-0.5 opacity-70 transition hover:opacity-100"
318
+ onclick={(e) => {
319
+ e.preventDefault()
320
+ copyBoard(board)
321
+ }}
322
+ aria-label="Copy"
323
+ title="Copy"
324
+ >
325
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24">
326
+ <g fill="none" stroke="currentColor" stroke-width="1.5">
327
+ <path
328
+ d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"
329
+ />
330
+ <path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3" />
331
+ </g>
332
+ </svg>
333
+ </button>
334
+ <CopiedChip show={isCopied} class="-top-1 right-0 -translate-y-full" />
335
+ </div>
336
+
337
+ <button
338
+ class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
339
+ aria-label="Hide"
340
+ title="Hide"
341
+ onclick={() => hideBoard(board.group)}
342
+ >
343
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24"
344
+ ><path
345
+ fill="none"
346
+ stroke="currentColor"
347
+ stroke-linecap="round"
348
+ stroke-linejoin="round"
349
+ stroke-width="1.5"
350
+ d="m19.5 5.5l-.62 10.025c-.158 2.561-.237 3.842-.88 4.763a4 4 0 0 1-1.2 1.128c-.957.584-2.24.584-4.806.584c-2.57 0-3.855 0-4.814-.585a4 4 0 0 1-1.2-1.13c-.642-.922-.72-2.205-.874-4.77L4.5 5.5M3 5.5h18m-4.944 0l-.683-1.408c-.453-.936-.68-1.403-1.071-1.695a2 2 0 0 0-.275-.172C13.594 2 13.074 2 12.035 2c-1.066 0-1.599 0-2.04.234a2 2 0 0 0-.278.18c-.395.303-.616.788-1.058 1.757L8.053 5.5m1.447 11v-6m5 6v-6"
351
+ /></svg
352
+ >
353
+ </button>
354
+ </div>
355
+ </div>
356
+ </Popup>
357
+ </Marker>
358
+ {/each}
359
+ </MapLibre>
360
+ </div>
361
+ {/if}
362
+ </div>
363
+
364
+ <style>
365
+ /* Попап по правилам карточки (design-handoff, раздел 9): радиус 14, линия, тень уровня 2 */
366
+ :global(.map-popup .maplibregl-popup-content) {
367
+ background-color: var(--back-color);
368
+ color: inherit;
369
+ border: 1px solid var(--hairline-color);
370
+ border-radius: 14px;
371
+ box-shadow: var(--elevation-2);
372
+ padding: 12px;
373
+ }
374
+ :global(.map-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) {
375
+ border-top-color: var(--back-color);
376
+ }
377
+ :global(.map-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip) {
378
+ border-bottom-color: var(--back-color);
379
+ }
380
+ :global(.map-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip) {
381
+ border-right-color: var(--back-color);
382
+ }
383
+ :global(.map-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip) {
384
+ border-left-color: var(--back-color);
385
+ }
386
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { IWidgetMapProps } from "../types";
2
+ declare const WidgetMap: import("svelte").Component<IWidgetMapProps, {}, "collapsed" | "trackEnabled" | "trackLength" | "markerTimeout">;
3
+ type WidgetMap = ReturnType<typeof WidgetMap>;
4
+ export default WidgetMap;