@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,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
|
+
}
|