svelte-comp 1.2.5 → 1.2.6
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 +1 -1
- package/package.json +1 -1
- package/dist/App.svelte +0 -551
- package/dist/App.svelte.d.ts +0 -3
- package/dist/Container.svelte +0 -60
- package/dist/Container.svelte.d.ts +0 -12
- package/dist/app.css +0 -235
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -6
- package/dist/lang.d.ts +0 -1081
- package/dist/lang.js +0 -1096
- package/dist/lib/Accordion.svelte +0 -155
- package/dist/lib/Accordion.svelte.d.ts +0 -40
- package/dist/lib/Button.svelte +0 -170
- package/dist/lib/Button.svelte.d.ts +0 -53
- package/dist/lib/Card.svelte +0 -103
- package/dist/lib/Card.svelte.d.ts +0 -42
- package/dist/lib/Carousel.svelte +0 -293
- package/dist/lib/Carousel.svelte.d.ts +0 -13
- package/dist/lib/CheckBox.svelte +0 -210
- package/dist/lib/CheckBox.svelte.d.ts +0 -53
- package/dist/lib/CodeView.svelte +0 -307
- package/dist/lib/CodeView.svelte.d.ts +0 -64
- package/dist/lib/ColorPicker.svelte +0 -161
- package/dist/lib/ColorPicker.svelte.d.ts +0 -40
- package/dist/lib/DatePicker.svelte +0 -170
- package/dist/lib/DatePicker.svelte.d.ts +0 -53
- package/dist/lib/Dialog.svelte +0 -235
- package/dist/lib/Dialog.svelte.d.ts +0 -58
- package/dist/lib/Field.svelte +0 -299
- package/dist/lib/Field.svelte.d.ts +0 -8
- package/dist/lib/FilePicker.svelte +0 -241
- package/dist/lib/FilePicker.svelte.d.ts +0 -52
- package/dist/lib/Form.svelte +0 -438
- package/dist/lib/Form.svelte.d.ts +0 -20
- package/dist/lib/Hamburger.svelte +0 -211
- package/dist/lib/Hamburger.svelte.d.ts +0 -52
- package/dist/lib/Menu.svelte +0 -623
- package/dist/lib/Menu.svelte.d.ts +0 -33
- package/dist/lib/PaginatedCard.svelte +0 -73
- package/dist/lib/PaginatedCard.svelte.d.ts +0 -11
- package/dist/lib/Pagination.svelte +0 -119
- package/dist/lib/Pagination.svelte.d.ts +0 -9
- package/dist/lib/PrimaryColorSelect.svelte +0 -113
- package/dist/lib/PrimaryColorSelect.svelte.d.ts +0 -9
- package/dist/lib/ProgressBar.svelte +0 -141
- package/dist/lib/ProgressBar.svelte.d.ts +0 -48
- package/dist/lib/ProgressCircle.svelte +0 -192
- package/dist/lib/ProgressCircle.svelte.d.ts +0 -39
- package/dist/lib/Radio.svelte +0 -189
- package/dist/lib/Radio.svelte.d.ts +0 -55
- package/dist/lib/SearchInput.svelte +0 -106
- package/dist/lib/SearchInput.svelte.d.ts +0 -13
- package/dist/lib/Select.svelte +0 -524
- package/dist/lib/Select.svelte.d.ts +0 -21
- package/dist/lib/Slider.svelte +0 -253
- package/dist/lib/Slider.svelte.d.ts +0 -56
- package/dist/lib/Splitter.svelte +0 -150
- package/dist/lib/Splitter.svelte.d.ts +0 -43
- package/dist/lib/Switch.svelte +0 -167
- package/dist/lib/Switch.svelte.d.ts +0 -42
- package/dist/lib/Table.svelte +0 -299
- package/dist/lib/Table.svelte.d.ts +0 -17
- package/dist/lib/Tabs.svelte +0 -213
- package/dist/lib/Tabs.svelte.d.ts +0 -48
- package/dist/lib/ThemeToggle.svelte +0 -127
- package/dist/lib/ThemeToggle.svelte.d.ts +0 -32
- package/dist/lib/TimePicker.svelte +0 -269
- package/dist/lib/TimePicker.svelte.d.ts +0 -48
- package/dist/lib/Toast.svelte +0 -226
- package/dist/lib/Toast.svelte.d.ts +0 -14
- package/dist/lib/Tooltip.svelte +0 -110
- package/dist/lib/Tooltip.svelte.d.ts +0 -40
- package/dist/lib/index.d.ts +0 -32
- package/dist/lib/index.js +0 -33
- package/dist/lib/lang.d.ts +0 -158
- package/dist/lib/lang.js +0 -150
- package/dist/lib/types/index.d.ts +0 -111
- package/dist/lib/types/index.js +0 -26
- package/dist/main.d.ts +0 -3
- package/dist/main.js +0 -7
- package/dist/styles.css +0 -232
- package/dist/utils/index.d.ts +0 -34
- package/dist/utils/index.js +0 -268
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @component Hamburger
|
|
3
|
-
* @description Off-canvas navigation drawer controlled by a floating hamburger button.
|
|
4
|
-
*
|
|
5
|
-
* @prop menuItems {Item[]} - Menu entries rendered in the drawer
|
|
6
|
-
* @default []
|
|
7
|
-
*
|
|
8
|
-
* @prop activeItem {string} - ID of the currently active item
|
|
9
|
-
* @default ""
|
|
10
|
-
*
|
|
11
|
-
* @prop header {Snippet} - Custom content rendered above the menu
|
|
12
|
-
*
|
|
13
|
-
* @prop footer {Snippet} - Custom content rendered below the menu
|
|
14
|
-
*
|
|
15
|
-
* @prop closeOnSelect {boolean} - Automatically closes after selecting an item
|
|
16
|
-
* @default true
|
|
17
|
-
*
|
|
18
|
-
* @prop onSelect {(id: string) => void} - Fired when a menu item is chosen
|
|
19
|
-
*
|
|
20
|
-
* @prop onOpenChange {(v: boolean) => void} - Fired when open state changes in controlled mode
|
|
21
|
-
*
|
|
22
|
-
* @prop pressed {boolean} - Controlled open state
|
|
23
|
-
*
|
|
24
|
-
* @prop class {string} - Extra classes applied to the trigger button
|
|
25
|
-
* @default ""
|
|
26
|
-
*
|
|
27
|
-
* @prop width {number | string} - Drawer width (px or CSS value)
|
|
28
|
-
* @default 300
|
|
29
|
-
*
|
|
30
|
-
* @note Clicking outside the panel or pressing `Escape` closes the drawer.
|
|
31
|
-
* @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
|
|
32
|
-
* @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
|
|
33
|
-
* @note When `menuItems` is empty, a "No items" placeholder is shown.
|
|
34
|
-
* @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
|
|
35
|
-
*/
|
|
36
|
-
import type { Snippet } from "svelte";
|
|
37
|
-
import type { Item } from "./types";
|
|
38
|
-
type Props = {
|
|
39
|
-
menuItems?: Item[];
|
|
40
|
-
activeItem?: string;
|
|
41
|
-
header?: Snippet;
|
|
42
|
-
footer?: Snippet;
|
|
43
|
-
closeOnSelect?: boolean;
|
|
44
|
-
onSelect?: (id: string) => void;
|
|
45
|
-
onOpenChange?: (v: boolean) => void;
|
|
46
|
-
pressed?: boolean;
|
|
47
|
-
class?: string;
|
|
48
|
-
width?: number | string;
|
|
49
|
-
};
|
|
50
|
-
declare const Hamburger: import("svelte").Component<Props, {}, "">;
|
|
51
|
-
type Hamburger = ReturnType<typeof Hamburger>;
|
|
52
|
-
export default Hamburger;
|
package/dist/lib/Menu.svelte
DELETED
|
@@ -1,623 +0,0 @@
|
|
|
1
|
-
<!-- src/lib/Menu.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* @component Menu
|
|
5
|
-
* @description A dropdown menu bar component with hover and click interactions.
|
|
6
|
-
*
|
|
7
|
-
* @prop menus {MenuItem[]} - Menu definitions with actions
|
|
8
|
-
* @default []
|
|
9
|
-
*
|
|
10
|
-
* @prop onSelect {(menu: string, action: MenuAction) => void} - Fired when an action is chosen
|
|
11
|
-
* @default () => {}
|
|
12
|
-
*
|
|
13
|
-
* @prop class {string} - Extra classes applied to the menu bar
|
|
14
|
-
* @default ""
|
|
15
|
-
*
|
|
16
|
-
* @prop sz {SizeKey} - Size preset for spacing and text
|
|
17
|
-
* @options xs|sm|md|lg|xl
|
|
18
|
-
* @default sm
|
|
19
|
-
*
|
|
20
|
-
* @note Fully keyboard-safe for focus and mouse interactions.
|
|
21
|
-
* @note Submenus open on hover when another menu is already open.
|
|
22
|
-
* @note Actions that match size keys (`xs`, `sm`, `md`, `lg`, `xl`) are automatically highlighted to reflect the current UI size.
|
|
23
|
-
* @note Uses the same CSS variable architecture as Tabs for consistent look across components.
|
|
24
|
-
* @note No slots; fully controlled via the `menus` structure and `onSelect`.
|
|
25
|
-
*/
|
|
26
|
-
import type { SizeKey, MenuItem, MenuAction } from "./types";
|
|
27
|
-
import { TEXT } from "./types";
|
|
28
|
-
import { cx } from "../utils";
|
|
29
|
-
|
|
30
|
-
type Props = {
|
|
31
|
-
menus?: MenuItem[];
|
|
32
|
-
onSelect?: (menu: string, action: MenuAction) => void;
|
|
33
|
-
class?: string;
|
|
34
|
-
sz?: SizeKey;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
let {
|
|
38
|
-
menus = [],
|
|
39
|
-
onSelect = () => {},
|
|
40
|
-
class: externalClass = "",
|
|
41
|
-
sz = "sm",
|
|
42
|
-
}: Props = $props();
|
|
43
|
-
|
|
44
|
-
let open = $state<string>("");
|
|
45
|
-
let openSub = $state<string>("");
|
|
46
|
-
let activeIndex = $state(-1);
|
|
47
|
-
let activeSubIndex = $state(-1);
|
|
48
|
-
|
|
49
|
-
// Refs for focus control
|
|
50
|
-
let triggerRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
51
|
-
let menuRefs = $state<Record<string, HTMLDivElement>>({});
|
|
52
|
-
let itemRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
53
|
-
let subItemRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
54
|
-
|
|
55
|
-
// Positioning
|
|
56
|
-
let menuTop = $state(0);
|
|
57
|
-
let menuLeft = $state(0);
|
|
58
|
-
|
|
59
|
-
let subMenuRefs = $state<Record<string, HTMLDivElement>>({});
|
|
60
|
-
let subMenuTop = $state(0);
|
|
61
|
-
let subMenuLeft = $state(0);
|
|
62
|
-
|
|
63
|
-
const sizes: Record<SizeKey, string> = {
|
|
64
|
-
xs: "h-7 px-3",
|
|
65
|
-
sm: "h-8 px-3",
|
|
66
|
-
md: "h-9 px-4",
|
|
67
|
-
lg: "h-10 px-4",
|
|
68
|
-
xl: "h-11 px-5",
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const navBase =
|
|
72
|
-
"flex items-stretch pl-2 gap-1 border-b relative z-10 bg-[var(--color-bg-surface)] text-[var(--color-text-default)] border-[var(--border-color-default)]";
|
|
73
|
-
|
|
74
|
-
const subMenuGutter = 8;
|
|
75
|
-
|
|
76
|
-
const topButtonBase =
|
|
77
|
-
"px-4 rounded-xs leading-none transition-colors outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]";
|
|
78
|
-
|
|
79
|
-
const topButtonActive =
|
|
80
|
-
"bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
|
|
81
|
-
const topButtonIdle =
|
|
82
|
-
"hover:bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
|
|
83
|
-
|
|
84
|
-
const menuStyle = $derived(
|
|
85
|
-
`position:fixed; top:${menuTop}px; left:${menuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
|
|
86
|
-
);
|
|
87
|
-
const subMenuStyle = $derived(
|
|
88
|
-
`position:fixed; top:${subMenuTop}px; left:${subMenuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const textCls = $derived(TEXT[sz]);
|
|
92
|
-
const hotkeyColCls = "flex items-center shrink-0";
|
|
93
|
-
|
|
94
|
-
const navCls = $derived(cx(navBase, sizes[sz], textCls, externalClass));
|
|
95
|
-
const topBtnBaseCls = $derived(cx(topButtonBase, sizes[sz], textCls));
|
|
96
|
-
|
|
97
|
-
function actionText(a: MenuAction) {
|
|
98
|
-
if (typeof a === "string") return a;
|
|
99
|
-
return a.label;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function actionId(a: MenuAction) {
|
|
103
|
-
if (typeof a === "string") return a;
|
|
104
|
-
return a.id ?? a.label ?? "";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function actionShortcut(a: MenuAction) {
|
|
108
|
-
return typeof a === "string" ? "" : (a.shortcut ?? "");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isSeparator(
|
|
112
|
-
a: MenuAction
|
|
113
|
-
): a is Exclude<MenuAction, string> & { type: "separator" } {
|
|
114
|
-
return typeof a !== "string" && "type" in a && a.type === "separator";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function hasSubmenu(
|
|
118
|
-
a: MenuAction
|
|
119
|
-
): a is Exclude<MenuAction, string> & { submenu: MenuAction[] } {
|
|
120
|
-
return (
|
|
121
|
-
typeof a !== "string" && Array.isArray(a.submenu) && a.submenu.length > 0
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function actionKey(a: MenuAction, idx: number) {
|
|
126
|
-
const id = actionId(a);
|
|
127
|
-
return id || `__action-${idx}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function select(menu: string, action: MenuAction) {
|
|
131
|
-
closeMenus();
|
|
132
|
-
onSelect(menu, action);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function closeMenus() {
|
|
136
|
-
open = "";
|
|
137
|
-
openSub = "";
|
|
138
|
-
activeIndex = -1;
|
|
139
|
-
activeSubIndex = -1;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Positioning dropdown
|
|
143
|
-
function updateMenuPosition(
|
|
144
|
-
triggerEl: HTMLElement,
|
|
145
|
-
menuEl?: HTMLElement | null
|
|
146
|
-
) {
|
|
147
|
-
const rect = triggerEl.getBoundingClientRect();
|
|
148
|
-
const menuWidth = Math.min(
|
|
149
|
-
menuEl?.getBoundingClientRect().width ?? rect.width,
|
|
150
|
-
window.innerWidth - 16
|
|
151
|
-
);
|
|
152
|
-
const spaceRight = window.innerWidth - rect.left;
|
|
153
|
-
const spaceLeft = rect.right;
|
|
154
|
-
const alignRight = spaceRight < menuWidth && spaceLeft > spaceRight;
|
|
155
|
-
const viewportLeft = window.scrollX;
|
|
156
|
-
const viewportRight = window.scrollX + window.innerWidth;
|
|
157
|
-
|
|
158
|
-
menuTop = rect.bottom + window.scrollY;
|
|
159
|
-
const targetLeft = alignRight
|
|
160
|
-
? rect.right + window.scrollX - menuWidth
|
|
161
|
-
: rect.left + window.scrollX;
|
|
162
|
-
const maxLeft = viewportRight - menuWidth;
|
|
163
|
-
menuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function updateSubMenuPosition(
|
|
167
|
-
parentItemEl: HTMLElement,
|
|
168
|
-
subMenuEl?: HTMLElement | null
|
|
169
|
-
) {
|
|
170
|
-
const rect = parentItemEl.getBoundingClientRect();
|
|
171
|
-
const subRect = subMenuEl?.getBoundingClientRect();
|
|
172
|
-
const subWidth = Math.min(
|
|
173
|
-
subRect?.width ?? rect.width,
|
|
174
|
-
window.innerWidth - 16
|
|
175
|
-
);
|
|
176
|
-
const spaceRight = window.innerWidth - rect.right;
|
|
177
|
-
const spaceLeft = rect.left;
|
|
178
|
-
const shouldFlipLeft = spaceRight < subWidth && spaceLeft > spaceRight;
|
|
179
|
-
|
|
180
|
-
subMenuTop = rect.top + window.scrollY;
|
|
181
|
-
const viewportLeft = window.scrollX;
|
|
182
|
-
const viewportRight = window.scrollX + window.innerWidth;
|
|
183
|
-
const targetLeft = shouldFlipLeft
|
|
184
|
-
? rect.left + window.scrollX - subWidth - subMenuGutter
|
|
185
|
-
: rect.right + window.scrollX + subMenuGutter;
|
|
186
|
-
const maxLeft = viewportRight - subWidth - subMenuGutter;
|
|
187
|
-
subMenuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function firstActionIndex(actions: MenuAction[]) {
|
|
191
|
-
return actions.findIndex((a) => !isSeparator(a));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function nextActionIndex(actions: MenuAction[], current: number) {
|
|
195
|
-
if (!actions.length) return -1;
|
|
196
|
-
let idx = current;
|
|
197
|
-
for (let i = 0; i < actions.length; i++) {
|
|
198
|
-
idx = (idx + 1 + actions.length) % actions.length;
|
|
199
|
-
if (!isSeparator(actions[idx])) return idx;
|
|
200
|
-
}
|
|
201
|
-
return current;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function prevActionIndex(actions: MenuAction[], current: number) {
|
|
205
|
-
if (!actions.length) return -1;
|
|
206
|
-
let idx = current;
|
|
207
|
-
for (let i = 0; i < actions.length; i++) {
|
|
208
|
-
idx = (idx - 1 + actions.length) % actions.length;
|
|
209
|
-
if (!isSeparator(actions[idx])) return idx;
|
|
210
|
-
}
|
|
211
|
-
return current;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function focusMenuAction(menuItem: MenuItem, index: number) {
|
|
215
|
-
if (index < 0 || index >= menuItem.actions.length) return;
|
|
216
|
-
const action = menuItem.actions[index];
|
|
217
|
-
if (!action || isSeparator(action)) return;
|
|
218
|
-
if (!hasSubmenu(action) || openSub !== actionId(action)) {
|
|
219
|
-
openSub = "";
|
|
220
|
-
activeSubIndex = -1;
|
|
221
|
-
}
|
|
222
|
-
activeIndex = index;
|
|
223
|
-
queueMicrotask(() => {
|
|
224
|
-
if (open === menuItem.name) {
|
|
225
|
-
itemRefs[actionId(action)]?.focus();
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function focusSubAction(parentAction: MenuAction, index: number) {
|
|
231
|
-
if (!hasSubmenu(parentAction)) return;
|
|
232
|
-
if (index < 0 || index >= parentAction.submenu.length) return;
|
|
233
|
-
const subAction = parentAction.submenu[index];
|
|
234
|
-
if (!subAction || isSeparator(subAction)) return;
|
|
235
|
-
activeSubIndex = index;
|
|
236
|
-
queueMicrotask(() => {
|
|
237
|
-
if (openSub === actionId(parentAction)) {
|
|
238
|
-
subItemRefs[actionId(subAction)]?.focus();
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function openMenu(menuItem: MenuItem, focusFirst = false) {
|
|
244
|
-
open = menuItem.name;
|
|
245
|
-
openSub = "";
|
|
246
|
-
activeSubIndex = -1;
|
|
247
|
-
const firstIndex = focusFirst ? firstActionIndex(menuItem.actions) : -1;
|
|
248
|
-
activeIndex = firstIndex;
|
|
249
|
-
const triggerEl = triggerRefs[menuItem.name];
|
|
250
|
-
if (triggerEl) {
|
|
251
|
-
updateMenuPosition(triggerEl, menuRefs[menuItem.name]);
|
|
252
|
-
}
|
|
253
|
-
if (focusFirst && firstIndex !== -1) {
|
|
254
|
-
focusMenuAction(menuItem, firstIndex);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function openSubMenu(parentAction: MenuAction, focusFirst = false) {
|
|
259
|
-
if (!hasSubmenu(parentAction)) return;
|
|
260
|
-
openSub = actionId(parentAction);
|
|
261
|
-
const parentEl = itemRefs[actionId(parentAction)];
|
|
262
|
-
if (parentEl) {
|
|
263
|
-
updateSubMenuPosition(parentEl, subMenuRefs[actionId(parentAction)]);
|
|
264
|
-
}
|
|
265
|
-
const firstIndex = focusFirst ? firstActionIndex(parentAction.submenu) : -1;
|
|
266
|
-
activeSubIndex = firstIndex;
|
|
267
|
-
if (focusFirst && firstIndex !== -1) {
|
|
268
|
-
focusSubAction(parentAction, firstIndex);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Keyboard navigation
|
|
273
|
-
function handleTopLevelKeydown(
|
|
274
|
-
e: KeyboardEvent,
|
|
275
|
-
menuItem: MenuItem,
|
|
276
|
-
index: number
|
|
277
|
-
) {
|
|
278
|
-
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
|
|
279
|
-
e.preventDefault();
|
|
280
|
-
openMenu(menuItem, true);
|
|
281
|
-
} else if (e.key === "ArrowRight") {
|
|
282
|
-
e.preventDefault();
|
|
283
|
-
const nextIndex = (index + 1) % menus.length;
|
|
284
|
-
triggerRefs[menus[nextIndex].name]?.focus();
|
|
285
|
-
} else if (e.key === "ArrowLeft") {
|
|
286
|
-
e.preventDefault();
|
|
287
|
-
const prevIndex = (index - 1 + menus.length) % menus.length;
|
|
288
|
-
triggerRefs[menus[prevIndex].name]?.focus();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function handleMenuKeydown(e: KeyboardEvent, menuItem: MenuItem) {
|
|
293
|
-
e.stopPropagation();
|
|
294
|
-
if (!open) return;
|
|
295
|
-
|
|
296
|
-
const actions = menuItem.actions;
|
|
297
|
-
const firstIndex = firstActionIndex(actions);
|
|
298
|
-
if (firstIndex === -1) return;
|
|
299
|
-
const currentIndex = activeIndex === -1 ? firstIndex : activeIndex;
|
|
300
|
-
|
|
301
|
-
if (e.key === "Escape") {
|
|
302
|
-
e.preventDefault();
|
|
303
|
-
closeMenus();
|
|
304
|
-
triggerRefs[menuItem.name]?.focus();
|
|
305
|
-
} else if (e.key === "ArrowDown") {
|
|
306
|
-
e.preventDefault();
|
|
307
|
-
const next = nextActionIndex(actions, currentIndex);
|
|
308
|
-
focusMenuAction(menuItem, next);
|
|
309
|
-
} else if (e.key === "ArrowUp") {
|
|
310
|
-
e.preventDefault();
|
|
311
|
-
const prev = prevActionIndex(actions, currentIndex);
|
|
312
|
-
focusMenuAction(menuItem, prev);
|
|
313
|
-
} else if (e.key === "ArrowRight") {
|
|
314
|
-
e.preventDefault();
|
|
315
|
-
const action = actions[currentIndex];
|
|
316
|
-
if (action && hasSubmenu(action)) {
|
|
317
|
-
openSubMenu(action, true);
|
|
318
|
-
}
|
|
319
|
-
} else if (e.key === "ArrowLeft" && openSub) {
|
|
320
|
-
e.preventDefault();
|
|
321
|
-
openSub = "";
|
|
322
|
-
activeSubIndex = -1;
|
|
323
|
-
focusMenuAction(menuItem, currentIndex);
|
|
324
|
-
} else if (e.key === "Enter" || e.key === " ") {
|
|
325
|
-
e.preventDefault();
|
|
326
|
-
const action = actions[currentIndex];
|
|
327
|
-
if (action) {
|
|
328
|
-
if (hasSubmenu(action)) {
|
|
329
|
-
openSubMenu(action, true);
|
|
330
|
-
} else {
|
|
331
|
-
select(menuItem.name, action);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
} else if (e.key === "Tab") {
|
|
335
|
-
e.preventDefault();
|
|
336
|
-
const target = e.shiftKey
|
|
337
|
-
? prevActionIndex(actions, currentIndex)
|
|
338
|
-
: nextActionIndex(actions, currentIndex);
|
|
339
|
-
focusMenuAction(menuItem, target);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function handleSubMenuKeydown(
|
|
344
|
-
e: KeyboardEvent,
|
|
345
|
-
parentAction: MenuAction,
|
|
346
|
-
menuName: string
|
|
347
|
-
) {
|
|
348
|
-
e.stopPropagation();
|
|
349
|
-
if (!openSub || !hasSubmenu(parentAction)) return;
|
|
350
|
-
|
|
351
|
-
const subActions = parentAction.submenu;
|
|
352
|
-
const firstIndex = firstActionIndex(subActions);
|
|
353
|
-
if (firstIndex === -1) return;
|
|
354
|
-
const currentIndex = activeSubIndex === -1 ? firstIndex : activeSubIndex;
|
|
355
|
-
|
|
356
|
-
if (e.key === "Escape") {
|
|
357
|
-
e.preventDefault();
|
|
358
|
-
openSub = "";
|
|
359
|
-
activeSubIndex = -1;
|
|
360
|
-
itemRefs[actionId(parentAction)]?.focus();
|
|
361
|
-
} else if (e.key === "ArrowDown") {
|
|
362
|
-
e.preventDefault();
|
|
363
|
-
const next = nextActionIndex(subActions, currentIndex);
|
|
364
|
-
focusSubAction(parentAction, next);
|
|
365
|
-
} else if (e.key === "ArrowUp") {
|
|
366
|
-
e.preventDefault();
|
|
367
|
-
const prev = prevActionIndex(subActions, currentIndex);
|
|
368
|
-
focusSubAction(parentAction, prev);
|
|
369
|
-
} else if (e.key === "ArrowLeft") {
|
|
370
|
-
e.preventDefault();
|
|
371
|
-
openSub = "";
|
|
372
|
-
activeSubIndex = -1;
|
|
373
|
-
itemRefs[actionId(parentAction)]?.focus();
|
|
374
|
-
} else if (e.key === "Enter" || e.key === " ") {
|
|
375
|
-
e.preventDefault();
|
|
376
|
-
const action = subActions[currentIndex];
|
|
377
|
-
if (action) {
|
|
378
|
-
select(menuName, action);
|
|
379
|
-
}
|
|
380
|
-
} else if (e.key === "Tab") {
|
|
381
|
-
e.preventDefault();
|
|
382
|
-
const target = e.shiftKey
|
|
383
|
-
? prevActionIndex(subActions, currentIndex)
|
|
384
|
-
: nextActionIndex(subActions, currentIndex);
|
|
385
|
-
focusSubAction(parentAction, target);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Position update
|
|
390
|
-
$effect(() => {
|
|
391
|
-
if (open) {
|
|
392
|
-
const triggerEl = triggerRefs[open];
|
|
393
|
-
if (triggerEl) {
|
|
394
|
-
updateMenuPosition(triggerEl, menuRefs[open]);
|
|
395
|
-
|
|
396
|
-
const handleScrollResize = () => {
|
|
397
|
-
updateMenuPosition(triggerEl, menuRefs[open]);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
window.addEventListener("scroll", handleScrollResize, true);
|
|
401
|
-
window.addEventListener("resize", handleScrollResize);
|
|
402
|
-
|
|
403
|
-
return () => {
|
|
404
|
-
window.removeEventListener("scroll", handleScrollResize, true);
|
|
405
|
-
window.removeEventListener("resize", handleScrollResize);
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
$effect(() => {
|
|
412
|
-
if (openSub) {
|
|
413
|
-
const itemEl = itemRefs[openSub];
|
|
414
|
-
const subEl = subMenuRefs[openSub];
|
|
415
|
-
if (itemEl) {
|
|
416
|
-
updateSubMenuPosition(itemEl, subEl);
|
|
417
|
-
|
|
418
|
-
const handleScrollResize = () => {
|
|
419
|
-
updateSubMenuPosition(itemEl, subMenuRefs[openSub]);
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
window.addEventListener("scroll", handleScrollResize, true);
|
|
423
|
-
window.addEventListener("resize", handleScrollResize);
|
|
424
|
-
|
|
425
|
-
return () => {
|
|
426
|
-
window.removeEventListener("scroll", handleScrollResize, true);
|
|
427
|
-
window.removeEventListener("resize", handleScrollResize);
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
</script>
|
|
433
|
-
|
|
434
|
-
<nav class={navCls} aria-label="Menu bar">
|
|
435
|
-
{#each menus as menuItem, idx (menuItem.name)}
|
|
436
|
-
<div role="group" class="relative inline-block overflow-visible">
|
|
437
|
-
<button
|
|
438
|
-
bind:this={triggerRefs[menuItem.name]}
|
|
439
|
-
type="button"
|
|
440
|
-
class={cx(
|
|
441
|
-
topBtnBaseCls,
|
|
442
|
-
open === menuItem.name ? topButtonActive : topButtonIdle
|
|
443
|
-
)}
|
|
444
|
-
aria-haspopup="menu"
|
|
445
|
-
aria-expanded={open === menuItem.name}
|
|
446
|
-
onmousedown={(e) => e.preventDefault()}
|
|
447
|
-
onclick={() => {
|
|
448
|
-
if (open === menuItem.name) {
|
|
449
|
-
closeMenus();
|
|
450
|
-
} else {
|
|
451
|
-
openMenu(menuItem, true);
|
|
452
|
-
}
|
|
453
|
-
}}
|
|
454
|
-
onmouseenter={() => {
|
|
455
|
-
if (open && open !== menuItem.name) {
|
|
456
|
-
openMenu(menuItem, true);
|
|
457
|
-
}
|
|
458
|
-
}}
|
|
459
|
-
onkeydown={(e) => handleTopLevelKeydown(e, menuItem, idx)}
|
|
460
|
-
>
|
|
461
|
-
{menuItem.name}
|
|
462
|
-
</button>
|
|
463
|
-
</div>
|
|
464
|
-
{/each}
|
|
465
|
-
</nav>
|
|
466
|
-
|
|
467
|
-
<!-- Dropdown Menu -->
|
|
468
|
-
{#if open}
|
|
469
|
-
{#each menus as menuItem (menuItem.name)}
|
|
470
|
-
{#if open === menuItem.name}
|
|
471
|
-
<!-- Overlay to close -->
|
|
472
|
-
<div
|
|
473
|
-
role="presentation"
|
|
474
|
-
tabindex="-1"
|
|
475
|
-
class="fixed inset-0 z-40"
|
|
476
|
-
onmousedown={closeMenus}
|
|
477
|
-
></div>
|
|
478
|
-
|
|
479
|
-
<!-- Main Menu -->
|
|
480
|
-
<div
|
|
481
|
-
bind:this={menuRefs[menuItem.name]}
|
|
482
|
-
class={cx(
|
|
483
|
-
"fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)] ",
|
|
484
|
-
"border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
|
|
485
|
-
)}
|
|
486
|
-
style={menuStyle}
|
|
487
|
-
role="menu"
|
|
488
|
-
tabindex="-1"
|
|
489
|
-
onkeydown={(e) => handleMenuKeydown(e, menuItem)}
|
|
490
|
-
>
|
|
491
|
-
{#each menuItem.actions as action, i (actionKey(action, i))}
|
|
492
|
-
{#if isSeparator(action)}
|
|
493
|
-
<div
|
|
494
|
-
class="my-1 mx-1 border-t border-[var(--border-color-default)]"
|
|
495
|
-
role="separator"
|
|
496
|
-
></div>
|
|
497
|
-
{:else}
|
|
498
|
-
<div class="relative">
|
|
499
|
-
<button
|
|
500
|
-
bind:this={itemRefs[actionId(action)]}
|
|
501
|
-
type="button"
|
|
502
|
-
role="menuitem"
|
|
503
|
-
class={cx(
|
|
504
|
-
"relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5 my-1 mr-1 min-w-full flex items-center",
|
|
505
|
-
"gap-3 hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
|
|
506
|
-
"focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
|
|
507
|
-
textCls
|
|
508
|
-
)}
|
|
509
|
-
onmousedown={(e) => e.preventDefault()}
|
|
510
|
-
onclick={() => {
|
|
511
|
-
if (!hasSubmenu(action)) {
|
|
512
|
-
select(menuItem.name, action);
|
|
513
|
-
} else {
|
|
514
|
-
focusMenuAction(menuItem, i);
|
|
515
|
-
openSubMenu(action, true);
|
|
516
|
-
}
|
|
517
|
-
}}
|
|
518
|
-
onmouseenter={() => {
|
|
519
|
-
activeIndex = i;
|
|
520
|
-
if (hasSubmenu(action) && openSub !== actionId(action)) {
|
|
521
|
-
openSubMenu(action);
|
|
522
|
-
} else if (!hasSubmenu(action)) {
|
|
523
|
-
openSub = "";
|
|
524
|
-
activeSubIndex = -1;
|
|
525
|
-
}
|
|
526
|
-
}}
|
|
527
|
-
onfocus={() => {
|
|
528
|
-
focusMenuAction(menuItem, i);
|
|
529
|
-
}}
|
|
530
|
-
>
|
|
531
|
-
<span class="flex items-center gap-2 flex-1 min-w-0">
|
|
532
|
-
<span class="truncate">{actionText(action)}</span>
|
|
533
|
-
</span>
|
|
534
|
-
|
|
535
|
-
<span class="flex items-center shrink-0 ml-auto gap-1">
|
|
536
|
-
{#if actionShortcut(action)}
|
|
537
|
-
<span
|
|
538
|
-
class={cx(
|
|
539
|
-
"text-[var(--color-text-muted)] text-right",
|
|
540
|
-
textCls
|
|
541
|
-
)}
|
|
542
|
-
>
|
|
543
|
-
{actionShortcut(action)}
|
|
544
|
-
</span>
|
|
545
|
-
{/if}
|
|
546
|
-
|
|
547
|
-
{#if hasSubmenu(action)}
|
|
548
|
-
<span
|
|
549
|
-
class={cx(
|
|
550
|
-
"text-[var(--color-text-muted)] flex-shrink-0",
|
|
551
|
-
textCls
|
|
552
|
-
)}
|
|
553
|
-
>
|
|
554
|
-
>
|
|
555
|
-
</span>
|
|
556
|
-
{/if}
|
|
557
|
-
</span>
|
|
558
|
-
</button>
|
|
559
|
-
|
|
560
|
-
<!-- Sub Menu -->
|
|
561
|
-
{#if hasSubmenu(action) && openSub === actionId(action)}
|
|
562
|
-
<div
|
|
563
|
-
bind:this={subMenuRefs[actionId(action)]}
|
|
564
|
-
class={cx(
|
|
565
|
-
"fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)]",
|
|
566
|
-
"border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
|
|
567
|
-
)}
|
|
568
|
-
style={subMenuStyle}
|
|
569
|
-
role="menu"
|
|
570
|
-
tabindex="0"
|
|
571
|
-
onkeydown={(e) =>
|
|
572
|
-
handleSubMenuKeydown(e, action, menuItem.name)}
|
|
573
|
-
>
|
|
574
|
-
{#each action.submenu as sub, j (actionKey(sub, j))}
|
|
575
|
-
{#if isSeparator(sub)}
|
|
576
|
-
<div
|
|
577
|
-
class="my-1 mx-1 border-t border-[var(--border-color-default)]"
|
|
578
|
-
role="separator"
|
|
579
|
-
></div>
|
|
580
|
-
{:else}
|
|
581
|
-
<button
|
|
582
|
-
bind:this={subItemRefs[actionId(sub)]}
|
|
583
|
-
type="button"
|
|
584
|
-
role="menuitem"
|
|
585
|
-
class={cx(
|
|
586
|
-
"relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5",
|
|
587
|
-
"my-1 mr-1 w-full flex items-center justify-between gap-3",
|
|
588
|
-
"hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
|
|
589
|
-
"focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
|
|
590
|
-
"decoration-[var(--color-text-default)]",
|
|
591
|
-
textCls
|
|
592
|
-
)}
|
|
593
|
-
onmousedown={(e) => e.preventDefault()}
|
|
594
|
-
onclick={() => select(menuItem.name, sub)}
|
|
595
|
-
onmouseenter={() => (activeSubIndex = j)}
|
|
596
|
-
onfocus={() => (activeSubIndex = j)}
|
|
597
|
-
>
|
|
598
|
-
<span class="flex items-center gap-2 flex-1 min-w-0">
|
|
599
|
-
<span class="truncate">{actionText(sub)}</span>
|
|
600
|
-
</span>
|
|
601
|
-
|
|
602
|
-
<span class={hotkeyColCls}>
|
|
603
|
-
{#if actionShortcut(sub)}
|
|
604
|
-
<span
|
|
605
|
-
class={cx(
|
|
606
|
-
"text-[var(--color-text-muted)]",
|
|
607
|
-
textCls
|
|
608
|
-
)}>{actionShortcut(sub)}</span
|
|
609
|
-
>
|
|
610
|
-
{/if}
|
|
611
|
-
</span>
|
|
612
|
-
</button>
|
|
613
|
-
{/if}
|
|
614
|
-
{/each}
|
|
615
|
-
</div>
|
|
616
|
-
{/if}
|
|
617
|
-
</div>
|
|
618
|
-
{/if}
|
|
619
|
-
{/each}
|
|
620
|
-
</div>
|
|
621
|
-
{/if}
|
|
622
|
-
{/each}
|
|
623
|
-
{/if}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @component Menu
|
|
3
|
-
* @description A dropdown menu bar component with hover and click interactions.
|
|
4
|
-
*
|
|
5
|
-
* @prop menus {MenuItem[]} - Menu definitions with actions
|
|
6
|
-
* @default []
|
|
7
|
-
*
|
|
8
|
-
* @prop onSelect {(menu: string, action: MenuAction) => void} - Fired when an action is chosen
|
|
9
|
-
* @default () => {}
|
|
10
|
-
*
|
|
11
|
-
* @prop class {string} - Extra classes applied to the menu bar
|
|
12
|
-
* @default ""
|
|
13
|
-
*
|
|
14
|
-
* @prop sz {SizeKey} - Size preset for spacing and text
|
|
15
|
-
* @options xs|sm|md|lg|xl
|
|
16
|
-
* @default sm
|
|
17
|
-
*
|
|
18
|
-
* @note Fully keyboard-safe for focus and mouse interactions.
|
|
19
|
-
* @note Submenus open on hover when another menu is already open.
|
|
20
|
-
* @note Actions that match size keys (`xs`, `sm`, `md`, `lg`, `xl`) are automatically highlighted to reflect the current UI size.
|
|
21
|
-
* @note Uses the same CSS variable architecture as Tabs for consistent look across components.
|
|
22
|
-
* @note No slots; fully controlled via the `menus` structure and `onSelect`.
|
|
23
|
-
*/
|
|
24
|
-
import type { SizeKey, MenuItem, MenuAction } from "./types";
|
|
25
|
-
type Props = {
|
|
26
|
-
menus?: MenuItem[];
|
|
27
|
-
onSelect?: (menu: string, action: MenuAction) => void;
|
|
28
|
-
class?: string;
|
|
29
|
-
sz?: SizeKey;
|
|
30
|
-
};
|
|
31
|
-
declare const Menu: import("svelte").Component<Props, {}, "">;
|
|
32
|
-
type Menu = ReturnType<typeof Menu>;
|
|
33
|
-
export default Menu;
|