@rakeyshgidwani/roger-ui-bank-theme-harvey 0.2.51 → 0.3.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 (128) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/dist/components/ui/button.d.ts +3 -1
  3. package/dist/components/ui/button.d.ts.map +1 -1
  4. package/dist/components/ui/button.esm.js +3 -2
  5. package/dist/components/ui/button.js +3 -2
  6. package/dist/components/ui/layout/container.d.ts +57 -0
  7. package/dist/components/ui/layout/container.d.ts.map +1 -0
  8. package/dist/components/ui/layout/container.esm.js +173 -0
  9. package/dist/components/ui/layout/container.js +173 -0
  10. package/dist/components/ui/layout/index.d.ts +9 -0
  11. package/dist/components/ui/layout/index.d.ts.map +1 -0
  12. package/dist/components/ui/layout/index.esm.js +6 -0
  13. package/dist/components/ui/layout/index.js +6 -0
  14. package/dist/components/ui/layout/responsive-grid.d.ts +93 -0
  15. package/dist/components/ui/layout/responsive-grid.d.ts.map +1 -0
  16. package/dist/components/ui/layout/responsive-grid.esm.js +124 -0
  17. package/dist/components/ui/layout/responsive-grid.js +124 -0
  18. package/dist/components/ui/navigation/index.d.ts +2 -1
  19. package/dist/components/ui/navigation/index.d.ts.map +1 -1
  20. package/dist/components/ui/navigation/index.esm.js +1 -0
  21. package/dist/components/ui/navigation/index.js +1 -0
  22. package/dist/components/ui/navigation/progressive-navigation.d.ts +37 -0
  23. package/dist/components/ui/navigation/progressive-navigation.d.ts.map +1 -0
  24. package/dist/components/ui/navigation/progressive-navigation.esm.js +145 -0
  25. package/dist/components/ui/navigation/progressive-navigation.js +145 -0
  26. package/dist/components/ui/navigation/types.d.ts +21 -0
  27. package/dist/components/ui/navigation/types.d.ts.map +1 -1
  28. package/dist/components/ui/theme-toggle.esm.js +1 -1
  29. package/dist/components/ui/theme-toggle.js +1 -1
  30. package/dist/hooks/use-adaptive-layout.d.ts +2 -1
  31. package/dist/hooks/use-adaptive-layout.d.ts.map +1 -1
  32. package/dist/hooks/use-adaptive-layout.esm.js +13 -8
  33. package/dist/hooks/use-adaptive-layout.js +13 -8
  34. package/dist/hooks/use-device.d.ts +3 -1
  35. package/dist/hooks/use-device.d.ts.map +1 -1
  36. package/dist/hooks/use-device.esm.js +14 -7
  37. package/dist/hooks/use-device.js +14 -7
  38. package/dist/index.d.ts +19 -4
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.esm.js +9 -4
  41. package/dist/index.js +9 -4
  42. package/dist/plugins/css-purge-optimizer.d.ts +25 -0
  43. package/dist/plugins/css-purge-optimizer.d.ts.map +1 -0
  44. package/dist/plugins/css-purge-optimizer.esm.js +414 -0
  45. package/dist/plugins/css-purge-optimizer.js +414 -0
  46. package/dist/plugins/performance-monitor.d.ts +29 -0
  47. package/dist/plugins/performance-monitor.d.ts.map +1 -0
  48. package/dist/plugins/performance-monitor.esm.js +221 -0
  49. package/dist/plugins/performance-monitor.js +221 -0
  50. package/dist/plugins/progressive-css-loader.d.ts +21 -0
  51. package/dist/plugins/progressive-css-loader.d.ts.map +1 -0
  52. package/dist/plugins/progressive-css-loader.esm.js +227 -0
  53. package/dist/plugins/progressive-css-loader.js +227 -0
  54. package/dist/plugins/theme-css-generator.d.ts.map +1 -1
  55. package/dist/plugins/theme-css-generator.esm.js +19 -6
  56. package/dist/plugins/theme-css-generator.js +19 -6
  57. package/dist/styles.css +1025 -110
  58. package/dist/theme.d.ts.map +1 -1
  59. package/dist/theme.esm.js +4 -1
  60. package/dist/theme.js +4 -1
  61. package/dist/themes/phase1-constants.d.ts +23 -0
  62. package/dist/themes/phase1-constants.d.ts.map +1 -0
  63. package/dist/themes/phase1-constants.esm.js +180 -0
  64. package/dist/themes/phase1-constants.js +180 -0
  65. package/dist/themes/themes/default.d.ts.map +1 -1
  66. package/dist/themes/themes/default.esm.js +4 -1
  67. package/dist/themes/themes/default.js +4 -1
  68. package/dist/themes/themes/harvey.d.ts.map +1 -1
  69. package/dist/themes/themes/harvey.esm.js +4 -1
  70. package/dist/themes/themes/harvey.js +4 -1
  71. package/dist/themes/types.d.ts +62 -0
  72. package/dist/themes/types.d.ts.map +1 -1
  73. package/dist/themes/validation.d.ts +17 -0
  74. package/dist/themes/validation.d.ts.map +1 -1
  75. package/dist/themes/validation.esm.js +218 -0
  76. package/dist/themes/validation.js +218 -0
  77. package/dist/types.d.ts +62 -0
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/utils/progressive-css-injector.d.ts +80 -0
  80. package/dist/utils/progressive-css-injector.d.ts.map +1 -0
  81. package/dist/utils/progressive-css-injector.esm.js +217 -0
  82. package/dist/utils/progressive-css-injector.js +217 -0
  83. package/package.json +1 -1
  84. package/src/components/ui/button.tsx +9 -6
  85. package/src/components/ui/layout/container.tsx +312 -0
  86. package/src/components/ui/layout/index.ts +10 -0
  87. package/src/components/ui/layout/responsive-grid.tsx +286 -0
  88. package/src/components/ui/navigation/index.ts +2 -0
  89. package/src/components/ui/navigation/progressive-navigation.tsx +453 -0
  90. package/src/components/ui/navigation/types.ts +41 -0
  91. package/src/components/ui/theme-toggle.tsx +4 -4
  92. package/src/hooks/use-adaptive-layout.ts +13 -9
  93. package/src/hooks/use-device.tsx +17 -10
  94. package/src/index.ts +19 -4
  95. package/src/plugins/css-purge-optimizer.ts +491 -0
  96. package/src/plugins/performance-monitor.ts +292 -0
  97. package/src/plugins/progressive-css-loader.ts +269 -0
  98. package/src/plugins/theme-css-generator.ts +22 -6
  99. package/src/styles/components/base/badge.css +2 -2
  100. package/src/styles/components/base/button.css +238 -35
  101. package/src/styles/components/base/card.css +2 -2
  102. package/src/styles/components/base/checkbox.css +3 -3
  103. package/src/styles/components/base/label.css +3 -3
  104. package/src/styles/components/feedback/skeleton.css +1 -1
  105. package/src/styles/components/feedback/toast.css +1 -1
  106. package/src/styles/components/index.css +3 -0
  107. package/src/styles/components/layout/container.css +466 -0
  108. package/src/styles/components/layout/index.css +5 -0
  109. package/src/styles/components/layout/responsive-grid.css +422 -0
  110. package/src/styles/components/navigation/breadcrumb.css +1 -1
  111. package/src/styles/components/navigation/index.css +1 -0
  112. package/src/styles/components/navigation/menu.css +2 -2
  113. package/src/styles/components/navigation/pagination.css +4 -4
  114. package/src/styles/components/navigation/progressive-navigation.css +633 -0
  115. package/src/styles/components/navigation/sidebar.css +4 -4
  116. package/src/styles/components/navigation/stepper.css +2 -2
  117. package/src/styles/components/navigation/tabs.css +1 -1
  118. package/src/styles/progressive.css +17 -0
  119. package/src/styles/themes/harvey.css +103 -19
  120. package/src/styles/utilities/semantic-input-system.css +7 -13
  121. package/src/theme.ts +5 -1
  122. package/src/themes/phase1-constants.ts +189 -0
  123. package/src/themes/themes/default.ts +5 -1
  124. package/src/themes/themes/harvey.ts +5 -1
  125. package/src/themes/types.ts +77 -1
  126. package/src/themes/validation.ts +249 -0
  127. package/src/types.ts +77 -1
  128. package/src/utils/progressive-css-injector.ts +254 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Performance Monitor Plugin
3
+ * Implements Phase 3 Performance Optimization: Bundle Size Monitoring
4
+ *
5
+ * Tracks:
6
+ * - CSS bundle sizes (critical vs progressive)
7
+ * - JavaScript bundle sizes
8
+ * - Performance metrics and alerts
9
+ * - Loading performance analysis
10
+ */
11
+
12
+ import type { Plugin } from 'vite'
13
+ import fs from 'fs'
14
+ import path from 'path'
15
+
16
+ interface PerformanceConfig {
17
+ // Bundle size thresholds (in KB)
18
+ thresholds: {
19
+ criticalCSS: number
20
+ progressiveCSS: number
21
+ totalCSS: number
22
+ javascript: number
23
+ }
24
+ // Output configuration
25
+ reportPath: string
26
+ enableAlerts: boolean
27
+ baselinePath?: string
28
+ }
29
+
30
+ interface BundleAnalysis {
31
+ timestamp: string
32
+ files: {
33
+ [filename: string]: {
34
+ size: number
35
+ gzipSize?: number
36
+ type: 'css' | 'js' | 'asset'
37
+ category?: 'critical' | 'progressive' | 'component' | 'vendor'
38
+ }
39
+ }
40
+ totals: {
41
+ criticalCSS: number
42
+ progressiveCSS: number
43
+ totalCSS: number
44
+ javascript: number
45
+ assets: number
46
+ total: number
47
+ }
48
+ performance: {
49
+ bundleTime: number
50
+ compressionRatio: number
51
+ }
52
+ alerts: string[]
53
+ }
54
+
55
+ const DEFAULT_CONFIG: PerformanceConfig = {
56
+ thresholds: {
57
+ criticalCSS: 50, // KB - Mobile-critical styles should be minimal
58
+ progressiveCSS: 150, // KB - Desktop enhancement styles
59
+ totalCSS: 200, // KB - Total CSS budget
60
+ javascript: 500 // KB - JS bundle threshold
61
+ },
62
+ reportPath: 'performance-report.json',
63
+ enableAlerts: true
64
+ }
65
+
66
+ export default function performanceMonitor(config: Partial<PerformanceConfig> = {}): Plugin {
67
+ const finalConfig = { ...DEFAULT_CONFIG, ...config }
68
+ const startTime = Date.now()
69
+
70
+ return {
71
+ name: 'performance-monitor',
72
+ apply: 'build',
73
+ generateBundle(_options, bundle) {
74
+ const analysis = analyzeBundle(bundle, startTime, finalConfig)
75
+
76
+ // Write performance report
77
+ const reportPath = path.resolve(finalConfig.reportPath)
78
+ fs.writeFileSync(reportPath, JSON.stringify(analysis, null, 2))
79
+
80
+ // Console output
81
+ logPerformanceReport(analysis, finalConfig)
82
+
83
+ // Alerts
84
+ if (finalConfig.enableAlerts && analysis.alerts.length > 0) {
85
+ console.warn('\n⚠️ Performance Alerts:')
86
+ analysis.alerts.forEach(alert => console.warn(` ${alert}`))
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Analyzes bundle contents and generates performance report
94
+ */
95
+ function analyzeBundle(bundle: any, startTime: number, config: PerformanceConfig): BundleAnalysis {
96
+ const analysis: BundleAnalysis = {
97
+ timestamp: new Date().toISOString(),
98
+ files: {},
99
+ totals: {
100
+ criticalCSS: 0,
101
+ progressiveCSS: 0,
102
+ totalCSS: 0,
103
+ javascript: 0,
104
+ assets: 0,
105
+ total: 0
106
+ },
107
+ performance: {
108
+ bundleTime: Date.now() - startTime,
109
+ compressionRatio: 0
110
+ },
111
+ alerts: []
112
+ }
113
+
114
+ // Analyze each file in the bundle
115
+ Object.entries(bundle).forEach(([fileName, asset]: [string, any]) => {
116
+ if (asset.type === 'asset' || asset.type === 'chunk') {
117
+ const size = getAssetSize(asset)
118
+ const type = getAssetType(fileName)
119
+ const category = getAssetCategory(fileName)
120
+
121
+ analysis.files[fileName] = {
122
+ size,
123
+ type,
124
+ category
125
+ }
126
+
127
+ // Update totals
128
+ if (type === 'css') {
129
+ if (category === 'critical') {
130
+ analysis.totals.criticalCSS += size
131
+ } else if (category === 'progressive') {
132
+ analysis.totals.progressiveCSS += size
133
+ }
134
+ analysis.totals.totalCSS += size
135
+ } else if (type === 'js') {
136
+ analysis.totals.javascript += size
137
+ } else {
138
+ analysis.totals.assets += size
139
+ }
140
+
141
+ analysis.totals.total += size
142
+ }
143
+ })
144
+
145
+ // Calculate content-aware estimated gzip compression ratio
146
+ const calculateEstimatedGzipRatio = (): number => {
147
+ // Realistic gzip compression ratios by content type (compressed size / original size)
148
+ const cssGzipRatio = 0.15 // CSS compresses to ~15% of original size
149
+ const jsGzipRatio = 0.25 // JavaScript compresses to ~25% of original size
150
+
151
+ const cssSize = analysis.totals.totalCSS
152
+ const jsSize = analysis.totals.javascript
153
+
154
+ // Only calculate gzip for CSS and JS (assets like fonts/images typically aren't gzipped by CDNs)
155
+ const compressibleSize = cssSize + jsSize
156
+
157
+ if (compressibleSize === 0) return 0
158
+
159
+ // Calculate estimated gzipped size for compressible content only
160
+ const estimatedGzipSize =
161
+ (cssSize * cssGzipRatio) +
162
+ (jsSize * jsGzipRatio)
163
+
164
+ // Return ratio of estimated gzipped size to total compressible size
165
+ return Math.round((estimatedGzipSize / compressibleSize) * 100) / 100
166
+ }
167
+
168
+ analysis.performance.compressionRatio = calculateEstimatedGzipRatio()
169
+
170
+ // Generate alerts
171
+ analysis.alerts = generateAlerts(analysis.totals, config.thresholds)
172
+
173
+ return analysis
174
+ }
175
+
176
+ /**
177
+ * Determines asset size in KB
178
+ */
179
+ function getAssetSize(asset: any): number {
180
+ const source = asset.source || asset.code || ''
181
+ const bytes = typeof source === 'string' ? Buffer.byteLength(source, 'utf8') : source.length
182
+ return Math.round(bytes / 1024 * 100) / 100 // KB with 2 decimal precision
183
+ }
184
+
185
+ /**
186
+ * Determines asset type from filename
187
+ */
188
+ function getAssetType(fileName: string): 'css' | 'js' | 'asset' {
189
+ if (fileName.endsWith('.css')) return 'css'
190
+ if (fileName.endsWith('.js') || fileName.endsWith('.mjs')) return 'js'
191
+ return 'asset'
192
+ }
193
+
194
+ /**
195
+ * Categorizes assets for performance analysis
196
+ */
197
+ function getAssetCategory(fileName: string): 'critical' | 'progressive' | 'component' | 'vendor' | undefined {
198
+ if (fileName.includes('critical')) return 'critical'
199
+ if (fileName.includes('progressive')) return 'progressive'
200
+ if (fileName.includes('vendor') || fileName.includes('node_modules')) return 'vendor'
201
+ if (fileName.includes('component')) return 'component'
202
+ return undefined
203
+ }
204
+
205
+ /**
206
+ * Generates performance alerts based on thresholds
207
+ */
208
+ function generateAlerts(totals: BundleAnalysis['totals'], thresholds: PerformanceConfig['thresholds']): string[] {
209
+ const alerts: string[] = []
210
+
211
+ if (totals.criticalCSS > thresholds.criticalCSS) {
212
+ alerts.push(`Critical CSS size (${totals.criticalCSS}KB) exceeds threshold (${thresholds.criticalCSS}KB)`)
213
+ }
214
+
215
+ if (totals.progressiveCSS > thresholds.progressiveCSS) {
216
+ alerts.push(`Progressive CSS size (${totals.progressiveCSS}KB) exceeds threshold (${thresholds.progressiveCSS}KB)`)
217
+ }
218
+
219
+ if (totals.totalCSS > thresholds.totalCSS) {
220
+ alerts.push(`Total CSS size (${totals.totalCSS}KB) exceeds threshold (${thresholds.totalCSS}KB)`)
221
+ }
222
+
223
+ if (totals.javascript > thresholds.javascript) {
224
+ alerts.push(`JavaScript bundle size (${totals.javascript}KB) exceeds threshold (${thresholds.javascript}KB)`)
225
+ }
226
+
227
+ return alerts
228
+ }
229
+
230
+ /**
231
+ * Logs performance report to console
232
+ */
233
+ function logPerformanceReport(analysis: BundleAnalysis, config: PerformanceConfig): void {
234
+ console.log('\n📊 Performance Analysis Report')
235
+ console.log('================================')
236
+
237
+ console.log('\n📦 Bundle Sizes:')
238
+ console.log(` Critical CSS: ${analysis.totals.criticalCSS}KB (threshold: ${config.thresholds.criticalCSS}KB)`)
239
+ console.log(` Progressive CSS: ${analysis.totals.progressiveCSS}KB (threshold: ${config.thresholds.progressiveCSS}KB)`)
240
+ console.log(` Total CSS: ${analysis.totals.totalCSS}KB (threshold: ${config.thresholds.totalCSS}KB)`)
241
+ console.log(` JavaScript: ${analysis.totals.javascript}KB (threshold: ${config.thresholds.javascript}KB)`)
242
+ console.log(` Assets: ${analysis.totals.assets}KB`)
243
+ console.log(` Total Bundle: ${analysis.totals.total}KB`)
244
+
245
+ console.log('\n⚡ Performance Metrics:')
246
+ console.log(` Build Time: ${analysis.performance.bundleTime}ms`)
247
+ const compressibleSize = analysis.totals.totalCSS + analysis.totals.javascript
248
+ console.log(` Gzip Ratio: ${analysis.performance.compressionRatio} (CSS+JS compression)`)
249
+ console.log(` Est. Gzip Size: ~${Math.round(compressibleSize * analysis.performance.compressionRatio)}KB (CSS+JS only)`)
250
+
251
+ console.log(`\n📝 Report saved to: ${config.reportPath}`)
252
+
253
+ if (analysis.alerts.length === 0) {
254
+ console.log('\n✅ All performance thresholds met!')
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Creates performance comparison utility
260
+ */
261
+ export function comparePerformance(currentReport: string, baselineReport?: string): void {
262
+ if (!baselineReport || !fs.existsSync(baselineReport)) {
263
+ console.log('\n📈 Baseline report not found. Current report will serve as baseline.')
264
+ return
265
+ }
266
+
267
+ try {
268
+ const current: BundleAnalysis = JSON.parse(fs.readFileSync(currentReport, 'utf8'))
269
+ const baseline: BundleAnalysis = JSON.parse(fs.readFileSync(baselineReport, 'utf8'))
270
+
271
+ console.log('\n📈 Performance Comparison (vs baseline)')
272
+ console.log('======================================')
273
+
274
+ const metrics = [
275
+ { name: 'Critical CSS', current: current.totals.criticalCSS, baseline: baseline.totals.criticalCSS },
276
+ { name: 'Progressive CSS', current: current.totals.progressiveCSS, baseline: baseline.totals.progressiveCSS },
277
+ { name: 'Total CSS', current: current.totals.totalCSS, baseline: baseline.totals.totalCSS },
278
+ { name: 'JavaScript', current: current.totals.javascript, baseline: baseline.totals.javascript },
279
+ { name: 'Total Bundle', current: current.totals.total, baseline: baseline.totals.total }
280
+ ]
281
+
282
+ metrics.forEach(metric => {
283
+ const diff = metric.current - metric.baseline
284
+ const diffPercent = metric.baseline > 0 ? Math.round((diff / metric.baseline) * 100) : 0
285
+ const indicator = diff > 0 ? '📈' : diff < 0 ? '📉' : '➡️'
286
+
287
+ console.log(` ${metric.name.padEnd(15)} ${indicator} ${metric.current}KB (${diff >= 0 ? '+' : ''}${diff}KB, ${diffPercent >= 0 ? '+' : ''}${diffPercent}%)`)
288
+ })
289
+ } catch (error) {
290
+ console.error('Failed to compare performance reports:', error)
291
+ }
292
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Progressive CSS Loader Plugin
3
+ * Implements Phase 3 Performance Optimization: Progressive Enhancement Loading
4
+ *
5
+ * Separates CSS into:
6
+ * - Critical: xs-md breakpoints (mobile-tablet) - loaded immediately
7
+ * - Progressive: lg-3xl breakpoints (desktop+) - loaded on demand
8
+ */
9
+
10
+ import type { Plugin } from 'vite'
11
+ import postcss from 'postcss'
12
+
13
+ interface ProgressiveCSSConfig {
14
+ // Critical breakpoints (always loaded)
15
+ critical: string[]
16
+ // Progressive breakpoints (loaded on demand)
17
+ progressive: string[]
18
+ // Progressive CSS output path
19
+ progressivePath: string
20
+ }
21
+
22
+ const DEFAULT_CONFIG: ProgressiveCSSConfig = {
23
+ critical: ['xs', 'sm', 'md'],
24
+ progressive: ['lg', 'xl', '2xl', '3xl'],
25
+ progressivePath: 'progressive.css'
26
+ }
27
+
28
+ export default function progressiveCSSLoader(config: Partial<ProgressiveCSSConfig> = {}): Plugin {
29
+ const finalConfig = { ...DEFAULT_CONFIG, ...config }
30
+
31
+ return {
32
+ name: 'progressive-css-loader',
33
+ apply: 'build',
34
+ generateBundle(_options, bundle) {
35
+ // Find CSS assets in bundle
36
+ const cssAssets = Object.keys(bundle).filter(key => key.endsWith('.css'))
37
+
38
+ cssAssets.forEach((assetKey, index) => {
39
+ const asset = bundle[assetKey]
40
+ if (asset.type === 'asset' && typeof asset.source === 'string') {
41
+ const originalSource = asset.source
42
+
43
+ // Safely separate CSS into critical and progressive parts
44
+ const { critical, progressive } = separateCSS(originalSource, finalConfig)
45
+ const originalSize = Math.round(originalSource.length / 1024)
46
+ const criticalSize = Math.round(critical.length / 1024)
47
+ const progressiveSize = Math.round(progressive.length / 1024)
48
+
49
+ console.log(` Split ${assetKey}: ${originalSize}KB -> ${criticalSize}KB critical + ${progressiveSize}KB progressive`)
50
+
51
+ // Update the main CSS asset to contain only critical CSS
52
+ asset.source = critical
53
+
54
+ // Create separate progressive CSS file with unique naming for desktop enhancement
55
+ if (progressive.trim()) {
56
+ const progressiveFileName = index === 0
57
+ ? finalConfig.progressivePath
58
+ : finalConfig.progressivePath.replace('.css', `-${index}.css`)
59
+
60
+ this.emitFile({
61
+ type: 'asset',
62
+ fileName: progressiveFileName,
63
+ source: progressive
64
+ })
65
+ console.log(` Created ${progressiveFileName} for desktop enhancement (${progressiveSize}KB)`)
66
+ }
67
+
68
+ // Skip creating explicit critical CSS file since main bundle already contains critical styles
69
+ }
70
+ })
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * CSS Variable to pixel value mapping based on design system
77
+ * This provides the authoritative mapping for breakpoint classification
78
+ */
79
+ const BREAKPOINT_VALUES: Record<string, number> = {
80
+ 'xs': 475,
81
+ 'sm': 640,
82
+ 'mobile': 640,
83
+ 'md': 768,
84
+ 'tablet': 768,
85
+ 'lg': 1024,
86
+ 'desktop': 1024,
87
+ 'xl': 1280,
88
+ 'wide': 1280,
89
+ '2xl': 1536,
90
+ 'ultra': 1536,
91
+ '3xl': 1920
92
+ }
93
+
94
+ /**
95
+ * Desktop breakpoint threshold - anything >= 1024px is considered desktop
96
+ */
97
+ const DESKTOP_THRESHOLD = 1024
98
+
99
+ /**
100
+ * Determines if a media query represents a desktop breakpoint
101
+ * Uses CSS variable resolution to properly classify breakpoints
102
+ */
103
+ function isDesktopMediaQuery(mediaQuery: string): boolean {
104
+ // Skip max-width queries - they should always stay in critical CSS
105
+ if (mediaQuery.includes('max-width')) {
106
+ return false
107
+ }
108
+
109
+ // Check for CSS variable-based breakpoints
110
+ const variableMatch = mediaQuery.match(/var\(--cs-breakpoints-([^)]+)\)/)
111
+ if (variableMatch) {
112
+ const breakpointName = variableMatch[1]
113
+ const pixelValue = BREAKPOINT_VALUES[breakpointName]
114
+
115
+ if (pixelValue !== undefined) {
116
+ return pixelValue >= DESKTOP_THRESHOLD
117
+ }
118
+
119
+ // If we don't recognize the variable, log it and assume it's critical for safety
120
+ console.warn(`Unknown breakpoint variable: --cs-breakpoints-${breakpointName}`)
121
+ return false
122
+ }
123
+
124
+ // Check for direct pixel values
125
+ const pixelMatch = mediaQuery.match(/min-width:\s*(\d+)px/)
126
+ if (pixelMatch) {
127
+ const pixelValue = parseInt(pixelMatch[1], 10)
128
+ return pixelValue >= DESKTOP_THRESHOLD
129
+ }
130
+
131
+ // If we can't classify it, keep it in critical CSS for safety
132
+ return false
133
+ }
134
+
135
+ /**
136
+ * Safely separates CSS content into critical and progressive sections
137
+ * Critical: < 1024px breakpoints (mobile/tablet) + base styles
138
+ * Progressive: >= 1024px breakpoints (desktop+) only
139
+ */
140
+ function separateCSS(cssContent: string, _config: ProgressiveCSSConfig) {
141
+ try {
142
+ const desktopRules: string[] = []
143
+
144
+ // Parse CSS using PostCSS
145
+ const root = postcss.parse(cssContent)
146
+
147
+ // Walk through all rules and find desktop media queries
148
+ root.walkAtRules('media', (rule) => {
149
+ const mediaQuery = rule.params
150
+ const shouldExtract = isDesktopMediaQuery(mediaQuery)
151
+
152
+ if (shouldExtract) {
153
+ // Convert the rule back to CSS string and collect it
154
+ desktopRules.push(rule.toString())
155
+ // Remove the rule from the original AST
156
+ rule.remove()
157
+ }
158
+ })
159
+
160
+ // Generate the critical CSS (without desktop rules)
161
+ const criticalCSS = root.toString()
162
+ const progressiveCSS = desktopRules.join('\n')
163
+
164
+ // Validate CSS syntax by checking for balanced braces
165
+ const criticalOpenBraces = (criticalCSS.match(/\{/g) || []).length
166
+ const criticalCloseBraces = (criticalCSS.match(/\}/g) || []).length
167
+ const progressiveOpenBraces = (progressiveCSS.match(/\{/g) || []).length
168
+ const progressiveCloseBraces = (progressiveCSS.match(/\}/g) || []).length
169
+
170
+ if (criticalOpenBraces !== criticalCloseBraces) {
171
+ console.error(`PostCSS extraction created malformed critical CSS: ${criticalOpenBraces} open braces vs ${criticalCloseBraces} close braces`)
172
+ console.log('Falling back to original CSS to avoid syntax errors')
173
+ return {
174
+ critical: cssContent,
175
+ progressive: ''
176
+ }
177
+ }
178
+
179
+ if (progressiveOpenBraces !== progressiveCloseBraces) {
180
+ console.error(`PostCSS extraction created malformed progressive CSS: ${progressiveOpenBraces} open braces vs ${progressiveCloseBraces} close braces`)
181
+ console.log('Falling back to original CSS to avoid syntax errors')
182
+ return {
183
+ critical: cssContent,
184
+ progressive: ''
185
+ }
186
+ }
187
+
188
+ console.log(`CSS separation: Extracted ${desktopRules.length} desktop-only media queries using PostCSS`)
189
+
190
+ // Note: Any remaining CSS syntax warnings are from pre-existing CSS generation issues,
191
+ // not from the progressive CSS extraction process
192
+
193
+ return {
194
+ critical: criticalCSS,
195
+ progressive: progressiveCSS
196
+ }
197
+
198
+ } catch (error) {
199
+ console.error('PostCSS separation failed, falling back to original CSS:', error)
200
+ return {
201
+ critical: cssContent,
202
+ progressive: ''
203
+ }
204
+ }
205
+ }
206
+
207
+
208
+ /**
209
+ * Creates progressive CSS loading script for runtime
210
+ */
211
+ export function createProgressiveLoader(): string {
212
+ return `
213
+ // Progressive CSS Loader - Phase 3 Performance Optimization
214
+ (function() {
215
+ 'use strict';
216
+
217
+ const DESKTOP_BREAKPOINT = 1024; // lg breakpoint
218
+ const PROGRESSIVE_CSS_PATH = '/assets/progressive.css';
219
+
220
+ let progressiveCSSLoaded = false;
221
+
222
+ function loadProgressiveCSS() {
223
+ if (progressiveCSSLoaded) return;
224
+
225
+ const link = document.createElement('link');
226
+ link.rel = 'stylesheet';
227
+ link.href = PROGRESSIVE_CSS_PATH;
228
+ link.media = 'screen and (min-width: ' + DESKTOP_BREAKPOINT + 'px)';
229
+
230
+ // Load asynchronously for non-blocking
231
+ link.onload = function() {
232
+ console.log('Progressive CSS loaded for desktop');
233
+ progressiveCSSLoaded = true;
234
+ };
235
+
236
+ document.head.appendChild(link);
237
+ }
238
+
239
+ function checkViewport() {
240
+ if (window.innerWidth >= DESKTOP_BREAKPOINT) {
241
+ loadProgressiveCSS();
242
+ }
243
+ }
244
+
245
+ // Load on resize to desktop
246
+ let resizeTimeout;
247
+ window.addEventListener('resize', function() {
248
+ clearTimeout(resizeTimeout);
249
+ resizeTimeout = setTimeout(checkViewport, 150);
250
+ });
251
+
252
+ // Check initial viewport
253
+ if (document.readyState === 'loading') {
254
+ document.addEventListener('DOMContentLoaded', checkViewport);
255
+ } else {
256
+ checkViewport();
257
+ }
258
+
259
+ // Preload for fast desktop switches
260
+ if (window.innerWidth >= 768) { // tablet+
261
+ const preload = document.createElement('link');
262
+ preload.rel = 'preload';
263
+ preload.href = PROGRESSIVE_CSS_PATH;
264
+ preload.as = 'style';
265
+ document.head.appendChild(preload);
266
+ }
267
+ })();
268
+ `;
269
+ }
@@ -106,7 +106,7 @@ export default function themeCSSGenerator(): Plugin {
106
106
  }
107
107
 
108
108
  // Recursively generate CSS variables from theme object
109
- const generateCSSVariables = (obj: any, path: string[] = [], rootTheme?: MultiThemeConfig): string => {
109
+ const generateCSSVariables = (obj: any, path: string[] = [], rootTheme?: MultiThemeConfig, localVars: Set<string> = new Set()): string => {
110
110
  let css = ''
111
111
 
112
112
  if (typeof obj !== 'object' || obj === null) {
@@ -125,17 +125,32 @@ export default function themeCSSGenerator(): Plugin {
125
125
  }
126
126
  }
127
127
 
128
+ // Skip breakpoints as they're handled by generateBreakpointVariables
129
+ if (key === 'breakpoints') {
130
+ return
131
+ }
132
+
128
133
  if (typeof value === 'string' || typeof value === 'number') {
129
134
  // Generate CSS variable for primitive values
130
135
  const cssVarName = createCSSVarName(currentPath)
131
- css += ` ${cssVarName}: ${value};\n`
136
+
137
+ // Only prevent duplicates within the same traversal (same CSS rule block)
138
+ if (!localVars.has(cssVarName)) {
139
+ css += ` ${cssVarName}: ${value};\n`
140
+ localVars.add(cssVarName)
141
+ }
132
142
  } else if (Array.isArray(value)) {
133
143
  // Handle arrays (like font weights, tags)
134
144
  const cssVarName = createCSSVarName(currentPath)
135
- css += ` ${cssVarName}: ${valueToString(value)};\n`
145
+
146
+ // Only prevent duplicates within the same traversal (same CSS rule block)
147
+ if (!localVars.has(cssVarName)) {
148
+ css += ` ${cssVarName}: ${valueToString(value)};\n`
149
+ localVars.add(cssVarName)
150
+ }
136
151
  } else if (typeof value === 'object' && value !== null) {
137
152
  // Recursively process nested objects
138
- css += generateCSSVariables(value, currentPath, rootTheme)
153
+ css += generateCSSVariables(value, currentPath, rootTheme, localVars)
139
154
  }
140
155
  })
141
156
 
@@ -165,8 +180,9 @@ export default function themeCSSGenerator(): Plugin {
165
180
  // NEW: Generate breakpoint variables first
166
181
  css += generateBreakpointVariables(breakpoints)
167
182
 
168
- // Generate all other CSS variables
169
- css += generateCSSVariables(themeObj, [], themeObj)
183
+ // Generate all other CSS variables (each CSS block has its own scope)
184
+ const localVars = new Set<string>()
185
+ css += generateCSSVariables(themeObj, [], themeObj, localVars)
170
186
  css += '}\n\n'
171
187
 
172
188
  // Generate dark mode variables
@@ -215,7 +215,7 @@
215
215
  }
216
216
 
217
217
  /* Responsive Design */
218
- @media (max-width: var(--cs-breakpoints-mobile)) {
218
+ @media (max-width: 640px) {
219
219
  .badge {
220
220
  font-size: var(--badge-mobile-font-size, var(--cs-fonts-primary-sizes-xs));
221
221
  padding: var(--badge-mobile-padding-y, var(--cs-spacing-scale-xs)) var(--badge-mobile-padding-x, var(--cs-spacing-scale-sm));
@@ -223,7 +223,7 @@
223
223
  }
224
224
 
225
225
  /* Container Queries for Adaptive Layouts */
226
- @container (min-width: var(--cs-breakpoints-tablet)) {
226
+ @container (min-width: 768px) {
227
227
  .badge {
228
228
  font-size: var(--badge-container-font-size, var(--cs-fonts-primary-sizes-sm));
229
229
  }