├── .gitignore ├── src ├── screens.js ├── utils.js └── clampwind.js ├── scripts └── build.js ├── package.json ├── README.md ├── LICENSE └── dist ├── clampwind.esm.js └── clampwind.cjs.cjs /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | .DS_Store 7 | .editorconfig 8 | -------------------------------------------------------------------------------- /src/screens.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default screen breakpoints 3 | * defined by the PostCSS plugin 4 | */ 5 | const defaultScreens = { 6 | sm: '40rem', // 640px 7 | md: '48rem', // 768px 8 | lg: '64rem', // 1024px 9 | xl: '80rem', // 1280px 10 | '2xl': '96rem' // 1536px 11 | }; 12 | 13 | const defaultContainerScreens = { 14 | '@3xs': '16rem', // 256px 15 | '@2xs': '18rem', // 288px 16 | '@xs': '20rem', // 320px 17 | '@sm': '24rem', // 384px 18 | '@md': '28rem', // 448px 19 | '@lg': '32rem', // 512px 20 | '@xl': '36rem', // 576px 21 | '@2xl': '42rem', // 672px 22 | '@3xl': '48rem', // 768px 23 | '@4xl': '56rem', // 896px 24 | '@5xl': '64rem', // 1024px 25 | '@6xl': '72rem', // 1152px 26 | '@7xl': '80rem' // 1280px 27 | }; 28 | 29 | export { 30 | defaultScreens, 31 | defaultContainerScreens 32 | }; -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | buildAll() 4 | 5 | async function buildAll() { 6 | return Promise.all([ 7 | build('esm', { 8 | entryPoints: ['src/clampwind.js'], 9 | platform: 'node', 10 | format: 'esm', 11 | mainFields: ['module', 'main'], 12 | external: ['postcss'], 13 | }), 14 | build('cjs', { 15 | entryPoints: ['src/clampwind.js'], 16 | target: ['node20.16'], 17 | platform: 'node', 18 | format: 'cjs', 19 | external: ['postcss'], 20 | }), 21 | ]) 22 | } 23 | 24 | async function build(name, options) { 25 | const path = `clampwind.${name}.${name === 'cjs' ? 'cjs' : 'js'}` 26 | console.log(`Building ${name}`) 27 | 28 | if (process.argv.includes('--watch')) { 29 | let ctx = await esbuild.context({ 30 | outfile: `./dist/${path}`, 31 | bundle: true, 32 | logLevel: 'info', 33 | sourcemap: true, 34 | minify: false, 35 | ...options, 36 | }) 37 | await ctx.watch() 38 | } 39 | else { 40 | return esbuild.build({ 41 | outfile: `./dist/${path}`, 42 | bundle: true, 43 | ...options, 44 | }) 45 | } 46 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-clampwind", 3 | "version": "0.0.10", 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 | "scripts": { 38 | "dev": "node scripts/build.js --watch", 39 | "build": "node scripts/build.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart rounding function that removes unnecessary decimal places 3 | * @param {number} value - The value to round 4 | * @param {number} maxDecimals - Maximum decimal places (default: 4) 5 | * @returns {string} The rounded value as a string 6 | */ 7 | const smartRound = (value, maxDecimals = 4) => { 8 | // Convert to string with max precision first 9 | const precise = value.toFixed(maxDecimals); 10 | 11 | // Remove trailing zeros after decimal point 12 | const trimmed = precise.replace(/\.?0+$/, ''); 13 | 14 | // If we removed everything after decimal, return the integer part 15 | return trimmed || '0'; 16 | }; 17 | 18 | /** 19 | * Extract two clamp args from a string 20 | * @param {string} value - The value to extract the clamp args from 21 | * @returns {Array} The lower and upper clamp args 22 | */ 23 | const extractTwoValidClampArgs = (value) => { 24 | const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/); 25 | return m ? [m[1].trim(), m[2].trim()] : null; 26 | }; 27 | 28 | /** 29 | * Extract the unit from a value 30 | * @param {string} value - The value to extract the unit from 31 | * @returns {string|null} The unit or null if no unit found 32 | */ 33 | const extractUnit = (value) => { 34 | const trimmedValue = value.replace(/\s+/g, ''); 35 | if (trimmedValue.includes('--')) { 36 | const match = trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, '$1'); 37 | return match ? match : null; 38 | } else { 39 | const match = trimmedValue.match(/\D+$/); 40 | return match ? match[0] : null; 41 | } 42 | }; 43 | 44 | /** 45 | * Format a property value 46 | * @param {string} value - The value to format 47 | * @returns {string} The formatted value 48 | */ 49 | const formatProperty = (value) => { 50 | const trimmedValue = value.replace(/\s+/g, ''); 51 | return trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, '$1'); 52 | }; 53 | 54 | /** 55 | * Convert a value to rem if px or has unit, otherwise return value 56 | * If the value is a custom property, it will be converted to the value of the custom property 57 | * @param {string} value - The value to convert to rem 58 | * @param {number} rootFontSize - The root font size 59 | * @param {string} spacingSize - The spacing size 60 | * @param {Object} customProperties - The custom properties 61 | * @returns {string} The converted value 62 | */ 63 | const convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) => { 64 | const unit = extractUnit(value); 65 | const formattedProperty = formatProperty(value); 66 | const fallbackValue = value.includes('var(') && value.includes(',') ? value.replace(/var\([^,]+,\s*([^)]+)\)/, '$1') : null; 67 | 68 | if (!unit) { 69 | const spacingSizeInt = parseFloat(spacingSize); 70 | const spacingUnit = extractUnit(spacingSize); 71 | 72 | if (spacingUnit === "px") { 73 | return `${smartRound(value * spacingSizeInt / rootFontSize)}rem`; 74 | } 75 | 76 | if (spacingUnit === "rem") { 77 | return `${smartRound(value * spacingSizeInt)}rem`; 78 | } 79 | } 80 | 81 | if (unit === "px") { 82 | return `${smartRound(value.replace("px", "") / rootFontSize)}rem`; 83 | } 84 | 85 | if (unit === "rem") { 86 | return value; 87 | } 88 | 89 | if (customProperties[formattedProperty]) { 90 | return customProperties[formattedProperty]; 91 | } 92 | 93 | if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) { 94 | const fallbackUnit = extractUnit(fallbackValue); 95 | 96 | if (!fallbackUnit) { 97 | return `${smartRound(fallbackValue * spacingSize)}rem`; 98 | } 99 | 100 | if (fallbackUnit === "px") { 101 | return `${smartRound(fallbackValue.replace("px", "") / rootFontSize)}rem`; 102 | } 103 | if (fallbackUnit === "rem") { 104 | return fallbackValue; 105 | } 106 | } 107 | 108 | return null; 109 | }; 110 | 111 | /** 112 | * Generate a clamp function 113 | * @param {string} lower - The lower clamp arg 114 | * @param {string} upper - The upper clamp arg 115 | * @param {string} minScreen - The minimum screen size 116 | * @param {string} maxScreen - The maximum screen size 117 | * @param {number} rootFontSize - The root font size 118 | * @param {string} spacingSize - The spacing size 119 | * @param {boolean} containerQuery - Whether to use container queries 120 | * @returns {string} The generated clamp function, 121 | */ 122 | const generateClamp = ( 123 | lower, 124 | upper, 125 | minScreen, 126 | maxScreen, 127 | rootFontSize = 16, 128 | spacingSize = "1px", 129 | containerQuery = false 130 | ) => { 131 | const maxScreenInt = parseFloat( 132 | convertToRem(maxScreen, rootFontSize, spacingSize) 133 | ); 134 | const minScreenInt = parseFloat( 135 | convertToRem(minScreen, rootFontSize, spacingSize) 136 | ); 137 | const lowerInt = parseFloat(lower); 138 | const upperInt = parseFloat(upper); 139 | 140 | const isDescending = lowerInt > upperInt; 141 | const min = isDescending ? upper : lower; 142 | const max = isDescending ? lower : upper; 143 | 144 | const widthUnit = containerQuery ? `100cqw` : `100vw`; 145 | 146 | const slopeInt = smartRound((upperInt - lowerInt) / (maxScreenInt - minScreenInt)); 147 | const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`; 148 | 149 | return clamp; 150 | }; 151 | 152 | /** 153 | * Convert and sort the screens 154 | * @param {Object} screens - The base screens object 155 | * @returns {Object} The sorted screens 156 | */ 157 | const sortScreens = (screens) => { 158 | 159 | // Sort by rem values 160 | const sortedKeys = Object.keys(screens).sort((a, b) => { 161 | const aValue = parseFloat(screens[a]); 162 | const bValue = parseFloat(screens[b]); 163 | return aValue - bValue; 164 | }); 165 | 166 | // Create new object with sorted keys 167 | return sortedKeys.reduce((acc, key) => { 168 | acc[key] = screens[key]; 169 | return acc; 170 | }, {}); 171 | }; 172 | 173 | /** 174 | * Extracts the maximum viewport/container width value from media query parameters. 175 | * Handles multiple syntax formats including modern range syntax, traditional syntax, 176 | * and build-optimized negation patterns. 177 | * 178 | * @param {string|null|undefined} params - The media/container query parameters string to parse. 179 | * @returns {string|null} The extracted maximum width value with its unit (e.g., "1024px", "48rem"), or null if no maximum width constraint is found. 180 | * 181 | */ 182 | const extractMaxValue = (params) => { 183 | if (!params) return null; 184 | 185 | // Try modern < syntax 186 | let match = params.match(/<\s*([^),\s]+)/); 187 | if (match) return match[1].trim(); 188 | 189 | // Try traditional max-width syntax 190 | match = params.match(/max-width:\s*([^),\s]+)/); 191 | if (match) return match[1].trim(); 192 | 193 | // Try "not all and (min-width:...)" 194 | match = params.match(/not\s+all\s+and\s*\(\s*min-width:\s*([^),\s]+)\s*\)/); 195 | if (match) return match[1].trim(); 196 | 197 | return null; 198 | }; 199 | 200 | /** 201 | * Extracts the minimum viewport/container width value from media query parameters. 202 | * Handles multiple syntax formats including modern range syntax, traditional syntax, 203 | * and build-optimized negation patterns. 204 | * 205 | * @param {string|null|undefined} params - The media/container query parameters string to parse. 206 | * @returns {string|null} The extracted minimum width value with its unit (e.g., "768px", "48rem"), or null if no minimum width constraint is found. 207 | * 208 | */ 209 | const extractMinValue = (params) => { 210 | if (!params) return null; 211 | 212 | // Try modern >= or > syntax 213 | let match = params.match(/>=?\s*([^),\s]+)/); 214 | if (match) return match[1].trim(); 215 | 216 | // Try traditional min-width syntax 217 | match = params.match(/min-width:\s*([^),\s]+)/); 218 | if (match) return match[1].trim(); 219 | 220 | // Try "not all and (max-width:...)" 221 | // This means everything NOT below X, so minimum is X+1 (though you might need to handle this differently) 222 | match = params.match(/not\s+all\s+and\s*\(\s*max-width:\s*([^),\s]+)\s*\)/); 223 | if (match) { 224 | return match[1].trim(); 225 | } 226 | 227 | return null; 228 | }; 229 | 230 | export { extractTwoValidClampArgs, convertToRem, generateClamp, sortScreens, extractMaxValue, extractMinValue }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-clampwind 2 | 3 | A PostCSS plugin that transforms any two‑argument `clamp()` call into a fully fluid value, seamlessly integrating with Tailwind CSS utilities. Made for Tailwind v4. 4 | 5 | ## How it works 6 | 7 | Instead of the standard three-value `clamp(min, preferred, max)`, you supply just a minimum and maximum: 8 | 9 | ```html 10 |
11 | ``` 12 | 13 | This will generate the following CSS: 14 | 15 | ```css 16 | .text-\[clamp\(16px\,50px\)\] { 17 | font-size: clamp(1rem, calc(1rem + 0.0379 * (100vw - 40rem)), 3.125rem); 18 | } 19 | ``` 20 | 21 | The supplied values are used to generate the expression inside the `clamp()` function, where the fluid transformation is calculated using Tailwind's smallest and largest breakpoints. 22 | 23 | 24 | ## Installation 25 | 26 | Install the plugin from npm: 27 | 28 | ```sh 29 | npm install -D postcss-clampwind 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) 46 | 47 | ### PostCSS setup 48 | 49 | Add the plugin to your `postcss.config.js` file: 50 | 51 | ```js 52 | // postcss.config.mjs 53 | import tailwindcss from "@tailwindcss/postcss" 54 | import clampwind from "postcss-clampwind" 55 | 56 | export default { 57 | plugins: [ 58 | tailwindcss(), 59 | clampwind(), 60 | ] 61 | } 62 | ``` 63 | 64 | **Demo on StackBlitz:** [postcss-clampwind-postcss](https://stackblitz.com/edit/postcss-clampwind-postcss?file=postcss.config.mjs) 65 | 66 | #### CommonJS usage 67 | 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. 69 | 70 | ```js 71 | // postcss.config.js 72 | module.exports = { 73 | plugins: { 74 | require("@tailwindcss/postcss"), 75 | require("postcss-clampwind").default 76 | } 77 | }; 78 | ``` 79 | 80 | ## Features 81 | 82 | ### Interchangeable px / rem units 83 | 84 | Allow clamped values to use either px or rem interchangeably. 85 | 86 | ```html 87 |
88 | ``` 89 | 90 | ### Use Tailwind breakpoint modifiers 91 | 92 | Use the native Tailwind syntax to clamp values within a specific range by using breakpoint modifiers. 93 | 94 | ```html 95 |
96 | ``` 97 | ### Unitless clamping 98 | 99 | If no unit is specified, default to your theme’s `--spacing` scale. 100 | 101 | ```html 102 |
103 | ``` 104 | 105 | ### Use Tailwind size variables 106 | 107 | Clamp using Tailwind’s predefined size tokens. 108 | 109 | ```html 110 |
111 | ``` 112 | 113 | ### Use CSS custom properties 114 | 115 | Clamp using CSS custom properties. 116 | 117 | ```html 118 |
119 | ``` 120 | 121 | ### Container query support 122 | 123 | Clamp values based on container query breakpoints. 124 | 125 | ```html 126 |
127 | ``` 128 | 129 | ### Decreasing and negative ranges 130 | 131 | Support clamped ranges that shrink or go below zero. 132 | 133 | ```html 134 |
135 | ``` 136 | 137 | ### Error reporting via CSS comments 138 | 139 | Output validation errors as CSS comments for easy debugging. 140 | 141 | ```css 142 | .text-\[clamp\(16%\,50px\)\] { 143 | font-size: clamp(16%,50px); /* Invalid clamp() values */ 144 | } 145 | ``` 146 | 147 | 148 | ## Usage 149 | 150 | To use this plugin you need to use the `clamp()` function but with **only two arguments**, the first one is the minimum value and the second one is the maximum value. 151 | 152 | ### Clamp between smallest and largest breakpoint 153 | 154 | Write the Tailwind utility you want to make fluid, without any breakpoint modifier, for example: 155 | 156 | ```html 157 |
158 | ``` 159 | 160 | This will use Tailwind default largest and smallest breakpoint. 161 | 162 | ```css 163 | .text-\[clamp\(16px\,50px\)\] { 164 | font-size: clamp(1rem, calc(...) , 3.125rem); 165 | } 166 | ``` 167 | 168 | ### Clamp between two breakpoints 169 | 170 | Simply add regular Tailwind breakpoint modifiers to the utility, for example: 171 | 172 | ```html 173 |
174 | ``` 175 | 176 | To clamp the CSS property between the two breakpoints you need to use the `max-` modifier, in this case the CSS property will be clamped between the `md` and `lg` breakpoints. 177 | 178 | This will generate the following css: 179 | 180 | ```css 181 | .md\:max-lg\:text-\[clamp\(16px\,50px\)\] { 182 | @media (width >= 48rem) { /* >= 768px */ 183 | @media (width < 64rem) { /* < 1024px */ 184 | font-size: clamp(1rem, calc(...), 3.125rem); 185 | } 186 | } 187 | } 188 | ``` 189 | 190 | ### Clamp from one breakpoint 191 | 192 | If you want to define a clamp value from a single breakpoint, postcss-clampwind will automatically generate the calculation from the defined breakpoint to the smallest or largest breakpoint depending on the direction, for example: 193 | 194 | ```html 195 |
196 | ``` 197 | 198 | This will generate the following css: 199 | 200 | ```css 201 | .md\:text-\[clamp\(16px\,50px\)\] { 202 | @media (width >= 48rem) { /* >= 768px */ 203 | font-size: clamp(1rem, calc(...), 3.125rem); 204 | } 205 | } 206 | ``` 207 | Or if you use the `max-` modifier: 208 | 209 | ```css 210 | .max-md\:text-\[clamp\(16px\,50px\)\] { 211 | @media (width < 48rem) { /* < 768px */ 212 | font-size: clamp(1rem, calc(...), 3.125rem); 213 | } 214 | } 215 | ``` 216 | 217 | ### Clamp between custom breakpoints 218 | 219 | With Tailwind v4 it's really easy to use one-time custom breakpoints, and this plugin will automatically detect them and use them to clamp the CSS property. 220 | 221 | ```html 222 |
223 | ``` 224 | 225 | This will generate the following css: 226 | 227 | ```css 228 | .min-\[1000px\]\:max-xl\:text-\[clamp\(16px\,50px\)\] { 229 | @media (width >= 1000px) { /* >= 1000px */ 230 | @media (width < 64rem) { /* < 1600px */ 231 | font-size: clamp(1rem, calc(...), 3.125rem); 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | ### Clamp between Tailwind spacing scale values 238 | 239 | A quick way to define two clamped values is to use the Tailwind spacing scale values, for example: 240 | 241 | ```html 242 |
243 | ``` 244 | 245 | The bare values size depends on the theme `--spacing` size, so if you have have it set to `1px` it will generate the following css: 246 | 247 | ```css 248 | .text-\[clamp\(16\,50\)\] { 249 | font-size: clamp(1rem, calc(...), 3.125rem); 250 | } 251 | ``` 252 | 253 | ### Clamp custom properties values 254 | 255 | You can also use custom properties in your clamped values, for example like this: 256 | 257 | ```html 258 |
259 | ``` 260 | or like this: 261 | 262 | ```html 263 |
264 | ``` 265 | 266 | But this won't work when using two custom properties directly in the CSS with `@apply`, so you need to use the `var()` function instead. 267 | 268 | ```css 269 | .h2 { 270 | @apply text-[clamp(var(--text-sm),var(--text-lg))]; 271 | } 272 | ``` 273 | 274 | 275 | 276 | ### Clamp container queries 277 | 278 | Postcss-clampwind supports container queries, just by using the normal Tailwind container query syntax, for example: 279 | 280 | ```html 281 |
282 | ``` 283 | 284 | This will generate the following css: 285 | 286 | ```css 287 | .@md\:text-\[clamp\(16px\,50px\)\] { 288 | @container (width >= 28rem) { /* >= 448px */ 289 | font-size: clamp(1rem, calc(...), 3.125rem); 290 | } 291 | } 292 | ``` 293 | 294 | ## Configuration 295 | 296 | Tailwind v4 introduced the new CSS-based configuration and postcss-clampwind embraces it. 297 | 298 | ### Add custom breakpoints 299 | 300 | To add new breakpoints in Tailwind v4 you normally define them inside the `@theme` directive. 301 | 302 | But Tailwind by default, will not output in your CSS any custom properties that are not referenced in your CSS, for this reason you should use the `@theme static` directive instead of `@theme` to create custom breakpoints. 303 | 304 | ```css 305 | @theme static { 306 | --breakpoint-4xl: 1600px; 307 | } 308 | ``` 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 |
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 |
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 | 337 | ### Use custom properties 338 | 339 | You can use any custom properties in your clamped values, for example: 340 | 341 | ```html 342 |
343 | ``` 344 | 345 | You just need to make sure that the custom property is defined in your `:root` selector. 346 | 347 | ```css 348 | :root { 349 | --custom-value: 16px; 350 | } 351 | ``` 352 | 353 | ### Pixel to rem conversion 354 | 355 | If you are using pixel values in your clamped values, clampwind will automatically convert them to rem. For the conversion it scans your generated css and if you have set pixel values for the root `font-size` or for your `--text-base` custom property in your `:root` selector, it will use that value to convert the pixel values to rem values. If you haven't set a font-size in your `:root` selector, it will use the default value of 16px. 356 | 357 | ```css 358 | :root { 359 | font-size: 18px; /* 18px = 1.125rem */ 360 | } 361 | ``` 362 | or like this: 363 | 364 | ```css 365 | :root { 366 | --text-base: 18px; /* 18px = 1.125rem */ 367 | } 368 | ``` 369 | 370 | ## License and Credits 371 | 372 | This project is licensed under the [Apache-2.0 license](https://apache.org/licenses/LICENSE-2.0). 373 | 374 | Copyright © 2025 Daniele De Pietri. 375 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/clampwind.js: -------------------------------------------------------------------------------- 1 | import { defaultScreens, defaultContainerScreens } from "./screens.js"; 2 | import { 3 | extractTwoValidClampArgs, 4 | convertToRem, 5 | generateClamp, 6 | sortScreens, 7 | extractMaxValue, 8 | extractMinValue 9 | } from "./utils.js"; 10 | 11 | const clampwind = (opts = {}) => { 12 | return { 13 | postcssPlugin: "postcss-clampwind", 14 | prepare(result) { 15 | // Configuration variables 16 | let rootFontSize = 16; 17 | let spacingSize = "0.25rem"; 18 | let customProperties = {}; 19 | let screens = defaultScreens || {}; 20 | let containerScreens = defaultContainerScreens || {}; 21 | let defaultClampRange = {}; 22 | 23 | // Configuration collected from theme layers and root 24 | const config = { 25 | themeLayerBreakpoints: {}, 26 | themeLayerContainerBreakpoints: {}, 27 | rootElementBreakpoints: {}, 28 | rootElementContainerBreakpoints: {}, 29 | configCollected: false, 30 | configReady: false, 31 | }; 32 | 33 | // Helper function to collect configuration 34 | const collectConfig = (root) => { 35 | if (config.configCollected) return; 36 | 37 | // Collect root font size from :root 38 | root.walkDecls((decl) => { 39 | if (decl.parent?.selector === ":root") { 40 | if (decl.prop === "font-size" && decl.value.includes("px")) { 41 | rootFontSize = parseFloat(decl.value); 42 | } 43 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 44 | rootFontSize = parseFloat(decl.value); 45 | } 46 | } 47 | }); 48 | 49 | // Collect custom properties from :root 50 | root.walkDecls((decl) => { 51 | if (decl.parent?.selector === ":root") { 52 | if (decl.prop.startsWith("--breakpoint-")) { 53 | const key = decl.prop.replace("--breakpoint-", ""); 54 | config.rootElementBreakpoints[key] = convertToRem( 55 | decl.value, 56 | rootFontSize, 57 | spacingSize, 58 | customProperties 59 | ); 60 | } 61 | if (decl.prop.startsWith("--container-")) { 62 | const key = decl.prop.replace("--container-", "@"); 63 | config.rootElementContainerBreakpoints[key] = convertToRem( 64 | decl.value, 65 | rootFontSize, 66 | spacingSize, 67 | customProperties 68 | ); 69 | } 70 | if (decl.prop === "--breakpoint-clamp-min") { 71 | defaultClampRange.min = convertToRem( 72 | decl.value, 73 | rootFontSize, 74 | spacingSize, 75 | customProperties 76 | ); 77 | } 78 | if (decl.prop === "--breakpoint-clamp-max") { 79 | defaultClampRange.max = convertToRem( 80 | decl.value, 81 | rootFontSize, 82 | spacingSize, 83 | customProperties 84 | ); 85 | } 86 | if (decl.prop === "--spacing") { 87 | spacingSize = decl.value; 88 | } 89 | if (decl.prop.startsWith("--")) { 90 | const value = convertToRem( 91 | decl.value, 92 | rootFontSize, 93 | spacingSize, 94 | customProperties 95 | ); 96 | if (value) customProperties[decl.prop] = value; 97 | } 98 | } 99 | }); 100 | 101 | // Collect root font size from theme layer 102 | root.walkAtRules("layer", (atRule) => { 103 | if (atRule.params === "theme") { 104 | atRule.walkDecls((decl) => { 105 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 106 | rootFontSize = parseFloat(decl.value); 107 | } 108 | }); 109 | } 110 | }); 111 | 112 | // Collect custom properties from layers 113 | root.walkAtRules("layer", (atRule) => { 114 | // Theme layer 115 | if (atRule.params === "theme") { 116 | atRule.walkDecls((decl) => { 117 | if (decl.prop.startsWith("--breakpoint-")) { 118 | const key = decl.prop.replace("--breakpoint-", ""); 119 | config.themeLayerBreakpoints[key] = convertToRem( 120 | decl.value, 121 | rootFontSize, 122 | spacingSize, 123 | customProperties 124 | ); 125 | } 126 | if (decl.prop.startsWith("--container-")) { 127 | const key = decl.prop.replace("--container-", "@"); 128 | config.themeLayerContainerBreakpoints[key] = convertToRem( 129 | decl.value, 130 | rootFontSize, 131 | spacingSize, 132 | customProperties 133 | ); 134 | } 135 | if (decl.prop === "--breakpoint-clamp-min") { 136 | defaultClampRange.min = convertToRem( 137 | decl.value, 138 | rootFontSize, 139 | spacingSize, 140 | customProperties 141 | ); 142 | } 143 | if (decl.prop === "--breakpoint-clamp-max") { 144 | defaultClampRange.max = convertToRem( 145 | decl.value, 146 | rootFontSize, 147 | spacingSize, 148 | customProperties 149 | ); 150 | } 151 | if (decl.prop === "--spacing") { 152 | spacingSize = decl.value; 153 | } 154 | if (decl.prop.startsWith("--")) { 155 | const value = convertToRem( 156 | decl.value, 157 | rootFontSize, 158 | spacingSize, 159 | customProperties 160 | ); 161 | if (value) customProperties[decl.prop] = value; 162 | } 163 | }); 164 | } 165 | }); 166 | 167 | config.configCollected = true; 168 | }; 169 | 170 | // Helper function to finalize configuration 171 | const finalizeConfig = () => { 172 | if (config.configReady) return; 173 | 174 | // Join, convert and sort screens breakpoints from theme, root and layer 175 | screens = Object.assign( 176 | {}, 177 | screens, 178 | config.rootElementBreakpoints, 179 | config.themeLayerBreakpoints 180 | ); 181 | screens = sortScreens(screens); 182 | 183 | // Join, convert and sort container breakpoints from theme, root and layer 184 | containerScreens = Object.assign( 185 | {}, 186 | containerScreens, 187 | config.rootElementContainerBreakpoints, 188 | config.themeLayerContainerBreakpoints 189 | ); 190 | containerScreens = sortScreens(containerScreens); 191 | 192 | config.configReady = true; 193 | }; 194 | 195 | // Helper function to process clamp declarations 196 | const processClampDeclaration = ( 197 | decl, 198 | minScreen, 199 | maxScreen, 200 | isContainer = false 201 | ) => { 202 | const args = extractTwoValidClampArgs(decl.value); 203 | const [lower, upper] = args.map((val) => 204 | convertToRem(val, rootFontSize, spacingSize, customProperties) 205 | ); 206 | 207 | if (!args || !lower || !upper) { 208 | result.warn( 209 | `Invalid clamp() values: "${decl.value}". Expected format: clamp(min, preferred, max)`, 210 | { 211 | node: decl, 212 | word: decl.value 213 | } 214 | ); 215 | 216 | decl.value = `${decl.value} /* Invalid clamp() values */`; 217 | return true; 218 | } 219 | const clamp = generateClamp( 220 | lower, 221 | upper, 222 | minScreen, 223 | maxScreen, 224 | rootFontSize, 225 | spacingSize, 226 | isContainer 227 | ); 228 | decl.value = clamp; 229 | return true; 230 | }; 231 | 232 | // Helper to check if we're in dev or build environment 233 | const getNestedStructure = (atRule) => { 234 | // Check if this atRule is nested inside another media query 235 | const isNested = atRule.parent?.type === "atrule" && atRule.parent?.name === "media"; 236 | 237 | // Check if the atRule contains nested media queries (build structure) 238 | const hasNestedMedia = atRule.nodes?.some( 239 | node => node.type === 'atrule' && node.name === 'media' 240 | ); 241 | 242 | return { isNested, hasNestedMedia }; 243 | }; 244 | 245 | // Process media queries with nested structure awareness 246 | const processMediaQuery = (atRule, parentAtRule = null) => { 247 | const clampDecls = []; 248 | atRule.walkDecls((decl) => { 249 | if (extractTwoValidClampArgs(decl.value)) { 250 | clampDecls.push(decl); 251 | } 252 | }); 253 | 254 | if (!clampDecls.length) return; 255 | 256 | const screenValues = Object.values(screens); 257 | 258 | // Handle nested media queries 259 | if (parentAtRule) { 260 | const currentParams = atRule.params; 261 | const parentParams = parentAtRule.params; 262 | 263 | const minScreen = extractMinValue(parentParams) || extractMinValue(currentParams); 264 | const maxScreen = extractMaxValue(parentParams) || extractMaxValue(currentParams); 265 | 266 | if (minScreen && maxScreen) { 267 | clampDecls.forEach((decl) => { 268 | processClampDeclaration(decl, minScreen, maxScreen, false); 269 | }); 270 | } 271 | } else { 272 | // MARK: Single MQ 273 | const currentParams = atRule.params; 274 | const minValue = extractMinValue(currentParams); 275 | const maxValue = extractMaxValue(currentParams); 276 | 277 | if (minValue && !maxValue) { 278 | const minScreen = minValue; 279 | const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1]; 280 | clampDecls.forEach((decl) => { 281 | processClampDeclaration(decl, minScreen, maxScreen, false); 282 | }); 283 | } else if (maxValue && !minValue) { 284 | const minScreen = defaultClampRange.min || screenValues[0]; 285 | const maxScreen = maxValue; 286 | clampDecls.forEach((decl) => { 287 | processClampDeclaration(decl, minScreen, maxScreen, false); 288 | }); 289 | } else if (minValue && maxValue) { 290 | clampDecls.forEach((decl) => { 291 | processClampDeclaration(decl, minValue, maxValue, false); 292 | }); 293 | } 294 | } 295 | }; 296 | 297 | // Process container queries with nested structure awareness 298 | const processContainerQuery = (atRule, parentAtRule = null) => { 299 | const clampDecls = []; 300 | atRule.walkDecls((decl) => { 301 | if (extractTwoValidClampArgs(decl.value)) { 302 | clampDecls.push(decl); 303 | } 304 | }); 305 | 306 | if (!clampDecls.length) return; 307 | 308 | const containerValues = Object.values(containerScreens); 309 | 310 | // Handle nested container queries 311 | if (parentAtRule) { 312 | const currentParams = atRule.params; 313 | const parentParams = parentAtRule.params; 314 | 315 | const minContainer = extractMinValue(parentParams) || extractMinValue(currentParams); 316 | const maxContainer = extractMaxValue(parentParams) || extractMaxValue(currentParams); 317 | 318 | if (minContainer && maxContainer) { 319 | clampDecls.forEach((decl) => { 320 | processClampDeclaration(decl, minContainer, maxContainer, true); 321 | }); 322 | } 323 | } else { 324 | // MARK: Single CQ 325 | const currentParams = atRule.params; 326 | const minValue = extractMinValue(currentParams); 327 | const maxValue = extractMaxValue(currentParams); 328 | 329 | if (minValue && !maxValue) { 330 | const minContainer = minValue; 331 | const maxContainer = containerValues[containerValues.length - 1]; 332 | clampDecls.forEach((decl) => { 333 | processClampDeclaration(decl, minContainer, maxContainer, true); 334 | }); 335 | } else if (maxValue && !minValue) { 336 | const minContainer = containerValues[0]; 337 | const maxContainer = maxValue; 338 | clampDecls.forEach((decl) => { 339 | processClampDeclaration(decl, minContainer, maxContainer, true); 340 | }); 341 | } else if (minValue && maxValue) { 342 | clampDecls.forEach((decl) => { 343 | processClampDeclaration(decl, minValue, maxValue, true); 344 | }); 345 | } 346 | } 347 | }; 348 | 349 | return { 350 | // Use OnceExit to ensure Tailwind has generated its content 351 | OnceExit(root, { result }) { 352 | // Collect all configuration after Tailwind has processed 353 | collectConfig(root); 354 | finalizeConfig(); 355 | 356 | // Track processed atRules to avoid double processing 357 | const processedAtRules = new WeakSet(); 358 | 359 | // Process media queries 360 | root.walkAtRules("media", (atRule) => { 361 | if (processedAtRules.has(atRule)) return; 362 | 363 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 364 | 365 | // MARK: Nested MQ 366 | // If this media query contains nested media queries 367 | if (hasNestedMedia) { 368 | atRule.walkAtRules("media", (nestedAtRule) => { 369 | processedAtRules.add(nestedAtRule); 370 | processMediaQuery(nestedAtRule, atRule); 371 | }); 372 | } 373 | // If this media query is nested inside another 374 | else if (isNested) { 375 | // Skip - it will be processed by its parent 376 | return; 377 | } 378 | // Single media query 379 | else { 380 | processMediaQuery(atRule); 381 | } 382 | }); 383 | 384 | // Process container queries 385 | root.walkAtRules("container", (atRule) => { 386 | if (processedAtRules.has(atRule)) return; 387 | 388 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 389 | 390 | // MARK: Nested CQ 391 | // If this container query contains nested container queries 392 | if (hasNestedMedia) { 393 | atRule.walkAtRules("container", (nestedAtRule) => { 394 | processedAtRules.add(nestedAtRule); 395 | processContainerQuery(nestedAtRule, atRule); 396 | }); 397 | } 398 | // If this container query is nested inside another 399 | else if (isNested) { 400 | // Skip - it will be processed by its parent 401 | return; 402 | } 403 | // Single container query 404 | else { 405 | processContainerQuery(atRule); 406 | } 407 | }); 408 | 409 | // MARK: No MQ or CQ 410 | root.walkRules((rule) => { 411 | // Skip if inside a media or container query (they were already processed) 412 | let parent = rule.parent; 413 | while (parent) { 414 | if ( 415 | parent.type === "atrule" && 416 | (parent.name === "media" || parent.name === "container") 417 | ) { 418 | return; // Skip this rule, it's inside a media/container query 419 | } 420 | parent = parent.parent; 421 | } 422 | 423 | // Find and process clamp declarations 424 | const clampDecls = []; 425 | rule.walkDecls((decl) => { 426 | if (extractTwoValidClampArgs(decl.value)) { 427 | clampDecls.push(decl); 428 | } 429 | }); 430 | 431 | if (clampDecls.length === 0) return; 432 | 433 | const screenValues = Object.values(screens); 434 | const minScreen = defaultClampRange.min || screenValues[0]; 435 | const maxScreen = 436 | defaultClampRange.max || screenValues[screenValues.length - 1]; 437 | 438 | clampDecls.forEach((decl) => { 439 | processClampDeclaration(decl, minScreen, maxScreen, false); 440 | }); 441 | }); 442 | }, 443 | }; 444 | }, 445 | }; 446 | }; 447 | 448 | clampwind.postcss = true; 449 | 450 | export default clampwind; -------------------------------------------------------------------------------- /dist/clampwind.esm.js: -------------------------------------------------------------------------------- 1 | // src/screens.js 2 | var defaultScreens = { 3 | sm: "40rem", 4 | // 640px 5 | md: "48rem", 6 | // 768px 7 | lg: "64rem", 8 | // 1024px 9 | xl: "80rem", 10 | // 1280px 11 | "2xl": "96rem" 12 | // 1536px 13 | }; 14 | var defaultContainerScreens = { 15 | "@3xs": "16rem", 16 | // 256px 17 | "@2xs": "18rem", 18 | // 288px 19 | "@xs": "20rem", 20 | // 320px 21 | "@sm": "24rem", 22 | // 384px 23 | "@md": "28rem", 24 | // 448px 25 | "@lg": "32rem", 26 | // 512px 27 | "@xl": "36rem", 28 | // 576px 29 | "@2xl": "42rem", 30 | // 672px 31 | "@3xl": "48rem", 32 | // 768px 33 | "@4xl": "56rem", 34 | // 896px 35 | "@5xl": "64rem", 36 | // 1024px 37 | "@6xl": "72rem", 38 | // 1152px 39 | "@7xl": "80rem" 40 | // 1280px 41 | }; 42 | 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 | }; 49 | var extractTwoValidClampArgs = (value) => { 50 | const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/); 51 | return m ? [m[1].trim(), m[2].trim()] : null; 52 | }; 53 | var extractUnit = (value) => { 54 | const trimmedValue = value.replace(/\s+/g, ""); 55 | if (trimmedValue.includes("--")) { 56 | const match = trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1"); 57 | return match ? match : null; 58 | } else { 59 | const match = trimmedValue.match(/\D+$/); 60 | return match ? match[0] : null; 61 | } 62 | }; 63 | var formatProperty = (value) => { 64 | const trimmedValue = value.replace(/\s+/g, ""); 65 | return trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1"); 66 | }; 67 | var convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) => { 68 | const unit = extractUnit(value); 69 | const formattedProperty = formatProperty(value); 70 | const fallbackValue = value.includes("var(") && value.includes(",") ? value.replace(/var\([^,]+,\s*([^)]+)\)/, "$1") : null; 71 | if (!unit) { 72 | const spacingSizeInt = parseFloat(spacingSize); 73 | const spacingUnit = extractUnit(spacingSize); 74 | if (spacingUnit === "px") { 75 | return `${smartRound(value * spacingSizeInt / rootFontSize)}rem`; 76 | } 77 | if (spacingUnit === "rem") { 78 | return `${smartRound(value * spacingSizeInt)}rem`; 79 | } 80 | } 81 | if (unit === "px") { 82 | return `${smartRound(value.replace("px", "") / rootFontSize)}rem`; 83 | } 84 | if (unit === "rem") { 85 | return value; 86 | } 87 | if (customProperties[formattedProperty]) { 88 | return customProperties[formattedProperty]; 89 | } 90 | if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) { 91 | const fallbackUnit = extractUnit(fallbackValue); 92 | if (!fallbackUnit) { 93 | return `${smartRound(fallbackValue * spacingSize)}rem`; 94 | } 95 | if (fallbackUnit === "px") { 96 | return `${smartRound(fallbackValue.replace("px", "") / rootFontSize)}rem`; 97 | } 98 | if (fallbackUnit === "rem") { 99 | return fallbackValue; 100 | } 101 | } 102 | return null; 103 | }; 104 | var generateClamp = (lower, upper, minScreen, maxScreen, rootFontSize = 16, spacingSize = "1px", containerQuery = false) => { 105 | const maxScreenInt = parseFloat( 106 | convertToRem(maxScreen, rootFontSize, spacingSize) 107 | ); 108 | const minScreenInt = parseFloat( 109 | convertToRem(minScreen, rootFontSize, spacingSize) 110 | ); 111 | const lowerInt = parseFloat(lower); 112 | const upperInt = parseFloat(upper); 113 | const isDescending = lowerInt > upperInt; 114 | const min = isDescending ? upper : lower; 115 | const max = isDescending ? lower : upper; 116 | const widthUnit = containerQuery ? `100cqw` : `100vw`; 117 | const slopeInt = smartRound((upperInt - lowerInt) / (maxScreenInt - minScreenInt)); 118 | const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`; 119 | return clamp; 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 | }; 132 | var extractMaxValue = (params) => { 133 | if (!params) return null; 134 | let match = params.match(/<\s*([^),\s]+)/); 135 | if (match) return match[1].trim(); 136 | match = params.match(/max-width:\s*([^),\s]+)/); 137 | if (match) return match[1].trim(); 138 | match = params.match(/not\s+all\s+and\s*\(\s*min-width:\s*([^),\s]+)\s*\)/); 139 | if (match) return match[1].trim(); 140 | return null; 141 | }; 142 | var extractMinValue = (params) => { 143 | if (!params) return null; 144 | let match = params.match(/>=?\s*([^),\s]+)/); 145 | if (match) return match[1].trim(); 146 | match = params.match(/min-width:\s*([^),\s]+)/); 147 | if (match) return match[1].trim(); 148 | match = params.match(/not\s+all\s+and\s*\(\s*max-width:\s*([^),\s]+)\s*\)/); 149 | if (match) { 150 | return match[1].trim(); 151 | } 152 | return null; 153 | }; 154 | 155 | // src/clampwind.js 156 | var clampwind = (opts = {}) => { 157 | return { 158 | postcssPlugin: "postcss-clampwind", 159 | prepare(result) { 160 | let rootFontSize = 16; 161 | let spacingSize = "0.25rem"; 162 | let customProperties = {}; 163 | let screens = defaultScreens || {}; 164 | let containerScreens = defaultContainerScreens || {}; 165 | let defaultClampRange = {}; 166 | const config = { 167 | themeLayerBreakpoints: {}, 168 | themeLayerContainerBreakpoints: {}, 169 | rootElementBreakpoints: {}, 170 | rootElementContainerBreakpoints: {}, 171 | configCollected: false, 172 | configReady: false 173 | }; 174 | const collectConfig = (root) => { 175 | if (config.configCollected) return; 176 | root.walkDecls((decl) => { 177 | if (decl.parent?.selector === ":root") { 178 | if (decl.prop === "font-size" && decl.value.includes("px")) { 179 | rootFontSize = parseFloat(decl.value); 180 | } 181 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 182 | rootFontSize = parseFloat(decl.value); 183 | } 184 | } 185 | }); 186 | root.walkDecls((decl) => { 187 | if (decl.parent?.selector === ":root") { 188 | if (decl.prop.startsWith("--breakpoint-")) { 189 | const key = decl.prop.replace("--breakpoint-", ""); 190 | config.rootElementBreakpoints[key] = convertToRem( 191 | decl.value, 192 | rootFontSize, 193 | spacingSize, 194 | customProperties 195 | ); 196 | } 197 | if (decl.prop.startsWith("--container-")) { 198 | const key = decl.prop.replace("--container-", "@"); 199 | config.rootElementContainerBreakpoints[key] = convertToRem( 200 | decl.value, 201 | rootFontSize, 202 | spacingSize, 203 | customProperties 204 | ); 205 | } 206 | if (decl.prop === "--breakpoint-clamp-min") { 207 | defaultClampRange.min = convertToRem( 208 | decl.value, 209 | rootFontSize, 210 | spacingSize, 211 | customProperties 212 | ); 213 | } 214 | if (decl.prop === "--breakpoint-clamp-max") { 215 | defaultClampRange.max = convertToRem( 216 | decl.value, 217 | rootFontSize, 218 | spacingSize, 219 | customProperties 220 | ); 221 | } 222 | if (decl.prop === "--spacing") { 223 | spacingSize = decl.value; 224 | } 225 | if (decl.prop.startsWith("--")) { 226 | const value = convertToRem( 227 | decl.value, 228 | rootFontSize, 229 | spacingSize, 230 | customProperties 231 | ); 232 | if (value) customProperties[decl.prop] = value; 233 | } 234 | } 235 | }); 236 | root.walkAtRules("layer", (atRule) => { 237 | if (atRule.params === "theme") { 238 | atRule.walkDecls((decl) => { 239 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 240 | rootFontSize = parseFloat(decl.value); 241 | } 242 | }); 243 | } 244 | }); 245 | root.walkAtRules("layer", (atRule) => { 246 | if (atRule.params === "theme") { 247 | atRule.walkDecls((decl) => { 248 | if (decl.prop.startsWith("--breakpoint-")) { 249 | const key = decl.prop.replace("--breakpoint-", ""); 250 | config.themeLayerBreakpoints[key] = convertToRem( 251 | decl.value, 252 | rootFontSize, 253 | spacingSize, 254 | customProperties 255 | ); 256 | } 257 | if (decl.prop.startsWith("--container-")) { 258 | const key = decl.prop.replace("--container-", "@"); 259 | config.themeLayerContainerBreakpoints[key] = convertToRem( 260 | decl.value, 261 | rootFontSize, 262 | spacingSize, 263 | customProperties 264 | ); 265 | } 266 | if (decl.prop === "--breakpoint-clamp-min") { 267 | defaultClampRange.min = convertToRem( 268 | decl.value, 269 | rootFontSize, 270 | spacingSize, 271 | customProperties 272 | ); 273 | } 274 | if (decl.prop === "--breakpoint-clamp-max") { 275 | defaultClampRange.max = convertToRem( 276 | decl.value, 277 | rootFontSize, 278 | spacingSize, 279 | customProperties 280 | ); 281 | } 282 | if (decl.prop === "--spacing") { 283 | spacingSize = decl.value; 284 | } 285 | if (decl.prop.startsWith("--")) { 286 | const value = convertToRem( 287 | decl.value, 288 | rootFontSize, 289 | spacingSize, 290 | customProperties 291 | ); 292 | if (value) customProperties[decl.prop] = value; 293 | } 294 | }); 295 | } 296 | }); 297 | config.configCollected = true; 298 | }; 299 | const finalizeConfig = () => { 300 | if (config.configReady) return; 301 | screens = Object.assign( 302 | {}, 303 | screens, 304 | config.rootElementBreakpoints, 305 | config.themeLayerBreakpoints 306 | ); 307 | screens = sortScreens(screens); 308 | containerScreens = Object.assign( 309 | {}, 310 | containerScreens, 311 | config.rootElementContainerBreakpoints, 312 | config.themeLayerContainerBreakpoints 313 | ); 314 | containerScreens = sortScreens(containerScreens); 315 | config.configReady = true; 316 | }; 317 | const processClampDeclaration = (decl, minScreen, maxScreen, isContainer = false) => { 318 | const args = extractTwoValidClampArgs(decl.value); 319 | const [lower, upper] = args.map( 320 | (val) => convertToRem(val, rootFontSize, spacingSize, customProperties) 321 | ); 322 | if (!args || !lower || !upper) { 323 | result.warn( 324 | `Invalid clamp() values: "${decl.value}". Expected format: clamp(min, preferred, max)`, 325 | { 326 | node: decl, 327 | word: decl.value 328 | } 329 | ); 330 | decl.value = `${decl.value} /* Invalid clamp() values */`; 331 | return true; 332 | } 333 | const clamp = generateClamp( 334 | lower, 335 | upper, 336 | minScreen, 337 | maxScreen, 338 | rootFontSize, 339 | spacingSize, 340 | isContainer 341 | ); 342 | decl.value = clamp; 343 | return true; 344 | }; 345 | const getNestedStructure = (atRule) => { 346 | const isNested = atRule.parent?.type === "atrule" && atRule.parent?.name === "media"; 347 | const hasNestedMedia = atRule.nodes?.some( 348 | (node) => node.type === "atrule" && node.name === "media" 349 | ); 350 | return { isNested, hasNestedMedia }; 351 | }; 352 | const processMediaQuery = (atRule, parentAtRule = null) => { 353 | const clampDecls = []; 354 | atRule.walkDecls((decl) => { 355 | if (extractTwoValidClampArgs(decl.value)) { 356 | clampDecls.push(decl); 357 | } 358 | }); 359 | if (!clampDecls.length) return; 360 | const screenValues = Object.values(screens); 361 | if (parentAtRule) { 362 | const currentParams = atRule.params; 363 | const parentParams = parentAtRule.params; 364 | const minScreen = extractMinValue(parentParams) || extractMinValue(currentParams); 365 | const maxScreen = extractMaxValue(parentParams) || extractMaxValue(currentParams); 366 | if (minScreen && maxScreen) { 367 | clampDecls.forEach((decl) => { 368 | processClampDeclaration(decl, minScreen, maxScreen, false); 369 | }); 370 | } 371 | } else { 372 | const currentParams = atRule.params; 373 | const minValue = extractMinValue(currentParams); 374 | const maxValue = extractMaxValue(currentParams); 375 | if (minValue && !maxValue) { 376 | const minScreen = minValue; 377 | const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1]; 378 | clampDecls.forEach((decl) => { 379 | processClampDeclaration(decl, minScreen, maxScreen, false); 380 | }); 381 | } else if (maxValue && !minValue) { 382 | const minScreen = defaultClampRange.min || screenValues[0]; 383 | const maxScreen = maxValue; 384 | clampDecls.forEach((decl) => { 385 | processClampDeclaration(decl, minScreen, maxScreen, false); 386 | }); 387 | } else if (minValue && maxValue) { 388 | clampDecls.forEach((decl) => { 389 | processClampDeclaration(decl, minValue, maxValue, false); 390 | }); 391 | } 392 | } 393 | }; 394 | const processContainerQuery = (atRule, parentAtRule = null) => { 395 | const clampDecls = []; 396 | atRule.walkDecls((decl) => { 397 | if (extractTwoValidClampArgs(decl.value)) { 398 | clampDecls.push(decl); 399 | } 400 | }); 401 | if (!clampDecls.length) return; 402 | const containerValues = Object.values(containerScreens); 403 | if (parentAtRule) { 404 | const currentParams = atRule.params; 405 | const parentParams = parentAtRule.params; 406 | const minContainer = extractMinValue(parentParams) || extractMinValue(currentParams); 407 | const maxContainer = extractMaxValue(parentParams) || extractMaxValue(currentParams); 408 | if (minContainer && maxContainer) { 409 | clampDecls.forEach((decl) => { 410 | processClampDeclaration(decl, minContainer, maxContainer, true); 411 | }); 412 | } 413 | } else { 414 | const currentParams = atRule.params; 415 | const minValue = extractMinValue(currentParams); 416 | const maxValue = extractMaxValue(currentParams); 417 | if (minValue && !maxValue) { 418 | const minContainer = minValue; 419 | const maxContainer = containerValues[containerValues.length - 1]; 420 | clampDecls.forEach((decl) => { 421 | processClampDeclaration(decl, minContainer, maxContainer, true); 422 | }); 423 | } else if (maxValue && !minValue) { 424 | const minContainer = containerValues[0]; 425 | const maxContainer = maxValue; 426 | clampDecls.forEach((decl) => { 427 | processClampDeclaration(decl, minContainer, maxContainer, true); 428 | }); 429 | } else if (minValue && maxValue) { 430 | clampDecls.forEach((decl) => { 431 | processClampDeclaration(decl, minValue, maxValue, true); 432 | }); 433 | } 434 | } 435 | }; 436 | return { 437 | // Use OnceExit to ensure Tailwind has generated its content 438 | OnceExit(root, { result: result2 }) { 439 | collectConfig(root); 440 | finalizeConfig(); 441 | const processedAtRules = /* @__PURE__ */ new WeakSet(); 442 | root.walkAtRules("media", (atRule) => { 443 | if (processedAtRules.has(atRule)) return; 444 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 445 | if (hasNestedMedia) { 446 | atRule.walkAtRules("media", (nestedAtRule) => { 447 | processedAtRules.add(nestedAtRule); 448 | processMediaQuery(nestedAtRule, atRule); 449 | }); 450 | } else if (isNested) { 451 | return; 452 | } else { 453 | processMediaQuery(atRule); 454 | } 455 | }); 456 | root.walkAtRules("container", (atRule) => { 457 | if (processedAtRules.has(atRule)) return; 458 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 459 | if (hasNestedMedia) { 460 | atRule.walkAtRules("container", (nestedAtRule) => { 461 | processedAtRules.add(nestedAtRule); 462 | processContainerQuery(nestedAtRule, atRule); 463 | }); 464 | } else if (isNested) { 465 | return; 466 | } else { 467 | processContainerQuery(atRule); 468 | } 469 | }); 470 | root.walkRules((rule) => { 471 | let parent = rule.parent; 472 | while (parent) { 473 | if (parent.type === "atrule" && (parent.name === "media" || parent.name === "container")) { 474 | return; 475 | } 476 | parent = parent.parent; 477 | } 478 | const clampDecls = []; 479 | rule.walkDecls((decl) => { 480 | if (extractTwoValidClampArgs(decl.value)) { 481 | clampDecls.push(decl); 482 | } 483 | }); 484 | if (clampDecls.length === 0) return; 485 | const screenValues = Object.values(screens); 486 | const minScreen = defaultClampRange.min || screenValues[0]; 487 | const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1]; 488 | clampDecls.forEach((decl) => { 489 | processClampDeclaration(decl, minScreen, maxScreen, false); 490 | }); 491 | }); 492 | } 493 | }; 494 | } 495 | }; 496 | }; 497 | clampwind.postcss = true; 498 | var clampwind_default = clampwind; 499 | export { 500 | clampwind_default as default 501 | }; 502 | -------------------------------------------------------------------------------- /dist/clampwind.cjs.cjs: -------------------------------------------------------------------------------- 1 | var __defProp = Object.defineProperty; 2 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 3 | var __getOwnPropNames = Object.getOwnPropertyNames; 4 | var __hasOwnProp = Object.prototype.hasOwnProperty; 5 | var __export = (target, all) => { 6 | for (var name in all) 7 | __defProp(target, name, { get: all[name], enumerable: true }); 8 | }; 9 | var __copyProps = (to, from, except, desc) => { 10 | if (from && typeof from === "object" || typeof from === "function") { 11 | for (let key of __getOwnPropNames(from)) 12 | if (!__hasOwnProp.call(to, key) && key !== except) 13 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 14 | } 15 | return to; 16 | }; 17 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 18 | 19 | // src/clampwind.js 20 | var clampwind_exports = {}; 21 | __export(clampwind_exports, { 22 | default: () => clampwind_default 23 | }); 24 | module.exports = __toCommonJS(clampwind_exports); 25 | 26 | // src/screens.js 27 | var defaultScreens = { 28 | sm: "40rem", 29 | // 640px 30 | md: "48rem", 31 | // 768px 32 | lg: "64rem", 33 | // 1024px 34 | xl: "80rem", 35 | // 1280px 36 | "2xl": "96rem" 37 | // 1536px 38 | }; 39 | var defaultContainerScreens = { 40 | "@3xs": "16rem", 41 | // 256px 42 | "@2xs": "18rem", 43 | // 288px 44 | "@xs": "20rem", 45 | // 320px 46 | "@sm": "24rem", 47 | // 384px 48 | "@md": "28rem", 49 | // 448px 50 | "@lg": "32rem", 51 | // 512px 52 | "@xl": "36rem", 53 | // 576px 54 | "@2xl": "42rem", 55 | // 672px 56 | "@3xl": "48rem", 57 | // 768px 58 | "@4xl": "56rem", 59 | // 896px 60 | "@5xl": "64rem", 61 | // 1024px 62 | "@6xl": "72rem", 63 | // 1152px 64 | "@7xl": "80rem" 65 | // 1280px 66 | }; 67 | 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 | }; 74 | var extractTwoValidClampArgs = (value) => { 75 | const m = value.match(/\bclamp\s*\(\s*(var\([^()]+\)|[^,()]+)\s*,\s*(var\([^()]+\)|[^,()]+)\s*\)$/); 76 | return m ? [m[1].trim(), m[2].trim()] : null; 77 | }; 78 | var extractUnit = (value) => { 79 | const trimmedValue = value.replace(/\s+/g, ""); 80 | if (trimmedValue.includes("--")) { 81 | const match = trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1"); 82 | return match ? match : null; 83 | } else { 84 | const match = trimmedValue.match(/\D+$/); 85 | return match ? match[0] : null; 86 | } 87 | }; 88 | var formatProperty = (value) => { 89 | const trimmedValue = value.replace(/\s+/g, ""); 90 | return trimmedValue.replace(/var\(([^,)]+)[^)]*\)/, "$1"); 91 | }; 92 | var convertToRem = (value, rootFontSize, spacingSize, customProperties = {}) => { 93 | const unit = extractUnit(value); 94 | const formattedProperty = formatProperty(value); 95 | const fallbackValue = value.includes("var(") && value.includes(",") ? value.replace(/var\([^,]+,\s*([^)]+)\)/, "$1") : null; 96 | if (!unit) { 97 | const spacingSizeInt = parseFloat(spacingSize); 98 | const spacingUnit = extractUnit(spacingSize); 99 | if (spacingUnit === "px") { 100 | return `${smartRound(value * spacingSizeInt / rootFontSize)}rem`; 101 | } 102 | if (spacingUnit === "rem") { 103 | return `${smartRound(value * spacingSizeInt)}rem`; 104 | } 105 | } 106 | if (unit === "px") { 107 | return `${smartRound(value.replace("px", "") / rootFontSize)}rem`; 108 | } 109 | if (unit === "rem") { 110 | return value; 111 | } 112 | if (customProperties[formattedProperty]) { 113 | return customProperties[formattedProperty]; 114 | } 115 | if (formattedProperty && !customProperties[formattedProperty] && fallbackValue) { 116 | const fallbackUnit = extractUnit(fallbackValue); 117 | if (!fallbackUnit) { 118 | return `${smartRound(fallbackValue * spacingSize)}rem`; 119 | } 120 | if (fallbackUnit === "px") { 121 | return `${smartRound(fallbackValue.replace("px", "") / rootFontSize)}rem`; 122 | } 123 | if (fallbackUnit === "rem") { 124 | return fallbackValue; 125 | } 126 | } 127 | return null; 128 | }; 129 | var generateClamp = (lower, upper, minScreen, maxScreen, rootFontSize = 16, spacingSize = "1px", containerQuery = false) => { 130 | const maxScreenInt = parseFloat( 131 | convertToRem(maxScreen, rootFontSize, spacingSize) 132 | ); 133 | const minScreenInt = parseFloat( 134 | convertToRem(minScreen, rootFontSize, spacingSize) 135 | ); 136 | const lowerInt = parseFloat(lower); 137 | const upperInt = parseFloat(upper); 138 | const isDescending = lowerInt > upperInt; 139 | const min = isDescending ? upper : lower; 140 | const max = isDescending ? lower : upper; 141 | const widthUnit = containerQuery ? `100cqw` : `100vw`; 142 | const slopeInt = smartRound((upperInt - lowerInt) / (maxScreenInt - minScreenInt)); 143 | const clamp = `clamp(${min}, calc(${lower} + ${slopeInt} * (${widthUnit} - ${minScreen})), ${max})`; 144 | return clamp; 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 | }; 157 | var extractMaxValue = (params) => { 158 | if (!params) return null; 159 | let match = params.match(/<\s*([^),\s]+)/); 160 | if (match) return match[1].trim(); 161 | match = params.match(/max-width:\s*([^),\s]+)/); 162 | if (match) return match[1].trim(); 163 | match = params.match(/not\s+all\s+and\s*\(\s*min-width:\s*([^),\s]+)\s*\)/); 164 | if (match) return match[1].trim(); 165 | return null; 166 | }; 167 | var extractMinValue = (params) => { 168 | if (!params) return null; 169 | let match = params.match(/>=?\s*([^),\s]+)/); 170 | if (match) return match[1].trim(); 171 | match = params.match(/min-width:\s*([^),\s]+)/); 172 | if (match) return match[1].trim(); 173 | match = params.match(/not\s+all\s+and\s*\(\s*max-width:\s*([^),\s]+)\s*\)/); 174 | if (match) { 175 | return match[1].trim(); 176 | } 177 | return null; 178 | }; 179 | 180 | // src/clampwind.js 181 | var clampwind = (opts = {}) => { 182 | return { 183 | postcssPlugin: "postcss-clampwind", 184 | prepare(result) { 185 | let rootFontSize = 16; 186 | let spacingSize = "0.25rem"; 187 | let customProperties = {}; 188 | let screens = defaultScreens || {}; 189 | let containerScreens = defaultContainerScreens || {}; 190 | let defaultClampRange = {}; 191 | const config = { 192 | themeLayerBreakpoints: {}, 193 | themeLayerContainerBreakpoints: {}, 194 | rootElementBreakpoints: {}, 195 | rootElementContainerBreakpoints: {}, 196 | configCollected: false, 197 | configReady: false 198 | }; 199 | const collectConfig = (root) => { 200 | if (config.configCollected) return; 201 | root.walkDecls((decl) => { 202 | if (decl.parent?.selector === ":root") { 203 | if (decl.prop === "font-size" && decl.value.includes("px")) { 204 | rootFontSize = parseFloat(decl.value); 205 | } 206 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 207 | rootFontSize = parseFloat(decl.value); 208 | } 209 | } 210 | }); 211 | root.walkDecls((decl) => { 212 | if (decl.parent?.selector === ":root") { 213 | if (decl.prop.startsWith("--breakpoint-")) { 214 | const key = decl.prop.replace("--breakpoint-", ""); 215 | config.rootElementBreakpoints[key] = convertToRem( 216 | decl.value, 217 | rootFontSize, 218 | spacingSize, 219 | customProperties 220 | ); 221 | } 222 | if (decl.prop.startsWith("--container-")) { 223 | const key = decl.prop.replace("--container-", "@"); 224 | config.rootElementContainerBreakpoints[key] = convertToRem( 225 | decl.value, 226 | rootFontSize, 227 | spacingSize, 228 | customProperties 229 | ); 230 | } 231 | if (decl.prop === "--breakpoint-clamp-min") { 232 | defaultClampRange.min = convertToRem( 233 | decl.value, 234 | rootFontSize, 235 | spacingSize, 236 | customProperties 237 | ); 238 | } 239 | if (decl.prop === "--breakpoint-clamp-max") { 240 | defaultClampRange.max = convertToRem( 241 | decl.value, 242 | rootFontSize, 243 | spacingSize, 244 | customProperties 245 | ); 246 | } 247 | if (decl.prop === "--spacing") { 248 | spacingSize = decl.value; 249 | } 250 | if (decl.prop.startsWith("--")) { 251 | const value = convertToRem( 252 | decl.value, 253 | rootFontSize, 254 | spacingSize, 255 | customProperties 256 | ); 257 | if (value) customProperties[decl.prop] = value; 258 | } 259 | } 260 | }); 261 | root.walkAtRules("layer", (atRule) => { 262 | if (atRule.params === "theme") { 263 | atRule.walkDecls((decl) => { 264 | if (decl.prop === "--text-base" && decl.value.includes("px")) { 265 | rootFontSize = parseFloat(decl.value); 266 | } 267 | }); 268 | } 269 | }); 270 | root.walkAtRules("layer", (atRule) => { 271 | if (atRule.params === "theme") { 272 | atRule.walkDecls((decl) => { 273 | if (decl.prop.startsWith("--breakpoint-")) { 274 | const key = decl.prop.replace("--breakpoint-", ""); 275 | config.themeLayerBreakpoints[key] = convertToRem( 276 | decl.value, 277 | rootFontSize, 278 | spacingSize, 279 | customProperties 280 | ); 281 | } 282 | if (decl.prop.startsWith("--container-")) { 283 | const key = decl.prop.replace("--container-", "@"); 284 | config.themeLayerContainerBreakpoints[key] = convertToRem( 285 | decl.value, 286 | rootFontSize, 287 | spacingSize, 288 | customProperties 289 | ); 290 | } 291 | if (decl.prop === "--breakpoint-clamp-min") { 292 | defaultClampRange.min = convertToRem( 293 | decl.value, 294 | rootFontSize, 295 | spacingSize, 296 | customProperties 297 | ); 298 | } 299 | if (decl.prop === "--breakpoint-clamp-max") { 300 | defaultClampRange.max = convertToRem( 301 | decl.value, 302 | rootFontSize, 303 | spacingSize, 304 | customProperties 305 | ); 306 | } 307 | if (decl.prop === "--spacing") { 308 | spacingSize = decl.value; 309 | } 310 | if (decl.prop.startsWith("--")) { 311 | const value = convertToRem( 312 | decl.value, 313 | rootFontSize, 314 | spacingSize, 315 | customProperties 316 | ); 317 | if (value) customProperties[decl.prop] = value; 318 | } 319 | }); 320 | } 321 | }); 322 | config.configCollected = true; 323 | }; 324 | const finalizeConfig = () => { 325 | if (config.configReady) return; 326 | screens = Object.assign( 327 | {}, 328 | screens, 329 | config.rootElementBreakpoints, 330 | config.themeLayerBreakpoints 331 | ); 332 | screens = sortScreens(screens); 333 | containerScreens = Object.assign( 334 | {}, 335 | containerScreens, 336 | config.rootElementContainerBreakpoints, 337 | config.themeLayerContainerBreakpoints 338 | ); 339 | containerScreens = sortScreens(containerScreens); 340 | config.configReady = true; 341 | }; 342 | const processClampDeclaration = (decl, minScreen, maxScreen, isContainer = false) => { 343 | const args = extractTwoValidClampArgs(decl.value); 344 | const [lower, upper] = args.map( 345 | (val) => convertToRem(val, rootFontSize, spacingSize, customProperties) 346 | ); 347 | if (!args || !lower || !upper) { 348 | result.warn( 349 | `Invalid clamp() values: "${decl.value}". Expected format: clamp(min, preferred, max)`, 350 | { 351 | node: decl, 352 | word: decl.value 353 | } 354 | ); 355 | decl.value = `${decl.value} /* Invalid clamp() values */`; 356 | return true; 357 | } 358 | const clamp = generateClamp( 359 | lower, 360 | upper, 361 | minScreen, 362 | maxScreen, 363 | rootFontSize, 364 | spacingSize, 365 | isContainer 366 | ); 367 | decl.value = clamp; 368 | return true; 369 | }; 370 | const getNestedStructure = (atRule) => { 371 | const isNested = atRule.parent?.type === "atrule" && atRule.parent?.name === "media"; 372 | const hasNestedMedia = atRule.nodes?.some( 373 | (node) => node.type === "atrule" && node.name === "media" 374 | ); 375 | return { isNested, hasNestedMedia }; 376 | }; 377 | const processMediaQuery = (atRule, parentAtRule = null) => { 378 | const clampDecls = []; 379 | atRule.walkDecls((decl) => { 380 | if (extractTwoValidClampArgs(decl.value)) { 381 | clampDecls.push(decl); 382 | } 383 | }); 384 | if (!clampDecls.length) return; 385 | const screenValues = Object.values(screens); 386 | if (parentAtRule) { 387 | const currentParams = atRule.params; 388 | const parentParams = parentAtRule.params; 389 | const minScreen = extractMinValue(parentParams) || extractMinValue(currentParams); 390 | const maxScreen = extractMaxValue(parentParams) || extractMaxValue(currentParams); 391 | if (minScreen && maxScreen) { 392 | clampDecls.forEach((decl) => { 393 | processClampDeclaration(decl, minScreen, maxScreen, false); 394 | }); 395 | } 396 | } else { 397 | const currentParams = atRule.params; 398 | const minValue = extractMinValue(currentParams); 399 | const maxValue = extractMaxValue(currentParams); 400 | if (minValue && !maxValue) { 401 | const minScreen = minValue; 402 | const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1]; 403 | clampDecls.forEach((decl) => { 404 | processClampDeclaration(decl, minScreen, maxScreen, false); 405 | }); 406 | } else if (maxValue && !minValue) { 407 | const minScreen = defaultClampRange.min || screenValues[0]; 408 | const maxScreen = maxValue; 409 | clampDecls.forEach((decl) => { 410 | processClampDeclaration(decl, minScreen, maxScreen, false); 411 | }); 412 | } else if (minValue && maxValue) { 413 | clampDecls.forEach((decl) => { 414 | processClampDeclaration(decl, minValue, maxValue, false); 415 | }); 416 | } 417 | } 418 | }; 419 | const processContainerQuery = (atRule, parentAtRule = null) => { 420 | const clampDecls = []; 421 | atRule.walkDecls((decl) => { 422 | if (extractTwoValidClampArgs(decl.value)) { 423 | clampDecls.push(decl); 424 | } 425 | }); 426 | if (!clampDecls.length) return; 427 | const containerValues = Object.values(containerScreens); 428 | if (parentAtRule) { 429 | const currentParams = atRule.params; 430 | const parentParams = parentAtRule.params; 431 | const minContainer = extractMinValue(parentParams) || extractMinValue(currentParams); 432 | const maxContainer = extractMaxValue(parentParams) || extractMaxValue(currentParams); 433 | if (minContainer && maxContainer) { 434 | clampDecls.forEach((decl) => { 435 | processClampDeclaration(decl, minContainer, maxContainer, true); 436 | }); 437 | } 438 | } else { 439 | const currentParams = atRule.params; 440 | const minValue = extractMinValue(currentParams); 441 | const maxValue = extractMaxValue(currentParams); 442 | if (minValue && !maxValue) { 443 | const minContainer = minValue; 444 | const maxContainer = containerValues[containerValues.length - 1]; 445 | clampDecls.forEach((decl) => { 446 | processClampDeclaration(decl, minContainer, maxContainer, true); 447 | }); 448 | } else if (maxValue && !minValue) { 449 | const minContainer = containerValues[0]; 450 | const maxContainer = maxValue; 451 | clampDecls.forEach((decl) => { 452 | processClampDeclaration(decl, minContainer, maxContainer, true); 453 | }); 454 | } else if (minValue && maxValue) { 455 | clampDecls.forEach((decl) => { 456 | processClampDeclaration(decl, minValue, maxValue, true); 457 | }); 458 | } 459 | } 460 | }; 461 | return { 462 | // Use OnceExit to ensure Tailwind has generated its content 463 | OnceExit(root, { result: result2 }) { 464 | collectConfig(root); 465 | finalizeConfig(); 466 | const processedAtRules = /* @__PURE__ */ new WeakSet(); 467 | root.walkAtRules("media", (atRule) => { 468 | if (processedAtRules.has(atRule)) return; 469 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 470 | if (hasNestedMedia) { 471 | atRule.walkAtRules("media", (nestedAtRule) => { 472 | processedAtRules.add(nestedAtRule); 473 | processMediaQuery(nestedAtRule, atRule); 474 | }); 475 | } else if (isNested) { 476 | return; 477 | } else { 478 | processMediaQuery(atRule); 479 | } 480 | }); 481 | root.walkAtRules("container", (atRule) => { 482 | if (processedAtRules.has(atRule)) return; 483 | const { isNested, hasNestedMedia } = getNestedStructure(atRule); 484 | if (hasNestedMedia) { 485 | atRule.walkAtRules("container", (nestedAtRule) => { 486 | processedAtRules.add(nestedAtRule); 487 | processContainerQuery(nestedAtRule, atRule); 488 | }); 489 | } else if (isNested) { 490 | return; 491 | } else { 492 | processContainerQuery(atRule); 493 | } 494 | }); 495 | root.walkRules((rule) => { 496 | let parent = rule.parent; 497 | while (parent) { 498 | if (parent.type === "atrule" && (parent.name === "media" || parent.name === "container")) { 499 | return; 500 | } 501 | parent = parent.parent; 502 | } 503 | const clampDecls = []; 504 | rule.walkDecls((decl) => { 505 | if (extractTwoValidClampArgs(decl.value)) { 506 | clampDecls.push(decl); 507 | } 508 | }); 509 | if (clampDecls.length === 0) return; 510 | const screenValues = Object.values(screens); 511 | const minScreen = defaultClampRange.min || screenValues[0]; 512 | const maxScreen = defaultClampRange.max || screenValues[screenValues.length - 1]; 513 | clampDecls.forEach((decl) => { 514 | processClampDeclaration(decl, minScreen, maxScreen, false); 515 | }); 516 | }); 517 | } 518 | }; 519 | } 520 | }; 521 | }; 522 | clampwind.postcss = true; 523 | var clampwind_default = clampwind; 524 | --------------------------------------------------------------------------------