poe-svelte-ui-lib 1.35.0 → 1.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,24 @@ npm install poe-svelte-ui-lib
22
22
  @source "../node_modules/poe-svelte-ui-lib/dist";
23
23
  ```
24
24
 
25
+ **Глобальные стили тегов — только в `@layer base`.** Компоненты задают размеры текста и отступы Tailwind-утилитами (например `text-[12px]` у подписи `<h5>`). Правило вне слоя (`h5 { font-size: 1rem }`, `label { … }`, `p { … }`) сильнее любой утилиты, и в приложении подписи станут крупнее задуманного и перестанут помещаться. Поэтому типографику тегов оборачивайте:
26
+
27
+ ```css
28
+ @layer base {
29
+ h5 {
30
+ font-size: 1rem;
31
+ font-weight: 600;
32
+ }
33
+ label {
34
+ font-size: 0.8rem;
35
+ }
36
+ }
37
+ ```
38
+
39
+ Классы ролей (`.bg-red`, `.bg-max`, …) и темы (`.light`/`.dark`) остаются вне слоя — они как раз должны перебивать утилиты.
40
+
41
+ Необязательный токен `--table-header-color` — фон шапки `Table` (по умолчанию `--container-color`); смарт-виджеты задают его сами из мастер-цвета.
42
+
25
43
  Справочник по регулярным выражениям (`docs/info/RegExp.md`) и протокол обмена данными с устройством (`docs/info/ExchangeRules.md`) — вспомогательные материалы для тех, кто настраивает валидацию полей и работу с `DeviceStore`.
26
44
 
27
45
  ## Использование
@@ -39,6 +57,12 @@ npm install poe-svelte-ui-lib
39
57
 
40
58
  Полный список компонентов и типов — в `src/lib/index.ts`. У каждого компонента есть README рядом с исходником (`src/lib/<Component>/README.md`, для `Modal`/`Dragging` — `src/lib/<Component>.README.md`) с описанием пропсов, примерами и внутренним устройством; живая витрина всех компонентов — `npm run dev` → `/components/all`.
41
59
 
60
+ **Витрина повторяет конструктор DevCloud:** слева компонент и код (прижат к низу), справа панель свойств шириной `24rem` с тем же оформлением, что в облаке, — переполнение подписей видно сразу. Кнопка (i) в шапке панели открывает описание компонента.
61
+
62
+ **Описания компонентов (i)** — ключи `component.info.<Тип>` в `src/lib/locales/translations.ts` (ru/en/zh, Markdown). Один источник для витрины и конструктора DevCloud: облако показывает кнопку (i) у любого компонента, для которого есть описание. Добавляя компонент, допишите и описание: для чего он, какую переменную устройства показывает, что и когда отправляет, главные настройки и ограничения.
63
+
64
+ **Иконки вариантов панелей свойств** — `src/lib/libIcons/propsIcons.ts` (контурные SVG 24×24 строками), подставляются в `IOption.icon` в `options.ts`.
65
+
42
66
  ## Разработка
43
67
 
44
68
  ```bash
@@ -42,12 +42,13 @@
42
42
  <h5 class={`${label.class}`}>{label.name}</h5>
43
43
  {/if}
44
44
  <!-- Карусель -->
45
- <div class="relative w-full overflow-hidden rounded-2xl bg-(--back-color)/50">
45
+ <!-- По design-handoff (раздел 9): сплошной фон, стрелки 32 px на поверхности с линией и тенью уровня 2 -->
46
+ <div class="relative w-full overflow-hidden rounded-2xl bg-(--container-color)">
46
47
  <!-- Кнопки навигации -->
47
48
  {#if !isAtStart}
48
49
  <button
49
50
  transition:slide={{ axis: "x" }}
50
- class="absolute z-10 size-10 -translate-y-1/2 cursor-pointer top-1/2 left-1 p-2 rounded-full bg-(--field-color) [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full shadow-[0_0_6px_var(--shadow-color)] rotate-180"
51
+ class="absolute z-10 size-[32px] -translate-y-1/2 cursor-pointer top-1/2 left-1 p-1.5 rounded-full border border-(--hairline-color) bg-(--back-color) text-(--muted-color) transition-colors duration-150 hover:text-(--font-color) [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full shadow-(--elevation-2) rotate-180"
51
52
  onclick={() => scrollCarousel(-scrollValue)}
52
53
  >
53
54
  <ArrowIcon />
@@ -56,7 +57,7 @@
56
57
  {#if !isAtEnd}
57
58
  <button
58
59
  transition:slide={{ axis: "x" }}
59
- class="absolute z-10 size-10 -translate-y-1/2 cursor-pointer top-1/2 right-1 p-2 rounded-full bg-(--field-color) [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full shadow-[0_0_6px_var(--shadow-color)]"
60
+ class="absolute z-10 size-[32px] -translate-y-1/2 cursor-pointer top-1/2 right-1 p-1.5 rounded-full border border-(--hairline-color) bg-(--back-color) text-(--muted-color) transition-colors duration-150 hover:text-(--font-color) [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full shadow-(--elevation-2)"
60
61
  onclick={() => scrollCarousel(scrollValue)}
61
62
  >
62
63
  <ArrowIcon />
@@ -137,7 +137,7 @@
137
137
  <div class="flex w-full flex-col gap-2">
138
138
  <!-- Выбор цвета -->
139
139
  <div
140
- class="hue-slider relative h-7 w-full cursor-pointer overflow-hidden rounded-2xl shadow-sm transition duration-200 hover:shadow-md"
140
+ class="hue-slider relative h-7 w-full cursor-pointer overflow-hidden rounded-[10px] border border-(--hairline-color)"
141
141
  role="slider"
142
142
  aria-valuenow={null}
143
143
  tabindex={null}
@@ -152,17 +152,15 @@
152
152
  ></div>
153
153
  {#if mode === "hsv"}
154
154
  <div
155
- class="pointer-events-none absolute top-1/2 h-7 w-1 -translate-x-1/2 -translate-y-1/2 rounded-2xl border-2 border-white"
156
- style={`left: ${huePointer}%; background: rgb(${hsvToRgb(hsv.h, 100, 100).join(",")})`}
155
+ class="pointer-events-none absolute inset-y-0 w-[2px] -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.25),0_1px_3px_rgb(0_0_0/0.4)]"
156
+ style={`left: ${huePointer}%`}
157
157
  ></div>
158
158
  {/if}
159
159
  </div>
160
160
 
161
161
  <!-- Яркость цвета -->
162
162
  <div
163
- class="brightness-slider relative h-4 w-full cursor-pointer overflow-hidden rounded-2xl {mode === 'hsv'
164
- ? 'shadow-sm transition duration-200 hover:shadow-md'
165
- : ''}"
163
+ class="brightness-slider relative h-4 w-full cursor-pointer overflow-hidden rounded-[10px] border border-(--hairline-color)"
166
164
  role="slider"
167
165
  aria-valuenow={null}
168
166
  tabindex={null}
@@ -172,15 +170,15 @@
172
170
  <div class="absolute inset-0" style={`background: linear-gradient(to right, rgb(0,0,0), rgb(${hsvToRgb(hsv.h, hsv.s, 100).join(",")}))`}></div>
173
171
 
174
172
  <div
175
- class="pointer-events-none absolute top-1/2 h-7 w-1 -translate-x-1/2 -translate-y-1/2 rounded-2xl border-2 border-white"
176
- style={`left: ${brightnessPointer}%; background: rgb(${hsvToRgb(hsv.h, hsv.s, hsv.v).join(",")})`}
173
+ class="pointer-events-none absolute inset-y-0 w-[2px] -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.25),0_1px_3px_rgb(0_0_0/0.4)]"
174
+ style={`left: ${brightnessPointer}%`}
177
175
  ></div>
178
176
  {/if}
179
177
  </div>
180
178
 
181
179
  <!-- Яркость белого цвета -->
182
180
  <div
183
- class="white-slider relative mt-4 h-4 w-full cursor-pointer overflow-hidden rounded-2xl shadow-sm transition duration-200 hover:shadow-md"
181
+ class="white-slider relative mt-4 h-4 w-full cursor-pointer overflow-hidden rounded-[10px] border border-(--hairline-color)"
184
182
  role="slider"
185
183
  aria-valuenow={null}
186
184
  tabindex={null}
@@ -189,21 +187,21 @@
189
187
  <div class="absolute inset-0 bg-linear-to-r from-black to-white"></div>
190
188
  {#if mode === "white"}
191
189
  <div
192
- class="pointer-events-none absolute top-1/2 h-7 w-1 -translate-x-1/2 -translate-y-1/2 rounded-2xl border-2 border-white"
193
- style={`left: ${whiteValue}%; background: rgb(${[255, 255, 255].map((c) => Math.round((whiteValue / 100) * c)).join(",")})`}
190
+ class="pointer-events-none absolute inset-y-0 w-[2px] -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.25),0_1px_3px_rgb(0_0_0/0.4)]"
191
+ style={`left: ${whiteValue}%`}
194
192
  ></div>
195
193
  {/if}
196
194
  </div>
197
195
  </div>
198
196
  {/if}
199
197
 
200
- <!-- Превью цвета -->
198
+ <!-- Превью цвета: квадрат 48 с радиусом 10 (design-handoff, раздел 9) -->
201
199
  <div class="flex flex-col items-center m-auto" style="width: 7rem;">
202
200
  <div
203
- class={`flex size-15 flex-col justify-center gap-1 rounded-full px-2 font-mono text-sm shadow-sm transition duration-200 select-none ${textColor()}`}
201
+ class={`size-[48px] rounded-[10px] border border-(--hairline-color) transition-colors duration-200 select-none ${textColor()}`}
204
202
  style={`background: rgb(${previewBaseColor().join(",")})`}
205
203
  ></div>
206
- <div class="w-full text-center font-semibold">{hex()}</div>
204
+ <div class="mt-1 w-full text-center font-mono text-[12px] font-semibold text-(--muted-color)">{hex()}</div>
207
205
  </div>
208
206
  </div>
209
207
  </div>
@@ -53,10 +53,8 @@
53
53
  }
54
54
  </script>
55
55
 
56
- <!-- Не входил в исходный design-handoff (пропущен вместе с остальными точками,
57
- которые авторы документа не осматривали) — сейчас приведён к общей палитре:
58
- радиус 10/высота 36 как у Input/Select, сегмент "Выбрать файл" залит акцентом
59
- с текстом --on-accent (было --font-color по тёмно-синему — нечитаемо). -->
56
+ <!-- По design-handoff (раздел 9): зона сброса — пунктир 1 px с радиусом 12 вместо залитой кнопки,
57
+ высота как у Input; «Выбрать файл» — акцентный текст, имя файла — обычным цветом, пока не выбрано — бледным. -->
60
58
  <div class={twMerge(`flex flex-col items-center gap-1`, wrapperClass)}>
61
59
  {#if label.name}
62
60
  <span class={twMerge(`w-full text-[12px] font-semibold text-(--muted-color)`, label.class)}>{label.name}</span>
@@ -65,8 +63,8 @@
65
63
  {#if type === "image"}
66
64
  <div class="relative">
67
65
  <button
68
- class="flex items-center justify-center overflow-hidden {imageSize.form === 'circle' ? 'rounded-full' : 'rounded-2xl'}
69
- border border-(--hairline-color) bg-(--container-color) transition-colors duration-150
66
+ class="flex items-center justify-center overflow-hidden {imageSize.form === 'circle' ? 'rounded-full' : 'rounded-[12px]'}
67
+ border border-dashed border-(--border-color) bg-(--container-color) transition-colors duration-150
70
68
  {disabled ? 'cursor-not-allowed opacity-45' : 'cursor-pointer hover:bg-(--border-color)'}"
71
69
  style={`height: ${imageSize.height}; width: ${imageSize.width}`}
72
70
  onclick={triggerFileInput}
@@ -98,13 +96,16 @@
98
96
  onchange={handleFileChange}
99
97
  />
100
98
  <div
101
- class="flex h-8 w-full overflow-hidden rounded-[10px] border border-(--border-color) text-[14px] transition-colors duration-150
102
- {disabled ? 'opacity-45' : 'hover:border-(--faint-color)'}"
99
+ class={twMerge(
100
+ `flex h-8 w-full items-center gap-3 overflow-hidden rounded-[12px] border border-dashed border-(--border-color) bg-(--field-color) px-3 text-[14px]
101
+ transition-colors duration-150 ${disabled ? "opacity-45" : "hover:border-(--accent-color)"}`,
102
+ componentClass,
103
+ )}
103
104
  >
104
- <div class="flex shrink-0 items-center justify-center bg-(--accent-color) px-3 font-semibold whitespace-nowrap text-(--on-accent)">
105
+ <span class="shrink-0 font-semibold whitespace-nowrap text-(--accent-color)">
105
106
  {$T("constructor.props.file.select")}
106
- </div>
107
- <div class={twMerge("flex min-w-0 flex-1 items-center justify-start truncate bg-(--field-color) px-3 text-(--font-color)", componentClass)}>
107
+ </span>
108
+ <div class="flex min-w-0 flex-1 items-center justify-start truncate {fileName ? 'text-(--font-color)' : 'text-(--faint-color)'}">
108
109
  <p class="w-full truncate">{fileName || $T("constructor.props.file.notselected")}</p>
109
110
  </div>
110
111
  </div>
@@ -6,24 +6,24 @@
6
6
 
7
7
  ## Пропсы
8
8
 
9
- | Название | Тип | По умолчанию | Описание |
10
- | ------------------- | -------------------------------------------- | --------------------- | ------------------------------------------------ |
11
- | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
- | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
9
+ | Название | Тип | По умолчанию | Описание |
10
+ | ------------------- | -------------------------------------------- | --------------------- | ---------------------------------------------------------------- |
11
+ | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
+ | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
13
13
  | `componentClass` | `string` | `""` | Дополнительные CSS-классы для элемента с именем выбранного файла |
14
- | `label` | `object` | `-` | Настройки заголовка компонента |
15
- | `label.name` | `string` | `""` | Текст заголовка компонента |
16
- | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
17
- | `type` | `"file" \| "image"` | `"file"` | Тип компонента (файл или изображение) |
18
- | `accept` | `string` | `"*/*"` | MIME-типы файлов, которые можно выбрать |
19
- | `imageSize` | `object` | `-` | Настройки отображения изображения |
20
- | `imageSize.height` | `string` | `"10rem"` | Высота области отображения изображения |
21
- | `imageSize.width` | `string` | `"10rem"` | Ширина области отображения изображения |
22
- | `imageSize.fitMode` | `"cover" \| "contain"` | `"cover"` | Режим масштабирования изображения |
23
- | `imageSize.form` | `"square" \| "circle"` | `"square"` | Форма области отображения изображения |
24
- | `disabled` | `boolean` | `false` | Состояние отключения компонента |
25
- | `currentImage` | `string \| null` | `""` | Текущее изображение (для связывания) |
26
- | `onChange` | `(event: Event, file: File \| null) => void` | `() => {}` | Обработчик изменения файла |
14
+ | `label` | `object` | `-` | Настройки заголовка компонента |
15
+ | `label.name` | `string` | `""` | Текст заголовка компонента |
16
+ | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
17
+ | `type` | `"file" \| "image"` | `"file"` | Тип компонента (файл или изображение) |
18
+ | `accept` | `string` | `"*/*"` | MIME-типы файлов, которые можно выбрать |
19
+ | `imageSize` | `object` | `-` | Настройки отображения изображения |
20
+ | `imageSize.height` | `string` | `"10rem"` | Высота области отображения изображения |
21
+ | `imageSize.width` | `string` | `"10rem"` | Ширина области отображения изображения |
22
+ | `imageSize.fitMode` | `"cover" \| "contain"` | `"cover"` | Режим масштабирования изображения |
23
+ | `imageSize.form` | `"square" \| "circle"` | `"square"` | Форма области отображения изображения |
24
+ | `disabled` | `boolean` | `false` | Состояние отключения компонента |
25
+ | `currentImage` | `string \| null` | `""` | Текущее изображение (для связывания) |
26
+ | `onChange` | `(event: Event, file: File \| null) => void` | `() => {}` | Обработчик изменения файла |
27
27
 
28
28
  ## События
29
29
 
@@ -390,12 +390,13 @@
390
390
 
391
391
  <div class="flex w-full flex-row gap-4">
392
392
  <!-- График -->
393
- <div bind:this={container} class="relative h-64 grow overflow-hidden rounded-xl border border-gray-200 shadow-sm">
393
+ <!-- design-handoff, раздел 9: рамка --hairline-color без тени, тултип в оформлении общего Tooltip.svelte -->
394
+ <div bind:this={container} class="relative h-64 grow overflow-hidden rounded-[14px] border border-(--hairline-color)">
394
395
  <canvas class="h-full w-full bg-(--back-color)" bind:this={canvas} onmousemove={handleMouseMove} onmouseleave={handleMouseLeave}></canvas>
395
396
 
396
397
  {#if tooltip.visible}
397
398
  <div
398
- class="pointer-events-none absolute z-10 rounded-lg border border-(--border-color) bg-(--back-color) px-2 py-1 text-xs whitespace-nowrap shadow-lg"
399
+ class="pointer-events-none absolute z-10 rounded-lg border border-(--hairline-color) bg-(--back-color) px-2.5 py-1.5 text-[12px] whitespace-nowrap shadow-(--elevation-2)"
399
400
  style="left: {Math.min(tooltip.x + 12, width - 150)}px; top: {Math.min(tooltip.y + 12, height - 16 - tooltip.items.length * 16)}px;"
400
401
  >
401
402
  <div class="mb-1 font-semibold">{formatTooltipTime(tooltip.time)}</div>
@@ -130,7 +130,7 @@
130
130
 
131
131
  {#if !readonly}
132
132
  <div class="flex w-1/2 items-center justify-center">
133
- <div class="relative z-10 flex size-40 min-h-40 min-w-40 items-center justify-center rounded-full bg-(--bg-color) shadow-[0_0_20px_rgb(0_0_0_/0.25)]">
133
+ <div class="relative z-10 flex size-40 min-h-40 min-w-40 items-center justify-center rounded-full bg-(--bg-color) shadow-(--elevation-2)">
134
134
  <!-- Основные кнопки (оси pitch и yaw) -->
135
135
  <div class="absolute h-full w-full overflow-hidden rounded-full">
136
136
  {#each directions as direction, index}
@@ -176,7 +176,7 @@
176
176
  {/each}
177
177
  </div>
178
178
  <!-- Кнопка по центру -->
179
- <div class="z-20 flex size-20 items-center justify-center rounded-full bg-(--bg-color) shadow-[0_0_15px_rgb(0_0_0_/0.25)]">
179
+ <div class="z-20 flex size-20 items-center justify-center rounded-full bg-(--bg-color) shadow-(--elevation-2)">
180
180
  <button
181
181
  class="flex size-18 cursor-pointer rounded-full p-2 origin-center active:scale-95
182
182
  [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full"
@@ -202,11 +202,11 @@
202
202
  </div>
203
203
  </div>
204
204
 
205
- <!-- Копка 0 -->
205
+ <!-- Кнопка 0 — 32 px по общему правилу кнопок-иконок (design-handoff, раздел 9) -->
206
206
  <div class="absolute {label.name ? 'top-4' : 'top-0'} w-55 flex items-center justify-end">
207
207
  <button
208
- class="flex shadow-sm hover:shadow-md transition duration-200 justify-center cursor-pointer items-center bg-(--back-color) rounded-full size-10 p-0.5
209
- font-semibold text-2xl [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full"
208
+ class="flex size-[32px] cursor-pointer items-center justify-center rounded-full border border-(--hairline-color) bg-(--back-color) p-0.5 text-[15px] font-semibold
209
+ transition-colors duration-150 hover:border-(--border-color) [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full"
210
210
  style="background: color-mix(in srgb, var(--bg-color), var(--back-color) 70%);"
211
211
  title=""
212
212
  onclick={() => (value = [0, 0, 0, 0])}
@@ -218,7 +218,7 @@
218
218
  <!-- Боковые кнопки (ось roll) -->
219
219
  {#if axes[2].name !== ""}
220
220
  <div
221
- class="absolute flex h-15 w-65 items-center justify-between rounded-full shadow-[0_0_15px_rgb(0_0_0_/0.25)]"
221
+ class="absolute flex h-15 w-65 items-center justify-between rounded-full shadow-(--elevation-2)"
222
222
  style="background: color-mix(in srgb, var(--bg-color), var(--shadow-color) 10%)"
223
223
  >
224
224
  <button
@@ -234,7 +234,7 @@
234
234
  onUpdate(value)
235
235
  }}
236
236
  onmouseenter={(e) => (e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--bg-color), var(--shadow-color) 30%)")}
237
- onmouseleave={(e) => (e.currentTarget.style.backgroundColor = "background: color-mix(in srgb, var(--bg-color), var(--shadow-color) 10%)")}
237
+ onmouseleave={(e) => (e.currentTarget.style.backgroundColor = "")}
238
238
  >
239
239
  <svg xmlns="http://www.w3.org/2000/svg" class="segment-icon rotate-270" width="24" height="24" viewBox="0 0 24 24"
240
240
  ><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"
@@ -258,7 +258,7 @@
258
258
  onUpdate(value)
259
259
  }}
260
260
  onmouseenter={(e) => (e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--bg-color), var(--shadow-color) 30%)")}
261
- onmouseleave={(e) => (e.currentTarget.style.backgroundColor = "vabackground: color-mix(in srgb, var(--bg-color), var(--shadow-color) 10%)")}
261
+ onmouseleave={(e) => (e.currentTarget.style.backgroundColor = "")}
262
262
  >
263
263
  <svg xmlns="http://www.w3.org/2000/svg" class="segment-icon rotate-90" width="24" height="24" viewBox="0 0 24 24"
264
264
  ><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"
@@ -164,7 +164,7 @@
164
164
  <h5 class={twMerge(` w-full px-4 text-center`, label.class)}>{label.name}</h5>
165
165
  {/if}
166
166
  <MapLibre
167
- class={twMerge("h-[calc(100%-2rem)] min-h-50 overflow-hidden rounded-xl shadow-sm transition duration-200 hover:shadow-md", roleRingClass)}
167
+ class={twMerge("h-[calc(100%-2rem)] min-h-50 overflow-hidden rounded-[14px] border border-(--hairline-color)", roleRingClass)}
168
168
  style={isDarkMode ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
169
169
  zoom={1.5}
170
170
  center={{ lat: 30, lng: 0 }}
@@ -222,7 +222,7 @@
222
222
  <div class="flex flex-col items-center justify-center leading-none">
223
223
  <div
224
224
  class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
225
- {device.isFresh ? 'text-green-500' : 'text-red-500'}"
225
+ {device.isFresh ? 'text-(--green-color)' : 'text-(--red-color)'}"
226
226
  style="rotate: {device.NavHeading}deg;"
227
227
  >
228
228
  {#if markerIcon}
@@ -251,16 +251,14 @@
251
251
  </div>
252
252
  {/snippet}
253
253
 
254
- <Popup closeButton={false} class="map-popup rounded-2xl text-left">
254
+ <Popup closeButton={false} class="map-popup text-left">
255
255
  <div class="flex flex-col gap-1.5">
256
256
  <div class="flex items-center gap-1.5">
257
- <span class="size-2 shrink-0 rounded-full {device.isFresh ? 'bg-green-500' : 'bg-red-500'}"></span>
257
+ <span class="size-2 shrink-0 rounded-full {device.isFresh ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
258
258
  <h6 class="text-sm leading-none font-bold">{device.DevName}</h6>
259
259
  </div>
260
260
 
261
- <div
262
- class={`${twMerge("grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs", roleBgClass)} ${roleBgPaint}`}
263
- >
261
+ <div class={`${twMerge("grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs", roleBgClass)} ${roleBgPaint}`}>
264
262
  <div>
265
263
  <p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
266
264
  <p class="font-medium">{device.NavLat.toFixed(5)}, {device.NavLon.toFixed(5)}</p>
@@ -281,7 +279,7 @@
281
279
 
282
280
  <p class="text-[0.6rem] font-mono text-center tracking-wide uppercase">{device.DevSN}</p>
283
281
 
284
- <div class="flex items-center justify-between border-t border-(--border-color) pt-1">
282
+ <div class="flex items-center justify-between border-t border-(--hairline-color) pt-1">
285
283
  <div class="relative flex items-center">
286
284
  <button
287
285
  class="flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-1 py-0.5 opacity-70 transition hover:opacity-100"
@@ -309,7 +307,7 @@
309
307
  </div>
310
308
 
311
309
  <button
312
- class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-red-500 transition hover:text-red-600"
310
+ class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
313
311
  aria-label="Delete"
314
312
  title="Delete"
315
313
  onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}
@@ -335,9 +333,14 @@
335
333
  </div>
336
334
 
337
335
  <style>
336
+ /* Попап по правилам карточки (design-handoff, раздел 9): радиус 14, линия, тень уровня 2 */
338
337
  :global(.map-popup .maplibregl-popup-content) {
339
338
  background-color: var(--back-color);
340
339
  color: inherit;
340
+ border: 1px solid var(--hairline-color);
341
+ border-radius: 14px;
342
+ box-shadow: var(--elevation-2);
343
+ padding: 12px;
341
344
  }
342
345
 
343
346
  :global(.map-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) {
@@ -6,21 +6,21 @@
6
6
 
7
7
  ## Пропсы
8
8
 
9
- | Название | Тип | По умолчанию | Описание |
10
- | -------------- | --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
11
- | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
- | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
13
- | `componentClass` | `string` | `""` | Дополнительные CSS-классы для самого поля/кнопок выбора |
14
- | `disabled` | `boolean` | `false` | Состояние отключения компонента |
15
- | `label` | `object` | `-` | Настройки заголовка компонента |
16
- | `label.name` | `string` | `""` | Текст заголовка компонента |
17
- | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
18
- | `type` | `"select" \| "buttons" \| "input"` | `"select"` | Тип отображения: `select` — выпадающий список с кнопкой, `buttons` — горизонтальный ряд кнопок, `input` — поле ввода с автодополнением и поиском |
19
- | `multiSelect` | `boolean` | `false` | Режим множественного выбора: работает только с `type: "buttons"`; при `true` позволяет выбирать несколько опций, значение передаётся как массив |
20
- | `listHeight` | `string` | `"16rem"` | Максимальная высота выпадающего списка опций (для типов `select`/`input`) |
21
- | `value` | `IOption<T> \| IOption<T>[] \| undefined` | `undefined` | Текущее выбранное значение(я); поддерживает двустороннее связывание (`$bindable`) |
22
- | `options` | `IOption<T>[]` | `[]` | Массив доступных опций |
23
- | `onUpdate` | `(value: IOption<T> \| IOption<T>[]) => void` | `undefined` | Обработчик изменения значения |
9
+ | Название | Тип | По умолчанию | Описание |
10
+ | ---------------- | --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
11
+ | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
12
+ | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
13
+ | `componentClass` | `string` | `""` | Дополнительные CSS-классы для самого поля/кнопок выбора |
14
+ | `disabled` | `boolean` | `false` | Состояние отключения компонента |
15
+ | `label` | `object` | `-` | Настройки заголовка компонента |
16
+ | `label.name` | `string` | `""` | Текст заголовка компонента |
17
+ | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
18
+ | `type` | `"select" \| "buttons" \| "input"` | `"select"` | Тип отображения: `select` — выпадающий список с кнопкой, `buttons` — горизонтальный ряд кнопок, `input` — поле ввода с автодополнением и поиском |
19
+ | `multiSelect` | `boolean` | `false` | Режим множественного выбора: работает только с `type: "buttons"`; при `true` позволяет выбирать несколько опций, значение передаётся как массив |
20
+ | `listHeight` | `string` | `"16rem"` | Максимальная высота выпадающего списка опций (для типов `select`/`input`) |
21
+ | `value` | `IOption<T> \| IOption<T>[] \| undefined` | `undefined` | Текущее выбранное значение(я); поддерживает двустороннее связывание (`$bindable`) |
22
+ | `options` | `IOption<T>[]` | `[]` | Массив доступных опций |
23
+ | `onUpdate` | `(value: IOption<T> \| IOption<T>[]) => void` | `undefined` | Обработчик изменения значения |
24
24
 
25
25
  > **`bitMode`/`range`**: эти поля объявлены в типе `ISelectProps` и настраиваются в конструкторской панели (`SelectProps.svelte`), но сам `Select.svelte` их не читает и не использует — они не оказывают никакого эффекта на голый `<UI.Select>`. Если нужен битовый выбор в реальном рантайме, интерпретируйте маску на стороне вызывающего кода и передавайте уже готовый `value`/`options`.
26
26
 
@@ -28,13 +28,16 @@
28
28
 
29
29
  #### IOption
30
30
 
31
- | Название | Тип | По умолчанию | Описание |
32
- | ---------- | --------- | ------------ | ------------------------------- |
33
- | `id` | `string` | `-` | Уникальный идентификатор опции |
34
- | `value` | `T` | `undefined` | Значение опции |
35
- | `name` | `string` | `undefined` | Отображаемое имя опции |
36
- | `class` | `string` | `undefined` | CSS-классы для стилизации опции |
37
- | `disabled` | `boolean` | `undefined` | Состояние отключения опции |
31
+ | Название | Тип | По умолчанию | Описание |
32
+ | ---------- | --------- | ------------ | ---------------------------------------------------------------- |
33
+ | `id` | `string` | `-` | Уникальный идентификатор опции |
34
+ | `value` | `T` | `undefined` | Значение опции |
35
+ | `name` | `string` | `undefined` | Отображаемое имя опции |
36
+ | `class` | `string` | `undefined` | CSS-классы для стилизации опции |
37
+ | `disabled` | `boolean` | `undefined` | Состояние отключения опции |
38
+ | `icon` | `string` | `undefined` | Иконка: SVG-разметка или короткая метка (`"S"`, `"XL"`, `"123"`) |
39
+
40
+ В `type="buttons"` вариант с `icon` показывает иконку вместо `name`, а `name` становится всплывающей подсказкой и `aria-label` — так узкие панели свойств не обрезают подписи. В остальных режимах `icon` не используется.
38
41
 
39
42
  ## События
40
43
 
@@ -180,6 +180,11 @@
180
180
  <UI.Table label={{ name: "Devices" }} header={columns} body={rows} onClick={handleButtonClick} footer={`rows: ${rows.length}`} />
181
181
  ```
182
182
 
183
+ ## Шапка
184
+
185
+ - Фон шапки — токен `--table-header-color`, по умолчанию `--container-color`. Внутри смарт-виджета он равен мягкому оттенку мастер-цвета (`widgetAccentStyle`), поэтому шапка перекрашивается вместе с виджетом.
186
+ - Когда у тела появляется полоса прокрутки, шапка сдвигается ровно на её фактическую ширину (`offsetWidth − clientWidth`, пересчёт при изменении строк и размеров) — столбцы шапки и строк совпадают. Для полос поверх содержимого (macOS) сдвиг 0.
187
+
183
188
  ## Внутренняя архитектура
184
189
 
185
190
  ### Реактивность
@@ -149,10 +149,11 @@
149
149
  {#if label.name}
150
150
  <h5 class={twMerge(`w-full px-4 text-center`, label.class)}>{label.name}</h5>
151
151
  {/if}
152
- <div class={twMerge("relative flex flex-1 w-full items-center justify-center rounded-2xl", roleBorderClass)}>
152
+ <!-- design-handoff, раздел 9: фон --container-color, радиус 14, оверлей загрузки без фиксированных отступов -->
153
+ <div class={twMerge("relative flex w-full flex-1 items-center justify-center rounded-[14px]", roleBorderClass)}>
153
154
  {#if isRemote}
154
155
  {#if status !== "live" || !remoteImgSrc}
155
- <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
156
+ <div class="absolute z-10 flex h-full w-full flex-col items-center justify-center gap-4 rounded-[14px] bg-(--container-color) p-4 text-center">
156
157
  {#if status === "error"}
157
158
  <svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
158
159
  <path
@@ -170,14 +171,14 @@
170
171
  {/if}
171
172
  </div>
172
173
  {/if}
173
- <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
174
+ <div class="absolute h-full w-full overflow-hidden rounded-[14px] bg-(--container-color)">
174
175
  {#if remoteImgSrc}
175
- <img src={remoteImgSrc} alt={label.name || ""} class="h-full w-full rounded-2xl object-contain block" />
176
+ <img src={remoteImgSrc} alt={label.name || ""} class="block h-full w-full rounded-[14px] object-contain" />
176
177
  {/if}
177
178
  </div>
178
179
  {:else}
179
180
  {#if loading || error}
180
- <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
181
+ <div class="absolute z-10 flex h-full w-full flex-col items-center justify-center gap-4 rounded-[14px] bg-(--container-color) p-4 text-center">
181
182
  {#if loading}
182
183
  <h2>{$T("constructor.props.camera.loading")}</h2>
183
184
  {:else if error}
@@ -194,8 +195,8 @@
194
195
  {/if}
195
196
  </div>
196
197
  {/if}
197
- <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
198
- <video bind:this={videoElement} class="h-full w-full rounded-2xl object-contain block {!isStreaming || loading ? 'invisible' : 'visible'}"></video>
198
+ <div class="absolute h-full w-full overflow-hidden rounded-[14px] bg-(--container-color)">
199
+ <video bind:this={videoElement} class="block h-full w-full rounded-[14px] object-contain {!isStreaming || loading ? 'invisible' : 'visible'}"></video>
199
200
  </div>
200
201
  {/if}
201
202
  </div>
@@ -157,6 +157,10 @@
157
157
  | `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string; eventHandler?: IUIComponentHandler }>) => void` | `-` | Коллбэк для обновления свойств компонента |
158
158
  | `forConstructor` | `boolean` | `true` | Режим отображения (для конструктора или редактирования) |
159
159
 
160
+ ### Мастер-цвет
161
+
162
+ Пикер «Мастер-цвет» (`WidgetAccentPicker`, общий со смарт-виджетами) пишет роль `bg-*` в `settings.class` и подобранный цвет иконки в `icons.class` — формат сохранённых интерфейсов прежний. Корень виджета получает `widgetAccentStyle(settings.class)`: роль перекрашивает переключатель, слайдер и фокус поля.
163
+
160
164
  ### Особенности конструктора
161
165
 
162
166
  - Предоставляет визуальные элементы управления для всех доступных свойств
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Описание
4
4
 
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки), без собственного ограничения; высота — по содержимому, в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает и шапку таблиц внутри виджета (`--table-header-color`).
6
+
5
7
  Смарт-виджет информации об устройстве. Как и `WidgetWiFi`, не привязывается к одной произвольной
6
8
  переменной устройства — работает с фиксированным набором полей, соответствующих реальной
7
9
  прошивочной конвенции (`main_config.c`, группа `CFG`, ProdFactory-ESP): `DevSN`, `DevID`, `DevFW`,
@@ -14,29 +16,29 @@
14
16
 
15
17
  ## Пропсы
16
18
 
17
- | Название | Тип | По умолчанию | Описание |
18
- | ---------------- | ----------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
19
- | `wrapperClass` | `string` | `""` | CSS-классы для обёртки компонента |
20
- | `componentClass` | `string` | `""` | "Мастер-цвет" виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`) |
21
- | `label` | `{ name?: string }` | `{ name: "Информация об устройстве" }` | Заголовок карточки |
22
- | `value` | `IWidgetDeviceInfoConfig` | внутренний дефолт | Текущая информация об устройстве |
23
- | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (заголовок остаётся); поддерживает двустороннее связывание (`$bindable`) |
24
- | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе состояние сворачивания переживает перезагрузку страницы |
25
- | `onSave` | `(info: IWidgetDeviceInfoConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — редактируемые поля одним пакетом |
26
- | `onRestart` | `() => void` | `undefined` | Срабатывает при нажатии "Перезагрузить" |
19
+ | Название | Тип | По умолчанию | Описание |
20
+ | ---------------- | ----------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `wrapperClass` | `string` | `""` | CSS-классы для обёртки компонента |
22
+ | `componentClass` | `string` | `""` | "Мастер-цвет" виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`) |
23
+ | `label` | `{ name?: string }` | `{ name: "Информация об устройстве" }` | Заголовок карточки |
24
+ | `value` | `IWidgetDeviceInfoConfig` | внутренний дефолт | Текущая информация об устройстве |
25
+ | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (заголовок остаётся); поддерживает двустороннее связывание (`$bindable`) |
26
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе состояние сворачивания переживает перезагрузку страницы |
27
+ | `onSave` | `(info: IWidgetDeviceInfoConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — редактируемые поля одним пакетом |
28
+ | `onRestart` | `() => void` | `undefined` | Срабатывает при нажатии "Перезагрузить" |
27
29
 
28
30
  ### IWidgetDeviceInfoConfig
29
31
 
30
- | Поле | Тип | Только чтение | Описание |
31
- | ---------- | -------- | :-----------: | --------------------------- |
32
- | `DevSN` | `string` | да | Серийный номер устройства |
33
- | `DevID` | `string` | да | ID устройства |
34
- | `DevFW` | `number` | да | Версия ПО |
35
- | `RunCnt` | `number` | да | Счётчик запусков |
36
- | `DevName` | `string` | нет | Имя устройства |
37
- | `HostName` | `string` | нет | Имя хоста |
38
- | `WebUser` | `string` | нет | Имя пользователя Web-панели |
39
- | `WebPsw` | `string` | нет | Пароль Web-панели |
32
+ | Поле | Тип | Только чтение | Описание |
33
+ | ---------- | -------- | :-----------: | ----------------------------- |
34
+ | `DevSN` | `string` | да | Серийный номер устройства |
35
+ | `DevID` | `string` | да | ID устройства |
36
+ | `DevFW` | `number` | да | Версия ПО |
37
+ | `RunCnt` | `number` | да | Счётчик запусков |
38
+ | `DevName` | `string` | нет | Имя устройства |
39
+ | `HostName` | `string` | нет | Имя хоста |
40
+ | `WebUser` | `string` | нет | Имя пользователя Web-панели |
41
+ | `WebPsw` | `string` | нет | Пароль Web-панели |
40
42
  | `StaIP` | `string` | да | Текущий IP-адрес в режиме STA |
41
43
  | `ApIP` | `string` | да | Текущий IP-адрес в режиме AP |
42
44
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Описание
4
4
 
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки), без собственного ограничения; высота — по содержимому, в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает и шапку таблиц внутри виджета (`--table-header-color`).
6
+
5
7
  Смарт-виджет файловой системы устройства: занятость памяти, список файлов с удалением и закачка
6
8
  нового файла с прогрессом. Работает с одним объектом `CFG.FS` ровно в том виде, в каком его отдаёт
7
9
  прошивка (`FS_WriteState`, ProdFactory-ESP), и с одной командой `FS`. Компонент ничего не знает про
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Описание
4
4
 
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки), без собственного ограничения; высота — по содержимому, в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает и шапку таблиц внутри виджета (`--table-header-color`).
6
+
5
7
  Отладочный смарт-виджет памяти и задач FreeRTOS: занятость каждой кучи и минимальный запас стека
6
8
  каждой задачи. Работает с одним объектом `StackInfo` ровно в том виде, в каком его отдаёт
7
9
  `API_GetStackInfo` (ProdFactory-ESP и ProdFactory-STM — контракт общий), и с одним запросом
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Описание
4
4
 
5
+ > **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки), без собственного ограничения; высота — по содержимому, в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает и шапку таблиц внутри виджета (`--table-header-color`).
6
+
5
7
  Смарт-виджет настройки WiFi (режимы STA / AP / STA+AP). В отличие от примитивов конструктора,
6
8
  не привязывается к одной произвольной переменной устройства через Select — работает с
7
9
  фиксированным набором полей, соответствующих реальной прошивочной конвенции (`main_config.c`,
@@ -25,7 +27,7 @@ DeviceStore/WebSocket — только `value`/`onSave`, вся отправка
25
27
  | `allowedModes` | `number[]` | `[1, 2, 3]` | Какие режимы показывать в сегмент-контроле (1=STA, 2=AP, 3=STA+AP) |
26
28
  | `confirmOnAP` | `boolean` | `true` | Показывать предупреждение при сохранении чистого режима AP |
27
29
  | `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (заголовок остаётся); поддерживает двустороннее связывание (`$bindable`) |
28
- | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе сворачивание переживает перезагрузку страницы |
30
+ | `persistKey` | `string` | `undefined` | Ключ для сохранения состояния `collapsed` в `localStorage` — при заданном ключе сворачивание переживает перезагрузку страницы |
29
31
  | `onScan` | `() => Promise<{ id: string; name: string; value: string }[]>` | `undefined` | Обработчик сканирования сетей (в проде — `Header: GET, Argument: APsList`) |
30
32
  | `onSave` | `(config: IWidgetWiFiConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — конфиг уходит одним пакетом |
31
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.35.0",
3
+ "version": "1.35.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {