poe-svelte-ui-lib 1.53.0 → 1.55.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
@@ -57,6 +57,8 @@ npm install poe-svelte-ui-lib
57
57
 
58
58
  **Поверхности — четыре ступени, заметные в обеих темах:** фон страницы `--bg-body` → панель `--back-color` → блок внутри панели `--container-color` → поле `--field-color` (в тёмной теме поле темнее блока — «утоплено»). Готовые рецепты — классы `ui-panel` (блок прямо на фоне страницы: карточка, шапка раздела, холст, область чата — заливка панели, волосяная рамка, лёгкая тень) и `ui-inset` (блок внутри панели: группа полей, шапка карточки, подложка сообщения). Отступы и скругление задаёт страница классами Tailwind. Панели не делаются полупрозрачными (`bg-(--back-color)/50` сливается с фоном): выпадающие меню и всплывающие блоки — `ui-panel` с тенью `--elevation-3`, свои сообщения в чатах — `--accent-soft`, чужие — `--container-color`.
59
59
 
60
+ **Подсказки смарт-виджетов** — `hints.ts`: у каждого виджета своя кнопка «?» в шапке (`WidgetHeader` с пропом `hints`), выбор запоминается в браузере для виджетов этого типа (`hintsSetting("WidgetPFD")`, ключ `localStorage` `Hints.<тип>`), глобального переключателя нет. Элемент получает подсказку действием `use:hint={{ text, enabled: $hints }}` — всплывающее окно в стиле темы над элементом; тексты — в словаре библиотеки (`widget.*.hint.*`), только там, где смысл поля не очевиден.
61
+
60
62
  **Предпросмотр темы** — вкладка «Тема» на главной витрины (`src/routes/ThemePreview.svelte`): палитра токенов и состояния, затем все компоненты и виджеты в порядке меню витрины — каждый тип в своём разделе с живыми демо-данными (графики, видео, полёт для PFD и карты), в конце типографика и Markdown; тема переключается кнопкой в шапке витрины. Правьте `theme.css` и смотрите результат там.
61
63
 
62
64
  Текст в Markdown, таблицы и код переносятся по ширине контейнера — горизонтальной прокрутки быть не должно, только вертикальная. Markdown рендерится с `breaks: true` (как в облаке), поэтому абзац и пункт списка пишутся одной строкой — ручной перенос внутри абзаца превращается в разрыв строки.
@@ -42,7 +42,7 @@
42
42
  | `disabled` | `boolean` | `false` | Состояние отключения компонента |
43
43
  | `label` | `object` | `-` | Настройки заголовка и подписей компонента |
44
44
  | `label.name` | `string` | `""` | Текст заголовка компонента |
45
- | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
45
+ | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка; по умолчанию заголовок по центру над переключателем, `text-left`/`text-right` меняют выравнивание |
46
46
  | `label.captionLeft` | `string` | `""` | Левая подпись для горизонтального типа (кликабельна, устанавливает значение 0 без `bitMode`) |
47
47
  | `label.captionRight` | `string` | `""` | Правая подпись для горизонтального типа (кликабельна, устанавливает значение 1 без `bitMode`) |
48
48
  | `hiddenInfo` | `string` | `""` | Текст всплывающей подсказки для режима `checkbox`: отображается при наведении на чекбокс |
@@ -60,7 +60,8 @@
60
60
  {#if type !== "checkbox"}
61
61
  <div class={twMerge(`relative flex w-full flex-col items-center justify-center gap-1.5`, wrapperClass)}>
62
62
  {#if label.name}
63
- <h5 class={twMerge(`w-full text-[12px] font-semibold text-(--muted-color)`, label.class)}>{label.name}</h5>
63
+ <!-- Подпись по умолчанию по центру — над переключателем, который тоже по центру; выравнивание из label.class важнее -->
64
+ <h5 class={twMerge(`w-full text-center text-[12px] font-semibold text-(--muted-color)`, label.class)}>{label.name}</h5>
64
65
  {/if}
65
66
  <div class="flex w-full {type == 'vertical' ? 'flex-wrap' : ''} items-center justify-around gap-4">
66
67
  {#each localOptions as option, index}
@@ -106,3 +106,7 @@
106
106
  ### Что настраивается
107
107
 
108
108
  - Префикс группы (ID), мастер-цвет
109
+
110
+ ### Подсказки
111
+
112
+ Кнопка «?» в шапке включает и выключает подсказки этого виджета; выбор общий для всех `WidgetDeviceInfo` в браузере (`hintsSetting("WidgetDeviceInfo")`, `hints.ts`). Подсказка всплывает при наведении на поле, смысл которого не очевиден, — что это за значение и на что влияет.
@@ -14,6 +14,7 @@
14
14
  import CpuIcon from "./CpuIcon.svelte"
15
15
  import RestartIcon from "./RestartIcon.svelte"
16
16
  import WidgetHeader from "../WidgetHeader.svelte"
17
+ import { hint, hintsSetting } from "../hints"
17
18
  import { widgetAccentStyle } from "../widgetAccent"
18
19
  import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
19
20
 
@@ -29,6 +30,9 @@
29
30
  onFactoryReset,
30
31
  }: IWidgetDeviceInfoProps = $props()
31
32
 
33
+ /* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetDeviceInfo */
34
+ const hints = hintsSetting("WidgetDeviceInfo")
35
+
32
36
  let confirmReset = $state(false)
33
37
  const factoryReset = () => {
34
38
  confirmReset = false
@@ -93,14 +97,16 @@
93
97
  style={accentStyle}
94
98
  >
95
99
  <!-- Заголовок -->
96
- <WidgetHeader icon={CpuIcon} label={label?.name || $T("widget.info.title")} bind:collapsed>
100
+ <WidgetHeader icon={CpuIcon} label={label?.name || $T("widget.info.title")} bind:collapsed {hints}>
97
101
  {#snippet right()}
98
- <UI.Button
99
- wrapperClass="w-auto"
100
- componentClass="bg-transparent px-3"
101
- content={{ icon: RestartIcon, name: $T("widget.info.restart") }}
102
- onClick={onRestart}
103
- />
102
+ <div use:hint={{ text: $T("widget.info.hint.restart"), enabled: $hints }}>
103
+ <UI.Button
104
+ wrapperClass="w-auto"
105
+ componentClass="bg-transparent px-3"
106
+ content={{ icon: RestartIcon, name: $T("widget.info.restart") }}
107
+ onClick={onRestart}
108
+ />
109
+ </div>
104
110
  {/snippet}
105
111
  </WidgetHeader>
106
112
 
@@ -109,15 +115,27 @@
109
115
  <!-- Только чтение -->
110
116
  <div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
111
117
  <!-- Серийник бывает длинным (напр. 00.00.000-00_0000AC670000B22B00007428:DBC5) — своя строка, чтобы не резался -->
112
- <UI.Input label={{ name: $T("widget.info.sn") }} value={info.DevSN} readonly help={{ copyButton: true }} />
118
+ <div use:hint={{ text: $T("widget.info.hint.sn"), enabled: $hints }}>
119
+ <UI.Input label={{ name: $T("widget.info.sn") }} value={info.DevSN} readonly help={{ copyButton: true }} />
120
+ </div>
113
121
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
114
- <UI.Input label={{ name: $T("widget.info.id") }} value={info.DevID} readonly />
115
- <UI.Input label={{ name: $T("widget.info.fw") }} value={info.DevFW} readonly />
116
- <UI.Input label={{ name: $T("widget.info.runs") }} value={info.RunCnt} readonly />
122
+ <div use:hint={{ text: $T("widget.info.hint.id"), enabled: $hints }}>
123
+ <UI.Input label={{ name: $T("widget.info.id") }} value={info.DevID} readonly />
124
+ </div>
125
+ <div use:hint={{ text: $T("widget.info.hint.fw"), enabled: $hints }}>
126
+ <UI.Input label={{ name: $T("widget.info.fw") }} value={info.DevFW} readonly />
127
+ </div>
128
+ <div use:hint={{ text: $T("widget.info.hint.runs"), enabled: $hints }}>
129
+ <UI.Input label={{ name: $T("widget.info.runs") }} value={info.RunCnt} readonly />
130
+ </div>
117
131
  </div>
118
132
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
119
- <UI.Input label={{ name: $T("widget.info.sta_ip") }} value={info.StaIP} readonly />
120
- <UI.Input label={{ name: $T("widget.info.ap_ip") }} value={info.ApIP} readonly />
133
+ <div use:hint={{ text: $T("widget.info.hint.sta_ip"), enabled: $hints }}>
134
+ <UI.Input label={{ name: $T("widget.info.sta_ip") }} value={info.StaIP} readonly />
135
+ </div>
136
+ <div use:hint={{ text: $T("widget.info.hint.ap_ip"), enabled: $hints }}>
137
+ <UI.Input label={{ name: $T("widget.info.ap_ip") }} value={info.ApIP} readonly />
138
+ </div>
121
139
  </div>
122
140
  </div>
123
141
 
@@ -125,10 +143,18 @@
125
143
  <div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
126
144
  <span class="text-[12px] font-semibold text-(--muted-color)">{$T("widget.info.settings")}</span>
127
145
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
128
- <UI.Input label={{ name: $T("widget.info.name") }} bind:value={info.DevName} maxlength={26} />
129
- <UI.Input label={{ name: $T("widget.info.host") }} bind:value={info.HostName} maxlength={32} />
130
- <UI.Input label={{ name: $T("widget.info.web_user") }} bind:value={info.WebUser} maxlength={16} />
131
- <UI.Input type="password" label={{ name: $T("widget.info.web_psw") }} bind:value={info.WebPsw} maxlength={16} help={{ autocomplete: "off" }} />
146
+ <div use:hint={{ text: $T("widget.info.hint.name"), enabled: $hints }}>
147
+ <UI.Input label={{ name: $T("widget.info.name") }} bind:value={info.DevName} maxlength={26} />
148
+ </div>
149
+ <div use:hint={{ text: $T("widget.info.hint.host"), enabled: $hints }}>
150
+ <UI.Input label={{ name: $T("widget.info.host") }} bind:value={info.HostName} maxlength={32} />
151
+ </div>
152
+ <div use:hint={{ text: $T("widget.info.hint.web_user"), enabled: $hints }}>
153
+ <UI.Input label={{ name: $T("widget.info.web_user") }} bind:value={info.WebUser} maxlength={16} />
154
+ </div>
155
+ <div use:hint={{ text: $T("widget.info.hint.web_psw"), enabled: $hints }}>
156
+ <UI.Input type="password" label={{ name: $T("widget.info.web_psw") }} bind:value={info.WebPsw} maxlength={16} help={{ autocomplete: "off" }} />
157
+ </div>
132
158
  </div>
133
159
  </div>
134
160
 
@@ -115,3 +115,7 @@
115
115
 
116
116
  - Префикс группы (ID), поле состояния ФС, аргумент команд, мастер-цвет
117
117
  - Допустимые файлы (фильтр в окне выбора), максимальная длина имени
118
+
119
+ ### Подсказки
120
+
121
+ Кнопка «?» в шапке включает и выключает подсказки этого виджета; выбор общий для всех `WidgetFileSystem` в браузере (`hintsSetting("WidgetFileSystem")`, `hints.ts`). Подсказка всплывает при наведении на поле, смысл которого не очевиден, — что это за значение и на что влияет.
@@ -12,6 +12,7 @@
12
12
  import StorageIcon from "./StorageIcon.svelte"
13
13
  import RefreshIcon from "./RefreshIcon.svelte"
14
14
  import WidgetHeader from "../WidgetHeader.svelte"
15
+ import { hint, hintsSetting } from "../hints"
15
16
  import { widgetAccentStyle } from "../widgetAccent"
16
17
  import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
17
18
 
@@ -29,6 +30,9 @@
29
30
  onUpload,
30
31
  }: IWidgetFileSystemProps = $props()
31
32
 
33
+ /* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetFileSystem */
34
+ const hints = hintsSetting("WidgetFileSystem")
35
+
32
36
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
33
37
 
34
38
  /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
@@ -114,7 +118,7 @@
114
118
  style={accentStyle}
115
119
  >
116
120
  <!-- Заголовок -->
117
- <WidgetHeader icon={StorageIcon} label={label?.name || $T("widget.fs.title")} bind:collapsed>
121
+ <WidgetHeader icon={StorageIcon} label={label?.name || $T("widget.fs.title")} bind:collapsed {hints}>
118
122
  {#snippet right()}
119
123
  <UI.Button
120
124
  wrapperClass="w-auto"
@@ -129,7 +133,10 @@
129
133
  {#if !collapsed}
130
134
  <div class="flex flex-col gap-4" transition:slide={{ duration: 150 }}>
131
135
  <!-- Занятость памяти -->
132
- <div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
136
+ <div
137
+ class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3"
138
+ use:hint={{ text: $T("widget.fs.hint.usage"), enabled: $hints }}
139
+ >
133
140
  <div class="flex items-baseline justify-between gap-2 text-[13px]">
134
141
  <span class="font-semibold">{$T("widget.fs.used", { used: formatBytes(used), total: formatBytes(total) })}</span>
135
142
  <span class="font-semibold tabular-nums" style="color: {percentColor}">{usedPercent}%</span>
@@ -144,7 +151,7 @@
144
151
  </div>
145
152
 
146
153
  <!-- Файлы -->
147
- <div class="flex max-h-72 min-h-0 flex-col">
154
+ <div class="flex max-h-72 min-h-0 flex-col" use:hint={{ text: $T("widget.fs.hint.files"), enabled: $hints }}>
148
155
  {#if rows.length}
149
156
  <UI.Table {header} body={rows} outline />
150
157
  {:else}
@@ -157,7 +164,9 @@
157
164
  <!-- Закачка -->
158
165
  {#if onUpload}
159
166
  <div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
160
- <UI.FileAttach label={{ name: $T("widget.fs.upload") }} {accept} disabled={uploading || !!upload} onChange={handleFile} />
167
+ <div use:hint={{ text: $T("widget.fs.hint.upload"), enabled: $hints }}>
168
+ <UI.FileAttach label={{ name: $T("widget.fs.upload") }} {accept} disabled={uploading || !!upload} onChange={handleFile} />
169
+ </div>
161
170
  {#if uploading || upload}
162
171
  <div class="flex items-center gap-2 text-[12px]" transition:slide={{ duration: 100 }}>
163
172
  {#if upload?.Name}
@@ -7,28 +7,33 @@
7
7
  означало бы двойной отступ и лишнюю высоту. -->
8
8
  <script lang="ts">
9
9
  import type { Component, Snippet } from "svelte"
10
+ import type { Writable } from "svelte/store"
10
11
  import ArrowIcon from "./libIcons/ArrowIcon.svelte"
12
+ import { T } from "./locales/i18n"
11
13
 
12
14
  let {
13
15
  icon,
14
16
  label,
15
17
  collapsed = $bindable(false),
16
18
  right,
19
+ hints,
17
20
  }: {
18
21
  icon: Component
19
22
  label: string
20
23
  collapsed?: boolean
21
24
  right?: Snippet
25
+ /* Настройка подсказок этого виджета (hintsSetting) — показывает кнопку «?», переключающую только его подсказки */
26
+ hints?: Writable<boolean>
22
27
  } = $props()
23
28
 
24
29
  const Icon = $derived(icon)
25
30
  </script>
26
31
 
27
- <!-- Фиксированная высота 32 px без переноса — свёрнутые виджеты одной высоты при любом содержимом справа -->
32
+ <!-- Фиксированная высота 32 px без переноса — свёрнутые виджеты одной высоты при любом содержимом справа; «?» — сразу за заголовком, видна и в узком виджете -->
28
33
  <div class="flex h-[32px] items-center justify-between gap-3">
29
34
  <button
30
35
  type="button"
31
- class="flex min-w-0 flex-1 cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left"
36
+ class="flex min-w-0 cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left"
32
37
  onclick={() => (collapsed = !collapsed)}
33
38
  aria-expanded={!collapsed}
34
39
  >
@@ -43,6 +48,19 @@
43
48
  <ArrowIcon />
44
49
  </span>
45
50
  </button>
51
+ {#if hints}
52
+ <button
53
+ type="button"
54
+ class="flex size-[22px] shrink-0 cursor-pointer items-center justify-center rounded-full border text-[12px] font-bold transition-colors duration-150
55
+ {$hints
56
+ ? 'border-(--accent-color) bg-(--accent-soft) text-(--accent-color)'
57
+ : 'border-(--hairline-color) bg-transparent text-(--faint-color) hover:text-(--muted-color)'}"
58
+ title={$hints ? $T("widget.hints.on") : $T("widget.hints.off")}
59
+ aria-pressed={$hints}
60
+ onclick={() => hints.update((on) => !on)}>?</button
61
+ >
62
+ {/if}
63
+ <div class="flex-1"></div>
46
64
  {#if right}
47
65
  <div class="flex min-w-0 shrink-0 items-center justify-end">
48
66
  {@render right()}
@@ -1,9 +1,11 @@
1
1
  import type { Component, Snippet } from "svelte";
2
+ import type { Writable } from "svelte/store";
2
3
  type $$ComponentProps = {
3
4
  icon: Component;
4
5
  label: string;
5
6
  collapsed?: boolean;
6
7
  right?: Snippet;
8
+ hints?: Writable<boolean>;
7
9
  };
8
10
  declare const WidgetHeader: Component<$$ComponentProps, {}, "collapsed">;
9
11
  type WidgetHeader = ReturnType<typeof WidgetHeader>;
@@ -95,3 +95,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
95
95
  ## Внутреннее устройство
96
96
 
97
97
  `mapWrapper/` — тонкие Svelte-обёртки над MapLibre GL (`MapLibre`, `Marker`, `Popup`, `Line`, контролы, `CustomControl` для своих элементов поверх карты). MapLibre подгружается с CDN один раз на страницу (несколько карт ждут одну загрузку); без интернета показывается заглушка «Карта не загружена».
98
+
99
+ ### Подсказки
100
+
101
+ Кнопка «?» в шапке включает и выключает подсказки этого виджета; выбор общий для всех `WidgetMap` в браузере (`hintsSetting("WidgetMap")`, `hints.ts`). Подсказка всплывает при наведении на поле, смысл которого не очевиден, — что это за значение и на что влияет.
@@ -10,6 +10,7 @@
10
10
  import type { IWidgetMapProps } from "../types"
11
11
  import CopiedChip from "../CopiedChip.svelte"
12
12
  import WidgetHeader from "../WidgetHeader.svelte"
13
+ import { hint, hintsSetting } from "../hints"
13
14
  import { widgetAccentStyle } from "../widgetAccent"
14
15
  import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
15
16
  import MapIcon from "./MapIcon.svelte"
@@ -32,6 +33,9 @@
32
33
  persistKey,
33
34
  }: IWidgetMapProps = $props()
34
35
 
36
+ /* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetMap */
37
+ const hints = hintsSetting("WidgetMap")
38
+
35
39
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
36
40
 
37
41
  /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
@@ -177,7 +181,7 @@
177
181
  class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
178
182
  style={accentStyle}
179
183
  >
180
- <WidgetHeader icon={MapIcon} label={label?.name || $T("widget.map.title")} bind:collapsed>
184
+ <WidgetHeader icon={MapIcon} label={label?.name || $T("widget.map.title")} bind:collapsed {hints}>
181
185
  {#snippet right()}
182
186
  <div class="flex items-center gap-3">
183
187
  <UI.Button
@@ -193,7 +197,7 @@
193
197
  : freshCount < groupList.length
194
198
  ? 'text-(--muted-color)'
195
199
  : 'text-(--green-color)'}"
196
- title={$T("constructor.props.map.boards")}
200
+ use:hint={{ text: $T("widget.map.hint.boards"), enabled: $hints }}
197
201
  >
198
202
  {freshCount} / {groupList.length}
199
203
  </span>
@@ -219,7 +223,7 @@
219
223
  <!-- Панель оператора поверх карты -->
220
224
  <CustomControl position="top-left">
221
225
  <div class="flex flex-wrap items-center gap-3 rounded-lg px-2 py-1 text-black">
222
- <label class="flex items-center gap-1.5 text-sm font-medium">
226
+ <label class="flex items-center gap-1.5 text-sm font-medium" use:hint={{ text: $T("widget.map.hint.track"), enabled: $hints }}>
223
227
  <input type="checkbox" bind:checked={trackEnabled} />
224
228
  {$T("constructor.props.map.track")}
225
229
  </label>
@@ -233,7 +237,7 @@
233
237
  </select>
234
238
  </div>
235
239
  {/if}
236
- <div class="flex items-center gap-2">
240
+ <div class="flex items-center gap-2" use:hint={{ text: $T("widget.map.hint.timeout"), enabled: $hints }}>
237
241
  <span class="text-sm font-medium">{$T("constructor.props.map.timeout")}</span>
238
242
  <select class="rounded px-2 py-1 text-sm" bind:value={markerTimeout}>
239
243
  {#each timeoutOptions as opt}
@@ -70,7 +70,7 @@
70
70
  - **Авиагоризонт** — небо и земля по крену и тангажу, лестница тангажа через 5°, дуга крена (10/20/30/45/60°), символ самолёта. Ориентация сглаживается между пакетами (Tween 120 мс).
71
71
  - **Лента скорости** (слева, путевая над землёй) и **лента высоты** (справа, над точкой взлёта) с вариометром у края, **лента курса** (снизу): розовый ▲ — трек (куда аппарат реально движется, по `Vx`/`Vy`), зелёный ● — направление на точку взлёта.
72
72
  - **Углы под лентами** — воздушная скорость (относительно воздуха, отличается от путевой на ветер) и вертикальная скорость (+ набор зелёным, − снижение оранжевым).
73
- - **Подсказки** — наведите курсор на любой элемент прибора, строки состояния или значений: расшифровка появляется в информационной строке внизу горизонта: первая строка — величина, единица и текущее значение (обновляется, пока курсор стоит на месте), ниже по строке — что означают знаки, цвета и пороги. Элемент помечен `data-tip` с ключом, тексты собраны в `tips` в скрипте виджета, поэтому её не обрезают края и прокрутка. Без наведения в той же строке — последнее сообщение автопилота (10 с), наведение на неё показывает историю. Галочка «Подсказки» в шапке включает и выключает их; выбор общий для всех PFD в браузере (`localStorage`, ключ `WidgetPFD.tips`).
73
+ - **Подсказки** — наведите курсор на любой элемент прибора, строки состояния или значений: расшифровка появляется в информационной строке внизу горизонта: первая строка — величина, единица и текущее значение (обновляется, пока курсор стоит на месте), ниже по строке — что означают знаки, цвета и пороги. Элемент помечен `data-tip` с ключом, тексты собраны в `tips` в скрипте виджета, поэтому её не обрезают края и прокрутка. Без наведения в той же строке — последнее сообщение автопилота (10 с), наведение на неё показывает историю. Включаются и выключаются кнопкой «?» в шапке; выбор общий для всех PFD в браузере (`hintsSetting("WidgetPFD")` из `hints.ts`, ключ `localStorage` `Hints.WidgetPFD`; прежний выбор `WidgetPFD.tips` переносится).
74
74
  - **Под прибором** — группы «Маршрут» (точка, расстояние, боковое отклонение), «Дом» (расстояние и направление), «Трек», «Позиция» (координаты, MSL), «Воздух» (давление, температура); каждая величина показана один раз.
75
75
  - **Метки целей** (бирюзовые) — целевая скорость и высота текущей точки на лентах, курс на точку ◆ на ленте курса; цель за пределами ленты прижимается к краю.
76
76
  - **Сообщения автопилота** (`STATUSTEXT`) — в информационной строке внизу горизонта на 10 с, цвет по важности; история последних пяти — при наведении на строку.
@@ -13,6 +13,7 @@
13
13
  import type { IWidgetPFDProps } from "../types"
14
14
  import PlaneIcon from "./PlaneIcon.svelte"
15
15
  import WidgetHeader from "../WidgetHeader.svelte"
16
+ import { hintsSetting } from "../hints"
16
17
  import { widgetAccentStyle } from "../widgetAccent"
17
18
  import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
18
19
 
@@ -244,27 +245,13 @@
244
245
  /* Подсказка: элементы прибора и строк помечены data-tip, текст всегда в одном месте — информационная строка внизу горизонта */
245
246
  /* Ключ подсказки элемента под курсором; текст берётся из tips — обновляется вместе с данными */
246
247
  let tip: string | null = $state(null)
247
- /* Галочка «Подсказки» в шапке — выбор оператора, общий для всех PFD в браузере */
248
- const TIPS_KEY = "WidgetPFD.tips"
249
- let tipsEnabled = $state(true)
248
+ /* Подсказки включаются кнопкой «?» в шапке — выбор общий для всех PFD в браузере (hints.ts) */
249
+ const hints = hintsSetting("WidgetPFD")
250
250
  $effect(() => {
251
- try {
252
- tipsEnabled = localStorage.getItem(TIPS_KEY) !== "0"
253
- } catch {
254
- /* хранилище недоступно — подсказки включены */
255
- }
251
+ if (!$hints) tip = null
256
252
  })
257
- const setTips = (on: boolean) => {
258
- tipsEnabled = on
259
- if (!on) tip = null
260
- try {
261
- localStorage.setItem(TIPS_KEY, on ? "1" : "0")
262
- } catch {
263
- /* хранилище недоступно — выбор действует до перезагрузки */
264
- }
265
- }
266
253
  const onTip = (e: PointerEvent) => {
267
- if (!tipsEnabled) return
254
+ if (!$hints) return
268
255
  const el = (e.target as Element | null)?.closest("[data-tip]")
269
256
  tip = el ? (el.getAttribute("data-tip") ?? null) : null
270
257
  }
@@ -386,13 +373,9 @@
386
373
  class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
387
374
  style={accentStyle}
388
375
  >
389
- <WidgetHeader icon={PlaneIcon} label={label?.name || $T("pfd.title")} bind:collapsed>
376
+ <WidgetHeader icon={PlaneIcon} label={label?.name || $T("pfd.title")} bind:collapsed {hints}>
390
377
  {#snippet right()}
391
378
  <div class="flex items-center gap-4">
392
- <label class="flex cursor-pointer items-center gap-1.5 text-[12px] font-semibold text-(--muted-color)">
393
- <input type="checkbox" class="cursor-pointer accent-(--accent-color)" checked={tipsEnabled} onchange={(e) => setTips(e.currentTarget.checked)} />
394
- {$T("pfd.tips")}
395
- </label>
396
379
  <span class="text-[12px] font-semibold tabular-nums {stale ? 'text-(--red-color)' : 'text-(--muted-color)'}">{fmtAge(age)}</span>
397
380
  </div>
398
381
  {/snippet}
@@ -78,3 +78,7 @@
78
78
  ### Что настраивается
79
79
 
80
80
  - Префикс группы (ID), мастер-цвет
81
+
82
+ ### Подсказки
83
+
84
+ Кнопка «?» в шапке включает и выключает подсказки этого виджета; выбор общий для всех `WidgetRGBStrip` в браузере (`hintsSetting("WidgetRGBStrip")`, `hints.ts`). Подсказка всплывает при наведении на поле, смысл которого не очевиден, — что это за значение и на что влияет.
@@ -12,12 +12,16 @@
12
12
  import LightIcon from "./LightIcon.svelte"
13
13
  import ColorPicker from "./ColorPicker.svelte"
14
14
  import WidgetHeader from "../WidgetHeader.svelte"
15
+ import { hint, hintsSetting } from "../hints"
15
16
  import ArrowIcon from "../libIcons/ArrowIcon.svelte"
16
17
  import { widgetAccentStyle } from "../widgetAccent"
17
18
  import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
18
19
 
19
20
  let { wrapperClass = "", componentClass = "", label = {}, value, collapsed = $bindable(false), persistKey, onChange, onSave }: IWidgetRGBStripProps = $props()
20
21
 
22
+ /* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetRGBStrip */
23
+ const hints = hintsSetting("WidgetRGBStrip")
24
+
21
25
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
22
26
 
23
27
  /* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
@@ -84,7 +88,7 @@
84
88
  style={accentStyle}
85
89
  >
86
90
  <!-- Заголовок: образец текущего цвета -->
87
- <WidgetHeader icon={LightIcon} label={label?.name || $T("widget.rgb.title")} bind:collapsed>
91
+ <WidgetHeader icon={LightIcon} label={label?.name || $T("widget.rgb.title")} bind:collapsed {hints}>
88
92
  {#snippet right()}
89
93
  <span
90
94
  class="size-6 rounded-md border border-(--hairline-color)"
@@ -100,28 +104,34 @@
100
104
  <div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
101
105
  <ColorPicker value={color.slice(0, 3)} readonly={!onChange} onChange={(rgb) => sendColor([...rgb, color[3]])} />
102
106
  {#if hasWhite}
103
- <UI.Slider
104
- label={{ name: $T("widget.rgb.white") }}
105
- value={color[3]}
106
- number={{ minNum: 0, maxNum: 255, step: 1 }}
107
+ <div use:hint={{ text: $T("widget.rgb.hint.white"), enabled: $hints }}>
108
+ <UI.Slider
109
+ label={{ name: $T("widget.rgb.white") }}
110
+ value={color[3]}
111
+ number={{ minNum: 0, maxNum: 255, step: 1 }}
112
+ disabled={!onChange}
113
+ onUpdate={(w) => sendColor([color[0], color[1], color[2], w as number])}
114
+ />
115
+ </div>
116
+ {/if}
117
+ <div use:hint={{ text: $T("widget.rgb.hint.effect"), enabled: $hints }}>
118
+ <UI.Select
119
+ label={{ name: $T("widget.rgb.effect") }}
120
+ options={EFFECTS}
121
+ value={EFFECTS.find((o) => o.value === effectIndex) ?? EFFECTS[0]}
107
122
  disabled={!onChange}
108
- onUpdate={(w) => sendColor([color[0], color[1], color[2], w as number])}
123
+ onUpdate={(option) => !Array.isArray(option) && onChange?.("Effect", option.value ?? 0)}
109
124
  />
110
- {/if}
111
- <UI.Select
112
- label={{ name: $T("widget.rgb.effect") }}
113
- options={EFFECTS}
114
- value={EFFECTS.find((o) => o.value === effectIndex) ?? EFFECTS[0]}
115
- disabled={!onChange}
116
- onUpdate={(option) => !Array.isArray(option) && onChange?.("Effect", option.value ?? 0)}
117
- />
118
- <UI.Slider
119
- label={{ name: $T("widget.rgb.speed") }}
120
- value={speed}
121
- number={{ minNum: 0, maxNum: 10, step: 1 }}
122
- disabled={!onChange || effectIndex === 0}
123
- onUpdate={(v) => onChange?.("Speed", v as number)}
124
- />
125
+ </div>
126
+ <div use:hint={{ text: $T("widget.rgb.hint.speed"), enabled: $hints }}>
127
+ <UI.Slider
128
+ label={{ name: $T("widget.rgb.speed") }}
129
+ value={speed}
130
+ number={{ minNum: 0, maxNum: 10, step: 1 }}
131
+ disabled={!onChange || effectIndex === 0}
132
+ onUpdate={(v) => onChange?.("Speed", v as number)}
133
+ />
134
+ </div>
125
135
  </div>
126
136
 
127
137
  <!-- Оборудование: свёрнуто по умолчанию, сохраняется одним пакетом, лента перезапускается -->
@@ -147,27 +157,33 @@
147
157
  </button>
148
158
  {#if hardwareOpen || isDirty}
149
159
  <div class="flex flex-col gap-3" transition:slide={{ duration: 150 }}>
150
- <UI.Input
151
- label={{ name: $T("widget.rgb.count", { max: maxCount }) }}
152
- type="number"
153
- number={{ minNum: 1, maxNum: maxCount, step: 1 }}
154
- value={hardware.Count}
155
- onUpdate={(v) => (hardware.Count = Number(v))}
156
- />
157
- <UI.Select
158
- label={{ name: $T("widget.rgb.model") }}
159
- type="buttons"
160
- options={MODELS}
161
- value={MODELS.find((o) => o.value === hardware.Model) ?? MODELS[0]}
162
- onUpdate={(option) => !Array.isArray(option) && (hardware.Model = option.value ?? 0)}
163
- />
164
- <UI.Select
165
- label={{ name: $T("widget.rgb.order") }}
166
- type="buttons"
167
- options={ORDERS}
168
- value={ORDERS.find((o) => o.value === hardware.Order) ?? ORDERS[0]}
169
- onUpdate={(option) => !Array.isArray(option) && (hardware.Order = option.value ?? 0)}
170
- />
160
+ <div use:hint={{ text: $T("widget.rgb.hint.count"), enabled: $hints }}>
161
+ <UI.Input
162
+ label={{ name: $T("widget.rgb.count", { max: maxCount }) }}
163
+ type="number"
164
+ number={{ minNum: 1, maxNum: maxCount, step: 1 }}
165
+ value={hardware.Count}
166
+ onUpdate={(v) => (hardware.Count = Number(v))}
167
+ />
168
+ </div>
169
+ <div use:hint={{ text: $T("widget.rgb.hint.model"), enabled: $hints }}>
170
+ <UI.Select
171
+ label={{ name: $T("widget.rgb.model") }}
172
+ type="buttons"
173
+ options={MODELS}
174
+ value={MODELS.find((o) => o.value === hardware.Model) ?? MODELS[0]}
175
+ onUpdate={(option) => !Array.isArray(option) && (hardware.Model = option.value ?? 0)}
176
+ />
177
+ </div>
178
+ <div use:hint={{ text: $T("widget.rgb.hint.order"), enabled: $hints }}>
179
+ <UI.Select
180
+ label={{ name: $T("widget.rgb.order") }}
181
+ type="buttons"
182
+ options={ORDERS}
183
+ value={ORDERS.find((o) => o.value === hardware.Order) ?? ORDERS[0]}
184
+ onUpdate={(option) => !Array.isArray(option) && (hardware.Order = option.value ?? 0)}
185
+ />
186
+ </div>
171
187
  {#if isDirty}
172
188
  <span class="text-[12px] text-(--muted-color)" transition:slide={{ duration: 100 }}>
173
189
  {countValid ? $T("widget.rgb.restart_note") : $T("widget.rgb.count_invalid", { max: maxCount })}
@@ -17,13 +17,13 @@
17
17
  "StackInfo": {
18
18
  "Heap": [
19
19
  { "Name": "Internal", "Total": 327680, "Free": 118000, "MinFree": 92000 },
20
- { "Name": "Default", "Total": 4521984, "Free": 4100000, "MinFree": 4040000 }
20
+ { "Name": "PSRAM", "Total": 4194304, "Free": 3980000, "MinFree": 3920000 }
21
21
  ],
22
22
  "Tasks": [{ "Name": "api_task", "FreeMin": 1240, "Prio": 5, "Core": 1, "State": 2 }]
23
23
  }
24
24
  ```
25
25
 
26
- Все размеры в байтах. ESP отдаёт две кучи (`Internal` — внутренний DRAM, `Default` — всё, что отдаёт `malloc`, включая PSRAM) и `Core` (-1 — без привязки к ядру); STM — одну кучу `Heap` (heap_4, `configTOTAL_HEAP_SIZE`) и без `Core`. `State` — `eTaskState`: 0 работает, 1 готова, 2 ожидает, 3 приостановлена, 4 удалена.
26
+ Коды куч виджет показывает понятными подписями на языке пользователя — «Встроенная ОЗУ», «Внешняя ОЗУ (PSRAM)», «ОЗУ» (`Heap` у STM) — с подсказкой при наведении, что это за память и чем опасна нехватка; неизвестный код — как пришёл. Все размеры в байтах. ESP отдаёт непересекающиеся кучи `Internal` (встроенная ОЗУ) и `PSRAM` (внешняя, только если есть на устройстве) и `Core` (-1 — без привязки к ядру); STM — одну кучу `Heap` (heap_4, `configTOTAL_HEAP_SIZE`) и без `Core`. `State` — `eTaskState`: 0 работает, 1 готова, 2 ожидает, 3 приостановлена, 4 удалена.
27
27
 
28
28
  ## Ключи
29
29
 
@@ -81,3 +81,7 @@
81
81
 
82
82
  - Порог запаса стека: меньше порога — красным, меньше удвоенного — оранжевым
83
83
  - Автоопрос: выкл / 1 / 5 / 10 с; мастер-цвет
84
+
85
+ ### Подсказки
86
+
87
+ Кнопка «?» в шапке включает и выключает подсказки этого виджета; выбор общий для всех `WidgetStackInfo` в браузере (`hintsSetting("WidgetStackInfo")`, `hints.ts`). Подсказка всплывает при наведении на поле, смысл которого не очевиден, — что это за значение и на что влияет.