poe-svelte-ui-lib 0.2.0 → 1.0.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.
- package/dist/Accordion/Accordion.svelte +53 -0
- package/dist/Accordion/Accordion.svelte.d.ts +4 -0
- package/dist/Accordion/AccordionProps.svelte +70 -0
- package/dist/Accordion/AccordionProps.svelte.d.ts +10 -0
- package/dist/{Button.svelte → Button/Button.svelte} +43 -24
- package/dist/{Button.svelte.d.ts → Button/Button.svelte.d.ts} +5 -5
- package/dist/Button/ButtonProps.svelte +200 -0
- package/dist/Button/ButtonProps.svelte.d.ts +10 -0
- package/dist/ColorPicker/ColorPicker.svelte +207 -0
- package/dist/ColorPicker/ColorPicker.svelte.d.ts +4 -0
- package/dist/ColorPicker/ColorPickerProps.svelte +100 -0
- package/dist/ColorPicker/ColorPickerProps.svelte.d.ts +10 -0
- package/dist/FileAttach/FileAttach.svelte +103 -0
- package/dist/FileAttach/FileAttach.svelte.d.ts +22 -0
- package/dist/Graph/Graph.svelte +270 -0
- package/dist/Graph/Graph.svelte.d.ts +4 -0
- package/dist/Graph/GraphProps.svelte +56 -0
- package/dist/Graph/GraphProps.svelte.d.ts +10 -0
- package/dist/Input/Input.svelte +239 -0
- package/dist/Input/Input.svelte.d.ts +4 -0
- package/dist/Input/InputProps.svelte +221 -0
- package/dist/Input/InputProps.svelte.d.ts +10 -0
- package/dist/Loader.svelte +12 -0
- package/dist/Loader.svelte.d.ts +5 -0
- package/dist/MessageModal.svelte +54 -0
- package/dist/MessageModal.svelte.d.ts +10 -0
- package/dist/ProgressBar/ProgressBar.svelte +48 -0
- package/dist/ProgressBar/ProgressBar.svelte.d.ts +4 -0
- package/dist/ProgressBar/ProgressBarProps.svelte +145 -0
- package/dist/ProgressBar/ProgressBarProps.svelte.d.ts +10 -0
- package/dist/Select/Select.svelte +187 -0
- package/dist/Select/Select.svelte.d.ts +18 -0
- package/dist/Select/SelectProps.svelte +260 -0
- package/dist/Select/SelectProps.svelte.d.ts +10 -0
- package/dist/Slider/Slider.svelte +260 -0
- package/dist/Slider/Slider.svelte.d.ts +4 -0
- package/dist/Slider/SliderProps.svelte +161 -0
- package/dist/Slider/SliderProps.svelte.d.ts +10 -0
- package/dist/Switch/Switch.svelte +83 -0
- package/dist/Switch/Switch.svelte.d.ts +4 -0
- package/dist/Switch/SwitchProps.svelte +144 -0
- package/dist/Switch/SwitchProps.svelte.d.ts +10 -0
- package/dist/Table/Table.svelte +276 -0
- package/dist/Table/Table.svelte.d.ts +4 -0
- package/dist/Table/TableProps.svelte +286 -0
- package/dist/Table/TableProps.svelte.d.ts +10 -0
- package/dist/TextField/TextField.svelte +22 -0
- package/dist/TextField/TextField.svelte.d.ts +4 -0
- package/dist/TextField/TextFieldProps.svelte +92 -0
- package/dist/TextField/TextFieldProps.svelte.d.ts +10 -0
- package/dist/appIcons/ButtonAdd.svelte +10 -0
- package/dist/appIcons/ButtonAdd.svelte.d.ts +18 -0
- package/dist/appIcons/ButtonDelete.svelte +13 -0
- package/dist/appIcons/ButtonDelete.svelte.d.ts +18 -0
- package/dist/appIcons/LoaderRotate.svelte +9 -0
- package/dist/appIcons/LoaderRotate.svelte.d.ts +18 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +27 -2
- package/dist/locales/CircleFlagsEn.svelte +14 -0
- package/dist/locales/CircleFlagsEn.svelte.d.ts +26 -0
- package/dist/locales/CircleFlagsRu.svelte +8 -0
- package/dist/locales/CircleFlagsRu.svelte.d.ts +26 -0
- package/dist/locales/CircleFlagsZh.svelte +8 -0
- package/dist/locales/CircleFlagsZh.svelte.d.ts +26 -0
- package/dist/locales/i18n.d.ts +10 -0
- package/dist/locales/i18n.js +36 -0
- package/dist/locales/translations.d.ts +7 -0
- package/dist/locales/translations.js +450 -0
- package/dist/options.d.ts +78 -0
- package/dist/options.js +71 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +1 -0
- package/package.json +28 -21
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<!-- $lib/ElementsUI/Graph.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import Select from '../Select/Select.svelte'
|
|
5
|
+
import type { IGraphDataObject, IGraphProps, ISelectOption } from '../types'
|
|
6
|
+
|
|
7
|
+
/* Инициализация пропсов с дефолтными значениями */
|
|
8
|
+
let {
|
|
9
|
+
id = { name: '', value: crypto.randomUUID() },
|
|
10
|
+
wrapperClass = '',
|
|
11
|
+
label = { name: '', class: '' },
|
|
12
|
+
streamingData = { data: [], timestamp: Date.now() },
|
|
13
|
+
isTest = false,
|
|
14
|
+
}: IGraphProps = $props()
|
|
15
|
+
|
|
16
|
+
/* Состояние компонента */
|
|
17
|
+
let graphData = $state<{ id: string; points: { x: number; y: number }[]; color: string; name: string }[]>([])
|
|
18
|
+
let currentValues = $state<number[]>([])
|
|
19
|
+
let container: HTMLDivElement
|
|
20
|
+
let canvas: HTMLCanvasElement
|
|
21
|
+
let ctx: CanvasRenderingContext2D
|
|
22
|
+
let width = $state(600)
|
|
23
|
+
let height = $state(125)
|
|
24
|
+
|
|
25
|
+
/* Константы и настройки */
|
|
26
|
+
const REFRESH_OPTIONS: ISelectOption[] = [
|
|
27
|
+
{ id: 'RefreshOption-AUTO', name: 'AUTO', value: 0, class: '' },
|
|
28
|
+
{ id: 'RefreshOption-10', name: '10', value: 10, class: '' },
|
|
29
|
+
{ id: 'RefreshOption-25', name: '25', value: 25, class: '' },
|
|
30
|
+
{ id: 'RefreshOption-50', name: '50', value: 50, class: '' },
|
|
31
|
+
{ id: 'RefreshOption-100', name: '100', value: 100, class: '' },
|
|
32
|
+
{ id: 'RefreshOption-250', name: '250', value: 250, class: '' },
|
|
33
|
+
{ id: 'RefreshOption-500', name: '500', value: 500, class: '' },
|
|
34
|
+
{ id: 'RefreshOption-1000', name: '1000', value: 1000, class: '' },
|
|
35
|
+
{ id: 'RefreshOption-5000', name: '5000', value: 5000, class: '' },
|
|
36
|
+
]
|
|
37
|
+
// const REFRESH_OPTIONS = [10, 25, 50, 100, 250, 500, 1000, 5000]
|
|
38
|
+
const SCALE_OPTIONS: ISelectOption[] = [
|
|
39
|
+
{ id: 'ScaleOption-50', name: '50', value: 50, class: '' },
|
|
40
|
+
{ id: 'ScaleOption-100', name: '100', value: 100, class: '' },
|
|
41
|
+
{ id: 'ScaleOption-500', name: '500', value: 500, class: '' },
|
|
42
|
+
{ id: 'ScaleOption-1000', name: '1000', value: 1000, class: '' },
|
|
43
|
+
{ id: 'ScaleOption-2000', name: '2000', value: 2000, class: '' },
|
|
44
|
+
]
|
|
45
|
+
// const SCALE_OPTIONS = [50, 100, 500, 1000, 2000]
|
|
46
|
+
let selectedRefreshRate = $state(0)
|
|
47
|
+
let selectedScale = $state(100)
|
|
48
|
+
const maxDataPoints = $derived(selectedRefreshRate == 0 ? 20 : 100)
|
|
49
|
+
const defaultColors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
|
|
50
|
+
|
|
51
|
+
let previousDataTimestamp: number = $state(0)
|
|
52
|
+
|
|
53
|
+
/* Инициализация данных графиков */
|
|
54
|
+
const initializeGraphData = () => {
|
|
55
|
+
if (typeof streamingData === 'string') {
|
|
56
|
+
streamingData = JSON.parse(streamingData)
|
|
57
|
+
}
|
|
58
|
+
if (!streamingData.data || streamingData.data.length === 0) {
|
|
59
|
+
graphData = []
|
|
60
|
+
currentValues = []
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const newGraphData = (streamingData.data as IGraphDataObject[]).slice(0, 6).map((d, i) => {
|
|
64
|
+
const existingData = graphData.find((g) => g.name === d.name) || graphData[i]
|
|
65
|
+
return {
|
|
66
|
+
id: existingData?.id || crypto.randomUUID(),
|
|
67
|
+
points: existingData?.points || [],
|
|
68
|
+
color: d.color || defaultColors[i % defaultColors.length],
|
|
69
|
+
name: d.name || `Value ${i}`,
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
if (JSON.stringify(graphData) !== JSON.stringify(newGraphData)) {
|
|
73
|
+
graphData = newGraphData
|
|
74
|
+
currentValues = (streamingData.data as IGraphDataObject[]).map((d) => d.value || 0)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Вызываем инициализацию при монтировании и при изменении streamingData */
|
|
79
|
+
onMount(initializeGraphData)
|
|
80
|
+
$effect(() => {
|
|
81
|
+
initializeGraphData()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/* Обработка входящих данных */
|
|
85
|
+
let intervalId: ReturnType<typeof setInterval>
|
|
86
|
+
$effect(() => {
|
|
87
|
+
clearInterval(intervalId)
|
|
88
|
+
if (selectedRefreshRate > 0 && streamingData.data && streamingData.data.length > 0) {
|
|
89
|
+
intervalId = setInterval(() => {
|
|
90
|
+
let newValues
|
|
91
|
+
if (isTest) newValues = graphData.map(() => Math.random() * 100 - 50)
|
|
92
|
+
else newValues = (streamingData.data as IGraphDataObject[]).map((dataset) => dataset.value)
|
|
93
|
+
const now = Date.now()
|
|
94
|
+
newValues.forEach((value, i) => {
|
|
95
|
+
if (!graphData[i]) return
|
|
96
|
+
graphData[i].points.push({ x: now, y: value })
|
|
97
|
+
if (graphData[i].points.length > maxDataPoints) {
|
|
98
|
+
graphData[i].points.shift()
|
|
99
|
+
}
|
|
100
|
+
currentValues[i] = value
|
|
101
|
+
})
|
|
102
|
+
drawAllGraphs()
|
|
103
|
+
}, selectedRefreshRate)
|
|
104
|
+
} else if (selectedRefreshRate == 0 && streamingData.data && streamingData.data.length > 0 && !isTest) {
|
|
105
|
+
intervalId = setInterval(() => {
|
|
106
|
+
if (previousDataTimestamp < (streamingData.timestamp ?? Date.now())) {
|
|
107
|
+
let newValues = (streamingData.data as IGraphDataObject[]).map((dataset) => dataset.value)
|
|
108
|
+
|
|
109
|
+
newValues.forEach((value, i) => {
|
|
110
|
+
if (!graphData[i]) return
|
|
111
|
+
graphData[i].points.push({ x: streamingData.timestamp ?? Date.now(), y: value })
|
|
112
|
+
if (graphData[i].points.length > maxDataPoints) {
|
|
113
|
+
graphData[i].points.shift()
|
|
114
|
+
}
|
|
115
|
+
currentValues[i] = value
|
|
116
|
+
})
|
|
117
|
+
drawAllGraphs()
|
|
118
|
+
previousDataTimestamp = streamingData.timestamp ?? Date.now()
|
|
119
|
+
}
|
|
120
|
+
}, 10)
|
|
121
|
+
}
|
|
122
|
+
console.log(streamingData.data)
|
|
123
|
+
return () => clearInterval(intervalId)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
let resizeObserver: ResizeObserver
|
|
127
|
+
$effect(() => {
|
|
128
|
+
if (!container || !canvas) return
|
|
129
|
+
const dpr = window.devicePixelRatio || 1
|
|
130
|
+
resizeObserver = new ResizeObserver(() => {
|
|
131
|
+
const rect = container.getBoundingClientRect()
|
|
132
|
+
width = rect.width
|
|
133
|
+
height = rect.height
|
|
134
|
+
canvas.width = width * dpr
|
|
135
|
+
canvas.height = height * dpr
|
|
136
|
+
canvas.style.width = `${width}px`
|
|
137
|
+
canvas.style.height = `${height}px`
|
|
138
|
+
ctx = canvas.getContext('2d')!
|
|
139
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
140
|
+
drawAllGraphs()
|
|
141
|
+
})
|
|
142
|
+
resizeObserver.observe(container)
|
|
143
|
+
return () => resizeObserver.disconnect()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const drawAllGraphs = () => {
|
|
147
|
+
if (!ctx) return
|
|
148
|
+
ctx.clearRect(0, 0, width, height)
|
|
149
|
+
|
|
150
|
+
const padding = {
|
|
151
|
+
top: 10,
|
|
152
|
+
right: 10,
|
|
153
|
+
bottom: 20,
|
|
154
|
+
left: 35,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const graphWidth = width - padding.left - padding.right
|
|
158
|
+
const graphHeight = height - padding.top - padding.bottom
|
|
159
|
+
|
|
160
|
+
const allPoints = graphData.flatMap((g) => g.points)
|
|
161
|
+
const minX = Math.min(...allPoints.map((p) => p.x))
|
|
162
|
+
const maxX = Math.max(...allPoints.map((p) => p.x))
|
|
163
|
+
const timeSpan = maxX - minX || 1
|
|
164
|
+
|
|
165
|
+
// Функции преобразования координат с учетом отступов
|
|
166
|
+
const getX = (x: number) => padding.left + ((x - minX) / timeSpan) * graphWidth
|
|
167
|
+
const getY = (y: number) => padding.top + graphHeight - (((y / selectedScale) * graphHeight) / 2 + graphHeight / 2)
|
|
168
|
+
|
|
169
|
+
/* Сетка X */
|
|
170
|
+
ctx.strokeStyle = '#777'
|
|
171
|
+
ctx.fillStyle = '#777'
|
|
172
|
+
ctx.lineWidth = 0.5
|
|
173
|
+
ctx.font = '10px monospace'
|
|
174
|
+
ctx.textAlign = 'center'
|
|
175
|
+
|
|
176
|
+
const now = Date.now()
|
|
177
|
+
for (let i = 0; i <= 10; i++) {
|
|
178
|
+
const t = minX + (i / 10) * timeSpan
|
|
179
|
+
const x = getX(t)
|
|
180
|
+
const secondsAgo = ((t - now) / 1000).toFixed(0)
|
|
181
|
+
ctx.beginPath()
|
|
182
|
+
ctx.moveTo(x, padding.top)
|
|
183
|
+
ctx.lineTo(x, height - padding.bottom)
|
|
184
|
+
ctx.stroke()
|
|
185
|
+
ctx.textBaseline = 'top'
|
|
186
|
+
ctx.fillText(`${secondsAgo}s`, x, height - padding.bottom + 2)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Сетка Y */
|
|
190
|
+
ctx.textAlign = 'right'
|
|
191
|
+
ctx.textBaseline = 'middle'
|
|
192
|
+
const ySteps = 8
|
|
193
|
+
for (let i = 0; i <= ySteps; i++) {
|
|
194
|
+
const yVal = selectedScale - (i * 2 * selectedScale) / ySteps
|
|
195
|
+
const y = getY(yVal)
|
|
196
|
+
ctx.beginPath()
|
|
197
|
+
ctx.moveTo(padding.left, y)
|
|
198
|
+
ctx.lineTo(width - padding.right, y)
|
|
199
|
+
ctx.stroke()
|
|
200
|
+
ctx.fillText(`${yVal.toFixed(0)}`, padding.left - 5, y)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Отрисовка графиков */
|
|
204
|
+
ctx.lineWidth = 2
|
|
205
|
+
graphData.forEach(({ points, color }) => {
|
|
206
|
+
if (points.length < 2) return
|
|
207
|
+
ctx.strokeStyle = color
|
|
208
|
+
ctx.beginPath()
|
|
209
|
+
ctx.moveTo(getX(points[0].x), getY(points[0].y))
|
|
210
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
211
|
+
const p1 = points[i]
|
|
212
|
+
const p2 = points[i + 1]
|
|
213
|
+
const xc2 = (p1.x + p2.x) / 2
|
|
214
|
+
const yc2 = (p1.y + p2.y) / 2
|
|
215
|
+
ctx.quadraticCurveTo(getX(p1.x), getY(p1.y), getX(xc2), getY(yc2))
|
|
216
|
+
}
|
|
217
|
+
const last = points[points.length - 1]
|
|
218
|
+
ctx.lineTo(getX(last.x), getY(last.y))
|
|
219
|
+
ctx.stroke()
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<div id={id.value} class={`relative flex w-full flex-col items-center justify-center ${wrapperClass}`}>
|
|
225
|
+
{#if label.name}
|
|
226
|
+
<h5 class={`w-full px-4 text-center ${label.class}`}>{label.name}</h5>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
<div class="flex w-full flex-row gap-4">
|
|
230
|
+
<!-- График -->
|
|
231
|
+
<div bind:this={container} class="h-64 flex-grow overflow-hidden rounded-md border border-gray-200">
|
|
232
|
+
<canvas class="h-full w-full bg-[var(--back-color)]" bind:this={canvas}></canvas>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<!-- Панель настроек -->
|
|
236
|
+
<div class="flex w-48 flex-col gap-2">
|
|
237
|
+
<!-- Развертка по горизонтали -->
|
|
238
|
+
<Select
|
|
239
|
+
label={{ name: 'Refresh rate', class: '' }}
|
|
240
|
+
options={REFRESH_OPTIONS}
|
|
241
|
+
value={REFRESH_OPTIONS.find((o) => o.value == selectedRefreshRate)}
|
|
242
|
+
onUpdate={(value) => (selectedRefreshRate = value.value as number)}
|
|
243
|
+
/>
|
|
244
|
+
|
|
245
|
+
<!-- Масштаб по вертикали -->
|
|
246
|
+
<Select
|
|
247
|
+
label={{ name: 'Scale' }}
|
|
248
|
+
options={SCALE_OPTIONS}
|
|
249
|
+
value={REFRESH_OPTIONS.find((o) => o.value == selectedScale)}
|
|
250
|
+
onUpdate={(value) => (selectedScale = value.value as number)}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
<!-- Переменные и их значение -->
|
|
254
|
+
<div>
|
|
255
|
+
<h5 class="px-4">Values</h5>
|
|
256
|
+
<table class="w-full font-mono text-sm">
|
|
257
|
+
<tbody>
|
|
258
|
+
{#each graphData as data, i (i)}
|
|
259
|
+
<tr>
|
|
260
|
+
<td><div class="mr-2 h-4 w-4 rounded-full" style="background-color: {data.color}"></div></td>
|
|
261
|
+
<td class="w-24 truncate text-left font-semibold">{(streamingData.data as IGraphDataObject[])?.[i]?.name}</td>
|
|
262
|
+
<td class="w-16 text-right">{currentValues[i].toFixed(2)}</td>
|
|
263
|
+
</tr>
|
|
264
|
+
{/each}
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- $lib/ElementsUI/SwitchProps.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { getContext } from 'svelte'
|
|
4
|
+
import { t } from '../locales/i18n'
|
|
5
|
+
import type { UIComponent, IGraphProps } from '../types'
|
|
6
|
+
import * as UI from '../index'
|
|
7
|
+
|
|
8
|
+
const { component, onPropertyChange } = $props<{
|
|
9
|
+
component: UIComponent & { properties: Partial<IGraphProps> }
|
|
10
|
+
onPropertyChange: (value: string | object) => void
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const DeviceVariables = getContext<{ value: string; name: string }[]>('DeviceVariables')
|
|
14
|
+
let VARIABLE_OPTIONS = $derived(
|
|
15
|
+
DeviceVariables.map((variable: { value: string; name: string }) => ({
|
|
16
|
+
id: variable.name,
|
|
17
|
+
value: variable.value,
|
|
18
|
+
name: `${variable.value} | ${variable.name}`,
|
|
19
|
+
})),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
/* Обновление свойства */
|
|
23
|
+
const updateProperty = (path: string, value: string | object) => {
|
|
24
|
+
const newProperties = JSON.parse(JSON.stringify(component.properties))
|
|
25
|
+
const parts = path.split('.')
|
|
26
|
+
let obj = newProperties
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
29
|
+
const part = parts[i]
|
|
30
|
+
if (!obj[part]) obj[part] = {}
|
|
31
|
+
obj = obj[part]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
obj[parts[parts.length - 1]] = value
|
|
35
|
+
onPropertyChange(newProperties)
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#if component && component.properties}
|
|
40
|
+
<div class="relative flex flex-row items-start justify-center">
|
|
41
|
+
<!-- Сообщение для отправки в ws по нажатию кнопки -->
|
|
42
|
+
<div class="flex w-1/3 flex-col items-center px-2">
|
|
43
|
+
<UI.Select
|
|
44
|
+
label={{ name: $t('service.constructor.props.variable') }}
|
|
45
|
+
options={VARIABLE_OPTIONS}
|
|
46
|
+
value={VARIABLE_OPTIONS.find((opt) => opt.value === component.properties.id.value)}
|
|
47
|
+
onUpdate={(value) => {
|
|
48
|
+
updateProperty('id.name', (value.name as string).split('|')[1].trim())
|
|
49
|
+
updateProperty('id.value', value.value as string)
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="flex w-1/3 flex-col px-2"></div>
|
|
54
|
+
<div class="flex w-1/3 flex-col px-2"></div>
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { UIComponent, IGraphProps } from '../types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
component: UIComponent & {
|
|
4
|
+
properties: Partial<IGraphProps>;
|
|
5
|
+
};
|
|
6
|
+
onPropertyChange: (value: string | object) => void;
|
|
7
|
+
};
|
|
8
|
+
declare const GraphProps: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type GraphProps = ReturnType<typeof GraphProps>;
|
|
10
|
+
export default GraphProps;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<!-- $lib/ElementsUI/Input.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import { fly } from 'svelte/transition'
|
|
5
|
+
import type { IInputProps } from '../types'
|
|
6
|
+
import { t } from '../locales/i18n'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
id = { name: '', value: crypto.randomUUID() },
|
|
10
|
+
wrapperClass = '',
|
|
11
|
+
label = { name: '', class: '' },
|
|
12
|
+
disabled = false,
|
|
13
|
+
readonly = false,
|
|
14
|
+
value = $bindable(),
|
|
15
|
+
type = 'text',
|
|
16
|
+
autocomplete = 'off',
|
|
17
|
+
componentClass = '',
|
|
18
|
+
maxlength = 100,
|
|
19
|
+
number = { minNum: -1000000, maxNum: 1000000, step: 1 },
|
|
20
|
+
textareaRows = 3,
|
|
21
|
+
copyButton = false,
|
|
22
|
+
regExp = '^[\\s\\S]*$',
|
|
23
|
+
help = { placeholder: '', info: '' },
|
|
24
|
+
onUpdate = () => {},
|
|
25
|
+
}: IInputProps = $props()
|
|
26
|
+
|
|
27
|
+
let showPassword = $state(false)
|
|
28
|
+
let showInfo = $state(false)
|
|
29
|
+
let isCopied = $state(false)
|
|
30
|
+
|
|
31
|
+
/* Закрытие INFO при клике вне компонента */
|
|
32
|
+
onMount(() => {
|
|
33
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
34
|
+
const target = event.target as HTMLElement
|
|
35
|
+
if (!target.closest('.info-container') && !target.closest('.button-info')) {
|
|
36
|
+
showInfo = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
window.addEventListener('click', handleClickOutside)
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('click', handleClickOutside)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (type === 'number') {
|
|
47
|
+
if (value === undefined || value === null || value === '') value = number.minNum
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/* Обработка регулярного выражения */
|
|
52
|
+
const parseRegExp = (pattern: string | RegExp): RegExp => {
|
|
53
|
+
if (pattern instanceof RegExp) return pattern
|
|
54
|
+
const match = pattern.match(/^\/(.*)\/([gimsuy]*)$/)
|
|
55
|
+
return match ? new RegExp(match[1], match[2]) : new RegExp(pattern)
|
|
56
|
+
}
|
|
57
|
+
let RegExpObj = $derived(() => parseRegExp(regExp))
|
|
58
|
+
let isValid = $derived(RegExpObj().test(typeof value === 'string' ? value : String(value)))
|
|
59
|
+
|
|
60
|
+
const handleInputChange = (value: string | number) => {
|
|
61
|
+
if (type === 'number') {
|
|
62
|
+
const numValue = typeof value === 'string' ? parseFloat(value.replace(',', '.')) : Number(value)
|
|
63
|
+
if (!isNaN(numValue)) onUpdate?.(numValue)
|
|
64
|
+
else onUpdate?.(value as string)
|
|
65
|
+
} else {
|
|
66
|
+
onUpdate?.(value as string)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div class="bg-max relative flex w-full flex-col items-center {type === 'text-area' ? 'h-full' : ''} {wrapperClass}">
|
|
72
|
+
{#if label.name}
|
|
73
|
+
<h5 class={`w-full px-4 text-center ${label.class}`}>{label.name}</h5>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
<div class="relative flex w-full items-center {type === 'text-area' ? 'h-full' : ''}">
|
|
77
|
+
{#if type === 'text' || type === 'password' || type === 'number'}
|
|
78
|
+
<input
|
|
79
|
+
bind:value
|
|
80
|
+
class="w-full rounded-2xl border px-4 py-1 text-center transition-all duration-300 outline-none focus:border-blue-400
|
|
81
|
+
[&::-webkit-inner-spin-button]:hidden [&::-webkit-outer-spin-button]:hidden
|
|
82
|
+
{isValid ? 'border-[var(--border-color)]' : '!border-red-400 shadow-[0_0_6px_var(--red-color)]'}
|
|
83
|
+
{disabled ? 'opacity-50' : 'hover:shadow-md'}
|
|
84
|
+
{readonly ? '' : 'hover:shadow-md'}
|
|
85
|
+
{help?.info ? 'pl-8' : ''}
|
|
86
|
+
{copyButton || type === 'password' || type === 'number' ? 'pr-8' : ''}
|
|
87
|
+
{componentClass}"
|
|
88
|
+
style="background: color-mix(in srgb, var(--bg-color), var(--back-color) 70%);"
|
|
89
|
+
id={id.value}
|
|
90
|
+
placeholder={help?.placeholder}
|
|
91
|
+
{disabled}
|
|
92
|
+
{autocomplete}
|
|
93
|
+
oninput={(e) => handleInputChange((e.currentTarget as HTMLInputElement).value)}
|
|
94
|
+
type={type === 'password' ? (showPassword ? 'text' : 'password') : type === 'number' ? 'number' : 'text'}
|
|
95
|
+
{maxlength}
|
|
96
|
+
min={number?.minNum}
|
|
97
|
+
max={number?.maxNum}
|
|
98
|
+
step={number?.step}
|
|
99
|
+
{readonly}
|
|
100
|
+
/>
|
|
101
|
+
{:else if type === 'text-area'}
|
|
102
|
+
<textarea
|
|
103
|
+
bind:value
|
|
104
|
+
class="h-full w-full resize-y rounded-2xl border border-[var(--border-color)] px-2 py-1 text-center font-mono transition-all duration-300 outline-none focus:border-blue-400
|
|
105
|
+
{isValid ? 'border-[var(--border-color)]' : '!border-red-400 shadow-[0_0_6px_var(--red-color)]'}
|
|
106
|
+
{disabled ? 'opacity-50' : 'hover:shadow-md'}
|
|
107
|
+
{readonly ? '' : 'hover:shadow-md'}
|
|
108
|
+
{help?.info ? 'pl-8' : ''}
|
|
109
|
+
{copyButton ? 'pr-8' : ''}
|
|
110
|
+
{componentClass}"
|
|
111
|
+
style="background: color-mix(in srgb, var(--bg-color), var(--back-color) 70%);"
|
|
112
|
+
id={id.value}
|
|
113
|
+
{disabled}
|
|
114
|
+
{maxlength}
|
|
115
|
+
rows={textareaRows}
|
|
116
|
+
placeholder={help?.placeholder}
|
|
117
|
+
{readonly}
|
|
118
|
+
oninput={(e) => handleInputChange((e.currentTarget as HTMLTextAreaElement).value)}
|
|
119
|
+
></textarea>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
{#if type === 'password' && !disabled}
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
class="absolute right-2 flex cursor-pointer border-none bg-transparent"
|
|
126
|
+
onclick={() => (showPassword = !showPassword)}
|
|
127
|
+
aria-label={showPassword ? 'Скрыть пароль' : 'Показать пароль'}
|
|
128
|
+
>
|
|
129
|
+
{#if showPassword}
|
|
130
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24">
|
|
131
|
+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
|
132
|
+
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0" />
|
|
133
|
+
<path d="M21 12q-3.6 6-9 6t-9-6q3.6-6 9-6t9 6" />
|
|
134
|
+
</g>
|
|
135
|
+
</svg>
|
|
136
|
+
{:else}
|
|
137
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24">
|
|
138
|
+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
|
139
|
+
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
|
140
|
+
<path d="M16.681 16.673A8.7 8.7 0 0 1 12 18q-5.4 0-9-6q1.908-3.18 4.32-4.674m2.86-1.146A9 9 0 0 1 12 6q5.4 0 9 6q-1 1.665-2.138 2.87M3 3l18 18" />
|
|
141
|
+
</g>
|
|
142
|
+
</svg>
|
|
143
|
+
{/if}
|
|
144
|
+
</button>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
147
|
+
{#if copyButton && (type === 'text' || type === 'text-area')}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="absolute right-2 flex cursor-pointer border-none bg-transparent {type === 'text-area' ? 'top-2' : ''}"
|
|
151
|
+
onclick={(e) => {
|
|
152
|
+
e.preventDefault()
|
|
153
|
+
navigator.clipboard.writeText(value as string)
|
|
154
|
+
isCopied = true
|
|
155
|
+
setTimeout(() => (isCopied = false), 1000)
|
|
156
|
+
}}
|
|
157
|
+
aria-label="Копировать текст"
|
|
158
|
+
>
|
|
159
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 24 24">
|
|
160
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5">
|
|
161
|
+
<path
|
|
162
|
+
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"
|
|
163
|
+
/>
|
|
164
|
+
<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" />
|
|
165
|
+
</g>
|
|
166
|
+
</svg>
|
|
167
|
+
</button>
|
|
168
|
+
|
|
169
|
+
{#if isCopied}
|
|
170
|
+
<div
|
|
171
|
+
class="absolute top-1/2 right-10 -translate-y-1/2 transform rounded-md bg-[var(--green-color)] px-2 py-1 text-sm shadow-lg"
|
|
172
|
+
transition:fly={{ x: 10, duration: 200 }}
|
|
173
|
+
>
|
|
174
|
+
{$t('component.input.copy')}
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
179
|
+
{#if type === 'number' && !readonly && !disabled}
|
|
180
|
+
<div class="absolute right-0 flex h-full w-8 flex-col items-center justify-center rounded-r-2xl border-l border-[var(--border-color)]">
|
|
181
|
+
<button
|
|
182
|
+
class="flex h-1/2 w-full items-center rounded-tr-2xl border-b border-[var(--border-color)] pl-2 transition-colors duration-150 hover:bg-[var(--gray-color)]/30 active:bg-[var(--gray-color)]/10"
|
|
183
|
+
onclick={() => {
|
|
184
|
+
if (!number.maxNum || !number.step) return
|
|
185
|
+
if (Number(value) + number.step >= number.maxNum) {
|
|
186
|
+
value = number.maxNum
|
|
187
|
+
onUpdate?.(value as number)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
value = Number(value) + (number.step ?? 1)
|
|
191
|
+
onUpdate?.(value as number)
|
|
192
|
+
}}
|
|
193
|
+
aria-label="Увеличить">+</button
|
|
194
|
+
>
|
|
195
|
+
|
|
196
|
+
<button
|
|
197
|
+
class="flex h-1/2 w-full items-center rounded-br-2xl pl-2 transition-colors duration-150 hover:bg-[var(--gray-color)]/30 active:bg-[var(--gray-color)]/10"
|
|
198
|
+
onclick={() => {
|
|
199
|
+
if (number.minNum === null || number.minNum === undefined || !number.step) return
|
|
200
|
+
if (Number(value) - number.step <= number.minNum) {
|
|
201
|
+
value = number.minNum
|
|
202
|
+
onUpdate?.(value as number)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
value = Number(value) - (number.step ?? 1)
|
|
206
|
+
onUpdate?.(value as number)
|
|
207
|
+
}}
|
|
208
|
+
aria-label="Уменьшить">−</button
|
|
209
|
+
>
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
|
|
213
|
+
{#if help.info}
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
class="button-info absolute left-2 flex border-none bg-transparent {type === 'text-area' ? 'top-2' : ''} {disabled ? 'opacity-50' : 'cursor-pointer'}"
|
|
217
|
+
onclick={() => (showInfo = !showInfo)}
|
|
218
|
+
aria-label={showInfo ? 'Скрыть инфо' : 'Показать инфо'}
|
|
219
|
+
>
|
|
220
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="1.5rem" width="1.5rem" viewBox="0 0 24 24">
|
|
221
|
+
<path
|
|
222
|
+
fill="currentColor"
|
|
223
|
+
d="M12 16.5q.214 0 .357-.144T12.5 16v-4.5q0-.213-.144-.356T11.999 11t-.356.144t-.143.356V16q0 .213.144.356t.357.144M12 9.577q.262 0 .439-.177t.176-.438t-.177-.439T12 8.346t-.438.177t-.177.439t.177.438t.438.177M12.003 21q-1.867 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"
|
|
224
|
+
/>
|
|
225
|
+
</svg>
|
|
226
|
+
</button>
|
|
227
|
+
|
|
228
|
+
{#if showInfo}
|
|
229
|
+
<div
|
|
230
|
+
transition:fly={{ x: -15, duration: 250 }}
|
|
231
|
+
class="info-container absolute z-50 w-auto rounded px-2 py-1 shadow-lg"
|
|
232
|
+
style="left: 2.5rem; top: 50%; transform: translateY(-50%); background: color-mix(in srgb, var(--yellow-color) 20%, var(--back-color));"
|
|
233
|
+
>
|
|
234
|
+
{help?.info}
|
|
235
|
+
</div>
|
|
236
|
+
{/if}
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|