@silvery/tea 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +54 -0
- package/src/core/index.ts +225 -0
- package/src/core/slice.ts +69 -0
- package/src/create-command-registry.ts +106 -0
- package/src/effects.ts +145 -0
- package/src/focus-events.ts +243 -0
- package/src/focus-manager.ts +491 -0
- package/src/focus-queries.ts +241 -0
- package/src/index.ts +213 -0
- package/src/keys.ts +1382 -0
- package/src/pipe.ts +110 -0
- package/src/plugins.ts +119 -0
- package/src/store/index.ts +306 -0
- package/src/streams/index.ts +405 -0
- package/src/tea/README.md +208 -0
- package/src/tea/index.ts +174 -0
- package/src/text-cursor.ts +206 -0
- package/src/text-decorations.ts +253 -0
- package/src/text-ops.ts +150 -0
- package/src/tree-utils.ts +27 -0
- package/src/types.ts +670 -0
- package/src/with-commands.ts +337 -0
- package/src/with-diagnostics.ts +955 -0
- package/src/with-dom-events.ts +168 -0
- package/src/with-focus.ts +162 -0
- package/src/with-keybindings.ts +180 -0
- package/src/with-react.ts +92 -0
- package/src/with-render.ts +92 -0
- package/src/with-terminal.ts +219 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-level Focus and Keyboard Events for silvery
|
|
3
|
+
*
|
|
4
|
+
* Provides React DOM-compatible focus/keyboard event infrastructure:
|
|
5
|
+
* - SilveryKeyEvent / SilveryFocusEvent synthetic event objects
|
|
6
|
+
* - Event dispatch with capture/target/bubble phases (key events)
|
|
7
|
+
* - Event dispatch with target + bubble (focus events)
|
|
8
|
+
*
|
|
9
|
+
* Follows the same patterns as mouse-events.ts for consistency.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Key } from "./keys"
|
|
13
|
+
import { getAncestorPath } from "./tree-utils.js"
|
|
14
|
+
import type { TeaNode } from "./types"
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Event Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Synthetic keyboard event, mirroring React.KeyboardEvent / DOM KeyboardEvent.
|
|
22
|
+
*/
|
|
23
|
+
export interface SilveryKeyEvent {
|
|
24
|
+
/** The printable character, or "" for non-printable keys */
|
|
25
|
+
key: string
|
|
26
|
+
/** Raw terminal input string */
|
|
27
|
+
input: string
|
|
28
|
+
/** Modifier keys */
|
|
29
|
+
ctrl: boolean
|
|
30
|
+
meta: boolean
|
|
31
|
+
shift: boolean
|
|
32
|
+
super: boolean
|
|
33
|
+
hyper: boolean
|
|
34
|
+
/** Kitty event type */
|
|
35
|
+
eventType?: "press" | "repeat" | "release"
|
|
36
|
+
/** Deepest focusable node that received this event */
|
|
37
|
+
target: TeaNode
|
|
38
|
+
/** Node whose handler is currently firing (changes during capture/bubble) */
|
|
39
|
+
currentTarget: TeaNode
|
|
40
|
+
/** Stop event from propagating further */
|
|
41
|
+
stopPropagation(): void
|
|
42
|
+
/** Prevent default behavior */
|
|
43
|
+
preventDefault(): void
|
|
44
|
+
/** Whether stopPropagation() was called */
|
|
45
|
+
readonly propagationStopped: boolean
|
|
46
|
+
/** Whether preventDefault() was called */
|
|
47
|
+
readonly defaultPrevented: boolean
|
|
48
|
+
/** Raw parsed key data */
|
|
49
|
+
nativeEvent: { input: string; key: Key }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Synthetic focus event, mirroring React.FocusEvent / DOM FocusEvent.
|
|
54
|
+
*/
|
|
55
|
+
export interface SilveryFocusEvent {
|
|
56
|
+
/** The node gaining or losing focus */
|
|
57
|
+
target: TeaNode
|
|
58
|
+
/** The other node involved (losing focus on 'focus', gaining on 'blur') */
|
|
59
|
+
relatedTarget: TeaNode | null
|
|
60
|
+
/** Event type */
|
|
61
|
+
type: "focus" | "blur"
|
|
62
|
+
/** Node whose handler is currently firing (changes during bubble) */
|
|
63
|
+
currentTarget: TeaNode
|
|
64
|
+
/** Stop event from bubbling to parent nodes */
|
|
65
|
+
stopPropagation(): void
|
|
66
|
+
/** Whether stopPropagation() was called */
|
|
67
|
+
readonly propagationStopped: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Focus Event Handler Props (added to BoxProps)
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export interface FocusEventProps {
|
|
75
|
+
/** Whether this node can receive focus */
|
|
76
|
+
focusable?: boolean
|
|
77
|
+
/** Whether this node should receive focus on mount */
|
|
78
|
+
autoFocus?: boolean
|
|
79
|
+
/** Whether this node creates a focus scope (focus trapping boundary) */
|
|
80
|
+
focusScope?: boolean
|
|
81
|
+
/** ID of the node to focus when pressing Up from this node */
|
|
82
|
+
nextFocusUp?: string
|
|
83
|
+
/** ID of the node to focus when pressing Down from this node */
|
|
84
|
+
nextFocusDown?: string
|
|
85
|
+
/** ID of the node to focus when pressing Left from this node */
|
|
86
|
+
nextFocusLeft?: string
|
|
87
|
+
/** ID of the node to focus when pressing Right from this node */
|
|
88
|
+
nextFocusRight?: string
|
|
89
|
+
/** Called when this node gains focus */
|
|
90
|
+
onFocus?: (event: SilveryFocusEvent) => void
|
|
91
|
+
/** Called when this node loses focus */
|
|
92
|
+
onBlur?: (event: SilveryFocusEvent) => void
|
|
93
|
+
/** Called on key down (bubble phase) */
|
|
94
|
+
onKeyDown?: (event: SilveryKeyEvent, dispatch?: (msg: unknown) => void) => void
|
|
95
|
+
/** Called on key up (bubble phase) */
|
|
96
|
+
onKeyUp?: (event: SilveryKeyEvent, dispatch?: (msg: unknown) => void) => void
|
|
97
|
+
/** Called on key down (capture phase — fires before target) */
|
|
98
|
+
onKeyDownCapture?: (event: SilveryKeyEvent) => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Event Factories
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a synthetic keyboard event.
|
|
107
|
+
*/
|
|
108
|
+
export function createKeyEvent(input: string, key: Key, target: TeaNode): SilveryKeyEvent {
|
|
109
|
+
let propagationStopped = false
|
|
110
|
+
let defaultPrevented = false
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
key: input,
|
|
114
|
+
input,
|
|
115
|
+
ctrl: key.ctrl,
|
|
116
|
+
meta: key.meta,
|
|
117
|
+
shift: key.shift,
|
|
118
|
+
super: key.super,
|
|
119
|
+
hyper: key.hyper,
|
|
120
|
+
eventType: key.eventType,
|
|
121
|
+
target,
|
|
122
|
+
currentTarget: target,
|
|
123
|
+
nativeEvent: { input, key },
|
|
124
|
+
get propagationStopped() {
|
|
125
|
+
return propagationStopped
|
|
126
|
+
},
|
|
127
|
+
get defaultPrevented() {
|
|
128
|
+
return defaultPrevented
|
|
129
|
+
},
|
|
130
|
+
stopPropagation() {
|
|
131
|
+
propagationStopped = true
|
|
132
|
+
},
|
|
133
|
+
preventDefault() {
|
|
134
|
+
defaultPrevented = true
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a synthetic focus event.
|
|
141
|
+
*/
|
|
142
|
+
export function createFocusEvent(
|
|
143
|
+
type: "focus" | "blur",
|
|
144
|
+
target: TeaNode,
|
|
145
|
+
relatedTarget: TeaNode | null,
|
|
146
|
+
): SilveryFocusEvent {
|
|
147
|
+
let propagationStopped = false
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
type,
|
|
151
|
+
target,
|
|
152
|
+
relatedTarget,
|
|
153
|
+
currentTarget: target,
|
|
154
|
+
get propagationStopped() {
|
|
155
|
+
return propagationStopped
|
|
156
|
+
},
|
|
157
|
+
stopPropagation() {
|
|
158
|
+
propagationStopped = true
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Tree Walking
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Event Dispatch
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Dispatch a keyboard event through the render tree with DOM-style
|
|
173
|
+
* capture/target/bubble phases.
|
|
174
|
+
*
|
|
175
|
+
* 1. Capture phase: root → target (onKeyDownCapture props)
|
|
176
|
+
* 2. Target phase: target's onKeyDown
|
|
177
|
+
* 3. Bubble phase: target parent → root (onKeyDown props)
|
|
178
|
+
*
|
|
179
|
+
* stopPropagation() halts traversal at any phase.
|
|
180
|
+
*/
|
|
181
|
+
export function dispatchKeyEvent(event: SilveryKeyEvent, dispatch?: (msg: unknown) => void): void {
|
|
182
|
+
const path = getAncestorPath(event.target)
|
|
183
|
+
const mutableEvent = event as { currentTarget: TeaNode }
|
|
184
|
+
|
|
185
|
+
// Capture phase: root → target (reversed path, excluding target)
|
|
186
|
+
for (let i = path.length - 1; i > 0; i--) {
|
|
187
|
+
if (event.propagationStopped) return
|
|
188
|
+
const node = path[i]!
|
|
189
|
+
const handler = (node.props as Record<string, unknown>).onKeyDownCapture as
|
|
190
|
+
| ((e: SilveryKeyEvent) => void)
|
|
191
|
+
| undefined
|
|
192
|
+
if (handler) {
|
|
193
|
+
mutableEvent.currentTarget = node
|
|
194
|
+
handler(event)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Target phase: fire onKeyDown on the target itself
|
|
199
|
+
if (!event.propagationStopped) {
|
|
200
|
+
const target = path[0]!
|
|
201
|
+
mutableEvent.currentTarget = target
|
|
202
|
+
const handler = (target.props as Record<string, unknown>).onKeyDown as
|
|
203
|
+
| ((e: SilveryKeyEvent, d?: (msg: unknown) => void) => void)
|
|
204
|
+
| undefined
|
|
205
|
+
if (handler) {
|
|
206
|
+
handler(event, dispatch)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Bubble phase: target parent → root
|
|
211
|
+
for (let i = 1; i < path.length; i++) {
|
|
212
|
+
if (event.propagationStopped) return
|
|
213
|
+
const node = path[i]!
|
|
214
|
+
const handler = (node.props as Record<string, unknown>).onKeyDown as
|
|
215
|
+
| ((e: SilveryKeyEvent, d?: (msg: unknown) => void) => void)
|
|
216
|
+
| undefined
|
|
217
|
+
if (handler) {
|
|
218
|
+
mutableEvent.currentTarget = node
|
|
219
|
+
handler(event, dispatch)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Dispatch a focus event through the render tree.
|
|
226
|
+
*
|
|
227
|
+
* Fires onFocus/onBlur on the target, then bubbles to ancestors.
|
|
228
|
+
*/
|
|
229
|
+
export function dispatchFocusEvent(event: SilveryFocusEvent): void {
|
|
230
|
+
const handlerProp = event.type === "focus" ? "onFocus" : "onBlur"
|
|
231
|
+
const path = getAncestorPath(event.target)
|
|
232
|
+
const mutableEvent = event as { currentTarget: TeaNode }
|
|
233
|
+
|
|
234
|
+
for (const node of path) {
|
|
235
|
+
if (event.propagationStopped) break
|
|
236
|
+
|
|
237
|
+
const handler = (node.props as Record<string, unknown>)[handlerProp] as ((e: SilveryFocusEvent) => void) | undefined
|
|
238
|
+
if (handler) {
|
|
239
|
+
mutableEvent.currentTarget = node
|
|
240
|
+
handler(event)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|