@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,480 @@
1
+ /**
2
+ * @mdxui/terminal Breadcrumb Renderer
3
+ *
4
+ * Multi-tier renderer for breadcrumb 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
+ generateBreadcrumbSegments,
11
+ formatSegmentLabel,
12
+ type RouterAdapter,
13
+ type RouteMatchMode,
14
+ } from './utils'
15
+
16
+ // ANSI constants
17
+ const RESET = '\x1b[0m'
18
+ const BOLD = '\x1b[1m'
19
+ const DIM = '\x1b[2m'
20
+ const UNDERLINE = '\x1b[4m'
21
+ const INVERSE = '\x1b[7m'
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ interface BreadcrumbSegment {
28
+ label: string
29
+ path?: string
30
+ icon?: string
31
+ }
32
+
33
+ interface BreadcrumbProps {
34
+ segments: BreadcrumbSegment[]
35
+ separator?: string
36
+ maxItems?: number
37
+ showHome?: boolean
38
+ homeLabel?: string
39
+ homeIcon?: string
40
+ onNavigate?: (path: string) => void
41
+ /** Current route path - if provided, segments can be auto-generated */
42
+ currentPath?: string
43
+ /** Custom labels for specific paths (used with currentPath) */
44
+ pathLabels?: Record<string, string>
45
+ /** Router adapter for framework integration */
46
+ router?: RouterAdapter
47
+ }
48
+
49
+ interface BreadcrumbState {
50
+ segments: BreadcrumbSegment[]
51
+ focusedIndex: number
52
+ wrapNavigation?: boolean
53
+ onNavigate?: (path: string) => void
54
+ onBlur?: () => void
55
+ /** Router adapter for framework integration */
56
+ router?: RouterAdapter
57
+ }
58
+
59
+ // ============================================================================
60
+ // Tier-specific Separators
61
+ // ============================================================================
62
+
63
+ function getSeparator(tier: RenderTier, customSeparator?: string): string {
64
+ if (customSeparator) return customSeparator
65
+
66
+ switch (tier) {
67
+ case 'text':
68
+ return ' / '
69
+ case 'markdown':
70
+ return ' / '
71
+ case 'ascii':
72
+ return ' > '
73
+ case 'unicode':
74
+ case 'ansi':
75
+ case 'interactive':
76
+ return ' \u203A ' // Single right-pointing angle quotation mark
77
+ default:
78
+ return ' / '
79
+ }
80
+ }
81
+
82
+ // ============================================================================
83
+ // Main Render Function
84
+ // ============================================================================
85
+
86
+ export function renderBreadcrumb(node: UINode, ctx: RenderContext): string {
87
+ const props = node.props as unknown as BreadcrumbProps
88
+ let { segments, separator, maxItems, showHome, homeLabel, homeIcon, currentPath, pathLabels, router } = props
89
+
90
+ // Generate segments from currentPath if not provided
91
+ if ((!segments || segments.length === 0) && currentPath) {
92
+ segments = generateBreadcrumbSegments(currentPath, {
93
+ labels: pathLabels,
94
+ includeHome: showHome !== false,
95
+ homeLabel: homeLabel || 'Home',
96
+ })
97
+ // Don't add home again if we just generated segments with home
98
+ showHome = false
99
+ }
100
+ // Also try to get path from router if no segments provided
101
+ if ((!segments || segments.length === 0) && router) {
102
+ const routerPath = router.getCurrentPath()
103
+ segments = generateBreadcrumbSegments(routerPath, {
104
+ labels: pathLabels,
105
+ includeHome: showHome !== false,
106
+ homeLabel: homeLabel || 'Home',
107
+ })
108
+ // Don't add home again if we just generated segments with home
109
+ showHome = false
110
+ }
111
+
112
+ if (!segments || segments.length === 0) {
113
+ return ''
114
+ }
115
+
116
+ const tier = ctx.tier
117
+ const maxWidth = ctx.width || 80
118
+
119
+ // Prepend home segment if requested
120
+ if (showHome) {
121
+ const homeSegment: BreadcrumbSegment = {
122
+ label: homeLabel || 'Home',
123
+ path: '/',
124
+ icon: homeIcon,
125
+ }
126
+ segments = [homeSegment, ...segments]
127
+ }
128
+
129
+ // Handle truncation with maxItems
130
+ let displaySegments = segments
131
+ let showEllipsis = false
132
+
133
+ if (maxItems && segments.length > maxItems) {
134
+ // Show first, ellipsis, and last (maxItems - 1) segments
135
+ const firstSegment = segments[0]
136
+ const lastSegments = segments.slice(-(maxItems - 1))
137
+ displaySegments = [firstSegment, ...lastSegments]
138
+ showEllipsis = true
139
+ }
140
+
141
+ // Get the separator for this tier
142
+ const sep = getSeparator(tier, separator)
143
+
144
+ // Render segments
145
+ const parts: string[] = []
146
+
147
+ for (let i = 0; i < displaySegments.length; i++) {
148
+ // Insert ellipsis after first segment if needed
149
+ if (showEllipsis && i === 1) {
150
+ const ellipsis = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
151
+ parts.push(ellipsis)
152
+ }
153
+
154
+ const segment = displaySegments[i]
155
+ const isLast = i === displaySegments.length - 1
156
+ const hasPath = !!segment.path && !isLast
157
+
158
+ parts.push(renderSegment(segment, isLast, hasPath, tier, ctx))
159
+ }
160
+
161
+ let result = parts.join(sep)
162
+
163
+ // Apply width truncation
164
+ const stripped = stripAnsi(result)
165
+ if (stripped.length > maxWidth) {
166
+ result = truncateResult(result, segments, maxWidth, tier, ctx)
167
+ }
168
+
169
+ return result
170
+ }
171
+
172
+ // ============================================================================
173
+ // Segment Rendering
174
+ // ============================================================================
175
+
176
+ function renderSegment(
177
+ segment: BreadcrumbSegment,
178
+ isCurrent: boolean,
179
+ hasPath: boolean,
180
+ tier: RenderTier,
181
+ ctx: RenderContext
182
+ ): string {
183
+ let label = segment.label
184
+
185
+ // Add icon for unicode/ansi/interactive tiers
186
+ if (segment.icon && (tier === 'unicode' || tier === 'ansi' || tier === 'interactive')) {
187
+ label = segment.icon + ' ' + label
188
+ }
189
+
190
+ switch (tier) {
191
+ case 'text':
192
+ return label
193
+
194
+ case 'markdown':
195
+ if (hasPath && segment.path) {
196
+ return `[${segment.label}](${segment.path})`
197
+ }
198
+ return label
199
+
200
+ case 'ascii':
201
+ return label
202
+
203
+ case 'unicode':
204
+ return label
205
+
206
+ case 'ansi':
207
+ if (isCurrent) {
208
+ return BOLD + label + RESET
209
+ }
210
+ if (hasPath) {
211
+ return ctx.theme.primary + UNDERLINE + label + RESET
212
+ }
213
+ return label
214
+
215
+ case 'interactive':
216
+ if (isCurrent) {
217
+ return BOLD + label + RESET
218
+ }
219
+ if (hasPath) {
220
+ return ctx.theme.primary + UNDERLINE + label + RESET
221
+ }
222
+ return label
223
+
224
+ default:
225
+ return label
226
+ }
227
+ }
228
+
229
+ // ============================================================================
230
+ // Truncation
231
+ // ============================================================================
232
+
233
+ function truncateResult(
234
+ result: string,
235
+ segments: BreadcrumbSegment[],
236
+ maxWidth: number,
237
+ tier: RenderTier,
238
+ ctx: RenderContext
239
+ ): string {
240
+ // For very narrow widths, just return ellipsis
241
+ if (maxWidth < 10) {
242
+ return tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
243
+ }
244
+
245
+ // Try to truncate individual segment labels
246
+ const ellipsis = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
247
+ const sep = getSeparator(tier, undefined)
248
+
249
+ // Truncate long labels
250
+ const truncatedParts: string[] = []
251
+ const targetLabelWidth = Math.floor((maxWidth - (segments.length - 1) * sep.length) / segments.length)
252
+
253
+ for (let i = 0; i < segments.length; i++) {
254
+ const segment = segments[i]
255
+ const isLast = i === segments.length - 1
256
+ const hasPath = !!segment.path && !isLast
257
+
258
+ let label = segment.label
259
+ if (label.length > targetLabelWidth && targetLabelWidth > 3) {
260
+ label = label.slice(0, targetLabelWidth - 1) + ellipsis
261
+ }
262
+
263
+ const modifiedSegment = { ...segment, label }
264
+ truncatedParts.push(renderSegment(modifiedSegment, isLast, hasPath, tier, ctx))
265
+ }
266
+
267
+ const truncatedResult = truncatedParts.join(sep)
268
+ const stripped = stripAnsi(truncatedResult)
269
+
270
+ // Final truncation if still too long
271
+ if (stripped.length > maxWidth) {
272
+ return truncateLine(truncatedResult, maxWidth, tier)
273
+ }
274
+
275
+ return truncatedResult
276
+ }
277
+
278
+ function truncateLine(line: string, maxWidth: number, tier: RenderTier): string {
279
+ const stripped = stripAnsi(line)
280
+ if (stripped.length <= maxWidth) return line
281
+
282
+ const ellipsis = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '\u2026' : '...'
283
+ const ellipsisLen = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? 1 : 3
284
+
285
+ let visibleLen = 0
286
+ let result = ''
287
+ let inEscape = false
288
+
289
+ for (let i = 0; i < line.length; i++) {
290
+ const char = line[i]
291
+
292
+ if (char === '\x1b') {
293
+ inEscape = true
294
+ result += char
295
+ continue
296
+ }
297
+
298
+ if (inEscape) {
299
+ result += char
300
+ if (char === 'm') {
301
+ inEscape = false
302
+ }
303
+ continue
304
+ }
305
+
306
+ if (visibleLen >= maxWidth - ellipsisLen) {
307
+ result += ellipsis + RESET
308
+ break
309
+ }
310
+
311
+ result += char
312
+ visibleLen++
313
+ }
314
+
315
+ return result
316
+ }
317
+
318
+ function stripAnsi(str: string): string {
319
+ return str.replace(/\x1b\[[\d;]*m/g, '')
320
+ }
321
+
322
+ // ============================================================================
323
+ // Keyboard Bindings
324
+ // ============================================================================
325
+
326
+ export function getBreadcrumbKeyBindings(): Record<string, string> {
327
+ return {
328
+ h: 'focus-prev',
329
+ l: 'focus-next',
330
+ left: 'focus-prev',
331
+ right: 'focus-next',
332
+ enter: 'navigate',
333
+ escape: 'blur',
334
+ home: 'focus-first',
335
+ end: 'focus-last',
336
+ }
337
+ }
338
+
339
+ // ============================================================================
340
+ // State Management
341
+ // ============================================================================
342
+
343
+ export function createBreadcrumbState(config: {
344
+ segments: BreadcrumbSegment[]
345
+ focusedIndex?: number
346
+ wrapNavigation?: boolean
347
+ onNavigate?: (path: string) => void
348
+ onBlur?: () => void
349
+ /** Router adapter for framework integration */
350
+ router?: RouterAdapter
351
+ }): BreadcrumbState {
352
+ const { segments, focusedIndex, wrapNavigation, onNavigate, onBlur, router } = config
353
+
354
+ return {
355
+ segments,
356
+ focusedIndex: focusedIndex ?? 0,
357
+ wrapNavigation,
358
+ onNavigate,
359
+ onBlur,
360
+ router,
361
+ }
362
+ }
363
+
364
+ export function handleBreadcrumbKey(state: BreadcrumbState, key: string): BreadcrumbState {
365
+ const { segments, focusedIndex, wrapNavigation, onNavigate, onBlur, router } = state
366
+ const maxIndex = segments.length - 1
367
+
368
+ switch (key) {
369
+ case 'l':
370
+ case 'right': {
371
+ let newIndex = focusedIndex + 1
372
+ if (newIndex > maxIndex) {
373
+ newIndex = wrapNavigation ? 0 : maxIndex
374
+ }
375
+ return { ...state, focusedIndex: newIndex }
376
+ }
377
+
378
+ case 'h':
379
+ case 'left': {
380
+ let newIndex = focusedIndex - 1
381
+ if (newIndex < 0) {
382
+ newIndex = wrapNavigation ? maxIndex : 0
383
+ }
384
+ return { ...state, focusedIndex: newIndex }
385
+ }
386
+
387
+ case 'home': {
388
+ return { ...state, focusedIndex: 0 }
389
+ }
390
+
391
+ case 'end': {
392
+ return { ...state, focusedIndex: maxIndex }
393
+ }
394
+
395
+ case 'enter': {
396
+ const segment = segments[focusedIndex]
397
+ const isLast = focusedIndex === maxIndex
398
+ if (segment && segment.path && !isLast) {
399
+ // Use router adapter if available, otherwise use callback
400
+ if (router) {
401
+ router.navigate(segment.path)
402
+ } else if (onNavigate) {
403
+ onNavigate(segment.path)
404
+ }
405
+ }
406
+ return state
407
+ }
408
+
409
+ case 'escape': {
410
+ if (onBlur) {
411
+ onBlur()
412
+ }
413
+ return state
414
+ }
415
+
416
+ default:
417
+ return state
418
+ }
419
+ }
420
+
421
+ // ============================================================================
422
+ // Click Handler
423
+ // ============================================================================
424
+
425
+ export function handleBreadcrumbClick(state: BreadcrumbState, index: number): void {
426
+ const { segments, onNavigate, router } = state
427
+ const segment = segments[index]
428
+ const isLast = index === segments.length - 1
429
+
430
+ if (segment && segment.path && !isLast) {
431
+ // Use router adapter if available, otherwise use callback
432
+ if (router) {
433
+ router.navigate(segment.path)
434
+ } else if (onNavigate) {
435
+ onNavigate(segment.path)
436
+ }
437
+ }
438
+ }
439
+
440
+ // ============================================================================
441
+ // Router Integration Functions
442
+ // ============================================================================
443
+
444
+ /**
445
+ * Generates breadcrumb segments from a URL path.
446
+ * This is a convenience re-export of the utility function.
447
+ *
448
+ * @param path - URL path (e.g., '/products/electronics/phones')
449
+ * @param options - Configuration for segment generation
450
+ * @returns Array of breadcrumb segments with labels and paths
451
+ */
452
+ export function breadcrumbsFromPath(
453
+ path: string,
454
+ options?: {
455
+ labels?: Record<string, string>
456
+ labelGenerator?: (segment: string, fullPath: string) => string
457
+ includeHome?: boolean
458
+ homeLabel?: string
459
+ }
460
+ ): BreadcrumbSegment[] {
461
+ return generateBreadcrumbSegments(path, options)
462
+ }
463
+
464
+ /**
465
+ * Creates a navigation handler that integrates with a router adapter.
466
+ * Returns a function compatible with the onNavigate callback.
467
+ *
468
+ * @param router - Router adapter instance
469
+ * @param fallback - Optional fallback handler if router navigation fails
470
+ * @returns Navigation handler function
471
+ */
472
+ export function createRouterNavigationHandler(
473
+ router: RouterAdapter,
474
+ fallback?: (path: string) => void
475
+ ): (path: string) => void {
476
+ return (path: string) => {
477
+ router.navigate(path)
478
+ fallback?.(path)
479
+ }
480
+ }