poe-svelte-ui-lib 1.89.0 → 1.91.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 +3 -1
- package/dist/Accordion/Accordion.svelte +35 -28
- package/dist/Table/Table.svelte +14 -8
- package/dist/Table/xPoeDebugColumns.js +1 -1
- package/dist/WidgetStackInfo/WidgetStackInfo.svelte +3 -5
- package/dist/html.d.ts +2 -0
- package/dist/html.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocols/xPoeDebugView.js +2 -2
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
**Компоненты.** Каждый привязывается к одной переменной устройства: `Input`, `Select`, `Switch`, `Slider`, `Button`, `Joystick`, `ProgressBar`, `Graph`, `PeriodChart`, `Table`, `FileAttach`, `VideoViewer`, `TextField`, `Widget`.
|
|
26
26
|
|
|
27
|
-
**Раскладка.** `Accordion`, `Tabs`, `Divider`, `Carousel`, `Modal`.
|
|
27
|
+
**Раскладка.** `Accordion`, `Tabs`, `Divider`, `Carousel`, `Modal`. У `Accordion` есть необязательный слот `actions` - кнопки справа в заголовке, нажатие на них блок не сворачивает.
|
|
28
28
|
|
|
29
29
|
**Ядро протоколов** (`poe-svelte-ui-lib/protocols`). Кодирование и разбор пакетов для всех каналов связи с устройством. Работает без Svelte, в том числе под Node.
|
|
30
30
|
|
|
@@ -117,6 +117,8 @@ npm install poe-svelte-ui-lib
|
|
|
117
117
|
|
|
118
118
|
Классы ролей (`.bg-red`, `.bg-max` и другие) и классы темы (`.light`, `.dark`) остаются вне слоя. Они как раз должны перебивать утилиты.
|
|
119
119
|
|
|
120
|
+
Текст ячейки `Table` выводится как текст, теги в данных устройства не работают. Если ячейке нужна разметка (цвет, ссылка), у колонки ставят `html: true`, а данные в ней экранируют сами функцией `escapeHtml`. `formatting` возвращает HTML для окна и подсказки и тоже сам экранирует текст.
|
|
121
|
+
|
|
120
122
|
Шапку `Table` можно перекрасить переменной `--table-header-color`. По умолчанию она `--accent-soft`, смарт-виджеты задают её сами от своего цвета.
|
|
121
123
|
|
|
122
124
|
Посмотреть всю тему разом можно на вкладке «Тема» на главной витрины. Там палитра, все компоненты с демо-данными, типографика и Markdown.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
size = { width: 1, height: 1 },
|
|
14
14
|
label = { name: "", class: "text-left", icon: null },
|
|
15
15
|
children,
|
|
16
|
+
actions,
|
|
16
17
|
image = "",
|
|
17
18
|
}: IAccordionProps = $props()
|
|
18
19
|
|
|
@@ -27,35 +28,41 @@
|
|
|
27
28
|
>
|
|
28
29
|
<div class="overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
|
|
29
30
|
<!-- Фон заголовка - мягкий оттенок мастер-цвета (светлый в светлой теме, тёмный в тёмной), текст - мастер-цвет с цветом шрифта 60/40, раскрытое содержимое остаётся на чистом фоне (--back-color). -->
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{#if label?.icon}
|
|
36
|
-
<span
|
|
37
|
-
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"
|
|
38
|
-
>
|
|
39
|
-
{#if typeof label?.icon === "string"}
|
|
40
|
-
{@html label.icon}
|
|
41
|
-
{:else}
|
|
42
|
-
{@const IconComponent = label?.icon}
|
|
43
|
-
<IconComponent />
|
|
44
|
-
{/if}
|
|
45
|
-
</span>
|
|
46
|
-
{/if}
|
|
47
|
-
<span class={twMerge(`min-w-0 truncate text-[14px] font-semibold`, label.class)}>{label?.name}</span>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<svg
|
|
51
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
-
class="h-[14px] w-[14px] shrink-0 text-[color-mix(in_srgb,var(--accent-color)_60%,var(--font-color))] transition-transform duration-200"
|
|
53
|
-
style="transform: rotate({isOpen ? 180 : 0}deg)"
|
|
54
|
-
viewBox="0 0 24 24"
|
|
31
|
+
<!-- Заголовок: кнопка сворачивания на всю ширину и необязательные кнопки справа (actions) - вне неё, чтобы не сворачивать блок -->
|
|
32
|
+
<div class="flex w-full items-center bg-(--accent-soft)">
|
|
33
|
+
<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))]"
|
|
35
|
+
onclick={toggle}
|
|
55
36
|
>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
38
|
+
{#if label?.icon}
|
|
39
|
+
<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"
|
|
41
|
+
>
|
|
42
|
+
{#if typeof label?.icon === "string"}
|
|
43
|
+
{@html label.icon}
|
|
44
|
+
{:else}
|
|
45
|
+
{@const IconComponent = label?.icon}
|
|
46
|
+
<IconComponent />
|
|
47
|
+
{/if}
|
|
48
|
+
</span>
|
|
49
|
+
{/if}
|
|
50
|
+
<span class={twMerge(`min-w-0 truncate text-[14px] font-semibold`, label.class)}>{label?.name}</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<svg
|
|
54
|
+
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"
|
|
56
|
+
style="transform: rotate({isOpen ? 180 : 0}deg)"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
>
|
|
59
|
+
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
{#if actions}
|
|
63
|
+
<div class="flex shrink-0 items-center gap-1 pr-3">{@render actions()}</div>
|
|
64
|
+
{/if}
|
|
65
|
+
</div>
|
|
59
66
|
|
|
60
67
|
{#if isOpen}
|
|
61
68
|
<div
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { T } from "../locales/i18n"
|
|
9
9
|
import { Button, Modal } from ".."
|
|
10
10
|
import CopiedChip from "../CopiedChip.svelte"
|
|
11
|
+
import { escapeHtml, htmlToText } from "../html"
|
|
11
12
|
|
|
12
13
|
let {
|
|
13
14
|
id = crypto.randomUUID(),
|
|
@@ -150,18 +151,21 @@
|
|
|
150
151
|
}, 250)
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/* Текст ячейки: из HTML-ячейки берётся видимый текст, formatting получает его и возвращает HTML */
|
|
155
|
+
const cellText = (value: unknown, html?: boolean) => (html ? htmlToText(String(value ?? "")) : String(value ?? ""))
|
|
156
|
+
|
|
153
157
|
const showModal = async (text: string, formatting?: (text: string) => string) => {
|
|
154
158
|
modalData = {
|
|
155
159
|
isOpen: true,
|
|
156
160
|
rawData: text,
|
|
157
|
-
formattedData: formatting ? formatting(text) : (text
|
|
161
|
+
formattedData: formatting ? formatting(text) : escapeHtml(text),
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
const showTooltip = (event: MouseEvent, text: string, formatting?: (text: string) => string) => {
|
|
162
166
|
tooltip = {
|
|
163
167
|
show: true,
|
|
164
|
-
text: formatting ? formatting(text) :
|
|
168
|
+
text: formatting ? htmlToText(formatting(text)) : text,
|
|
165
169
|
x: event.clientX,
|
|
166
170
|
y: event.clientY,
|
|
167
171
|
}
|
|
@@ -442,7 +446,7 @@
|
|
|
442
446
|
<div class="flex w-full min-w-0 items-center">
|
|
443
447
|
<div
|
|
444
448
|
class="w-full max-w-full min-w-0 wrap-break-word {text?.truncated ? 'truncate' : ' whitespace-normal'}"
|
|
445
|
-
onmouseenter={text?.tooltip ? (e) => showTooltip(e, row[text?.key ?? ""], text?.formatting) : undefined}
|
|
449
|
+
onmouseenter={text?.tooltip ? (e) => showTooltip(e, cellText(row[text?.key ?? ""], text?.html), text?.formatting) : undefined}
|
|
446
450
|
onmouseleave={() => (text?.tooltip ? (tooltip.show = false) : undefined)}
|
|
447
451
|
onmousemove={text?.tooltip
|
|
448
452
|
? (e) => {
|
|
@@ -458,13 +462,15 @@
|
|
|
458
462
|
class="cursor-pointer text-left {text?.truncated ? 'block w-full max-w-full truncate' : 'w-fit'}"
|
|
459
463
|
onclick={(e) => {
|
|
460
464
|
e.stopPropagation()
|
|
461
|
-
showModal(data
|
|
465
|
+
showModal(cellText(data, text?.html), text?.formatting)
|
|
462
466
|
}}
|
|
463
467
|
>
|
|
464
|
-
{@html data}
|
|
468
|
+
{#if text?.html}{@html data}{:else}{data}{/if}
|
|
465
469
|
</button>
|
|
466
|
-
{:else}
|
|
470
|
+
{:else if text?.html}
|
|
467
471
|
{@html data}
|
|
472
|
+
{:else}
|
|
473
|
+
{data}
|
|
468
474
|
{/if}
|
|
469
475
|
</div>
|
|
470
476
|
|
|
@@ -473,7 +479,7 @@
|
|
|
473
479
|
class="ml-1.5 flex size-5 shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent text-(--faint-color) transition-colors duration-150 hover:bg-(--container-color) hover:text-(--font-color)"
|
|
474
480
|
onclick={(e) => {
|
|
475
481
|
e.preventDefault()
|
|
476
|
-
navigator.clipboard.writeText(row[text?.key ?? ""]
|
|
482
|
+
navigator.clipboard.writeText(cellText(row[text?.key ?? ""], text?.html))
|
|
477
483
|
copiedCell = { x: j, y: i }
|
|
478
484
|
setTimeout(() => (copiedCell = null), 1000)
|
|
479
485
|
}}
|
|
@@ -514,7 +520,7 @@
|
|
|
514
520
|
transition:fly={{ y: 8, duration: 200 }}
|
|
515
521
|
role="tooltip"
|
|
516
522
|
>
|
|
517
|
-
{
|
|
523
|
+
{tooltip.text}
|
|
518
524
|
</div>
|
|
519
525
|
{/if}
|
|
520
526
|
<!-- Нижнее поле для сводной информации -->
|
|
@@ -8,7 +8,7 @@ export const xPoeDebugTableColumns = [
|
|
|
8
8
|
{ label: { name: "ARG" }, content: [{ type: "text", data: { key: "argument" } }], width: tableMonoWidth(16, 3), align: "left" },
|
|
9
9
|
{
|
|
10
10
|
label: { name: "VALUE" },
|
|
11
|
-
content: [{ type: "text", data: { key: "value", truncated: true, formatting: formatXPoeDebugModal, copy: true, modal: true } }],
|
|
11
|
+
content: [{ type: "text", data: { key: "value", html: true, truncated: true, formatting: formatXPoeDebugModal, copy: true, modal: true } }],
|
|
12
12
|
width: "1fr",
|
|
13
13
|
align: "left",
|
|
14
14
|
},
|
|
@@ -78,8 +78,6 @@
|
|
|
78
78
|
}
|
|
79
79
|
const percentColor = (p: number) => (p >= 90 ? "var(--red-color)" : p >= 75 ? "var(--orange-color)" : "var(--muted-color)")
|
|
80
80
|
|
|
81
|
-
/* Имена задач приходят с устройства, а Table выводит ячейки через {@html} - экранируем */
|
|
82
|
-
const escapeHtml = (text: string) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`)
|
|
83
81
|
let STATES = $derived(["running", "ready", "blocked", "suspended", "deleted"].map((state) => $T(`widget.stack.state.${state}`)))
|
|
84
82
|
const stackColor = (bytes: number) => (bytes < warnBytes ? "var(--red-color)" : bytes < warnBytes * 2 ? "var(--orange-color)" : "")
|
|
85
83
|
|
|
@@ -97,7 +95,7 @@
|
|
|
97
95
|
const color = stackColor(t.FreeMin)
|
|
98
96
|
const cpu = t.CPU === undefined ? "" : t.CPU.toFixed(1)
|
|
99
97
|
return {
|
|
100
|
-
Name:
|
|
98
|
+
Name: t.Name,
|
|
101
99
|
StateText: STATES[t.State] ?? "-",
|
|
102
100
|
Prio: t.Prio,
|
|
103
101
|
CoreText: t.Core === undefined ? "" : t.Core < 0 ? $T("widget.stack.core_any") : String(t.Core),
|
|
@@ -124,13 +122,13 @@
|
|
|
124
122
|
? [
|
|
125
123
|
{
|
|
126
124
|
label: { name: $T("widget.stack.cpu") },
|
|
127
|
-
content: [{ type: "text", data: { key: "CpuText" } }],
|
|
125
|
+
content: [{ type: "text", data: { key: "CpuText", html: true } }],
|
|
128
126
|
width: "5.5rem",
|
|
129
127
|
align: "right",
|
|
130
128
|
} as ITableHeader<TaskRow>,
|
|
131
129
|
]
|
|
132
130
|
: []),
|
|
133
|
-
{ label: { name: $T("widget.stack.free_min") }, content: [{ type: "text", data: { key: "FreeMinText" } }], width: "6rem", align: "right" },
|
|
131
|
+
{ label: { name: $T("widget.stack.free_min") }, content: [{ type: "text", data: { key: "FreeMinText", html: true } }], width: "6rem", align: "right" },
|
|
134
132
|
])
|
|
135
133
|
</script>
|
|
136
134
|
|
package/dist/html.d.ts
ADDED
package/dist/html.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* $lib/html.ts */
|
|
2
|
+
const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
3
|
+
/* Текст для вставки в HTML: спецсимволы заменяются сущностями */
|
|
4
|
+
export const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
|
|
5
|
+
/* Видимый текст HTML-строки: DOMParser создаёт неактивный документ, скрипты и обработчики в нём не запускаются */
|
|
6
|
+
export const htmlToText = (html) => {
|
|
7
|
+
if (typeof DOMParser === "undefined")
|
|
8
|
+
return html.replace(/<[^>]*>?/g, "");
|
|
9
|
+
return new DOMParser().parseFromString(html, "text/html").body.textContent ?? "";
|
|
10
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export * from "./locales/i18n";
|
|
|
60
60
|
export * from "./locales/languages";
|
|
61
61
|
export * from "./locales/localize";
|
|
62
62
|
export { widgetAccentStyle } from "./widgetAccent";
|
|
63
|
+
export { escapeHtml, htmlToText } from "./html";
|
|
63
64
|
export { hintsSetting, hint, type HintParams } from "./hints";
|
|
64
65
|
export { applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle";
|
|
65
66
|
export * from "./ModalStackStore";
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export * from "./locales/i18n";
|
|
|
61
61
|
export * from "./locales/languages";
|
|
62
62
|
export * from "./locales/localize";
|
|
63
63
|
export { widgetAccentStyle } from "./widgetAccent";
|
|
64
|
+
export { escapeHtml, htmlToText } from "./html";
|
|
64
65
|
export { hintsSetting, hint } from "./hints";
|
|
65
66
|
export { applyDragGhost, clearDragGhost, createDropPlaceholder } from "./dragStyle";
|
|
66
67
|
export * from "./ModalStackStore";
|
|
@@ -207,7 +207,7 @@ export function formatXPoeDebugModal(text) {
|
|
|
207
207
|
const hexIndex = text.lastIndexOf(separator);
|
|
208
208
|
const hex = hexIndex < 0 ? "" : text.slice(hexIndex + separator.length);
|
|
209
209
|
const pretty = hex ? formatXPoeDebugPrettyHtml(hex) : null;
|
|
210
|
-
const decoded = pretty === null ? (hexIndex < 0 ? text : text.slice(0, hexIndex)) : pretty;
|
|
210
|
+
const decoded = pretty === null ? escapeHtml(hexIndex < 0 ? text : text.slice(0, hexIndex)) : pretty;
|
|
211
211
|
const modalStyle = "display:flex;flex-direction:column;gap:.75rem;max-height:80vh;min-width:0;overflow-y:auto;overflow-x:hidden";
|
|
212
212
|
const sectionStyle = "min-width:0;padding:.25rem 0 .75rem";
|
|
213
213
|
const headingStyle = "display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:0 0 .5rem;font-weight:600";
|
|
@@ -215,5 +215,5 @@ export function formatXPoeDebugModal(text) {
|
|
|
215
215
|
const buttonClass = "flex size-7 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--faint-color) transition-colors duration-150 hover:bg-(--container-color) hover:text-(--font-color)";
|
|
216
216
|
const textStyle = "display:block;min-width:0;max-width:100%;margin:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font:inherit";
|
|
217
217
|
const pane = (title, target, value) => `<section style="${sectionStyle}"><div style="${headingStyle}"><span>${title}</span><button type="button" class="${buttonClass}" data-xpoe-copy="${target}" aria-label="Копировать ${title}" style="${buttonStyle}">${COPY_ICON}</button></div><pre style="${textStyle}">${value}</pre></section>`;
|
|
218
|
-
return `<div data-xpoe-debug-modal style="${modalStyle}">${pane("Value", "value", decoded)}${hexIndex < 0 ? "" : pane("HEX", "hex", hex)}</div>`;
|
|
218
|
+
return `<div data-xpoe-debug-modal style="${modalStyle}">${pane("Value", "value", decoded)}${hexIndex < 0 ? "" : pane("HEX", "hex", escapeHtml(hex))}</div>`;
|
|
219
219
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface IAccordionProps {
|
|
|
70
70
|
icon?: ConstructorOfATypedSvelteComponent | string | null;
|
|
71
71
|
};
|
|
72
72
|
children?: Snippet;
|
|
73
|
+
actions?: Snippet;
|
|
73
74
|
image?: string;
|
|
74
75
|
}
|
|
75
76
|
export interface IButtonProps {
|
|
@@ -316,6 +317,7 @@ export interface ITableText<T extends object> {
|
|
|
316
317
|
sortable?: boolean;
|
|
317
318
|
truncated?: boolean;
|
|
318
319
|
tooltip?: boolean;
|
|
320
|
+
html?: boolean;
|
|
319
321
|
formatting?: (text: string) => string;
|
|
320
322
|
copy?: boolean;
|
|
321
323
|
modal?: boolean;
|