lutra 0.1.32 → 0.1.34

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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Dialog.svelte +49 -34
  3. package/dist/components/Dialog.svelte.d.ts +5 -5
  4. package/dist/components/Layout.svelte +25 -24
  5. package/dist/components/Layout.svelte.d.ts +1 -1
  6. package/dist/components/MenuDropdown.svelte +172 -191
  7. package/dist/components/MenuDropdown.svelte.d.ts +2 -1
  8. package/dist/components/MenuItem.svelte +11 -2
  9. package/dist/components/MenuItem.svelte.d.ts +1 -0
  10. package/dist/components/MenuItemContent.svelte +5 -0
  11. package/dist/components/MenuItemContent.svelte.d.ts +2 -0
  12. package/dist/components/MenuTypes.d.ts +7 -2
  13. package/dist/components/Modal.svelte +236 -82
  14. package/dist/components/Modal.svelte.d.ts +20 -8
  15. package/dist/components/ModalContent.svelte +49 -7
  16. package/dist/components/ModalContent.svelte.d.ts +5 -0
  17. package/dist/components/ModalTypes.d.ts +4 -4
  18. package/dist/components/ModalTypes.js +1 -1
  19. package/dist/components/Popover.svelte +222 -0
  20. package/dist/components/Popover.svelte.d.ts +41 -0
  21. package/dist/components/Toast.svelte +150 -0
  22. package/dist/components/Toast.svelte.d.ts +20 -0
  23. package/dist/components/ToastContainer.svelte +128 -0
  24. package/dist/components/ToastContainer.svelte.d.ts +3 -0
  25. package/dist/components/Tooltip.svelte +60 -66
  26. package/dist/components/Tooltip.svelte.d.ts +1 -1
  27. package/dist/components/index.d.ts +4 -6
  28. package/dist/components/index.js +5 -7
  29. package/dist/components/modals.svelte.d.ts +15 -0
  30. package/dist/components/modals.svelte.js +74 -56
  31. package/dist/components/toasts.svelte.d.ts +77 -0
  32. package/dist/components/toasts.svelte.js +69 -0
  33. package/dist/css/1-props.css +21 -1
  34. package/dist/css/2-base.css +27 -0
  35. package/dist/css/themes/DefaultTheme.css +4 -0
  36. package/dist/form/ImageUpload.svelte +8 -4
  37. package/dist/form/types.d.ts +3 -3
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.js +1 -0
  40. package/dist/types.d.ts +8 -0
  41. package/dist/util/RenderContent.svelte +49 -0
  42. package/dist/util/RenderContent.svelte.d.ts +32 -0
  43. package/dist/util/StringOrSnippet.svelte +8 -3
  44. package/dist/util/StringOrSnippet.svelte.d.ts +1 -1
  45. package/package.json +10 -10
  46. package/dist/components/Notification.svelte +0 -115
  47. package/dist/components/Notification.svelte.d.ts +0 -12
  48. package/dist/components/Overlay.svelte +0 -31
  49. package/dist/components/Overlay.svelte.d.ts +0 -14
  50. package/dist/components/OverlayContainer.svelte +0 -31
  51. package/dist/components/OverlayContainer.svelte.d.ts +0 -18
  52. package/dist/components/OverlayLayer.svelte +0 -168
  53. package/dist/components/OverlayLayer.svelte.d.ts +0 -8
  54. package/dist/components/notifications.svelte.d.ts +0 -21
  55. package/dist/components/notifications.svelte.js +0 -30
  56. package/dist/components/overlays.svelte.d.ts +0 -36
  57. package/dist/components/overlays.svelte.js +0 -44
  58. package/dist/util/StringOrComponent.svelte +0 -20
  59. package/dist/util/StringOrComponent.svelte.d.ts +0 -8
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lutra
2
2
 
3
- UI library for Svelte 5.
3
+ UI library for Svelte 5 currently under active development.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1,52 +1,55 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import { slidefade } from "../util/transitions.js";
4
3
 
5
4
  /**
6
- * @description
7
- * A dialog component that follows the HTML dialog element spec.
8
- * Provides a modal dialog with a backdrop and proper focus management.
5
+ * A simple dialog component using the native `<dialog>` element.
6
+ * For more features (buttons, scrim control, etc.), use `<Modal>` instead.
9
7
  */
10
8
  let {
11
9
  open = $bindable(false),
12
10
  title,
13
11
  children,
14
- onClose,
12
+ onclose,
15
13
  }: {
16
- /** [Bindable] Whether the dialog is open */
14
+ /** Whether the dialog is open (bindable) */
17
15
  open?: boolean;
18
- /** The title of the dialog */
16
+ /** Optional title for the dialog header */
19
17
  title?: string;
20
- /** The content of the dialog */
18
+ /** Dialog content */
21
19
  children: Snippet;
22
- /** Callback when the dialog is closed */
23
- onClose?: () => void;
20
+ /** Callback when dialog closes */
21
+ onclose?: () => void;
24
22
  } = $props();
25
23
 
26
- let dialog: HTMLDialogElement;
27
-
28
- $effect(() => {
29
- if (open && dialog) {
30
- dialog.showModal();
31
- } else if (dialog) {
32
- dialog.close();
33
- }
34
- });
24
+ let dialogEl: HTMLDialogElement | null = $state(null);
35
25
 
36
26
  function handleClose() {
37
- if (onClose) onClose();
27
+ open = false;
28
+ onclose?.();
38
29
  }
39
30
 
31
+ function handleCancel(e: Event) {
32
+ handleClose();
33
+ }
34
+
35
+ $effect(() => {
36
+ if (open && dialogEl && !dialogEl.open) {
37
+ dialogEl.showModal();
38
+ } else if (!open && dialogEl?.open) {
39
+ dialogEl.close();
40
+ }
41
+ });
40
42
  </script>
41
43
 
42
44
  <dialog
43
- bind:this={dialog}
45
+ bind:this={dialogEl}
44
46
  class="Dialog"
45
- transition:slidefade
47
+ onclose={handleClose}
48
+ oncancel={handleCancel}
46
49
  >
47
50
  {#if title}
48
51
  <header class="DialogHeader">
49
- <h6>{title}</h6>
52
+ <h4>{title}</h4>
50
53
  </header>
51
54
  {/if}
52
55
  <div class="DialogContent">
@@ -57,22 +60,34 @@
57
60
  <style>
58
61
  .Dialog {
59
62
  padding: 0;
60
- border: var(--field-border-size) var(--field-border-style) var(--field-border-color);
61
- border-radius: var(--field-radius);
62
- background: var(--bg-surface);
63
- color: var(--fg-surface);
63
+ border: var(--modal-border-size) var(--modal-border-style) var(--modal-border-color);
64
+ border-radius: var(--modal-border-radius);
65
+ background: var(--modal-background);
66
+ color: var(--text-color-p);
64
67
  max-width: min(calc(100vw - 2rem), 32rem);
65
- max-height: min(calc(100vh - 2rem), 32rem);
68
+ max-height: min(calc(100vh - 2rem), 80vh);
69
+ box-shadow: 0 0.5rem 1rem var(--modal-shadow-color);
66
70
  }
71
+
67
72
  .Dialog::backdrop {
68
- background: var(--bg-scrim);
69
- backdrop-filter: blur(2px);
73
+ background: var(--scrim-background);
74
+ backdrop-filter: var(--scrim-backdrop-filter);
70
75
  }
76
+
71
77
  .DialogHeader {
72
- padding: var(--pad-m);
73
- border-bottom: var(--field-border-size) var(--field-border-style) var(--field-border-color);
78
+ padding: var(--modal-padding-block) var(--modal-padding-inline);
79
+ border-bottom: var(--modal-border-size) var(--modal-border-style) var(--modal-border-color);
74
80
  }
81
+
82
+ .DialogHeader h4 {
83
+ margin: 0;
84
+ font-size: var(--font-size-h5);
85
+ font-weight: var(--font-weight-semibold);
86
+ color: var(--text-color-heading);
87
+ }
88
+
75
89
  .DialogContent {
76
- padding: var(--pad-m);
90
+ padding: var(--modal-padding-block) var(--modal-padding-inline);
91
+ overflow: auto;
77
92
  }
78
- </style>
93
+ </style>
@@ -1,13 +1,13 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
- /** [Bindable] Whether the dialog is open */
3
+ /** Whether the dialog is open (bindable) */
4
4
  open?: boolean;
5
- /** The title of the dialog */
5
+ /** Optional title for the dialog header */
6
6
  title?: string;
7
- /** The content of the dialog */
7
+ /** Dialog content */
8
8
  children: Snippet;
9
- /** Callback when the dialog is closed */
10
- onClose?: () => void;
9
+ /** Callback when dialog closes */
10
+ onclose?: () => void;
11
11
  };
12
12
  declare const Dialog: import("svelte").Component<$$ComponentProps, {}, "open">;
13
13
  type Dialog = ReturnType<typeof Dialog>;
@@ -1,33 +1,34 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import "../css/lutra.css";
3
+ import "../css/lutra.css";
4
4
  import Theme from "./Theme.svelte";
5
- import OverlayContainer from "./OverlayContainer.svelte";
6
- /**
7
- * @description
8
- * Default layout component that imports default styles and wraps the entire application in a theme.
9
- */
10
- let {
11
- theme,
12
- children,
13
- }: {
14
- /** The theme to use for the layout. Leave empty to detect automatically or get from user preferences, if any exist. */
15
- theme?: 'light' | 'dark' | undefined;
16
- /** The content to display. */
17
- children: Snippet;
18
- } = $props();
5
+ import ToastContainer from "./ToastContainer.svelte";
6
+
7
+ /**
8
+ * Default layout component that imports styles and provides theming.
9
+ * Includes ToastContainer for toast notifications.
10
+ */
11
+ let {
12
+ theme,
13
+ children,
14
+ }: {
15
+ /** The theme to use. Leave empty for auto-detection. */
16
+ theme?: 'light' | 'dark' | undefined;
17
+ /** The content to display. */
18
+ children: Snippet;
19
+ } = $props();
19
20
  </script>
20
21
 
21
22
  <Theme theme={theme}>
22
- <div class="Layout">
23
- {@render children()}
24
- </div>
25
- <OverlayContainer />
23
+ <div class="Layout">
24
+ {@render children()}
25
+ </div>
26
+ <ToastContainer />
26
27
  </Theme>
27
28
 
28
29
  <style>
29
- .Layout {
30
- min-height: 100dvh;
31
- height: 100dvh;
32
- }
33
- </style>
30
+ .Layout {
31
+ min-height: 100dvh;
32
+ height: 100dvh;
33
+ }
34
+ </style>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import "../css/lutra.css";
3
3
  type $$ComponentProps = {
4
- /** The theme to use for the layout. Leave empty to detect automatically or get from user preferences, if any exist. */
4
+ /** The theme to use. Leave empty for auto-detection. */
5
5
  theme?: 'light' | 'dark' | undefined;
6
6
  /** The content to display. */
7
7
  children: Snippet;
@@ -1,202 +1,183 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import type { MenuItem as Item } from "./MenuTypes.js";
3
+ import type { MenuItem as Item } from "./MenuTypes.js";
4
4
  import MenuItem from "./MenuItem.svelte";
5
+ import Popover from "./Popover.svelte";
5
6
  import UiContent from "./UIContent.svelte";
6
- import { arrowNavigation, getNextFocusableElement, matchOnType } from "../util/keyboard.svelte.js";
7
- import { findContainingBlock, getPossiblyContainedPosition } from "../util/dom.js";
8
- import Overlay from "./Overlay.svelte";
9
-
10
- /**
11
- * @description
12
- * A menu component that can be used to create dropdown menus.
13
- */
14
- let {
15
- open = $bindable(false),
16
- items,
17
- trigger,
18
- width,
19
- maxWidth,
20
- }: {
21
- /** Whether the menu is open */
22
- open?: boolean;
23
- /** The items to display in the menu */
24
- items: Item[];
25
- /** The trigger for the menu */
26
- trigger: string | Snippet<[{ toggle: () => void, isOpen: boolean }]>;
27
- /** The width of the menu */
28
- width?: string;
29
- /** The max width of the menu */
30
- maxWidth?: string;
31
- } = $props();
32
-
33
- let _open = $state(open);
34
- let triggerEl: HTMLDivElement | null = $state(null);
35
- let contentEl: HTMLDivElement | null = $state(null);
36
- let menuEl: HTMLDivElement | null = $state(null);
37
- let currentIndex: number = $state(-1);
38
- let keyboardHasFocus: boolean = $state(false);
39
-
40
- const id = crypto.randomUUID();
41
-
42
- $effect(() => {
43
- if(_open) {
44
- window.addEventListener('click', clickoutside);
45
- window.addEventListener('keydown', onkeydown);
46
- } else {
47
- window.removeEventListener('click', clickoutside);
48
- window.removeEventListener('keydown', onkeydown);
49
- }
50
- });
51
-
52
- function toggle() {
53
- _open = !_open;
54
- }
55
-
56
- let scrollable = $derived.by(() => {
57
- if(!contentEl) return false;
58
- return contentEl.scrollHeight > contentEl.clientHeight;
59
- });
60
-
61
- function onclick(e: MouseEvent) {
62
- e.preventDefault();
63
- _open = !_open;
64
- }
65
-
66
- function clickoutside(e: MouseEvent) {
67
- if(!_open) return;
68
- if(contentEl && !contentEl.contains(e.target as Node) && !triggerEl?.contains(e.target as Node)) {
69
- _open = false;
70
- }
71
- }
72
-
73
- function onkeydown(e: KeyboardEvent) {
74
- if(!_open) return;
75
- const active = document.activeElement as HTMLButtonElement | HTMLAnchorElement;
76
- switch(e.key) {
77
- case "Escape":
78
- e.preventDefault();
79
- _open = false;
80
- break;
81
- case "Tab":
82
- // try to open the next menu if it exists
83
- e.preventDefault();
84
- e.stopPropagation();
85
- _open = false;
86
- setTimeout(() => {
87
- const nextEl = getNextFocusableElement(menuEl, triggerEl, e.shiftKey ? "previous" : "next");
88
- console.log('nextEl', nextEl)
89
- if(nextEl) {
90
- nextEl.focus();
91
- if(nextEl.tagName === "BUTTON" || nextEl.tagName === "A" && nextEl.parentElement?.classList.contains("Trigger")) {
92
- nextEl.click();
93
- }
94
- }
95
- }, 0);
96
- break;
97
- case "ArrowDown":
98
- e.preventDefault();
99
- arrowNavigation(contentEl, "down");
100
- matchOnType(contentEl, e); // call to reset the search
101
- keyboardHasFocus = true;
102
- break;
103
- case "ArrowUp":
104
- e.preventDefault();
105
- arrowNavigation(contentEl, "up");
106
- matchOnType(contentEl, e); // call to reset the search
107
- keyboardHasFocus = true;
108
- break;
109
- case "Enter":
110
- case "Space":
111
- e.preventDefault();
112
- active.click();
113
- break;
114
- default:
115
- matchOnType(contentEl, e);
116
- }
117
- }
118
-
119
- function mouseover(e: MouseEvent, item: Item, index: number) {
120
- if(item.type === "item") {
121
- currentIndex = index;
122
- }
123
- }
124
-
7
+ import { arrowNavigation, matchOnType } from "../util/keyboard.svelte.js";
8
+
9
+ /**
10
+ * A dropdown menu using the base Popover component.
11
+ * Handles menu-specific keyboard navigation and item rendering.
12
+ */
13
+ let {
14
+ open = $bindable(false),
15
+ items,
16
+ trigger: triggerProp,
17
+ width,
18
+ maxWidth,
19
+ }: {
20
+ /** Whether the menu is open */
21
+ open?: boolean;
22
+ /** The items to display in the menu */
23
+ items: Item[];
24
+ /** The trigger snippet or string */
25
+ trigger: string | Snippet<[{ toggle: () => void; isOpen: boolean; triggerAttrs?: Record<string, string> }]>;
26
+ /** The width of the menu */
27
+ width?: string;
28
+ /** The max width of the menu */
29
+ maxWidth?: string;
30
+ } = $props();
31
+
32
+ let menuEl: HTMLDivElement | null = $state(null);
33
+ let keyboardHasFocus: boolean = $state(false);
34
+
35
+ function handleOpen() {
36
+ window.addEventListener("keydown", onkeydown);
37
+ }
38
+
39
+ function handleClose() {
40
+ window.removeEventListener("keydown", onkeydown);
41
+ keyboardHasFocus = false;
42
+ }
43
+
44
+ function onkeydown(e: KeyboardEvent) {
45
+ if (!open || !menuEl) return;
46
+
47
+ switch (e.key) {
48
+ case "ArrowDown":
49
+ e.preventDefault();
50
+ arrowNavigation(menuEl, "down");
51
+ matchOnType(menuEl, e);
52
+ keyboardHasFocus = true;
53
+ break;
54
+ case "ArrowUp":
55
+ e.preventDefault();
56
+ arrowNavigation(menuEl, "up");
57
+ matchOnType(menuEl, e);
58
+ keyboardHasFocus = true;
59
+ break;
60
+ case "Enter":
61
+ case " ":
62
+ e.preventDefault();
63
+ (document.activeElement as HTMLElement)?.click();
64
+ break;
65
+ case "Tab":
66
+ e.preventDefault();
67
+ open = false;
68
+ break;
69
+ default:
70
+ matchOnType(menuEl, e);
71
+ }
72
+ }
73
+
74
+ function handleMouseover(e: MouseEvent, item: Item, index: number) {
75
+ if (item.type === "item") {
76
+ keyboardHasFocus = false;
77
+ }
78
+ }
79
+
80
+ function handleSelect(item: Item, index: number) {
81
+ open = false;
82
+ }
83
+
84
+ let scrollable = $derived.by(() => {
85
+ if (!menuEl) return false;
86
+ return menuEl.scrollHeight > menuEl.clientHeight;
87
+ });
88
+
89
+ const isStringTrigger = $derived(typeof triggerProp === "string");
125
90
  </script>
126
91
 
127
-
128
92
  <UiContent>
129
- <div class="MenuDropdown" class:open={_open} bind:this={menuEl}>
130
- <div
131
- class="Trigger"
132
- bind:this={triggerEl}
133
- >
134
- {#if typeof trigger === "string"}
135
- <button type="button" class="button" {onclick} aria-haspopup="true" aria-controls={id} aria-expanded="{_open}">
136
- {trigger}
137
- </button>
138
- {:else}
139
- {@render trigger({ toggle: toggle, isOpen: _open })}
140
- {/if}
141
- </div>
142
- {#if _open && triggerEl}
143
- <Overlay position="anchor" id="o-{id}" anchor={triggerEl} layer="menu">
144
- <div {id}
145
- class="MenuDropdownContent"
146
- style="--menu-width: {width}; --menu-max-width: {maxWidth};"
147
- class:scrollable={scrollable}
148
- role="menu"
149
- bind:this={contentEl}
150
- >
151
- <ul>
152
- {#each items as item, index}
153
- <MenuItem {keyboardHasFocus} onmouseover={mouseover} item={item} {index} />
154
- {/each}
155
- </ul>
156
- </div>
157
- </Overlay>
158
- {/if}
159
- </div>
93
+ <Popover
94
+ bind:open
95
+ position="block-end span-inline-end"
96
+ fallbacks={["flip-block", "flip-inline", "flip-block flip-inline"]}
97
+ offset="0.5rem 0 0 0"
98
+ {width}
99
+ {maxWidth}
100
+ onopen={handleOpen}
101
+ onclose={handleClose}
102
+ class="MenuDropdownPopover"
103
+ >
104
+ {#snippet trigger(popoverArgs)}
105
+ <span class="MenuDropdownTrigger" style={popoverArgs.anchorAttrs.style}>
106
+ {#if isStringTrigger}
107
+ <button
108
+ type="button"
109
+ class="button"
110
+ {...popoverArgs.triggerAttrs}
111
+ aria-haspopup="menu"
112
+ >
113
+ {triggerProp}
114
+ </button>
115
+ {:else if typeof triggerProp !== "string"}
116
+ {@render triggerProp({
117
+ toggle: popoverArgs.toggle,
118
+ isOpen: popoverArgs.isOpen,
119
+ triggerAttrs: popoverArgs.triggerAttrs,
120
+ })}
121
+ {/if}
122
+ </span>
123
+ {/snippet}
124
+
125
+ <div
126
+ class="MenuDropdownContent"
127
+ class:scrollable
128
+ role="menu"
129
+ bind:this={menuEl}
130
+ >
131
+ <ul>
132
+ {#each items as item, index}
133
+ <MenuItem
134
+ {keyboardHasFocus}
135
+ onmouseover={handleMouseover}
136
+ onselect={handleSelect}
137
+ {item}
138
+ {index}
139
+ />
140
+ {/each}
141
+ </ul>
142
+ </div>
143
+ </Popover>
160
144
  </UiContent>
161
145
 
162
146
  <style>
163
- .MenuDropdown {
164
- position: relative;
165
- }
166
-
167
- .Trigger {
168
- position: relative;
169
- }
170
-
171
- .MenuDropdownContent {
172
- max-height: calc(50vh - 2rem);
173
- margin: 0;
174
- z-index: 1000;
175
- margin: 0;
176
- border: var(--menu-border-size) var(--menu-border-style) var(--menu-border-color);
177
- border-radius: var(--menu-border-radius);
178
- box-shadow: 0 0.5rem 1rem var(--shadow-color);
179
- background-color: var(--menu-background-color);
180
- width: var(--menu-width, fit-content);
181
- max-width: var(--menu-max-width, 50ch);
182
- overflow-x: clip;
183
- overflow-y: auto;
184
- scrollbar-width: thin;
185
- scrollbar-color: var(--scrollbar-color);
186
- }
187
-
188
- .MenuDropdownContent.scrollable {
189
- border-top-right-radius: 0;
190
- border-bottom-right-radius: 0;
191
- }
192
-
193
- .MenuDropdownContent :global(:has(li:last-of-type[data-type="item"])) {
194
- padding-block-end: 0.5rem;
195
- }
196
-
197
- ul {
198
- margin: 0;
199
- list-style: none;
200
- padding: 0;
201
- }
147
+ .MenuDropdownTrigger {
148
+ display: inline-block;
149
+ }
150
+
151
+ :global(.MenuDropdownPopover) {
152
+ /* Override Popover defaults for menu styling */
153
+ border: var(--menu-border-size) var(--menu-border-style) var(--menu-border-color);
154
+ border-radius: var(--menu-border-radius);
155
+ box-shadow: 0 0.5rem 1rem var(--shadow-color);
156
+ background-color: var(--menu-background-color);
157
+ }
158
+
159
+ .MenuDropdownContent {
160
+ width: var(--popover-width, fit-content);
161
+ max-width: var(--popover-max-width, 50ch);
162
+ max-height: calc(50vh - 2rem);
163
+ overflow-x: clip;
164
+ overflow-y: auto;
165
+ scrollbar-width: thin;
166
+ scrollbar-color: var(--scrollbar-color);
167
+ }
168
+
169
+ .MenuDropdownContent.scrollable {
170
+ border-top-right-radius: 0;
171
+ border-bottom-right-radius: 0;
172
+ }
173
+
174
+ .MenuDropdownContent :global(:has(li:last-of-type[data-type="item"])) {
175
+ padding-block-end: 0.5rem;
176
+ }
177
+
178
+ ul {
179
+ margin: 0;
180
+ list-style: none;
181
+ padding: 0;
182
+ }
202
183
  </style>
@@ -5,10 +5,11 @@ type $$ComponentProps = {
5
5
  open?: boolean;
6
6
  /** The items to display in the menu */
7
7
  items: Item[];
8
- /** The trigger for the menu */
8
+ /** The trigger snippet or string */
9
9
  trigger: string | Snippet<[{
10
10
  toggle: () => void;
11
11
  isOpen: boolean;
12
+ triggerAttrs?: Record<string, string>;
12
13
  }]>;
13
14
  /** The width of the menu */
14
15
  width?: string;
@@ -7,12 +7,14 @@
7
7
  item,
8
8
  index,
9
9
  onmouseover,
10
+ onselect,
10
11
  keyboardHasFocus,
11
12
  shape = 'default',
12
13
  }: {
13
14
  item: Item;
14
15
  index: number;
15
16
  onmouseover?: (e: MouseEvent, item: Item, index: number) => void;
17
+ onselect?: (item: Item, index: number) => void;
16
18
  keyboardHasFocus?: boolean;
17
19
  shape?: 'default' | 'rounded' | 'pill';
18
20
  } = $props();
@@ -25,6 +27,13 @@
25
27
  el?.focus();
26
28
  }
27
29
  }
30
+
31
+ function handleClick(e: MouseEvent | KeyboardEvent) {
32
+ if(item.type === 'item' && item.onclick) {
33
+ item.onclick(e as MouseEvent, item);
34
+ onselect?.(item, index);
35
+ }
36
+ }
28
37
  </script>
29
38
 
30
39
  <!-- svelte-ignore a11y_mouse_events_have_key_events -->
@@ -59,7 +68,7 @@
59
68
  {/if}
60
69
  </a>
61
70
  {:else if item.onclick}
62
- <button type="button" onclick={(e) => item.type === 'item' ? item.onclick!(e, item) : undefined} class="Item" bind:this={el}>
71
+ <button type="button" onclick={handleClick} class="Item" bind:this={el}>
63
72
  <span class="Content">
64
73
  <MenuItemContent {...item} />
65
74
  </span>
@@ -67,7 +76,7 @@
67
76
  <span class="Shortcut">{item.shortcut}</span>
68
77
  {/if}
69
78
  </button>
70
- {:else if item.component}
79
+ {:else if item.component || item.render || item.snippet}
71
80
  <div class="Item Custom">
72
81
  <MenuItemContent {...item} />
73
82
  </div>