postcss-clampwind 0.0.1

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.
@@ -0,0 +1,431 @@
1
+ // src/clampwind.js
2
+ import postcss from "postcss";
3
+
4
+ // src/screens.js
5
+ var defaultScreens = {
6
+ sm: "40rem",
7
+ // 640px
8
+ md: "48rem",
9
+ // 768px
10
+ lg: "64rem",
11
+ // 1024px
12
+ xl: "80rem",
13
+ // 1280px
14
+ "2xl": "96rem"
15
+ // 1536px
16
+ };
17
+ var defaultContainerScreens = {
18
+ "@3xs": "16rem",
19
+ // 256px
20
+ "@2xs": "18rem",
21
+ // 288px
22
+ "@xs": "20rem",
23
+ // 320px
24
+ "@sm": "24rem",
25
+ // 384px
26
+ "@md": "28rem",
27
+ // 448px
28
+ "@lg": "32rem",
29
+ // 512px
30
+ "@xl": "36rem",
31
+ // 576px
32
+ "@2xl": "42rem",
33
+ // 672px
34
+ "@3xl": "48rem",
35
+ // 768px
36
+ "@4xl": "56rem",
37
+ // 896px
38
+ "@5xl": "64rem",
39
+ // 1024px
40
+ "@6xl": "72rem",
41
+ // 1152px
42
+ "@7xl": "80rem"
43
+ // 1280px
44
+ };
45
+ var convertSortScreens = (screens, rootFontSize = 16) => {
46
+ const convertedScreens = Object.entries(screens).reduce((acc, [key, value]) => {
47
+ if (value.includes("px")) {
48
+ const pxValue = parseFloat(value);
49
+ acc[key] = `${pxValue / rootFontSize}rem`;
50
+ } else {
51
+ acc[key] = value;
52
+ }
53
+ return acc;
54
+ }, {});
55
+ const sortedKeys = Object.keys(convertedScreens).sort((a, b) => {
56
+ const aValue = parseFloat(convertedScreens[a]);
57
+ const bValue = parseFloat(convertedScreens[b]);
58
+ return aValue - bValue;
59
+ });
60
+ return sortedKeys.reduce((acc, key) => {
61
+ acc[key] = convertedScreens[key];
62
+ return acc;
63
+ }, {});
64
+ };
65
+
66
+ // src/utils.js
67
+ var extractTwoValidClampArgs = (value) => {
68
+ const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/);
69
+ return m ? [m[1].trim(), m[2].trim()] : null;
70
+ };
71
+ var extractUnit = (value) => {
72
+ const trimmedValue = value.replace(/\s+/g, "");
73
+ if (trimmedValue.includes("--")) {
74
+ const match = trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1");
75
+ return match ? match : null;
76
+ } else {
77
+ const match = trimmedValue.match(/\D+$/);
78
+ return match ? match[0] : null;
79
+ }
80
+ };
81
+ var formatProperty = (value) => {
82
+ const trimmedValue = value.replace(/\s+/g, "");
83
+ return trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1");
84
+ };
85
+ var convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) => {
86
+ const unit = extractUnit(value);
87
+ const formattedProperty = formatProperty(value);
88
+ const fallbackValue = value.includes("var(") && value.includes(",") ? value.replace(/var\([^,]+,\s*([^)]+)\)/, "$1") : null;
89
+ if (!unit) {
90
+ const spacingSizeInt = parseFloat(spacingSize);
91
+ const spacingUnit = extractUnit(spacingSize);
92
+ if (spacingUnit === "px") {
93
+ return `${parseFloat(value * spacingSizeInt / rootFontSize).toFixed(4)}rem`;
94
+ }
95
+ if (spacingUnit === "rem") {
96
+ return `${parseFloat(value * spacingSizeInt).toFixed(4)}rem`;
97
+ }
98
+ }
99
+ if (unit === "px") {
100
+ return `${parseFloat(value.replace("px", "") / rootFontSize).toFixed(4)}rem`;
101
+ }
102
+ if (unit === "rem") {
103
+ return value;
104
+ }
105
+ if (customProperties[formattedProperty]) {
106
+ return `${customProperties[formattedProperty]}rem`;
107
+ }
108
+ if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) {
109
+ const fallbackUnit = extractUnit(fallbackValue);
110
+ if (!fallbackUnit) {
111
+ return `${parseFloat(fallbackValue * spacingSize).toFixed(4)}rem`;
112
+ }
113
+ if (fallbackUnit === "px") {
114
+ return `${parseFloat(fallbackValue.replace("px", "") / rootFontSize).toFixed(4)}rem`;
115
+ }
116
+ if (fallbackUnit === "rem") {
117
+ return fallbackValue;
118
+ }
119
+ }
120
+ return null;
121
+ };
122
+ var generateClamp = (lower, upper, minScreen, maxScreen, rootFontSize = 16, spacingSize = "1px", containerQuery = false) => {
123
+ const maxScreenInt = parseFloat(
124
+ convertToRem(maxScreen, rootFontSize, spacingSize)
125
+ );
126
+ const minScreenInt = parseFloat(
127
+ convertToRem(minScreen, rootFontSize, spacingSize)
128
+ );
129
+ const lowerInt = parseFloat(lower);
130
+ const upperInt = parseFloat(upper);
131
+ const isDescending = lowerInt > upperInt;
132
+ const min = isDescending ? upper : lower;
133
+ const max = isDescending ? lower : upper;
134
+ const widthUnit = containerQuery ? `100cqw` : `100vw`;
135
+ const slopeInt = parseFloat(((upperInt - lowerInt) / (maxScreenInt - minScreenInt)).toFixed(4));
136
+ const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`;
137
+ return clamp;
138
+ };
139
+
140
+ // src/clampwind.js
141
+ var clampwind = (opts = {}) => {
142
+ return {
143
+ postcssPlugin: "clampwind",
144
+ prepare() {
145
+ let rootFontSize = 16;
146
+ let spacingSize = "1px";
147
+ let customProperties = {};
148
+ let screens = defaultScreens || {};
149
+ let containerScreens = defaultContainerScreens || {};
150
+ const config = {
151
+ defaultLayerBreakpoints: {},
152
+ defaultLayerContainerBreakpoints: {},
153
+ themeLayerBreakpoints: {},
154
+ themeLayerContainerBreakpoints: {},
155
+ rootElementBreakpoints: {},
156
+ rootElementContainerBreakpoints: {},
157
+ configCollected: false,
158
+ configReady: false
159
+ };
160
+ const collectConfig = (root) => {
161
+ if (config.configCollected) return;
162
+ root.walkDecls((decl) => {
163
+ if (decl.parent?.selector === ":root") {
164
+ if (decl.prop === "font-size" && decl.value.includes("px")) {
165
+ rootFontSize = parseFloat(decl.value);
166
+ }
167
+ if (decl.prop === "--text-base" && decl.value.includes("px")) {
168
+ rootFontSize = parseFloat(decl.value);
169
+ }
170
+ }
171
+ });
172
+ root.walkDecls((decl) => {
173
+ if (decl.parent?.selector === ":root") {
174
+ if (decl.prop.startsWith("--breakpoint-")) {
175
+ const key = decl.prop.replace("--breakpoint-", "");
176
+ config.rootElementBreakpoints[key] = decl.value;
177
+ }
178
+ if (decl.prop.startsWith("--container-")) {
179
+ const key = decl.prop.replace("--container-", "@");
180
+ config.rootElementContainerBreakpoints[key] = decl.value;
181
+ }
182
+ if (decl.prop === "--spacing") {
183
+ spacingSize = decl.value;
184
+ }
185
+ if (decl.prop.startsWith("--")) {
186
+ const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
187
+ if (value) customProperties[decl.prop] = value;
188
+ }
189
+ }
190
+ });
191
+ root.walkAtRules("layer", (atRule) => {
192
+ if (atRule.params === "theme") {
193
+ atRule.walkDecls((decl) => {
194
+ if (decl.prop === "--text-base" && decl.value.includes("px")) {
195
+ rootFontSize = parseFloat(decl.value);
196
+ }
197
+ });
198
+ }
199
+ });
200
+ root.walkAtRules("layer", (atRule) => {
201
+ if (atRule.params === "default") {
202
+ if (!Object.keys(config.defaultLayerBreakpoints).length) {
203
+ atRule.walkDecls((decl) => {
204
+ if (decl.prop.startsWith("--breakpoint-")) {
205
+ const key = decl.prop.replace("--breakpoint-", "");
206
+ config.defaultLayerBreakpoints[key] = decl.value;
207
+ }
208
+ if (decl.prop.startsWith("--container-")) {
209
+ const key = decl.prop.replace("--container-", "@");
210
+ config.defaultLayerContainerBreakpoints[key] = decl.value;
211
+ }
212
+ });
213
+ }
214
+ }
215
+ if (atRule.params === "theme") {
216
+ atRule.walkDecls((decl) => {
217
+ if (decl.prop.startsWith("--breakpoint-")) {
218
+ const key = decl.prop.replace("--breakpoint-", "");
219
+ config.themeLayerBreakpoints[key] = decl.value;
220
+ }
221
+ if (decl.prop.startsWith("--container-")) {
222
+ const key = decl.prop.replace("--container-", "@");
223
+ config.themeLayerContainerBreakpoints[key] = decl.value;
224
+ }
225
+ if (decl.prop === "--spacing") {
226
+ spacingSize = decl.value;
227
+ }
228
+ if (decl.prop.startsWith("--")) {
229
+ const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
230
+ if (value) customProperties[decl.prop] = value;
231
+ }
232
+ });
233
+ }
234
+ });
235
+ config.configCollected = true;
236
+ };
237
+ const finalizeConfig = () => {
238
+ if (config.configReady) return;
239
+ screens = Object.assign(
240
+ {},
241
+ screens,
242
+ config.defaultLayerBreakpoints,
243
+ config.rootElementBreakpoints,
244
+ config.themeLayerBreakpoints
245
+ );
246
+ screens = convertSortScreens(screens, rootFontSize);
247
+ containerScreens = Object.assign(
248
+ {},
249
+ containerScreens,
250
+ config.defaultLayerContainerBreakpoints,
251
+ config.rootElementContainerBreakpoints,
252
+ config.themeLayerContainerBreakpoints
253
+ );
254
+ containerScreens = convertSortScreens(containerScreens, rootFontSize);
255
+ config.configReady = true;
256
+ };
257
+ const processClampDeclaration = (decl, minScreen, maxScreen, isContainer = false) => {
258
+ const args = extractTwoValidClampArgs(decl.value);
259
+ const [lower, upper] = args.map((val) => convertToRem(val, rootFontSize, spacingSize, customProperties));
260
+ if (!args || !lower || !upper) {
261
+ console.warn("Invalid clamp() values", { node: decl });
262
+ decl.value = ` ${decl.value} /* Invalid clamp() values */`;
263
+ return true;
264
+ }
265
+ const clamp = generateClamp(lower, upper, minScreen, maxScreen, rootFontSize, spacingSize, isContainer);
266
+ decl.value = clamp;
267
+ return true;
268
+ };
269
+ return {
270
+ // Use OnceExit to ensure Tailwind has generated its content
271
+ OnceExit(root, { result }) {
272
+ collectConfig(root);
273
+ finalizeConfig();
274
+ root.walkAtRules("media", (atRule) => {
275
+ const isNested = atRule.parent?.type === "atrule";
276
+ const isSameAtRule = atRule.parent?.name === atRule.name;
277
+ const clampDecls = [];
278
+ atRule.walkDecls((decl) => {
279
+ if (extractTwoValidClampArgs(decl.value)) {
280
+ clampDecls.push(decl);
281
+ }
282
+ });
283
+ if (!clampDecls.length) return;
284
+ if (isNested && isSameAtRule) {
285
+ const parentParams = atRule.parent.params;
286
+ const currentParams = atRule.params;
287
+ let minScreen = null;
288
+ let maxScreen = null;
289
+ if (parentParams.includes(">")) {
290
+ const match = parentParams.match(/>=?\s*([^)]+)/);
291
+ if (match) minScreen = match[1].trim();
292
+ }
293
+ if (currentParams.includes(">") && !minScreen) {
294
+ const match = currentParams.match(/>=?\s*([^)]+)/);
295
+ if (match) minScreen = match[1].trim();
296
+ }
297
+ if (parentParams.includes("<")) {
298
+ const match = parentParams.match(/<\s*([^)]+)/);
299
+ if (match) maxScreen = match[1].trim();
300
+ }
301
+ if (currentParams.includes("<") && !maxScreen) {
302
+ const match = currentParams.match(/<\s*([^)]+)/);
303
+ if (match) maxScreen = match[1].trim();
304
+ }
305
+ if (minScreen && maxScreen) {
306
+ clampDecls.forEach((decl) => {
307
+ processClampDeclaration(decl, minScreen, maxScreen, false);
308
+ });
309
+ }
310
+ return;
311
+ }
312
+ if (isNested && !isSameAtRule) {
313
+ clampDecls.forEach((decl) => {
314
+ decl.value = ` ${decl.value} /* Invalid nested @media rules */`;
315
+ });
316
+ return;
317
+ }
318
+ const screenValues = Object.values(screens);
319
+ clampDecls.forEach((decl) => {
320
+ if (atRule.params.includes(">")) {
321
+ const match = atRule.params.match(/>=?\s*([^)]+)/);
322
+ if (match) {
323
+ const minScreen = match[1].trim();
324
+ const maxScreen = screenValues[screenValues.length - 1];
325
+ processClampDeclaration(decl, minScreen, maxScreen, false);
326
+ }
327
+ } else if (atRule.params.includes("<")) {
328
+ const match = atRule.params.match(/<\s*([^)]+)/);
329
+ if (match) {
330
+ const minScreen = screenValues[0];
331
+ const maxScreen = match[1].trim();
332
+ processClampDeclaration(decl, minScreen, maxScreen, false);
333
+ }
334
+ }
335
+ });
336
+ });
337
+ root.walkAtRules("container", (atRule) => {
338
+ const isNested = atRule.parent?.type === "atrule";
339
+ const isSameAtRule = atRule.parent?.name === atRule.name;
340
+ const clampDecls = [];
341
+ atRule.walkDecls((decl) => {
342
+ if (extractTwoValidClampArgs(decl.value)) {
343
+ clampDecls.push(decl);
344
+ }
345
+ });
346
+ if (!clampDecls.length) return;
347
+ if (isNested && isSameAtRule) {
348
+ const parentParams = atRule.parent.params;
349
+ const currentParams = atRule.params;
350
+ let minContainer = null;
351
+ let maxContainer = null;
352
+ if (parentParams.includes(">")) {
353
+ const match = parentParams.match(/>=?\s*([^)]+)/);
354
+ if (match) minContainer = match[1].trim();
355
+ }
356
+ if (currentParams.includes(">") && !minContainer) {
357
+ const match = currentParams.match(/>=?\s*([^)]+)/);
358
+ if (match) minContainer = match[1].trim();
359
+ }
360
+ if (parentParams.includes("<")) {
361
+ const match = parentParams.match(/<\s*([^)]+)/);
362
+ if (match) maxContainer = match[1].trim();
363
+ }
364
+ if (currentParams.includes("<") && !maxContainer) {
365
+ const match = currentParams.match(/<\s*([^)]+)/);
366
+ if (match) maxContainer = match[1].trim();
367
+ }
368
+ if (minContainer && maxContainer) {
369
+ clampDecls.forEach((decl) => {
370
+ processClampDeclaration(decl, minContainer, maxContainer, true);
371
+ });
372
+ }
373
+ return;
374
+ }
375
+ if (isNested && !isSameAtRule) {
376
+ clampDecls.forEach((decl) => {
377
+ decl.value = ` ${decl.value} /* Invalid nested @container rules */`;
378
+ });
379
+ return;
380
+ }
381
+ const screenValues = Object.values(containerScreens);
382
+ clampDecls.forEach((decl) => {
383
+ if (atRule.params.includes(">")) {
384
+ const match = atRule.params.match(/>=?\s*([^)]+)/);
385
+ if (match) {
386
+ const minContainer = match[1].trim();
387
+ const maxContainer = screenValues[screenValues.length - 1];
388
+ processClampDeclaration(decl, minContainer, maxContainer, true);
389
+ }
390
+ } else if (atRule.params.includes("<")) {
391
+ const match = atRule.params.match(/<\s*([^)]+)/);
392
+ if (match) {
393
+ const minContainer = screenValues[0];
394
+ const maxContainer = match[1].trim();
395
+ processClampDeclaration(decl, minContainer, maxContainer, true);
396
+ }
397
+ }
398
+ });
399
+ });
400
+ root.walkRules((rule) => {
401
+ let parent = rule.parent;
402
+ while (parent) {
403
+ if (parent.type === "atrule" && (parent.name === "media" || parent.name === "container")) {
404
+ return;
405
+ }
406
+ parent = parent.parent;
407
+ }
408
+ const clampDecls = [];
409
+ rule.walkDecls((decl) => {
410
+ if (extractTwoValidClampArgs(decl.value)) {
411
+ clampDecls.push(decl);
412
+ }
413
+ });
414
+ if (clampDecls.length === 0) return;
415
+ const screenValues = Object.values(screens);
416
+ const minScreen = screenValues[0];
417
+ const maxScreen = screenValues[screenValues.length - 1];
418
+ clampDecls.forEach((decl) => {
419
+ processClampDeclaration(decl, minScreen, maxScreen, false);
420
+ });
421
+ });
422
+ }
423
+ };
424
+ }
425
+ };
426
+ };
427
+ clampwind.postcss = true;
428
+ var clampwind_default = clampwind;
429
+ export {
430
+ clampwind_default as default
431
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "postcss-clampwind",
3
+ "version": "0.0.1",
4
+ "description": "A PostCSS plugin to create fluid clamp values for any Tailwind CSS utility",
5
+ "license": "Apache-2.0",
6
+ "keywords": [
7
+ "clampwind",
8
+ "postcss-plugin",
9
+ "tailwindcss",
10
+ "clamp",
11
+ "fluid",
12
+ "variants"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/danieledep/postcss-clampwind.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/danieledep/postcss-clampwind/issues"
20
+ },
21
+ "type": "module",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "main": "./dist/clampwind.esm.js",
26
+ "exports": {
27
+ "require": "./dist/clampwind.cjs.cjs",
28
+ "import": "./dist/clampwind.esm.js",
29
+ "default": "./dist/clampwind.esm.js"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "devDependencies": {
35
+ "esbuild": "^0.25.6"
36
+ },
37
+ "peerDependencies": {
38
+ "postcss": "^8.4.39"
39
+ },
40
+ "scripts": {
41
+ "dev": "node scripts/build.js --watch",
42
+ "build": "node scripts/build.js"
43
+ }
44
+ }