@rokkit/actions 1.0.0-next.133 → 1.0.0-next.135

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/README.md CHANGED
@@ -1,57 +1,189 @@
1
- # Actions
1
+ # @rokkit/actions
2
2
 
3
- This package provides a set of actions that can be used to perform various tasks.
3
+ Svelte actions and DOM utilities for Rokkit components.
4
4
 
5
- ## Keyboard
5
+ ## Installation
6
6
 
7
- The keyboard action can be used to map keyboard events to actions. The following example shows how to map the `K` key combination to an action:
7
+ ```bash
8
+ npm install @rokkit/actions
9
+ # or
10
+ bun add @rokkit/actions
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ `@rokkit/actions` provides the DOM event layer that `@rokkit/ui` components build on. The main export is `Navigator` — a class that wires keyboard, click, and focus events on a container element to a `Wrapper` or `ListController` instance. The package also includes standalone Svelte `use:` actions for effects like ripple, magnetic snap, reveal animations, dismissal on click-outside, and declarative keyboard shortcuts.
16
+
17
+ ## Usage
18
+
19
+ ### navigator — keyboard and click navigation
20
+
21
+ The `navigator` action connects a container element's DOM events to a Wrapper controller. It handles `ArrowUp`/`ArrowDown`/`ArrowLeft`/`ArrowRight`, `Home`, `End`, `Enter`, `Space`, typeahead, and click-to-select.
22
+
23
+ ```svelte
24
+ <script>
25
+ import { navigator } from '@rokkit/actions'
26
+ import { ProxyTree, Wrapper } from '@rokkit/states'
27
+
28
+ const tree = new ProxyTree(items)
29
+ const wrapper = new Wrapper(tree, { onselect })
30
+ </script>
31
+
32
+ <ul use:navigator={{ controller: wrapper, orientation: 'vertical' }}>
33
+ {#each wrapper.flatView as node (node.key)}
34
+ <li data-path={node.key}>{node.proxy.label}</li>
35
+ {/each}
36
+ </ul>
37
+ ```
38
+
39
+ The `data-path` attribute on each item is required — Navigator uses it to resolve which item was clicked or focused.
40
+
41
+ ### Navigator class (imperative usage)
42
+
43
+ ```js
44
+ import { Navigator } from '@rokkit/actions'
45
+
46
+ const nav = new Navigator(containerEl, wrapper, {
47
+ orientation: 'vertical', // 'vertical' | 'horizontal'
48
+ collapsible: true // enable expand/collapse key handling
49
+ })
50
+
51
+ // Clean up when done
52
+ nav.destroy()
53
+ ```
54
+
55
+ ### keyboard — declarative shortcut binding
56
+
57
+ ```svelte
58
+ <script>
59
+ import { keyboard } from '@rokkit/actions'
60
+ </script>
61
+
62
+ <div
63
+ use:keyboard={{ submit: 'enter', cancel: 'escape' }}
64
+ onsubmit={() => save()}
65
+ oncancel={() => close()}
66
+ >
67
+ ...
68
+ </div>
69
+ ```
70
+
71
+ Default mappings: alphabet keys dispatch `add`, Enter dispatches `submit`, Escape dispatches `cancel`, Backspace/Delete dispatch `delete`.
72
+
73
+ ### ripple — click ripple effect
74
+
75
+ ```svelte
76
+ <script>
77
+ import { ripple } from '@rokkit/actions'
78
+ </script>
79
+
80
+ <button use:ripple>Click me</button>
81
+
82
+ <!-- With options -->
83
+ <button use:ripple={{ color: 'white', opacity: 0.2, duration: 400 }}>Click me</button>
84
+ ```
8
85
 
9
- The default behavior is to listen to keyup events.
86
+ ### hoverLift elevation shadow on hover
10
87
 
11
- - [x] Configuration driven
12
- - [x] Custom events can be defined
13
- - [x] Supports mapping an array of keys to an event
14
- - [x] Supports mapping a regex to an event
15
- - [ ] Support key modifiers
16
- - [ ] Support a combination of regex patterns and array of keys
88
+ ```svelte
89
+ <script>
90
+ import { hoverLift } from '@rokkit/actions'
91
+ </script>
92
+
93
+ <div use:hoverLift>Card content</div>
94
+ ```
95
+
96
+ ### magnetic — snap-to-cursor effect
97
+
98
+ ```svelte
99
+ <script>
100
+ import { magnetic } from '@rokkit/actions'
101
+ </script>
17
102
 
18
- Default configuration
103
+ <button use:magnetic>Hover me</button>
104
+ ```
19
105
 
20
- - _add_: alphabet keys cause an `add` event
21
- - _submit_: enter causes a `submit` event
22
- - _cancel_: escape causes a `cancel` event
23
- - _delete_: backspace or delete causes a `delete` event
106
+ ### reveal intersection observer reveal animation
24
107
 
25
- ### Basic Usage
108
+ ```svelte
109
+ <script>
110
+ import { reveal } from '@rokkit/actions'
111
+ </script>
112
+
113
+ <section use:reveal>Fades in when scrolled into view</section>
114
+ ```
115
+
116
+ ### dismissable — click-outside dismissal
26
117
 
27
118
  ```svelte
28
119
  <script>
29
- import { keyboard } from '@fumbl/actions'
120
+ import { dismissable } from '@rokkit/actions'
30
121
 
31
- function handleKey(event) {
32
- console.log(`${event.detail} pressed`)
33
- }
122
+ let open = $state(false)
34
123
  </script>
35
124
 
36
- <div use:keyboard onadd={handleKey}></div>
125
+ <div use:dismissable={{ enabled: open, ondismiss: () => (open = false) }}>Dropdown content</div>
37
126
  ```
38
127
 
39
- ### Custom Events
128
+ ### swipeable — touch swipe detection
40
129
 
41
130
  ```svelte
42
131
  <script>
43
- import { keyboard } from '@fumbl/actions'
44
- function handleKey(event) {
45
- console.log(`${event.detail} pressed`)
46
- }
132
+ import { swipeable } from '@rokkit/actions'
133
+ </script>
47
134
 
48
- const config = {
49
- add: ['a', 'b', 'c'],
50
- submit: 'enter',
51
- cancel: 'escape',
52
- delete: ['backspace', 'delete']
53
- }
135
+ <div use:swipeable onswipeleft={() => next()} onswiperight={() => prev()}>Swipeable content</div>
136
+ ```
137
+
138
+ ### themable — apply theme CSS variables
139
+
140
+ ```svelte
141
+ <script>
142
+ import { themable } from '@rokkit/actions'
54
143
  </script>
55
144
 
56
- <div use:keyboard={config} onadd={handleKey}></div>
145
+ <div use:themable={{ theme: 'ocean' }}>Themed content</div>
57
146
  ```
147
+
148
+ ## API Reference
149
+
150
+ ### Navigator options
151
+
152
+ | Option | Type | Default | Description |
153
+ | ------------- | ---------------------------- | ------------ | ------------------------------------- |
154
+ | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Arrow key axis for prev/next movement |
155
+ | `collapsible` | `boolean` | `false` | Enable expand/collapse via arrow keys |
156
+
157
+ ### buildKeymap / resolveAction
158
+
159
+ Low-level utilities for constructing custom keymaps:
160
+
161
+ ```js
162
+ import { buildKeymap, resolveAction, ACTIONS } from '@rokkit/actions'
163
+
164
+ const keymap = buildKeymap({ orientation: 'vertical', collapsible: true })
165
+ const action = resolveAction(keymap, event) // returns action string or null
166
+ ```
167
+
168
+ ## Exports
169
+
170
+ | Export | Type | Description |
171
+ | --------------- | ------------- | ---------------------------------------------- |
172
+ | `Navigator` | Class | DOM event wiring for Wrapper/ListController |
173
+ | `navigator` | Svelte action | `use:navigator` wrapper around Navigator class |
174
+ | `keyboard` | Svelte action | Declarative keyboard shortcut binding |
175
+ | `ripple` | Svelte action | Material Design ink ripple on click |
176
+ | `hoverLift` | Svelte action | Elevation shadow on hover |
177
+ | `magnetic` | Svelte action | Snap-to-cursor magnetic effect |
178
+ | `reveal` | Svelte action | Intersection-observer reveal animation |
179
+ | `dismissable` | Svelte action | Click-outside dismissal |
180
+ | `pannable` | Svelte action | Pan / drag detection |
181
+ | `swipeable` | Svelte action | Touch swipe detection |
182
+ | `themable` | Svelte action | Apply theme CSS vars to element |
183
+ | `buildKeymap` | Function | Build a keymap for given orientation/options |
184
+ | `resolveAction` | Function | Resolve a keyboard event to an action string |
185
+ | `ACTIONS` | Object | Named action constants |
186
+
187
+ ---
188
+
189
+ Part of [Rokkit](https://github.com/jerrythomas/rokkit) — a Svelte 5 component library and design system.
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.133",
3
+ "version": "1.0.0-next.135",
4
4
  "description": "Contains generic actions that can be used in various components.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jerrythomas/rokkit.git"
8
+ },
5
9
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
10
  "license": "MIT",
7
11
  "type": "module",
@@ -10,6 +14,7 @@
10
14
  },
11
15
  "scripts": {
12
16
  "prepublishOnly": "cp ../../LICENSE . && tsc --project tsconfig.build.json",
17
+ "postpublish": "rm -f LICENSE",
13
18
  "clean": "rm -rf dist",
14
19
  "build": "bun clean && bun prepublishOnly"
15
20
  },
@@ -31,10 +36,10 @@
31
36
  },
32
37
  "dependencies": {
33
38
  "ramda": "^0.32.0",
34
- "@rokkit/core": "latest"
39
+ "@rokkit/core": "workspace:latest"
35
40
  },
36
41
  "devDependencies": {
37
- "@rokkit/helpers": "latest",
38
- "@rokkit/states": "latest"
42
+ "@rokkit/helpers": "workspace:latest",
43
+ "@rokkit/states": "workspace:latest"
39
44
  }
40
45
  }
@@ -20,8 +20,7 @@ export function hoverLift(node, options = {}) {
20
20
  }
21
21
 
22
22
  const reducedMotion =
23
- typeof window !== 'undefined' &&
24
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
23
+ typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
25
24
 
26
25
  if (reducedMotion) return
27
26
 
@@ -18,8 +18,7 @@ export function magnetic(node, options = {}) {
18
18
  }
19
19
 
20
20
  const reducedMotion =
21
- typeof window !== 'undefined' &&
22
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
21
+ typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
23
22
 
24
23
  if (reducedMotion) return
25
24
 
package/src/navigator.js CHANGED
@@ -153,9 +153,7 @@ export class Navigator {
153
153
  // Focused the container itself (user tabbed in, no roving tabindex item yet)
154
154
  // Redirect focus to the currently focused item, or first item if none
155
155
  const targetKey = this.#wrapper.focusedKey
156
- const selector = targetKey
157
- ? `[data-path="${targetKey}"]`
158
- : '[data-path]:not([disabled])'
156
+ const selector = targetKey ? `[data-path="${targetKey}"]` : '[data-path]:not([disabled])'
159
157
  const el = /** @type {HTMLElement|null} */ (this.#root.querySelector(selector))
160
158
  if (el) {
161
159
  el.focus()
@@ -162,8 +162,7 @@ export function navigator(node, options) {
162
162
  resetTypeahead()
163
163
  emitAction(node, wrapper, action, true)
164
164
  // If expand/collapse moved focus, also emit move so components update DOM focus
165
- const focusMoved =
166
- ['expand', 'collapse'].includes(action) && wrapper.focusedKey !== prevKey
165
+ const focusMoved = ['expand', 'collapse'].includes(action) && wrapper.focusedKey !== prevKey
167
166
  if (focusMoved) {
168
167
  node.dispatchEvent(
169
168
  new CustomEvent('action', {
@@ -31,8 +31,7 @@ export function reveal(node, options = {}) {
31
31
  }
32
32
 
33
33
  const reducedMotion =
34
- typeof window !== 'undefined' &&
35
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
34
+ typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
36
35
 
37
36
  const isStagger = opts.stagger > 0
38
37
 
@@ -20,8 +20,7 @@ export function ripple(node, options = {}) {
20
20
  }
21
21
 
22
22
  const reducedMotion =
23
- typeof window !== 'undefined' &&
24
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
23
+ typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
25
24
 
26
25
  if (reducedMotion) return
27
26
 
package/src/trigger.js CHANGED
@@ -49,7 +49,9 @@ export class Trigger {
49
49
  document.addEventListener('keydown', this.#handleDocKeydown)
50
50
  }
51
51
 
52
- get isOpen() { return this.#isOpenFn() }
52
+ get isOpen() {
53
+ return this.#isOpenFn()
54
+ }
53
55
 
54
56
  open() {
55
57
  if (this.isOpen) return
package/src/utils.js CHANGED
@@ -26,7 +26,6 @@ export function getClosestAncestorWithAttribute(element, attribute) {
26
26
  * @returns {string|null} - The event name or null if no match is found.
27
27
  */
28
28
  export const getEventForKey = (keyMapping, key) => {
29
-
30
29
  const matchEvent = ([_eventName, keys]) =>
31
30
  (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key))
32
31