clampography 2.0.0-beta.9 → 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.
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();
@@ -0,0 +1,39 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ import { light } from './themes.js';
6
+ import baseStyles from './base.js';
7
+ import extraStyles from './extra.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ // 1. Get color variables from themes
13
+ const colorVars = Object.keys(light).filter(k => k.startsWith('--'));
14
+
15
+ // 2. Get variables from base styles
16
+ const baseObj = baseStyles();
17
+ const rootBaseVars = Object.keys(baseObj[':where(:root)'] || {}).filter(k => k.startsWith('--'));
18
+
19
+ // 3. Get variables from extra styles
20
+ const extraObj = extraStyles();
21
+ const rootExtraVars = Object.keys(extraObj[':where(:root)'] || {}).filter(k => k.startsWith('--'));
22
+
23
+ // Merge and deduplicate
24
+ const allVars = [...new Set([...colorVars, ...rootBaseVars, ...rootExtraVars])].sort();
25
+
26
+ // Generate TypeScript content
27
+ const tsContent = `/**
28
+ * Auto-generated by Clampography
29
+ * TypeScript definitions for Clampography CSS Variables
30
+ */
31
+
32
+ export type ClampographyVars =
33
+ ${allVars.map(v => ` | "${v}"`).join('\n')};
34
+ `;
35
+
36
+ const outputPath = path.join(__dirname, 'types', 'vars.d.ts');
37
+ fs.writeFileSync(outputPath, tsContent, 'utf-8');
38
+
39
+ console.log(`✅ Exported ${allVars.length} CSS variable types to src/types/vars.d.ts`);