poe-svelte-ui-lib 1.75.0 → 1.76.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.
@@ -2,6 +2,10 @@
2
2
  import { T } from "../locales/i18n"
3
3
  import type { IFileAttachProps } from "../types"
4
4
  import { twMerge } from "tailwind-merge"
5
+ import Button from "../Button/Button.svelte"
6
+
7
+ /* Иконка «открыть файл» для type "button" — документ со стрелкой вверх, контур 1.75 как у остальных кнопок */
8
+ const OPEN_FILE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M12 18v-6"/><path d="m9 15 3-3 3 3"/></svg>`
5
9
 
6
10
  let {
7
11
  id = crypto.randomUUID(),
@@ -13,6 +17,8 @@
13
17
  imageSize = { height: "10rem", width: "10rem", fitMode: "cover", form: "square" },
14
18
  disabled = false,
15
19
  currentImage = $bindable(""),
20
+ info,
21
+ icon,
16
22
  onChange = () => {},
17
23
  }: IFileAttachProps = $props()
18
24
 
@@ -45,6 +51,8 @@
45
51
  }
46
52
 
47
53
  onChange(event, file)
54
+ /* Кнопка «Открыть файл» — повторный выбор того же файла тоже должен срабатывать */
55
+ if (type === "button") input.value = ""
48
56
  }
49
57
 
50
58
  const triggerFileInput = () => {
@@ -60,7 +68,11 @@
60
68
  <span class={twMerge(`w-full text-[12px] font-semibold text-(--muted-color)`, label.class)}>{label.name}</span>
61
69
  {/if}
62
70
 
63
- {#if type === "image"}
71
+ {#if type === "button"}
72
+ <!-- Иконочная кнопка с подсказкой, как UI.Button; выбор файла — скрытый input за ней -->
73
+ <Button {componentClass} {disabled} content={{ icon: icon ?? OPEN_FILE_ICON, info }} onClick={triggerFileInput} />
74
+ <input id={ID} type="file" class="hidden" {accept} {disabled} onchange={handleFileChange} />
75
+ {:else if type === "image"}
64
76
  <div class="relative">
65
77
  <button
66
78
  class="flex items-center justify-center overflow-hidden {imageSize.form === 'circle' ? 'rounded-full' : 'rounded-[12px]'}
@@ -17,6 +17,18 @@
17
17
 
18
18
  ## Особенности
19
19
 
20
+ - `type="button"` — иконочная кнопка «Открыть файл» (скрытый `input` за `UI.Button`) с подсказкой `info`, для панелей инструментов: открыть файл для редактирования рядом с кнопками «Сохранить в файл», «Загрузить с сервера»; сохранение в файл — обычная `UI.Button`
21
+
22
+ ```svelte
23
+ <UI.FileAttach
24
+ type="button"
25
+ accept=".yaml,.gz"
26
+ componentClass={TOOL_BUTTON}
27
+ info={{ text: "Открыть файл", side: "top" }}
28
+ onChange={(event, file) => open(file)}
29
+ />
30
+ ```
31
+
20
32
  - Для списка файлов, удаления и прогресса закачки используйте виджет «Файловая система»
21
33
 
22
34
  ### Адаптивность
@@ -35,24 +47,26 @@
35
47
 
36
48
  ## Пропсы
37
49
 
38
- | Название | Тип | По умолчанию | Описание |
39
- | ------------------- | -------------------------------------------- | --------------------- | ---------------------------------------------------------------- |
40
- | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
41
- | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
42
- | `componentClass` | `string` | `""` | Дополнительные CSS-классы для элемента с именем выбранного файла |
43
- | `label` | `object` | `-` | Настройки заголовка компонента |
44
- | `label.name` | `string` | `""` | Текст заголовка компонента |
45
- | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
46
- | `type` | `"file" \| "image"` | `"file"` | Тип компонента (файл или изображение) |
47
- | `accept` | `string` | `"*/*"` | MIME-типы файлов, которые можно выбрать |
48
- | `imageSize` | `object` | `-` | Настройки отображения изображения |
49
- | `imageSize.height` | `string` | `"10rem"` | Высота области отображения изображения |
50
- | `imageSize.width` | `string` | `"10rem"` | Ширина области отображения изображения |
51
- | `imageSize.fitMode` | `"cover" \| "contain"` | `"cover"` | Режим масштабирования изображения |
52
- | `imageSize.form` | `"square" \| "circle"` | `"square"` | Форма области отображения изображения |
53
- | `disabled` | `boolean` | `false` | Состояние отключения компонента |
54
- | `currentImage` | `string \| null` | `""` | Текущее изображение (для связывания) |
55
- | `onChange` | `(event: Event, file: File \| null) => void` | `() => {}` | Обработчик изменения файла |
50
+ | Название | Тип | По умолчанию | Описание |
51
+ | ------------------- | -------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------- |
52
+ | `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
53
+ | `wrapperClass` | `string` | `""` | Дополнительные CSS-классы для обертки компонента |
54
+ | `componentClass` | `string` | `""` | Дополнительные CSS-классы для элемента с именем выбранного файла |
55
+ | `label` | `object` | `-` | Настройки заголовка компонента |
56
+ | `label.name` | `string` | `""` | Текст заголовка компонента |
57
+ | `label.class` | `string` | `""` | CSS-классы для стилизации текста заголовка |
58
+ | `type` | `"file" \| "image" \| "button"` | `"file"` | Тип компонента: поле выбора файла, изображение или иконочная кнопка «Открыть файл» |
59
+ | `info` | `{ text, side }` | `-` | Только `button`: всплывающая подсказка, как у `UI.Button` |
60
+ | `icon` | `Component \| string` | документ со стрелкой | Только `button`: своя иконка (компонент или SVG-строка) |
61
+ | `accept` | `string` | `"*/*"` | MIME-типы файлов, которые можно выбрать |
62
+ | `imageSize` | `object` | `-` | Настройки отображения изображения |
63
+ | `imageSize.height` | `string` | `"10rem"` | Высота области отображения изображения |
64
+ | `imageSize.width` | `string` | `"10rem"` | Ширина области отображения изображения |
65
+ | `imageSize.fitMode` | `"cover" \| "contain"` | `"cover"` | Режим масштабирования изображения |
66
+ | `imageSize.form` | `"square" \| "circle"` | `"square"` | Форма области отображения изображения |
67
+ | `disabled` | `boolean` | `false` | Состояние отключения компонента |
68
+ | `currentImage` | `string \| null` | `""` | Текущее изображение (для связывания) |
69
+ | `onChange` | `(event: Event, file: File \| null) => void` | `() => {}` | Обработчик изменения файла |
56
70
 
57
71
  ### Колбэки
58
72
 
@@ -70,6 +70,15 @@
70
70
  let boards: Board[] = $state([])
71
71
  let now = $state(Date.now())
72
72
  let mapInstance: any = $state()
73
+ /* Вид карты (центр, масштаб, поворот) переживает сворачивание: тело виджета пересоздаёт карту, и она открывается там, где её оставили */
74
+ let view = $state.raw({ center: { lat: 30, lng: 0 }, zoom: 1.5, bearing: 0 })
75
+ $effect(() => {
76
+ const map = mapInstance
77
+ if (!map) return
78
+ const save = () => (view = { center: map.getCenter(), zoom: map.getZoom(), bearing: map.getBearing() })
79
+ map.on("moveend", save)
80
+ return () => map.off("moveend", save)
81
+ })
73
82
  let isCopied = $state(false)
74
83
  let isDarkMode = $state(false)
75
84
 
@@ -211,8 +220,9 @@
211
220
  bind:map={mapInstance}
212
221
  class="h-full w-full overflow-hidden rounded-[14px] border border-(--hairline-color)"
213
222
  style={isDarkMode ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
214
- zoom={1.5}
215
- center={{ lat: 30, lng: 0 }}
223
+ zoom={view.zoom}
224
+ center={view.center}
225
+ bearing={view.bearing}
216
226
  maxPitch={0}
217
227
  >
218
228
  <NavigationControl />
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onDestroy } from "svelte"
2
+ import { onDestroy, untrack } from "svelte"
3
3
  import { getMapContext } from "./contexts.svelte.js"
4
4
 
5
5
  interface Props {
@@ -23,31 +23,36 @@
23
23
  properties: {},
24
24
  })
25
25
 
26
+ /* Источник и слой — один раз; пока стиль карты грузится, добавление ждёт в очереди waitForStyleLoaded */
26
27
  $effect(() => {
27
28
  if (!mapCtx.map || added) return
28
-
29
- mapCtx.addSource(id, { type: "geojson", data: toGeoJSON() })
30
- mapCtx.addLayer({
31
- id,
32
- type: "line",
33
- source: id,
34
- layout: { "line-join": "round", "line-cap": "round" },
35
- paint: { "line-color": color, "line-width": width, "line-opacity": opacity },
29
+ untrack(() => {
30
+ mapCtx.addSource(id, { type: "geojson", data: toGeoJSON() })
31
+ mapCtx.addLayer({
32
+ id,
33
+ type: "line",
34
+ source: id,
35
+ layout: { "line-join": "round", "line-cap": "round" },
36
+ paint: { "line-color": color, "line-width": width, "line-opacity": opacity },
37
+ })
36
38
  })
37
39
  added = true
38
40
  })
39
41
 
42
+ /* Координаты и оформление читаются до любых проверок — иначе эффект не подписывается на них и, запущенный до загрузки стиля, больше не срабатывает */
40
43
  $effect(() => {
41
- if (!added || !mapCtx.map) return
42
- const source = mapCtx.map.getSource(id) as any
43
- source?.setData(toGeoJSON())
44
+ const data = toGeoJSON()
45
+ if (!added) return
46
+ mapCtx.waitForStyleLoaded((map) => (map.getSource(id) as any)?.setData(data))
44
47
  })
45
48
 
46
49
  $effect(() => {
47
- if (!added || !mapCtx.map || !mapCtx.map.getLayer(id)) return
48
- mapCtx.map.setPaintProperty(id, "line-color", color)
49
- mapCtx.map.setPaintProperty(id, "line-width", width)
50
- mapCtx.map.setPaintProperty(id, "line-opacity", opacity)
50
+ const paint = { "line-color": color, "line-width": width, "line-opacity": opacity }
51
+ if (!added) return
52
+ mapCtx.waitForStyleLoaded((map) => {
53
+ if (!map.getLayer(id)) return
54
+ for (const [name, value] of Object.entries(paint)) map.setPaintProperty(id, name, value)
55
+ })
51
56
  })
52
57
 
53
58
  onDestroy(() => {
@@ -9,6 +9,7 @@
9
9
  style?: any | string
10
10
  zoom?: number
11
11
  center?: any
12
+ bearing?: number
12
13
  maxPitch?: number
13
14
  map?: any
14
15
  children?: Snippet<[any]>
@@ -19,6 +20,7 @@
19
20
  style = { version: 8, sources: {}, layers: [] },
20
21
  zoom = $bindable(undefined),
21
22
  center = $bindable(undefined),
23
+ bearing = 0,
22
24
  maxPitch = 60,
23
25
  map = $bindable(undefined),
24
26
  children,
@@ -45,13 +47,18 @@
45
47
  target.on("sourcedata", tryCollapse)
46
48
  }
47
49
 
50
+ /* Стиль, с которым создана или на который переключена карта — setStyle только при его реальной смене */
51
+ let appliedStyle: any = undefined
52
+
48
53
  const createMap = () => {
49
54
  if (!window.maplibregl || map || !container) return
55
+ appliedStyle = style
50
56
  map = new window.maplibregl.Map({
51
57
  container,
52
58
  style,
53
59
  zoom,
54
60
  center,
61
+ bearing,
55
62
  maxPitch,
56
63
  attributionControl: { compact: true },
57
64
  })
@@ -63,7 +70,8 @@
63
70
  $effect(createMap)
64
71
 
65
72
  $effect(() => {
66
- if (map && style) {
73
+ if (map && style && style !== appliedStyle) {
74
+ appliedStyle = style
67
75
  mapCtx.setStyle(style)
68
76
  }
69
77
  })
@@ -4,6 +4,7 @@ interface Props {
4
4
  style?: any | string;
5
5
  zoom?: number;
6
6
  center?: any;
7
+ bearing?: number;
7
8
  maxPitch?: number;
8
9
  map?: any;
9
10
  children?: Snippet<[any]>;
package/dist/types.d.ts CHANGED
@@ -116,8 +116,13 @@ export interface IFileAttachProps {
116
116
  name?: string;
117
117
  class?: string;
118
118
  };
119
- type?: "file" | "image";
119
+ type?: "file" | "image" | "button";
120
120
  accept?: string;
121
+ info?: {
122
+ text: string;
123
+ side: "top" | "bottom" | "left" | "right";
124
+ };
125
+ icon?: ConstructorOfATypedSvelteComponent | string | null;
121
126
  imageSize?: {
122
127
  height?: string;
123
128
  width?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.75.0",
3
+ "version": "1.76.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"