@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,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Renderers
|
|
3
|
+
*
|
|
4
|
+
* Multi-tier rendering architecture for Universal Terminal UI.
|
|
5
|
+
* Each tier provides progressively richer output capabilities:
|
|
6
|
+
*
|
|
7
|
+
* - TEXT: Plain text without formatting (tier 1)
|
|
8
|
+
* - MARKDOWN: Markdown syntax for AI agents (tier 2)
|
|
9
|
+
* - ASCII: ASCII art and basic drawing characters (tier 3)
|
|
10
|
+
* - UNICODE: Unicode box drawing and symbols (tier 4)
|
|
11
|
+
* - ANSI: Full ANSI escape sequences for colors/styles (tier 5)
|
|
12
|
+
* - INTERACTIVE: Full interactive terminal UI with input handling (tier 6)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// TEXT tier renderer
|
|
16
|
+
export { renderText } from './text'
|
|
17
|
+
export type { TextRenderOptions } from './text'
|
|
18
|
+
|
|
19
|
+
// MARKDOWN tier renderer
|
|
20
|
+
export { renderMarkdown } from './markdown'
|
|
21
|
+
export type { MarkdownRenderOptions } from './markdown'
|
|
22
|
+
|
|
23
|
+
// ASCII tier renderer
|
|
24
|
+
export { renderASCII } from './ascii'
|
|
25
|
+
|
|
26
|
+
// UNICODE tier renderer
|
|
27
|
+
export { renderUnicode } from './unicode'
|
|
28
|
+
|
|
29
|
+
// ANSI tier renderer
|
|
30
|
+
export { renderANSI } from './ansi'
|
|
31
|
+
|
|
32
|
+
// INTERACTIVE tier renderer
|
|
33
|
+
export { renderInteractive, createInteractiveRenderer } from './interactive'
|
|
34
|
+
export type {
|
|
35
|
+
InteractiveRendererConfig,
|
|
36
|
+
InteractiveRenderer,
|
|
37
|
+
FocusableOptions,
|
|
38
|
+
ClickableOptions,
|
|
39
|
+
InputOptions,
|
|
40
|
+
ComponentOptions,
|
|
41
|
+
} from './interactive'
|
|
42
|
+
|
|
43
|
+
// ANSI to CSS conversion utilities for web rendering
|
|
44
|
+
export {
|
|
45
|
+
ansiToCSS,
|
|
46
|
+
ansiToHTML,
|
|
47
|
+
parseAnsiToSpans,
|
|
48
|
+
spanToInlineStyle,
|
|
49
|
+
} from './ansi-css'
|
|
50
|
+
export type {
|
|
51
|
+
CSSStyleProperties,
|
|
52
|
+
StyledSpan,
|
|
53
|
+
ANSIToCSSResult,
|
|
54
|
+
} from './ansi-css'
|
|
55
|
+
|
|
56
|
+
// Common utilities used across renderers
|
|
57
|
+
export {
|
|
58
|
+
getIndentStr,
|
|
59
|
+
getTextWidth,
|
|
60
|
+
padText,
|
|
61
|
+
wrapText,
|
|
62
|
+
sanitizeToASCII,
|
|
63
|
+
boxDrawingToASCII,
|
|
64
|
+
buildBox,
|
|
65
|
+
extractStringArray,
|
|
66
|
+
extractHeaders,
|
|
67
|
+
extractRowValues,
|
|
68
|
+
joinParts,
|
|
69
|
+
getProp,
|
|
70
|
+
calculateColumnWidths,
|
|
71
|
+
buildTableSeparator,
|
|
72
|
+
buildTableRow,
|
|
73
|
+
matchPath,
|
|
74
|
+
normalizePath,
|
|
75
|
+
matchPathPattern,
|
|
76
|
+
generateBreadcrumbSegments,
|
|
77
|
+
formatSegmentLabel,
|
|
78
|
+
findActiveItemByPath,
|
|
79
|
+
findActiveItemInSections,
|
|
80
|
+
joinPath,
|
|
81
|
+
extractPathParams,
|
|
82
|
+
createCallbackRouterAdapter,
|
|
83
|
+
ASCII_BOX_CHARS,
|
|
84
|
+
ASCII_DOUBLE_BOX_CHARS,
|
|
85
|
+
UNICODE_SINGLE_BOX_CHARS,
|
|
86
|
+
UNICODE_DOUBLE_BOX_CHARS,
|
|
87
|
+
UNICODE_ROUNDED_BOX_CHARS,
|
|
88
|
+
getASCIIBoxChars,
|
|
89
|
+
getUnicodeBoxChars,
|
|
90
|
+
UNICODE_SYMBOLS,
|
|
91
|
+
SPINNER_FRAMES,
|
|
92
|
+
DEFAULT_THEME_TOKENS,
|
|
93
|
+
DEFAULT_RENDER_CONTEXT,
|
|
94
|
+
} from './utils'
|
|
95
|
+
export type {
|
|
96
|
+
BoxChars,
|
|
97
|
+
TextAlign,
|
|
98
|
+
RendererRegistry,
|
|
99
|
+
RouterAdapter,
|
|
100
|
+
RouteMatchMode,
|
|
101
|
+
} from './utils'
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Component Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles per-component rendering and state management
|
|
5
|
+
* for the interactive renderer.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ComponentOptions,
|
|
12
|
+
ComponentManagerState,
|
|
13
|
+
FocusManagerState,
|
|
14
|
+
InputManagerState,
|
|
15
|
+
UINode,
|
|
16
|
+
InteractiveRenderer,
|
|
17
|
+
} from './types'
|
|
18
|
+
import { registerFocusable, unregisterFocusable } from './focus-manager'
|
|
19
|
+
import { handleInputKey } from './input-handler'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates the component manager state
|
|
23
|
+
*/
|
|
24
|
+
export function createComponentManagerState(): ComponentManagerState {
|
|
25
|
+
return {
|
|
26
|
+
components: new Map<string, ComponentOptions>(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a component
|
|
32
|
+
*/
|
|
33
|
+
export function registerComponent(
|
|
34
|
+
componentState: ComponentManagerState,
|
|
35
|
+
focusState: FocusManagerState,
|
|
36
|
+
id: string,
|
|
37
|
+
options: ComponentOptions,
|
|
38
|
+
destroyed?: boolean
|
|
39
|
+
): void {
|
|
40
|
+
if (destroyed) return
|
|
41
|
+
componentState.components.set(id, { ...options })
|
|
42
|
+
|
|
43
|
+
// Register as focusable if applicable
|
|
44
|
+
if (['select', 'checkbox', 'radiogroup', 'slider', 'tree', 'scrollview'].includes(options.type)) {
|
|
45
|
+
registerFocusable(focusState, id, { tabIndex: 0 }, destroyed)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Unregister a component
|
|
51
|
+
*/
|
|
52
|
+
export function unregisterComponent(
|
|
53
|
+
componentState: ComponentManagerState,
|
|
54
|
+
focusState: FocusManagerState,
|
|
55
|
+
id: string,
|
|
56
|
+
destroyed?: boolean
|
|
57
|
+
): void {
|
|
58
|
+
if (destroyed) return
|
|
59
|
+
componentState.components.delete(id)
|
|
60
|
+
unregisterFocusable(focusState, id, destroyed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get a component
|
|
65
|
+
*/
|
|
66
|
+
export function getComponent(
|
|
67
|
+
componentState: ComponentManagerState,
|
|
68
|
+
id: string,
|
|
69
|
+
destroyed?: boolean
|
|
70
|
+
): ComponentOptions | undefined {
|
|
71
|
+
if (destroyed) return undefined
|
|
72
|
+
return componentState.components.get(id)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update a component
|
|
77
|
+
*/
|
|
78
|
+
export function updateComponent(
|
|
79
|
+
componentState: ComponentManagerState,
|
|
80
|
+
id: string,
|
|
81
|
+
updates: Partial<ComponentOptions>,
|
|
82
|
+
destroyed?: boolean
|
|
83
|
+
): void {
|
|
84
|
+
if (destroyed) return
|
|
85
|
+
const existing = componentState.components.get(id)
|
|
86
|
+
if (existing) {
|
|
87
|
+
Object.assign(existing, updates)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get visible tree nodes
|
|
93
|
+
*/
|
|
94
|
+
export function getVisibleTreeNodes(
|
|
95
|
+
nodes: Array<{ id: string; children?: string[]; expanded?: boolean; parent?: string }>
|
|
96
|
+
): typeof nodes {
|
|
97
|
+
const result: typeof nodes = []
|
|
98
|
+
const rootNodes = nodes.filter(n => !n.parent)
|
|
99
|
+
|
|
100
|
+
const addNode = (node: (typeof nodes)[0]) => {
|
|
101
|
+
result.push(node)
|
|
102
|
+
if (node.expanded && node.children) {
|
|
103
|
+
for (const childId of node.children) {
|
|
104
|
+
const child = nodes.find(n => n.id === childId)
|
|
105
|
+
if (child) addNode(child)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const root of rootNodes) {
|
|
111
|
+
addNode(root)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle component key press
|
|
119
|
+
*/
|
|
120
|
+
export function handleComponentKeyInternal(
|
|
121
|
+
id: string,
|
|
122
|
+
component: ComponentOptions,
|
|
123
|
+
key: string
|
|
124
|
+
): boolean {
|
|
125
|
+
const {
|
|
126
|
+
type,
|
|
127
|
+
onChange,
|
|
128
|
+
onSelect,
|
|
129
|
+
onToggle,
|
|
130
|
+
options,
|
|
131
|
+
selectedIndex,
|
|
132
|
+
checked,
|
|
133
|
+
value,
|
|
134
|
+
min,
|
|
135
|
+
max,
|
|
136
|
+
step,
|
|
137
|
+
searchable,
|
|
138
|
+
onScroll,
|
|
139
|
+
nodes,
|
|
140
|
+
selectedId,
|
|
141
|
+
} = component as ComponentOptions & {
|
|
142
|
+
onChange?: (index: number, value?: string) => void
|
|
143
|
+
onSelect?: (index: number | string, value?: string) => void
|
|
144
|
+
onToggle?: (id: string, expanded: boolean) => void
|
|
145
|
+
options?: string[]
|
|
146
|
+
selectedIndex?: number
|
|
147
|
+
checked?: boolean
|
|
148
|
+
value?: number
|
|
149
|
+
min?: number
|
|
150
|
+
max?: number
|
|
151
|
+
step?: number
|
|
152
|
+
searchable?: boolean
|
|
153
|
+
onScroll?: (event: { scrollY: number }) => void
|
|
154
|
+
nodes?: Array<{ id: string; label: string; children?: string[]; expanded?: boolean; parent?: string }>
|
|
155
|
+
selectedId?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (type === 'select') {
|
|
159
|
+
const opts = options as string[]
|
|
160
|
+
const idx = selectedIndex as number
|
|
161
|
+
|
|
162
|
+
if (key === 'down' || key === 'j') {
|
|
163
|
+
const newIdx = Math.min(idx + 1, opts.length - 1)
|
|
164
|
+
component.selectedIndex = newIdx
|
|
165
|
+
onChange?.(newIdx, opts[newIdx])
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
if (key === 'up' || key === 'k') {
|
|
169
|
+
const newIdx = Math.max(idx - 1, 0)
|
|
170
|
+
component.selectedIndex = newIdx
|
|
171
|
+
onChange?.(newIdx, opts[newIdx])
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
if (key === 'enter') {
|
|
175
|
+
// If onSelect is provided, call it
|
|
176
|
+
if (onSelect) {
|
|
177
|
+
onSelect(idx, opts[idx])
|
|
178
|
+
}
|
|
179
|
+
// Toggle open state for dropdown behavior
|
|
180
|
+
component.isOpen = !component.isOpen
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
// Type-ahead search
|
|
184
|
+
if (searchable && key.length === 1) {
|
|
185
|
+
const matchIdx = opts.findIndex(o => o.toLowerCase().startsWith(key.toLowerCase()))
|
|
186
|
+
if (matchIdx !== -1) {
|
|
187
|
+
component.selectedIndex = matchIdx
|
|
188
|
+
onChange?.(matchIdx, opts[matchIdx])
|
|
189
|
+
}
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (type === 'checkbox') {
|
|
196
|
+
if (key === 'space' || key === 'enter') {
|
|
197
|
+
const newChecked = !checked
|
|
198
|
+
component.checked = newChecked
|
|
199
|
+
;(component.onChange as (checked: boolean) => void)?.(newChecked)
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (type === 'radiogroup') {
|
|
206
|
+
const opts = options as string[]
|
|
207
|
+
const idx = selectedIndex as number
|
|
208
|
+
|
|
209
|
+
if (key === 'down' || key === 'up') {
|
|
210
|
+
const newIdx = key === 'down' ? Math.min(idx + 1, opts.length - 1) : Math.max(idx - 1, 0)
|
|
211
|
+
component.selectedIndex = newIdx
|
|
212
|
+
;(component.onChange as (index: number) => void)?.(newIdx)
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
if (key === 'space') {
|
|
216
|
+
onSelect?.(idx, opts[idx])
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (type === 'slider') {
|
|
223
|
+
const v = value as number
|
|
224
|
+
const minV = min as number
|
|
225
|
+
const maxV = max as number
|
|
226
|
+
const stepV = step ?? 1
|
|
227
|
+
|
|
228
|
+
if (key === 'right' || key === 'up') {
|
|
229
|
+
const newV = Math.min(v + stepV, maxV)
|
|
230
|
+
component.value = newV
|
|
231
|
+
;(component.onChange as (value: number) => void)?.(newV)
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
if (key === 'left' || key === 'down') {
|
|
235
|
+
const newV = Math.max(v - stepV, minV)
|
|
236
|
+
component.value = newV
|
|
237
|
+
;(component.onChange as (value: number) => void)?.(newV)
|
|
238
|
+
return true
|
|
239
|
+
}
|
|
240
|
+
if (key === 'home') {
|
|
241
|
+
component.value = minV
|
|
242
|
+
;(component.onChange as (value: number) => void)?.(minV)
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
if (key === 'end') {
|
|
246
|
+
component.value = maxV
|
|
247
|
+
;(component.onChange as (value: number) => void)?.(maxV)
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (type === 'tree') {
|
|
254
|
+
const nodeList = nodes as Array<{
|
|
255
|
+
id: string
|
|
256
|
+
label: string
|
|
257
|
+
children?: string[]
|
|
258
|
+
expanded?: boolean
|
|
259
|
+
parent?: string
|
|
260
|
+
}>
|
|
261
|
+
const currentId = selectedId as string
|
|
262
|
+
const currentNode = nodeList.find(n => n.id === currentId)
|
|
263
|
+
|
|
264
|
+
if (key === 'enter' && currentNode?.children) {
|
|
265
|
+
const newExpanded = !currentNode.expanded
|
|
266
|
+
currentNode.expanded = newExpanded
|
|
267
|
+
onToggle?.(currentId, newExpanded)
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
if (key === 'down') {
|
|
271
|
+
// Find next visible node
|
|
272
|
+
const visibleNodes = getVisibleTreeNodes(nodeList)
|
|
273
|
+
const currentIndex = visibleNodes.findIndex(n => n.id === currentId)
|
|
274
|
+
if (currentIndex < visibleNodes.length - 1) {
|
|
275
|
+
const nextNode = visibleNodes[currentIndex + 1]
|
|
276
|
+
component.selectedId = nextNode.id
|
|
277
|
+
;(component.onSelect as (id: string) => void)?.(nextNode.id)
|
|
278
|
+
}
|
|
279
|
+
return true
|
|
280
|
+
}
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (type === 'scrollview') {
|
|
285
|
+
const currentScrollY = (component.scrollY as number) ?? 0
|
|
286
|
+
const viewportHeight = (component.viewportHeight as number) ?? 20
|
|
287
|
+
|
|
288
|
+
if (key === 'down') {
|
|
289
|
+
const newScrollY = currentScrollY + 1
|
|
290
|
+
component.scrollY = newScrollY
|
|
291
|
+
onScroll?.({ scrollY: newScrollY })
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
if (key === 'up') {
|
|
295
|
+
const newScrollY = Math.max(0, currentScrollY - 1)
|
|
296
|
+
component.scrollY = newScrollY
|
|
297
|
+
onScroll?.({ scrollY: newScrollY })
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
if (key === 'pagedown') {
|
|
301
|
+
const newScrollY = currentScrollY + viewportHeight
|
|
302
|
+
component.scrollY = newScrollY
|
|
303
|
+
onScroll?.({ scrollY: newScrollY })
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
if (key === 'pageup') {
|
|
307
|
+
const newScrollY = Math.max(0, currentScrollY - viewportHeight)
|
|
308
|
+
component.scrollY = newScrollY
|
|
309
|
+
onScroll?.({ scrollY: newScrollY })
|
|
310
|
+
return true
|
|
311
|
+
}
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle key for focused component
|
|
320
|
+
*/
|
|
321
|
+
export function handleComponentKey(
|
|
322
|
+
focusedId: string | null,
|
|
323
|
+
inputState: InputManagerState,
|
|
324
|
+
componentState: ComponentManagerState,
|
|
325
|
+
focusState: FocusManagerState,
|
|
326
|
+
key: string
|
|
327
|
+
): boolean {
|
|
328
|
+
if (!focusedId) return false
|
|
329
|
+
|
|
330
|
+
// Check if focused element is an input
|
|
331
|
+
const input = inputState.inputs.get(focusedId)
|
|
332
|
+
if (input) {
|
|
333
|
+
return handleInputKey(focusedId, input, key)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check if focused element is a component
|
|
337
|
+
const component = componentState.components.get(focusedId)
|
|
338
|
+
if (component) {
|
|
339
|
+
return handleComponentKeyInternal(focusedId, component, key)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check focusable handlers
|
|
343
|
+
const focusable = focusState.focusables.get(focusedId)
|
|
344
|
+
if (focusable) {
|
|
345
|
+
if (key === 'enter' && focusable.options.onActivate) {
|
|
346
|
+
focusable.options.onActivate({ id: focusedId })
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
if (key === 'space' && focusable.options.onToggle) {
|
|
350
|
+
focusable.options.onToggle({ id: focusedId })
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return false
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Clear component state on destroy
|
|
360
|
+
*/
|
|
361
|
+
export function clearComponentState(componentState: ComponentManagerState): void {
|
|
362
|
+
componentState.components.clear()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Registers a UINode with the interactive renderer (for component state management).
|
|
367
|
+
* This is used internally for focus management and event handling.
|
|
368
|
+
*/
|
|
369
|
+
export function registerInteractiveNode(
|
|
370
|
+
node: UINode,
|
|
371
|
+
renderer: InteractiveRenderer,
|
|
372
|
+
options?: { group?: string }
|
|
373
|
+
): void {
|
|
374
|
+
const { type, props } = node
|
|
375
|
+
const id = props.id as string | undefined
|
|
376
|
+
const group = options?.group
|
|
377
|
+
|
|
378
|
+
if (!id) return
|
|
379
|
+
|
|
380
|
+
// Handle different node types
|
|
381
|
+
switch (type) {
|
|
382
|
+
case 'button': {
|
|
383
|
+
const { onClick, disabled } = props as { onClick?: () => void; disabled?: boolean }
|
|
384
|
+
renderer.registerFocusable(id, {
|
|
385
|
+
tabIndex: 0,
|
|
386
|
+
disabled,
|
|
387
|
+
group,
|
|
388
|
+
onActivate: onClick ? () => !disabled && onClick() : undefined,
|
|
389
|
+
onToggle: onClick ? () => !disabled && onClick() : undefined,
|
|
390
|
+
})
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'input': {
|
|
395
|
+
const { placeholder, value, type: inputType } = props as {
|
|
396
|
+
placeholder?: string
|
|
397
|
+
value?: string
|
|
398
|
+
type?: string
|
|
399
|
+
}
|
|
400
|
+
renderer.registerComponent(id, {
|
|
401
|
+
type: 'input',
|
|
402
|
+
placeholder,
|
|
403
|
+
value: value ?? '',
|
|
404
|
+
displayValue: inputType === 'password' && value ? '*'.repeat(value.length) : value,
|
|
405
|
+
})
|
|
406
|
+
renderer.registerFocusable(id, { tabIndex: 0, group })
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'select': {
|
|
411
|
+
const { options: selectOptions } = props as { options?: Array<{ value: string; label: string }> }
|
|
412
|
+
renderer.registerComponent(id, {
|
|
413
|
+
type: 'select',
|
|
414
|
+
options: selectOptions ?? [],
|
|
415
|
+
selectedIndex: 0,
|
|
416
|
+
isOpen: false,
|
|
417
|
+
})
|
|
418
|
+
renderer.registerFocusable(id, { tabIndex: 0, group })
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
case 'dialog': {
|
|
423
|
+
const { open, onClose, children } = props as {
|
|
424
|
+
open?: boolean
|
|
425
|
+
onClose?: () => void
|
|
426
|
+
children?: UINode[]
|
|
427
|
+
}
|
|
428
|
+
if (open) {
|
|
429
|
+
// Push focus trap
|
|
430
|
+
renderer.pushFocusTrap(id)
|
|
431
|
+
|
|
432
|
+
// Register cancel handler
|
|
433
|
+
if (onClose) {
|
|
434
|
+
renderer.onCancel(onClose)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Render children with this dialog's group
|
|
438
|
+
if (children) {
|
|
439
|
+
for (const child of children) {
|
|
440
|
+
registerInteractiveNode(child, renderer, { group: id })
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
break
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'table': {
|
|
448
|
+
const { rows, onRowSelect, onCellSelect, cellNavigation, columns, data } = props as {
|
|
449
|
+
rows?: Array<{ id: string; cells: string[] }>
|
|
450
|
+
onRowSelect?: (id: string) => void
|
|
451
|
+
onCellSelect?: (rowId: string, cellIndex: number) => void
|
|
452
|
+
cellNavigation?: boolean
|
|
453
|
+
columns?: Array<{ key: string; header: string }>
|
|
454
|
+
data?: Array<Record<string, unknown>>
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get data from node.data (TDD) or props.data
|
|
458
|
+
const nodeData = node.data as Array<Record<string, unknown>> | undefined
|
|
459
|
+
const tableData = nodeData ?? data
|
|
460
|
+
|
|
461
|
+
let currentRowIndex = 0
|
|
462
|
+
let currentCellIndex = 0
|
|
463
|
+
|
|
464
|
+
renderer.registerComponent(id, {
|
|
465
|
+
type: 'table',
|
|
466
|
+
rows: rows ?? [],
|
|
467
|
+
columns: columns ?? [],
|
|
468
|
+
data: tableData ?? [],
|
|
469
|
+
selectedRowIndex: 0,
|
|
470
|
+
selectedCellIndex: 0,
|
|
471
|
+
cellNavigation,
|
|
472
|
+
onRowSelect,
|
|
473
|
+
onCellSelect,
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
renderer.registerFocusable(id, {
|
|
477
|
+
tabIndex: 0,
|
|
478
|
+
onActivate: () => {
|
|
479
|
+
if (rows && rows[currentRowIndex]) {
|
|
480
|
+
onRowSelect?.(rows[currentRowIndex].id)
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// Custom key handling for table
|
|
486
|
+
renderer.onKeyPress(
|
|
487
|
+
'down',
|
|
488
|
+
() => {
|
|
489
|
+
if (renderer.getFocusedId() !== id || !rows) return false
|
|
490
|
+
if (currentRowIndex < rows.length - 1) {
|
|
491
|
+
currentRowIndex++
|
|
492
|
+
onRowSelect?.(rows[currentRowIndex].id)
|
|
493
|
+
}
|
|
494
|
+
return true
|
|
495
|
+
},
|
|
496
|
+
{ priority: 10 }
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
renderer.onKeyPress(
|
|
500
|
+
'right',
|
|
501
|
+
() => {
|
|
502
|
+
if (renderer.getFocusedId() !== id || !rows || !cellNavigation) return false
|
|
503
|
+
const row = rows[currentRowIndex]
|
|
504
|
+
if (row && currentCellIndex < row.cells.length - 1) {
|
|
505
|
+
currentCellIndex++
|
|
506
|
+
onCellSelect?.(row.id, currentCellIndex)
|
|
507
|
+
}
|
|
508
|
+
return true
|
|
509
|
+
},
|
|
510
|
+
{ priority: 10 }
|
|
511
|
+
)
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case 'progress': {
|
|
516
|
+
const { value, max } = props as { value?: number; max?: number }
|
|
517
|
+
renderer.registerComponent(id, {
|
|
518
|
+
type: 'progress',
|
|
519
|
+
value: value ?? 0,
|
|
520
|
+
max: max ?? 100,
|
|
521
|
+
})
|
|
522
|
+
break
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
case 'spinner': {
|
|
526
|
+
renderer.registerComponent(id, {
|
|
527
|
+
type: 'spinner',
|
|
528
|
+
isAnimating: true,
|
|
529
|
+
})
|
|
530
|
+
break
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case 'scrollview': {
|
|
534
|
+
const { contentHeight, viewportHeight, onScroll, scrollY } = props as {
|
|
535
|
+
contentHeight?: number
|
|
536
|
+
viewportHeight?: number
|
|
537
|
+
onScroll?: (event: { scrollY: number }) => void
|
|
538
|
+
scrollY?: number
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
renderer.registerComponent(id, {
|
|
542
|
+
type: 'scrollview',
|
|
543
|
+
contentHeight: contentHeight ?? 100,
|
|
544
|
+
viewportHeight: viewportHeight ?? 20,
|
|
545
|
+
scrollY: scrollY ?? 0,
|
|
546
|
+
onScroll,
|
|
547
|
+
})
|
|
548
|
+
renderer.registerFocusable(id, { tabIndex: 0 })
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case 'list': {
|
|
553
|
+
const { items, numbered, taskList, children } = props as {
|
|
554
|
+
items?: Array<string | { text: string; checked?: boolean }>
|
|
555
|
+
numbered?: boolean
|
|
556
|
+
taskList?: boolean
|
|
557
|
+
children?: UINode[]
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
renderer.registerComponent(id, {
|
|
561
|
+
type: 'list',
|
|
562
|
+
items: items ?? [],
|
|
563
|
+
numbered: numbered ?? false,
|
|
564
|
+
taskList: taskList ?? false,
|
|
565
|
+
children,
|
|
566
|
+
})
|
|
567
|
+
break
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'card': {
|
|
571
|
+
const { title, subtitle, badge, titleAction, pairs, actions } = props as {
|
|
572
|
+
title?: string
|
|
573
|
+
subtitle?: string
|
|
574
|
+
badge?: { content: string; variant?: string }
|
|
575
|
+
titleAction?: { label: string; action?: string }
|
|
576
|
+
pairs?: Array<{ key: string; value: unknown }>
|
|
577
|
+
actions?: Array<{ label: string; action?: string }>
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
renderer.registerComponent(id, {
|
|
581
|
+
type: 'card',
|
|
582
|
+
title,
|
|
583
|
+
subtitle,
|
|
584
|
+
badge,
|
|
585
|
+
titleAction,
|
|
586
|
+
pairs,
|
|
587
|
+
actions,
|
|
588
|
+
})
|
|
589
|
+
break
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'metrics': {
|
|
593
|
+
const { metrics } = props as {
|
|
594
|
+
metrics?: Array<{ label: string; value: unknown; format?: string; unit?: string; trend?: string }>
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
renderer.registerComponent(id, {
|
|
598
|
+
type: 'metrics',
|
|
599
|
+
metrics: metrics ?? [],
|
|
600
|
+
})
|
|
601
|
+
break
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
case 'metric': {
|
|
605
|
+
const { label, value, format, unit } = props as {
|
|
606
|
+
label?: string
|
|
607
|
+
value?: unknown
|
|
608
|
+
format?: string
|
|
609
|
+
unit?: string
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
renderer.registerComponent(id, {
|
|
613
|
+
type: 'metric',
|
|
614
|
+
label,
|
|
615
|
+
value,
|
|
616
|
+
format,
|
|
617
|
+
unit,
|
|
618
|
+
})
|
|
619
|
+
break
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|