postcss-clampwind 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -28,6 +28,21 @@ Install the plugin from npm:
28
28
  ```sh
29
29
  npm install -D postcss-clampwind
30
30
  ```
31
+ ### Vite project setup
32
+
33
+ If you are using Vite, you are probably using Tailwind with `@tailwindcss/vite`. You need to import the plugin and use it in your `postcss.config.js` file.
34
+
35
+ ```js
36
+ // postcss.config.js
37
+ import clampwind from "postcss-clampwind";
38
+
39
+ export default {
40
+ plugins: [
41
+ clampwind()
42
+ ]
43
+ };
44
+ ```
45
+ **Demo on StackBlitz:** [postcss-clampwind-vite](https://stackblitz.com/edit/postcss-clampwind-vite?file=postcss.config.js)
31
46
 
32
47
  ### PostCSS setup
33
48
 
@@ -46,6 +61,8 @@ export default {
46
61
  }
47
62
  ```
48
63
 
64
+ **Demo on StackBlitz:** [postcss-clampwind-postcss](https://stackblitz.com/edit/postcss-clampwind-postcss?file=postcss.config.mjs)
65
+
49
66
  #### CommonJS usage
50
67
 
51
68
  If you are using CommonJS-based build tools like Webpack, you will need to use the `require` syntax and add `.default` to the import.
@@ -60,21 +77,6 @@ module.exports = {
60
77
  };
61
78
  ```
62
79
 
63
- ### Vite project setup
64
-
65
- If you are using Vite, you are probably using Tailwind with `@tailwindcss/vite`. You need to import the plugin and use it in your `postcss.config.js` file.
66
-
67
- ```js
68
- // postcss.config.js
69
- import clampwind from "postcss-clampwind";
70
-
71
- export default {
72
- plugins: [
73
- clampwind()
74
- ]
75
- };
76
- ```
77
-
78
80
  ## Features
79
81
 
80
82
  ### Interchangeable px / rem units
@@ -305,6 +307,33 @@ But Tailwind by default, will not output in your CSS any custom properties that
305
307
  }
306
308
  ```
307
309
 
310
+ ### Set a default clamp range
311
+
312
+ You can set a default clamp range to use when no breakpoint modifier is used, like this:
313
+
314
+ ```html
315
+ <div class="text-[clamp(16px,50px)]"></div>
316
+ ```
317
+
318
+ To set a default clamp range you need to use the `--breakpoint-clamp-min` and `--breakpoint-clamp-max` custom properties, defined inside the `@theme static` directive.
319
+
320
+ ```css
321
+ @theme static {
322
+ --breakpoint-clamp-min: 600px;
323
+ --breakpoint-clamp-max: 1200px;
324
+ }
325
+ ```
326
+
327
+ This will also apply for utilities that use only one breakpoint modifier. In this example the `md` breakpoint will be used as the minimum breakpoint, and `--breakpoint-clamp-max` will be used as the maximum breakpoint:
328
+
329
+ ```html
330
+ <div class="md:text-[clamp(16px,50px)]"></div>
331
+ ```
332
+
333
+ The default clamp range will let you to simplify your utilities, since usually you don't need to clamp between the smallest and largest Tailwind breakpoints, but only between two breakpoints.
334
+
335
+ You will still be able to clamp between any other Tailwind or custom breakpoints, even if out of the default clamp range.
336
+
308
337
  ### Use custom properties
309
338
 
310
339
  You can use any custom properties in your clamped values, for example:
@@ -342,4 +371,4 @@ or like this:
342
371
 
343
372
  This project is licensed under the [Apache-2.0 license](https://apache.org/licenses/LICENSE-2.0).
344
373
 
345
- Copyright © 2025 Daniele De Pietri.
374
+ Copyright © 2025 Daniele De Pietri.
@@ -64,28 +64,13 @@ var defaultContainerScreens = {
64
64
  "@7xl": "80rem"
65
65
  // 1280px
66
66
  };
67
- var convertSortScreens = (screens, rootFontSize = 16) => {
68
- const convertedScreens = Object.entries(screens).reduce((acc, [key, value]) => {
69
- if (value.includes("px")) {
70
- const pxValue = parseFloat(value);
71
- acc[key] = `${pxValue / rootFontSize}rem`;
72
- } else {
73
- acc[key] = value;
74
- }
75
- return acc;
76
- }, {});
77
- const sortedKeys = Object.keys(convertedScreens).sort((a, b) => {
78
- const aValue = parseFloat(convertedScreens[a]);
79
- const bValue = parseFloat(convertedScreens[b]);
80
- return aValue - bValue;
81
- });
82
- return sortedKeys.reduce((acc, key) => {
83
- acc[key] = convertedScreens[key];
84
- return acc;
85
- }, {});
86
- };
87
67
 
88
68
  // src/utils.js
69
+ var smartRound = (value, maxDecimals = 4) => {
70
+ const precise = value.toFixed(maxDecimals);
71
+ const trimmed = precise.replace(/\.?0+$/, "");
72
+ return trimmed || "0";
73
+ };
89
74
  var extractTwoValidClampArgs = (value) => {
90
75
  const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/);
91
76
  return m ? [m[1].trim(), m[2].trim()] : null;
@@ -112,28 +97,28 @@ var convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) =>
112
97
  const spacingSizeInt = parseFloat(spacingSize);
113
98
  const spacingUnit = extractUnit(spacingSize);
114
99
  if (spacingUnit === "px") {
115
- return `${parseFloat(value * spacingSizeInt / rootFontSize).toFixed(4)}rem`;
100
+ return `${smartRound(value * spacingSizeInt / rootFontSize)}rem`;
116
101
  }
117
102
  if (spacingUnit === "rem") {
118
- return `${parseFloat(value * spacingSizeInt).toFixed(4)}rem`;
103
+ return `${smartRound(value * spacingSizeInt)}rem`;
119
104
  }
120
105
  }
121
106
  if (unit === "px") {
122
- return `${parseFloat(value.replace("px", "") / rootFontSize).toFixed(4)}rem`;
107
+ return `${smartRound(value.replace("px", "") / rootFontSize)}rem`;
123
108
  }
124
109
  if (unit === "rem") {
125
110
  return value;
126
111
  }
127
112
  if (customProperties[formattedProperty]) {
128
- return `${customProperties[formattedProperty]}rem`;
113
+ return customProperties[formattedProperty];
129
114
  }
130
115
  if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) {
131
116
  const fallbackUnit = extractUnit(fallbackValue);
132
117
  if (!fallbackUnit) {
133
- return `${parseFloat(fallbackValue * spacingSize).toFixed(4)}rem`;
118
+ return `${smartRound(fallbackValue * spacingSize)}rem`;
134
119
  }
135
120
  if (fallbackUnit === "px") {
136
- return `${parseFloat(fallbackValue.replace("px", "") / rootFontSize).toFixed(4)}rem`;
121
+ return `${smartRound(fallbackValue.replace("px", "") / rootFontSize)}rem`;
137
122
  }
138
123
  if (fallbackUnit === "rem") {
139
124
  return fallbackValue;
@@ -154,10 +139,21 @@ var generateClamp = (lower, upper, minScreen, maxScreen, rootFontSize = 16, spac
154
139
  const min = isDescending ? upper : lower;
155
140
  const max = isDescending ? lower : upper;
156
141
  const widthUnit = containerQuery ? `100cqw` : `100vw`;
157
- const slopeInt = parseFloat(((upperInt - lowerInt) / (maxScreenInt - minScreenInt)).toFixed(4));
142
+ const slopeInt = smartRound((upperInt - lowerInt) / (maxScreenInt - minScreenInt));
158
143
  const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`;
159
144
  return clamp;
160
145
  };
146
+ var sortScreens = (screens) => {
147
+ const sortedKeys = Object.keys(screens).sort((a, b) => {
148
+ const aValue = parseFloat(screens[a]);
149
+ const bValue = parseFloat(screens[b]);
150
+ return aValue - bValue;
151
+ });
152
+ return sortedKeys.reduce((acc, key) => {
153
+ acc[key] = screens[key];
154
+ return acc;
155
+ }, {});
156
+ };
161
157
 
162
158
  // src/clampwind.js
163
159
  var clampwind = (opts = {}) => {
@@ -169,9 +165,8 @@ var clampwind = (opts = {}) => {
169
165
  let customProperties = {};
170
166
  let screens = defaultScreens || {};
171
167
  let containerScreens = defaultContainerScreens || {};
168
+ let defaultClampRange = {};
172
169
  const config = {
173
- defaultLayerBreakpoints: {},
174
- defaultLayerContainerBreakpoints: {},
175
170
  themeLayerBreakpoints: {},
176
171
  themeLayerContainerBreakpoints: {},
177
172
  rootElementBreakpoints: {},
@@ -195,17 +190,48 @@ var clampwind = (opts = {}) => {
195
190
  if (decl.parent?.selector === ":root") {
196
191
  if (decl.prop.startsWith("--breakpoint-")) {
197
192
  const key = decl.prop.replace("--breakpoint-", "");
198
- config.rootElementBreakpoints[key] = decl.value;
193
+ config.rootElementBreakpoints[key] = convertToRem(
194
+ decl.value,
195
+ rootFontSize,
196
+ spacingSize,
197
+ customProperties
198
+ );
199
199
  }
200
200
  if (decl.prop.startsWith("--container-")) {
201
201
  const key = decl.prop.replace("--container-", "@");
202
- config.rootElementContainerBreakpoints[key] = decl.value;
202
+ config.rootElementContainerBreakpoints[key] = convertToRem(
203
+ decl.value,
204
+ rootFontSize,
205
+ spacingSize,
206
+ customProperties
207
+ );
208
+ }
209
+ if (decl.prop === "--breakpoint-clamp-min") {
210
+ defaultClampRange.min = convertToRem(
211
+ decl.value,
212
+ rootFontSize,
213
+ spacingSize,
214
+ customProperties
215
+ );
216
+ }
217
+ if (decl.prop === "--breakpoint-clamp-max") {
218
+ defaultClampRange.max = convertToRem(
219
+ decl.value,
220
+ rootFontSize,
221
+ spacingSize,
222
+ customProperties
223
+ );
203
224
  }
204
225
  if (decl.prop === "--spacing") {
205
226
  spacingSize = decl.value;
206
227
  }
207
228
  if (decl.prop.startsWith("--")) {
208
- const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
229
+ const value = convertToRem(
230
+ decl.value,
231
+ rootFontSize,
232
+ spacingSize,
233
+ customProperties
234
+ );
209
235
  if (value) customProperties[decl.prop] = value;
210
236
  }
211
237
  }
@@ -220,35 +246,52 @@ var clampwind = (opts = {}) => {
220
246
  }
221
247
  });
222
248
  root.walkAtRules("layer", (atRule) => {
223
- if (atRule.params === "default") {
224
- if (!Object.keys(config.defaultLayerBreakpoints).length) {
225
- atRule.walkDecls((decl) => {
226
- if (decl.prop.startsWith("--breakpoint-")) {
227
- const key = decl.prop.replace("--breakpoint-", "");
228
- config.defaultLayerBreakpoints[key] = decl.value;
229
- }
230
- if (decl.prop.startsWith("--container-")) {
231
- const key = decl.prop.replace("--container-", "@");
232
- config.defaultLayerContainerBreakpoints[key] = decl.value;
233
- }
234
- });
235
- }
236
- }
237
249
  if (atRule.params === "theme") {
238
250
  atRule.walkDecls((decl) => {
239
251
  if (decl.prop.startsWith("--breakpoint-")) {
240
252
  const key = decl.prop.replace("--breakpoint-", "");
241
- config.themeLayerBreakpoints[key] = decl.value;
253
+ config.themeLayerBreakpoints[key] = convertToRem(
254
+ decl.value,
255
+ rootFontSize,
256
+ spacingSize,
257
+ customProperties
258
+ );
242
259
  }
243
260
  if (decl.prop.startsWith("--container-")) {
244
261
  const key = decl.prop.replace("--container-", "@");
245
- config.themeLayerContainerBreakpoints[key] = decl.value;
262
+ config.themeLayerContainerBreakpoints[key] = convertToRem(
263
+ decl.value,
264
+ rootFontSize,
265
+ spacingSize,
266
+ customProperties
267
+ );
268
+ }
269
+ if (decl.prop === "--breakpoint-clamp-min") {
270
+ defaultClampRange.min = convertToRem(
271
+ decl.value,
272
+ rootFontSize,
273
+ spacingSize,
274
+ customProperties
275
+ );
276
+ }
277
+ if (decl.prop === "--breakpoint-clamp-max") {
278
+ defaultClampRange.max = convertToRem(
279
+ decl.value,
280
+ rootFontSize,
281
+ spacingSize,
282
+ customProperties
283
+ );
246
284
  }
247
285
  if (decl.prop === "--spacing") {
248
286
  spacingSize = decl.value;
249
287
  }
250
288
  if (decl.prop.startsWith("--")) {
251
- const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
289
+ const value = convertToRem(
290
+ decl.value,
291
+ rootFontSize,
292
+ spacingSize,
293
+ customProperties
294
+ );
252
295
  if (value) customProperties[decl.prop] = value;
253
296
  }
254
297
  });
@@ -261,30 +304,38 @@ var clampwind = (opts = {}) => {
261
304
  screens = Object.assign(
262
305
  {},
263
306
  screens,
264
- config.defaultLayerBreakpoints,
265
307
  config.rootElementBreakpoints,
266
308
  config.themeLayerBreakpoints
267
309
  );
268
- screens = convertSortScreens(screens, rootFontSize);
310
+ screens = sortScreens(screens);
269
311
  containerScreens = Object.assign(
270
312
  {},
271
313
  containerScreens,
272
- config.defaultLayerContainerBreakpoints,
273
314
  config.rootElementContainerBreakpoints,
274
315
  config.themeLayerContainerBreakpoints
275
316
  );
276
- containerScreens = convertSortScreens(containerScreens, rootFontSize);
317
+ containerScreens = sortScreens(containerScreens);
277
318
  config.configReady = true;
278
319
  };
279
320
  const processClampDeclaration = (decl, minScreen, maxScreen, isContainer = false) => {
280
321
  const args = extractTwoValidClampArgs(decl.value);
281
- const [lower, upper] = args.map((val) => convertToRem(val, rootFontSize, spacingSize, customProperties));
322
+ const [lower, upper] = args.map(
323
+ (val) => convertToRem(val, rootFontSize, spacingSize, customProperties)
324
+ );
282
325
  if (!args || !lower || !upper) {
283
326
  console.warn("Invalid clamp() values", { node: decl });
284
327
  decl.value = ` ${decl.value} /* Invalid clamp() values */`;
285
328
  return true;
286
329
  }
287
- const clamp = generateClamp(lower, upper, minScreen, maxScreen, rootFontSize, spacingSize, isContainer);
330
+ const clamp = generateClamp(
331
+ lower,
332
+ upper,
333
+ minScreen,
334
+ maxScreen,
335
+ rootFontSize,
336
+ spacingSize,
337
+ isContainer
338
+ );
288
339
  decl.value = clamp;
289
340
  return true;
290
341
  };
@@ -294,8 +345,6 @@ var clampwind = (opts = {}) => {
294
345
  collectConfig(root);
295
346
  finalizeConfig();
296
347
  root.walkAtRules("media", (atRule) => {
297
- const isNested = atRule.parent?.type === "atrule";
298
- const isSameAtRule = atRule.parent?.name === atRule.name;
299
348
  const clampDecls = [];
300
349
  atRule.walkDecls((decl) => {
301
350
  if (extractTwoValidClampArgs(decl.value)) {
@@ -303,53 +352,56 @@ var clampwind = (opts = {}) => {
303
352
  }
304
353
  });
305
354
  if (!clampDecls.length) return;
306
- if (isNested && isSameAtRule) {
307
- const parentParams = atRule.parent.params;
308
- const currentParams = atRule.params;
309
- let minScreen = null;
310
- let maxScreen = null;
311
- if (parentParams.includes(">")) {
312
- const match = parentParams.match(/>=?\s*([^)]+)/);
313
- if (match) minScreen = match[1].trim();
314
- }
315
- if (currentParams.includes(">") && !minScreen) {
316
- const match = currentParams.match(/>=?\s*([^)]+)/);
317
- if (match) minScreen = match[1].trim();
318
- }
319
- if (parentParams.includes("<")) {
320
- const match = parentParams.match(/<\s*([^)]+)/);
321
- if (match) maxScreen = match[1].trim();
322
- }
323
- if (currentParams.includes("<") && !maxScreen) {
324
- const match = currentParams.match(/<\s*([^)]+)/);
325
- if (match) maxScreen = match[1].trim();
355
+ clampDecls.forEach((decl) => {
356
+ const isNested = decl.parent?.type === "atrule" && decl.parent?.parent.type === "atrule";
357
+ const isSameAtRule = decl.parent?.name === decl.parent?.parent.name;
358
+ if (isNested && isSameAtRule) {
359
+ const currentParams2 = decl.parent.params;
360
+ const parentParams = decl.parent.parent.params;
361
+ let minScreen = null;
362
+ let maxScreen = null;
363
+ if (parentParams.includes(">")) {
364
+ const match = parentParams.match(/>=?\s*([^)]+)/);
365
+ if (match) minScreen = match[1].trim();
366
+ }
367
+ if (currentParams2.includes(">") && !minScreen) {
368
+ const match = currentParams2.match(/>=?\s*([^)]+)/);
369
+ if (match) minScreen = match[1].trim();
370
+ }
371
+ if (parentParams.includes("<")) {
372
+ const match = parentParams.match(/<\s*([^)]+)/);
373
+ if (match) maxScreen = match[1].trim();
374
+ }
375
+ if (currentParams2.includes("<") && !maxScreen) {
376
+ const match = currentParams2.match(/<\s*([^)]+)/);
377
+ if (match) maxScreen = match[1].trim();
378
+ }
379
+ if (minScreen && maxScreen) {
380
+ clampDecls.forEach((decl2) => {
381
+ processClampDeclaration(decl2, minScreen, maxScreen, false);
382
+ });
383
+ }
384
+ return;
326
385
  }
327
- if (minScreen && maxScreen) {
328
- clampDecls.forEach((decl) => {
329
- processClampDeclaration(decl, minScreen, maxScreen, false);
386
+ if (isNested && !isSameAtRule) {
387
+ clampDecls.forEach((decl2) => {
388
+ decl2.value = ` ${decl2.value} /* Invalid nested @media rules */`;
330
389
  });
390
+ return;
331
391
  }
332
- return;
333
- }
334
- if (isNested && !isSameAtRule) {
335
- clampDecls.forEach((decl) => {
336
- decl.value = ` ${decl.value} /* Invalid nested @media rules */`;
337
- });
338
- return;
339
- }
340
- const screenValues = Object.values(screens);
341
- clampDecls.forEach((decl) => {
342
- if (atRule.params.includes(">")) {
343
- const match = atRule.params.match(/>=?\s*([^)]+)/);
392
+ const screenValues = Object.values(screens);
393
+ const currentParams = decl.parent.params;
394
+ if (currentParams.includes(">")) {
395
+ const match = currentParams.match(/>=?\s*([^)]+)/);
344
396
  if (match) {
345
397
  const minScreen = match[1].trim();
346
- const maxScreen = screenValues[screenValues.length - 1];
398
+ const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1];
347
399
  processClampDeclaration(decl, minScreen, maxScreen, false);
348
400
  }
349
- } else if (atRule.params.includes("<")) {
350
- const match = atRule.params.match(/<\s*([^)]+)/);
401
+ } else if (currentParams.includes("<")) {
402
+ const match = currentParams.match(/<\s*([^)]+)/);
351
403
  if (match) {
352
- const minScreen = screenValues[0];
404
+ const minScreen = defaultClampRange.min || screenValues[0];
353
405
  const maxScreen = match[1].trim();
354
406
  processClampDeclaration(decl, minScreen, maxScreen, false);
355
407
  }
@@ -357,8 +409,6 @@ var clampwind = (opts = {}) => {
357
409
  });
358
410
  });
359
411
  root.walkAtRules("container", (atRule) => {
360
- const isNested = atRule.parent?.type === "atrule";
361
- const isSameAtRule = atRule.parent?.name === atRule.name;
362
412
  const clampDecls = [];
363
413
  atRule.walkDecls((decl) => {
364
414
  if (extractTwoValidClampArgs(decl.value)) {
@@ -366,55 +416,73 @@ var clampwind = (opts = {}) => {
366
416
  }
367
417
  });
368
418
  if (!clampDecls.length) return;
369
- if (isNested && isSameAtRule) {
370
- const parentParams = atRule.parent.params;
371
- const currentParams = atRule.params;
372
- let minContainer = null;
373
- let maxContainer = null;
374
- if (parentParams.includes(">")) {
375
- const match = parentParams.match(/>=?\s*([^)]+)/);
376
- if (match) minContainer = match[1].trim();
377
- }
378
- if (currentParams.includes(">") && !minContainer) {
379
- const match = currentParams.match(/>=?\s*([^)]+)/);
380
- if (match) minContainer = match[1].trim();
381
- }
382
- if (parentParams.includes("<")) {
383
- const match = parentParams.match(/<\s*([^)]+)/);
384
- if (match) maxContainer = match[1].trim();
385
- }
386
- if (currentParams.includes("<") && !maxContainer) {
387
- const match = currentParams.match(/<\s*([^)]+)/);
388
- if (match) maxContainer = match[1].trim();
419
+ clampDecls.forEach((decl) => {
420
+ const isNested = decl.parent?.type === "atrule" && decl.parent?.parent.type === "atrule";
421
+ const isSameAtRule = decl.parent?.name === decl.parent?.parent.name;
422
+ if (isNested && isSameAtRule) {
423
+ const currentParams2 = decl.parent.params;
424
+ const parentParams = decl.parent.parent.params;
425
+ let minContainer = null;
426
+ let maxContainer = null;
427
+ if (parentParams.includes(">")) {
428
+ const match = parentParams.match(/>=?\s*([^)]+)/);
429
+ if (match) minContainer = match[1].trim();
430
+ }
431
+ if (currentParams2.includes(">") && !minContainer) {
432
+ const match = currentParams2.match(/>=?\s*([^)]+)/);
433
+ if (match) minContainer = match[1].trim();
434
+ }
435
+ if (parentParams.includes("<")) {
436
+ const match = parentParams.match(/<\s*([^)]+)/);
437
+ if (match) maxContainer = match[1].trim();
438
+ }
439
+ if (currentParams2.includes("<") && !maxContainer) {
440
+ const match = currentParams2.match(/<\s*([^)]+)/);
441
+ if (match) maxContainer = match[1].trim();
442
+ }
443
+ if (minContainer && maxContainer) {
444
+ clampDecls.forEach((decl2) => {
445
+ processClampDeclaration(
446
+ decl2,
447
+ minContainer,
448
+ maxContainer,
449
+ true
450
+ );
451
+ });
452
+ }
453
+ return;
389
454
  }
390
- if (minContainer && maxContainer) {
391
- clampDecls.forEach((decl) => {
392
- processClampDeclaration(decl, minContainer, maxContainer, true);
455
+ if (isNested && !isSameAtRule) {
456
+ clampDecls.forEach((decl2) => {
457
+ decl2.value = ` ${decl2.value} /* Invalid nested @container rules */`;
393
458
  });
459
+ return;
394
460
  }
395
- return;
396
- }
397
- if (isNested && !isSameAtRule) {
398
- clampDecls.forEach((decl) => {
399
- decl.value = ` ${decl.value} /* Invalid nested @container rules */`;
400
- });
401
- return;
402
- }
403
- const screenValues = Object.values(containerScreens);
404
- clampDecls.forEach((decl) => {
405
- if (atRule.params.includes(">")) {
406
- const match = atRule.params.match(/>=?\s*([^)]+)/);
461
+ const containerValues = Object.values(containerScreens);
462
+ const currentParams = decl.parent.params;
463
+ if (currentParams.includes(">")) {
464
+ const match = currentParams.match(/>=?\s*([^)]+)/);
407
465
  if (match) {
408
466
  const minContainer = match[1].trim();
409
- const maxContainer = screenValues[screenValues.length - 1];
410
- processClampDeclaration(decl, minContainer, maxContainer, true);
467
+ const maxContainer = containerValues[containerValues.length - 1];
468
+ processClampDeclaration(
469
+ decl,
470
+ minContainer,
471
+ maxContainer,
472
+ true
473
+ );
411
474
  }
412
- } else if (atRule.params.includes("<")) {
413
- const match = atRule.params.match(/<\s*([^)]+)/);
475
+ } else if (currentParams.includes("<")) {
476
+ const match = currentParams.match(/<\s*([^)]+)/);
414
477
  if (match) {
415
- const minContainer = screenValues[0];
478
+ const minContainer = containerValues[0];
416
479
  const maxContainer = match[1].trim();
417
- processClampDeclaration(decl, minContainer, maxContainer, true);
480
+ processClampDeclaration(
481
+ decl,
482
+ minContainer,
483
+ maxContainer,
484
+ true
485
+ );
418
486
  }
419
487
  }
420
488
  });
@@ -435,8 +503,8 @@ var clampwind = (opts = {}) => {
435
503
  });
436
504
  if (clampDecls.length === 0) return;
437
505
  const screenValues = Object.values(screens);
438
- const minScreen = screenValues[0];
439
- const maxScreen = screenValues[screenValues.length - 1];
506
+ const minScreen = defaultClampRange.min || screenValues[0];
507
+ const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1];
440
508
  clampDecls.forEach((decl) => {
441
509
  processClampDeclaration(decl, minScreen, maxScreen, false);
442
510
  });
@@ -39,28 +39,13 @@ var defaultContainerScreens = {
39
39
  "@7xl": "80rem"
40
40
  // 1280px
41
41
  };
42
- var convertSortScreens = (screens, rootFontSize = 16) => {
43
- const convertedScreens = Object.entries(screens).reduce((acc, [key, value]) => {
44
- if (value.includes("px")) {
45
- const pxValue = parseFloat(value);
46
- acc[key] = `${pxValue / rootFontSize}rem`;
47
- } else {
48
- acc[key] = value;
49
- }
50
- return acc;
51
- }, {});
52
- const sortedKeys = Object.keys(convertedScreens).sort((a, b) => {
53
- const aValue = parseFloat(convertedScreens[a]);
54
- const bValue = parseFloat(convertedScreens[b]);
55
- return aValue - bValue;
56
- });
57
- return sortedKeys.reduce((acc, key) => {
58
- acc[key] = convertedScreens[key];
59
- return acc;
60
- }, {});
61
- };
62
42
 
63
43
  // src/utils.js
44
+ var smartRound = (value, maxDecimals = 4) => {
45
+ const precise = value.toFixed(maxDecimals);
46
+ const trimmed = precise.replace(/\.?0+$/, "");
47
+ return trimmed || "0";
48
+ };
64
49
  var extractTwoValidClampArgs = (value) => {
65
50
  const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/);
66
51
  return m ? [m[1].trim(), m[2].trim()] : null;
@@ -87,28 +72,28 @@ var convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) =>
87
72
  const spacingSizeInt = parseFloat(spacingSize);
88
73
  const spacingUnit = extractUnit(spacingSize);
89
74
  if (spacingUnit === "px") {
90
- return `${parseFloat(value * spacingSizeInt / rootFontSize).toFixed(4)}rem`;
75
+ return `${smartRound(value * spacingSizeInt / rootFontSize)}rem`;
91
76
  }
92
77
  if (spacingUnit === "rem") {
93
- return `${parseFloat(value * spacingSizeInt).toFixed(4)}rem`;
78
+ return `${smartRound(value * spacingSizeInt)}rem`;
94
79
  }
95
80
  }
96
81
  if (unit === "px") {
97
- return `${parseFloat(value.replace("px", "") / rootFontSize).toFixed(4)}rem`;
82
+ return `${smartRound(value.replace("px", "") / rootFontSize)}rem`;
98
83
  }
99
84
  if (unit === "rem") {
100
85
  return value;
101
86
  }
102
87
  if (customProperties[formattedProperty]) {
103
- return `${customProperties[formattedProperty]}rem`;
88
+ return customProperties[formattedProperty];
104
89
  }
105
90
  if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) {
106
91
  const fallbackUnit = extractUnit(fallbackValue);
107
92
  if (!fallbackUnit) {
108
- return `${parseFloat(fallbackValue * spacingSize).toFixed(4)}rem`;
93
+ return `${smartRound(fallbackValue * spacingSize)}rem`;
109
94
  }
110
95
  if (fallbackUnit === "px") {
111
- return `${parseFloat(fallbackValue.replace("px", "") / rootFontSize).toFixed(4)}rem`;
96
+ return `${smartRound(fallbackValue.replace("px", "") / rootFontSize)}rem`;
112
97
  }
113
98
  if (fallbackUnit === "rem") {
114
99
  return fallbackValue;
@@ -129,10 +114,21 @@ var generateClamp = (lower, upper, minScreen, maxScreen, rootFontSize = 16, spac
129
114
  const min = isDescending ? upper : lower;
130
115
  const max = isDescending ? lower : upper;
131
116
  const widthUnit = containerQuery ? `100cqw` : `100vw`;
132
- const slopeInt = parseFloat(((upperInt - lowerInt) / (maxScreenInt - minScreenInt)).toFixed(4));
117
+ const slopeInt = smartRound((upperInt - lowerInt) / (maxScreenInt - minScreenInt));
133
118
  const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`;
134
119
  return clamp;
135
120
  };
121
+ var sortScreens = (screens) => {
122
+ const sortedKeys = Object.keys(screens).sort((a, b) => {
123
+ const aValue = parseFloat(screens[a]);
124
+ const bValue = parseFloat(screens[b]);
125
+ return aValue - bValue;
126
+ });
127
+ return sortedKeys.reduce((acc, key) => {
128
+ acc[key] = screens[key];
129
+ return acc;
130
+ }, {});
131
+ };
136
132
 
137
133
  // src/clampwind.js
138
134
  var clampwind = (opts = {}) => {
@@ -144,9 +140,8 @@ var clampwind = (opts = {}) => {
144
140
  let customProperties = {};
145
141
  let screens = defaultScreens || {};
146
142
  let containerScreens = defaultContainerScreens || {};
143
+ let defaultClampRange = {};
147
144
  const config = {
148
- defaultLayerBreakpoints: {},
149
- defaultLayerContainerBreakpoints: {},
150
145
  themeLayerBreakpoints: {},
151
146
  themeLayerContainerBreakpoints: {},
152
147
  rootElementBreakpoints: {},
@@ -170,17 +165,48 @@ var clampwind = (opts = {}) => {
170
165
  if (decl.parent?.selector === ":root") {
171
166
  if (decl.prop.startsWith("--breakpoint-")) {
172
167
  const key = decl.prop.replace("--breakpoint-", "");
173
- config.rootElementBreakpoints[key] = decl.value;
168
+ config.rootElementBreakpoints[key] = convertToRem(
169
+ decl.value,
170
+ rootFontSize,
171
+ spacingSize,
172
+ customProperties
173
+ );
174
174
  }
175
175
  if (decl.prop.startsWith("--container-")) {
176
176
  const key = decl.prop.replace("--container-", "@");
177
- config.rootElementContainerBreakpoints[key] = decl.value;
177
+ config.rootElementContainerBreakpoints[key] = convertToRem(
178
+ decl.value,
179
+ rootFontSize,
180
+ spacingSize,
181
+ customProperties
182
+ );
183
+ }
184
+ if (decl.prop === "--breakpoint-clamp-min") {
185
+ defaultClampRange.min = convertToRem(
186
+ decl.value,
187
+ rootFontSize,
188
+ spacingSize,
189
+ customProperties
190
+ );
191
+ }
192
+ if (decl.prop === "--breakpoint-clamp-max") {
193
+ defaultClampRange.max = convertToRem(
194
+ decl.value,
195
+ rootFontSize,
196
+ spacingSize,
197
+ customProperties
198
+ );
178
199
  }
179
200
  if (decl.prop === "--spacing") {
180
201
  spacingSize = decl.value;
181
202
  }
182
203
  if (decl.prop.startsWith("--")) {
183
- const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
204
+ const value = convertToRem(
205
+ decl.value,
206
+ rootFontSize,
207
+ spacingSize,
208
+ customProperties
209
+ );
184
210
  if (value) customProperties[decl.prop] = value;
185
211
  }
186
212
  }
@@ -195,35 +221,52 @@ var clampwind = (opts = {}) => {
195
221
  }
196
222
  });
197
223
  root.walkAtRules("layer", (atRule) => {
198
- if (atRule.params === "default") {
199
- if (!Object.keys(config.defaultLayerBreakpoints).length) {
200
- atRule.walkDecls((decl) => {
201
- if (decl.prop.startsWith("--breakpoint-")) {
202
- const key = decl.prop.replace("--breakpoint-", "");
203
- config.defaultLayerBreakpoints[key] = decl.value;
204
- }
205
- if (decl.prop.startsWith("--container-")) {
206
- const key = decl.prop.replace("--container-", "@");
207
- config.defaultLayerContainerBreakpoints[key] = decl.value;
208
- }
209
- });
210
- }
211
- }
212
224
  if (atRule.params === "theme") {
213
225
  atRule.walkDecls((decl) => {
214
226
  if (decl.prop.startsWith("--breakpoint-")) {
215
227
  const key = decl.prop.replace("--breakpoint-", "");
216
- config.themeLayerBreakpoints[key] = decl.value;
228
+ config.themeLayerBreakpoints[key] = convertToRem(
229
+ decl.value,
230
+ rootFontSize,
231
+ spacingSize,
232
+ customProperties
233
+ );
217
234
  }
218
235
  if (decl.prop.startsWith("--container-")) {
219
236
  const key = decl.prop.replace("--container-", "@");
220
- config.themeLayerContainerBreakpoints[key] = decl.value;
237
+ config.themeLayerContainerBreakpoints[key] = convertToRem(
238
+ decl.value,
239
+ rootFontSize,
240
+ spacingSize,
241
+ customProperties
242
+ );
243
+ }
244
+ if (decl.prop === "--breakpoint-clamp-min") {
245
+ defaultClampRange.min = convertToRem(
246
+ decl.value,
247
+ rootFontSize,
248
+ spacingSize,
249
+ customProperties
250
+ );
251
+ }
252
+ if (decl.prop === "--breakpoint-clamp-max") {
253
+ defaultClampRange.max = convertToRem(
254
+ decl.value,
255
+ rootFontSize,
256
+ spacingSize,
257
+ customProperties
258
+ );
221
259
  }
222
260
  if (decl.prop === "--spacing") {
223
261
  spacingSize = decl.value;
224
262
  }
225
263
  if (decl.prop.startsWith("--")) {
226
- const value = parseFloat(convertToRem(decl.value, rootFontSize, spacingSize, customProperties));
264
+ const value = convertToRem(
265
+ decl.value,
266
+ rootFontSize,
267
+ spacingSize,
268
+ customProperties
269
+ );
227
270
  if (value) customProperties[decl.prop] = value;
228
271
  }
229
272
  });
@@ -236,30 +279,38 @@ var clampwind = (opts = {}) => {
236
279
  screens = Object.assign(
237
280
  {},
238
281
  screens,
239
- config.defaultLayerBreakpoints,
240
282
  config.rootElementBreakpoints,
241
283
  config.themeLayerBreakpoints
242
284
  );
243
- screens = convertSortScreens(screens, rootFontSize);
285
+ screens = sortScreens(screens);
244
286
  containerScreens = Object.assign(
245
287
  {},
246
288
  containerScreens,
247
- config.defaultLayerContainerBreakpoints,
248
289
  config.rootElementContainerBreakpoints,
249
290
  config.themeLayerContainerBreakpoints
250
291
  );
251
- containerScreens = convertSortScreens(containerScreens, rootFontSize);
292
+ containerScreens = sortScreens(containerScreens);
252
293
  config.configReady = true;
253
294
  };
254
295
  const processClampDeclaration = (decl, minScreen, maxScreen, isContainer = false) => {
255
296
  const args = extractTwoValidClampArgs(decl.value);
256
- const [lower, upper] = args.map((val) => convertToRem(val, rootFontSize, spacingSize, customProperties));
297
+ const [lower, upper] = args.map(
298
+ (val) => convertToRem(val, rootFontSize, spacingSize, customProperties)
299
+ );
257
300
  if (!args || !lower || !upper) {
258
301
  console.warn("Invalid clamp() values", { node: decl });
259
302
  decl.value = ` ${decl.value} /* Invalid clamp() values */`;
260
303
  return true;
261
304
  }
262
- const clamp = generateClamp(lower, upper, minScreen, maxScreen, rootFontSize, spacingSize, isContainer);
305
+ const clamp = generateClamp(
306
+ lower,
307
+ upper,
308
+ minScreen,
309
+ maxScreen,
310
+ rootFontSize,
311
+ spacingSize,
312
+ isContainer
313
+ );
263
314
  decl.value = clamp;
264
315
  return true;
265
316
  };
@@ -269,8 +320,6 @@ var clampwind = (opts = {}) => {
269
320
  collectConfig(root);
270
321
  finalizeConfig();
271
322
  root.walkAtRules("media", (atRule) => {
272
- const isNested = atRule.parent?.type === "atrule";
273
- const isSameAtRule = atRule.parent?.name === atRule.name;
274
323
  const clampDecls = [];
275
324
  atRule.walkDecls((decl) => {
276
325
  if (extractTwoValidClampArgs(decl.value)) {
@@ -278,53 +327,56 @@ var clampwind = (opts = {}) => {
278
327
  }
279
328
  });
280
329
  if (!clampDecls.length) return;
281
- if (isNested && isSameAtRule) {
282
- const parentParams = atRule.parent.params;
283
- const currentParams = atRule.params;
284
- let minScreen = null;
285
- let maxScreen = null;
286
- if (parentParams.includes(">")) {
287
- const match = parentParams.match(/>=?\s*([^)]+)/);
288
- if (match) minScreen = match[1].trim();
289
- }
290
- if (currentParams.includes(">") && !minScreen) {
291
- const match = currentParams.match(/>=?\s*([^)]+)/);
292
- if (match) minScreen = match[1].trim();
293
- }
294
- if (parentParams.includes("<")) {
295
- const match = parentParams.match(/<\s*([^)]+)/);
296
- if (match) maxScreen = match[1].trim();
297
- }
298
- if (currentParams.includes("<") && !maxScreen) {
299
- const match = currentParams.match(/<\s*([^)]+)/);
300
- if (match) maxScreen = match[1].trim();
330
+ clampDecls.forEach((decl) => {
331
+ const isNested = decl.parent?.type === "atrule" && decl.parent?.parent.type === "atrule";
332
+ const isSameAtRule = decl.parent?.name === decl.parent?.parent.name;
333
+ if (isNested && isSameAtRule) {
334
+ const currentParams2 = decl.parent.params;
335
+ const parentParams = decl.parent.parent.params;
336
+ let minScreen = null;
337
+ let maxScreen = null;
338
+ if (parentParams.includes(">")) {
339
+ const match = parentParams.match(/>=?\s*([^)]+)/);
340
+ if (match) minScreen = match[1].trim();
341
+ }
342
+ if (currentParams2.includes(">") && !minScreen) {
343
+ const match = currentParams2.match(/>=?\s*([^)]+)/);
344
+ if (match) minScreen = match[1].trim();
345
+ }
346
+ if (parentParams.includes("<")) {
347
+ const match = parentParams.match(/<\s*([^)]+)/);
348
+ if (match) maxScreen = match[1].trim();
349
+ }
350
+ if (currentParams2.includes("<") && !maxScreen) {
351
+ const match = currentParams2.match(/<\s*([^)]+)/);
352
+ if (match) maxScreen = match[1].trim();
353
+ }
354
+ if (minScreen && maxScreen) {
355
+ clampDecls.forEach((decl2) => {
356
+ processClampDeclaration(decl2, minScreen, maxScreen, false);
357
+ });
358
+ }
359
+ return;
301
360
  }
302
- if (minScreen && maxScreen) {
303
- clampDecls.forEach((decl) => {
304
- processClampDeclaration(decl, minScreen, maxScreen, false);
361
+ if (isNested && !isSameAtRule) {
362
+ clampDecls.forEach((decl2) => {
363
+ decl2.value = ` ${decl2.value} /* Invalid nested @media rules */`;
305
364
  });
365
+ return;
306
366
  }
307
- return;
308
- }
309
- if (isNested && !isSameAtRule) {
310
- clampDecls.forEach((decl) => {
311
- decl.value = ` ${decl.value} /* Invalid nested @media rules */`;
312
- });
313
- return;
314
- }
315
- const screenValues = Object.values(screens);
316
- clampDecls.forEach((decl) => {
317
- if (atRule.params.includes(">")) {
318
- const match = atRule.params.match(/>=?\s*([^)]+)/);
367
+ const screenValues = Object.values(screens);
368
+ const currentParams = decl.parent.params;
369
+ if (currentParams.includes(">")) {
370
+ const match = currentParams.match(/>=?\s*([^)]+)/);
319
371
  if (match) {
320
372
  const minScreen = match[1].trim();
321
- const maxScreen = screenValues[screenValues.length - 1];
373
+ const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1];
322
374
  processClampDeclaration(decl, minScreen, maxScreen, false);
323
375
  }
324
- } else if (atRule.params.includes("<")) {
325
- const match = atRule.params.match(/<\s*([^)]+)/);
376
+ } else if (currentParams.includes("<")) {
377
+ const match = currentParams.match(/<\s*([^)]+)/);
326
378
  if (match) {
327
- const minScreen = screenValues[0];
379
+ const minScreen = defaultClampRange.min || screenValues[0];
328
380
  const maxScreen = match[1].trim();
329
381
  processClampDeclaration(decl, minScreen, maxScreen, false);
330
382
  }
@@ -332,8 +384,6 @@ var clampwind = (opts = {}) => {
332
384
  });
333
385
  });
334
386
  root.walkAtRules("container", (atRule) => {
335
- const isNested = atRule.parent?.type === "atrule";
336
- const isSameAtRule = atRule.parent?.name === atRule.name;
337
387
  const clampDecls = [];
338
388
  atRule.walkDecls((decl) => {
339
389
  if (extractTwoValidClampArgs(decl.value)) {
@@ -341,55 +391,73 @@ var clampwind = (opts = {}) => {
341
391
  }
342
392
  });
343
393
  if (!clampDecls.length) return;
344
- if (isNested && isSameAtRule) {
345
- const parentParams = atRule.parent.params;
346
- const currentParams = atRule.params;
347
- let minContainer = null;
348
- let maxContainer = null;
349
- if (parentParams.includes(">")) {
350
- const match = parentParams.match(/>=?\s*([^)]+)/);
351
- if (match) minContainer = match[1].trim();
352
- }
353
- if (currentParams.includes(">") && !minContainer) {
354
- const match = currentParams.match(/>=?\s*([^)]+)/);
355
- if (match) minContainer = match[1].trim();
356
- }
357
- if (parentParams.includes("<")) {
358
- const match = parentParams.match(/<\s*([^)]+)/);
359
- if (match) maxContainer = match[1].trim();
360
- }
361
- if (currentParams.includes("<") && !maxContainer) {
362
- const match = currentParams.match(/<\s*([^)]+)/);
363
- if (match) maxContainer = match[1].trim();
394
+ clampDecls.forEach((decl) => {
395
+ const isNested = decl.parent?.type === "atrule" && decl.parent?.parent.type === "atrule";
396
+ const isSameAtRule = decl.parent?.name === decl.parent?.parent.name;
397
+ if (isNested && isSameAtRule) {
398
+ const currentParams2 = decl.parent.params;
399
+ const parentParams = decl.parent.parent.params;
400
+ let minContainer = null;
401
+ let maxContainer = null;
402
+ if (parentParams.includes(">")) {
403
+ const match = parentParams.match(/>=?\s*([^)]+)/);
404
+ if (match) minContainer = match[1].trim();
405
+ }
406
+ if (currentParams2.includes(">") && !minContainer) {
407
+ const match = currentParams2.match(/>=?\s*([^)]+)/);
408
+ if (match) minContainer = match[1].trim();
409
+ }
410
+ if (parentParams.includes("<")) {
411
+ const match = parentParams.match(/<\s*([^)]+)/);
412
+ if (match) maxContainer = match[1].trim();
413
+ }
414
+ if (currentParams2.includes("<") && !maxContainer) {
415
+ const match = currentParams2.match(/<\s*([^)]+)/);
416
+ if (match) maxContainer = match[1].trim();
417
+ }
418
+ if (minContainer && maxContainer) {
419
+ clampDecls.forEach((decl2) => {
420
+ processClampDeclaration(
421
+ decl2,
422
+ minContainer,
423
+ maxContainer,
424
+ true
425
+ );
426
+ });
427
+ }
428
+ return;
364
429
  }
365
- if (minContainer && maxContainer) {
366
- clampDecls.forEach((decl) => {
367
- processClampDeclaration(decl, minContainer, maxContainer, true);
430
+ if (isNested && !isSameAtRule) {
431
+ clampDecls.forEach((decl2) => {
432
+ decl2.value = ` ${decl2.value} /* Invalid nested @container rules */`;
368
433
  });
434
+ return;
369
435
  }
370
- return;
371
- }
372
- if (isNested && !isSameAtRule) {
373
- clampDecls.forEach((decl) => {
374
- decl.value = ` ${decl.value} /* Invalid nested @container rules */`;
375
- });
376
- return;
377
- }
378
- const screenValues = Object.values(containerScreens);
379
- clampDecls.forEach((decl) => {
380
- if (atRule.params.includes(">")) {
381
- const match = atRule.params.match(/>=?\s*([^)]+)/);
436
+ const containerValues = Object.values(containerScreens);
437
+ const currentParams = decl.parent.params;
438
+ if (currentParams.includes(">")) {
439
+ const match = currentParams.match(/>=?\s*([^)]+)/);
382
440
  if (match) {
383
441
  const minContainer = match[1].trim();
384
- const maxContainer = screenValues[screenValues.length - 1];
385
- processClampDeclaration(decl, minContainer, maxContainer, true);
442
+ const maxContainer = containerValues[containerValues.length - 1];
443
+ processClampDeclaration(
444
+ decl,
445
+ minContainer,
446
+ maxContainer,
447
+ true
448
+ );
386
449
  }
387
- } else if (atRule.params.includes("<")) {
388
- const match = atRule.params.match(/<\s*([^)]+)/);
450
+ } else if (currentParams.includes("<")) {
451
+ const match = currentParams.match(/<\s*([^)]+)/);
389
452
  if (match) {
390
- const minContainer = screenValues[0];
453
+ const minContainer = containerValues[0];
391
454
  const maxContainer = match[1].trim();
392
- processClampDeclaration(decl, minContainer, maxContainer, true);
455
+ processClampDeclaration(
456
+ decl,
457
+ minContainer,
458
+ maxContainer,
459
+ true
460
+ );
393
461
  }
394
462
  }
395
463
  });
@@ -410,8 +478,8 @@ var clampwind = (opts = {}) => {
410
478
  });
411
479
  if (clampDecls.length === 0) return;
412
480
  const screenValues = Object.values(screens);
413
- const minScreen = screenValues[0];
414
- const maxScreen = screenValues[screenValues.length - 1];
481
+ const minScreen = defaultClampRange.min || screenValues[0];
482
+ const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1];
415
483
  clampDecls.forEach((decl) => {
416
484
  processClampDeclaration(decl, minScreen, maxScreen, false);
417
485
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-clampwind",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A PostCSS plugin to create fluid clamp values for any Tailwind CSS utility",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [