poe-svelte-ui-lib 1.31.7 → 1.32.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/dist/WidgetFileSystem/README.md +84 -0
- package/dist/WidgetFileSystem/RefreshIcon.svelte +12 -0
- package/dist/WidgetFileSystem/RefreshIcon.svelte.d.ts +18 -0
- package/dist/WidgetFileSystem/StorageIcon.svelte +12 -0
- package/dist/WidgetFileSystem/StorageIcon.svelte.d.ts +18 -0
- package/dist/WidgetFileSystem/WidgetFileSystem.svelte +188 -0
- package/dist/WidgetFileSystem/WidgetFileSystem.svelte.d.ts +4 -0
- package/dist/WidgetFileSystem/WidgetFileSystemProps.svelte +83 -0
- package/dist/WidgetFileSystem/WidgetFileSystemProps.svelte.d.ts +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/locales/translations.js +5 -0
- package/dist/types.d.ts +44 -4
- package/package.json +3 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# WidgetFileSystem Component
|
|
2
|
+
|
|
3
|
+
## Описание
|
|
4
|
+
|
|
5
|
+
Смарт-виджет файловой системы устройства: занятость памяти, список файлов с удалением и закачка
|
|
6
|
+
нового файла с прогрессом. Работает с объектом `FSInfo` группы `CFG` ровно в том виде, в каком
|
|
7
|
+
его отдаёт прошивка (`FS_WriteFullInfo`, ProdFactory-ESP), и с прогрессом закачки `UlProg`.
|
|
8
|
+
Компонент ничего не знает про DeviceStore/WebSocket — только `value`/`uploadProgress` и колбэки
|
|
9
|
+
`onRefresh`/`onDelete`/`onUpload`, вся отправка (в том числе протокол закачки `FUpS` → `FUpP` →
|
|
10
|
+
`FUpD`) на стороне вызывающего кода.
|
|
11
|
+
|
|
12
|
+
Заменяет собой блок из отдельных компонентов (поля «Всего/Использовано/Свободно», кнопка
|
|
13
|
+
`GET FSInfo`, таблица `FSList` с кнопкой `DelFile`, `FileAttach`, `ProgressBar` по `UlProg`) —
|
|
14
|
+
одной карточкой в стиле `WidgetWiFi`/`WidgetDeviceInfo`.
|
|
15
|
+
|
|
16
|
+
- **Занятость** — «занято X из Y», процент и полоса; от 75% полоса оранжевая, от 90% — красная.
|
|
17
|
+
Размеры показываются в Б/КБ/МБ.
|
|
18
|
+
- **Файлы** — таблица «Путь / Размер / Удалить»; длинный путь обрезается «…», удаление
|
|
19
|
+
подтверждается внутри карточки. Пустая ФС — «Файлов нет». Колонка удаления есть, только
|
|
20
|
+
если передан `onDelete`.
|
|
21
|
+
- **Закачка** — блок есть, только если передан `onUpload`. Имя файла проверяется до отправки
|
|
22
|
+
(`maxNameLength`, по умолчанию 31 — ограничение прошивки), пока `onUpload` не завершится,
|
|
23
|
+
показывается прогресс из `uploadProgress`; ошибка из `onUpload` выводится под полем.
|
|
24
|
+
|
|
25
|
+
## Пропсы
|
|
26
|
+
|
|
27
|
+
| Название | Тип | По умолчанию | Описание |
|
|
28
|
+
| ---------------- | ---------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `id` | `string` | `undefined` | Префикс прошивочной группы (обычно `"CFG"`), используется вызывающим кодом для чтения `FSInfo`/`UlProg` |
|
|
30
|
+
| `wrapperClass` | `string` | `""` | CSS-классы обёртки |
|
|
31
|
+
| `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`) |
|
|
32
|
+
| `label` | `{ name?: string }` | `{ name: "Файловая система" }` | Заголовок карточки |
|
|
33
|
+
| `value` | `IWidgetFileSystemInfo` | `undefined` | Состояние файловой системы (`CFG.FSInfo`) |
|
|
34
|
+
| `uploadProgress` | `number` | `0` | Прогресс текущей закачки, % (`CFG.UlProg`) |
|
|
35
|
+
| `keys` | `{ FSInfo?: string; UlProg?: string }` | `undefined` | Переопределение имён полей устройства, по умолчанию `FSInfo` и `UlProg` |
|
|
36
|
+
| `accept` | `string` | `"*/*"` | Какие файлы можно выбрать (атрибут `accept`), например `".bin, .txt, .pem"` |
|
|
37
|
+
| `maxNameLength` | `number` | `31` | Максимальная длина имени файла |
|
|
38
|
+
| `infoCommand` | `{ header?: string; argument?: string }` | `undefined` | Команда обновления, по умолчанию `GET FSInfo` (используется вызывающим кодом) |
|
|
39
|
+
| `deleteCommand` | `{ header?: string; argument?: string }` | `undefined` | Команда удаления, по умолчанию `SET DelFile` (используется вызывающим кодом) |
|
|
40
|
+
| `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
|
|
41
|
+
| `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
|
|
42
|
+
| `onRefresh` | `() => void` | `undefined` | Кнопка «Обновить» в шапке; без обработчика кнопка неактивна |
|
|
43
|
+
| `onDelete` | `(name: string) => void` | `undefined` | Удаление файла после подтверждения; `name` — полный путь из `FSList` |
|
|
44
|
+
| `onUpload` | `(file: File) => void \| Promise<void>` | `undefined` | Закачка выбранного файла; пока промис не завершён — показывается прогресс, исключение показывается как ошибка под полем |
|
|
45
|
+
|
|
46
|
+
### IWidgetFileSystemInfo
|
|
47
|
+
|
|
48
|
+
| Поле | Тип | Описание |
|
|
49
|
+
| ----------- | ---------------------------------- | ----------------------------------------------------------------- |
|
|
50
|
+
| `FSTotal` | `number` | Размер файловой системы, байт |
|
|
51
|
+
| `FSUsed` | `number` | Занято, байт |
|
|
52
|
+
| `FSFree` | `number` | Свободно, байт |
|
|
53
|
+
| `FileCount` | `number` | Количество файлов (если нет — длина `FSList`) |
|
|
54
|
+
| `FSList` | `{ Name: string; Size: number }[]` | Файлы: полный путь (его же принимает `DelFile`) и размер в байтах |
|
|
55
|
+
|
|
56
|
+
## Примеры
|
|
57
|
+
|
|
58
|
+
```svelte
|
|
59
|
+
<script>
|
|
60
|
+
import * as UI from "poe-svelte-ui-lib"
|
|
61
|
+
|
|
62
|
+
let fsInfo = { FSTotal: 1441792, FSUsed: 98304, FSFree: 1343488, FileCount: 1, FSList: [{ Name: "/littlefs/cert.pem", Size: 1834 }] }
|
|
63
|
+
let progress = 0
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<UI.WidgetFileSystem
|
|
67
|
+
value={fsInfo}
|
|
68
|
+
uploadProgress={progress}
|
|
69
|
+
accept=".bin, .txt, .pem"
|
|
70
|
+
onRefresh={() => console.log("GET FSInfo")}
|
|
71
|
+
onDelete={(name) => console.log("SET DelFile", { Name: name })}
|
|
72
|
+
onUpload={async (file) => console.log("FUpS/FUpP/FUpD", file.name)}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Конструктор свойств (WidgetFileSystemProps.svelte)
|
|
77
|
+
|
|
78
|
+
| Название | Тип | Описание |
|
|
79
|
+
| ------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
80
|
+
| `component` | `UIComponent & { properties: Partial<IWidgetFileSystemProps> }` | Объект компонента с его свойствами |
|
|
81
|
+
| `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string }>) => void` | Коллбэк для обновления свойств компонента |
|
|
82
|
+
|
|
83
|
+
Группы панели: общие (заголовок, префикс группы, мастер-цвет), закачка (`accept`,
|
|
84
|
+
`maxNameLength`), ключи устройства (`FSInfo`, `UlProg`), команды обновления и удаления.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
|
|
4
|
+
><path
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
stroke-linecap="round"
|
|
8
|
+
stroke-linejoin="round"
|
|
9
|
+
stroke-width="2"
|
|
10
|
+
d="M21 12a9 9 0 0 0-9-9a9.75 9.75 0 0 0-6.74 2.74L3 8m0-5v5h5m-5 4a9 9 0 0 0 9 9a9.75 9.75 0 0 0 6.74-2.74L21 16m-5 0h5v5"
|
|
11
|
+
/></svg
|
|
12
|
+
>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const RefreshIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type RefreshIcon = InstanceType<typeof RefreshIcon>;
|
|
18
|
+
export default RefreshIcon;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
|
|
4
|
+
><path
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
stroke-linecap="round"
|
|
8
|
+
stroke-linejoin="round"
|
|
9
|
+
stroke-width="2"
|
|
10
|
+
d="M22 12H2m20 0v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6m20 0-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11L2 12m4 4h.01M10 16h.01"
|
|
11
|
+
/></svg
|
|
12
|
+
>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const StorageIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type StorageIcon = InstanceType<typeof StorageIcon>;
|
|
18
|
+
export default StorageIcon;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!-- $lib/WidgetFileSystem/WidgetFileSystem.svelte — смарт-виджет файловой системы устройства.
|
|
2
|
+
Данные — объект CFG.FSInfo в том виде, в каком его отдаёт прошивка (FS_WriteFullInfo, ProdFactory-ESP):
|
|
3
|
+
FSTotal/FSUsed/FSFree/FileCount/FSList[{Name, Size}]; прогресс закачки — CFG.UlProg.
|
|
4
|
+
Ничего не знает про DeviceStore/WebSocket — value/uploadProgress/onRefresh/onDelete/onUpload,
|
|
5
|
+
протокол закачки (FUpS/FUpP/FUpD) и отправка команд на стороне вызывающего. -->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { slide, fade, scale } from "svelte/transition"
|
|
8
|
+
import { twMerge } from "tailwind-merge"
|
|
9
|
+
import * as UI from ".."
|
|
10
|
+
import type { IWidgetFileSystemFile, IWidgetFileSystemProps, ITableHeader } from "../types"
|
|
11
|
+
import StorageIcon from "./StorageIcon.svelte"
|
|
12
|
+
import RefreshIcon from "./RefreshIcon.svelte"
|
|
13
|
+
import WidgetHeader from "../WidgetHeader.svelte"
|
|
14
|
+
import { widgetAccentStyle } from "../widgetAccent"
|
|
15
|
+
import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
wrapperClass = "",
|
|
19
|
+
componentClass = "",
|
|
20
|
+
label = { name: "Файловая система" },
|
|
21
|
+
value,
|
|
22
|
+
uploadProgress = 0,
|
|
23
|
+
accept = "*/*",
|
|
24
|
+
maxNameLength = 31,
|
|
25
|
+
collapsed = $bindable(false),
|
|
26
|
+
persistKey,
|
|
27
|
+
onRefresh,
|
|
28
|
+
onDelete,
|
|
29
|
+
onUpload,
|
|
30
|
+
}: IWidgetFileSystemProps = $props()
|
|
31
|
+
|
|
32
|
+
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
33
|
+
|
|
34
|
+
/* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
|
|
35
|
+
let hasMergedPersisted = false
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (hasMergedPersisted) return
|
|
38
|
+
hasMergedPersisted = true
|
|
39
|
+
const persisted = readPersistedCollapsed(persistKey, collapsed)
|
|
40
|
+
if (persisted !== collapsed) collapsed = persisted
|
|
41
|
+
})
|
|
42
|
+
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
43
|
+
|
|
44
|
+
/* Размер в человекочитаемом виде: Б / КБ / МБ, одна цифра после запятой начиная с КБ */
|
|
45
|
+
const formatBytes = (bytes: number) => {
|
|
46
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "—"
|
|
47
|
+
if (bytes < 1024) return `${bytes} Б`
|
|
48
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`
|
|
49
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let total = $derived(value?.FSTotal ?? 0)
|
|
53
|
+
let used = $derived(value?.FSUsed ?? 0)
|
|
54
|
+
let free = $derived(value?.FSFree ?? Math.max(total - used, 0))
|
|
55
|
+
let files = $derived(value?.FSList ?? [])
|
|
56
|
+
let usedPercent = $derived(total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0)
|
|
57
|
+
/* Почти заполненная ФС подсвечивается: закачка новых файлов скоро начнёт падать */
|
|
58
|
+
let usageColor = $derived(usedPercent >= 90 ? "var(--red-color)" : usedPercent >= 75 ? "var(--orange-color)" : "var(--accent-color)")
|
|
59
|
+
|
|
60
|
+
/* Таблица файлов: путь обрезается «…», размер — уже отформатирован, удаление — с подтверждением */
|
|
61
|
+
type FileRow = IWidgetFileSystemFile & { SizeText: string }
|
|
62
|
+
let rows: FileRow[] = $derived(files.map((f) => ({ ...f, SizeText: formatBytes(f.Size) })))
|
|
63
|
+
let header: ITableHeader<FileRow>[] = $derived([
|
|
64
|
+
{ label: { name: "Путь" }, content: [{ type: "text", data: { key: "Name", truncated: true, sortable: true } }], width: "1fr", align: "left" },
|
|
65
|
+
{ label: { name: "Размер" }, content: [{ type: "text", data: { key: "SizeText" } }], width: "6rem", align: "right" },
|
|
66
|
+
...(onDelete
|
|
67
|
+
? [
|
|
68
|
+
{
|
|
69
|
+
label: { name: "" },
|
|
70
|
+
content: [{ type: "button", data: { name: "Удалить", class: "bg-red", onClick: (row: FileRow) => (pendingDelete = row.Name) } }],
|
|
71
|
+
width: "6.5rem",
|
|
72
|
+
align: "center",
|
|
73
|
+
} as ITableHeader<FileRow>,
|
|
74
|
+
]
|
|
75
|
+
: []),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
let pendingDelete: string | null = $state(null)
|
|
79
|
+
const confirmDelete = () => {
|
|
80
|
+
if (pendingDelete) onDelete?.(pendingDelete)
|
|
81
|
+
pendingDelete = null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Закачка: имя проверяется до отправки (прошивка отвергнет длинное имя уже после FUpS), прогресс —
|
|
85
|
+
из uploadProgress, пока onUpload не завершится */
|
|
86
|
+
let uploading = $state(false)
|
|
87
|
+
let uploadError = $state("")
|
|
88
|
+
const handleFile = async (_event: Event, file: File | null) => {
|
|
89
|
+
uploadError = ""
|
|
90
|
+
if (!file || !onUpload || uploading) return
|
|
91
|
+
if (file.name.length > maxNameLength) {
|
|
92
|
+
uploadError = `Имя файла длиннее ${maxNameLength} символов`
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
uploading = true
|
|
96
|
+
try {
|
|
97
|
+
await onUpload(file)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
uploadError = `Закачка прервана: ${error instanceof Error ? error.message : error}`
|
|
100
|
+
} finally {
|
|
101
|
+
uploading = false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<div
|
|
107
|
+
class={twMerge("relative flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
|
|
108
|
+
style={accentStyle}
|
|
109
|
+
>
|
|
110
|
+
<!-- Заголовок -->
|
|
111
|
+
<WidgetHeader icon={StorageIcon} label={label?.name ?? "Файловая система"} bind:collapsed>
|
|
112
|
+
{#snippet right()}
|
|
113
|
+
<UI.Button
|
|
114
|
+
wrapperClass="w-auto"
|
|
115
|
+
componentClass="bg-transparent px-3"
|
|
116
|
+
content={{ icon: RefreshIcon, name: "Обновить" }}
|
|
117
|
+
disabled={!onRefresh}
|
|
118
|
+
onClick={() => onRefresh?.()}
|
|
119
|
+
/>
|
|
120
|
+
{/snippet}
|
|
121
|
+
</WidgetHeader>
|
|
122
|
+
|
|
123
|
+
{#if !collapsed}
|
|
124
|
+
<div class="flex flex-col gap-4" transition:slide={{ duration: 150 }}>
|
|
125
|
+
<!-- Занятость памяти -->
|
|
126
|
+
<div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
|
|
127
|
+
<div class="flex items-baseline justify-between gap-2 text-[13px]">
|
|
128
|
+
<span class="font-semibold">Занято {formatBytes(used)} из {formatBytes(total)}</span>
|
|
129
|
+
<span class="font-semibold tabular-nums" style="color: {usageColor}">{usedPercent}%</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="h-2 w-full overflow-hidden rounded-full bg-(--container-color)">
|
|
132
|
+
<div class="h-full rounded-full transition-[width] duration-300" style="width: {usedPercent}%; background: {usageColor}"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="flex justify-between gap-2 text-[12px] text-(--muted-color)">
|
|
135
|
+
<span>Свободно {formatBytes(free)}</span>
|
|
136
|
+
<span>Файлов: {value?.FileCount ?? files.length}</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Файлы -->
|
|
141
|
+
<div class="flex max-h-72 min-h-0 flex-col">
|
|
142
|
+
{#if rows.length}
|
|
143
|
+
<UI.Table {header} body={rows} outline />
|
|
144
|
+
{:else}
|
|
145
|
+
<div class="rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3 text-center text-[13px] text-(--faint-color)">Файлов нет</div>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Закачка -->
|
|
150
|
+
{#if onUpload}
|
|
151
|
+
<div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
|
|
152
|
+
<UI.FileAttach label={{ name: "Закачать файл" }} {accept} disabled={uploading} onChange={handleFile} />
|
|
153
|
+
{#if uploading}
|
|
154
|
+
<div class="flex items-center gap-2 text-[12px]" transition:slide={{ duration: 100 }}>
|
|
155
|
+
<div class="h-2 flex-1 overflow-hidden rounded-full bg-(--container-color)">
|
|
156
|
+
<div
|
|
157
|
+
class="h-full rounded-full bg-(--accent-color) transition-[width] duration-300"
|
|
158
|
+
style="width: {Math.min(100, Math.max(0, uploadProgress))}%"
|
|
159
|
+
></div>
|
|
160
|
+
</div>
|
|
161
|
+
<span class="w-10 text-right tabular-nums">{Math.round(uploadProgress)}%</span>
|
|
162
|
+
</div>
|
|
163
|
+
{/if}
|
|
164
|
+
{#if uploadError}
|
|
165
|
+
<span class="text-[12px] text-(--red-color)" transition:slide={{ duration: 100 }}>{uploadError}</span>
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
|
|
172
|
+
{#if pendingDelete}
|
|
173
|
+
<!-- Подтверждение локально к виджету, как предупреждение AP у WidgetWiFi -->
|
|
174
|
+
<div class="absolute inset-0 z-10 flex items-center justify-center rounded-2xl bg-black/40 backdrop-blur-[2px]" transition:fade={{ duration: 150 }}>
|
|
175
|
+
<div
|
|
176
|
+
class="flex w-full max-w-80 flex-col gap-3 rounded-[16px] border border-(--hairline-color) bg-(--back-color) p-4 shadow-(--elevation-3)"
|
|
177
|
+
transition:scale={{ duration: 150, start: 0.96 }}
|
|
178
|
+
>
|
|
179
|
+
<h4 class="text-[15px] font-semibold">Удалить файл?</h4>
|
|
180
|
+
<p class="text-[13px] break-all text-(--muted-color)">{pendingDelete}</p>
|
|
181
|
+
<div class="flex gap-2">
|
|
182
|
+
<UI.Button wrapperClass="flex-1" componentClass="bg-transparent" content={{ name: "Отмена" }} onClick={() => (pendingDelete = null)} />
|
|
183
|
+
<UI.Button wrapperClass="flex-1" componentClass="bg-red" content={{ name: "Удалить" }} onClick={confirmDelete} />
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
</div>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!-- $lib/WidgetFileSystem/WidgetFileSystemProps.svelte — панель пропсов конструктора для
|
|
2
|
+
WidgetFileSystem: заголовок/мастер-цвет/префикс группы, имена полей устройства (keys),
|
|
3
|
+
фильтр закачки и команды обновления/удаления — все с дефолтами, равными прошивочной
|
|
4
|
+
конвенции (см. DEFAULT_PROPS.WidgetFileSystem в DevCloud). -->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { T } from "../locales/i18n"
|
|
7
|
+
import * as UI from ".."
|
|
8
|
+
import { updateProperty, type UIComponent, type IWidgetFileSystemProps, type IOption } from "../types"
|
|
9
|
+
import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
|
|
10
|
+
import PropsGroup from "../PropsGroup.svelte"
|
|
11
|
+
import { optionsStore } from "../options"
|
|
12
|
+
|
|
13
|
+
const FS_KEYS = ["FSInfo", "UlProg"] as const
|
|
14
|
+
|
|
15
|
+
const { component, onPropertyChange } = $props<{
|
|
16
|
+
component: UIComponent & { properties: Partial<IWidgetFileSystemProps> }
|
|
17
|
+
onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string }>) => void
|
|
18
|
+
}>()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{#snippet Command(key: "infoCommand" | "deleteCommand", header: string, argument: string)}
|
|
22
|
+
<UI.Select
|
|
23
|
+
label={{ name: "Заголовок пакета" }}
|
|
24
|
+
type="buttons"
|
|
25
|
+
options={$optionsStore.HEADER_OPTIONS}
|
|
26
|
+
value={$optionsStore.HEADER_OPTIONS.find((o) => o.value === (component.properties[key]?.header || header))}
|
|
27
|
+
onUpdate={(option) => updateProperty(`${key}.header`, (option as IOption<string>).value as string, component, onPropertyChange)}
|
|
28
|
+
/>
|
|
29
|
+
<UI.Input
|
|
30
|
+
label={{ name: "Аргумент" }}
|
|
31
|
+
value={component.properties[key]?.argument ?? argument}
|
|
32
|
+
onUpdate={(value) => updateProperty(`${key}.argument`, value as string, component, onPropertyChange)}
|
|
33
|
+
/>
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
<div class="flex flex-col gap-2">
|
|
37
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
38
|
+
<UI.Input
|
|
39
|
+
label={{ name: "Заголовок" }}
|
|
40
|
+
value={component.properties.label?.name}
|
|
41
|
+
onUpdate={(value) => updateProperty("label.name", value as string, component, onPropertyChange)}
|
|
42
|
+
/>
|
|
43
|
+
<UI.Input
|
|
44
|
+
label={{ name: "Префикс группы (ID)" }}
|
|
45
|
+
value={component.properties.id}
|
|
46
|
+
onUpdate={(value) => updateProperty("id", value as string, component, onPropertyChange)}
|
|
47
|
+
/>
|
|
48
|
+
<WidgetAccentPicker
|
|
49
|
+
value={component.properties.componentClass ?? ""}
|
|
50
|
+
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
51
|
+
/>
|
|
52
|
+
</PropsGroup>
|
|
53
|
+
<PropsGroup label="Закачка файлов">
|
|
54
|
+
<UI.Input
|
|
55
|
+
label={{ name: "Допустимые файлы (accept)" }}
|
|
56
|
+
value={component.properties.accept ?? "*/*"}
|
|
57
|
+
placeholder=".bin, .txt, .pem"
|
|
58
|
+
onUpdate={(value) => updateProperty("accept", value as string, component, onPropertyChange)}
|
|
59
|
+
/>
|
|
60
|
+
<UI.Input
|
|
61
|
+
label={{ name: "Максимальная длина имени" }}
|
|
62
|
+
type="number"
|
|
63
|
+
number={{ minNum: 1, maxNum: 255, step: 1 }}
|
|
64
|
+
value={component.properties.maxNameLength ?? 31}
|
|
65
|
+
onUpdate={(value) => updateProperty("maxNameLength", Number(value), component, onPropertyChange)}
|
|
66
|
+
/>
|
|
67
|
+
</PropsGroup>
|
|
68
|
+
<PropsGroup label="Ключи устройства">
|
|
69
|
+
{#each FS_KEYS as key (key)}
|
|
70
|
+
<UI.Input
|
|
71
|
+
label={{ name: key }}
|
|
72
|
+
value={component.properties.keys?.[key] ?? key}
|
|
73
|
+
onUpdate={(value) => updateProperty(`keys.${key}`, value as string, component, onPropertyChange)}
|
|
74
|
+
/>
|
|
75
|
+
{/each}
|
|
76
|
+
</PropsGroup>
|
|
77
|
+
<PropsGroup label="Команда обновления">
|
|
78
|
+
{@render Command("infoCommand", "GET", "FSInfo")}
|
|
79
|
+
</PropsGroup>
|
|
80
|
+
<PropsGroup label="Команда удаления">
|
|
81
|
+
{@render Command("deleteCommand", "SET", "DelFile")}
|
|
82
|
+
</PropsGroup>
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UIComponent, type IWidgetFileSystemProps } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
component: UIComponent & {
|
|
4
|
+
properties: Partial<IWidgetFileSystemProps>;
|
|
5
|
+
};
|
|
6
|
+
onPropertyChange: (updates: Partial<{
|
|
7
|
+
properties?: string | object;
|
|
8
|
+
name?: string;
|
|
9
|
+
access?: string;
|
|
10
|
+
}>) => void;
|
|
11
|
+
};
|
|
12
|
+
declare const WidgetFileSystemProps: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type WidgetFileSystemProps = ReturnType<typeof WidgetFileSystemProps>;
|
|
14
|
+
export default WidgetFileSystemProps;
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,9 @@ export { default as WidgetWiFi } from "./WidgetWiFi/WidgetWiFi.svelte";
|
|
|
43
43
|
export { default as WidgetWiFiProps } from "./WidgetWiFi/WidgetWiFiProps.svelte";
|
|
44
44
|
export { default as WidgetDeviceInfo } from "./WidgetDeviceInfo/WidgetDeviceInfo.svelte";
|
|
45
45
|
export { default as WidgetDeviceInfoProps } from "./WidgetDeviceInfo/WidgetDeviceInfoProps.svelte";
|
|
46
|
+
export { default as WidgetFileSystem } from "./WidgetFileSystem/WidgetFileSystem.svelte";
|
|
47
|
+
export { default as WidgetFileSystemProps } from "./WidgetFileSystem/WidgetFileSystemProps.svelte";
|
|
46
48
|
export * from "./locales/i18n";
|
|
47
49
|
export * from "./locales/translations";
|
|
48
50
|
export * from "./ModalStackStore";
|
|
49
|
-
export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
|
|
51
|
+
export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ export { default as WidgetWiFi } from "./WidgetWiFi/WidgetWiFi.svelte";
|
|
|
44
44
|
export { default as WidgetWiFiProps } from "./WidgetWiFi/WidgetWiFiProps.svelte";
|
|
45
45
|
export { default as WidgetDeviceInfo } from "./WidgetDeviceInfo/WidgetDeviceInfo.svelte";
|
|
46
46
|
export { default as WidgetDeviceInfoProps } from "./WidgetDeviceInfo/WidgetDeviceInfoProps.svelte";
|
|
47
|
+
export { default as WidgetFileSystem } from "./WidgetFileSystem/WidgetFileSystem.svelte";
|
|
48
|
+
export { default as WidgetFileSystemProps } from "./WidgetFileSystem/WidgetFileSystemProps.svelte";
|
|
47
49
|
export * from "./locales/i18n";
|
|
48
50
|
export * from "./locales/translations";
|
|
49
51
|
export * from "./ModalStackStore";
|
|
@@ -23,6 +23,11 @@ const translations = {
|
|
|
23
23
|
'ℹ️ The widget works with a fixed set of fields in the device\'s `CFG` group — no manual variable binding needed.\n\n**🔒 Read-only**\n- `DevSN`, `DevID`, `DevFW`, `RunCnt`\n- `StaIP`, `ApIP` — current IP addresses in STA/AP mode\n\n**✏️ Editable** (saved as one packet via the "Save" button)\n- `DevName`, `HostName`, `WebUser`, `WebPsw`\n\n🔌 The "Restart" button sends `Argument: Restart`.\n\n⚙️ If the device uses a different group name instead of `CFG`, change the prefix (ID) in the widget settings.',
|
|
24
24
|
"ℹ️ 该组件使用设备 `CFG` 分组下的一组固定字段——无需手动绑定变量。\n\n**🔒 只读字段**\n- `DevSN`、`DevID`、`DevFW`、`RunCnt`\n- `StaIP`、`ApIP` —— STA/AP 模式下的当前 IP 地址\n\n**✏️ 可编辑字段**(通过「保存」按钮一次性发送)\n- `DevName`、`HostName`、`WebUser`、`WebPsw`\n\n🔌 「重启」按钮会发送 `Argument: Restart`。\n\n⚙️ 如果设备使用的分组名称不是 `CFG`,请在组件设置中修改前缀(ID)。",
|
|
25
25
|
],
|
|
26
|
+
"component.info.WidgetFileSystem": [
|
|
27
|
+
"🗂️ Виджет работает с объектом `FSInfo` группы `CFG` на устройстве — привязка переменных вручную не нужна.\n\n**📊 Занятость**\n- `FSTotal`, `FSUsed`, `FSFree`, `FileCount`\n\n**📄 Файлы**\n- `FSList` — путь и размер; «Удалить» отправляет `DelFile` с `Name` (после подтверждения)\n\n**⬆️ Закачка**\n- протокол `FUpS` → `FUpP` → `FUpD`, прогресс — `UlProg`\n- имя файла не длиннее 31 символа (ограничение прошивки)\n\n🔄 «Обновить» отправляет `GET FSInfo`.\n\n⚙️ Если у прошивки другие имена, поменяйте префикс (ID) и ключи в настройках виджета.",
|
|
28
|
+
"🗂️ The widget works with the `FSInfo` object of the device's `CFG` group — no manual variable binding needed.\n\n**📊 Usage**\n- `FSTotal`, `FSUsed`, `FSFree`, `FileCount`\n\n**📄 Files**\n- `FSList` — path and size; \"Delete\" sends `DelFile` with `Name` (after confirmation)\n\n**⬆️ Upload**\n- `FUpS` → `FUpP` → `FUpD` protocol, progress — `UlProg`\n- file name up to 31 characters (firmware limit)\n\n🔄 \"Refresh\" sends `GET FSInfo`.\n\n⚙️ If the firmware uses different names, change the prefix (ID) and keys in the widget settings.",
|
|
29
|
+
"🗂️ 该组件使用设备 `CFG` 分组下的 `FSInfo` 对象——无需手动绑定变量。\n\n**📊 占用**\n- `FSTotal`、`FSUsed`、`FSFree`、`FileCount`\n\n**📄 文件**\n- `FSList` —— 路径和大小;「删除」在确认后发送带 `Name` 的 `DelFile`\n\n**⬆️ 上传**\n- `FUpS` → `FUpP` → `FUpD` 协议,进度 —— `UlProg`\n- 文件名不超过 31 个字符(固件限制)\n\n🔄 「刷新」发送 `GET FSInfo`。\n\n⚙️ 如果固件使用其他名称,请在组件设置中修改前缀(ID)和键名。",
|
|
30
|
+
],
|
|
26
31
|
"component.info.Tabs": [
|
|
27
32
|
"📑 Ширина и позиция иконки задаются через служебные CSS-классы каждой вкладки, а не отдельными полями.\n\n**↔️ Ширина кнопок** (переключатель «Авто/Равные»)\n- `w-auto` — по содержимому\n- `flex-1` — поровну между всеми вкладками\n\n**🔤 Позиция иконки**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Максимум 10 вкладок.",
|
|
28
33
|
'📑 Tab width and icon position are set via each tab\'s internal CSS classes, not separate fields.\n\n**↔️ Button width** ("Auto/Equal" toggle)\n- `w-auto` — sized to content\n- `flex-1` — equal share among all tabs\n\n**🔤 Icon position**\n- `flex-row` / `flex-col` / `flex-row-reverse` / `flex-col-reverse`\n\n⚠️ Maximum 10 tabs.',
|
package/dist/types.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare const updateComponent: (component: UIComponent, updates: Partial<
|
|
|
15
15
|
}>) => {
|
|
16
16
|
access: "full" | "viewOnly" | "hidden" | undefined;
|
|
17
17
|
name: string | undefined;
|
|
18
|
-
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps;
|
|
18
|
+
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps;
|
|
19
19
|
eventHandler: IUIComponentHandler | undefined;
|
|
20
20
|
id: string;
|
|
21
|
-
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo";
|
|
21
|
+
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem";
|
|
22
22
|
position: Required<Position>;
|
|
23
23
|
parentId: string;
|
|
24
24
|
};
|
|
@@ -26,8 +26,8 @@ export interface UIComponent {
|
|
|
26
26
|
id: string;
|
|
27
27
|
name?: string;
|
|
28
28
|
access?: "full" | "viewOnly" | "hidden";
|
|
29
|
-
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo";
|
|
30
|
-
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps;
|
|
29
|
+
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem";
|
|
30
|
+
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps;
|
|
31
31
|
position: Required<Position>;
|
|
32
32
|
parentId: string;
|
|
33
33
|
eventHandler?: IUIComponentHandler;
|
|
@@ -560,3 +560,43 @@ export interface IWidgetDeviceInfoProps {
|
|
|
560
560
|
onSave?: (info: IWidgetDeviceInfoConfig) => void;
|
|
561
561
|
onRestart?: () => void;
|
|
562
562
|
}
|
|
563
|
+
export interface IWidgetFileSystemFile {
|
|
564
|
+
Name: string;
|
|
565
|
+
Size: number;
|
|
566
|
+
}
|
|
567
|
+
export interface IWidgetFileSystemInfo {
|
|
568
|
+
FSTotal: number;
|
|
569
|
+
FSUsed: number;
|
|
570
|
+
FSFree: number;
|
|
571
|
+
FileCount?: number;
|
|
572
|
+
FSList: IWidgetFileSystemFile[];
|
|
573
|
+
}
|
|
574
|
+
export interface IWidgetFileSystemProps {
|
|
575
|
+
id?: string;
|
|
576
|
+
wrapperClass?: string;
|
|
577
|
+
componentClass?: string;
|
|
578
|
+
label?: {
|
|
579
|
+
name?: string;
|
|
580
|
+
};
|
|
581
|
+
value?: IWidgetFileSystemInfo;
|
|
582
|
+
uploadProgress?: number;
|
|
583
|
+
keys?: {
|
|
584
|
+
FSInfo?: string;
|
|
585
|
+
UlProg?: string;
|
|
586
|
+
};
|
|
587
|
+
accept?: string;
|
|
588
|
+
maxNameLength?: number;
|
|
589
|
+
infoCommand?: {
|
|
590
|
+
header?: string;
|
|
591
|
+
argument?: string;
|
|
592
|
+
};
|
|
593
|
+
deleteCommand?: {
|
|
594
|
+
header?: string;
|
|
595
|
+
argument?: string;
|
|
596
|
+
};
|
|
597
|
+
collapsed?: boolean;
|
|
598
|
+
persistKey?: string;
|
|
599
|
+
onRefresh?: () => void;
|
|
600
|
+
onDelete?: (name: string) => void;
|
|
601
|
+
onUpload?: (file: File) => void | Promise<void>;
|
|
602
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poe-svelte-ui-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
10
|
"prepack": "svelte-kit sync && svelte-package && publint",
|
|
11
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
12
|
"CheckUpdate": "npx npm-check-updates -u && npm install",
|
|
12
13
|
"UpdateIconsLib": "tsx src/lib/IconsCatalog/iconsLib.ts",
|
|
13
14
|
"Formatting": "prettier --write ."
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
52
53
|
"publint": "^0.3.21",
|
|
53
54
|
"svelte": "^5.56.4",
|
|
55
|
+
"svelte-check": "^4.7.6",
|
|
54
56
|
"tailwindcss": "^4.3.1",
|
|
55
57
|
"tsx": "^4.22.4",
|
|
56
58
|
"typescript": "^6.0.3",
|