@marianmeres/stuic 2.18.0 → 2.20.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/dist/actions/popover/popover.svelte.js +2 -4
- package/dist/components/AnimatedElipsis/AnimatedEllipsis.svelte +73 -39
- package/dist/components/AnimatedElipsis/AnimatedEllipsis.svelte.d.ts +2 -0
- package/dist/components/AvatarInitials/README.md +169 -0
- package/dist/components/DropdownMenu/README.md +315 -0
- package/package.json +1 -1
|
@@ -324,10 +324,8 @@ export function popover(anchorEl, fn) {
|
|
|
324
324
|
popoverEl.addEventListener("mouseenter", cancelHide);
|
|
325
325
|
popoverEl.addEventListener("mouseleave", scheduleHide);
|
|
326
326
|
}
|
|
327
|
-
// For click mode with closeOnClickOutside
|
|
328
|
-
if (currentOptions.
|
|
329
|
-
currentOptions.closeOnClickOutside !== false &&
|
|
330
|
-
useAnchorPositioning) {
|
|
327
|
+
// For click or hover mode with closeOnClickOutside
|
|
328
|
+
if (currentOptions.closeOnClickOutside !== false && useAnchorPositioning) {
|
|
331
329
|
// Delay adding click listener to avoid immediate close
|
|
332
330
|
setTimeout(() => {
|
|
333
331
|
document.addEventListener("click", onClickOutside);
|
|
@@ -2,53 +2,87 @@
|
|
|
2
2
|
export interface Props {
|
|
3
3
|
class?: string;
|
|
4
4
|
enabled?: boolean;
|
|
5
|
+
/** Animation cycle duration in ms (default: 1000) */
|
|
6
|
+
speed?: number;
|
|
5
7
|
}
|
|
6
8
|
</script>
|
|
7
9
|
|
|
8
10
|
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
import { onMount } from "svelte";
|
|
11
|
-
|
|
12
|
-
let { class: _class, enabled = true }: Props = $props();
|
|
13
|
-
|
|
14
|
-
const speed = 250;
|
|
15
|
-
let visible = $state([false, false, false]);
|
|
16
|
-
let i = $state(0);
|
|
17
|
-
|
|
18
|
-
onMount(() => {
|
|
19
|
-
const ticker = createTickerRAF(speed, true);
|
|
20
|
-
const unsub = ticker.subscribe((t) => {
|
|
21
|
-
if (i > visible.length - 1) {
|
|
22
|
-
i = 0;
|
|
23
|
-
visible = visible.map((v) => false);
|
|
24
|
-
} else {
|
|
25
|
-
visible[i] = true;
|
|
26
|
-
i++;
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
return () => {
|
|
30
|
-
ticker.stop();
|
|
31
|
-
unsub();
|
|
32
|
-
};
|
|
33
|
-
});
|
|
11
|
+
let { class: _class, enabled = true, speed = 1000 }: Props = $props();
|
|
34
12
|
</script>
|
|
35
13
|
|
|
36
14
|
<!-- prettier-ignore -->
|
|
37
|
-
<span class={_class}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class={visible[1] || !enabled ? 'opacity-100' : 'opacity-0'}
|
|
43
|
-
style="transition-duration: {speed}ms;"
|
|
44
|
-
>.</span><span
|
|
45
|
-
class={visible[2] || !enabled ? 'opacity-100' : 'opacity-0'}
|
|
46
|
-
style="transition-duration: {speed}ms;"
|
|
47
|
-
>.</span>
|
|
48
|
-
</span>
|
|
15
|
+
<span class={_class} style:--duration="{speed}ms"
|
|
16
|
+
><span class="dot dot1" class:paused={!enabled}>.</span
|
|
17
|
+
><span class="dot dot2" class:paused={!enabled}>.</span
|
|
18
|
+
><span class="dot dot3" class:paused={!enabled}>.</span
|
|
19
|
+
></span>
|
|
49
20
|
|
|
50
21
|
<style>
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
.dot {
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dot1 {
|
|
27
|
+
animation: dot1 var(--duration, 1s) infinite linear;
|
|
28
|
+
}
|
|
29
|
+
.dot2 {
|
|
30
|
+
animation: dot2 var(--duration, 1s) infinite linear;
|
|
31
|
+
}
|
|
32
|
+
.dot3 {
|
|
33
|
+
animation: dot3 var(--duration, 1s) infinite linear;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.dot.paused {
|
|
37
|
+
animation: none;
|
|
38
|
+
opacity: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@keyframes dot1 {
|
|
42
|
+
0%,
|
|
43
|
+
20% {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
}
|
|
46
|
+
25% {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
80% {
|
|
50
|
+
opacity: 1;
|
|
51
|
+
}
|
|
52
|
+
100% {
|
|
53
|
+
opacity: 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes dot2 {
|
|
58
|
+
0%,
|
|
59
|
+
45% {
|
|
60
|
+
opacity: 0;
|
|
61
|
+
}
|
|
62
|
+
50% {
|
|
63
|
+
opacity: 1;
|
|
64
|
+
}
|
|
65
|
+
80% {
|
|
66
|
+
opacity: 1;
|
|
67
|
+
}
|
|
68
|
+
100% {
|
|
69
|
+
opacity: 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes dot3 {
|
|
74
|
+
0%,
|
|
75
|
+
70% {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
}
|
|
78
|
+
75% {
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
80% {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
}
|
|
84
|
+
100% {
|
|
85
|
+
opacity: 0;
|
|
86
|
+
}
|
|
53
87
|
}
|
|
54
88
|
</style>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface Props {
|
|
2
2
|
class?: string;
|
|
3
3
|
enabled?: boolean;
|
|
4
|
+
/** Animation cycle duration in ms (default: 1000) */
|
|
5
|
+
speed?: number;
|
|
4
6
|
}
|
|
5
7
|
declare const AnimatedEllipsis: import("svelte").Component<Props, {}, "">;
|
|
6
8
|
type AnimatedEllipsis = ReturnType<typeof AnimatedEllipsis>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# AvatarInitials
|
|
2
|
+
|
|
3
|
+
A circular avatar component displaying initials extracted from names or emails, with optional auto-generated colors and size presets.
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Default | Description |
|
|
8
|
+
|------|------|---------|-------------|
|
|
9
|
+
| `input` | `string` | - | String to extract initials from (name, initials, or email) |
|
|
10
|
+
| `hashSource` | `string` | - | Optional string for color hash calculation (falls back to `input`) |
|
|
11
|
+
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| string` | `"md"` | Size preset or custom Tailwind class |
|
|
12
|
+
| `onclick` | `(event: MouseEvent) => void` | - | Click handler (renders as button when provided) |
|
|
13
|
+
| `bg` | `string` | - | Background color Tailwind class (ignored if autoColor) |
|
|
14
|
+
| `textColor` | `string` | - | Text color Tailwind class (ignored if autoColor) |
|
|
15
|
+
| `autoColor` | `boolean` | `false` | Generate deterministic pastel colors from input |
|
|
16
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
17
|
+
| `el` | `HTMLDivElement \| HTMLButtonElement` | - | Element reference (bindable) |
|
|
18
|
+
|
|
19
|
+
## Size Presets
|
|
20
|
+
|
|
21
|
+
| Size | Dimensions | Font Size |
|
|
22
|
+
|------|------------|-----------|
|
|
23
|
+
| `sm` | 32px (size-8) | text-xs |
|
|
24
|
+
| `md` | 40px (size-10) | text-sm |
|
|
25
|
+
| `lg` | 56px (size-14) | text-base |
|
|
26
|
+
| `xl` | 64px (size-16) | text-lg |
|
|
27
|
+
|
|
28
|
+
Custom sizes can be passed as Tailwind classes: `size="size-20 text-2xl"`
|
|
29
|
+
|
|
30
|
+
## Initials Extraction Logic
|
|
31
|
+
|
|
32
|
+
The component intelligently extracts up to 2 characters from the input:
|
|
33
|
+
|
|
34
|
+
1. **Email addresses** (`john.doe@example.com`):
|
|
35
|
+
- Splits username by `.`, `_`, `+`, `-`
|
|
36
|
+
- Takes first letter of each part
|
|
37
|
+
- Result: `JD`
|
|
38
|
+
|
|
39
|
+
2. **Full names** (`John Doe`):
|
|
40
|
+
- Splits by whitespace
|
|
41
|
+
- Takes first letter of each word
|
|
42
|
+
- Result: `JD`
|
|
43
|
+
|
|
44
|
+
3. **Short strings** (`AB` or `Jo`):
|
|
45
|
+
- Takes first 2 characters
|
|
46
|
+
- Result: `AB` or `JO`
|
|
47
|
+
|
|
48
|
+
4. **Empty input**:
|
|
49
|
+
- Returns `?`
|
|
50
|
+
|
|
51
|
+
All initials are uppercase.
|
|
52
|
+
|
|
53
|
+
## Auto Color Generation
|
|
54
|
+
|
|
55
|
+
When `autoColor` is enabled, the component generates deterministic pastel colors:
|
|
56
|
+
|
|
57
|
+
- Colors are derived from a hash of `hashSource` or `input`
|
|
58
|
+
- Same input always produces the same color
|
|
59
|
+
- Colors are designed as accessible pastel tones
|
|
60
|
+
- Text color automatically contrasts with background
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Basic Display
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<script lang="ts">
|
|
68
|
+
import { AvatarInitials } from 'stuic';
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<AvatarInitials input="John Doe" />
|
|
72
|
+
<AvatarInitials input="jane.smith@example.com" />
|
|
73
|
+
<AvatarInitials input="AB" />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Size Variants
|
|
77
|
+
|
|
78
|
+
```svelte
|
|
79
|
+
<AvatarInitials input="JD" size="sm" />
|
|
80
|
+
<AvatarInitials input="JD" size="md" />
|
|
81
|
+
<AvatarInitials input="JD" size="lg" />
|
|
82
|
+
<AvatarInitials input="JD" size="xl" />
|
|
83
|
+
|
|
84
|
+
<!-- Custom size -->
|
|
85
|
+
<AvatarInitials input="JD" size="size-24 text-3xl" />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Auto Color (Deterministic)
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<!-- Same email always produces same color -->
|
|
92
|
+
<AvatarInitials input="john@example.com" autoColor />
|
|
93
|
+
<AvatarInitials input="jane@example.com" autoColor />
|
|
94
|
+
|
|
95
|
+
<!-- Use ID for consistent color regardless of display name -->
|
|
96
|
+
<AvatarInitials input="John Doe" hashSource="user-123" autoColor />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Custom Colors
|
|
100
|
+
|
|
101
|
+
```svelte
|
|
102
|
+
<AvatarInitials
|
|
103
|
+
input="JD"
|
|
104
|
+
bg="bg-blue-500"
|
|
105
|
+
textColor="text-white"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<AvatarInitials
|
|
109
|
+
input="AB"
|
|
110
|
+
bg="bg-gradient-to-br from-purple-500 to-pink-500"
|
|
111
|
+
textColor="text-white"
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Clickable Avatar
|
|
116
|
+
|
|
117
|
+
```svelte
|
|
118
|
+
<script lang="ts">
|
|
119
|
+
function handleClick() {
|
|
120
|
+
console.log('Avatar clicked');
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<AvatarInitials
|
|
125
|
+
input="john@example.com"
|
|
126
|
+
autoColor
|
|
127
|
+
onclick={handleClick}
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### In Header Dropdown
|
|
132
|
+
|
|
133
|
+
```svelte
|
|
134
|
+
<script lang="ts">
|
|
135
|
+
import { AvatarInitials, DropdownMenu } from 'stuic';
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<DropdownMenu
|
|
139
|
+
items={[
|
|
140
|
+
{ type: "action", id: "profile", label: "View Profile" },
|
|
141
|
+
{ type: "action", id: "logout", label: "Logout" },
|
|
142
|
+
]}
|
|
143
|
+
>
|
|
144
|
+
{#snippet trigger({ toggle })}
|
|
145
|
+
<AvatarInitials
|
|
146
|
+
input={userEmail}
|
|
147
|
+
onclick={toggle}
|
|
148
|
+
autoColor
|
|
149
|
+
class="cursor-pointer hover:ring-2 hover:ring-blue-500"
|
|
150
|
+
/>
|
|
151
|
+
{/snippet}
|
|
152
|
+
</DropdownMenu>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Avatar List
|
|
156
|
+
|
|
157
|
+
```svelte
|
|
158
|
+
<div class="flex -space-x-2">
|
|
159
|
+
{#each users as user}
|
|
160
|
+
<AvatarInitials
|
|
161
|
+
input={user.email}
|
|
162
|
+
hashSource={user.id}
|
|
163
|
+
autoColor
|
|
164
|
+
size="sm"
|
|
165
|
+
class="ring-2 ring-white"
|
|
166
|
+
/>
|
|
167
|
+
{/each}
|
|
168
|
+
</div>
|
|
169
|
+
```
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# DropdownMenu
|
|
2
|
+
|
|
3
|
+
A feature-rich dropdown menu component with CSS Anchor Positioning (with fallback), full keyboard navigation, and support for multiple item types including expandable sections.
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Default | Description |
|
|
8
|
+
|------|------|---------|-------------|
|
|
9
|
+
| `items` | `DropdownMenuItem[]` | - | Menu items to display |
|
|
10
|
+
| `isOpen` | `boolean` | `false` | Controlled open state (bindable) |
|
|
11
|
+
| `position` | `DropdownMenuPosition` | `"bottom-span-left"` | Popover position relative to trigger |
|
|
12
|
+
| `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
|
|
13
|
+
| `maxHeight` | `string` | `"300px"` | Max height of dropdown |
|
|
14
|
+
| `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
|
|
15
|
+
| `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
|
|
16
|
+
| `closeOnEscape` | `boolean` | `true` | Close on Escape key |
|
|
17
|
+
| `forceFallback` | `boolean` | `false` | Force fallback positioning (for testing) |
|
|
18
|
+
| `class` | `string` | - | Classes for wrapper element |
|
|
19
|
+
| `classTrigger` | `string` | - | Classes for trigger button |
|
|
20
|
+
| `classDropdown` | `string` | - | Classes for dropdown container |
|
|
21
|
+
| `classItem` | `string` | - | Classes for action items |
|
|
22
|
+
| `classItemActive` | `string` | - | Classes for active/focused item |
|
|
23
|
+
| `classItemDisabled` | `string` | - | Classes for disabled items |
|
|
24
|
+
| `classDivider` | `string` | - | Classes for dividers |
|
|
25
|
+
| `classHeader` | `string` | - | Classes for header items |
|
|
26
|
+
| `classExpandable` | `string` | - | Classes for expandable section header |
|
|
27
|
+
| `classExpandableContent` | `string` | - | Classes for expandable section content |
|
|
28
|
+
| `triggerEl` | `HTMLButtonElement` | - | Trigger element reference (bindable) |
|
|
29
|
+
| `dropdownEl` | `HTMLDivElement` | - | Dropdown element reference (bindable) |
|
|
30
|
+
|
|
31
|
+
## Snippets
|
|
32
|
+
|
|
33
|
+
| Snippet | Parameters | Description |
|
|
34
|
+
|---------|------------|-------------|
|
|
35
|
+
| `trigger` | `{ isOpen, toggle, triggerProps }` | Custom trigger with full ARIA control |
|
|
36
|
+
| `children` | - | Simple content for default trigger button |
|
|
37
|
+
|
|
38
|
+
## Item Types
|
|
39
|
+
|
|
40
|
+
### Action Item
|
|
41
|
+
|
|
42
|
+
Clickable menu item with optional icon and shortcut.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface DropdownMenuActionItem {
|
|
46
|
+
type: "action";
|
|
47
|
+
id: string | number;
|
|
48
|
+
label: THC; // Text, HTML, or component
|
|
49
|
+
icon?: THC; // Optional leading icon
|
|
50
|
+
shortcut?: string; // Keyboard shortcut hint
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
onSelect?: () => void | boolean;
|
|
53
|
+
class?: string;
|
|
54
|
+
data?: Record<string, any>;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Divider Item
|
|
59
|
+
|
|
60
|
+
Visual separator between items.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
interface DropdownMenuDividerItem {
|
|
64
|
+
type: "divider";
|
|
65
|
+
id?: string | number;
|
|
66
|
+
class?: string;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Header Item
|
|
71
|
+
|
|
72
|
+
Non-interactive section header.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
interface DropdownMenuHeaderItem {
|
|
76
|
+
type: "header";
|
|
77
|
+
id?: string | number;
|
|
78
|
+
label: THC;
|
|
79
|
+
class?: string;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Custom Item
|
|
84
|
+
|
|
85
|
+
Render arbitrary content (non-interactive).
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface DropdownMenuCustomItem {
|
|
89
|
+
type: "custom";
|
|
90
|
+
id?: string | number;
|
|
91
|
+
content: THC;
|
|
92
|
+
class?: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Expandable Item
|
|
97
|
+
|
|
98
|
+
Collapsible section containing nested items.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface DropdownMenuExpandableItem {
|
|
102
|
+
type: "expandable";
|
|
103
|
+
id: string | number;
|
|
104
|
+
label: THC;
|
|
105
|
+
icon?: THC;
|
|
106
|
+
items: DropdownMenuFlatItem[]; // Nested items (no nested expandables)
|
|
107
|
+
defaultExpanded?: boolean;
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
class?: string;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Position Options
|
|
114
|
+
|
|
115
|
+
| Position | Description |
|
|
116
|
+
|----------|-------------|
|
|
117
|
+
| `top`, `bottom` | Centered above/below trigger |
|
|
118
|
+
| `top-left`, `top-right` | Above, aligned to left/right edge |
|
|
119
|
+
| `bottom-left`, `bottom-right` | Below, aligned to left/right edge |
|
|
120
|
+
| `top-span-left`, `top-span-right` | Above, spanning from left/right |
|
|
121
|
+
| `bottom-span-left`, `bottom-span-right` | Below, spanning from left/right |
|
|
122
|
+
| `left`, `right` | Side-by-side with trigger |
|
|
123
|
+
|
|
124
|
+
## Callbacks
|
|
125
|
+
|
|
126
|
+
| Callback | Parameters | Description |
|
|
127
|
+
|----------|------------|-------------|
|
|
128
|
+
| `onOpen` | - | Called when menu opens |
|
|
129
|
+
| `onClose` | - | Called when menu closes |
|
|
130
|
+
| `onSelect` | `(item: DropdownMenuActionItem)` | Called when action item selected (fallback) |
|
|
131
|
+
|
|
132
|
+
## Keyboard Navigation
|
|
133
|
+
|
|
134
|
+
| Key | Action |
|
|
135
|
+
|-----|--------|
|
|
136
|
+
| `Arrow Down` | Move to next item |
|
|
137
|
+
| `Arrow Up` | Move to previous item |
|
|
138
|
+
| `Home` | Move to first item |
|
|
139
|
+
| `End` | Move to last item |
|
|
140
|
+
| `Cmd/Ctrl + Arrow` | Jump to first/last |
|
|
141
|
+
| `Enter` / `Space` | Select item or toggle expandable |
|
|
142
|
+
| `Arrow Right` | Expand section (on expandable) |
|
|
143
|
+
| `Arrow Left` | Collapse section (on expandable) |
|
|
144
|
+
| `Escape` | Close menu |
|
|
145
|
+
| `Tab` | Close menu |
|
|
146
|
+
|
|
147
|
+
## Usage
|
|
148
|
+
|
|
149
|
+
### Basic Menu
|
|
150
|
+
|
|
151
|
+
```svelte
|
|
152
|
+
<script lang="ts">
|
|
153
|
+
import { DropdownMenu } from 'stuic';
|
|
154
|
+
|
|
155
|
+
const items = [
|
|
156
|
+
{ type: "action", id: "edit", label: "Edit" },
|
|
157
|
+
{ type: "action", id: "duplicate", label: "Duplicate" },
|
|
158
|
+
{ type: "divider" },
|
|
159
|
+
{ type: "action", id: "delete", label: "Delete", class: "text-red-500" },
|
|
160
|
+
];
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<DropdownMenu
|
|
164
|
+
{items}
|
|
165
|
+
onSelect={(item) => console.log('Selected:', item.id)}
|
|
166
|
+
>
|
|
167
|
+
Actions
|
|
168
|
+
</DropdownMenu>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### With Icons and Shortcuts
|
|
172
|
+
|
|
173
|
+
```svelte
|
|
174
|
+
<script lang="ts">
|
|
175
|
+
import { DropdownMenu } from 'stuic';
|
|
176
|
+
import { iconLucideEdit } from '@marianmeres/icons-fns/lucide/iconLucideEdit.js';
|
|
177
|
+
import { iconLucideTrash } from '@marianmeres/icons-fns/lucide/iconLucideTrash.js';
|
|
178
|
+
|
|
179
|
+
const items = [
|
|
180
|
+
{
|
|
181
|
+
type: "action",
|
|
182
|
+
id: "edit",
|
|
183
|
+
label: "Edit",
|
|
184
|
+
icon: iconLucideEdit({ size: 16 }),
|
|
185
|
+
shortcut: "Cmd+E",
|
|
186
|
+
onSelect: () => handleEdit(),
|
|
187
|
+
},
|
|
188
|
+
{ type: "divider" },
|
|
189
|
+
{
|
|
190
|
+
type: "action",
|
|
191
|
+
id: "delete",
|
|
192
|
+
label: "Delete",
|
|
193
|
+
icon: iconLucideTrash({ size: 16 }),
|
|
194
|
+
shortcut: "Cmd+D",
|
|
195
|
+
onSelect: () => handleDelete(),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<DropdownMenu {items} position="bottom-right">
|
|
201
|
+
More Options
|
|
202
|
+
</DropdownMenu>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### With Section Headers
|
|
206
|
+
|
|
207
|
+
```svelte
|
|
208
|
+
<DropdownMenu items={[
|
|
209
|
+
{ type: "header", label: "Navigation" },
|
|
210
|
+
{ type: "action", id: "dashboard", label: "Dashboard" },
|
|
211
|
+
{ type: "action", id: "settings", label: "Settings" },
|
|
212
|
+
{ type: "divider" },
|
|
213
|
+
{ type: "header", label: "Account" },
|
|
214
|
+
{ type: "action", id: "profile", label: "Profile" },
|
|
215
|
+
{ type: "action", id: "logout", label: "Logout" },
|
|
216
|
+
]} />
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Expandable Sections
|
|
220
|
+
|
|
221
|
+
```svelte
|
|
222
|
+
<DropdownMenu items={[
|
|
223
|
+
{ type: "action", id: "new", label: "New File" },
|
|
224
|
+
{
|
|
225
|
+
type: "expandable",
|
|
226
|
+
id: "recent",
|
|
227
|
+
label: "Recent Files",
|
|
228
|
+
defaultExpanded: true,
|
|
229
|
+
items: [
|
|
230
|
+
{ type: "action", id: "file1", label: "document.pdf" },
|
|
231
|
+
{ type: "action", id: "file2", label: "report.xlsx" },
|
|
232
|
+
{ type: "action", id: "file3", label: "notes.txt" },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{ type: "divider" },
|
|
236
|
+
{ type: "action", id: "settings", label: "Settings" },
|
|
237
|
+
]} />
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Custom Trigger
|
|
241
|
+
|
|
242
|
+
```svelte
|
|
243
|
+
<script lang="ts">
|
|
244
|
+
import { AvatarInitials, DropdownMenu } from 'stuic';
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<DropdownMenu
|
|
248
|
+
items={[
|
|
249
|
+
{ type: "action", id: "profile", label: "View Profile" },
|
|
250
|
+
{ type: "action", id: "logout", label: "Logout" },
|
|
251
|
+
]}
|
|
252
|
+
>
|
|
253
|
+
{#snippet trigger({ isOpen, toggle, triggerProps })}
|
|
254
|
+
<button {...triggerProps} onclick={toggle}>
|
|
255
|
+
<AvatarInitials input="john.doe@example.com" autoColor />
|
|
256
|
+
</button>
|
|
257
|
+
{/snippet}
|
|
258
|
+
</DropdownMenu>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### With Custom Content
|
|
262
|
+
|
|
263
|
+
```svelte
|
|
264
|
+
<script lang="ts">
|
|
265
|
+
import { DropdownMenu } from 'stuic';
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<DropdownMenu items={[
|
|
269
|
+
{
|
|
270
|
+
type: "custom",
|
|
271
|
+
content: customHeader,
|
|
272
|
+
},
|
|
273
|
+
{ type: "divider" },
|
|
274
|
+
{ type: "action", id: "settings", label: "Settings" },
|
|
275
|
+
{ type: "action", id: "logout", label: "Logout" },
|
|
276
|
+
]} />
|
|
277
|
+
|
|
278
|
+
{#snippet customHeader()}
|
|
279
|
+
<div class="px-3 py-2 text-center">
|
|
280
|
+
<img src="/avatar.jpg" class="w-12 h-12 rounded-full mx-auto" alt="User" />
|
|
281
|
+
<div class="mt-2 font-semibold">John Doe</div>
|
|
282
|
+
<div class="text-sm text-gray-500">john@example.com</div>
|
|
283
|
+
</div>
|
|
284
|
+
{/snippet}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Controlled State
|
|
288
|
+
|
|
289
|
+
```svelte
|
|
290
|
+
<script lang="ts">
|
|
291
|
+
import { DropdownMenu } from 'stuic';
|
|
292
|
+
|
|
293
|
+
let isOpen = $state(false);
|
|
294
|
+
</script>
|
|
295
|
+
|
|
296
|
+
<button onclick={() => isOpen = true}>Open Menu</button>
|
|
297
|
+
|
|
298
|
+
<DropdownMenu
|
|
299
|
+
bind:isOpen
|
|
300
|
+
items={[
|
|
301
|
+
{ type: "action", id: "option1", label: "Option 1" },
|
|
302
|
+
{ type: "action", id: "option2", label: "Option 2" },
|
|
303
|
+
]}
|
|
304
|
+
/>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Features
|
|
308
|
+
|
|
309
|
+
- **CSS Anchor Positioning**: Uses modern CSS anchor positioning with automatic fallback for unsupported browsers
|
|
310
|
+
- **Full Keyboard Navigation**: Complete arrow key navigation with Home/End support
|
|
311
|
+
- **Expandable Sections**: Collapsible groups with independent toggle state
|
|
312
|
+
- **ARIA Compliant**: Proper menu roles and keyboard interaction
|
|
313
|
+
- **Reduced Motion**: Respects user's reduced motion preference
|
|
314
|
+
- **Click Outside**: Automatically closes when clicking outside
|
|
315
|
+
- **Focus Management**: Returns focus to trigger on close
|