@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.
@@ -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
  }
@@ -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.125",
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.31.3",
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 modifier keys first (highest priority)
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