poe-svelte-ui-lib 1.33.1 → 1.34.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.
@@ -259,7 +259,7 @@
259
259
  <div class="relative flex h-full w-full flex-col overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
260
260
  <!-- Table Header -->
261
261
  <div
262
- class="grid border-b border-(--hairline-color) bg-(--container-color) text-[11px] font-bold tracking-[0.06em] text-(--muted-color) uppercase"
262
+ class="grid border-b border-(--hairline-color) bg-[var(--table-header-color,var(--container-color))] text-[11px] font-bold tracking-[0.06em] text-(--muted-color) uppercase"
263
263
  style={`grid-template-columns: ${gridTemplate}; padding-right: ${scrollbarWidth}px;`}
264
264
  >
265
265
  {#each header as column, index (column)}
@@ -5,6 +5,7 @@
5
5
  <script lang="ts">
6
6
  import type { IWidgetProps } from "../types"
7
7
  import { twMerge } from "tailwind-merge"
8
+ import { widgetAccentStyle } from "../widgetAccent"
8
9
 
9
10
  let {
10
11
  id = crypto.randomUUID(),
@@ -17,6 +18,9 @@
17
18
  onUpdate = () => {},
18
19
  }: IWidgetProps = $props()
19
20
 
21
+ /* Мастер-цвет — роль из settings.class, тот же механизм, что у смарт-виджетов: перекрашивает переключатель, слайдер и фокус поля */
22
+ let accentStyle = $derived(widgetAccentStyle(settings.class ?? ""))
23
+
20
24
  let currentIndex = $state(0)
21
25
  let intervalId: number | null = null
22
26
 
@@ -125,7 +129,7 @@
125
129
  focus-visible:outline-none`
126
130
  </script>
127
131
 
128
- <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge("h-full w-full", wrapperClass)}>
132
+ <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge("h-full w-full", wrapperClass)} style={accentStyle}>
129
133
  <div class="flex h-full flex-col overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
130
134
  <!-- Шапка: иконка + название, фиксированная высота -->
131
135
  <div class="flex h-11 shrink-0 items-center gap-2.5 border-b border-(--hairline-color) px-3.5">
@@ -7,6 +7,7 @@
7
7
  import { twMerge } from "tailwind-merge"
8
8
  import CommonSnippets from "../CommonSnippets.svelte"
9
9
  import PropsGroup from "../PropsGroup.svelte"
10
+ import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
10
11
  import { getContext } from "svelte"
11
12
 
12
13
  const {
@@ -43,14 +44,9 @@
43
44
  {/snippet}
44
45
 
45
46
  {#snippet WidgetSettingsColor()}
46
- <UI.Select
47
- wrapperClass="!h-14"
48
- label={{ name: $T("constructor.props.componentcolor") }}
49
- type="buttons"
50
- options={$optionsStore.COLOR_OPTIONS}
51
- value={initialColor}
52
- onUpdate={(option) => {
53
- const bgClass = (option as UI.IOption<string>).value as string
47
+ <WidgetAccentPicker
48
+ value={(initialColor?.value as string) ?? ""}
49
+ onUpdate={(bgClass) => {
54
50
  updateProperty("settings.class", twMerge(component.properties.settings.class, bgClass), component, onPropertyChange)
55
51
  updateProperty("icons.class", twMerge(component.properties.icons.class, deriveIconColorClass(bgClass)), component, onPropertyChange)
56
52
  }}
@@ -80,7 +80,7 @@
80
80
  </script>
81
81
 
82
82
  <div
83
- class={twMerge("flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
83
+ class={twMerge("flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
84
84
  style={accentStyle}
85
85
  >
86
86
  <!-- Заголовок -->
@@ -107,7 +107,7 @@
107
107
  </script>
108
108
 
109
109
  <div
110
- class={twMerge("relative flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
110
+ class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
111
111
  style={accentStyle}
112
112
  >
113
113
  <!-- Заголовок -->
@@ -0,0 +1,12 @@
1
+ <script lang="ts"></script>
2
+
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
4
+ ><path
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="2"
10
+ d="M6 19v-3m4 3v-3m4 3v-3m4 3v-3M8 11V9m8 2V9m-4 2V9M2 15h20M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z"
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 MemoryIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type MemoryIcon = InstanceType<typeof MemoryIcon>;
18
+ export default MemoryIcon;
@@ -0,0 +1,72 @@
1
+ # WidgetStackInfo Component
2
+
3
+ ## Описание
4
+
5
+ Отладочный смарт-виджет памяти и задач FreeRTOS: занятость каждой кучи и минимальный запас стека
6
+ каждой задачи. Работает с одним объектом `StackInfo` ровно в том виде, в каком его отдаёт
7
+ `API_GetStackInfo` (ProdFactory-ESP и ProdFactory-STM — контракт общий), и с одним запросом
8
+ `GET StackInfo`. Компонент ничего не знает про DeviceStore/WebSocket — только `value` и `onRefresh`.
9
+
10
+ - **Кучи** — карточка на каждую: «занято X из Y», процент (оранжевый от 75%, красный от 90%) и
11
+ полоса мастер-цвета; светлая подложка полосы — пик занятости с момента старта (`Total − MinFree`).
12
+ - **Задачи** — таблица «Задача / Состояние / Приоритет / Ядро / Запас». Порядок всегда по
13
+ возрастанию запаса стека: самые рискованные сверху (сортировка кликом при автоопросе сбрасывалась
14
+ бы на каждом ответе). Запас меньше `warnBytes` — красный, меньше удвоенного — оранжевый, над
15
+ таблицей — число задач ниже порога. Колонка «Ядро» есть, только если устройство передаёт `Core`.
16
+ - **Обновление** — кнопка «Обновить» и автоопрос «Выкл / 1 с / 5 с / 10 с» (`period`); при
17
+ включении опрос сразу шлёт запрос, свёрнутый виджет не опрашивает.
18
+
19
+ ## Протокол устройства
20
+
21
+ ```
22
+ "StackInfo": {
23
+ "Heap": [
24
+ { "Name": "Internal", "Total": 327680, "Free": 118000, "MinFree": 92000 },
25
+ { "Name": "Default", "Total": 4521984, "Free": 4100000, "MinFree": 4040000 }
26
+ ],
27
+ "Tasks": [{ "Name": "api_task", "FreeMin": 1240, "Prio": 5, "Core": 1, "State": 2 }]
28
+ }
29
+ ```
30
+
31
+ Все размеры в байтах. ESP отдаёт две кучи (`Internal` — внутренний DRAM, `Default` — всё, что
32
+ отдаёт `malloc`, включая PSRAM) и `Core` (-1 — без привязки к ядру); STM — одну кучу `Heap`
33
+ (heap_4, `configTOTAL_HEAP_SIZE`) и без `Core`. `State` — `eTaskState`: 0 работает, 1 готова,
34
+ 2 ожидает, 3 приостановлена, 4 удалена.
35
+
36
+ ## Пропсы
37
+
38
+ | Название | Тип | По умолчанию | Описание |
39
+ | ---------------- | ------------------------ | ----------------------------- | ------------------------------------------------------------------------- |
40
+ | `wrapperClass` | `string` | `""` | CSS-классы обёртки |
41
+ | `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-red"`, перекрашивает весь виджет |
42
+ | `label` | `{ name?: string }` | `{ name: "Память и задачи" }` | Заголовок карточки |
43
+ | `value` | `IWidgetStackInfo` | `undefined` | Снимок `StackInfo`; без него — «Нет данных» |
44
+ | `keys` | `{ StackInfo?: string }` | `undefined` | Переопределение имени объекта ответа, по умолчанию `StackInfo` |
45
+ | `argument` | `string` | `undefined` | Аргумент запроса, по умолчанию `"StackInfo"` (используется вызывающим) |
46
+ | `warnBytes` | `number` | `512` | Порог подсветки запаса стека, байт |
47
+ | `period` | `number` | `0` | Период автоопроса, мс; 0 — только по кнопке (`$bindable`) |
48
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
49
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
50
+ | `onRefresh` | `() => void` | `undefined` | Запрос снимка (`GET StackInfo`); без обработчика кнопка и опрос неактивны |
51
+
52
+ ## Примеры
53
+
54
+ ```svelte
55
+ <script>
56
+ import * as UI from "poe-svelte-ui-lib"
57
+
58
+ let stackInfo = $state()
59
+ </script>
60
+
61
+ <UI.WidgetStackInfo value={stackInfo} warnBytes={768} onRefresh={() => console.log("GET StackInfo")} />
62
+ ```
63
+
64
+ ## Конструктор свойств (WidgetStackInfoProps.svelte)
65
+
66
+ | Название | Тип | Описание |
67
+ | ------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------- |
68
+ | `component` | `UIComponent & { properties: Partial<IWidgetStackInfoProps> }` | Объект компонента с его свойствами |
69
+ | `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string }>) => void` | Коллбэк для обновления свойств компонента |
70
+
71
+ Группы панели: общие (заголовок, мастер-цвет), отображение (`warnBytes`, `period`), устройство
72
+ (объект ответа, аргумент запроса).
@@ -0,0 +1,12 @@
1
+ <script lang="ts"></script>
2
+
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
4
+ ><path
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="2"
10
+ d="M21 12a9 9 0 0 0-9-9a9.75 9.75 0 0 0-6.74 2.74L3 8m0-5v5h5m-5 4a9 9 0 0 0 9 9a9.75 9.75 0 0 0 6.74-2.74L21 16m-5 0h5v5"
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 RefreshIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type RefreshIcon = InstanceType<typeof RefreshIcon>;
18
+ export default RefreshIcon;
@@ -0,0 +1,179 @@
1
+ <!-- $lib/WidgetStackInfo/WidgetStackInfo.svelte — отладочный смарт-виджет памяти и задач FreeRTOS.
2
+ Данные — один объект StackInfo в том виде, в каком его отдают ProdFactory-ESP и ProdFactory-STM
3
+ (API_GetStackInfo): Heap[{Name, Total, Free, MinFree}] и Tasks[{Name, FreeMin, Prio, Core?, State}].
4
+ Ничего не знает про DeviceStore/WebSocket — value/onRefresh, автоопрос лишь периодически зовёт onRefresh. -->
5
+ <script lang="ts">
6
+ import { slide } from "svelte/transition"
7
+ import { twMerge } from "tailwind-merge"
8
+ import * as UI from ".."
9
+ import type { IOption, ITableHeader, IWidgetStackInfoProps, IWidgetStackInfoTask } from "../types"
10
+ import MemoryIcon from "./MemoryIcon.svelte"
11
+ import RefreshIcon from "./RefreshIcon.svelte"
12
+ import WidgetHeader from "../WidgetHeader.svelte"
13
+ import { widgetAccentStyle } from "../widgetAccent"
14
+ import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
15
+
16
+ let {
17
+ wrapperClass = "",
18
+ componentClass = "",
19
+ label = { name: "Память и задачи" },
20
+ value,
21
+ warnBytes = 512,
22
+ period = $bindable(0),
23
+ collapsed = $bindable(false),
24
+ persistKey,
25
+ onRefresh,
26
+ }: IWidgetStackInfoProps = $props()
27
+
28
+ let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
29
+
30
+ /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
31
+ let hasMergedPersisted = false
32
+ $effect(() => {
33
+ if (hasMergedPersisted) return
34
+ hasMergedPersisted = true
35
+ const persisted = readPersistedCollapsed(persistKey, collapsed)
36
+ if (persisted !== collapsed) collapsed = persisted
37
+ })
38
+ $effect(() => writePersistedCollapsed(persistKey, collapsed))
39
+
40
+ /* Автоопрос: сразу при включении и далее раз в period; свёрнутый виджет не опрашивает */
41
+ const periods: IOption<number>[] = [
42
+ { id: "off", value: 0, name: "Выкл" },
43
+ { id: "1s", value: 1000, name: "1 с" },
44
+ { id: "5s", value: 5000, name: "5 с" },
45
+ { id: "10s", value: 10000, name: "10 с" },
46
+ ]
47
+ $effect(() => {
48
+ if (!period || period <= 0 || collapsed || !onRefresh) return
49
+ onRefresh()
50
+ const timer = setInterval(() => onRefresh?.(), period)
51
+ return () => clearInterval(timer)
52
+ })
53
+
54
+ /* Размер в человекочитаемом виде: Б / КБ / МБ, одна цифра после запятой начиная с КБ */
55
+ const formatBytes = (bytes: number) => {
56
+ if (!Number.isFinite(bytes) || bytes < 0) return "—"
57
+ if (bytes < 1024) return `${bytes} Б`
58
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`
60
+ }
61
+ const percent = (part: number, total: number) => (total > 0 ? Math.min(100, Math.max(0, Math.round((part / total) * 100))) : 0)
62
+
63
+ /* Текущая занятость — мастер-цвет, пик занятости с момента старта (Total − MinFree) — его светлая подложка */
64
+ let heaps = $derived(
65
+ (value?.Heap ?? []).map((h) => {
66
+ const used = Math.max(h.Total - h.Free, 0)
67
+ const peak = Math.max(h.Total - h.MinFree, used)
68
+ return { ...h, used, usedPercent: percent(used, h.Total), peakPercent: percent(peak, h.Total) }
69
+ }),
70
+ )
71
+ const percentColor = (p: number) => (p >= 90 ? "var(--red-color)" : p >= 75 ? "var(--orange-color)" : "var(--muted-color)")
72
+
73
+ /* Имена задач приходят с устройства, а Table выводит ячейки через {@html} — экранируем */
74
+ const escapeHtml = (text: string) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`)
75
+ const STATES = ["Работает", "Готова", "Ожидает", "Приостановлена", "Удалена"]
76
+ const stackColor = (bytes: number) => (bytes < warnBytes ? "var(--red-color)" : bytes < warnBytes * 2 ? "var(--orange-color)" : "")
77
+
78
+ type TaskRow = { Name: string; StateText: string; Prio: number; CoreText: string; FreeMinText: string }
79
+ let tasks = $derived([...(value?.Tasks ?? [])].sort((a, b) => a.FreeMin - b.FreeMin))
80
+ let hasCore = $derived(tasks.some((t) => t.Core !== undefined))
81
+ let lowCount = $derived(tasks.filter((t) => t.FreeMin < warnBytes).length)
82
+
83
+ /* Порядок фиксированный — по возрастанию запаса стека: при автоопросе сортировка кликом сбрасывалась бы каждый ответ */
84
+ let rows: TaskRow[] = $derived(
85
+ tasks.map((t: IWidgetStackInfoTask) => {
86
+ const color = stackColor(t.FreeMin)
87
+ return {
88
+ Name: escapeHtml(t.Name),
89
+ StateText: STATES[t.State] ?? "—",
90
+ Prio: t.Prio,
91
+ CoreText: t.Core === undefined ? "" : t.Core < 0 ? "любое" : String(t.Core),
92
+ FreeMinText: color ? `<span style="color: ${color}; font-weight: 600">${t.FreeMin}</span>` : String(t.FreeMin),
93
+ }
94
+ }),
95
+ )
96
+ let header: ITableHeader<TaskRow>[] = $derived([
97
+ { label: { name: "Задача" }, content: [{ type: "text", data: { key: "Name", truncated: true } }], width: "1fr", align: "left" },
98
+ { label: { name: "Состояние" }, content: [{ type: "text", data: { key: "StateText", truncated: true } }], width: "9.5rem", align: "left" },
99
+ { label: { name: "Приор." }, content: [{ type: "text", data: { key: "Prio" } }], width: "5.5rem", align: "center" },
100
+ ...(hasCore
101
+ ? [{ label: { name: "Ядро" }, content: [{ type: "text", data: { key: "CoreText" } }], width: "5.5rem", align: "center" } as ITableHeader<TaskRow>]
102
+ : []),
103
+ { label: { name: "Запас, Б" }, content: [{ type: "text", data: { key: "FreeMinText" } }], width: "6rem", align: "right" },
104
+ ])
105
+ </script>
106
+
107
+ <div
108
+ class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
109
+ style={accentStyle}
110
+ >
111
+ <!-- Заголовок -->
112
+ <WidgetHeader icon={MemoryIcon} label={label?.name ?? "Память и задачи"} bind:collapsed>
113
+ {#snippet right()}
114
+ <div class="flex items-center gap-2">
115
+ <UI.Select
116
+ wrapperClass="w-64"
117
+ type="buttons"
118
+ options={periods}
119
+ value={periods.find((p) => p.value === period) ?? periods[0]}
120
+ disabled={!onRefresh}
121
+ onUpdate={(option) => !Array.isArray(option) && (period = option.value ?? 0)}
122
+ />
123
+ <UI.Button
124
+ wrapperClass="w-auto"
125
+ componentClass="bg-transparent px-3"
126
+ content={{ icon: RefreshIcon, name: "Обновить" }}
127
+ disabled={!onRefresh}
128
+ onClick={() => onRefresh?.()}
129
+ />
130
+ </div>
131
+ {/snippet}
132
+ </WidgetHeader>
133
+
134
+ {#if !collapsed}
135
+ <div class="flex flex-col gap-4" transition:slide={{ duration: 150 }}>
136
+ {#if !value}
137
+ <div class="rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3 text-center text-[13px] text-(--faint-color)">
138
+ Нет данных — нажмите «Обновить»
139
+ </div>
140
+ {:else}
141
+ <!-- Кучи -->
142
+ <div class="grid gap-3 {heaps.length > 1 ? 'sm:grid-cols-2' : ''}">
143
+ {#each heaps as heap (heap.Name)}
144
+ <div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
145
+ <div class="flex items-baseline justify-between gap-2 text-[13px]">
146
+ <span class="truncate font-semibold">{heap.Name}: {formatBytes(heap.used)} из {formatBytes(heap.Total)}</span>
147
+ <span class="font-semibold tabular-nums" style="color: {percentColor(heap.usedPercent)}">{heap.usedPercent}%</span>
148
+ </div>
149
+ <div class="relative h-2 w-full overflow-hidden rounded-full bg-(--container-color)">
150
+ <div class="absolute inset-y-0 left-0 rounded-full bg-(--accent-soft) transition-[width] duration-300" style="width: {heap.peakPercent}%"></div>
151
+ <div
152
+ class="absolute inset-y-0 left-0 rounded-full bg-(--accent-color) transition-[width] duration-300"
153
+ style="width: {heap.usedPercent}%"
154
+ ></div>
155
+ </div>
156
+ <div class="flex justify-between gap-2 text-[12px] text-(--muted-color)">
157
+ <span>Свободно {formatBytes(heap.Free)}</span>
158
+ <span>Минимум {formatBytes(heap.MinFree)}</span>
159
+ </div>
160
+ </div>
161
+ {/each}
162
+ </div>
163
+
164
+ <!-- Задачи -->
165
+ <div class="flex flex-col gap-2">
166
+ <div class="flex items-baseline justify-between gap-2 text-[12px] text-(--muted-color)">
167
+ <span>Задач: {tasks.length}</span>
168
+ {#if lowCount}
169
+ <span class="font-semibold text-(--red-color)">Запас стека меньше {warnBytes} Б: {lowCount}</span>
170
+ {/if}
171
+ </div>
172
+ <div class="flex max-h-80 min-h-0 flex-col">
173
+ <UI.Table {header} body={rows} outline />
174
+ </div>
175
+ </div>
176
+ {/if}
177
+ </div>
178
+ {/if}
179
+ </div>
@@ -0,0 +1,4 @@
1
+ import * as UI from "..";
2
+ declare const WidgetStackInfo: import("svelte").Component<UI.IWidgetStackInfoProps, {}, "collapsed" | "period">;
3
+ type WidgetStackInfo = ReturnType<typeof WidgetStackInfo>;
4
+ export default WidgetStackInfo;
@@ -0,0 +1,58 @@
1
+ <!-- $lib/WidgetStackInfo/WidgetStackInfoProps.svelte — панель пропсов конструктора для
2
+ WidgetStackInfo: заголовок/мастер-цвет, порог подсветки запаса стека, автоопрос по умолчанию,
3
+ имя объекта ответа (keys.StackInfo) и аргумент запроса — все с дефолтами прошивочной конвенции
4
+ (см. DEFAULT_PROPS.WidgetStackInfo в DevCloud). -->
5
+ <script lang="ts">
6
+ import { T } from "../locales/i18n"
7
+ import * as UI from ".."
8
+ import { updateProperty, type UIComponent, type IWidgetStackInfoProps } from "../types"
9
+ import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
10
+ import PropsGroup from "../PropsGroup.svelte"
11
+
12
+ const { component, onPropertyChange } = $props<{
13
+ component: UIComponent & { properties: Partial<IWidgetStackInfoProps> }
14
+ onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string }>) => void
15
+ }>()
16
+ </script>
17
+
18
+ <div class="flex flex-col gap-2">
19
+ <PropsGroup label={$T("constructor.props.group.general")}>
20
+ <UI.Input
21
+ label={{ name: "Заголовок" }}
22
+ value={component.properties.label?.name}
23
+ onUpdate={(value) => updateProperty("label.name", value as string, component, onPropertyChange)}
24
+ />
25
+ <WidgetAccentPicker
26
+ value={component.properties.componentClass ?? ""}
27
+ onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
28
+ />
29
+ </PropsGroup>
30
+ <PropsGroup label="Отображение">
31
+ <UI.Input
32
+ label={{ name: "Порог запаса стека, Б" }}
33
+ type="number"
34
+ number={{ minNum: 0, maxNum: 65535, step: 64 }}
35
+ value={component.properties.warnBytes ?? 512}
36
+ onUpdate={(value) => updateProperty("warnBytes", Number(value), component, onPropertyChange)}
37
+ />
38
+ <UI.Input
39
+ label={{ name: "Автоопрос по умолчанию, мс (0 — выкл)" }}
40
+ type="number"
41
+ number={{ minNum: 0, maxNum: 60000, step: 1000 }}
42
+ value={component.properties.period ?? 0}
43
+ onUpdate={(value) => updateProperty("period", Number(value), component, onPropertyChange)}
44
+ />
45
+ </PropsGroup>
46
+ <PropsGroup label="Устройство">
47
+ <UI.Input
48
+ label={{ name: "Объект ответа" }}
49
+ value={component.properties.keys?.StackInfo ?? "StackInfo"}
50
+ onUpdate={(value) => updateProperty("keys.StackInfo", value as string, component, onPropertyChange)}
51
+ />
52
+ <UI.Input
53
+ label={{ name: "Аргумент запроса" }}
54
+ value={component.properties.argument ?? "StackInfo"}
55
+ onUpdate={(value) => updateProperty("argument", value as string, component, onPropertyChange)}
56
+ />
57
+ </PropsGroup>
58
+ </div>
@@ -0,0 +1,14 @@
1
+ import { type UIComponent, type IWidgetStackInfoProps } from "../types";
2
+ type $$ComponentProps = {
3
+ component: UIComponent & {
4
+ properties: Partial<IWidgetStackInfoProps>;
5
+ };
6
+ onPropertyChange: (updates: Partial<{
7
+ properties?: string | object;
8
+ name?: string;
9
+ access?: string;
10
+ }>) => void;
11
+ };
12
+ declare const WidgetStackInfoProps: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type WidgetStackInfoProps = ReturnType<typeof WidgetStackInfoProps>;
14
+ export default WidgetStackInfoProps;
@@ -135,7 +135,7 @@
135
135
  </script>
136
136
 
137
137
  <div
138
- class={twMerge("relative flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
138
+ class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
139
139
  style={accentStyle}
140
140
  >
141
141
  <!-- Заголовок и переключатель режима -->
package/dist/index.d.ts CHANGED
@@ -45,7 +45,9 @@ export { default as WidgetDeviceInfo } from "./WidgetDeviceInfo/WidgetDeviceInfo
45
45
  export { default as WidgetDeviceInfoProps } from "./WidgetDeviceInfo/WidgetDeviceInfoProps.svelte";
46
46
  export { default as WidgetFileSystem } from "./WidgetFileSystem/WidgetFileSystem.svelte";
47
47
  export { default as WidgetFileSystemProps } from "./WidgetFileSystem/WidgetFileSystemProps.svelte";
48
+ export { default as WidgetStackInfo } from "./WidgetStackInfo/WidgetStackInfo.svelte";
49
+ export { default as WidgetStackInfoProps } from "./WidgetStackInfo/WidgetStackInfoProps.svelte";
48
50
  export * from "./locales/i18n";
49
51
  export * from "./locales/translations";
50
52
  export * from "./ModalStackStore";
51
- 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 IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, 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";
53
+ 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 IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, 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/dist/index.js CHANGED
@@ -46,6 +46,8 @@ export { default as WidgetDeviceInfo } from "./WidgetDeviceInfo/WidgetDeviceInfo
46
46
  export { default as WidgetDeviceInfoProps } from "./WidgetDeviceInfo/WidgetDeviceInfoProps.svelte";
47
47
  export { default as WidgetFileSystem } from "./WidgetFileSystem/WidgetFileSystem.svelte";
48
48
  export { default as WidgetFileSystemProps } from "./WidgetFileSystem/WidgetFileSystemProps.svelte";
49
+ export { default as WidgetStackInfo } from "./WidgetStackInfo/WidgetStackInfo.svelte";
50
+ export { default as WidgetStackInfoProps } from "./WidgetStackInfo/WidgetStackInfoProps.svelte";
49
51
  export * from "./locales/i18n";
50
52
  export * from "./locales/translations";
51
53
  export * from "./ModalStackStore";
@@ -25,9 +25,14 @@ const translations = {
25
25
  ],
26
26
  "component.info.WidgetFileSystem": [
27
27
  "🗂️ Виджет работает с одним объектом `FS` группы `CFG` на устройстве — привязка переменных вручную не нужна.\n\n**📊 Занятость**\n- `Total`, `Used`, `Free`\n\n**📄 Файлы**\n- `List` — путь и размер; «Удалить» отправляет `SET FS` с `Op: Delete` и `Name` (после подтверждения)\n\n**⬆️ Закачка**\n- `SET FS` с `Op`: `UploadStart` → `UploadChunk` → `UploadDone`, прогресс — `Upload.Progress`\n- имя файла не длиннее 31 символа (ограничение прошивки)\n\n🔄 «Обновить» отправляет `GET FS`.\n\n⚙️ Если у прошивки другие имена, поменяйте префикс (ID), поле и аргумент в настройках виджета.",
28
- "🗂️ The widget works with a single `FS` object of the device's `CFG` group — no manual variable binding needed.\n\n**📊 Usage**\n- `Total`, `Used`, `Free`\n\n**📄 Files**\n- `List` — path and size; \"Delete\" sends `SET FS` with `Op: Delete` and `Name` (after confirmation)\n\n**⬆️ Upload**\n- `SET FS` with `Op`: `UploadStart` → `UploadChunk` → `UploadDone`, progress — `Upload.Progress`\n- file name up to 31 characters (firmware limit)\n\n🔄 \"Refresh\" sends `GET FS`.\n\n⚙️ If the firmware uses different names, change the prefix (ID), field and argument in the widget settings.",
28
+ '🗂️ The widget works with a single `FS` object of the device\'s `CFG` group — no manual variable binding needed.\n\n**📊 Usage**\n- `Total`, `Used`, `Free`\n\n**📄 Files**\n- `List` — path and size; "Delete" sends `SET FS` with `Op: Delete` and `Name` (after confirmation)\n\n**⬆️ Upload**\n- `SET FS` with `Op`: `UploadStart` → `UploadChunk` → `UploadDone`, progress — `Upload.Progress`\n- file name up to 31 characters (firmware limit)\n\n🔄 "Refresh" sends `GET FS`.\n\n⚙️ If the firmware uses different names, change the prefix (ID), field and argument in the widget settings.',
29
29
  "🗂️ 该组件使用设备 `CFG` 分组下的单个 `FS` 对象——无需手动绑定变量。\n\n**📊 占用**\n- `Total`、`Used`、`Free`\n\n**📄 文件**\n- `List` —— 路径和大小;「删除」在确认后发送带 `Op: Delete` 和 `Name` 的 `SET FS`\n\n**⬆️ 上传**\n- `SET FS`,`Op`:`UploadStart` → `UploadChunk` → `UploadDone`,进度 —— `Upload.Progress`\n- 文件名不超过 31 个字符(固件限制)\n\n🔄 「刷新」发送 `GET FS`。\n\n⚙️ 如果固件使用其他名称,请在组件设置中修改前缀(ID)、字段和参数。",
30
30
  ],
31
+ "component.info.WidgetStackInfo": [
32
+ "🧠 Виджет работает с одним объектом `StackInfo` из ответа устройства — привязка переменных вручную не нужна.\n\n**📊 Память**\n- `Heap[]` — `Total`, `Free`, `MinFree` по каждой куче (ESP: Internal и Default, STM: Heap)\n\n**🧵 Задачи**\n- `Tasks[]` — `Name`, `FreeMin` (минимальный запас стека, байт), `Prio`, `State`, `Core` (только ESP)\n- малый запас стека подсвечивается\n\n🔄 «Обновить» и автоопрос отправляют `GET StackInfo`.\n\n⚙️ Если у прошивки другие имена, поменяйте поле и аргумент в настройках виджета.",
33
+ '🧠 The widget works with a single `StackInfo` object from the device response — no manual variable binding needed.\n\n**📊 Memory**\n- `Heap[]` — `Total`, `Free`, `MinFree` per heap (ESP: Internal and Default, STM: Heap)\n\n**🧵 Tasks**\n- `Tasks[]` — `Name`, `FreeMin` (minimum stack headroom, bytes), `Prio`, `State`, `Core` (ESP only)\n- low stack headroom is highlighted\n\n🔄 "Refresh" and auto-polling send `GET StackInfo`.\n\n⚙️ If the firmware uses different names, change the field and argument in the widget settings.',
34
+ "🧠 该组件使用设备响应中的单个 `StackInfo` 对象——无需手动绑定变量。\n\n**📊 内存**\n- `Heap[]` —— 每个堆的 `Total`、`Free`、`MinFree`(ESP:Internal 和 Default,STM:Heap)\n\n**🧵 任务**\n- `Tasks[]` —— `Name`、`FreeMin`(最小栈余量,字节)、`Prio`、`State`、`Core`(仅 ESP)\n- 栈余量不足时高亮显示\n\n🔄 「刷新」和自动轮询发送 `GET StackInfo`。\n\n⚙️ 如果固件使用其他名称,请在组件设置中修改字段和参数。",
35
+ ],
31
36
  "component.info.Tabs": [
32
37
  "📑 Ширина и позиция иконки задаются через служебные CSS-классы каждой вкладки, а не отдельными полями.\n\n**↔️ Ширина кнопок** (переключатель «Авто/Равные»)\n- `w-auto` — по содержимому\n- `flex-1` — поровну между всеми вкладками\n\n**🔤 Позиция иконки**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Максимум 10 вкладок.",
33
38
  '📑 Tab width and icon position are set via each tab\'s internal CSS classes, not separate fields.\n\n**↔️ Button width** ("Auto/Equal" toggle)\n- `w-auto` — sized to content\n- `flex-1` — equal share among all tabs\n\n**🔤 Icon position**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Maximum 10 tabs.',
package/dist/types.d.ts CHANGED
@@ -15,10 +15,10 @@ export declare const updateComponent: (component: UIComponent, updates: Partial<
15
15
  }>) => {
16
16
  access: "full" | "viewOnly" | "hidden" | undefined;
17
17
  name: string | undefined;
18
- properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps;
18
+ properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps;
19
19
  eventHandler: IUIComponentHandler | undefined;
20
20
  id: string;
21
- type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem";
21
+ type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo";
22
22
  position: Required<Position>;
23
23
  parentId: string;
24
24
  };
@@ -26,8 +26,8 @@ export interface UIComponent {
26
26
  id: string;
27
27
  name?: string;
28
28
  access?: "full" | "viewOnly" | "hidden";
29
- type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem";
30
- properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps;
29
+ type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo";
30
+ properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps;
31
31
  position: Required<Position>;
32
32
  parentId: string;
33
33
  eventHandler?: IUIComponentHandler;
@@ -596,3 +596,38 @@ export interface IWidgetFileSystemProps {
596
596
  onDelete?: (name: string) => void;
597
597
  onUpload?: (file: File) => void | Promise<void>;
598
598
  }
599
+ export interface IWidgetStackInfoHeap {
600
+ Name: string;
601
+ Total: number;
602
+ Free: number;
603
+ MinFree: number;
604
+ }
605
+ export interface IWidgetStackInfoTask {
606
+ Name: string;
607
+ FreeMin: number;
608
+ Prio: number;
609
+ Core?: number;
610
+ State: number;
611
+ }
612
+ export interface IWidgetStackInfo {
613
+ Heap: IWidgetStackInfoHeap[];
614
+ Tasks: IWidgetStackInfoTask[];
615
+ }
616
+ export interface IWidgetStackInfoProps {
617
+ id?: string;
618
+ wrapperClass?: string;
619
+ componentClass?: string;
620
+ label?: {
621
+ name?: string;
622
+ };
623
+ value?: IWidgetStackInfo;
624
+ keys?: {
625
+ StackInfo?: string;
626
+ };
627
+ argument?: string;
628
+ warnBytes?: number;
629
+ period?: number;
630
+ collapsed?: boolean;
631
+ persistKey?: string;
632
+ onRefresh?: () => void;
633
+ }
@@ -8,7 +8,8 @@
8
8
  export const widgetAccentStyle = (componentClass) => {
9
9
  /* --bg-color = акцент: кнопки виджета с маркером "bg-accent" (класса в теме нет — только роль "залитая")
10
10
  заливаются им по наследованию, поэтому "Сохранить"/"Удалить" перекрашиваются вместе со всем виджетом */
11
- const fill = "--bg-color: var(--accent-color);";
11
+ /* --table-header-color — шапка таблиц внутри виджета (Table) в мягком оттенке мастер-цвета */
12
+ const fill = "--bg-color: var(--accent-color); --table-header-color: var(--accent-soft);";
12
13
  const role = componentClass.match(/(?:^|\s)bg-(\w+)/)?.[1];
13
14
  if (!role || role === "max")
14
15
  return fill;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.33.1",
3
+ "version": "1.34.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {