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 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
- <UI.Button content={{ name: initialValue.name }} componentClass="bg-blue" onClick={() => (showIconLib = true)} />
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 wrapperClass="w-8" componentClass="p-0.5 bg-red" content={{ icon: CrossIcon }} onClick={() => initialValue.updateProperty("")} />
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}
@@ -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-[...]`, что и у строк; при пустом списке не показывается. Тексты элементов правятся только в окне «Тексты и переводы», новые элементы создаются без текста-заглушки.
@@ -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
- const allElements = document.elementsFromPoint(dragState.startX, e.clientY)
97
- const cell = allElements.find((el) => el.id.startsWith(`${elementsId}-`)) as HTMLElement
98
- const newIndex =
99
- cell && (dragState.containerIndex !== null ? Number(cell.id.split("-")[2]) === dragState.containerIndex : true) ? Number(cell.id.split("-")[1]) : null
100
-
101
- /* Находим целевой элемент перед которым нужно вставить placeholder */
102
- if (newIndex === null) return
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
- const targetElement = commandItems[dragState.dropIndex]
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
- <PropsGroup label={$T("constructor.props.periodchart.title")} wrapperClass="mt-3">
54
- {#snippet headerActions()}
55
- <UI.Button
56
- wrapperClass="w-8"
57
- content={{ icon: ButtonAdd }}
58
- onClick={() => {
59
- const newLevel: IPeriodChartLevel = { name: `Level ${(component.properties?.levels?.length ?? 0) + 1}`, data: [] }
60
- const levels = [...(component.properties?.levels || []), newLevel]
61
- updateLevels(levels)
62
- }}
63
- />
64
- {/snippet}
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
- <div class="flex flex-wrap items-end gap-2">
80
- <UI.Select
81
- label={{ name: $T("constructor.props.variable") }}
82
- wrapperClass="min-w-32 flex-1"
83
- type="input"
84
- options={VARIABLE_OPTIONS}
85
- value={VARIABLE_OPTIONS.find((opt) => opt.value === level.variable)}
86
- onUpdate={(value) => {
87
- const levels = [...(component.properties?.levels || [])]
88
- levels[index] = { ...levels[index], variable: (value as IOption).value as string }
89
- updateLevels(levels)
90
- }}
91
- />
92
- <UI.Input
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
- {/each}
126
- <div id="item-{component.properties.levels?.length ?? 0}" class="min-h-4"></div>
127
- </div>
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
- {@render PeriodChartLevels()}
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
- {@render PeriodChartLevels()}
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[]`) — тот же паттерн повторяющегося списка, что у `TabsSettings`/ `ProgressBarOptions`: перетаскивание (`UI.Dragging`) для порядка вкладок, добавление/удаление через `ButtonAdd`/`ButtonDelete`, на каждый уровень — поля `name`, `variable`, `data` и `labels`. `variable` — тот же `Select` по списку переменных устройства (`DeviceVariables` из контекста), что и у `Graph`/`CommonSnippets.Variable`, только по одному на каждый уровень; при любом изменении списка уровней редактор пересобирает `eventHandler.Variables` как объединение всех непустых `variable` со всех уровней — так рантайм подписывается сразу на все нужные ключи. `data`/`labels` остаются полями для литеральных демо-данных — массивы чисел/строк вводятся одной строкой через запятую (`"10, 20, 30"`) и парсятся на лету
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: `Label ${component.properties?.items.length + 1}`,
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 grid-cols-[auto_1fr_auto] items-center gap-2 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 p-2"
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 flex-wrap items-end gap-2">
88
- {#if forConstructor}
89
- <UI.Select
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 shrink-0"
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>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ gridClass: string;
3
+ captions: string[];
4
+ };
5
+ declare const PropsListHeader: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type PropsListHeader = ReturnType<typeof PropsListHeader>;
7
+ export default PropsListHeader;
@@ -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: `Option ${component.properties?.options.length + 1}`,
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 grid-cols-[auto_1fr_auto] items-center gap-2 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 p-2"
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 flex-wrap items-end gap-2">
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
- label={{ name: $T("constructor.props.optionvalue") }}
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]["value"] = value
254
+ options[index]["class"] = value as string
219
255
  updateProperty("options", options, component, onPropertyChange)
220
256
  }}
221
257
  />
222
- {#if forConstructor}
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 shrink-0"
260
+ wrapperClass="w-8"
250
261
  content={{ icon: ButtonDelete }}
251
262
  onClick={() => {
252
263
  const options = [...(component.properties?.options || [])]
@@ -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 grid-cols-[1fr_auto] items-center gap-2 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 p-2">
96
- <div class="flex flex-wrap items-end gap-2">
97
- <!-- Имя варианта — короткая метка бита (до 4 символов) в битовом режиме, не переводится -->
98
- <UI.Input
99
- label={{ name: $T("constructor.props.optionname") }}
100
- wrapperClass="min-w-24 flex-1"
101
- value={option.name}
102
- maxlength={4}
103
- onUpdate={(value) => {
104
- const options = [...(component.properties?.options || [])]
105
- options[index]["name"] = value
106
- updateProperty("options", options, component, onPropertyChange)
107
- }}
108
- />
109
- <UI.Input
110
- label={{ name: $T("constructor.props.optionposition") }}
111
- wrapperClass="min-w-24 flex-1"
112
- value={option.value}
113
- type="number"
114
- number={{ minNum: 0, maxNum: 31, step: 1 }}
115
- onUpdate={(value) => {
116
- const options = [...(component.properties?.options || [])]
117
- options[index]["value"] = value
118
- updateProperty("options", options, component, onPropertyChange)
119
- }}
120
- />
121
- <UI.Select
122
- wrapperClass="min-w-56 flex-[2]"
123
- label={{ name: $T("constructor.props.colors") }}
124
- type="buttons"
125
- options={$optionsStore.COLOR_OPTIONS.filter((option) => option.value !== "bg-max" && option.value !== "bg-gray")}
126
- value={findColorOption($optionsStore.COLOR_OPTIONS, option.class)}
127
- onUpdate={(option) => {
128
- const options = [...(component.properties?.options || [])]
129
- options[index]["class"] = (option as UI.IOption).value
130
- updateProperty("options", options, component, onPropertyChange)
131
- }}
132
- />
133
- <UI.Switch
134
- wrapperClass="min-w-24 bg-blue"
135
- label={{ name: $T("constructor.props.disabled") }}
136
- value={option.disabled}
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 shrink-0"
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-[auto_1fr_auto] items-center gap-2 rounded-lg border border-(--hairline-color) bg-(--container-color)/60 p-2"
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
- <div class="flex flex-wrap items-end gap-2">
140
- <!-- Какая это вкладка: номер и название на текущем языке (название правится в «Тексты и переводы») -->
141
- <div class="flex w-full items-baseline gap-2 text-[13px]">
142
- <span class="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
- <div class="relative min-w-32">
146
- <CommonSnippets
147
- snippet="IconsLib"
148
- initialValue={{
149
- name: $T("constructor.props.table.type.icon"),
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]["disabled"] = value
152
+ items[index]["icon"] = icon as string
171
153
  updateProperty("items", items, component, onPropertyChange)
172
- }}
173
- />
174
- </div>
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"
@@ -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");
@@ -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;
@@ -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,3 @@
1
+ export * from "./languages.js";
2
+ export * from "./i18n.js";
3
+ export * from "./localize.js";
@@ -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;
@@ -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) */
@@ -1,3 +1,3 @@
1
- import type { Translations } from "./languages";
1
+ import type { Translations } from "./languages.js";
2
2
  declare const translations: Translations;
3
3
  export default translations;
@@ -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: "Данные (через пробел)", en: "Data (comma-separated)", zh: "数据(逗号分隔)" },
312
- "constructor.props.periodchart.labels": { ru: "Подписи столбиков (через пробел)", en: "Bar labels (comma-separated)", zh: "柱标签(逗号分隔)" },
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.50.0",
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": [