svelte-multiselect 11.5.2 → 11.6.1

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.
@@ -6,8 +6,8 @@
6
6
  // Avoid matching inside src={...} attributes by requiring these specific contexts
7
7
  // Note: [^>]* for attributes won't match if an attribute value contains > (e.g., data-foo="a>b")
8
8
  // This edge case is rare in practice and would require significantly more complex parsing
9
- const heading_regex_line_start = /^(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\2>/gim;
10
- const heading_regex_after_tag = /(>)(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\3>/gi;
9
+ const heading_regex_line_start = /^(\s*)<(h[1-6])([^>]*)>([\s\S]*?)<\/\2>/gim;
10
+ const heading_regex_after_tag = /(>)(\s*)<(h[1-6])([^>]*)>([\s\S]*?)<\/\3>/gi;
11
11
  // Remove Svelte expressions handling nested braces (e.g., {fn({a: 1})})
12
12
  // Treats unmatched } as literal text to avoid dropping content
13
13
  function strip_svelte_expressions(str) {
@@ -94,25 +94,19 @@ function add_anchor_to_heading(heading, icon_svg = link_svg) {
94
94
  export const heading_anchors = (options = {}) => (node) => {
95
95
  if (typeof document === `undefined`)
96
96
  return;
97
- const selector = options.selector ?? `h2, h3, h4, h5, h6`;
97
+ // :scope refers to the element on which querySelectorAll is called
98
+ // This works whether the attachment is on <main> or a parent element
99
+ const selector = options.selector ??
100
+ `:scope > :is(h1, h2, h3, h4, h5, h6), :scope > * > :is(h1, h2, h3, h4, h5, h6)`;
98
101
  const icon_svg = options.icon_svg ?? link_svg;
99
102
  // Process existing headings
100
103
  for (const heading of Array.from(node.querySelectorAll(selector))) {
101
104
  add_anchor_to_heading(heading, icon_svg);
102
105
  }
103
- // Watch for new headings
104
- const observer = new MutationObserver((mutations) => {
105
- for (const { addedNodes } of mutations) {
106
- for (const added of Array.from(addedNodes)) {
107
- if (added.nodeType !== Node.ELEMENT_NODE)
108
- continue;
109
- const el = added;
110
- if (el.matches?.(selector))
111
- add_anchor_to_heading(el, icon_svg);
112
- for (const hdn of Array.from(el.querySelectorAll(selector))) {
113
- add_anchor_to_heading(hdn, icon_svg);
114
- }
115
- }
106
+ // Watch for new headings - requery the container to respect nesting depth constraints
107
+ const observer = new MutationObserver(() => {
108
+ for (const heading of Array.from(node.querySelectorAll(selector))) {
109
+ add_anchor_to_heading(heading, icon_svg);
116
110
  }
117
111
  });
118
112
  observer.observe(node, { childList: true, subtree: true });
@@ -0,0 +1,35 @@
1
+ // Starry-night highlighter for mdsvex
2
+ import { common, createStarryNight } from '@wooorm/starry-night';
3
+ import source_svelte from '@wooorm/starry-night/source.svelte';
4
+ // Escape HTML special characters in text content (not for attribute values)
5
+ const escape_html_text = (str) => str.replace(/&/g, `&amp;`).replace(/</g, `&lt;`).replace(/>/g, `&gt;`);
6
+ // Convert HAST to HTML string (simplified - only handles what starry-night outputs)
7
+ export const hast_to_html = (node) => {
8
+ if (node.type === `text`)
9
+ return escape_html_text(node.value);
10
+ if (node.type === `root`)
11
+ return node.children.map(hast_to_html).join(``);
12
+ const { tagName, properties, children } = node;
13
+ const cls = properties?.className?.join(` `);
14
+ const attrs = cls ? ` class="${cls}"` : ``;
15
+ const inner = children?.map(hast_to_html).join(``) ?? ``;
16
+ return `<${tagName}${attrs}>${inner}</${tagName}>`;
17
+ };
18
+ // Escape characters that would be interpreted as Svelte template syntax
19
+ const escape_svelte = (html) => html.replace(/\{/g, `&#123;`).replace(/\}/g, `&#125;`);
20
+ // Shared starry-night instance (grammars loaded once at build time)
21
+ // Uses common bundle (34 grammars) + Svelte
22
+ export const starry_night = await createStarryNight([...common, source_svelte]);
23
+ // mdsvex highlighter function
24
+ export function starry_night_highlighter(code, lang) {
25
+ const lang_key = lang?.toLowerCase();
26
+ const scope = lang_key ? starry_night.flagToScope(lang_key) : undefined;
27
+ if (!scope) {
28
+ // Return escaped code if language not supported
29
+ const escaped = escape_svelte(escape_html_text(code));
30
+ return `<pre class="highlight"><code>${escaped}</code></pre>`;
31
+ }
32
+ const tree = starry_night.highlight(code, scope);
33
+ const html = escape_svelte(hast_to_html(tree));
34
+ return `<pre class="highlight highlight-${lang_key}"><code>${html}</code></pre>`;
35
+ }
@@ -0,0 +1,7 @@
1
+ export { hast_to_html, starry_night, starry_night_highlighter } from './highlighter.js';
2
+ export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
3
+ export { default as vite_plugin } from './vite-plugin.js';
4
+ import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
5
+ type SveltePreprocessOptions = Parameters<typeof _sveltePreprocess>[0];
6
+ type PreprocessorGroup = ReturnType<typeof _sveltePreprocess>;
7
+ export declare function sveltePreprocess(opts?: SveltePreprocessOptions): PreprocessorGroup;
@@ -0,0 +1,23 @@
1
+ // Live examples - transforms ```svelte example code blocks into rendered components
2
+ // with syntax highlighting and live preview
3
+ export { hast_to_html, starry_night, starry_night_highlighter } from './highlighter.js';
4
+ export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
5
+ export { default as vite_plugin } from './vite-plugin.js';
6
+ import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
7
+ // Wrap sveltePreprocess to skip markdown files - otherwise it transpiles code inside
8
+ // markdown code fences, losing whitespace formatting
9
+ const is_markdown = (filename) => /\.(md|mdx|svx)$/.test(filename ?? ``);
10
+ export function sveltePreprocess(opts) {
11
+ const base = _sveltePreprocess(opts);
12
+ return {
13
+ markup: async (args) => is_markdown(args.filename)
14
+ ? { code: args.content }
15
+ : (await base.markup?.(args)) ?? { code: args.content },
16
+ script: async (args) => is_markdown(args.filename)
17
+ ? { code: args.content }
18
+ : (await base.script?.(args)) ?? { code: args.content },
19
+ style: async (args) => is_markdown(args.filename)
20
+ ? { code: args.content }
21
+ : (await base.style?.(args)) ?? { code: args.content },
22
+ };
23
+ }
@@ -0,0 +1,32 @@
1
+ export declare const EXAMPLE_MODULE_PREFIX = "___live_example___";
2
+ export declare const EXAMPLE_COMPONENT_PREFIX = "LiveExample___";
3
+ interface RemarkMeta {
4
+ Wrapper?: string | [string, string];
5
+ filename?: string;
6
+ csr?: boolean;
7
+ example?: boolean;
8
+ hideScript?: boolean;
9
+ hideStyle?: boolean;
10
+ [key: string]: unknown;
11
+ }
12
+ interface RemarkOptions {
13
+ defaults?: Partial<RemarkMeta>;
14
+ }
15
+ interface RemarkTree {
16
+ type: string;
17
+ children: RemarkNode[];
18
+ }
19
+ interface RemarkNode {
20
+ type: string;
21
+ lang?: string;
22
+ meta?: string;
23
+ value?: string;
24
+ children?: RemarkNode[];
25
+ }
26
+ interface RemarkFile {
27
+ filename: string;
28
+ cwd: string;
29
+ }
30
+ type RemarkTransformer = (tree: RemarkTree, file: RemarkFile) => void;
31
+ declare function remark(options?: RemarkOptions): RemarkTransformer;
32
+ export default remark;
@@ -0,0 +1,185 @@
1
+ // Remark plugin - transforms ```svelte example code blocks into rendered components
2
+ import { Buffer } from 'node:buffer';
3
+ import path from 'node:path';
4
+ import { hast_to_html, starry_night } from './highlighter.js';
5
+ // Base64 encode to prevent preprocessors from modifying the content
6
+ const to_base64 = (src) => Buffer.from(src, `utf-8`).toString(`base64`);
7
+ // Escape backticks and template literal syntax for embedding in template literals
8
+ const encode_escapes = (src) => src.replace(/`/g, `\\\``).replace(/\$\{/g, `\\$\{`);
9
+ // Regex to find <script> block in svelte
10
+ // Note: These patterns handle common cases but may have edge cases with nested
11
+ // comments containing </script> strings or complex attribute syntax
12
+ const RE_SCRIPT_START = /<script(?:\s+?[a-zA-Z]+(=(?:["']){0,1}[a-zA-Z0-9]+(?:["']){0,1}){0,1})*\s*?>/;
13
+ const RE_SCRIPT_BLOCK = /(<script[\s\S]*?>)([\s\S]*?)(<\/script>)/g;
14
+ const RE_STYLE_BLOCK = /(<style[\s\S]*?>)([\s\S]*?)(<\/style>)/g;
15
+ // Parses key=value pairs from a string. Supports strings (with escaped quotes),
16
+ // numbers, booleans, and arrays. Note: nested structures in arrays are not supported.
17
+ const RE_PARSE_META = /(\w+=\d+|\w+="(?:[^"\\]|\\.)*"|\w+=\[[^\]]*\]|\w+)/g;
18
+ export const EXAMPLE_MODULE_PREFIX = `___live_example___`;
19
+ export const EXAMPLE_COMPONENT_PREFIX = `LiveExample___`;
20
+ // Languages that render as live Svelte components (O(1) lookup)
21
+ const LIVE_LANGUAGES = new Set([`svelte`, `html`]);
22
+ // Simple tree traversal - finds all nodes of a given type
23
+ const visit = (tree, type, callback) => {
24
+ const walk = (nodes) => {
25
+ for (const node of nodes) {
26
+ if (node.type === type)
27
+ callback(node);
28
+ if (node.children)
29
+ walk(node.children);
30
+ }
31
+ };
32
+ walk(tree.children);
33
+ };
34
+ // Default wrapper component
35
+ const DEFAULT_WRAPPER = `$lib/CodeExample.svelte`;
36
+ function remark(options = {}) {
37
+ const { defaults = {} } = options;
38
+ return function transformer(tree, file) {
39
+ const examples = [];
40
+ // Track wrapper imports to avoid duplicates and generate unique aliases
41
+ const wrapper_aliases = new Map(); // wrapper key -> alias name
42
+ const filename = path.relative(file.cwd, file.filename);
43
+ // Helper to get or create a wrapper alias
44
+ function get_wrapper_alias(wrapper) {
45
+ // Use '::' as delimiter to avoid misparsing paths with colons (Windows, URLs)
46
+ const wrapper_key = typeof wrapper === `string`
47
+ ? wrapper
48
+ : `${wrapper[0]}::${wrapper[1]}`;
49
+ let alias = wrapper_aliases.get(wrapper_key);
50
+ if (!alias) {
51
+ alias = `Example_${wrapper_aliases.size}`;
52
+ wrapper_aliases.set(wrapper_key, alias);
53
+ }
54
+ return alias;
55
+ }
56
+ visit(tree, `code`, (node) => {
57
+ const meta = {
58
+ Wrapper: DEFAULT_WRAPPER,
59
+ filename,
60
+ ...defaults,
61
+ ...parse_meta(node.meta || ``),
62
+ };
63
+ const { csr, example, Wrapper } = meta;
64
+ // find code blocks with `example` meta in supported languages
65
+ if (example && node.lang && starry_night.flagToScope(node.lang)) {
66
+ const is_live = LIVE_LANGUAGES.has(node.lang);
67
+ const wrapper_alias = is_live ? get_wrapper_alias(Wrapper ?? DEFAULT_WRAPPER) : ``;
68
+ const value = create_example_component(node.value || ``, meta, is_live ? examples.length : -1, // -1 for code-only (no component import needed)
69
+ node.lang, is_live, wrapper_alias);
70
+ // Only track live examples for component imports
71
+ if (is_live) {
72
+ examples.push({ csr, wrapper_alias });
73
+ }
74
+ node.type = `paragraph`;
75
+ node.children = [{ type: `text`, value }];
76
+ delete node.lang;
77
+ delete node.meta;
78
+ delete node.value;
79
+ }
80
+ });
81
+ // add imports for each generated example
82
+ let scripts = ``;
83
+ // Add wrapper imports
84
+ // Use '::' as the tuple delimiter to avoid misparsing Windows paths (C:\path)
85
+ // or URLs (https://example.com) that contain single colons
86
+ for (const [wrapper_key, alias] of wrapper_aliases) {
87
+ const double_colon_idx = wrapper_key.indexOf(`::`);
88
+ if (double_colon_idx === -1) {
89
+ // Simple string path (default import)
90
+ scripts += `import ${alias} from "${wrapper_key}";\n`;
91
+ }
92
+ else {
93
+ // Tuple [module, export] using '::' delimiter
94
+ const module_path = wrapper_key.slice(0, double_colon_idx);
95
+ const export_name = wrapper_key.slice(double_colon_idx + 2);
96
+ scripts += `import { ${export_name} as ${alias} } from "${module_path}";\n`;
97
+ }
98
+ }
99
+ // Add example component imports
100
+ for (const [idx, ex] of examples.entries()) {
101
+ if (!ex.csr) {
102
+ scripts +=
103
+ `import ${EXAMPLE_COMPONENT_PREFIX}${idx} from "${EXAMPLE_MODULE_PREFIX}${idx}.svelte";\n`;
104
+ }
105
+ }
106
+ // Try to inject imports into existing script block
107
+ let injected = false;
108
+ visit(tree, `html`, (node) => {
109
+ if (!injected && node.value && RE_SCRIPT_START.test(node.value)) {
110
+ node.value = node.value.replace(RE_SCRIPT_START, (opening_tag) => `${opening_tag}\n${scripts}`);
111
+ injected = true;
112
+ }
113
+ });
114
+ // Create script block if none existed
115
+ if (!injected) {
116
+ tree.children.push({ type: `html`, value: `<script>\n${scripts}</script>` });
117
+ }
118
+ };
119
+ }
120
+ function parse_meta(meta) {
121
+ const result = {};
122
+ for (const part of meta.match(RE_PARSE_META) ?? []) {
123
+ const eq = part.indexOf(`=`);
124
+ const key = eq === -1 ? part : part.slice(0, eq);
125
+ const value = eq === -1 ? `true` : part.slice(eq + 1);
126
+ try {
127
+ result[key] = JSON.parse(value);
128
+ }
129
+ catch {
130
+ throw new Error(`Unable to parse meta \`${key}=${value}\``);
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ function format_code(code, meta) {
136
+ let result = code;
137
+ if (meta.hideScript)
138
+ result = result.replace(RE_SCRIPT_BLOCK, ``);
139
+ if (meta.hideStyle)
140
+ result = result.replace(RE_STYLE_BLOCK, ``);
141
+ return result.trim();
142
+ }
143
+ function create_example_component(value, meta, index, lang, is_live, wrapper_alias) {
144
+ const code = format_code(value, meta);
145
+ const scope = starry_night.flagToScope(lang);
146
+ if (!scope)
147
+ throw new Error(`Unsupported language: ${lang}`);
148
+ const tree = starry_night.highlight(code, scope);
149
+ // Convert newlines to &#10; to prevent bundlers from stripping whitespace
150
+ const highlighted = hast_to_html(tree).replace(/\n/g, `&#10;`);
151
+ // Code-only examples (ts, js, css, etc.) - just render highlighted code block
152
+ if (!is_live) {
153
+ // Close and reopen <p> to avoid block-in-inline HTML nesting issues
154
+ return `</p><pre class="highlight highlight-${lang}"><code>{@html ${JSON.stringify(highlighted)}}</code></pre><p>`;
155
+ }
156
+ // Live examples (svelte, html) - render with CodeExample wrapper
157
+ const component = `${EXAMPLE_COMPONENT_PREFIX}${index}`;
158
+ const base64_src = to_base64(value);
159
+ const escaped_src = JSON.stringify(encode_escapes(code));
160
+ const escaped_meta = encode_escapes(JSON.stringify(meta));
161
+ // Close and reopen <p> to avoid block-in-inline HTML nesting issues
162
+ return `</p>
163
+ <${wrapper_alias}
164
+ __live_example_src={"${base64_src}"}
165
+ src={${escaped_src}}
166
+ meta={${escaped_meta}}
167
+ >
168
+ {#snippet example()}
169
+ ${meta.csr
170
+ ? `{#if typeof window !== 'undefined'}
171
+ {#await import("${EXAMPLE_MODULE_PREFIX}${index}.svelte") then module}
172
+ {@const ${component} = module.default}
173
+ <${component} />
174
+ {/await}
175
+ {/if}`
176
+ : `<${component} />`}
177
+ {/snippet}
178
+
179
+ {#snippet code()}
180
+ {@html ${JSON.stringify(highlighted)}}
181
+ {/snippet}
182
+ </${wrapper_alias}>
183
+ <p>`;
184
+ }
185
+ export default remark;
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'vite';
2
+ import { EXAMPLE_MODULE_PREFIX } from './mdsvex-transform.js';
3
+ export { EXAMPLE_MODULE_PREFIX };
4
+ export default function live_examples_plugin(options?: {
5
+ extensions?: string[];
6
+ }): Plugin;
@@ -0,0 +1,170 @@
1
+ // Vite plugin - handles virtual module resolution for example components
2
+ // @ts-expect-error no types available
3
+ import ast from 'abstract-syntax-tree';
4
+ import { Buffer } from 'node:buffer';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { EXAMPLE_MODULE_PREFIX } from './mdsvex-transform.js';
8
+ export { EXAMPLE_MODULE_PREFIX };
9
+ // Max chars to scan after property end for trailing comma/whitespace cleanup
10
+ const TRAILING_CLEANUP_BOUND = 50; // Generous bound - typical trailing content is ", " (2 chars)
11
+ // Apply edits in reverse order so positions stay valid
12
+ const apply_edits = (source, edits) => edits
13
+ .sort((a, b) => b.start - a.start)
14
+ .reduce((str, { start, end, content }) => str.slice(0, start) + content + str.slice(end), source);
15
+ export default function live_examples_plugin(options = {}) {
16
+ const { extensions = [`.svelte.md`, `.md`, `.svx`] } = options;
17
+ // Extracted examples as individual virtual files (id -> svelte source)
18
+ const virtual_files = new Map();
19
+ // Reverse lookup: parent markdown path -> set of virtual file IDs (for O(1) HMR lookups)
20
+ const parent_to_virtual = new Map();
21
+ let vite_server;
22
+ return {
23
+ name: `live-examples-plugin`,
24
+ configureServer(server) {
25
+ vite_server = server;
26
+ },
27
+ resolveId(id) {
28
+ if (id.includes(EXAMPLE_MODULE_PREFIX)) {
29
+ // Force absolute path (dev uses relative, prod uses absolute)
30
+ // Use posix.join to ensure forward slashes on all platforms (Vite normalizes to /)
31
+ const cwd = process.cwd().replace(/\\/g, `/`);
32
+ return id.includes(cwd) ? id : path.posix.join(cwd, id);
33
+ }
34
+ },
35
+ load(id) {
36
+ if (id.includes(EXAMPLE_MODULE_PREFIX)) {
37
+ // Strip query parameters - Vite requests derived modules (styles, etc.) with queries
38
+ // like ?inline&svelte&type=style&lang.css but we store the base path
39
+ const [base_id, query = ``] = id.split(`?`);
40
+ const src = virtual_files.get(base_id);
41
+ if (src)
42
+ return src;
43
+ // Virtual file not found - can happen during SSR/parallel builds when derived
44
+ // modules (styles, scripts) are requested before parent markdown is transformed.
45
+ // For derived module requests, return empty content to avoid crashes.
46
+ if (query.includes(`type=style`) || query.includes(`type=script`) ||
47
+ query.includes(`type=module`)) {
48
+ return ``;
49
+ }
50
+ // For main component requests in production, fail the build
51
+ const msg = `Example src not found for ${id}`;
52
+ if (process.env.NODE_ENV === `production`) {
53
+ throw new Error(msg);
54
+ }
55
+ // In dev, warn and return error component to surface issue visibly
56
+ this.warn(msg);
57
+ return `<script>console.error(${JSON.stringify(msg)})</script><p style="color:red">${msg}</p>`;
58
+ }
59
+ },
60
+ transform(code, id) {
61
+ // Strip query params for extension check (Vite adds ?query for HMR, styles, etc.)
62
+ const base_id = id.split(`?`)[0];
63
+ // Skip non-matching files
64
+ const is_example_module = id.includes(EXAMPLE_MODULE_PREFIX);
65
+ const is_markdown = extensions.some((ext) => base_id.endsWith(ext));
66
+ if (!is_example_module && !is_markdown)
67
+ return;
68
+ // Skip derived modules (styles, etc.) - only process the main markdown file
69
+ // Vite creates derived modules like ?svelte&type=style&lang.css for style blocks
70
+ if (id.includes(`?svelte&type=`))
71
+ return { code, map: { mappings: `` } };
72
+ if (is_markdown) {
73
+ // Use AST for precise node location, collect edits to apply at end
74
+ let tree;
75
+ try {
76
+ tree = ast.parse(code, { ranges: true });
77
+ }
78
+ catch {
79
+ // Code may contain Svelte syntax that the JS parser can't handle
80
+ // (e.g., template blocks, special directives). Skip transformation.
81
+ return { code, map: { mappings: `` } };
82
+ }
83
+ const edits = [];
84
+ // Find all __live_example_src properties
85
+ const src_props = ast.find(tree, {
86
+ type: `Property`,
87
+ key: { name: `__live_example_src` },
88
+ });
89
+ for (const [idx, prop] of src_props.entries()) {
90
+ // Extract the string literal content (base64 encoded)
91
+ const string_literals = ast.find(prop, {
92
+ type: `Literal`,
93
+ });
94
+ if (string_literals.length === 0)
95
+ continue;
96
+ const value_node = string_literals[0];
97
+ // AST Literal nodes store value in .value property (string for literals)
98
+ const src = Buffer.from(String(value_node.value ?? ``), `base64`).toString(`utf-8`);
99
+ // Use base_id (without query params) to ensure consistent virtual file IDs
100
+ const virtual_id = `${base_id}${EXAMPLE_MODULE_PREFIX}${idx}.svelte`;
101
+ if (src !== virtual_files.get(virtual_id)) {
102
+ virtual_files.set(virtual_id, src);
103
+ // Update reverse lookup for HMR (get-or-create pattern)
104
+ const virtual_set = parent_to_virtual.get(base_id) ?? new Set();
105
+ if (!parent_to_virtual.has(base_id)) {
106
+ parent_to_virtual.set(base_id, virtual_set);
107
+ }
108
+ virtual_set.add(virtual_id);
109
+ // Invalidate modules for HMR
110
+ if (vite_server) {
111
+ const mod = vite_server.moduleGraph.getModuleById(virtual_id);
112
+ const parent_mod = vite_server.moduleGraph.getModuleById(base_id);
113
+ if (mod)
114
+ vite_server.moduleGraph.invalidateModule(mod);
115
+ if (parent_mod)
116
+ vite_server.moduleGraph.invalidateModule(parent_mod);
117
+ }
118
+ }
119
+ // Remove the property (including trailing comma/whitespace)
120
+ if (prop.start !== undefined && prop.end !== undefined) {
121
+ let end = prop.end;
122
+ const max_end = Math.min(prop.end + TRAILING_CLEANUP_BOUND, code.length);
123
+ while (end < max_end && /[\s,]/.test(code[end]))
124
+ end++;
125
+ edits.push({ start: prop.start, end, content: `` });
126
+ }
127
+ }
128
+ // Update import paths (static and dynamic) to use virtual file IDs
129
+ const imports = [
130
+ ...ast.find(tree, { type: `ImportDeclaration` }),
131
+ ...ast.find(tree, { type: `ImportExpression` }),
132
+ ];
133
+ for (const { source } of imports) {
134
+ const match = source?.value?.match(/___live_example___(\d+)\.svelte/);
135
+ if (match && source?.start !== undefined && source?.end !== undefined) {
136
+ const virtual_id = `${base_id}${EXAMPLE_MODULE_PREFIX}${match[1]}.svelte`;
137
+ edits.push({
138
+ start: source.start + 1,
139
+ end: source.end - 1,
140
+ content: virtual_id,
141
+ });
142
+ }
143
+ }
144
+ return {
145
+ code: apply_edits(code, edits),
146
+ map: { mappings: `` },
147
+ };
148
+ }
149
+ return { code, map: { mappings: `` } };
150
+ },
151
+ handleHotUpdate(ctx) {
152
+ // Collect virtual file modules that need HMR updates
153
+ const additional_modules = [];
154
+ // Normalize to forward slashes (ctx.file uses OS separators, Map keys use Vite's forward slashes)
155
+ const file = ctx.file.replace(/\\/g, `/`);
156
+ // O(1) lookup using reverse map instead of iterating all virtual files
157
+ if (extensions.some((ext) => file.endsWith(ext))) {
158
+ const virtual_ids = parent_to_virtual.get(file);
159
+ if (virtual_ids) {
160
+ for (const id of virtual_ids) {
161
+ const mod = ctx.server.moduleGraph.getModuleById(id);
162
+ if (mod)
163
+ additional_modules.push(mod);
164
+ }
165
+ }
166
+ }
167
+ return [...additional_modules, ...ctx.modules];
168
+ },
169
+ };
170
+ }
package/dist/types.d.ts CHANGED
@@ -78,6 +78,14 @@ export interface MultiSelectEvents<T extends Option = Option> {
78
78
  option: T | null;
79
79
  index: number | null;
80
80
  }) => unknown;
81
+ onundo?: (data: {
82
+ previous: T[];
83
+ current: T[];
84
+ }) => unknown;
85
+ onredo?: (data: {
86
+ previous: T[];
87
+ current: T[];
88
+ }) => unknown;
81
89
  }
82
90
  export interface LoadOptionsParams {
83
91
  search: string;
@@ -152,7 +160,7 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
152
160
  disabled?: boolean;
153
161
  disabledInputTitle?: string;
154
162
  duplicateOptionMsg?: string;
155
- duplicates?: boolean;
163
+ duplicates?: boolean | `case-insensitive`;
156
164
  keepSelectedInDropdown?: false | `plain` | `checkboxes`;
157
165
  key?: (opt: T) => unknown;
158
166
  filterFunc?: (opt: T, searchText: string) => boolean;
@@ -222,12 +230,19 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
222
230
  collapseAllGroups?: () => void;
223
231
  expandAllGroups?: () => void;
224
232
  shortcuts?: Partial<KeyboardShortcuts>;
233
+ history?: boolean | number;
234
+ undo?: () => boolean;
235
+ redo?: () => boolean;
236
+ canUndo?: boolean;
237
+ canRedo?: boolean;
225
238
  }
226
239
  export interface KeyboardShortcuts {
227
240
  select_all?: string | null;
228
241
  clear_all?: string | null;
229
242
  open?: string | null;
230
243
  close?: string | null;
244
+ undo?: string | null;
245
+ redo?: string | null;
231
246
  }
232
247
  export interface NavRouteObject {
233
248
  href: string;
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Option } from './types';
2
+ export declare function get_uuid(): string;
2
3
  export declare const is_object: (val: unknown) => val is Record<string, unknown>;
3
4
  export declare const has_group: <T extends Option>(opt: T) => opt is T & {
4
5
  group: string;
5
6
  };
6
7
  export declare const get_label: (opt: Option) => string | number;
8
+ export declare const get_option_key: (opt: Option) => unknown;
7
9
  export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
8
10
  export declare function fuzzy_match(search_text: string, target_text: string): boolean;
package/dist/utils.js CHANGED
@@ -1,3 +1,14 @@
1
+ let uuid_counter = 0;
2
+ // Generates a UUID for component IDs. Uses native crypto.randomUUID when available.
3
+ // Fallback uses timestamp+counter - sufficient for DOM IDs (uniqueness, not security).
4
+ // Cryptographic randomness is unnecessary here since these IDs are only used for
5
+ // associating labels with inputs and ensuring unique DOM element identifiers.
6
+ export function get_uuid() {
7
+ if (globalThis.crypto?.randomUUID)
8
+ return globalThis.crypto.randomUUID();
9
+ const hex = (Date.now().toString(16) + (uuid_counter++).toString(16)).padStart(32, `0`);
10
+ return hex.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, `$1-$2-$3-$4-$5`);
11
+ }
1
12
  // Type guard for checking if a value is a non-null object
2
13
  export const is_object = (val) => typeof val === `object` && val !== null;
3
14
  // Type guard for checking if an option has a group key
@@ -14,6 +25,10 @@ export const get_label = (opt) => {
14
25
  }
15
26
  return `${opt}`;
16
27
  };
28
+ // Generate a unique key for an option, preserving value identity
29
+ // For object options: uses value if defined, otherwise label (no case normalization)
30
+ // For primitives: the primitive itself
31
+ export const get_option_key = (opt) => is_object(opt) ? opt.value ?? get_label(opt) : opt;
17
32
  // This function is used extract CSS strings from a {selected, option} style
18
33
  // object to be used in the style attribute of the option.
19
34
  // If the style is a string, it will be returned as is