@marianmeres/stuic 2.17.0 → 2.19.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/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/Backdrop/Backdrop.svelte +26 -6
- package/dist/components/DropdownMenu/README.md +315 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/input-history.svelte.d.ts +102 -0
- package/dist/utils/input-history.svelte.js +197 -0
- package/package.json +1 -1
|
@@ -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
|
+
```
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
visible?: boolean;
|
|
15
15
|
noScrollLock?: boolean;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
// Stack to track visible Backdrops - only topmost handles Escape
|
|
19
|
+
const escapeStack: Set<symbol> = new Set();
|
|
17
20
|
</script>
|
|
18
21
|
|
|
19
22
|
<script lang="ts">
|
|
@@ -104,17 +107,34 @@
|
|
|
104
107
|
// Note, that this will also reset if nested... (which is not desired, but ignoring)
|
|
105
108
|
onDestroy(BodyScroll.unlock);
|
|
106
109
|
|
|
110
|
+
// Unique ID for this Backdrop instance
|
|
111
|
+
const instanceId = Symbol();
|
|
112
|
+
|
|
107
113
|
$effect(() => {
|
|
114
|
+
if (!visible || typeof onEscape !== "function") return;
|
|
115
|
+
|
|
116
|
+
// Add to stack when visible
|
|
117
|
+
escapeStack.add(instanceId);
|
|
118
|
+
|
|
108
119
|
function onkeydown(e: KeyboardEvent) {
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// Skip if already handled by another component (ModalDialog, DropdownMenu, etc.)
|
|
121
|
+
if (e.defaultPrevented) return;
|
|
122
|
+
|
|
123
|
+
// Only handle if this is the topmost Backdrop
|
|
124
|
+
const stack = [...escapeStack];
|
|
125
|
+
if (stack[stack.length - 1] !== instanceId) return;
|
|
126
|
+
|
|
127
|
+
if (e.key === "Escape") {
|
|
112
128
|
e.preventDefault();
|
|
113
|
-
onEscape();
|
|
129
|
+
onEscape?.();
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
|
|
133
|
+
window.addEventListener("keydown", onkeydown);
|
|
134
|
+
return () => {
|
|
135
|
+
escapeStack.delete(instanceId);
|
|
136
|
+
window.removeEventListener("keydown", onkeydown);
|
|
137
|
+
};
|
|
118
138
|
});
|
|
119
139
|
</script>
|
|
120
140
|
|
|
@@ -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
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./file-from-bloburl.js";
|
|
|
11
11
|
export * from "./force-download.js";
|
|
12
12
|
export * from "./get-file-type-label.js";
|
|
13
13
|
export * from "./get-id.js";
|
|
14
|
+
export * from "./input-history.svelte.js";
|
|
14
15
|
export * from "./is-browser.js";
|
|
15
16
|
export * from "./is-image.js";
|
|
16
17
|
export * from "./is-mac.js";
|
package/dist/utils/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./file-from-bloburl.js";
|
|
|
11
11
|
export * from "./force-download.js";
|
|
12
12
|
export * from "./get-file-type-label.js";
|
|
13
13
|
export * from "./get-id.js";
|
|
14
|
+
export * from "./input-history.svelte.js";
|
|
14
15
|
export * from "./is-browser.js";
|
|
15
16
|
export * from "./is-image.js";
|
|
16
17
|
export * from "./is-mac.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for InputHistory
|
|
3
|
+
*/
|
|
4
|
+
export interface InputHistoryOptions {
|
|
5
|
+
/** Composite key parts for namespacing (e.g., [projectId, domain, entity, type]) */
|
|
6
|
+
keyParts: string[];
|
|
7
|
+
/** Maximum number of entries to store (default: 10) */
|
|
8
|
+
maxEntries?: number;
|
|
9
|
+
/** App ID prefix for the storage key (default: "app") */
|
|
10
|
+
appId?: string;
|
|
11
|
+
/** Feature name for the key (default: "input-history") */
|
|
12
|
+
featureName?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A reactive input history manager with localStorage persistence and arrow key navigation.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const history = new InputHistory({
|
|
20
|
+
* keyParts: [projectId, domain, entity, type],
|
|
21
|
+
* appId: "joy",
|
|
22
|
+
* featureName: "filter-history"
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Add entry on submit
|
|
26
|
+
* history.add(query);
|
|
27
|
+
*
|
|
28
|
+
* // Navigate with arrow keys
|
|
29
|
+
* history.navigateUp(); // Go to older entry
|
|
30
|
+
* history.navigateDown(); // Go to newer entry
|
|
31
|
+
*
|
|
32
|
+
* // Get current entry for display
|
|
33
|
+
* const current = history.getCurrent();
|
|
34
|
+
*
|
|
35
|
+
* // Reset navigation when user starts typing
|
|
36
|
+
* history.reset();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class InputHistory {
|
|
40
|
+
#private;
|
|
41
|
+
/** Storage key for this history instance */
|
|
42
|
+
readonly key: string;
|
|
43
|
+
/** Maximum entries to store */
|
|
44
|
+
readonly maxEntries: number;
|
|
45
|
+
constructor(options: InputHistoryOptions);
|
|
46
|
+
/** Get the stored history entries (newest first) */
|
|
47
|
+
get entries(): string[];
|
|
48
|
+
/** Get current navigation index (-1 when not navigating) */
|
|
49
|
+
get navigationIndex(): number;
|
|
50
|
+
/** Check if currently navigating through history */
|
|
51
|
+
get isNavigating(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Add a new query to history (called on Enter/submit).
|
|
54
|
+
* Deduplicates and limits to maxEntries.
|
|
55
|
+
*/
|
|
56
|
+
add(query: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Navigate up (to older entries).
|
|
59
|
+
* On first call, saves current input value.
|
|
60
|
+
* @param currentValue - The current input value (saved on first navigation)
|
|
61
|
+
*/
|
|
62
|
+
navigateUp(currentValue?: string): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Navigate down (to newer entries).
|
|
65
|
+
* When reaching past newest, returns to temp value.
|
|
66
|
+
*/
|
|
67
|
+
navigateDown(): string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Get the current history entry based on navigation index.
|
|
70
|
+
* Returns null if not navigating.
|
|
71
|
+
*/
|
|
72
|
+
getCurrent(): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Reset navigation state (call when user starts typing).
|
|
75
|
+
*/
|
|
76
|
+
reset(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Clear all history for this key.
|
|
79
|
+
*/
|
|
80
|
+
clear(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Clear all histories matching a pattern prefix.
|
|
83
|
+
* Call this on logout to clean up user data.
|
|
84
|
+
*
|
|
85
|
+
* @param pattern - Key prefix to match (e.g., "joy:input-history")
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* // On logout, clear all input histories
|
|
90
|
+
* InputHistory.clearAllMatching("joy:input-history");
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
static clearAllMatching(pattern: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Clear all registered histories (nuclear option).
|
|
96
|
+
*/
|
|
97
|
+
static clearAll(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Get all registered history keys (for debugging).
|
|
100
|
+
*/
|
|
101
|
+
static getRegisteredKeys(): string[];
|
|
102
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { localStorageState } from "./persistent-state.svelte.js";
|
|
2
|
+
/**
|
|
3
|
+
* A reactive input history manager with localStorage persistence and arrow key navigation.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const history = new InputHistory({
|
|
8
|
+
* keyParts: [projectId, domain, entity, type],
|
|
9
|
+
* appId: "joy",
|
|
10
|
+
* featureName: "filter-history"
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // Add entry on submit
|
|
14
|
+
* history.add(query);
|
|
15
|
+
*
|
|
16
|
+
* // Navigate with arrow keys
|
|
17
|
+
* history.navigateUp(); // Go to older entry
|
|
18
|
+
* history.navigateDown(); // Go to newer entry
|
|
19
|
+
*
|
|
20
|
+
* // Get current entry for display
|
|
21
|
+
* const current = history.getCurrent();
|
|
22
|
+
*
|
|
23
|
+
* // Reset navigation when user starts typing
|
|
24
|
+
* history.reset();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class InputHistory {
|
|
28
|
+
/** Storage key for this history instance */
|
|
29
|
+
key;
|
|
30
|
+
/** Maximum entries to store */
|
|
31
|
+
maxEntries;
|
|
32
|
+
/** Persistent state for stored history entries */
|
|
33
|
+
#storage;
|
|
34
|
+
/** Current navigation index (-1 means "not navigating", 0 is newest, length-1 is oldest) */
|
|
35
|
+
#navigationIndex = $state(-1);
|
|
36
|
+
/** Temporary value holder for current input before navigation started */
|
|
37
|
+
#tempValue = $state("");
|
|
38
|
+
constructor(options) {
|
|
39
|
+
const { keyParts, maxEntries = 10, appId = "app", featureName = "input-history", } = options;
|
|
40
|
+
this.maxEntries = maxEntries;
|
|
41
|
+
// Build composite key: "joy:input-history:projectId:domain:entity:type"
|
|
42
|
+
this.key = [appId, featureName, ...keyParts].filter(Boolean).join(":");
|
|
43
|
+
// Initialize persistent storage
|
|
44
|
+
this.#storage = localStorageState(this.key, []);
|
|
45
|
+
// Register this instance for cleanup
|
|
46
|
+
InputHistory.#register(this.key);
|
|
47
|
+
}
|
|
48
|
+
// ─────────────────────────────────────────────────────────────
|
|
49
|
+
// Public API
|
|
50
|
+
// ─────────────────────────────────────────────────────────────
|
|
51
|
+
/** Get the stored history entries (newest first) */
|
|
52
|
+
get entries() {
|
|
53
|
+
return this.#storage.current;
|
|
54
|
+
}
|
|
55
|
+
/** Get current navigation index (-1 when not navigating) */
|
|
56
|
+
get navigationIndex() {
|
|
57
|
+
return this.#navigationIndex;
|
|
58
|
+
}
|
|
59
|
+
/** Check if currently navigating through history */
|
|
60
|
+
get isNavigating() {
|
|
61
|
+
return this.#navigationIndex >= 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Add a new query to history (called on Enter/submit).
|
|
65
|
+
* Deduplicates and limits to maxEntries.
|
|
66
|
+
*/
|
|
67
|
+
add(query) {
|
|
68
|
+
query = query.trim();
|
|
69
|
+
if (!query)
|
|
70
|
+
return;
|
|
71
|
+
const current = [...this.#storage.current];
|
|
72
|
+
// Remove duplicates (case-sensitive)
|
|
73
|
+
const filtered = current.filter((item) => item !== query);
|
|
74
|
+
// Add to beginning (newest first)
|
|
75
|
+
filtered.unshift(query);
|
|
76
|
+
// Limit to maxEntries
|
|
77
|
+
this.#storage.current = filtered.slice(0, this.maxEntries);
|
|
78
|
+
// Reset navigation after adding
|
|
79
|
+
this.reset();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Navigate up (to older entries).
|
|
83
|
+
* On first call, saves current input value.
|
|
84
|
+
* @param currentValue - The current input value (saved on first navigation)
|
|
85
|
+
*/
|
|
86
|
+
navigateUp(currentValue) {
|
|
87
|
+
const entries = this.entries;
|
|
88
|
+
if (entries.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
// If not navigating yet, save current value and start
|
|
91
|
+
if (this.#navigationIndex < 0) {
|
|
92
|
+
this.#tempValue = currentValue ?? "";
|
|
93
|
+
this.#navigationIndex = 0;
|
|
94
|
+
}
|
|
95
|
+
else if (this.#navigationIndex < entries.length - 1) {
|
|
96
|
+
// Move to older entry
|
|
97
|
+
this.#navigationIndex++;
|
|
98
|
+
}
|
|
99
|
+
// At oldest entry, stay there
|
|
100
|
+
return this.getCurrent();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Navigate down (to newer entries).
|
|
104
|
+
* When reaching past newest, returns to temp value.
|
|
105
|
+
*/
|
|
106
|
+
navigateDown() {
|
|
107
|
+
if (this.#navigationIndex < 0)
|
|
108
|
+
return null;
|
|
109
|
+
if (this.#navigationIndex > 0) {
|
|
110
|
+
// Move to newer entry
|
|
111
|
+
this.#navigationIndex--;
|
|
112
|
+
return this.getCurrent();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// At newest entry, go back to temp value
|
|
116
|
+
const temp = this.#tempValue;
|
|
117
|
+
this.reset();
|
|
118
|
+
return temp;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the current history entry based on navigation index.
|
|
123
|
+
* Returns null if not navigating.
|
|
124
|
+
*/
|
|
125
|
+
getCurrent() {
|
|
126
|
+
if (this.#navigationIndex < 0)
|
|
127
|
+
return null;
|
|
128
|
+
return this.entries[this.#navigationIndex] ?? null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Reset navigation state (call when user starts typing).
|
|
132
|
+
*/
|
|
133
|
+
reset() {
|
|
134
|
+
this.#navigationIndex = -1;
|
|
135
|
+
this.#tempValue = "";
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Clear all history for this key.
|
|
139
|
+
*/
|
|
140
|
+
clear() {
|
|
141
|
+
this.#storage.current = [];
|
|
142
|
+
this.reset();
|
|
143
|
+
}
|
|
144
|
+
// ─────────────────────────────────────────────────────────────
|
|
145
|
+
// Static cleanup registration
|
|
146
|
+
// ─────────────────────────────────────────────────────────────
|
|
147
|
+
/** Registry of all history keys (for cleanup on logout) */
|
|
148
|
+
static #registeredKeys = new Set();
|
|
149
|
+
/** Register a key for potential cleanup */
|
|
150
|
+
static #register(key) {
|
|
151
|
+
InputHistory.#registeredKeys.add(key);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Clear all histories matching a pattern prefix.
|
|
155
|
+
* Call this on logout to clean up user data.
|
|
156
|
+
*
|
|
157
|
+
* @param pattern - Key prefix to match (e.g., "joy:input-history")
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* // On logout, clear all input histories
|
|
162
|
+
* InputHistory.clearAllMatching("joy:input-history");
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
static clearAllMatching(pattern) {
|
|
166
|
+
// Clear from our registry
|
|
167
|
+
for (const key of InputHistory.#registeredKeys) {
|
|
168
|
+
if (key.startsWith(pattern)) {
|
|
169
|
+
localStorage.removeItem(key);
|
|
170
|
+
InputHistory.#registeredKeys.delete(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Also scan localStorage for any keys we might have missed
|
|
174
|
+
// (e.g., from previous sessions)
|
|
175
|
+
for (let i = localStorage.length - 1; i >= 0; i--) {
|
|
176
|
+
const key = localStorage.key(i);
|
|
177
|
+
if (key?.startsWith(pattern)) {
|
|
178
|
+
localStorage.removeItem(key);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Clear all registered histories (nuclear option).
|
|
184
|
+
*/
|
|
185
|
+
static clearAll() {
|
|
186
|
+
for (const key of InputHistory.#registeredKeys) {
|
|
187
|
+
localStorage.removeItem(key);
|
|
188
|
+
}
|
|
189
|
+
InputHistory.#registeredKeys.clear();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get all registered history keys (for debugging).
|
|
193
|
+
*/
|
|
194
|
+
static getRegisteredKeys() {
|
|
195
|
+
return [...InputHistory.#registeredKeys];
|
|
196
|
+
}
|
|
197
|
+
}
|