sveltekit-ui 1.1.80 → 1.1.82

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 CHANGED
@@ -45,13 +45,23 @@ npm run dev
45
45
 
46
46
  Copy `.env.example` to `.env` and provide `PUBLIC_APPLE_MAPKIT_JS_API_KEY` if you want to run the MapKit showcases locally.
47
47
 
48
+ Keep TypeScript on version 6 for the current Svelte tooling, even when writing JavaScript. The `.ncurc.js` configuration lets `ncu -u` update TypeScript within version 6 while other packages continue receiving normal updates.
49
+
48
50
  Before publishing, run the complete verification suite:
49
51
 
50
52
  ```sh
51
53
  npm run verify
52
54
  ```
53
55
 
54
- This checks the Svelte source, runs tests, generates the distributable package and type declarations, and builds the documentation site.
56
+ This checks the Svelte source, runs tests, generates and verifies the distributable package, and builds the documentation site.
57
+
58
+ Publish with `npm run publish:npm`. It checks npm for the newest published release, selects the next patch version when needed, keeps `package.json` and `package-lock.json` in sync, and runs verification before publishing. A prepared version that is newer than the published releases is reused, so retrying a failed publish does not skip another version.
59
+
60
+ Preview the version without changing files or publishing:
61
+
62
+ ```sh
63
+ npm run version:available -- --dry-run
64
+ ```
55
65
 
56
66
  ## Compatibility and releases
57
67
 
@@ -1,22 +1,24 @@
1
1
  <script>
2
2
  import SuccessCheck from "../SuccessCheck/index.svelte"
3
3
  import ErrorX from "../ErrorX/index.svelte"
4
- import Icon from "../Icon/index.svelte"
5
4
  import Button from "../Button/index.svelte"
5
+ import { untrack } from "svelte"
6
+ import { flip } from "svelte/animate"
6
7
  import { fly } from "svelte/transition"
7
8
 
8
9
  let { manager = null, options = null, on_alert_added } = $props()
9
10
 
10
11
  $effect(() => {
11
12
  const current_manager = manager
12
- current_manager?.run_auto_delete?.()
13
+ // Only manager replacement should restart the lifecycle, not timer or queue updates.
14
+ untrack(() => current_manager?.run_auto_delete?.())
13
15
  return () => current_manager?.destroy?.()
14
16
  })
15
17
  </script>
16
18
 
17
19
  <div class="alerts_container" role="region" aria-label="Notifications" aria-live="polite">
18
20
  {#if Array.isArray(manager?.alerts) && manager?.alerts.length > 0}
19
- {#each manager?.alerts as alert, i (alert)}
21
+ {#each manager?.alerts as alert (alert.id)}
20
22
  <div
21
23
  class="card alert_box"
22
24
  role="status"
@@ -24,6 +26,7 @@
24
26
  class:small_alert_container={manager?.is_small_screen}
25
27
  style="--br: {manager?.border_radius}rem;"
26
28
  transition:fly={{ x: 300, duration: 400 }}
29
+ animate:flip={{ duration: 250 }}
27
30
  >
28
31
  <div class="icon">
29
32
  {#if alert.icon === "check"}
@@ -46,11 +49,17 @@
46
49
 
47
50
  <style>
48
51
  .alerts_container {
49
- position: absolute;
52
+ position: fixed;
53
+ top: 0;
50
54
  right: 0;
51
- overflow: scroll;
55
+ width: 32rem;
56
+ max-width: 100%;
57
+ overflow-x: hidden;
58
+ overflow-y: auto;
52
59
  max-height: 100vh;
53
- z-index: 10;
60
+ max-height: 100dvh;
61
+ z-index: 11;
62
+ pointer-events: none;
54
63
  }
55
64
  .alerts_container h3 {
56
65
  font-size: clamp(2.2rem, 4.2vw, 2.4rem);
@@ -58,7 +67,6 @@
58
67
  }
59
68
  .alert_container {
60
69
  position: relative;
61
- width: 30rem;
62
70
  height: 7rem;
63
71
  display: flex;
64
72
  align-items: center;
@@ -70,6 +78,7 @@
70
78
  align-items: center;
71
79
  }
72
80
  .alert_box {
81
+ pointer-events: auto;
73
82
  border-radius: var(--br);
74
83
  border: 1px solid var(--g15-t);
75
84
  box-shadow: 0 0.2rem 1rem var(--shadow3);
@@ -94,7 +94,13 @@ export function create_checkbox_manager(config) {
94
94
  is_no_wrap: true,
95
95
  mr: 0.5,
96
96
  text: () => (val == null ? "Select Value" : "Set Null"),
97
- on_click: () => set_val(val == null ? true : null),
97
+ is_disabled: () => is_disabled || is_read_only || is_loading,
98
+ on_click: () => {
99
+ set_val(val == null ? on_val : null)
100
+ if (typeof config?.on_change == "function") {
101
+ config.on_change(val)
102
+ }
103
+ },
98
104
  })
99
105
  }
100
106
 
@@ -84,6 +84,7 @@ export function create_dropdown_manager(config) {
84
84
  font_weight: font_weight,
85
85
  is_compressed: type == "horiz_selector",
86
86
  is_no_wrap: type == "horiz_selector",
87
+ is_disabled: () => is_disabled,
87
88
  mb: item_mb,
88
89
  l: 2,
89
90
  c: 1,
@@ -130,6 +131,7 @@ export function create_dropdown_manager(config) {
130
131
  }
131
132
 
132
133
  function item_clicked(item) {
134
+ if (is_disabled) return
133
135
  if (item == null || item?.[val_key] == null) {
134
136
  val = null
135
137
  } else if (is_multiselect) {
@@ -224,6 +226,7 @@ export function create_dropdown_manager(config) {
224
226
  val = set_closurable(config?.val ?? null)
225
227
  search_text_input_manager = create_text_input_manager({
226
228
  type: "search",
229
+ is_disabled: () => is_disabled,
227
230
  on_change: (input) => {
228
231
  if (typeof config?.on_search == "function") {
229
232
  get_db_search_results(input)
@@ -261,7 +264,7 @@ export function create_dropdown_manager(config) {
261
264
  font_size: font_size,
262
265
  font_weight: font_weight,
263
266
  mb: item_mb,
264
- is_disabled: () => val == null || (Array.isArray(val) && val.length < 1),
267
+ is_disabled: () => is_disabled || val == null || (Array.isArray(val) && val.length < 1),
265
268
  is_compressed: type == "horiz_selector",
266
269
  is_no_wrap: type == "horiz_selector",
267
270
  on_click: () => item_clicked(null),
@@ -11,6 +11,7 @@
11
11
  </script>
12
12
 
13
13
  <button
14
+ type="button"
14
15
  class="side_bar_toggle_button"
15
16
  style="--color_base: {color_base}; --color_hover: {color_hover};"
16
17
  onpointerenter={() => (is_hovered = true)}
@@ -18,6 +19,7 @@
18
19
  class:is_hovered
19
20
  onclick={() => toggle()}
20
21
  aria-label="Hamburger Toggle"
22
+ aria-expanded={!!val}
21
23
  >
22
24
  <span class="bar_base top_line" class:top_line_x={val}></span>
23
25
  <span class="bar_base mid_line" class:mid_line_x={val}></span>
@@ -9,13 +9,16 @@
9
9
  if (typeof manager?.handle_mount == "function") {
10
10
  manager?.handle_mount()
11
11
  }
12
+ return () => manager?.handle_pointer_event(null, "cancel", manager?.id)
12
13
  })
13
14
  </script>
14
15
 
15
16
  <svelte:document
16
17
  onpointerup={(e) => manager?.handle_pointer_event(e, "up", manager?.id)}
17
18
  onpointermove={(e) => manager?.handle_pointer_event(e, "move", manager?.id)}
19
+ onpointercancel={(e) => manager?.handle_pointer_event(e, "cancel", manager?.id)}
18
20
  />
21
+ <svelte:window onblur={() => manager?.handle_pointer_event(null, "cancel", manager?.id)} />
19
22
 
20
23
  <div
21
24
  id={`container_${manager?.id}`}
@@ -29,17 +32,20 @@
29
32
  <img class="top_img" src={manager?.src2} alt="New" style="width: calc({manager?.percent_shown}% - 2px);" />
30
33
  <input
31
34
  type="range"
32
- min="1"
35
+ min="0"
33
36
  max="100"
37
+ step="any"
34
38
  value={manager?.percent_shown}
35
39
  class="slider"
36
40
  name="slider"
37
- id="slider"
41
+ id={`image_slider_${manager?.id}`}
42
+ aria-label={manager?.aria_label}
43
+ oninput={(e) => manager?.set_percent_shown(e.currentTarget.value)}
38
44
  onpointerdown={(e) => manager?.handle_pointer_event(e, "down", manager?.id)}
39
45
  />
40
46
  {#if manager?.aspect_ratio}
41
47
  <div class="line" style="left: calc({manager?.percent_shown}% - 6px);"></div>
42
- <div class="slider_button" style="left: calc({manager?.percent_shown}% - 18px);">
48
+ <div class="slider_button" aria-hidden="true" style="left: calc({manager?.percent_shown}% - 18px);">
43
49
  <Button manager={manager?.button_manager} />
44
50
  </div>
45
51
  {/if}
@@ -88,6 +94,12 @@
88
94
  display: flex;
89
95
  justify-content: center;
90
96
  align-items: center;
97
+ touch-action: none;
98
+ }
99
+ .slider:focus-visible {
100
+ outline: 2px solid var(--selected-t);
101
+ outline-offset: 2px;
102
+ border-radius: 1rem;
91
103
  }
92
104
  .slider::-webkit-slider-thumb {
93
105
  -webkit-appearance: none;
@@ -7,14 +7,17 @@ export function create_image_slider_manager(config) {
7
7
  let src2 = $derived(set_closurable(config?.src2, null))
8
8
  let color1 = $derived(set_closurable(config?.color1, "var(--primary-t)"))
9
9
  let color2 = $derived(set_closurable(config?.color2, "color-mix(in oklab, var(--primary-t), var(--bg) 80%)"))
10
+ let aria_label = $derived(set_closurable(config?.aria_label, "Image comparison"))
10
11
  let percent_shown = $state(50)
11
12
 
12
13
  function init(config) {
13
- percent_shown = config?.percent_shown ?? 50
14
+ set_percent_shown(config?.percent_shown ?? 50)
14
15
  }
15
16
  init(config)
16
17
 
17
- let touch_start_x = $state(0)
18
+ let touch_start_x = $state(null)
19
+ let is_dragging = false
20
+ let active_pointer_id = null
18
21
  let aspect_ratio = $state(null)
19
22
 
20
23
  function handle_mount() {
@@ -46,25 +49,43 @@ export function create_image_slider_manager(config) {
46
49
  min_height: 3,
47
50
  is_icon_bg_tint: false,
48
51
  is_uniform: true,
52
+ tabindex: -1,
49
53
  })
50
54
 
55
+ function set_percent_shown(input) {
56
+ const next_percent = Number(input)
57
+ if (Number.isFinite(next_percent)) {
58
+ percent_shown = Math.max(0, Math.min(100, next_percent))
59
+ }
60
+ }
61
+
51
62
  function handle_pointer_event(event, type, id_input) {
52
- if (id != id_input) {
63
+ if (id != id_input || !["down", "move", "up", "cancel"].includes(type)) {
53
64
  return
54
65
  }
55
- if (event) {
56
- event.preventDefault()
57
- }
58
- if (type == "move" && !touch_start_x) {
66
+ if (type !== "down" && !is_dragging) return
67
+ if (type !== "down" && event?.pointerId != null && active_pointer_id != null && event.pointerId !== active_pointer_id) {
59
68
  return
60
- } else if (type == "up") {
69
+ }
70
+ if (type == "up" || type == "cancel") {
71
+ is_dragging = false
72
+ active_pointer_id = null
61
73
  touch_start_x = null
62
74
  return
63
75
  }
76
+ if (!event || typeof document === "undefined") return
77
+ if (type == "down" && (is_dragging || (event.button != null && event.button !== 0))) return
78
+ const container_el = document.querySelector(`#container_${id}`)
79
+ const container_rect = container_el?.getBoundingClientRect()
80
+ if (!container_rect?.width) return
81
+ if (type == "down") {
82
+ is_dragging = true
83
+ active_pointer_id = event.pointerId ?? null
84
+ event.currentTarget?.focus?.({ preventScroll: true })
85
+ }
86
+ event.preventDefault()
64
87
  touch_start_x = event.clientX
65
- let container_el = document?.querySelector(`#container_${id}`)
66
- const container_rect = container_el.getBoundingClientRect()
67
- percent_shown = Math.max(0, Math.min(100, ((touch_start_x - container_rect.left) / container_rect.width) * 100))
88
+ set_percent_shown(((touch_start_x - container_rect.left) / container_rect.width) * 100)
68
89
  }
69
90
 
70
91
  return {
@@ -81,6 +102,9 @@ export function create_image_slider_manager(config) {
81
102
  get color2() {
82
103
  return color2
83
104
  },
105
+ get aria_label() {
106
+ return aria_label
107
+ },
84
108
  get percent_shown() {
85
109
  return percent_shown
86
110
  },
@@ -95,5 +119,6 @@ export function create_image_slider_manager(config) {
95
119
  },
96
120
  handle_mount,
97
121
  handle_pointer_event,
122
+ set_percent_shown,
98
123
  }
99
124
  }
@@ -48,6 +48,9 @@
48
48
  <span style="vertical-align: middle; color: {bracket_color};">{"{"}</span>
49
49
  <div style="display: inline-block; vertical-align: middle;">
50
50
  <button
51
+ type="button"
52
+ aria-expanded={!manager?.is_collapsed}
53
+ aria-label={manager?.is_collapsed ? "Expand object" : "Collapse object"}
51
54
  style="width:1.6rem; height:1.6rem; display:flex; align-items:center; justify-content:center; padding:0; border-radius:.5rem; background:var(--shadow2-t); border:1px solid var(--shadow8-t); cursor:pointer; line-height:1; "
52
55
  onclick={manager?.toggle_is_collapsed}>{manager?.is_collapsed ? "+" : "-"}</button
53
56
  >
@@ -99,6 +102,9 @@
99
102
  <span style="vertical-align: middle; color: {bracket_color};">{"["}</span>
100
103
  <div style="display: inline-block; vertical-align: middle;">
101
104
  <button
105
+ type="button"
106
+ aria-expanded={!manager?.is_collapsed}
107
+ aria-label={manager?.is_collapsed ? "Expand array" : "Collapse array"}
102
108
  style="width:1.6rem; height:1.6rem; display:flex; align-items:center; justify-content:center; padding:0; border-radius:.5rem; background:var(--shadow2-t); border:1px solid var(--shadow8-t); cursor:pointer; line-height:1; "
103
109
  onclick={manager?.toggle_is_collapsed}>{manager?.is_collapsed ? "+" : "-"}</button
104
110
  >
@@ -57,7 +57,7 @@ export function create_json_manager(config) {
57
57
  }
58
58
 
59
59
  function get_replace_key_in_object(obj, path_remaining, old_key, new_key) {
60
- let return_obj = { ...obj }
60
+ let return_obj = Array.isArray(obj) ? [...obj] : { ...obj }
61
61
  if (Array.isArray(path_remaining) && path_remaining.length > 0) {
62
62
  const current_key = path_remaining[0]
63
63
  if (return_obj[current_key]) {
@@ -294,7 +294,7 @@ export function create_json_manager(config) {
294
294
  null,
295
295
  json_prepped_loc?.nested_levels,
296
296
  indentation,
297
- [...path, json_prepped_loc.val.length - 1],
297
+ [...path, json_prepped_loc.val.length],
298
298
  true
299
299
  ),
300
300
  })
@@ -354,7 +354,6 @@ export function create_json_manager(config) {
354
354
  json_prepped_loc.type = "array"
355
355
  json_prepped_loc.val = []
356
356
  for (let i = 0; i < input.length; i++) {
357
- const test = get_json_prepped(input?.[i], json_prepped_loc?.nested_levels, indentation, [...path, i], true)
358
357
  json_prepped_loc.val.push({
359
358
  json_prepped: get_json_prepped(
360
359
  input?.[i],
@@ -494,18 +493,18 @@ export function create_json_manager(config) {
494
493
  rows: 15,
495
494
  placeholder: '{"hello_world": true }',
496
495
  max_length: 10000000000,
497
- on_change: (input) => {
498
- if (input && typeof input == "object") {
499
- set_val(JSON.parse(JSON.stringify(input)))
500
- } else {
501
- set_val(null)
502
- }
503
- },
504
496
  })
505
497
  finish_data_upload_button_manager = create_button_manager({
506
498
  text: "Finish",
507
499
  on_click: () => {
508
- set_val(data_text_input_manager?.val ? JSON.parse(data_text_input_manager?.val) : null)
500
+ let next_val
501
+ try {
502
+ next_val = data_text_input_manager?.val ? JSON.parse(data_text_input_manager.val) : null
503
+ } catch {
504
+ data_upload_popover_manager.show_temp_message("Enter valid JSON before finishing.", "error", 1800, false)
505
+ return
506
+ }
507
+ set_val(next_val)
509
508
  data_upload_popover_manager.close()
510
509
  },
511
510
  })
@@ -25,11 +25,10 @@
25
25
  class="container"
26
26
  style="margin-left: {ml}rem; margin-right: {mr}rem; margin-top: {mt}rem; margin-bottom: {mb}rem;"
27
27
  >
28
- <div class="attr_label">
28
+ <div class="attr_label" style="--heading_min_width: {heading_min_width}rem;">
29
29
  <h4
30
30
  class:required={is_required}
31
- class:is_disabled
32
- style="color: {color}; font-weight: {font_weight}; font-size: {font_size}rem; --disabled_color: {disabled_color};--heading_min_width: {heading_min_width}rem;"
31
+ style="color: {is_disabled ? disabled_color : color}; font-weight: {font_weight}; font-size: {font_size}rem;"
33
32
  >
34
33
  {label}:
35
34
  </h4>
@@ -81,9 +80,6 @@
81
80
  place-content: center;
82
81
  font-size: 1.2rem;
83
82
  }
84
- .is_disabled {
85
- color: var(--disabled_color, var(--g12-t, "gray"));
86
- }
87
83
  .required::after {
88
84
  content: "*";
89
85
  color: var(--g12-t);
@@ -1,25 +1,52 @@
1
1
  <script>
2
2
  import { fly } from "svelte/transition"
3
+ import { tick } from "svelte"
3
4
 
4
5
  let { manager, full_nav } = $props()
6
+
7
+ function manage_keyboard_focus(node) {
8
+ const opener = document.activeElement
9
+ let destroyed = false
10
+ if (opener instanceof HTMLElement && opener.matches(":focus-visible")) {
11
+ tick().then(() => {
12
+ if (!destroyed && manager?.is_full_nav_shown && document.activeElement === opener) {
13
+ node.querySelector("input:not(:disabled), a[href], button:not(:disabled), [tabindex='0']")?.focus()
14
+ }
15
+ })
16
+ }
17
+
18
+ function handle_escape(event) {
19
+ if (event.key !== "Escape" || event.isComposing || event.defaultPrevented || !manager?.is_full_nav_shown) return
20
+ // Let nested native popovers handle their own Escape key first.
21
+ if (document.querySelector(":popover-open")) return
22
+ event.preventDefault()
23
+ manager.set_is_full_nav_toggled_on(false)
24
+ if (opener instanceof HTMLElement && opener.isConnected) opener.focus()
25
+ }
26
+
27
+ window.addEventListener("keydown", handle_escape)
28
+ return {
29
+ destroy() {
30
+ destroyed = true
31
+ window.removeEventListener("keydown", handle_escape)
32
+ },
33
+ }
34
+ }
5
35
  </script>
6
36
 
7
37
  {#if manager?.is_full_nav_shown}
8
- <div
9
- role="button"
10
- tabindex="0"
38
+ <button
39
+ type="button"
40
+ aria-label="Close navigation"
41
+ tabindex="-1"
11
42
  class="underlay"
12
43
  onwheel={(e) => e.stopPropagation()}
13
44
  ontouchmove={(e) => e.stopPropagation()}
14
45
  onclick={() => manager.set_is_full_nav_toggled_on(false)}
15
- onkeydown={(event) => {
16
- if (event.key === "Enter" || event.key === " ") {
17
- manager.set_is_full_nav_toggled_on(false)
18
- }
19
- }}
20
- ></div>
46
+ ></button>
21
47
  <div
22
48
  class="container"
49
+ use:manage_keyboard_focus
23
50
  style="height: {manager?.available_content_height + 1}px; top: {manager?.shown_nav_bar_height - 1}px;"
24
51
  in:fly={{ duration: 300, x: -255, opacity: "100%" }}
25
52
  out:fly={{ duration: 300, x: -255, opacity: "100%" }}
@@ -32,6 +59,8 @@
32
59
 
33
60
  <style>
34
61
  .underlay {
62
+ padding: 0;
63
+ border: 0;
35
64
  position: fixed;
36
65
  top: 0;
37
66
  left: 0;
@@ -33,6 +33,7 @@
33
33
  onclick={() => manager.to_home()}
34
34
  onkeydown={(event) => {
35
35
  if (event.key === "Enter" || event.key === " ") {
36
+ event.preventDefault()
36
37
  manager.to_home()
37
38
  }
38
39
  }}
@@ -34,6 +34,9 @@ export function create_link_manager(config) {
34
34
  let is_show_preview = $derived(set_closurable(config?.is_show_preview, !is_internal_link))
35
35
 
36
36
  function handle_a_click(e) {
37
+ if (e?.defaultPrevented || e?.metaKey || e?.ctrlKey || e?.shiftKey || e?.altKey || (e?.button != null && e.button !== 0)) {
38
+ return
39
+ }
37
40
  e?.preventDefault?.()
38
41
  if (!href) return
39
42
  if (is_internal_link) {
@@ -17,7 +17,7 @@
17
17
  {#if manager?.label || manager?.error_message}
18
18
  <div style="display: inline-flex;">
19
19
  {#if manager?.label}
20
- <label style="--label_font_size: {manager?.label_font_size}rem;" for={`input_${manager?.id}`}>{manager?.label}:</label>
20
+ <p class="label" style="--label_font_size: {manager?.label_font_size}rem;">{manager?.label}:</p>
21
21
  {/if}
22
22
  {#if manager?.error_message}
23
23
  <p id={`slider_error_${manager?.id}`} class="error_text">{manager?.error_message}</p>
@@ -60,12 +60,12 @@
60
60
  <div
61
61
  id={`thumb_${i}_${manager?.id}`}
62
62
  role="slider"
63
- tabindex={manager?.is_disabled ? -1 : 0}
63
+ tabindex={manager?.is_disabled || manager?.loading_i != null ? -1 : 0}
64
64
  aria-label={`${manager?.label ?? manager?.name ?? "Slider"}${manager?.working_val?.length > 1 ? ` ${i + 1}` : ""}`}
65
65
  aria-valuemin={manager?.min}
66
66
  aria-valuemax={manager?.max}
67
67
  aria-valuenow={thumb}
68
- aria-disabled={manager?.is_disabled || manager?.loading_i == i}
68
+ aria-disabled={manager?.is_disabled || manager?.loading_i != null}
69
69
  aria-readonly={manager?.is_read_only}
70
70
  aria-invalid={manager?.error_i == i || !!manager?.error_message}
71
71
  aria-describedby={manager?.error_message ? `slider_error_${manager?.id}` : undefined}
@@ -100,6 +100,7 @@
100
100
  id={`input_${manager?.id}`}
101
101
  type="range"
102
102
  tabindex="-1"
103
+ aria-hidden="true"
103
104
  name={manager?.name}
104
105
  required={manager?.is_required}
105
106
  aria-readonly={manager?.is_read_only}
@@ -211,6 +212,10 @@
211
212
  0 0 0.4rem 0.1rem var(--shadow8),
212
213
  0 0 0 0.7rem var(--shadow8-t);
213
214
  }
215
+ .thumb:focus-visible {
216
+ outline: 2px solid var(--selected-t);
217
+ outline-offset: 2px;
218
+ }
214
219
  .thumb_dot {
215
220
  font-size: var(--font_size);
216
221
  color: var(--g2);
@@ -237,7 +242,7 @@
237
242
  .display_none {
238
243
  display: none;
239
244
  }
240
- label {
245
+ .label {
241
246
  font-size: var(--label_font_size);
242
247
  font-weight: 600;
243
248
  color: var(--g12-t);
@@ -65,13 +65,30 @@ export function create_slider_manager(config) {
65
65
  new_val = input
66
66
  }
67
67
  if (step > 0) {
68
- new_val = Math.round(new_val / step) * step
69
- const step_decimal_places = (step.toString().split(".")[1] || "").length
70
- new_val = parseFloat(new_val.toFixed(step_decimal_places))
68
+ new_val = min + Math.round((new_val - min) / step) * step
69
+ const decimal_places = Math.max(get_decimal_places(min), get_decimal_places(step))
70
+ new_val = Number(new_val.toFixed(Math.min(decimal_places, 100)))
71
71
  }
72
72
  return Math.max(min, Math.min(max, new_val))
73
73
  }
74
74
 
75
+ function get_decimal_places(input) {
76
+ const [coefficient, exponent = "0"] = String(input).toLowerCase().split("e")
77
+ return Math.max(0, (coefficient.split(".")[1] ?? "").length - Number(exponent))
78
+ }
79
+
80
+ function get_incremented_val(input, increments) {
81
+ if (!(step > 0)) return clean_val(input + increments * (max - min) / 100)
82
+ let step_index = (input - min) / step
83
+ const nearest_index = Math.round(step_index)
84
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(step_index)) * 4
85
+ if (Math.abs(step_index - nearest_index) <= tolerance) {
86
+ step_index = nearest_index
87
+ }
88
+ step_index = (increments > 0 ? Math.floor(step_index) : Math.ceil(step_index)) + increments
89
+ return clean_val(min + step_index * step)
90
+ }
91
+
75
92
  function set_hover(e) {
76
93
  const percent_decimal = get_e_percentage(e)
77
94
  if (Array.isArray(working_val) && working_val.length > 0) {
@@ -217,20 +234,20 @@ export function create_slider_manager(config) {
217
234
  return
218
235
  }
219
236
  const key = typeof key_or_event == "string" ? key_or_event : key_or_event?.key
220
- const key_step = step > 0 ? step : (max - min) / 100
221
237
  let next_val = working_val?.[i]
238
+ if (!Number.isFinite(next_val)) return
222
239
  if (["ArrowLeft", "ArrowDown"].includes(key)) {
223
- next_val = clean_val(next_val - key_step)
240
+ next_val = get_incremented_val(next_val, -1)
224
241
  } else if (["ArrowRight", "ArrowUp"].includes(key)) {
225
- next_val = clean_val(next_val + key_step)
242
+ next_val = get_incremented_val(next_val, 1)
226
243
  } else if (key == "Home") {
227
244
  next_val = min
228
245
  } else if (key == "End") {
229
246
  next_val = max
230
247
  } else if (key == "PageDown") {
231
- next_val = clean_val(next_val - key_step * 10)
248
+ next_val = get_incremented_val(next_val, -10)
232
249
  } else if (key == "PageUp") {
233
- next_val = clean_val(next_val + key_step * 10)
250
+ next_val = get_incremented_val(next_val, 10)
234
251
  } else {
235
252
  return
236
253
  }
@@ -160,11 +160,10 @@ export function create_table_advanced_manager(config) {
160
160
  return default_json_schema
161
161
  }
162
162
 
163
- let cur_page_to_use = $derived(
164
- !Array.isArray(row_indexes_display_order) || row_indexes_display_order.length < rows_per_page_dropdown_manager?.val
165
- ? 1
166
- : cur_page
163
+ let total_pages = $derived(
164
+ Math.max(1, Math.ceil(row_indexes_display_order.length / (rows_per_page_dropdown_manager?.val ?? 100)))
167
165
  )
166
+ let cur_page_to_use = $derived(Math.max(1, Math.min(cur_page, total_pages)))
168
167
 
169
168
  let row_indexes_bulk_selected = $derived(
170
169
  Array.isArray(rows_prepped)
@@ -227,10 +226,10 @@ export function create_table_advanced_manager(config) {
227
226
  function get_row_indexes_shown() {
228
227
  if (!Array.isArray(row_indexes_display_order)) {
229
228
  return []
230
- } else if (row_indexes_display_order.length < rows_per_page_dropdown_manager?.val) {
229
+ } else if (!is_paginate || row_indexes_display_order.length <= rows_per_page_dropdown_manager?.val) {
231
230
  return row_indexes_display_order
232
231
  }
233
- const start_index = (cur_page - 1) * rows_per_page_dropdown_manager?.val
232
+ const start_index = (cur_page_to_use - 1) * rows_per_page_dropdown_manager?.val
234
233
  const end_index = start_index + rows_per_page_dropdown_manager?.val
235
234
  return row_indexes_display_order.slice(start_index, end_index)
236
235
  }
@@ -835,13 +834,7 @@ export function create_table_advanced_manager(config) {
835
834
  })
836
835
  pagination_manager = create_pagination_manager({
837
836
  cur_page: () => cur_page_to_use,
838
- total_pages: () => {
839
- if (Array.isArray(row_indexes_display_order)) {
840
- return Math.ceil(row_indexes_display_order.length / rows_per_page_dropdown_manager?.val)
841
- } else {
842
- return 1
843
- }
844
- },
837
+ total_pages: () => total_pages,
845
838
  on_change: (input) => (cur_page = input),
846
839
  })
847
840
  rows_per_page_dropdown_manager = create_dropdown_manager({
@@ -16,7 +16,7 @@
16
16
  <label for={`input_${manager?.id}`}>{manager?.label}:</label>
17
17
  {/if}
18
18
  {#if manager?.error_message}
19
- <p class="error_text">{manager?.error_message}</p>
19
+ <p id={`input_error_${manager?.id}`} class="error_text">{manager?.error_message}</p>
20
20
  {/if}
21
21
  </div>
22
22
  {/if}
@@ -35,6 +35,9 @@
35
35
  style="max-height: {manager?.max_height}rem; border-radius: {manager?.border_radius}rem; margin-left: {manager?.ml}rem; margin-right: {manager?.mr}rem; margin-top: {manager?.mt}rem; margin-bottom: {manager?.mb}rem;"
36
36
  type={manager?.input_type}
37
37
  name={manager?.name}
38
+ aria-label={manager?.aria_label ?? undefined}
39
+ aria-invalid={manager?.error_message ? "true" : undefined}
40
+ aria-describedby={manager?.error_message ? `input_error_${manager?.id}` : undefined}
38
41
  autocomplete={manager?.autocomplete}
39
42
  placeholder={manager?.placeholder}
40
43
  required={manager?.is_required}
@@ -56,6 +59,9 @@
56
59
  id={`input_${manager?.id}`}
57
60
  type={manager?.input_type}
58
61
  name={manager?.name}
62
+ aria-label={manager?.aria_label ?? undefined}
63
+ aria-invalid={manager?.error_message ? "true" : undefined}
64
+ aria-describedby={manager?.error_message ? `input_error_${manager?.id}` : undefined}
59
65
  autocomplete={manager?.autocomplete}
60
66
  placeholder={manager?.placeholder}
61
67
  {...["number", "tel"].includes(manager?.input_type) ? { pattern: "[0-9]*" } : {}}
@@ -82,6 +82,7 @@ export function create_text_input_manager(config) {
82
82
  let is_loading = $derived(set_closurable(config?.is_loading, false))
83
83
  let popover_header = $derived(set_closurable(config?.popover_header, null))
84
84
  let label = $derived(set_closurable(config?.label, null))
85
+ let aria_label = $derived(set_closurable(config?.aria_label, null))
85
86
  let name = $derived(set_closurable(config?.name, defaults?.[type]?.name ?? "text"))
86
87
  let is_required = $derived(set_closurable(config?.is_required, false))
87
88
  let is_read_only = $derived(set_closurable(config?.is_read_only, false))
@@ -319,6 +320,9 @@ export function create_text_input_manager(config) {
319
320
  get label() {
320
321
  return label
321
322
  },
323
+ get aria_label() {
324
+ return aria_label
325
+ },
322
326
  get autocomplete() {
323
327
  return autocomplete
324
328
  },
@@ -7,11 +7,31 @@
7
7
  let screen_width = $state(null)
8
8
  let screen_height = $state(null)
9
9
  let positions = $state(null)
10
+ let is_pointer_inside = $state(false)
11
+ let has_focus = $state(false)
12
+ let is_dismissed = $state(false)
13
+ let is_visible = $derived(!is_disabled && !is_dismissed && (is_pointer_inside || has_focus))
10
14
 
11
15
  function handle_pointer_enter() {
16
+ is_pointer_inside = true
17
+ is_dismissed = false
12
18
  positions = calc_positions()
13
19
  }
14
20
 
21
+ function handle_focus_in() {
22
+ has_focus = true
23
+ is_dismissed = false
24
+ positions = calc_positions()
25
+ }
26
+
27
+ function handle_focus_out(event) {
28
+ if (!event.currentTarget.contains(event.relatedTarget)) has_focus = false
29
+ }
30
+
31
+ function handle_keydown(event) {
32
+ if (event.key === "Escape" && is_visible) is_dismissed = true
33
+ }
34
+
15
35
  function calc_positions() {
16
36
  let edge_forgiveness = 10
17
37
  let target_width = 400
@@ -78,17 +98,24 @@
78
98
  }
79
99
  </script>
80
100
 
81
- <svelte:window bind:innerWidth={screen_width} bind:innerHeight={screen_height} />
101
+ <svelte:window bind:innerWidth={screen_width} bind:innerHeight={screen_height} onkeydown={handle_keydown} />
82
102
 
83
- <div id={`container_${id}`} class="container" class:is_disabled>
103
+ <div
104
+ id={`container_${id}`}
105
+ role="presentation"
106
+ class="container"
107
+ class:is_disabled
108
+ onpointerenter={handle_pointer_enter}
109
+ onpointerleave={() => (is_pointer_inside = false)}
110
+ onfocusin={handle_focus_in}
111
+ onfocusout={handle_focus_out}
112
+ >
84
113
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
85
114
  <div
86
115
  id={`toggle_${id}`}
87
116
  role="group"
88
117
  tabindex={is_disabled ? undefined : 0}
89
118
  aria-describedby={is_disabled ? undefined : `tooltip_${id}`}
90
- onpointerenter={handle_pointer_enter}
91
- onfocusin={handle_pointer_enter}
92
119
  >
93
120
  {#if target_content}
94
121
  {@render target_content()}
@@ -97,7 +124,9 @@
97
124
  <div
98
125
  id={`tooltip_${id}`}
99
126
  class="tooltip"
127
+ class:is_visible
100
128
  role="tooltip"
129
+ aria-hidden={!is_visible}
101
130
  style="
102
131
  --modal_top_pos: {positions?.modal_top_pos};
103
132
  --modal_left_pos: {positions?.modal_left_pos};
@@ -139,9 +168,7 @@
139
168
  overflow: auto;
140
169
  z-index: 11;
141
170
  }
142
- .container:hover .tooltip,
143
- .container:focus-within .tooltip,
144
- .container:active .tooltip {
171
+ .tooltip.is_visible {
145
172
  visibility: visible;
146
173
  opacity: 1;
147
174
  }
@@ -15,6 +15,7 @@ export {
15
15
  } from "../Components/TimeInput/index.js"
16
16
  export { intersection_observer } from "../actions/index.js"
17
17
  export { normalize_link_href } from "./links.js"
18
+ export { v } from "./value.svelte.js"
18
19
 
19
20
  function has_own(value, key) {
20
21
  return (
@@ -833,35 +834,3 @@ export function set_definition_stack(input, definition_stack, pass_event_down) {
833
834
  }
834
835
  return finish_update(defined_variables_loc)
835
836
  }
836
-
837
- // tbd could also turn into a typed thing with arrays of v or objects, color, etc
838
- export function v(config) {
839
- let val = $state(null)
840
- function on_change(from_id = null) {
841
- if (typeof config?.on_change == "function") {
842
- config?.on_change({ val, from_id: from_id })
843
- }
844
- }
845
- function init(config) {
846
- val = config?.val
847
- }
848
- init(config)
849
- return {
850
- get val() {
851
- return val
852
- },
853
- set val(input) {
854
- val = input
855
- on_change()
856
- },
857
- set_val(input) {
858
- if (typeof input == "object" && input?.from_id) {
859
- val = input?.val
860
- on_change(input?.from_id)
861
- } else {
862
- val = input
863
- on_change()
864
- }
865
- },
866
- }
867
- }
@@ -0,0 +1,30 @@
1
+ export function v(config) {
2
+ let val = $state(null)
3
+ function on_change(from_id = null) {
4
+ if (typeof config?.on_change == "function") {
5
+ config?.on_change({ val, from_id: from_id })
6
+ }
7
+ }
8
+ function init(config) {
9
+ val = config?.val
10
+ }
11
+ init(config)
12
+ return {
13
+ get val() {
14
+ return val
15
+ },
16
+ set val(input) {
17
+ val = input
18
+ on_change()
19
+ },
20
+ set_val(input) {
21
+ if (typeof input == "object" && input?.from_id) {
22
+ val = input?.val
23
+ on_change(input?.from_id)
24
+ } else {
25
+ val = input
26
+ on_change()
27
+ }
28
+ },
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltekit-ui",
3
- "version": "1.1.80",
3
+ "version": "1.1.82",
4
4
  "description": "A SvelteKit UI component library for building modern web applications",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "version:available": "node ./scripts/use-next-npm-version.js",
42
42
  "postpublish": "npm run clean:dist",
43
43
  "prepublishOnly": "npm run verify",
44
- "publish:npm": "npm run package && npm run version:available && (npm whoami >/dev/null 2>&1 || npm login --auth-type=web) && npm publish"
44
+ "publish:npm": "npm run version:available && (npm whoami >/dev/null 2>&1 || npm login --auth-type=web) && npm publish"
45
45
  },
46
46
  "dependencies": {
47
47
  "context-filter-polyfill": "^0.3.23",
@@ -58,11 +58,11 @@
58
58
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
59
59
  "@vercel/analytics": "^2.0.1",
60
60
  "jsdom": "^30.0.1",
61
- "svelte": "^5.56.10",
61
+ "svelte": "^5.57.0",
62
62
  "svelte-check": "^4.7.6",
63
63
  "typescript": "^6.0.3",
64
64
  "vite": "^8.2.2",
65
- "vitest": "^4.1.11"
65
+ "vitest": "^5.0.0"
66
66
  },
67
67
  "homepage": "https://www.sveltekit-ui.com",
68
68
  "keywords": [