poe-svelte-ui-lib 1.11.2 → 1.11.3

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.
@@ -10,7 +10,8 @@
10
10
 
11
11
  interface MapDevice extends IDeviceGNSS {
12
12
  isFresh: boolean
13
- timeoutId: number | null
13
+ staleTimeoutId: number | null
14
+ removeTimeoutId: number | null
14
15
  }
15
16
 
16
17
  let devices: MapDevice[] = $state([])
@@ -18,21 +19,29 @@
18
19
  let isDarkMode = $state(false)
19
20
  let markerTimeout = $state(30_000)
20
21
 
21
- const restartFreshTimer = (index: number) => {
22
- const device = devices[index]
23
- // Очистить старый таймер, если есть
24
- if (device.timeoutId !== null) {
25
- clearTimeout(device.timeoutId)
26
- }
27
- // Запустить новый
28
- const id = setTimeout(() => {
29
- if (index < devices.length && devices[index].DevSN === device.DevSN) {
30
- devices[index].isFresh = false
31
- devices[index].timeoutId = null
32
- }
22
+ const clearDeviceTimers = (device: MapDevice) => {
23
+ if (device.staleTimeoutId !== null) clearTimeout(device.staleTimeoutId)
24
+ if (device.removeTimeoutId !== null) clearTimeout(device.removeTimeoutId)
25
+ }
26
+
27
+ // Перезапускает цикл "устройство свежее" -> (без обновлений markerTimeout) "устарело" -> (без обновлений ещё markerTimeout) "удалено"
28
+ const restartFreshTimer = (devSN: string) => {
29
+ const idx = devices.findIndex((d) => d.DevSN === devSN)
30
+ if (idx === -1) return
31
+
32
+ clearDeviceTimers(devices[idx])
33
+ devices[idx].isFresh = true
34
+
35
+ devices[idx].staleTimeoutId = setTimeout(() => {
36
+ const staleIdx = devices.findIndex((d) => d.DevSN === devSN)
37
+ if (staleIdx === -1) return
38
+
39
+ devices[staleIdx].isFresh = false
40
+ devices[staleIdx].staleTimeoutId = null
41
+ devices[staleIdx].removeTimeoutId = setTimeout(() => {
42
+ devices = devices.filter((d) => d.DevSN !== devSN)
43
+ }, markerTimeout) as unknown as number
33
44
  }, markerTimeout) as unknown as number
34
- devices[index].timeoutId = id
35
- devices[index].isFresh = true
36
45
  }
37
46
 
38
47
  // Обработка входящих данных
@@ -42,17 +51,17 @@
42
51
  if (idx !== -1) {
43
52
  /* Обновление существующего */
44
53
  devices[idx] = { ...devices[idx], ...data }
45
- restartFreshTimer(idx)
46
54
  } else {
47
55
  /* Новое устройство */
48
56
  const newDevice: MapDevice = {
49
57
  ...data,
50
58
  isFresh: true,
51
- timeoutId: null,
59
+ staleTimeoutId: null,
60
+ removeTimeoutId: null,
52
61
  }
53
62
  devices.push(newDevice)
54
- restartFreshTimer(devices.length - 1)
55
63
  }
64
+ restartFreshTimer(data.DevSN)
56
65
  data = null
57
66
  }
58
67
  })
@@ -69,12 +78,10 @@
69
78
  })
70
79
 
71
80
  onDestroy(() => {
72
- if (typeof window !== "undefined") window.addEventListener("ThemeChange", handleThemeChange as EventListener)
81
+ if (typeof window !== "undefined") window.removeEventListener("ThemeChange", handleThemeChange as EventListener)
73
82
 
74
83
  for (const device of devices) {
75
- if (device.timeoutId !== null) {
76
- clearTimeout(device.timeoutId)
77
- }
84
+ clearDeviceTimers(device)
78
85
  }
79
86
  })
80
87
 
@@ -90,7 +97,7 @@
90
97
 
91
98
  const changeTimeout = (val: number) => {
92
99
  markerTimeout = val
93
- devices.forEach((_, idx) => restartFreshTimer(idx))
100
+ devices.forEach((device) => restartFreshTimer(device.DevSN))
94
101
  }
95
102
  </script>
96
103
 
@@ -159,56 +166,88 @@
159
166
  <p class="font-bold">{device.DevName}</p>
160
167
  </div>
161
168
  {/snippet}
162
- <Popup closeButton={false} class="map-popup rounded-2xl text-left w-[26rem]">
163
- <p>DevSN: {device.DevSN}</p>
164
- <p>DevName: {device.DevName}</p>
165
- <p>Lat: {`${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt.toFixed(1)}`}</p>
166
- <p>Heading: {device.NavHeading.toFixed(1)} | Sat Use: {device.NavSatUse}</p>
167
-
168
- <div class="relative flex justify-between">
169
- <button
170
- class="absolute right-0 flex cursor-pointer border-none bg-transparent"
171
- onclick={(e) => {
172
- e.preventDefault()
173
- navigator.clipboard.writeText(
174
- `DevName: ${device.DevName}\nDevSN: ${device.DevSN}\nLat: ${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt}\nHeading: ${device.NavHeading} | Sat Use: ${device.NavSatUse}`,
175
- )
176
- isCopied = true
177
- setTimeout(() => (isCopied = false), 1000)
178
- }}
179
- aria-label="Copy"
180
- >
181
- <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24">
182
- <g fill="none" stroke="currentColor" stroke-width="1.5">
183
- <path
184
- 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"
185
- />
186
- <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" />
187
- </g>
188
- </svg>
189
- </button>
190
- {#if isCopied}
191
- <div
192
- class="absolute top-1/2 right-0 -translate-y-1/2 transform rounded-md bg-(--green-color) px-1.5 py-1 text-sm shadow-lg"
193
- transition:fade={{ duration: 200 }}
194
- >
195
- ✓
169
+
170
+ <Popup closeButton={false} class="map-popup rounded-2xl text-left">
171
+ <div class="flex flex-col gap-1.5">
172
+ <div class="flex items-center gap-1.5">
173
+ <span class="size-2 shrink-0 rounded-full {device.isFresh ? 'bg-green-500' : 'bg-red-500'}"></span>
174
+ <h6 class="text-sm leading-none font-bold">{device.DevName}</h6>
175
+ </div>
176
+
177
+ <div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
178
+ <div>
179
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
180
+ <p class="font-medium">{device.NavLat.toFixed(5)}, {device.NavLon.toFixed(5)}</p>
181
+ </div>
182
+ <div>
183
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
184
+ <p class="font-medium">{device.NavAlt.toFixed(1)} m</p>
185
+ </div>
186
+ <div>
187
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
188
+ <p class="font-medium">{device.NavHeading.toFixed(1)}°</p>
189
+ </div>
190
+ <div>
191
+ <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
192
+ <p class="font-medium">{device.NavSatUse}</p>
193
+ </div>
194
+ </div>
195
+
196
+ <p class="text-[0.6rem] font-mono text-center tracking-wide uppercase">{device.DevSN}</p>
197
+
198
+ <div class="flex items-center justify-between border-t border-(--border-color) pt-1">
199
+ <div class="relative flex items-center">
200
+ <button
201
+ 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"
202
+ onclick={(e) => {
203
+ e.preventDefault()
204
+ navigator.clipboard.writeText(
205
+ `DevName: ${device.DevName}\nDevSN: ${device.DevSN}\nLat: ${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt}\nHeading: ${device.NavHeading} | Satellites: ${device.NavSatUse}`,
206
+ )
207
+ isCopied = true
208
+ setTimeout(() => (isCopied = false), 1000)
209
+ }}
210
+ aria-label="Copy"
211
+ title="Copy"
212
+ >
213
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24">
214
+ <g fill="none" stroke="currentColor" stroke-width="1.5">
215
+ <path
216
+ 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"
217
+ />
218
+ <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" />
219
+ </g>
220
+ </svg>
221
+ </button>
222
+ {#if isCopied}
223
+ <div
224
+ class="absolute -top-1 right-0 -translate-y-full rounded-md bg-(--green-color) px-1.5 py-1 text-sm shadow-lg"
225
+ transition:fade={{ duration: 200 }}
226
+ >
227
+ ✓
228
+ </div>
229
+ {/if}
196
230
  </div>
197
- {/if}
198
-
199
- <button class="size-6 cursor-pointer" aria-label="Delete" onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}>
200
- <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24"
201
- ><path
202
- fill="none"
203
- stroke="currentColor"
204
- stroke-linecap="round"
205
- stroke-linejoin="round"
206
- stroke-width="1.5"
207
- 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"
208
- color="currentColor"
209
- /></svg
231
+
232
+ <button
233
+ class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-red-500 transition hover:text-red-600"
234
+ aria-label="Delete"
235
+ title="Delete"
236
+ onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}
210
237
  >
211
- </button>
238
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24"
239
+ ><path
240
+ fill="none"
241
+ stroke="currentColor"
242
+ stroke-linecap="round"
243
+ stroke-linejoin="round"
244
+ stroke-width="1.5"
245
+ 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"
246
+ color="currentColor"
247
+ /></svg
248
+ >
249
+ </button>
250
+ </div>
212
251
  </div>
213
252
  </Popup>
214
253
  </Marker>
@@ -5,10 +5,11 @@
5
5
  interface Props {
6
6
  closeButton?: boolean
7
7
  class?: string
8
+ maxWidth?: string
8
9
  children?: Snippet
9
10
  }
10
11
 
11
- let { closeButton = true, class: className = undefined, children }: Props = $props()
12
+ let { closeButton = true, class: className = undefined, maxWidth = "none", children }: Props = $props()
12
13
 
13
14
  let container = $state<HTMLElement | null>(null)
14
15
 
@@ -24,6 +25,7 @@
24
25
 
25
26
  const options: any = {
26
27
  closeButton,
28
+ maxWidth,
27
29
  }
28
30
 
29
31
  if (className) {
@@ -2,6 +2,7 @@ import { type Snippet } from "svelte";
2
2
  interface Props {
3
3
  closeButton?: boolean;
4
4
  class?: string;
5
+ maxWidth?: string;
5
6
  children?: Snippet;
6
7
  }
7
8
  declare const Popup: import("svelte").Component<Props, {}, "">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.11.2",
3
+ "version": "1.11.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -56,4 +56,4 @@
56
56
  "vite": "^8.1.0",
57
57
  "vite-plugin-compression": "^0.5.1"
58
58
  }
59
- }
59
+ }