@rokkit/actions 1.0.0-next.137 → 1.0.0-next.139
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 +1 -13
- package/dist/magnetic.svelte.d.ts +1 -12
- package/dist/reveal.svelte.d.ts +1 -19
- package/dist/ripple.svelte.d.ts +1 -13
- package/dist/trigger.d.ts +0 -23
- package/package.json +1 -1
- package/src/hover-lift.svelte.js +46 -39
- package/src/kbd.js +37 -49
- package/src/keymap.js +17 -9
- package/src/magnetic.svelte.js +39 -35
- package/src/navigator.js +24 -16
- package/src/navigator.svelte.js +95 -82
- package/src/reveal.svelte.js +111 -93
- package/src/ripple.svelte.js +63 -62
- package/src/trigger.js +17 -5
- package/src/utils.js +8 -21
|
@@ -1,16 +1,4 @@
|
|
|
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;
|
|
1
|
+
export function hoverLift(node: any, options?: {}): void;
|
|
14
2
|
/**
|
|
15
3
|
* Hover lift action — adds translateY + elevated shadow on hover.
|
|
16
4
|
* Sets transition on mount, applies transform + box-shadow on mouseenter, resets on mouseleave.
|
|
@@ -1,15 +1,4 @@
|
|
|
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;
|
|
1
|
+
export function magnetic(node: any, options?: {}): void;
|
|
13
2
|
/**
|
|
14
3
|
* Magnetic action — element shifts subtly toward the cursor on hover.
|
|
15
4
|
* Calculates cursor offset from element center and translates proportionally.
|
package/dist/reveal.svelte.d.ts
CHANGED
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Scroll-triggered reveal action using IntersectionObserver.
|
|
3
|
-
* Applies CSS transitions (opacity + translate) when element enters viewport.
|
|
4
|
-
* When stagger > 0, applies reveal to each child element independently.
|
|
5
|
-
*
|
|
6
|
-
* @param {HTMLElement} node
|
|
7
|
-
* @param {RevealOptions} [options]
|
|
8
|
-
*
|
|
9
|
-
* @typedef {Object} RevealOptions
|
|
10
|
-
* @property {'up' | 'down' | 'left' | 'right' | 'none'} [direction='up'] Slide direction
|
|
11
|
-
* @property {string} [distance='1.5rem'] Slide distance (CSS unit)
|
|
12
|
-
* @property {number} [duration=600] Animation duration (ms)
|
|
13
|
-
* @property {number} [delay=0] Delay before animation starts (ms)
|
|
14
|
-
* @property {number} [stagger=0] Delay increment per child in ms (0 = disabled)
|
|
15
|
-
* @property {boolean} [once=true] Only animate once
|
|
16
|
-
* @property {number} [threshold=0.1] IntersectionObserver threshold (0–1)
|
|
17
|
-
* @property {string} [easing='cubic-bezier(0.4, 0, 0.2, 1)'] CSS easing function
|
|
18
|
-
*/
|
|
19
|
-
export function reveal(node: HTMLElement, options?: RevealOptions): void;
|
|
1
|
+
export function reveal(node: any, options?: {}): void;
|
|
20
2
|
/**
|
|
21
3
|
* Scroll-triggered reveal action using IntersectionObserver.
|
|
22
4
|
* Applies CSS transitions (opacity + translate) when element enters viewport.
|
package/dist/ripple.svelte.d.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
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;
|
|
1
|
+
export function ripple(node: any, options?: {}): void;
|
|
14
2
|
/**
|
|
15
3
|
* Ripple action — material-design inspired click ripple effect.
|
|
16
4
|
* Appends a circular expanding span at click coordinates that scales and fades out.
|
package/dist/trigger.d.ts
CHANGED
|
@@ -1,26 +1,3 @@
|
|
|
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
1
|
export class Trigger {
|
|
25
2
|
/**
|
|
26
3
|
* @param {HTMLElement} trigger — the trigger button element
|
package/package.json
CHANGED
package/src/hover-lift.svelte.js
CHANGED
|
@@ -10,46 +10,53 @@
|
|
|
10
10
|
* @property {string} [shadow='0 10px 25px -5px rgba(0,0,0,0.1)'] Box shadow on hover
|
|
11
11
|
* @property {number} [duration=200] Transition duration (ms)
|
|
12
12
|
*/
|
|
13
|
+
|
|
14
|
+
function resolveHoverLiftOpts(options) {
|
|
15
|
+
return {
|
|
16
|
+
distance: '-0.25rem',
|
|
17
|
+
shadow: '0 10px 25px -5px rgba(0,0,0,0.1)',
|
|
18
|
+
duration: 200,
|
|
19
|
+
...options
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isReducedMotion() {
|
|
24
|
+
return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyHoverLift(node, opts) {
|
|
28
|
+
const originalTransform = node.style.transform
|
|
29
|
+
const originalBoxShadow = node.style.boxShadow
|
|
30
|
+
const originalTransition = node.style.transition
|
|
31
|
+
|
|
32
|
+
node.style.transition = `transform ${opts.duration}ms ease, box-shadow ${opts.duration}ms ease`
|
|
33
|
+
|
|
34
|
+
function onEnter() {
|
|
35
|
+
node.style.transform = `translateY(${opts.distance})`
|
|
36
|
+
node.style.boxShadow = opts.shadow
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function onLeave() {
|
|
40
|
+
node.style.transform = originalTransform
|
|
41
|
+
node.style.boxShadow = originalBoxShadow
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
node.addEventListener('mouseenter', onEnter)
|
|
45
|
+
node.addEventListener('mouseleave', onLeave)
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
node.removeEventListener('mouseenter', onEnter)
|
|
49
|
+
node.removeEventListener('mouseleave', onLeave)
|
|
50
|
+
node.style.transform = originalTransform
|
|
51
|
+
node.style.boxShadow = originalBoxShadow
|
|
52
|
+
node.style.transition = originalTransition
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
export function hoverLift(node, options = {}) {
|
|
14
57
|
$effect(() => {
|
|
15
|
-
const opts =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
duration: 200,
|
|
19
|
-
...options
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const reducedMotion =
|
|
23
|
-
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
24
|
-
|
|
25
|
-
if (reducedMotion) return
|
|
26
|
-
|
|
27
|
-
// Store original values
|
|
28
|
-
const originalTransform = node.style.transform
|
|
29
|
-
const originalBoxShadow = node.style.boxShadow
|
|
30
|
-
const originalTransition = node.style.transition
|
|
31
|
-
|
|
32
|
-
node.style.transition = `transform ${opts.duration}ms ease, box-shadow ${opts.duration}ms ease`
|
|
33
|
-
|
|
34
|
-
function onEnter() {
|
|
35
|
-
node.style.transform = `translateY(${opts.distance})`
|
|
36
|
-
node.style.boxShadow = opts.shadow
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function onLeave() {
|
|
40
|
-
node.style.transform = originalTransform
|
|
41
|
-
node.style.boxShadow = originalBoxShadow
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
node.addEventListener('mouseenter', onEnter)
|
|
45
|
-
node.addEventListener('mouseleave', onLeave)
|
|
46
|
-
|
|
47
|
-
return () => {
|
|
48
|
-
node.removeEventListener('mouseenter', onEnter)
|
|
49
|
-
node.removeEventListener('mouseleave', onLeave)
|
|
50
|
-
node.style.transform = originalTransform
|
|
51
|
-
node.style.boxShadow = originalBoxShadow
|
|
52
|
-
node.style.transition = originalTransition
|
|
53
|
-
}
|
|
58
|
+
const opts = resolveHoverLiftOpts(options)
|
|
59
|
+
if (isReducedMotion()) return
|
|
60
|
+
return applyHoverLift(node, opts)
|
|
54
61
|
})
|
|
55
62
|
}
|
package/src/kbd.js
CHANGED
|
@@ -93,6 +93,23 @@ export function getKeyboardActions(options, handlers) {
|
|
|
93
93
|
return { ...common, ...movement, ...expandCollapse }
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function buildHorizontalMovementMap(dir) {
|
|
97
|
+
return dir === 'rtl'
|
|
98
|
+
? { ArrowRight: 'previous', ArrowLeft: 'next' }
|
|
99
|
+
: { ArrowLeft: 'previous', ArrowRight: 'next' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildVerticalNestedMap(dir) {
|
|
103
|
+
return dir === 'rtl'
|
|
104
|
+
? { ArrowRight: 'collapse', ArrowLeft: 'expand' }
|
|
105
|
+
: { ArrowLeft: 'collapse', ArrowRight: 'expand' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildNestedActions(isHorizontal, dir) {
|
|
109
|
+
if (isHorizontal) return { ArrowUp: 'collapse', ArrowDown: 'expand' }
|
|
110
|
+
return buildVerticalNestedMap(dir)
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
/**
|
|
97
114
|
* Creates a keyboard action mapping based on navigation options
|
|
98
115
|
*
|
|
@@ -106,31 +123,12 @@ export function createKeyboardActionMap(options) {
|
|
|
106
123
|
const { orientation, dir, nested } = options
|
|
107
124
|
const isHorizontal = orientation === 'horizontal'
|
|
108
125
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
movementActions =
|
|
113
|
-
dir === 'rtl'
|
|
114
|
-
? { ArrowRight: 'previous', ArrowLeft: 'next' }
|
|
115
|
-
: { ArrowLeft: 'previous', ArrowRight: 'next' }
|
|
116
|
-
} else {
|
|
117
|
-
movementActions = { ArrowUp: 'previous', ArrowDown: 'next' }
|
|
118
|
-
}
|
|
126
|
+
const movementActions = isHorizontal
|
|
127
|
+
? buildHorizontalMovementMap(dir)
|
|
128
|
+
: { ArrowUp: 'previous', ArrowDown: 'next' }
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
let nestedActions = {}
|
|
122
|
-
if (nested) {
|
|
123
|
-
if (isHorizontal) {
|
|
124
|
-
nestedActions = { ArrowUp: 'collapse', ArrowDown: 'expand' }
|
|
125
|
-
} else {
|
|
126
|
-
nestedActions =
|
|
127
|
-
dir === 'rtl'
|
|
128
|
-
? { ArrowRight: 'collapse', ArrowLeft: 'expand' }
|
|
129
|
-
: { ArrowLeft: 'collapse', ArrowRight: 'expand' }
|
|
130
|
-
}
|
|
131
|
-
}
|
|
130
|
+
const nestedActions = nested ? buildNestedActions(isHorizontal, dir) : {}
|
|
132
131
|
|
|
133
|
-
// Common actions regardless of options
|
|
134
132
|
const commonActions = {
|
|
135
133
|
Enter: 'select',
|
|
136
134
|
' ': 'select',
|
|
@@ -138,12 +136,7 @@ export function createKeyboardActionMap(options) {
|
|
|
138
136
|
End: 'last'
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
...commonActions,
|
|
144
|
-
...movementActions,
|
|
145
|
-
...nestedActions
|
|
146
|
-
}
|
|
139
|
+
return { ...commonActions, ...movementActions, ...nestedActions }
|
|
147
140
|
}
|
|
148
141
|
|
|
149
142
|
/**
|
|
@@ -171,6 +164,19 @@ export function createShiftKeyboardActionMap() {
|
|
|
171
164
|
return { ' ': 'range' }
|
|
172
165
|
}
|
|
173
166
|
|
|
167
|
+
const KEY_LAYER_RESOLVERS = {
|
|
168
|
+
shift: (key, _opts) => createShiftKeyboardActionMap()[key] || null,
|
|
169
|
+
modifier: (key, opts) => createModifierKeyboardActionMap(opts)[key] || null,
|
|
170
|
+
plain: (key, opts) => createKeyboardActionMap(opts)[key] || null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getKeyLayer(ctrlKey, metaKey, shiftKey) {
|
|
174
|
+
if (ctrlKey) return 'modifier'
|
|
175
|
+
if (metaKey) return 'modifier'
|
|
176
|
+
if (shiftKey) return 'shift'
|
|
177
|
+
return 'plain'
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
/**
|
|
175
181
|
* Gets the keyboard action for a key event
|
|
176
182
|
* @param {KeyboardEvent} event - The keyboard event
|
|
@@ -179,25 +185,7 @@ export function createShiftKeyboardActionMap() {
|
|
|
179
185
|
*/
|
|
180
186
|
export function getKeyboardAction(event, options = {}) {
|
|
181
187
|
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
182
|
-
|
|
183
|
-
// Use updated options with defaults
|
|
184
188
|
const mergedOptions = { ...defaultNavigationOptions, ...options }
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (shiftKey && !ctrlKey && !metaKey) {
|
|
188
|
-
const shiftMap = createShiftKeyboardActionMap()
|
|
189
|
-
return shiftMap[key] || null
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Check for modifier keys (ctrl/cmd)
|
|
193
|
-
if (ctrlKey || metaKey) {
|
|
194
|
-
const modifierMap = createModifierKeyboardActionMap(mergedOptions)
|
|
195
|
-
return modifierMap[key] || null
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get the action map based on options
|
|
199
|
-
const actionMap = createKeyboardActionMap(mergedOptions)
|
|
200
|
-
|
|
201
|
-
// Return the action or null if no matching key
|
|
202
|
-
return actionMap[key] || null
|
|
189
|
+
const layer = getKeyLayer(ctrlKey, metaKey, shiftKey)
|
|
190
|
+
return KEY_LAYER_RESOLVERS[layer](key, mergedOptions)
|
|
203
191
|
}
|
package/src/keymap.js
CHANGED
|
@@ -34,6 +34,14 @@ function getArrows(orientation, dir) {
|
|
|
34
34
|
return ARROWS[`vertical-${dir}`]
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function buildPlainLayer(arrows, collapsible) {
|
|
38
|
+
return {
|
|
39
|
+
...PLAIN_FIXED,
|
|
40
|
+
...arrows.move,
|
|
41
|
+
...(collapsible ? arrows.nested : {})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
// ─── buildKeymap ──────────────────────────────────────────────────────────────
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -52,11 +60,7 @@ export function buildKeymap({ orientation = 'vertical', dir = 'ltr', collapsible
|
|
|
52
60
|
const arrows = getArrows(orientation, dir)
|
|
53
61
|
|
|
54
62
|
return {
|
|
55
|
-
plain:
|
|
56
|
-
...PLAIN_FIXED,
|
|
57
|
-
...arrows.move,
|
|
58
|
-
...(collapsible ? arrows.nested : {})
|
|
59
|
-
},
|
|
63
|
+
plain: buildPlainLayer(arrows, collapsible),
|
|
60
64
|
shift: { ...SHIFT_FIXED },
|
|
61
65
|
ctrl: { ...CTRL_FIXED }
|
|
62
66
|
}
|
|
@@ -64,6 +68,13 @@ export function buildKeymap({ orientation = 'vertical', dir = 'ltr', collapsible
|
|
|
64
68
|
|
|
65
69
|
// ─── resolveAction ────────────────────────────────────────────────────────────
|
|
66
70
|
|
|
71
|
+
function pickLayer(shiftKey, ctrlKey, metaKey) {
|
|
72
|
+
if (ctrlKey) return 'ctrl'
|
|
73
|
+
if (metaKey) return 'ctrl'
|
|
74
|
+
if (shiftKey) return 'shift'
|
|
75
|
+
return 'plain'
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
/**
|
|
68
79
|
* Resolve the action for a keyboard event given a pre-built keymap.
|
|
69
80
|
* Returns null if the key has no binding.
|
|
@@ -74,8 +85,5 @@ export function buildKeymap({ orientation = 'vertical', dir = 'ltr', collapsible
|
|
|
74
85
|
*/
|
|
75
86
|
export function resolveAction(event, keymap) {
|
|
76
87
|
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
|
|
88
|
+
return keymap[pickLayer(shiftKey, ctrlKey, metaKey)][key] ?? null
|
|
81
89
|
}
|
package/src/magnetic.svelte.js
CHANGED
|
@@ -9,49 +9,53 @@
|
|
|
9
9
|
* @property {number} [strength=0.3] Maximum displacement as fraction of element size (0–1)
|
|
10
10
|
* @property {number} [duration=300] Transition duration for return to center (ms)
|
|
11
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' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
function resolveMagneticOpts(options) {
|
|
14
|
+
return { strength: 0.3, duration: 300, ...options }
|
|
15
|
+
}
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
function isReducedMotion() {
|
|
18
|
+
return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
19
|
+
}
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
function applyMagnetic(node, opts) {
|
|
22
|
+
const originalTransform = node.style.transform
|
|
23
|
+
const originalTransition = node.style.transition
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
const rect = node.getBoundingClientRect()
|
|
32
|
-
const centerX = rect.left + rect.width / 2
|
|
33
|
-
const centerY = rect.top + rect.height / 2
|
|
25
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
function onMove(e) {
|
|
28
|
+
const rect = node.getBoundingClientRect()
|
|
29
|
+
const centerX = rect.left + rect.width / 2
|
|
30
|
+
const centerY = rect.top + rect.height / 2
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
32
|
+
const offsetX = (e.clientX - centerX) * opts.strength
|
|
33
|
+
const offsetY = (e.clientY - centerY) * opts.strength
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
35
|
+
node.style.transition = 'none'
|
|
36
|
+
node.style.transform = `translate(${offsetX}px, ${offsetY}px)`
|
|
37
|
+
}
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
node.
|
|
39
|
+
function onLeave() {
|
|
40
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
41
|
+
node.style.transform = originalTransform
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
node.addEventListener('mousemove', onMove)
|
|
45
|
+
node.addEventListener('mouseleave', onLeave)
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
node.removeEventListener('mousemove', onMove)
|
|
49
|
+
node.removeEventListener('mouseleave', onLeave)
|
|
50
|
+
node.style.transform = originalTransform
|
|
51
|
+
node.style.transition = originalTransition
|
|
52
|
+
}
|
|
53
|
+
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
55
|
+
export function magnetic(node, options = {}) {
|
|
56
|
+
$effect(() => {
|
|
57
|
+
const opts = resolveMagneticOpts(options)
|
|
58
|
+
if (isReducedMotion()) return
|
|
59
|
+
return applyMagnetic(node, opts)
|
|
56
60
|
})
|
|
57
61
|
}
|
package/src/navigator.js
CHANGED
|
@@ -38,9 +38,9 @@ import { buildKeymap, resolveAction } from './keymap.js'
|
|
|
38
38
|
*/
|
|
39
39
|
function getClickAction(event) {
|
|
40
40
|
const { shiftKey, ctrlKey, metaKey, target } = event
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
if (
|
|
41
|
+
if (shiftKey) return 'range'
|
|
42
|
+
if (ctrlKey) return 'extend'
|
|
43
|
+
if (metaKey) return 'extend'
|
|
44
44
|
if (target.closest('[data-accordion-trigger]')) return 'toggle'
|
|
45
45
|
return 'select'
|
|
46
46
|
}
|
|
@@ -201,6 +201,25 @@ export class Navigator {
|
|
|
201
201
|
|
|
202
202
|
// ─── Typeahead ───────────────────────────────────────────────────────────
|
|
203
203
|
|
|
204
|
+
#isPrintableKey(key, ctrlKey, metaKey, altKey) {
|
|
205
|
+
if (ctrlKey) return false
|
|
206
|
+
if (metaKey) return false
|
|
207
|
+
if (altKey) return false
|
|
208
|
+
if (key.length !== 1) return false
|
|
209
|
+
return key !== ' '
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#appendBuffer(key) {
|
|
213
|
+
const startAfter = this.#buffer.length === 0 ? this.#wrapper.focusedKey : null
|
|
214
|
+
this.#buffer += key
|
|
215
|
+
if (this.#bufferTimer) {
|
|
216
|
+
clearTimeout(this.#bufferTimer)
|
|
217
|
+
this.#bufferTimer = null
|
|
218
|
+
}
|
|
219
|
+
this.#bufferTimer = setTimeout(() => this.#clearTypeahead(), TYPEAHEAD_RESET_MS)
|
|
220
|
+
return startAfter
|
|
221
|
+
}
|
|
222
|
+
|
|
204
223
|
/**
|
|
205
224
|
* Handle printable character keys for typeahead search.
|
|
206
225
|
* Returns true if the event was consumed.
|
|
@@ -211,20 +230,9 @@ export class Navigator {
|
|
|
211
230
|
#tryTypeahead(event) {
|
|
212
231
|
const { key, ctrlKey, metaKey, altKey } = event
|
|
213
232
|
|
|
214
|
-
|
|
215
|
-
if (ctrlKey || metaKey || altKey) return false
|
|
216
|
-
if (key.length !== 1) return false
|
|
217
|
-
if (key === ' ') return false // Space is a keymap action, not typeahead
|
|
218
|
-
|
|
219
|
-
const startAfter = this.#buffer.length === 0 ? this.#wrapper.focusedKey : null
|
|
220
|
-
this.#buffer += key
|
|
233
|
+
if (!this.#isPrintableKey(key, ctrlKey, metaKey, altKey)) return false
|
|
221
234
|
|
|
222
|
-
|
|
223
|
-
if (this.#bufferTimer) {
|
|
224
|
-
clearTimeout(this.#bufferTimer)
|
|
225
|
-
this.#bufferTimer = null
|
|
226
|
-
}
|
|
227
|
-
this.#bufferTimer = setTimeout(() => this.#clearTypeahead(), TYPEAHEAD_RESET_MS)
|
|
235
|
+
const startAfter = this.#appendBuffer(key)
|
|
228
236
|
|
|
229
237
|
const matchKey = this.#wrapper.findByText(this.#buffer, startAfter)
|
|
230
238
|
if (matchKey !== null) {
|
package/src/navigator.svelte.js
CHANGED
|
@@ -99,6 +99,98 @@ function getHandlers(wrapper) {
|
|
|
99
99
|
toggle: (path) => wrapper.toggleExpansion(path)
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
function isTypeaheadEvent(event) {
|
|
104
|
+
if (event.ctrlKey) return false
|
|
105
|
+
if (event.metaKey) return false
|
|
106
|
+
if (event.altKey) return false
|
|
107
|
+
if (event.key.length !== 1) return false
|
|
108
|
+
return event.key !== ' '
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function dispatchFocusMoveEvent(node, wrapper) {
|
|
112
|
+
node.dispatchEvent(
|
|
113
|
+
new CustomEvent('action', {
|
|
114
|
+
detail: {
|
|
115
|
+
name: 'move',
|
|
116
|
+
data: { value: wrapper.focused, selected: wrapper.selected }
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const SCROLL_ACTIONS = new Set(['first', 'last', 'previous', 'next', 'expand', 'collapse'])
|
|
123
|
+
|
|
124
|
+
function isNativeLink(action, target) {
|
|
125
|
+
return action === 'select' && Boolean(target.closest('a[href]'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function notifyFocusMove(node, wrapper, action, prevKey) {
|
|
129
|
+
if (action !== 'expand' && action !== 'collapse') return false
|
|
130
|
+
if (wrapper.focusedKey === prevKey) return false
|
|
131
|
+
dispatchFocusMoveEvent(node, wrapper)
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runTypeahead(config, wrapper, ta, event) {
|
|
136
|
+
if (!config.typeahead) return
|
|
137
|
+
if (!wrapper.findByText) return
|
|
138
|
+
ta.handle(event)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function makeKeydownHandler(ctx, config, handlers, ta) {
|
|
142
|
+
const { node, wrapper } = ctx
|
|
143
|
+
return (event) => {
|
|
144
|
+
const action = getKeyboardAction(event, config)
|
|
145
|
+
if (isNativeLink(action, event.target)) return
|
|
146
|
+
|
|
147
|
+
const prevKey = wrapper.focusedKey
|
|
148
|
+
const handled = handleAction(event, handlers[action])
|
|
149
|
+
if (!handled) { runTypeahead(config, wrapper, ta, event); return }
|
|
150
|
+
|
|
151
|
+
ta.reset()
|
|
152
|
+
emitAction(node, wrapper, action, true)
|
|
153
|
+
notifyFocusMove(node, wrapper, action, prevKey)
|
|
154
|
+
if (SCROLL_ACTIONS.has(action)) setTimeout(() => scrollFocusedIntoView(node, wrapper), 0)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getTypeaheadStart(buffer, wrapper) {
|
|
159
|
+
if (buffer.length === 0) return wrapper.focusedKey
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function makeTypeahead(node, wrapper) {
|
|
164
|
+
let buffer = ''
|
|
165
|
+
let timer = null
|
|
166
|
+
|
|
167
|
+
function reset() {
|
|
168
|
+
buffer = ''
|
|
169
|
+
if (timer) { clearTimeout(timer); timer = null }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handle(event) {
|
|
173
|
+
if (!isTypeaheadEvent(event)) return false
|
|
174
|
+
|
|
175
|
+
const startAfter = getTypeaheadStart(buffer, wrapper)
|
|
176
|
+
buffer += event.key
|
|
177
|
+
if (timer) clearTimeout(timer)
|
|
178
|
+
timer = setTimeout(reset, 500)
|
|
179
|
+
|
|
180
|
+
const matchKey = wrapper.findByText(buffer, startAfter)
|
|
181
|
+
if (matchKey === null) return false
|
|
182
|
+
if (!wrapper.moveTo(matchKey)) return false
|
|
183
|
+
|
|
184
|
+
event.preventDefault()
|
|
185
|
+
event.stopPropagation()
|
|
186
|
+
emitAction(node, wrapper, 'first', true)
|
|
187
|
+
setTimeout(() => scrollFocusedIntoView(node, wrapper), 0)
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { reset, handle }
|
|
192
|
+
}
|
|
193
|
+
|
|
102
194
|
/**
|
|
103
195
|
* A svelte action function that captures keyboard evvents and emits event for corresponding movements.
|
|
104
196
|
*
|
|
@@ -110,88 +202,13 @@ export function navigator(node, options) {
|
|
|
110
202
|
const { wrapper } = options
|
|
111
203
|
const config = { ...defaultNavigationOptions, ...omit(['wrapper'], options) }
|
|
112
204
|
const handlers = getHandlers(wrapper)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let typeaheadBuffer = ''
|
|
116
|
-
let typeaheadTimer = null
|
|
117
|
-
|
|
118
|
-
function resetTypeahead() {
|
|
119
|
-
typeaheadBuffer = ''
|
|
120
|
-
if (typeaheadTimer) {
|
|
121
|
-
clearTimeout(typeaheadTimer)
|
|
122
|
-
typeaheadTimer = null
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function handleTypeahead(event) {
|
|
127
|
-
const { key, ctrlKey, metaKey, altKey } = event
|
|
128
|
-
if (ctrlKey || metaKey || altKey) return false
|
|
129
|
-
if (key.length !== 1 || key === ' ') return false
|
|
130
|
-
|
|
131
|
-
// Single-char repeat: start after current to cycle through matches
|
|
132
|
-
const startAfter = typeaheadBuffer.length === 0 ? wrapper.focusedKey : null
|
|
133
|
-
|
|
134
|
-
typeaheadBuffer += key
|
|
135
|
-
if (typeaheadTimer) clearTimeout(typeaheadTimer)
|
|
136
|
-
typeaheadTimer = setTimeout(resetTypeahead, 500)
|
|
137
|
-
|
|
138
|
-
const matchKey = wrapper.findByText(typeaheadBuffer, startAfter)
|
|
139
|
-
if (matchKey !== null && wrapper.moveTo(matchKey)) {
|
|
140
|
-
event.preventDefault()
|
|
141
|
-
event.stopPropagation()
|
|
142
|
-
emitAction(node, wrapper, 'first', true) // emit 'move'
|
|
143
|
-
setTimeout(() => scrollFocusedIntoView(node, wrapper), 0)
|
|
144
|
-
return true
|
|
145
|
-
}
|
|
146
|
-
return false
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const handleKeydown = (event) => {
|
|
150
|
-
const action = getKeyboardAction(event, config)
|
|
151
|
-
const prevKey = wrapper.focusedKey
|
|
152
|
-
|
|
153
|
-
// For activation keys (Enter/Space) on anchor elements, let the browser
|
|
154
|
-
// navigate natively. The click handler will update controller state when
|
|
155
|
-
// the browser fires the synthetic click.
|
|
156
|
-
if (action === 'select' && event.target.closest('a[href]')) {
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const handled = handleAction(event, handlers[action])
|
|
161
|
-
if (handled) {
|
|
162
|
-
resetTypeahead()
|
|
163
|
-
emitAction(node, wrapper, action, true)
|
|
164
|
-
// If expand/collapse moved focus, also emit move so components update DOM focus
|
|
165
|
-
const focusMoved = ['expand', 'collapse'].includes(action) && wrapper.focusedKey !== prevKey
|
|
166
|
-
if (focusMoved) {
|
|
167
|
-
node.dispatchEvent(
|
|
168
|
-
new CustomEvent('action', {
|
|
169
|
-
detail: {
|
|
170
|
-
name: 'move',
|
|
171
|
-
data: { value: wrapper.focused, selected: wrapper.selected }
|
|
172
|
-
}
|
|
173
|
-
})
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
// Scroll focused element into view for navigation and focus-moving expand/collapse
|
|
177
|
-
if (focusMoved || ['first', 'last', 'previous', 'next'].includes(action)) {
|
|
178
|
-
setTimeout(() => scrollFocusedIntoView(node, wrapper), 0)
|
|
179
|
-
}
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Type-ahead: when no navigation action matched and typeahead is enabled
|
|
184
|
-
if (config.typeahead && wrapper.findByText) {
|
|
185
|
-
handleTypeahead(event)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
205
|
+
const ta = makeTypeahead(node, wrapper)
|
|
206
|
+
const handleKeydown = makeKeydownHandler({ node, wrapper }, config, handlers, ta)
|
|
188
207
|
|
|
189
208
|
const handleClick = (event) => {
|
|
190
209
|
const action = getClickAction(event)
|
|
191
210
|
const path = getPathFromEvent(event)
|
|
192
211
|
|
|
193
|
-
// Anchor elements with href handle navigation natively — don't preventDefault.
|
|
194
|
-
// Still call the handler so focus/selection state stays in sync.
|
|
195
212
|
if (event.target.closest('a[href]')) {
|
|
196
213
|
const handler = handlers[action]
|
|
197
214
|
if (handler?.(path)) emitAction(node, options.wrapper, action)
|
|
@@ -204,10 +221,6 @@ export function navigator(node, options) {
|
|
|
204
221
|
|
|
205
222
|
$effect(() => {
|
|
206
223
|
const cleanup = [on(node, 'keydown', handleKeydown), on(node, 'click', handleClick)]
|
|
207
|
-
|
|
208
|
-
return () => {
|
|
209
|
-
resetTypeahead()
|
|
210
|
-
cleanup.forEach((fn) => fn())
|
|
211
|
-
}
|
|
224
|
+
return () => { ta.reset(); cleanup.forEach((fn) => fn()) }
|
|
212
225
|
})
|
|
213
226
|
}
|
package/src/reveal.svelte.js
CHANGED
|
@@ -16,114 +16,132 @@
|
|
|
16
16
|
* @property {number} [threshold=0.1] IntersectionObserver threshold (0–1)
|
|
17
17
|
* @property {string} [easing='cubic-bezier(0.4, 0, 0.2, 1)'] CSS easing function
|
|
18
18
|
*/
|
|
19
|
-
export function reveal(node, options = {}) {
|
|
20
|
-
$effect(() => {
|
|
21
|
-
const opts = {
|
|
22
|
-
direction: 'up',
|
|
23
|
-
distance: '1.5rem',
|
|
24
|
-
duration: 600,
|
|
25
|
-
delay: 0,
|
|
26
|
-
stagger: 0,
|
|
27
|
-
once: true,
|
|
28
|
-
threshold: 0.1,
|
|
29
|
-
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
30
|
-
...options
|
|
31
|
-
}
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
function resolveRevealOpts(options) {
|
|
21
|
+
return {
|
|
22
|
+
direction: 'up',
|
|
23
|
+
distance: '1.5rem',
|
|
24
|
+
duration: 600,
|
|
25
|
+
delay: 0,
|
|
26
|
+
stagger: 0,
|
|
27
|
+
once: true,
|
|
28
|
+
threshold: 0.1,
|
|
29
|
+
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
30
|
+
...options
|
|
31
|
+
}
|
|
32
|
+
}
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
function applyReveal(el, opts) {
|
|
35
|
+
el.style.setProperty('--reveal-duration', `${opts.duration}ms`)
|
|
36
|
+
el.style.setProperty('--reveal-distance', opts.distance)
|
|
37
|
+
el.style.setProperty('--reveal-easing', opts.easing)
|
|
38
|
+
el.setAttribute('data-reveal', opts.direction)
|
|
39
|
+
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function cleanReveal(el) {
|
|
42
|
+
el.removeAttribute('data-reveal')
|
|
43
|
+
el.removeAttribute('data-reveal-visible')
|
|
44
|
+
el.style.removeProperty('--reveal-duration')
|
|
45
|
+
el.style.removeProperty('--reveal-distance')
|
|
46
|
+
el.style.removeProperty('--reveal-easing')
|
|
47
|
+
el.style.removeProperty('transition-delay')
|
|
48
|
+
}
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
function initRevealElements(node, opts, isStagger) {
|
|
51
|
+
if (isStagger) {
|
|
52
|
+
Array.from(node.children).forEach((child) => applyReveal(child, opts))
|
|
53
|
+
} else {
|
|
54
|
+
applyReveal(node, opts)
|
|
55
|
+
if (opts.delay > 0) node.style.transitionDelay = `${opts.delay}ms`
|
|
56
|
+
}
|
|
57
|
+
}
|
|
53
58
|
|
|
59
|
+
function handleReducedMotion(node, isStagger) {
|
|
60
|
+
if (isStagger) {
|
|
61
|
+
Array.from(node.children).forEach((child) => child.setAttribute('data-reveal-visible', ''))
|
|
62
|
+
} else {
|
|
63
|
+
node.setAttribute('data-reveal-visible', '')
|
|
64
|
+
}
|
|
65
|
+
node.dispatchEvent(new CustomEvent('reveal', { detail: { visible: true } }))
|
|
66
|
+
return () => {
|
|
54
67
|
if (isStagger) {
|
|
55
|
-
Array.from(node.children).forEach((child) =>
|
|
68
|
+
Array.from(node.children).forEach((child) => cleanReveal(child))
|
|
56
69
|
} else {
|
|
57
|
-
|
|
58
|
-
if (opts.delay > 0) {
|
|
59
|
-
node.style.transitionDelay = `${opts.delay}ms`
|
|
60
|
-
}
|
|
70
|
+
cleanReveal(node)
|
|
61
71
|
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
function handleIntersectEnter(node, opts, isStagger, timers) {
|
|
76
|
+
if (isStagger) {
|
|
77
|
+
timers.forEach((t) => clearTimeout(t))
|
|
78
|
+
const kids = Array.from(node.children)
|
|
79
|
+
const newTimers = kids.map((child, i) => {
|
|
80
|
+
if (!child.hasAttribute('data-reveal')) applyReveal(child, opts)
|
|
81
|
+
return setTimeout(
|
|
82
|
+
() => child.setAttribute('data-reveal-visible', ''),
|
|
83
|
+
opts.delay + i * opts.stagger
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
return newTimers
|
|
87
|
+
}
|
|
88
|
+
node.setAttribute('data-reveal-visible', '')
|
|
89
|
+
return timers
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleIntersectLeave(node, isStagger, timers) {
|
|
93
|
+
if (isStagger) {
|
|
94
|
+
timers.forEach((t) => clearTimeout(t))
|
|
95
|
+
Array.from(node.children).forEach((child) => child.removeAttribute('data-reveal-visible'))
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
node.removeAttribute('data-reveal-visible')
|
|
99
|
+
return timers
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildObserver(node, opts, isStagger) {
|
|
103
|
+
let timers = []
|
|
104
|
+
|
|
105
|
+
const observer = new IntersectionObserver(
|
|
106
|
+
(entries) => {
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.isIntersecting) {
|
|
109
|
+
timers = handleIntersectEnter(node, opts, isStagger, timers)
|
|
110
|
+
node.dispatchEvent(new CustomEvent('reveal', { detail: { visible: true } }))
|
|
111
|
+
if (opts.once) observer.unobserve(node)
|
|
112
|
+
} else if (!opts.once) {
|
|
113
|
+
timers = handleIntersectLeave(node, isStagger, timers)
|
|
114
|
+
node.dispatchEvent(new CustomEvent('reveal', { detail: { visible: false } }))
|
|
75
115
|
}
|
|
76
116
|
}
|
|
117
|
+
},
|
|
118
|
+
{ threshold: opts.threshold }
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
observer.observe(node)
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
timers.forEach((t) => clearTimeout(t))
|
|
125
|
+
observer.disconnect()
|
|
126
|
+
if (isStagger) {
|
|
127
|
+
Array.from(node.children).forEach((child) => cleanReveal(child))
|
|
128
|
+
} else {
|
|
129
|
+
cleanReveal(node)
|
|
77
130
|
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
78
133
|
|
|
79
|
-
|
|
134
|
+
export function reveal(node, options = {}) {
|
|
135
|
+
$effect(() => {
|
|
136
|
+
const opts = resolveRevealOpts(options)
|
|
137
|
+
const reducedMotion =
|
|
138
|
+
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
139
|
+
const isStagger = opts.stagger > 0
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
(entries) => {
|
|
83
|
-
for (const entry of entries) {
|
|
84
|
-
if (entry.isIntersecting) {
|
|
85
|
-
if (isStagger) {
|
|
86
|
-
timers.forEach((t) => clearTimeout(t))
|
|
87
|
-
const kids = Array.from(node.children)
|
|
88
|
-
timers = kids.map((child, i) => {
|
|
89
|
-
if (!child.hasAttribute('data-reveal')) applyReveal(child)
|
|
90
|
-
return setTimeout(
|
|
91
|
-
() => child.setAttribute('data-reveal-visible', ''),
|
|
92
|
-
opts.delay + i * opts.stagger
|
|
93
|
-
)
|
|
94
|
-
})
|
|
95
|
-
} else {
|
|
96
|
-
node.setAttribute('data-reveal-visible', '')
|
|
97
|
-
}
|
|
98
|
-
node.dispatchEvent(new CustomEvent('reveal', { detail: { visible: true } }))
|
|
99
|
-
if (opts.once) observer.unobserve(node)
|
|
100
|
-
} else if (!opts.once) {
|
|
101
|
-
if (isStagger) {
|
|
102
|
-
timers.forEach((t) => clearTimeout(t))
|
|
103
|
-
timers = []
|
|
104
|
-
Array.from(node.children).forEach((child) =>
|
|
105
|
-
child.removeAttribute('data-reveal-visible')
|
|
106
|
-
)
|
|
107
|
-
} else {
|
|
108
|
-
node.removeAttribute('data-reveal-visible')
|
|
109
|
-
}
|
|
110
|
-
node.dispatchEvent(new CustomEvent('reveal', { detail: { visible: false } }))
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
{ threshold: opts.threshold }
|
|
115
|
-
)
|
|
141
|
+
initRevealElements(node, opts, isStagger)
|
|
116
142
|
|
|
117
|
-
|
|
143
|
+
if (reducedMotion) return handleReducedMotion(node, isStagger)
|
|
118
144
|
|
|
119
|
-
return (
|
|
120
|
-
timers.forEach((t) => clearTimeout(t))
|
|
121
|
-
observer.disconnect()
|
|
122
|
-
if (isStagger) {
|
|
123
|
-
Array.from(node.children).forEach((child) => cleanReveal(child))
|
|
124
|
-
} else {
|
|
125
|
-
cleanReveal(node)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
145
|
+
return buildObserver(node, opts, isStagger)
|
|
128
146
|
})
|
|
129
147
|
}
|
package/src/ripple.svelte.js
CHANGED
|
@@ -10,78 +10,79 @@
|
|
|
10
10
|
* @property {number} [opacity=0.15] Ripple opacity
|
|
11
11
|
* @property {number} [duration=500] Ripple animation duration (ms)
|
|
12
12
|
*/
|
|
13
|
-
export function ripple(node, options = {}) {
|
|
14
|
-
$effect(() => {
|
|
15
|
-
const opts = {
|
|
16
|
-
color: 'currentColor',
|
|
17
|
-
opacity: 0.15,
|
|
18
|
-
duration: 500,
|
|
19
|
-
...options
|
|
20
|
-
}
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
function resolveRippleOpts(options) {
|
|
15
|
+
return { color: 'currentColor', opacity: 0.15, duration: 500, ...options }
|
|
16
|
+
}
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
function isReducedMotion() {
|
|
19
|
+
return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
20
|
+
}
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
function injectRippleKeyframes() {
|
|
23
|
+
if (document.querySelector('#rokkit-ripple-keyframes')) return
|
|
24
|
+
const style = document.createElement('style')
|
|
25
|
+
style.id = 'rokkit-ripple-keyframes'
|
|
26
|
+
style.textContent = `
|
|
27
|
+
@keyframes rokkit-ripple {
|
|
28
|
+
to {
|
|
29
|
+
transform: scale(1);
|
|
30
|
+
opacity: 0;
|
|
31
|
+
}
|
|
33
32
|
}
|
|
34
|
-
|
|
33
|
+
`
|
|
34
|
+
document.head.appendChild(style)
|
|
35
|
+
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
function createRippleSpan(e, node, opts) {
|
|
38
|
+
const rect = node.getBoundingClientRect()
|
|
39
|
+
const size = Math.max(rect.width, rect.height) * 2
|
|
40
|
+
const x = e.clientX - rect.left - size / 2
|
|
41
|
+
const y = e.clientY - rect.top - size / 2
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
const span = document.createElement('span')
|
|
44
|
+
span.style.position = 'absolute'
|
|
45
|
+
span.style.left = `${x}px`
|
|
46
|
+
span.style.top = `${y}px`
|
|
47
|
+
span.style.width = `${size}px`
|
|
48
|
+
span.style.height = `${size}px`
|
|
49
|
+
span.style.borderRadius = '50%'
|
|
50
|
+
span.style.background = opts.color
|
|
51
|
+
span.style.opacity = String(opts.opacity)
|
|
52
|
+
span.style.transform = 'scale(0)'
|
|
53
|
+
span.style.pointerEvents = 'none'
|
|
54
|
+
span.style.animation = `rokkit-ripple ${opts.duration}ms ease-out forwards`
|
|
55
|
+
return span
|
|
56
|
+
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
function applyRipple(node, opts) {
|
|
59
|
+
const originalOverflow = node.style.overflow
|
|
60
|
+
const originalPosition = node.style.position
|
|
61
|
+
const computed = getComputedStyle(node).position
|
|
62
|
+
if (!computed || computed === 'static') node.style.position = 'relative'
|
|
63
|
+
node.style.overflow = 'hidden'
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
65
|
+
function onClick(e) {
|
|
66
|
+
const span = createRippleSpan(e, node, opts)
|
|
67
|
+
node.appendChild(span)
|
|
68
|
+
span.addEventListener('animationend', () => span.remove(), { once: true })
|
|
69
|
+
setTimeout(() => { if (span.parentNode) span.remove() }, opts.duration + 100)
|
|
70
|
+
}
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const style = document.createElement('style')
|
|
67
|
-
style.id = 'rokkit-ripple-keyframes'
|
|
68
|
-
style.textContent = `
|
|
69
|
-
@keyframes rokkit-ripple {
|
|
70
|
-
to {
|
|
71
|
-
transform: scale(1);
|
|
72
|
-
opacity: 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
`
|
|
76
|
-
document.head.appendChild(style)
|
|
77
|
-
}
|
|
72
|
+
injectRippleKeyframes()
|
|
73
|
+
node.addEventListener('click', onClick)
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
return () => {
|
|
76
|
+
node.removeEventListener('click', onClick)
|
|
77
|
+
node.style.overflow = originalOverflow
|
|
78
|
+
node.style.position = originalPosition
|
|
79
|
+
}
|
|
80
|
+
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
export function ripple(node, options = {}) {
|
|
83
|
+
$effect(() => {
|
|
84
|
+
const opts = resolveRippleOpts(options)
|
|
85
|
+
if (isReducedMotion()) return
|
|
86
|
+
return applyRipple(node, opts)
|
|
86
87
|
})
|
|
87
88
|
}
|
package/src/trigger.js
CHANGED
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
* trigger.destroy()
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
function handleToggleKey(self) {
|
|
26
|
+
if (self.isOpen) self.close()
|
|
27
|
+
else self.open()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleArrowDown(self) {
|
|
31
|
+
self.open()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleArrowUp(self, onlast) {
|
|
35
|
+
self.open()
|
|
36
|
+
onlast?.()
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
export class Trigger {
|
|
26
40
|
#trigger
|
|
27
41
|
#container
|
|
@@ -78,15 +92,13 @@ export class Trigger {
|
|
|
78
92
|
const { key } = event
|
|
79
93
|
if (key === 'Enter' || key === ' ') {
|
|
80
94
|
event.preventDefault()
|
|
81
|
-
|
|
82
|
-
else this.open()
|
|
95
|
+
handleToggleKey(this)
|
|
83
96
|
} else if (key === 'ArrowDown') {
|
|
84
97
|
event.preventDefault()
|
|
85
|
-
this
|
|
98
|
+
handleArrowDown(this)
|
|
86
99
|
} else if (key === 'ArrowUp') {
|
|
87
100
|
event.preventDefault()
|
|
88
|
-
this
|
|
89
|
-
this.#onlast?.()
|
|
101
|
+
handleArrowUp(this, this.#onlast)
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
104
|
|
package/src/utils.js
CHANGED
|
@@ -125,6 +125,10 @@ function isAccordionTrigger(target) {
|
|
|
125
125
|
}
|
|
126
126
|
// getKeyboardAction moved to kbd.js
|
|
127
127
|
|
|
128
|
+
function isToggleTarget(target) {
|
|
129
|
+
return isNodeToggle(target) || isNodeToggle(target.parentElement) || isAccordionTrigger(target)
|
|
130
|
+
}
|
|
131
|
+
|
|
128
132
|
/**
|
|
129
133
|
* Determines an action based on a click event
|
|
130
134
|
*
|
|
@@ -134,26 +138,9 @@ function isAccordionTrigger(target) {
|
|
|
134
138
|
export const getClickAction = (event) => {
|
|
135
139
|
const { ctrlKey, metaKey, shiftKey, target } = event
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Check for modifier keys (toggle selection)
|
|
143
|
-
if (ctrlKey || metaKey) {
|
|
144
|
-
return 'extend'
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check if clicked on icon with collapsed/expanded state
|
|
148
|
-
if (isNodeToggle(target) || isNodeToggle(target.parentElement)) {
|
|
149
|
-
return 'toggle'
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check if clicked on accordion trigger (header area)
|
|
153
|
-
if (isAccordionTrigger(target)) {
|
|
154
|
-
return 'toggle'
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Default action
|
|
141
|
+
if (shiftKey) return 'range'
|
|
142
|
+
if (ctrlKey) return 'extend'
|
|
143
|
+
if (metaKey) return 'extend'
|
|
144
|
+
if (isToggleTarget(target)) return 'toggle'
|
|
158
145
|
return 'select'
|
|
159
146
|
}
|