swatchkit 1.1.0 → 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.
Files changed (58) hide show
  1. package/build.js +82 -90
  2. package/package.json +2 -2
  3. package/src/blueprints/colors.json +11 -0
  4. package/src/blueprints/compositions/cluster.css +20 -0
  5. package/src/blueprints/compositions/flow.css +10 -0
  6. package/src/blueprints/compositions/grid.css +36 -0
  7. package/src/blueprints/compositions/index.css +12 -0
  8. package/src/blueprints/compositions/prose.css +89 -0
  9. package/src/blueprints/compositions/repel.css +23 -0
  10. package/src/blueprints/compositions/sidebar.css +44 -0
  11. package/src/blueprints/compositions/switcher.css +32 -0
  12. package/src/blueprints/compositions/wrapper.css +14 -0
  13. package/src/blueprints/fonts.json +24 -0
  14. package/src/blueprints/global/elements.css +609 -0
  15. package/src/blueprints/global/index.css +13 -0
  16. package/src/blueprints/global/reset.css +91 -0
  17. package/src/blueprints/global/variables.css +58 -0
  18. package/src/blueprints/main.css +27 -0
  19. package/src/blueprints/spacing.json +19 -0
  20. package/src/blueprints/swatches/hello.css +11 -0
  21. package/src/blueprints/swatches/index.css +7 -0
  22. package/src/blueprints/swatchkit-ui.css +79 -0
  23. package/src/blueprints/text-leading.json +13 -0
  24. package/src/blueprints/text-sizes.json +21 -0
  25. package/src/blueprints/text-weights.json +11 -0
  26. package/src/blueprints/utilities/index.css +9 -0
  27. package/src/blueprints/utilities/region.css +12 -0
  28. package/src/blueprints/utilities/visually-hidden.css +18 -0
  29. package/src/blueprints/viewports.json +10 -0
  30. package/src/generators/index.js +437 -0
  31. package/src/layout.html +0 -1
  32. package/src/preview-layout.html +13 -0
  33. package/src/templates/compositions/cluster/description.html +7 -0
  34. package/src/templates/compositions/cluster/index.html +7 -0
  35. package/src/templates/compositions/flow/description.html +8 -0
  36. package/src/templates/compositions/flow/index.html +6 -0
  37. package/src/templates/compositions/grid/description.html +12 -0
  38. package/src/templates/compositions/grid/index.html +7 -0
  39. package/src/templates/compositions/prose/description.html +8 -0
  40. package/src/templates/compositions/prose/index.html +6 -0
  41. package/src/templates/compositions/repel/description.html +9 -0
  42. package/src/templates/compositions/repel/index.html +4 -0
  43. package/src/templates/compositions/sidebar/description.html +13 -0
  44. package/src/templates/compositions/sidebar/index.html +4 -0
  45. package/src/templates/compositions/switcher/description.html +8 -0
  46. package/src/templates/compositions/switcher/index.html +29 -0
  47. package/src/templates/compositions/wrapper/description.html +8 -0
  48. package/src/templates/compositions/wrapper/index.html +5 -0
  49. package/src/templates/hello/README.md +83 -0
  50. package/src/templates/hello/index.html +1 -0
  51. package/src/templates/prose.html +366 -0
  52. package/src/templates/script.js +41 -0
  53. package/src/templates/utilities/region/description.html +8 -0
  54. package/src/templates/utilities/region/index.html +5 -0
  55. package/src/templates/utilities/visually-hidden/description.html +7 -0
  56. package/src/templates/utilities/visually-hidden/index.html +7 -0
  57. package/src/tokens.js +428 -0
  58. package/src/utils/clamp-generator.js +38 -0
package/src/tokens.js ADDED
@@ -0,0 +1,428 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const clampGenerator = require('./utils/clamp-generator');
4
+
5
+ function slugify(text) {
6
+ return text
7
+ .toString()
8
+ .toLowerCase()
9
+ .trim()
10
+ .replace(/\s+/g, '-'); // Replace spaces with -
11
+ }
12
+
13
+ function processTokens(tokensDir, cssDir) {
14
+ const outputFile = path.join(cssDir, 'tokens.css');
15
+ let cssContent = '/* AUTO-GENERATED by SwatchKit — do not edit manually. */\n';
16
+ cssContent += '/* Edit the JSON files in your tokens directory and rebuild. */\n\n';
17
+ cssContent += ':root {\n';
18
+ let hasTokens = false;
19
+
20
+ // Store structured data for utility generation
21
+ const tokensContext = {
22
+ colors: [],
23
+ textWeights: [],
24
+ textLeading: [],
25
+ textSizes: [],
26
+ spacing: [],
27
+ fonts: [],
28
+ viewports: {}
29
+ };
30
+
31
+ // Helper to process a generic token file
32
+ function processFile(filename, contextKey) {
33
+ const filePath = path.join(tokensDir, filename);
34
+ if (!fs.existsSync(filePath)) return;
35
+
36
+ try {
37
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
38
+ const data = JSON.parse(fileContent);
39
+
40
+ if (data.items && Array.isArray(data.items)) {
41
+ hasTokens = true;
42
+ cssContent += ` /* ${data.title || filename} */\n`;
43
+ data.items.forEach(item => {
44
+ if (item.name && item.value) {
45
+ const slug = slugify(item.name);
46
+ cssContent += ` --${slug}: ${item.value};\n`;
47
+
48
+ // Store for utilities if a key is provided
49
+ if (contextKey && tokensContext[contextKey]) {
50
+ tokensContext[contextKey].push({ name: slug, value: item.value });
51
+ }
52
+ }
53
+ });
54
+ cssContent += '\n';
55
+ }
56
+ } catch (error) {
57
+ console.error(`[SwatchKit] Error processing ${filename}:`, error.message);
58
+ }
59
+ }
60
+
61
+ // 1. Process Viewports (First, so they are available for fluid calculations)
62
+ const viewportsFile = path.join(tokensDir, 'viewports.json');
63
+ if (fs.existsSync(viewportsFile)) {
64
+ try {
65
+ const fileContent = fs.readFileSync(viewportsFile, 'utf-8');
66
+ const data = JSON.parse(fileContent);
67
+
68
+ if (data.items && Array.isArray(data.items)) {
69
+ // Build a lookup object for clamp-generator (keyed by name)
70
+ const viewportsLookup = {};
71
+ data.items.forEach(item => {
72
+ viewportsLookup[item.name] = item.value;
73
+ });
74
+ tokensContext.viewports = viewportsLookup;
75
+
76
+ hasTokens = true;
77
+ cssContent += ` /* ${data.title || 'Viewports'} */\n`;
78
+
79
+ data.items.forEach(item => {
80
+ if (item.name && item.value !== undefined) {
81
+ const slug = slugify(item.name);
82
+ // Append 'px' if it's a number
83
+ const cssValue = typeof item.value === 'number' ? `${item.value}px` : item.value;
84
+ cssContent += ` --${slug}: ${cssValue};\n`;
85
+ }
86
+ });
87
+ cssContent += '\n';
88
+ }
89
+
90
+ } catch (error) {
91
+ console.error(`[SwatchKit] Error processing viewports.json:`, error.message);
92
+ }
93
+ }
94
+
95
+ // 2. Process Colors
96
+ processFile('colors.json', 'colors');
97
+
98
+ // 3. Process Text Weights
99
+ processFile('text-weights.json', 'textWeights');
100
+
101
+ // 4. Process Text Leading
102
+ const leadingFile = path.join(tokensDir, 'text-leading.json');
103
+ if (fs.existsSync(leadingFile)) {
104
+ try {
105
+ const fileContent = fs.readFileSync(leadingFile, 'utf-8');
106
+ const data = JSON.parse(fileContent);
107
+
108
+ if (data.base && data.ratio && data.items) {
109
+ hasTokens = true;
110
+ cssContent += ` /* ${data.title || 'Text Leading'} */\n`;
111
+ cssContent += ` --leading-scale-base: ${data.base};\n`;
112
+ cssContent += ` --leading-scale-ratio: ${data.ratio};\n`;
113
+
114
+ data.items.forEach(item => {
115
+ const slug = slugify(item.name);
116
+ let value;
117
+
118
+ if (item.value !== undefined) {
119
+ // Manual value (e.g. 1.5)
120
+ value = item.value;
121
+ cssContent += ` --${slug}: ${value};\n`;
122
+ } else if (item.step !== undefined) {
123
+ // Modular scale step (e.g. 1)
124
+ // We can't resolve calc() here for context easily, but utilities use the var anyway
125
+ value = `calc(var(--leading-scale-base) * pow(var(--leading-scale-ratio), ${item.step}))`;
126
+ cssContent += ` --${slug}: ${value};\n`;
127
+ }
128
+
129
+ // Store for utilities
130
+ if (tokensContext.textLeading) {
131
+ tokensContext.textLeading.push({ name: slug, value });
132
+ }
133
+ });
134
+ cssContent += '\n';
135
+ }
136
+ } catch (error) {
137
+ console.error(`[SwatchKit] Error processing text-leading.json:`, error.message);
138
+ }
139
+ }
140
+
141
+ // 5. Process Text Sizes
142
+ const textSizesFile = path.join(tokensDir, 'text-sizes.json');
143
+ if (fs.existsSync(textSizesFile)) {
144
+ try {
145
+ const fileContent = fs.readFileSync(textSizesFile, 'utf-8');
146
+ const data = JSON.parse(fileContent);
147
+
148
+ if (data.items && Array.isArray(data.items)) {
149
+ hasTokens = true;
150
+ cssContent += ` /* ${data.title || 'Text Sizes'} */\n`;
151
+
152
+ // Check if we have viewports for fluid generation
153
+ const hasFluidData = tokensContext.viewports &&
154
+ tokensContext.viewports['viewport-min'] &&
155
+ tokensContext.viewports['viewport-max'];
156
+
157
+ const fileRatio = data.fluidRatio || 1.125;
158
+ const fluidItems = [];
159
+ const staticItems = [];
160
+
161
+ data.items.forEach(item => {
162
+ // If "value" is present, it's static (user explicit override)
163
+ if (item.value !== undefined) {
164
+ staticItems.push(item);
165
+ return;
166
+ }
167
+
168
+ // If min or max is present, it's fluid (calculate missing side)
169
+ if (item.min !== undefined || item.max !== undefined) {
170
+ const ratio = item.fluidRatio || fileRatio;
171
+ let min = item.min;
172
+ let max = item.max;
173
+
174
+ if (min === undefined) min = parseFloat((max / ratio).toFixed(2));
175
+ if (max === undefined) max = parseFloat((min * ratio).toFixed(2));
176
+
177
+ fluidItems.push({ ...item, min, max });
178
+ }
179
+ });
180
+
181
+ // Process Fluid Items
182
+ if (hasFluidData && fluidItems.length > 0) {
183
+ const fluidTokens = clampGenerator(fluidItems, tokensContext.viewports);
184
+ fluidTokens.forEach(item => {
185
+ const slug = slugify(item.name);
186
+ cssContent += ` --${slug}: ${item.value};\n`;
187
+ tokensContext.textSizes.push({ name: slug, value: item.value });
188
+ });
189
+ } else if (fluidItems.length > 0) {
190
+ console.warn('[SwatchKit] Fluid text sizes detected but viewports missing. Skipping fluid generation.');
191
+ }
192
+
193
+ // Process Static Items (fallback or simple values)
194
+ staticItems.forEach(item => {
195
+ if (item.name && item.value) {
196
+ const slug = slugify(item.name);
197
+ cssContent += ` --${slug}: ${item.value};\n`;
198
+ tokensContext.textSizes.push({ name: slug, value: item.value });
199
+ }
200
+ });
201
+
202
+ cssContent += '\n';
203
+ }
204
+
205
+ } catch (error) {
206
+ console.error(`[SwatchKit] Error processing text-sizes.json:`, error.message);
207
+ }
208
+ }
209
+
210
+ // 6. Process Spacing
211
+ const spacingFile = path.join(tokensDir, 'spacing.json');
212
+ if (fs.existsSync(spacingFile)) {
213
+ try {
214
+ const fileContent = fs.readFileSync(spacingFile, 'utf-8');
215
+ const data = JSON.parse(fileContent);
216
+
217
+ if (data.items && Array.isArray(data.items)) {
218
+ hasTokens = true;
219
+ cssContent += ` /* ${data.title || 'Spacing'} */\n`;
220
+
221
+ // Check if we have viewports for fluid generation
222
+ const hasFluidData = tokensContext.viewports &&
223
+ tokensContext.viewports['viewport-min'] &&
224
+ tokensContext.viewports['viewport-max'];
225
+
226
+ const fileRatio = data.fluidRatio || 1.125;
227
+ const fluidItems = [];
228
+ const staticItems = [];
229
+
230
+ data.items.forEach(item => {
231
+ // If "value" is present, it's static (user explicit override)
232
+ if (item.value !== undefined) {
233
+ staticItems.push(item);
234
+ return;
235
+ }
236
+
237
+ // If min or max is present, it's fluid (calculate missing side)
238
+ if (item.min !== undefined || item.max !== undefined) {
239
+ const ratio = item.fluidRatio || fileRatio;
240
+ let min = item.min;
241
+ let max = item.max;
242
+
243
+ if (min === undefined) min = parseFloat((max / ratio).toFixed(2));
244
+ if (max === undefined) max = parseFloat((min * ratio).toFixed(2));
245
+
246
+ fluidItems.push({ ...item, min, max });
247
+ }
248
+ });
249
+
250
+ // Process Fluid Items
251
+ if (hasFluidData && fluidItems.length > 0) {
252
+ const fluidTokens = clampGenerator(fluidItems, tokensContext.viewports);
253
+ fluidTokens.forEach(item => {
254
+ const slug = slugify(item.name);
255
+ cssContent += ` --${slug}: ${item.value};\n`;
256
+ tokensContext.spacing.push({ name: slug, value: item.value });
257
+ });
258
+ } else if (fluidItems.length > 0) {
259
+ console.warn('[SwatchKit] Fluid spacing detected but viewports missing. Skipping fluid generation.');
260
+ }
261
+
262
+ // Process Static Items (fallback or simple values)
263
+ staticItems.forEach(item => {
264
+ if (item.name && item.value) {
265
+ const slug = slugify(item.name);
266
+ cssContent += ` --${slug}: ${item.value};\n`;
267
+ tokensContext.spacing.push({ name: slug, value: item.value });
268
+ }
269
+ });
270
+
271
+ cssContent += '\n';
272
+ }
273
+
274
+ } catch (error) {
275
+ console.error(`[SwatchKit] Error processing spacing.json:`, error.message);
276
+ }
277
+ }
278
+
279
+ // 7. Process Fonts
280
+ const fontsFile = path.join(tokensDir, 'fonts.json');
281
+ if (fs.existsSync(fontsFile)) {
282
+ try {
283
+ const fileContent = fs.readFileSync(fontsFile, 'utf-8');
284
+ const data = JSON.parse(fileContent);
285
+
286
+ if (data.items && Array.isArray(data.items)) {
287
+ hasTokens = true;
288
+ cssContent += ` /* ${data.title || 'Fonts'} */\n`;
289
+ data.items.forEach(item => {
290
+ if (item.name && item.value && Array.isArray(item.value)) {
291
+ const slug = slugify(item.name);
292
+ // Join font names with commas, quoting if necessary (though mostly optional in modern CSS if no special chars)
293
+ // But let's just join them as requested.
294
+ const fontStack = item.value.join(', ');
295
+ cssContent += ` --${slug}: ${fontStack};\n`;
296
+ }
297
+ });
298
+ cssContent += '\n';
299
+ }
300
+ } catch (error) {
301
+ console.error(`[SwatchKit] Error processing fonts.json:`, error.message);
302
+ }
303
+ }
304
+
305
+ cssContent += '}\n';
306
+
307
+ // If no tokens found, exit early
308
+ if (!hasTokens) {
309
+ return tokensContext;
310
+ }
311
+
312
+ // Ensure cssDir exists
313
+ if (!fs.existsSync(cssDir)) {
314
+ fs.mkdirSync(cssDir, { recursive: true });
315
+ }
316
+
317
+ const existing = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8') : null;
318
+ if (existing !== cssContent) {
319
+ fs.writeFileSync(outputFile, cssContent);
320
+ console.log(`+ Generated CSS: ${outputFile} (Do not edit manually)`);
321
+ }
322
+
323
+ return tokensContext;
324
+ }
325
+
326
+ function generateTokenUtilities(tokensContext, cssDir) {
327
+ const outputFile = path.join(cssDir, 'tokens.css');
328
+ let cssContent = '/* AUTO-GENERATED Token Utilities */\n';
329
+ cssContent += '/* Classes that map directly to your tokens */\n\n';
330
+
331
+ // 1. Colors (.color\:name, .background-color\:name)
332
+ if (tokensContext.colors && tokensContext.colors.length > 0) {
333
+ cssContent += '/* Colors */\n';
334
+ tokensContext.colors.forEach(item => {
335
+ // Escape the colon for CSS selector but use normal name for variable
336
+ // Using !important to ensure utility classes always win specificity wars (CUBE/Every Layout methodology)
337
+ cssContent += `.color\\:${item.name} { color: var(--${item.name}) !important; }\n`;
338
+ cssContent += `.background-color\\:${item.name} { background-color: var(--${item.name}) !important; }\n`;
339
+ });
340
+ cssContent += '\n';
341
+ }
342
+
343
+ // 2. Text Sizes (.font-size\:name)
344
+ if (tokensContext.textSizes && tokensContext.textSizes.length > 0) {
345
+ cssContent += '/* Text Sizes */\n';
346
+ tokensContext.textSizes.forEach(item => {
347
+ cssContent += `.font-size\\:${item.name} { font-size: var(--${item.name}) !important; }\n`;
348
+ });
349
+ cssContent += '\n';
350
+ }
351
+
352
+ // 3. Text Weights (.font-weight\:name)
353
+ if (tokensContext.textWeights && tokensContext.textWeights.length > 0) {
354
+ cssContent += '/* Text Weights */\n';
355
+ tokensContext.textWeights.forEach(item => {
356
+ cssContent += `.font-weight\\:${item.name} { font-weight: var(--${item.name}) !important; }\n`;
357
+ });
358
+ cssContent += '\n';
359
+ }
360
+
361
+ // 4. Leading (.line-height\:name)
362
+ if (tokensContext.textLeading && tokensContext.textLeading.length > 0) {
363
+ cssContent += '/* Line Heights */\n';
364
+ tokensContext.textLeading.forEach(item => {
365
+ cssContent += `.line-height\\:${item.name} { line-height: var(--${item.name}) !important; }\n`;
366
+ });
367
+ cssContent += '\n';
368
+ }
369
+
370
+ // 5. Spacing logical properties (12 per token)
371
+ if (tokensContext.spacing && tokensContext.spacing.length > 0) {
372
+ const spacingProperties = [
373
+ 'margin-block',
374
+ 'margin-block-start',
375
+ 'margin-block-end',
376
+ 'margin-inline',
377
+ 'margin-inline-start',
378
+ 'margin-inline-end',
379
+ 'padding-block',
380
+ 'padding-block-start',
381
+ 'padding-block-end',
382
+ 'padding-inline',
383
+ 'padding-inline-start',
384
+ 'padding-inline-end',
385
+ ];
386
+ cssContent += '/* Spacing */\n';
387
+ tokensContext.spacing.forEach(item => {
388
+ spacingProperties.forEach(prop => {
389
+ cssContent += `.${prop}\\:${item.name} { ${prop}: var(--${item.name}) !important; }\n`;
390
+ });
391
+ });
392
+ cssContent += '\n';
393
+
394
+ // 6. Region space (sets --region-space custom property)
395
+ cssContent += '/* Region Space */\n';
396
+ tokensContext.spacing.forEach(item => {
397
+ cssContent += `.region-space\\:${item.name} { --region-space: var(--${item.name}); }\n`;
398
+ });
399
+ cssContent += '\n';
400
+
401
+ // 7. Flow space (sets --flow-space custom property)
402
+ cssContent += '/* Flow Space */\n';
403
+ tokensContext.spacing.forEach(item => {
404
+ cssContent += `.flow-space\\:${item.name} { --flow-space: var(--${item.name}); }\n`;
405
+ });
406
+ cssContent += '\n';
407
+
408
+ // 8. Gutter (sets --gutter custom property)
409
+ cssContent += '/* Gutter */\n';
410
+ tokensContext.spacing.forEach(item => {
411
+ cssContent += `.gutter\\:${item.name} { --gutter: var(--${item.name}); }\n`;
412
+ });
413
+ cssContent += '\n';
414
+ }
415
+
416
+ // Ensure cssDir exists
417
+ if (!fs.existsSync(cssDir)) {
418
+ fs.mkdirSync(cssDir, { recursive: true });
419
+ }
420
+
421
+ const existing = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8') : null;
422
+ if (existing !== cssContent) {
423
+ fs.writeFileSync(outputFile, cssContent);
424
+ console.log(`+ Generated Utilities: ${outputFile} (Do not edit manually)`);
425
+ }
426
+ }
427
+
428
+ module.exports = { processTokens, generateTokenUtilities };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Takes an array of tokens and sends back and array of name
3
+ * and clamp pairs for CSS fluid values.
4
+ *
5
+ * @param {array} tokens array of {name: string, min: number, max: number}
6
+ * @param {object} viewports {min: number, max: number}
7
+ * @returns {array} {name: string, value: string}
8
+ */
9
+ const clampGenerator = (tokens, viewports) => {
10
+ const rootSize = 16;
11
+
12
+ return tokens.map(({ name, min, max }) => {
13
+ if (min === max) {
14
+ return `${min / rootSize}rem`;
15
+ }
16
+
17
+ // Convert the min and max sizes to rems
18
+ const minSize = min / rootSize;
19
+ const maxSize = max / rootSize;
20
+
21
+ // Convert the pixel viewport sizes into rems
22
+ const minViewport = viewports['viewport-min'] / rootSize;
23
+ const maxViewport = viewports['viewport-max'] / rootSize;
24
+
25
+ // Slope and intersection allow us to have a fluid value but also keep that sensible
26
+ const slope = (maxSize - minSize) / (maxViewport - minViewport);
27
+ const intersection = -1 * minViewport * slope + minSize;
28
+
29
+ return {
30
+ name,
31
+ value: `clamp(${minSize}rem, ${intersection.toFixed(2)}rem + ${(
32
+ slope * 100
33
+ ).toFixed(2)}vw, ${maxSize}rem)`,
34
+ };
35
+ });
36
+ };
37
+
38
+ module.exports = clampGenerator;