poe-svelte-ui-lib 1.50.0 → 1.52.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/README.md +1 -1
- package/dist/CommonSnippets.svelte +25 -3
- package/dist/Dragging.README.md +6 -0
- package/dist/Dragging.svelte +24 -13
- 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 +1 -0
- package/dist/dragStyle.js +23 -0
- package/dist/locales/i18n.d.ts +1 -1
- package/dist/locales/i18n.js +2 -2
- package/dist/locales/index.d.ts +3 -0
- package/dist/locales/index.js +6 -0
- package/dist/locales/localize.d.ts +1 -1
- package/dist/locales/localize.js +1 -1
- package/dist/locales/translations.d.ts +1 -1
- package/dist/locales/translations.js +6 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ npm install poe-svelte-ui-lib
|
|
|
110
110
|
|
|
111
111
|
**README компонента** (`src/lib/<Компонент>/README.md`, вкладка «Инфо» витрины) — развёрнутая документация для разработчика. Первые разделы повторяют описание (i) и не должны ему противоречить, дальше идут разработческие: `## Назначение` (плюс заметка о размерах) → `## Пакет данных` (плюс `### Протокол устройства` / пример кода прошивки) → `## Ключи` (таблица «Ключ / Описание», те же формулировки, что в (i)) → `## Особенности` (поведение, ограничения) → `## Пропсы` (таблица «Название / Тип / По умолчанию / Описание», колбэки — `### Колбэки`) → `## Примеры` (Svelte) → `## Конструктор свойств (<Компонент>Props.svelte)` → `## Внутреннее устройство`.
|
|
112
112
|
|
|
113
|
-
**Языки — единственный источник в библиотеке.** Список языков — `LANGUAGES` в `locales/languages.ts`: код, название и флаг (SVG-строка), без Svelte — годится и для серверного кода. **Новый язык — одна запись в `LANGUAGES`**: переключатели языка во всех приложениях (`LOCALES`), колонки окна «Тексты и переводы», многоязычные тексты GUI и словари подхватывают его сами; пока перевода нет — показывается текст языка по умолчанию (`DEFAULT_LANGUAGE`, `ru`). Словари (`Translations`) — строки-объекты по кодам `{ ru: "…", en: "…", zh: "…" }`: обязателен только язык по умолчанию, порядок не важен; недостающие переводы показывает `findMissingTranslations(словарь)`. `locales/i18n.ts` — текущий язык `Language`/`setLanguage` (неизвестный код не применяется, выбор сохраняется в `localStorage` `AppLanguage`), `restoreLanguage()` (сохранённый или язык браузера; стартовое значение всегда язык по умолчанию — серверный рендер совпадает с гидратацией, поэтому SSR-приложения вызывают его в `onMount` корневого layout, SPA — при старте), `LOCALES` (с флагом `flag`) и механизм перевода `createTranslator(словарь)`. Код вне компонентов берёт текущий язык через `getLanguage()` (например, для заголовка `Accept-Language`), ключ хранилища и язык по умолчанию в приложениях не повторяются; серверный код переводит функцией `translate(словарь, язык, ключ)` на язык запроса, разобранный `resolveLanguage(Accept-Language)`. Приложения (DevCloud, WebApp, терминал) свой список языков, хранилище и функцию перевода не держат: берут их из библиотеки и строят свой `$T` из своего словаря — `export const T = createTranslator(translations)`. Одно `setLanguage` переключает и компоненты библиотеки, и приложение. Многоязычные тексты GUI распознаются по виду ключей (коды языков), поэтому данные с языком, убранным из списка, не ломаются.
|
|
113
|
+
**Языки — единственный источник в библиотеке.** Список языков — `LANGUAGES` в `locales/languages.ts`: код, название и флаг (SVG-строка), без Svelte — годится и для серверного кода. **Новый язык — одна запись в `LANGUAGES`**: переключатели языка во всех приложениях (`LOCALES`), колонки окна «Тексты и переводы», многоязычные тексты GUI и словари подхватывают его сами; пока перевода нет — показывается текст языка по умолчанию (`DEFAULT_LANGUAGE`, `ru`). Словари (`Translations`) — строки-объекты по кодам `{ ru: "…", en: "…", zh: "…" }`: обязателен только язык по умолчанию, порядок не важен; недостающие переводы показывает `findMissingTranslations(словарь)`. `locales/i18n.ts` — текущий язык `Language`/`setLanguage` (неизвестный код не применяется, выбор сохраняется в `localStorage` `AppLanguage`), `restoreLanguage()` (сохранённый или язык браузера; стартовое значение всегда язык по умолчанию — серверный рендер совпадает с гидратацией, поэтому SSR-приложения вызывают его в `onMount` корневого layout, SPA — при старте), `LOCALES` (с флагом `flag`) и механизм перевода `createTranslator(словарь)`. Код вне компонентов берёт текущий язык через `getLanguage()` (например, для заголовка `Accept-Language`), ключ хранилища и язык по умолчанию в приложениях не повторяются; серверный код переводит функцией `translate(словарь, язык, ключ)` на язык запроса, разобранный `resolveLanguage(Accept-Language)`. Серверный код, который Node запускает без Vite (например WebSocket-сервер облака через `tsx`), импортирует языки и перевод из `poe-svelte-ui-lib/locales` — корень пакета экспортируется только для Svelte (условие `svelte`) и тянет компоненты, Node его не загрузит. Приложения (DevCloud, WebApp, терминал) свой список языков, хранилище и функцию перевода не держат: берут их из библиотеки и строят свой `$T` из своего словаря — `export const T = createTranslator(translations)`. Одно `setLanguage` переключает и компоненты библиотеки, и приложение. Многоязычные тексты GUI распознаются по виду ключей (коды языков), поэтому данные с языком, убранным из списка, не ломаются.
|
|
114
114
|
|
|
115
115
|
**Языки (ru/en/zh) — два слоя:**
|
|
116
116
|
|
|
@@ -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
|
@@ -76,3 +76,9 @@
|
|
|
76
76
|
## Вид перетаскивания
|
|
77
77
|
|
|
78
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,7 +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 { applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle"
|
|
5
|
+
import { animateReorder, applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle"
|
|
6
6
|
|
|
7
7
|
let dragState = $state({
|
|
8
8
|
isDragging: false,
|
|
@@ -93,21 +93,32 @@
|
|
|
93
93
|
const ghost = dragState.ghostElement
|
|
94
94
|
ghost.style.top = `${e.clientY - dragState.offsetY}px`
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
103
117
|
dragState.dropIndex = newIndex
|
|
104
118
|
|
|
105
119
|
const placeholder = dragState.placeholderElement
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (targetElement && targetElement.parentNode) {
|
|
109
|
-
const nextSibling = placeholder.nextSibling
|
|
110
|
-
if (nextSibling !== targetElement) targetElement.parentNode.insertBefore(placeholder, targetElement)
|
|
120
|
+
if (target.parentNode && placeholder.nextSibling !== target) {
|
|
121
|
+
animateReorder(items, () => target.parentNode?.insertBefore(placeholder, target))
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
|
@@ -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"
|
package/dist/dragStyle.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare const applyDragGhost: (element: HTMLElement) => void;
|
|
2
2
|
export declare const clearDragGhost: (element?: HTMLElement | null) => void;
|
|
3
|
+
export declare const animateReorder: (elements: Element[], mutate: () => void, duration?: number) => void;
|
|
3
4
|
export declare const createDropPlaceholder: (height: number) => HTMLElement;
|
package/dist/dragStyle.js
CHANGED
|
@@ -20,6 +20,29 @@ export const clearDragGhost = (element) => {
|
|
|
20
20
|
element.style[key] = "";
|
|
21
21
|
document.body.style.cursor = "";
|
|
22
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
|
+
};
|
|
23
46
|
/* Место вставки: заливка мягким акцентом и штриховая рамка акцентом */
|
|
24
47
|
export const createDropPlaceholder = (height) => {
|
|
25
48
|
const placeholder = document.createElement("div");
|
package/dist/locales/i18n.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Readable } from "svelte/store";
|
|
2
|
-
import { type LanguageCode, type Translations } from "./languages";
|
|
2
|
+
import { type LanguageCode, type Translations } from "./languages.js";
|
|
3
3
|
export declare const Language: import("svelte/store").Writable<string>;
|
|
4
4
|
export declare const getLanguage: () => LanguageCode;
|
|
5
5
|
export declare function restoreLanguage(): void;
|
package/dist/locales/i18n.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { derived, get, writable } from "svelte/store";
|
|
2
|
-
import translations from "./translations";
|
|
3
|
-
import { DEFAULT_LANGUAGE, LANGUAGES, isLanguageCode, resolveLanguage } from "./languages";
|
|
2
|
+
import translations from "./translations.js";
|
|
3
|
+
import { DEFAULT_LANGUAGE, LANGUAGES, isLanguageCode, resolveLanguage } from "./languages.js";
|
|
4
4
|
/* Язык и механизм перевода — общие для библиотеки и приложений: приложение берёт отсюда Language/setLanguage/LOCALES и строит свой $T
|
|
5
5
|
из своего словаря через createTranslator (см. README «Языки»); переключение языка одним setLanguage меняет и компоненты, и приложение */
|
|
6
6
|
/*
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* $lib/locales/index.ts */
|
|
2
|
+
/* Языки и механизм перевода без Svelte-компонентов — точка входа "poe-svelte-ui-lib/locales" для серверного кода, который Node запускает без Vite
|
|
3
|
+
(tsx: WebSocket-сервер облака); импорты с расширением .js, чтобы их разрешал сам Node */
|
|
4
|
+
export * from "./languages.js";
|
|
5
|
+
export * from "./i18n.js";
|
|
6
|
+
export * from "./localize.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LanguageCode } from "./languages";
|
|
1
|
+
import { type LanguageCode } from "./languages.js";
|
|
2
2
|
export type LocalizedText = string | Partial<Record<LanguageCode, string>>;
|
|
3
3
|
export declare const isLocalizedText: (value: unknown) => value is Partial<Record<LanguageCode, string>>;
|
|
4
4
|
export declare const localizeText: (value: LocalizedText | undefined, lang: string) => string;
|
package/dist/locales/localize.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* $lib/locales/localize.ts */
|
|
2
2
|
/* Многоязычные тексты, введённые в конструкторе: строка (язык по умолчанию) или объект по кодам языков { ru, en, zh, ... };
|
|
3
3
|
язык по умолчанию (DEFAULT_LANGUAGE) — основной и запасной; набор и порядок языков — LANGUAGES (languages.ts) */
|
|
4
|
-
import { DEFAULT_LANGUAGE, LANGUAGE_CODES } from "./languages";
|
|
4
|
+
import { DEFAULT_LANGUAGE, LANGUAGE_CODES } from "./languages.js";
|
|
5
5
|
/* Ключ многоязычного текста: код языка из LANGUAGES или похожий на код (ru, en, pt-BR) — данные с языком, убранным из списка, не ломаются */
|
|
6
6
|
const LANGUAGE_KEY = /^[a-z]{2,3}(-[A-Z]{2})?$/;
|
|
7
7
|
/* Поля свойств, которые считаются текстом для пользователя — их показывает и правит окно «Тексты и переводы» (TextsEditor) */
|
|
@@ -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: "粗体" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poe-svelte-ui-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"svelte": "./dist/index.js"
|
|
26
26
|
},
|
|
27
|
+
"./locales": {
|
|
28
|
+
"types": "./dist/locales/index.d.ts",
|
|
29
|
+
"default": "./dist/locales/index.js"
|
|
30
|
+
},
|
|
27
31
|
"./theme.css": "./dist/theme.css"
|
|
28
32
|
},
|
|
29
33
|
"files": [
|