@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,802 @@
1
+ /**
2
+ * @mdxui/terminal Command Palette Renderer
3
+ *
4
+ * Multi-tier renderer for command palette 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 { type RouterAdapter } from './utils'
10
+
11
+ // ANSI constants
12
+ const RESET = '\x1b[0m'
13
+ const BOLD = '\x1b[1m'
14
+ const DIM = '\x1b[2m'
15
+ const UNDERLINE = '\x1b[4m'
16
+ const INVERSE = '\x1b[7m'
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ interface Command {
23
+ id: string
24
+ label: string
25
+ description?: string
26
+ shortcut?: string
27
+ icon?: string
28
+ category?: string
29
+ disabled?: boolean
30
+ keywords?: string[]
31
+ /** Path for route-based navigation commands */
32
+ path?: string
33
+ /** Action type: 'navigate' triggers router navigation, 'action' uses callback */
34
+ actionType?: 'navigate' | 'action'
35
+ }
36
+
37
+ interface CommandGroup {
38
+ id: string
39
+ label: string
40
+ commands: Command[]
41
+ }
42
+
43
+ interface CommandPaletteProps {
44
+ commands: Command[]
45
+ groups?: CommandGroup[]
46
+ searchQuery?: string
47
+ selectedIndex?: number
48
+ placeholder?: string
49
+ maxResults?: number
50
+ showShortcuts?: boolean
51
+ onSelect?: (commandId: string) => void
52
+ onQueryChange?: (query: string) => void
53
+ onClose?: () => void
54
+ /** Router adapter for navigation commands */
55
+ router?: RouterAdapter
56
+ }
57
+
58
+ interface CommandPaletteState {
59
+ commands: Command[]
60
+ groups?: CommandGroup[]
61
+ searchQuery: string
62
+ selectedIndex: number
63
+ wrapNavigation?: boolean
64
+ onSelect?: (commandId: string) => void
65
+ onQueryChange?: (query: string) => void
66
+ onClose?: () => void
67
+ /** Router adapter for navigation commands */
68
+ router?: RouterAdapter
69
+ }
70
+
71
+ // ============================================================================
72
+ // Main Render Function
73
+ // ============================================================================
74
+
75
+ export function renderCommandPalette(node: UINode, ctx: RenderContext): string {
76
+ const props = node.props as unknown as CommandPaletteProps
77
+ const {
78
+ searchQuery = '',
79
+ selectedIndex = 0,
80
+ placeholder = 'Search commands...',
81
+ maxResults = 10,
82
+ showShortcuts = true,
83
+ } = props
84
+
85
+ // Extract commands from node or props
86
+ const { commands, groups } = extractCommandsAndGroups(node, props)
87
+
88
+ const tier = ctx.tier
89
+ const maxWidth = ctx.width || 80
90
+ const lines: string[] = []
91
+
92
+ // Render the input box
93
+ lines.push(renderInputBox(searchQuery, placeholder, tier, ctx, maxWidth))
94
+
95
+ // Filter commands based on search
96
+ let filteredCommands = commands
97
+ let filteredGroups = groups
98
+
99
+ if (searchQuery) {
100
+ if (groups && groups.length > 0) {
101
+ filteredGroups = filterGroupedCommands(groups, searchQuery)
102
+ filteredCommands = []
103
+ } else {
104
+ filteredCommands = filterCommands(commands, searchQuery, { maxResults })
105
+ }
106
+ }
107
+
108
+ // Limit results
109
+ if (!groups && filteredCommands.length > maxResults) {
110
+ filteredCommands = filteredCommands.slice(0, maxResults)
111
+ }
112
+
113
+ // Check if no results
114
+ const hasResults = filteredGroups?.some(g => g.commands.length > 0) || filteredCommands.length > 0
115
+
116
+ if (!hasResults && searchQuery) {
117
+ lines.push(renderNoResults(tier, ctx))
118
+ } else if (filteredGroups && filteredGroups.length > 0) {
119
+ // Render grouped commands
120
+ let globalIndex = 0
121
+ for (const group of filteredGroups) {
122
+ if (group.commands.length === 0) continue
123
+ lines.push(renderGroupHeader(group.label, tier, ctx))
124
+ for (const cmd of group.commands) {
125
+ const isSelected = globalIndex === selectedIndex
126
+ lines.push(renderCommandItem(cmd, isSelected, showShortcuts, tier, ctx, maxWidth))
127
+ globalIndex++
128
+ }
129
+ }
130
+ } else {
131
+ // Render flat command list
132
+ for (let i = 0; i < filteredCommands.length; i++) {
133
+ const cmd = filteredCommands[i]
134
+ const isSelected = i === selectedIndex
135
+ lines.push(renderCommandItem(cmd, isSelected, showShortcuts, tier, ctx, maxWidth))
136
+ }
137
+ }
138
+
139
+ // Wrap in box for better tiers
140
+ return wrapInBox(lines, tier, ctx, maxWidth)
141
+ }
142
+
143
+ // ============================================================================
144
+ // Command Extraction
145
+ // ============================================================================
146
+
147
+ function extractCommandsAndGroups(node: UINode, props: CommandPaletteProps): {
148
+ commands: Command[]
149
+ groups?: CommandGroup[]
150
+ } {
151
+ if (props.commands && props.commands.length > 0) {
152
+ return { commands: props.commands, groups: props.groups }
153
+ }
154
+
155
+ // Extract from children
156
+ const commands: Command[] = []
157
+ const groups: CommandGroup[] = []
158
+ const rawChildren = node.children
159
+ const children: UINode[] = Array.isArray(rawChildren) ? rawChildren : []
160
+
161
+ for (const child of children) {
162
+ if (child.type === 'command-palette-list') {
163
+ const rawListChildren = child.children
164
+ const listChildren: UINode[] = Array.isArray(rawListChildren) ? rawListChildren : []
165
+ for (const item of listChildren) {
166
+ if (item.type === 'command-group') {
167
+ const groupCommands: Command[] = []
168
+ const rawGroupChildren = item.children
169
+ const groupChildren: UINode[] = Array.isArray(rawGroupChildren) ? rawGroupChildren : []
170
+ for (const cmdItem of groupChildren) {
171
+ if (cmdItem.type === 'command-item') {
172
+ groupCommands.push(extractCommand(cmdItem))
173
+ }
174
+ }
175
+ const itemProps = item.props || {}
176
+ groups.push({
177
+ id: itemProps.id as string,
178
+ label: itemProps.label as string,
179
+ commands: groupCommands,
180
+ })
181
+ } else if (item.type === 'command-item') {
182
+ commands.push(extractCommand(item))
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ return { commands, groups: groups.length > 0 ? groups : undefined }
189
+ }
190
+
191
+ function extractCommand(node: UINode): Command {
192
+ const props = node.props || {}
193
+ return {
194
+ id: props.id as string,
195
+ label: props.label as string,
196
+ description: props.description as string | undefined,
197
+ shortcut: props.shortcut as string | undefined,
198
+ icon: props.icon as string | undefined,
199
+ category: props.category as string | undefined,
200
+ disabled: props.disabled as boolean | undefined,
201
+ keywords: props.keywords as string[] | undefined,
202
+ }
203
+ }
204
+
205
+ // ============================================================================
206
+ // Search/Filter Functions
207
+ // ============================================================================
208
+
209
+ export function filterCommands(
210
+ commands: Command[],
211
+ query: string,
212
+ options?: { maxResults?: number; recentIds?: string[] }
213
+ ): Command[] {
214
+ const { maxResults, recentIds } = options || {}
215
+ const lowerQuery = query.toLowerCase()
216
+
217
+ // Score each command
218
+ const scored = commands.map((cmd, originalIndex) => {
219
+ let score = 0
220
+ let recentRank = -1
221
+
222
+ // Boost recent commands and track their order
223
+ if (recentIds?.includes(cmd.id)) {
224
+ score += 100
225
+ recentRank = recentIds.indexOf(cmd.id)
226
+ }
227
+
228
+ // For empty query, include all commands (but give base score to non-recent)
229
+ if (!query) {
230
+ if (recentRank < 0) {
231
+ score += 1 // Base score so they're included
232
+ }
233
+ } else {
234
+ // Match label
235
+ if (cmd.label.toLowerCase().includes(lowerQuery)) {
236
+ score += 50
237
+ }
238
+
239
+ // Fuzzy match label
240
+ if (fuzzyMatch(cmd.label, query)) {
241
+ score += 30
242
+ }
243
+
244
+ // Match keywords
245
+ if (cmd.keywords?.some(k => k.toLowerCase().includes(lowerQuery))) {
246
+ score += 40
247
+ }
248
+
249
+ // Match description
250
+ if (cmd.description?.toLowerCase().includes(lowerQuery)) {
251
+ score += 20
252
+ }
253
+ }
254
+
255
+ return { cmd, score, recentRank, originalIndex }
256
+ })
257
+
258
+ // Filter and sort
259
+ let results = scored
260
+ .filter(s => s.score > 0)
261
+ .sort((a, b) => {
262
+ // Primary sort by score
263
+ if (b.score !== a.score) return b.score - a.score
264
+ // Secondary sort by recent rank (lower is better, -1 means not recent)
265
+ if (a.recentRank >= 0 && b.recentRank >= 0) return a.recentRank - b.recentRank
266
+ if (a.recentRank >= 0) return -1
267
+ if (b.recentRank >= 0) return 1
268
+ // Tertiary sort by original order
269
+ return a.originalIndex - b.originalIndex
270
+ })
271
+ .map(s => s.cmd)
272
+
273
+ // Apply max results
274
+ if (maxResults && results.length > maxResults) {
275
+ results = results.slice(0, maxResults)
276
+ }
277
+
278
+ return results
279
+ }
280
+
281
+ export function filterGroupedCommands(groups: CommandGroup[], query: string): CommandGroup[] {
282
+ return groups
283
+ .map(group => ({
284
+ ...group,
285
+ commands: filterCommands(group.commands, query),
286
+ }))
287
+ .filter(group => group.commands.length > 0)
288
+ }
289
+
290
+ function fuzzyMatch(text: string, pattern: string): boolean {
291
+ const textLower = text.toLowerCase()
292
+ const patternLower = pattern.toLowerCase()
293
+
294
+ let textIndex = 0
295
+ let patternIndex = 0
296
+
297
+ while (textIndex < textLower.length && patternIndex < patternLower.length) {
298
+ if (textLower[textIndex] === patternLower[patternIndex]) {
299
+ patternIndex++
300
+ }
301
+ textIndex++
302
+ }
303
+
304
+ return patternIndex === patternLower.length
305
+ }
306
+
307
+ // ============================================================================
308
+ // Render Components
309
+ // ============================================================================
310
+
311
+ function renderInputBox(
312
+ query: string,
313
+ placeholder: string,
314
+ tier: RenderTier,
315
+ ctx: RenderContext,
316
+ maxWidth: number
317
+ ): string {
318
+ const displayText = query || placeholder
319
+ const isPlaceholder = !query
320
+ const cursorChar = tier === 'interactive' ? '\u2588' : ''
321
+
322
+ let content = displayText
323
+ if (tier === 'interactive') {
324
+ content = query + cursorChar
325
+ if (!query) {
326
+ content = ctx.theme.muted + placeholder + RESET
327
+ }
328
+ } else if (isPlaceholder) {
329
+ if (tier === 'ansi') {
330
+ content = ctx.theme.muted + placeholder + RESET
331
+ }
332
+ }
333
+
334
+ const prefix = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2315 ' : '> '
335
+ return prefix + content
336
+ }
337
+
338
+ function renderGroupHeader(label: string, tier: RenderTier, ctx: RenderContext): string {
339
+ if (tier === 'ansi' || tier === 'interactive') {
340
+ return ctx.theme.muted + label + RESET
341
+ }
342
+ if (tier === 'markdown') {
343
+ return `### ${label}`
344
+ }
345
+ return label
346
+ }
347
+
348
+ function renderNoResults(tier: RenderTier, ctx: RenderContext): string {
349
+ const message = 'No results found'
350
+ if (tier === 'ansi' || tier === 'interactive') {
351
+ return ctx.theme.muted + message + RESET
352
+ }
353
+ return message
354
+ }
355
+
356
+ function renderCommandItem(
357
+ cmd: Command,
358
+ isSelected: boolean,
359
+ showShortcuts: boolean,
360
+ tier: RenderTier,
361
+ ctx: RenderContext,
362
+ maxWidth: number
363
+ ): string {
364
+ const indent = ' '
365
+ let prefix = isSelected ? '> ' : ' '
366
+
367
+ // Build label with icon
368
+ let label = cmd.label
369
+ if (cmd.icon && (tier === 'unicode' || tier === 'ansi' || tier === 'interactive')) {
370
+ label = cmd.icon + ' ' + label
371
+ }
372
+
373
+ // Add description
374
+ let description = ''
375
+ if (cmd.description) {
376
+ if (tier === 'ansi' || tier === 'interactive') {
377
+ description = ' ' + ctx.theme.muted + cmd.description + RESET
378
+ } else {
379
+ description = ' - ' + cmd.description
380
+ }
381
+ }
382
+
383
+ // Add shortcut
384
+ let shortcut = ''
385
+ if (showShortcuts && cmd.shortcut) {
386
+ if (tier === 'markdown') {
387
+ shortcut = ' `' + cmd.shortcut + '`'
388
+ } else if (tier === 'ansi' || tier === 'interactive') {
389
+ shortcut = ' ' + ctx.theme.muted + cmd.shortcut + RESET
390
+ } else {
391
+ shortcut = ' ' + cmd.shortcut
392
+ }
393
+ }
394
+
395
+ // Build the line
396
+ let line = indent + prefix + label + description + shortcut
397
+
398
+ // Apply styling
399
+ if (tier === 'text') {
400
+ if (isSelected) {
401
+ line = indent + '> ' + label + description + shortcut
402
+ }
403
+ if (cmd.disabled) {
404
+ line += ' (disabled)'
405
+ }
406
+ } else if (tier === 'ansi' || tier === 'interactive') {
407
+ if (cmd.disabled) {
408
+ return indent + ' ' + ctx.theme.muted + label + RESET
409
+ }
410
+ if (isSelected) {
411
+ return INVERSE + indent + prefix + label + RESET + description + shortcut
412
+ }
413
+ }
414
+
415
+ return line
416
+ }
417
+
418
+ function wrapInBox(lines: string[], tier: RenderTier, ctx: RenderContext, maxWidth: number): string {
419
+ if (tier === 'text' || tier === 'markdown') {
420
+ return lines.join('\n')
421
+ }
422
+
423
+ // Add box border
424
+ const horizontal = tier === 'ascii' ? '-' : '\u2500'
425
+ const vertical = tier === 'ascii' ? '|' : '\u2502'
426
+ const topLeft = tier === 'ascii' ? '+' : '\u250C'
427
+ const topRight = tier === 'ascii' ? '+' : '\u2510'
428
+ const bottomLeft = tier === 'ascii' ? '+' : '\u2514'
429
+ const bottomRight = tier === 'ascii' ? '+' : '\u2518'
430
+
431
+ // Calculate content width - account for border (4 chars: "| " + " |")
432
+ const maxContentWidth = maxWidth - 4
433
+ const contentWidth = Math.min(maxContentWidth, Math.max(...lines.map(l => stripAnsi(l).length)))
434
+ // Ensure minimum width of 20, but not exceeding maxWidth - 4
435
+ const innerWidth = Math.min(Math.max(contentWidth, 20), maxContentWidth)
436
+
437
+ const boxLines: string[] = []
438
+ boxLines.push(topLeft + horizontal.repeat(innerWidth + 2) + topRight)
439
+
440
+ for (const line of lines) {
441
+ let truncatedLine = line
442
+ const stripped = stripAnsi(line)
443
+
444
+ // Truncate line if it exceeds inner width
445
+ if (stripped.length > innerWidth) {
446
+ truncatedLine = truncateLineToWidth(line, innerWidth, tier)
447
+ }
448
+
449
+ const truncatedStripped = stripAnsi(truncatedLine)
450
+ const padding = innerWidth - truncatedStripped.length
451
+ boxLines.push(vertical + ' ' + truncatedLine + ' '.repeat(Math.max(0, padding)) + ' ' + vertical)
452
+ }
453
+
454
+ boxLines.push(bottomLeft + horizontal.repeat(innerWidth + 2) + bottomRight)
455
+
456
+ return boxLines.join('\n')
457
+ }
458
+
459
+ function truncateLineToWidth(line: string, maxWidth: number, tier: RenderTier): string {
460
+ const stripped = stripAnsi(line)
461
+ if (stripped.length <= maxWidth) return line
462
+
463
+ const ellipsis = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
464
+ const ellipsisLen = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? 1 : 3
465
+ const RESET = '\x1b[0m'
466
+
467
+ let visibleLen = 0
468
+ let result = ''
469
+ let inEscape = false
470
+
471
+ for (let i = 0; i < line.length; i++) {
472
+ const char = line[i]
473
+
474
+ if (char === '\x1b') {
475
+ inEscape = true
476
+ result += char
477
+ continue
478
+ }
479
+
480
+ if (inEscape) {
481
+ result += char
482
+ if (char === 'm') {
483
+ inEscape = false
484
+ }
485
+ continue
486
+ }
487
+
488
+ if (visibleLen >= maxWidth - ellipsisLen) {
489
+ result += ellipsis + RESET
490
+ break
491
+ }
492
+
493
+ result += char
494
+ visibleLen++
495
+ }
496
+
497
+ return result
498
+ }
499
+
500
+ // ============================================================================
501
+ // Keyboard Bindings
502
+ // ============================================================================
503
+
504
+ export function getCommandPaletteKeyBindings(): Record<string, string> {
505
+ return {
506
+ up: 'select-prev',
507
+ down: 'select-next',
508
+ k: 'select-prev',
509
+ j: 'select-next',
510
+ enter: 'execute',
511
+ escape: 'close',
512
+ 'ctrl+n': 'select-next',
513
+ 'ctrl+p': 'select-prev',
514
+ 'ctrl+u': 'clear-query',
515
+ home: 'select-first',
516
+ end: 'select-last',
517
+ tab: 'select-next',
518
+ 'shift+tab': 'select-prev',
519
+ backspace: 'delete-char',
520
+ }
521
+ }
522
+
523
+ // ============================================================================
524
+ // State Management
525
+ // ============================================================================
526
+
527
+ export function createCommandPaletteState(config: {
528
+ commands: Command[]
529
+ groups?: CommandGroup[]
530
+ searchQuery?: string
531
+ selectedIndex?: number
532
+ wrapNavigation?: boolean
533
+ onSelect?: (commandId: string) => void
534
+ onQueryChange?: (query: string) => void
535
+ onClose?: () => void
536
+ /** Router adapter for navigation commands */
537
+ router?: RouterAdapter
538
+ }): CommandPaletteState {
539
+ return {
540
+ commands: config.commands,
541
+ groups: config.groups,
542
+ searchQuery: config.searchQuery || '',
543
+ selectedIndex: config.selectedIndex || 0,
544
+ wrapNavigation: config.wrapNavigation,
545
+ onSelect: config.onSelect,
546
+ onQueryChange: config.onQueryChange,
547
+ onClose: config.onClose,
548
+ router: config.router,
549
+ }
550
+ }
551
+
552
+ export function handleCommandPaletteKey(state: CommandPaletteState, key: string): CommandPaletteState {
553
+ const { commands, groups, searchQuery, selectedIndex, wrapNavigation, onSelect, onQueryChange, onClose, router } =
554
+ state
555
+
556
+ // Helper to execute a command (with router support)
557
+ const executeCommand = (cmd: Command): void => {
558
+ if (cmd.disabled) return
559
+
560
+ // Check if this is a navigation command
561
+ const isNavigationCommand = cmd.actionType === 'navigate' || (cmd.path && !cmd.actionType)
562
+
563
+ if (isNavigationCommand && cmd.path && router) {
564
+ // Use router for navigation commands
565
+ router.navigate(cmd.path)
566
+ } else if (onSelect) {
567
+ // Use callback for action commands or when no router available
568
+ onSelect(cmd.id)
569
+ }
570
+ }
571
+
572
+ // Get all commands (maintaining original order and indices)
573
+ let allCommands: Command[]
574
+ if (groups && groups.length > 0) {
575
+ allCommands = groups.flatMap(g => g.commands)
576
+ } else {
577
+ allCommands = commands
578
+ }
579
+
580
+ // Filter by search if needed
581
+ if (searchQuery) {
582
+ allCommands = filterCommands(allCommands, searchQuery)
583
+ }
584
+
585
+ const maxIndex = allCommands.length - 1
586
+
587
+ // Helper to find next non-disabled index
588
+ function findNextEnabled(fromIndex: number, direction: 1 | -1): number {
589
+ let newIndex = fromIndex + direction
590
+
591
+ // Keep moving in direction until we find a non-disabled command
592
+ while (newIndex >= 0 && newIndex <= maxIndex && allCommands[newIndex]?.disabled) {
593
+ newIndex += direction
594
+ }
595
+
596
+ // Handle boundary cases
597
+ if (newIndex < 0) {
598
+ if (wrapNavigation) {
599
+ // Wrap to end and search backwards for enabled
600
+ newIndex = maxIndex
601
+ while (newIndex > fromIndex && allCommands[newIndex]?.disabled) {
602
+ newIndex--
603
+ }
604
+ } else {
605
+ newIndex = 0
606
+ // Find first non-disabled from start
607
+ while (newIndex <= maxIndex && allCommands[newIndex]?.disabled) {
608
+ newIndex++
609
+ }
610
+ if (newIndex > maxIndex) newIndex = fromIndex
611
+ }
612
+ } else if (newIndex > maxIndex) {
613
+ if (wrapNavigation) {
614
+ // Wrap to start and search forward for enabled
615
+ newIndex = 0
616
+ while (newIndex < fromIndex && allCommands[newIndex]?.disabled) {
617
+ newIndex++
618
+ }
619
+ } else {
620
+ newIndex = maxIndex
621
+ // Find last non-disabled from end
622
+ while (newIndex >= 0 && allCommands[newIndex]?.disabled) {
623
+ newIndex--
624
+ }
625
+ if (newIndex < 0) newIndex = fromIndex
626
+ }
627
+ }
628
+
629
+ return newIndex
630
+ }
631
+
632
+ switch (key) {
633
+ case 'down':
634
+ case 'j':
635
+ case 'ctrl+n':
636
+ case 'tab': {
637
+ const newIndex = findNextEnabled(selectedIndex, 1)
638
+ return { ...state, selectedIndex: newIndex }
639
+ }
640
+
641
+ case 'up':
642
+ case 'k':
643
+ case 'ctrl+p':
644
+ case 'shift+tab': {
645
+ const newIndex = findNextEnabled(selectedIndex, -1)
646
+ return { ...state, selectedIndex: newIndex }
647
+ }
648
+
649
+ case 'home': {
650
+ // Find first non-disabled
651
+ let newIndex = 0
652
+ while (newIndex <= maxIndex && allCommands[newIndex]?.disabled) {
653
+ newIndex++
654
+ }
655
+ return { ...state, selectedIndex: newIndex > maxIndex ? 0 : newIndex }
656
+ }
657
+
658
+ case 'end': {
659
+ // Find last non-disabled
660
+ let newIndex = maxIndex
661
+ while (newIndex >= 0 && allCommands[newIndex]?.disabled) {
662
+ newIndex--
663
+ }
664
+ return { ...state, selectedIndex: newIndex < 0 ? 0 : newIndex }
665
+ }
666
+
667
+ case 'enter': {
668
+ const selectedCmd = allCommands[selectedIndex]
669
+ if (selectedCmd && !selectedCmd.disabled) {
670
+ executeCommand(selectedCmd)
671
+ }
672
+ return state
673
+ }
674
+
675
+ case 'escape': {
676
+ if (onClose) {
677
+ onClose()
678
+ }
679
+ return state
680
+ }
681
+
682
+ case 'backspace': {
683
+ if (searchQuery && onQueryChange) {
684
+ onQueryChange(searchQuery.slice(0, -1))
685
+ }
686
+ return { ...state, searchQuery: searchQuery.slice(0, -1), selectedIndex: 0 }
687
+ }
688
+
689
+ case 'ctrl+u': {
690
+ if (onQueryChange) {
691
+ onQueryChange('')
692
+ }
693
+ return { ...state, searchQuery: '', selectedIndex: 0 }
694
+ }
695
+
696
+ default: {
697
+ // Typing a character
698
+ if (key.length === 1 && /[a-zA-Z0-9 ]/.test(key)) {
699
+ const newQuery = searchQuery + key
700
+ if (onQueryChange) {
701
+ onQueryChange(newQuery)
702
+ }
703
+ return { ...state, searchQuery: newQuery, selectedIndex: 0 }
704
+ }
705
+ return state
706
+ }
707
+ }
708
+ }
709
+
710
+ // ============================================================================
711
+ // Utility Functions
712
+ // ============================================================================
713
+
714
+ function stripAnsi(str: string): string {
715
+ return str.replace(/\x1b\[[\d;]*m/g, '')
716
+ }
717
+
718
+ // ============================================================================
719
+ // Router Integration Functions
720
+ // ============================================================================
721
+
722
+ /**
723
+ * Creates navigation commands from route definitions.
724
+ * Useful for generating commands from a list of routes/pages.
725
+ *
726
+ * @param routes - Array of route definitions
727
+ * @returns Array of Command objects with navigation action type
728
+ */
729
+ export function navigationCommandsFromRoutes(
730
+ routes: Array<{
731
+ path: string
732
+ label: string
733
+ description?: string
734
+ icon?: string
735
+ shortcut?: string
736
+ keywords?: string[]
737
+ category?: string
738
+ }>
739
+ ): Command[] {
740
+ return routes.map((route) => ({
741
+ id: `nav:${route.path}`,
742
+ label: route.label,
743
+ description: route.description,
744
+ icon: route.icon,
745
+ shortcut: route.shortcut,
746
+ keywords: route.keywords,
747
+ category: route.category || 'Navigation',
748
+ path: route.path,
749
+ actionType: 'navigate' as const,
750
+ }))
751
+ }
752
+
753
+ /**
754
+ * Creates a command execution handler that integrates with a router adapter.
755
+ * Navigation commands will be handled by the router, action commands by the callback.
756
+ *
757
+ * @param commands - Array of commands (needed to look up command details by ID)
758
+ * @param router - Router adapter instance
759
+ * @param onAction - Callback for non-navigation actions
760
+ * @returns Command selection handler function
761
+ */
762
+ export function createRouterCommandHandler(
763
+ commands: Command[],
764
+ router: RouterAdapter,
765
+ onAction?: (commandId: string, command: Command) => void
766
+ ): (commandId: string) => void {
767
+ return (commandId: string) => {
768
+ const command = commands.find((c) => c.id === commandId)
769
+ if (!command) return
770
+
771
+ // Check if this is a navigation command
772
+ const isNavigationCommand = command.actionType === 'navigate' || (command.path && !command.actionType)
773
+
774
+ if (isNavigationCommand && command.path) {
775
+ router.navigate(command.path)
776
+ } else if (onAction) {
777
+ onAction(commandId, command)
778
+ }
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Filters commands to only include navigation commands.
784
+ * Useful for creating a page navigation palette.
785
+ *
786
+ * @param commands - Array of commands
787
+ * @returns Array of navigation-only commands
788
+ */
789
+ export function getNavigationCommands(commands: Command[]): Command[] {
790
+ return commands.filter((cmd) => cmd.actionType === 'navigate' || cmd.path)
791
+ }
792
+
793
+ /**
794
+ * Filters commands to only include action commands.
795
+ * Useful for creating an actions palette.
796
+ *
797
+ * @param commands - Array of commands
798
+ * @returns Array of action-only commands
799
+ */
800
+ export function getActionCommands(commands: Command[]): Command[] {
801
+ return commands.filter((cmd) => cmd.actionType === 'action' || (!cmd.path && !cmd.actionType))
802
+ }