chaincss 2.0.7 → 2.1.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 (159) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CODE_OF_CONDUCT.md +21 -0
  3. package/CONTRIBUTING.md +28 -0
  4. package/README.md +455 -226
  5. package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +1 -0
  6. package/demo/index.html +16 -0
  7. package/demo/package.json +20 -0
  8. package/demo/src/App.tsx +117 -0
  9. package/demo/src/chaincss-barrel.ts +9 -0
  10. package/demo/src/main.tsx +8 -0
  11. package/demo/src/styles.chain.ts +300 -0
  12. package/demo/vite.config.ts +46 -0
  13. package/dist/cli/commands/build.d.ts +0 -1
  14. package/dist/cli/commands/cache.d.ts +1 -0
  15. package/dist/cli/commands/init.d.ts +6 -3
  16. package/dist/cli/commands/timeline.d.ts +0 -1
  17. package/dist/cli/commands/watch.d.ts +0 -1
  18. package/dist/cli/index.d.ts +0 -1
  19. package/dist/cli/index.js +3213 -5296
  20. package/dist/cli/types.d.ts +51 -20
  21. package/dist/cli/utils/config-loader.d.ts +0 -1
  22. package/dist/cli/utils/file-utils.d.ts +27 -3
  23. package/dist/cli/utils/logger.d.ts +0 -1
  24. package/dist/compiler/Chain.d.ts +215 -0
  25. package/dist/compiler/animations.d.ts +76 -0
  26. package/dist/compiler/atomic-optimizer.d.ts +47 -12
  27. package/dist/compiler/breakpoints.d.ts +46 -0
  28. package/dist/compiler/btt.d.ts +36 -60
  29. package/dist/compiler/cache-manager.d.ts +58 -4
  30. package/dist/compiler/commonProps.d.ts +0 -1
  31. package/dist/compiler/content-addressable-cache.d.ts +78 -0
  32. package/dist/compiler/helpers.d.ts +54 -0
  33. package/dist/compiler/index.d.ts +16 -9
  34. package/dist/compiler/index.js +4450 -4316
  35. package/dist/compiler/prefixer.d.ts +17 -1
  36. package/dist/compiler/shorthands.d.ts +28 -0
  37. package/dist/compiler/suggestions.d.ts +43 -0
  38. package/dist/compiler/theme-contract.d.ts +16 -27
  39. package/dist/compiler/token-resolver.d.ts +69 -0
  40. package/dist/compiler/tokens.d.ts +33 -8
  41. package/dist/core/auto-detector.d.ts +34 -0
  42. package/dist/core/common-utils.d.ts +97 -0
  43. package/dist/core/compiler.d.ts +63 -23
  44. package/dist/core/constants.d.ts +137 -36
  45. package/dist/core/smart-chain.d.ts +3 -0
  46. package/dist/core/types.d.ts +122 -15
  47. package/dist/core/utils.d.ts +134 -17
  48. package/dist/index.d.ts +52 -8
  49. package/dist/index.js +7090 -5578
  50. package/dist/plugins/vite.d.ts +7 -5
  51. package/dist/plugins/vite.js +2964 -25641
  52. package/dist/plugins/webpack.d.ts +24 -1
  53. package/dist/plugins/webpack.js +209 -72
  54. package/dist/runtime/Chain.d.ts +32 -0
  55. package/dist/runtime/auto-hooks.d.ts +11 -0
  56. package/dist/runtime/hmr.d.ts +22 -2
  57. package/dist/runtime/index.d.ts +3 -2
  58. package/dist/runtime/index.js +3648 -301
  59. package/dist/runtime/injector.d.ts +39 -72
  60. package/dist/runtime/react.d.ts +17 -12
  61. package/dist/runtime/svelte.d.ts +15 -0
  62. package/dist/runtime/types.d.ts +126 -4
  63. package/dist/runtime/utils.d.ts +0 -1
  64. package/dist/runtime/vue.d.ts +34 -14
  65. package/package.json +59 -66
  66. package/src/cli/commands/build.ts +133 -0
  67. package/src/cli/commands/cache.ts +371 -0
  68. package/src/cli/commands/init.ts +230 -0
  69. package/src/cli/commands/timeline.ts +435 -0
  70. package/src/cli/commands/watch.ts +211 -0
  71. package/src/cli/index.ts +226 -0
  72. package/src/cli/types.ts +100 -0
  73. package/src/cli/utils/config-loader.ts +174 -0
  74. package/src/cli/utils/file-utils.ts +139 -0
  75. package/src/cli/utils/logger.ts +74 -0
  76. package/src/compiler/Chain.ts +831 -0
  77. package/src/compiler/animations.ts +517 -0
  78. package/src/compiler/atomic-optimizer.ts +786 -0
  79. package/src/compiler/breakpoints.ts +347 -0
  80. package/src/compiler/btt.ts +1147 -0
  81. package/src/compiler/cache-manager.ts +446 -0
  82. package/src/compiler/commonProps.ts +18 -0
  83. package/src/compiler/content-addressable-cache.ts +478 -0
  84. package/src/compiler/helpers.ts +407 -0
  85. package/src/compiler/index.ts +72 -0
  86. package/src/compiler/prefixer.ts +724 -0
  87. package/src/compiler/shorthands.ts +558 -0
  88. package/src/compiler/suggestions.ts +436 -0
  89. package/src/compiler/theme-contract.ts +197 -0
  90. package/src/compiler/token-resolver.ts +241 -0
  91. package/src/compiler/tokens.ts +612 -0
  92. package/src/core/auto-detector.ts +187 -0
  93. package/src/core/common-utils.ts +423 -0
  94. package/src/core/compiler.ts +835 -0
  95. package/src/core/constants.ts +424 -0
  96. package/src/core/index.ts +107 -0
  97. package/src/core/smart-chain.ts +163 -0
  98. package/src/core/types.ts +257 -0
  99. package/src/core/utils.ts +598 -0
  100. package/src/index.ts +208 -0
  101. package/src/plugins/vite.d.ts +316 -0
  102. package/src/plugins/vite.ts +424 -0
  103. package/src/plugins/webpack.d.ts +289 -0
  104. package/src/plugins/webpack.ts +416 -0
  105. package/src/runtime/Chain.ts +242 -0
  106. package/src/runtime/auto-hooks.tsx +127 -0
  107. package/src/runtime/auto-vue.ts +72 -0
  108. package/src/runtime/hmr.ts +212 -0
  109. package/src/runtime/index.ts +82 -0
  110. package/src/runtime/injector.ts +273 -0
  111. package/src/runtime/react.tsx +269 -0
  112. package/src/runtime/svelte.ts +15 -0
  113. package/src/runtime/types.ts +256 -0
  114. package/src/runtime/utils.ts +128 -0
  115. package/src/runtime/vite-env.d.ts +120 -0
  116. package/src/runtime/vue.ts +231 -0
  117. package/tsconfig.build.json +41 -0
  118. package/tsconfig.json +25 -0
  119. package/tsconfig.runtimes.json +18 -0
  120. package/dist/cli/cli.cjs +0 -7
  121. package/dist/cli/commands/build.d.ts.map +0 -1
  122. package/dist/cli/commands/compile.d.ts +0 -3
  123. package/dist/cli/commands/compile.d.ts.map +0 -1
  124. package/dist/cli/commands/init.d.ts.map +0 -1
  125. package/dist/cli/commands/timeline.d.ts.map +0 -1
  126. package/dist/cli/commands/watch.d.ts.map +0 -1
  127. package/dist/cli/index.d.ts.map +0 -1
  128. package/dist/cli/types.d.ts.map +0 -1
  129. package/dist/cli/utils/config-loader.d.ts.map +0 -1
  130. package/dist/cli/utils/file-utils.d.ts.map +0 -1
  131. package/dist/cli/utils/logger.d.ts.map +0 -1
  132. package/dist/compiler/atomic-optimizer.d.ts.map +0 -1
  133. package/dist/compiler/btt.d.ts.map +0 -1
  134. package/dist/compiler/cache-manager.d.ts.map +0 -1
  135. package/dist/compiler/commonProps.d.ts.map +0 -1
  136. package/dist/compiler/index.d.ts.map +0 -1
  137. package/dist/compiler/prefixer.d.ts.map +0 -1
  138. package/dist/compiler/theme-contract.d.ts.map +0 -1
  139. package/dist/compiler/tokens.d.ts.map +0 -1
  140. package/dist/compiler/types.d.ts +0 -57
  141. package/dist/compiler/types.d.ts.map +0 -1
  142. package/dist/core/compiler.d.ts.map +0 -1
  143. package/dist/core/constants.d.ts.map +0 -1
  144. package/dist/core/index.d.ts +0 -4
  145. package/dist/core/index.d.ts.map +0 -1
  146. package/dist/core/types.d.ts.map +0 -1
  147. package/dist/core/utils.d.ts.map +0 -1
  148. package/dist/index.d.ts.map +0 -1
  149. package/dist/plugins/vite.d.ts.map +0 -1
  150. package/dist/plugins/webpack.d.ts.map +0 -1
  151. package/dist/runtime/hmr.d.ts.map +0 -1
  152. package/dist/runtime/index.d.ts.map +0 -1
  153. package/dist/runtime/injector.d.ts.map +0 -1
  154. package/dist/runtime/react.d.ts.map +0 -1
  155. package/dist/runtime/react.js +0 -324
  156. package/dist/runtime/types.d.ts.map +0 -1
  157. package/dist/runtime/utils.d.ts.map +0 -1
  158. package/dist/runtime/vue.d.ts.map +0 -1
  159. package/dist/runtime/vue.js +0 -286
@@ -0,0 +1,230 @@
1
+ // src/cli/commands/init.ts
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import chalk from 'chalk';
6
+ import { createLogger } from '../utils/logger.js';
7
+ import { saveConfigTemplate } from '../utils/config-loader.js';
8
+
9
+ export interface InitOptions {
10
+ force?: boolean;
11
+ verbose?: boolean;
12
+ template?: 'full' | 'minimal';
13
+ typescript?: boolean;
14
+ framework?: 'react' | 'vue' | 'svelte' | 'solid';
15
+ }
16
+
17
+ export async function initCommand(options: InitOptions): Promise<void> {
18
+ const logger = createLogger(options.verbose);
19
+
20
+ logger.header('ChainCSS Initialization');
21
+
22
+ const configPath = path.join(process.cwd(), 'chaincss.config.js');
23
+
24
+ if (fs.existsSync(configPath) && !options.force) {
25
+ logger.warn('Config file already exists. Use --force to overwrite.');
26
+ return;
27
+ }
28
+
29
+ logger.step('Creating chaincss.config.js...');
30
+
31
+ // Save config template with options
32
+ saveConfigTemplate(configPath);
33
+
34
+ logger.success(`Created ${configPath}`);
35
+
36
+ // Create example style file
37
+ const exampleDir = path.join(process.cwd(), 'src');
38
+ const examplePath = path.join(exampleDir, 'styles.chain.js');
39
+ const isTypeScript = options.typescript || false;
40
+ const exampleExt = isTypeScript ? 'ts' : 'js';
41
+ const finalExamplePath = isTypeScript
42
+ ? path.join(exampleDir, 'styles.chain.ts')
43
+ : examplePath;
44
+
45
+ if (!fs.existsSync(finalExamplePath) || options.force) {
46
+ if (!fs.existsSync(exampleDir)) {
47
+ fs.mkdirSync(exampleDir, { recursive: true });
48
+ }
49
+
50
+ // Framework-specific example
51
+ let frameworkImport = "import { $, recipe } from 'chaincss';";
52
+ let frameworkSpecificCode = '';
53
+
54
+ if (options.framework === 'react') {
55
+ frameworkSpecificCode = `
56
+ // React component example
57
+ import React from 'react';
58
+
59
+ export const Button = ({ children, variant = 'primary' }) => {
60
+ const className = buttonVariants({ color: variant });
61
+ return <button className={className}>{children}</button>;
62
+ };`;
63
+ } else if (options.framework === 'vue') {
64
+ frameworkSpecificCode = `
65
+ // Vue component example
66
+ export default {
67
+ name: 'Button',
68
+ props: {
69
+ variant: { type: String, default: 'primary' }
70
+ },
71
+ computed: {
72
+ buttonClass() {
73
+ return buttonVariants({ color: this.variant });
74
+ }
75
+ }
76
+ };`;
77
+ } else if (options.framework === 'svelte') {
78
+ frameworkSpecificCode = `
79
+ <!-- Svelte component example -->
80
+ <script>
81
+ import { buttonVariants } from './styles.chain';
82
+ export let variant = 'primary';
83
+ $: buttonClass = buttonVariants({ color: variant });
84
+ </script>
85
+
86
+ <button class={buttonClass}>
87
+ <slot />
88
+ </button>`;
89
+ }
90
+
91
+ const exampleContent = `/**
92
+ * ChainCSS Example Style File
93
+ *
94
+ * This file demonstrates ChainCSS syntax.
95
+ * Run \`npx chaincss build\` to compile all styles.
96
+ */
97
+
98
+ ${frameworkImport}
99
+
100
+ // Simple button style
101
+ export const button = $()
102
+ .backgroundColor('#667eea')
103
+ .color('white')
104
+ .padding('12px 24px')
105
+ .borderRadius('8px')
106
+ .border('none')
107
+ .fontSize('16px')
108
+ .fontWeight('600')
109
+ .cursor('pointer')
110
+ .transition('all 0.2s ease')
111
+ .hover()
112
+ .backgroundColor('#5a67d8')
113
+ .transform('scale(1.05)')
114
+ .end()
115
+ .$el('button');
116
+
117
+ // Card style
118
+ export const card = $()
119
+ .backgroundColor('white')
120
+ .borderRadius('12px')
121
+ .padding('24px')
122
+ .boxShadow('0 10px 15px -3px rgba(0,0,0,0.1)')
123
+ .transition('all 0.3s ease')
124
+ .hover()
125
+ .boxShadow('0 20px 25px -5px rgba(0,0,0,0.15)')
126
+ .transform('translateY(-4px)')
127
+ .end()
128
+ .$el('.card');
129
+
130
+ // Container style
131
+ export const container = $()
132
+ .maxWidth('1200px')
133
+ .margin('0 auto')
134
+ .padding('0 1rem')
135
+ .$el('.container');
136
+
137
+ // Responsive design
138
+ export const responsive = $()
139
+ .display('grid')
140
+ .gridTemplateColumns('1fr')
141
+ .gap('1rem')
142
+ .md()
143
+ .gridTemplateColumns('repeat(2, 1fr)')
144
+ .end()
145
+ .lg()
146
+ .gridTemplateColumns('repeat(3, 1fr)')
147
+ .end()
148
+ .$el('.grid');
149
+
150
+ // Variant-based button component
151
+ export const buttonVariants = recipe({
152
+ base: $()
153
+ .padding('8px 16px')
154
+ .borderRadius('4px')
155
+ .fontWeight('500')
156
+ .cursor('pointer')
157
+ .transition('all 0.2s')
158
+ .border('none')
159
+ .$el('button'),
160
+
161
+ variants: {
162
+ color: {
163
+ primary: $().backgroundColor('#667eea').color('white').hover().backgroundColor('#5a67d8').end().$el(),
164
+ secondary: $().backgroundColor('#48bb78').color('white').hover().backgroundColor('#38a169').end().$el(),
165
+ danger: $().backgroundColor('#f56565').color('white').hover().backgroundColor('#e53e3e').end().$el(),
166
+ outline: $()
167
+ .backgroundColor('transparent')
168
+ .color('#667eea')
169
+ .border('2px solid #667eea')
170
+ .hover()
171
+ .backgroundColor('#667eea')
172
+ .color('white')
173
+ .end()
174
+ .$el()
175
+ },
176
+ size: {
177
+ sm: $().padding('4px 8px').fontSize('12px').$el(),
178
+ md: $().padding('8px 16px').fontSize('14px').$el(),
179
+ lg: $().padding('12px 24px').fontSize('16px').$el()
180
+ },
181
+ fullWidth: {
182
+ true: $().width('100%').$el()
183
+ }
184
+ },
185
+
186
+ compoundVariants: [
187
+ {
188
+ variants: { color: 'outline', size: 'lg' },
189
+ style: $().borderWidth('3px').$el()
190
+ }
191
+ ],
192
+
193
+ defaultVariants: {
194
+ color: 'primary',
195
+ size: 'md'
196
+ }
197
+ });
198
+ ${frameworkSpecificCode}
199
+ `;
200
+
201
+ fs.writeFileSync(finalExamplePath, exampleContent, 'utf8');
202
+ logger.success(`Created example: ${finalExamplePath}`);
203
+ } else {
204
+ logger.info(`Example file already exists at ${finalExamplePath}`);
205
+ }
206
+
207
+ // Create .gitignore entry for cache if needed
208
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
209
+ const cacheIgnore = '\n# ChainCSS cache\n.chaincss-cache/\n.chaincss/\n';
210
+
211
+ if (fs.existsSync(gitignorePath)) {
212
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
213
+ if (!gitignore.includes('.chaincss-cache')) {
214
+ fs.appendFileSync(gitignorePath, cacheIgnore);
215
+ logger.info('Added cache directories to .gitignore');
216
+ }
217
+ }
218
+
219
+ logger.divider();
220
+ logger.success('ChainCSS initialized successfully!');
221
+ logger.info('\nšŸ“š Next steps:');
222
+ logger.info(` 1. Edit ${chalk.cyan('chaincss.config.js')} to customize settings`);
223
+ logger.info(` 2. Write styles in ${chalk.cyan(finalExamplePath)}`);
224
+ logger.info(` 3. Run ${chalk.cyan('npx chaincss build')} to compile all styles`);
225
+ logger.info('');
226
+ logger.info(` Or watch for changes:`);
227
+ logger.info(` ${chalk.cyan('npx chaincss watch')}`);
228
+ logger.info('');
229
+ logger.info(` For more information, visit: ${chalk.blue('https://chaincss.dev')}\n`);
230
+ }
@@ -0,0 +1,435 @@
1
+ // chaincss/src/commands/timeline.ts
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { createRequire } from 'module';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ // Types
10
+ interface StyleSnapshot {
11
+ id: string;
12
+ timestamp: number;
13
+ selector: string;
14
+ styles: Record<string, any>;
15
+ source: string;
16
+ hash: string;
17
+ }
18
+
19
+ interface StyleChange {
20
+ id: string;
21
+ timestamp: number;
22
+ selector: string;
23
+ property: string;
24
+ oldValue: any;
25
+ newValue: any;
26
+ type: 'add' | 'remove' | 'modify';
27
+ }
28
+
29
+ interface TimelineData {
30
+ history: StyleSnapshot[];
31
+ changes: StyleChange[];
32
+ exportedAt?: number;
33
+ stats?: {
34
+ totalSnapshots: number;
35
+ totalChanges: number;
36
+ firstRecorded: number;
37
+ lastRecorded: number;
38
+ };
39
+ }
40
+
41
+ // Helper to format bytes
42
+ function formatBytes(bytes: number): string {
43
+ if (bytes === 0) return '0 Bytes';
44
+ const k = 1024;
45
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
46
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
47
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
48
+ }
49
+
50
+ // Helper to format duration
51
+ function formatDuration(ms: number): string {
52
+ if (ms < 1000) return `${ms}ms`;
53
+ const seconds = ms / 1000;
54
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
55
+ const minutes = seconds / 60;
56
+ if (minutes < 60) return `${minutes.toFixed(1)}min`;
57
+ const hours = minutes / 60;
58
+ return `${hours.toFixed(1)}h`;
59
+ }
60
+
61
+ // Load timeline data with validation
62
+ function loadTimelineData(timelineFile: string): TimelineData | null {
63
+ if (!fs.existsSync(timelineFile)) {
64
+ return null;
65
+ }
66
+
67
+ try {
68
+ const content = fs.readFileSync(timelineFile, 'utf8');
69
+ const data = JSON.parse(content);
70
+
71
+ // Validate data structure
72
+ if (!data.history || !Array.isArray(data.history)) {
73
+ throw new Error('Invalid timeline data: missing history array');
74
+ }
75
+
76
+ return data;
77
+ } catch (error) {
78
+ console.error(chalk.red(`Failed to load timeline data: ${(error as Error).message}`));
79
+ return null;
80
+ }
81
+ }
82
+
83
+ // Display snapshot details
84
+ function displaySnapshotDetails(snapshot: StyleSnapshot, index: number): void {
85
+ const date = new Date(snapshot.timestamp).toLocaleString();
86
+ const propCount = Object.keys(snapshot.styles).length;
87
+
88
+ console.log(`\n${chalk.green(`[${index}]`)} ${chalk.white.bold(snapshot.selector)}`);
89
+ console.log(` ${chalk.gray(`ID: ${snapshot.id}`)}`);
90
+ console.log(` ${chalk.gray(`Time: ${date}`)}`);
91
+ console.log(` ${chalk.gray(`Source: ${snapshot.source}`)}`);
92
+ console.log(` ${chalk.gray(`Properties: ${propCount}`)}`);
93
+ console.log(` ${chalk.gray(`Hash: ${snapshot.hash.slice(0, 8)}...`)}`);
94
+
95
+ // Show first few properties as preview
96
+ const previewProps = Object.entries(snapshot.styles).slice(0, 5);
97
+ if (previewProps.length > 0) {
98
+ console.log(` ${chalk.dim('Preview:')}`);
99
+ for (const [prop, value] of previewProps) {
100
+ console.log(` ${chalk.blue(prop)}: ${chalk.yellow(value)}`);
101
+ }
102
+ if (Object.keys(snapshot.styles).length > 5) {
103
+ console.log(` ${chalk.dim(`... and ${Object.keys(snapshot.styles).length - 5} more`)}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Calculate diff between two snapshots
109
+ function calculateDiff(snapshot1: StyleSnapshot, snapshot2: StyleSnapshot): {
110
+ added: Record<string, any>;
111
+ removed: Record<string, any>;
112
+ modified: Record<string, { old: any; new: any }>;
113
+ } {
114
+ const added: Record<string, any> = {};
115
+ const removed: Record<string, any> = {};
116
+ const modified: Record<string, { old: any; new: any }> = {};
117
+
118
+ // Find added and modified properties
119
+ for (const [key, value] of Object.entries(snapshot2.styles)) {
120
+ if (!(key in snapshot1.styles)) {
121
+ added[key] = value;
122
+ } else if (JSON.stringify(snapshot1.styles[key]) !== JSON.stringify(value)) {
123
+ modified[key] = {
124
+ old: snapshot1.styles[key],
125
+ new: value
126
+ };
127
+ }
128
+ }
129
+
130
+ // Find removed properties
131
+ for (const [key, value] of Object.entries(snapshot1.styles)) {
132
+ if (!(key in snapshot2.styles)) {
133
+ removed[key] = value;
134
+ }
135
+ }
136
+
137
+ return { added, removed, modified };
138
+ }
139
+
140
+ // Display diff with colors
141
+ function displayDiff(diff: ReturnType<typeof calculateDiff>, selector1: string, selector2: string): void {
142
+ const { added, removed, modified } = diff;
143
+
144
+ const totalChanges = Object.keys(added).length + Object.keys(removed).length + Object.keys(modified).length;
145
+
146
+ if (totalChanges === 0) {
147
+ console.log(chalk.gray(' No changes detected'));
148
+ return;
149
+ }
150
+
151
+ // Display removed properties
152
+ for (const [prop, value] of Object.entries(removed)) {
153
+ console.log(`${chalk.red('āˆ’')} ${chalk.red(prop)}: ${chalk.red(String(value))}`);
154
+ }
155
+
156
+ // Display added properties
157
+ for (const [prop, value] of Object.entries(added)) {
158
+ console.log(`${chalk.green('+')} ${chalk.green(prop)}: ${chalk.green(String(value))}`);
159
+ }
160
+
161
+ // Display modified properties
162
+ for (const [prop, { old, new: newVal }] of Object.entries(modified)) {
163
+ console.log(`${chalk.yellow('~')} ${chalk.yellow(prop)}: ${chalk.red(String(old))} → ${chalk.green(String(newVal))}`);
164
+ }
165
+ }
166
+
167
+ export async function timelineCommand(action: string, options: any) {
168
+ const timelineFile = path.join(process.cwd(), '.chaincss-timeline.json');
169
+ const data = loadTimelineData(timelineFile);
170
+
171
+ if (!data) {
172
+ console.log(chalk.yellow('\nāš ļø No timeline data found.'));
173
+ console.log(chalk.gray(' Run your build with --timeline flag first:'));
174
+ console.log(chalk.cyan(' $ chaincss build --timeline\n'));
175
+ return;
176
+ }
177
+
178
+ // Calculate stats if not present
179
+ if (!data.stats && data.history.length > 0) {
180
+ data.stats = {
181
+ totalSnapshots: data.history.length,
182
+ totalChanges: data.changes?.length || 0,
183
+ firstRecorded: data.history[0]?.timestamp || 0,
184
+ lastRecorded: data.history[data.history.length - 1]?.timestamp || 0
185
+ };
186
+ }
187
+
188
+ switch (action) {
189
+ case 'list':
190
+ console.log(chalk.cyan.bold('\nšŸ“Š Style Timeline History\n'));
191
+ console.log(chalk.gray(` Total Snapshots: ${data.stats?.totalSnapshots || data.history.length}`));
192
+ console.log(chalk.gray(` Total Changes: ${data.stats?.totalChanges || data.changes?.length || 0}`));
193
+
194
+ if (data.stats?.firstRecorded && data.stats?.lastRecorded) {
195
+ const duration = data.stats.lastRecorded - data.stats.firstRecorded;
196
+ console.log(chalk.gray(` Duration: ${formatDuration(duration)}`));
197
+ }
198
+
199
+ console.log(chalk.gray('\n Snapshots:\n'));
200
+
201
+ data.history.forEach((snapshot: StyleSnapshot, index: number) => {
202
+ displaySnapshotDetails(snapshot, index);
203
+ });
204
+
205
+ // Show file size
206
+ try {
207
+ const stats = fs.statSync(timelineFile);
208
+ console.log(chalk.gray(`\nšŸ“ Timeline file size: ${formatBytes(stats.size)}`));
209
+ } catch (e) {}
210
+
211
+ break;
212
+
213
+ case 'diff':
214
+ const id1 = options.snapshot1;
215
+ const id2 = options.snapshot2;
216
+
217
+ if (!id1 || !id2) {
218
+ console.log(chalk.red('\nāŒ Both snapshot1 and snapshot2 are required for diff'));
219
+ console.log(chalk.gray(' Usage: chaincss timeline diff --snapshot1 <id> --snapshot2 <id>\n'));
220
+ return;
221
+ }
222
+
223
+ // Find snapshots by ID or selector
224
+ const snapshot1 = data.history.find((s: StyleSnapshot) =>
225
+ s.id === id1 || s.selector === id1
226
+ );
227
+ const snapshot2 = data.history.find((s: StyleSnapshot) =>
228
+ s.id === id2 || s.selector === id2
229
+ );
230
+
231
+ if (!snapshot1) {
232
+ console.log(chalk.red(`\nāŒ Snapshot not found: ${id1}`));
233
+ console.log(chalk.gray(' Use "chaincss timeline list" to see available snapshots\n'));
234
+ return;
235
+ }
236
+
237
+ if (!snapshot2) {
238
+ console.log(chalk.red(`\nāŒ Snapshot not found: ${id2}`));
239
+ console.log(chalk.gray(' Use "chaincss timeline list" to see available snapshots\n'));
240
+ return;
241
+ }
242
+
243
+ const diff = calculateDiff(snapshot1, snapshot2);
244
+ const totalChanges = Object.keys(diff.added).length +
245
+ Object.keys(diff.removed).length +
246
+ Object.keys(diff.modified).length;
247
+
248
+ console.log(chalk.cyan.bold(`\nšŸ” Diff: ${snapshot1.selector} → ${snapshot2.selector}\n`));
249
+ console.log(chalk.gray(` Changes: ${totalChanges}`));
250
+ console.log(chalk.gray(` Time between: ${formatDuration(snapshot2.timestamp - snapshot1.timestamp)}\n`));
251
+
252
+ displayDiff(diff, snapshot1.selector, snapshot2.selector);
253
+
254
+ if (totalChanges > 0) {
255
+ console.log(chalk.gray(`\n Legend: ${chalk.red('āˆ’')} removed ${chalk.green('+')} added ${chalk.yellow('~')} modified\n`));
256
+ }
257
+ break;
258
+
259
+ case 'changes':
260
+ console.log(chalk.cyan.bold('\nšŸ“ Style Change History\n'));
261
+
262
+ if (!data.changes || data.changes.length === 0) {
263
+ console.log(chalk.gray(' No changes recorded\n'));
264
+ return;
265
+ }
266
+
267
+ // Group changes by selector
268
+ const changesBySelector = new Map<string, StyleChange[]>();
269
+ for (const change of data.changes) {
270
+ if (!changesBySelector.has(change.selector)) {
271
+ changesBySelector.set(change.selector, []);
272
+ }
273
+ changesBySelector.get(change.selector)!.push(change);
274
+ }
275
+
276
+ for (const [selector, changes] of changesBySelector) {
277
+ console.log(chalk.white.bold(`\n ${selector}`));
278
+
279
+ for (const change of changes) {
280
+ const date = new Date(change.timestamp).toLocaleTimeString();
281
+ switch (change.type) {
282
+ case 'add':
283
+ console.log(` ${chalk.green(`+ ${change.property}: ${change.newValue}`)} ${chalk.gray(`[${date}]`)}`);
284
+ break;
285
+ case 'remove':
286
+ console.log(` ${chalk.red(`- ${change.property}: ${change.oldValue}`)} ${chalk.gray(`[${date}]`)}`);
287
+ break;
288
+ case 'modify':
289
+ console.log(` ${chalk.yellow(`~ ${change.property}: ${change.oldValue} → ${change.newValue}`)} ${chalk.gray(`[${date}]`)}`);
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ console.log();
295
+ break;
296
+
297
+ case 'stats':
298
+ console.log(chalk.cyan.bold('\nšŸ“ˆ Timeline Statistics\n'));
299
+ console.log(chalk.gray(` Total Snapshots: ${data.history.length}`));
300
+ console.log(chalk.gray(` Total Changes: ${data.changes?.length || 0}`));
301
+
302
+ if (data.history.length > 0) {
303
+ const firstSnapshot = data.history[0];
304
+ const lastSnapshot = data.history[data.history.length - 1];
305
+ const duration = lastSnapshot.timestamp - firstSnapshot.timestamp;
306
+
307
+ console.log(chalk.gray(` First Recorded: ${new Date(firstSnapshot.timestamp).toLocaleString()}`));
308
+ console.log(chalk.gray(` Last Recorded: ${new Date(lastSnapshot.timestamp).toLocaleString()}`));
309
+ console.log(chalk.gray(` Duration: ${formatDuration(duration)}`));
310
+
311
+ // Count changes by type
312
+ const changesByType = { add: 0, remove: 0, modify: 0 };
313
+ for (const change of data.changes || []) {
314
+ changesByType[change.type]++;
315
+ }
316
+
317
+ console.log(chalk.gray(`\n Changes by Type:`));
318
+ console.log(chalk.green(` Added: ${changesByType.add}`));
319
+ console.log(chalk.red(` Removed: ${changesByType.remove}`));
320
+ console.log(chalk.yellow(` Modified: ${changesByType.modify}`));
321
+
322
+ // Most active selectors
323
+ const changesBySelector: Record<string, number> = {};
324
+ for (const change of data.changes || []) {
325
+ changesBySelector[change.selector] = (changesBySelector[change.selector] || 0) + 1;
326
+ }
327
+
328
+ const topSelectors = Object.entries(changesBySelector)
329
+ .sort((a, b) => b[1] - a[1])
330
+ .slice(0, 5);
331
+
332
+ if (topSelectors.length > 0) {
333
+ console.log(chalk.gray(`\n Most Active Selectors:`));
334
+ for (const [selector, count] of topSelectors) {
335
+ console.log(chalk.gray(` ${selector}: ${count} changes`));
336
+ }
337
+ }
338
+ }
339
+
340
+ try {
341
+ const stats = fs.statSync(timelineFile);
342
+ console.log(chalk.gray(`\n File Size: ${formatBytes(stats.size)}`));
343
+ } catch (e) {}
344
+
345
+ console.log();
346
+ break;
347
+
348
+ case 'export':
349
+ const exportPath = options.output || `chaincss-timeline-${Date.now()}.json`;
350
+ const fullExportPath = path.isAbsolute(exportPath)
351
+ ? exportPath
352
+ : path.join(process.cwd(), exportPath);
353
+
354
+ // Add export metadata
355
+ const exportData = {
356
+ ...data,
357
+ exportedAt: Date.now(),
358
+ exportedFrom: timelineFile,
359
+ version: '1.0.0'
360
+ };
361
+
362
+ try {
363
+ fs.writeFileSync(fullExportPath, JSON.stringify(exportData, null, 2));
364
+ const stats = fs.statSync(fullExportPath);
365
+ console.log(chalk.green(`\nāœ“ Timeline exported to ${fullExportPath}`));
366
+ console.log(chalk.gray(` Size: ${formatBytes(stats.size)}`));
367
+ } catch (error) {
368
+ console.log(chalk.red(`\nāŒ Failed to export: ${(error as Error).message}`));
369
+ }
370
+ break;
371
+
372
+ case 'clear':
373
+ try {
374
+ // Create backup before clearing
375
+ const backupPath = `${timelineFile}.backup-${Date.now()}`;
376
+ fs.copyFileSync(timelineFile, backupPath);
377
+ fs.unlinkSync(timelineFile);
378
+ console.log(chalk.green(`\nāœ“ Timeline cleared`));
379
+ console.log(chalk.gray(` Backup saved to ${backupPath}`));
380
+ } catch (error) {
381
+ console.log(chalk.red(`\nāŒ Failed to clear timeline: ${(error as Error).message}`));
382
+ }
383
+ break;
384
+
385
+ case 'watch':
386
+ console.log(chalk.cyan.bold('\nšŸ‘ļø Watching timeline changes...\n'));
387
+ console.log(chalk.gray(' Press Ctrl+C to stop\n'));
388
+
389
+ let lastModified = fs.statSync(timelineFile).mtimeMs;
390
+
391
+ const watcher = setInterval(() => {
392
+ try {
393
+ const stats = fs.statSync(timelineFile);
394
+ if (stats.mtimeMs > lastModified) {
395
+ lastModified = stats.mtimeMs;
396
+ const newData = loadTimelineData(timelineFile);
397
+ if (newData && newData.history.length !== data.history.length) {
398
+ console.log(chalk.yellow(`\nšŸ“ Timeline updated - ${new Date().toLocaleTimeString()}`));
399
+ console.log(chalk.gray(` New snapshot count: ${newData.history.length}`));
400
+
401
+ // Show the latest snapshot
402
+ const latest = newData.history[newData.history.length - 1];
403
+ if (latest) {
404
+ console.log(chalk.green(` Latest: ${latest.selector}`));
405
+ console.log(chalk.gray(` Properties: ${Object.keys(latest.styles).length}`));
406
+ }
407
+ }
408
+ // Update data reference
409
+ Object.assign(data, newData);
410
+ }
411
+ } catch (e) {
412
+ // File might be temporarily locked
413
+ }
414
+ }, 1000);
415
+
416
+ // Handle cleanup on exit
417
+ process.on('SIGINT', () => {
418
+ clearInterval(watcher);
419
+ console.log(chalk.gray('\n\nšŸ‘‹ Stopped watching\n'));
420
+ process.exit(0);
421
+ });
422
+ break;
423
+
424
+ default:
425
+ console.log(chalk.yellow(`\nāŒ Unknown action: ${action}`));
426
+ console.log(chalk.gray('\nAvailable actions:'));
427
+ console.log(chalk.cyan(' list ') + chalk.gray('- List all timeline snapshots'));
428
+ console.log(chalk.cyan(' diff ') + chalk.gray('- Compare two snapshots'));
429
+ console.log(chalk.cyan(' changes ') + chalk.gray('- Show individual style changes'));
430
+ console.log(chalk.cyan(' stats ') + chalk.gray('- Show timeline statistics'));
431
+ console.log(chalk.cyan(' export ') + chalk.gray('- Export timeline to JSON'));
432
+ console.log(chalk.cyan(' clear ') + chalk.gray('- Clear timeline data'));
433
+ console.log(chalk.cyan(' watch ') + chalk.gray('- Watch for timeline updates\n'));
434
+ }
435
+ }