@mdxui/terminal 2.0.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/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Input Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles input field registration and key handling
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { InputOptions, InputState, InputManagerState, FocusManagerState } from './types'
|
|
11
|
+
import { registerFocusable } from './focus-manager'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates the input manager state
|
|
15
|
+
*/
|
|
16
|
+
export function createInputManagerState(): InputManagerState {
|
|
17
|
+
return {
|
|
18
|
+
inputs: new Map<string, InputState>(),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register an input field
|
|
24
|
+
*/
|
|
25
|
+
export function registerInput(
|
|
26
|
+
inputState: InputManagerState,
|
|
27
|
+
focusState: FocusManagerState,
|
|
28
|
+
id: string,
|
|
29
|
+
options: InputOptions,
|
|
30
|
+
destroyed?: boolean
|
|
31
|
+
): void {
|
|
32
|
+
if (destroyed) return
|
|
33
|
+
inputState.inputs.set(id, {
|
|
34
|
+
...options,
|
|
35
|
+
currentValue: options.value,
|
|
36
|
+
currentCursorIndex: options.cursorIndex ?? options.value.length,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Also register as focusable
|
|
40
|
+
registerFocusable(focusState, id, { tabIndex: 0 }, destroyed)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get input cursor index
|
|
45
|
+
*/
|
|
46
|
+
export function getInputCursorIndex(
|
|
47
|
+
inputState: InputManagerState,
|
|
48
|
+
id: string,
|
|
49
|
+
destroyed?: boolean
|
|
50
|
+
): number {
|
|
51
|
+
if (destroyed) return 0
|
|
52
|
+
const input = inputState.inputs.get(id)
|
|
53
|
+
return input?.currentCursorIndex ?? 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handle input key press
|
|
58
|
+
*/
|
|
59
|
+
export function handleInputKey(
|
|
60
|
+
id: string,
|
|
61
|
+
input: InputState,
|
|
62
|
+
key: string
|
|
63
|
+
): boolean {
|
|
64
|
+
const { currentValue, currentCursorIndex, onChange, onSubmit, maxLength, validate, mask, multiline } = input
|
|
65
|
+
|
|
66
|
+
// Handle special keys
|
|
67
|
+
if (key === 'backspace') {
|
|
68
|
+
if (currentCursorIndex > 0) {
|
|
69
|
+
const newValue = currentValue.slice(0, currentCursorIndex - 1) + currentValue.slice(currentCursorIndex)
|
|
70
|
+
input.currentValue = newValue
|
|
71
|
+
input.currentCursorIndex = currentCursorIndex - 1
|
|
72
|
+
onChange?.(newValue)
|
|
73
|
+
}
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (key === 'delete') {
|
|
78
|
+
if (currentCursorIndex < currentValue.length) {
|
|
79
|
+
const newValue = currentValue.slice(0, currentCursorIndex) + currentValue.slice(currentCursorIndex + 1)
|
|
80
|
+
input.currentValue = newValue
|
|
81
|
+
onChange?.(newValue)
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (key === 'left') {
|
|
87
|
+
if (currentCursorIndex > 0) {
|
|
88
|
+
input.currentCursorIndex = currentCursorIndex - 1
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (key === 'right') {
|
|
94
|
+
if (currentCursorIndex < currentValue.length) {
|
|
95
|
+
input.currentCursorIndex = currentCursorIndex + 1
|
|
96
|
+
}
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (key === 'home') {
|
|
101
|
+
input.currentCursorIndex = 0
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (key === 'end') {
|
|
106
|
+
input.currentCursorIndex = currentValue.length
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (key === 'enter') {
|
|
111
|
+
if (multiline) {
|
|
112
|
+
const newValue = currentValue + '\n'
|
|
113
|
+
input.currentValue = newValue
|
|
114
|
+
input.currentCursorIndex = newValue.length
|
|
115
|
+
onChange?.(newValue)
|
|
116
|
+
} else {
|
|
117
|
+
onSubmit?.(currentValue)
|
|
118
|
+
}
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (key === 'ctrl+enter') {
|
|
123
|
+
onSubmit?.(currentValue)
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle regular character input
|
|
128
|
+
if (key.length === 1) {
|
|
129
|
+
// Check max length
|
|
130
|
+
if (maxLength !== undefined && currentValue.length >= maxLength) {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let newValue = currentValue + key
|
|
135
|
+
|
|
136
|
+
// Apply mask if present
|
|
137
|
+
if (mask) {
|
|
138
|
+
// Simple mask handling: insert formatting chars
|
|
139
|
+
const maskChar = mask[newValue.length - 1]
|
|
140
|
+
if (maskChar && maskChar !== '#') {
|
|
141
|
+
newValue = currentValue + maskChar + key
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Validate if validator present
|
|
146
|
+
if (validate && !validate(newValue)) {
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
input.currentValue = newValue
|
|
151
|
+
input.currentCursorIndex = newValue.length
|
|
152
|
+
onChange?.(newValue)
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear input state on destroy
|
|
161
|
+
*/
|
|
162
|
+
export function clearInputState(inputState: InputManagerState): void {
|
|
163
|
+
inputState.inputs.clear()
|
|
164
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Keyboard Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles key binding, input handling, sequences, and vim bindings
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { KeyHandler, KeyboardManagerState } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the keyboard manager state
|
|
14
|
+
*/
|
|
15
|
+
export function createKeyboardManagerState(
|
|
16
|
+
vimBindings: boolean,
|
|
17
|
+
sequenceTimeout: number
|
|
18
|
+
): KeyboardManagerState {
|
|
19
|
+
return {
|
|
20
|
+
keyHandlers: new Map<string, KeyHandler[]>(),
|
|
21
|
+
sequenceHandlers: new Map<string, () => void>(),
|
|
22
|
+
pendingSequence: '',
|
|
23
|
+
sequenceTimer: null,
|
|
24
|
+
sequenceTimeout,
|
|
25
|
+
mode: 'normal',
|
|
26
|
+
searchModeHandler: null,
|
|
27
|
+
cancelHandler: null,
|
|
28
|
+
vimBindings,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a key press handler
|
|
34
|
+
*/
|
|
35
|
+
export function onKeyPress(
|
|
36
|
+
keyboardState: KeyboardManagerState,
|
|
37
|
+
key: string,
|
|
38
|
+
handler: () => boolean | void,
|
|
39
|
+
options?: { priority?: number },
|
|
40
|
+
destroyed?: boolean
|
|
41
|
+
): void {
|
|
42
|
+
if (destroyed) return
|
|
43
|
+
const handlers = keyboardState.keyHandlers.get(key) || []
|
|
44
|
+
|
|
45
|
+
// Check for duplicate handler
|
|
46
|
+
if (handlers.some(h => h.handler === handler)) return
|
|
47
|
+
|
|
48
|
+
handlers.push({ handler, priority: options?.priority ?? 0 })
|
|
49
|
+
// Sort by priority descending
|
|
50
|
+
handlers.sort((a, b) => b.priority - a.priority)
|
|
51
|
+
keyboardState.keyHandlers.set(key, handlers)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unregister a key press handler
|
|
56
|
+
*/
|
|
57
|
+
export function offKeyPress(
|
|
58
|
+
keyboardState: KeyboardManagerState,
|
|
59
|
+
key: string,
|
|
60
|
+
handler: () => boolean | void,
|
|
61
|
+
destroyed?: boolean
|
|
62
|
+
): void {
|
|
63
|
+
if (destroyed) return
|
|
64
|
+
const handlers = keyboardState.keyHandlers.get(key)
|
|
65
|
+
if (handlers) {
|
|
66
|
+
const idx = handlers.findIndex(h => h.handler === handler)
|
|
67
|
+
if (idx !== -1) {
|
|
68
|
+
handlers.splice(idx, 1)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Emit a key event
|
|
75
|
+
*/
|
|
76
|
+
export function emitKey(
|
|
77
|
+
keyboardState: KeyboardManagerState,
|
|
78
|
+
key: string,
|
|
79
|
+
handleComponentKey: (key: string) => boolean,
|
|
80
|
+
focusedId: string | null,
|
|
81
|
+
destroyed?: boolean
|
|
82
|
+
): void {
|
|
83
|
+
if (destroyed) return
|
|
84
|
+
|
|
85
|
+
// Check for sequence handling
|
|
86
|
+
if (keyboardState.sequenceHandlers.size > 0) {
|
|
87
|
+
const newSequence = keyboardState.pendingSequence + key
|
|
88
|
+
|
|
89
|
+
// Check if this completes a sequence
|
|
90
|
+
const sequenceHandler = keyboardState.sequenceHandlers.get(newSequence)
|
|
91
|
+
if (sequenceHandler) {
|
|
92
|
+
keyboardState.pendingSequence = ''
|
|
93
|
+
if (keyboardState.sequenceTimer) {
|
|
94
|
+
clearTimeout(keyboardState.sequenceTimer)
|
|
95
|
+
keyboardState.sequenceTimer = null
|
|
96
|
+
}
|
|
97
|
+
sequenceHandler()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if this could be part of a sequence
|
|
102
|
+
const couldBeSequence = Array.from(keyboardState.sequenceHandlers.keys()).some(s =>
|
|
103
|
+
s.startsWith(newSequence)
|
|
104
|
+
)
|
|
105
|
+
if (couldBeSequence) {
|
|
106
|
+
keyboardState.pendingSequence = newSequence
|
|
107
|
+
// Reset timeout
|
|
108
|
+
if (keyboardState.sequenceTimer) {
|
|
109
|
+
clearTimeout(keyboardState.sequenceTimer)
|
|
110
|
+
}
|
|
111
|
+
keyboardState.sequenceTimer = setTimeout(() => {
|
|
112
|
+
keyboardState.pendingSequence = ''
|
|
113
|
+
keyboardState.sequenceTimer = null
|
|
114
|
+
}, keyboardState.sequenceTimeout)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Not part of any sequence, clear and process normally
|
|
119
|
+
keyboardState.pendingSequence = ''
|
|
120
|
+
if (keyboardState.sequenceTimer) {
|
|
121
|
+
clearTimeout(keyboardState.sequenceTimer)
|
|
122
|
+
keyboardState.sequenceTimer = null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Process key handlers
|
|
127
|
+
const handlers = keyboardState.keyHandlers.get(key)
|
|
128
|
+
if (handlers) {
|
|
129
|
+
for (const { handler } of handlers) {
|
|
130
|
+
const result = handler()
|
|
131
|
+
if (result === true) {
|
|
132
|
+
return // Stop propagation
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If it's a single character, pass to focused input
|
|
138
|
+
if (key.length === 1 && focusedId) {
|
|
139
|
+
handleComponentKey(key)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a key sequence handler
|
|
145
|
+
*/
|
|
146
|
+
export function onKeySequence(
|
|
147
|
+
keyboardState: KeyboardManagerState,
|
|
148
|
+
sequence: string,
|
|
149
|
+
handler: () => void,
|
|
150
|
+
destroyed?: boolean
|
|
151
|
+
): void {
|
|
152
|
+
if (destroyed) return
|
|
153
|
+
keyboardState.sequenceHandlers.set(sequence, handler)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the pending sequence
|
|
158
|
+
*/
|
|
159
|
+
export function getPendingSequence(
|
|
160
|
+
keyboardState: KeyboardManagerState,
|
|
161
|
+
destroyed?: boolean
|
|
162
|
+
): string {
|
|
163
|
+
if (destroyed) return ''
|
|
164
|
+
return keyboardState.pendingSequence
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Register search mode handler
|
|
169
|
+
*/
|
|
170
|
+
export function onSearchMode(
|
|
171
|
+
keyboardState: KeyboardManagerState,
|
|
172
|
+
handler: () => void,
|
|
173
|
+
destroyed?: boolean
|
|
174
|
+
): void {
|
|
175
|
+
if (destroyed) return
|
|
176
|
+
keyboardState.searchModeHandler = handler
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Register cancel handler
|
|
181
|
+
*/
|
|
182
|
+
export function onCancel(
|
|
183
|
+
keyboardState: KeyboardManagerState,
|
|
184
|
+
handler: () => void,
|
|
185
|
+
destroyed?: boolean
|
|
186
|
+
): void {
|
|
187
|
+
if (destroyed) return
|
|
188
|
+
keyboardState.cancelHandler = handler
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the current mode
|
|
193
|
+
*/
|
|
194
|
+
export function getMode(
|
|
195
|
+
keyboardState: KeyboardManagerState,
|
|
196
|
+
destroyed?: boolean
|
|
197
|
+
): string {
|
|
198
|
+
if (destroyed) return 'normal'
|
|
199
|
+
return keyboardState.mode
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clear keyboard state on destroy
|
|
204
|
+
*/
|
|
205
|
+
export function clearKeyboardState(keyboardState: KeyboardManagerState): void {
|
|
206
|
+
keyboardState.keyHandlers.clear()
|
|
207
|
+
keyboardState.sequenceHandlers.clear()
|
|
208
|
+
if (keyboardState.sequenceTimer) {
|
|
209
|
+
clearTimeout(keyboardState.sequenceTimer)
|
|
210
|
+
keyboardState.sequenceTimer = null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Mouse Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles click events, clickable areas, and scroll events
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ClickableOptions, MouseManagerState, FocusManagerState, CursorManagerState } from './types'
|
|
11
|
+
import { focusById } from './focus-manager'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates the mouse manager state
|
|
15
|
+
*/
|
|
16
|
+
export function createMouseManagerState(): MouseManagerState {
|
|
17
|
+
return {
|
|
18
|
+
clickables: new Map<string, ClickableOptions>(),
|
|
19
|
+
clickHandlers: [],
|
|
20
|
+
scrollHandler: null,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a click handler
|
|
26
|
+
*/
|
|
27
|
+
export function onClick(
|
|
28
|
+
mouseState: MouseManagerState,
|
|
29
|
+
handler: (event: { x: number; y: number }) => void,
|
|
30
|
+
destroyed?: boolean
|
|
31
|
+
): void {
|
|
32
|
+
if (destroyed) return
|
|
33
|
+
mouseState.clickHandlers.push(handler)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unregister a click handler
|
|
38
|
+
*/
|
|
39
|
+
export function offClick(
|
|
40
|
+
mouseState: MouseManagerState,
|
|
41
|
+
handler: (event: { x: number; y: number }) => void,
|
|
42
|
+
destroyed?: boolean
|
|
43
|
+
): void {
|
|
44
|
+
if (destroyed) return
|
|
45
|
+
const idx = mouseState.clickHandlers.indexOf(handler)
|
|
46
|
+
if (idx !== -1) {
|
|
47
|
+
mouseState.clickHandlers.splice(idx, 1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emit a click event
|
|
53
|
+
*/
|
|
54
|
+
export function emitClick(
|
|
55
|
+
mouseState: MouseManagerState,
|
|
56
|
+
focusState: FocusManagerState,
|
|
57
|
+
cursorState: CursorManagerState,
|
|
58
|
+
x: number,
|
|
59
|
+
y: number,
|
|
60
|
+
options?: { clickCount?: number; button?: string },
|
|
61
|
+
destroyed?: boolean
|
|
62
|
+
): void {
|
|
63
|
+
if (destroyed) return
|
|
64
|
+
|
|
65
|
+
// Find clickable at position with highest z-index
|
|
66
|
+
const clickableEntries = Array.from(mouseState.clickables.entries())
|
|
67
|
+
.filter(([, c]) => {
|
|
68
|
+
return x >= c.x && x < c.x + c.width && y >= c.y && y < c.y + c.height
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => (b[1].zIndex ?? 0) - (a[1].zIndex ?? 0))
|
|
71
|
+
|
|
72
|
+
if (clickableEntries.length > 0) {
|
|
73
|
+
const [id, clickable] = clickableEntries[0]
|
|
74
|
+
|
|
75
|
+
// Handle focus
|
|
76
|
+
if (clickable.focusable !== false && focusState.focusables.has(id)) {
|
|
77
|
+
focusById(focusState, cursorState, id, false)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle click type
|
|
81
|
+
if (options?.button === 'right' && clickable.onRightClick) {
|
|
82
|
+
clickable.onRightClick()
|
|
83
|
+
} else if (options?.clickCount === 2 && clickable.onDoubleClick) {
|
|
84
|
+
clickable.onDoubleClick()
|
|
85
|
+
} else if (clickable.onClick) {
|
|
86
|
+
clickable.onClick({ x, y, id })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Call global click handlers
|
|
91
|
+
for (const handler of mouseState.clickHandlers) {
|
|
92
|
+
handler({ x, y })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a clickable area
|
|
98
|
+
*/
|
|
99
|
+
export function registerClickable(
|
|
100
|
+
mouseState: MouseManagerState,
|
|
101
|
+
id: string,
|
|
102
|
+
options: ClickableOptions,
|
|
103
|
+
destroyed?: boolean
|
|
104
|
+
): void {
|
|
105
|
+
if (destroyed) return
|
|
106
|
+
mouseState.clickables.set(id, options)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Unregister a clickable area
|
|
111
|
+
*/
|
|
112
|
+
export function unregisterClickable(
|
|
113
|
+
mouseState: MouseManagerState,
|
|
114
|
+
id: string,
|
|
115
|
+
destroyed?: boolean
|
|
116
|
+
): void {
|
|
117
|
+
if (destroyed) return
|
|
118
|
+
mouseState.clickables.delete(id)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all clickable areas
|
|
123
|
+
*/
|
|
124
|
+
export function getClickableAreas(
|
|
125
|
+
mouseState: MouseManagerState,
|
|
126
|
+
destroyed?: boolean
|
|
127
|
+
): Array<{ id: string } & ClickableOptions> {
|
|
128
|
+
if (destroyed) return []
|
|
129
|
+
return Array.from(mouseState.clickables.entries()).map(([id, options]) => ({
|
|
130
|
+
id,
|
|
131
|
+
...options,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register scroll handler
|
|
137
|
+
*/
|
|
138
|
+
export function onScroll(
|
|
139
|
+
mouseState: MouseManagerState,
|
|
140
|
+
handler: (event: { deltaY: number }) => void,
|
|
141
|
+
destroyed?: boolean
|
|
142
|
+
): void {
|
|
143
|
+
if (destroyed) return
|
|
144
|
+
mouseState.scrollHandler = handler
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Emit scroll event
|
|
149
|
+
*/
|
|
150
|
+
export function emitScroll(
|
|
151
|
+
mouseState: MouseManagerState,
|
|
152
|
+
_x: number,
|
|
153
|
+
_y: number,
|
|
154
|
+
options: { deltaY: number },
|
|
155
|
+
destroyed?: boolean
|
|
156
|
+
): void {
|
|
157
|
+
if (destroyed) return
|
|
158
|
+
mouseState.scrollHandler?.(options)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Clear mouse state on destroy
|
|
163
|
+
*/
|
|
164
|
+
export function clearMouseState(mouseState: MouseManagerState): void {
|
|
165
|
+
mouseState.clickables.clear()
|
|
166
|
+
mouseState.clickHandlers.length = 0
|
|
167
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal State Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles component state tracking, subscriptions, and render callbacks
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StateManagerState } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the state manager state
|
|
14
|
+
*/
|
|
15
|
+
export function createStateManagerState(targetFps: number): StateManagerState {
|
|
16
|
+
return {
|
|
17
|
+
state: new Map<string, unknown>(),
|
|
18
|
+
stateSubscribers: new Map<string, Set<(newValue: unknown, oldValue: unknown) => void>>(),
|
|
19
|
+
renderHandler: null,
|
|
20
|
+
targetFps,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set a state value
|
|
26
|
+
*/
|
|
27
|
+
export function setState(
|
|
28
|
+
stateManager: StateManagerState,
|
|
29
|
+
key: string,
|
|
30
|
+
value: unknown,
|
|
31
|
+
destroyed?: boolean
|
|
32
|
+
): void {
|
|
33
|
+
if (destroyed) return
|
|
34
|
+
const oldValue = stateManager.state.get(key)
|
|
35
|
+
stateManager.state.set(key, value)
|
|
36
|
+
|
|
37
|
+
// Notify subscribers
|
|
38
|
+
const subscribers = stateManager.stateSubscribers.get(key)
|
|
39
|
+
if (subscribers) {
|
|
40
|
+
for (const subscriber of subscribers) {
|
|
41
|
+
subscriber(value, oldValue)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Trigger render
|
|
46
|
+
stateManager.renderHandler?.()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a state value
|
|
51
|
+
*/
|
|
52
|
+
export function getState(
|
|
53
|
+
stateManager: StateManagerState,
|
|
54
|
+
key: string,
|
|
55
|
+
destroyed?: boolean
|
|
56
|
+
): unknown {
|
|
57
|
+
if (destroyed) return undefined
|
|
58
|
+
return stateManager.state.get(key)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Subscribe to state changes
|
|
63
|
+
*/
|
|
64
|
+
export function subscribe(
|
|
65
|
+
stateManager: StateManagerState,
|
|
66
|
+
key: string,
|
|
67
|
+
subscriber: (newValue: unknown, oldValue: unknown) => void,
|
|
68
|
+
destroyed?: boolean
|
|
69
|
+
): () => void {
|
|
70
|
+
if (destroyed) return () => {}
|
|
71
|
+
if (!stateManager.stateSubscribers.has(key)) {
|
|
72
|
+
stateManager.stateSubscribers.set(key, new Set())
|
|
73
|
+
}
|
|
74
|
+
stateManager.stateSubscribers.get(key)!.add(subscriber)
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
const subscribers = stateManager.stateSubscribers.get(key)
|
|
78
|
+
if (subscribers) {
|
|
79
|
+
subscribers.delete(subscriber)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register render handler
|
|
86
|
+
*/
|
|
87
|
+
export function onRender(
|
|
88
|
+
stateManager: StateManagerState,
|
|
89
|
+
handler: () => void,
|
|
90
|
+
destroyed?: boolean
|
|
91
|
+
): void {
|
|
92
|
+
if (destroyed) return
|
|
93
|
+
stateManager.renderHandler = handler
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get target FPS
|
|
98
|
+
*/
|
|
99
|
+
export function getTargetFps(stateManager: StateManagerState): number {
|
|
100
|
+
return stateManager.targetFps
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Clear state manager on destroy
|
|
105
|
+
*/
|
|
106
|
+
export function clearStateManager(stateManager: StateManagerState): void {
|
|
107
|
+
stateManager.state.clear()
|
|
108
|
+
stateManager.stateSubscribers.clear()
|
|
109
|
+
}
|