poe-svelte-ui-lib 1.28.6 → 1.30.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/FileAttach/FileAttach.svelte +14 -4
- package/dist/FileAttach/README.md +2 -3
- package/dist/ProgressBar/ProgressBarProps.svelte +13 -14
- package/dist/ProgressBar/README.md +2 -2
- package/dist/Switch/README.md +1 -1
- package/dist/Switch/Switch.svelte +6 -6
- package/dist/Switch/SwitchProps.svelte +12 -10
- package/dist/Table/README.md +9 -29
- package/dist/Table/Table.svelte +4 -2
- package/dist/Tabs/README.md +23 -23
- package/dist/Tabs/Tabs.svelte +1 -6
- package/dist/Tabs/TabsProps.svelte +19 -21
- package/dist/Widget/Widget.svelte +1 -1
- package/dist/WidgetDeviceInfo/README.md +14 -6
- package/dist/WidgetDeviceInfo/WidgetDeviceInfo.svelte +20 -1
- package/dist/WidgetDeviceInfo/WidgetDeviceInfoProps.svelte +15 -2
- package/dist/WidgetWiFi/WidgetWiFi.svelte +15 -1
- package/dist/locales/translations.js +37 -3
- package/dist/options.js +7 -3
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
}: IFileAttachProps = $props()
|
|
18
18
|
|
|
19
19
|
let ID = $derived(`${id}-${crypto.randomUUID().slice(0, 6)}`)
|
|
20
|
-
let selectedFile = $state<File | null>(null)
|
|
21
20
|
let previewUrl = $derived(currentImage ? (currentImage.startsWith("data:") ? currentImage : `data:image/png;base64,${currentImage}`) : null)
|
|
22
21
|
let fileName = $state("")
|
|
23
22
|
|
|
23
|
+
/* Blob-URL для мгновенного превью выбранного файла до того, как onChange/родитель
|
|
24
|
+
подтвердит загрузку и обновит currentImage. Предыдущий blob обязательно отзывается
|
|
25
|
+
перед созданием нового и при размонтировании — иначе течёт память при частой смене файла. */
|
|
26
|
+
let objectUrl: string | null = null
|
|
27
|
+
$effect(() => () => {
|
|
28
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
|
29
|
+
})
|
|
30
|
+
|
|
24
31
|
const handleFileChange = (event: Event) => {
|
|
25
32
|
const input = event.target as HTMLInputElement
|
|
26
33
|
if (!input.files || input.files.length === 0) {
|
|
@@ -29,10 +36,13 @@
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const file = input.files[0]
|
|
32
|
-
selectedFile = file
|
|
33
39
|
fileName = file.name
|
|
34
40
|
|
|
35
|
-
if (file.type.startsWith("image/"))
|
|
41
|
+
if (file.type.startsWith("image/")) {
|
|
42
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
|
43
|
+
objectUrl = URL.createObjectURL(file)
|
|
44
|
+
previewUrl = objectUrl
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
onChange(event, file)
|
|
38
48
|
}
|
|
@@ -64,7 +74,7 @@
|
|
|
64
74
|
>
|
|
65
75
|
{#if previewUrl || currentImage}
|
|
66
76
|
<img
|
|
67
|
-
src={previewUrl
|
|
77
|
+
src={previewUrl}
|
|
68
78
|
alt="Preview"
|
|
69
79
|
class={`
|
|
70
80
|
h-full w-full
|
|
@@ -97,15 +97,14 @@
|
|
|
97
97
|
|
|
98
98
|
### Реактивность
|
|
99
99
|
|
|
100
|
-
- Переменная `
|
|
101
|
-
- Переменная `previewUrl` вычисляет URL для предварительного просмотра изображения
|
|
100
|
+
- Переменная `previewUrl` вычисляет URL для предварительного просмотра изображения (из `currentImage` или из только что выбранного файла)
|
|
102
101
|
- Переменная `fileName` хранит имя выбранного файла
|
|
103
102
|
- Объект `ID` генерирует уникальный идентификатор для элемента ввода
|
|
104
103
|
|
|
105
104
|
### Сторы и зависимости
|
|
106
105
|
|
|
107
106
|
- Используется стор `twMerge` для объединения Tailwind CSS классов
|
|
108
|
-
- Используется `URL.createObjectURL` для
|
|
107
|
+
- Используется `URL.createObjectURL` для мгновенного превью выбранного файла — предыдущий blob-URL отзывается через `URL.revokeObjectURL` перед созданием нового и при размонтировании компонента
|
|
109
108
|
|
|
110
109
|
### Директивы
|
|
111
110
|
|
|
@@ -93,8 +93,9 @@
|
|
|
93
93
|
wrapperClass="min-w-28 flex-1"
|
|
94
94
|
value={progress.name}
|
|
95
95
|
onUpdate={(value) => {
|
|
96
|
-
const progresses =
|
|
97
|
-
|
|
96
|
+
const progresses = (component.properties?.items || []).map((p: { name?: string; class?: string }, i: number) =>
|
|
97
|
+
i === index ? { ...p, name: value } : p,
|
|
98
|
+
)
|
|
98
99
|
updateProperty("items", progresses, component, onPropertyChange)
|
|
99
100
|
}}
|
|
100
101
|
/>
|
|
@@ -106,8 +107,9 @@
|
|
|
106
107
|
options={$optionsStore.COLOR_OPTIONS.filter((option) => option.value !== "bg-max")}
|
|
107
108
|
value={findColorOption($optionsStore.COLOR_OPTIONS, progress.class)}
|
|
108
109
|
onUpdate={(value) => {
|
|
109
|
-
const progresses =
|
|
110
|
-
|
|
110
|
+
const progresses = (component.properties?.items || []).map((p: { name?: string; class?: string }, i: number) =>
|
|
111
|
+
i === index ? { ...p, class: (value as UI.IOption).value } : p,
|
|
112
|
+
)
|
|
111
113
|
updateProperty("items", progresses, component, onPropertyChange)
|
|
112
114
|
}}
|
|
113
115
|
/>
|
|
@@ -118,16 +120,13 @@
|
|
|
118
120
|
value={(component.properties.value || [])[index]?.Value}
|
|
119
121
|
type="number"
|
|
120
122
|
onUpdate={(value) => {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
})),
|
|
129
|
-
]
|
|
130
|
-
progresses[index].Value = value
|
|
123
|
+
const base =
|
|
124
|
+
component.properties?.value ||
|
|
125
|
+
component.properties?.items.map((item: { name?: string; class?: string }, i: number) => ({
|
|
126
|
+
Name: item.name,
|
|
127
|
+
Value: i == index ? value : 0,
|
|
128
|
+
}))
|
|
129
|
+
const progresses = base.map((p: { Name?: string; Value?: number }, i: number) => (i === index ? { ...p, Value: value } : p))
|
|
131
130
|
updateProperty("value", progresses, component, onPropertyChange)
|
|
132
131
|
}}
|
|
133
132
|
/>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<script>
|
|
37
37
|
import * as UI from "poe-svelte-ui-lib"
|
|
38
38
|
|
|
39
|
-
let progressBarValue = 40
|
|
39
|
+
let progressBarValue = [{ Value: 40 }]
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
<UI.ProgressBar value={progressBarValue} wrapperClass="bg-red" />
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<script>
|
|
49
49
|
import * as UI from "poe-svelte-ui-lib"
|
|
50
50
|
|
|
51
|
-
let progressBarValue = 65
|
|
51
|
+
let progressBarValue = [{ Value: 65 }]
|
|
52
52
|
</script>
|
|
53
53
|
|
|
54
54
|
<UI.ProgressBar value={progressBarValue} type="vertical" wrapperClass="bg-red h-50" />
|
package/dist/Switch/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| `label.captionLeft` | `string` | `""` | Левая подпись для горизонтального типа (кликабельна, устанавливает значение 0 без `bitMode`) |
|
|
19
19
|
| `label.captionRight` | `string` | `""` | Правая подпись для горизонтального типа (кликабельна, устанавливает значение 1 без `bitMode`) |
|
|
20
20
|
| `hiddenInfo` | `string` | `""` | Текст всплывающей подсказки для режима `checkbox`: отображается при наведении на чекбокс |
|
|
21
|
-
| `options` | `IOption<number>[]` | `[]` | Массив опций для
|
|
21
|
+
| `options` | `IOption<number>[]` | `[]` | Массив опций для переключателя; для `type="checkbox"` можно не передавать (используется как простой булев тумблер) |
|
|
22
22
|
| `bitMode` | `boolean` | `false` | Режим битовых флагов: при `true` значение интерпретируется как битовая маска, каждая опция управляет отдельным битом через побитовые операции; при `false` — простой переключатель 0/1 |
|
|
23
23
|
| `type` | `"horizontal" \| "vertical" \| "checkbox"` | `"horizontal"` | Тип отображения: `horizontal` — горизонтальный слайдер, `vertical` — вертикальный слайдер, `checkbox` — стандартный чекбокс с галочкой и поддержкой `hiddenInfo` |
|
|
24
24
|
| `value` | `number` | `undefined` | Текущее значение переключателя: в обычном режиме — 0 или 1, в `bitMode` — битовая маска; поддерживает двустороннее связывание (`$bindable`) |
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const handleToggle = (index: number) => {
|
|
42
|
-
if (localOptions[index]
|
|
43
|
-
value = ((value ?? 0) ^ (1 << (bitMode ? (localOptions[index]
|
|
42
|
+
if (localOptions[index]?.disabled) return
|
|
43
|
+
value = ((value ?? 0) ^ (1 << (bitMode ? (localOptions[index]?.value ?? 0) : 0))) >>> 0
|
|
44
44
|
onChange(value)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const handleCaptionClick = (newValue: number) => {
|
|
48
|
-
if (localOptions[0]
|
|
48
|
+
if (localOptions[0]?.disabled || value === newValue) return
|
|
49
49
|
value = newValue
|
|
50
50
|
onChange(newValue)
|
|
51
51
|
}
|
|
@@ -134,14 +134,14 @@
|
|
|
134
134
|
class={twMerge(
|
|
135
135
|
"relative flex items-center justify-center gap-2",
|
|
136
136
|
wrapperClass,
|
|
137
|
-
options[0]
|
|
137
|
+
options[0]?.class?.split(" ").find((cl) => cl.startsWith("bg-")),
|
|
138
138
|
)}
|
|
139
139
|
>
|
|
140
140
|
<input
|
|
141
141
|
id={ID}
|
|
142
142
|
type="checkbox"
|
|
143
143
|
checked={checkedOptions[0]}
|
|
144
|
-
disabled={localOptions[0]
|
|
144
|
+
disabled={localOptions[0]?.disabled ?? false}
|
|
145
145
|
class="
|
|
146
146
|
relative size-5 shrink-0 cursor-pointer appearance-none rounded-[6px] border border-(--border-color)
|
|
147
147
|
bg-(--field-color) transition-colors duration-150 after:origin-center after:opacity-0
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
/>
|
|
165
165
|
<Tooltip show={showInfo} text={hiddenInfo} side="top" />
|
|
166
166
|
{#if label.name}
|
|
167
|
-
<label for={ID} class={twMerge(`text-[13px] select-none ${options[0]
|
|
167
|
+
<label for={ID} class={twMerge(`text-[13px] select-none ${options[0]?.disabled ? "text-(--faint-color)" : "cursor-pointer"}`, label.class)}>
|
|
168
168
|
{label.name}
|
|
169
169
|
</label>
|
|
170
170
|
{/if}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}>()
|
|
22
22
|
const DeviceVariables = getContext<{ id: string; value: string; name: string }[]>("DeviceVariables")
|
|
23
23
|
let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
|
|
24
|
-
let initialColor = $derived(findColorOption($optionsStore.COLOR_OPTIONS, component.properties.options[0]
|
|
24
|
+
let initialColor = $derived(findColorOption($optionsStore.COLOR_OPTIONS, component.properties.options?.[0]?.class))
|
|
25
25
|
const initialAlign = $derived(
|
|
26
26
|
$optionsStore.TEXT_ALIGN_OPTIONS.find((a) =>
|
|
27
27
|
(a.value as string).includes(component.properties.label?.class?.split(" ").find((cls: string) => cls.startsWith("text-"))),
|
|
@@ -156,15 +156,17 @@
|
|
|
156
156
|
}}
|
|
157
157
|
/>
|
|
158
158
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
{#if (component.properties.options?.length ?? 0) > 1}
|
|
160
|
+
<UI.Button
|
|
161
|
+
wrapperClass="w-8 shrink-0"
|
|
162
|
+
content={{ icon: ButtonDelete }}
|
|
163
|
+
onClick={() => {
|
|
164
|
+
const options = [...(component.properties?.options || [])]
|
|
165
|
+
options.splice(index, 1)
|
|
166
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
{/if}
|
|
168
170
|
</div>
|
|
169
171
|
{/each}
|
|
170
172
|
</PropsGroup>
|
package/dist/Table/README.md
CHANGED
|
@@ -127,51 +127,31 @@
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const columns = [
|
|
130
|
-
{ label: { name: "ID" },
|
|
130
|
+
{ label: { name: "ID" }, width: "5%", align: "center", content: [{ type: "text", data: { key: "id", sortable: true } }] },
|
|
131
131
|
{
|
|
132
132
|
label: { name: "Image" },
|
|
133
|
-
key: "imageUrl",
|
|
134
133
|
width: "20%",
|
|
135
|
-
image: {
|
|
136
|
-
src: (row: ITableRow) => row.imageUrl,
|
|
137
|
-
alt: "Image",
|
|
138
|
-
width: "5rem",
|
|
139
|
-
height: "5rem",
|
|
140
|
-
},
|
|
134
|
+
content: [{ type: "image", data: { src: (row: ITableRow) => row.imageUrl, alt: "Image", width: "5rem", height: "5rem" } }],
|
|
141
135
|
},
|
|
142
|
-
{ label: { name: "Name" },
|
|
136
|
+
{ label: { name: "Name" }, width: "12%", content: [{ type: "text", data: { key: "name", sortable: true } }] },
|
|
143
137
|
{
|
|
144
138
|
label: { name: "Status" },
|
|
145
|
-
key: "status",
|
|
146
139
|
width: "10%",
|
|
147
|
-
|
|
148
|
-
truncated: true,
|
|
149
|
-
formatting: (text) => {
|
|
150
|
-
if (text === "online") {
|
|
151
|
-
return "1"
|
|
152
|
-
} else return "2"
|
|
153
|
-
},
|
|
154
|
-
},
|
|
140
|
+
content: [{ type: "text", data: { key: "status", truncated: true, formatting: (text) => (text === "online" ? "🟢" : "⚪") } }],
|
|
155
141
|
},
|
|
156
142
|
{
|
|
157
143
|
label: { name: "Last Active" },
|
|
158
|
-
key: "lastActive",
|
|
159
144
|
width: "auto",
|
|
160
|
-
sortable: true,
|
|
161
|
-
overflow: { truncated: true, copy: true, modal: true },
|
|
145
|
+
content: [{ type: "text", data: { key: "lastActive", sortable: true, truncated: true, copy: true, modal: true } }],
|
|
162
146
|
},
|
|
163
147
|
{
|
|
164
148
|
label: { name: "Actions" },
|
|
165
|
-
key: "action",
|
|
166
149
|
width: "15%",
|
|
167
150
|
align: "center",
|
|
168
|
-
|
|
169
|
-
type: "
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{ name: "Action 2 (name)", class: "bg-green", onClick: (row) => console.log(row.name) },
|
|
173
|
-
],
|
|
174
|
-
},
|
|
151
|
+
content: [
|
|
152
|
+
{ type: "button", data: { name: "Action 1", class: "bg-blue", eventHandler: { Header: "SET", Argument: "save", Variables: ["name"] } } },
|
|
153
|
+
{ type: "button", data: { name: "Action 2", class: "bg-green", onClick: (row) => console.log(row.name) } },
|
|
154
|
+
],
|
|
175
155
|
},
|
|
176
156
|
]
|
|
177
157
|
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
let isAutoscroll = $state(true)
|
|
31
31
|
let container: HTMLElement | null = $state(null)
|
|
32
32
|
let buffer: any[] = $state([])
|
|
33
|
+
/* body типизирован как T[] | T | null — нормализуем в массив один раз для рендера */
|
|
34
|
+
let bodyArray = $derived(body == null ? [] : Array.isArray(body) ? body : [body])
|
|
33
35
|
|
|
34
36
|
let isDropdownOpen: { x: number; y: number; index: number } | null = $state(null)
|
|
35
37
|
let selectSlideDuration: number = $state(250)
|
|
@@ -290,8 +292,8 @@
|
|
|
290
292
|
</button>
|
|
291
293
|
{/if}
|
|
292
294
|
|
|
293
|
-
{#if
|
|
294
|
-
{@const rows = dataBuffer.stashData ? buffer.slice(-(dataBuffer.bufferSize ?? 10)) :
|
|
295
|
+
{#if bodyArray.length > 0 || buffer.length > 0}
|
|
296
|
+
{@const rows = dataBuffer.stashData ? buffer.slice(-(dataBuffer.bufferSize ?? 10)) : bodyArray.filter((row: any) => Object.entries(row).length != 0)}
|
|
295
297
|
<!-- Table Body с прокруткой -->
|
|
296
298
|
<div class="relative flex-1 overflow-y-auto bg-(--back-color)" bind:this={container} onscroll={handleScroll}>
|
|
297
299
|
<div class="min-w-0" style={`height: ${dataBuffer.visibleRows && tableHeight && rows.length > dataBuffer.visibleRows ? `${tableHeight}px` : ""};`}>
|
package/dist/Tabs/README.md
CHANGED
|
@@ -6,24 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
## Пропсы
|
|
8
8
|
|
|
9
|
-
| Название | Тип | По умолчанию | Описание
|
|
10
|
-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
11
|
-
| `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента
|
|
12
|
-
| `wrapperClass` | `string` | `"
|
|
13
|
-
| `size` | `object` | `-` | Размеры сетки содержимого активной вкладки
|
|
14
|
-
| `size.width` | `number` | `12` | Количество колонок в сетке содержимого
|
|
15
|
-
| `size.height` | `number` | `6` | Количество строк в сетке содержимого
|
|
16
|
-
| `activeTab` | `number` | `0` | Индекс активной вкладки: изменение значения переключает вкладку программно
|
|
17
|
-
| `items` | `{ name?: string; icon?: ConstructorOfATypedSvelteComponent \| string; class?: string; children?: Snippet; onClick?: () => {}; disabled?: boolean }[]` | `[{ name: "tab 1", icon: "", class: "" }, { name: "tab 2", icon: "", class: "" }]` | Массив элементов вкладок
|
|
18
|
-
| `items[].name` | `string` | `undefined` | Название (заголовок) вкладки
|
|
19
|
-
| `items[].icon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка вкладки (SVG строка или Svelte-компонент)
|
|
20
|
-
| `items[].class` | `string` | `undefined` | CSS-классы для стилизации вкладки;
|
|
21
|
-
| `items[].children` | `Snippet` | `undefined` | Слот для содержимого конкретной вкладки: используется, если не переданы общий `children` или `Components`
|
|
22
|
-
| `items[].onClick` | `() => {}` | `undefined` | Обработчик клика по вкладке
|
|
23
|
-
| `items[].disabled` | `boolean` | `undefined` | Состояние отключения вкладки
|
|
24
|
-
| `children` | `Snippet<[any]>` | `undefined` | Слот для динамического рендеринга контента активной вкладки; принимает `items[currentTabIndex]`
|
|
25
|
-
| `apiArray` | `UIComponent[]` | `[]` | Массив данных компонентов для отображения через `Components`
|
|
26
|
-
| `Components` | `Snippet<[component: UIComponent, fixedHeight: boolean]>` | `undefined` | Сниппет для отображения динамических компонентов из `apiArray`
|
|
9
|
+
| Название | Тип | По умолчанию | Описание |
|
|
10
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
|
|
12
|
+
| `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для внутренней обёртки полосы вкладок (например `justify-*`) |
|
|
13
|
+
| `size` | `object` | `-` | Размеры сетки содержимого активной вкладки |
|
|
14
|
+
| `size.width` | `number` | `12` | Количество колонок в сетке содержимого |
|
|
15
|
+
| `size.height` | `number` | `6` | Количество строк в сетке содержимого |
|
|
16
|
+
| `activeTab` | `number` | `0` | Индекс активной вкладки: изменение значения переключает вкладку программно |
|
|
17
|
+
| `items` | `{ name?: string; icon?: ConstructorOfATypedSvelteComponent \| string; class?: string; children?: Snippet; onClick?: () => {}; disabled?: boolean }[]` | `[{ name: "tab 1", icon: "", class: "" }, { name: "tab 2", icon: "", class: "" }]` | Массив элементов вкладок |
|
|
18
|
+
| `items[].name` | `string` | `undefined` | Название (заголовок) вкладки |
|
|
19
|
+
| `items[].icon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка вкладки (SVG строка или Svelte-компонент) |
|
|
20
|
+
| `items[].class` | `string` | `undefined` | CSS-классы для стилизации вкладки; `flex-row`/`flex-col`/`flex-row-reverse`/`flex-col-reverse` — позиция иконки относительно текста; `flex-1` — вкладка растягивается на равную долю ширины (учитывает отступы между кнопками), `w-auto` — ширина по контенту |
|
|
21
|
+
| `items[].children` | `Snippet` | `undefined` | Слот для содержимого конкретной вкладки: используется, если не переданы общий `children` или `Components` |
|
|
22
|
+
| `items[].onClick` | `() => {}` | `undefined` | Обработчик клика по вкладке |
|
|
23
|
+
| `items[].disabled` | `boolean` | `undefined` | Состояние отключения вкладки |
|
|
24
|
+
| `children` | `Snippet<[any]>` | `undefined` | Слот для динамического рендеринга контента активной вкладки; принимает `items[currentTabIndex]` |
|
|
25
|
+
| `apiArray` | `UIComponent[]` | `[]` | Массив данных компонентов для отображения через `Components` |
|
|
26
|
+
| `Components` | `Snippet<[component: UIComponent, fixedHeight: boolean]>` | `undefined` | Сниппет для отображения динамических компонентов из `apiArray` |
|
|
27
27
|
|
|
28
28
|
## События
|
|
29
29
|
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
{/snippet}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
### Вкладки
|
|
81
|
+
### Вкладки равной ширины
|
|
82
82
|
|
|
83
83
|
```svelte
|
|
84
84
|
<script>
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
|
|
88
88
|
<UI.Tabs
|
|
89
89
|
items={[
|
|
90
|
-
{ name: "Tab1", class: "
|
|
91
|
-
{ name: "Tab2", class: "
|
|
92
|
-
{ name: "Tab3", class: "
|
|
93
|
-
{ name: "Tab4", class: "
|
|
90
|
+
{ name: "Tab1", class: "flex-1" },
|
|
91
|
+
{ name: "Tab2", class: "flex-1" },
|
|
92
|
+
{ name: "Tab3", class: "flex-1" },
|
|
93
|
+
{ name: "Tab4", class: "flex-1" },
|
|
94
94
|
]}
|
|
95
95
|
children={Tab}
|
|
96
96
|
size={{ height: 1, width: 1 }}
|
package/dist/Tabs/Tabs.svelte
CHANGED
|
@@ -52,11 +52,6 @@
|
|
|
52
52
|
item.class,
|
|
53
53
|
)}
|
|
54
54
|
disabled={item.disabled}
|
|
55
|
-
style="width: {item.class
|
|
56
|
-
?.split(' ')
|
|
57
|
-
.find((cls: string) => cls.startsWith('w-'))
|
|
58
|
-
?.replace('w-[', '')
|
|
59
|
-
.slice(0, -1)};"
|
|
60
55
|
onclick={() => {
|
|
61
56
|
currentTabIndex = index
|
|
62
57
|
if (item.onClick) item.onClick()
|
|
@@ -93,7 +88,7 @@
|
|
|
93
88
|
style="grid-template-columns: repeat({size.width || 1}, minmax(0, 1fr)); grid-template-rows: repeat({size.height || 1}, auto);"
|
|
94
89
|
>
|
|
95
90
|
{#if Components}
|
|
96
|
-
{#each (apiArray ?? []).filter((c) => c.id.
|
|
91
|
+
{#each (apiArray ?? []).filter((c) => c.id.match(/(\d+)$/)?.[1] === `${currentTabIndex}`) as comp}
|
|
97
92
|
{@render Components(comp, false)}
|
|
98
93
|
{/each}
|
|
99
94
|
{:else if children}
|
|
@@ -25,24 +25,23 @@
|
|
|
25
25
|
|
|
26
26
|
const initialColor = $derived(findColorOption($optionsStore.COLOR_OPTIONS, component.properties.wrapperClass))
|
|
27
27
|
|
|
28
|
-
const initialPosition = $derived(
|
|
29
|
-
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
),
|
|
36
|
-
),
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
const initialWidth = $derived(() => {
|
|
40
|
-
let width = component.properties.items.find((item: IOption) => item.class?.split(" ").find((cls: string) => cls.startsWith("w-")))
|
|
41
|
-
if (!width) {
|
|
42
|
-
return 0
|
|
43
|
-
} else if (width.class.includes("w-auto")) return 0
|
|
44
|
-
else return 1
|
|
28
|
+
const initialPosition = $derived.by(() => {
|
|
29
|
+
const flexClass = component.properties.items
|
|
30
|
+
?.find((item: IOption) => item.class?.includes("flex-"))
|
|
31
|
+
?.class?.split(" ")
|
|
32
|
+
.find((cls: string) => cls.startsWith("flex-"))
|
|
33
|
+
if (!flexClass) return undefined
|
|
34
|
+
return $optionsStore.ICON_TEXT_POSITION_OPTIONS.find((c) => (c.value as string).includes(flexClass))
|
|
45
35
|
})
|
|
36
|
+
|
|
37
|
+
const initialWidth = $derived(() => (component.properties.items?.some((item: IOption) => item.class?.split(" ").includes("flex-1")) ? 1 : 0))
|
|
38
|
+
|
|
39
|
+
/* "Равные" — flex-1 (flexbox сам поровну распределяет место с учётом gap между кнопками);
|
|
40
|
+
"Авто" — w-auto (ширина по контенту). Раньше "Равные" считалось как w-[X%] через inline
|
|
41
|
+
style в Tabs.svelte — не учитывало gap-6 между кнопками и создавало паразитный горизонтальный
|
|
42
|
+
скролл. flex-1/w-auto — обычные статические Tailwind-классы, отдельный inline style не нужен. */
|
|
43
|
+
const setTabWidthClass = (existingClass: string | undefined, equal: boolean): string =>
|
|
44
|
+
twMerge((existingClass ?? "").replace(/\bflex-1\b|\bw-auto\b/g, "").trim(), equal ? "flex-1" : "w-auto")
|
|
46
45
|
</script>
|
|
47
46
|
|
|
48
47
|
{#snippet TabsIconPosition()}
|
|
@@ -74,7 +73,7 @@
|
|
|
74
73
|
onChange={(value) => {
|
|
75
74
|
component.properties.items.forEach((_item: any, index: number) => {
|
|
76
75
|
const items = [...(component.properties?.items || [])]
|
|
77
|
-
items[index]["class"] =
|
|
76
|
+
items[index]["class"] = setTabWidthClass(items[index].class, Boolean(value))
|
|
78
77
|
updateProperty("items", items, component, onPropertyChange)
|
|
79
78
|
})
|
|
80
79
|
}}
|
|
@@ -98,7 +97,7 @@
|
|
|
98
97
|
}
|
|
99
98
|
const items = [...(component.properties?.items || []), newItem]
|
|
100
99
|
items.forEach((_item: any, index: number) => {
|
|
101
|
-
items[index]["class"] =
|
|
100
|
+
items[index]["class"] = setTabWidthClass(items[index].class, Boolean(initialWidth()))
|
|
102
101
|
updateProperty("items", items, component, onPropertyChange)
|
|
103
102
|
})
|
|
104
103
|
}}
|
|
@@ -170,8 +169,7 @@
|
|
|
170
169
|
const items = [...(component.properties?.items || [])]
|
|
171
170
|
items.splice(index, 1)
|
|
172
171
|
items.forEach((_item: any, index: number) => {
|
|
173
|
-
items[index]["class"] =
|
|
174
|
-
updateProperty("items", items, component, onPropertyChange)
|
|
172
|
+
items[index]["class"] = setTabWidthClass(items[index].class, Boolean(initialWidth()))
|
|
175
173
|
})
|
|
176
174
|
updateProperty("items", items, component, onPropertyChange)
|
|
177
175
|
}}
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
Смарт-виджет информации об устройстве. Как и `WidgetWiFi`, не привязывается к одной произвольной
|
|
6
6
|
переменной устройства — работает с фиксированным набором полей, соответствующих реальной
|
|
7
7
|
прошивочной конвенции (`main_config.c`, группа `CFG`, ProdFactory-ESP): `DevSN`, `DevID`, `DevFW`,
|
|
8
|
-
`RunCnt` — только чтение; `DevName`, `HostName`, `WebUser`, `WebPsw` —
|
|
9
|
-
сохраняются одним пакетом по кнопке "Сохранить". Отдельная кнопка
|
|
10
|
-
заголовком вызывает `onRestart` немедленно (не входит в пакет сохранения
|
|
11
|
-
|
|
8
|
+
`RunCnt`, `StaIP`, `ApIP` — только чтение; `DevName`, `HostName`, `WebUser`, `WebPsw` —
|
|
9
|
+
редактируются локально и сохраняются одним пакетом по кнопке "Сохранить". Отдельная кнопка
|
|
10
|
+
"Перезагрузить" рядом с заголовком вызывает `onRestart` немедленно (не входит в пакет сохранения
|
|
11
|
+
полей). Реальное имя поля устройства для каждого ключа можно переопределить через `keys` (по
|
|
12
|
+
умолчанию совпадает с именем ключа). Компонент ничего не знает про DeviceStore/WebSocket —
|
|
13
|
+
только `value`/`onSave`/`onRestart`.
|
|
12
14
|
|
|
13
15
|
## Пропсы
|
|
14
16
|
|
|
@@ -33,6 +35,8 @@
|
|
|
33
35
|
| `HostName` | `string` | нет | Имя хоста |
|
|
34
36
|
| `WebUser` | `string` | нет | Имя пользователя Web-панели |
|
|
35
37
|
| `WebPsw` | `string` | нет | Пароль Web-панели |
|
|
38
|
+
| `StaIP` | `string` | да | Текущий IP-адрес в режиме STA |
|
|
39
|
+
| `ApIP` | `string` | да | Текущий IP-адрес в режиме AP |
|
|
36
40
|
|
|
37
41
|
## Примеры
|
|
38
42
|
|
|
@@ -55,6 +59,8 @@
|
|
|
55
59
|
HostName: "syp-device-01",
|
|
56
60
|
WebUser: "admin",
|
|
57
61
|
WebPsw: "",
|
|
62
|
+
StaIP: "192.168.1.42",
|
|
63
|
+
ApIP: "192.168.4.1",
|
|
58
64
|
}}
|
|
59
65
|
onSave={handleSave}
|
|
60
66
|
onRestart={() => console.log("Перезагрузка устройства")}
|
|
@@ -70,8 +76,10 @@
|
|
|
70
76
|
| `component` | `UIComponent & { properties: Partial<IWidgetDeviceInfoProps> }` | Объект компонента с его свойствами |
|
|
71
77
|
| `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string }>) => void` | Коллбэк для обновления свойств компонента |
|
|
72
78
|
|
|
73
|
-
Панель
|
|
74
|
-
|
|
79
|
+
Панель содержит заголовок, мастер-цвет (через общий `WidgetAccentPicker.svelte`), префикс
|
|
80
|
+
прошивочной группы (ID), переопределение имени поля устройства для каждого ключа
|
|
81
|
+
`IWidgetDeviceInfoConfig` (по умолчанию совпадает с именем ключа) и команду перезагрузки
|
|
82
|
+
(Header/Argument, по умолчанию `SET`/`Restart`).
|
|
75
83
|
|
|
76
84
|
## Заметки
|
|
77
85
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
DevSN/DevID/DevFW/RunCnt — только чтение; DevName/HostName/WebUser/WebPsw — сохраняются
|
|
5
5
|
одним пакетом. Ничего не знает про DeviceStore/WebSocket — value/onSave/onRestart. -->
|
|
6
6
|
<script lang="ts">
|
|
7
|
+
import { untrack } from "svelte"
|
|
7
8
|
import { slide } from "svelte/transition"
|
|
8
9
|
import { twMerge } from "tailwind-merge"
|
|
9
10
|
import * as UI from ".."
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
HostName: "",
|
|
38
39
|
WebUser: "",
|
|
39
40
|
WebPsw: "",
|
|
41
|
+
StaIP: "",
|
|
42
|
+
ApIP: "",
|
|
40
43
|
},
|
|
41
44
|
)
|
|
42
45
|
|
|
@@ -55,8 +58,20 @@
|
|
|
55
58
|
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
56
59
|
|
|
57
60
|
/* Поля редактируются локально (как Argument: NoSend в реальной GUI) — отправка одним пакетом по кнопке */
|
|
58
|
-
let savedSnapshot = $state(JSON.stringify(info))
|
|
61
|
+
let savedSnapshot = $state(untrack(() => JSON.stringify(info)))
|
|
59
62
|
let isDirty = $derived(JSON.stringify(info) !== savedSnapshot)
|
|
63
|
+
|
|
64
|
+
/* value — живой поток данных с устройства (readonly-поля StaIP/DevFW/RunCnt и т.п. должны
|
|
65
|
+
обновляться постоянно), а не однократный дефолт. Подхватываем свежие данные, только если
|
|
66
|
+
у пользователя нет несохранённых правок редактируемых полей (DevName/HostName/...) —
|
|
67
|
+
иначе входящее значение затёрло бы то, что он ещё не успел сохранить. */
|
|
68
|
+
$effect(() => {
|
|
69
|
+
if (value !== undefined && !untrack(() => isDirty)) {
|
|
70
|
+
info = value
|
|
71
|
+
savedSnapshot = JSON.stringify(info)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
60
75
|
const save = () => {
|
|
61
76
|
onSave?.($state.snapshot(info))
|
|
62
77
|
savedSnapshot = JSON.stringify(info)
|
|
@@ -85,6 +100,10 @@
|
|
|
85
100
|
<UI.Input label={{ name: "Версия ПО" }} value={info.DevFW} readonly />
|
|
86
101
|
<UI.Input label={{ name: "Запусков" }} value={info.RunCnt} readonly />
|
|
87
102
|
</div>
|
|
103
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
104
|
+
<UI.Input label={{ name: "IP-адрес (STA)" }} value={info.StaIP} readonly />
|
|
105
|
+
<UI.Input label={{ name: "IP-адрес (AP)" }} value={info.ApIP} readonly />
|
|
106
|
+
</div>
|
|
88
107
|
</div>
|
|
89
108
|
|
|
90
109
|
<!-- Редактируемые настройки -->
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<!-- $lib/WidgetDeviceInfo/WidgetDeviceInfoProps.svelte — панель пропсов конструктора
|
|
2
|
-
для WidgetDeviceInfo
|
|
2
|
+
для WidgetDeviceInfo: заголовок/мастер-цвет/префикс группы, переопределение имён полей
|
|
3
|
+
устройства (keys) и команда перезагрузки — все с дефолтами, равными текущей прошивочной
|
|
4
|
+
конвенции (см. DEFAULT_PROPS.WidgetDeviceInfo в DevCloud). -->
|
|
3
5
|
<script lang="ts">
|
|
4
6
|
import { T } from "../locales/i18n"
|
|
5
7
|
import * as UI from ".."
|
|
@@ -8,7 +10,18 @@
|
|
|
8
10
|
import PropsGroup from "../PropsGroup.svelte"
|
|
9
11
|
import { optionsStore } from "../options"
|
|
10
12
|
|
|
11
|
-
const DEVICE_INFO_KEYS: (keyof IWidgetDeviceInfoConfig)[] = [
|
|
13
|
+
const DEVICE_INFO_KEYS: (keyof IWidgetDeviceInfoConfig)[] = [
|
|
14
|
+
"DevSN",
|
|
15
|
+
"DevID",
|
|
16
|
+
"DevFW",
|
|
17
|
+
"RunCnt",
|
|
18
|
+
"DevName",
|
|
19
|
+
"HostName",
|
|
20
|
+
"WebUser",
|
|
21
|
+
"WebPsw",
|
|
22
|
+
"StaIP",
|
|
23
|
+
"ApIP",
|
|
24
|
+
]
|
|
12
25
|
|
|
13
26
|
const { component, onPropertyChange } = $props<{
|
|
14
27
|
component: UIComponent & { properties: Partial<IWidgetDeviceInfoProps> }
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
WiFiMode / StaSSID/StaPSK/StaticIP/StaIP/StaMS/StaGW / ApSSID/ApPSK/ApIP/ApMS/ApGW.
|
|
6
6
|
Ничего не знает про DeviceStore/WebSocket — value/onSave, вся отправка на стороне вызывающего. -->
|
|
7
7
|
<script lang="ts">
|
|
8
|
+
import { untrack } from "svelte"
|
|
8
9
|
import { slide, fade, scale } from "svelte/transition"
|
|
9
10
|
import { twMerge } from "tailwind-merge"
|
|
10
11
|
import * as UI from ".."
|
|
@@ -103,8 +104,21 @@
|
|
|
103
104
|
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
104
105
|
|
|
105
106
|
/* Поля редактируются локально (как Argument: NoSend в реальной GUI) — отправка одним пакетом по кнопке */
|
|
106
|
-
let savedSnapshot = $state(JSON.stringify(cfg))
|
|
107
|
+
let savedSnapshot = $state(untrack(() => JSON.stringify(cfg)))
|
|
107
108
|
let isDirty = $derived(JSON.stringify(cfg) !== savedSnapshot)
|
|
109
|
+
|
|
110
|
+
/* value — не однократный дефолт, а живой поток данных с устройства (GUIPreview.svelte
|
|
111
|
+
пересчитывает его на каждое обновление DeviceStore). Подхватываем свежие данные, только
|
|
112
|
+
если у пользователя нет несохранённых правок — иначе входящее значение затирало бы то,
|
|
113
|
+
что он ещё не успел сохранить. untrack(isDirty) — чтобы этот эффект не перезапускался на
|
|
114
|
+
каждое изменение cfg от самого пользователя, только на смену value. */
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (value !== undefined && !untrack(() => isDirty)) {
|
|
117
|
+
cfg = value
|
|
118
|
+
savedSnapshot = JSON.stringify(cfg)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
108
122
|
const commitSave = () => {
|
|
109
123
|
onSave?.($state.snapshot(cfg))
|
|
110
124
|
savedSnapshot = JSON.stringify(cfg)
|
|
@@ -18,9 +18,39 @@ const translations = {
|
|
|
18
18
|
"📶 该组件使用设备 `CFG` 分组下的一组固定字段——无需手动绑定变量。\n\n**📡 客户端模式(STA)**\n- `WiFiMode`、`StaSSID`、`StaPSK`\n- `StaticIP`、`StaIP`、`StaMS`、`StaGW`\n\n**📡 接入点(AP)**\n- `ApSSID`、`ApPSK`\n- `ApIP`、`ApMS`、`ApGW`\n\n🔍 扫描网络会发送 `Header: GET`、`Argument: APsList`——设备应在 `CFG.StaSSID-S` 字段中返回结果。\n\n⚙️ 如果设备使用的分组名称不是 `CFG`,请在组件设置中修改前缀(ID)。",
|
|
19
19
|
],
|
|
20
20
|
"component.info.WidgetDeviceInfo": [
|
|
21
|
-
"ℹ️ Виджет работает с фиксированным набором полей группы `CFG` на устройстве — привязка переменных вручную не нужна.\n\n**🔒 Только чтение**\n- `DevSN`, `DevID`, `DevFW`, `RunCnt`\n\n**✏️ Редактируемые** (сохраняются одним пакетом по кнопке «Сохранить»)\n- `DevName`, `HostName`, `WebUser`, `WebPsw`\n\n🔌 Кнопка «Перезагрузить» отправляет `Argument: Restart`.\n\n⚙️ Если на устройстве используется другое имя группы вместо `CFG`, поменяйте префикс (ID) в настройках виджета.",
|
|
22
|
-
|
|
23
|
-
"ℹ️ 该组件使用设备 `CFG` 分组下的一组固定字段——无需手动绑定变量。\n\n**🔒 只读字段**\n- `DevSN`、`DevID`、`DevFW`、`RunCnt`\n\n**✏️ 可编辑字段**(通过「保存」按钮一次性发送)\n- `DevName`、`HostName`、`WebUser`、`WebPsw`\n\n🔌 「重启」按钮会发送 `Argument: Restart`。\n\n⚙️ 如果设备使用的分组名称不是 `CFG`,请在组件设置中修改前缀(ID)。",
|
|
21
|
+
"ℹ️ Виджет работает с фиксированным набором полей группы `CFG` на устройстве — привязка переменных вручную не нужна.\n\n**🔒 Только чтение**\n- `DevSN`, `DevID`, `DevFW`, `RunCnt`\n- `StaIP`, `ApIP` — текущие IP-адреса в режимах STA/AP\n\n**✏️ Редактируемые** (сохраняются одним пакетом по кнопке «Сохранить»)\n- `DevName`, `HostName`, `WebUser`, `WebPsw`\n\n🔌 Кнопка «Перезагрузить» отправляет `Argument: Restart`.\n\n⚙️ Если на устройстве используется другое имя группы вместо `CFG`, поменяйте префикс (ID) в настройках виджета.",
|
|
22
|
+
'ℹ️ The widget works with a fixed set of fields in the device\'s `CFG` group — no manual variable binding needed.\n\n**🔒 Read-only**\n- `DevSN`, `DevID`, `DevFW`, `RunCnt`\n- `StaIP`, `ApIP` — current IP addresses in STA/AP mode\n\n**✏️ Editable** (saved as one packet via the "Save" button)\n- `DevName`, `HostName`, `WebUser`, `WebPsw`\n\n🔌 The "Restart" button sends `Argument: Restart`.\n\n⚙️ If the device uses a different group name instead of `CFG`, change the prefix (ID) in the widget settings.',
|
|
23
|
+
"ℹ️ 该组件使用设备 `CFG` 分组下的一组固定字段——无需手动绑定变量。\n\n**🔒 只读字段**\n- `DevSN`、`DevID`、`DevFW`、`RunCnt`\n- `StaIP`、`ApIP` —— STA/AP 模式下的当前 IP 地址\n\n**✏️ 可编辑字段**(通过「保存」按钮一次性发送)\n- `DevName`、`HostName`、`WebUser`、`WebPsw`\n\n🔌 「重启」按钮会发送 `Argument: Restart`。\n\n⚙️ 如果设备使用的分组名称不是 `CFG`,请在组件设置中修改前缀(ID)。",
|
|
24
|
+
],
|
|
25
|
+
"component.info.Tabs": [
|
|
26
|
+
"📑 Ширина и позиция иконки задаются через служебные CSS-классы каждой вкладки, а не отдельными полями.\n\n**↔️ Ширина кнопок** (переключатель «Авто/Равные»)\n- `w-auto` — по содержимому\n- `flex-1` — поровну между всеми вкладками\n\n**🔤 Позиция иконки**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Максимум 10 вкладок.",
|
|
27
|
+
'📑 Tab width and icon position are set via each tab\'s internal CSS classes, not separate fields.\n\n**↔️ Button width** ("Auto/Equal" toggle)\n- `w-auto` — sized to content\n- `flex-1` — equal share among all tabs\n\n**🔤 Icon position**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Maximum 10 tabs.',
|
|
28
|
+
"📑 每个标签页的宽度和图标位置通过内部 CSS 类设置,而非独立字段。\n\n**↔️ 按钮宽度**(「自动/均等」切换)\n- `w-auto` —— 按内容自适应\n- `flex-1` —— 在所有标签页间均分\n\n**🔤 图标位置**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ 最多 10 个标签页。",
|
|
29
|
+
],
|
|
30
|
+
"component.info.Switch": [
|
|
31
|
+
"🔘 Один и тот же компонент под тремя разными задачами — тип определяет, что означает `options`.\n\n**↔️/↕️ horizontal / vertical** — обычный слайдер 0/1, `options[0]` задаёт мастер-цвет.\n\n**☑️ checkbox** — простой булев тумблер, `options` можно не задавать.\n\n**🔢 bitMode** — `value` становится битовой маской, каждый элемент `options` управляет одним битом через свою `value` (номер бита 0-31); недоступен вместе с `checkbox`.",
|
|
32
|
+
"🔘 One component, three different jobs — `type` decides what `options` means.\n\n**↔️/↕️ horizontal / vertical** — a plain 0/1 slider, `options[0]` sets the master color.\n\n**☑️ checkbox** — a simple boolean toggle, `options` may be omitted.\n\n**🔢 bitMode** — `value` becomes a bitmask, each `options` entry controls one bit via its `value` (bit number 0-31); not available together with `checkbox`.",
|
|
33
|
+
"🔘 同一个组件承担三种不同职责——`type` 决定 `options` 的含义。\n\n**↔️/↕️ horizontal / vertical** —— 普通的 0/1 滑块,`options[0]` 设置主色。\n\n**☑️ checkbox** —— 简单的布尔开关,可省略 `options`。\n\n**🔢 bitMode** —— `value` 变为位掩码,每个 `options` 项通过自己的 `value`(位号 0-31)控制一个比特位;不能与 `checkbox` 同时使用。",
|
|
34
|
+
],
|
|
35
|
+
"component.info.Table": [
|
|
36
|
+
"📊 Тип `type` определяет поведение буфера строк.\n\n**📋 table** — статическая таблица, `body` заменяется целиком при каждом обновлении.\n\n**📜 logger** — `dataBuffer.stashData` копит входящие строки в буфер (`dataBuffer.bufferSize` — сколько хранить), новые строки добавляются сверху, старые вытесняются.\n\n⚙️ `dataBuffer.visibleRows` ограничивает видимую высоту прокруткой, не количество данных.",
|
|
37
|
+
"📊 The `type` field determines row-buffer behavior.\n\n**📋 table** — a static table, `body` is fully replaced on every update.\n\n**📜 logger** — `dataBuffer.stashData` accumulates incoming rows in a buffer (`dataBuffer.bufferSize` — how many to keep), new rows are added on top and old ones are evicted.\n\n⚙️ `dataBuffer.visibleRows` limits the visible scroll height, not the amount of data.",
|
|
38
|
+
"📊 `type` 字段决定行缓冲区的行为。\n\n**📋 table** —— 静态表格,每次更新时整体替换 `body`。\n\n**📜 logger** —— `dataBuffer.stashData` 将传入的行累积到缓冲区(`dataBuffer.bufferSize` —— 保留多少条),新行添加在顶部,旧行被淘汰。\n\n⚙️ `dataBuffer.visibleRows` 限制可视滚动高度,而非数据量。",
|
|
39
|
+
],
|
|
40
|
+
"component.info.ProgressBar": [
|
|
41
|
+
"📶 `items` и `value` — два независимых массива, связанные позиционно (по индексу, не по имени).\n\n- `items[i]` — статическое оформление полосы: `name` (подпись) и `class` (цвет через `bg-*`)\n- `value[i].Value` — текущее числовое значение этой же полосы (0-100 или в границах `number.minNum`/`maxNum`)\n\n⚠️ Порядок элементов в обоих массивах должен совпадать.",
|
|
42
|
+
"📶 `items` and `value` are two independent arrays linked positionally (by index, not by name).\n\n- `items[i]` — the bar's static appearance: `name` (label) and `class` (color via `bg-*`)\n- `value[i].Value` — the current numeric value of that same bar (0-100 or within `number.minNum`/`maxNum`)\n\n⚠️ The order of elements in both arrays must match.",
|
|
43
|
+
"📶 `items` 和 `value` 是两个通过位置(索引,而非名称)关联的独立数组。\n\n- `items[i]` —— 该进度条的静态外观:`name`(标签)和 `class`(通过 `bg-*` 设置颜色)\n- `value[i].Value` —— 同一进度条的当前数值(0-100 或在 `number.minNum`/`maxNum` 范围内)\n\n⚠️ 两个数组中元素的顺序必须一致。",
|
|
44
|
+
],
|
|
45
|
+
"component.info.FileAttach": [
|
|
46
|
+
"📎 `type` меняет не только внешний вид, но и формат `currentImage`.\n\n**📄 file** — обычная кнопка выбора файла, показывает только имя.\n\n**🖼️ image** — превью-кнопка; `currentImage` ожидается как data-URI (`data:image/...`) или голая base64-строка (тогда PNG-префикс подставляется автоматически). После выбора нового файла показывается его локальное превью — до того, как `onChange` обработает загрузку и обновит `currentImage`.",
|
|
47
|
+
"📎 `type` changes not only the appearance but also the `currentImage` format.\n\n**📄 file** — a plain file-picker button, shows only the file name.\n\n**🖼️ image** — a preview button; `currentImage` is expected to be a data URI (`data:image/...`) or a bare base64 string (the PNG prefix is then added automatically). After picking a new file, its local preview is shown right away — before `onChange` finishes the upload and updates `currentImage`.",
|
|
48
|
+
"📎 `type` 不仅改变外观,还改变 `currentImage` 的格式。\n\n**📄 file** —— 普通文件选择按钮,仅显示文件名。\n\n**🖼️ image** —— 预览按钮;`currentImage` 应为 data URI(`data:image/...`)或纯 base64 字符串(此时会自动添加 PNG 前缀)。选择新文件后会立即显示本地预览——早于 `onChange` 完成上传并更新 `currentImage` 之前。",
|
|
49
|
+
],
|
|
50
|
+
"component.info.Widget": [
|
|
51
|
+
"🔢 Диапазон значения задаётся `settings.number.minNum`/`maxNum` (по умолчанию 0/0). Иконки в `icons.array` распределяются по этому диапазону равномерно — при `icons.cycling: true` перебираются автоматически по таймеру вместо ручного выбора.",
|
|
52
|
+
"🔢 The value range is set by `settings.number.minNum`/`maxNum` (defaults to 0/0). Icons in `icons.array` are spread evenly across this range — with `icons.cycling: true` they cycle automatically on a timer instead of manual selection.",
|
|
53
|
+
"🔢 数值范围由 `settings.number.minNum`/`maxNum` 设置(默认 0/0)。`icons.array` 中的图标在此范围内均匀分布——当 `icons.cycling: true` 时,图标按计时器自动循环,而非手动选择。",
|
|
24
54
|
],
|
|
25
55
|
/* Опции редактора свойств */
|
|
26
56
|
"constructor.props.action.update": ["Обновить", "Update", "更新"],
|
|
@@ -170,6 +200,10 @@ const translations = {
|
|
|
170
200
|
"constructor.props.regexp.colorhex": ["Цвет HEX", "HEX color", "HEX 颜色"],
|
|
171
201
|
"constructor.props.regexp.devid": ["ID устройства", "Device ID", "设备 ID"],
|
|
172
202
|
"constructor.props.regexp.serialnum": ["Серийный номер устройства", "Device serial number", "设备序列号"],
|
|
203
|
+
"constructor.props.regexp.phone": ["Телефон (РФ)", "Phone number (RU)", "电话号码(俄罗斯)"],
|
|
204
|
+
"constructor.props.regexp.mac": ["MAC-адрес", "MAC address", "MAC 地址"],
|
|
205
|
+
"constructor.props.regexp.username": ["Логин (3-16 символов)", "Username (3-16 characters)", "用户名(3-16 个字符)"],
|
|
206
|
+
"constructor.props.regexp.password": ["Пароль (мин. 8, буквы и цифры)", "Password (min. 8, letters and digits)", "密码(至少 8 位,字母和数字)"],
|
|
173
207
|
"constructor.props.min": ["Мин.", "Min.", "最小值"],
|
|
174
208
|
"constructor.props.max": ["Макс.", "Max.", "最大值"],
|
|
175
209
|
"constructor.props.maxnum": ["Максимальное число", "Maximum number", "最大数值"],
|
package/dist/options.js
CHANGED
|
@@ -218,14 +218,18 @@ export const optionsStore = derived(T, ($T) => {
|
|
|
218
218
|
name: $T("constructor.props.regexp.ipv4"),
|
|
219
219
|
},
|
|
220
220
|
{ id: id(), value: "/((^|:)([0-9a-fA-F]{0,4})){1,8}$/", name: $T("constructor.props.regexp.ipv6") },
|
|
221
|
-
{ id: id(), value: "
|
|
221
|
+
{ id: id(), value: "/^(0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20)\\d\\d$/", name: $T("constructor.props.regexp.date") },
|
|
222
222
|
{ id: id(), value: "/^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/", name: $T("constructor.props.regexp.time") },
|
|
223
|
-
{ id: id(), value: "/^[a-zA-Z0-9.!#$%&'*+/=?^_{|}~-]+@[a-zA-Z0-9-]+(
|
|
224
|
-
{ id: id(), value: "
|
|
223
|
+
{ id: id(), value: "/^[a-zA-Z0-9.!#$%&'*+/=?^_{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)+$/", name: $T("constructor.props.regexp.email") },
|
|
224
|
+
{ id: id(), value: "/^https?:\\/\\/[\\w.-]+(?:\\.[a-zA-Z]{2,})+(?:[/?#]\\S*)?$/", name: $T("constructor.props.regexp.url") },
|
|
225
225
|
{ id: id(), value: "/^-?\\d+(\\.\\d+)?$/", name: $T("constructor.props.regexp.numbers") },
|
|
226
226
|
{ id: id(), value: "/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/", name: $T("constructor.props.regexp.colorhex") },
|
|
227
227
|
{ id: id(), value: "/^[0-9A-F]{0,4}$/", name: $T("constructor.props.regexp.devid") },
|
|
228
228
|
{ id: id(), value: "/^[0-9a-fA-F]{4}-[0-9a-fA-F]{24}[:][0-9a-fA-F]{2}$|^$/", name: $T("constructor.props.regexp.serialnum") },
|
|
229
|
+
{ id: id(), value: "/^(\\+7|8)[\\s-]?\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{2}[\\s-]?\\d{2}$/", name: $T("constructor.props.regexp.phone") },
|
|
230
|
+
{ id: id(), value: "/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/", name: $T("constructor.props.regexp.mac") },
|
|
231
|
+
{ id: id(), value: "/^[a-zA-Z0-9_]{3,16}$/", name: $T("constructor.props.regexp.username") },
|
|
232
|
+
{ id: id(), value: "/^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d!@#$%^&*]{8,}$/", name: $T("constructor.props.regexp.password") },
|
|
229
233
|
],
|
|
230
234
|
AUTOCOMPLETE_CONSTRUCTOR_OPTIONS: [
|
|
231
235
|
{ id: id(), value: "on", name: $T("constructor.props.autocomplete.on") },
|
package/dist/types.d.ts
CHANGED