poe-svelte-ui-lib 1.2.26 → 1.2.27

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.
@@ -179,13 +179,6 @@
179
179
  value={$optionsStore.INFO_SIDE_OPTIONS.find((h) => h.value === component.properties.content.info.side)}
180
180
  onUpdate={(option) => updateProperty('content.info.side', option.value as string, component, onPropertyChange)}
181
181
  />
182
- <UI.Input
183
- label={{ name: $t('constructor.props.svgicon') }}
184
- type="text-area"
185
- maxlength={100000}
186
- value={component.properties.content.icon}
187
- onUpdate={(value) => updateProperty('content.icon', value as string, component, onPropertyChange)}
188
- />
189
182
  </div>
190
183
  <div class="flex w-1/3 flex-col px-2">
191
184
  <UI.Input
@@ -193,14 +186,6 @@
193
186
  value={component.properties.componentClass}
194
187
  onUpdate={(value) => updateProperty('componentClass', value as string, component, onPropertyChange)}
195
188
  />
196
- <UI.Select
197
- label={{ name: $t('constructor.props.height') }}
198
- type="buttons"
199
- options={$optionsStore.HEIGHT_OPTIONS}
200
- value={initialHeight}
201
- onUpdate={(option) =>
202
- updateProperty('componentClass', twMerge(component.properties.componentClass, option.value), component, onPropertyChange)}
203
- />
204
189
  <UI.Select
205
190
  wrapperClass="h-14"
206
191
  label={{ name: $t('constructor.props.colors') }}
@@ -210,6 +195,14 @@
210
195
  onUpdate={(option) =>
211
196
  updateProperty('componentClass', twMerge(component.properties.componentClass, option.value), component, onPropertyChange)}
212
197
  />
198
+
199
+ <UI.Input
200
+ label={{ name: $t('constructor.props.svgicon') }}
201
+ type="text-area"
202
+ maxlength={100000}
203
+ value={component.properties.content.icon}
204
+ onUpdate={(value) => updateProperty('content.icon', value as string, component, onPropertyChange)}
205
+ />
213
206
  </div>
214
207
  </div>
215
208
  {/if}
@@ -31,6 +31,8 @@
31
31
  value={component.properties.wrapperClass}
32
32
  onUpdate={(value) => updateProperty('wrapperClass', value as string, component, onPropertyChange)}
33
33
  />
34
+ </div>
35
+ <div class="flex w-1/3 flex-col px-2">
34
36
  <UI.Input
35
37
  label={{ name: $t('constructor.props.label') }}
36
38
  value={component.properties.label.name}
@@ -128,7 +128,10 @@
128
128
  maxlength={150}
129
129
  help={{ info: $t('constructor.props.regexp.info') }}
130
130
  componentClass={isValidRegExp === false ? '!border-2 !border-red-400' : ''}
131
- onUpdate={(value) => updateProperty('help.regExp', value)}
131
+ onUpdate={(value) => {
132
+ console.log(value)
133
+ updateProperty('help.regExp', value as string)
134
+ }}
132
135
  />
133
136
  {:else if component.properties.type === 'number' && !component.properties.readonly && !component.properties.disabled}
134
137
  <UI.Input
@@ -170,7 +173,10 @@
170
173
  label={{ name: $t('constructor.props.readonly') }}
171
174
  value={component.properties.readonly}
172
175
  options={[{ id: crypto.randomUUID(), value: 0, class: '' }]}
173
- onChange={(value) => updateProperty('readonly', value)}
176
+ onChange={(value) => {
177
+ updateProperty('readonly', value)
178
+ console.log(component.properties)
179
+ }}
174
180
  />
175
181
  <UI.Switch
176
182
  label={{ name: $t('constructor.props.copy') }}
@@ -4,7 +4,6 @@
4
4
 
5
5
  let {
6
6
  id = crypto.randomUUID(),
7
-
8
7
  wrapperClass = '',
9
8
  label = { name: '', class: '' },
10
9
  value = $bindable([0, 0, 0]),
@@ -0,0 +1,205 @@
1
+ <!-- $lib/ElementsUI/Map.svelte -->
2
+ <script lang="ts">
3
+ import { t } from '../locales/i18n'
4
+ import type { IDeviceGNSS, IMapProps } from '../types'
5
+ import { onDestroy, onMount } from 'svelte'
6
+ import { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, CustomControl } from 'svelte-maplibre-gl'
7
+ import { fade } from 'svelte/transition'
8
+ import { twMerge } from 'tailwind-merge'
9
+
10
+ let { id = crypto.randomUUID(), label = { name: '', class: '' }, data = $bindable(), markerIcon }: IMapProps = $props()
11
+
12
+ interface MapDevice extends IDeviceGNSS {
13
+ isFresh: boolean
14
+ timeoutId: number | null
15
+ }
16
+
17
+ let devices: MapDevice[] = $state([])
18
+ let isCopied = $state(false)
19
+ let isDarkMode = $state(false)
20
+ let markerTimeout = $state(30_000)
21
+
22
+ const restartFreshTimer = (index: number) => {
23
+ const device = devices[index]
24
+ // Очистить старый таймер, если есть
25
+ if (device.timeoutId !== null) {
26
+ clearTimeout(device.timeoutId)
27
+ }
28
+ // Запустить новый
29
+ const id = setTimeout(() => {
30
+ if (index < devices.length && devices[index].DevSN === device.DevSN) {
31
+ devices[index].isFresh = false
32
+ devices[index].timeoutId = null
33
+ }
34
+ }, markerTimeout) as unknown as number
35
+ devices[index].timeoutId = id
36
+ devices[index].isFresh = true
37
+ }
38
+
39
+ // Обработка входящих данных
40
+ $effect(() => {
41
+ if (data) {
42
+ const idx = devices.findIndex((d) => d.DevSN === data?.DevSN)
43
+ if (idx !== -1) {
44
+ // Обновление существующего
45
+ devices[idx] = { ...devices[idx], ...data }
46
+ restartFreshTimer(idx)
47
+ } else {
48
+ // Новое устройство
49
+ const newDevice: MapDevice = {
50
+ ...data,
51
+ isFresh: true,
52
+ timeoutId: null,
53
+ }
54
+ devices.push(newDevice)
55
+ restartFreshTimer(devices.length - 1)
56
+ }
57
+ data = null
58
+ }
59
+ })
60
+
61
+ const handleThemeChange = (event: CustomEvent) => {
62
+ isDarkMode = !event.detail.currentTheme
63
+ }
64
+
65
+ onMount(() => {
66
+ if (typeof window !== 'undefined') {
67
+ isDarkMode = localStorage.getItem('AppTheme') !== 'light'
68
+ window.addEventListener('ThemeChange', handleThemeChange as EventListener)
69
+ }
70
+ })
71
+
72
+ onDestroy(() => {
73
+ if (typeof window !== 'undefined') window.addEventListener('ThemeChange', handleThemeChange as EventListener)
74
+
75
+ for (const device of devices) {
76
+ if (device.timeoutId !== null) {
77
+ clearTimeout(device.timeoutId)
78
+ }
79
+ }
80
+ })
81
+
82
+ const timeoutOptions: { label: string; value: number }[] = [
83
+ { label: '30 sec', value: 30_000 },
84
+ { label: '1 min', value: 60_000 },
85
+ { label: '3 min', value: 180_000 },
86
+ { label: '5 min', value: 300_000 },
87
+ { label: '10 min', value: 600_000 },
88
+ { label: '30 min', value: 1_800_000 },
89
+ { label: '1 h', value: 3_600_000 },
90
+ ]
91
+
92
+ const changeTimeout = (val: number) => {
93
+ markerTimeout = val
94
+ // перезапускаем таймеры для всех устройств
95
+ devices.forEach((_, idx) => restartFreshTimer(idx))
96
+ }
97
+ </script>
98
+
99
+ <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class="h-full min-h-[200px]">
100
+ {#if label.name}
101
+ <h5 class={twMerge(` w-full px-4 text-center`, label.class)}>{label.name}</h5>
102
+ {/if}
103
+ <MapLibre
104
+ class="h-[calc(100%-2rem)] min-h-[200px]"
105
+ style={isDarkMode
106
+ ? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'
107
+ : 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json'}
108
+ zoom={1.5}
109
+ center={{ lat: 30, lng: 0 }}
110
+ >
111
+ <NavigationControl />
112
+ <ScaleControl />
113
+ <GeolocateControl />
114
+ <FullScreenControl />
115
+
116
+ <CustomControl position="top-left">
117
+ <div class="flex items-center gap-2 px-2 py-1 text-black">
118
+ <label for="timeout" class="text-sm font-medium">{$t('constructor.props.map.timeout')}</label>
119
+ <select
120
+ id="timeout"
121
+ class="rounded px-2 py-1 text-sm"
122
+ bind:value={markerTimeout}
123
+ onchange={(e) => changeTimeout(parseInt((e.target as HTMLSelectElement).value))}
124
+ >
125
+ {#each timeoutOptions as opt}
126
+ <option value={opt.value}>{opt.label}</option>
127
+ {/each}
128
+ </select>
129
+ </div>
130
+ </CustomControl>
131
+
132
+ {#each devices as device}
133
+ <Marker lnglat={{ lng: device.NavLon, lat: device.NavLat }}>
134
+ {#snippet content()}
135
+ <div class="flex flex-col items-center justify-center leading-none">
136
+ <div
137
+ class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
138
+ {device.isFresh ? 'text-green-500' : 'text-red-500'}"
139
+ style="rotate: {device.NavHeading - 90}deg;"
140
+ >
141
+ {@html markerIcon ||
142
+ '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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"/></svg>'}
143
+ </div>
144
+ <p class="font-bold">{device.DevName}</p>
145
+ </div>
146
+ {/snippet}
147
+ <Popup closeButton={false} class="rounded-2xl text-left">
148
+ <p>DevSN: {device.DevSN}</p>
149
+ <p>Lat: {`${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt}`}</p>
150
+ <p>Heading: {device.NavHeading} | Sat Use: {device.NavSatUse}</p>
151
+
152
+ <div class="relative flex justify-between">
153
+ <button
154
+ class="absolute right-0 flex cursor-pointer border-none bg-transparent"
155
+ onclick={(e) => {
156
+ e.preventDefault()
157
+ navigator.clipboard.writeText(
158
+ `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}`,
159
+ )
160
+ isCopied = true
161
+ setTimeout(() => (isCopied = false), 1000)
162
+ }}
163
+ aria-label="Копировать текст"
164
+ >
165
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24">
166
+ <g fill="none" stroke="currentColor" stroke-width="1.5">
167
+ <path
168
+ 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"
169
+ />
170
+ <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" />
171
+ </g>
172
+ </svg>
173
+ </button>
174
+ {#if isCopied}
175
+ <div
176
+ class="absolute top-1/2 right-0 -translate-y-1/2 transform rounded-md bg-(--green-color) px-2 py-1 text-sm shadow-lg"
177
+ transition:fade={{ duration: 200 }}
178
+ >
179
+
180
+ </div>
181
+ {/if}
182
+
183
+ <button
184
+ class="size-6 cursor-pointer"
185
+ aria-label="Удалить"
186
+ onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}
187
+ >
188
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24"
189
+ ><path
190
+ fill="none"
191
+ stroke="currentColor"
192
+ stroke-linecap="round"
193
+ stroke-linejoin="round"
194
+ stroke-width="1.5"
195
+ 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"
196
+ color="currentColor"
197
+ /></svg
198
+ >
199
+ </button>
200
+ </div>
201
+ </Popup>
202
+ </Marker>
203
+ {/each}
204
+ </MapLibre>
205
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { IMapProps } from '../types';
2
+ declare const Map: import("svelte").Component<IMapProps, {}, "data">;
3
+ type Map = ReturnType<typeof Map>;
4
+ export default Map;
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import { t } from '../locales/i18n'
4
+ import { type UIComponent, type IGraphProps, updateProperty } from '../types'
5
+ import * as UI from '..'
6
+ import Modal from '../Modal.svelte'
7
+ import { ICONS } from '../icons'
8
+ import Button from '../Button/Button.svelte'
9
+ import CrossIcon from '../libIcons/CrossIcon.svelte'
10
+
11
+ const {
12
+ component,
13
+ onPropertyChange,
14
+ forConstructor = true,
15
+ } = $props<{
16
+ component: UIComponent & { properties: Partial<IGraphProps> }
17
+ onPropertyChange: (value?: string | object, name?: string, access?: string) => void
18
+ forConstructor?: boolean
19
+ }>()
20
+
21
+ let showIconLib = $state(false)
22
+
23
+ const DeviceVariables = getContext<{ id: string; value: string; name: string }[]>('DeviceVariables')
24
+ let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
25
+ </script>
26
+
27
+ {#if forConstructor}
28
+ <div class="relative flex flex-row items-start justify-center">
29
+ <!-- Сообщение для отправки в ws по нажатию кнопки -->
30
+ <div class="flex w-1/3 flex-col items-center px-2">
31
+ <UI.Select
32
+ label={{ name: $t('constructor.props.variable') }}
33
+ options={VARIABLE_OPTIONS}
34
+ value={VARIABLE_OPTIONS.find((opt) => opt.value === component.properties.id)}
35
+ onUpdate={(value) => {
36
+ updateProperty('id', value.value as string, component, onPropertyChange)
37
+ updateProperty('eventHandler.Variables', value.value as string, component, onPropertyChange)
38
+ onPropertyChange(null, value.name?.split('—')[1].trim(), null)
39
+ }}
40
+ />
41
+ </div>
42
+ <div class="flex w-1/3 flex-col px-2">
43
+ <UI.Input
44
+ label={{ name: $t('constructor.props.label') }}
45
+ value={component.properties.label.name}
46
+ onUpdate={(value) => updateProperty('label.name', value as string, component, onPropertyChange)}
47
+ />
48
+ <UI.Input
49
+ label={{ name: $t('constructor.props.label.class') }}
50
+ value={component.properties.label.class}
51
+ onUpdate={(value) => updateProperty('label.class', value as string, component, onPropertyChange)}
52
+ />
53
+ </div>
54
+ <div class="flex w-1/3 flex-col px-2">
55
+ <div class="mt-6 flex gap-2">
56
+ <UI.Button content={{ name: $t('constructor.props.markerIcon') }} onClick={() => (showIconLib = true)} />
57
+ {#if showIconLib}
58
+ <Modal bind:isOpen={showIconLib} wrapperClass="w-130">
59
+ {#snippet main()}
60
+ <div class="grid grid-cols-3">
61
+ {#each ICONS as category}
62
+ <div class="relative m-1.5 rounded-xl border-2 border-(--border-color) p-3">
63
+ <div class="absolute -top-3.5 bg-(--back-color) px-1">{$t(`constructor.props.icon.${category[0]}`)}</div>
64
+ <div class="grid grid-cols-3 place-items-center gap-2">
65
+ {#each category[1] as icon}
66
+ <button
67
+ class="h-8 w-8 cursor-pointer [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full"
68
+ onclick={() => {
69
+ updateProperty('markerIcon', icon as string, component, onPropertyChange)
70
+ }}
71
+ >
72
+ {@html icon}
73
+ </button>{/each}
74
+ </div>
75
+ </div>
76
+ {/each}
77
+ </div>
78
+ {/snippet}
79
+ </Modal>
80
+ {/if}
81
+ {#if component.properties.markerIcon}
82
+ <Button
83
+ wrapperClass="w-8.5 "
84
+ componentClass="p-0.5 bg-red"
85
+ content={{ icon: CrossIcon }}
86
+ onClick={() => {
87
+ updateProperty('markerIcon', '', component, onPropertyChange)
88
+ }}
89
+ />
90
+ {/if}
91
+ </div>
92
+ </div>
93
+ </div>
94
+ {:else}
95
+ <div class="relative mb-2 flex flex-row items-start justify-center">
96
+ <!-- Сообщение для отправки в ws по нажатию кнопки -->
97
+ <div class="flex w-1/3 flex-col items-center px-2">
98
+ <UI.Input
99
+ label={{ name: $t('constructor.props.id') }}
100
+ value={component.properties.id}
101
+ onUpdate={(value) => updateProperty('id', value as string, component, onPropertyChange)}
102
+ />
103
+ </div>
104
+ <div class="flex w-1/3 flex-col px-2">
105
+ <UI.Input
106
+ label={{ name: $t('constructor.props.label') }}
107
+ value={component.properties.label.name}
108
+ onUpdate={(value) => updateProperty('label.name', value as string, component, onPropertyChange)}
109
+ />
110
+ <UI.Input
111
+ label={{ name: $t('constructor.props.label.class') }}
112
+ value={component.properties.label.class}
113
+ onUpdate={(value) => updateProperty('label.class', value as string, component, onPropertyChange)}
114
+ />
115
+ </div>
116
+
117
+ <div class="flex w-1/3 flex-col px-2">
118
+ <div class="mt-6 flex gap-2">
119
+ <UI.Button content={{ name: $t('constructor.props.markerIcon') }} onClick={() => (showIconLib = true)} />
120
+ {#if showIconLib}
121
+ <Modal bind:isOpen={showIconLib} wrapperClass="w-130">
122
+ {#snippet main()}
123
+ <div class="grid grid-cols-3">
124
+ {#each ICONS as category}
125
+ <div class="relative m-1.5 rounded-xl border-2 border-(--border-color) p-3">
126
+ <div class="absolute -top-3.5 bg-(--back-color) px-1">{$t(`constructor.props.icon.${category[0]}`)}</div>
127
+ <div class="grid grid-cols-3 place-items-center gap-2">
128
+ {#each category[1] as icon}
129
+ <button
130
+ class="h-8 w-8 cursor-pointer [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full"
131
+ onclick={() => {
132
+ updateProperty('markerIcon', icon as string, component, onPropertyChange)
133
+ }}
134
+ >
135
+ {@html icon}
136
+ </button>{/each}
137
+ </div>
138
+ </div>
139
+ {/each}
140
+ </div>
141
+ {/snippet}
142
+ </Modal>
143
+ {/if}
144
+ {#if component.properties.markerIcon}
145
+ <Button
146
+ wrapperClass="w-8.5 "
147
+ componentClass="p-0.5 bg-red"
148
+ content={{ icon: CrossIcon }}
149
+ onClick={() => {
150
+ updateProperty('markerIcon', '', component, onPropertyChange)
151
+ }}
152
+ />
153
+ {/if}
154
+ </div>
155
+ </div>
156
+ </div>
157
+ {/if}
@@ -0,0 +1,11 @@
1
+ import { type UIComponent, type IGraphProps } from '../types';
2
+ type $$ComponentProps = {
3
+ component: UIComponent & {
4
+ properties: Partial<IGraphProps>;
5
+ };
6
+ onPropertyChange: (value?: string | object, name?: string, access?: string) => void;
7
+ forConstructor?: boolean;
8
+ };
9
+ declare const MapProps: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type MapProps = ReturnType<typeof MapProps>;
11
+ export default MapProps;
@@ -144,9 +144,12 @@
144
144
  number={{ minNum: 0, maxNum: 31, step: 1 }}
145
145
  value={[component.properties.range.start, component.properties.range.end]}
146
146
  onUpdate={(value) => {
147
- updateProperty('range.start', value[0] as [number, number], component, onPropertyChange)
148
- updateProperty('range.end', value as number[][1] as number, component, onPropertyChange)
149
- generateBitOptions(component.properties.range.start, component.properties.range.end)
147
+ if (Array.isArray(value)) {
148
+ if (value[1] - value[0] > 6) value = [value[0], value[0] + 6]
149
+ updateProperty('range.start', value[0] as number, component, onPropertyChange)
150
+ updateProperty('range.end', value[1] as number, component, onPropertyChange)
151
+ generateBitOptions(component.properties.range.start, component.properties.range.end)
152
+ }
150
153
  }}
151
154
  />
152
155
  {/if}
@@ -25,6 +25,10 @@
25
25
  let lowerValue = $derived(isRange && Array.isArray(value) ? value[0] : number.minNum)
26
26
  let upperValue = $derived(isRange && Array.isArray(value) ? value[1] : number.maxNum)
27
27
 
28
+ let activeRound: 'floor' | 'ceil' = $state('floor')
29
+
30
+ let centerNum = $derived(lowerValue + Math[activeRound]((upperValue - lowerValue) / 2 / number.step) * number.step)
31
+
28
32
  $effect(() => {
29
33
  if (value === undefined || value === null) {
30
34
  if (type === 'single' && !value) value = number.minNum
@@ -36,15 +40,15 @@
36
40
  const stepValue = direction === 'increment' ? number.step : -number.step
37
41
  if (isRange && target !== 'single') {
38
42
  if (target === 'lower') {
39
- lowerValue = Math.max(number.minNum, Math.min(lowerValue + stepValue, upperValue))
40
- lowerValue = lowerValue == upperValue ? upperValue - number.step : lowerValue
43
+ lowerValue = roundToClean(Math.max(number.minNum, Math.min(lowerValue + stepValue, upperValue)))
44
+ lowerValue = roundToClean(lowerValue == upperValue ? upperValue - number.step : lowerValue)
41
45
  } else {
42
- upperValue = Math.min(number.maxNum, Math.max(upperValue + stepValue, lowerValue))
43
- upperValue = upperValue == lowerValue ? upperValue + number.step : upperValue
46
+ upperValue = roundToClean(Math.min(number.maxNum, Math.max(upperValue + stepValue, lowerValue)))
47
+ upperValue = roundToClean(upperValue == lowerValue ? upperValue + number.step : upperValue)
44
48
  }
45
49
  onUpdate([lowerValue, upperValue])
46
50
  } else {
47
- singleValue = Math.max(number.minNum, Math.min(singleValue + stepValue, number.maxNum))
51
+ singleValue = roundToClean(Math.max(number.minNum, Math.min(singleValue + stepValue, number.maxNum)))
48
52
  onUpdate(singleValue)
49
53
  }
50
54
  }
@@ -58,79 +62,17 @@
58
62
  }
59
63
  })
60
64
 
61
- let activeThumb = $state<'lower' | 'upper'>('lower')
62
- const handleTrackClick = (e: MouseEvent) => {
63
- e.stopPropagation()
64
- const track = e.currentTarget as HTMLElement
65
- const rect = track.getBoundingClientRect()
66
- const clickPercent = ((e.clientX - rect.left) / rect.width) * 100
67
- const rawValue = number.minNum + (clickPercent / 100) * (number.maxNum - number.minNum)
68
- const clickValue = Math.round((rawValue - number.minNum) / number.step) * number.step + number.minNum
69
-
70
- if (isRange) {
71
- const lowerDiff = Math.abs(clickValue - lowerValue)
72
- const upperDiff = Math.abs(clickValue - upperValue)
73
-
74
- activeThumb = lowerDiff < upperDiff ? 'lower' : 'upper'
75
-
76
- if (activeThumb === 'lower') {
77
- lowerValue = Math.max(number.minNum, Math.min(clickValue, upperValue))
78
- lowerValue = lowerValue == upperValue ? upperValue - number.step : lowerValue
79
- } else {
80
- upperValue = Math.min(number.maxNum, Math.max(clickValue, lowerValue))
81
- upperValue = upperValue == lowerValue ? upperValue + number.step : upperValue
82
- }
83
- onUpdate([lowerValue, upperValue])
84
- } else {
85
- singleValue = Math.max(number.minNum, Math.min(clickValue, number.maxNum))
86
- onUpdate(singleValue)
87
- }
88
- }
89
-
90
- let rangeRefLower: HTMLElement | null = $state(null)
91
- let rangeRefUpper: HTMLElement | null = $state(null)
92
- let shadowWidth = $state()
93
-
94
- const updateShadowWidth = () => {
95
- let thumbCenterLower
96
- let thumbCenterUpper
65
+ const roundToClean = (num: number): number => {
66
+ if (Number.isInteger(num)) return num
97
67
 
98
- if (rangeRefLower) {
99
- const rect = rangeRefLower.getBoundingClientRect()
100
- const percent = (lowerValue - number.minNum) / (number.maxNum - number.minNum)
101
- thumbCenterLower = rect.left + rect.width * percent
102
- }
68
+ const rounded1 = Number(num.toFixed(1))
69
+ if (Math.abs(rounded1 - num) < 1e-10) return rounded1
103
70
 
104
- if (rangeRefUpper) {
105
- const rect = rangeRefUpper.getBoundingClientRect()
106
- const percent = (upperValue - number.minNum) / (number.maxNum - number.minNum)
107
- thumbCenterUpper = rect.left + rect.width * percent
108
- }
71
+ const rounded2 = Number(num.toFixed(2))
72
+ if (Math.abs(rounded2 - num) < 1e-10) return rounded2
109
73
 
110
- if (thumbCenterUpper && thumbCenterLower) {
111
- shadowWidth = (thumbCenterUpper - thumbCenterLower) / 3.5
112
- }
74
+ return rounded2
113
75
  }
114
-
115
- $effect(() => {
116
- lowerValue
117
- upperValue
118
- updateShadowWidth()
119
- })
120
-
121
- onMount(() => {
122
- if (window.visualViewport) {
123
- const handleResize = () => {
124
- updateShadowWidth()
125
- }
126
-
127
- window.visualViewport.addEventListener('resize', handleResize)
128
-
129
- onDestroy(() => {
130
- if (window.visualViewport) window.visualViewport.removeEventListener('resize', handleResize)
131
- })
132
- }
133
- })
134
76
  </script>
135
77
 
136
78
  <div class={twMerge(`bg-blue relative flex w-full flex-col items-center `, wrapperClass)}>
@@ -141,57 +83,41 @@
141
83
  <!-- Слайдер -->
142
84
  <div
143
85
  id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
144
- class="relative flex h-9 w-full justify-center rounded-full {disabled ? 'cursor-not-allowed opacity-50' : ''}"
86
+ class="relative flex h-8 w-full items-center justify-center rounded-full {disabled ? 'cursor-not-allowed opacity-50' : ''}"
145
87
  >
146
88
  {#if isRange}
147
89
  {@const userAgent = navigator.userAgent}
148
- <!-- Трек и активная зона -->
149
- <div
150
- class={`absolute z-10 h-full w-full rounded-full bg-transparent ${disabled ? '' : 'cursor-pointer'}`}
151
- role="button"
152
- tabindex={null}
153
- onkeydown={null}
154
- onclick={(e) => {
155
- disabled ? undefined : handleTrackClick(e)
156
- }}
157
- ></div>
158
-
159
- <!-- Ползунки -->
160
- <input
161
- bind:this={rangeRefLower}
162
- type="range"
163
- min={number.minNum}
164
- max={number.maxNum}
165
- step={number.step}
166
- bind:value={lowerValue}
167
- oninput={disabled
168
- ? undefined
169
- : (e) => {
170
- const newValue = Math.min(Number((e.target as HTMLInputElement).value), upperValue)
171
- lowerValue = newValue
172
- lowerValue = newValue == upperValue ? upperValue - number.step : newValue
173
- }}
174
- onmouseup={(e) => {
175
- handleTrackClick(e)
176
- disabled ? undefined : () => onUpdate([lowerValue, upperValue])
177
- }}
178
- {disabled}
179
- class={twMerge(
180
- `slider-bg absolute h-8 w-full appearance-none overflow-hidden rounded-full accent-(--back-color)
181
- [&::-webkit-slider-runnable-track]:rounded-full
90
+ <div class="flex w-full">
91
+ <input
92
+ type="range"
93
+ min={number.minNum}
94
+ max={centerNum}
95
+ step={number.step}
96
+ bind:value={lowerValue}
97
+ oninput={disabled
98
+ ? undefined
99
+ : (e) => {
100
+ const newValue = Math.min(Number((e.target as HTMLInputElement).value), upperValue)
101
+ lowerValue = roundToClean(newValue == upperValue ? upperValue - number.step : newValue)
102
+ }}
103
+ onmousedown={() => (activeRound = 'ceil')}
104
+ {disabled}
105
+ class={twMerge(
106
+ `slider-bg basis-[calc(${(centerNum / number.maxNum) * 100}%+2rem+5px)] h-8 w-full appearance-none overflow-hidden
107
+ accent-(--back-color)
108
+ [&::-webkit-slider-runnable-track]:rounded-l-full
182
109
  [&::-webkit-slider-runnable-track]:bg-(--gray-color)
183
- [&::-webkit-slider-thumb]:relative
184
- [&::-webkit-slider-thumb]:z-100
185
- [&::-webkit-slider-thumb]:ml-[-0.4rem]
186
- [&::-webkit-slider-thumb]:h-4
187
- [&::-webkit-slider-thumb]:w-4
110
+ [&::-webkit-slider-runnable-track]:px-2
111
+ [&::-webkit-slider-thumb]:relative
112
+ [&::-webkit-slider-thumb]:z-100
113
+ [&::-webkit-slider-thumb]:size-4
188
114
  [&::-webkit-slider-thumb]:cursor-pointer
189
115
  [&::-webkit-slider-thumb]:rounded-full
190
- [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
116
+ [&::-webkit-slider-thumb]:shadow-red-500
191
117
  ${
192
118
  userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')
193
- ? 'pl-3.5 [&::-webkit-slider-thumb]:ring-[6.5px]'
194
- : 'pl-3 [&::-webkit-slider-thumb]:ring-[5px]'
119
+ ? '[&::-webkit-slider-thumb]:ring-[6.5px]'
120
+ : '[&::-webkit-slider-thumb]:ring-[5px] '
195
121
  }
196
122
  [&::-moz-range-thumb]:relative
197
123
  [&::-moz-range-thumb]:ml-[-0.4rem]
@@ -203,45 +129,40 @@
203
129
  [&::-moz-range-track]:rounded-full
204
130
  [&::-moz-range-track]:bg-(--gray-color)
205
131
  `,
206
- `[&::-moz-range-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]
207
- [&::-webkit-slider-thumb]:shadow-[calc(${shadowWidth}px+0.5rem)_0_0_${shadowWidth}px]`,
208
- )}
209
- />
210
-
211
- <input
212
- bind:this={rangeRefUpper}
213
- type="range"
214
- min={number.minNum}
215
- max={number.maxNum}
216
- step={number.step}
217
- bind:value={upperValue}
218
- oninput={disabled
219
- ? undefined
220
- : (e) => {
221
- const newValue = Math.max(Number((e.target as HTMLInputElement).value), lowerValue)
222
- upperValue = newValue
223
- upperValue = newValue == lowerValue ? newValue + number.step : upperValue
224
- }}
225
- onmouseup={(e) => {
226
- handleTrackClick(e)
227
- disabled ? undefined : () => onUpdate([lowerValue, upperValue])
228
- }}
229
- {disabled}
230
- class={twMerge(
231
- `slider-bg absolute h-8 w-full appearance-none overflow-hidden rounded-full accent-(--back-color)
232
- [&::-webkit-slider-runnable-track]:rounded-full
233
- [&::-webkit-slider-thumb]:relative
132
+ `[&::-moz-range-thumb]:shadow-[calc(100rem+0.5rem)_0_0_100rem]
133
+ [&::-webkit-slider-thumb]:shadow-[calc(100rem+0.5rem)_0_0_100rem]`,
134
+ )}
135
+ />
136
+ <input
137
+ type="range"
138
+ min={centerNum}
139
+ max={number.maxNum}
140
+ step={number.step}
141
+ bind:value={upperValue}
142
+ oninput={disabled
143
+ ? undefined
144
+ : (e) => {
145
+ const newValue = Math.max(Number((e.target as HTMLInputElement).value), lowerValue)
146
+ upperValue = roundToClean(newValue == lowerValue ? newValue + number.step : upperValue)
147
+ }}
148
+ onmousedown={() => (activeRound = 'floor')}
149
+ {disabled}
150
+ class={twMerge(
151
+ `slider-bg basis-[calc(${100 - (centerNum / number.maxNum) * 100}%+2rem+5px)] h-8 w-full appearance-none overflow-hidden
152
+ accent-(--back-color)
153
+ [&::-webkit-slider-runnable-track]:rounded-r-full
154
+ [&::-webkit-slider-runnable-track]:bg-(--gray-color)
155
+ [&::-webkit-slider-runnable-track]:px-2
156
+ [&::-webkit-slider-thumb]:relative
234
157
  [&::-webkit-slider-thumb]:z-100
235
- [&::-webkit-slider-thumb]:ml-[-0.4rem]
236
- [&::-webkit-slider-thumb]:h-4
237
- [&::-webkit-slider-thumb]:w-4
158
+ [&::-webkit-slider-thumb]:size-4
238
159
  [&::-webkit-slider-thumb]:cursor-pointer
239
160
  [&::-webkit-slider-thumb]:rounded-full
240
- [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
161
+ [&::-webkit-slider-thumb]:shadow-red-500
241
162
  ${
242
163
  userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')
243
- ? 'pl-3.5 [&::-webkit-slider-thumb]:ring-[6.5px]'
244
- : 'pl-3 [&::-webkit-slider-thumb]:ring-[5px]'
164
+ ? '[&::-webkit-slider-thumb]:ring-[6.5px]'
165
+ : '[&::-webkit-slider-thumb]:ring-[5px] '
245
166
  }
246
167
  [&::-moz-range-thumb]:relative
247
168
  [&::-moz-range-thumb]:ml-[-0.4rem]
@@ -253,10 +174,11 @@
253
174
  [&::-moz-range-track]:rounded-full
254
175
  [&::-moz-range-track]:bg-(--gray-color)
255
176
  `,
256
- `[&::-moz-range-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]
257
- ${shadowWidth ? '' : ''} [&::-webkit-slider-thumb]:shadow-[calc(${shadowWidth}px*-1-0.5rem)_0_0_${shadowWidth}px]`,
258
- )}
259
- />
177
+ `[&::-moz-range-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]
178
+ [&::-webkit-slider-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]`,
179
+ )}
180
+ />
181
+ </div>
260
182
  {:else}
261
183
  {@const userAgent = navigator.userAgent}
262
184
  <!-- Одиночный слайдер -->
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { default as InputProps } from './Input/InputProps.svelte';
12
12
  export { default as Joystick } from './Joystick/Joystick.svelte';
13
13
  export { default as JoystickProps } from './Joystick/JoystickProps.svelte';
14
14
  export { default as Modal } from './Modal.svelte';
15
+ export { default as Map } from './Map/Map.svelte';
16
+ export { default as MapProps } from './Map/MapProps.svelte';
15
17
  export { default as ProgressBar } from './ProgressBar/ProgressBar.svelte';
16
18
  export { default as ProgressBarProps } from './ProgressBar/ProgressBarProps.svelte';
17
19
  export { default as Select } from './Select/Select.svelte';
@@ -28,4 +30,4 @@ export { default as TextField } from './TextField/TextField.svelte';
28
30
  export { default as TextFieldProps } from './TextField/TextFieldProps.svelte';
29
31
  export * from './locales/i18n';
30
32
  export * from './locales/translations';
31
- export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type ISelectOption, type ISwitchProps, type IColorPickerProps, type ISliderProps, type ITextFieldProps, type IProgressBarProps, type IGraphProps, type IGraphDataObject, type ITableHeader, type ITableProps, type ITabsProps, type IJoystickProps, } from './types';
33
+ export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type ISelectOption, type ISwitchProps, type IColorPickerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IGraphProps, type IGraphDataObject, type ITableHeader, type ITableProps, type ITabsProps, type IJoystickProps, } from './types';
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ export { default as InputProps } from './Input/InputProps.svelte';
13
13
  export { default as Joystick } from './Joystick/Joystick.svelte';
14
14
  export { default as JoystickProps } from './Joystick/JoystickProps.svelte';
15
15
  export { default as Modal } from './Modal.svelte';
16
+ export { default as Map } from './Map/Map.svelte';
17
+ export { default as MapProps } from './Map/MapProps.svelte';
16
18
  export { default as ProgressBar } from './ProgressBar/ProgressBar.svelte';
17
19
  export { default as ProgressBarProps } from './ProgressBar/ProgressBarProps.svelte';
18
20
  export { default as Select } from './Select/Select.svelte';
@@ -72,6 +72,7 @@ const translations = {
72
72
  'constructor.props.align.content': 'Выравнивание контента',
73
73
  'constructor.props.image': 'Фоновое изображение',
74
74
  'constructor.props.labelicon': 'Иконка заголовка',
75
+ 'constructor.props.markerIcon': 'Иконка маркера',
75
76
  'constructor.props.removeimage': 'Удалить изображение',
76
77
  'constructor.props.name': 'Текст',
77
78
  'constructor.props.height': 'Высота',
@@ -125,6 +126,7 @@ const translations = {
125
126
  'constructor.props.equal': 'Равные',
126
127
  'constructor.props.bitmode': 'Битовый режим',
127
128
  'constructor.props.access': 'Доступ (не для владельца)',
129
+ 'constructor.props.map.timeout': 'Таймаут маркеров:',
128
130
  'constructor.props.table.columns': 'Колонки таблицы',
129
131
  'constructor.props.table.columns.key': 'Ключ',
130
132
  'constructor.props.table.columns.label': 'Название колонки',
package/dist/types.d.ts CHANGED
@@ -8,8 +8,8 @@ export interface UIComponent {
8
8
  id: string;
9
9
  name?: string;
10
10
  access?: 'full' | 'viewOnly' | 'hidden';
11
- type: 'Button' | 'Accordion' | 'Input' | 'Select' | 'Switch' | 'ColorPicker' | 'Slider' | 'TextField' | 'Joystick' | 'ProgressBar' | 'Graph' | 'Table' | 'Tabs' | 'FileAttach';
12
- properties: IAccordionProps | IButtonProps | IInputProps | ISelectProps | ISwitchProps | IColorPickerProps | ISliderProps | ITextFieldProps | IProgressBarProps | IGraphProps | ITableProps<object> | ITabsProps | IFileInputProps | IJoystickProps;
11
+ type: 'Button' | 'Accordion' | 'Input' | 'Select' | 'Switch' | 'ColorPicker' | 'Slider' | 'TextField' | 'Joystick' | 'ProgressBar' | 'Graph' | 'Table' | 'Tabs' | 'FileAttach' | 'Map';
12
+ properties: IAccordionProps | IButtonProps | IInputProps | ISelectProps | ISwitchProps | IColorPickerProps | ISliderProps | ITextFieldProps | IProgressBarProps | IGraphProps | ITableProps<object> | ITabsProps | IFileInputProps | IJoystickProps | IMapProps;
13
13
  position: Position;
14
14
  parentId: string;
15
15
  }
@@ -298,3 +298,21 @@ export interface IJoystickProps {
298
298
  }[];
299
299
  onUpdate?: (value: number[]) => void;
300
300
  }
301
+ export interface IDeviceGNSS {
302
+ NavLat: number;
303
+ NavLon: number;
304
+ NavAlt: number;
305
+ DevName: string;
306
+ DevSN: string;
307
+ NavHeading: number;
308
+ NavSatUse: number;
309
+ }
310
+ export interface IMapProps {
311
+ id?: string;
312
+ label?: {
313
+ name?: string;
314
+ class?: string;
315
+ };
316
+ data: IDeviceGNSS | null;
317
+ markerIcon?: string;
318
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -36,6 +36,7 @@
36
36
  "prettier": "^3.6.2",
37
37
  "prettier-plugin-svelte": "^3.4.0",
38
38
  "prettier-plugin-tailwindcss": "^0.7.1",
39
+ "svelte-maplibre-gl": "^1.0.2",
39
40
  "tailwind-merge": "^3.4.0",
40
41
  "tailwindcss": "^4.1.17",
41
42
  "tsx": "^4.20.6",
@@ -43,14 +44,14 @@
43
44
  },
44
45
  "devDependencies": {
45
46
  "@sveltejs/adapter-static": "^3.0.10",
46
- "@sveltejs/kit": "^2.48.5",
47
- "@sveltejs/package": "^2.5.4",
47
+ "@sveltejs/kit": "^2.49.0",
48
+ "@sveltejs/package": "^2.5.6",
48
49
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
49
50
  "@types/node": "^24.10.1",
50
51
  "publint": "^0.3.15",
51
- "svelte": "^5.43.12",
52
+ "svelte": "^5.43.14",
52
53
  "svelte-preprocess": "^6.0.3",
53
- "vite": "^7.2.2",
54
+ "vite": "^7.2.4",
54
55
  "vite-plugin-compression": "^0.5.1"
55
56
  }
56
57
  }