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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/emilyui.js +17 -0
- package/dist/emily.css +23068 -0
- package/dist/emily.demo.css +110 -0
- package/dist/emily.demo.min.css +1 -0
- package/dist/emily.min.css +1 -0
- package/dist/emily.purged.css +840 -0
- package/dist/emily.purged.min.css +1 -0
- package/fonts/inter/Inter-Variable.woff2 +0 -0
- package/fonts/lexend/Lexend-Variable.woff2 +0 -0
- package/package.json +42 -0
- package/src/generators.js +506 -0
- package/src/index.js +952 -0
- package/src/init.js +252 -0
- package/src/purge.js +194 -0
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 };
|