Repository: francoismassart/eslint-plugin-tailwindcss (js)

Issue:
[Feature request] classnames order, group by responsive variant
Right now the ordering in applied in this order:

1. Affected property (e.g. `flex`, `padding`...)
2. Responsive variant (e.g. `sm:`, `md:`, `lg:`...)
3. Theme variant (e.g. `dark:`...)
4. State variant (e.g. `hover:`, `disabled:`...)

e.g. `<div class="p-4 sm:p-6 lg:p-8 rounded sm:rounded-lg lg:rounded-2xl">...</div>`

While I like this approach, some user might prefer grouping by responsive variant first like so:

1. Responsive variant (e.g. `sm:`, `md:`, `lg:`...)
2. Affected property (e.g. `flex`, `padding`...)
3. Theme variant (e.g. `dark:`...)
4. State variant (e.g. `hover:`, `disabled:`...)

e.g. `<div class="p-4 rounded sm:p-6 sm:rounded-lg lg:p-8 lg:rounded-2xl">...</div>`

Both looks fine to me, I would add an optional parameter `groupByResponsive` set to `false` in order to keep using current order rules by default.

What do you think about this feature ?

Pull request description:
feat(classnames-order): group by responsive variant
# Reorder by grouping responsive variants first

## Description

Reading the thread from @adamwathan about [the best way to structure your classes and markup with Tailwind CSS](https://threadreaderapp.com/thread/1399473267606528005.html) I though people might want to 

> Group responsive classes together by screen size (...)

setting the option `groupByResponsive` to `true` will do just that and then within each responsive variant group, it'll order classnames as you would expect.

Fixes #30 

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

## How Has This Been Tested?

- `npm test`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have checked my code and corrected any misspellings


Patch:
diff --git a/docs/rules/classnames-order.md b/docs/rules/classnames-order.md
index 7b7baf8..ef20c15 100644
--- a/docs/rules/classnames-order.md
+++ b/docs/rules/classnames-order.md
@@ -25,6 +25,7 @@ Examples of **correct** code for this rule:
 "tailwindcss/classnames-order": [<enabled>, {
   "callees": Array<string>,
   "config": <string>|<object>,
+  "groupByResponsive": <boolean>,
   "groups": Array<object>,
   "prependCustom": <boolean>,
   "removeDuplicates": <boolean>
@@ -52,6 +53,20 @@ It is also possible to directly inject a configuration as plain `object` like `{
 
 Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js).
 
+### `groupByResponsive` (default: `false`)
+
+Linting this code:
+
+`<div class="rounded sm:rounded-lg lg:rounded-2xl p-4 sm:p-6 lg:p-8">...</div>`
+
+By default, the ordering process will group the classnames by properties, then by variants:
+
+`<div class="p-4 sm:p-6 lg:p-8 rounded sm:rounded-lg lg:rounded-2xl">...</div>`
+
+Set `groupByResponsive` to `true` and the ordering and you will get:
+
+`<div class="p-4 rounded sm:p-6 sm:rounded-lg lg:p-8 lg:rounded-2xl">...</div>`
+
 ### `groups` (default defined in [groups.js](../../lib/config/groups.js))
 
 If you really need to, you can write your own configuration.
diff --git a/lib/rules/classnames-order.js b/lib/rules/classnames-order.js
index 36bed07..db4b636 100644
--- a/lib/rules/classnames-order.js
+++ b/lib/rules/classnames-order.js
@@ -67,6 +67,7 @@ module.exports = {
     const callees = getOption(context, 'callees');
     const twConfig = getOption(context, 'config');
     const groupsConfig = getOption(context, 'groups');
+    const groupByResponsive = getOption(context, 'groupByResponsive');
     const prependCustom = getOption(context, 'prependCustom');
     const removeDuplicates = getOption(context, 'removeDuplicates');
 
@@ -106,6 +107,23 @@ module.exports = {
       }
       return str;
     };
+    /**
+     * Generate an Array of Array, grouping class by responsive variants
+     * @param {Array} classNames
+     * @returns {Array} an Array (one entry per responsive variant), each entry is an array of classnames
+     */
+    const getResponsiveGroups = (classNames) => {
+      const responsiveVariants = Object.keys(mergedConfig.theme.screens);
+      const classnamesByResponsive = [[]];
+      responsiveVariants.forEach((prefix) => {
+        classnamesByResponsive.push([]);
+      });
+      classNames.forEach((cls) => {
+        const idx = parseInt(getSpecificity(attrUtil.cleanClassname(cls), responsiveVariants, true), 10);
+        classnamesByResponsive[idx].push(cls);
+      });
+      return classnamesByResponsive;
+    };
 
     /**
      * Parse each classname and populate the `sorted` and `extra` arrays
@@ -259,11 +277,24 @@ module.exports = {
       }
 
       // Sorting
-      const { sorted, extras } = getSortedGroups(classNames);
+      const mergedSorted = [];
+      const mergedExtras = [];
+      if (groupByResponsive) {
+        const respGroups = getResponsiveGroups(classNames);
+        respGroups.forEach((clsGroup) => {
+          const { sorted, extras } = getSortedGroups(clsGroup);
+          mergedSorted.push(...sorted);
+          mergedExtras.push(...extras);
+        });
+      } else {
+        const { sorted, extras } = getSortedGroups(classNames);
+        mergedSorted.push(...sorted);
+        mergedExtras.push(...extras);
+      }
 
       // Generates the validated/sorted attribute value
-      const flatted = sorted.flat();
-      const union = prependCustom ? [...extras, ...flatted] : [...flatted, ...extras];
+      const flatted = mergedSorted.flat();
+      const union = prependCustom ? [...mergedExtras, ...flatted] : [...flatted, ...mergedExtras];
       if (before !== null) {
         union.unshift(before);
       }
diff --git a/lib/util/settings.js b/lib/util/settings.js
index 0613d57..0efe5bc 100644
--- a/lib/util/settings.js
+++ b/lib/util/settings.js
@@ -19,6 +19,8 @@ function getOption(context, name) {
       return 'tailwind.config.js';
     case 'cssFiles':
       return ['**/*.css', '!**/node_modules'];
+    case 'groupByResponsive':
+      return false;
     case 'groups':
       return defaultGroups;
     case 'prependCustom':


Test patch:
diff --git a/tests/lib/rules/classnames-order.js b/tests/lib/rules/classnames-order.js
index cc4aa24..4305dc7 100644
--- a/tests/lib/rules/classnames-order.js
+++ b/tests/lib/rules/classnames-order.js
@@ -39,6 +39,14 @@ ruleTester.run("classnames-order", rule, {
     {
       code: `<div class='box-content lg:box-border'>Simple quotes</div>`,
     },
+    {
+      code: `<div class="p-4 rounded sm:p-6 sm:rounded-lg lg:p-8 lg:rounded-2xl">groupByResponsive</div>`,
+      options: [
+        {
+          groupByResponsive: true,
+        },
+      ],
+    },
     {
       code: `<div class="p-5 lg:p-4 md:py-2 sm:px-3 xl:px-6">'p', then 'py' then 'px'</div>`,
     },