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