poe-svelte-ui-lib 1.67.0 → 1.69.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
@@ -59,7 +59,7 @@ npm install poe-svelte-ui-lib
59
59
 
60
60
  **Названия смарт-виджетов** — в словаре библиотеки на всех языках (WiFi, «Об устройстве», «Файловая система», «Стек и задачи», «RGB лента», «Пилотажный дисплей», «Карта»); пустой `label.name` показывает название на языке пользователя, `widgetTitle(type)` отдаёт его сразу на всех языках `{ ru, en, zh }` — так конструктор заполняет заголовок нового виджета.
61
61
 
62
- **Ядро протоколов** — `poe-svelte-ui-lib/protocols` (без Svelte, работает и под Node без Vite): общий для DevCloud, WebApp и SYP-DevTerminal CBOR-кодек (RFC 8949) — `encodeCbor`/`decodeCbor`, `CborEncoder`/`CborDecoder`; `valueToCbor`/`cborToValue` — обычные значения, двоичные данные как `Uint8Array` (WebSocket, хранилище, телеметрия); `jsonToCbor`/`cborToJson` — JSON-совместимые значения, двоичные данные в base64, строки под ключами с `BIN` — байты (поля ввода терминала и отладки). Пакет `poe_cbor_pkt` `{ TrgBusID : [RetBusID, HA_Code, Payload, CRC16] }` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты с одинаковым `TrgBusID` не склеиваются, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `0xFFFF`), `makeHaCode`/`splitHaCode`, `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc` — строго побайтные, как в `common.c` прошивок. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом). Рамка POESerial (UART-консоль `system_console`, `[SOH] HDR [US] ARG [STX] VAL [ETX] CRC8 [US] FHS [EOT]`): `encodePoeSerialFrame` — команда устройству, `createPoeSerialParser().push(байты)` — потоковый разбор кадров, строк Teleplot (`plot`) и прочего текста (`raw`), с проверкой CRC `crc8Rohc` строго побайтно, как `crc8_rohc` в `common.c` прошивок. CAN/CAN FD через slcan-адаптер (`can_service` прошивки): `canFragments`/`encodeCanPacket` — нарезка пакета на кадры как `CAN_Send` (бит 10 ID — «последний», биты 0..9 — `packetId`, по 8 / 64 байта), `encodeSlcanFrame`, `createCanParser().push(байты)` — разбор кадров slcan со сборкой фрагментов и обрезкой паддинга CAN FD. RS485 — сырой поток пакетов: `createPacketStreamParser().push(байты)` (пауза обрывает недособранное, мусор пропускается по байту). Открытие slcan-адаптера — `slcanOpenCommands(скорость, скоростьДанных?)`. Поле Value команды POESerial из ввода — `poeSerialValueFromText` (hex как есть, JSON → CBOR). Словарь протокола — `HEADER_CODES`, `DEFAULT_ARGUMENT_CODES` (из `.framework/Dictionary.yaml` прошивок), `parseArgumentDictionary` (строки `- [Строка, Enum, Число]`), `nameByCode`. Своих копий протоколов и словаря в приложениях не держим: терминал и отладка облака читают и пишут порт сырыми байтами, разбор и сборка — здесь.
62
+ **Ядро протоколов** — `poe-svelte-ui-lib/protocols` (без Svelte, работает и под Node без Vite): общий для DevCloud, WebApp и SYP-DevTerminal CBOR-кодек (RFC 8949) — `encodeCbor`/`decodeCbor`, `CborEncoder`/`CborDecoder`; `valueToCbor`/`cborToValue` — обычные значения, двоичные данные как `Uint8Array` (WebSocket, хранилище, телеметрия); `jsonToCbor`/`cborToJson` — JSON-совместимые значения, двоичные данные в base64, строки под ключами с `BIN` — байты (поля ввода терминала и отладки). Пакет `poe_cbor_pkt` `{ TrgBusID : [RetBusID, HA_Code, Payload, CRC16] }` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты с одинаковым `TrgBusID` не склеиваются, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `0xFFFF`), `makeHaCode`/`splitHaCode`, `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc` — строго побайтные, как в `common.c` прошивок. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом). Рамка POESerial (UART-консоль `system_console`, `[SOH] HDR [US] ARG [STX] VAL [ETX] CRC8 [US] FHS [EOT]`): `encodePoeSerialFrame` — команда устройству, `createPoeSerialParser().push(байты)` — потоковый разбор кадров, строк Teleplot (`plot`) и прочего текста (`raw`), с проверкой CRC `crc8Rohc` строго побайтно, как `crc8_rohc` в `common.c` прошивок. CAN/CAN FD через slcan-адаптер (`can_service` прошивки): `canFragments`/`encodeCanPacket` — нарезка пакета на кадры как `CAN_Send` (бит 10 ID — «последний», биты 0..9 — `packetId`, по 8 / 64 байта), `encodeSlcanFrame`, `createCanParser().push(байты)` — разбор кадров slcan со сборкой фрагментов и обрезкой паддинга CAN FD. RS485 — сырой поток пакетов: `createPacketStreamParser().push(байты)` (пауза обрывает недособранное, мусор пропускается по байту). Открытие slcan-адаптера — `slcanOpenCommands(скорость, скоростьДанных?)`. Поле Value команды POESerial из ввода — `poeSerialValueFromText` (hex как есть, JSON → CBOR). Словарь протокола — `HEADER_CODES`, `DEFAULT_ARGUMENT_CODES` (генерируется из мастер-словаря `src/lib/protocols/dictionary.yaml` — `npm run Dictionary`, перед упаковкой автоматически; сам файл поставляется как `poe-svelte-ui-lib/protocols/dictionary.yaml`, `.framework/Dictionary.yaml` прошивок — его справочные копии, сборка прошивок сверяет с ними `api_ha.h`), `parseArgumentDictionary` (строки `- [Строка, Enum, Число]`), `nameByCode`. Своих копий протоколов и словаря в приложениях не держим: терминал и отладка облака читают и пишут порт сырыми байтами, разбор и сборка — здесь.
63
63
 
64
64
  **Шапка смарт-виджета** (`WidgetHeader`): значок, заголовок и сворачивание слева, «?» в правом краю; правая часть (режимы, «Обновить» и т. п.) стоит в той же строке, а если не помещается — уходит второй строкой под заголовок и там переносится.
65
65
 
@@ -389,11 +389,12 @@
389
389
  <h5 class={`w-full px-4 text-center ${label.class}`}>{label.name}</h5>
390
390
  {/if}
391
391
 
392
- <div class="flex w-full flex-row gap-4">
393
- <!-- График -->
392
+ <div class="flex w-full min-w-0 flex-row gap-4">
393
+ <!-- График: холст абсолютно поверх контейнера — его пиксельная ширина не распирает контейнер, иначе в сетке/flex с 1fr ширина растёт по кругу (ResizeObserver) -->
394
394
  <!-- design-handoff, раздел 9: рамка --hairline-color без тени, тултип в оформлении общего Tooltip.svelte -->
395
- <div bind:this={container} class="relative h-64 grow overflow-hidden rounded-[14px] border border-(--hairline-color)">
396
- <canvas class="h-full w-full bg-(--back-color)" bind:this={canvas} onmousemove={handleMouseMove} onmouseleave={handleMouseLeave}></canvas>
395
+ <div bind:this={container} class="relative h-64 min-w-0 grow overflow-hidden rounded-[14px] border border-(--hairline-color)">
396
+ <canvas class="absolute inset-0 h-full w-full bg-(--back-color)" bind:this={canvas} onmousemove={handleMouseMove} onmouseleave={handleMouseLeave}
397
+ ></canvas>
397
398
 
398
399
  {#if tooltip.visible}
399
400
  <div
@@ -23,6 +23,8 @@
23
23
 
24
24
  ### Адаптивность
25
25
 
26
+ - Холст лежит поверх контейнера (`absolute`), контейнер может сжиматься (`min-w-0`) — в сетке или flex с колонкой `1fr` график не распирает колонку своей пиксельной шириной и не растёт по кругу при каждом пересчёте размера
27
+
26
28
  - График автоматически масштабируется под размер контейнера
27
29
 
28
30
  ### Ограничения
@@ -28,6 +28,8 @@
28
28
 
29
29
  - Таблица автоматически адаптируется под размеры контейнера
30
30
  - Размер текста строк задаётся CSS-переменной `--table-font-size` на любом родителе (по умолчанию `13px`), например `style="--table-font-size: 11.5px"` — так потребитель меняет масштаб данных без правки компонента
31
+ - Ширина колонки в символах моноширинного текста — `tableMonoWidth(знаков, знаковПодписи?)`: растёт вместе с `--table-font-size` и не бывает уже подписи шапки, например `width: tableMonoWidth(6, 2)` для `0x0000`
32
+ - Окно полного значения (`modal`) переносит длинные строки (hex пакета, base64) и сохраняет отступы форматирования — содержимое видно целиком, без горизонтальной прокрутки
31
33
 
32
34
  ### Ограничения
33
35
 
@@ -110,13 +112,14 @@
110
112
 
111
113
  #### ITableButton\<T\>
112
114
 
113
- | Название | Тип | По умолчанию | Описание |
114
- | -------------- | ---------------------------------------------- | ------------ | ------------------------------------------------------- |
115
- | `name` | `string \| ((row: T) => string)` | `undefined` | Текст кнопки (статичный или вычисляемый по строке) |
116
- | `icon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка кнопки (Svelte-компонент или строка SVG) |
117
- | `class` | `string \| ((row: T) => string)` | `undefined` | CSS-классы кнопки (статичные или вычисляемые по строке) |
118
- | `eventHandler` | `IUIComponentHandler` | `undefined` | Обработчик события конструктора |
119
- | `onClick` | `(row: T) => void` | `undefined` | Обработчик клика по кнопке |
115
+ | Название | Тип | По умолчанию | Описание |
116
+ | -------------- | ---------------------------------------------- | ------------ | --------------------------------------------------------------------------------- |
117
+ | `name` | `string \| ((row: T) => string)` | `undefined` | Текст кнопки (статичный или вычисляемый по строке) |
118
+ | `icon` | `ConstructorOfATypedSvelteComponent \| string` | `undefined` | Иконка кнопки (Svelte-компонент или строка SVG) |
119
+ | `title` | `string \| ((row: T) => string)` | `undefined` | Подсказка при наведении и `aria-label` — обязательна для кнопки-значка без `name` |
120
+ | `class` | `string \| ((row: T) => string)` | `undefined` | CSS-классы кнопки (статичные или вычисляемые по строке) |
121
+ | `eventHandler` | `IUIComponentHandler` | `undefined` | Обработчик события конструктора |
122
+ | `onClick` | `(row: T) => void` | `undefined` | Обработчик клика по кнопке |
120
123
 
121
124
  #### ITableProgressBar\<T\>
122
125
 
@@ -340,6 +340,8 @@
340
340
  }
341
341
  ${typeof button.class === 'function' ? button.class(row) : button.class}`)} bg-(--bg-color)"
342
342
  onclick={() => buttonClick(row, button)}
343
+ title={typeof button.title === "function" ? button.title(row) : button.title}
344
+ aria-label={typeof button.title === "function" ? button.title(row) : button.title}
343
345
  >
344
346
  {#if button?.icon}
345
347
  <span class={`flex size-4 shrink-0 items-center justify-center overflow-visible [&_svg]:h-full [&_svg]:w-full`}>
@@ -526,7 +528,8 @@
526
528
 
527
529
  <Modal isOpen={modalData.isOpen} title={$T("constructor.props.table.fulldata")} wrapperClass="max-h-[80%]" width="{0.8 * (container?.offsetWidth ?? 0)}px">
528
530
  {#snippet main()}
529
- <div class="text-left whitespace-pre">
531
+ <!-- Длинные строки (hex пакета, base64, длинные значения) переносятся, отступы форматирования сохраняются — содержимое видно целиком -->
532
+ <div class="text-left whitespace-pre-wrap [overflow-wrap:anywhere]">
530
533
  {@html modalData.formattedData}
531
534
  </div>
532
535
  {/snippet}
@@ -0,0 +1 @@
1
+ export declare const tableMonoWidth: (chars: number, labelChars?: number) => string;
@@ -0,0 +1,4 @@
1
+ /* $lib/Table/columnWidth.ts */
2
+ /* Ширина колонки моноширинного текста таблицы в символах: растёт вместе с --table-font-size (знак моноширинного шрифта — 0.6–0.62em, берётся 0.64 с запасом),
3
+ не уже подписи шапки (11px, заглавные, ~0.5rem на знак); 1.5rem — внутренние отступы ячейки, 1px — разделитель колонок */
4
+ export const tableMonoWidth = (chars, labelChars = 0) => `max(calc(var(--table-font-size, 13px) * ${+(chars * 0.64).toFixed(2)} + 1.5rem + 1px), calc(${labelChars * 0.5}rem + 1.5rem + 1px))`;
@@ -49,30 +49,31 @@
49
49
 
50
50
  ## Особенности
51
51
 
52
- Смарт-виджет файловой системы устройства: занятость памяти, список файлов с удалением и закачка нового файла с прогрессом. Работает с одним объектом `CFG.FS` ровно в том виде, в каком его отдаёт прошивка (`FS_WriteState`, ProdFactory-ESP), и с одной командой `FS`. Компонент ничего не знает про DeviceStore/WebSocket — только `value` и колбэки `onRefresh`/`onDelete`/`onUpload`, вся отправка (в том числе протокол закачки) на стороне вызывающего кода.
52
+ Смарт-виджет файловой системы устройства: занятость памяти, список файлов со скачиванием и удалением и закачка нового файла с прогрессом. Работает с одним объектом `CFG.FS` ровно в том виде, в каком его отдаёт прошивка (`FS_WriteState`, ProdFactory-ESP), и с одной командой `FS`. Компонент ничего не знает про DeviceStore/WebSocket — только `value` и колбэки `onRefresh`/`onDelete`/`onUpload`/`onDownload`, вся отправка (в том числе протоколы закачки и скачивания) на стороне вызывающего кода.
53
53
 
54
54
  - **Занятость** — «занято X из Y», процент и полоса мастер-цвета; процент оранжевый от 75%, красный от 90%. Размеры в Б/КБ/МБ.
55
- - **Файлы** — таблица «Путь / Размер / Удалить»; длинный путь обрезается «…», удаление подтверждается внутри карточки. Пустая ФС — «Файлов нет». Колонка удаления есть, только если передан `onDelete`.
55
+ - **Файлы** — таблица «Путь / Размер / действия»; длинный путь обрезается «…». Действия — значки без текста с подсказкой при наведении: «Скачать» (есть, только если передан `onDownload`) и «Удалить» (только с `onDelete`, подтверждается внутри карточки). Одновременно идёт одно скачивание — его имя и ошибка из `onDownload` выводятся под таблицей. Пустая ФС — «Файлов нет».
56
56
  - **Закачка** — блок есть, только если передан `onUpload`. Имя файла проверяется до отправки (`maxNameLength`, по умолчанию 31 — ограничение прошивки). Прогресс берётся из `value.Upload` и виден, пока закачку ведёт этот клиент или пока устройство сообщает `Upload` (закачка с другого клиента). Ошибка из `onUpload` выводится под полем.
57
57
 
58
58
  ## Пропсы
59
59
 
60
- | Название | Тип | По умолчанию | Описание |
61
- | ---------------- | --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------ |
62
- | `id` | `string` | `undefined` | Префикс прошивочной группы (обычно `"CFG"`), используется вызывающим кодом для чтения `FS` |
63
- | `wrapperClass` | `string` | `""` | CSS-классы обёртки |
64
- | `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-red"`, перекрашивает весь виджет |
65
- | `label` | `{ name?: string }` | `{}` | Заголовок карточки; пустой — «Файловая система» на текущем языке |
66
- | `value` | `IWidgetFileSystemInfo` | `undefined` | Состояние файловой системы (`CFG.FS`) |
67
- | `keys` | `{ FS?: string }` | `undefined` | Переопределение имени поля устройства, по умолчанию `FS` |
68
- | `argument` | `string` | `undefined` | Аргумент команд, по умолчанию `"FS"` (используется вызывающим кодом) |
69
- | `accept` | `string` | `"*/*"` | Какие файлы можно выбрать (атрибут `accept`), например `".bin, .txt, .pem"` |
70
- | `maxNameLength` | `number` | `31` | Максимальная длина имени файла |
71
- | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
72
- | `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
73
- | `onRefresh` | `() => void` | `undefined` | Кнопка «Обновить» в шапке (`GET FS`); без обработчика кнопка неактивна |
74
- | `onDelete` | `(name: string) => void` | `undefined` | Удаление файла после подтверждения; `name` — полный путь из `List` |
75
- | `onUpload` | `(file: File) => void \| Promise<void>` | `undefined` | Закачка выбранного файла; исключение показывается как ошибка под полем |
60
+ | Название | Тип | По умолчанию | Описание |
61
+ | ---------------- | ----------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `id` | `string` | `undefined` | Префикс прошивочной группы (обычно `"CFG"`), используется вызывающим кодом для чтения `FS` |
63
+ | `wrapperClass` | `string` | `""` | CSS-классы обёртки |
64
+ | `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-red"`, перекрашивает весь виджет |
65
+ | `label` | `{ name?: string }` | `{}` | Заголовок карточки; пустой — «Файловая система» на текущем языке |
66
+ | `value` | `IWidgetFileSystemInfo` | `undefined` | Состояние файловой системы (`CFG.FS`) |
67
+ | `keys` | `{ FS?: string }` | `undefined` | Переопределение имени поля устройства, по умолчанию `FS` |
68
+ | `argument` | `string` | `undefined` | Аргумент команд, по умолчанию `"FS"` (используется вызывающим кодом) |
69
+ | `accept` | `string` | `"*/*"` | Какие файлы можно выбрать (атрибут `accept`), например `".bin, .txt, .pem"` |
70
+ | `maxNameLength` | `number` | `31` | Максимальная длина имени файла |
71
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
72
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
73
+ | `onRefresh` | `() => void` | `undefined` | Кнопка «Обновить» в шапке (`GET FS`); без обработчика кнопка неактивна |
74
+ | `onDelete` | `(name: string) => void` | `undefined` | Удаление файла после подтверждения; `name` — полный путь из `List` |
75
+ | `onUpload` | `(file: File) => void \| Promise<void>` | `undefined` | Закачка выбранного файла; исключение показывается как ошибка под полем |
76
+ | `onDownload` | `(name: string) => void \| Promise<void>` | `undefined` | Скачивание файла (`SET FS` с `Op` `DownloadStart`/`DownloadChunk`/`DownloadDone`) и сохранение; `name` — полный путь из `List`, исключение показывается под таблицей |
76
77
 
77
78
  ### IWidgetFileSystemInfo
78
79
 
@@ -99,6 +100,7 @@
99
100
  onRefresh={() => console.log("GET FS")}
100
101
  onDelete={(name) => console.log("SET FS", { Op: "Delete", Name: name })}
101
102
  onUpload={async (file) => console.log("SET FS UploadStart/UploadChunk/UploadDone", file.name)}
103
+ onDownload={async (name) => console.log("SET FS DownloadStart/DownloadChunk/DownloadDone", name)}
102
104
  />
103
105
  ```
104
106
 
@@ -1,16 +1,19 @@
1
1
  <!-- $lib/WidgetFileSystem/WidgetFileSystem.svelte — смарт-виджет файловой системы устройства.
2
2
  Данные — один объект CFG.FS в том виде, в каком его отдаёт прошивка (FS_WriteState, ProdFactory-ESP):
3
3
  Total/Used/Free/List[{Name, Size}] и Upload{Name, Size, Progress} — только пока идёт закачка.
4
- Ничего не знает про DeviceStore/WebSocket — value/onRefresh/onDelete/onUpload, протокол
5
- закачки (SET FS с Op UploadStart/UploadChunk/UploadDone) и отправка команд на стороне вызывающего. -->
4
+ Ничего не знает про DeviceStore/WebSocket — value/onRefresh/onDelete/onUpload/onDownload, протоколы
5
+ закачки и скачивания (SET FS с Op UploadStart/UploadChunk/UploadDone и DownloadStart/DownloadChunk/DownloadDone)
6
+ и отправка команд на стороне вызывающего. -->
6
7
  <script lang="ts">
7
8
  import { T } from "../locales/i18n"
8
9
  import { slide, fade, scale } from "svelte/transition"
9
10
  import { twMerge } from "tailwind-merge"
10
11
  import * as UI from ".."
11
- import type { IWidgetFileSystemFile, IWidgetFileSystemProps, ITableHeader } from "../types"
12
+ import type { IWidgetFileSystemFile, IWidgetFileSystemProps, ITableHeader, ITableContent } from "../types"
12
13
  import StorageIcon from "./StorageIcon.svelte"
13
14
  import RefreshIcon from "./RefreshIcon.svelte"
15
+ import ButtonDownload from "../libIcons/ButtonDownload.svelte"
16
+ import ButtonDelete from "../libIcons/ButtonDelete.svelte"
14
17
  import WidgetHeader from "../WidgetHeader.svelte"
15
18
  import { hint, hintsSetting } from "../hints"
16
19
  import { widgetAccentStyle } from "../widgetAccent"
@@ -28,6 +31,7 @@
28
31
  onRefresh,
29
32
  onDelete,
30
33
  onUpload,
34
+ onDownload,
31
35
  }: IWidgetFileSystemProps = $props()
32
36
 
33
37
  /* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetFileSystem */
@@ -61,24 +65,30 @@
61
65
  /* Полоса — мастер-цвет виджета; почти заполненную ФС (закачка скоро начнёт падать) выдаёт только процент */
62
66
  let percentColor = $derived(usedPercent >= 90 ? "var(--red-color)" : usedPercent >= 75 ? "var(--orange-color)" : "var(--muted-color)")
63
67
 
64
- /* Таблица файлов: путь обрезается «…», размер — уже отформатирован, удаление — с подтверждением */
68
+ /* Таблица файлов: путь обрезается «…», размер — уже отформатирован; действия — значки без текста: скачать и удалить (с подтверждением) */
65
69
  type FileRow = IWidgetFileSystemFile & { SizeText: string }
66
70
  let rows: FileRow[] = $derived(files.map((f) => ({ ...f, SizeText: formatBytes(f.Size) })))
71
+ let actions = $derived(
72
+ [
73
+ onDownload && {
74
+ type: "button",
75
+ data: { icon: ButtonDownload, title: $T("widget.fs.download"), class: "bg-transparent px-0", onClick: (row: FileRow) => handleDownload(row.Name) },
76
+ },
77
+ onDelete && {
78
+ type: "button",
79
+ data: {
80
+ icon: ButtonDelete,
81
+ title: $T("widget.common.delete"),
82
+ class: "bg-transparent px-0 text-(--red-color) hover:bg-(--red-color)/10",
83
+ onClick: (row: FileRow) => (pendingDelete = row.Name),
84
+ },
85
+ },
86
+ ].filter(Boolean) as ITableContent<FileRow>[],
87
+ )
67
88
  let header: ITableHeader<FileRow>[] = $derived([
68
89
  { label: { name: $T("widget.fs.path") }, content: [{ type: "text", data: { key: "Name", truncated: true, sortable: true } }], width: "1fr", align: "left" },
69
90
  { label: { name: $T("widget.fs.size") }, content: [{ type: "text", data: { key: "SizeText" } }], width: "6rem", align: "right" },
70
- ...(onDelete
71
- ? [
72
- {
73
- label: { name: "" },
74
- content: [
75
- { type: "button", data: { name: $T("widget.common.delete"), class: "bg-accent", onClick: (row: FileRow) => (pendingDelete = row.Name) } },
76
- ],
77
- width: "6.5rem",
78
- align: "center",
79
- } as ITableHeader<FileRow>,
80
- ]
81
- : []),
91
+ ...(actions.length ? [{ label: { name: "" }, content: actions, width: `${actions.length * 2.5 + 1}rem`, align: "center" } as ITableHeader<FileRow>] : []),
82
92
  ])
83
93
 
84
94
  /* Закачка видна, пока её ведёт этот клиент или пока устройство сообщает Upload (закачка с другого клиента);
@@ -92,6 +102,22 @@
92
102
  pendingDelete = null
93
103
  }
94
104
 
105
+ /* Скачивание: одно за раз, ошибка вызывающего показывается под таблицей */
106
+ let downloading: string | null = $state(null)
107
+ let downloadError = $state("")
108
+ const handleDownload = async (name: string) => {
109
+ if (!onDownload || downloading) return
110
+ downloadError = ""
111
+ downloading = name
112
+ try {
113
+ await onDownload(name)
114
+ } catch (error) {
115
+ downloadError = $T("widget.fs.download_failed", { error: error instanceof Error ? error.message : String(error) })
116
+ } finally {
117
+ downloading = null
118
+ }
119
+ }
120
+
95
121
  /* Закачка: имя проверяется до отправки (прошивка отвергнет длинное имя уже после UploadStart) */
96
122
  let uploading = $state(false)
97
123
  let uploadError = $state("")
@@ -159,6 +185,14 @@
159
185
  {$T("widget.fs.empty")}
160
186
  </div>
161
187
  {/if}
188
+ {#if downloading}
189
+ <span class="mt-2 truncate text-[12px] text-(--muted-color)" transition:slide={{ duration: 100 }}
190
+ >{$T("widget.fs.downloading", { name: downloading })}</span
191
+ >
192
+ {/if}
193
+ {#if downloadError}
194
+ <span class="mt-2 text-[12px] text-(--red-color)" transition:slide={{ duration: 100 }}>{downloadError}</span>
195
+ {/if}
162
196
  </div>
163
197
 
164
198
  <!-- Закачка -->
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export { default as Switch } from "./Switch/Switch.svelte";
28
28
  export { default as SwitchProps } from "./Switch/SwitchProps.svelte";
29
29
  export { default as Table } from "./Table/Table.svelte";
30
30
  export { default as TableProps } from "./Table/TableProps.svelte";
31
+ export { tableMonoWidth } from "./Table/columnWidth";
31
32
  export { default as Tabs } from "./Tabs/Tabs.svelte";
32
33
  export { default as TabsProps } from "./Tabs/TabsProps.svelte";
33
34
  export { default as TextField } from "./TextField/TextField.svelte";
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ export { default as Switch } from "./Switch/Switch.svelte";
29
29
  export { default as SwitchProps } from "./Switch/SwitchProps.svelte";
30
30
  export { default as Table } from "./Table/Table.svelte";
31
31
  export { default as TableProps } from "./Table/TableProps.svelte";
32
+ export { tableMonoWidth } from "./Table/columnWidth";
32
33
  export { default as Tabs } from "./Tabs/Tabs.svelte";
33
34
  export { default as TabsProps } from "./Tabs/TabsProps.svelte";
34
35
  export { default as TextField } from "./TextField/TextField.svelte";
@@ -0,0 +1,12 @@
1
+ <script lang="ts"></script>
2
+
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24"
4
+ ><path
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="1.5"
10
+ d="M12 3.5v12m0 0l-4.5-4.5m4.5 4.5l4.5-4.5M4.5 15.5v1.25c0 1.875 0 2.813.477 3.47a2.5 2.5 0 0 0 .553.553c.657.477 1.595.477 3.47.477h6c1.875 0 2.813 0 3.47-.477a2.5 2.5 0 0 0 .553-.553c.477-.657.477-1.595.477-3.47V15.5"
11
+ /></svg
12
+ >
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const ButtonDownload: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type ButtonDownload = InstanceType<typeof ButtonDownload>;
18
+ export default ButtonDownload;
@@ -644,9 +644,9 @@ const translations = {
644
644
  zh: "设备文件系统占用;存满后无法写入新文件、日志和更新",
645
645
  },
646
646
  "widget.fs.hint.files": {
647
- ru: "Файлы на устройстве: сортировка — по заголовку колонки, удаление — кнопкой в строке",
648
- en: "Files on the device: sort by the column header, delete with the button in the row",
649
- zh: "设备上的文件:点击列标题排序,点击行内按钮删除",
647
+ ru: "Файлы на устройстве: сортировка — по заголовку колонки, скачивание и удаление — значками в строке",
648
+ en: "Files on the device: sort by the column header, download and delete with the icons in the row",
649
+ zh: "设备上的文件:点击列标题排序,点击行内图标下载或删除",
650
650
  },
651
651
  "widget.fs.hint.upload": {
652
652
  ru: "Загрузить файл на устройство; файл с тем же именем будет заменён",
@@ -712,6 +712,9 @@ const translations = {
712
712
  "widget.fs.files": { ru: "Файлов: {n}", en: "Files: {n}", zh: "文件:{n}" },
713
713
  "widget.fs.empty": { ru: "Файлов нет", en: "No files", zh: "没有文件" },
714
714
  "widget.fs.upload": { ru: "Закачать файл", en: "Upload file", zh: "上传文件" },
715
+ "widget.fs.download": { ru: "Скачать", en: "Download", zh: "下载" },
716
+ "widget.fs.downloading": { ru: "Скачивание: {name}", en: "Downloading: {name}", zh: "正在下载:{name}" },
717
+ "widget.fs.download_failed": { ru: "Скачивание прервано: {error}", en: "Download aborted: {error}", zh: "下载中断:{error}" },
715
718
  "widget.fs.delete_confirm": { ru: "Удалить файл?", en: "Delete file?", zh: "删除文件?" },
716
719
  "widget.fs.upload_group": { ru: "Закачка файлов", en: "File upload", zh: "文件上传" },
717
720
  "widget.fs.accept": { ru: "Допустимые файлы (accept)", en: "Allowed files (accept)", zh: "允许的文件 (accept)" },
@@ -1,4 +1,4 @@
1
1
  export declare const HEADER_CODES: Record<string, number>;
2
- export declare const DEFAULT_ARGUMENT_CODES: Record<string, number>;
2
+ export { DEFAULT_ARGUMENT_CODES } from "./dictionaryCodes.js";
3
3
  export declare const parseArgumentDictionary: (text: string) => Record<string, number>;
4
4
  export declare const nameByCode: (codes: Record<string, number>, code: number) => string;
@@ -1,67 +1,15 @@
1
1
  /* $lib/protocols/dictionary.ts */
2
2
  /* Словарь заголовков и аргументов протокола HA_Code — заголовок в 2 старших битах, аргумент в 14 младших (makeHaCode). Аргументы по умолчанию —
3
- из .framework/Dictionary.yaml прошивок (ProdFactory-ESP и ProdFactory-STM, одинаковые); у устройства словарь может быть свой — его загружают
4
- из файла (parseArgumentDictionary), своих копий словаря в приложениях не держим */
3
+ из мастер-словаря dictionary.yaml рядом (единственный источник; .framework/Dictionary.yaml прошивок — его справочные копии), DEFAULT_ARGUMENT_CODES
4
+ генерируется из него (dictionaryCodes.ts, npm run Dictionary); у устройства словарь может быть свой — его загружают из файла
5
+ (parseArgumentDictionary), своих копий словаря в приложениях не держим */
5
6
  export const HEADER_CODES = {
6
7
  GET: 0,
7
8
  SET: 1,
8
9
  "OK!": 2,
9
10
  "ER!": 3,
10
11
  };
11
- export const DEFAULT_ARGUMENT_CODES = {
12
- Unknown: 0,
13
- Restart: 1,
14
- DefCfg: 2,
15
- DlFW: 3,
16
- RstNVS: 5,
17
- LocTime: 6,
18
- Update: 7,
19
- StackInfo: 8,
20
- BackUp: 9,
21
- BindMKey: 10,
22
- DataStream: 15,
23
- APsList: 21,
24
- FS: 22,
25
- JoinDev: 23,
26
- JoinUser: 24,
27
- LeaveUser: 25,
28
- ModFind: 31,
29
- ModList: 32,
30
- ModCap: 33,
31
- ModInfo: 34,
32
- ModMode: 35,
33
- ModCfg: 36,
34
- ModT1: 37,
35
- ModT2: 38,
36
- ModT3: 39,
37
- ModT4: 40,
38
- ModT5: 41,
39
- ModS1: 42,
40
- ModS2: 43,
41
- ModS3: 44,
42
- ModS4: 45,
43
- ModS5: 46,
44
- FwUpgS: 51,
45
- FwUpgP: 52,
46
- FwUpgD: 53,
47
- FwUpgE: 54,
48
- FDwS: 65,
49
- FDwP: 66,
50
- FDwD: 67,
51
- FDwE: 68,
52
- TestReq: 91,
53
- MotInit: 111,
54
- MotMode: 112,
55
- MotPos: 113,
56
- MotPID: 114,
57
- MotInfo: 115,
58
- MotCfg: 116,
59
- MotCal: 117,
60
- OrData: 141,
61
- OrOfs: 142,
62
- OrAccCal: 143,
63
- OrMagCal: 144,
64
- };
12
+ export { DEFAULT_ARGUMENT_CODES } from "./dictionaryCodes.js";
65
13
  /* Аргументы из текста Dictionary.yaml (или его фрагмента): строки «- [Строка, Enum, Число]», комментарии и прочее пропускаются */
66
14
  export const parseArgumentDictionary = (text) => Object.fromEntries([...text.matchAll(/^\s*-\s*\[\s*([^,\]]+?)\s*,\s*[^,\]]+?\s*,\s*(\d+)\s*\]/gm)].map(([, name, code]) => [name, Number(code)]));
67
15
  /* Имя по коду (обратный поиск) — для показа принятых пакетов; неизвестный код — числом */
@@ -0,0 +1,66 @@
1
+ # Словарь аргументов протокола HA_Code — единый для всех устройств, обоих фреймворков и приложений
2
+ # Строковое представление аргумента | Название в перечислении | Числовой код
3
+ # Источник — poe-svelte-ui-lib (src/lib/protocols/dictionary.yaml); .framework/Dictionary.yaml фреймворков — копии, не править
4
+
5
+ # 0 — отсутствие аргумента
6
+ - [Unknown, A_UNKNOWN, 0] # Аргумент отсутствует
7
+
8
+ # Служебные команды
9
+ - [Restart, A_RESTART, 1] # SET: перезагрузка устройства
10
+ - [DefCfg, A_DEF_CONFIG, 2] # SET: сброс конфигурации к заводской
11
+ - [DlFW, A_DOWNLOAD_FIRMWARE, 3] # SET: скачивание прошивки с сервера (http_client)
12
+ - [RstNVS, A_RESET_NVS, 5] # SET: очистка NVS и перезагрузка
13
+ - [LocTime, A_LOCAL_TIME, 6] # GET: локальное время устройства
14
+ - [Update, A_UPDATE, 7] # OK!/ER!: обновление данных в интерфейсе (DevCloud ретранслирует DataStream)
15
+ - [StackInfo, A_STACK_INFO, 8] # GET: снимок куч и стеков задач
16
+ - [BackUp, A_BACKUP, 9] # Резервная копия конфигурации
17
+ - [BindMKey, A_BIND_MOD_KEY, 10] # SET: привязка ключа модуля (ModBindKey)
18
+ - [DataStream, A_DATA_STREAM, 15] # OK!: поток данных групп конфигурации (CS_PushGroupStream)
19
+
20
+ # Локальная сеть
21
+ - [APsList, A_APS_LIST, 21] # GET: список точек доступа Wi-Fi
22
+ - [FS, A_FS, 22] # Файловая система: GET FS — снимок, SET FS {Op: Delete|UploadStart|UploadChunk|UploadDone|DownloadStart|DownloadChunk|DownloadDone}
23
+ - [JoinDev, A_JOIN_DEV, 23] # SET: подтверждение подключения устройства к облаку
24
+ - [JoinUser, A_JOIN_USER, 24] # SET: пользователь подключился к устройству
25
+ - [LeaveUser, A_LEAVE_USER, 25] # SET: пользователь отключился от устройства
26
+
27
+ # Модули в рамках изделия
28
+ - [ModFind, A_MODULE_FIND, 31] # GET/OK!: поиск модулей на шинах изделия
29
+ - [ModList, A_MODULE_LIST, 32] # Список модулей изделия (CFG.ModList)
30
+ - [ModCap, A_MODULE_CAPTURE, 33] # GET/OK!: захват модуля для привязки
31
+ - [ModInfo, A_MODULE_INFO, 34] # GET/OK!: информация о модуле
32
+ - [ModMode, A_MODULE_MODE, 35] # SET: режим работы модуля
33
+ - [ModCfg, A_MODULE_CFG, 36] # GET/SET: конфигурация модуля (группы config_service)
34
+ - [ModT1, A_MODULE_TELEMETRY_1, 37] # Телеметрия модуля, канал 1
35
+ - [ModT2, A_MODULE_TELEMETRY_2, 38] # Телеметрия модуля, канал 2
36
+ - [ModT3, A_MODULE_TELEMETRY_3, 39] # Телеметрия модуля, канал 3
37
+ - [ModT4, A_MODULE_TELEMETRY_4, 40] # Телеметрия модуля, канал 4
38
+ - [ModT5, A_MODULE_TELEMETRY_5, 41] # Телеметрия модуля, канал 5
39
+ - [ModS1, A_MODULE_STREAM_1, 42] # Поток данных модуля, канал 1
40
+ - [ModS2, A_MODULE_STREAM_2, 43] # Поток данных модуля, канал 2
41
+ - [ModS3, A_MODULE_STREAM_3, 44] # Поток данных модуля, канал 3
42
+ - [ModS4, A_MODULE_STREAM_4, 45] # Поток данных модуля, канал 4
43
+ - [ModS5, A_MODULE_STREAM_5, 46] # Поток данных модуля, канал 5
44
+
45
+ # Обновление прошивки
46
+ - [FwUpgS, A_UPGRADE_START, 51] # Старт обновления прошивки модуля по шине
47
+ - [FwUpgP, A_UPGRADE_PROC, 52] # Блок прошивки при обновлении по шине
48
+ - [FwUpgD, A_UPGRADE_DONE, 53] # Завершение обновления прошивки по шине
49
+ - [FwUpgE, A_UPGRADE_ERR, 54] # Ошибка обновления прошивки по шине
50
+
51
+ - [TestReq, TEST_REQUEST, 91] # GET: тестовый запрос
52
+
53
+ # Работа с модулями перемещения (оси подвесов, линейные перемещения, серво приводы и подобные)
54
+ - [MotInit, A_MOTION_INIT, 111] # SET: включение/калибровка (1 байт: команда)
55
+ - [MotMode, A_MOTION_MODE, 112] # SET: режим управления (2 байта: ctrl_mode + torque_mode)
56
+ - [MotPos, A_MOTION_POS, 113] # GET/SET: целевое значение (4 байта: float)
57
+ - [MotPID, A_MOTION_PID, 114] # SET: коэффициенты PID (13 байт: selector + Kp + Ki + Kd)
58
+ - [MotInfo, A_MOTION_INFO, 115] # GET: телеметрия (32 байта: angle+vel+Iq+Id+target+flags)
59
+ - [MotCfg, A_MOTION_CFG, 116] # GET/SET: конфигурация мотора (28 байт: лимиты+параметры)
60
+ - [MotCal, A_MOTION_CAL, 117] # SET: запуск калибровки нулевого угла (0 байт)
61
+
62
+ # Работа с модулями ориентации (IMU, магнетометры, GNSS и подобные)
63
+ - [OrData, A_ORIENTATION_DATA, 141] # SET: углы ориентации от модуля
64
+ - [OrOfs, A_ORIENTATION_OFFSET, 142] # Смещения ориентации
65
+ - [OrAccCal, A_ORIENTATION_ACC_CALIB, 143] # Калибровка акселерометра
66
+ - [OrMagCal, A_ORIENTATION_MAG_CALIB, 144] # Калибровка магнетометра
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_ARGUMENT_CODES: Record<string, number>;
@@ -0,0 +1,51 @@
1
+ /* $lib/protocols/dictionaryCodes.ts — СГЕНЕРИРОВАНО из dictionary.yaml (npm run Dictionary), руками не править */
2
+ export const DEFAULT_ARGUMENT_CODES = {
3
+ Unknown: 0,
4
+ Restart: 1,
5
+ DefCfg: 2,
6
+ DlFW: 3,
7
+ RstNVS: 5,
8
+ LocTime: 6,
9
+ Update: 7,
10
+ StackInfo: 8,
11
+ BackUp: 9,
12
+ BindMKey: 10,
13
+ DataStream: 15,
14
+ APsList: 21,
15
+ FS: 22,
16
+ JoinDev: 23,
17
+ JoinUser: 24,
18
+ LeaveUser: 25,
19
+ ModFind: 31,
20
+ ModList: 32,
21
+ ModCap: 33,
22
+ ModInfo: 34,
23
+ ModMode: 35,
24
+ ModCfg: 36,
25
+ ModT1: 37,
26
+ ModT2: 38,
27
+ ModT3: 39,
28
+ ModT4: 40,
29
+ ModT5: 41,
30
+ ModS1: 42,
31
+ ModS2: 43,
32
+ ModS3: 44,
33
+ ModS4: 45,
34
+ ModS5: 46,
35
+ FwUpgS: 51,
36
+ FwUpgP: 52,
37
+ FwUpgD: 53,
38
+ FwUpgE: 54,
39
+ TestReq: 91,
40
+ MotInit: 111,
41
+ MotMode: 112,
42
+ MotPos: 113,
43
+ MotPID: 114,
44
+ MotInfo: 115,
45
+ MotCfg: 116,
46
+ MotCal: 117,
47
+ OrData: 141,
48
+ OrOfs: 142,
49
+ OrAccCal: 143,
50
+ OrMagCal: 144,
51
+ };
@@ -16,5 +16,5 @@ export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacke
16
16
  export { encodePoeSerialFrame, createPoeSerialParser, poeSerialValueFromText } from "./poeSerial.js";
17
17
  /* CAN/CAN FD через slcan-адаптер: нарезка пакета на кадры как в can_service прошивки, кадры slcan, потоковый разбор со сборкой фрагментов */
18
18
  export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, } from "./can.js";
19
- /* Словарь заголовков и аргументов HA_Code — по умолчанию из Dictionary.yaml прошивок, разбор файла словаря */
19
+ /* Словарь заголовков и аргументов HA_Code — по умолчанию из мастер-словаря dictionary.yaml библиотеки, разбор файла словаря */
20
20
  export { HEADER_CODES, DEFAULT_ARGUMENT_CODES, parseArgumentDictionary, nameByCode } from "./dictionary.js";
package/dist/types.d.ts CHANGED
@@ -319,6 +319,7 @@ export interface ITableSelect<T extends object> {
319
319
  export interface ITableButton<T extends object> {
320
320
  name?: string | ((row: T) => string);
321
321
  icon?: ConstructorOfATypedSvelteComponent | string;
322
+ title?: string | ((row: T) => string);
322
323
  class?: string | ((row: T) => string);
323
324
  eventHandler?: IUIComponentHandler;
324
325
  onClick?: (row: T) => void;
@@ -571,6 +572,7 @@ export interface IWidgetFileSystemProps {
571
572
  persistKey?: string;
572
573
  onRefresh?: () => void;
573
574
  onDelete?: (name: string) => void;
575
+ onDownload?: (name: string) => void | Promise<void>;
574
576
  onUpload?: (file: File) => void | Promise<void>;
575
577
  }
576
578
  export interface IWidgetStackInfoHeap {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.67.0",
3
+ "version": "1.69.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"
@@ -11,12 +11,13 @@
11
11
  "dev": "vite dev",
12
12
  "build": "vite build",
13
13
  "preview": "vite preview",
14
- "prepack": "svelte-kit sync && svelte-package && publint",
14
+ "prepack": "node scripts/GenerateDictionary.mjs && svelte-kit sync && svelte-package && publint",
15
15
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
16
16
  "CheckUpdate": "npx npm-check-updates -u && npm install",
17
17
  "UpdateIconsLib": "tsx src/lib/IconsCatalog/iconsLib.ts",
18
18
  "Formatting": "prettier --write .",
19
- "TestProtocols": "svelte-package && node --test tests/*.test.mjs"
19
+ "Dictionary": "node scripts/GenerateDictionary.mjs",
20
+ "TestProtocols": "node scripts/GenerateDictionary.mjs && svelte-package && node --test tests/*.test.mjs"
20
21
  },
21
22
  "svelte": "./dist/index.js",
22
23
  "types": "./dist/index.d.ts",
@@ -29,6 +30,7 @@
29
30
  "types": "./dist/protocols/index.d.ts",
30
31
  "default": "./dist/protocols/index.js"
31
32
  },
33
+ "./protocols/dictionary.yaml": "./dist/protocols/dictionary.yaml",
32
34
  "./locales": {
33
35
  "types": "./dist/locales/index.d.ts",
34
36
  "default": "./dist/locales/index.js"