clampography 2.0.0-beta.3 ā 2.0.0-beta.30
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 +20 -20
- package/README.md +133 -35
- package/css/base.css +501 -0
- package/css/base.min.css +1 -0
- package/css/clampography.css +1050 -0
- package/css/clampography.min.css +1 -0
- package/css/extra.css +189 -0
- package/css/extra.min.css +1 -0
- package/css/figma-tokens.json +110 -0
- package/css/forms.css +244 -0
- package/css/forms.min.css +1 -0
- package/css/kbd.css +28 -0
- package/css/kbd.min.css +1 -0
- package/css/theme.css +73 -0
- package/css/theme.min.css +1 -0
- package/package.json +42 -22
- package/src/base.js +628 -428
- package/src/convert.js +285 -0
- package/src/export-figma.js +89 -0
- package/src/extra.js +256 -137
- package/src/forms.js +298 -0
- package/src/index.js +254 -90
- package/src/kbd.js +88 -0
- package/src/print.js +92 -0
- package/src/theme-plugin.js +68 -14
- package/src/theme.js +34 -0
- package/src/themes.js +43 -48
- package/src/types/index.d.ts +22 -0
- package/src/types/theme-plugin.d.ts +32 -0
- package/src/types/themes.d.ts +28 -0
package/src/convert.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JS to CSS Converter for Bun.sh
|
|
5
|
+
* Converts multiple .js files to .css and .css.min
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
9
|
+
import { basename, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
const FILES_TO_CONVERT = ["base.js", "theme.js", "extra.js", "forms.js", "kbd.js"];
|
|
13
|
+
const OUTPUT_DIR = "css";
|
|
14
|
+
|
|
15
|
+
// Options passed to the JS functions (simulating plugin config)
|
|
16
|
+
const DEFAULT_OPTIONS = {
|
|
17
|
+
root: ":root",
|
|
18
|
+
prefix: "clampography",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Import the JS module dynamically
|
|
23
|
+
*/
|
|
24
|
+
async function loadJSModule(filePath) {
|
|
25
|
+
const module = await import(`./${filePath}`);
|
|
26
|
+
const exported = module.default;
|
|
27
|
+
|
|
28
|
+
// Check if it's a function (new structure) or object (old structure)
|
|
29
|
+
if (typeof exported === "function") {
|
|
30
|
+
// Invoke the function with default options
|
|
31
|
+
return exported(DEFAULT_OPTIONS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return exported;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert JS object to CSS string
|
|
39
|
+
*/
|
|
40
|
+
function toCSSString(obj, indentLevel = 0) {
|
|
41
|
+
const spaces = " ".repeat(indentLevel);
|
|
42
|
+
const lines = [];
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
45
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
46
|
+
// It's a selector or nested object
|
|
47
|
+
|
|
48
|
+
// Special handling for @layer base wrapper if present in object
|
|
49
|
+
if (key === "@layer base") {
|
|
50
|
+
lines.push(`${spaces}@layer base {`);
|
|
51
|
+
lines.push(toCSSString(value, indentLevel + 1));
|
|
52
|
+
lines.push(`${spaces}}`);
|
|
53
|
+
} else {
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push(`${spaces}${key} {`);
|
|
56
|
+
|
|
57
|
+
// Recursive call for nested properties or media queries
|
|
58
|
+
// But first, separate properties from nested objects
|
|
59
|
+
const properties = [];
|
|
60
|
+
const nestedObjects = {};
|
|
61
|
+
|
|
62
|
+
for (const [prop, val] of Object.entries(value)) {
|
|
63
|
+
if (typeof val === "object" && val !== null) {
|
|
64
|
+
nestedObjects[prop] = val;
|
|
65
|
+
} else {
|
|
66
|
+
properties.push(`${spaces} ${prop}: ${val};`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add properties first
|
|
71
|
+
lines.push(...properties);
|
|
72
|
+
|
|
73
|
+
// Then process nested objects (like pseudo-elements or media queries inside)
|
|
74
|
+
if (Object.keys(nestedObjects).length > 0) {
|
|
75
|
+
// This handles nesting if the JS structure supports it (like SCSS nesting)
|
|
76
|
+
// Standard CSS doesn't support nesting without post-processing,
|
|
77
|
+
// but we'll output it as nested blocks for clarity if present.
|
|
78
|
+
lines.push(toCSSString(nestedObjects, indentLevel + 1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push(`${spaces}}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert JS object to minified CSS string
|
|
90
|
+
*/
|
|
91
|
+
function toMinifiedCSS(obj) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
95
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
96
|
+
if (key === "@layer base") {
|
|
97
|
+
parts.push(`@layer base{${toMinifiedCSS(value)}}`);
|
|
98
|
+
} else {
|
|
99
|
+
const properties = [];
|
|
100
|
+
const nestedParts = [];
|
|
101
|
+
|
|
102
|
+
for (const [prop, val] of Object.entries(value)) {
|
|
103
|
+
if (typeof val === "object" && val !== null) {
|
|
104
|
+
nestedParts.push(toMinifiedCSS({ [prop]: val }));
|
|
105
|
+
} else {
|
|
106
|
+
properties.push(`${prop}:${val}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let blockContent = properties.join(";");
|
|
111
|
+
if (nestedParts.length > 0) {
|
|
112
|
+
blockContent += nestedParts.join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (blockContent.length > 0) {
|
|
116
|
+
parts.push(`${key}{${blockContent}}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return parts.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get output file names from input file name
|
|
126
|
+
*/
|
|
127
|
+
function getOutputFileNames(inputFile) {
|
|
128
|
+
const baseNameWithoutExt = basename(inputFile, ".js");
|
|
129
|
+
return {
|
|
130
|
+
css: `${baseNameWithoutExt}.css`,
|
|
131
|
+
min: `${baseNameWithoutExt}.min.css`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert single JS file to CSS
|
|
137
|
+
*/
|
|
138
|
+
async function convertFile(inputFile) {
|
|
139
|
+
const outputNames = getOutputFileNames(inputFile);
|
|
140
|
+
const outputCSS = resolve(OUTPUT_DIR, outputNames.css);
|
|
141
|
+
const outputMin = resolve(OUTPUT_DIR, outputNames.min);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
console.log(`\nš Loading ${inputFile}...`);
|
|
145
|
+
const jsObject = await loadJSModule(inputFile);
|
|
146
|
+
|
|
147
|
+
console.log(`āļø Converting to CSS...`);
|
|
148
|
+
let cssContent = toCSSString(jsObject, 1);
|
|
149
|
+
|
|
150
|
+
// Explicitly wrap in @layer base for the final file
|
|
151
|
+
// (Assuming the JS object is just the rules, without @layer wrapper)
|
|
152
|
+
cssContent = `@layer base {\n${cssContent}\n}\n`;
|
|
153
|
+
|
|
154
|
+
console.log(`š¾ Writing ${outputNames.css}...`);
|
|
155
|
+
writeFileSync(outputCSS, cssContent, "utf-8");
|
|
156
|
+
|
|
157
|
+
// Generate minified version
|
|
158
|
+
console.log(`āļø Generating minified version...`);
|
|
159
|
+
let minifiedContent = toMinifiedCSS(jsObject);
|
|
160
|
+
minifiedContent = `@layer base{${minifiedContent}}`;
|
|
161
|
+
|
|
162
|
+
console.log(`š¾ Writing ${outputNames.min}...`);
|
|
163
|
+
writeFileSync(outputMin, minifiedContent, "utf-8");
|
|
164
|
+
|
|
165
|
+
// Show file sizes
|
|
166
|
+
const cssSize = statSync(outputCSS).size;
|
|
167
|
+
const minSize = statSync(outputMin).size;
|
|
168
|
+
const reduction = ((1 - minSize / cssSize) * 100).toFixed(1);
|
|
169
|
+
|
|
170
|
+
console.log(`ā
${inputFile} converted successfully!`);
|
|
171
|
+
console.log(` š ${outputNames.css}: ${(cssSize / 1024).toFixed(2)} KB`);
|
|
172
|
+
console.log(
|
|
173
|
+
` š ${outputNames.min}: ${
|
|
174
|
+
(minSize / 1024).toFixed(2)
|
|
175
|
+
} KB (${reduction}% smaller)`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
input: inputFile,
|
|
180
|
+
output: outputNames.css,
|
|
181
|
+
outputMin: outputNames.min,
|
|
182
|
+
cssSize,
|
|
183
|
+
minSize,
|
|
184
|
+
reduction,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`ā Error converting ${inputFile}:`, error.message);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Main conversion function
|
|
194
|
+
*/
|
|
195
|
+
async function convertAllFiles() {
|
|
196
|
+
console.log("š Starting CSS conversion...");
|
|
197
|
+
console.log(`š Output directory: ${OUTPUT_DIR}`);
|
|
198
|
+
console.log(`š Files to convert: ${FILES_TO_CONVERT.length}`);
|
|
199
|
+
|
|
200
|
+
// Create output directory if it doesn't exist
|
|
201
|
+
if (!existsSync(OUTPUT_DIR)) {
|
|
202
|
+
console.log(`š Creating output directory: ${OUTPUT_DIR}`);
|
|
203
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const results = [];
|
|
207
|
+
|
|
208
|
+
// Convert all files
|
|
209
|
+
for (const file of FILES_TO_CONVERT) {
|
|
210
|
+
const result = await convertFile(file);
|
|
211
|
+
if (result) {
|
|
212
|
+
results.push(result);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Summary
|
|
217
|
+
console.log("\n" + "=".repeat(60));
|
|
218
|
+
console.log("š CONVERSION SUMMARY");
|
|
219
|
+
console.log("=".repeat(60));
|
|
220
|
+
|
|
221
|
+
if (results.length === 0) {
|
|
222
|
+
console.log("ā No files were converted successfully.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(
|
|
227
|
+
`ā
Successfully converted: ${results.length}/${FILES_TO_CONVERT.length} files\n`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let totalCSSSize = 0;
|
|
231
|
+
let totalMinSize = 0;
|
|
232
|
+
|
|
233
|
+
results.forEach((result) => {
|
|
234
|
+
console.log(`š ${result.input}`);
|
|
235
|
+
console.log(
|
|
236
|
+
` ā ${OUTPUT_DIR}/${result.output} (${
|
|
237
|
+
(result.cssSize / 1024).toFixed(2)
|
|
238
|
+
} KB)`,
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
` ā ${OUTPUT_DIR}/${result.outputMin} (${
|
|
242
|
+
(result.minSize / 1024).toFixed(2)
|
|
243
|
+
} KB, ${result.reduction}% smaller)`,
|
|
244
|
+
);
|
|
245
|
+
totalCSSSize += result.cssSize;
|
|
246
|
+
totalMinSize += result.minSize;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const totalReduction = ((1 - totalMinSize / totalCSSSize) * 100).toFixed(1);
|
|
250
|
+
|
|
251
|
+
// Generate unified bundle
|
|
252
|
+
console.log("\nš¦ Generating unified bundle...");
|
|
253
|
+
const bundleCSSPath = resolve(OUTPUT_DIR, "clampography.css");
|
|
254
|
+
const bundleMinPath = resolve(OUTPUT_DIR, "clampography.min.css");
|
|
255
|
+
|
|
256
|
+
let combinedCSS = "/**\n * Clampography CSS Bundle\n * Contains: " + FILES_TO_CONVERT.join(", ") + "\n */\n\n";
|
|
257
|
+
let combinedMinCSS = "";
|
|
258
|
+
|
|
259
|
+
for (const file of FILES_TO_CONVERT) {
|
|
260
|
+
const names = getOutputFileNames(file);
|
|
261
|
+
combinedCSS += `/* --- ${file} --- */\n`;
|
|
262
|
+
combinedCSS += readFileSync(resolve(OUTPUT_DIR, names.css), "utf-8") + "\n";
|
|
263
|
+
combinedMinCSS += readFileSync(resolve(OUTPUT_DIR, names.min), "utf-8");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
writeFileSync(bundleCSSPath, combinedCSS, "utf-8");
|
|
267
|
+
writeFileSync(bundleMinPath, combinedMinCSS, "utf-8");
|
|
268
|
+
|
|
269
|
+
console.log(`ā
Created clampography.css (${(statSync(bundleCSSPath).size / 1024).toFixed(2)} KB)`);
|
|
270
|
+
console.log(`ā
Created clampography.min.css (${(statSync(bundleMinPath).size / 1024).toFixed(2)} KB)`);
|
|
271
|
+
|
|
272
|
+
console.log("\n" + "-".repeat(60));
|
|
273
|
+
console.log(`š Total CSS size: ${(totalCSSSize / 1024).toFixed(2)} KB`);
|
|
274
|
+
console.log(`š Total Minified size: ${(totalMinSize / 1024).toFixed(2)} KB`);
|
|
275
|
+
console.log(`š Total reduction: ${totalReduction}%`);
|
|
276
|
+
console.log("=".repeat(60));
|
|
277
|
+
console.log("š All conversions completed!");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Run conversion
|
|
281
|
+
convertAllFiles().catch((error) => {
|
|
282
|
+
console.error("ā Fatal error:", error.message);
|
|
283
|
+
console.error(error.stack);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { themes } from "./themes.js";
|
|
4
|
+
|
|
5
|
+
const OUTPUT_DIR = "css";
|
|
6
|
+
const OUTPUT_FILE = "figma-tokens.json";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts an OKLCH color string to a hex color string.
|
|
10
|
+
* Handles both percentage (oklch(10% 0 0)) and decimal (oklch(0.1 0 0)) lightness.
|
|
11
|
+
*/
|
|
12
|
+
function oklchToHex(oklchStr) {
|
|
13
|
+
const m = oklchStr.match(/oklch\(\s*([\d.]+)%?\s+([\d.e+-]+)\s+([\d.e+-]+)/i);
|
|
14
|
+
if (!m) return "#000000";
|
|
15
|
+
|
|
16
|
+
let L = parseFloat(m[1]);
|
|
17
|
+
const C = parseFloat(m[2]);
|
|
18
|
+
const H = parseFloat(m[3]);
|
|
19
|
+
|
|
20
|
+
// Normalize L to 0ā1 if given as percentage
|
|
21
|
+
if (L > 1) L = L / 100;
|
|
22
|
+
|
|
23
|
+
// OKLCH ā OKLab
|
|
24
|
+
const hRad = (H * Math.PI) / 180;
|
|
25
|
+
const a = C * Math.cos(hRad);
|
|
26
|
+
const b = C * Math.sin(hRad);
|
|
27
|
+
|
|
28
|
+
// OKLab ā Linear sRGB
|
|
29
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
30
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
31
|
+
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
32
|
+
|
|
33
|
+
const lc = l_ * l_ * l_;
|
|
34
|
+
const mc = m_ * m_ * m_;
|
|
35
|
+
const sc = s_ * s_ * s_;
|
|
36
|
+
|
|
37
|
+
let r = +4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
|
|
38
|
+
let g = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
|
|
39
|
+
let bv = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
40
|
+
|
|
41
|
+
// Linear sRGB ā sRGB (gamma correction + clamp)
|
|
42
|
+
const toSRGB = (c) => {
|
|
43
|
+
c = Math.max(0, Math.min(1, c));
|
|
44
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
r = toSRGB(r);
|
|
48
|
+
g = toSRGB(g);
|
|
49
|
+
bv = toSRGB(bv);
|
|
50
|
+
|
|
51
|
+
const toHex = (c) => Math.round(c * 255).toString(16).padStart(2, "0");
|
|
52
|
+
return `#${toHex(r)}${toHex(g)}${toHex(bv)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function exportTokens() {
|
|
56
|
+
console.log("šØ Starting Figma Design Tokens export...");
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
59
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tokens = {};
|
|
63
|
+
let themeCount = 0;
|
|
64
|
+
|
|
65
|
+
for (const [themeName, themeData] of Object.entries(themes)) {
|
|
66
|
+
tokens[themeName] = {};
|
|
67
|
+
themeCount++;
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(themeData)) {
|
|
70
|
+
// Skip non-color attributes like color-scheme
|
|
71
|
+
if (!key.startsWith("--clampography-")) continue;
|
|
72
|
+
|
|
73
|
+
const colorName = key.replace("--clampography-", "");
|
|
74
|
+
|
|
75
|
+
// W3C DTCG format: $value and $type (with $ prefix)
|
|
76
|
+
tokens[themeName][colorName] = {
|
|
77
|
+
$value: oklchToHex(value),
|
|
78
|
+
$type: "color",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const outputPath = resolve(OUTPUT_DIR, OUTPUT_FILE);
|
|
84
|
+
fs.writeFileSync(outputPath, JSON.stringify(tokens, null, 2), "utf-8");
|
|
85
|
+
|
|
86
|
+
console.log(`ā
Successfully exported ${themeCount} themes to ${OUTPUT_DIR}/${OUTPUT_FILE}!`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
exportTokens();
|