poe-svelte-ui-lib 1.91.0 → 1.93.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.
Files changed (34) hide show
  1. package/README.md +24 -11
  2. package/dist/Accordion/Accordion.svelte +6 -4
  3. package/dist/Button/Button.svelte +1 -1
  4. package/dist/CopiedChip.svelte +1 -1
  5. package/dist/Graph/Graph.svelte +1 -1
  6. package/dist/Input/Input.svelte +1 -1
  7. package/dist/Modal.README.md +12 -11
  8. package/dist/Modal.svelte +1 -1
  9. package/dist/PeriodChart/PeriodChart.svelte +2 -2
  10. package/dist/Table/Table.svelte +3 -3
  11. package/dist/WidgetAccentPicker.svelte +5 -2
  12. package/dist/WidgetFileSystem/WidgetFileSystem.svelte +4 -4
  13. package/dist/WidgetMap/README.md +13 -9
  14. package/dist/WidgetMap/WidgetMap.svelte +11 -11
  15. package/dist/WidgetMap/mission/HelpText.svelte +41 -0
  16. package/dist/WidgetMap/mission/HelpText.svelte.d.ts +6 -0
  17. package/dist/WidgetMap/mission/MapAgro.svelte +108 -64
  18. package/dist/WidgetMap/mission/MapRoute.svelte +70 -51
  19. package/dist/WidgetMap/mission/MissionLayers.svelte +60 -22
  20. package/dist/WidgetMap/mission/MissionLayers.svelte.d.ts +2 -0
  21. package/dist/WidgetMap/mission/MissionSim.svelte +19 -9
  22. package/dist/WidgetMap/mission/MissionSim.svelte.d.ts +2 -0
  23. package/dist/WidgetMap/mission/coverage.js +31 -6
  24. package/dist/WidgetMap/mission/plan.js +64 -18
  25. package/dist/WidgetPFD/WidgetPFD.svelte +27 -18
  26. package/dist/WidgetStackInfo/WidgetStackInfo.svelte +4 -4
  27. package/dist/WidgetWiFi/WidgetWiFi.svelte +1 -1
  28. package/dist/locales/translations.js +15 -10
  29. package/dist/options.js +2 -1
  30. package/dist/protocols/xPoeDebugView.js +6 -6
  31. package/dist/theme.css +102 -49
  32. package/dist/widgetAccent.d.ts +8 -0
  33. package/dist/widgetAccent.js +12 -1
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -70,11 +70,11 @@ npm install poe-svelte-ui-lib
70
70
 
71
71
  У каждого компонента есть парный `<Компонент>Props.svelte`. Это панель свойств для конструктора DevCloud. Пропсы у всех панелей одинаковые:
72
72
 
73
- | Пропс | Что это |
74
- |---|---|
75
- | `component` | Компонент интерфейса и его свойства |
76
- | `onPropertyChange` | Сообщает об изменении свойств, имени, доступа или события |
77
- | `forConstructor` | `true` - полная панель конструктора, `false` - упрощённая для редактирования |
73
+ | Пропс | Что это |
74
+ | ------------------ | ---------------------------------------------------------------------------- |
75
+ | `component` | Компонент интерфейса и его свойства |
76
+ | `onPropertyChange` | Сообщает об изменении свойств, имени, доступа или события |
77
+ | `forConstructor` | `true` - полная панель конструктора, `false` - упрощённая для редактирования |
78
78
 
79
79
  ## Протоколы
80
80
 
@@ -102,6 +102,19 @@ npm install poe-svelte-ui-lib
102
102
 
103
103
  Поверхности идут в четыре ступени: фон страницы `--bg-body`, панель `--back-color`, блок внутри панели `--container-color`, поле ввода `--field-color`. Для частых случаев есть готовые классы. `ui-panel` для блока прямо на фоне страницы, `ui-inset` для блока внутри панели.
104
104
 
105
+ Текст идёт в три ступени: основной `--font-color`, вторичный `--muted-color`, подсказки `--faint-color`. Все три проходят WCAG AA (не ниже 4,5:1) на любом фоне темы. Текст не делают полупрозрачным (`opacity`), для вторичного текста берут `--muted-color`. Мельче 11px подписи не делают.
106
+
107
+ Цвета ролей взяты у Apple: красный, оранжевый, жёлтый, зелёный, голубой, синий, фиолетовый, розовый и серый. У каждого цвета четыре варианта:
108
+
109
+ - `--*-color` - заливка. Текст на ней белый, а на оранжевой и жёлтой тёмный (`--on-orange`, `--on-yellow`). Компоненты берут его из `--on-accent`, классы `bg-orange` и `bg-yellow` подставляют его сами;
110
+ - `--*-text` - цветной текст;
111
+ - `--*-soft` - мягкий фон блока или строки;
112
+ - `--*-mark` - чистый цвет Apple для полос, точек и индикаторов. Для текста он не годится, жёлтый и голубой на белом не читаются.
113
+
114
+ Акцентный цвет `--accent-color` - это синий Apple.
115
+
116
+ Цветной текст пишут через `--red-text`, `--green-text`, `--orange-text` и так далее (классы `text-(--red-text)`), а не через `--red-color`. Заливки `--*-color` как текст плохо читаются: в тёмной теме они тёмные, а жёлтая и оранжевая слишком светлые на белом.
117
+
105
118
  Панели не делают полупрозрачными, на фоне они теряются. Выпадающие меню делают через `ui-panel` с тенью `--elevation-3`.
106
119
 
107
120
  Стили тегов в приложении пишите только внутри `@layer base`. Правило вне слоя сильнее утилит Tailwind, и подписи в компонентах станут крупнее, чем задумано.
@@ -148,13 +161,13 @@ export const T = createTranslator(translations)
148
161
 
149
162
  **Короткое**, по кнопке (i). Лежит в словаре под ключом `component.info.<Тип>`. Его показывают витрина и конструктор DevCloud. Разделы идут в таком порядке, ненужные пропускаются:
150
163
 
151
- | Раздел | Что писать |
152
- |---|---|
153
- | Назначение | Одна-две фразы: что это и когда применять |
164
+ | Раздел | Что писать |
165
+ | ------------ | ------------------------------------------------------------------------------- |
166
+ | Назначение | Одна-две фразы: что это и когда применять |
154
167
  | Пакет данных | Откуда данные, каким пакетом и как часто их слать, что компонент отправляет сам |
155
- | Ключи | По пункту на ключ: тип, единицы, смысл |
156
- | Особенности | Поведение, ограничения, советы |
157
- | Настройки | Что меняется в панели свойств |
168
+ | Ключи | По пункту на ключ: тип, единицы, смысл |
169
+ | Особенности | Поведение, ограничения, советы |
170
+ | Настройки | Что меняется в панели свойств |
158
171
 
159
172
  **Подробное**, `src/lib/<Компонент>/README.md`. Оно открывается на вкладке «Инфо» витрины. Начало повторяет короткое описание и не должно ему противоречить. Дальше идут пропсы, примеры, панель свойств и внутреннее устройство.
160
173
 
@@ -29,15 +29,17 @@
29
29
  <div class="overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
30
30
  <!-- Фон заголовка - мягкий оттенок мастер-цвета (светлый в светлой теме, тёмный в тёмной), текст - мастер-цвет с цветом шрифта 60/40, раскрытое содержимое остаётся на чистом фоне (--back-color). -->
31
31
  <!-- Заголовок: кнопка сворачивания на всю ширину и необязательные кнопки справа (actions) - вне неё, чтобы не сворачивать блок -->
32
- <div class="flex w-full items-center bg-(--accent-soft)">
32
+ <div
33
+ class="flex w-full items-center bg-(--accent-soft) transition-colors duration-150 hover:bg-[color-mix(in_srgb,var(--accent-color)_12%,var(--accent-soft))]"
34
+ >
33
35
  <button
34
- class="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left text-[color-mix(in_srgb,var(--accent-color)_60%,var(--font-color))] transition-colors duration-150 hover:bg-[color-mix(in_srgb,var(--accent-color)_12%,var(--accent-soft))]"
36
+ class="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))]"
35
37
  onclick={toggle}
36
38
  >
37
39
  <div class="flex min-w-0 flex-1 items-center gap-3">
38
40
  {#if label?.icon}
39
41
  <span
40
- class="flex h-[18px] w-[18px] shrink-0 items-center justify-center text-[color-mix(in_srgb,var(--accent-color)_60%,var(--font-color))] [&_svg]:h-full [&_svg]:w-full"
42
+ class="flex h-[18px] w-[18px] shrink-0 items-center justify-center text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] [&_svg]:h-full [&_svg]:w-full"
41
43
  >
42
44
  {#if typeof label?.icon === "string"}
43
45
  {@html label.icon}
@@ -52,7 +54,7 @@
52
54
 
53
55
  <svg
54
56
  xmlns="http://www.w3.org/2000/svg"
55
- class="h-[14px] w-[14px] shrink-0 text-[color-mix(in_srgb,var(--accent-color)_60%,var(--font-color))] transition-transform duration-200"
57
+ class="h-[14px] w-[14px] shrink-0 text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] transition-transform duration-200"
56
58
  style="transform: rotate({isOpen ? 180 : 0}deg)"
57
59
  viewBox="0 0 24 24"
58
60
  >
@@ -126,7 +126,7 @@
126
126
  {#if content.name?.trim()}
127
127
  <span class="truncate">{content.name}</span>
128
128
  {#if keyBind.key && keyBind.showHint}
129
- <span class="ml-1 text-[11px] font-medium tabular-nums opacity-60">
129
+ <span class="ml-1 text-[11px] font-medium tabular-nums opacity-80">
130
130
  {keyBind.ctrlKey ? "Ctrl+" : ""}{keyBind.shiftKey ? "Shift+" : ""}{keyBind.altKey ? "Alt+" : ""}{keyBind.key}
131
131
  </span>
132
132
  {/if}
@@ -8,7 +8,7 @@
8
8
 
9
9
  {#if show}
10
10
  <span
11
- class="{className} absolute flex items-center gap-1 rounded-lg bg-(--green-soft) px-2 py-1 text-[11px] font-bold whitespace-nowrap text-(--green-color)"
11
+ class="{className} absolute flex items-center gap-1 rounded-lg bg-(--green-soft) px-2 py-1 text-[11px] font-bold whitespace-nowrap text-(--green-text)"
12
12
  transition:fade={{ duration: 150 }}
13
13
  >
14
14
  {$T("ui.copied")}
@@ -312,7 +312,7 @@
312
312
  const { padding, graphWidth, graphHeight, minX, timeSpan, yMin, yMax, getX, getY } = computeGeometry()
313
313
 
314
314
  /* Сетка в стиле PeriodChart: пунктир цветом --faint-color темы, подписи тем же цветом без прозрачности */
315
- const gridColor = getComputedStyle(canvas).getPropertyValue("--faint-color").trim() || "#8b93a1"
315
+ const gridColor = getComputedStyle(canvas).getPropertyValue("--faint-color").trim() || "#5f6774"
316
316
  ctx.strokeStyle = gridColor
317
317
  ctx.fillStyle = gridColor
318
318
  ctx.lineWidth = 1
@@ -245,6 +245,6 @@
245
245
 
246
246
  {#if help.regExp && !isValid}
247
247
  <!-- Текст ошибки в потоке под полем и только когда поле невалидно: место заранее не резервируется, на соседей не наезжает -->
248
- <span class="block text-[12px] font-semibold text-(--red-color)">{$T("ui.invalid_format")}</span>
248
+ <span class="block text-[12px] font-semibold text-(--red-text)">{$T("ui.invalid_format")}</span>
249
249
  {/if}
250
250
  </div>
@@ -7,6 +7,7 @@
7
7
  ## Особенности
8
8
 
9
9
  - Подложка затемняет страницу на 40% и размывает её.
10
+ - Окно не выше 90% экрана. Длинное содержимое прокручивается внутри, шапка с крестиком остаётся на месте.
10
11
  - Escape и клик мимо окна закрывают только верхнее окно из открытых.
11
12
  - Выпадающие списки `Select` внутри окна вынесены в `body`. Клик по ним не считается кликом мимо окна.
12
13
  - Содержимое передаётся сниппетом `main`, обычные дочерние элементы не подойдут. Подвал `footer` необязателен.
@@ -14,16 +15,16 @@
14
15
 
15
16
  ## Пропсы
16
17
 
17
- | Название | Тип | По умолчанию | Описание |
18
- |---|---|---|---|
19
- | `isOpen` | `boolean` | `false` | Открыто ли окно. Можно связать через `bind:isOpen` |
20
- | `title` | `string` | | Заголовок |
21
- | `width` | `string` | `""` | Ширина, например `"600px"`. По умолчанию из `wrapperClass` |
22
- | `wrapperClass` | `string` | `""` | Классы окна |
23
- | `mainClass` | `string` | `""` | Классы области содержимого |
24
- | `main` | `Snippet` | обязателен | Содержимое |
25
- | `footer` | `Snippet` | | Подвал, обычно кнопки |
26
- | `onCancel` | `() => void` | закрыть окно | Окно закрывают крестиком, кликом мимо или Escape |
18
+ | Название | Тип | По умолчанию | Описание |
19
+ | -------------- | ------------ | ------------ | ---------------------------------------------------------- |
20
+ | `isOpen` | `boolean` | `false` | Открыто ли окно. Можно связать через `bind:isOpen` |
21
+ | `title` | `string` | | Заголовок |
22
+ | `width` | `string` | `""` | Ширина, например `"600px"`. По умолчанию из `wrapperClass` |
23
+ | `wrapperClass` | `string` | `""` | Классы окна |
24
+ | `mainClass` | `string` | `""` | Классы области содержимого |
25
+ | `main` | `Snippet` | обязателен | Содержимое |
26
+ | `footer` | `Snippet` | | Подвал, обычно кнопки |
27
+ | `onCancel` | `() => void` | закрыть окно | Окно закрывают крестиком, кликом мимо или Escape |
27
28
 
28
29
  ## Пример
29
30
 
@@ -52,7 +53,7 @@
52
53
 
53
54
  ```ts
54
55
  ModalStack.subscribe // список id в порядке открытия
55
- ModalStack.open(id) // добавить в конец
56
+ ModalStack.open(id) // добавить в конец
56
57
  ModalStack.close(id) // убрать
57
58
  ```
58
59
 
package/dist/Modal.svelte CHANGED
@@ -82,7 +82,7 @@
82
82
  <div
83
83
  data-modal
84
84
  class={twMerge(
85
- "flex w-300 flex-col overflow-hidden rounded-[20px] border border-(--hairline-color) bg-(--back-color) shadow-(--elevation-3)",
85
+ "flex max-h-[90vh] w-300 flex-col overflow-hidden rounded-[20px] border border-(--hairline-color) bg-(--back-color) shadow-(--elevation-3)",
86
86
  wrapperClass,
87
87
  )}
88
88
  style="width: {width}; z-index: {zIndex};"
@@ -110,7 +110,7 @@
110
110
  <!-- Шкала Y: подписи нулевой высоты, центр каждой ровно на своей линии сетки -->
111
111
  <div class="flex h-40 shrink-0 flex-col justify-between">
112
112
  {#each axis.ticks as tick}
113
- <div class="flex h-0 items-center justify-end text-[10px] font-medium text-(--faint-color) tabular-nums">
113
+ <div class="flex h-0 items-center justify-end text-[11px] font-medium text-(--faint-color) tabular-nums">
114
114
  {formatTick(tick)}
115
115
  </div>
116
116
  {/each}
@@ -152,7 +152,7 @@
152
152
 
153
153
  <div class="flex w-full gap-1">
154
154
  {#each values as _, i}
155
- <div class="flex-1 truncate text-center text-[10px] font-bold text-(--faint-color)">
155
+ <div class="flex-1 truncate text-center text-[11px] font-bold text-(--faint-color)">
156
156
  {i % labelStep === 0 ? barLabel(i) : ""}
157
157
  </div>
158
158
  {/each}
@@ -263,7 +263,7 @@
263
263
  <div class="relative flex h-full w-full flex-col overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
264
264
  <!-- Table Header -->
265
265
  <div
266
- class="grid border-b border-(--hairline-color) bg-[var(--table-header-color,var(--accent-soft))] text-[11px] font-bold tracking-[0.06em] text-[color-mix(in_srgb,var(--accent-color)_60%,var(--font-color))] uppercase"
266
+ class="grid border-b border-(--hairline-color) bg-[var(--table-header-color,var(--accent-soft))] text-[11px] font-bold tracking-[0.06em] text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] uppercase"
267
267
  style={`grid-template-columns: ${gridTemplate}; padding-right: ${scrollbarWidth}px;`}
268
268
  >
269
269
  {#each header as column, index (column)}
@@ -294,7 +294,7 @@
294
294
  {#if dataBuffer.clearButton}
295
295
  <div class="flex items-center justify-center tracking-normal normal-case">
296
296
  <Button
297
- componentClass={twMerge("size-7 p-1.5 text-(--muted-color) hover:text-(--red-color)", dataBuffer.clearClass)}
297
+ componentClass={twMerge("size-7 p-1.5 text-(--muted-color) hover:text-(--red-text)", dataBuffer.clearClass)}
298
298
  content={{ icon: ButtonClear, info: { text: $T("common.table.clear"), side: "left" } }}
299
299
  onClick={clearBuffer}
300
300
  />
@@ -380,7 +380,7 @@
380
380
  {#if isDropdownOpen?.x === j && isDropdownOpen.y === i && isDropdownOpen.index === index}
381
381
  {@const cords = document.getElementById(`select${i}-${j}-${index}`)?.getBoundingClientRect()}
382
382
  <div
383
- class="fixed z-50 overflow-hidden rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-1 shadow-(--elevation-2)"
383
+ class="fixed z-50 max-h-[40vh] overflow-y-auto rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-1 shadow-(--elevation-2)"
384
384
  style="top: calc({cords?.bottom}px + 4px); left: {cords?.left}px; width: {cords?.width}px;"
385
385
  transition:slide={{ duration: selectSlideDuration }}
386
386
  >
@@ -14,13 +14,16 @@
14
14
  onUpdate: (value: string) => void
15
15
  wrapperClass?: string
16
16
  } = $props()
17
+
18
+ /* bg-max не показываем: без выбора виджет и так синий, это цвет по умолчанию */
19
+ const options = $derived($optionsStore.COLOR_OPTIONS.filter((o) => o.value !== "bg-max"))
17
20
  </script>
18
21
 
19
22
  <Select
20
23
  {wrapperClass}
21
24
  label={{ name: $T("ui.accent") }}
22
25
  type="buttons"
23
- options={$optionsStore.COLOR_OPTIONS}
24
- value={$optionsStore.COLOR_OPTIONS.find((o) => o.value === value) ?? $optionsStore.COLOR_OPTIONS[0]}
26
+ {options}
27
+ value={options.find((o) => o.value === value) ?? options.find((o) => o.value === "bg-blue")}
25
28
  onUpdate={(option) => onUpdate((option as IOption<string>).value ?? "")}
26
29
  />
@@ -58,7 +58,7 @@
58
58
  let files = $derived(value?.List ?? [])
59
59
  let usedPercent = $derived(total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0)
60
60
  /* Полоса - мастер-цвет виджета; почти заполненную ФС (закачка скоро начнёт падать) выдаёт только процент */
61
- let percentColor = $derived(usedPercent >= 90 ? "var(--red-color)" : usedPercent >= 75 ? "var(--orange-color)" : "var(--muted-color)")
61
+ let percentColor = $derived(usedPercent >= 90 ? "var(--red-text)" : usedPercent >= 75 ? "var(--orange-text)" : "var(--muted-color)")
62
62
 
63
63
  /* Таблица файлов: путь обрезается «…», размер - уже отформатирован; действия - значки без текста: скачать и удалить (с подтверждением) */
64
64
  type FileRow = IWidgetFileSystemFile & { SizeText: string }
@@ -74,7 +74,7 @@
74
74
  data: {
75
75
  icon: ButtonDelete,
76
76
  title: $T("widget.common.delete"),
77
- class: "bg-transparent px-0 text-(--red-color) hover:bg-(--red-color)/10",
77
+ class: "bg-transparent px-0 text-(--red-text) hover:bg-(--red-color)/10",
78
78
  onClick: (row: FileRow) => (pendingDelete = row.Name),
79
79
  },
80
80
  },
@@ -185,7 +185,7 @@
185
185
  >
186
186
  {/if}
187
187
  {#if downloadError}
188
- <span class="mt-2 text-[12px] text-(--red-color)" transition:slide={{ duration: 100 }}>{downloadError}</span>
188
+ <span class="mt-2 text-[12px] text-(--red-text)" transition:slide={{ duration: 100 }}>{downloadError}</span>
189
189
  {/if}
190
190
  </div>
191
191
 
@@ -207,7 +207,7 @@
207
207
  </div>
208
208
  {/if}
209
209
  {#if uploadError}
210
- <span class="text-[12px] text-(--red-color)" transition:slide={{ duration: 100 }}>{uploadError}</span>
210
+ <span class="text-[12px] text-(--red-text)" transition:slide={{ duration: 100 }}>{uploadError}</span>
211
211
  {/if}
212
212
  </div>
213
213
  {/if}
@@ -60,18 +60,19 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
60
60
  - MapLibre загружается с CDN один раз на страницу. Без интернета видна заглушка «Карта не загружена».
61
61
  - Источник подложки (CARTO и OpenStreetMap) указывать обязательно. Подпись свёрнута в кнопку «i» в углу карты.
62
62
  - Сворачивание, мастер-цвет и кнопка «?» с подсказками работают как у `WidgetWiFi`.
63
+ - Мастер-цвет красит весь виджет: кнопки, линию маршрута, точки, точку взлёта, переходы и аппарат в имитации. Цвета со смыслом не меняются: поля зелёные, запретные зоны красные, развороты оранжевые, «Очистить», удаление и ошибка отправки красные.
63
64
 
64
65
  ## Режим имитации
65
66
 
66
67
  `isTest` включает имитацию шести аппаратов `SIM-1`-`SIM-6` без устройства. Они летают над одним районом Минска, в пределах пары километров, и обновляются раз в полсекунды. На связи всегда три аппарата. Через 40-100 секунд один из них пропадает, и его сразу сменяет тот, кто молчит дольше всех. Поэтому видно все состояния маркера: свежий, красный устаревший, скрытый и снова появившийся. При включении карта переходит к этому району. Пока режим включён, `value` и `groups` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат.
67
68
 
68
- В режимах «Маршрут» и «Агро» `isTest` включает имитацию полёта по рассчитанной траектории. Аппарат летит по маршруту со скоростями его точек или по проходам «Агро» и оставляет голубой след. В «Агро» маркер - квадрокоптер, он зелёный, пока идёт распыление. Панель поверх карты показывает время полёта, процент пройденного пути, высоту, скорость и распыление; там же запуск и пауза, ускорение ×1, ×5, ×10 или ×25 (по умолчанию ×5) и полоса прокрутки пути: её перетаскивание ставит полёт на паузу, чтобы рассмотреть любое место траектории. Полёт запускается вручную кнопкой ▶ и останавливается в конце пути; повторный запуск начинает его заново. При любом изменении маршрута или полей аппарат возвращается в начало и встаёт на паузу. Аппараты из `groups` в это время не рисуются.
69
+ В режимах «Маршрут» и «Агро» `isTest` включает имитацию полёта по рассчитанной траектории. Аппарат летит по маршруту со скоростями его точек или по проходам «Агро» и оставляет след цвета виджета. В «Агро» маркер - квадрокоптер, он зелёный, пока идёт распыление. Панель внизу карты по центру показывает время полёта, процент пройденного пути, высоту, скорость и распыление; там же запуск и пауза, ускорение ×1, ×5, ×10 или ×25 (по умолчанию ×5) и полоса прокрутки пути: её перетаскивание ставит полёт на паузу, чтобы рассмотреть любое место траектории. Полёт запускается вручную кнопкой ▶ и останавливается в конце пути; повторный запуск начинает его заново. При любом изменении маршрута или полей аппарат возвращается в начало и встаёт на паузу. Аппараты из `groups` в это время не рисуются.
69
70
 
70
71
  ## Режим «Маршрут»
71
72
 
72
73
  `mode="route"` - обычный планировщик миссии: точки по порядку облёта, у каждой своя высота и скорость. Аппараты из `groups` рисуются поверх маршрута, как в режиме «Объекты».
73
74
 
74
- 1. **«Изменить точки»** включает правку. Клик по карте добавляет точку в конец маршрута. Новая точка берёт высоту и скорость предыдущей, первая получает 30 м и 8 м/с. Esc или двойной клик заканчивают правку.
75
+ 1. **«Редактор точек»** на карте включает правку. Клик по карте добавляет точку в конец маршрута. Новая точка берёт высоту и скорость предыдущей, первая получает 30 м и 8 м/с. Esc или двойной клик заканчивают правку.
75
76
  2. В режиме правки точки можно перетаскивать. Потяните за середину отрезка, чтобы вставить точку между соседними. Правый клик удаляет точку. Без правки на карте видны только номера точек, их не сдвинуть случайно. Точку взлёта `H` можно перетаскивать всегда.
76
77
  3. Блок **«Итог»**: сверху расчёт (число точек, длина пути, время, наибольшая высота), ниже список точек с высотой, скоростью, расстоянием от предыдущей точки и удалением; порядок меняется перетаскиванием строки за ручку, как в списках панелей свойств, под ним флажок возврата с посадкой (RTL). Клик по номеру показывает точку на карте.
77
78
 
@@ -93,18 +94,21 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
93
94
 
94
95
  `mode="agro"` превращает карту в генератор миссии для агродрона. Оператор обводит на карте одно или несколько полей и запретные зоны, задаёт угол проходов и ширину захвата. Карта строит траекторию, которая покрывает все поля, обходит зоны и выключает распылитель на разворотах и переходах. Готовую миссию можно скачать, скопировать или отправить на борт в формате QGC WPL 110 для автопилота ArduPilot.
95
96
 
96
- Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются два демо-поля под Минском: в большом три запретные зоны, в малом вырез и дерево.
97
+ Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое с рощей, опорой ЛЭП и постройкой, среднее сложной формы с выступами, крюком и прудом, малое с вырезом и деревом. Между полями стоят зона-«галочка» и большая зона под средним полем, она задевает все три поля; перелёты их обходят. Ширина захвата по умолчанию 5 м, отступ от границ 2 м. Ширина захвата по умолчанию 5 м, отступ от границ 2 м.
97
98
 
98
99
  ### Как пользоваться
99
100
 
100
- 1. **«Нарисовать поле»**: кликами по карте обведите участок. Контур замыкается кликом по первой точке, двойным кликом, Enter или кнопкой «Замкнуть». Backspace убирает последнюю точку, Esc сбрасывает начатый контур. Когда поле есть, кнопка называется **«Поля»**: в этом режиме клики по карте обводят ещё одно поле.
101
- 2. **«Запретные зоны»**: обведите места, где нельзя лететь и распылять: деревья, опоры ЛЭП, постройки, водоёмы. Зон может быть сколько угодно.
102
- 3. **Точка взлёта** (синяя) видна всегда, её можно перетащить в любой момент.
103
- 4. В блоке **«Маршрут и внесение»** задайте ширину захвата, отступ от границ, высоту, скорость, норму внесения и объём бака. У каждого поля свой угол проходов: новое поле сразу получает лучший угол, «Авторасчёт» пересчитывает углы всех полей приближениями по полному расчёту траектории: шаг 5° по всем направлениям и вдоль сторон поля, затем 1° около трёх лучших, затем 0,1° около победителя; лучший угол даёт наименьшее время облёта с учётом разворотов. Пока идёт расчёт, на карте видно колесо ожидания «Расчёт оптимального маршрута» с полосой и процентом (на большом поле это несколько секунд), а ползунок «Угол для всех полей» ставит всем один угол, например чтобы проходы шли вдоль рядов посадки. Угол отдельного поля правится в его строке в блоке «Итог».
101
+ 1. **«Редактор полей»** на карте: кликами по карте обведите участок. Контур замыкается кликом по первой точке, двойным кликом, Enter или кнопкой «Замкнуть». Backspace убирает последнюю точку, Esc сбрасывает начатый контур. Следующий контур станет ещё одним полем.
102
+ 2. **«Редактор запретных зон»** на карте: обведите места, где нельзя лететь и распылять: деревья, опоры ЛЭП, постройки, водоёмы. Зон может быть сколько угодно. Зона может лежать внутри поля, выходить за его край или стоять вне полей. Из проходов вырезается только часть зоны внутри поля, и площадь поля уменьшается только на эту часть. Зона относится к каждому полю, которое задевает.
103
+
104
+ **Важно.** Перелёты от точки взлёта к полю, между полями и домой обходят все зоны с отступом, в том числе стоящие вне полей. Опоры ЛЭП, вышки, деревья и постройки на пути между полями обязательно обводите запретными зонами. Иначе аппарат полетит напрямую и может в них врезаться.
105
+
106
+ 3. **Точка взлёта** (цвета виджета) видна всегда, её можно перетащить в любой момент.
107
+ 4. В блоке **«Маршрут и внесение»** задайте ширину захвата, отступ от границ, высоту, скорость, норму внесения и объём бака. У каждого поля свой угол проходов: новое поле сразу получает лучший угол, «Авторасчёт» пересчитывает углы всех полей приближениями по полному расчёту траектории: шаг 5° по всем направлениям и вдоль сторон поля, затем 1° около трёх лучших, затем 0,1° около победителя; лучший угол даёт наименьшую цену: время облёта плюс около 4 с на разворот плюс штраф за недолив, где метр недолитого прохода стоит как три метра полёта. Пока идёт расчёт, на карте видно колесо ожидания «Расчёт оптимального маршрута» с полосой и процентом (на большом поле это несколько секунд), а ползунок «Угол для всех полей» ставит всем один угол, например чтобы проходы шли вдоль рядов посадки. Угол отдельного поля правится в его строке в блоке «Итог». Кнопка «i» справа от «Авторасчёта» открывает подробное описание алгоритма: проходы, ячейки, порядок облёта, штрафы и поиск угла приближениями. Текст лежит в словаре под ключом `widget.mission.autoInfo`.
104
108
  5. Блок **«Итог»** показывает контуры, площадь к обработке, покрытие, длину пути, время, расход раствора и число дозаправок.
105
109
  6. **«Скачать»**, **«Копировать»** или **«Отправить»** миссию.
106
110
 
107
- Точки контуров видны и перетаскиваются, только пока нажата кнопка «Поля» или «Запретные зоны». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
111
+ Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
108
112
 
109
113
  ### Как строится траектория
110
114
 
@@ -170,7 +174,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
170
174
  | `onSend` | `(mission, plan) => void \| Promise` | | Отправка миссии, `mission` - текст QGC WPL 110 |
171
175
  | `label.name` | `string` | | Заголовок. Пусто - «Карта» на языке пользователя |
172
176
  | `wrapperClass` | `string` | `""` | Классы обёртки |
173
- | `componentClass` | `string` | `""` | Мастер-цвет `bg-*` шапки |
177
+ | `componentClass` | `string` | `""` | Мастер-цвет `bg-*` всего виджета |
174
178
  | `collapsed` | `boolean` | `false` | Свёрнут ли |
175
179
  | `persistKey` | `string` | | Ключ для запоминания свёрнутого состояния |
176
180
 
@@ -328,7 +328,7 @@
328
328
  <div class="flex flex-col items-center justify-center leading-none">
329
329
  <div
330
330
  class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
331
- {isFresh(board) ? 'text-(--green-color)' : 'text-(--red-color)'}"
331
+ {isFresh(board) ? 'text-(--green-text)' : 'text-(--red-text)'}"
332
332
  style="rotate: {board.heading}deg;"
333
333
  >
334
334
  {#if markerIcon}
@@ -366,29 +366,29 @@
366
366
  <div class="flex items-center gap-1.5">
367
367
  <span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
368
368
  <h6 class="text-sm leading-none font-bold">{board.name}</h6>
369
- {#if board.name !== board.group}<span class="text-[0.6rem] font-semibold uppercase opacity-60">{board.group}</span>{/if}
369
+ {#if board.name !== board.group}<span class="text-[11px] font-semibold uppercase text-(--muted-color)">{board.group}</span>{/if}
370
370
  </div>
371
371
 
372
372
  <div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
373
373
  <div>
374
- <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
374
+ <p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Lat / Lon</p>
375
375
  <p class="font-medium">{board.lat.toFixed(6)}, {board.lon.toFixed(6)}</p>
376
376
  </div>
377
377
  <div>
378
- <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
378
+ <p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Alt</p>
379
379
  <p class="font-medium">{board.alt.toFixed(1)} m</p>
380
380
  </div>
381
381
  <div>
382
- <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
382
+ <p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Heading</p>
383
383
  <p class="font-medium">{board.heading.toFixed(1)}°</p>
384
384
  </div>
385
385
  <div>
386
- <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
386
+ <p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Satellites</p>
387
387
  <p class="font-medium">{board.sat}</p>
388
388
  </div>
389
389
  </div>
390
390
 
391
- {#if board.sn}<p class="text-center font-mono text-[0.6rem] tracking-wide uppercase">{board.sn}</p>{/if}
391
+ {#if board.sn}<p class="text-center font-mono text-[11px] tracking-wide uppercase">{board.sn}</p>{/if}
392
392
 
393
393
  <div class="grid grid-cols-[1fr_auto_1fr] items-center border-t border-(--hairline-color) pt-1">
394
394
  <div class="relative flex items-center justify-self-start">
@@ -415,7 +415,7 @@
415
415
 
416
416
  <button
417
417
  class="flex cursor-pointer items-center gap-1 rounded-lg border px-2 py-0.5 text-xs font-semibold transition {isFollowed
418
- ? 'border-(--accent-color) bg-(--accent-color) text-white'
418
+ ? 'border-(--accent-color) bg-(--accent-color) text-(--on-accent)'
419
419
  : 'border-(--hairline-color) bg-transparent opacity-80 hover:opacity-100'}"
420
420
  aria-pressed={isFollowed}
421
421
  onclick={() => toggleFollow(board.group)}
@@ -431,7 +431,7 @@
431
431
  </button>
432
432
 
433
433
  <button
434
- class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
434
+ class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-text) transition hover:opacity-80"
435
435
  aria-label="Hide"
436
436
  title="Hide"
437
437
  onclick={() => hideBoard(board.group)}
@@ -503,10 +503,10 @@
503
503
  />
504
504
  <span
505
505
  class="text-[12px] font-semibold tabular-nums {freshCount === 0
506
- ? 'text-(--red-color)'
506
+ ? 'text-(--red-text)'
507
507
  : freshCount < groupList.length
508
508
  ? 'text-(--muted-color)'
509
- : 'text-(--green-color)'}"
509
+ : 'text-(--green-text)'}"
510
510
  use:hint={{ text: $T("widget.map.hint.boards"), enabled: $hints }}
511
511
  >
512
512
  {freshCount} / {groupList.length}
@@ -0,0 +1,41 @@
1
+ <!-- $lib/WidgetMap/mission/HelpText.svelte - справка из простого текста: строки "## " - заголовки, "- " - пункты списка, остальное - абзацы, **жирный** внутри строки -->
2
+ <script lang="ts">
3
+ let { text }: { text: string } = $props()
4
+
5
+ type Block = { kind: "h"; text: string } | { kind: "p"; text: string } | { kind: "ul"; items: string[] }
6
+
7
+ let blocks = $derived.by(() => {
8
+ const out: Block[] = []
9
+ for (const raw of text.split("\n")) {
10
+ const line = raw.trim()
11
+ if (!line) continue
12
+ if (line.startsWith("## ")) out.push({ kind: "h", text: line.slice(3) })
13
+ else if (line.startsWith("- ")) {
14
+ const last = out[out.length - 1]
15
+ if (last?.kind === "ul") last.items.push(line.slice(2))
16
+ else out.push({ kind: "ul", items: [line.slice(2)] })
17
+ } else out.push({ kind: "p", text: line })
18
+ }
19
+ return out
20
+ })
21
+ </script>
22
+
23
+ {#snippet inline(line: string)}
24
+ {#each line.split("**") as part, i}{#if i % 2}<strong class="font-semibold">{part}</strong>{:else}{part}{/if}{/each}
25
+ {/snippet}
26
+
27
+ <div class="flex flex-col gap-1.5 text-[14px] leading-snug text-(--font-color)">
28
+ {#each blocks as block}
29
+ {#if block.kind === "h"}
30
+ <h3 class="m-0 mt-2.5 text-[15px] font-semibold">{@render inline(block.text)}</h3>
31
+ {:else if block.kind === "ul"}
32
+ <ul class="m-0 flex list-none flex-col gap-0.5 p-0">
33
+ {#each block.items as item}
34
+ <li class="relative m-0 pl-4 before:absolute before:left-0 before:text-(--accent-color) before:content-['•']">{@render inline(item)}</li>
35
+ {/each}
36
+ </ul>
37
+ {:else}
38
+ <p class="m-0">{@render inline(block.text)}</p>
39
+ {/if}
40
+ {/each}
41
+ </div>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ text: string;
3
+ };
4
+ declare const HelpText: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type HelpText = ReturnType<typeof HelpText>;
6
+ export default HelpText;