@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,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Sidebar Renderer
|
|
3
|
+
*
|
|
4
|
+
* Multi-tier renderer for sidebar navigation component.
|
|
5
|
+
* Supports all 6 tiers: text, markdown, ascii, unicode, ansi, interactive.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UINode, RenderContext, RenderTier } from '../core/types'
|
|
9
|
+
import {
|
|
10
|
+
getIndentStr,
|
|
11
|
+
padText,
|
|
12
|
+
UNICODE_SINGLE_BOX_CHARS,
|
|
13
|
+
ASCII_BOX_CHARS,
|
|
14
|
+
UNICODE_SYMBOLS,
|
|
15
|
+
matchPath,
|
|
16
|
+
findActiveItemInSections,
|
|
17
|
+
type RouteMatchMode,
|
|
18
|
+
type RouterAdapter,
|
|
19
|
+
} from './utils'
|
|
20
|
+
|
|
21
|
+
// ANSI constants
|
|
22
|
+
const RESET = '\x1b[0m'
|
|
23
|
+
const BOLD = '\x1b[1m'
|
|
24
|
+
const DIM = '\x1b[2m'
|
|
25
|
+
const UNDERLINE = '\x1b[4m'
|
|
26
|
+
const INVERSE = '\x1b[7m'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
interface SidebarItem {
|
|
33
|
+
id: string
|
|
34
|
+
label: string
|
|
35
|
+
icon?: string
|
|
36
|
+
path?: string
|
|
37
|
+
badge?: string | number
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
active?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SidebarSection {
|
|
43
|
+
id: string
|
|
44
|
+
title: string
|
|
45
|
+
collapsible?: boolean
|
|
46
|
+
collapsed?: boolean
|
|
47
|
+
items: SidebarItem[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SidebarProps {
|
|
51
|
+
sections: SidebarSection[]
|
|
52
|
+
activeId?: string
|
|
53
|
+
/** Current route path for automatic active item detection */
|
|
54
|
+
currentPath?: string
|
|
55
|
+
/** Route matching mode: 'exact', 'prefix', or 'pattern' */
|
|
56
|
+
routeMatchMode?: RouteMatchMode
|
|
57
|
+
collapsedIds?: string[]
|
|
58
|
+
onNavigate?: (id: string, path?: string) => void
|
|
59
|
+
onToggleCollapse?: (sectionId: string) => void
|
|
60
|
+
/** Router adapter for framework integration */
|
|
61
|
+
router?: RouterAdapter
|
|
62
|
+
width?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SidebarState {
|
|
66
|
+
sections: SidebarSection[]
|
|
67
|
+
focusedId: string
|
|
68
|
+
wrapNavigation?: boolean
|
|
69
|
+
onNavigate?: (id: string, path?: string) => void
|
|
70
|
+
onToggleCollapse?: (sectionId: string) => void
|
|
71
|
+
pendingG?: boolean
|
|
72
|
+
/** Current route path for automatic active detection */
|
|
73
|
+
currentPath?: string
|
|
74
|
+
/** Route matching mode */
|
|
75
|
+
routeMatchMode?: RouteMatchMode
|
|
76
|
+
/** Router adapter for framework integration */
|
|
77
|
+
router?: RouterAdapter
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Main Render Function
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export function renderSidebar(node: UINode, ctx: RenderContext): string {
|
|
85
|
+
const props = node.props as unknown as SidebarProps
|
|
86
|
+
const { sections, activeId, currentPath, routeMatchMode = 'prefix', router, width } = props
|
|
87
|
+
|
|
88
|
+
if (!sections || sections.length === 0) {
|
|
89
|
+
return ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Determine active item ID - prefer explicit activeId, then route-based detection
|
|
93
|
+
let effectiveActiveId = activeId
|
|
94
|
+
if (!effectiveActiveId && currentPath) {
|
|
95
|
+
effectiveActiveId = findActiveItemInSections(sections, currentPath, routeMatchMode)
|
|
96
|
+
}
|
|
97
|
+
if (!effectiveActiveId && router) {
|
|
98
|
+
const routerPath = router.getCurrentPath()
|
|
99
|
+
effectiveActiveId = findActiveItemInSections(sections, routerPath, routeMatchMode)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const maxWidth = width || ctx.width || 80
|
|
103
|
+
const lines: string[] = []
|
|
104
|
+
const tier = ctx.tier
|
|
105
|
+
|
|
106
|
+
for (const section of sections) {
|
|
107
|
+
// Render section header
|
|
108
|
+
const sectionLine = renderSectionHeader(section, tier)
|
|
109
|
+
lines.push(sectionLine)
|
|
110
|
+
|
|
111
|
+
// Only render items if not collapsed
|
|
112
|
+
if (!section.collapsed) {
|
|
113
|
+
for (const item of section.items) {
|
|
114
|
+
const itemActive = effectiveActiveId === item.id
|
|
115
|
+
const itemLine = renderItem(item, itemActive, tier, ctx)
|
|
116
|
+
lines.push(itemLine)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Apply width constraint and truncation
|
|
122
|
+
const result = lines.map((line) => {
|
|
123
|
+
const stripped = stripAnsi(line)
|
|
124
|
+
if (stripped.length > maxWidth) {
|
|
125
|
+
return truncateLine(line, maxWidth, tier)
|
|
126
|
+
}
|
|
127
|
+
return line
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return result.join('\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Section Header Rendering
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
function renderSectionHeader(section: SidebarSection, tier: RenderTier): string {
|
|
138
|
+
let indicator = ''
|
|
139
|
+
|
|
140
|
+
if (section.collapsible) {
|
|
141
|
+
if (tier === 'ascii' || tier === 'text') {
|
|
142
|
+
indicator = section.collapsed ? '+ ' : '- '
|
|
143
|
+
} else if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
|
|
144
|
+
indicator = section.collapsed ? '\u25B6 ' : '\u25BC '
|
|
145
|
+
} else if (tier === 'markdown') {
|
|
146
|
+
indicator = section.collapsed ? '+ ' : '- '
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const title = indicator + section.title
|
|
151
|
+
|
|
152
|
+
if (tier === 'ansi' || tier === 'interactive') {
|
|
153
|
+
return BOLD + title + RESET
|
|
154
|
+
} else if (tier === 'markdown') {
|
|
155
|
+
return '## ' + title
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return title
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Item Rendering
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
function renderItem(
|
|
166
|
+
item: SidebarItem,
|
|
167
|
+
active: boolean,
|
|
168
|
+
tier: RenderTier,
|
|
169
|
+
ctx: RenderContext
|
|
170
|
+
): string {
|
|
171
|
+
const indent = getIndentStr(1)
|
|
172
|
+
let prefix = ' '
|
|
173
|
+
let suffix = ''
|
|
174
|
+
|
|
175
|
+
// Active indicator
|
|
176
|
+
if (active) {
|
|
177
|
+
if (tier === 'text') {
|
|
178
|
+
prefix = '> '
|
|
179
|
+
} else if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
|
|
180
|
+
prefix = '\u25BA '
|
|
181
|
+
} else if (tier === 'ascii') {
|
|
182
|
+
prefix = '> '
|
|
183
|
+
} else if (tier === 'markdown') {
|
|
184
|
+
prefix = '> '
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build label with icon
|
|
189
|
+
let label = item.label
|
|
190
|
+
if (item.icon && (tier === 'unicode' || tier === 'ansi' || tier === 'interactive')) {
|
|
191
|
+
label = item.icon + ' ' + label
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Badge
|
|
195
|
+
if (item.badge !== undefined) {
|
|
196
|
+
suffix = ` (${item.badge})`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Disabled state
|
|
200
|
+
if (item.disabled) {
|
|
201
|
+
if (tier === 'text' || tier === 'markdown' || tier === 'ascii') {
|
|
202
|
+
suffix += ' (disabled)'
|
|
203
|
+
} else if (tier === 'ansi' || tier === 'interactive') {
|
|
204
|
+
// Will apply muted styling below
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const content = indent + prefix + label + suffix
|
|
209
|
+
|
|
210
|
+
// Apply styling
|
|
211
|
+
if (tier === 'ansi' || tier === 'interactive') {
|
|
212
|
+
if (item.disabled) {
|
|
213
|
+
return ctx.theme.muted + content + RESET
|
|
214
|
+
}
|
|
215
|
+
if (active) {
|
|
216
|
+
if (tier === 'interactive') {
|
|
217
|
+
return INVERSE + content + RESET
|
|
218
|
+
}
|
|
219
|
+
return ctx.theme.primary + BOLD + content + RESET
|
|
220
|
+
}
|
|
221
|
+
return content
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return content
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Keyboard Bindings
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
export function getSidebarKeyBindings(): Record<string, string> {
|
|
232
|
+
return {
|
|
233
|
+
j: 'move-down',
|
|
234
|
+
k: 'move-up',
|
|
235
|
+
enter: 'select',
|
|
236
|
+
space: 'toggle-collapse',
|
|
237
|
+
h: 'collapse',
|
|
238
|
+
l: 'expand',
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// State Management
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
export function createSidebarState(config: {
|
|
247
|
+
sections: SidebarSection[]
|
|
248
|
+
focusedId?: string
|
|
249
|
+
wrapNavigation?: boolean
|
|
250
|
+
onNavigate?: (id: string, path?: string) => void
|
|
251
|
+
onToggleCollapse?: (sectionId: string) => void
|
|
252
|
+
/** Current route path for automatic active detection */
|
|
253
|
+
currentPath?: string
|
|
254
|
+
/** Route matching mode */
|
|
255
|
+
routeMatchMode?: RouteMatchMode
|
|
256
|
+
/** Router adapter for framework integration */
|
|
257
|
+
router?: RouterAdapter
|
|
258
|
+
}): SidebarState {
|
|
259
|
+
const { sections, focusedId, wrapNavigation, onNavigate, onToggleCollapse, currentPath, routeMatchMode, router } =
|
|
260
|
+
config
|
|
261
|
+
|
|
262
|
+
// Get all focusable IDs
|
|
263
|
+
const focusableIds = getAllFocusableIds(sections)
|
|
264
|
+
const initialFocusedId = focusedId || focusableIds[0] || ''
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
sections,
|
|
268
|
+
focusedId: initialFocusedId,
|
|
269
|
+
wrapNavigation,
|
|
270
|
+
onNavigate,
|
|
271
|
+
onToggleCollapse,
|
|
272
|
+
pendingG: false,
|
|
273
|
+
currentPath,
|
|
274
|
+
routeMatchMode,
|
|
275
|
+
router,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function handleSidebarKey(state: SidebarState, key: string): SidebarState {
|
|
280
|
+
const focusableIds = getAllFocusableIds(state.sections)
|
|
281
|
+
const currentIndex = focusableIds.indexOf(state.focusedId)
|
|
282
|
+
|
|
283
|
+
// Handle gg sequence
|
|
284
|
+
if (key === 'g') {
|
|
285
|
+
if (state.pendingG) {
|
|
286
|
+
// Second g: go to first
|
|
287
|
+
return {
|
|
288
|
+
...state,
|
|
289
|
+
focusedId: focusableIds[0] || state.focusedId,
|
|
290
|
+
pendingG: false,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { ...state, pendingG: true }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Clear pending g for any other key
|
|
297
|
+
const newState = { ...state, pendingG: false }
|
|
298
|
+
|
|
299
|
+
switch (key) {
|
|
300
|
+
case 'j':
|
|
301
|
+
case 'down': {
|
|
302
|
+
const nextIndex = findNextFocusable(state.sections, focusableIds, currentIndex, 1, state.wrapNavigation)
|
|
303
|
+
return { ...newState, focusedId: focusableIds[nextIndex] || state.focusedId }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'k':
|
|
307
|
+
case 'up': {
|
|
308
|
+
const prevIndex = findNextFocusable(state.sections, focusableIds, currentIndex, -1, state.wrapNavigation)
|
|
309
|
+
return { ...newState, focusedId: focusableIds[prevIndex] || state.focusedId }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case 'G': {
|
|
313
|
+
// Go to last item
|
|
314
|
+
return { ...newState, focusedId: focusableIds[focusableIds.length - 1] || state.focusedId }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
case 'enter': {
|
|
318
|
+
const item = findItemById(state.sections, state.focusedId)
|
|
319
|
+
if (item && !item.disabled) {
|
|
320
|
+
// Use router adapter if available, otherwise use callback
|
|
321
|
+
if (item.path && state.router) {
|
|
322
|
+
state.router.navigate(item.path)
|
|
323
|
+
} else if (state.onNavigate) {
|
|
324
|
+
state.onNavigate(item.id, item.path)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return newState
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'space': {
|
|
331
|
+
// Toggle collapse on section header
|
|
332
|
+
const section = findSectionById(state.sections, state.focusedId)
|
|
333
|
+
if (section && section.collapsible && state.onToggleCollapse) {
|
|
334
|
+
state.onToggleCollapse(section.id)
|
|
335
|
+
}
|
|
336
|
+
return newState
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'h': {
|
|
340
|
+
// Collapse parent section when on an item
|
|
341
|
+
const parentSection = findParentSection(state.sections, state.focusedId)
|
|
342
|
+
if (parentSection && parentSection.collapsible && !parentSection.collapsed && state.onToggleCollapse) {
|
|
343
|
+
state.onToggleCollapse(parentSection.id)
|
|
344
|
+
}
|
|
345
|
+
return newState
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case 'l': {
|
|
349
|
+
// Expand section when focused on collapsed section
|
|
350
|
+
const section = findSectionById(state.sections, state.focusedId)
|
|
351
|
+
if (section && section.collapsible && section.collapsed && state.onToggleCollapse) {
|
|
352
|
+
state.onToggleCollapse(section.id)
|
|
353
|
+
}
|
|
354
|
+
return newState
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
default:
|
|
358
|
+
return newState
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// Helper Functions
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
function getAllFocusableIds(sections: SidebarSection[]): string[] {
|
|
367
|
+
const ids: string[] = []
|
|
368
|
+
for (const section of sections) {
|
|
369
|
+
// Only add section id if section is collapsible (so it can be focused for collapse toggle)
|
|
370
|
+
// But navigation tests expect only items to be navigable, so we don't add sections
|
|
371
|
+
if (!section.collapsed) {
|
|
372
|
+
for (const item of section.items) {
|
|
373
|
+
if (!item.disabled) {
|
|
374
|
+
ids.push(item.id)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return ids
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function getAllItemIds(sections: SidebarSection[]): string[] {
|
|
383
|
+
const ids: string[] = []
|
|
384
|
+
for (const section of sections) {
|
|
385
|
+
if (!section.collapsed) {
|
|
386
|
+
for (const item of section.items) {
|
|
387
|
+
if (!item.disabled) {
|
|
388
|
+
ids.push(item.id)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return ids
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function findNextFocusable(
|
|
397
|
+
sections: SidebarSection[],
|
|
398
|
+
focusableIds: string[],
|
|
399
|
+
currentIndex: number,
|
|
400
|
+
direction: 1 | -1,
|
|
401
|
+
wrap?: boolean
|
|
402
|
+
): number {
|
|
403
|
+
if (focusableIds.length === 0) return 0
|
|
404
|
+
|
|
405
|
+
let nextIndex = currentIndex + direction
|
|
406
|
+
|
|
407
|
+
if (wrap) {
|
|
408
|
+
if (nextIndex < 0) nextIndex = focusableIds.length - 1
|
|
409
|
+
if (nextIndex >= focusableIds.length) nextIndex = 0
|
|
410
|
+
} else {
|
|
411
|
+
if (nextIndex < 0) nextIndex = 0
|
|
412
|
+
if (nextIndex >= focusableIds.length) nextIndex = focusableIds.length - 1
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Skip disabled items
|
|
416
|
+
const id = focusableIds[nextIndex]
|
|
417
|
+
const item = findItemById(sections, id)
|
|
418
|
+
if (item && item.disabled) {
|
|
419
|
+
return findNextFocusable(sections, focusableIds, nextIndex, direction, wrap)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return nextIndex
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function findItemById(sections: SidebarSection[], id: string): SidebarItem | undefined {
|
|
426
|
+
for (const section of sections) {
|
|
427
|
+
for (const item of section.items) {
|
|
428
|
+
if (item.id === id) return item
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return undefined
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function findSectionById(sections: SidebarSection[], id: string): SidebarSection | undefined {
|
|
435
|
+
return sections.find((s) => s.id === id)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function findParentSection(sections: SidebarSection[], itemId: string): SidebarSection | undefined {
|
|
439
|
+
for (const section of sections) {
|
|
440
|
+
for (const item of section.items) {
|
|
441
|
+
if (item.id === itemId) return section
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return undefined
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function stripAnsi(str: string): string {
|
|
448
|
+
return str.replace(/\x1b\[[\d;]*m/g, '')
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function truncateLine(line: string, maxWidth: number, tier: RenderTier): string {
|
|
452
|
+
const stripped = stripAnsi(line)
|
|
453
|
+
if (stripped.length <= maxWidth) return line
|
|
454
|
+
|
|
455
|
+
const ellipsis = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
|
|
456
|
+
const ellipsisLen = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? 1 : 3
|
|
457
|
+
|
|
458
|
+
// For lines with ANSI codes, we need to carefully truncate
|
|
459
|
+
let visibleLen = 0
|
|
460
|
+
let result = ''
|
|
461
|
+
let inEscape = false
|
|
462
|
+
|
|
463
|
+
for (let i = 0; i < line.length; i++) {
|
|
464
|
+
const char = line[i]
|
|
465
|
+
|
|
466
|
+
if (char === '\x1b') {
|
|
467
|
+
inEscape = true
|
|
468
|
+
result += char
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (inEscape) {
|
|
473
|
+
result += char
|
|
474
|
+
if (char === 'm') {
|
|
475
|
+
inEscape = false
|
|
476
|
+
}
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (visibleLen >= maxWidth - ellipsisLen) {
|
|
481
|
+
result += ellipsis + RESET
|
|
482
|
+
break
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
result += char
|
|
486
|
+
visibleLen++
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return result
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ============================================================================
|
|
493
|
+
// Router Integration Functions
|
|
494
|
+
// ============================================================================
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Gets the active sidebar item ID based on the current route.
|
|
498
|
+
* Uses path-based matching to determine which item should be highlighted.
|
|
499
|
+
*
|
|
500
|
+
* @param sections - Sidebar sections containing items with paths
|
|
501
|
+
* @param currentPath - Current route path
|
|
502
|
+
* @param mode - Route matching mode ('exact', 'prefix', or 'pattern')
|
|
503
|
+
* @returns The ID of the active item, or undefined if no match
|
|
504
|
+
*/
|
|
505
|
+
export function getActiveItemByRoute(
|
|
506
|
+
sections: SidebarSection[],
|
|
507
|
+
currentPath: string,
|
|
508
|
+
mode: RouteMatchMode = 'prefix'
|
|
509
|
+
): string | undefined {
|
|
510
|
+
return findActiveItemInSections(sections, currentPath, mode)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Checks if a specific sidebar item is active based on the current route.
|
|
515
|
+
*
|
|
516
|
+
* @param item - Sidebar item to check
|
|
517
|
+
* @param currentPath - Current route path
|
|
518
|
+
* @param mode - Route matching mode
|
|
519
|
+
* @returns true if the item is active
|
|
520
|
+
*/
|
|
521
|
+
export function isItemActiveByRoute(
|
|
522
|
+
item: SidebarItem,
|
|
523
|
+
currentPath: string,
|
|
524
|
+
mode: RouteMatchMode = 'prefix'
|
|
525
|
+
): boolean {
|
|
526
|
+
if (!item.path) return false
|
|
527
|
+
return matchPath(currentPath, item.path, mode)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Creates a navigation handler that integrates with a router adapter.
|
|
532
|
+
* Returns a function compatible with the onNavigate callback.
|
|
533
|
+
*
|
|
534
|
+
* @param router - Router adapter instance
|
|
535
|
+
* @param fallback - Optional fallback handler if router navigation fails
|
|
536
|
+
* @returns Navigation handler function
|
|
537
|
+
*/
|
|
538
|
+
export function createRouterNavigationHandler(
|
|
539
|
+
router: RouterAdapter,
|
|
540
|
+
fallback?: (id: string, path?: string) => void
|
|
541
|
+
): (id: string, path?: string) => void {
|
|
542
|
+
return (id: string, path?: string) => {
|
|
543
|
+
if (path) {
|
|
544
|
+
router.navigate(path)
|
|
545
|
+
} else if (fallback) {
|
|
546
|
+
fallback(id, path)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|