svelte-multiselect 11.2.3 → 11.3.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.
@@ -25,22 +25,25 @@ export declare const sortable: ({ header_selector, asc_class, desc_class, sorted
25
25
  backgroundColor: string;
26
26
  } | undefined;
27
27
  }) => (node: HTMLElement) => () => void;
28
- type HighlightOptions = {
28
+ export type HighlightOptions = {
29
29
  query?: string;
30
30
  disabled?: boolean;
31
+ fuzzy?: boolean;
31
32
  node_filter?: (node: Node) => number;
32
33
  css_class?: string;
33
34
  };
34
- export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => void) | undefined;
35
- export declare const tooltip: (options?: {
35
+ export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => boolean) | undefined;
36
+ export interface TooltipOptions {
36
37
  content?: string;
37
38
  placement?: `top` | `bottom` | `left` | `right`;
38
39
  delay?: number;
39
- }) => Attachment;
40
- type ClickOutsideConfig<T extends HTMLElement> = {
40
+ disabled?: boolean;
41
+ style?: string;
42
+ }
43
+ export declare const tooltip: (options?: TooltipOptions) => Attachment;
44
+ export type ClickOutsideConfig<T extends HTMLElement> = {
41
45
  enabled?: boolean;
42
46
  exclude?: string[];
43
47
  callback?: (node: T, config: ClickOutsideConfig<T>) => void;
44
48
  };
45
49
  export declare const click_outside: <T extends HTMLElement>(config?: ClickOutsideConfig<T>) => (node: T) => () => void;
46
- export {};
@@ -158,11 +158,15 @@ export const sortable = ({ header_selector = `thead th`, asc_class = `table-sort
158
158
  };
159
159
  };
160
160
  export const highlight_matches = (ops) => (node) => {
161
- const { query = ``, disabled = false, node_filter = () => NodeFilter.FILTER_ACCEPT, css_class = `highlight-match`, } = ops;
162
- // clear previous ranges from HighlightRegistry
163
- CSS.highlights.clear();
164
- if (!query || disabled || typeof CSS === `undefined` || !CSS.highlights)
165
- return; // abort if CSS highlight API not supported
161
+ const { query = ``, disabled = false, fuzzy = false, node_filter = () => NodeFilter.FILTER_ACCEPT, css_class = `highlight-match`, } = ops;
162
+ // abort if CSS highlight API not supported
163
+ if (typeof CSS === `undefined` || !CSS.highlights)
164
+ return;
165
+ // always clear our own highlight first
166
+ CSS.highlights.delete(css_class);
167
+ // if disabled or empty query, stop after cleanup
168
+ if (!query || disabled)
169
+ return;
166
170
  const tree_walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
167
171
  acceptNode: node_filter,
168
172
  });
@@ -175,28 +179,57 @@ export const highlight_matches = (ops) => (node) => {
175
179
  // iterate over all text nodes and find matches
176
180
  const ranges = text_nodes.map((el) => {
177
181
  const text = el.textContent?.toLowerCase();
178
- const indices = [];
179
- let start_pos = 0;
180
- while (text && start_pos < text.length) {
181
- const index = text.indexOf(query, start_pos);
182
- if (index === -1)
183
- break;
184
- indices.push(index);
185
- start_pos = index + query.length;
182
+ if (!text)
183
+ return [];
184
+ const search = query.toLowerCase();
185
+ if (fuzzy) {
186
+ // Fuzzy highlighting: highlight individual characters that match in order
187
+ const matching_indices = [];
188
+ let search_idx = 0;
189
+ let target_idx = 0;
190
+ // Find matching character indices
191
+ while (search_idx < search.length && target_idx < text.length) {
192
+ if (search[search_idx] === text[target_idx]) {
193
+ matching_indices.push(target_idx);
194
+ search_idx++;
195
+ }
196
+ target_idx++;
197
+ }
198
+ // Only create ranges if we found all characters in order
199
+ if (search_idx === search.length) {
200
+ return matching_indices.map((index) => {
201
+ const range = new Range();
202
+ range.setStart(el, index);
203
+ range.setEnd(el, index + 1); // highlight single character
204
+ return range;
205
+ });
206
+ }
207
+ return [];
208
+ }
209
+ else {
210
+ // Substring highlighting: highlight consecutive substrings
211
+ const indices = [];
212
+ let start_pos = 0;
213
+ while (start_pos < text.length) {
214
+ const index = text.indexOf(search, start_pos);
215
+ if (index === -1)
216
+ break;
217
+ indices.push(index);
218
+ start_pos = index + search.length;
219
+ }
220
+ // create range object for each substring found in the text node
221
+ return indices.map((index) => {
222
+ const range = new Range();
223
+ range.setStart(el, index);
224
+ range.setEnd(el, index + search.length);
225
+ return range;
226
+ });
186
227
  }
187
- // create range object for each str found in the text node
188
- return indices.map((index) => {
189
- const range = new Range();
190
- range.setStart(el, index);
191
- range.setEnd(el, index + query?.length);
192
- return range;
193
- });
194
228
  });
195
229
  // create Highlight object from ranges and add to registry
196
230
  CSS.highlights.set(css_class, new Highlight(...ranges.flat()));
197
- return () => {
198
- CSS.highlights.delete(css_class);
199
- };
231
+ // Return cleanup function
232
+ return () => CSS.highlights.delete(css_class);
200
233
  };
201
234
  // Global tooltip state to ensure only one tooltip is shown at a time
202
235
  let current_tooltip = null;
@@ -220,7 +253,7 @@ export const tooltip = (options = {}) => (node) => {
220
253
  const safe_options = options || {};
221
254
  const cleanup_functions = [];
222
255
  function setup_tooltip(element) {
223
- if (!element)
256
+ if (!element || safe_options.disabled)
224
257
  return;
225
258
  const content = safe_options.content || element.title ||
226
259
  element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
@@ -239,6 +272,7 @@ export const tooltip = (options = {}) => (node) => {
239
272
  tooltip.className = `custom-tooltip`;
240
273
  const placement = safe_options.placement || `bottom`;
241
274
  tooltip.setAttribute(`data-placement`, placement);
275
+ // Apply base styles
242
276
  tooltip.style.cssText = `
243
277
  position: absolute; z-index: 9999; opacity: 0;
244
278
  background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
@@ -246,6 +280,16 @@ export const tooltip = (options = {}) => (node) => {
246
280
  max-width: var(--tooltip-max-width, 280px); word-wrap: break-word; pointer-events: none;
247
281
  filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
248
282
  `;
283
+ // Apply custom styles if provided (these will override base styles due to CSS specificity)
284
+ if (safe_options.style) {
285
+ // Parse and apply custom styles as individual properties for better control
286
+ const custom_styles = safe_options.style.split(`;`).filter((style) => style.trim());
287
+ custom_styles.forEach((style) => {
288
+ const [property, value] = style.split(`:`).map((s) => s.trim());
289
+ if (property && value)
290
+ tooltip.style.setProperty(property, value);
291
+ });
292
+ }
249
293
  tooltip.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
250
294
  // Mirror CSS custom properties from the trigger node onto the tooltip element
251
295
  const trigger_styles = getComputedStyle(element);
@@ -265,7 +309,10 @@ export const tooltip = (options = {}) => (node) => {
265
309
  if (value)
266
310
  tooltip.style.setProperty(name, value);
267
311
  });
268
- // Arrow element pointing to the trigger, oriented by placement
312
+ // Append early so we can read computed border styles for arrow border
313
+ document.body.appendChild(tooltip);
314
+ // Arrow elements: optional border triangle behind fill triangle
315
+ const tooltip_styles = getComputedStyle(tooltip);
269
316
  const arrow = document.createElement(`div`);
270
317
  arrow.className = `custom-tooltip-arrow`;
271
318
  arrow.style.cssText =
@@ -274,6 +321,49 @@ export const tooltip = (options = {}) => (node) => {
274
321
  .trim();
275
322
  const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
276
323
  const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
324
+ const border_color = tooltip_styles.borderTopColor;
325
+ const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
326
+ const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
327
+ border_width_num > 0;
328
+ const maybe_append_border_arrow = () => {
329
+ if (!has_border)
330
+ return;
331
+ const border_arrow = document.createElement(`div`);
332
+ border_arrow.className = `custom-tooltip-arrow-border`;
333
+ border_arrow.style.cssText =
334
+ `position: absolute; width: 0; height: 0; pointer-events: none;`;
335
+ const border_size = arrow_px + (border_width_num * 1.4);
336
+ if (placement === `top`) {
337
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
338
+ border_arrow.style.bottom = `-${border_size}px`;
339
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
340
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
341
+ border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
342
+ }
343
+ else if (placement === `left`) {
344
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
345
+ border_arrow.style.right = `-${border_size}px`;
346
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
347
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
348
+ border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
349
+ }
350
+ else if (placement === `right`) {
351
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
352
+ border_arrow.style.left = `-${border_size}px`;
353
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
354
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
355
+ border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
356
+ }
357
+ else { // bottom
358
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
359
+ border_arrow.style.top = `-${border_size}px`;
360
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
361
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
362
+ border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
363
+ }
364
+ tooltip.appendChild(border_arrow);
365
+ };
366
+ // Create the fill arrow on top
277
367
  if (placement === `top`) {
278
368
  arrow.style.left = `calc(50% - ${arrow_px}px)`;
279
369
  arrow.style.bottom = `-${arrow_px}px`;
@@ -302,8 +392,8 @@ export const tooltip = (options = {}) => (node) => {
302
392
  arrow.style.borderRight = `${arrow_px}px solid transparent`;
303
393
  arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
304
394
  }
395
+ maybe_append_border_arrow();
305
396
  tooltip.appendChild(arrow);
306
- document.body.appendChild(tooltip);
307
397
  // Position tooltip
308
398
  const rect = element.getBoundingClientRect();
309
399
  const tooltip_rect = tooltip.getBoundingClientRect();
@@ -337,23 +427,22 @@ export const tooltip = (options = {}) => (node) => {
337
427
  }
338
428
  function hide_tooltip() {
339
429
  clear_tooltip();
340
- hide_timeout = setTimeout(() => {
430
+ if (current_tooltip) {
431
+ current_tooltip.style.opacity = `0`;
341
432
  if (current_tooltip) {
342
- current_tooltip.style.opacity = `0`;
343
- setTimeout(() => {
344
- if (current_tooltip) {
345
- current_tooltip.remove();
346
- current_tooltip = null;
347
- }
348
- }, 150);
433
+ current_tooltip.remove();
434
+ current_tooltip = null;
349
435
  }
350
- }, 50);
436
+ }
351
437
  }
352
438
  const events = [`mouseenter`, `mouseleave`, `focus`, `blur`];
353
439
  const handlers = [show_tooltip, hide_tooltip, show_tooltip, hide_tooltip];
354
440
  events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
441
+ // Hide tooltip when user scrolls
442
+ globalThis.addEventListener(`scroll`, hide_tooltip, true);
355
443
  return () => {
356
444
  events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
445
+ globalThis.removeEventListener(`scroll`, hide_tooltip, true);
357
446
  const original_title = element.getAttribute(`data-original-title`);
358
447
  if (original_title) {
359
448
  element.setAttribute(`title`, original_title);
package/dist/index.d.ts CHANGED
@@ -7,9 +7,10 @@ export { default as FileDetails } from './FileDetails.svelte';
7
7
  export { default as GitHubCorner } from './GitHubCorner.svelte';
8
8
  export { default as Icon } from './Icon.svelte';
9
9
  export { default, default as MultiSelect } from './MultiSelect.svelte';
10
+ export { default as Nav } from './Nav.svelte';
10
11
  export { default as PrevNext } from './PrevNext.svelte';
11
- export { default as RadioButtons } from './RadioButtons.svelte';
12
12
  export { default as Toggle } from './Toggle.svelte';
13
13
  export * from './types';
14
+ export * from './utils';
14
15
  export { default as Wiggle } from './Wiggle.svelte';
15
16
  export declare function scroll_into_view_if_needed_polyfill(element: Element, centerIfNeeded?: boolean): IntersectionObserver;
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@ export { default as FileDetails } from './FileDetails.svelte';
7
7
  export { default as GitHubCorner } from './GitHubCorner.svelte';
8
8
  export { default as Icon } from './Icon.svelte';
9
9
  export { default, default as MultiSelect } from './MultiSelect.svelte';
10
+ export { default as Nav } from './Nav.svelte';
10
11
  export { default as PrevNext } from './PrevNext.svelte';
11
- export { default as RadioButtons } from './RadioButtons.svelte';
12
12
  export { default as Toggle } from './Toggle.svelte';
13
13
  export * from './types';
14
+ export * from './utils';
14
15
  export { default as Wiggle } from './Wiggle.svelte';
15
16
  // Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded).
16
17
  // See https://github.com/janosh/svelte-multiselect/issues/87
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { FocusEventHandler, HTMLInputAttributes, KeyboardEventHandler, MouseEventHandler, TouchEventHandler } from 'svelte/elements';
2
+ import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
3
3
  export type Option = string | number | ObjectOption;
4
4
  export type OptionStyle = string | {
5
5
  option: string;
@@ -16,20 +16,6 @@ export type ObjectOption = {
16
16
  style?: OptionStyle;
17
17
  [key: string]: unknown;
18
18
  };
19
- export interface MultiSelectNativeEvents {
20
- onblur?: FocusEventHandler<HTMLInputElement>;
21
- onclick?: MouseEventHandler<HTMLInputElement>;
22
- onfocus?: FocusEventHandler<HTMLInputElement>;
23
- onkeydown?: KeyboardEventHandler<HTMLInputElement>;
24
- onkeyup?: KeyboardEventHandler<HTMLInputElement>;
25
- onmousedown?: MouseEventHandler<HTMLInputElement>;
26
- onmouseenter?: MouseEventHandler<HTMLInputElement>;
27
- onmouseleave?: MouseEventHandler<HTMLInputElement>;
28
- ontouchcancel?: TouchEventHandler<HTMLInputElement>;
29
- ontouchend?: TouchEventHandler<HTMLInputElement>;
30
- ontouchmove?: TouchEventHandler<HTMLInputElement>;
31
- ontouchstart?: TouchEventHandler<HTMLInputElement>;
32
- }
33
19
  export interface MultiSelectEvents<T extends Option = Option> {
34
20
  onadd?: (data: {
35
21
  option: T;
@@ -55,7 +41,7 @@ export interface MultiSelectEvents<T extends Option = Option> {
55
41
  event: Event;
56
42
  }) => unknown;
57
43
  }
58
- type AfterInputProps = Pick<MultiSelectParameters, `selected` | `disabled` | `invalid` | `id` | `placeholder` | `open` | `required`>;
44
+ type AfterInputProps = Pick<MultiSelectProps, `selected` | `disabled` | `invalid` | `id` | `placeholder` | `open` | `required`>;
59
45
  type UserMsgProps = {
60
46
  searchText: string;
61
47
  msgType: false | `dupe` | `create` | `no-match`;
@@ -87,7 +73,7 @@ export interface PortalParams {
87
73
  target_node?: HTMLElement | null;
88
74
  active?: boolean;
89
75
  }
90
- export interface MultiSelectParameters<T extends Option = Option> {
76
+ export interface MultiSelectProps<T extends Option = Option> extends MultiSelectEvents<T>, MultiSelectSnippets<T>, Omit<HTMLAttributes<HTMLDivElement>, `children` | `onchange` | `onclose`> {
91
77
  activeIndex?: number | null;
92
78
  activeOption?: T | null;
93
79
  createOptionMsg?: string | null;
@@ -104,6 +90,7 @@ export interface MultiSelectParameters<T extends Option = Option> {
104
90
  keepSelectedInDropdown?: false | `plain` | `checkboxes`;
105
91
  key?: (opt: T) => unknown;
106
92
  filterFunc?: (opt: T, searchText: string) => boolean;
93
+ fuzzy?: boolean;
107
94
  closeDropdownOnSelect?: boolean | `if-mobile` | `retain-focus`;
108
95
  form_input?: HTMLInputElement | null;
109
96
  highlightMatches?: boolean;
@@ -151,8 +138,5 @@ export interface MultiSelectParameters<T extends Option = Option> {
151
138
  ulOptionsStyle?: string | null;
152
139
  value?: T | T[] | null;
153
140
  portal?: PortalParams;
154
- [key: string]: unknown;
155
- }
156
- export interface MultiSelectProps<T extends Option = Option> extends MultiSelectNativeEvents, MultiSelectEvents<T>, MultiSelectSnippets<T>, MultiSelectParameters<T> {
157
141
  }
158
142
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,9 +1,4 @@
1
- import type { Option, OptionStyle } from './types';
1
+ import type { Option } from './types';
2
2
  export declare const get_label: (opt: Option) => string | number;
3
- export declare function get_style(option: {
4
- style?: OptionStyle;
5
- [key: string]: unknown;
6
- } | string | number, key?: `selected` | `option` | null): string;
7
- export declare function highlight_matching_nodes(element: HTMLElement, // parent element
8
- query?: string, // search query
9
- noMatchingOptionsMsg?: string): void;
3
+ export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
4
+ export declare function fuzzy_match(search_text: string, target_text: string): boolean;
package/dist/utils.js CHANGED
@@ -14,14 +14,15 @@ export const get_label = (opt) => {
14
14
  // object to be used in the style attribute of the option.
15
15
  // If the style is a string, it will be returned as is
16
16
  export function get_style(option, key = null) {
17
+ if (key === undefined)
18
+ key = null;
17
19
  let css_str = ``;
18
- if (![`selected`, `option`, null].includes(key)) {
20
+ const valid_key = key === null || key === `selected` || key === `option`;
21
+ if (!valid_key)
19
22
  console.error(`MultiSelect: Invalid key=${key} for get_style`);
20
- }
21
23
  if (typeof option === `object` && option.style) {
22
- if (typeof option.style === `string`) {
24
+ if (typeof option.style === `string`)
23
25
  css_str = option.style;
24
- }
25
26
  if (typeof option.style === `object`) {
26
27
  if (key && key in option.style)
27
28
  return option.style[key] ?? ``;
@@ -35,49 +36,24 @@ export function get_style(option, key = null) {
35
36
  css_str += `;`;
36
37
  return css_str;
37
38
  }
38
- // Highlights text nodes that matching the string query
39
- export function highlight_matching_nodes(element, // parent element
40
- query, // search query
41
- noMatchingOptionsMsg) {
42
- if (typeof CSS === `undefined` || !CSS.highlights || !query)
43
- return; // abort if CSS highlight API not supported
44
- // clear previous ranges from HighlightRegistry
45
- CSS.highlights.clear();
46
- const tree_walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
47
- acceptNode: (node) => {
48
- // don't highlight text in the "no matching options" message
49
- if (node?.textContent === noMatchingOptionsMsg) {
50
- return NodeFilter.FILTER_REJECT;
51
- }
52
- return NodeFilter.FILTER_ACCEPT;
53
- },
54
- });
55
- const text_nodes = [];
56
- let current_node = tree_walker.nextNode();
57
- while (current_node) {
58
- text_nodes.push(current_node);
59
- current_node = tree_walker.nextNode();
39
+ // Fuzzy string matching function
40
+ // Returns true if the search string can be found as a subsequence in the target string
41
+ // e.g., "tageoo" matches "tasks/geo-opt" because t-a-g-e-o-o appears in order
42
+ export function fuzzy_match(search_text, target_text) {
43
+ // Handle null/undefined inputs first
44
+ if (search_text === null || search_text === undefined || target_text === null ||
45
+ target_text === undefined)
46
+ return false;
47
+ if (!search_text)
48
+ return true;
49
+ if (!target_text)
50
+ return false;
51
+ const [search, target] = [search_text.toLowerCase(), target_text.toLowerCase()];
52
+ let [search_idx, target_idx] = [0, 0];
53
+ while (search_idx < search.length && target_idx < target.length) {
54
+ if (search[search_idx] === target[target_idx])
55
+ search_idx++;
56
+ target_idx++;
60
57
  }
61
- // iterate over all text nodes and find matches
62
- const ranges = text_nodes.map((el) => {
63
- const text = el.textContent?.toLowerCase();
64
- const indices = [];
65
- let start_pos = 0;
66
- while (text && start_pos < text.length) {
67
- const index = text.indexOf(query, start_pos);
68
- if (index === -1)
69
- break;
70
- indices.push(index);
71
- start_pos = index + query.length;
72
- }
73
- // create range object for each str found in the text node
74
- return indices.map((index) => {
75
- const range = new Range();
76
- range.setStart(el, index);
77
- range.setEnd(el, index + query.length);
78
- return range;
79
- });
80
- });
81
- // create Highlight object from ranges and add to registry
82
- CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
58
+ return search_idx === search.length;
83
59
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "homepage": "https://janosh.github.io/svelte-multiselect",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "11.2.3",
8
+ "version": "11.3.0",
9
9
  "type": "module",
10
10
  "svelte": "./dist/index.js",
11
11
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
@@ -13,31 +13,31 @@
13
13
  "svelte": "^5.35.6"
14
14
  },
15
15
  "devDependencies": {
16
- "@playwright/test": "^1.54.2",
17
- "@stylistic/eslint-plugin": "^5.2.3",
18
- "@sveltejs/adapter-static": "^3.0.9",
19
- "@sveltejs/kit": "^2.28.0",
20
- "@sveltejs/package": "2.4.1",
21
- "@sveltejs/vite-plugin-svelte": "^6.1.1",
22
- "@types/node": "^24.2.1",
23
- "@vitest/coverage-v8": "^3.2.4",
24
- "eslint": "^9.33.0",
25
- "eslint-plugin-svelte": "^3.11.0",
26
- "happy-dom": "^18.0.1",
16
+ "@playwright/test": "^1.56.1",
17
+ "@stylistic/eslint-plugin": "^5.5.0",
18
+ "@sveltejs/adapter-static": "^3.0.10",
19
+ "@sveltejs/kit": "^2.48.4",
20
+ "@sveltejs/package": "2.5.4",
21
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
22
+ "@types/node": "^24.10.0",
23
+ "@vitest/coverage-v8": "^4.0.8",
24
+ "eslint": "^9.39.1",
25
+ "eslint-plugin-svelte": "^3.13.0",
26
+ "happy-dom": "^20.0.10",
27
27
  "hastscript": "^9.0.1",
28
28
  "mdsvex": "^0.12.6",
29
29
  "mdsvexamples": "^0.5.0",
30
30
  "rehype-autolink-headings": "^7.1.0",
31
31
  "rehype-slug": "^6.0.0",
32
- "svelte": "^5.38.1",
33
- "svelte-check": "^4.3.1",
32
+ "svelte": "^5.43.5",
33
+ "svelte-check": "^4.3.3",
34
34
  "svelte-preprocess": "^6.0.3",
35
35
  "svelte-toc": "^0.6.2",
36
- "svelte2tsx": "^0.7.42",
37
- "typescript": "5.9.2",
38
- "typescript-eslint": "^8.39.1",
39
- "vite": "^7.1.2",
40
- "vitest": "^3.2.4"
36
+ "svelte2tsx": "^0.7.45",
37
+ "typescript": "5.9.3",
38
+ "typescript-eslint": "^8.46.3",
39
+ "vite": "^7.2.2",
40
+ "vitest": "^4.0.8"
41
41
  },
42
42
  "keywords": [
43
43
  "svelte",
package/readme.md CHANGED
@@ -32,8 +32,6 @@
32
32
  - **Single / multiple select:** pass `maxSelect={1, 2, 3, ...}` prop to restrict the number of selectable options
33
33
  - **Configurable:** see props
34
34
 
35
- <slot name="nav" />
36
-
37
35
  ## 🧪 &thinsp; Coverage
38
36
 
39
37
  | Statements | Branches | Lines |
@@ -643,10 +641,10 @@ For example, here's how you might annoy your users with an alert every time one
643
641
 
644
642
  ```svelte
645
643
  <MultiSelect
646
- onchange={(e) => {
647
- if (e.detail.type === 'add') alert(`You added ${e.detail.option}`)
648
- if (e.detail.type === 'remove') alert(`You removed ${e.detail.option}`)
649
- if (e.detail.type === 'removeAll') alert(`You removed ${e.detail.options}`)
644
+ onchange={(event) => {
645
+ if (event.detail.type === 'add') alert(`You added ${event.detail.option}`)
646
+ if (event.detail.type === 'remove') alert(`You removed ${event.detail.option}`)
647
+ if (event.detail.type === 'removeAll') alert(`You removed ${event.detail.options}`)
650
648
  }}
651
649
  />
652
650
  ```
@@ -1,67 +0,0 @@
1
- <script lang="ts">// get the label key from an option object or the option itself if it's a string or number
2
- const get_label = (op) => {
3
- if (op instanceof Object) {
4
- if (op.label === undefined) {
5
- console.error(`RadioButton option ${JSON.stringify(op)} is an object but has no label key`);
6
- }
7
- return op.label;
8
- }
9
- return op;
10
- };
11
- let { options, selected = $bindable(), id = null, name = null, disabled = false, required = false, aria_label = null, onclick, onchange, oninput, option_snippet, children, ...rest } = $props();
12
- export {};
13
- </script>
14
-
15
- <div {id} {...rest}>
16
- {#each options as option (JSON.stringify(option))}
17
- {@const label = get_label(option)}
18
- {@const active = selected && get_label(option) === get_label(selected)}
19
- <label class:active aria-label={aria_label}>
20
- <input
21
- type="radio"
22
- value={option}
23
- {name}
24
- {disabled}
25
- {required}
26
- bind:group={selected}
27
- {onchange}
28
- {oninput}
29
- {onclick}
30
- />
31
- {#if option_snippet}
32
- {@render option_snippet({ option, selected, active })}
33
- {:else if children}
34
- {@render children({ option, selected, active })}
35
- {:else}<span>{label}</span>{/if}
36
- </label>
37
- {/each}
38
- </div>
39
-
40
- <style>
41
- div {
42
- max-width: max-content;
43
- overflow: hidden;
44
- height: fit-content;
45
- display: var(--radio-btn-display, inline-flex);
46
- border-radius: var(--radio-btn-border-radius, 0.5em);
47
- }
48
- input {
49
- display: none;
50
- }
51
- span {
52
- cursor: pointer;
53
- display: inline-block;
54
- color: var(--radio-btn-color, white);
55
- padding: var(--radio-btn-padding, 2pt 5pt);
56
- background: var(--radio-btn-bg, black);
57
- transition: var(--radio-btn-transition, background 0.3s, transform 0.3s);
58
- }
59
- label:not(.active) span:hover {
60
- background: var(--radio-btn-hover-bg, cornflowerblue);
61
- color: var(--radio-btn-hover-color, white);
62
- }
63
- label.active span {
64
- box-shadow: var(--radio-btn-checked-shadow, inset 0 0 1em -3pt black);
65
- background: var(--radio-btn-checked-bg, darkcyan);
66
- }
67
- </style>