@rokkit/actions 1.0.0-next.125 → 1.0.0-next.128
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/hover-lift.svelte.d.ts +31 -0
- package/dist/index.d.ts +7 -0
- package/dist/kbd.d.ts +7 -0
- package/dist/keymap.d.ts +35 -0
- package/dist/magnetic.svelte.d.ts +26 -0
- package/dist/nav-constants.d.ts +65 -0
- package/dist/navigator.d.ts +14 -0
- package/dist/reveal.svelte.d.ts +51 -0
- package/dist/ripple.svelte.d.ts +31 -0
- package/dist/trigger.d.ts +41 -0
- package/package.json +2 -2
- package/src/hover-lift.svelte.js +56 -0
- package/src/index.js +7 -0
- package/src/kbd.js +19 -3
- package/src/keymap.js +81 -0
- package/src/magnetic.svelte.js +58 -0
- package/src/nav-constants.js +61 -0
- package/src/navigator.js +250 -0
- package/src/navigator.svelte.js +87 -13
- package/src/reveal.svelte.js +86 -0
- package/src/ripple.svelte.js +88 -0
- package/src/trigger.js +112 -0
- package/src/utils.js +38 -5
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover lift action — adds translateY + elevated shadow on hover.
|
|
3
|
+
* Sets transition on mount, applies transform + box-shadow on mouseenter, resets on mouseleave.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {HoverLiftOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} HoverLiftOptions
|
|
9
|
+
* @property {string} [distance='-0.25rem'] Translate distance on hover (negative = up)
|
|
10
|
+
* @property {string} [shadow='0 10px 25px -5px rgba(0,0,0,0.1)'] Box shadow on hover
|
|
11
|
+
* @property {number} [duration=200] Transition duration (ms)
|
|
12
|
+
*/
|
|
13
|
+
export function hoverLift(node: HTMLElement, options?: HoverLiftOptions): void;
|
|
14
|
+
/**
|
|
15
|
+
* Hover lift action — adds translateY + elevated shadow on hover.
|
|
16
|
+
* Sets transition on mount, applies transform + box-shadow on mouseenter, resets on mouseleave.
|
|
17
|
+
*/
|
|
18
|
+
export type HoverLiftOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Translate distance on hover (negative = up)
|
|
21
|
+
*/
|
|
22
|
+
distance?: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Box shadow on hover
|
|
25
|
+
*/
|
|
26
|
+
shadow?: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Transition duration (ms)
|
|
29
|
+
*/
|
|
30
|
+
duration?: number | undefined;
|
|
31
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
+
export { Navigator } from "./navigator.js";
|
|
3
|
+
export { Trigger } from "./trigger.js";
|
|
2
4
|
export { keyboard } from "./keyboard.svelte.js";
|
|
3
5
|
export { pannable } from "./pannable.svelte.js";
|
|
4
6
|
export { swipeable } from "./swipeable.svelte.js";
|
|
@@ -9,3 +11,8 @@ export { dismissable } from "./dismissable.svelte.js";
|
|
|
9
11
|
export { navigable } from "./navigable.svelte.js";
|
|
10
12
|
export { fillable } from "./fillable.svelte.js";
|
|
11
13
|
export { delegateKeyboardEvents } from "./delegate.svelte.js";
|
|
14
|
+
export { reveal } from "./reveal.svelte.js";
|
|
15
|
+
export { hoverLift } from "./hover-lift.svelte.js";
|
|
16
|
+
export { magnetic } from "./magnetic.svelte.js";
|
|
17
|
+
export { ripple } from "./ripple.svelte.js";
|
|
18
|
+
export { buildKeymap, resolveAction, ACTIONS } from "./keymap.js";
|
package/dist/kbd.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export function createKeyboardActionMap(options: {
|
|
|
29
29
|
export function createModifierKeyboardActionMap(options: {
|
|
30
30
|
orientation: string;
|
|
31
31
|
}): Object;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a keyboard action mapping for shift key combinations
|
|
34
|
+
*
|
|
35
|
+
* @returns {Object} Mapping of keys to actions
|
|
36
|
+
*/
|
|
37
|
+
export function createShiftKeyboardActionMap(): Object;
|
|
32
38
|
/**
|
|
33
39
|
* Gets the keyboard action for a key event
|
|
34
40
|
* @param {KeyboardEvent} event - The keyboard event
|
|
@@ -41,4 +47,5 @@ export namespace defaultNavigationOptions {
|
|
|
41
47
|
let dir: string;
|
|
42
48
|
let nested: boolean;
|
|
43
49
|
let enabled: boolean;
|
|
50
|
+
let typeahead: boolean;
|
|
44
51
|
}
|
package/dist/keymap.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a complete keymap for the given options.
|
|
3
|
+
*
|
|
4
|
+
* Returns three layers — plain, shift, ctrl — each mapping key name → action name.
|
|
5
|
+
* Call resolveAction(event, keymap) to look up the action for a keyboard event.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} [options]
|
|
8
|
+
* @param {'vertical'|'horizontal'} [options.orientation='vertical']
|
|
9
|
+
* @param {'ltr'|'rtl'} [options.dir='ltr']
|
|
10
|
+
* @param {boolean} [options.collapsible=false]
|
|
11
|
+
* @returns {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }}
|
|
12
|
+
*/
|
|
13
|
+
export function buildKeymap({ orientation, dir, collapsible }?: {
|
|
14
|
+
orientation?: "vertical" | "horizontal" | undefined;
|
|
15
|
+
dir?: "ltr" | "rtl" | undefined;
|
|
16
|
+
collapsible?: boolean | undefined;
|
|
17
|
+
}): {
|
|
18
|
+
plain: Record<string, string>;
|
|
19
|
+
shift: Record<string, string>;
|
|
20
|
+
ctrl: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the action for a keyboard event given a pre-built keymap.
|
|
24
|
+
* Returns null if the key has no binding.
|
|
25
|
+
*
|
|
26
|
+
* @param {KeyboardEvent} event
|
|
27
|
+
* @param {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }} keymap
|
|
28
|
+
* @returns {string|null}
|
|
29
|
+
*/
|
|
30
|
+
export function resolveAction(event: KeyboardEvent, keymap: {
|
|
31
|
+
plain: Record<string, string>;
|
|
32
|
+
shift: Record<string, string>;
|
|
33
|
+
ctrl: Record<string, string>;
|
|
34
|
+
}): string | null;
|
|
35
|
+
export { ACTIONS } from "./nav-constants.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magnetic action — element shifts subtly toward the cursor on hover.
|
|
3
|
+
* Calculates cursor offset from element center and translates proportionally.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {MagneticOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} MagneticOptions
|
|
9
|
+
* @property {number} [strength=0.3] Maximum displacement as fraction of element size (0–1)
|
|
10
|
+
* @property {number} [duration=300] Transition duration for return to center (ms)
|
|
11
|
+
*/
|
|
12
|
+
export function magnetic(node: HTMLElement, options?: MagneticOptions): void;
|
|
13
|
+
/**
|
|
14
|
+
* Magnetic action — element shifts subtly toward the cursor on hover.
|
|
15
|
+
* Calculates cursor offset from element center and translates proportionally.
|
|
16
|
+
*/
|
|
17
|
+
export type MagneticOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* Maximum displacement as fraction of element size (0–1)
|
|
20
|
+
*/
|
|
21
|
+
strength?: number | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Transition duration for return to center (ms)
|
|
24
|
+
*/
|
|
25
|
+
duration?: number | undefined;
|
|
26
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigator constants — keyboard actions, key bindings, and typeahead config.
|
|
3
|
+
* These are used by the Navigator class and keymap builder.
|
|
4
|
+
*/
|
|
5
|
+
export const ACTIONS: Readonly<{
|
|
6
|
+
next: "next";
|
|
7
|
+
prev: "prev";
|
|
8
|
+
first: "first";
|
|
9
|
+
last: "last";
|
|
10
|
+
expand: "expand";
|
|
11
|
+
collapse: "collapse";
|
|
12
|
+
select: "select";
|
|
13
|
+
extend: "extend";
|
|
14
|
+
range: "range";
|
|
15
|
+
cancel: "cancel";
|
|
16
|
+
}>;
|
|
17
|
+
export const PLAIN_FIXED: {
|
|
18
|
+
Enter: "select";
|
|
19
|
+
' ': "select";
|
|
20
|
+
Home: "first";
|
|
21
|
+
End: "last";
|
|
22
|
+
Escape: "cancel";
|
|
23
|
+
};
|
|
24
|
+
export const CTRL_FIXED: {
|
|
25
|
+
' ': "extend";
|
|
26
|
+
Home: "first";
|
|
27
|
+
End: "last";
|
|
28
|
+
};
|
|
29
|
+
export const SHIFT_FIXED: {
|
|
30
|
+
' ': "range";
|
|
31
|
+
};
|
|
32
|
+
export const ARROWS: {
|
|
33
|
+
'vertical-ltr': {
|
|
34
|
+
move: {
|
|
35
|
+
ArrowUp: "prev";
|
|
36
|
+
ArrowDown: "next";
|
|
37
|
+
};
|
|
38
|
+
nested: {
|
|
39
|
+
ArrowLeft: "collapse";
|
|
40
|
+
ArrowRight: "expand";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
'vertical-rtl': {
|
|
44
|
+
move: {
|
|
45
|
+
ArrowUp: "prev";
|
|
46
|
+
ArrowDown: "next";
|
|
47
|
+
};
|
|
48
|
+
nested: {
|
|
49
|
+
ArrowRight: "collapse";
|
|
50
|
+
ArrowLeft: "expand";
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
horizontal: {
|
|
54
|
+
move: {
|
|
55
|
+
ArrowLeft: "prev";
|
|
56
|
+
ArrowRight: "next";
|
|
57
|
+
};
|
|
58
|
+
nested: {
|
|
59
|
+
ArrowUp: "collapse";
|
|
60
|
+
ArrowDown: "expand";
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Milliseconds of inactivity before the typeahead buffer resets. */
|
|
65
|
+
export const TYPEAHEAD_RESET_MS: 500;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class Navigator {
|
|
2
|
+
/**
|
|
3
|
+
* @param {HTMLElement} root
|
|
4
|
+
* @param {import('@rokkit/states').Wrapper} wrapper
|
|
5
|
+
* @param {{ orientation?: string, dir?: string, collapsible?: boolean }} [options]
|
|
6
|
+
*/
|
|
7
|
+
constructor(root: HTMLElement, wrapper: import("@rokkit/states").Wrapper, options?: {
|
|
8
|
+
orientation?: string;
|
|
9
|
+
dir?: string;
|
|
10
|
+
collapsible?: boolean;
|
|
11
|
+
});
|
|
12
|
+
destroy(): void;
|
|
13
|
+
#private;
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-triggered reveal action using IntersectionObserver.
|
|
3
|
+
* Applies CSS transitions (opacity + translate) when element enters viewport.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {RevealOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} RevealOptions
|
|
9
|
+
* @property {'up' | 'down' | 'left' | 'right' | 'none'} [direction='up'] Slide direction
|
|
10
|
+
* @property {string} [distance='1.5rem'] Slide distance (CSS unit)
|
|
11
|
+
* @property {number} [duration=600] Animation duration (ms)
|
|
12
|
+
* @property {number} [delay=0] Delay before animation starts (ms)
|
|
13
|
+
* @property {boolean} [once=true] Only animate once
|
|
14
|
+
* @property {number} [threshold=0.1] IntersectionObserver threshold (0–1)
|
|
15
|
+
* @property {string} [easing='cubic-bezier(0.4, 0, 0.2, 1)'] CSS easing function
|
|
16
|
+
*/
|
|
17
|
+
export function reveal(node: HTMLElement, options?: RevealOptions): void;
|
|
18
|
+
/**
|
|
19
|
+
* Scroll-triggered reveal action using IntersectionObserver.
|
|
20
|
+
* Applies CSS transitions (opacity + translate) when element enters viewport.
|
|
21
|
+
*/
|
|
22
|
+
export type RevealOptions = {
|
|
23
|
+
/**
|
|
24
|
+
* Slide direction
|
|
25
|
+
*/
|
|
26
|
+
direction?: "up" | "down" | "left" | "right" | "none" | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Slide distance (CSS unit)
|
|
29
|
+
*/
|
|
30
|
+
distance?: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Animation duration (ms)
|
|
33
|
+
*/
|
|
34
|
+
duration?: number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Delay before animation starts (ms)
|
|
37
|
+
*/
|
|
38
|
+
delay?: number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Only animate once
|
|
41
|
+
*/
|
|
42
|
+
once?: boolean | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* IntersectionObserver threshold (0–1)
|
|
45
|
+
*/
|
|
46
|
+
threshold?: number | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* CSS easing function
|
|
49
|
+
*/
|
|
50
|
+
easing?: string | undefined;
|
|
51
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripple action — material-design inspired click ripple effect.
|
|
3
|
+
* Appends a circular expanding span at click coordinates that scales and fades out.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {RippleOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} RippleOptions
|
|
9
|
+
* @property {string} [color='currentColor'] Ripple color
|
|
10
|
+
* @property {number} [opacity=0.15] Ripple opacity
|
|
11
|
+
* @property {number} [duration=500] Ripple animation duration (ms)
|
|
12
|
+
*/
|
|
13
|
+
export function ripple(node: HTMLElement, options?: RippleOptions): void;
|
|
14
|
+
/**
|
|
15
|
+
* Ripple action — material-design inspired click ripple effect.
|
|
16
|
+
* Appends a circular expanding span at click coordinates that scales and fades out.
|
|
17
|
+
*/
|
|
18
|
+
export type RippleOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Ripple color
|
|
21
|
+
*/
|
|
22
|
+
color?: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Ripple opacity
|
|
25
|
+
*/
|
|
26
|
+
opacity?: number | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Ripple animation duration (ms)
|
|
29
|
+
*/
|
|
30
|
+
duration?: number | undefined;
|
|
31
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger
|
|
3
|
+
*
|
|
4
|
+
* Manages dropdown open/close from a trigger button.
|
|
5
|
+
* Pairs with Navigator (on the dropdown) to form a complete dropdown component.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - click on trigger → toggle open/close
|
|
9
|
+
* - Enter / Space → toggle open/close
|
|
10
|
+
* - ArrowDown → open (callback can focus first item)
|
|
11
|
+
* - ArrowUp → open (callback can focus last item)
|
|
12
|
+
* - Escape (document) → close + return focus to trigger
|
|
13
|
+
* - Click outside (doc) → close
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const trigger = new Trigger(triggerEl, containerEl, {
|
|
17
|
+
* onopen: () => { isOpen = true },
|
|
18
|
+
* onclose: () => { isOpen = false },
|
|
19
|
+
* onlast: () => { wrapper.last(null) } // optional: ArrowUp opens at end
|
|
20
|
+
* })
|
|
21
|
+
* // …
|
|
22
|
+
* trigger.destroy()
|
|
23
|
+
*/
|
|
24
|
+
export class Trigger {
|
|
25
|
+
/**
|
|
26
|
+
* @param {HTMLElement} trigger — the trigger button element
|
|
27
|
+
* @param {HTMLElement} container — the menu root (for click-outside detection)
|
|
28
|
+
* @param {{ onopen: () => void, onclose: () => void, onlast?: () => void, isOpen: () => boolean }} callbacks
|
|
29
|
+
*/
|
|
30
|
+
constructor(trigger: HTMLElement, container: HTMLElement, { onopen, onclose, onlast, isOpen }: {
|
|
31
|
+
onopen: () => void;
|
|
32
|
+
onclose: () => void;
|
|
33
|
+
onlast?: () => void;
|
|
34
|
+
isOpen: () => boolean;
|
|
35
|
+
});
|
|
36
|
+
get isOpen(): boolean;
|
|
37
|
+
open(): void;
|
|
38
|
+
close(): void;
|
|
39
|
+
destroy(): void;
|
|
40
|
+
#private;
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/actions",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.128",
|
|
4
4
|
"description": "Contains generic actions that can be used in various components.",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"ramda": "^0.
|
|
32
|
+
"ramda": "^0.32.0",
|
|
33
33
|
"@rokkit/core": "latest"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover lift action — adds translateY + elevated shadow on hover.
|
|
3
|
+
* Sets transition on mount, applies transform + box-shadow on mouseenter, resets on mouseleave.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {HoverLiftOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} HoverLiftOptions
|
|
9
|
+
* @property {string} [distance='-0.25rem'] Translate distance on hover (negative = up)
|
|
10
|
+
* @property {string} [shadow='0 10px 25px -5px rgba(0,0,0,0.1)'] Box shadow on hover
|
|
11
|
+
* @property {number} [duration=200] Transition duration (ms)
|
|
12
|
+
*/
|
|
13
|
+
export function hoverLift(node, options = {}) {
|
|
14
|
+
$effect(() => {
|
|
15
|
+
const opts = {
|
|
16
|
+
distance: '-0.25rem',
|
|
17
|
+
shadow: '0 10px 25px -5px rgba(0,0,0,0.1)',
|
|
18
|
+
duration: 200,
|
|
19
|
+
...options
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const reducedMotion =
|
|
23
|
+
typeof window !== 'undefined' &&
|
|
24
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
25
|
+
|
|
26
|
+
if (reducedMotion) return
|
|
27
|
+
|
|
28
|
+
// Store original values
|
|
29
|
+
const originalTransform = node.style.transform
|
|
30
|
+
const originalBoxShadow = node.style.boxShadow
|
|
31
|
+
const originalTransition = node.style.transition
|
|
32
|
+
|
|
33
|
+
node.style.transition = `transform ${opts.duration}ms ease, box-shadow ${opts.duration}ms ease`
|
|
34
|
+
|
|
35
|
+
function onEnter() {
|
|
36
|
+
node.style.transform = `translateY(${opts.distance})`
|
|
37
|
+
node.style.boxShadow = opts.shadow
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onLeave() {
|
|
41
|
+
node.style.transform = originalTransform
|
|
42
|
+
node.style.boxShadow = originalBoxShadow
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
node.addEventListener('mouseenter', onEnter)
|
|
46
|
+
node.addEventListener('mouseleave', onLeave)
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
node.removeEventListener('mouseenter', onEnter)
|
|
50
|
+
node.removeEventListener('mouseleave', onLeave)
|
|
51
|
+
node.style.transform = originalTransform
|
|
52
|
+
node.style.boxShadow = originalBoxShadow
|
|
53
|
+
node.style.transition = originalTransition
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// skipcq: JS-E1004 - Needed for exposing all types
|
|
2
2
|
export * from './types.js'
|
|
3
|
+
export { Navigator } from './navigator.js'
|
|
4
|
+
export { Trigger } from './trigger.js'
|
|
5
|
+
export { buildKeymap, resolveAction, ACTIONS } from './keymap.js'
|
|
3
6
|
export { keyboard } from './keyboard.svelte.js'
|
|
4
7
|
export { pannable } from './pannable.svelte.js'
|
|
5
8
|
export { swipeable } from './swipeable.svelte.js'
|
|
@@ -10,3 +13,7 @@ export { dismissable } from './dismissable.svelte.js'
|
|
|
10
13
|
export { navigable } from './navigable.svelte.js'
|
|
11
14
|
export { fillable } from './fillable.svelte.js'
|
|
12
15
|
export { delegateKeyboardEvents } from './delegate.svelte.js'
|
|
16
|
+
export { reveal } from './reveal.svelte.js'
|
|
17
|
+
export { hoverLift } from './hover-lift.svelte.js'
|
|
18
|
+
export { magnetic } from './magnetic.svelte.js'
|
|
19
|
+
export { ripple } from './ripple.svelte.js'
|
package/src/kbd.js
CHANGED
|
@@ -57,7 +57,8 @@ export const defaultNavigationOptions = {
|
|
|
57
57
|
orientation: 'vertical',
|
|
58
58
|
dir: 'ltr',
|
|
59
59
|
nested: false,
|
|
60
|
-
enabled: true
|
|
60
|
+
enabled: true,
|
|
61
|
+
typeahead: false
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -161,6 +162,15 @@ export function createModifierKeyboardActionMap(options) {
|
|
|
161
162
|
return { ...common, ...directional }
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Creates a keyboard action mapping for shift key combinations
|
|
167
|
+
*
|
|
168
|
+
* @returns {Object} Mapping of keys to actions
|
|
169
|
+
*/
|
|
170
|
+
export function createShiftKeyboardActionMap() {
|
|
171
|
+
return { ' ': 'range' }
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
/**
|
|
165
175
|
* Gets the keyboard action for a key event
|
|
166
176
|
* @param {KeyboardEvent} event - The keyboard event
|
|
@@ -168,12 +178,18 @@ export function createModifierKeyboardActionMap(options) {
|
|
|
168
178
|
* @returns {string|null} The action to perform, or null if no action is defined
|
|
169
179
|
*/
|
|
170
180
|
export function getKeyboardAction(event, options = {}) {
|
|
171
|
-
const { key, ctrlKey, metaKey } = event
|
|
181
|
+
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
172
182
|
|
|
173
183
|
// Use updated options with defaults
|
|
174
184
|
const mergedOptions = { ...defaultNavigationOptions, ...options }
|
|
175
185
|
|
|
176
|
-
// Check for
|
|
186
|
+
// Check for shift key (range selection)
|
|
187
|
+
if (shiftKey && !ctrlKey && !metaKey) {
|
|
188
|
+
const shiftMap = createShiftKeyboardActionMap()
|
|
189
|
+
return shiftMap[key] || null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check for modifier keys (ctrl/cmd)
|
|
177
193
|
if (ctrlKey || metaKey) {
|
|
178
194
|
const modifierMap = createModifierKeyboardActionMap(mergedOptions)
|
|
179
195
|
return modifierMap[key] || null
|
package/src/keymap.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Keymap
|
|
3
|
+
*
|
|
4
|
+
* Maps keyboard inputs to semantic actions.
|
|
5
|
+
*
|
|
6
|
+
* Design principle: orientation is just a rotation of arrow key assignments.
|
|
7
|
+
*
|
|
8
|
+
* vertical ltr up/down = prev/next left/right = collapse/expand (when collapsible)
|
|
9
|
+
* vertical rtl up/down = prev/next right/left = collapse/expand (expand/collapse reversed)
|
|
10
|
+
* horizontal left/right = prev/next up/down = collapse/expand (dir ignored — use CSS flex-reverse for RTL)
|
|
11
|
+
*
|
|
12
|
+
* ─── Actions ────────────────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* next focus next visible item, skip disabled
|
|
15
|
+
* prev focus previous visible item, skip disabled
|
|
16
|
+
* first jump to first visible item
|
|
17
|
+
* last jump to last visible item
|
|
18
|
+
* expand when collapsible: expand collapsed group
|
|
19
|
+
* if already expanded: move focus to first child
|
|
20
|
+
* on leaf: no-op
|
|
21
|
+
* collapse when collapsible: collapse expanded group
|
|
22
|
+
* if already collapsed or leaf: move focus to parent
|
|
23
|
+
* at root level: no-op
|
|
24
|
+
* select activate the focused item
|
|
25
|
+
* extend toggle individual selection (multiselect ctrl/cmd + space)
|
|
26
|
+
* range select contiguous range (multiselect shift + space)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { PLAIN_FIXED, CTRL_FIXED, SHIFT_FIXED, ARROWS } from './nav-constants.js'
|
|
30
|
+
export { ACTIONS } from './nav-constants.js'
|
|
31
|
+
|
|
32
|
+
function getArrows(orientation, dir) {
|
|
33
|
+
if (orientation === 'horizontal') return ARROWS.horizontal
|
|
34
|
+
return ARROWS[`vertical-${dir}`]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── buildKeymap ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a complete keymap for the given options.
|
|
41
|
+
*
|
|
42
|
+
* Returns three layers — plain, shift, ctrl — each mapping key name → action name.
|
|
43
|
+
* Call resolveAction(event, keymap) to look up the action for a keyboard event.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} [options]
|
|
46
|
+
* @param {'vertical'|'horizontal'} [options.orientation='vertical']
|
|
47
|
+
* @param {'ltr'|'rtl'} [options.dir='ltr']
|
|
48
|
+
* @param {boolean} [options.collapsible=false]
|
|
49
|
+
* @returns {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }}
|
|
50
|
+
*/
|
|
51
|
+
export function buildKeymap({ orientation = 'vertical', dir = 'ltr', collapsible = false } = {}) {
|
|
52
|
+
const arrows = getArrows(orientation, dir)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
plain: {
|
|
56
|
+
...PLAIN_FIXED,
|
|
57
|
+
...arrows.move,
|
|
58
|
+
...(collapsible ? arrows.nested : {})
|
|
59
|
+
},
|
|
60
|
+
shift: { ...SHIFT_FIXED },
|
|
61
|
+
ctrl: { ...CTRL_FIXED }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── resolveAction ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the action for a keyboard event given a pre-built keymap.
|
|
69
|
+
* Returns null if the key has no binding.
|
|
70
|
+
*
|
|
71
|
+
* @param {KeyboardEvent} event
|
|
72
|
+
* @param {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }} keymap
|
|
73
|
+
* @returns {string|null}
|
|
74
|
+
*/
|
|
75
|
+
export function resolveAction(event, keymap) {
|
|
76
|
+
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
77
|
+
|
|
78
|
+
if (shiftKey && !ctrlKey && !metaKey) return keymap.shift[key] ?? null
|
|
79
|
+
if (ctrlKey || metaKey) return keymap.ctrl[key] ?? null
|
|
80
|
+
return keymap.plain[key] ?? null
|
|
81
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magnetic action — element shifts subtly toward the cursor on hover.
|
|
3
|
+
* Calculates cursor offset from element center and translates proportionally.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {MagneticOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} MagneticOptions
|
|
9
|
+
* @property {number} [strength=0.3] Maximum displacement as fraction of element size (0–1)
|
|
10
|
+
* @property {number} [duration=300] Transition duration for return to center (ms)
|
|
11
|
+
*/
|
|
12
|
+
export function magnetic(node, options = {}) {
|
|
13
|
+
$effect(() => {
|
|
14
|
+
const opts = {
|
|
15
|
+
strength: 0.3,
|
|
16
|
+
duration: 300,
|
|
17
|
+
...options
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const reducedMotion =
|
|
21
|
+
typeof window !== 'undefined' &&
|
|
22
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
23
|
+
|
|
24
|
+
if (reducedMotion) return
|
|
25
|
+
|
|
26
|
+
const originalTransform = node.style.transform
|
|
27
|
+
const originalTransition = node.style.transition
|
|
28
|
+
|
|
29
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
30
|
+
|
|
31
|
+
function onMove(e) {
|
|
32
|
+
const rect = node.getBoundingClientRect()
|
|
33
|
+
const centerX = rect.left + rect.width / 2
|
|
34
|
+
const centerY = rect.top + rect.height / 2
|
|
35
|
+
|
|
36
|
+
const offsetX = (e.clientX - centerX) * opts.strength
|
|
37
|
+
const offsetY = (e.clientY - centerY) * opts.strength
|
|
38
|
+
|
|
39
|
+
node.style.transition = 'none'
|
|
40
|
+
node.style.transform = `translate(${offsetX}px, ${offsetY}px)`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onLeave() {
|
|
44
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
45
|
+
node.style.transform = originalTransform
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
node.addEventListener('mousemove', onMove)
|
|
49
|
+
node.addEventListener('mouseleave', onLeave)
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
node.removeEventListener('mousemove', onMove)
|
|
53
|
+
node.removeEventListener('mouseleave', onLeave)
|
|
54
|
+
node.style.transform = originalTransform
|
|
55
|
+
node.style.transition = originalTransition
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigator constants — keyboard actions, key bindings, and typeahead config.
|
|
3
|
+
* These are used by the Navigator class and keymap builder.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ─── Navigator actions ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const ACTIONS = Object.freeze({
|
|
9
|
+
next: 'next',
|
|
10
|
+
prev: 'prev',
|
|
11
|
+
first: 'first',
|
|
12
|
+
last: 'last',
|
|
13
|
+
expand: 'expand',
|
|
14
|
+
collapse: 'collapse',
|
|
15
|
+
select: 'select',
|
|
16
|
+
extend: 'extend',
|
|
17
|
+
range: 'range',
|
|
18
|
+
cancel: 'cancel' // Escape — close dropdown, deselect, or dismiss
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// ─── Keymap: fixed key bindings (orientation-independent) ────────────────────
|
|
22
|
+
|
|
23
|
+
export const PLAIN_FIXED = {
|
|
24
|
+
Enter: ACTIONS.select,
|
|
25
|
+
' ': ACTIONS.select,
|
|
26
|
+
Home: ACTIONS.first,
|
|
27
|
+
End: ACTIONS.last,
|
|
28
|
+
Escape: ACTIONS.cancel
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const CTRL_FIXED = {
|
|
32
|
+
' ': ACTIONS.extend,
|
|
33
|
+
Home: ACTIONS.first,
|
|
34
|
+
End: ACTIONS.last
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SHIFT_FIXED = {
|
|
38
|
+
' ': ACTIONS.range
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Keymap: arrow key assignments per orientation/direction ──────────────────
|
|
42
|
+
|
|
43
|
+
export const ARROWS = {
|
|
44
|
+
'vertical-ltr': {
|
|
45
|
+
move: { ArrowUp: ACTIONS.prev, ArrowDown: ACTIONS.next },
|
|
46
|
+
nested: { ArrowLeft: ACTIONS.collapse, ArrowRight: ACTIONS.expand }
|
|
47
|
+
},
|
|
48
|
+
'vertical-rtl': {
|
|
49
|
+
move: { ArrowUp: ACTIONS.prev, ArrowDown: ACTIONS.next },
|
|
50
|
+
nested: { ArrowRight: ACTIONS.collapse, ArrowLeft: ACTIONS.expand }
|
|
51
|
+
},
|
|
52
|
+
horizontal: {
|
|
53
|
+
move: { ArrowLeft: ACTIONS.prev, ArrowRight: ACTIONS.next },
|
|
54
|
+
nested: { ArrowUp: ACTIONS.collapse, ArrowDown: ACTIONS.expand }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Typeahead ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Milliseconds of inactivity before the typeahead buffer resets. */
|
|
61
|
+
export const TYPEAHEAD_RESET_MS = 500
|