poe-svelte-ui-lib 1.2.26 → 1.2.28

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()
65
+ const roundToClean = (num: number): number => {
66
+ if (Number.isInteger(num)) return num
93
67
 
94
- const updateShadowWidth = () => {
95
- let thumbCenterLower
96
- let thumbCenterUpper
68
+ const rounded1 = Number(num.toFixed(1))
69
+ if (Math.abs(rounded1 - num) < 1e-10) return rounded1
97
70
 
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
- }
71
+ const rounded2 = Number(num.toFixed(2))
72
+ if (Math.abs(rounded2 - num) < 1e-10) return rounded2
103
73
 
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
- }
109
-
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,42 @@
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
+ onUpdate([lowerValue, upperValue])
103
+ }}
104
+ onmousedown={() => (activeRound = 'ceil')}
105
+ {disabled}
106
+ class={twMerge(
107
+ `slider-bg basis-[calc(${(centerNum / number.maxNum) * 100}%+2rem+5px)] h-8 w-full appearance-none overflow-hidden
108
+ accent-(--back-color)
109
+ [&::-webkit-slider-runnable-track]:rounded-l-full
182
110
  [&::-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
111
+ [&::-webkit-slider-runnable-track]:px-2
112
+ [&::-webkit-slider-thumb]:relative
113
+ [&::-webkit-slider-thumb]:z-100
114
+ [&::-webkit-slider-thumb]:size-4
188
115
  [&::-webkit-slider-thumb]:cursor-pointer
189
116
  [&::-webkit-slider-thumb]:rounded-full
190
- [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
117
+ [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
191
118
  ${
192
119
  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]'
120
+ ? '[&::-webkit-slider-thumb]:ring-[6.5px]'
121
+ : '[&::-webkit-slider-thumb]:ring-[5px] '
195
122
  }
196
123
  [&::-moz-range-thumb]:relative
197
124
  [&::-moz-range-thumb]:ml-[-0.4rem]
@@ -203,45 +130,42 @@
203
130
  [&::-moz-range-track]:rounded-full
204
131
  [&::-moz-range-track]:bg-(--gray-color)
205
132
  `,
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
133
+ `[&::-moz-range-thumb]:shadow-[calc(100rem+0.5rem)_0_0_100rem]
134
+ [&::-webkit-slider-thumb]:shadow-[calc(100rem+0.5rem)_0_0_100rem]`,
135
+ )}
136
+ style={`flex-basis: calc(${(centerNum / number.maxNum) * 100}%+2rem+5px)`}
137
+ />
138
+ <input
139
+ type="range"
140
+ min={centerNum}
141
+ max={number.maxNum}
142
+ step={number.step}
143
+ bind:value={upperValue}
144
+ oninput={disabled
145
+ ? undefined
146
+ : (e) => {
147
+ const newValue = Math.max(Number((e.target as HTMLInputElement).value), lowerValue)
148
+ upperValue = roundToClean(newValue == lowerValue ? newValue + number.step : upperValue)
149
+ onUpdate([lowerValue, upperValue])
150
+ }}
151
+ onmousedown={() => (activeRound = 'floor')}
152
+ {disabled}
153
+ class={twMerge(
154
+ `slider-bg basis-[calc(${100 - (centerNum / number.maxNum) * 100}%+2rem+5px)] h-8 w-full appearance-none overflow-hidden
155
+ accent-(--back-color)
156
+ [&::-webkit-slider-runnable-track]:rounded-r-full
157
+ [&::-webkit-slider-runnable-track]:bg-(--gray-color)
158
+ [&::-webkit-slider-runnable-track]:px-2
159
+ [&::-webkit-slider-thumb]:relative
234
160
  [&::-webkit-slider-thumb]:z-100
235
- [&::-webkit-slider-thumb]:ml-[-0.4rem]
236
- [&::-webkit-slider-thumb]:h-4
237
- [&::-webkit-slider-thumb]:w-4
161
+ [&::-webkit-slider-thumb]:size-4
238
162
  [&::-webkit-slider-thumb]:cursor-pointer
239
163
  [&::-webkit-slider-thumb]:rounded-full
240
- [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
164
+ [&::-webkit-slider-thumb]:shadow-[var(--focus-shadow),]
241
165
  ${
242
166
  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]'
167
+ ? '[&::-webkit-slider-thumb]:ring-[6.5px]'
168
+ : '[&::-webkit-slider-thumb]:ring-[5px] '
245
169
  }
246
170
  [&::-moz-range-thumb]:relative
247
171
  [&::-moz-range-thumb]:ml-[-0.4rem]
@@ -253,16 +177,23 @@
253
177
  [&::-moz-range-track]:rounded-full
254
178
  [&::-moz-range-track]:bg-(--gray-color)
255
179
  `,
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
- />
180
+ `[&::-moz-range-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]
181
+ [&::-webkit-slider-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]`,
182
+ )}
183
+ style={`flex-basis: calc(${(centerNum / number.maxNum) * 100}%+2rem+5px)`}
184
+ />
185
+ </div>
260
186
  {:else}
261
187
  {@const userAgent = navigator.userAgent}
262
188
  <!-- Одиночный слайдер -->
263
189
  <div class="absolute h-full w-full">
264
190
  <input
265
191
  type="range"
192
+ min={number.minNum}
193
+ max={number.maxNum}
194
+ step={number.step}
195
+ bind:value={singleValue}
196
+ oninput={() => onUpdate(singleValue)}
266
197
  class={twMerge(
267
198
  `slider-bg h-8 w-full appearance-none overflow-hidden rounded-full accent-(--back-color)
268
199
  [&::-webkit-slider-runnable-track]:rounded-full
@@ -293,10 +224,6 @@
293
224
  `[&::-moz-range-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]
294
225
  [&::-webkit-slider-thumb]:shadow-[calc(100rem*-1-0.5rem)_0_0_100rem]`,
295
226
  )}
296
- min={number.minNum}
297
- max={number.maxNum}
298
- step={number.step}
299
- bind:value={singleValue}
300
227
  />
301
228
  </div>
302
229
  {/if}
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.28",
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
  }