poe-svelte-ui-lib 1.30.1 → 1.31.1

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
@@ -1 +1,52 @@
1
- Набор компонентов на Svelte в режиме рун
1
+ # poe-svelte-ui-lib
2
+
3
+ Набор UI-компонентов на Svelte 5 (в режиме рун) для конструктора интерфейсов устройств: поля ввода, селекторы, таблицы, графики, виджеты устройства (WiFi, информация об устройстве) и вспомогательные примитивы (модалки, перетаскивание, drag&drop-конструктор свойств).
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ npm install poe-svelte-ui-lib
9
+ ```
10
+
11
+ ### Требования (peerDependencies)
12
+
13
+ - `svelte ^5.0.0`
14
+ - `tailwindcss ^4.0.0` — компоненты стилизуются исключительно Tailwind-утилитами и CSS-переменными темы, без собственного скомпилированного CSS в пакете
15
+
16
+ ### Подключение стилей и темы
17
+
18
+ Пакет не поставляет готовый CSS-файл — цветовые токены (`--back-color`, `--accent-color`, `--red-color` и т.д.) и типографика ожидаются от проекта-потребителя. Возьмите `src/app.css` этого репозитория как стартовый шаблон и скопируйте в свой проект (палитру можно переопределить своими значениями — имена переменных и структура классов `bg-*`/`border-*` должны остаться прежними, чтобы компоненты стилизовались корректно). В `app.css` потребителя также нужно указать Tailwind, откуда сканировать классы из собранного пакета:
19
+
20
+ ```css
21
+ @import "tailwindcss";
22
+ @source "../node_modules/poe-svelte-ui-lib/dist";
23
+ ```
24
+
25
+ Справочник по регулярным выражениям (`docs/info/RegExp.md`) и протокол обмена данными с устройством (`docs/info/ExchangeRules.md`) — вспомогательные материалы для тех, кто настраивает валидацию полей и работу с `DeviceStore`.
26
+
27
+ ## Использование
28
+
29
+ ```svelte
30
+ <script lang="ts">
31
+ import * as UI from "poe-svelte-ui-lib"
32
+
33
+ let value = $state("")
34
+ </script>
35
+
36
+ <UI.Input label={{ name: "Имя" }} bind:value type="text" />
37
+ <UI.Button content={{ name: "Сохранить" }} onClick={() => console.log(value)} />
38
+ ```
39
+
40
+ Полный список компонентов и типов — в `src/lib/index.ts`. У каждого компонента есть README рядом с исходником (`src/lib/<Component>/README.md`, для `Modal`/`Dragging` — `src/lib/<Component>.README.md`) с описанием пропсов, примерами и внутренним устройством; живая витрина всех компонентов — `npm run dev` → `/components/all`.
41
+
42
+ ## Разработка
43
+
44
+ ```bash
45
+ npm run dev # витрина компонентов (SvelteKit dev-сервер)
46
+ npm run build # сборка пакета (svelte-kit sync + svelte-package + publint) → dist/
47
+ npm run Formatting # prettier --write .
48
+ ```
49
+
50
+ ---
51
+
52
+ Лицензия: MIT — см. `LICENSE`.
@@ -11,8 +11,8 @@
11
11
  | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
12
  | `wrapperClass` | `string` | `""` | CSS-классы для обёртки компонента |
13
13
  | `type` | `"line" \| "empty"` | `"line"` | `line` — видимая полоса, `empty` — чистый отступ без полосы |
14
- | `width` | `number` | `2` | Толщина полосы в пикселях (применяется только при `type="line"`) |
15
- | `color` | `string` | `"bg-max"` | Класс цвета полосы из общей палитры (`bg-max`, `bg-red`, `bg-yellow`, `bg-green`, `bg-blue`, `bg-purple`, `bg-gray`) — сам по себе только переопределяет `--bg-color`, красит его рядом стоящий статический класс `bg-(--bg-color)` (как в `Button.svelte`) |
14
+ | `width` | `number` | `1` | Толщина полосы в пикселях (применяется только при `type="line"`) |
15
+ | `color` | `string` | `""` | Класс цвета полосы из общей палитры (`bg-max`, `bg-red`, `bg-yellow`, `bg-green`, `bg-blue`, `bg-purple`, `bg-gray`); при пустом значении полоса красится в нейтральный `--hairline-color`, при заданном `bg-*` — в `--bg-color` (как в `Button.svelte`) |
16
16
  | `anchor` | `"top" \| "center" \| "bottom"` | `"center"` | Положение полосы внутри выделенной компоненту высоты сетки |
17
17
 
18
18
  ## События
@@ -0,0 +1,74 @@
1
+ # Dragging Component
2
+
3
+ ## Описание
4
+
5
+ Перетаскивание элементов списка мышью — самостоятельная "ручка" (иконка `MoveVerticalIcon`), которая при зажатии и перемещении мыши переставляет элемент внутри переданного массива (`array`) и визуально показывает призрак перетаскиваемого элемента и плейсхолдер на месте вставки. Компонент не рендерит сам список — только ручку; сортировкой самого массива и его перерисовкой занимается вызывающий код через `onUpdate`.
6
+
7
+ Лежит прямо в `src/lib/Dragging.svelte` (без отдельной папки), у него нет парного `*Props.svelte` — это низкоуровневый примитив для чужих конструкторских панелей (например `TabsSettings`/`PeriodChartProps` используют его для сортировки вкладок/уровней), а не то, что добавляется на холст конструктора напрямую.
8
+
9
+ ## Пропсы
10
+
11
+ | Название | Тип | По умолчанию | Описание |
12
+ | ---------------- | --------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
13
+ | `wrapperClass` | `string` | `undefined` | CSS-классы для обёртки ручки перетаскивания |
14
+ | `container` | `HTMLDivElement` | `-` (обязателен) | Контейнер, внутри которого ищутся элементы списка (по атрибуту `id`, см. ниже) для определения точки вставки |
15
+ | `array` | `any[]` | `-` (обязателен) | Массив элементов списка — мутируется на месте (`splice`) в процессе перетаскивания |
16
+ | `elementsId` | `string` | `"item"` | Префикс `id` DOM-элементов списка: ожидается `#{elementsId}-{index}` или `#{elementsId}-{index}-{containerIndex}` |
17
+ | `elementIndex` | `number` | `-` (обязателен) | Индекс элемента, которому принадлежит эта ручка, в `array` |
18
+ | `containerIndex` | `number` | `undefined` | Индекс контейнера — нужен, когда на странице несколько независимых списков и id элементов включают ещё и номер контейнера |
19
+ | `onUpdate` | `(arr: any[], index?: number \| null) => void` | `undefined` | Вызывается дважды за одно перетаскивание: сразу после извлечения элемента (`array` без него) и по завершении (`array` с элементом на новом месте, вторым аргументом — `containerIndex`) |
20
+
21
+ ## События
22
+
23
+ Компонент не генерирует DOM-события — используйте `onUpdate` для сохранения нового порядка `array`.
24
+
25
+ ## Пример
26
+
27
+ Список из N строк, каждая — `<div id="item-{i}">...</div>` внутри общего контейнера; ручка перетаскивания — `UI.Dragging` с тем же `elementsId`/`elementIndex`, что и у `id` строки:
28
+
29
+ ```svelte
30
+ <script>
31
+ import * as UI from "poe-svelte-ui-lib"
32
+
33
+ let container = $state()
34
+ let items = $state(["Первый", "Второй", "Третий"])
35
+
36
+ const handleUpdate = (arr) => {
37
+ items = arr
38
+ }
39
+ </script>
40
+
41
+ <div bind:this={container} class="flex flex-col gap-1">
42
+ {#each items as item, index (item)}
43
+ <div id={`item-${index}`} class="flex items-center gap-2 bg-(--field-color) p-2">
44
+ <UI.Dragging {container} array={items} elementIndex={index} onUpdate={handleUpdate} />
45
+ <span>{item}</span>
46
+ </div>
47
+ {/each}
48
+ </div>
49
+ ```
50
+
51
+ ## Внутренняя архитектура
52
+
53
+ ### Реактивность
54
+
55
+ - `dragState` (`$state`) — служебное состояние текущего перетаскивания: перетаскиваемый элемент, экранные координаты, DOM-узлы призрака/плейсхолдера, целевой индекс вставки
56
+ - `commandItems` (`$state`) — снимок всех DOM-элементов списка на момент начала перетаскивания (`container.querySelectorAll`), используется для определения элемента под курсором
57
+ - `$effect` навешивает/снимает глобальные обработчики `mousemove`/`mouseup` на весь документ, пока идёт перетаскивание (`dragState.isDragging`)
58
+
59
+ ### Директивы
60
+
61
+ - `onmousedown` на ручке — запускает перетаскивание (`startDrag`), если в массиве больше одного элемента
62
+ - Глобальные `mousemove`/`mouseup` (см. выше) — двигают призрак/плейсхолдер (`processDrag`) и завершают перестановку (`endDrag`)
63
+
64
+ ### Слоты
65
+
66
+ - Компонент не использует слоты — контент фиксирован (иконка `MoveVerticalIcon`)
67
+
68
+ ## Заметки
69
+
70
+ ### Ограничения
71
+
72
+ - При единственном элементе в `array` (`array.length == 1`) ручка визуально приглушена (`opacity-50`) и перетаскивание не запускается
73
+ - Работает только с `MouseEvent` — нет отдельной поддержки touch/pointer-событий и клавиатурной перестановки (ручка фокусируема как `role="button"`, но `onkeydown` не обрабатывается — известное ограничение доступности)
74
+ - `container`/`elementIndex`/`array` обязательны — компонент не проверяет их наличие и упадёт при обращении к `array.length` в классе, если `array` не передан
@@ -10,6 +10,7 @@
10
10
  | ------------------- | -------------------------------------------- | --------------------- | ------------------------------------------------ |
11
11
  | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
12
  | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
13
+ | `componentClass` | `string` | `""` | Дополнительные CSS-классы для элемента с именем выбранного файла |
13
14
  | `label` | `object` | `-` | Настройки заголовка компонента |
14
15
  | `label.name` | `string` | `""` | Текст заголовка компонента |
15
16
  | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
@@ -18,9 +18,9 @@
18
18
  | `disabled` | `boolean` | `false` | Отключает ввод: блокирует взаимодействие с элементом и визуально затемняет компонент |
19
19
  | `readonly` | `boolean` | `false` | Режим только для чтения: позволяет выделять и копировать текст, но запрещает редактирование |
20
20
  | `value` | `string \| number` | `undefined` | Текущее значение поля; поддерживает двустороннее связывание (`$bindable`) |
21
- | `range` | `object` | `undefined` | Диапазон битов для типа `bitMode`: `start`/`end` — индексы, используемые для расчёта `number.maxNum` (`2^(end - start + 1) - 1`) |
22
- | `range.start` | `number` | `undefined` | Начальный индекс бита |
23
- | `range.end` | `number` | `undefined` | Конечный индекс бита |
21
+ | `range` | `object` | `undefined` | ⚠️ Читается только конструкторской панелью `InputProps.svelte` (для расчёта `number.maxNum`, `2^(end - start + 1) - 1`) — сам `Input.svelte` проп `range` не использует. На голом `<UI.Input type="bitMode">` диапазон задаётся напрямую через `number.maxNum` |
22
+ | `range.start` | `number` | `undefined` | Начальный индекс бита (только для конструкторской панели) |
23
+ | `range.end` | `number` | `undefined` | Конечный индекс бита (только для конструкторской панели) |
24
24
  | `maxlength` | `number` | `100` | Максимальное количество символов, разрешённых для ввода |
25
25
  | `textareaRows` | `number` | `3` | Количество видимых строк для типа `text-area` |
26
26
  | `placeholder` | `string` | `""` | Текст-подсказка, отображаемый в пустом поле ввода |
@@ -0,0 +1,104 @@
1
+ # Modal Component
2
+
3
+ ## Описание
4
+
5
+ Модальное окно — оверлей с подложкой (40% чёрного + blur), закрывающийся по клику вне окна, по `Escape` или по кнопке-крестику в шапке. Поддерживает стек: несколько одновременно открытых модалок укладываются по `z-index` в порядке открытия, `Escape`/клик-вне обрабатывает только самая верхняя (`isTopmost`). Содержимое передаётся через обязательный snippet `main`, футер (кнопки действий) — через необязательный `footer`.
6
+
7
+ Компонент лежит прямо в `src/lib/Modal.svelte` (без отдельной папки, в отличие от большинства других компонентов библиотеки), поэтому у него нет парного файла `*Props.svelte` — конструкторской панели для него не предусмотрено.
8
+
9
+ ## Пропсы
10
+
11
+ | Название | Тип | По умолчанию | Описание |
12
+ | -------------- | ---------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
13
+ | `isOpen` | `boolean` | `false` | Открыто ли модальное окно; поддерживает двустороннее связывание (`$bindable`) |
14
+ | `title` | `string` | `undefined` | Заголовок в шапке окна |
15
+ | `wrapperClass` | `string` | `""` | CSS-классы для окна (например для другой ширины/фона) |
16
+ | `mainClass` | `string` | `""` | CSS-классы для контейнера содержимого (область под `main`) |
17
+ | `width` | `string` | `""` | Явная ширина окна (CSS-значение, например `"600px"`); по умолчанию берётся из `wrapperClass`/`w-300` |
18
+ | `main` | `Snippet` | `-` (обязателен) | Содержимое окна |
19
+ | `footer` | `Snippet` | `undefined` | Содержимое подвала (обычно кнопки действий, выравниваются в ряд справа налево) |
20
+ | `onCancel` | `() => void` | `() => (isOpen = false)` | Вызывается при закрытии окна (крестик, клик вне окна, `Escape`) |
21
+
22
+ ## События
23
+
24
+ Компонент не генерирует DOM-события — используйте `onCancel` для реакции на закрытие.
25
+
26
+ ## Примеры
27
+
28
+ ### Базовое окно
29
+
30
+ ```svelte
31
+ <script>
32
+ import * as UI from "poe-svelte-ui-lib"
33
+
34
+ let isOpen = $state(false)
35
+ </script>
36
+
37
+ <UI.Button content={{ name: "Открыть" }} onClick={() => (isOpen = true)} />
38
+
39
+ <UI.Modal bind:isOpen title="Заголовок окна">
40
+ {#snippet main()}
41
+ <p>Содержимое модального окна.</p>
42
+ {/snippet}
43
+ </UI.Modal>
44
+ ```
45
+
46
+ ### С футером (кнопки действий)
47
+
48
+ ```svelte
49
+ <script>
50
+ import * as UI from "poe-svelte-ui-lib"
51
+
52
+ let isOpen = $state(false)
53
+ </script>
54
+
55
+ <UI.Modal bind:isOpen title="Подтверждение" width="400px">
56
+ {#snippet main()}
57
+ <p>Вы уверены?</p>
58
+ {/snippet}
59
+ {#snippet footer()}
60
+ <UI.Button content={{ name: "Подтвердить" }} onClick={() => (isOpen = false)} />
61
+ <UI.Button content={{ name: "Отмена" }} wrapperClass="bg-gray" onClick={() => (isOpen = false)} />
62
+ {/snippet}
63
+ </UI.Modal>
64
+ ```
65
+
66
+ ## Стек модалок (`ModalStack`)
67
+
68
+ Экспортируется отдельно из пакета (`src/lib/ModalStackStore.ts`) — обычный Svelte-стор со списком идентификаторов открытых модалок в порядке открытия:
69
+
70
+ ```ts
71
+ export const ModalStack = {
72
+ subscribe: ..., // Writable<string[]>["subscribe"]
73
+ open: (id: string) => void, // добавляет id в конец стека
74
+ close: (id: string) => void, // убирает id из стека
75
+ }
76
+ ```
77
+
78
+ Каждый экземпляр `Modal.svelte` сам регистрируется в `ModalStack` при открытии/закрытии (по случайному `crypto.randomUUID()`, не связанному с пропсами) — вручную вызывать `ModalStack.open`/`close` не требуется, это внутренний механизм для вычисления `z-index` и того, какая модалка "самая верхняя" (обрабатывает `Escape`/клик вне себя). Использовать `ModalStack` напрямую имеет смысл только если нужно узнать, сколько модалок сейчас открыто, или в каком порядке — например `$ModalStack.length > 0` для блокировки скролла `body`.
79
+
80
+ ## Внутренняя архитектура
81
+
82
+ ### Реактивность
83
+
84
+ - `modalId` — случайный идентификатор экземпляра, генерируется один раз при создании компонента
85
+ - `zIndex` (`$derived.by`) — вычисляется по позиции `modalId` в `$ModalStack` (`100 + indexInStack`)
86
+ - `isTopmost` (`$derived`) — правда, если этот экземпляр последний в стеке
87
+ - `$effect` синхронизирует `isOpen` с `ModalStack.open`/`close`, включая очистку при размонтировании
88
+
89
+ ### Директивы
90
+
91
+ - `transition:fade` для подложки, `transition:scale` для окна
92
+ - `data-modal`/`data-modal-backdrop` — атрибуты для распознавания "клика внутри" и "клика по подложке"
93
+ - `data-ui-portal` — элементы, вынесенные в портал (например выпадающий список `Select`), не считаются кликом "снаружи" модалки
94
+
95
+ ### Слоты
96
+
97
+ - `main` (обязателен) и `footer` (опционален) — оба snippet, не `children`/default slot
98
+
99
+ ## Заметки
100
+
101
+ ### Ограничения
102
+
103
+ - `main` обязателен — компонент не поддерживает вызов без содержимого через обычные дочерние элементы (`children`), только через `{#snippet main()}...{/snippet}`
104
+ - Нет собственной конструкторской панели (`*Props.svelte`) — используется напрямую в коде приложения, не как примитив визуального конструктора
@@ -37,7 +37,30 @@
37
37
 
38
38
  const currentLevel = $derived<IPeriodChartLevel | undefined>(levels[currentLevelIndex])
39
39
  const values = $derived(currentLevel?.data ?? [])
40
- const maxValue = $derived(values.length ? Math.max(...values, 0) * 1.1 || 1 : 1)
40
+
41
+ /* Шкала Y: шаг 1/2/2.5/5·10ⁿ, ~4 деления, верх округляется вверх до шага, низ всегда 0 */
42
+ const AXIS_DIVISIONS = 4
43
+ const axis = $derived.by(() => {
44
+ const max = values.reduce((m, v) => (Number.isFinite(v) && v > m ? v : m), 0)
45
+ if (max <= 0) return { step: 0.25, top: 1, ticks: [1, 0.75, 0.5, 0.25, 0] }
46
+ const raw = max / AXIS_DIVISIONS
47
+ const mag = 10 ** Math.floor(Math.log10(raw))
48
+ const step = ([1, 2, 2.5, 5, 10].find((m) => m * mag >= raw * (1 - 1e-9)) ?? 10) * mag
49
+ const count = Math.max(1, Math.ceil(max / step - 1e-9))
50
+ return { step, top: step * count, ticks: Array.from({ length: count + 1 }, (_, i) => step * (count - i)) }
51
+ })
52
+
53
+ /* Подпись деления: k/M для больших значений, число знаков после запятой — по величине шага */
54
+ const formatTick = (v: number) => {
55
+ const [div, suffix] = axis.top >= 1e6 ? [1e6, "M"] : axis.top >= 1e4 ? [1e3, "k"] : [1, ""]
56
+ const step = axis.step / div
57
+ let decimals = 0
58
+ while (decimals < 10 && Math.abs(Math.round(step * 10 ** decimals) - step * 10 ** decimals) > 1e-6) decimals++
59
+ return (v / div).toFixed(decimals) + suffix
60
+ }
61
+
62
+ /* Значение во всплывающей подсказке: 4 значащие цифры, без потери малых величин вроде 0.0734 */
63
+ const formatValue = (v: number) => (Number.isInteger(v) ? String(v) : String(Number(v.toPrecision(4))))
41
64
 
42
65
  const selectLevel = (index: number) => {
43
66
  if (index === currentLevelIndex) return
@@ -46,7 +69,7 @@
46
69
  }
47
70
 
48
71
  const barLabel = (i: number) => currentLevel?.labels?.[i] ?? String(i + 1)
49
- const barHeightPercent = (value: number) => Math.max((value / maxValue) * 100, value > 0 ? 2 : 0)
72
+ const barHeightPercent = (value: number) => Math.max((value / axis.top) * 100, value > 0 ? 2 : 0)
50
73
 
51
74
  /* Подписи по оси прореживаются: при 31 значении все они не читаются */
52
75
  const labelStep = $derived(Math.ceil(values.length / 12) || 1)
@@ -80,38 +103,60 @@
80
103
  {#if values.length === 0}
81
104
  <div class="flex h-40 w-full items-center justify-center text-[13px] text-(--faint-color)">Нет данных</div>
82
105
  {:else}
83
- <div class="flex h-40 w-full items-end gap-1">
84
- {#each values as value, i}
85
- <div
86
- class="relative flex h-full flex-1 flex-col items-center justify-end"
87
- role="img"
88
- aria-label={`${barLabel(i)}: ${value}${unit}`}
89
- onmouseenter={() => (hoveredIndex = i)}
90
- onmouseleave={() => (hoveredIndex = null)}
91
- >
92
- {#if hoveredIndex === i}
93
- <span
94
- class="pointer-events-none absolute -top-1 z-10 -translate-y-full rounded-lg border border-(--hairline-color) bg-(--back-color) px-2 py-1 text-[12px] font-semibold tabular-nums whitespace-nowrap shadow-(--elevation-2)"
106
+ <div class="flex w-full gap-2">
107
+ <!-- Шкала Y: подписи нулевой высоты, центр каждой ровно на своей линии сетки -->
108
+ <div class="flex h-40 shrink-0 flex-col justify-between">
109
+ {#each axis.ticks as tick}
110
+ <div class="flex h-0 items-center justify-end text-[10px] font-medium text-(--faint-color) tabular-nums">
111
+ {formatTick(tick)}
112
+ </div>
113
+ {/each}
114
+ </div>
115
+
116
+ <div class="flex min-w-0 flex-1 flex-col gap-1.5">
117
+ <div class="relative flex h-40 w-full items-end gap-1">
118
+ {#each axis.ticks as tick}
119
+ <div
120
+ class="pointer-events-none absolute inset-x-0 border-t {tick === 0
121
+ ? 'border-(--hairline-color)'
122
+ : 'border-dashed border-(--hairline-color)/60'}"
123
+ style="bottom: {(tick / axis.top) * 100}%"
124
+ ></div>
125
+ {/each}
126
+
127
+ {#each values as value, i}
128
+ <div
129
+ class="relative flex h-full flex-1 flex-col items-center justify-end"
130
+ role="img"
131
+ aria-label={`${barLabel(i)}: ${value}${unit}`}
132
+ onmouseenter={() => (hoveredIndex = i)}
133
+ onmouseleave={() => (hoveredIndex = null)}
95
134
  >
96
- {barLabel(i)}: {Math.round(value)}{unit}
97
- </span>
98
- {/if}
99
- <div
100
- class="w-full rounded-t-[3px] transition-[height,background-color] duration-150 {hoveredIndex === i
101
- ? 'bg-(--accent-color)'
102
- : 'bg-(--accent-color)/35'}"
103
- style="height: {barHeightPercent(value)}%"
104
- ></div>
135
+ {#if hoveredIndex === i}
136
+ <span
137
+ class="pointer-events-none absolute -top-1 z-10 -translate-y-full rounded-lg border border-(--hairline-color) bg-(--back-color) px-2 py-1 text-[12px] font-semibold tabular-nums whitespace-nowrap shadow-(--elevation-2)"
138
+ >
139
+ {barLabel(i)}: {formatValue(value)}{unit}
140
+ </span>
141
+ {/if}
142
+ <div
143
+ class="w-full rounded-t-[3px] transition-[height,background-color] duration-150 {hoveredIndex === i
144
+ ? 'bg-(--accent-color)'
145
+ : 'bg-(--accent-color)/35'}"
146
+ style="height: {barHeightPercent(value)}%"
147
+ ></div>
148
+ </div>
149
+ {/each}
105
150
  </div>
106
- {/each}
107
- </div>
108
151
 
109
- <div class="flex w-full gap-1">
110
- {#each values as _, i}
111
- <div class="flex-1 truncate text-center text-[10px] font-bold text-(--faint-color)">
112
- {i % labelStep === 0 ? barLabel(i) : ""}
152
+ <div class="flex w-full gap-1">
153
+ {#each values as _, i}
154
+ <div class="flex-1 truncate text-center text-[10px] font-bold text-(--faint-color)">
155
+ {i % labelStep === 0 ? barLabel(i) : ""}
156
+ </div>
157
+ {/each}
113
158
  </div>
114
- {/each}
159
+ </div>
115
160
  </div>
116
161
  {/if}
117
162
  </div>
@@ -13,14 +13,14 @@
13
13
  | Название | Тип | По умолчанию | Описание |
14
14
  | ------------------- | --------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
15
  | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
16
- | `wrapperClass` | `string` | `"bg-blue"` | CSS-классы обёртки — через `bg-*` задаёт цвет переключателя и столбиков |
16
+ | `wrapperClass` | `string` | `""` | CSS-классы обёртки — через `bg-*` задаёт цвет переключателя и столбиков (по умолчанию нейтрально, без декоративной заливки) |
17
17
  | `label` | `{ name?: string; class?: string }` | `{ name: "", class: "" }` | Заголовок над графиком |
18
18
  | `levels` | `IPeriodChartLevel[]` | 4 демо-уровня со случайными данными | Уровни детализации, порядок = порядок вкладок переключателя; открывается всегда на первом (индекс 0) |
19
19
  | `levels[].name` | `string` | `-` | Подпись вкладки, напр. `"Час"` |
20
20
  | `levels[].variable` | `string` | `undefined` | Имя переменной устройства, откуда берутся `data` этого уровня, напр. `"PWR.EHour"` — используется конструктором для подписки, сам компонент по нему ничего не запрашивает |
21
21
  | `levels[].data` | `number[]` | `-` | Значения — длина массива = число столбиков, ничем не ограничена |
22
22
  | `levels[].labels` | `string[]` | `undefined` | Подписи столбиков по X; если не заданы — используется индекс+1 |
23
- | `unit` | `string` | `""` | Единицы измерения, добавляются к значению в `aria-label`, напр. `"Вт·ч"` |
23
+ | `unit` | `string` | `""` | Единицы измерения, добавляются к значению в подсказке и `aria-label`, напр. `" Вт·ч"` |
24
24
  | `onLevelChange` | `(index: number, level: IPeriodChartLevel) => void` | `() => {}` | Вызывается при переключении вкладки — только уведомление, без фетча внутри |
25
25
 
26
26
  ## События
@@ -72,7 +72,9 @@
72
72
 
73
73
  - `currentLevelIndex` (`$state`) — индекс активной вкладки, всегда инициализируется `0` (первый уровень)
74
74
  - `currentLevel`/`values` (`$derived`) — текущий уровень и его массив значений
75
- - `maxValue` (`$derived`) — максимум текущего массива с отступом ×1.1 для автомасштаба высоты столбиков
75
+ - `axis` (`$derived`) — шкала Y по максимуму текущего массива: шаг из ряда 1 / 2 / 2.5 / 5 × 10ⁿ
76
+ (~4 деления), верх шкалы = максимум, округлённый вверх до шага, низ всегда 0; высота столбиков
77
+ считается от верха шкалы
76
78
 
77
79
  ### Сторы и зависимости
78
80
 
@@ -92,23 +94,24 @@
92
94
  ### Адаптивность
93
95
 
94
96
  - Столбики растягиваются на всю ширину контейнера (`flex-1` на каждый), высота — в процентах от
95
- зоны столбиков фиксированной высоты (`h-40`); значение (округлено до целого, `Math.round`)
96
- выводится прямо внутри столбика, по центру — видно всегда, без наведения. Для маленьких значений
97
- столбик слишком низкий, чтобы число влезло внутрь (`overflow-hidden` его обрежет) — если высота
98
- столбика меньше `MIN_LABEL_PX` (18px из `BAR_ZONE_PX`=160px = `h-40`), число рисуется НАД
99
- столбиком вместо "внутри". Подписи (`labels[i]`/индекс+1) выводятся отдельной строкой под этой
100
- зоной, тоже без наведения
101
- - Никакого тултипа — компонент не реагирует на hover вообще
102
- - Полностью на CSS-переменных темы (`--back-color`, `--border-color`, `--shadow-color`,
103
- `--bg-color` через `wrapperClass`) — тёмная/светлая тема не требует отдельной настройки
97
+ верха шкалы в зоне фиксированной высоты (`h-40`); ненулевое значение всегда видно минимум на 2 %
98
+ - Шкала Y слева: подписи делений + горизонтальная сетка за столбиками (пунктир, базовая линия 0 —
99
+ сплошная). Шкала пересчитывается автоматически при смене уровня или данных: `0…100`,
100
+ `0…0.08` с шагом `0.02`, `0…3M`. Число знаков после запятой — по величине шага, для больших
101
+ значений — суффиксы `k` (верх ≥ 10 000) и `M` (≥ 1 000 000)
102
+ - Точное значение — в подсказке при наведении на столбик: 4 значащие цифры + `unit`
103
+ (`910: 0.0734 мкВт/см²`), малые величины не теряются
104
+ - Подписи по X (`labels[i]`/индекс+1) — отдельной строкой под столбиками, при большом числе
105
+ столбиков прореживаются (не больше ~12 подписей)
106
+ - Полностью на CSS-переменных темы (`--back-color`, `--accent-color`, `--hairline-color`,
107
+ `--faint-color`) — тёмная/светлая тема не требует отдельной настройки
104
108
 
105
109
  ### Ограничения
106
110
 
107
111
  - Компонент презентационный — не хранит историю, не опрашивает устройство сам; при смене
108
112
  `levels` извне (например после ответа на `onLevelChange`) график перерисовывается автоматически
109
- - Значение внутри столбика округляется до целого только для отображения (`Math.round`) — исходные
110
- дробные данные в `levels[].data` не изменяются; `unit` в самом столбике не показывается (мало
111
- места), только в `aria-label` для доступности
113
+ - Отрицательные значения не отображаются — шкала всегда начинается с 0
114
+ - Округление только для отображения — исходные данные в `levels[].data` не изменяются
112
115
 
113
116
  ### Производительность
114
117
 
@@ -10,21 +10,20 @@
10
10
  | -------------- | --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
11
11
  | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
12
  | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
13
+ | `componentClass` | `string` | `""` | Дополнительные CSS-классы для самого поля/кнопок выбора |
13
14
  | `disabled` | `boolean` | `false` | Состояние отключения компонента |
14
15
  | `label` | `object` | `-` | Настройки заголовка компонента |
15
16
  | `label.name` | `string` | `""` | Текст заголовка компонента |
16
17
  | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
17
18
  | `type` | `"select" \| "buttons" \| "input"` | `"select"` | Тип отображения: `select` — выпадающий список с кнопкой, `buttons` — горизонтальный ряд кнопок, `input` — поле ввода с автодополнением и поиском |
18
19
  | `multiSelect` | `boolean` | `false` | Режим множественного выбора: работает только с `type: "buttons"`; при `true` позволяет выбирать несколько опций, значение передаётся как массив |
19
- | `listHeight` | `string` | `undefined` | Максимальная высота выпадающего списка опций (для типов `select`/`input`) |
20
- | `value` | `IOption<T> \| IOption<T>[] \| null` | `null` | Текущее выбранное значение(я); поддерживает двустороннее связывание (`$bindable`) |
20
+ | `listHeight` | `string` | `"16rem"` | Максимальная высота выпадающего списка опций (для типов `select`/`input`) |
21
+ | `value` | `IOption<T> \| IOption<T>[] \| undefined` | `undefined` | Текущее выбранное значение(я); поддерживает двустороннее связывание (`$bindable`) |
21
22
  | `options` | `IOption<T>[]` | `[]` | Массив доступных опций |
22
- | `bitMode` | `boolean` | `false` | Режим битового выбора: значение и `range` интерпретируются как битовая маска |
23
- | `range` | `object` | `undefined` | Диапазон битов, используемый в `bitMode` |
24
- | `range.start` | `number` | `-` | Начальный бит диапазона |
25
- | `range.end` | `number` | `-` | Конечный бит диапазона |
26
23
  | `onUpdate` | `(value: IOption<T> \| IOption<T>[]) => void` | `undefined` | Обработчик изменения значения |
27
24
 
25
+ > **`bitMode`/`range`**: эти поля объявлены в типе `ISelectProps` и настраиваются в конструкторской панели (`SelectProps.svelte`), но сам `Select.svelte` их не читает и не использует — они не оказывают никакого эффекта на голый `<UI.Select>`. Если нужен битовый выбор в реальном рантайме, интерпретируйте маску на стороне вызывающего кода и передавайте уже готовый `value`/`options`.
26
+
28
27
  ### Вложенные типы
29
28
 
30
29
  #### IOption
@@ -9,7 +9,7 @@
9
9
  | Название | Тип | По умолчанию | Описание |
10
10
  | -------------------- | ------------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
11
  | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
- | `height` | `string` | `"2rem"` | Высота переключателя: определяет размеры трека и ползунка; ширина в режиме `horizontal` рассчитывается как `height * 2` |
12
+ | `height` | `string` | `"1.625rem"` | Высота переключателя: определяет размеры трека и ползунка; ширина в режиме `horizontal` рассчитывается как `height * 2` |
13
13
  | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
14
14
  | `disabled` | `boolean` | `false` | Состояние отключения компонента |
15
15
  | `label` | `object` | `-` | Настройки заголовка и подписей компонента |
@@ -20,6 +20,8 @@
20
20
  | `componentClass` | `string` | `""` | "Мастер-цвет" виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`) |
21
21
  | `label` | `{ name?: string }` | `{ name: "Информация об устройстве" }` | Заголовок карточки |
22
22
  | `value` | `IWidgetDeviceInfoConfig` | внутренний дефолт | Текущая информация об устройстве |
23
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (заголовок остаётся); поддерживает двустороннее связывание (`$bindable`) |
24
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе состояние сворачивания переживает перезагрузку страницы |
23
25
  | `onSave` | `(info: IWidgetDeviceInfoConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — редактируемые поля одним пакетом |
24
26
  | `onRestart` | `() => void` | `undefined` | Срабатывает при нажатии "Перезагрузить" |
25
27
 
@@ -24,6 +24,8 @@ DeviceStore/WebSocket — только `value`/`onSave`, вся отправка
24
24
  | `value` | `IWidgetWiFiConfig` | внутренний дефолт | Текущая конфигурация WiFi |
25
25
  | `allowedModes` | `number[]` | `[1, 2, 3]` | Какие режимы показывать в сегмент-контроле (1=STA, 2=AP, 3=STA+AP) |
26
26
  | `confirmOnAP` | `boolean` | `true` | Показывать предупреждение при сохранении чистого режима AP |
27
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (заголовок остаётся); поддерживает двустороннее связывание (`$bindable`) |
28
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе сворачивание переживает перезагрузку страницы |
27
29
  | `onScan` | `() => Promise<{ id: string; name: string; value: string }[]>` | `undefined` | Обработчик сканирования сетей (в проде — `Header: GET, Argument: APsList`) |
28
30
  | `onSave` | `(config: IWidgetWiFiConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — конфиг уходит одним пакетом |
29
31
 
package/dist/index.d.ts CHANGED
@@ -46,4 +46,4 @@ export { default as WidgetDeviceInfoProps } from "./WidgetDeviceInfo/WidgetDevic
46
46
  export * from "./locales/i18n";
47
47
  export * from "./locales/translations";
48
48
  export * from "./ModalStackStore";
49
- export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IGraphProps, type IGraphDataObject, type ITableHeader, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
49
+ export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.30.1",
3
+ "version": "1.31.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -30,7 +30,8 @@
30
30
  "runes"
31
31
  ],
32
32
  "peerDependencies": {
33
- "svelte": "^5.0.0"
33
+ "svelte": "^5.0.0",
34
+ "tailwindcss": "^4.0.0"
34
35
  },
35
36
  "dependencies": {
36
37
  "tailwind-merge": "^3.6.0"