poe-svelte-ui-lib 1.37.2 → 1.38.0

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 (44) hide show
  1. package/dist/CommonSnippets.svelte +0 -4
  2. package/dist/WidgetMap/MapIcon.svelte +12 -0
  3. package/dist/WidgetMap/MapIcon.svelte.d.ts +18 -0
  4. package/dist/WidgetMap/README.md +66 -0
  5. package/dist/WidgetMap/WidgetMap.svelte +381 -0
  6. package/dist/WidgetMap/WidgetMap.svelte.d.ts +4 -0
  7. package/dist/WidgetMap/WidgetMapProps.svelte +78 -0
  8. package/dist/WidgetMap/WidgetMapProps.svelte.d.ts +14 -0
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +2 -2
  11. package/dist/locales/translations.js +6 -5
  12. package/dist/types.d.ts +29 -25
  13. package/package.json +1 -1
  14. package/dist/Map/Map.svelte +0 -388
  15. package/dist/Map/Map.svelte.d.ts +0 -4
  16. package/dist/Map/MapProps.svelte +0 -115
  17. package/dist/Map/MapProps.svelte.d.ts +0 -16
  18. package/dist/Map/README.md +0 -149
  19. /package/dist/{Map → WidgetMap}/mapWrapper/Line.svelte +0 -0
  20. /package/dist/{Map → WidgetMap}/mapWrapper/Line.svelte.d.ts +0 -0
  21. /package/dist/{Map → WidgetMap}/mapWrapper/MapLibre.svelte +0 -0
  22. /package/dist/{Map → WidgetMap}/mapWrapper/MapLibre.svelte.d.ts +0 -0
  23. /package/dist/{Map → WidgetMap}/mapWrapper/Marker.svelte +0 -0
  24. /package/dist/{Map → WidgetMap}/mapWrapper/Marker.svelte.d.ts +0 -0
  25. /package/dist/{Map → WidgetMap}/mapWrapper/Popup.svelte +0 -0
  26. /package/dist/{Map → WidgetMap}/mapWrapper/Popup.svelte.d.ts +0 -0
  27. /package/dist/{Map → WidgetMap}/mapWrapper/contexts.svelte.d.ts +0 -0
  28. /package/dist/{Map → WidgetMap}/mapWrapper/contexts.svelte.js +0 -0
  29. /package/dist/{Map → WidgetMap}/mapWrapper/controls/CustomControl.svelte +0 -0
  30. /package/dist/{Map → WidgetMap}/mapWrapper/controls/CustomControl.svelte.d.ts +0 -0
  31. /package/dist/{Map → WidgetMap}/mapWrapper/controls/FullScreenControl.svelte +0 -0
  32. /package/dist/{Map → WidgetMap}/mapWrapper/controls/FullScreenControl.svelte.d.ts +0 -0
  33. /package/dist/{Map → WidgetMap}/mapWrapper/controls/GeolocateControl.svelte +0 -0
  34. /package/dist/{Map → WidgetMap}/mapWrapper/controls/GeolocateControl.svelte.d.ts +0 -0
  35. /package/dist/{Map → WidgetMap}/mapWrapper/controls/NavigationControl.svelte +0 -0
  36. /package/dist/{Map → WidgetMap}/mapWrapper/controls/NavigationControl.svelte.d.ts +0 -0
  37. /package/dist/{Map → WidgetMap}/mapWrapper/controls/ScaleControl.svelte +0 -0
  38. /package/dist/{Map → WidgetMap}/mapWrapper/controls/ScaleControl.svelte.d.ts +0 -0
  39. /package/dist/{Map → WidgetMap}/mapWrapper/index.d.ts +0 -0
  40. /package/dist/{Map → WidgetMap}/mapWrapper/index.js +0 -0
  41. /package/dist/{Map → WidgetMap}/mapWrapper/types.d.ts +0 -0
  42. /package/dist/{Map → WidgetMap}/mapWrapper/types.js +0 -0
  43. /package/dist/{Map → WidgetMap}/mapWrapper/utils.d.ts +0 -0
  44. /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="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,66 @@
1
+ # WidgetMap Component
2
+
3
+ ## Описание
4
+
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки); высота карты задаётся пропом `height` (px), в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает шапку.
6
+
7
+ Смарт-виджет «Карта» — положение бортов на карте MapLibre с траекториями. Работает с полями групп устройства
8
+ (по умолчанию `MAV`): каждая группа из `groups` — отдельный борт со своим маркером и своей траекторией, например
9
+ `["MAV", "GNSS"]` — позиция автопилота и собственного GNSS-приёмника модуля. Только отображение: ничего не
10
+ отправляет и не знает про DeviceStore/WebSocket. Заменяет удалённый компонент `Map`.
11
+
12
+ - **Поверх карты** (решает оператор): траектория вкл/выкл и число её точек, таймаут маркеров, кнопка «Все борта»
13
+ (иконка рамки под зумом и полноэкранным режимом) — вписывает в экран все видимые борта, один борт — приближение
14
+ к нему (зум 16). Сама карта вид не меняет.
15
+ - **Свежесть.** Борт зелёный, пока его значения менялись за таймаут; без изменений — красный; ещё через таймаут
16
+ скрывается до новых данных. Кнопка в карточке борта скрывает его до следующего изменения.
17
+ - **Траектория** — точка добавляется только при смене координат (курс и высота могут обновляться чаще позиции).
18
+ - **Шапка** — сколько бортов на связи из заданных: зелёный — все, серый — часть, красный — ни одного.
19
+ - Карта светлая/тёмная по теме приложения (событие `ThemeChange`).
20
+
21
+ ## Поля группы
22
+
23
+ | Поле | Что |
24
+ | --------- | ----------------------------------------- |
25
+ | `Lat` | широта, °; вместе с `Lon` = 0 — нет фикса |
26
+ | `Lon` | долгота, ° |
27
+ | `Alt` | высота, м — в карточке борта |
28
+ | `Heading` | курс, 0..360° — поворот маркера |
29
+ | `Sat` | спутников — в карточке борта |
30
+
31
+ В DevCloud и WebApp GUIPreview собирает `value` сам: `{ [группа]: { Lat: <ModSN>|<группа>.Lat, ... } }`.
32
+
33
+ ## Пропсы
34
+
35
+ | Название | Тип | По умолчанию | Описание |
36
+ | ---------------- | ----------------------------------------------- | ------------------- | --------------------------------------------------------------- |
37
+ | `wrapperClass` | `string` | `""` | CSS-классы обёртки |
38
+ | `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-blue"` |
39
+ | `label` | `{ name?: string }` | `{ name: "Карта" }` | Заголовок карточки |
40
+ | `groups` | `string[]` | `["MAV"]` | Префиксы групп полей — по борту на группу |
41
+ | `value` | `Record<string, Partial<IWidgetMapData>>` | `undefined` | Значения по группам |
42
+ | `keys` | `Partial<Record<keyof IWidgetMapData, string>>` | `undefined` | Переопределение имён полей устройства (используется GUIPreview) |
43
+ | `height` | `number` | `400` | Высота карты, px |
44
+ | `markerIcon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка маркера (SVG-строка или компонент), по умолчанию стрелка |
45
+ | `trackEnabled` | `boolean` | `false` | Начальное состояние «Траектория» (`$bindable`) |
46
+ | `trackLength` | `number` | `1000` | Начальное число точек траектории (`$bindable`) |
47
+ | `markerTimeout` | `number` | `30000` | Начальный таймаут маркеров, мс (`$bindable`) |
48
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
49
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
50
+
51
+ ## Пример
52
+
53
+ ```svelte
54
+ <UI.WidgetMap groups={["MAV", "GNSS"]} value={{ MAV: mav, GNSS: gnss }} height={400} trackEnabled />
55
+ ```
56
+
57
+ ## Конструктор свойств (WidgetMapProps.svelte)
58
+
59
+ Группы панели: общие (заголовок, группы полей через запятую, мастер-цвет), оформление (высота карты, иконка
60
+ маркера), по умолчанию (траектория и её длина — оператор меняет их на карте).
61
+
62
+ ## Внутренняя архитектура
63
+
64
+ `mapWrapper/` — тонкие Svelte-обёртки над MapLibre GL (`MapLibre`, `Marker`, `Popup`, `Line`, контролы,
65
+ `CustomControl` для своих элементов поверх карты). MapLibre подгружается при монтировании; без интернета
66
+ показывается заглушка «Карта не загружена».
@@ -0,0 +1,381 @@
1
+ <!-- $lib/WidgetMap/WidgetMap.svelte — смарт-виджет «Карта»: борта из групп полей устройства (по умолчанию MAV) с полями
2
+ Lat/Lon/Alt/Heading/Sat (см. 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 { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, Line, CustomControl } from "./mapWrapper"
17
+
18
+ let {
19
+ wrapperClass = "",
20
+ componentClass = "",
21
+ label = { name: "Карта" },
22
+ groups = ["MAV"],
23
+ value,
24
+ height = 400,
25
+ markerIcon,
26
+ trackEnabled = $bindable(false),
27
+ trackLength = $bindable(1000),
28
+ markerTimeout = $bindable(30_000),
29
+ collapsed = $bindable(false),
30
+ persistKey,
31
+ }: IWidgetMapProps = $props()
32
+
33
+ let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
34
+
35
+ /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
36
+ let hasMergedPersisted = false
37
+ $effect(() => {
38
+ if (hasMergedPersisted) return
39
+ hasMergedPersisted = true
40
+ const persisted = readPersistedCollapsed(persistKey, collapsed)
41
+ if (persisted !== collapsed) collapsed = persisted
42
+ })
43
+ $effect(() => writePersistedCollapsed(persistKey, collapsed))
44
+
45
+ interface Board {
46
+ group: string
47
+ lat: number
48
+ lon: number
49
+ alt: number
50
+ heading: number
51
+ sat: number
52
+ track: [number, number][]
53
+ lastChange: number
54
+ hiddenAt: number
55
+ }
56
+
57
+ const num = (v: unknown, fallback = 0) => (typeof v === "number" && Number.isFinite(v) ? v : fallback)
58
+ /* (0, 0) и нечисловые координаты — признак отсутствия фикса */
59
+ const hasFix = (lat: unknown, lon: unknown) => Number.isFinite(lat) && Number.isFinite(lon) && (lat !== 0 || lon !== 0)
60
+
61
+ let groupList = $derived(groups.map((g) => g.trim()).filter(Boolean))
62
+ let boards: Board[] = $state([])
63
+ let now = $state(Date.now())
64
+ let mapInstance: any = $state()
65
+ let isCopied = $state(false)
66
+ let isDarkMode = $state(false)
67
+
68
+ /* Борт обновляется только при реальной смене значений — свежесть считается от последнего изменения, а не от перерисовки */
69
+ $effect(() => {
70
+ for (const group of groupList) {
71
+ const v = value?.[group]
72
+ if (!v || !hasFix(v.Lat, v.Lon)) continue
73
+ const lat = v.Lat as number
74
+ const lon = v.Lon as number
75
+ const alt = num(v.Alt)
76
+ const heading = num(v.Heading)
77
+ const sat = num(v.Sat)
78
+ untrack(() => {
79
+ const idx = boards.findIndex((b) => b.group === group)
80
+ const prev = idx === -1 ? null : boards[idx]
81
+ if (prev && prev.lat === lat && prev.lon === lon && prev.alt === alt && prev.heading === heading && prev.sat === sat) return
82
+ const moved = !prev || prev.lat !== lat || prev.lon !== lon
83
+ const track = moved ? [...(prev?.track ?? []), [lon, lat] as [number, number]].slice(-trackLength) : prev!.track
84
+ const next: Board = { group, lat, lon, alt, heading, sat, track, lastChange: Date.now(), hiddenAt: prev?.hiddenAt ?? 0 }
85
+ if (idx === -1) boards.push(next)
86
+ else boards[idx] = next
87
+ })
88
+ }
89
+ })
90
+
91
+ /* Подрезает накопленные траектории при уменьшении trackLength */
92
+ $effect(() => {
93
+ const limit = trackLength
94
+ untrack(() => boards.forEach((b) => b.track.length > limit && (b.track = b.track.slice(-limit))))
95
+ })
96
+
97
+ $effect(() => {
98
+ const timer = setInterval(() => (now = Date.now()), 1000)
99
+ return () => clearInterval(timer)
100
+ })
101
+
102
+ /* Свежий — менялся за markerTimeout; устаревший (красный) — ещё markerTimeout; дальше скрыт до новых данных */
103
+ let visibleBoards = $derived(boards.filter((b) => groupList.includes(b.group) && b.lastChange > b.hiddenAt && now - b.lastChange < markerTimeout * 2))
104
+ const isFresh = (b: Board) => now - b.lastChange < markerTimeout
105
+ let freshCount = $derived(visibleBoards.filter(isFresh).length)
106
+
107
+ /* Вписывает в экран все видимые борта, один борт — приближение к нему */
108
+ const fitAllBoards = () => {
109
+ if (!mapInstance || visibleBoards.length === 0) return
110
+ if (visibleBoards.length === 1) return mapInstance.flyTo({ center: [visibleBoards[0].lon, visibleBoards[0].lat], zoom: 16 })
111
+ const lons = visibleBoards.map((b) => b.lon)
112
+ const lats = visibleBoards.map((b) => b.lat)
113
+ mapInstance.fitBounds(
114
+ [
115
+ [Math.min(...lons), Math.min(...lats)],
116
+ [Math.max(...lons), Math.max(...lats)],
117
+ ],
118
+ { padding: 60, maxZoom: 16 },
119
+ )
120
+ }
121
+
122
+ const hideBoard = (group: string) => {
123
+ const b = boards.find((x) => x.group === group)
124
+ if (b) b.hiddenAt = b.lastChange
125
+ }
126
+
127
+ const copyBoard = (b: Board) => {
128
+ navigator.clipboard.writeText(
129
+ `${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}`,
130
+ )
131
+ isCopied = true
132
+ setTimeout(() => (isCopied = false), 1000)
133
+ }
134
+
135
+ const handleThemeChange = (event: CustomEvent) => {
136
+ isDarkMode = !event.detail.currentTheme
137
+ }
138
+ onMount(() => {
139
+ isDarkMode = localStorage.getItem("AppTheme") !== "light"
140
+ window.addEventListener("ThemeChange", handleThemeChange as EventListener)
141
+ return () => window.removeEventListener("ThemeChange", handleThemeChange as EventListener)
142
+ })
143
+
144
+ const trackLengthOptions = [100, 500, 1000, 5000, 10000, 50000]
145
+ const timeoutOptions: { label: string; value: number }[] = [
146
+ { label: "30 sec", value: 30_000 },
147
+ { label: "1 min", value: 60_000 },
148
+ { label: "3 min", value: 180_000 },
149
+ { label: "5 min", value: 300_000 },
150
+ { label: "10 min", value: 600_000 },
151
+ { label: "30 min", value: 1_800_000 },
152
+ { label: "1 h", value: 3_600_000 },
153
+ ]
154
+ </script>
155
+
156
+ <div
157
+ class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
158
+ style={accentStyle}
159
+ >
160
+ <WidgetHeader icon={MapIcon} label={label?.name ?? "Карта"} bind:collapsed>
161
+ {#snippet right()}
162
+ <span
163
+ class="text-[12px] font-semibold tabular-nums {freshCount === 0
164
+ ? 'text-(--red-color)'
165
+ : freshCount < groupList.length
166
+ ? 'text-(--muted-color)'
167
+ : 'text-(--green-color)'}"
168
+ title={$T("constructor.props.map.boards")}
169
+ >
170
+ {freshCount} / {groupList.length}
171
+ </span>
172
+ {/snippet}
173
+ </WidgetHeader>
174
+
175
+ {#if !collapsed}
176
+ <div transition:slide={{ duration: 150 }} style="height: {height}px">
177
+ <MapLibre
178
+ bind:map={mapInstance}
179
+ class="h-full w-full overflow-hidden rounded-[14px] border border-(--hairline-color)"
180
+ style={isDarkMode ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
181
+ zoom={1.5}
182
+ center={{ lat: 30, lng: 0 }}
183
+ >
184
+ <NavigationControl />
185
+ <ScaleControl />
186
+ <GeolocateControl />
187
+ <FullScreenControl />
188
+
189
+ <!-- Панель оператора поверх карты -->
190
+ <CustomControl position="top-left">
191
+ <div class="flex flex-wrap items-center gap-3 rounded-lg px-2 py-1 text-black">
192
+ <label class="flex items-center gap-1.5 text-sm font-medium">
193
+ <input type="checkbox" bind:checked={trackEnabled} />
194
+ {$T("constructor.props.map.track")}
195
+ </label>
196
+ {#if trackEnabled}
197
+ <div class="flex items-center gap-2">
198
+ <span class="text-sm font-medium">{$T("constructor.props.map.trackLength")}</span>
199
+ <select class="rounded px-2 py-1 text-sm" bind:value={trackLength}>
200
+ {#each trackLengthOptions as opt}
201
+ <option value={opt}>{opt}</option>
202
+ {/each}
203
+ </select>
204
+ </div>
205
+ {/if}
206
+ <div class="flex items-center gap-2">
207
+ <span class="text-sm font-medium">{$T("constructor.props.map.timeout")}</span>
208
+ <select class="rounded px-2 py-1 text-sm" bind:value={markerTimeout}>
209
+ {#each timeoutOptions as opt}
210
+ <option value={opt.value}>{opt.label}</option>
211
+ {/each}
212
+ </select>
213
+ </div>
214
+ </div>
215
+ </CustomControl>
216
+
217
+ <!-- Штатная кнопка карты в колонке зума/геолокации/полного экрана -->
218
+ <CustomControl position="top-right">
219
+ <button
220
+ type="button"
221
+ class="flex! items-center justify-center text-[#333] disabled:opacity-40"
222
+ title={$T("constructor.props.map.fitAll")}
223
+ aria-label={$T("constructor.props.map.fitAll")}
224
+ disabled={visibleBoards.length === 0}
225
+ onclick={fitAllBoards}
226
+ >
227
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
228
+ <path
229
+ fill="none"
230
+ stroke="currentColor"
231
+ stroke-linecap="round"
232
+ stroke-linejoin="round"
233
+ stroke-width="2"
234
+ 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"
235
+ />
236
+ </svg>
237
+ </button>
238
+ </CustomControl>
239
+
240
+ {#if trackEnabled}
241
+ {#each visibleBoards as board (board.group)}
242
+ {#if board.track.length >= 2}
243
+ <Line id={`track-${board.group}`} coordinates={board.track} color={isFresh(board) ? "#22c55e" : "#ef4444"} />
244
+ {/if}
245
+ {/each}
246
+ {/if}
247
+
248
+ {#each visibleBoards as board (board.group)}
249
+ <Marker lnglat={{ lng: board.lon, lat: board.lat }}>
250
+ {#snippet content()}
251
+ <div class="flex flex-col items-center justify-center leading-none">
252
+ <div
253
+ class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
254
+ {isFresh(board) ? 'text-(--green-color)' : 'text-(--red-color)'}"
255
+ style="rotate: {board.heading}deg;"
256
+ >
257
+ {#if markerIcon}
258
+ {#if typeof markerIcon === "string"}
259
+ {@html markerIcon}
260
+ {:else}
261
+ {@const IconComponent = markerIcon}
262
+ <IconComponent />
263
+ {/if}
264
+ {:else}
265
+ <div class="rotate-270">
266
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
267
+ <path
268
+ fill="none"
269
+ stroke="currentColor"
270
+ stroke-linecap="round"
271
+ stroke-linejoin="round"
272
+ stroke-width="1.5"
273
+ 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"
274
+ /></svg
275
+ >
276
+ </div>
277
+ {/if}
278
+ </div>
279
+ <p class="font-bold">{board.group}</p>
280
+ </div>
281
+ {/snippet}
282
+
283
+ <Popup closeButton={false} class="map-popup text-left">
284
+ <div class="flex flex-col gap-1.5">
285
+ <div class="flex items-center gap-1.5">
286
+ <span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
287
+ <h6 class="text-sm leading-none font-bold">{board.group}</h6>
288
+ </div>
289
+
290
+ <div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
291
+ <div>
292
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
293
+ <p class="font-medium">{board.lat.toFixed(6)}, {board.lon.toFixed(6)}</p>
294
+ </div>
295
+ <div>
296
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
297
+ <p class="font-medium">{board.alt.toFixed(1)} m</p>
298
+ </div>
299
+ <div>
300
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
301
+ <p class="font-medium">{board.heading.toFixed(1)}°</p>
302
+ </div>
303
+ <div>
304
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
305
+ <p class="font-medium">{board.sat}</p>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="flex items-center justify-between border-t border-(--hairline-color) pt-1">
310
+ <div class="relative flex items-center">
311
+ <button
312
+ 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"
313
+ onclick={(e) => {
314
+ e.preventDefault()
315
+ copyBoard(board)
316
+ }}
317
+ aria-label="Copy"
318
+ title="Copy"
319
+ >
320
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24">
321
+ <g fill="none" stroke="currentColor" stroke-width="1.5">
322
+ <path
323
+ 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"
324
+ />
325
+ <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" />
326
+ </g>
327
+ </svg>
328
+ </button>
329
+ <CopiedChip show={isCopied} class="-top-1 right-0 -translate-y-full" />
330
+ </div>
331
+
332
+ <button
333
+ class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
334
+ aria-label="Hide"
335
+ title="Hide"
336
+ onclick={() => hideBoard(board.group)}
337
+ >
338
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24"
339
+ ><path
340
+ fill="none"
341
+ stroke="currentColor"
342
+ stroke-linecap="round"
343
+ stroke-linejoin="round"
344
+ stroke-width="1.5"
345
+ 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"
346
+ /></svg
347
+ >
348
+ </button>
349
+ </div>
350
+ </div>
351
+ </Popup>
352
+ </Marker>
353
+ {/each}
354
+ </MapLibre>
355
+ </div>
356
+ {/if}
357
+ </div>
358
+
359
+ <style>
360
+ /* Попап по правилам карточки (design-handoff, раздел 9): радиус 14, линия, тень уровня 2 */
361
+ :global(.map-popup .maplibregl-popup-content) {
362
+ background-color: var(--back-color);
363
+ color: inherit;
364
+ border: 1px solid var(--hairline-color);
365
+ border-radius: 14px;
366
+ box-shadow: var(--elevation-2);
367
+ padding: 12px;
368
+ }
369
+ :global(.map-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) {
370
+ border-top-color: var(--back-color);
371
+ }
372
+ :global(.map-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip) {
373
+ border-bottom-color: var(--back-color);
374
+ }
375
+ :global(.map-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip) {
376
+ border-right-color: var(--back-color);
377
+ }
378
+ :global(.map-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip) {
379
+ border-left-color: var(--back-color);
380
+ }
381
+ </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;
@@ -0,0 +1,78 @@
1
+ <!-- $lib/WidgetMap/WidgetMapProps.svelte — панель пропсов конструктора для WidgetMap: заголовок, группы полей
2
+ (каждая — борт), мастер-цвет, высота карты, иконка маркера и начальные значения переключателей оператора. -->
3
+ <script lang="ts">
4
+ import { T } from "../locales/i18n"
5
+ import * as UI from ".."
6
+ import { updateProperty, type UIComponent, type IWidgetMapProps } from "../types"
7
+ import { ICONS } from "../icons"
8
+ import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
9
+ import CommonSnippets from "../CommonSnippets.svelte"
10
+ import PropsGroup from "../PropsGroup.svelte"
11
+
12
+ const { component, onPropertyChange } = $props<{
13
+ component: UIComponent & { properties: Partial<IWidgetMapProps> }
14
+ onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string }>) => void
15
+ }>()
16
+
17
+ const HEIGHT_OPTIONS = [250, 300, 400, 500, 600, 800].map((value) => ({ id: crypto.randomUUID(), value, name: `${value} px` }))
18
+ const TRACK_LENGTH_OPTIONS = [100, 500, 1000, 5000, 10000, 50000].map((value) => ({ id: crypto.randomUUID(), value, name: String(value) }))
19
+
20
+ /* Группы вводятся через запятую: "MAV, GNSS" */
21
+ const setGroups = (raw: unknown) => {
22
+ const list = String(raw ?? "")
23
+ .split(",")
24
+ .map((g) => g.trim())
25
+ .filter(Boolean)
26
+ updateProperty("groups", list.length ? list : ["MAV"], component, onPropertyChange)
27
+ }
28
+ </script>
29
+
30
+ <div class="flex flex-col gap-2">
31
+ <PropsGroup label={$T("constructor.props.group.general")}>
32
+ <UI.Input
33
+ label={{ name: "Заголовок" }}
34
+ value={component.properties.label?.name}
35
+ onUpdate={(value) => updateProperty("label.name", value as string, component, onPropertyChange)}
36
+ />
37
+ <UI.Input label={{ name: "Группы полей (через запятую)" }} value={(component.properties.groups ?? ["MAV"]).join(", ")} onUpdate={setGroups} />
38
+ <WidgetAccentPicker
39
+ value={component.properties.componentClass ?? ""}
40
+ onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
41
+ />
42
+ </PropsGroup>
43
+ <PropsGroup label={$T("constructor.props.group.appearance")}>
44
+ <UI.Select
45
+ label={{ name: "Высота карты" }}
46
+ type="buttons"
47
+ options={HEIGHT_OPTIONS}
48
+ value={HEIGHT_OPTIONS.find((opt) => opt.value === (component.properties.height ?? 400))}
49
+ onUpdate={(option) => updateProperty("height", (option as { value: number }).value, component, onPropertyChange)}
50
+ />
51
+ <CommonSnippets
52
+ snippet="IconsLib"
53
+ initialValue={{
54
+ name: $T("constructor.props.markerIcon"),
55
+ icon: component.properties.markerIcon,
56
+ updateProperty: (icon: string) => updateProperty("markerIcon", icon as string, component, onPropertyChange),
57
+ icons: { array: ICONS },
58
+ }}
59
+ {component}
60
+ {onPropertyChange}
61
+ />
62
+ </PropsGroup>
63
+ <PropsGroup label="По умолчанию (оператор меняет на карте)">
64
+ <UI.Switch
65
+ label={{ name: $T("constructor.props.map.track") }}
66
+ value={component.properties.trackEnabled ? 1 : 0}
67
+ options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
68
+ onChange={(value) => updateProperty("trackEnabled", Boolean(value), component, onPropertyChange)}
69
+ />
70
+ <UI.Select
71
+ label={{ name: $T("constructor.props.map.trackLength") }}
72
+ type="input"
73
+ options={TRACK_LENGTH_OPTIONS}
74
+ value={TRACK_LENGTH_OPTIONS.find((opt) => opt.value === (component.properties.trackLength ?? 1000))}
75
+ onUpdate={(option) => updateProperty("trackLength", (option as { value: number }).value, component, onPropertyChange)}
76
+ />
77
+ </PropsGroup>
78
+ </div>
@@ -0,0 +1,14 @@
1
+ import { type UIComponent, type IWidgetMapProps } from "../types";
2
+ type $$ComponentProps = {
3
+ component: UIComponent & {
4
+ properties: Partial<IWidgetMapProps>;
5
+ };
6
+ onPropertyChange: (updates: Partial<{
7
+ properties?: string | object;
8
+ name?: string;
9
+ access?: string;
10
+ }>) => void;
11
+ };
12
+ declare const WidgetMapProps: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type WidgetMapProps = ReturnType<typeof WidgetMapProps>;
14
+ export default WidgetMapProps;
package/dist/index.d.ts CHANGED
@@ -17,8 +17,6 @@ export { default as InputProps } from "./Input/InputProps.svelte";
17
17
  export { default as Joystick } from "./Joystick/Joystick.svelte";
18
18
  export { default as JoystickProps } from "./Joystick/JoystickProps.svelte";
19
19
  export { default as Modal } from "./Modal.svelte";
20
- export { default as Map } from "./Map/Map.svelte";
21
- export { default as MapProps } from "./Map/MapProps.svelte";
22
20
  export { default as PeriodChart } from "./PeriodChart/PeriodChart.svelte";
23
21
  export { default as PeriodChartProps } from "./PeriodChart/PeriodChartProps.svelte";
24
22
  export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte";
@@ -51,7 +49,9 @@ export { default as WidgetRGBStrip } from "./WidgetRGBStrip/WidgetRGBStrip.svelt
51
49
  export { default as WidgetRGBStripProps } from "./WidgetRGBStrip/WidgetRGBStripProps.svelte";
52
50
  export { default as WidgetPFD } from "./WidgetPFD/WidgetPFD.svelte";
53
51
  export { default as WidgetPFDProps } from "./WidgetPFD/WidgetPFDProps.svelte";
52
+ export { default as WidgetMap } from "./WidgetMap/WidgetMap.svelte";
53
+ export { default as WidgetMapProps } from "./WidgetMap/WidgetMapProps.svelte";
54
54
  export * from "./locales/i18n";
55
55
  export * from "./locales/translations";
56
56
  export * from "./ModalStackStore";
57
- export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, type IWidgetRGBStripProps, type IWidgetRGBStripConfig, type IWidgetPFDProps, type IWidgetPFDData, type IWidgetPFDLimits, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
57
+ export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, type IWidgetRGBStripProps, type IWidgetRGBStripConfig, type IWidgetPFDProps, type IWidgetPFDData, type IWidgetPFDLimits, type IWidgetMapProps, type IWidgetMapData, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
package/dist/index.js CHANGED
@@ -18,8 +18,6 @@ export { default as InputProps } from "./Input/InputProps.svelte";
18
18
  export { default as Joystick } from "./Joystick/Joystick.svelte";
19
19
  export { default as JoystickProps } from "./Joystick/JoystickProps.svelte";
20
20
  export { default as Modal } from "./Modal.svelte";
21
- export { default as Map } from "./Map/Map.svelte";
22
- export { default as MapProps } from "./Map/MapProps.svelte";
23
21
  export { default as PeriodChart } from "./PeriodChart/PeriodChart.svelte";
24
22
  export { default as PeriodChartProps } from "./PeriodChart/PeriodChartProps.svelte";
25
23
  export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte";
@@ -52,6 +50,8 @@ export { default as WidgetRGBStrip } from "./WidgetRGBStrip/WidgetRGBStrip.svelt
52
50
  export { default as WidgetRGBStripProps } from "./WidgetRGBStrip/WidgetRGBStripProps.svelte";
53
51
  export { default as WidgetPFD } from "./WidgetPFD/WidgetPFD.svelte";
54
52
  export { default as WidgetPFDProps } from "./WidgetPFD/WidgetPFDProps.svelte";
53
+ export { default as WidgetMap } from "./WidgetMap/WidgetMap.svelte";
54
+ export { default as WidgetMapProps } from "./WidgetMap/WidgetMapProps.svelte";
55
55
  export * from "./locales/i18n";
56
56
  export * from "./locales/translations";
57
57
  export * from "./ModalStackStore";