@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Cursor Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles cursor position, visibility, style, and blinking
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CursorManagerState, FocusManagerState } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the cursor manager state
|
|
14
|
+
*/
|
|
15
|
+
export function createCursorManagerState(): CursorManagerState {
|
|
16
|
+
return {
|
|
17
|
+
cursorPosition: { x: 0, y: 0 },
|
|
18
|
+
cursorVisible: true,
|
|
19
|
+
cursorStyle: 'block',
|
|
20
|
+
cursorBlinking: false,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set cursor position
|
|
26
|
+
*/
|
|
27
|
+
export function setCursorPosition(
|
|
28
|
+
cursorState: CursorManagerState,
|
|
29
|
+
x: number,
|
|
30
|
+
y: number,
|
|
31
|
+
destroyed?: boolean
|
|
32
|
+
): void {
|
|
33
|
+
if (destroyed) return
|
|
34
|
+
cursorState.cursorPosition = { x, y }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get cursor position
|
|
39
|
+
*/
|
|
40
|
+
export function getCursorPosition(
|
|
41
|
+
cursorState: CursorManagerState,
|
|
42
|
+
destroyed?: boolean
|
|
43
|
+
): { x: number; y: number } {
|
|
44
|
+
if (destroyed) return { x: 0, y: 0 }
|
|
45
|
+
return { ...cursorState.cursorPosition }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Show the cursor
|
|
50
|
+
*/
|
|
51
|
+
export function showCursor(
|
|
52
|
+
cursorState: CursorManagerState,
|
|
53
|
+
destroyed?: boolean
|
|
54
|
+
): void {
|
|
55
|
+
if (destroyed) return
|
|
56
|
+
cursorState.cursorVisible = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hide the cursor
|
|
61
|
+
*/
|
|
62
|
+
export function hideCursor(
|
|
63
|
+
cursorState: CursorManagerState,
|
|
64
|
+
destroyed?: boolean
|
|
65
|
+
): void {
|
|
66
|
+
if (destroyed) return
|
|
67
|
+
cursorState.cursorVisible = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if cursor is visible
|
|
72
|
+
*/
|
|
73
|
+
export function isCursorVisible(
|
|
74
|
+
cursorState: CursorManagerState,
|
|
75
|
+
destroyed?: boolean
|
|
76
|
+
): boolean {
|
|
77
|
+
if (destroyed) return false
|
|
78
|
+
return cursorState.cursorVisible
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set cursor style
|
|
83
|
+
*/
|
|
84
|
+
export function setCursorStyle(
|
|
85
|
+
cursorState: CursorManagerState,
|
|
86
|
+
style: 'block' | 'underline' | 'bar',
|
|
87
|
+
destroyed?: boolean
|
|
88
|
+
): void {
|
|
89
|
+
if (destroyed) return
|
|
90
|
+
cursorState.cursorStyle = style
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get cursor style
|
|
95
|
+
*/
|
|
96
|
+
export function getCursorStyle(
|
|
97
|
+
cursorState: CursorManagerState,
|
|
98
|
+
destroyed?: boolean
|
|
99
|
+
): string {
|
|
100
|
+
if (destroyed) return 'block'
|
|
101
|
+
return cursorState.cursorStyle
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Set cursor blink state
|
|
106
|
+
*/
|
|
107
|
+
export function setCursorBlink(
|
|
108
|
+
cursorState: CursorManagerState,
|
|
109
|
+
blink: boolean,
|
|
110
|
+
destroyed?: boolean
|
|
111
|
+
): void {
|
|
112
|
+
if (destroyed) return
|
|
113
|
+
cursorState.cursorBlinking = blink
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if cursor is blinking
|
|
118
|
+
*/
|
|
119
|
+
export function isCursorBlinking(
|
|
120
|
+
cursorState: CursorManagerState,
|
|
121
|
+
destroyed?: boolean
|
|
122
|
+
): boolean {
|
|
123
|
+
if (destroyed) return false
|
|
124
|
+
return cursorState.cursorBlinking
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update cursor position for a focusable element
|
|
129
|
+
*/
|
|
130
|
+
export function updateCursorPosition(
|
|
131
|
+
cursorState: CursorManagerState,
|
|
132
|
+
focusState: FocusManagerState,
|
|
133
|
+
id: string,
|
|
134
|
+
position: { x: number; y: number },
|
|
135
|
+
destroyed?: boolean
|
|
136
|
+
): void {
|
|
137
|
+
if (destroyed) return
|
|
138
|
+
// Update focusable's cursor position
|
|
139
|
+
const entry = focusState.focusables.get(id)
|
|
140
|
+
if (entry) {
|
|
141
|
+
entry.options.cursorPosition = position
|
|
142
|
+
}
|
|
143
|
+
// If this is the focused element, update cursor
|
|
144
|
+
if (focusState.focusedId === id) {
|
|
145
|
+
cursorState.cursorPosition = { ...position }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Focus Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles focus navigation, focus traps, and focus groups for the interactive renderer.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
FocusableOptions,
|
|
11
|
+
FocusableEntry,
|
|
12
|
+
FocusTrapEntry,
|
|
13
|
+
FocusManagerState,
|
|
14
|
+
CursorManagerState,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the focus manager state
|
|
19
|
+
*/
|
|
20
|
+
export function createFocusManagerState(wrapFocus: boolean): FocusManagerState {
|
|
21
|
+
return {
|
|
22
|
+
focusables: new Map<string, FocusableEntry>(),
|
|
23
|
+
focusedId: null,
|
|
24
|
+
activeGroup: null,
|
|
25
|
+
focusTrapStack: [],
|
|
26
|
+
registrationCounter: 0,
|
|
27
|
+
wrapFocus,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gets sorted focusable IDs based on tabIndex and registration order
|
|
33
|
+
*/
|
|
34
|
+
export function getSortedFocusableIds(
|
|
35
|
+
focusState: FocusManagerState,
|
|
36
|
+
destroyed: boolean
|
|
37
|
+
): string[] {
|
|
38
|
+
if (destroyed) return []
|
|
39
|
+
|
|
40
|
+
const { focusables, activeGroup, focusTrapStack } = focusState
|
|
41
|
+
|
|
42
|
+
const entries = Array.from(focusables.values())
|
|
43
|
+
.filter(e => {
|
|
44
|
+
// Exclude negative tabIndex
|
|
45
|
+
if (e.options.tabIndex < 0) return false
|
|
46
|
+
// Exclude disabled
|
|
47
|
+
if (e.options.disabled) return false
|
|
48
|
+
// Filter by active group if set
|
|
49
|
+
if (activeGroup !== null && e.options.group !== activeGroup) return false
|
|
50
|
+
// If in focus trap, only include elements from that group
|
|
51
|
+
if (focusTrapStack.length > 0) {
|
|
52
|
+
const currentTrap = focusTrapStack[focusTrapStack.length - 1]
|
|
53
|
+
if (e.options.group !== currentTrap.group) return false
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
})
|
|
57
|
+
.sort((a, b) => {
|
|
58
|
+
// Sort by tabIndex first
|
|
59
|
+
if (a.options.tabIndex !== b.options.tabIndex) {
|
|
60
|
+
return a.options.tabIndex - b.options.tabIndex
|
|
61
|
+
}
|
|
62
|
+
// Then by registration order
|
|
63
|
+
return a.registrationOrder - b.registrationOrder
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return entries.map(e => e.id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the current focus index
|
|
71
|
+
*/
|
|
72
|
+
export function getCurrentFocusIndex(
|
|
73
|
+
focusState: FocusManagerState,
|
|
74
|
+
destroyed: boolean
|
|
75
|
+
): number {
|
|
76
|
+
if (focusState.focusedId === null) return -1
|
|
77
|
+
const ids = getSortedFocusableIds(focusState, destroyed)
|
|
78
|
+
return ids.indexOf(focusState.focusedId)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Focus the next focusable element
|
|
83
|
+
*/
|
|
84
|
+
export function focusNext(
|
|
85
|
+
focusState: FocusManagerState,
|
|
86
|
+
cursorState: CursorManagerState,
|
|
87
|
+
destroyed: boolean
|
|
88
|
+
): void {
|
|
89
|
+
if (destroyed) return
|
|
90
|
+
const ids = getSortedFocusableIds(focusState, destroyed)
|
|
91
|
+
if (ids.length === 0) return
|
|
92
|
+
|
|
93
|
+
const currentIdx = getCurrentFocusIndex(focusState, destroyed)
|
|
94
|
+
|
|
95
|
+
if (currentIdx === -1) {
|
|
96
|
+
// Nothing focused, focus first
|
|
97
|
+
focusById(focusState, cursorState, ids[0], destroyed)
|
|
98
|
+
} else if (currentIdx < ids.length - 1) {
|
|
99
|
+
// Focus next
|
|
100
|
+
focusById(focusState, cursorState, ids[currentIdx + 1], destroyed)
|
|
101
|
+
} else if (focusState.wrapFocus) {
|
|
102
|
+
// Wrap to first
|
|
103
|
+
focusById(focusState, cursorState, ids[0], destroyed)
|
|
104
|
+
}
|
|
105
|
+
// If not wrapping and at end, stay at current
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Focus the previous focusable element
|
|
110
|
+
*/
|
|
111
|
+
export function focusPrev(
|
|
112
|
+
focusState: FocusManagerState,
|
|
113
|
+
cursorState: CursorManagerState,
|
|
114
|
+
destroyed: boolean
|
|
115
|
+
): void {
|
|
116
|
+
if (destroyed) return
|
|
117
|
+
const ids = getSortedFocusableIds(focusState, destroyed)
|
|
118
|
+
if (ids.length === 0) return
|
|
119
|
+
|
|
120
|
+
const currentIdx = getCurrentFocusIndex(focusState, destroyed)
|
|
121
|
+
|
|
122
|
+
if (currentIdx === -1) {
|
|
123
|
+
// Nothing focused, focus last
|
|
124
|
+
focusById(focusState, cursorState, ids[ids.length - 1], destroyed)
|
|
125
|
+
} else if (currentIdx > 0) {
|
|
126
|
+
// Focus prev
|
|
127
|
+
focusById(focusState, cursorState, ids[currentIdx - 1], destroyed)
|
|
128
|
+
} else if (focusState.wrapFocus) {
|
|
129
|
+
// Wrap to last
|
|
130
|
+
focusById(focusState, cursorState, ids[ids.length - 1], destroyed)
|
|
131
|
+
}
|
|
132
|
+
// If not wrapping and at start, stay at current
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Focus an element by ID
|
|
137
|
+
*/
|
|
138
|
+
export function focusById(
|
|
139
|
+
focusState: FocusManagerState,
|
|
140
|
+
cursorState: CursorManagerState,
|
|
141
|
+
id: string,
|
|
142
|
+
destroyed: boolean
|
|
143
|
+
): void {
|
|
144
|
+
if (destroyed) return
|
|
145
|
+
const entry = focusState.focusables.get(id)
|
|
146
|
+
if (!entry) return
|
|
147
|
+
|
|
148
|
+
// Skip if already focused
|
|
149
|
+
if (focusState.focusedId === id) return
|
|
150
|
+
|
|
151
|
+
// Blur previous
|
|
152
|
+
if (focusState.focusedId) {
|
|
153
|
+
const prevEntry = focusState.focusables.get(focusState.focusedId)
|
|
154
|
+
if (prevEntry?.options.onBlur) {
|
|
155
|
+
prevEntry.options.onBlur({ id: focusState.focusedId })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Focus new
|
|
160
|
+
focusState.focusedId = id
|
|
161
|
+
if (entry.options.onFocus) {
|
|
162
|
+
entry.options.onFocus({ id })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update cursor
|
|
166
|
+
if (entry.options.cursorPosition) {
|
|
167
|
+
cursorState.cursorPosition = { ...entry.options.cursorPosition }
|
|
168
|
+
}
|
|
169
|
+
if (entry.options.showCursor !== undefined) {
|
|
170
|
+
cursorState.cursorVisible = entry.options.showCursor
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the currently focused ID
|
|
176
|
+
*/
|
|
177
|
+
export function getFocusedId(
|
|
178
|
+
focusState: FocusManagerState,
|
|
179
|
+
destroyed: boolean
|
|
180
|
+
): string | null {
|
|
181
|
+
if (destroyed) return null
|
|
182
|
+
return focusState.focusedId
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get all focusable IDs
|
|
187
|
+
*/
|
|
188
|
+
export function getFocusableIds(
|
|
189
|
+
focusState: FocusManagerState,
|
|
190
|
+
destroyed: boolean
|
|
191
|
+
): string[] {
|
|
192
|
+
return getSortedFocusableIds(focusState, destroyed)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register a focusable element
|
|
197
|
+
*/
|
|
198
|
+
export function registerFocusable(
|
|
199
|
+
focusState: FocusManagerState,
|
|
200
|
+
id: string,
|
|
201
|
+
options: FocusableOptions,
|
|
202
|
+
destroyed?: boolean
|
|
203
|
+
): void {
|
|
204
|
+
if (destroyed) return
|
|
205
|
+
// Remove existing if present (handles re-registration)
|
|
206
|
+
focusState.focusables.delete(id)
|
|
207
|
+
focusState.focusables.set(id, {
|
|
208
|
+
id,
|
|
209
|
+
options,
|
|
210
|
+
registrationOrder: focusState.registrationCounter++,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Unregister a focusable element
|
|
216
|
+
*/
|
|
217
|
+
export function unregisterFocusable(
|
|
218
|
+
focusState: FocusManagerState,
|
|
219
|
+
id: string,
|
|
220
|
+
destroyed?: boolean
|
|
221
|
+
): void {
|
|
222
|
+
if (destroyed) return
|
|
223
|
+
focusState.focusables.delete(id)
|
|
224
|
+
if (focusState.focusedId === id) {
|
|
225
|
+
focusState.focusedId = null
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Set the active focus group
|
|
231
|
+
*/
|
|
232
|
+
export function setActiveGroup(
|
|
233
|
+
focusState: FocusManagerState,
|
|
234
|
+
group: string,
|
|
235
|
+
destroyed: boolean
|
|
236
|
+
): void {
|
|
237
|
+
if (destroyed) return
|
|
238
|
+
focusState.activeGroup = group
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Push a focus trap onto the stack
|
|
243
|
+
*/
|
|
244
|
+
export function pushFocusTrap(
|
|
245
|
+
focusState: FocusManagerState,
|
|
246
|
+
group: string,
|
|
247
|
+
destroyed: boolean
|
|
248
|
+
): void {
|
|
249
|
+
if (destroyed) return
|
|
250
|
+
focusState.focusTrapStack.push({
|
|
251
|
+
group,
|
|
252
|
+
previousFocusId: focusState.focusedId,
|
|
253
|
+
})
|
|
254
|
+
focusState.activeGroup = group
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Pop a focus trap from the stack
|
|
259
|
+
*/
|
|
260
|
+
export function popFocusTrap(
|
|
261
|
+
focusState: FocusManagerState,
|
|
262
|
+
cursorState: CursorManagerState,
|
|
263
|
+
destroyed: boolean
|
|
264
|
+
): void {
|
|
265
|
+
if (destroyed) return
|
|
266
|
+
const trap = focusState.focusTrapStack.pop()
|
|
267
|
+
if (trap) {
|
|
268
|
+
// Restore previous focus
|
|
269
|
+
if (trap.previousFocusId) {
|
|
270
|
+
focusById(focusState, cursorState, trap.previousFocusId, destroyed)
|
|
271
|
+
}
|
|
272
|
+
// Update active group
|
|
273
|
+
if (focusState.focusTrapStack.length > 0) {
|
|
274
|
+
focusState.activeGroup = focusState.focusTrapStack[focusState.focusTrapStack.length - 1].group
|
|
275
|
+
} else {
|
|
276
|
+
focusState.activeGroup = null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|