@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,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Router Utilities
|
|
3
|
+
*
|
|
4
|
+
* Path matching, normalization, and breadcrumb generation utilities.
|
|
5
|
+
* These are re-exported from renderers/utils for consistency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
RouteMatchMode,
|
|
10
|
+
NavigableItem,
|
|
11
|
+
BreadcrumbSegment,
|
|
12
|
+
BreadcrumbGeneratorOptions,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a path by removing trailing slashes (except for root).
|
|
17
|
+
*
|
|
18
|
+
* @param path - Path to normalize
|
|
19
|
+
* @returns Normalized path
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* normalizePath('/products/') // '/products'
|
|
24
|
+
* normalizePath('products') // '/products'
|
|
25
|
+
* normalizePath('/') // '/'
|
|
26
|
+
* normalizePath('') // '/'
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function normalizePath(path: string): string {
|
|
30
|
+
if (!path || path === '/') return '/'
|
|
31
|
+
// Remove trailing slash
|
|
32
|
+
const normalized = path.endsWith('/') ? path.slice(0, -1) : path
|
|
33
|
+
// Ensure leading slash
|
|
34
|
+
return normalized.startsWith('/') ? normalized : '/' + normalized
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Matches a current path against a target path using the specified mode.
|
|
39
|
+
*
|
|
40
|
+
* @param currentPath - The current route path
|
|
41
|
+
* @param targetPath - The path to match against
|
|
42
|
+
* @param mode - Match mode: 'exact', 'prefix', or 'pattern'
|
|
43
|
+
* @returns true if the paths match according to the mode
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* matchPath('/products/123', '/products', 'prefix') // true
|
|
48
|
+
* matchPath('/products/123', '/products', 'exact') // false
|
|
49
|
+
* matchPath('/users/123', '/users/:id', 'pattern') // true
|
|
50
|
+
* matchPath('/products/all', '/products/*', 'pattern') // true
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function matchPath(
|
|
54
|
+
currentPath: string,
|
|
55
|
+
targetPath: string,
|
|
56
|
+
mode: RouteMatchMode = 'exact'
|
|
57
|
+
): boolean {
|
|
58
|
+
// Normalize paths (remove trailing slashes, except for root)
|
|
59
|
+
const normCurrent = normalizePath(currentPath)
|
|
60
|
+
const normTarget = normalizePath(targetPath)
|
|
61
|
+
|
|
62
|
+
switch (mode) {
|
|
63
|
+
case 'exact':
|
|
64
|
+
return normCurrent === normTarget
|
|
65
|
+
|
|
66
|
+
case 'prefix':
|
|
67
|
+
// Root path only matches itself in prefix mode
|
|
68
|
+
if (normTarget === '/') {
|
|
69
|
+
return normCurrent === '/'
|
|
70
|
+
}
|
|
71
|
+
return normCurrent === normTarget || normCurrent.startsWith(normTarget + '/')
|
|
72
|
+
|
|
73
|
+
case 'pattern':
|
|
74
|
+
// Simple pattern matching with :param and * wildcards
|
|
75
|
+
return matchPathPattern(normCurrent, normTarget)
|
|
76
|
+
|
|
77
|
+
default:
|
|
78
|
+
return normCurrent === normTarget
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Matches a path against a pattern with :param and * wildcards.
|
|
84
|
+
*
|
|
85
|
+
* @param path - Actual path to test
|
|
86
|
+
* @param pattern - Pattern with :param segments or * wildcard
|
|
87
|
+
* @returns true if the path matches the pattern
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* matchPathPattern('/users/123', '/users/:id') // true
|
|
92
|
+
* matchPathPattern('/users/123/posts', '/users/:id/*') // true
|
|
93
|
+
* matchPathPattern('/products', '/users/:id') // false
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function matchPathPattern(path: string, pattern: string): boolean {
|
|
97
|
+
// Handle wildcard at end
|
|
98
|
+
if (pattern.endsWith('*')) {
|
|
99
|
+
const prefix = pattern.slice(0, -1)
|
|
100
|
+
return path.startsWith(prefix)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pathSegments = path.split('/').filter(Boolean)
|
|
104
|
+
const patternSegments = pattern.split('/').filter(Boolean)
|
|
105
|
+
|
|
106
|
+
if (pathSegments.length !== patternSegments.length) {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
111
|
+
const patternSeg = patternSegments[i]
|
|
112
|
+
const pathSeg = pathSegments[i]
|
|
113
|
+
|
|
114
|
+
// :param matches any single segment
|
|
115
|
+
if (patternSeg.startsWith(':')) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (patternSeg !== pathSeg) {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extracts path parameters from a path using a pattern.
|
|
129
|
+
*
|
|
130
|
+
* @param path - Actual path (e.g., '/users/123/edit')
|
|
131
|
+
* @param pattern - Pattern with :param segments (e.g., '/users/:id/edit')
|
|
132
|
+
* @returns Object with extracted parameters or null if no match
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* extractPathParams('/users/123/posts/456', '/users/:userId/posts/:postId')
|
|
137
|
+
* // { userId: '123', postId: '456' }
|
|
138
|
+
*
|
|
139
|
+
* extractPathParams('/products/shoes', '/users/:id')
|
|
140
|
+
* // null (no match)
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function extractPathParams(
|
|
144
|
+
path: string,
|
|
145
|
+
pattern: string
|
|
146
|
+
): Record<string, string> | null {
|
|
147
|
+
const pathSegments = path.split('/').filter(Boolean)
|
|
148
|
+
const patternSegments = pattern.split('/').filter(Boolean)
|
|
149
|
+
|
|
150
|
+
if (pathSegments.length !== patternSegments.length) {
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const params: Record<string, string> = {}
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
157
|
+
const patternSeg = patternSegments[i]
|
|
158
|
+
const pathSeg = pathSegments[i]
|
|
159
|
+
|
|
160
|
+
if (patternSeg.startsWith(':')) {
|
|
161
|
+
const paramName = patternSeg.slice(1)
|
|
162
|
+
params[paramName] = pathSeg
|
|
163
|
+
} else if (patternSeg !== pathSeg) {
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return params
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds a path by joining segments.
|
|
173
|
+
*
|
|
174
|
+
* @param segments - Path segments to join
|
|
175
|
+
* @returns Joined and normalized path
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* joinPath('/users', '123', 'posts') // '/users/123/posts'
|
|
180
|
+
* joinPath('users/', '/123/', '/posts/') // '/users/123/posts'
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function joinPath(...segments: string[]): string {
|
|
184
|
+
const joined = segments
|
|
185
|
+
.map((s) => s.replace(/^\/+|\/+$/g, ''))
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join('/')
|
|
188
|
+
return '/' + joined
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Formats a URL segment into a human-readable label.
|
|
193
|
+
* Converts kebab-case or snake_case to Title Case.
|
|
194
|
+
*
|
|
195
|
+
* @param segment - URL path segment (e.g., 'my-products')
|
|
196
|
+
* @returns Formatted label (e.g., 'My Products')
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* formatSegmentLabel('my-products') // 'My Products'
|
|
201
|
+
* formatSegmentLabel('user_settings') // 'User Settings'
|
|
202
|
+
* formatSegmentLabel('api') // 'Api'
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function formatSegmentLabel(segment: string): string {
|
|
206
|
+
return segment
|
|
207
|
+
.replace(/[-_]/g, ' ')
|
|
208
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generates breadcrumb segments from a URL path.
|
|
213
|
+
*
|
|
214
|
+
* @param path - URL path (e.g., '/products/electronics/phones')
|
|
215
|
+
* @param options - Configuration for segment generation
|
|
216
|
+
* @returns Array of breadcrumb segments with labels and paths
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* generateBreadcrumbSegments('/products/electronics/phones')
|
|
221
|
+
* // [
|
|
222
|
+
* // { label: 'Home', path: '/' },
|
|
223
|
+
* // { label: 'Products', path: '/products' },
|
|
224
|
+
* // { label: 'Electronics', path: '/products/electronics' },
|
|
225
|
+
* // { label: 'Phones', path: '/products/electronics/phones' },
|
|
226
|
+
* // ]
|
|
227
|
+
*
|
|
228
|
+
* generateBreadcrumbSegments('/users/123', {
|
|
229
|
+
* labels: { '/users': 'Members', '/users/123': 'John Doe' },
|
|
230
|
+
* includeHome: false,
|
|
231
|
+
* })
|
|
232
|
+
* // [
|
|
233
|
+
* // { label: 'Members', path: '/users' },
|
|
234
|
+
* // { label: 'John Doe', path: '/users/123' },
|
|
235
|
+
* // ]
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function generateBreadcrumbSegments(
|
|
239
|
+
path: string,
|
|
240
|
+
options?: BreadcrumbGeneratorOptions
|
|
241
|
+
): BreadcrumbSegment[] {
|
|
242
|
+
const {
|
|
243
|
+
labels = {},
|
|
244
|
+
labelGenerator = (s: string) => formatSegmentLabel(s),
|
|
245
|
+
includeHome = true,
|
|
246
|
+
homeLabel = 'Home',
|
|
247
|
+
homeIcon,
|
|
248
|
+
} = options || {}
|
|
249
|
+
|
|
250
|
+
const segments: BreadcrumbSegment[] = []
|
|
251
|
+
|
|
252
|
+
// Add home segment if requested
|
|
253
|
+
if (includeHome) {
|
|
254
|
+
segments.push({
|
|
255
|
+
label: labels['/'] || homeLabel,
|
|
256
|
+
path: '/',
|
|
257
|
+
icon: homeIcon,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Split path and build cumulative paths
|
|
262
|
+
const parts = path.split('/').filter(Boolean)
|
|
263
|
+
let cumulativePath = ''
|
|
264
|
+
|
|
265
|
+
for (const part of parts) {
|
|
266
|
+
cumulativePath += '/' + part
|
|
267
|
+
const label = labels[cumulativePath] || labelGenerator(part, cumulativePath)
|
|
268
|
+
segments.push({ label, path: cumulativePath })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return segments
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Finds the active item in a navigation structure based on current path.
|
|
276
|
+
*
|
|
277
|
+
* @param items - Array of items with path property
|
|
278
|
+
* @param currentPath - Current route path
|
|
279
|
+
* @param mode - Match mode for path comparison
|
|
280
|
+
* @returns The matching item's ID or undefined
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const items = [
|
|
285
|
+
* { id: 'home', path: '/' },
|
|
286
|
+
* { id: 'products', path: '/products' },
|
|
287
|
+
* { id: 'product-detail', path: '/products/:id' },
|
|
288
|
+
* ]
|
|
289
|
+
*
|
|
290
|
+
* findActiveItemByPath(items, '/products/123', 'prefix')
|
|
291
|
+
* // 'products'
|
|
292
|
+
*
|
|
293
|
+
* findActiveItemByPath(items, '/products/123', 'pattern')
|
|
294
|
+
* // 'product-detail'
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export function findActiveItemByPath<T extends NavigableItem>(
|
|
298
|
+
items: T[],
|
|
299
|
+
currentPath: string,
|
|
300
|
+
mode: RouteMatchMode = 'prefix'
|
|
301
|
+
): string | undefined {
|
|
302
|
+
// First try exact match
|
|
303
|
+
const exactMatch = items.find(
|
|
304
|
+
(item) => item.path && matchPath(currentPath, item.path, 'exact')
|
|
305
|
+
)
|
|
306
|
+
if (exactMatch) return exactMatch.id
|
|
307
|
+
|
|
308
|
+
// Then try prefix match (for nested routes)
|
|
309
|
+
if (mode === 'prefix') {
|
|
310
|
+
// Sort by path length descending to get most specific match
|
|
311
|
+
const sortedItems = [...items]
|
|
312
|
+
.filter((item) => item.path)
|
|
313
|
+
.sort((a, b) => (b.path?.length ?? 0) - (a.path?.length ?? 0))
|
|
314
|
+
|
|
315
|
+
for (const item of sortedItems) {
|
|
316
|
+
if (item.path && matchPath(currentPath, item.path, 'prefix')) {
|
|
317
|
+
return item.id
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Try pattern match
|
|
323
|
+
if (mode === 'pattern') {
|
|
324
|
+
for (const item of items) {
|
|
325
|
+
if (item.path && matchPath(currentPath, item.path, 'pattern')) {
|
|
326
|
+
return item.id
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return undefined
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Finds the active item in nested navigation sections based on current path.
|
|
336
|
+
*
|
|
337
|
+
* @param sections - Array of sections containing items with path property
|
|
338
|
+
* @param currentPath - Current route path
|
|
339
|
+
* @param mode - Match mode for path comparison
|
|
340
|
+
* @returns The matching item's ID or undefined
|
|
341
|
+
*/
|
|
342
|
+
export function findActiveItemInSections<T extends NavigableItem>(
|
|
343
|
+
sections: Array<{ items: T[] }>,
|
|
344
|
+
currentPath: string,
|
|
345
|
+
mode: RouteMatchMode = 'prefix'
|
|
346
|
+
): string | undefined {
|
|
347
|
+
const allItems = sections.flatMap((s) => s.items)
|
|
348
|
+
return findActiveItemByPath(allItems, currentPath, mode)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Resolves relative paths against a base path.
|
|
353
|
+
*
|
|
354
|
+
* @param relativePath - Relative path (can start with ./ or ../)
|
|
355
|
+
* @param basePath - Base path to resolve against
|
|
356
|
+
* @returns Resolved absolute path
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```typescript
|
|
360
|
+
* resolvePath('./edit', '/users/123') // '/users/123/edit'
|
|
361
|
+
* resolvePath('../posts', '/users/123') // '/users/posts'
|
|
362
|
+
* resolvePath('/dashboard', '/users') // '/dashboard'
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export function resolvePath(relativePath: string, basePath: string): string {
|
|
366
|
+
// Absolute paths stay as-is
|
|
367
|
+
if (relativePath.startsWith('/')) {
|
|
368
|
+
return normalizePath(relativePath)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const baseSegments = basePath.split('/').filter(Boolean)
|
|
372
|
+
const relativeSegments = relativePath.split('/').filter(Boolean)
|
|
373
|
+
|
|
374
|
+
for (const segment of relativeSegments) {
|
|
375
|
+
if (segment === '..') {
|
|
376
|
+
baseSegments.pop()
|
|
377
|
+
} else if (segment !== '.') {
|
|
378
|
+
baseSegments.push(segment)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return '/' + baseSegments.join('/')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Checks if a path is within a base path.
|
|
387
|
+
*
|
|
388
|
+
* @param path - Path to check
|
|
389
|
+
* @param basePath - Base path
|
|
390
|
+
* @returns true if path is within basePath
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* isWithinPath('/products/123', '/products') // true
|
|
395
|
+
* isWithinPath('/users/123', '/products') // false
|
|
396
|
+
* isWithinPath('/products', '/products') // true
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
export function isWithinPath(path: string, basePath: string): boolean {
|
|
400
|
+
const normPath = normalizePath(path)
|
|
401
|
+
const normBase = normalizePath(basePath)
|
|
402
|
+
|
|
403
|
+
if (normBase === '/') {
|
|
404
|
+
return true
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return normPath === normBase || normPath.startsWith(normBase + '/')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Gets the parent path of a given path.
|
|
412
|
+
*
|
|
413
|
+
* @param path - Path to get parent of
|
|
414
|
+
* @returns Parent path or '/' if at root
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```typescript
|
|
418
|
+
* getParentPath('/users/123/posts') // '/users/123'
|
|
419
|
+
* getParentPath('/users') // '/'
|
|
420
|
+
* getParentPath('/') // '/'
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
export function getParentPath(path: string): string {
|
|
424
|
+
const normalized = normalizePath(path)
|
|
425
|
+
if (normalized === '/') return '/'
|
|
426
|
+
|
|
427
|
+
const lastSlash = normalized.lastIndexOf('/')
|
|
428
|
+
if (lastSlash <= 0) return '/'
|
|
429
|
+
|
|
430
|
+
return normalized.slice(0, lastSlash)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Gets the last segment of a path.
|
|
435
|
+
*
|
|
436
|
+
* @param path - Path to get segment from
|
|
437
|
+
* @returns Last path segment
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```typescript
|
|
441
|
+
* getLastSegment('/users/123/posts') // 'posts'
|
|
442
|
+
* getLastSegment('/users') // 'users'
|
|
443
|
+
* getLastSegment('/') // ''
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export function getLastSegment(path: string): string {
|
|
447
|
+
const normalized = normalizePath(path)
|
|
448
|
+
if (normalized === '/') return ''
|
|
449
|
+
|
|
450
|
+
const segments = normalized.split('/').filter(Boolean)
|
|
451
|
+
return segments[segments.length - 1] || ''
|
|
452
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Schemas for Runtime Validation
|
|
3
|
+
*
|
|
4
|
+
* Provides Zod schemas for validating public API inputs at runtime.
|
|
5
|
+
* These schemas ensure type safety beyond TypeScript's compile-time checks.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Color Schemas
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Schema for hex color strings.
|
|
18
|
+
*
|
|
19
|
+
* Accepts:
|
|
20
|
+
* - 3-character hex: #RGB or RGB
|
|
21
|
+
* - 6-character hex: #RRGGBB or RRGGBB
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* HexColorSchema.parse('#ff0000') // Valid
|
|
26
|
+
* HexColorSchema.parse('ff0000') // Valid
|
|
27
|
+
* HexColorSchema.parse('#f00') // Valid
|
|
28
|
+
* HexColorSchema.parse('invalid') // Throws ZodError
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const HexColorSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(
|
|
34
|
+
/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,
|
|
35
|
+
'Invalid hex color format. Expected #RGB, RGB, #RRGGBB, or RRGGBB'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Schema for RGB component values (0-255).
|
|
40
|
+
*/
|
|
41
|
+
export const RgbComponentSchema = z
|
|
42
|
+
.number()
|
|
43
|
+
.int('RGB component must be an integer')
|
|
44
|
+
.min(0, 'RGB component must be >= 0')
|
|
45
|
+
.max(255, 'RGB component must be <= 255')
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Schema for RGB color object.
|
|
49
|
+
*/
|
|
50
|
+
export const RgbColorSchema = z.object({
|
|
51
|
+
r: RgbComponentSchema,
|
|
52
|
+
g: RgbComponentSchema,
|
|
53
|
+
b: RgbComponentSchema,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Schema for ANSI 256 color codes (0-255).
|
|
58
|
+
*/
|
|
59
|
+
export const Ansi256CodeSchema = z
|
|
60
|
+
.number()
|
|
61
|
+
.int('ANSI 256 code must be an integer')
|
|
62
|
+
.min(0, 'ANSI 256 code must be >= 0')
|
|
63
|
+
.max(255, 'ANSI 256 code must be <= 255')
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Schema for ANSI escape sequence strings.
|
|
67
|
+
* Validates that a string is a valid ANSI escape sequence.
|
|
68
|
+
*
|
|
69
|
+
* Accepts:
|
|
70
|
+
* - Empty string (valid for 'no color')
|
|
71
|
+
* - ANSI escape sequences like \x1b[38;5;33m
|
|
72
|
+
*/
|
|
73
|
+
export const AnsiEscapeSchema = z
|
|
74
|
+
.string()
|
|
75
|
+
.refine(
|
|
76
|
+
(val) => {
|
|
77
|
+
if (val === '') return true
|
|
78
|
+
// eslint-disable-next-line no-control-regex
|
|
79
|
+
return /^\x1b\[\d+(;\d+)*m$/.test(val)
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
message: 'Invalid ANSI escape sequence',
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Theme Schemas
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Schema for theme color mode.
|
|
92
|
+
*/
|
|
93
|
+
export const ThemeModeSchema = z.enum(['dark', 'light'])
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Schema for terminal theme colors.
|
|
97
|
+
* All values are ANSI escape sequences.
|
|
98
|
+
*/
|
|
99
|
+
export const TerminalThemeColorsSchema = z.object({
|
|
100
|
+
primary: AnsiEscapeSchema,
|
|
101
|
+
secondary: AnsiEscapeSchema,
|
|
102
|
+
accent: AnsiEscapeSchema,
|
|
103
|
+
muted: AnsiEscapeSchema,
|
|
104
|
+
success: AnsiEscapeSchema,
|
|
105
|
+
warning: AnsiEscapeSchema,
|
|
106
|
+
error: AnsiEscapeSchema,
|
|
107
|
+
info: AnsiEscapeSchema,
|
|
108
|
+
border: AnsiEscapeSchema,
|
|
109
|
+
background: AnsiEscapeSchema,
|
|
110
|
+
foreground: AnsiEscapeSchema,
|
|
111
|
+
selection: AnsiEscapeSchema,
|
|
112
|
+
focus: AnsiEscapeSchema,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Schema for CreateTerminalThemeOptions.
|
|
117
|
+
*/
|
|
118
|
+
export const CreateTerminalThemeOptionsSchema = z.object({
|
|
119
|
+
mode: ThemeModeSchema.optional(),
|
|
120
|
+
colors: TerminalThemeColorsSchema.partial().optional(),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Schema for legacy terminal theme.
|
|
125
|
+
*/
|
|
126
|
+
export const LegacyTerminalThemeSchema = z.object({
|
|
127
|
+
primary: AnsiEscapeSchema,
|
|
128
|
+
secondary: AnsiEscapeSchema,
|
|
129
|
+
accent: AnsiEscapeSchema,
|
|
130
|
+
muted: AnsiEscapeSchema,
|
|
131
|
+
success: AnsiEscapeSchema,
|
|
132
|
+
warning: AnsiEscapeSchema,
|
|
133
|
+
error: AnsiEscapeSchema,
|
|
134
|
+
info: AnsiEscapeSchema,
|
|
135
|
+
border: AnsiEscapeSchema,
|
|
136
|
+
background: AnsiEscapeSchema,
|
|
137
|
+
foreground: AnsiEscapeSchema,
|
|
138
|
+
selection: AnsiEscapeSchema,
|
|
139
|
+
focus: AnsiEscapeSchema,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Schema for typography options in legacy theme.
|
|
144
|
+
*/
|
|
145
|
+
export const TypographyOptionsSchema = z.object({
|
|
146
|
+
headingWeight: z.string(),
|
|
147
|
+
bodyWeight: z.string(),
|
|
148
|
+
codeFont: z.string(),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Schema for spacing options in legacy theme.
|
|
153
|
+
*/
|
|
154
|
+
export const SpacingOptionsSchema = z.object({
|
|
155
|
+
xs: z.number(),
|
|
156
|
+
sm: z.number(),
|
|
157
|
+
md: z.number(),
|
|
158
|
+
lg: z.number(),
|
|
159
|
+
xl: z.number(),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Schema for legacy terminal theme with extras.
|
|
164
|
+
*/
|
|
165
|
+
export const LegacyTerminalThemeWithExtrasSchema = LegacyTerminalThemeSchema.extend({
|
|
166
|
+
typography: TypographyOptionsSchema.optional(),
|
|
167
|
+
spacing: SpacingOptionsSchema.optional(),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Schema for createTheme() function input (partial legacy theme).
|
|
172
|
+
*/
|
|
173
|
+
export const CreateThemeInputSchema = LegacyTerminalThemeWithExtrasSchema.partial()
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Color Conversion Schemas
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Schema for RGB to ANSI conversion options.
|
|
181
|
+
*/
|
|
182
|
+
export const RgbToAnsiOptionsSchema = z.object({
|
|
183
|
+
background: z.boolean().optional(),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Color Support Schemas
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Schema for color support levels.
|
|
192
|
+
*/
|
|
193
|
+
export const ColorSupportSchema = z.enum(['none', '16', '256', 'truecolor'])
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Type Exports (inferred from schemas)
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
export type HexColor = z.infer<typeof HexColorSchema>
|
|
200
|
+
export type RgbColor = z.infer<typeof RgbColorSchema>
|
|
201
|
+
export type Ansi256Code = z.infer<typeof Ansi256CodeSchema>
|
|
202
|
+
export type ThemeMode = z.infer<typeof ThemeModeSchema>
|
|
203
|
+
export type CreateTerminalThemeOptionsInput = z.infer<typeof CreateTerminalThemeOptionsSchema>
|
|
204
|
+
export type CreateThemeInput = z.infer<typeof CreateThemeInputSchema>
|
|
205
|
+
export type ColorSupport = z.infer<typeof ColorSupportSchema>
|