@marianmeres/stuic 3.167.0 → 3.168.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.
- package/AGENTS.md +6 -6
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +111 -0
- package/package.json +11 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type {
|
|
6
|
+
DropdownMenuActionItem,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuPosition,
|
|
9
|
+
DropdownMenuSearchConfig,
|
|
10
|
+
} from "../DropdownMenu/DropdownMenu.svelte";
|
|
11
|
+
|
|
12
|
+
/** ContextMenu shares the DropdownMenu item model — aliases for consumer DX. */
|
|
13
|
+
export type ContextMenuItem = DropdownMenuItem;
|
|
14
|
+
export type ContextMenuActionItem = DropdownMenuActionItem;
|
|
15
|
+
export type ContextMenuPosition = DropdownMenuPosition;
|
|
16
|
+
export type ContextMenuSearchConfig = DropdownMenuSearchConfig;
|
|
17
|
+
|
|
18
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
19
|
+
/** Menu items to display (the DropdownMenu item model) */
|
|
20
|
+
items: ContextMenuItem[];
|
|
21
|
+
/** The right-clickable / long-pressable target area content */
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
/** Controlled open state (bindable). Setting `true` programmatically with no
|
|
24
|
+
* recorded coordinates positions the menu at the target area's top-left. */
|
|
25
|
+
isOpen?: boolean;
|
|
26
|
+
/** When true, triggers are inert and the browser's native menu works again */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Long-press duration in ms for touch/pen pointers (default 500);
|
|
29
|
+
* `false` disables the long-press trigger entirely. */
|
|
30
|
+
longPress?: number | false;
|
|
31
|
+
/** Menu placement relative to the pointer (default "bottom-span-right" —
|
|
32
|
+
* below-right of the cursor, the native context menu convention) */
|
|
33
|
+
position?: ContextMenuPosition;
|
|
34
|
+
/** Offset of the menu from the pointer (CSS value, default "0px") */
|
|
35
|
+
offset?: string;
|
|
36
|
+
/** Max height of the menu (CSS value) */
|
|
37
|
+
maxHeight?: string;
|
|
38
|
+
/** Whether to close the menu when an action item is selected */
|
|
39
|
+
closeOnSelect?: boolean;
|
|
40
|
+
/** Close on click outside */
|
|
41
|
+
closeOnClickOutside?: boolean;
|
|
42
|
+
/** Close on Escape key */
|
|
43
|
+
closeOnEscape?: boolean;
|
|
44
|
+
/** Force fallback positioning mode (for testing) */
|
|
45
|
+
forceFallback?: boolean;
|
|
46
|
+
/** Enable search/filter functionality (true for defaults, or config object) */
|
|
47
|
+
search?: boolean | ContextMenuSearchConfig;
|
|
48
|
+
/** Show backdrop in fallback mode (default: true) */
|
|
49
|
+
showBackdrop?: boolean;
|
|
50
|
+
/** Reserve scrollbar space to prevent layout shift on open (see DropdownMenu) */
|
|
51
|
+
scrollbarGutter?: boolean;
|
|
52
|
+
/** Skip the body scroll lock in fallback mode */
|
|
53
|
+
noScrollLock?: boolean;
|
|
54
|
+
/** Called when the menu opens */
|
|
55
|
+
onOpen?: () => void;
|
|
56
|
+
/** Called when the menu closes */
|
|
57
|
+
onClose?: () => void;
|
|
58
|
+
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
59
|
+
onSelect?: (item: ContextMenuActionItem) => void | boolean | Promise<void | boolean>;
|
|
60
|
+
/** i18n translate function (see `createContextMenuT`) — localizes the
|
|
61
|
+
* screen-reader-only menu label */
|
|
62
|
+
t?: TranslateFn;
|
|
63
|
+
/** Opt out of stuic base classes for full styling control */
|
|
64
|
+
unstyled?: boolean;
|
|
65
|
+
/** Classes for the target area wrapper */
|
|
66
|
+
class?: string;
|
|
67
|
+
/** Classes for the menu container */
|
|
68
|
+
classDropdown?: string;
|
|
69
|
+
/** Classes for action items */
|
|
70
|
+
classItem?: string;
|
|
71
|
+
/** Classes for active/focused action item */
|
|
72
|
+
classItemActive?: string;
|
|
73
|
+
/** Classes for disabled items */
|
|
74
|
+
classItemDisabled?: string;
|
|
75
|
+
/** Classes for content before label */
|
|
76
|
+
classItemBefore?: string;
|
|
77
|
+
/** Classes for content after label */
|
|
78
|
+
classItemAfter?: string;
|
|
79
|
+
/** Classes for dividers */
|
|
80
|
+
classDivider?: string;
|
|
81
|
+
/** Classes for header items */
|
|
82
|
+
classHeader?: string;
|
|
83
|
+
/** Classes for expandable section header */
|
|
84
|
+
classExpandable?: string;
|
|
85
|
+
/** Classes for expandable section content */
|
|
86
|
+
classExpandableContent?: string;
|
|
87
|
+
/** Classes for backdrop (fallback mode only) */
|
|
88
|
+
classBackdrop?: string;
|
|
89
|
+
/** Classes for search input container */
|
|
90
|
+
classSearchContainer?: string;
|
|
91
|
+
/** Classes for search input */
|
|
92
|
+
classSearchInput?: string;
|
|
93
|
+
/** Reference to the target area wrapper element */
|
|
94
|
+
el?: HTMLDivElement;
|
|
95
|
+
/** Reference to the open menu element */
|
|
96
|
+
dropdownEl?: HTMLDivElement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const CONTEXT_MENU_BASE_CLASSES = `stuic-context-menu`;
|
|
100
|
+
|
|
101
|
+
export const CONTEXT_MENU_ANCHOR_CLASSES = `stuic-context-menu-anchor`;
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<script lang="ts">
|
|
105
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
106
|
+
import { fixedContainingBlockRect } from "../../utils/containing-block.js";
|
|
107
|
+
import { longPress as longPressAttachment } from "../../attachments/long-press.js";
|
|
108
|
+
import DropdownMenu from "../DropdownMenu/DropdownMenu.svelte";
|
|
109
|
+
import { t_default } from "./i18n.js";
|
|
110
|
+
|
|
111
|
+
let {
|
|
112
|
+
items,
|
|
113
|
+
children,
|
|
114
|
+
isOpen = $bindable(false),
|
|
115
|
+
disabled = false,
|
|
116
|
+
longPress = 500,
|
|
117
|
+
position = "bottom-span-right",
|
|
118
|
+
offset = "0px",
|
|
119
|
+
maxHeight,
|
|
120
|
+
closeOnSelect,
|
|
121
|
+
closeOnClickOutside,
|
|
122
|
+
closeOnEscape,
|
|
123
|
+
forceFallback,
|
|
124
|
+
search,
|
|
125
|
+
showBackdrop,
|
|
126
|
+
scrollbarGutter,
|
|
127
|
+
noScrollLock,
|
|
128
|
+
onOpen,
|
|
129
|
+
onClose,
|
|
130
|
+
onSelect,
|
|
131
|
+
t = t_default,
|
|
132
|
+
unstyled = false,
|
|
133
|
+
class: classProp,
|
|
134
|
+
classDropdown,
|
|
135
|
+
classItem,
|
|
136
|
+
classItemActive,
|
|
137
|
+
classItemDisabled,
|
|
138
|
+
classItemBefore,
|
|
139
|
+
classItemAfter,
|
|
140
|
+
classDivider,
|
|
141
|
+
classHeader,
|
|
142
|
+
classExpandable,
|
|
143
|
+
classExpandableContent,
|
|
144
|
+
classBackdrop,
|
|
145
|
+
classSearchContainer,
|
|
146
|
+
classSearchInput,
|
|
147
|
+
el = $bindable(),
|
|
148
|
+
dropdownEl = $bindable(),
|
|
149
|
+
...rest
|
|
150
|
+
}: Props = $props();
|
|
151
|
+
|
|
152
|
+
// Anchor coordinates within the anchor's fixed containing block (viewport,
|
|
153
|
+
// unless a transformed/contained ancestor establishes one).
|
|
154
|
+
let x = $state(0);
|
|
155
|
+
let y = $state(0);
|
|
156
|
+
let hasPosition = $state(false);
|
|
157
|
+
|
|
158
|
+
// For keyboard users: what had focus when the menu opened, restored on close.
|
|
159
|
+
let prevFocused: HTMLElement | null = null;
|
|
160
|
+
|
|
161
|
+
/** Open the menu with its anchor at the given viewport coordinates. */
|
|
162
|
+
function openAt(clientX: number, clientY: number) {
|
|
163
|
+
if (disabled) return;
|
|
164
|
+
// clientX/Y are viewport coords, but the fixed anchor resolves against its
|
|
165
|
+
// containing block — translate when a transformed ancestor establishes one
|
|
166
|
+
const cb = el ? fixedContainingBlockRect(el) : undefined;
|
|
167
|
+
x = clientX - (cb?.left ?? 0);
|
|
168
|
+
y = clientY - (cb?.top ?? 0);
|
|
169
|
+
hasPosition = true;
|
|
170
|
+
if (!isOpen) {
|
|
171
|
+
const active = document.activeElement;
|
|
172
|
+
prevFocused =
|
|
173
|
+
active instanceof HTMLElement && active !== document.body ? active : null;
|
|
174
|
+
isOpen = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Open at an element's bottom-left corner (keyboard invocation convention). */
|
|
179
|
+
function openAtElement(target?: Element | null) {
|
|
180
|
+
const rect = (target ?? el)?.getBoundingClientRect();
|
|
181
|
+
if (!rect) return;
|
|
182
|
+
openAt(rect.left, rect.bottom);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function focusedElementWithin(): Element | null {
|
|
186
|
+
const active = document.activeElement;
|
|
187
|
+
return active && el?.contains(active) ? active : (el ?? null);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _onContextMenu(e: MouseEvent) {
|
|
191
|
+
if (disabled) return;
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
// nested context areas: the innermost one wins
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
if (e.clientX === 0 && e.clientY === 0) {
|
|
196
|
+
// keyboard-invoked (menu key synthesizes contextmenu with no coords)
|
|
197
|
+
openAtElement(focusedElementWithin());
|
|
198
|
+
} else {
|
|
199
|
+
openAt(e.clientX, e.clientY);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _onKeydown(e: KeyboardEvent) {
|
|
204
|
+
if (disabled) return;
|
|
205
|
+
if (e.key === "ContextMenu" || (e.shiftKey && e.key === "F10")) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
openAtElement(focusedElementWithin());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function _onClose() {
|
|
213
|
+
onClose?.();
|
|
214
|
+
if (prevFocused?.isConnected) prevFocused.focus();
|
|
215
|
+
prevFocused = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Programmatic open (isOpen set true from outside) with no coords yet —
|
|
219
|
+
// anchor to the target area instead of the (0,0) default.
|
|
220
|
+
$effect(() => {
|
|
221
|
+
if (isOpen && !hasPosition && el) {
|
|
222
|
+
openAtElement(el);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const _longPressAttach = $derived(
|
|
227
|
+
longPress !== false &&
|
|
228
|
+
!disabled &&
|
|
229
|
+
longPressAttachment({
|
|
230
|
+
duration: longPress,
|
|
231
|
+
onLongPress: (e) => openAt(e.clientX, e.clientY),
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
<div
|
|
237
|
+
bind:this={el}
|
|
238
|
+
class={unstyled ? classProp : twMerge(CONTEXT_MENU_BASE_CLASSES, classProp)}
|
|
239
|
+
data-longpress={!unstyled && longPress !== false && !disabled ? "" : undefined}
|
|
240
|
+
data-open={!unstyled && isOpen ? "" : undefined}
|
|
241
|
+
aria-haspopup="menu"
|
|
242
|
+
{...rest}
|
|
243
|
+
oncontextmenu={_onContextMenu}
|
|
244
|
+
onkeydown={_onKeydown}
|
|
245
|
+
{@attach _longPressAttach}
|
|
246
|
+
>
|
|
247
|
+
{@render children?.()}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<!-- Invisible 0x0 anchor moved to the pointer — the DropdownMenu positions against it -->
|
|
251
|
+
<div
|
|
252
|
+
role="presentation"
|
|
253
|
+
class={unstyled ? undefined : CONTEXT_MENU_ANCHOR_CLASSES}
|
|
254
|
+
style:position="fixed"
|
|
255
|
+
style:left="{x}px"
|
|
256
|
+
style:top="{y}px"
|
|
257
|
+
style:width="0"
|
|
258
|
+
style:height="0"
|
|
259
|
+
oncontextmenu={(e) => e.preventDefault()}
|
|
260
|
+
>
|
|
261
|
+
<DropdownMenu
|
|
262
|
+
bind:isOpen
|
|
263
|
+
bind:dropdownEl
|
|
264
|
+
{items}
|
|
265
|
+
{position}
|
|
266
|
+
{offset}
|
|
267
|
+
{maxHeight}
|
|
268
|
+
{closeOnSelect}
|
|
269
|
+
{closeOnClickOutside}
|
|
270
|
+
{closeOnEscape}
|
|
271
|
+
{forceFallback}
|
|
272
|
+
{search}
|
|
273
|
+
{showBackdrop}
|
|
274
|
+
{scrollbarGutter}
|
|
275
|
+
{noScrollLock}
|
|
276
|
+
{onOpen}
|
|
277
|
+
onClose={_onClose}
|
|
278
|
+
{onSelect}
|
|
279
|
+
{unstyled}
|
|
280
|
+
{classDropdown}
|
|
281
|
+
{classItem}
|
|
282
|
+
{classItemActive}
|
|
283
|
+
{classItemDisabled}
|
|
284
|
+
{classItemBefore}
|
|
285
|
+
{classItemAfter}
|
|
286
|
+
{classDivider}
|
|
287
|
+
{classHeader}
|
|
288
|
+
{classExpandable}
|
|
289
|
+
{classExpandableContent}
|
|
290
|
+
{classBackdrop}
|
|
291
|
+
{classSearchContainer}
|
|
292
|
+
{classSearchInput}
|
|
293
|
+
>
|
|
294
|
+
{#snippet trigger({ triggerProps })}
|
|
295
|
+
<!-- No visible trigger — an sr-only label keeps the menu's
|
|
296
|
+
aria-labelledby reference valid and names it for AT -->
|
|
297
|
+
<span class="sr-only" id={triggerProps.id}>
|
|
298
|
+
{t("context_menu", null, "Context menu")}
|
|
299
|
+
</span>
|
|
300
|
+
{/snippet}
|
|
301
|
+
</DropdownMenu>
|
|
302
|
+
</div>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { DropdownMenuActionItem, DropdownMenuItem, DropdownMenuPosition, DropdownMenuSearchConfig } from "../DropdownMenu/DropdownMenu.svelte";
|
|
5
|
+
/** ContextMenu shares the DropdownMenu item model — aliases for consumer DX. */
|
|
6
|
+
export type ContextMenuItem = DropdownMenuItem;
|
|
7
|
+
export type ContextMenuActionItem = DropdownMenuActionItem;
|
|
8
|
+
export type ContextMenuPosition = DropdownMenuPosition;
|
|
9
|
+
export type ContextMenuSearchConfig = DropdownMenuSearchConfig;
|
|
10
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
11
|
+
/** Menu items to display (the DropdownMenu item model) */
|
|
12
|
+
items: ContextMenuItem[];
|
|
13
|
+
/** The right-clickable / long-pressable target area content */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
/** Controlled open state (bindable). Setting `true` programmatically with no
|
|
16
|
+
* recorded coordinates positions the menu at the target area's top-left. */
|
|
17
|
+
isOpen?: boolean;
|
|
18
|
+
/** When true, triggers are inert and the browser's native menu works again */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Long-press duration in ms for touch/pen pointers (default 500);
|
|
21
|
+
* `false` disables the long-press trigger entirely. */
|
|
22
|
+
longPress?: number | false;
|
|
23
|
+
/** Menu placement relative to the pointer (default "bottom-span-right" —
|
|
24
|
+
* below-right of the cursor, the native context menu convention) */
|
|
25
|
+
position?: ContextMenuPosition;
|
|
26
|
+
/** Offset of the menu from the pointer (CSS value, default "0px") */
|
|
27
|
+
offset?: string;
|
|
28
|
+
/** Max height of the menu (CSS value) */
|
|
29
|
+
maxHeight?: string;
|
|
30
|
+
/** Whether to close the menu when an action item is selected */
|
|
31
|
+
closeOnSelect?: boolean;
|
|
32
|
+
/** Close on click outside */
|
|
33
|
+
closeOnClickOutside?: boolean;
|
|
34
|
+
/** Close on Escape key */
|
|
35
|
+
closeOnEscape?: boolean;
|
|
36
|
+
/** Force fallback positioning mode (for testing) */
|
|
37
|
+
forceFallback?: boolean;
|
|
38
|
+
/** Enable search/filter functionality (true for defaults, or config object) */
|
|
39
|
+
search?: boolean | ContextMenuSearchConfig;
|
|
40
|
+
/** Show backdrop in fallback mode (default: true) */
|
|
41
|
+
showBackdrop?: boolean;
|
|
42
|
+
/** Reserve scrollbar space to prevent layout shift on open (see DropdownMenu) */
|
|
43
|
+
scrollbarGutter?: boolean;
|
|
44
|
+
/** Skip the body scroll lock in fallback mode */
|
|
45
|
+
noScrollLock?: boolean;
|
|
46
|
+
/** Called when the menu opens */
|
|
47
|
+
onOpen?: () => void;
|
|
48
|
+
/** Called when the menu closes */
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
51
|
+
onSelect?: (item: ContextMenuActionItem) => void | boolean | Promise<void | boolean>;
|
|
52
|
+
/** i18n translate function (see `createContextMenuT`) — localizes the
|
|
53
|
+
* screen-reader-only menu label */
|
|
54
|
+
t?: TranslateFn;
|
|
55
|
+
/** Opt out of stuic base classes for full styling control */
|
|
56
|
+
unstyled?: boolean;
|
|
57
|
+
/** Classes for the target area wrapper */
|
|
58
|
+
class?: string;
|
|
59
|
+
/** Classes for the menu container */
|
|
60
|
+
classDropdown?: string;
|
|
61
|
+
/** Classes for action items */
|
|
62
|
+
classItem?: string;
|
|
63
|
+
/** Classes for active/focused action item */
|
|
64
|
+
classItemActive?: string;
|
|
65
|
+
/** Classes for disabled items */
|
|
66
|
+
classItemDisabled?: string;
|
|
67
|
+
/** Classes for content before label */
|
|
68
|
+
classItemBefore?: string;
|
|
69
|
+
/** Classes for content after label */
|
|
70
|
+
classItemAfter?: string;
|
|
71
|
+
/** Classes for dividers */
|
|
72
|
+
classDivider?: string;
|
|
73
|
+
/** Classes for header items */
|
|
74
|
+
classHeader?: string;
|
|
75
|
+
/** Classes for expandable section header */
|
|
76
|
+
classExpandable?: string;
|
|
77
|
+
/** Classes for expandable section content */
|
|
78
|
+
classExpandableContent?: string;
|
|
79
|
+
/** Classes for backdrop (fallback mode only) */
|
|
80
|
+
classBackdrop?: string;
|
|
81
|
+
/** Classes for search input container */
|
|
82
|
+
classSearchContainer?: string;
|
|
83
|
+
/** Classes for search input */
|
|
84
|
+
classSearchInput?: string;
|
|
85
|
+
/** Reference to the target area wrapper element */
|
|
86
|
+
el?: HTMLDivElement;
|
|
87
|
+
/** Reference to the open menu element */
|
|
88
|
+
dropdownEl?: HTMLDivElement;
|
|
89
|
+
}
|
|
90
|
+
export declare const CONTEXT_MENU_BASE_CLASSES = "stuic-context-menu";
|
|
91
|
+
export declare const CONTEXT_MENU_ANCHOR_CLASSES = "stuic-context-menu-anchor";
|
|
92
|
+
declare const ContextMenu: import("svelte").Component<Props, {}, "el" | "isOpen" | "dropdownEl">;
|
|
93
|
+
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
94
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ContextMenu
|
|
2
|
+
|
|
3
|
+
Right-click / long-press triggered menu, positioned at the pointer. Wrap any content
|
|
4
|
+
in `<ContextMenu items={...}>` and the wrapped area becomes a context target:
|
|
5
|
+
a `contextmenu` event (right-click), a long-press (touch/pen pointers), or
|
|
6
|
+
Shift+F10 / the dedicated menu key (keyboard) opens the menu anchored at the
|
|
7
|
+
interaction point.
|
|
8
|
+
|
|
9
|
+
The menu panel itself **is** a [`DropdownMenu`](../DropdownMenu/README.md) — the full
|
|
10
|
+
item model (actions, dividers, headers, custom content, expandable sections), keyboard
|
|
11
|
+
navigation, optional search, viewport-overflow handling (CSS anchor positioning with a
|
|
12
|
+
centered-modal fallback), and theming all come from there. What ContextMenu adds is the
|
|
13
|
+
trigger semantics and the cursor anchoring.
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
| Prop | Type | Default | Description |
|
|
18
|
+
| -------------------------------------------------- | -------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `items` | `ContextMenuItem[]` | required | Menu items (the DropdownMenu item model) |
|
|
20
|
+
| `children` | `Snippet` | - | The right-clickable / long-pressable target area content |
|
|
21
|
+
| `isOpen` | `boolean` | `false` | Controlled open state (bindable); setting `true` programmatically anchors at the target area |
|
|
22
|
+
| `disabled` | `boolean` | `false` | Triggers are inert — the browser's native context menu works again |
|
|
23
|
+
| `longPress` | `number \| false` | `500` | Long-press duration in ms (touch/pen); `false` disables the long-press trigger |
|
|
24
|
+
| `position` | `ContextMenuPosition` | `"bottom-span-right"` | Menu placement relative to the pointer (below-right = the native convention); flips near the edges |
|
|
25
|
+
| `offset` | `string` | `"0px"` | Offset of the menu from the pointer (CSS value) |
|
|
26
|
+
| `maxHeight` | `string` | `"300px"` | Max menu height (CSS value) |
|
|
27
|
+
| `closeOnSelect` | `boolean` | `true` | Close when an action item is selected |
|
|
28
|
+
| `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
|
|
29
|
+
| `closeOnEscape` | `boolean` | `true` | Close on Escape |
|
|
30
|
+
| `search` | `boolean \| ContextMenuSearchConfig` | - | Search/filter input inside the menu (see DropdownMenu) |
|
|
31
|
+
| `showBackdrop` | `boolean` | `true` | Backdrop in fallback (centered modal) mode |
|
|
32
|
+
| `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto-enables at ≥ 7 items) |
|
|
33
|
+
| `noScrollLock` | `boolean` | - | Skip the body scroll lock in fallback mode |
|
|
34
|
+
| `forceFallback` | `boolean` | `false` | Force the centered-modal fallback (testing) |
|
|
35
|
+
| `onOpen` / `onClose` | `() => void` | - | Open/close callbacks |
|
|
36
|
+
| `onSelect` | `(item) => void \| boolean \| Promise` | - | Fallback select handler when an item has no own `onSelect` |
|
|
37
|
+
| `t` | `TranslateFn` | English | i18n — localizes the screen-reader-only menu label (see below) |
|
|
38
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
39
|
+
| `class` | `string` | - | Classes for the target area wrapper |
|
|
40
|
+
| `classDropdown`, `classItem`, `classItemActive`, … | `string` | - | Menu part classes, passed to the underlying DropdownMenu |
|
|
41
|
+
| `el` | `HTMLDivElement` | - | Target area wrapper element (bindable) |
|
|
42
|
+
| `dropdownEl` | `HTMLDivElement` | - | Open menu element (bindable) |
|
|
43
|
+
|
|
44
|
+
`ContextMenuItem` / `ContextMenuActionItem` / `ContextMenuPosition` /
|
|
45
|
+
`ContextMenuSearchConfig` are aliases of the corresponding `DropdownMenu*` types —
|
|
46
|
+
see the [DropdownMenu README](../DropdownMenu/README.md) for the full item model
|
|
47
|
+
(action, divider, header, custom, expandable).
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Basic
|
|
52
|
+
|
|
53
|
+
```svelte
|
|
54
|
+
<script lang="ts">
|
|
55
|
+
import { ContextMenu, type ContextMenuItem } from "@marianmeres/stuic";
|
|
56
|
+
|
|
57
|
+
const items: ContextMenuItem[] = [
|
|
58
|
+
{ type: "action", id: "copy", label: "Copy", onSelect: () => copy() },
|
|
59
|
+
{ type: "action", id: "rename", label: "Rename", onSelect: () => rename() },
|
|
60
|
+
{ type: "divider", id: "d" },
|
|
61
|
+
{ type: "action", id: "delete", label: "Delete", onSelect: () => remove() },
|
|
62
|
+
];
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<ContextMenu {items}>
|
|
66
|
+
<div class="p-8">Right-click (or long-press) anywhere in here…</div>
|
|
67
|
+
</ContextMenu>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Per-row menus (list/table)
|
|
71
|
+
|
|
72
|
+
Each `<ContextMenu>` instance is one target area — wrap each row and build the items
|
|
73
|
+
from the row's data:
|
|
74
|
+
|
|
75
|
+
```svelte
|
|
76
|
+
{#each files as file (file.id)}
|
|
77
|
+
<ContextMenu items={itemsFor(file)}>
|
|
78
|
+
<div class="row">{file.name}</div>
|
|
79
|
+
</ContextMenu>
|
|
80
|
+
{/each}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(Nested target areas are fine — the innermost one wins.)
|
|
84
|
+
|
|
85
|
+
### Controlled / programmatic
|
|
86
|
+
|
|
87
|
+
```svelte
|
|
88
|
+
<ContextMenu {items} bind:isOpen>...</ContextMenu>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Setting `isOpen = true` programmatically (with no pointer interaction recorded yet)
|
|
92
|
+
anchors the menu at the target area's bottom-left corner.
|
|
93
|
+
|
|
94
|
+
### i18n
|
|
95
|
+
|
|
96
|
+
Only the screen-reader-only menu label ("Context menu") is localized — the item
|
|
97
|
+
labels are yours. Built-in English; bundled Slovak (`CONTEXT_MENU_MESSAGES_SK`) is
|
|
98
|
+
opt-in:
|
|
99
|
+
|
|
100
|
+
```svelte
|
|
101
|
+
<script lang="ts">
|
|
102
|
+
import {
|
|
103
|
+
ContextMenu,
|
|
104
|
+
createContextMenuT,
|
|
105
|
+
CONTEXT_MENU_MESSAGES_SK,
|
|
106
|
+
} from "@marianmeres/stuic";
|
|
107
|
+
const t = createContextMenuT(CONTEXT_MENU_MESSAGES_SK);
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<ContextMenu {items} {t}>...</ContextMenu>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Trigger semantics
|
|
114
|
+
|
|
115
|
+
- **Right-click** (`contextmenu` event): opens at the cursor, `preventDefault`s the
|
|
116
|
+
native menu, `stopPropagation`s so the innermost of nested context areas wins.
|
|
117
|
+
Right-clicking again while open just moves the menu.
|
|
118
|
+
- **Long-press** (touch/pen, via the exported [`longPress`](../../attachments/long-press.ts)
|
|
119
|
+
attachment): fires after `longPress` ms without moving beyond ~10px; the platform's
|
|
120
|
+
own long-press behaviors are suppressed (Android's native `contextmenu` synthesis is
|
|
121
|
+
deduplicated; iOS's callout/text-selection via CSS on the target area — coarse
|
|
122
|
+
pointers only, so desktop text selection inside the area stays intact). The click
|
|
123
|
+
that some platforms fire when the finger lifts is swallowed so it can't activate
|
|
124
|
+
whatever sits under it.
|
|
125
|
+
- **Keyboard**: Shift+F10 or the dedicated menu key while focus is inside the target
|
|
126
|
+
area opens the menu at the focused element's bottom-left corner. Note the wrapper
|
|
127
|
+
itself is not focusable — put focusable content inside (or add `tabindex="0"`
|
|
128
|
+
yourself) for keyboard invocation to have somewhere to happen. Focus returns to the
|
|
129
|
+
previously focused element on close.
|
|
130
|
+
- **`disabled`**: all triggers inert, the browser's native context menu works again.
|
|
131
|
+
|
|
132
|
+
## Accessibility
|
|
133
|
+
|
|
134
|
+
- The target area wrapper carries `aria-haspopup="menu"`; the menu is a
|
|
135
|
+
`role="menu"` with `role="menuitem"` items (DropdownMenu's keyboard navigation:
|
|
136
|
+
arrows, Home/End, Enter/Space, Escape).
|
|
137
|
+
- The menu's `aria-labelledby` points at a visually hidden label ("Context menu",
|
|
138
|
+
localizable via `t`).
|
|
139
|
+
- Keyboard invocation (Shift+F10 / menu key) is supported, and focus is restored on
|
|
140
|
+
close.
|
|
141
|
+
|
|
142
|
+
## Styling
|
|
143
|
+
|
|
144
|
+
The menu panel is a DropdownMenu — **theme it via the `--stuic-dropdown-menu-*`
|
|
145
|
+
tokens** (see the [DropdownMenu README](../DropdownMenu/README.md#css-variables)).
|
|
146
|
+
ContextMenu itself only styles the target area:
|
|
147
|
+
|
|
148
|
+
- `.stuic-context-menu` — the target area wrapper (`class` prop merges here)
|
|
149
|
+
- `.stuic-context-menu-anchor` — the invisible 0×0 fixed anchor the menu positions
|
|
150
|
+
against (inline-positioned; not a styling hook)
|
|
151
|
+
|
|
152
|
+
## Data Attributes
|
|
153
|
+
|
|
154
|
+
On the target area wrapper:
|
|
155
|
+
|
|
156
|
+
- `data-open` — empty attribute while the menu is open (e.g. highlight the target:
|
|
157
|
+
`.stuic-context-menu[data-open] { background: ... }`)
|
|
158
|
+
- `data-longpress` — empty attribute when the long-press trigger is enabled; drives
|
|
159
|
+
the CSS that suppresses the platform's own long-press behaviors
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ContextMenuMessages } from "./i18n.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slovak message catalog for `ContextMenu`. Opt-in — English stays the built-in
|
|
4
|
+
* default, and this module is only pulled into a bundle when it is actually imported
|
|
5
|
+
* (the component itself never references it).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { ContextMenu, createContextMenuT, CONTEXT_MENU_MESSAGES_SK } from "@marianmeres/stuic";
|
|
11
|
+
* const t = createContextMenuT(CONTEXT_MENU_MESSAGES_SK);
|
|
12
|
+
* </script>
|
|
13
|
+
*
|
|
14
|
+
* <ContextMenu {items} {t}>...</ContextMenu>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const CONTEXT_MENU_MESSAGES_SK: ContextMenuMessages;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slovak message catalog for `ContextMenu`. Opt-in — English stays the built-in
|
|
3
|
+
* default, and this module is only pulled into a bundle when it is actually imported
|
|
4
|
+
* (the component itself never references it).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script>
|
|
9
|
+
* import { ContextMenu, createContextMenuT, CONTEXT_MENU_MESSAGES_SK } from "@marianmeres/stuic";
|
|
10
|
+
* const t = createContextMenuT(CONTEXT_MENU_MESSAGES_SK);
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* <ContextMenu {items} {t}>...</ContextMenu>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const CONTEXT_MENU_MESSAGES_SK = {
|
|
17
|
+
context_menu: "Kontextové menu",
|
|
18
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TranslateFn } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The built-in (English) message catalog of `ContextMenu`. Also the fallback of every
|
|
4
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTEXT_MENU_MESSAGES_EN: {
|
|
7
|
+
context_menu: string;
|
|
8
|
+
};
|
|
9
|
+
/** Every message key `ContextMenu` may look up. */
|
|
10
|
+
export type ContextMenuMessageKey = keyof typeof CONTEXT_MENU_MESSAGES_EN;
|
|
11
|
+
/** A (possibly partial) catalog for one locale. */
|
|
12
|
+
export type ContextMenuMessages = Record<ContextMenuMessageKey, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Builds the `t` prop of `ContextMenu` from a message catalog. Unknown or untranslated
|
|
15
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
16
|
+
* partial and never renders a raw key.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <script>
|
|
21
|
+
* import { ContextMenu, createContextMenuT, CONTEXT_MENU_MESSAGES_SK } from "@marianmeres/stuic";
|
|
22
|
+
* const t = createContextMenuT(CONTEXT_MENU_MESSAGES_SK);
|
|
23
|
+
* </script>
|
|
24
|
+
*
|
|
25
|
+
* <ContextMenu {items} {t}>...</ContextMenu>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function createContextMenuT(messages: Partial<ContextMenuMessages> | Record<string, string>, fallbackMessages?: Partial<ContextMenuMessages> | Record<string, string>): TranslateFn;
|
|
29
|
+
/** The component's built-in English `t`. */
|
|
30
|
+
export declare const t_default: TranslateFn;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
2
|
+
import { replaceMap } from "../../utils/replace-map.js";
|
|
3
|
+
/**
|
|
4
|
+
* The built-in (English) message catalog of `ContextMenu`. Also the fallback of every
|
|
5
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
6
|
+
*/
|
|
7
|
+
export const CONTEXT_MENU_MESSAGES_EN = {
|
|
8
|
+
context_menu: "Context menu",
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds the `t` prop of `ContextMenu` from a message catalog. Unknown or untranslated
|
|
12
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
13
|
+
* partial and never renders a raw key.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { ContextMenu, createContextMenuT, CONTEXT_MENU_MESSAGES_SK } from "@marianmeres/stuic";
|
|
19
|
+
* const t = createContextMenuT(CONTEXT_MENU_MESSAGES_SK);
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <ContextMenu {items} {t}>...</ContextMenu>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createContextMenuT(messages, fallbackMessages = CONTEXT_MENU_MESSAGES_EN) {
|
|
26
|
+
return (k, values = null, fallback = "") => {
|
|
27
|
+
const out = messages[k] ??
|
|
28
|
+
fallbackMessages[k] ??
|
|
29
|
+
(typeof fallback === "string" ? fallback : k);
|
|
30
|
+
return isPlainObject(values)
|
|
31
|
+
? replaceMap(out, values, {
|
|
32
|
+
preSearchKeyTransform: (k) => `{${k}}`,
|
|
33
|
+
})
|
|
34
|
+
: out;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The component's built-in English `t`. */
|
|
38
|
+
export const t_default = createContextMenuT(CONTEXT_MENU_MESSAGES_EN);
|