poe-svelte-ui-lib 1.17.5 → 1.18.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.
@@ -1,3 +1,5 @@
1
+ <!-- $lib/Accordion/Accordion.svelte — заголовок больше не увеличивается при раскрытии,
2
+ тень на hover заменена фоном, радиус 14. Логика toggle не тронута. -->
1
3
  <script lang="ts">
2
4
  import { slide } from "svelte/transition"
3
5
  import type { IAccordionProps } from "../types"
@@ -16,64 +18,42 @@
16
18
  const toggle = () => (isOpen = !isOpen)
17
19
  </script>
18
20
 
19
- <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge(`w-full p-1`, wrapperClass)} transition:slide={{ duration: 250 }}>
20
- <div class="shadow-(--border-shadow-color) hover:shadow-(--focus-shadow-color) transition-shadow duration-250 rounded-xl bg-(--container-color)">
21
- <button class="flex w-full cursor-pointer items-center justify-between p-4 py-3.5" onclick={toggle}>
22
- <div class="flex w-full items-center">
23
- <span class={`flex h-7 w-7 shrink-0 items-center justify-center overflow-visible [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full`}>
24
- {#if label?.icon}
21
+ <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge(`w-full`, wrapperClass)} transition:slide={{ duration: 200 }}>
22
+ <div class="overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
23
+ <button
24
+ class="flex w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left transition-colors duration-150 hover:bg-(--container-color)"
25
+ onclick={toggle}
26
+ >
27
+ <div class="flex min-w-0 flex-1 items-center gap-3">
28
+ {#if label?.icon}
29
+ <span class="flex h-[18px] w-[18px] shrink-0 items-center justify-center text-(--muted-color) [&_svg]:h-full [&_svg]:w-full">
25
30
  {#if typeof label?.icon === "string"}
26
31
  {@html label.icon}
27
32
  {:else}
28
33
  {@const IconComponent = label?.icon}
29
34
  <IconComponent />
30
35
  {/if}
31
- {/if}
32
- </span>
33
-
34
- <div
35
- class="{twMerge(
36
- 'flex w-full justify-start ',
37
- label.class?.split(' ').find((cls: string) => cls.startsWith('justify-')),
38
- )} "
39
- >
40
- <span
41
- class="{twMerge(
42
- `m-0 origin-top-left cursor-pointer justify-end px-3 font-semibold transition duration-200 ${isOpen ? 'scale-110' : ''}`,
43
- label.class,
44
- )} text-lg"
45
- >
46
- {label?.name}
47
36
  </span>
48
- </div>
37
+ {/if}
38
+ <span class={twMerge(`min-w-0 truncate text-[14px] font-semibold`, label.class)}>{label?.name}</span>
49
39
  </div>
50
40
 
51
- <div class="w-10 pl-4">
52
- <svg
53
- xmlns="http://www.w3.org/2000/svg"
54
- class="h-[1.1rem] w-[1.1rem] transition-transform duration-250"
55
- style="transform: rotate({isOpen ? 180 : 0}deg)"
56
- viewBox="0 0 24 24"
57
- >
58
- <path
59
- fill="none"
60
- stroke="currentColor"
61
- stroke-linecap="round"
62
- stroke-linejoin="round"
63
- stroke-width="2"
64
- d="M18 12.5s-4.419 6-6 6s-6-6-6-6m12-7s-4.419 6-6 6s-6-6-6-6"
65
- color="currentColor"
66
- />
67
- </svg>
68
- </div>
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ class="h-[14px] w-[14px] shrink-0 text-(--faint-color) transition-transform duration-200"
44
+ style="transform: rotate({isOpen ? 180 : 0}deg)"
45
+ viewBox="0 0 24 24"
46
+ >
47
+ <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
48
+ </svg>
69
49
  </button>
70
50
 
71
51
  {#if isOpen}
72
52
  <div
73
- class="grid w-full p-4 sm:p-3 {image ? 'gap-x-2' : 'gap-2'} inset-shadow-[0_10px_10px_-15px_rgb(0_0_0_/0.5)]"
74
- transition:slide={{ duration: 250 }}
75
- style="grid-template-columns: repeat({size.width || 10}, minmax(0, 1fr));
76
- grid-template-rows: repeat({size.height || 2}, {image ? 'minmax(6.5rem, auto)' : 'auto'});
53
+ class="grid w-full gap-3 border-t border-(--hairline-color) bg-(--container-color) p-4"
54
+ transition:slide={{ duration: 200 }}
55
+ style="grid-template-columns: repeat({size.width || 1}, minmax(0, 1fr));
56
+ grid-template-rows: repeat({size.height || 1}, {image ? 'minmax(6.5rem, auto)' : 'auto'});
77
57
  {image ? `background-image: url(${image}); background-size: cover; background-position: center;` : ''}"
78
58
  >
79
59
  {@render children?.()}
@@ -1,9 +1,17 @@
1
- <!-- $lib/ElementsUI/Button.svelte -->
1
+ <!-- $lib/Button/Button.svelte — шаг 3 плана.
2
+ API не изменён: id, wrapperClass, componentClass, disabled, content, keyBind, onClick.
3
+ Что изменилось внутри:
4
+ • роль определяется значением bg-* (намерение), а не фактом его наличия;
5
+ • убрано автоподчёркивание текста — кнопка больше не выглядит ссылкой;
6
+ • единый радиус 10px (иконка без подписи — круг), высота 36px, кегль 14/600;
7
+ • кольцо фокуса вместо размытой тени;
8
+ • disabled через токены, а не opacity: 0.5;
9
+ • иконка встала в поток (flex + gap) вместо absolute left-3. -->
2
10
  <script lang="ts">
3
11
  import { onMount } from "svelte"
4
12
  import type { IButtonProps } from "../types"
5
- import { fly } from "svelte/transition"
6
13
  import { twMerge } from "tailwind-merge"
14
+ import Tooltip from "../Tooltip.svelte"
7
15
 
8
16
  let {
9
17
  id = crypto.randomUUID(),
@@ -21,31 +29,45 @@
21
29
 
22
30
  let showInfo = $state(false)
23
31
 
24
- let tooltipConfig = $derived({
25
- top: {
26
- pos: "bottom-full left-1/2 mb-2",
27
- tr: "translateX(-50%)",
28
- arr: "top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-45",
29
- off: "y:-15",
30
- },
31
- bottom: {
32
- pos: "top-full left-1/2 mt-2",
33
- tr: "translateX(-50%)",
34
- arr: "bottom-full left-1/2 -translate-x-1/2 translate-y-1/2 rotate-45",
35
- off: "y:15",
36
- },
37
- left: {
38
- pos: "top-1/2 right-full mr-2",
39
- tr: "translateY(-50%)",
40
- arr: "top-1/2 -right-2 -translate-x-1/2 -translate-y-1/2 rotate-45",
41
- off: "x:15",
42
- },
43
- right: {
44
- pos: "top-1/2 left-full ml-2",
45
- tr: "translateY(-50%)",
46
- arr: "top-1/2 -left-2 translate-x-1/2 -translate-y-1/2 rotate-45 ",
47
- off: "x:-15",
48
- },
32
+ /* ─── Роль кнопки по последнему классу bg-* ─────────────────────────
33
+ bg-blue / bg-green / bg-cyan / bg-purple / bg-orange / bg-yellow → primary (заливка + белый текст)
34
+ bg-red / bg-pink → destructive
35
+ bg-max / bg-gray / bg-*-soft → secondary (поверхность + контур)
36
+ bg-transparent / без bg-* → plain (текст акцентом) */
37
+ const SURFACE_BG = ["bg-max", "bg-gray"]
38
+ const DANGER_BG = ["bg-red", "bg-pink"]
39
+
40
+ const role = $derived.by(() => {
41
+ const bg = componentClass.split(/\s+/).filter((c) => c.startsWith("bg-")).at(-1) ?? ""
42
+ if (!bg || bg === "bg-transparent") return "plain"
43
+ if (SURFACE_BG.includes(bg) || bg.endsWith("-soft")) return "secondary"
44
+ if (DANGER_BG.includes(bg)) return "destructive"
45
+ return "primary"
46
+ })
47
+
48
+ const isIconOnly = $derived(!!content?.icon && !content.name?.trim())
49
+
50
+ /* bg-(--bg-color) намеренно не входит в roleClass: он приклеивается к готовому
51
+ className вне twMerge (см. разметку ниже) — иначе twMerge видит его и явный
52
+ bg-* из componentClass (bg-red, bg-max…) как конфликтующие утилиты одной
53
+ группы и вырезает один из них, из-за чего заливка кнопки пропадает. */
54
+ const roleBg = $derived.by(() => {
55
+ if (disabled) return "bg-(--gray-color)"
56
+ if (role === "plain") return "bg-transparent"
57
+ return "bg-(--bg-color)"
58
+ })
59
+
60
+ const roleClass = $derived.by(() => {
61
+ if (disabled) return "text-(--faint-color) border border-transparent cursor-not-allowed"
62
+ switch (role) {
63
+ case "primary":
64
+ case "destructive":
65
+ return "text-(--on-accent) border border-transparent shadow-(--elevation-1) hover:brightness-90 dark:hover:brightness-115"
66
+ case "secondary":
67
+ return "text-(--font-color) border border-(--border-color) hover:bg-(--container-color)"
68
+ default:
69
+ return "text-(--accent-color) border border-transparent hover:bg-(--accent-soft)"
70
+ }
49
71
  })
50
72
 
51
73
  const svgSize = $derived(() => {
@@ -60,30 +82,25 @@
60
82
  return `${widthClass} ${heightClass}`
61
83
  })
62
84
 
63
- /* Обработчик клика */
64
85
  const handleClick = (event: Event) => {
65
86
  if (disabled || !onClick) return
66
87
  event?.stopPropagation()
67
88
  onClick()
68
89
  }
69
90
 
70
- /* Обработчик горячих клавиш */
71
91
  const handleKeyDown = (event: KeyboardEvent) => {
72
92
  if (disabled || !keyBind.key || !onClick) return
73
-
74
93
  const isKeyMatch = event.key === keyBind.key
75
94
  const isCtrlMatch = keyBind.ctrlKey === undefined || event.ctrlKey === keyBind.ctrlKey
76
95
  const isShiftMatch = keyBind.shiftKey === undefined || event.shiftKey === keyBind.shiftKey
77
96
  const isAltMatch = keyBind.altKey === undefined || event.altKey === keyBind.altKey
78
97
  const isMetaMatch = keyBind.metaKey === undefined || event.metaKey === keyBind.metaKey
79
-
80
98
  if (isKeyMatch && isCtrlMatch && isShiftMatch && isAltMatch && isMetaMatch) {
81
99
  event.preventDefault()
82
100
  onClick()
83
101
  }
84
102
  }
85
103
 
86
- /* Подписка на события клавиатуры */
87
104
  onMount(() => {
88
105
  if (keyBind) window.addEventListener("keydown", handleKeyDown)
89
106
  return () => {
@@ -92,20 +109,21 @@
92
109
  })
93
110
  </script>
94
111
 
95
- <div class={twMerge(`relative flex w-full flex-col items-center `, wrapperClass)}>
112
+ <div class={twMerge(`relative flex w-full flex-col items-center`, wrapperClass)}>
96
113
  <button
97
- id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
98
- class="{twMerge(
99
- `relative inline-block items-center font-semibold w-full transition duration-200 select-none
100
- ${componentClass.split(' ').find((cl: string) => cl === 'bg-transparent') || !componentClass.split(' ').find((cl: string) => cl.startsWith('bg-')) ? 'underline' : 'border border-(--bg-color) shadow-sm hover:shadow-md'}
101
- ${content.icon && !content.name?.trim() ? 'rounded-full' : 'rounded-2xl py-1 px-2'}
102
- ${disabled ? 'cursor-not-allowed opacity-50 hover:shadow-none' : 'cursor-pointer active:scale-97'} `,
114
+ {id}
115
+ class={`${twMerge(
116
+ `relative inline-flex w-full items-center justify-center gap-2 leading-none font-semibold text-[14px] select-none
117
+ transition-[background-color,filter,box-shadow] duration-150
118
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent-color)
119
+ ${isIconOnly ? "aspect-square rounded-full p-0" : "min-h-9 rounded-[10px] px-4 py-2"}
120
+ ${disabled ? "" : "cursor-pointer active:scale-[0.97]"}`,
121
+ roleClass,
103
122
  componentClass,
104
- )} bg-(--bg-color)"
105
- style={!content.name?.trim() && content.name && content.icon ? `padding-inline: ${content.name.length / 2}rem` : ""}
123
+ )} ${roleBg}`}
106
124
  onclick={(event) => handleClick(event)}
107
125
  {disabled}
108
- aria-label={content.name}
126
+ aria-label={content.name || undefined}
109
127
  onmouseenter={() => {
110
128
  if (content.info?.text) showInfo = true
111
129
  }}
@@ -113,43 +131,30 @@
113
131
  if (content.info?.text) showInfo = false
114
132
  }}
115
133
  >
116
- <div class="flex flex-row items-center justify-center gap-2">
117
- {#if content?.icon}
118
- <span
119
- class={` ${content.name?.trim() ? "absolute left-3" : ""} ${typeof content?.icon == "string" ? "p-0.5" : ""} flex items-center justify-center overflow-visible
120
- ${content.name?.trim() ? "h-8 w-8" : `${svgSize()}`} [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full`}
121
- >
122
- {#if typeof content?.icon === "string"}
123
- {@html content.icon}
124
- {:else}
125
- {@const IconComponent = content?.icon}
126
- <IconComponent />
127
- {/if}
128
- </span>
129
- {/if}
134
+ {#if content?.icon}
135
+ <span
136
+ class={`flex shrink-0 items-center justify-center overflow-visible
137
+ ${content.name?.trim() ? "h-[18px] w-[18px]" : `${svgSize()}`}
138
+ [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full`}
139
+ >
140
+ {#if typeof content?.icon === "string"}
141
+ {@html content.icon}
142
+ {:else}
143
+ {@const IconComponent = content?.icon}
144
+ <IconComponent />
145
+ {/if}
146
+ </span>
147
+ {/if}
130
148
 
131
- {#if content.name?.trim()}
132
- <div class="flex-1 truncate">
133
- {content.name}
134
- {#if keyBind.key && keyBind.showHint}
135
- <div class="text-xs opacity-70">
136
- ({keyBind.ctrlKey ? "Ctrl+" : ""}{keyBind.shiftKey ? "Shift+" : ""}{keyBind.altKey ? "Alt+" : ""}{keyBind.key})
137
- </div>
138
- {/if}
139
- </div>
149
+ {#if content.name?.trim()}
150
+ <span class="truncate">{content.name}</span>
151
+ {#if keyBind.key && keyBind.showHint}
152
+ <span class="ml-1 text-[11px] font-medium tabular-nums opacity-60">
153
+ {keyBind.ctrlKey ? "Ctrl+" : ""}{keyBind.shiftKey ? "Shift+" : ""}{keyBind.altKey ? "Alt+" : ""}{keyBind.key}
154
+ </span>
140
155
  {/if}
141
- </div>
156
+ {/if}
142
157
  </button>
143
158
 
144
- {#if showInfo && content.info?.side && tooltipConfig[content.info?.side]}
145
- {@const config = tooltipConfig[content.info?.side]}
146
- <div
147
- transition:fly={{ [config.off.split(":")[0]]: parseInt(config.off.split(":")[1]), duration: 300 }}
148
- class={`absolute z-50 w-max max-w-xs rounded-md bg-(--container-color) px-3 py-1 text-sm shadow-lg ${config.pos}`}
149
- style={`transform: ${config.tr};`}
150
- >
151
- {content.info?.text}
152
- <div class={`absolute ${config.arr} h-2 w-2 transform bg-(--container-color)`}></div>
153
- </div>
154
- {/if}
159
+ <Tooltip show={showInfo} text={content.info?.text ?? ""} side={content.info?.side ?? "top"} />
155
160
  </div>
@@ -2,6 +2,7 @@
2
2
  import { Select, Switch, T, type IOption } from "./"
3
3
  import type { Snippet } from "svelte"
4
4
  import { fade } from "svelte/transition"
5
+ import CopiedChip from "./CopiedChip.svelte"
5
6
 
6
7
  let {
7
8
  component,
@@ -65,14 +66,7 @@
65
66
  aria-label="Копировать текст"
66
67
  >
67
68
  <div class="size-6 text-sm [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full">
68
- {#if isCopied}
69
- <div
70
- class="right-1..5 absolute top-1/2 -translate-y-1/2 transform rounded-md bg-(--green-color) px-1.5 py-1 shadow-lg"
71
- transition:fade={{ duration: 200 }}
72
- >
73
- ✓
74
- </div>
75
- {:else}
69
+ {#if !isCopied}
76
70
  <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
77
71
  <g fill="none" stroke="currentColor" stroke-width="1.5">
78
72
  <path
@@ -82,6 +76,7 @@
82
76
  </g>
83
77
  </svg>
84
78
  {/if}
79
+ <CopiedChip show={isCopied} class="right-1.5 top-1/2 -translate-y-1/2" />
85
80
  </div>
86
81
  </button>
87
82
  <pre class="overflow-x-auto">{codeText}</pre>
@@ -0,0 +1,20 @@
1
+ <!-- $lib/CopiedChip.svelte — единая всплывающая метка "Скопировано" после клика
2
+ на кнопку копирования. Раньше в каждом месте (Input, Table, Map,
3
+ ComponentExample) была своя версия: где-то текст, где-то просто "✓",
4
+ разные цвета (--green-color вместо --green-soft) и радиусы, а в
5
+ ComponentExample — опечатка в классе позиционирования (right-1..5,
6
+ невалидный Tailwind-класс, физически ничего не позиционировал). -->
7
+ <script lang="ts">
8
+ import { fade } from "svelte/transition"
9
+
10
+ let { show = false, class: className = "" }: { show?: boolean; class?: string } = $props()
11
+ </script>
12
+
13
+ {#if show}
14
+ <span
15
+ class="{className} absolute flex items-center gap-1 rounded-lg bg-(--green-soft) px-2 py-1 text-[11px] font-bold whitespace-nowrap text-(--green-color)"
16
+ transition:fade={{ duration: 150 }}
17
+ >
18
+ Скопировано
19
+ </span>
20
+ {/if}
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ show?: boolean;
3
+ class?: string;
4
+ };
5
+ declare const CopiedChip: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type CopiedChip = ReturnType<typeof CopiedChip>;
7
+ export default CopiedChip;
@@ -1,15 +1,19 @@
1
- <!-- $lib/ElementsUI/Divider.svelte -->
1
+ <!-- $lib/Divider/Divider.svelte — волосяная линия вместо скруглённой полосы 2px. -->
2
2
  <script lang="ts">
3
3
  import { twMerge } from "tailwind-merge"
4
4
  import type { IDividerProps } from "../types"
5
5
 
6
- let { id = crypto.randomUUID(), wrapperClass = "", type = "line", width = 2, color = "bg-max", anchor = "center" }: IDividerProps = $props()
6
+ let { id = crypto.randomUUID(), wrapperClass = "", type = "line", width = 1, color = "", anchor = "center" }: IDividerProps = $props()
7
7
 
8
8
  const anchorClass = { top: "justify-start", center: "justify-center", bottom: "justify-end" } as const
9
9
  </script>
10
10
 
11
11
  <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge(`flex h-full w-full flex-col ${anchorClass[anchor]}`, wrapperClass)}>
12
12
  {#if type === "line"}
13
- <div class="{twMerge('w-full rounded-full', color)} bg-(--bg-color)" style="height: {width}px;"></div>
13
+ <!-- Без bg-* класса берём --hairline-color; явный bg-* по-прежнему уважается через --bg-color -->
14
+ <div
15
+ class={twMerge("w-full", color)}
16
+ style="height: {width}px; background: {color ? 'var(--bg-color)' : 'var(--hairline-color)'};"
17
+ ></div>
14
18
  {/if}
15
19
  </div>