@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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. 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
+ }