emily-css 1.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/src/init.js ADDED
@@ -0,0 +1,252 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+
5
+ const rl = readline.createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout
8
+ });
9
+
10
+ // Helper function for prompts
11
+ function prompt(question) {
12
+ return new Promise(resolve => {
13
+ rl.question(question, answer => {
14
+ resolve(answer);
15
+ });
16
+ });
17
+ }
18
+
19
+ // Validate hex colour
20
+ function isValidHex(hex) {
21
+ return /^#[0-9A-F]{6}$/i.test(hex);
22
+ }
23
+
24
+ // Default colours
25
+ const defaultColours = {
26
+ primary: '#0077b6',
27
+ secondary: '#006d9e',
28
+ success: '#017F65',
29
+ warning: '#ffc107',
30
+ error: '#b20000',
31
+ neutral: '#6b7280'
32
+ };
33
+
34
+ // Default config template - matches emily.config.json structure
35
+ function createDefaultConfig(name, colours, fonts, baseUnit) {
36
+ return {
37
+ name,
38
+ description: `${name} design system`,
39
+ baseUnit: `${baseUnit}px`,
40
+ baseFontSize: '16px',
41
+ fontFamily: 'system-ui',
42
+ customFonts: [],
43
+ colours,
44
+ breakpoints: {
45
+ sm: '640px',
46
+ md: '768px',
47
+ lg: '1024px',
48
+ xl: '1280px',
49
+ '2xl': '1536px'
50
+ },
51
+ spacing: {
52
+ scale: {
53
+ '0': '0px',
54
+ 'px': '1px',
55
+ '0.5': '0.125rem',
56
+ '1': '0.25rem',
57
+ '1.5': '0.375rem',
58
+ '2': '0.5rem',
59
+ '2.5': '0.625rem',
60
+ '3': '0.75rem',
61
+ '3.5': '0.875rem',
62
+ '4': '1rem',
63
+ '5': '1.25rem',
64
+ '6': '1.5rem',
65
+ '7': '1.75rem',
66
+ '8': '2rem',
67
+ '9': '2.25rem',
68
+ '10': '2.5rem',
69
+ '11': '2.75rem',
70
+ '12': '3rem',
71
+ '14': '3.5rem',
72
+ '16': '4rem',
73
+ '20': '5rem',
74
+ '24': '6rem',
75
+ '28': '7rem',
76
+ '32': '8rem',
77
+ '36': '9rem',
78
+ '40': '10rem',
79
+ '44': '11rem',
80
+ '48': '12rem',
81
+ '52': '13rem',
82
+ '56': '14rem',
83
+ '60': '15rem',
84
+ '64': '16rem',
85
+ '72': '18rem',
86
+ '80': '20rem',
87
+ '96': '24rem'
88
+ },
89
+ borderWidths: [0, 2, 4, 8],
90
+ borderRadius: {
91
+ 'none': '0',
92
+ 'sm': '4px',
93
+ 'base': '8px',
94
+ 'md': '12px',
95
+ 'lg': '16px',
96
+ 'full': '9999px'
97
+ }
98
+ },
99
+ typography: {
100
+ lineHeightRatio: 1.5,
101
+ fontWeights: {
102
+ light: 300,
103
+ normal: 400,
104
+ medium: 500,
105
+ semibold: 600,
106
+ bold: 700
107
+ },
108
+ fontSizes: [
109
+ { name: 'xs', value: '12px', lineHeight: 1.5 },
110
+ { name: 'sm', value: '14px', lineHeight: 1.5 },
111
+ { name: 'base', value: '16px', lineHeight: 1.6 },
112
+ { name: 'lg', value: '18px', lineHeight: 1.6 },
113
+ { name: 'xl', value: '20px', lineHeight: 1.6 },
114
+ { name: '2xl', value: '24px', lineHeight: 1.4 },
115
+ { name: '3xl', value: '30px', lineHeight: 1.4 },
116
+ { name: '4xl', value: '36px', lineHeight: 1.3 }
117
+ ]
118
+ },
119
+ shadows: {
120
+ sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
121
+ base: '0 4px 6px rgba(0, 0, 0, 0.1)',
122
+ md: '0 10px 15px rgba(0, 0, 0, 0.1)',
123
+ lg: '0 20px 25px rgba(0, 0, 0, 0.15)',
124
+ none: 'none'
125
+ },
126
+ transitions: {
127
+ fast: '100ms',
128
+ base: '200ms',
129
+ slow: '300ms',
130
+ timing: 'cubic-bezier(0.4, 0, 0.2, 1)'
131
+ },
132
+ zIndex: {
133
+ auto: 'auto',
134
+ 0: '0',
135
+ 10: '10',
136
+ 20: '20',
137
+ 30: '30',
138
+ 40: '40',
139
+ 50: '50',
140
+ dropdown: '1000',
141
+ sticky: '1020',
142
+ fixed: '1030',
143
+ modal: '1040',
144
+ popover: '1060',
145
+ tooltip: '1070'
146
+ },
147
+ opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100]
148
+ };
149
+ }
150
+
151
+ async function init() {
152
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
153
+ console.log(' EmilyUI Setup');
154
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
155
+
156
+ try {
157
+ // 1. Project name
158
+ const name = await prompt('Project name: ');
159
+ if (!name.trim()) {
160
+ console.log('❌ Project name is required');
161
+ rl.close();
162
+ return;
163
+ }
164
+
165
+ // 2. Brand colours
166
+ console.log('\nBrand colours (hex format, e.g., #0077b6):');
167
+
168
+ const colours = {};
169
+ const colourNames = ['primary', 'secondary', 'success', 'warning', 'error', 'neutral'];
170
+
171
+ for (const colourName of colourNames) {
172
+ let colour;
173
+ let valid = false;
174
+
175
+ while (!valid) {
176
+ colour = await prompt(` ${colourName} [${defaultColours[colourName]}]: `);
177
+ colour = colour || defaultColours[colourName];
178
+
179
+ if (isValidHex(colour)) {
180
+ colours[colourName] = colour;
181
+ valid = true;
182
+ } else {
183
+ console.log(` ❌ Invalid hex colour. Use format: #0077b6`);
184
+ }
185
+ }
186
+ }
187
+
188
+ // 3. Fonts
189
+ console.log('\nFont families (optional, press Enter to skip):');
190
+
191
+ const fonts = {
192
+ sans: await prompt(' Sans-serif font: ') || 'system-ui, -apple-system, sans-serif',
193
+ serif: await prompt(' Serif font: ') || 'Georgia, serif',
194
+ mono: await prompt(' Monospace font: ') || 'Menlo, Monaco, monospace'
195
+ };
196
+
197
+ // 4. Base unit
198
+ let baseUnit = 8;
199
+ const baseUnitInput = await prompt('\nBase spacing unit (px) [8]: ');
200
+ if (baseUnitInput.trim()) {
201
+ const parsed = parseInt(baseUnitInput);
202
+ if (!isNaN(parsed) && parsed > 0) {
203
+ baseUnit = parsed;
204
+ } else {
205
+ console.log(' ⚠️ Invalid number, using default: 8px');
206
+ }
207
+ }
208
+
209
+ // 5. Create config
210
+ const config = createDefaultConfig(name, colours, fonts, baseUnit);
211
+
212
+ // 6. Write config file to the user's project directory
213
+ const configPath = path.join(process.cwd(), 'emily.config.json');
214
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
215
+
216
+ console.log('\n✅ Configuration created!');
217
+ console.log(` Project: ${name}`);
218
+ console.log(` Primary colour: ${colours.primary}`);
219
+ console.log(` Base unit: ${baseUnit}px`);
220
+ console.log(`\n📝 Config saved: ${configPath}`);
221
+
222
+ // 7. Run build
223
+ console.log('\n🔨 Building CSS...\n');
224
+ rl.close();
225
+
226
+ // Spawn build process
227
+ const { spawn } = require('child_process');
228
+ const build = spawn('npx', ['emily-ui', 'build'], {
229
+ cwd: process.cwd(),
230
+ stdio: 'inherit'
231
+ });
232
+
233
+ build.on('close', code => {
234
+ if (code === 0) {
235
+ console.log('\n✅ Setup complete!');
236
+ console.log('\n💡 Next steps:');
237
+ console.log(' 1. Open showcase.html in your browser to see components');
238
+ console.log(' 2. Copy component code into your project');
239
+ console.log(' 3. Update emily.config.json to customize colours/fonts');
240
+ console.log(' 4. Run: npm run build -- --purge ./src (to reduce CSS size)');
241
+ } else {
242
+ console.log('\n❌ Build failed');
243
+ }
244
+ });
245
+
246
+ } catch (err) {
247
+ console.log(`\n❌ Error: ${err.message}`);
248
+ rl.close();
249
+ }
250
+ }
251
+
252
+ init();
package/src/purge.js ADDED
@@ -0,0 +1,194 @@
1
+ // new version
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function getAllFiles(dir, extensions = ['.html', '.htm', '.twig', '.njk', '.liquid', '.hbs', '.jsx', '.tsx', '.vue', '.php', '.astro', '.svelte', '.blade.php', '.jinja', '.jinja2', '.j2', '.md']) {
7
+ let files = [];
8
+
9
+ try {
10
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
11
+
12
+ for (const entry of entries) {
13
+ const fullPath = path.join(dir, entry.name);
14
+
15
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') {
16
+ continue;
17
+ }
18
+
19
+ if (entry.isDirectory()) {
20
+ files = files.concat(getAllFiles(fullPath, extensions));
21
+ } else if (extensions.some(ext => entry.name.endsWith(ext))) {
22
+ files.push(fullPath);
23
+ }
24
+ }
25
+ } catch (err) {
26
+ console.warn(`Warning: Could not read directory ${dir}: ${err.message}`);
27
+ }
28
+
29
+ return files;
30
+ }
31
+
32
+ function extractClassNames(content) {
33
+ const classNames = new Set();
34
+ const classRegex = /class\s*=\s*["']([^"']+)["']/g;
35
+ let match;
36
+
37
+ while ((match = classRegex.exec(content)) !== null) {
38
+ const classes = match[1].split(/\s+/);
39
+ classes.forEach(cls => {
40
+ if (cls.trim()) classNames.add(cls.trim());
41
+ });
42
+ }
43
+
44
+ const vueRegex = /(?::class|class\.|v-bind:class)\s*=\s*["'{]([^"'}]+)["'}]/g;
45
+ while ((match = vueRegex.exec(content)) !== null) {
46
+ const classes = match[1].split(/[\s,]+/);
47
+ classes.forEach(cls => {
48
+ const cleaned = cls.replace(/['"`{}"]/g, '').trim();
49
+ if (cleaned) classNames.add(cleaned);
50
+ });
51
+ }
52
+
53
+ return classNames;
54
+ }
55
+
56
+ function extractBlocks(css) {
57
+ const blocks = [];
58
+ let current = '';
59
+ let depth = 0;
60
+
61
+ for (let i = 0; i < css.length; i++) {
62
+ current += css[i];
63
+ if (css[i] === '{') {
64
+ depth++;
65
+ } else if (css[i] === '}') {
66
+ depth--;
67
+ if (depth === 0) {
68
+ blocks.push(current.trim());
69
+ current = '';
70
+ }
71
+ }
72
+ }
73
+
74
+ if (current.trim()) {
75
+ blocks.push(current.trim());
76
+ }
77
+
78
+ return blocks;
79
+ }
80
+
81
+ function purgeBlock(block, usedClasses) {
82
+ if (
83
+ block.startsWith(':root') ||
84
+ block.startsWith('*,') ||
85
+ block.startsWith('html') ||
86
+ block.startsWith('body') ||
87
+ block.startsWith('@layer theme,') // Keep layer definition
88
+ ) {
89
+ return block;
90
+ }
91
+
92
+ if (block.startsWith('@') && block.includes('{')) {
93
+ const firstBrace = block.indexOf('{');
94
+ const lastBrace = block.lastIndexOf('}');
95
+
96
+ if (firstBrace === -1 || lastBrace === -1) return block;
97
+
98
+ const wrapperSignature = block.substring(0, firstBrace + 1);
99
+ const innerContent = block.substring(firstBrace + 1, lastBrace);
100
+
101
+ const innerBlocks = extractBlocks(innerContent);
102
+ const purgedInner = innerBlocks
103
+ .map(b => purgeBlock(b, usedClasses))
104
+ .filter(b => b.trim() !== '')
105
+ .join('\n ');
106
+
107
+ if (!purgedInner.trim()) return '';
108
+
109
+ return `${wrapperSignature}\n ${purgedInner}\n}`;
110
+ }
111
+
112
+ const selectorPart = block.split('{')[0];
113
+ if (!selectorPart) return '';
114
+
115
+ const cleanSelectorPart = selectorPart.replace(/\/\*[\s\S]*?\*\//g, '').trim();
116
+ const selectors = cleanSelectorPart.split(',').map(s => s.trim());
117
+
118
+ const isUsed = selectors.some(selector => {
119
+ if (!selector.includes('.')) return true;
120
+
121
+ for (const used of usedClasses) {
122
+ const escapedUsed = used.replace(/:/g, '\\\\:').replace(/\./g, '\\\\.');
123
+ const boundaryRegex = new RegExp(`\\.${escapedUsed}(?::[\\w\\-]+|[\\s,>+~]|$)`);
124
+ if (boundaryRegex.test(selector)) return true;
125
+ }
126
+ return false;
127
+ });
128
+
129
+ return isUsed ? block : '';
130
+ }
131
+
132
+ function purgeCSS(css, scanDir, config) {
133
+ const extensions = config && config.purge && config.purge.extensions
134
+ ? config.purge.extensions
135
+ : ['.html', '.htm', '.njk', '.liquid', '.hbs', '.jsx', '.tsx', '.vue', '.php', '.astro', '.svelte', '.blade.php'];
136
+
137
+ console.log(`\n🔍 Scanning for files in: ${scanDir}`);
138
+ console.log(` Extensions: ${extensions.join(', ')}`);
139
+
140
+ const files = getAllFiles(scanDir, extensions);
141
+
142
+ // Show per-extension breakdown so missing extensions are immediately obvious
143
+ const countsByExt = {};
144
+ for (const ext of extensions) countsByExt[ext] = 0;
145
+ for (const file of files) {
146
+ const ext = extensions.find(e => file.endsWith(e)) || 'other';
147
+ countsByExt[ext] = (countsByExt[ext] || 0) + 1;
148
+ }
149
+ const extSummary = Object.entries(countsByExt)
150
+ .filter(([, count]) => count > 0)
151
+ .map(([ext, count]) => `${count} ${ext}`)
152
+ .join(', ');
153
+ console.log(` Found: ${files.length === 0 ? 'no files' : extSummary}`);
154
+
155
+ if (files.length === 0) {
156
+ console.warn(' ⚠️ No template files found. Check that --purge points to the right directory and extensions are configured.');
157
+ console.warn(` Expected extensions: ${extensions.join(', ')}`);
158
+ return css;
159
+ }
160
+
161
+ const usedClasses = new Set();
162
+
163
+ for (const file of files) {
164
+ try {
165
+ const content = fs.readFileSync(file, 'utf8');
166
+ const classes = extractClassNames(content);
167
+ classes.forEach(cls => usedClasses.add(cls));
168
+ } catch (err) {
169
+ console.warn(` ⚠️ Could not read ${file}: ${err.message}`);
170
+ }
171
+ }
172
+
173
+ console.log(` Extracted ${usedClasses.size} unique class names from HTML`);
174
+
175
+ const blocks = extractBlocks(css);
176
+ const purgedBlocks = blocks
177
+ .map(block => purgeBlock(block, usedClasses))
178
+ .filter(block => block.trim() !== '');
179
+
180
+ const purgedCss = purgedBlocks.join('\n\n');
181
+
182
+ const beforeSize = (css.length / 1024).toFixed(2);
183
+ const afterSize = (purgedCss.length / 1024).toFixed(2);
184
+ const reduction = (((css.length - purgedCss.length) / css.length) * 100).toFixed(1);
185
+
186
+ console.log(`\n📦 Purge results:`);
187
+ console.log(` Before: ${beforeSize} KB`);
188
+ console.log(` After: ${afterSize} KB`);
189
+ console.log(` Reduction: ${reduction}%`);
190
+
191
+ return purgedCss;
192
+ }
193
+
194
+ module.exports = { purgeCSS, getAllFiles, extractClassNames };