poe-svelte-ui-lib 1.49.0 → 1.51.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/CommonSnippets.svelte +25 -3
- package/dist/Dragging.README.md +10 -0
- package/dist/Dragging.svelte +29 -21
- package/dist/PeriodChart/PeriodChartProps.svelte +98 -75
- package/dist/PeriodChart/README.md +1 -1
- package/dist/ProgressBar/ProgressBarProps.svelte +60 -48
- package/dist/PropsListHeader.svelte +11 -0
- package/dist/PropsListHeader.svelte.d.ts +7 -0
- package/dist/PropsListModal.svelte +1 -1
- package/dist/Select/SelectProps.svelte +51 -40
- package/dist/Switch/README.md +7 -7
- package/dist/Switch/Switch.svelte +1 -1
- package/dist/Switch/SwitchProps.svelte +54 -51
- package/dist/Tabs/TabsProps.svelte +32 -35
- package/dist/dragStyle.d.ts +4 -0
- package/dist/dragStyle.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/locales/translations.js +6 -2
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
import { optionsStore } from "./options"
|
|
6
6
|
import { updateProperty } from "./types"
|
|
7
7
|
import { twMerge } from "tailwind-merge"
|
|
8
|
+
|
|
9
|
+
/* Значок «нет иконки» для компактной кнопки выбора иконки */
|
|
10
|
+
const ICON_PLACEHOLDER = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg>`
|
|
8
11
|
import TextsEditor from "./TextsEditor.svelte"
|
|
9
12
|
|
|
10
13
|
let {
|
|
@@ -235,9 +238,23 @@
|
|
|
235
238
|
icons: { array: [string, string[]][]; selectArray?: false }
|
|
236
239
|
icon: string | string[]
|
|
237
240
|
updateProperty: (icon: string | string[]) => {}
|
|
241
|
+
/* compact — для строк списков в одну линию: квадратная кнопка с текущей иконкой и подсказкой вместо кнопки с текстом */
|
|
242
|
+
compact?: boolean
|
|
238
243
|
})}
|
|
239
|
-
<div class="relative mt-6 flex w-full gap-2">
|
|
240
|
-
|
|
244
|
+
<div class={initialValue.compact ? "relative flex shrink-0 items-center gap-1" : "relative mt-6 flex w-full gap-2"}>
|
|
245
|
+
{#if initialValue.compact}
|
|
246
|
+
<UI.Button
|
|
247
|
+
wrapperClass="w-auto"
|
|
248
|
+
componentClass="size-8 rounded-[10px] p-[6px] bg-max"
|
|
249
|
+
content={{
|
|
250
|
+
icon: typeof initialValue.icon === "string" && initialValue.icon ? initialValue.icon : ICON_PLACEHOLDER,
|
|
251
|
+
info: { text: initialValue.name, side: "top" },
|
|
252
|
+
}}
|
|
253
|
+
onClick={() => (showIconLib = true)}
|
|
254
|
+
/>
|
|
255
|
+
{:else}
|
|
256
|
+
<UI.Button content={{ name: initialValue.name }} componentClass="bg-blue" onClick={() => (showIconLib = true)} />
|
|
257
|
+
{/if}
|
|
241
258
|
{#if showIconLib}
|
|
242
259
|
<UI.Modal bind:isOpen={showIconLib} wrapperClass="w-130">
|
|
243
260
|
{#snippet main()}
|
|
@@ -268,7 +285,12 @@
|
|
|
268
285
|
</UI.Modal>
|
|
269
286
|
{/if}
|
|
270
287
|
{#if initialValue.icon}
|
|
271
|
-
<UI.Button
|
|
288
|
+
<UI.Button
|
|
289
|
+
wrapperClass={initialValue.compact ? "w-auto" : "w-8"}
|
|
290
|
+
componentClass={initialValue.compact ? "size-8 rounded-[10px] p-[7px] bg-transparent" : "p-0.5 bg-red"}
|
|
291
|
+
content={{ icon: CrossIcon }}
|
|
292
|
+
onClick={() => initialValue.updateProperty("")}
|
|
293
|
+
/>
|
|
272
294
|
{/if}
|
|
273
295
|
</div>
|
|
274
296
|
{/snippet}
|
package/dist/Dragging.README.md
CHANGED
|
@@ -72,3 +72,13 @@
|
|
|
72
72
|
- При единственном элементе в `array` (`array.length == 1`) ручка визуально приглушена (`opacity-50`) и перетаскивание не запускается
|
|
73
73
|
- Работает только с `MouseEvent` — нет отдельной поддержки touch/pointer-событий и клавиатурной перестановки (ручка фокусируема как `role="button"`, но `onkeydown` не обрабатывается — известное ограничение доступности)
|
|
74
74
|
- `container`/`elementIndex`/`array` обязательны — компонент не проверяет их наличие и упадёт при обращении к `array.length` в классе, если `array` не передан
|
|
75
|
+
|
|
76
|
+
## Вид перетаскивания
|
|
77
|
+
|
|
78
|
+
Один на библиотеку и приложения — `dragStyle.ts` (`applyDragGhost`, `clearDragGhost`, `createDropPlaceholder`, экспортируются из пакета): перетаскиваемый элемент — фон с заметным оттенком акцента (30%), рамка акцентом и тень, курсор «захват»; место вставки — заливка мягким акцентом (`--accent-soft`) со штриховой рамкой акцентом. Тот же вид — у перетаскивания компонентов на холсте конструктора DevCloud.
|
|
79
|
+
|
|
80
|
+
Соседние элементы раздвигаются плавно: место вставки — первый элемент, чья середина (по раскладке, без текущего сдвига анимации) ниже курсора, поэтому плейсхолдер не прыгает туда-обратно на границе; перестановка плейсхолдера анимируется `animateReorder` (FLIP, 160 мс, отключается при `prefers-reduced-motion`).
|
|
81
|
+
|
|
82
|
+
## Списки в окнах свойств
|
|
83
|
+
|
|
84
|
+
Элементы списков конструктора (вкладки Tabs, варианты Select, биты Switch, шкалы ProgressBar, уровни PeriodChart) — одна строка: ручка, номер и текст элемента («Вариант 2 · Ручной»), поля без собственных подписей, удаление. Подписи колонок один раз сверху — `PropsListHeader.svelte` с тем же шаблоном колонок `grid-cols-[...]`, что и у строк; при пустом списке не показывается. Тексты элементов правятся только в окне «Тексты и переводы», новые элементы создаются без текста-заглушки.
|
package/dist/Dragging.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { tick } from "svelte"
|
|
3
3
|
import MoveVerticalIcon from "./libIcons/MoveVerticalIcon.svelte"
|
|
4
4
|
import { twMerge } from "tailwind-merge"
|
|
5
|
+
import { animateReorder, applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle"
|
|
5
6
|
|
|
6
7
|
let dragState = $state({
|
|
7
8
|
isDragging: false,
|
|
@@ -48,24 +49,19 @@
|
|
|
48
49
|
const element = movedElement.cloneNode(true) as HTMLElement
|
|
49
50
|
const rect = movedElement.getBoundingClientRect()
|
|
50
51
|
|
|
51
|
-
/*
|
|
52
|
+
/* Визуальный эффект перетаскивания — общий для библиотеки и приложений (dragStyle.ts) */
|
|
52
53
|
element.id = "drag-element"
|
|
53
54
|
element.style.position = "fixed"
|
|
54
55
|
element.style.zIndex = "9999"
|
|
55
56
|
element.style.minHeight = `${rect.height}px`
|
|
56
57
|
element.style.pointerEvents = "none"
|
|
57
58
|
element.style.width = `${rect.width}px`
|
|
58
|
-
element.style.
|
|
59
|
-
element.style.padding = "0.25rem"
|
|
60
|
-
element.style.borderRadius = "1rem"
|
|
59
|
+
element.style.left = `${rect.left}px`
|
|
61
60
|
element.style.top = `${rect.top}px`
|
|
61
|
+
applyDragGhost(element)
|
|
62
62
|
movedElement.parentNode?.appendChild(element)
|
|
63
63
|
|
|
64
|
-
const placeholder =
|
|
65
|
-
placeholder.style.minHeight = `${rect.height}px`
|
|
66
|
-
placeholder.style.width = "100%"
|
|
67
|
-
placeholder.style.border = "2px dashed var(--blue-color)"
|
|
68
|
-
placeholder.style.borderRadius = "1rem"
|
|
64
|
+
const placeholder = createDropPlaceholder(rect.height)
|
|
69
65
|
element.parentNode?.insertBefore(placeholder, movedElement)
|
|
70
66
|
|
|
71
67
|
const [movedItem] = array.splice(elementIndex, 1)
|
|
@@ -97,21 +93,32 @@
|
|
|
97
93
|
const ghost = dragState.ghostElement
|
|
98
94
|
ghost.style.top = `${e.clientY - dragState.offsetY}px`
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
/*
|
|
97
|
+
Место вставки — перед первой строкой, середина которой ниже курсора (иначе — в конец): меняется только при пересечении середины
|
|
98
|
+
строки, без дрожания на границах. Соседние строки сдвигаются плавно (animateReorder)
|
|
99
|
+
*/
|
|
100
|
+
const items = commandItems.filter((item) =>
|
|
101
|
+
dragState.containerIndex === null ? true : Number(item.id.split("-")[2]) === dragState.containerIndex,
|
|
102
|
+
) as HTMLElement[]
|
|
103
|
+
if (!items.length) return
|
|
104
|
+
let target = items[items.length - 1]
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const rect = item.getBoundingClientRect()
|
|
107
|
+
/* Положение в раскладке, без сдвига идущей анимации — иначе движущаяся строка снова «пересекала» бы курсор */
|
|
108
|
+
const transform = getComputedStyle(item).transform
|
|
109
|
+
const shift = transform && transform !== "none" ? new DOMMatrixReadOnly(transform).m42 : 0
|
|
110
|
+
if (e.clientY < rect.top - shift + rect.height / 2) {
|
|
111
|
+
target = item
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const newIndex = Number(target.id.split("-")[1])
|
|
116
|
+
if (newIndex === dragState.dropIndex && dragState.placeholderElement.nextSibling === target) return
|
|
107
117
|
dragState.dropIndex = newIndex
|
|
108
118
|
|
|
109
119
|
const placeholder = dragState.placeholderElement
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (targetElement && targetElement.parentNode) {
|
|
113
|
-
const nextSibling = placeholder.nextSibling
|
|
114
|
-
if (nextSibling !== targetElement) targetElement.parentNode.insertBefore(placeholder, targetElement)
|
|
120
|
+
if (target.parentNode && placeholder.nextSibling !== target) {
|
|
121
|
+
animateReorder(items, () => target.parentNode?.insertBefore(placeholder, target))
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -127,6 +134,7 @@
|
|
|
127
134
|
|
|
128
135
|
if (dragState.placeholderElement) dragState.placeholderElement.remove()
|
|
129
136
|
if (dragState.ghostElement) dragState.ghostElement.remove()
|
|
137
|
+
clearDragGhost()
|
|
130
138
|
|
|
131
139
|
dragState = {
|
|
132
140
|
isDragging: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext } from "svelte"
|
|
3
|
-
import { T } from "../locales/i18n"
|
|
3
|
+
import { Language, T } from "../locales/i18n"
|
|
4
|
+
import { localizeText } from "../locales/localize"
|
|
4
5
|
import { updateProperty, type IOption, type IPeriodChartLevel, type IPeriodChartProps, type IUIComponentHandler, type UIComponent } from "../types"
|
|
5
6
|
import * as UI from ".."
|
|
6
7
|
import { optionsStore, findColorOption } from "../options"
|
|
@@ -8,6 +9,8 @@
|
|
|
8
9
|
import ButtonDelete from "../libIcons/ButtonDelete.svelte"
|
|
9
10
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
10
11
|
import PropsGroup from "../PropsGroup.svelte"
|
|
12
|
+
import PropsListHeader from "../PropsListHeader.svelte"
|
|
13
|
+
import PropsListModal from "../PropsListModal.svelte"
|
|
11
14
|
|
|
12
15
|
const {
|
|
13
16
|
component,
|
|
@@ -23,6 +26,8 @@
|
|
|
23
26
|
let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
|
|
24
27
|
|
|
25
28
|
let itemsContainer: HTMLDivElement | null = $state(null)
|
|
29
|
+
/* Колонки строки уровня: ручка, номер и текст, переменная, данные, подписи, удаление */
|
|
30
|
+
const LEVEL_GRID = "grid-cols-[2rem_minmax(6rem,auto)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_2rem]"
|
|
26
31
|
|
|
27
32
|
const initialColor = $derived(findColorOption($optionsStore.COLOR_OPTIONS, component.properties.wrapperClass))
|
|
28
33
|
|
|
@@ -50,82 +55,96 @@
|
|
|
50
55
|
</script>
|
|
51
56
|
|
|
52
57
|
{#snippet PeriodChartLevels()}
|
|
53
|
-
<
|
|
54
|
-
{#snippet
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<div bind:this={itemsContainer} class="flex flex-col gap-2">
|
|
67
|
-
{#each component.properties.levels || [] as level, index}
|
|
68
|
-
<div
|
|
69
|
-
id="item-{index}"
|
|
70
|
-
class="grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 p-2"
|
|
71
|
-
>
|
|
72
|
-
<UI.Dragging
|
|
73
|
-
wrapperClass="shrink-0"
|
|
74
|
-
container={itemsContainer}
|
|
75
|
-
array={component.properties.levels}
|
|
76
|
-
elementIndex={index}
|
|
77
|
-
onUpdate={(updatedArray) => updateLevels(updatedArray)}
|
|
58
|
+
<PropsListModal label={$T("constructor.props.periodchart.title")} count={component.properties.levels?.length ?? 0} wrapperClass="w-250 h-[70%]">
|
|
59
|
+
{#snippet main()}
|
|
60
|
+
<PropsGroup>
|
|
61
|
+
{#snippet headerActions()}
|
|
62
|
+
<UI.Button
|
|
63
|
+
wrapperClass="w-8"
|
|
64
|
+
content={{ icon: ButtonAdd }}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
const newLevel: IPeriodChartLevel = { name: "", data: [] }
|
|
67
|
+
const levels = [...(component.properties?.levels || []), newLevel]
|
|
68
|
+
updateLevels(levels)
|
|
69
|
+
}}
|
|
78
70
|
/>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
label={{ name: $T("constructor.props.periodchart.data") }}
|
|
94
|
-
wrapperClass="min-w-32 flex-1"
|
|
95
|
-
value={formatNumbers(level.data)}
|
|
96
|
-
onUpdate={(value) => {
|
|
97
|
-
const levels = [...(component.properties?.levels || [])]
|
|
98
|
-
levels[index] = { ...levels[index], data: parseNumbers(value as string) }
|
|
99
|
-
updateLevels(levels)
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
<UI.Input
|
|
103
|
-
label={{ name: $T("constructor.props.periodchart.labels") }}
|
|
104
|
-
wrapperClass="min-w-32 flex-1"
|
|
105
|
-
value={formatStrings(level.labels)}
|
|
106
|
-
onUpdate={(value) => {
|
|
107
|
-
const levels = [...(component.properties?.levels || [])]
|
|
108
|
-
levels[index] = { ...levels[index], labels: parseStrings(value as string) }
|
|
109
|
-
updateLevels(levels)
|
|
110
|
-
}}
|
|
111
|
-
/>
|
|
112
|
-
</div>
|
|
113
|
-
{#if (component.properties.levels?.length ?? 0) > 1}
|
|
114
|
-
<UI.Button
|
|
115
|
-
wrapperClass="w-8 shrink-0"
|
|
116
|
-
content={{ icon: ButtonDelete }}
|
|
117
|
-
onClick={() => {
|
|
118
|
-
const levels = [...(component.properties?.levels || [])]
|
|
119
|
-
levels.splice(index, 1)
|
|
120
|
-
updateLevels(levels)
|
|
121
|
-
}}
|
|
71
|
+
{/snippet}
|
|
72
|
+
|
|
73
|
+
<div bind:this={itemsContainer} class="flex flex-col gap-2">
|
|
74
|
+
{#if component.properties.levels?.length}
|
|
75
|
+
<PropsListHeader
|
|
76
|
+
gridClass={LEVEL_GRID}
|
|
77
|
+
captions={[
|
|
78
|
+
"",
|
|
79
|
+
$T("constructor.props.item"),
|
|
80
|
+
$T("constructor.props.variable"),
|
|
81
|
+
$T("constructor.props.periodchart.data"),
|
|
82
|
+
$T("constructor.props.periodchart.labels"),
|
|
83
|
+
"",
|
|
84
|
+
]}
|
|
122
85
|
/>
|
|
123
86
|
{/if}
|
|
87
|
+
{#each component.properties.levels || [] as level, index}
|
|
88
|
+
<div
|
|
89
|
+
id="item-{index}"
|
|
90
|
+
class="grid {LEVEL_GRID} items-center gap-3 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 px-2 py-1.5"
|
|
91
|
+
>
|
|
92
|
+
<UI.Dragging
|
|
93
|
+
container={itemsContainer}
|
|
94
|
+
array={component.properties.levels}
|
|
95
|
+
elementIndex={index}
|
|
96
|
+
onUpdate={(updatedArray) => updateLevels(updatedArray)}
|
|
97
|
+
/>
|
|
98
|
+
<div class="flex min-w-0 flex-col text-[13px]">
|
|
99
|
+
<span class="font-semibold">{$T("constructor.props.periodchart.item", { n: index + 1 })}</span>
|
|
100
|
+
<span class="truncate text-(--muted-color)">{localizeText(level.name, $Language) || $T("constructor.props.tabs.untitled")}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<UI.Select
|
|
103
|
+
type="input"
|
|
104
|
+
options={VARIABLE_OPTIONS}
|
|
105
|
+
value={VARIABLE_OPTIONS.find((opt) => opt.value === level.variable)}
|
|
106
|
+
onUpdate={(value) => {
|
|
107
|
+
const levels = [...(component.properties?.levels || [])]
|
|
108
|
+
levels[index] = { ...levels[index], variable: (value as IOption).value as string }
|
|
109
|
+
updateLevels(levels)
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
<UI.Input
|
|
113
|
+
value={formatNumbers(level.data)}
|
|
114
|
+
onUpdate={(value) => {
|
|
115
|
+
const levels = [...(component.properties?.levels || [])]
|
|
116
|
+
levels[index] = { ...levels[index], data: parseNumbers(value as string) }
|
|
117
|
+
updateLevels(levels)
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
<UI.Input
|
|
121
|
+
value={formatStrings(level.labels)}
|
|
122
|
+
onUpdate={(value) => {
|
|
123
|
+
const levels = [...(component.properties?.levels || [])]
|
|
124
|
+
levels[index] = { ...levels[index], labels: parseStrings(value as string) }
|
|
125
|
+
updateLevels(levels)
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
{#if (component.properties.levels?.length ?? 0) > 1}
|
|
129
|
+
<UI.Button
|
|
130
|
+
wrapperClass="w-8"
|
|
131
|
+
content={{ icon: ButtonDelete }}
|
|
132
|
+
onClick={() => {
|
|
133
|
+
const levels = [...(component.properties?.levels || [])]
|
|
134
|
+
levels.splice(index, 1)
|
|
135
|
+
updateLevels(levels)
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
{:else}
|
|
139
|
+
<span></span>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
{/each}
|
|
143
|
+
<div id="item-{component.properties.levels?.length ?? 0}" class="min-h-4"></div>
|
|
124
144
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</PropsGroup>
|
|
145
|
+
</PropsGroup>
|
|
146
|
+
{/snippet}
|
|
147
|
+
</PropsListModal>
|
|
129
148
|
{/snippet}
|
|
130
149
|
|
|
131
150
|
{#if forConstructor}
|
|
@@ -136,7 +155,9 @@
|
|
|
136
155
|
<CommonSnippets snippet="Label" {component} {onPropertyChange} />
|
|
137
156
|
</PropsGroup>
|
|
138
157
|
</div>
|
|
139
|
-
|
|
158
|
+
<PropsGroup>
|
|
159
|
+
{@render PeriodChartLevels()}
|
|
160
|
+
</PropsGroup>
|
|
140
161
|
{:else}
|
|
141
162
|
<div class="flex flex-col gap-2">
|
|
142
163
|
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
@@ -149,5 +170,7 @@
|
|
|
149
170
|
<CommonSnippets snippet="Colors" initialValue={{ color: initialColor, uselessColors: ["bg-max"] }} {component} {onPropertyChange} />
|
|
150
171
|
</PropsGroup>
|
|
151
172
|
</div>
|
|
152
|
-
|
|
173
|
+
<PropsGroup>
|
|
174
|
+
{@render PeriodChartLevels()}
|
|
175
|
+
</PropsGroup>
|
|
153
176
|
{/if}
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
|
|
111
111
|
- Общие поля через `CommonSnippets`: `Access` (без `viewOnly` — компонент чисто отображающий, как `Graph`/`Tabs`/`ProgressBar`), `Colors` (пишет в `wrapperClass`, тот же цвет уходит на переключатель и столбики), `Label`; в режиме редактирования дополнительно `Identificator`, `WrapperClass`
|
|
112
112
|
- `unit` — отдельное текстовое поле ввода
|
|
113
|
-
- Редактор уровней (`levels[]`) —
|
|
113
|
+
- Редактор уровней (`levels[]`) — в окне «Уровни детализации» (`PropsListModal`), по строке на уровень, как у вкладок Tabs и шкал ProgressBar: перетаскивание (`UI.Dragging`) для порядка вкладок, добавление/удаление через `ButtonAdd`/`ButtonDelete`, на каждый уровень — поля `variable`, `data` и `labels` (подпись `name` — в окне «Тексты и переводы»). `variable` — тот же `Select` по списку переменных устройства (`DeviceVariables` из контекста), что и у `Graph`/`CommonSnippets.Variable`, только по одному на каждый уровень; при любом изменении списка уровней редактор пересобирает `eventHandler.Variables` как объединение всех непустых `variable` со всех уровней — так рантайм подписывается сразу на все нужные ключи. `data`/`labels` остаются полями для литеральных демо-данных — массивы чисел/строк вводятся одной строкой через запятую (`"10, 20, 30"`) и парсятся на лету
|
|
114
114
|
|
|
115
115
|
### Использование
|
|
116
116
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import TextsEditor from "../TextsEditor.svelte"
|
|
3
3
|
import { getContext } from "svelte"
|
|
4
|
-
import { T } from "../locales/i18n"
|
|
4
|
+
import { Language, T } from "../locales/i18n"
|
|
5
|
+
import { localizeText } from "../locales/localize"
|
|
5
6
|
import { type UIComponent, type IProgressBarProps, updateProperty, type IUIComponentHandler } from "../types"
|
|
6
7
|
import * as UI from ".."
|
|
7
8
|
import { optionsStore, findColorOption } from "../options"
|
|
8
9
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
9
10
|
import PropsGroup from "../PropsGroup.svelte"
|
|
10
11
|
import PropsListModal from "../PropsListModal.svelte"
|
|
12
|
+
import PropsListHeader from "../PropsListHeader.svelte"
|
|
11
13
|
import ButtonAdd from "../libIcons/ButtonAdd.svelte"
|
|
12
14
|
import ButtonDelete from "../libIcons/ButtonDelete.svelte"
|
|
13
15
|
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
const DeviceVariables = getContext<{ id: string; value: string; name: string }[]>("DeviceVariables")
|
|
25
27
|
let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
|
|
26
28
|
let itemsContainer: HTMLDivElement | null = $state(null)
|
|
29
|
+
/* Колонки строки шкалы: ручка, номер и текст, цвет (в конструкторе на две колонки) или значение и стили, удаление */
|
|
30
|
+
const ITEM_GRID = "grid-cols-[2rem_minmax(8rem,1fr)_minmax(6rem,1fr)_minmax(0,1fr)_2rem]"
|
|
27
31
|
</script>
|
|
28
32
|
|
|
29
33
|
{#snippet ProgressBarType()}
|
|
@@ -52,7 +56,7 @@
|
|
|
52
56
|
content={{ icon: ButtonAdd }}
|
|
53
57
|
onClick={() => {
|
|
54
58
|
const newProgress = {
|
|
55
|
-
name:
|
|
59
|
+
name: "",
|
|
56
60
|
class: "bg-blue",
|
|
57
61
|
}
|
|
58
62
|
const progresses = [...(component.properties?.items || []), newProgress]
|
|
@@ -70,13 +74,24 @@
|
|
|
70
74
|
{/snippet}
|
|
71
75
|
|
|
72
76
|
<div bind:this={itemsContainer} class="flex flex-col gap-2">
|
|
77
|
+
{#if component.properties.items?.length}
|
|
78
|
+
<PropsListHeader
|
|
79
|
+
gridClass={ITEM_GRID}
|
|
80
|
+
captions={[
|
|
81
|
+
"",
|
|
82
|
+
$T("constructor.props.item"),
|
|
83
|
+
forConstructor ? $T("constructor.props.colors") : $T("constructor.props.optionvalue"),
|
|
84
|
+
forConstructor ? "" : $T("constructor.props.optionclass"),
|
|
85
|
+
"",
|
|
86
|
+
]}
|
|
87
|
+
/>
|
|
88
|
+
{/if}
|
|
73
89
|
{#each component.properties.items || [] as progress, index}
|
|
74
90
|
<div
|
|
75
91
|
id="item-{index}"
|
|
76
|
-
class="grid
|
|
92
|
+
class="grid {ITEM_GRID} items-center gap-3 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 px-2 py-1.5"
|
|
77
93
|
>
|
|
78
94
|
<UI.Dragging
|
|
79
|
-
wrapperClass="shrink-0"
|
|
80
95
|
container={itemsContainer}
|
|
81
96
|
array={component.properties.items}
|
|
82
97
|
elementIndex={index}
|
|
@@ -84,52 +99,49 @@
|
|
|
84
99
|
updateProperty("items", updatedArray, component, onPropertyChange)
|
|
85
100
|
}}
|
|
86
101
|
/>
|
|
87
|
-
<div class="flex
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
label={{ name: $T("constructor.props.colors") }}
|
|
91
|
-
wrapperClass="min-w-56 flex-[2]"
|
|
92
|
-
type="buttons"
|
|
93
|
-
options={$optionsStore.COLOR_OPTIONS.filter((option) => option.value !== "bg-max")}
|
|
94
|
-
value={findColorOption($optionsStore.COLOR_OPTIONS, progress.class)}
|
|
95
|
-
onUpdate={(value) => {
|
|
96
|
-
const progresses = (component.properties?.items || []).map((p: { name?: string; class?: string }, i: number) =>
|
|
97
|
-
i === index ? { ...p, class: (value as UI.IOption).value } : p,
|
|
98
|
-
)
|
|
99
|
-
updateProperty("items", progresses, component, onPropertyChange)
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
{:else}
|
|
103
|
-
<UI.Input
|
|
104
|
-
label={{ name: $T("constructor.props.optionvalue") }}
|
|
105
|
-
wrapperClass="min-w-24 flex-1"
|
|
106
|
-
value={(component.properties.value || [])[index]?.Value}
|
|
107
|
-
type="number"
|
|
108
|
-
onUpdate={(value) => {
|
|
109
|
-
const base =
|
|
110
|
-
component.properties?.value ||
|
|
111
|
-
component.properties?.items.map((item: { name?: string; class?: string }, i: number) => ({
|
|
112
|
-
Name: item.name,
|
|
113
|
-
Value: i == index ? value : 0,
|
|
114
|
-
}))
|
|
115
|
-
const progresses = base.map((p: { Name?: string; Value?: number }, i: number) => (i === index ? { ...p, Value: value } : p))
|
|
116
|
-
updateProperty("value", progresses, component, onPropertyChange)
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
<UI.Input
|
|
120
|
-
label={{ name: $T("constructor.props.optionclass") }}
|
|
121
|
-
wrapperClass="min-w-32 flex-[2]"
|
|
122
|
-
value={progress.class}
|
|
123
|
-
onUpdate={(value) => {
|
|
124
|
-
const progresses = [...(component.properties?.items || [])]
|
|
125
|
-
progresses[index]["class"] = value
|
|
126
|
-
updateProperty("items", progresses, component, onPropertyChange)
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
{/if}
|
|
102
|
+
<div class="flex min-w-0 items-baseline gap-2 text-[13px]">
|
|
103
|
+
<span class="shrink-0 font-semibold">{$T("constructor.props.progressbar.item", { n: index + 1 })}</span>
|
|
104
|
+
<span class="truncate text-(--muted-color)">{localizeText(progress.name, $Language) || $T("constructor.props.tabs.untitled")}</span>
|
|
130
105
|
</div>
|
|
106
|
+
{#if forConstructor}
|
|
107
|
+
<UI.Select
|
|
108
|
+
wrapperClass="col-span-2"
|
|
109
|
+
type="buttons"
|
|
110
|
+
options={$optionsStore.COLOR_OPTIONS.filter((option) => option.value !== "bg-max")}
|
|
111
|
+
value={findColorOption($optionsStore.COLOR_OPTIONS, progress.class)}
|
|
112
|
+
onUpdate={(value) => {
|
|
113
|
+
const progresses = (component.properties?.items || []).map((p: { name?: string; class?: string }, i: number) =>
|
|
114
|
+
i === index ? { ...p, class: (value as UI.IOption).value } : p,
|
|
115
|
+
)
|
|
116
|
+
updateProperty("items", progresses, component, onPropertyChange)
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
{:else}
|
|
120
|
+
<UI.Input
|
|
121
|
+
value={(component.properties.value || [])[index]?.Value}
|
|
122
|
+
type="number"
|
|
123
|
+
onUpdate={(value) => {
|
|
124
|
+
const base =
|
|
125
|
+
component.properties?.value ||
|
|
126
|
+
component.properties?.items.map((item: { name?: string; class?: string }, i: number) => ({
|
|
127
|
+
Name: item.name,
|
|
128
|
+
Value: i == index ? value : 0,
|
|
129
|
+
}))
|
|
130
|
+
const progresses = base.map((p: { Name?: string; Value?: number }, i: number) => (i === index ? { ...p, Value: value } : p))
|
|
131
|
+
updateProperty("value", progresses, component, onPropertyChange)
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
<UI.Input
|
|
135
|
+
value={progress.class}
|
|
136
|
+
onUpdate={(value) => {
|
|
137
|
+
const progresses = [...(component.properties?.items || [])]
|
|
138
|
+
progresses[index]["class"] = value
|
|
139
|
+
updateProperty("items", progresses, component, onPropertyChange)
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
{/if}
|
|
131
143
|
<UI.Button
|
|
132
|
-
wrapperClass="w-8
|
|
144
|
+
wrapperClass="w-8"
|
|
133
145
|
content={{ icon: ButtonDelete }}
|
|
134
146
|
onClick={() => {
|
|
135
147
|
const progresses = [...(component.properties?.items || [])]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- $lib/PropsListHeader.svelte — подписи колонок над однострочными элементами списка в окнах свойств (вместо подписи у каждого поля);
|
|
2
|
+
gridClass — тот же шаблон колонок, что у строк списка, пустая подпись — колонка без заголовка (ручка, удаление) -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
let { gridClass, captions }: { gridClass: string; captions: string[] } = $props()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="grid {gridClass} items-center gap-3 px-2 text-[12px] font-semibold text-(--muted-color)">
|
|
8
|
+
{#each captions as caption, index (index)}
|
|
9
|
+
<span class="truncate">{caption}</span>
|
|
10
|
+
{/each}
|
|
11
|
+
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- $lib/PropsListModal.svelte — вынос "тяжёлых" списковых секций панелей свойств (столбцы
|
|
2
|
-
Table, опции ProgressBar/Select/Switch/Tabs) в модалку вместо попытки втиснуть их в узкий
|
|
2
|
+
Table, опции ProgressBar/Select/Switch/Tabs, уровни PeriodChart) в модалку вместо попытки втиснуть их в узкий
|
|
3
3
|
сайдбар конструктора (ConstructorUI.svelte). Кнопка-триггер показывает счётчик элементов. -->
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { T } from "./locales/i18n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext, onMount } from "svelte"
|
|
3
|
-
import { T } from "../locales/i18n"
|
|
3
|
+
import { Language, T } from "../locales/i18n"
|
|
4
4
|
import { type UIComponent, type ISelectProps, type IOption, updateProperty, type IUIComponentHandler } from "../types"
|
|
5
5
|
import * as UI from ".."
|
|
6
6
|
import ButtonDelete from "../libIcons/ButtonDelete.svelte"
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
10
10
|
import PropsGroup from "../PropsGroup.svelte"
|
|
11
11
|
import PropsListModal from "../PropsListModal.svelte"
|
|
12
|
+
import PropsListHeader from "../PropsListHeader.svelte"
|
|
13
|
+
import { localizeText } from "../locales/localize"
|
|
12
14
|
import { twMerge } from "tailwind-merge"
|
|
13
15
|
|
|
14
16
|
const {
|
|
@@ -25,6 +27,8 @@
|
|
|
25
27
|
let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
|
|
26
28
|
|
|
27
29
|
let itemsContainer: HTMLDivElement | null = $state(null)
|
|
30
|
+
/* Колонки строки варианта: ручка, номер и текст, значение, цвет/стили, удаление */
|
|
31
|
+
const OPTION_GRID = "grid-cols-[2rem_minmax(8rem,1fr)_minmax(6rem,1fr)_minmax(0,2fr)_2rem]"
|
|
28
32
|
|
|
29
33
|
type ValueTypeOption = {
|
|
30
34
|
id: string
|
|
@@ -179,7 +183,7 @@
|
|
|
179
183
|
onClick={() => {
|
|
180
184
|
const newOption: IOption = {
|
|
181
185
|
id: crypto.randomUUID(),
|
|
182
|
-
name:
|
|
186
|
+
name: "",
|
|
183
187
|
value: component.properties?.options.length + 1,
|
|
184
188
|
class: "bg-max",
|
|
185
189
|
}
|
|
@@ -190,13 +194,24 @@
|
|
|
190
194
|
{/snippet}
|
|
191
195
|
|
|
192
196
|
<div bind:this={itemsContainer} class="flex flex-col gap-2">
|
|
197
|
+
{#if component.properties.options?.length}
|
|
198
|
+
<PropsListHeader
|
|
199
|
+
gridClass={OPTION_GRID}
|
|
200
|
+
captions={[
|
|
201
|
+
"",
|
|
202
|
+
$T("constructor.props.item"),
|
|
203
|
+
$T("constructor.props.optionvalue"),
|
|
204
|
+
forConstructor ? $T("constructor.props.colors") : $T("constructor.props.optionclass"),
|
|
205
|
+
"",
|
|
206
|
+
]}
|
|
207
|
+
/>
|
|
208
|
+
{/if}
|
|
193
209
|
{#each component.properties.options || [] as option, index (option.id)}
|
|
194
210
|
<div
|
|
195
211
|
id="item-{index}"
|
|
196
|
-
class="grid
|
|
212
|
+
class="grid {OPTION_GRID} items-center gap-3 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 px-2 py-1.5"
|
|
197
213
|
>
|
|
198
214
|
<UI.Dragging
|
|
199
|
-
wrapperClass="shrink-0"
|
|
200
215
|
container={itemsContainer}
|
|
201
216
|
array={component.properties.options}
|
|
202
217
|
elementIndex={index}
|
|
@@ -204,49 +219,45 @@
|
|
|
204
219
|
updateProperty("options", updatedArray, component, onPropertyChange)
|
|
205
220
|
}}
|
|
206
221
|
/>
|
|
207
|
-
<div class="flex
|
|
222
|
+
<div class="flex min-w-0 items-baseline gap-2 text-[13px]">
|
|
223
|
+
<span class="shrink-0 font-semibold">{$T("constructor.props.options.item", { n: index + 1 })}</span>
|
|
224
|
+
<span class="truncate text-(--muted-color)">{localizeText(option.name, $Language) || $T("constructor.props.tabs.untitled")}</span>
|
|
225
|
+
</div>
|
|
226
|
+
<UI.Input
|
|
227
|
+
value={option.value}
|
|
228
|
+
type={currentValueType.value}
|
|
229
|
+
number={component.properties.bitMode
|
|
230
|
+
? { minNum: 0, maxNum: Math.pow(2, component.properties.range.end - component.properties.range.start + 1) - 1, step: 1 }
|
|
231
|
+
: { minNum: -1000000, maxNum: 1000000, step: 1 }}
|
|
232
|
+
onUpdate={(value) => {
|
|
233
|
+
const options = [...(component.properties?.options || [])]
|
|
234
|
+
options[index]["value"] = value
|
|
235
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
{#if forConstructor}
|
|
239
|
+
<UI.Select
|
|
240
|
+
type="buttons"
|
|
241
|
+
options={$optionsStore.COLOR_OPTIONS}
|
|
242
|
+
value={findColorOption($optionsStore.COLOR_OPTIONS, option.class)}
|
|
243
|
+
onUpdate={(option) => {
|
|
244
|
+
const options = [...(component.properties?.options || [])]
|
|
245
|
+
options[index]["class"] = (option as UI.IOption).value
|
|
246
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
{:else}
|
|
208
250
|
<UI.Input
|
|
209
|
-
|
|
210
|
-
wrapperClass="min-w-28 flex-1"
|
|
211
|
-
value={option.value}
|
|
212
|
-
type={currentValueType.value}
|
|
213
|
-
number={component.properties.bitMode
|
|
214
|
-
? { minNum: 0, maxNum: Math.pow(2, component.properties.range.end - component.properties.range.start + 1) - 1, step: 1 }
|
|
215
|
-
: { minNum: -1000000, maxNum: 1000000, step: 1 }}
|
|
251
|
+
value={option.class}
|
|
216
252
|
onUpdate={(value) => {
|
|
217
253
|
const options = [...(component.properties?.options || [])]
|
|
218
|
-
options[index]["
|
|
254
|
+
options[index]["class"] = value as string
|
|
219
255
|
updateProperty("options", options, component, onPropertyChange)
|
|
220
256
|
}}
|
|
221
257
|
/>
|
|
222
|
-
|
|
223
|
-
<UI.Select
|
|
224
|
-
wrapperClass="min-w-56 flex-[2]"
|
|
225
|
-
label={{ name: $T("constructor.props.colors") }}
|
|
226
|
-
type="buttons"
|
|
227
|
-
options={$optionsStore.COLOR_OPTIONS}
|
|
228
|
-
value={findColorOption($optionsStore.COLOR_OPTIONS, option.class)}
|
|
229
|
-
onUpdate={(option) => {
|
|
230
|
-
const options = [...(component.properties?.options || [])]
|
|
231
|
-
options[index]["class"] = (option as UI.IOption).value
|
|
232
|
-
updateProperty("options", options, component, onPropertyChange)
|
|
233
|
-
}}
|
|
234
|
-
/>
|
|
235
|
-
{:else}
|
|
236
|
-
<UI.Input
|
|
237
|
-
label={{ name: $T("constructor.props.optionclass") }}
|
|
238
|
-
wrapperClass="min-w-32 flex-[2]"
|
|
239
|
-
value={option.class}
|
|
240
|
-
onUpdate={(value) => {
|
|
241
|
-
const options = [...(component.properties?.options || [])]
|
|
242
|
-
options[index]["class"] = value as string
|
|
243
|
-
updateProperty("options", options, component, onPropertyChange)
|
|
244
|
-
}}
|
|
245
|
-
/>
|
|
246
|
-
{/if}
|
|
247
|
-
</div>
|
|
258
|
+
{/if}
|
|
248
259
|
<UI.Button
|
|
249
|
-
wrapperClass="w-8
|
|
260
|
+
wrapperClass="w-8"
|
|
250
261
|
content={{ icon: ButtonDelete }}
|
|
251
262
|
onClick={() => {
|
|
252
263
|
const options = [...(component.properties?.options || [])]
|
package/dist/Switch/README.md
CHANGED
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
|
|
57
57
|
#### IOption
|
|
58
58
|
|
|
59
|
-
| Название | Тип | По умолчанию | Описание
|
|
60
|
-
| ---------- | --------- | ------------ |
|
|
61
|
-
| `id` | `string` | `-` | Уникальный идентификатор опции
|
|
62
|
-
| `value` | `number` | `undefined` | Значение опции (номер бита или значение)
|
|
63
|
-
| `name` | `string` | `undefined` | Отображаемое имя опции (используется в `bitMode
|
|
64
|
-
| `class` | `string` | `undefined` | CSS-классы для стилизации опции
|
|
65
|
-
| `disabled` | `boolean` | `undefined` | Состояние отключения опции
|
|
59
|
+
| Название | Тип | По умолчанию | Описание |
|
|
60
|
+
| ---------- | --------- | ------------ | ------------------------------------------------------------------------------------- |
|
|
61
|
+
| `id` | `string` | `-` | Уникальный идентификатор опции |
|
|
62
|
+
| `value` | `number` | `undefined` | Значение опции (номер бита или значение) |
|
|
63
|
+
| `name` | `string` | `undefined` | Отображаемое имя опции (используется в `bitMode`, по центру над своим переключателем) |
|
|
64
|
+
| `class` | `string` | `undefined` | CSS-классы для стилизации опции |
|
|
65
|
+
| `disabled` | `boolean` | `undefined` | Состояние отключения опции |
|
|
66
66
|
|
|
67
67
|
### Колбэки
|
|
68
68
|
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
{#each localOptions as option, index}
|
|
67
67
|
<div class={twMerge(`flex flex-col gap-1`, option.class)}>
|
|
68
68
|
{#if option.name && bitMode}
|
|
69
|
-
<span class="text-[12px] font-medium text-(--muted-color)">{option.name}</span>
|
|
69
|
+
<span class="text-center text-[12px] font-medium text-(--muted-color)">{option.name}</span>
|
|
70
70
|
{/if}
|
|
71
71
|
|
|
72
72
|
<div class="relative flex w-full grow items-center justify-center bg-transparent">
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
10
10
|
import PropsGroup from "../PropsGroup.svelte"
|
|
11
11
|
import PropsListModal from "../PropsListModal.svelte"
|
|
12
|
+
import PropsListHeader from "../PropsListHeader.svelte"
|
|
12
13
|
|
|
13
14
|
const {
|
|
14
15
|
component,
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
}>()
|
|
22
23
|
const DeviceVariables = getContext<{ id: string; value: string; name: string }[]>("DeviceVariables")
|
|
23
24
|
let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
|
|
25
|
+
/* Колонки строки бита: метка, позиция, цвет, отключение, удаление */
|
|
26
|
+
const BIT_GRID = "grid-cols-[minmax(4rem,1fr)_minmax(4rem,1fr)_minmax(0,2fr)_auto_2rem]"
|
|
24
27
|
let initialColor = $derived(findColorOption($optionsStore.COLOR_OPTIONS, component.properties.options?.[0]?.class))
|
|
25
28
|
const initialAlign = $derived(
|
|
26
29
|
$optionsStore.TEXT_ALIGN_OPTIONS.find((a) =>
|
|
@@ -91,60 +94,58 @@
|
|
|
91
94
|
{/if}
|
|
92
95
|
{/snippet}
|
|
93
96
|
|
|
97
|
+
{#if component.properties.options?.length}
|
|
98
|
+
<PropsListHeader
|
|
99
|
+
gridClass={BIT_GRID}
|
|
100
|
+
captions={[$T("constructor.props.optionname"), $T("constructor.props.optionposition"), $T("constructor.props.colors"), "", ""]}
|
|
101
|
+
/>
|
|
102
|
+
{/if}
|
|
94
103
|
{#each component.properties.options || [] as option, index (option.id)}
|
|
95
|
-
<div class="grid
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
value=
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
138
|
-
onChange={(value) => {
|
|
139
|
-
const options = [...(component.properties?.options || [])]
|
|
140
|
-
options[index]["disabled"] = value
|
|
141
|
-
updateProperty("options", options, component, onPropertyChange)
|
|
142
|
-
}}
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
104
|
+
<div class="grid {BIT_GRID} items-center gap-3 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 px-2 py-1.5">
|
|
105
|
+
<!-- Имя варианта — короткая метка бита (до 4 символов) в битовом режиме, не переводится -->
|
|
106
|
+
<UI.Input
|
|
107
|
+
value={option.name}
|
|
108
|
+
maxlength={4}
|
|
109
|
+
onUpdate={(value) => {
|
|
110
|
+
const options = [...(component.properties?.options || [])]
|
|
111
|
+
options[index]["name"] = value
|
|
112
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
<UI.Input
|
|
116
|
+
value={option.value}
|
|
117
|
+
type="number"
|
|
118
|
+
number={{ minNum: 0, maxNum: 31, step: 1 }}
|
|
119
|
+
onUpdate={(value) => {
|
|
120
|
+
const options = [...(component.properties?.options || [])]
|
|
121
|
+
options[index]["value"] = value
|
|
122
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<UI.Select
|
|
126
|
+
type="buttons"
|
|
127
|
+
options={$optionsStore.COLOR_OPTIONS.filter((option) => option.value !== "bg-max" && option.value !== "bg-gray")}
|
|
128
|
+
value={findColorOption($optionsStore.COLOR_OPTIONS, option.class)}
|
|
129
|
+
onUpdate={(option) => {
|
|
130
|
+
const options = [...(component.properties?.options || [])]
|
|
131
|
+
options[index]["class"] = (option as UI.IOption).value
|
|
132
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
<UI.Switch
|
|
136
|
+
wrapperClass="bg-blue"
|
|
137
|
+
label={{ captionRight: $T("constructor.props.disabled") }}
|
|
138
|
+
value={option.disabled}
|
|
139
|
+
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
140
|
+
onChange={(value) => {
|
|
141
|
+
const options = [...(component.properties?.options || [])]
|
|
142
|
+
options[index]["disabled"] = value
|
|
143
|
+
updateProperty("options", options, component, onPropertyChange)
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
145
146
|
{#if (component.properties.options?.length ?? 0) > 1}
|
|
146
147
|
<UI.Button
|
|
147
|
-
wrapperClass="w-8
|
|
148
|
+
wrapperClass="w-8"
|
|
148
149
|
content={{ icon: ButtonDelete }}
|
|
149
150
|
onClick={() => {
|
|
150
151
|
const options = [...(component.properties?.options || [])]
|
|
@@ -152,6 +153,8 @@
|
|
|
152
153
|
updateProperty("options", options, component, onPropertyChange)
|
|
153
154
|
}}
|
|
154
155
|
/>
|
|
156
|
+
{:else}
|
|
157
|
+
<span></span>
|
|
155
158
|
{/if}
|
|
156
159
|
</div>
|
|
157
160
|
{/each}
|
|
@@ -123,9 +123,10 @@
|
|
|
123
123
|
|
|
124
124
|
<div bind:this={itemsContainer} class="flex flex-col gap-2">
|
|
125
125
|
{#each component.properties.items || [] as tab, index}
|
|
126
|
+
<!-- Строка в одну линию: ручка, какая это вкладка, иконка, «Отключить», удаление -->
|
|
126
127
|
<div
|
|
127
128
|
id="item-{index}"
|
|
128
|
-
class="grid grid-cols-[
|
|
129
|
+
class="grid grid-cols-[auto_minmax(0,1fr)_auto_auto_auto] items-center gap-3 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 px-2 py-1.5"
|
|
129
130
|
>
|
|
130
131
|
<UI.Dragging
|
|
131
132
|
wrapperClass="shrink-0"
|
|
@@ -136,42 +137,38 @@
|
|
|
136
137
|
updateProperty("items", updatedArray, component, onPropertyChange)
|
|
137
138
|
}}
|
|
138
139
|
/>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
icon: component.properties.items[index].icon,
|
|
151
|
-
updateProperty: (icon: string) => {
|
|
152
|
-
const items = [...(component.properties?.items || [])]
|
|
153
|
-
items[index]["icon"] = icon as string
|
|
154
|
-
updateProperty("items", items, component, onPropertyChange)
|
|
155
|
-
},
|
|
156
|
-
icons: { array: ICONS },
|
|
157
|
-
}}
|
|
158
|
-
{component}
|
|
159
|
-
{onPropertyChange}
|
|
160
|
-
/>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
<UI.Switch
|
|
164
|
-
wrapperClass="min-w-24"
|
|
165
|
-
label={{ name: $T("constructor.props.disabled") }}
|
|
166
|
-
value={tab?.disabled ? 1 : 0}
|
|
167
|
-
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
168
|
-
onChange={(value) => {
|
|
140
|
+
<!-- Номер и название на текущем языке (название правится в «Тексты и переводы») -->
|
|
141
|
+
<div class="flex min-w-0 items-baseline gap-2 text-[13px]">
|
|
142
|
+
<span class="shrink-0 font-semibold">{$T("constructor.props.tabs.item", { n: index + 1 })}</span>
|
|
143
|
+
<span class="truncate text-(--muted-color)">{localizeText(tab.name, $Language) || $T("constructor.props.tabs.untitled")}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<CommonSnippets
|
|
146
|
+
snippet="IconsLib"
|
|
147
|
+
initialValue={{
|
|
148
|
+
name: $T("constructor.props.table.type.icon"),
|
|
149
|
+
icon: component.properties.items[index].icon,
|
|
150
|
+
updateProperty: (icon: string) => {
|
|
169
151
|
const items = [...(component.properties?.items || [])]
|
|
170
|
-
items[index]["
|
|
152
|
+
items[index]["icon"] = icon as string
|
|
171
153
|
updateProperty("items", items, component, onPropertyChange)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
154
|
+
},
|
|
155
|
+
icons: { array: ICONS },
|
|
156
|
+
compact: true,
|
|
157
|
+
}}
|
|
158
|
+
{component}
|
|
159
|
+
{onPropertyChange}
|
|
160
|
+
/>
|
|
161
|
+
<UI.Switch
|
|
162
|
+
wrapperClass="w-auto"
|
|
163
|
+
label={{ captionRight: $T("constructor.props.disabled") }}
|
|
164
|
+
value={tab?.disabled ? 1 : 0}
|
|
165
|
+
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
166
|
+
onChange={(value) => {
|
|
167
|
+
const items = [...(component.properties?.items || [])]
|
|
168
|
+
items[index]["disabled"] = value === 1
|
|
169
|
+
updateProperty("items", items, component, onPropertyChange)
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
175
172
|
{#if component.properties.items.length > 1}
|
|
176
173
|
<UI.Button
|
|
177
174
|
wrapperClass="w-8 shrink-0"
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const applyDragGhost: (element: HTMLElement) => void;
|
|
2
|
+
export declare const clearDragGhost: (element?: HTMLElement | null) => void;
|
|
3
|
+
export declare const animateReorder: (elements: Element[], mutate: () => void, duration?: number) => void;
|
|
4
|
+
export declare const createDropPlaceholder: (height: number) => HTMLElement;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* $lib/dragStyle.ts — единый вид перетаскивания для библиотеки и приложений (списки Dragging, холст конструктора DevCloud):
|
|
2
|
+
перетаскиваемый элемент — фон с заметным оттенком акцента, рамка акцентом и тень; место вставки — заливка мягким акцентом со штриховой рамкой */
|
|
3
|
+
const GHOST_STYLE = {
|
|
4
|
+
background: "color-mix(in srgb, var(--accent-color) 30%, var(--back-color))",
|
|
5
|
+
outline: "2px solid var(--accent-color)",
|
|
6
|
+
outlineOffset: "-2px",
|
|
7
|
+
boxShadow: "var(--elevation-3)",
|
|
8
|
+
borderRadius: "12px",
|
|
9
|
+
opacity: "1",
|
|
10
|
+
};
|
|
11
|
+
/* Оформить перетаскиваемый элемент; курсор «захват» — на всё время перетаскивания */
|
|
12
|
+
export const applyDragGhost = (element) => {
|
|
13
|
+
Object.assign(element.style, GHOST_STYLE);
|
|
14
|
+
document.body.style.cursor = "grabbing";
|
|
15
|
+
};
|
|
16
|
+
/* Снять оформление перетаскивания с элемента (если он остаётся на странице) и вернуть курсор */
|
|
17
|
+
export const clearDragGhost = (element) => {
|
|
18
|
+
if (element)
|
|
19
|
+
for (const key of Object.keys(GHOST_STYLE))
|
|
20
|
+
element.style[key] = "";
|
|
21
|
+
document.body.style.cursor = "";
|
|
22
|
+
};
|
|
23
|
+
/*
|
|
24
|
+
Плавный сдвиг соседей при перестановке (FLIP): запоминаем положение элементов, выполняем перестановку в DOM и анимируем каждый
|
|
25
|
+
сдвинувшийся элемент из старого положения в новое. При системной настройке «уменьшить движение» — без анимации
|
|
26
|
+
*/
|
|
27
|
+
export const animateReorder = (elements, mutate, duration = 160) => {
|
|
28
|
+
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
29
|
+
const before = new Map(elements.map((element) => [element, element.getBoundingClientRect().top]));
|
|
30
|
+
mutate();
|
|
31
|
+
if (reduceMotion)
|
|
32
|
+
return;
|
|
33
|
+
for (const element of elements) {
|
|
34
|
+
const node = element;
|
|
35
|
+
const shift = (before.get(element) ?? 0) - node.getBoundingClientRect().top;
|
|
36
|
+
if (!shift)
|
|
37
|
+
continue;
|
|
38
|
+
node.style.transition = "none";
|
|
39
|
+
node.style.transform = `translateY(${shift}px)`;
|
|
40
|
+
void node.offsetHeight; /* Применить стартовое положение до запуска перехода */
|
|
41
|
+
node.style.transition = `transform ${duration}ms cubic-bezier(0.2, 0, 0, 1)`;
|
|
42
|
+
node.style.transform = "";
|
|
43
|
+
node.addEventListener("transitionend", () => (node.style.transition = ""), { once: true });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/* Место вставки: заливка мягким акцентом и штриховая рамка акцентом */
|
|
47
|
+
export const createDropPlaceholder = (height) => {
|
|
48
|
+
const placeholder = document.createElement("div");
|
|
49
|
+
Object.assign(placeholder.style, {
|
|
50
|
+
minHeight: `${height}px`,
|
|
51
|
+
width: "100%",
|
|
52
|
+
background: "var(--accent-soft)",
|
|
53
|
+
border: "2px dashed var(--accent-color)",
|
|
54
|
+
borderRadius: "12px",
|
|
55
|
+
boxSizing: "border-box",
|
|
56
|
+
});
|
|
57
|
+
return placeholder;
|
|
58
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -54,5 +54,6 @@ export * from "./locales/i18n";
|
|
|
54
54
|
export * from "./locales/languages";
|
|
55
55
|
export * from "./locales/localize";
|
|
56
56
|
export { widgetAccentStyle } from "./widgetAccent";
|
|
57
|
+
export { applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle";
|
|
57
58
|
export * from "./ModalStackStore";
|
|
58
59
|
export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, type IWidgetRGBStripProps, type IWidgetRGBStripConfig, type IWidgetPFDProps, type IWidgetPFDData, type IWidgetPFDLimits, type IWidgetMapProps, type IWidgetMapData, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -55,5 +55,6 @@ export * from "./locales/i18n";
|
|
|
55
55
|
export * from "./locales/languages";
|
|
56
56
|
export * from "./locales/localize";
|
|
57
57
|
export { widgetAccentStyle } from "./widgetAccent";
|
|
58
|
+
export { applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle";
|
|
58
59
|
export * from "./ModalStackStore";
|
|
59
60
|
export {} from "./types";
|
|
@@ -306,10 +306,14 @@ const translations = {
|
|
|
306
306
|
"constructor.props.bits.title": { ru: "Биты", en: "Bits", zh: "位" },
|
|
307
307
|
"constructor.props.tabs.title": { ru: "Вкладки", en: "Tabs", zh: "标签页" },
|
|
308
308
|
"constructor.props.tabs.item": { ru: "Вкладка {n}", en: "Tab {n}", zh: "标签页 {n}" },
|
|
309
|
+
"constructor.props.options.item": { ru: "Вариант {n}", en: "Option {n}", zh: "选项 {n}" },
|
|
310
|
+
"constructor.props.progressbar.item": { ru: "Шкала {n}", en: "Bar {n}", zh: "进度条 {n}" },
|
|
311
|
+
"constructor.props.periodchart.item": { ru: "Уровень {n}", en: "Level {n}", zh: "层级 {n}" },
|
|
312
|
+
"constructor.props.item": { ru: "Элемент", en: "Item", zh: "项目" },
|
|
309
313
|
"constructor.props.tabs.untitled": { ru: "без названия", en: "untitled", zh: "未命名" },
|
|
310
314
|
"constructor.props.periodchart.title": { ru: "Уровни детализации", en: "Detail levels", zh: "详细级别" },
|
|
311
|
-
"constructor.props.periodchart.data": { ru: "Данные (через
|
|
312
|
-
"constructor.props.periodchart.labels": { ru: "Подписи столбиков (через
|
|
315
|
+
"constructor.props.periodchart.data": { ru: "Данные (через запятую)", en: "Data (comma-separated)", zh: "数据(逗号分隔)" },
|
|
316
|
+
"constructor.props.periodchart.labels": { ru: "Подписи столбиков (через запятую)", en: "Bar labels (comma-separated)", zh: "柱标签(逗号分隔)" },
|
|
313
317
|
"constructor.props.valuetype": { ru: "Тип значения", en: "Value type", zh: "值类型" },
|
|
314
318
|
"constructor.props.action": { ru: "Действие при переключении", en: "Action on switch", zh: "切换时的操作" },
|
|
315
319
|
"constructor.props.bold": { ru: "Жирный", en: "Bold", zh: "粗体" },
|