@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,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Tier Switcher Control
|
|
3
|
+
*
|
|
4
|
+
* Provides runtime tier switching capabilities for the Universal Terminal UI.
|
|
5
|
+
* Allows users to dynamically switch between render tiers based on terminal
|
|
6
|
+
* capabilities and user preferences.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RenderTier } from './types'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Capabilities flags for each render tier
|
|
17
|
+
*/
|
|
18
|
+
export interface TierCapabilities {
|
|
19
|
+
text: boolean
|
|
20
|
+
markdown: boolean
|
|
21
|
+
ascii: boolean
|
|
22
|
+
unicode: boolean
|
|
23
|
+
ansi: boolean
|
|
24
|
+
interactive: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Storage interface for persisting user preferences
|
|
29
|
+
*/
|
|
30
|
+
export interface TierStorage {
|
|
31
|
+
get(key: string): string | null
|
|
32
|
+
set(key: string, value: string): void
|
|
33
|
+
remove(key: string): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for detecting tier capabilities
|
|
38
|
+
*/
|
|
39
|
+
export interface DetectOptions {
|
|
40
|
+
env?: Record<string, string | undefined>
|
|
41
|
+
isTTY?: boolean
|
|
42
|
+
isStdinTTY?: boolean
|
|
43
|
+
isStdoutTTY?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for the setTier method
|
|
48
|
+
*/
|
|
49
|
+
export interface SetTierOptions {
|
|
50
|
+
fallback?: boolean
|
|
51
|
+
rerender?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Options for creating a TierSwitcher
|
|
56
|
+
*/
|
|
57
|
+
export interface TierSwitcherOptions {
|
|
58
|
+
initialTier?: RenderTier
|
|
59
|
+
capabilities?: TierCapabilities
|
|
60
|
+
autoDetect?: boolean
|
|
61
|
+
onChange?: (event: TierChangeEvent) => void
|
|
62
|
+
onRerender?: () => void
|
|
63
|
+
userPreference?: RenderTier
|
|
64
|
+
storage?: TierStorage
|
|
65
|
+
fallbackBehavior?: 'downgrade' | 'none'
|
|
66
|
+
batchRerenders?: boolean
|
|
67
|
+
batchDelay?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Event emitted when tier changes
|
|
72
|
+
*/
|
|
73
|
+
export interface TierChangeEvent {
|
|
74
|
+
previousTier: RenderTier
|
|
75
|
+
newTier: RenderTier
|
|
76
|
+
timestamp: number
|
|
77
|
+
capabilities: TierCapabilities
|
|
78
|
+
shouldRerender: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Event emitted when fallback occurs
|
|
83
|
+
*/
|
|
84
|
+
export interface TierFallbackEvent {
|
|
85
|
+
requestedTier: RenderTier
|
|
86
|
+
actualTier: RenderTier
|
|
87
|
+
reason: 'unavailable'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Event types supported by the tier switcher
|
|
92
|
+
*/
|
|
93
|
+
export type TierEventType = 'change' | 'fallback'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Event listener function type
|
|
97
|
+
*/
|
|
98
|
+
export type TierEventListener<T> = (event: T) => void
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* TierSwitcher interface
|
|
102
|
+
*/
|
|
103
|
+
export interface TierSwitcher {
|
|
104
|
+
getCurrentTier(): RenderTier
|
|
105
|
+
setTier(tier: RenderTier, options?: SetTierOptions): boolean
|
|
106
|
+
getCapabilities(): Readonly<TierCapabilities>
|
|
107
|
+
getAvailableTiers(): RenderTier[]
|
|
108
|
+
canSwitchTo(tier: RenderTier): boolean
|
|
109
|
+
upgradeToHighest(): RenderTier
|
|
110
|
+
downgradeToLowest(): RenderTier
|
|
111
|
+
upgradeOneLevel(): RenderTier
|
|
112
|
+
downgradeOneLevel(): RenderTier
|
|
113
|
+
setUserPreference(tier: RenderTier): void
|
|
114
|
+
getUserPreference(): RenderTier | null
|
|
115
|
+
clearUserPreference(): void
|
|
116
|
+
updateCapabilities(capabilities: TierCapabilities): void
|
|
117
|
+
on(event: 'change', listener: TierEventListener<TierChangeEvent>): () => void
|
|
118
|
+
on(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): () => void
|
|
119
|
+
off(event: 'change', listener: TierEventListener<TierChangeEvent>): void
|
|
120
|
+
off(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): void
|
|
121
|
+
once(event: 'change', listener: TierEventListener<TierChangeEvent>): void
|
|
122
|
+
once(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): void
|
|
123
|
+
batch(fn: () => void): void
|
|
124
|
+
destroy(): void
|
|
125
|
+
isDestroyed(): boolean
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Constants
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
const TIER_ORDER: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
|
|
133
|
+
|
|
134
|
+
const TIER_ORDER_MAP: Record<RenderTier, number> = {
|
|
135
|
+
text: 0,
|
|
136
|
+
markdown: 1,
|
|
137
|
+
ascii: 2,
|
|
138
|
+
unicode: 3,
|
|
139
|
+
ansi: 4,
|
|
140
|
+
interactive: 5,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const STORAGE_KEY = 'tier-preference'
|
|
144
|
+
|
|
145
|
+
const VALID_TIERS = new Set<string>(TIER_ORDER)
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Utility Functions
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Detect terminal tier capabilities based on environment
|
|
153
|
+
*/
|
|
154
|
+
export function detectTierCapabilities(options?: DetectOptions): TierCapabilities {
|
|
155
|
+
const env = options?.env ?? (typeof process !== 'undefined' ? process.env : {})
|
|
156
|
+
|
|
157
|
+
// Get TTY values, defaulting to undefined when not explicitly set
|
|
158
|
+
const isTTY = options?.isTTY ?? (typeof process !== 'undefined' ? process.stdout?.isTTY : undefined)
|
|
159
|
+
const isStdinTTY = options?.isStdinTTY ?? (typeof process !== 'undefined' ? process.stdin?.isTTY : undefined)
|
|
160
|
+
const isStdoutTTY = options?.isStdoutTTY ?? (typeof process !== 'undefined' ? process.stdout?.isTTY : undefined)
|
|
161
|
+
|
|
162
|
+
// Text, markdown, and ASCII are always available
|
|
163
|
+
const text = true
|
|
164
|
+
const markdown = true
|
|
165
|
+
const ascii = true
|
|
166
|
+
|
|
167
|
+
// Unicode support - check for UTF-8 encoding, or assume true in modern environments
|
|
168
|
+
const encoding = env.LANG ?? env.LC_ALL ?? ''
|
|
169
|
+
const unicode = encoding.toLowerCase().includes('utf') || isTTY !== false
|
|
170
|
+
|
|
171
|
+
// ANSI support - check for NO_COLOR and FORCE_COLOR
|
|
172
|
+
let ansi: boolean
|
|
173
|
+
if (env.NO_COLOR !== undefined) {
|
|
174
|
+
ansi = false
|
|
175
|
+
} else if (env.FORCE_COLOR !== undefined) {
|
|
176
|
+
ansi = true
|
|
177
|
+
} else {
|
|
178
|
+
// Check for color terminal
|
|
179
|
+
const term = env.TERM ?? ''
|
|
180
|
+
const colorterm = env.COLORTERM ?? ''
|
|
181
|
+
// Require explicit TTY or TERM environment to enable ANSI
|
|
182
|
+
// isTTY is undefined when not a TTY, so we use === true check
|
|
183
|
+
ansi = isTTY === true || term.includes('color') || term.includes('xterm') || colorterm !== ''
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Interactive support - requires both stdin and stdout to be TTY
|
|
187
|
+
// Must be explicitly true, undefined defaults to false for safety
|
|
188
|
+
const interactive = isStdinTTY === true && isStdoutTTY === true
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
text,
|
|
192
|
+
markdown,
|
|
193
|
+
ascii,
|
|
194
|
+
unicode,
|
|
195
|
+
ansi,
|
|
196
|
+
interactive,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get tier order as an array or map
|
|
202
|
+
*/
|
|
203
|
+
export function getTierOrder(): RenderTier[]
|
|
204
|
+
export function getTierOrder(format: 'array'): RenderTier[]
|
|
205
|
+
export function getTierOrder(format: 'map'): Record<RenderTier, number>
|
|
206
|
+
export function getTierOrder(format?: 'array' | 'map'): RenderTier[] | Record<RenderTier, number> {
|
|
207
|
+
if (format === 'map') {
|
|
208
|
+
return { ...TIER_ORDER_MAP }
|
|
209
|
+
}
|
|
210
|
+
return [...TIER_ORDER]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if a tier can be switched to given capabilities
|
|
215
|
+
*/
|
|
216
|
+
export function canSwitchToTier(tier: RenderTier, capabilities: TierCapabilities): boolean {
|
|
217
|
+
return capabilities[tier] === true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// TierSwitcher Implementation
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
class TierSwitcherImpl implements TierSwitcher {
|
|
225
|
+
private currentTier: RenderTier
|
|
226
|
+
private capabilities: TierCapabilities
|
|
227
|
+
private userPreference: RenderTier | null = null
|
|
228
|
+
private storage: TierStorage | null = null
|
|
229
|
+
private onChange: ((event: TierChangeEvent) => void) | null = null
|
|
230
|
+
private onRerender: (() => void) | null = null
|
|
231
|
+
private fallbackBehavior: 'downgrade' | 'none'
|
|
232
|
+
private batchRerenders: boolean
|
|
233
|
+
private batchDelay: number
|
|
234
|
+
private destroyed = false
|
|
235
|
+
private batching = false
|
|
236
|
+
private pendingRerender = false
|
|
237
|
+
private batchTimeout: ReturnType<typeof setTimeout> | null = null
|
|
238
|
+
|
|
239
|
+
private changeListeners: Set<TierEventListener<TierChangeEvent>> = new Set()
|
|
240
|
+
private fallbackListeners: Set<TierEventListener<TierFallbackEvent>> = new Set()
|
|
241
|
+
|
|
242
|
+
constructor(options: TierSwitcherOptions = {}) {
|
|
243
|
+
// Set up capabilities first
|
|
244
|
+
this.capabilities = options.capabilities ?? detectTierCapabilities()
|
|
245
|
+
this.fallbackBehavior = options.fallbackBehavior ?? 'none'
|
|
246
|
+
this.batchRerenders = options.batchRerenders ?? false
|
|
247
|
+
this.batchDelay = options.batchDelay ?? 0
|
|
248
|
+
this.onChange = options.onChange ?? null
|
|
249
|
+
this.onRerender = options.onRerender ?? null
|
|
250
|
+
this.storage = options.storage ?? null
|
|
251
|
+
|
|
252
|
+
// Load user preference from storage
|
|
253
|
+
if (this.storage) {
|
|
254
|
+
const stored = this.storage.get(STORAGE_KEY)
|
|
255
|
+
if (stored && VALID_TIERS.has(stored)) {
|
|
256
|
+
this.userPreference = stored as RenderTier
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Apply user preference if provided in options (overrides storage)
|
|
261
|
+
if (options.userPreference) {
|
|
262
|
+
this.userPreference = options.userPreference
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Determine initial tier
|
|
266
|
+
if (options.initialTier) {
|
|
267
|
+
this.currentTier = options.initialTier
|
|
268
|
+
} else if (options.autoDetect !== false) {
|
|
269
|
+
// Auto-detect: use user preference if available and supported, otherwise highest available
|
|
270
|
+
// Only auto-select highest tier when explicit capabilities are provided
|
|
271
|
+
// This ensures predictable behavior in test environments
|
|
272
|
+
if (this.userPreference && this.capabilities[this.userPreference]) {
|
|
273
|
+
this.currentTier = this.userPreference
|
|
274
|
+
} else if (this.userPreference && !this.capabilities[this.userPreference]) {
|
|
275
|
+
// User preference not available, fall back to highest
|
|
276
|
+
this.currentTier = this.getHighestAvailableTier()
|
|
277
|
+
} else if (options.capabilities) {
|
|
278
|
+
// Explicit capabilities provided, use highest available
|
|
279
|
+
this.currentTier = this.getHighestAvailableTier()
|
|
280
|
+
} else {
|
|
281
|
+
// No explicit capabilities, default to text for predictable behavior
|
|
282
|
+
this.currentTier = 'text'
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
this.currentTier = 'text'
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private getHighestAvailableTier(): RenderTier {
|
|
290
|
+
for (let i = TIER_ORDER.length - 1; i >= 0; i--) {
|
|
291
|
+
const tier = TIER_ORDER[i]
|
|
292
|
+
if (this.capabilities[tier]) {
|
|
293
|
+
return tier
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return 'text'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private getLowestAvailableTier(): RenderTier {
|
|
300
|
+
return 'text'
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private findNearestLowerTier(tier: RenderTier): RenderTier {
|
|
304
|
+
const index = TIER_ORDER_MAP[tier]
|
|
305
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
306
|
+
const lowerTier = TIER_ORDER[i]
|
|
307
|
+
if (this.capabilities[lowerTier]) {
|
|
308
|
+
return lowerTier
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return 'text'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private findNearestHigherTier(tier: RenderTier): RenderTier | null {
|
|
315
|
+
const index = TIER_ORDER_MAP[tier]
|
|
316
|
+
for (let i = index + 1; i < TIER_ORDER.length; i++) {
|
|
317
|
+
const higherTier = TIER_ORDER[i]
|
|
318
|
+
if (this.capabilities[higherTier]) {
|
|
319
|
+
return higherTier
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private emitChange(previousTier: RenderTier, newTier: RenderTier, shouldRerender: boolean): void {
|
|
326
|
+
const event: TierChangeEvent = {
|
|
327
|
+
previousTier,
|
|
328
|
+
newTier,
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
capabilities: Object.freeze({ ...this.capabilities }),
|
|
331
|
+
shouldRerender,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Call global onChange callback
|
|
335
|
+
if (this.onChange) {
|
|
336
|
+
this.onChange(event)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Call registered listeners
|
|
340
|
+
for (const listener of this.changeListeners) {
|
|
341
|
+
listener(event)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Handle rerender
|
|
345
|
+
if (shouldRerender && this.onRerender) {
|
|
346
|
+
if (this.batching) {
|
|
347
|
+
this.pendingRerender = true
|
|
348
|
+
} else if (this.batchRerenders && this.batchDelay > 0) {
|
|
349
|
+
// Debounce rerenders
|
|
350
|
+
if (this.batchTimeout) {
|
|
351
|
+
clearTimeout(this.batchTimeout)
|
|
352
|
+
}
|
|
353
|
+
this.batchTimeout = setTimeout(() => {
|
|
354
|
+
if (this.onRerender && !this.destroyed) {
|
|
355
|
+
this.onRerender()
|
|
356
|
+
}
|
|
357
|
+
this.batchTimeout = null
|
|
358
|
+
}, this.batchDelay)
|
|
359
|
+
} else {
|
|
360
|
+
this.onRerender()
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private emitFallback(requestedTier: RenderTier, actualTier: RenderTier): void {
|
|
366
|
+
const event: TierFallbackEvent = {
|
|
367
|
+
requestedTier,
|
|
368
|
+
actualTier,
|
|
369
|
+
reason: 'unavailable',
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const listener of this.fallbackListeners) {
|
|
373
|
+
listener(event)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getCurrentTier(): RenderTier {
|
|
378
|
+
return this.currentTier
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
setTier(tier: RenderTier, options: SetTierOptions = {}): boolean {
|
|
382
|
+
if (this.destroyed) {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate tier
|
|
387
|
+
if (!VALID_TIERS.has(tier)) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { fallback = false, rerender = true } = options
|
|
392
|
+
|
|
393
|
+
// Check if tier is available
|
|
394
|
+
if (!this.capabilities[tier]) {
|
|
395
|
+
if (fallback && this.fallbackBehavior === 'downgrade') {
|
|
396
|
+
// Find nearest available tier
|
|
397
|
+
const fallbackTier = this.findFallbackTier(tier)
|
|
398
|
+
// Always emit fallback event when falling back
|
|
399
|
+
this.emitFallback(tier, fallbackTier)
|
|
400
|
+
if (fallbackTier !== this.currentTier) {
|
|
401
|
+
const previousTier = this.currentTier
|
|
402
|
+
this.currentTier = fallbackTier
|
|
403
|
+
this.emitChange(previousTier, fallbackTier, rerender)
|
|
404
|
+
}
|
|
405
|
+
return true
|
|
406
|
+
}
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Same tier, no change needed
|
|
411
|
+
if (tier === this.currentTier) {
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const previousTier = this.currentTier
|
|
416
|
+
this.currentTier = tier
|
|
417
|
+
this.emitChange(previousTier, tier, rerender)
|
|
418
|
+
return true
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private findFallbackTier(requestedTier: RenderTier): RenderTier {
|
|
422
|
+
const requestedIndex = TIER_ORDER_MAP[requestedTier]
|
|
423
|
+
|
|
424
|
+
// Find highest available tier at or below requested
|
|
425
|
+
for (let i = requestedIndex; i >= 0; i--) {
|
|
426
|
+
const tier = TIER_ORDER[i]
|
|
427
|
+
if (this.capabilities[tier]) {
|
|
428
|
+
return tier
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return 'text'
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getCapabilities(): Readonly<TierCapabilities> {
|
|
436
|
+
return Object.freeze({ ...this.capabilities })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
getAvailableTiers(): RenderTier[] {
|
|
440
|
+
return TIER_ORDER.filter(tier => this.capabilities[tier])
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
canSwitchTo(tier: RenderTier): boolean {
|
|
444
|
+
return canSwitchToTier(tier, this.capabilities)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
upgradeToHighest(): RenderTier {
|
|
448
|
+
const highest = this.getHighestAvailableTier()
|
|
449
|
+
if (highest !== this.currentTier) {
|
|
450
|
+
this.setTier(highest)
|
|
451
|
+
}
|
|
452
|
+
return highest
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
downgradeToLowest(): RenderTier {
|
|
456
|
+
const lowest = this.getLowestAvailableTier()
|
|
457
|
+
if (lowest !== this.currentTier) {
|
|
458
|
+
this.setTier(lowest)
|
|
459
|
+
}
|
|
460
|
+
return lowest
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
upgradeOneLevel(): RenderTier {
|
|
464
|
+
const nextTier = this.findNearestHigherTier(this.currentTier)
|
|
465
|
+
if (nextTier) {
|
|
466
|
+
this.setTier(nextTier)
|
|
467
|
+
return nextTier
|
|
468
|
+
}
|
|
469
|
+
return this.currentTier
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
downgradeOneLevel(): RenderTier {
|
|
473
|
+
const currentIndex = TIER_ORDER_MAP[this.currentTier]
|
|
474
|
+
if (currentIndex === 0) {
|
|
475
|
+
return this.currentTier
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Find the next lower available tier
|
|
479
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
480
|
+
const tier = TIER_ORDER[i]
|
|
481
|
+
if (this.capabilities[tier]) {
|
|
482
|
+
this.setTier(tier)
|
|
483
|
+
return tier
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return this.currentTier
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
setUserPreference(tier: RenderTier): void {
|
|
491
|
+
this.userPreference = tier
|
|
492
|
+
if (this.storage) {
|
|
493
|
+
this.storage.set(STORAGE_KEY, tier)
|
|
494
|
+
}
|
|
495
|
+
// Apply preference if available
|
|
496
|
+
if (this.capabilities[tier]) {
|
|
497
|
+
this.setTier(tier)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getUserPreference(): RenderTier | null {
|
|
502
|
+
return this.userPreference
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
clearUserPreference(): void {
|
|
506
|
+
this.userPreference = null
|
|
507
|
+
if (this.storage) {
|
|
508
|
+
this.storage.remove(STORAGE_KEY)
|
|
509
|
+
}
|
|
510
|
+
// Upgrade to highest available tier
|
|
511
|
+
this.upgradeToHighest()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
updateCapabilities(capabilities: TierCapabilities): void {
|
|
515
|
+
this.capabilities = { ...capabilities }
|
|
516
|
+
|
|
517
|
+
// If current tier is no longer available, fall back
|
|
518
|
+
if (!this.capabilities[this.currentTier]) {
|
|
519
|
+
const fallbackTier = this.getHighestAvailableTier()
|
|
520
|
+
const previousTier = this.currentTier
|
|
521
|
+
this.currentTier = fallbackTier
|
|
522
|
+
this.emitChange(previousTier, fallbackTier, true)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
on(event: 'change', listener: TierEventListener<TierChangeEvent>): () => void
|
|
527
|
+
on(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): () => void
|
|
528
|
+
on(event: TierEventType, listener: TierEventListener<any>): () => void {
|
|
529
|
+
if (event === 'change') {
|
|
530
|
+
this.changeListeners.add(listener)
|
|
531
|
+
return () => this.changeListeners.delete(listener)
|
|
532
|
+
} else if (event === 'fallback') {
|
|
533
|
+
this.fallbackListeners.add(listener)
|
|
534
|
+
return () => this.fallbackListeners.delete(listener)
|
|
535
|
+
}
|
|
536
|
+
return () => {}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
off(event: 'change', listener: TierEventListener<TierChangeEvent>): void
|
|
540
|
+
off(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): void
|
|
541
|
+
off(event: TierEventType, listener: TierEventListener<any>): void {
|
|
542
|
+
if (event === 'change') {
|
|
543
|
+
this.changeListeners.delete(listener)
|
|
544
|
+
} else if (event === 'fallback') {
|
|
545
|
+
this.fallbackListeners.delete(listener)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
once(event: 'change', listener: TierEventListener<TierChangeEvent>): void
|
|
550
|
+
once(event: 'fallback', listener: TierEventListener<TierFallbackEvent>): void
|
|
551
|
+
once(event: TierEventType, listener: TierEventListener<any>): void {
|
|
552
|
+
const onceListener = (e: any) => {
|
|
553
|
+
this.off(event as any, onceListener)
|
|
554
|
+
listener(e)
|
|
555
|
+
}
|
|
556
|
+
this.on(event as any, onceListener)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
batch(fn: () => void): void {
|
|
560
|
+
this.batching = true
|
|
561
|
+
this.pendingRerender = false
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
fn()
|
|
565
|
+
} finally {
|
|
566
|
+
this.batching = false
|
|
567
|
+
|
|
568
|
+
// Execute pending rerender
|
|
569
|
+
if (this.pendingRerender && this.onRerender && !this.destroyed) {
|
|
570
|
+
this.onRerender()
|
|
571
|
+
}
|
|
572
|
+
this.pendingRerender = false
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
destroy(): void {
|
|
577
|
+
if (this.destroyed) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.destroyed = true
|
|
582
|
+
this.changeListeners.clear()
|
|
583
|
+
this.fallbackListeners.clear()
|
|
584
|
+
this.onChange = null
|
|
585
|
+
this.onRerender = null
|
|
586
|
+
|
|
587
|
+
if (this.batchTimeout) {
|
|
588
|
+
clearTimeout(this.batchTimeout)
|
|
589
|
+
this.batchTimeout = null
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
isDestroyed(): boolean {
|
|
594
|
+
return this.destroyed
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ============================================================================
|
|
599
|
+
// Factory Function
|
|
600
|
+
// ============================================================================
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Create a new TierSwitcher instance
|
|
604
|
+
*/
|
|
605
|
+
export function createTierSwitcher(options?: TierSwitcherOptions): TierSwitcher {
|
|
606
|
+
return new TierSwitcherImpl(options)
|
|
607
|
+
}
|