wx-svelte-core 2.4.1 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wx-svelte-core",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "SVAR Svelte Core - Svelte UI library of 20+ components and form controls",
5
5
  "productTag": "core",
6
6
  "productTrial": false,
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "homepage": "https://svar.dev/svelte/core/",
35
35
  "dependencies": {
36
- "@svar-ui/core-locales": "2.4.1",
37
- "@svar-ui/lib-dom": "0.12.0",
36
+ "@svar-ui/core-locales": "2.5.0",
37
+ "@svar-ui/lib-dom": "0.12.1",
38
38
  "@svar-ui/lib-svelte": "0.5.2"
39
39
  },
40
40
  "files": [
package/readme.md CHANGED
@@ -18,7 +18,9 @@
18
18
 
19
19
  </div>
20
20
 
21
- [SVAR Svelte Core library](https://svar.dev/svelte/core/) offers a set of 20+ ready-made Svelte UI components: form controls, popups, date and time picker, selects, notifications, and more. All components are lightweight, responsive, fast-performing, and support TypeScript. The library comes in beautifully designed light and dark themes that are easy to customize.
21
+ [SVAR Svelte Core library](https://svar.dev/svelte/core/) offers 30+ lightweight, fast-performing Svelte UI components with TypeScript support. Beautifully designed light and dark themes are included and fully customizable via CSS variables. A straightforward API and comprehensive documentation make it easy to start building feature-rich Svelte interfaces faster.
22
+
23
+
22
24
 
23
25
  <img src="https://svar.dev/images/github/github-core.png" alt="SVAR Core - Svelte UI Components Library" style="width: 752px;">
24
26
 
@@ -0,0 +1,153 @@
1
+ <script>
2
+ let { value, size = 32, limit } = $props();
3
+
4
+ const DEFAULT_BG = "#dfe2e6";
5
+ const DEFAULT_FONT = "#2c2f3c";
6
+
7
+ /** Overlap factor: each avatar after the first adds 75% of size (25% overlap). */
8
+ const OVERLAP_FACTOR = 0.75;
9
+
10
+ let containerEl = $state(null);
11
+ let containerWidth = $state(null);
12
+
13
+ const users = $derived.by(() => {
14
+ if (!value) return [];
15
+ return Array.isArray(value) ? value : [value];
16
+ });
17
+
18
+ /** Max avatars that fit in container. Formula: width = size + (n-1) * size * 0.75. */
19
+ const maxFitting = $derived.by(() => {
20
+ if (containerWidth == null || containerWidth <= 0) {
21
+ return null;
22
+ }
23
+ const n = 1 + (containerWidth / size - 1) / OVERLAP_FACTOR;
24
+ return Math.max(1, Math.floor(n));
25
+ });
26
+
27
+ const displayCount = $derived.by(() => {
28
+ const cap =
29
+ limit != null ? Math.min(users.length, limit) : users.length;
30
+ if (maxFitting != null) {
31
+ return Math.min(cap, maxFitting);
32
+ }
33
+ return cap;
34
+ });
35
+
36
+ const displayUsers = $derived(users.slice(0, displayCount));
37
+ const overflowCount = $derived(Math.max(0, users.length - displayCount));
38
+
39
+ $effect(() => {
40
+ const el = containerEl;
41
+ if (!el) return;
42
+ const ro = new ResizeObserver(entries => {
43
+ const entry = entries[0];
44
+ if (entry) containerWidth = entry.contentRect.width;
45
+ });
46
+ ro.observe(el);
47
+ return () => ro.disconnect();
48
+ });
49
+
50
+ function getInitials(name) {
51
+ name = name?.trim() || "";
52
+ if (!name) return "";
53
+ const words = name.split(/\s+/);
54
+ return (words[0][0] + (words[1]?.[0] || "")).toUpperCase().slice(0, 2);
55
+ }
56
+
57
+ function getContrastColor(hex) {
58
+ if (!hex) return DEFAULT_FONT;
59
+ let h = hex.replace("#", "");
60
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
61
+ if (h.length !== 6) return DEFAULT_FONT;
62
+ const r = parseInt(h.slice(0, 2), 16) / 255;
63
+ const g = parseInt(h.slice(2, 4), 16) / 255;
64
+ const b = parseInt(h.slice(4, 6), 16) / 255;
65
+ const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
66
+ return luminance > 0.5 ? DEFAULT_FONT : "#ffffff";
67
+ }
68
+
69
+ const fontSize = $derived(Math.round(size * 0.4));
70
+ const avatarBaseStyle = $derived(
71
+ `width:${size}px;height:${size}px;min-width:${size}px;min-height:${size}px;font-size:${fontSize}px;`
72
+ );
73
+
74
+ function getAvatarItemStyle(user, index) {
75
+ const margin = index === 0 ? "0" : `${size * -0.25}px`;
76
+ const bg = user.avatar ? "transparent" : user.color || DEFAULT_BG;
77
+ const color = user.avatar
78
+ ? "transparent"
79
+ : getContrastColor(user.color || DEFAULT_BG);
80
+ return `margin-left:${margin};background-color:${bg};color:${color};`;
81
+ }
82
+ </script>
83
+
84
+ <div class="wx-avatar-root" bind:this={containerEl}>
85
+ {#if displayUsers.length > 0}
86
+ <div class="wx-avatar-stack">
87
+ {#each displayUsers as user, index (user.id)}
88
+ <div
89
+ class="wx-avatar wx-avatar-item"
90
+ class:wx-avatar-overflow={index ===
91
+ displayUsers.length - 1 && overflowCount > 0}
92
+ style={avatarBaseStyle + getAvatarItemStyle(user, index)}
93
+ >
94
+ {#if user.avatar}
95
+ <img src={user.avatar} alt="" loading="lazy" />
96
+ {:else if getInitials(user.name)}
97
+ <span>{getInitials(user.name)}</span>
98
+ {/if}
99
+ {#if index === displayUsers.length - 1 && overflowCount > 0}
100
+ <span class="wx-avatar-overflow-badge"
101
+ >+{overflowCount}</span
102
+ >
103
+ {/if}
104
+ </div>
105
+ {/each}
106
+ </div>
107
+ {/if}
108
+ </div>
109
+
110
+ <style>
111
+ .wx-avatar {
112
+ position: relative;
113
+ border-radius: 50%;
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ overflow: hidden;
118
+ font-weight: 600;
119
+ line-height: 1;
120
+ user-select: none;
121
+ }
122
+
123
+ .wx-avatar img {
124
+ width: 100%;
125
+ height: 100%;
126
+ object-fit: cover;
127
+ }
128
+
129
+ .wx-avatar span {
130
+ text-transform: uppercase;
131
+ }
132
+
133
+ .wx-avatar-overflow .wx-avatar-overflow-badge {
134
+ position: absolute;
135
+ inset: 0;
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ background: rgba(0, 0, 0, 0.5);
140
+ color: #fff;
141
+ text-transform: none;
142
+ }
143
+
144
+ .wx-avatar-stack {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ }
148
+
149
+ .wx-avatar-root {
150
+ display: block;
151
+ min-width: 0;
152
+ }
153
+ </style>
@@ -87,7 +87,7 @@
87
87
  setCurrentColor(true);
88
88
  }
89
89
 
90
- onMount(() => setSlidersPosition());
90
+ onMount(() => requestAnimationFrame(setSlidersPosition));
91
91
 
92
92
  function setSlidersPosition() {
93
93
  const [h, s, v] = colorTransformator.hexToHvs(color);
@@ -12,6 +12,7 @@
12
12
  error = false,
13
13
  clear = false,
14
14
  onchange,
15
+ dropdown = {},
15
16
  } = $props();
16
17
 
17
18
  const inputId = $state(getInputId(id));
@@ -58,7 +59,7 @@
58
59
  {/if}
59
60
 
60
61
  {#if popup}
61
- <Dropdown oncancel={() => (popup = false)}>
62
+ <Dropdown oncancel={() => (popup = false)} {...dropdown}>
62
63
  <ColorBoard {value} button="true" onchange={selectColor} />
63
64
  </Dropdown>
64
65
  {/if}
@@ -22,8 +22,8 @@
22
22
  disabled = false,
23
23
  error = false,
24
24
  onchange,
25
+ dropdown = {},
25
26
  } = $props();
26
-
27
27
  const inputId = $state(getInputId(id));
28
28
 
29
29
  let popup = $state(false);
@@ -75,7 +75,7 @@
75
75
  {/if}
76
76
 
77
77
  {#if popup}
78
- <Dropdown oncancel={() => (popup = false)}>
78
+ <Dropdown oncancel={() => (popup = false)} {...dropdown}>
79
79
  <div class="wx-colors">
80
80
  <!-- svelte-ignore a11y_click_events_have_key_events -->
81
81
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -16,6 +16,7 @@
16
16
  clear = false,
17
17
  children: kids,
18
18
  onchange,
19
+ dropdown = {},
19
20
  } = $props();
20
21
 
21
22
  const inputId = $state(getInputId(id));
@@ -135,7 +136,12 @@
135
136
  {:else}<i class="wx-icon wxi-angle-down"></i>{/if}
136
137
 
137
138
  {#if !disabled}
138
- <List items={filteredOptions} onready={ready} onselect={selectByEvent}>
139
+ <List
140
+ items={filteredOptions}
141
+ onready={ready}
142
+ onselect={selectByEvent}
143
+ {...dropdown}
144
+ >
139
145
  {#snippet children({ option })}
140
146
  {#if kids}{@render kids({ option })}{:else}{option[
141
147
  textField
@@ -6,14 +6,13 @@
6
6
  import Dropdown from "./Dropdown.svelte";
7
7
  import Calendar from "./Calendar.svelte";
8
8
  import { defaultLocale } from "./helpers/locale";
9
+ import { toDateDropdown } from "./helpers/dropdown";
9
10
 
10
11
  let {
11
12
  value = $bindable(),
12
13
  id,
13
14
  disabled = false,
14
15
  error = false,
15
- width = "unset",
16
- align = "start",
17
16
  placeholder = "",
18
17
  format = "",
19
18
  buttons = ["clear", "today"],
@@ -22,6 +21,7 @@
22
21
  editable = false,
23
22
  clear = false,
24
23
  onchange,
24
+ dropdown = {},
25
25
  } = $props();
26
26
 
27
27
  const { calendar: calendarLocale, formats } = (
@@ -98,7 +98,7 @@
98
98
  />
99
99
 
100
100
  {#if popup && !disabled}
101
- <Dropdown {oncancel} {width} {align} autoFit={!!align}>
101
+ <Dropdown {oncancel} {...toDateDropdown(dropdown)}>
102
102
  <Calendar {buttons} {value} onchange={e => doChange(e.value)} />
103
103
  </Dropdown>
104
104
  {/if}
@@ -6,14 +6,13 @@
6
6
  import Dropdown from "./Dropdown.svelte";
7
7
  import RangeCalendar from "./RangeCalendar.svelte";
8
8
  import { defaultLocale } from "./helpers/locale";
9
+ import { toDateDropdown } from "./helpers/dropdown";
9
10
 
10
11
  let {
11
12
  value = $bindable(),
12
13
  id,
13
14
  disabled = false,
14
15
  error = false,
15
- width = "unset",
16
- align = "start",
17
16
  placeholder = "",
18
17
  css = "",
19
18
  title = "",
@@ -23,6 +22,7 @@
23
22
  editable = false,
24
23
  clear = false,
25
24
  onchange,
25
+ dropdown,
26
26
  } = $props();
27
27
 
28
28
  const { calendar: calendarLocale, formats } = (
@@ -113,7 +113,7 @@
113
113
  />
114
114
 
115
115
  {#if popup && !disabled}
116
- <Dropdown {oncancel} {width} {align} autoFit={!!align}>
116
+ <Dropdown {oncancel} {...toDateDropdown(dropdown)}>
117
117
  <RangeCalendar
118
118
  {oncancel}
119
119
  {buttons}
@@ -1,126 +1,44 @@
1
1
  <script>
2
- import { clickOutside, env } from "@svar-ui/lib-dom";
2
+ import { onMount } from "svelte";
3
+ import { Portal } from "../index.js";
4
+ import Popup from "./Popup.svelte";
5
+ import InlineDropdown from "./helpers/InlineDropdown.svelte";
3
6
 
4
7
  let {
5
8
  position = "bottom",
6
9
  align = "start",
7
10
  autoFit = true,
11
+ inline = false,
8
12
  oncancel,
9
13
  width = "100%",
10
- children,
14
+ ...props
11
15
  } = $props();
12
16
 
13
- let node;
14
- $effect(() => {
15
- if (autoFit) {
16
- const nodeCoords = node.getBoundingClientRect();
17
- const bodyCoords = env.getTopNode(node).getBoundingClientRect();
17
+ let target = $state();
18
+ let node = $state();
18
19
 
19
- if (nodeCoords.right >= bodyCoords.right) {
20
- align = "end";
21
- }
20
+ const at = $derived(`${position}-${align}`);
22
21
 
23
- if (nodeCoords.bottom >= bodyCoords.bottom) {
24
- position = "top";
25
- }
26
- return `${position}-${align}`;
27
- }
22
+ onMount(() => {
23
+ // get the parent element before
24
+ // the popup is moved to the portal
25
+ target = node.parentNode;
28
26
  });
29
-
30
- function down(e) {
31
- oncancel && oncancel(e);
32
- }
33
27
  </script>
34
28
 
35
- <div
36
- use:clickOutside={down}
37
- bind:this={node}
38
- class="wx-dropdown {`wx-${position}-${align}`}"
39
- style="width:{width}"
40
- >
41
- {@render children?.()}
42
- </div>
43
-
44
- <style>
45
- .wx-dropdown {
46
- position: absolute;
47
- z-index: 5;
48
- background: var(--wx-popup-background);
49
- box-shadow: var(--wx-popup-shadow);
50
- border: var(--wx-popup-border);
51
- border-radius: var(--wx-popup-border-radius);
52
- overflow: hidden;
53
- }
54
-
55
- .wx-top-center {
56
- top: 0;
57
- left: 50%;
58
- transform: translate(-50%, -100%) translateY(-2px);
59
- }
60
-
61
- .wx-top-start {
62
- top: 0;
63
- left: 0;
64
- transform: translateY(-100%) translateY(-2px);
65
- }
66
-
67
- .wx-top-end {
68
- top: 0;
69
- right: 0;
70
- transform: translateY(-100%) translateY(-2px);
71
- }
29
+ {#if inline}
30
+ <InlineDropdown {oncancel} {position} {align} {autoFit} {width} {...props}
31
+ ></InlineDropdown>
32
+ {:else}
33
+ <Portal>
34
+ <Popup parent={target} {at} {oncancel} {width} {...props}></Popup>
35
+ </Portal>
36
+ {/if}
72
37
 
73
- .wx-bottom-center {
74
- bottom: 0;
75
- left: 50%;
76
- transform: translate(-50%, 100%) translateY(2px);
77
- }
78
-
79
- .wx-bottom-start {
80
- bottom: 0;
81
- left: 0;
82
- transform: translateY(100%) translateY(2px);
83
- }
38
+ <span bind:this={node} class="wx-portal-node"></span>
84
39
 
85
- .wx-bottom-end {
86
- bottom: 0;
87
- right: 0;
88
- transform: translateY(100%) translateY(2px);
89
- }
90
-
91
- .wx-left-center {
92
- bottom: 50%;
93
- left: 0;
94
- transform: translate(-100%, 50%) translateX(-2px);
95
- }
96
-
97
- .wx-left-start {
98
- top: 0;
99
- left: 0;
100
- transform: translateX(-100%) translateX(-2px);
101
- }
102
-
103
- .wx-left-end {
104
- bottom: 0;
105
- left: 0;
106
- transform: translateX(-100%) translateX(-2px);
107
- }
108
-
109
- .wx-right-center {
110
- bottom: 50%;
111
- right: 0;
112
- transform: translate(100%, 50%) translateX(2px);
113
- }
114
-
115
- .wx-right-start {
116
- top: 0;
117
- right: 0;
118
- transform: translateX(100%) translateX(2px);
119
- }
120
-
121
- .wx-right-end {
122
- bottom: 0;
123
- right: 0;
124
- transform: translateX(100%) translateX(2px);
40
+ <style>
41
+ .wx-portal-node {
42
+ display: none;
125
43
  }
126
44
  </style>
@@ -8,18 +8,12 @@
8
8
  error = false,
9
9
  type = "",
10
10
  required = false,
11
+ id,
11
12
  children,
12
13
  } = $props();
13
14
 
14
- let firstInputId = $state(null);
15
-
16
- const registerInput = () => {
17
- const id = uid();
18
- if (!firstInputId) firstInputId = id;
19
- return id;
20
- };
21
-
22
- setContext("wx-input-id", registerInput);
15
+ const inputId = id === undefined ? uid() : id;
16
+ setContext("wx-input-id", inputId);
23
17
  </script>
24
18
 
25
19
  <div
@@ -29,8 +23,8 @@
29
23
  style={width ? `width: ${width}` : ""}
30
24
  >
31
25
  {#if label}
32
- {#if firstInputId}
33
- <label class="wx-label" for={firstInputId}>{label}</label>
26
+ {#if inputId}
27
+ <label class="wx-label" for={inputId}>{label}</label>
34
28
  {:else}
35
29
  <div class="wx-label">{label}</div>
36
30
  {/if}
@@ -1,6 +1,5 @@
1
1
  <script>
2
2
  import List from "./helpers/SuggestDropdown.svelte";
3
- import Checkbox from "./Checkbox.svelte";
4
3
  import { getInputId } from "./helpers/getInputId.js";
5
4
 
6
5
  let {
@@ -16,6 +15,7 @@
16
15
  checkboxes = false,
17
16
  onchange,
18
17
  children,
18
+ dropdown = {},
19
19
  } = $props();
20
20
 
21
21
  const inputId = $state(getInputId(id));
@@ -48,22 +48,9 @@
48
48
  }
49
49
  function onselect(ev) {
50
50
  const { id } = ev;
51
-
52
51
  if (id) {
53
- let next;
54
- if (value) {
55
- if (value.includes(id)) {
56
- next = value.filter(i => i !== id);
57
- } else {
58
- next = [...value, id];
59
- }
60
- } else {
61
- next = [id];
62
- }
63
-
64
- value = next;
65
- onchange && onchange({ value });
66
-
52
+ value = id;
53
+ onchange && onchange({ value: id });
67
54
  inputElement.focus();
68
55
  }
69
56
  }
@@ -134,15 +121,16 @@
134
121
  </div>
135
122
 
136
123
  {#if !disabled}
137
- <List items={filterOptions} {onready} {onselect}>
124
+ <List
125
+ items={filterOptions}
126
+ multiselect={true}
127
+ {onready}
128
+ {onselect}
129
+ {checkboxes}
130
+ {value}
131
+ {...dropdown}
132
+ >
138
133
  {#snippet children({ option })}
139
- {#if checkboxes}
140
- <Checkbox
141
- style="margin-right: 8px; pointer-events: none;"
142
- name={option.id}
143
- value={value && value.includes(option.id)}
144
- />
145
- {/if}
146
134
  {#if children}{@render children({
147
135
  option,
148
136
  })}{:else}{option[textField]}{/if}
@@ -103,6 +103,7 @@
103
103
  <style>
104
104
  .wx-pager {
105
105
  display: flex;
106
+ flex-wrap: wrap;
106
107
  gap: var(--wx-padding);
107
108
  align-items: center;
108
109
  padding: var(--wx-padding);
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { clickOutside, calculatePosition } from "@svar-ui/lib-dom";
2
+ import { clickOutside, calculatePosition, getAbsParent } from "@svar-ui/lib-dom";
3
3
  import { onMount } from "svelte";
4
4
 
5
5
  let {
@@ -7,32 +7,60 @@
7
7
  top = 0,
8
8
  at = "bottom",
9
9
  parent = null,
10
+ width = "auto",
11
+ css = "",
10
12
  oncancel,
11
13
  children,
14
+ trackScroll = false,
12
15
  } = $props();
13
16
 
14
- let self = null;
17
+ let self = $state(null);
15
18
  let x = $state(0);
16
19
  let y = $state(0);
17
- let width = $state("auto");
20
+ let w = $state("auto");
21
+ let portal;
22
+
23
+ function getWidth(calcWidth) {
24
+ if (parent && (width + "").indexOf("%") > -1) {
25
+ return width.replace(/(\d+)%/, (match, value) => {
26
+ value = (value * parent.offsetWidth) / 100 + "px";
27
+ return width.replace(match, value);
28
+ });
29
+ }
30
+ return width && width !== "auto" ? width : calcWidth;
31
+ }
18
32
 
19
33
  function updatePosition() {
20
34
  if (!self) return;
21
-
22
35
  const result = calculatePosition(self, parent, at, left, top);
23
36
  if (result) {
24
37
  x = result.x;
25
38
  y = result.y;
26
- width = result.width;
39
+ w = getWidth(result.width);
27
40
  }
28
41
  }
29
42
 
43
+ function onScroll(e) {
44
+ if (oncancel && e.target !== portal && !self.contains(e.target))
45
+ oncancel(e);
46
+ }
47
+
30
48
  onMount(() => {
31
- updatePosition();
32
- requestAnimationFrame(updatePosition);
49
+ requestAnimationFrame(() => {
50
+ updatePosition();
51
+ if (trackScroll) {
52
+ portal = getAbsParent(self);
53
+ if (portal) portal.addEventListener("scroll", onScroll, true);
54
+ }
55
+ });
56
+ return () => {
57
+ if (trackScroll && portal)
58
+ portal.removeEventListener("scroll", onScroll, true);
59
+ };
33
60
  });
61
+
34
62
  $effect(() => {
35
- updatePosition(parent);
63
+ updatePosition();
36
64
  });
37
65
 
38
66
  function down(e) {
@@ -43,8 +71,8 @@
43
71
  <div
44
72
  use:clickOutside={down}
45
73
  bind:this={self}
46
- class="wx-popup"
47
- style="position:absolute;top:{y}px;left:{x}px;width:{width};"
74
+ class="wx-popup {css}"
75
+ style="position:absolute;top:{y}px;left:{x}px;width:{w};"
48
76
  >
49
77
  {@render children?.()}
50
78
  </div>
@@ -20,6 +20,7 @@
20
20
  }
21
21
  return p;
22
22
  }
23
+
23
24
  onMount(() => {
24
25
  let currentTarget = target || getParentRoot(portal);
25
26
  currentTarget.appendChild(portal);
@@ -13,6 +13,7 @@
13
13
  clear = false,
14
14
  children: kids,
15
15
  onchange,
16
+ dropdown = {},
16
17
  } = $props();
17
18
 
18
19
  let navigate;
@@ -73,7 +74,7 @@
73
74
  {:else}<i class="wx-icon wxi-angle-down"></i>{/if}
74
75
 
75
76
  {#if !disabled}
76
- <List items={options} onready={ready} onselect={select}>
77
+ <List items={options} onready={ready} onselect={select} {...dropdown}>
77
78
  {#snippet children({ option })}
78
79
  {#if kids}{@render kids(option)}{:else}{option[textField]}{/if}
79
80
  {/snippet}