@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,639 @@
1
+ /**
2
+ * @mdxui/terminal Theme System Tests
3
+ *
4
+ * TDD RED Phase: These tests define the contract for terminal theme/color system.
5
+ * All tests should FAIL initially because the implementation doesn't exist yet.
6
+ *
7
+ * The theme system maps:
8
+ * - Tailwind CSS classes → ANSI escape codes
9
+ * - CSS design tokens → Terminal-safe colors
10
+ * - Semantic colors → Context-aware ANSI codes
11
+ */
12
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
13
+
14
+ describe('@mdxui/terminal theme system', () => {
15
+ // ============================================================================
16
+ // Tailwind → ANSI Color Mapping
17
+ // ============================================================================
18
+
19
+ describe('tailwindToAnsi', () => {
20
+ it('converts text-blue-500 to ANSI 256 blue', async () => {
21
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
22
+ // ANSI 256 color 33 is a blue that approximates Tailwind blue-500
23
+ expect(tailwindToAnsi('text-blue-500')).toBe('\x1b[38;5;33m')
24
+ })
25
+
26
+ it('converts bg-red-600 to ANSI 256 background red', async () => {
27
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
28
+ // ANSI 256 color 160 is a red that approximates Tailwind red-600
29
+ // 38;5;N = foreground, 48;5;N = background
30
+ expect(tailwindToAnsi('bg-red-600')).toBe('\x1b[48;5;160m')
31
+ })
32
+
33
+ it('converts text-green-400 to ANSI 256 green', async () => {
34
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
35
+ expect(tailwindToAnsi('text-green-400')).toBe('\x1b[38;5;71m')
36
+ })
37
+
38
+ it('converts text-yellow-500 to ANSI 256 yellow', async () => {
39
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
40
+ expect(tailwindToAnsi('text-yellow-500')).toBe('\x1b[38;5;220m')
41
+ })
42
+
43
+ it('converts text-purple-600 to ANSI 256 purple', async () => {
44
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
45
+ expect(tailwindToAnsi('text-purple-600')).toBe('\x1b[38;5;128m')
46
+ })
47
+
48
+ it('converts bg-gray-800 to ANSI 256 dark gray background', async () => {
49
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
50
+ expect(tailwindToAnsi('bg-gray-800')).toBe('\x1b[48;5;238m')
51
+ })
52
+
53
+ it('converts text-white to ANSI bright white', async () => {
54
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
55
+ expect(tailwindToAnsi('text-white')).toBe('\x1b[38;5;15m')
56
+ })
57
+
58
+ it('converts text-black to ANSI black', async () => {
59
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
60
+ expect(tailwindToAnsi('text-black')).toBe('\x1b[38;5;0m')
61
+ })
62
+
63
+ it('handles unknown classes by returning empty string', async () => {
64
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
65
+ expect(tailwindToAnsi('unknown-class')).toBe('')
66
+ })
67
+
68
+ it('supports Tailwind gray scale variants', async () => {
69
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
70
+ expect(tailwindToAnsi('text-gray-100')).toMatch(/\x1b\[38;5;\d+m/)
71
+ expect(tailwindToAnsi('text-gray-300')).toMatch(/\x1b\[38;5;\d+m/)
72
+ expect(tailwindToAnsi('text-gray-500')).toMatch(/\x1b\[38;5;\d+m/)
73
+ expect(tailwindToAnsi('text-gray-700')).toMatch(/\x1b\[38;5;\d+m/)
74
+ expect(tailwindToAnsi('text-gray-900')).toMatch(/\x1b\[38;5;\d+m/)
75
+ })
76
+
77
+ it('supports slate, zinc, neutral, and stone scales', async () => {
78
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
79
+ expect(tailwindToAnsi('text-slate-500')).toMatch(/\x1b\[38;5;\d+m/)
80
+ expect(tailwindToAnsi('text-zinc-500')).toMatch(/\x1b\[38;5;\d+m/)
81
+ expect(tailwindToAnsi('text-neutral-500')).toMatch(/\x1b\[38;5;\d+m/)
82
+ expect(tailwindToAnsi('text-stone-500')).toMatch(/\x1b\[38;5;\d+m/)
83
+ })
84
+
85
+ it('handles all Tailwind color intensities (50-950)', async () => {
86
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
87
+ const intensities = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
88
+ for (const intensity of intensities) {
89
+ expect(tailwindToAnsi(`text-blue-${intensity}`)).toMatch(/\x1b\[38;5;\d+m/)
90
+ }
91
+ })
92
+
93
+ it('converts font-bold to ANSI bold', async () => {
94
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
95
+ expect(tailwindToAnsi('font-bold')).toBe('\x1b[1m')
96
+ })
97
+
98
+ it('converts italic to ANSI italic', async () => {
99
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
100
+ expect(tailwindToAnsi('italic')).toBe('\x1b[3m')
101
+ })
102
+
103
+ it('converts underline to ANSI underline', async () => {
104
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
105
+ expect(tailwindToAnsi('underline')).toBe('\x1b[4m')
106
+ })
107
+
108
+ it('converts line-through to ANSI strikethrough', async () => {
109
+ const { tailwindToAnsi } = await import('@mdxui/terminal')
110
+ expect(tailwindToAnsi('line-through')).toBe('\x1b[9m')
111
+ })
112
+ })
113
+
114
+ // ============================================================================
115
+ // Theme Token System
116
+ // ============================================================================
117
+
118
+ describe('createTerminalTheme', () => {
119
+ it('creates a dark mode theme with all semantic colors', async () => {
120
+ const { createTerminalTheme } = await import('@mdxui/terminal')
121
+ const theme = createTerminalTheme({ mode: 'dark' })
122
+
123
+ expect(theme.mode).toBe('dark')
124
+ expect(theme.colors).toBeDefined()
125
+ expect(theme.colors.primary).toBeDefined()
126
+ expect(theme.colors.secondary).toBeDefined()
127
+ expect(theme.colors.accent).toBeDefined()
128
+ expect(theme.colors.muted).toBeDefined()
129
+ })
130
+
131
+ it('creates a light mode theme with appropriate contrast', async () => {
132
+ const { createTerminalTheme } = await import('@mdxui/terminal')
133
+ const theme = createTerminalTheme({ mode: 'light' })
134
+
135
+ expect(theme.mode).toBe('light')
136
+ expect(theme.colors.foreground).toBeDefined()
137
+ expect(theme.colors.background).toBeDefined()
138
+ // Light mode should have dark foreground for contrast
139
+ expect(theme.colors.foreground).not.toBe(theme.colors.background)
140
+ })
141
+
142
+ it('includes all status colors', async () => {
143
+ const { createTerminalTheme } = await import('@mdxui/terminal')
144
+ const theme = createTerminalTheme({ mode: 'dark' })
145
+
146
+ expect(theme.colors.success).toBeDefined()
147
+ expect(theme.colors.warning).toBeDefined()
148
+ expect(theme.colors.error).toBeDefined()
149
+ expect(theme.colors.info).toBeDefined()
150
+ })
151
+
152
+ it('includes UI colors', async () => {
153
+ const { createTerminalTheme } = await import('@mdxui/terminal')
154
+ const theme = createTerminalTheme({ mode: 'dark' })
155
+
156
+ expect(theme.colors.border).toBeDefined()
157
+ expect(theme.colors.background).toBeDefined()
158
+ expect(theme.colors.foreground).toBeDefined()
159
+ expect(theme.colors.selection).toBeDefined()
160
+ expect(theme.colors.focus).toBeDefined()
161
+ })
162
+
163
+ it('allows custom color overrides', async () => {
164
+ const { createTerminalTheme } = await import('@mdxui/terminal')
165
+ const customPrimary = '\x1b[38;5;45m' // Custom cyan
166
+ const theme = createTerminalTheme({
167
+ mode: 'dark',
168
+ colors: {
169
+ primary: customPrimary,
170
+ },
171
+ })
172
+
173
+ expect(theme.colors.primary).toBe(customPrimary)
174
+ })
175
+
176
+ it('defaults to dark mode when mode not specified', async () => {
177
+ const { createTerminalTheme } = await import('@mdxui/terminal')
178
+ const theme = createTerminalTheme({})
179
+
180
+ expect(theme.mode).toBe('dark')
181
+ })
182
+
183
+ it('provides ANSI escape codes for all colors', async () => {
184
+ const { createTerminalTheme } = await import('@mdxui/terminal')
185
+ const theme = createTerminalTheme({ mode: 'dark' })
186
+
187
+ // All colors should be valid ANSI escape sequences
188
+ const ansiPattern = /^\x1b\[\d+(;\d+)*m$/
189
+ for (const [key, value] of Object.entries(theme.colors)) {
190
+ if (value) {
191
+ expect(value).toMatch(ansiPattern)
192
+ }
193
+ }
194
+ })
195
+ })
196
+
197
+ // ============================================================================
198
+ // Semantic Color Retrieval
199
+ // ============================================================================
200
+
201
+ describe('getThemeColor', () => {
202
+ it('retrieves primary color from theme', async () => {
203
+ const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
204
+ const theme = createTerminalTheme({ mode: 'dark' })
205
+ const color = getThemeColor(theme, 'primary')
206
+
207
+ expect(color).toBeDefined()
208
+ expect(typeof color).toBe('string')
209
+ expect(color).toMatch(/\x1b\[/)
210
+ })
211
+
212
+ it('retrieves error color from theme', async () => {
213
+ const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
214
+ const theme = createTerminalTheme({ mode: 'dark' })
215
+ const color = getThemeColor(theme, 'error')
216
+
217
+ expect(color).toBeDefined()
218
+ expect(color).toContain('\x1b[')
219
+ })
220
+
221
+ it('returns fallback for unknown color keys', async () => {
222
+ const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
223
+ const theme = createTerminalTheme({ mode: 'dark' })
224
+ const color = getThemeColor(theme, 'unknownColor' as any, '\x1b[37m')
225
+
226
+ expect(color).toBe('\x1b[37m')
227
+ })
228
+
229
+ it('throws when no fallback provided for unknown key', async () => {
230
+ const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
231
+ const theme = createTerminalTheme({ mode: 'dark' })
232
+
233
+ expect(() => getThemeColor(theme, 'unknownColor' as any)).toThrow()
234
+ })
235
+ })
236
+
237
+ // ============================================================================
238
+ // Color Support Detection
239
+ // ============================================================================
240
+
241
+ describe('detectColorSupport', () => {
242
+ let originalEnv: NodeJS.ProcessEnv
243
+
244
+ beforeEach(() => {
245
+ originalEnv = { ...process.env }
246
+ })
247
+
248
+ afterEach(() => {
249
+ process.env = originalEnv
250
+ })
251
+
252
+ it('returns color support level as string', async () => {
253
+ const { detectColorSupport } = await import('@mdxui/terminal')
254
+ const support = detectColorSupport()
255
+
256
+ expect(['none', '16', '256', 'truecolor']).toContain(support)
257
+ })
258
+
259
+ it('detects truecolor from COLORTERM env', async () => {
260
+ process.env.COLORTERM = 'truecolor'
261
+ // Clear the module cache to pick up new env
262
+ vi.resetModules()
263
+ const { detectColorSupport } = await import('@mdxui/terminal')
264
+ const support = detectColorSupport()
265
+
266
+ expect(support).toBe('truecolor')
267
+ })
268
+
269
+ it('detects 24bit from COLORTERM env', async () => {
270
+ process.env.COLORTERM = '24bit'
271
+ vi.resetModules()
272
+ const { detectColorSupport } = await import('@mdxui/terminal')
273
+ const support = detectColorSupport()
274
+
275
+ expect(support).toBe('truecolor')
276
+ })
277
+
278
+ it('detects 256 colors from TERM containing 256color', async () => {
279
+ delete process.env.COLORTERM
280
+ process.env.TERM = 'xterm-256color'
281
+ vi.resetModules()
282
+ const { detectColorSupport } = await import('@mdxui/terminal')
283
+ const support = detectColorSupport()
284
+
285
+ expect(support).toBe('256')
286
+ })
287
+
288
+ it('detects 16 colors from basic TERM', async () => {
289
+ delete process.env.COLORTERM
290
+ process.env.TERM = 'xterm'
291
+ vi.resetModules()
292
+ const { detectColorSupport } = await import('@mdxui/terminal')
293
+ const support = detectColorSupport()
294
+
295
+ expect(support).toBe('16')
296
+ })
297
+
298
+ it('returns none for dumb terminal', async () => {
299
+ delete process.env.COLORTERM
300
+ process.env.TERM = 'dumb'
301
+ vi.resetModules()
302
+ const { detectColorSupport } = await import('@mdxui/terminal')
303
+ const support = detectColorSupport()
304
+
305
+ expect(support).toBe('none')
306
+ })
307
+
308
+ it('respects NO_COLOR environment variable', async () => {
309
+ process.env.NO_COLOR = '1'
310
+ process.env.COLORTERM = 'truecolor'
311
+ vi.resetModules()
312
+ const { detectColorSupport } = await import('@mdxui/terminal')
313
+ const support = detectColorSupport()
314
+
315
+ expect(support).toBe('none')
316
+ })
317
+
318
+ it('respects FORCE_COLOR environment variable', async () => {
319
+ process.env.FORCE_COLOR = '3'
320
+ delete process.env.NO_COLOR
321
+ vi.resetModules()
322
+ const { detectColorSupport } = await import('@mdxui/terminal')
323
+ const support = detectColorSupport()
324
+
325
+ expect(support).toBe('truecolor')
326
+ })
327
+ })
328
+
329
+ // ============================================================================
330
+ // CSS Variable Mapping
331
+ // ============================================================================
332
+
333
+ describe('cssVarToAnsi', () => {
334
+ it('maps --primary to theme primary color', async () => {
335
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
336
+ const theme = createTerminalTheme({ mode: 'dark' })
337
+ const color = cssVarToAnsi('--primary', theme)
338
+
339
+ expect(color).toBe(theme.colors.primary)
340
+ })
341
+
342
+ it('maps --foreground to theme foreground color', async () => {
343
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
344
+ const theme = createTerminalTheme({ mode: 'dark' })
345
+ const color = cssVarToAnsi('--foreground', theme)
346
+
347
+ expect(color).toBe(theme.colors.foreground)
348
+ })
349
+
350
+ it('maps --background to theme background color', async () => {
351
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
352
+ const theme = createTerminalTheme({ mode: 'dark' })
353
+ const color = cssVarToAnsi('--background', theme)
354
+
355
+ expect(color).toBe(theme.colors.background)
356
+ })
357
+
358
+ it('maps --destructive to theme error color', async () => {
359
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
360
+ const theme = createTerminalTheme({ mode: 'dark' })
361
+ const color = cssVarToAnsi('--destructive', theme)
362
+
363
+ expect(color).toBe(theme.colors.error)
364
+ })
365
+
366
+ it('maps --muted-foreground to theme muted color', async () => {
367
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
368
+ const theme = createTerminalTheme({ mode: 'dark' })
369
+ const color = cssVarToAnsi('--muted-foreground', theme)
370
+
371
+ expect(color).toBe(theme.colors.muted)
372
+ })
373
+
374
+ it('returns empty string for unknown CSS variables', async () => {
375
+ const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
376
+ const theme = createTerminalTheme({ mode: 'dark' })
377
+ const color = cssVarToAnsi('--unknown-var', theme)
378
+
379
+ expect(color).toBe('')
380
+ })
381
+ })
382
+
383
+ // ============================================================================
384
+ // Color Degradation (graceful fallback for limited color support)
385
+ // ============================================================================
386
+
387
+ describe('degradeColor', () => {
388
+ it('passes through ANSI 256 color when support is 256', async () => {
389
+ const { degradeColor } = await import('@mdxui/terminal')
390
+ const color = '\x1b[38;5;33m'
391
+ const degraded = degradeColor(color, '256')
392
+
393
+ expect(degraded).toBe(color)
394
+ })
395
+
396
+ it('converts ANSI 256 to basic 16 color when support is 16', async () => {
397
+ const { degradeColor } = await import('@mdxui/terminal')
398
+ const color = '\x1b[38;5;33m' // 256-color blue
399
+ const degraded = degradeColor(color, '16')
400
+
401
+ // Should convert to basic blue (34) or bright blue (94)
402
+ expect(degraded).toMatch(/\x1b\[3[0-7]m|\x1b\[9[0-7]m/)
403
+ })
404
+
405
+ it('returns empty string when support is none', async () => {
406
+ const { degradeColor } = await import('@mdxui/terminal')
407
+ const color = '\x1b[38;5;33m'
408
+ const degraded = degradeColor(color, 'none')
409
+
410
+ expect(degraded).toBe('')
411
+ })
412
+
413
+ it('handles truecolor to 256 conversion', async () => {
414
+ const { degradeColor } = await import('@mdxui/terminal')
415
+ // Truecolor blue: RGB(59, 130, 246) - Tailwind blue-500
416
+ const color = '\x1b[38;2;59;130;246m'
417
+ const degraded = degradeColor(color, '256')
418
+
419
+ // Should be a valid 256-color code
420
+ expect(degraded).toMatch(/\x1b\[38;5;\d+m/)
421
+ })
422
+ })
423
+
424
+ // ============================================================================
425
+ // Theme-Aware Styling
426
+ // ============================================================================
427
+
428
+ describe('applyThemeStyles', () => {
429
+ it('wraps text with theme-appropriate colors', async () => {
430
+ const { applyThemeStyles, createTerminalTheme } = await import('@mdxui/terminal')
431
+ const theme = createTerminalTheme({ mode: 'dark' })
432
+ const styled = applyThemeStyles('Hello', theme, 'primary')
433
+
434
+ expect(styled).toContain(theme.colors.primary)
435
+ expect(styled).toContain('Hello')
436
+ expect(styled).toContain('\x1b[0m') // Reset at end
437
+ })
438
+
439
+ it('supports multiple style types', async () => {
440
+ const { applyThemeStyles, createTerminalTheme } = await import('@mdxui/terminal')
441
+ const theme = createTerminalTheme({ mode: 'dark' })
442
+ const styled = applyThemeStyles('Error!', theme, 'error')
443
+
444
+ expect(styled).toContain(theme.colors.error)
445
+ })
446
+
447
+ it('handles background styling', async () => {
448
+ const { applyThemeStyles, createTerminalTheme } = await import('@mdxui/terminal')
449
+ const theme = createTerminalTheme({ mode: 'dark' })
450
+ const styled = applyThemeStyles('Selected', theme, 'selection')
451
+
452
+ expect(styled).toContain(theme.colors.selection)
453
+ })
454
+ })
455
+
456
+ // ============================================================================
457
+ // Dark/Light Mode Contrast
458
+ // ============================================================================
459
+
460
+ describe('theme mode contrast', () => {
461
+ it('dark mode has light foreground on dark background', async () => {
462
+ const { createTerminalTheme } = await import('@mdxui/terminal')
463
+ const theme = createTerminalTheme({ mode: 'dark' })
464
+
465
+ // Dark mode: foreground should be light-colored
466
+ // Using ANSI 256 grayscale: 232-255 (232=black, 255=white)
467
+ // Light text would be 250+ range
468
+ expect(theme.colors.foreground).toBeDefined()
469
+ expect(theme.colors.background).toBeDefined()
470
+ })
471
+
472
+ it('light mode has dark foreground on light background', async () => {
473
+ const { createTerminalTheme } = await import('@mdxui/terminal')
474
+ const theme = createTerminalTheme({ mode: 'light' })
475
+
476
+ // Light mode: foreground should be dark-colored
477
+ expect(theme.colors.foreground).toBeDefined()
478
+ expect(theme.colors.background).toBeDefined()
479
+ })
480
+
481
+ it('primary color has good contrast in both modes', async () => {
482
+ const { createTerminalTheme } = await import('@mdxui/terminal')
483
+ const darkTheme = createTerminalTheme({ mode: 'dark' })
484
+ const lightTheme = createTerminalTheme({ mode: 'light' })
485
+
486
+ // Primary should be different for each mode to maintain contrast
487
+ expect(darkTheme.colors.primary).toBeDefined()
488
+ expect(lightTheme.colors.primary).toBeDefined()
489
+ })
490
+ })
491
+
492
+ // ============================================================================
493
+ // RGB/Hex to ANSI Conversion
494
+ // ============================================================================
495
+
496
+ describe('rgbToAnsi', () => {
497
+ it('converts RGB values to ANSI 256 color', async () => {
498
+ const { rgbToAnsi } = await import('@mdxui/terminal')
499
+ // Blue-500: RGB(59, 130, 246)
500
+ const ansi = rgbToAnsi(59, 130, 246)
501
+
502
+ expect(ansi).toMatch(/\x1b\[38;5;\d+m/)
503
+ })
504
+
505
+ it('converts black to ANSI 256 color 0', async () => {
506
+ const { rgbToAnsi } = await import('@mdxui/terminal')
507
+ const ansi = rgbToAnsi(0, 0, 0)
508
+
509
+ expect(ansi).toBe('\x1b[38;5;0m')
510
+ })
511
+
512
+ it('converts white to ANSI 256 color 15', async () => {
513
+ const { rgbToAnsi } = await import('@mdxui/terminal')
514
+ const ansi = rgbToAnsi(255, 255, 255)
515
+
516
+ expect(ansi).toBe('\x1b[38;5;15m')
517
+ })
518
+
519
+ it('supports background mode', async () => {
520
+ const { rgbToAnsi } = await import('@mdxui/terminal')
521
+ const ansi = rgbToAnsi(255, 0, 0, { background: true })
522
+
523
+ expect(ansi).toMatch(/\x1b\[48;5;\d+m/)
524
+ })
525
+ })
526
+
527
+ describe('hexToAnsi', () => {
528
+ it('converts hex color to ANSI 256 color', async () => {
529
+ const { hexToAnsi } = await import('@mdxui/terminal')
530
+ // Blue-500: #3b82f6
531
+ const ansi = hexToAnsi('#3b82f6')
532
+
533
+ expect(ansi).toMatch(/\x1b\[38;5;\d+m/)
534
+ })
535
+
536
+ it('handles hex without hash prefix', async () => {
537
+ const { hexToAnsi } = await import('@mdxui/terminal')
538
+ const ansi = hexToAnsi('3b82f6')
539
+
540
+ expect(ansi).toMatch(/\x1b\[38;5;\d+m/)
541
+ })
542
+
543
+ it('handles 3-character hex shorthand', async () => {
544
+ const { hexToAnsi } = await import('@mdxui/terminal')
545
+ const ansi = hexToAnsi('#fff')
546
+
547
+ expect(ansi).toBe('\x1b[38;5;15m')
548
+ })
549
+
550
+ it('supports background mode', async () => {
551
+ const { hexToAnsi } = await import('@mdxui/terminal')
552
+ const ansi = hexToAnsi('#ff0000', { background: true })
553
+
554
+ expect(ansi).toMatch(/\x1b\[48;5;\d+m/)
555
+ })
556
+ })
557
+
558
+ // ============================================================================
559
+ // ANSI Code Constants
560
+ // ============================================================================
561
+
562
+ describe('ANSI constants', () => {
563
+ it('exports ANSI reset code', async () => {
564
+ const { ANSI } = await import('@mdxui/terminal')
565
+ expect(ANSI.reset).toBe('\x1b[0m')
566
+ })
567
+
568
+ it('exports ANSI style codes', async () => {
569
+ const { ANSI } = await import('@mdxui/terminal')
570
+ expect(ANSI.bold).toBe('\x1b[1m')
571
+ expect(ANSI.dim).toBe('\x1b[2m')
572
+ expect(ANSI.italic).toBe('\x1b[3m')
573
+ expect(ANSI.underline).toBe('\x1b[4m')
574
+ })
575
+
576
+ it('exports basic foreground colors', async () => {
577
+ const { ANSI } = await import('@mdxui/terminal')
578
+ expect(ANSI.black).toBe('\x1b[30m')
579
+ expect(ANSI.red).toBe('\x1b[31m')
580
+ expect(ANSI.green).toBe('\x1b[32m')
581
+ expect(ANSI.yellow).toBe('\x1b[33m')
582
+ expect(ANSI.blue).toBe('\x1b[34m')
583
+ expect(ANSI.magenta).toBe('\x1b[35m')
584
+ expect(ANSI.cyan).toBe('\x1b[36m')
585
+ expect(ANSI.white).toBe('\x1b[37m')
586
+ })
587
+
588
+ it('exports bright foreground colors', async () => {
589
+ const { ANSI } = await import('@mdxui/terminal')
590
+ expect(ANSI.brightBlack).toBe('\x1b[90m')
591
+ expect(ANSI.brightRed).toBe('\x1b[91m')
592
+ expect(ANSI.brightGreen).toBe('\x1b[92m')
593
+ expect(ANSI.brightYellow).toBe('\x1b[93m')
594
+ expect(ANSI.brightBlue).toBe('\x1b[94m')
595
+ expect(ANSI.brightMagenta).toBe('\x1b[95m')
596
+ expect(ANSI.brightCyan).toBe('\x1b[96m')
597
+ expect(ANSI.brightWhite).toBe('\x1b[97m')
598
+ })
599
+
600
+ it('exports background colors', async () => {
601
+ const { ANSI } = await import('@mdxui/terminal')
602
+ expect(ANSI.bgBlack).toBe('\x1b[40m')
603
+ expect(ANSI.bgRed).toBe('\x1b[41m')
604
+ expect(ANSI.bgGreen).toBe('\x1b[42m')
605
+ expect(ANSI.bgYellow).toBe('\x1b[43m')
606
+ expect(ANSI.bgBlue).toBe('\x1b[44m')
607
+ expect(ANSI.bgMagenta).toBe('\x1b[45m')
608
+ expect(ANSI.bgCyan).toBe('\x1b[46m')
609
+ expect(ANSI.bgWhite).toBe('\x1b[47m')
610
+ })
611
+ })
612
+
613
+ // ============================================================================
614
+ // Styled Text Helper
615
+ // ============================================================================
616
+
617
+ describe('styled helper', () => {
618
+ it('wraps text with ANSI codes and reset', async () => {
619
+ const { styled, ANSI } = await import('@mdxui/terminal')
620
+ const result = styled('Hello', ANSI.blue)
621
+
622
+ expect(result).toBe('\x1b[34mHello\x1b[0m')
623
+ })
624
+
625
+ it('combines multiple ANSI codes', async () => {
626
+ const { styled, ANSI } = await import('@mdxui/terminal')
627
+ const result = styled('Bold Blue', ANSI.bold, ANSI.blue)
628
+
629
+ expect(result).toBe('\x1b[1m\x1b[34mBold Blue\x1b[0m')
630
+ })
631
+
632
+ it('returns plain text when no codes provided', async () => {
633
+ const { styled } = await import('@mdxui/terminal')
634
+ const result = styled('Plain')
635
+
636
+ expect(result).toBe('Plain')
637
+ })
638
+ })
639
+ })