Repository: tailwindlabs/tailwindcss

Problem statement:
[Feature Request] Support negative prefix syntax in config for letterSpacing and boxShadow
### Background
When I'm using letter spacing utilities, I prefer numerical class names like `tracking-1`, `tracking-2`, etc. instead of `wide`, `wider`, etc. 

I also like to set my negative letter spacing values to `-tracking-1`, `-tracking-2`, etc.—the negative prefix makes it more explicit that I'm shrinking the font's normal letter spacing.

Same goes for box shadows—I prefer `shadow-1`, `shadow-2`, etc., and I dig `-shadow` instead of `shadow-inner`. Also feels cleaner to do something like `-shadow-1`, `-shadow-2`, etc. instead of `shadow-inner-md`, `shadow-inner-lg`, etc.

### Problem

For `zIndex`, `margin`, and `inset`, you can easily create these prefixed classes in your config like so:

```
zIndex: {
  '10': '10', // z-10
  '-10': '-10' // -z-10
}
```

But the negative prefix syntax isn't supported for any other utilities (I edit Tailwind's source code in my projects to get it working).

### Solution

Support negative prefix syntax for `letterSpacing` and `boxShadow`. Looking through the other plugins, it might make sense to add support for `lineHeight` too.

If this is considered a breaking change, it's probably not worth it. Otherwise, I'd be happy to make the pull request.


Hints:
Happy to accept a PR for this, it's probably technically a breaking change but it's just as much a bugfix IMO 👍 

Patch:
diff --git a/src/plugins/boxShadow.js b/src/plugins/boxShadow.js
--- a/src/plugins/boxShadow.js
+++ b/src/plugins/boxShadow.js
@@ -1,12 +1,14 @@
 import _ from 'lodash'
+import prefixNegativeModifiers from '../util/prefixNegativeModifiers'
 
 export default function() {
   return function({ addUtilities, e, theme, variants }) {
     const utilities = _.fromPairs(
       _.map(theme('boxShadow'), (value, modifier) => {
-        const className = modifier === 'default' ? 'shadow' : `shadow-${modifier}`
+        const className =
+          modifier === 'default' ? 'shadow' : `${e(prefixNegativeModifiers('shadow', modifier))}`
         return [
-          `.${e(className)}`,
+          `.${className}`,
           {
             'box-shadow': value,
           },
diff --git a/src/plugins/letterSpacing.js b/src/plugins/letterSpacing.js
--- a/src/plugins/letterSpacing.js
+++ b/src/plugins/letterSpacing.js
@@ -1,11 +1,12 @@
 import _ from 'lodash'
+import prefixNegativeModifiers from '../util/prefixNegativeModifiers'
 
 export default function() {
   return function({ addUtilities, theme, variants, e }) {
     const utilities = _.fromPairs(
       _.map(theme('letterSpacing'), (value, modifier) => {
         return [
-          `.${e(`tracking-${modifier}`)}`,
+          `.${e(prefixNegativeModifiers('tracking', modifier))}`,
           {
             'letter-spacing': value,
           },
diff --git a/src/util/prefixNegativeModifiers.js b/src/util/prefixNegativeModifiers.js
--- a/src/util/prefixNegativeModifiers.js
+++ b/src/util/prefixNegativeModifiers.js
@@ -1,5 +1,11 @@
 import _ from 'lodash'
 
 export default function prefixNegativeModifiers(base, modifier) {
-  return _.startsWith(modifier, '-') ? `-${base}-${modifier.slice(1)}` : `${base}-${modifier}`
+  if (modifier === '-') {
+    return `-${base}`
+  } else if (_.startsWith(modifier, '-')) {
+    return `-${base}-${modifier.slice(1)}`
+  } else {
+    return `${base}-${modifier}`
+  }
 }


Test patch:
diff --git a/__tests__/plugins/boxShadow.test.js b/__tests__/plugins/boxShadow.test.js
new file mode 100644
--- /dev/null
+++ b/__tests__/plugins/boxShadow.test.js
@@ -0,0 +1,63 @@
+import _ from 'lodash'
+import escapeClassName from '../../src/util/escapeClassName'
+import plugin from '../../src/plugins/boxShadow'
+
+test('box shadow can use default keyword and negative prefix syntax', () => {
+  const addedUtilities = []
+
+  const config = {
+    theme: {
+      boxShadow: {
+        default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
+        md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+        '-': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
+        '-md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+      },
+    },
+    variants: {
+      boxShadow: ['responsive'],
+    },
+  }
+
+  const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
+  const pluginApi = {
+    config: getConfigValue,
+    e: escapeClassName,
+    theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
+    variants: (path, defaultValue) => {
+      if (_.isArray(config.variants)) {
+        return config.variants
+      }
+
+      return getConfigValue(`variants.${path}`, defaultValue)
+    },
+    addUtilities(utilities, variants) {
+      addedUtilities.push({
+        utilities,
+        variants,
+      })
+    },
+  }
+
+  plugin()(pluginApi)
+
+  expect(addedUtilities).toEqual([
+    {
+      utilities: {
+        '.shadow': {
+          'box-shadow': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
+        },
+        '.shadow-md': {
+          'box-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+        },
+        '.-shadow': {
+          'box-shadow': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
+        },
+        '.-shadow-md': {
+          'box-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+        },
+      },
+      variants: ['responsive'],
+    },
+  ])
+})
diff --git a/__tests__/plugins/letterSpacing.test.js b/__tests__/plugins/letterSpacing.test.js
new file mode 100644
--- /dev/null
+++ b/__tests__/plugins/letterSpacing.test.js
@@ -0,0 +1,51 @@
+import _ from 'lodash'
+import escapeClassName from '../../src/util/escapeClassName'
+import plugin from '../../src/plugins/letterSpacing'
+
+test('letter spacing can use negative prefix syntax', () => {
+  const addedUtilities = []
+
+  const config = {
+    theme: {
+      letterSpacing: {
+        '-1': '-0.025em',
+        '1': '0.025em',
+      },
+    },
+    variants: {
+      letterSpacing: ['responsive'],
+    },
+  }
+
+  const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
+  const pluginApi = {
+    config: getConfigValue,
+    e: escapeClassName,
+    theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
+    variants: (path, defaultValue) => {
+      if (_.isArray(config.variants)) {
+        return config.variants
+      }
+
+      return getConfigValue(`variants.${path}`, defaultValue)
+    },
+    addUtilities(utilities, variants) {
+      addedUtilities.push({
+        utilities,
+        variants,
+      })
+    },
+  }
+
+  plugin()(pluginApi)
+
+  expect(addedUtilities).toEqual([
+    {
+      utilities: {
+        '.-tracking-1': { 'letter-spacing': '-0.025em' },
+        '.tracking-1': { 'letter-spacing': '0.025em' },
+      },
+      variants: ['responsive'],
+    },
+  ])
+})
diff --git a/__tests__/plugins/zIndex.test.js b/__tests__/plugins/zIndex.test.js
new file mode 100644
--- /dev/null
+++ b/__tests__/plugins/zIndex.test.js
@@ -0,0 +1,55 @@
+import _ from 'lodash'
+import escapeClassName from '../../src/util/escapeClassName'
+import plugin from '../../src/plugins/zIndex'
+
+test('z index can use negative prefix syntax', () => {
+  const addedUtilities = []
+
+  const config = {
+    theme: {
+      zIndex: {
+        '-20': '-20',
+        '-10': '-10',
+        '10': '10',
+        '20': '20',
+      },
+    },
+    variants: {
+      zIndex: ['responsive'],
+    },
+  }
+
+  const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
+  const pluginApi = {
+    config: getConfigValue,
+    e: escapeClassName,
+    theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
+    variants: (path, defaultValue) => {
+      if (_.isArray(config.variants)) {
+        return config.variants
+      }
+
+      return getConfigValue(`variants.${path}`, defaultValue)
+    },
+    addUtilities(utilities, variants) {
+      addedUtilities.push({
+        utilities,
+        variants,
+      })
+    },
+  }
+
+  plugin()(pluginApi)
+
+  expect(addedUtilities).toEqual([
+    {
+      utilities: {
+        '.-z-20': { 'z-index': '-20' },
+        '.-z-10': { 'z-index': '-10' },
+        '.z-10': { 'z-index': '10' },
+        '.z-20': { 'z-index': '20' },
+      },
+      variants: ['responsive'],
+    },
+  ])
+})
diff --git a/__tests__/prefixNegativeModifiers.test.js b/__tests__/prefixNegativeModifiers.test.js
new file mode 100644
--- /dev/null
+++ b/__tests__/prefixNegativeModifiers.test.js
@@ -0,0 +1,13 @@
+import prefixNegativeModifiers from '../src/util/prefixNegativeModifiers'
+
+test('it does not prefix classes using standard syntax', () => {
+  expect(prefixNegativeModifiers('base', 'modifier')).toEqual('base-modifier')
+})
+
+test('it prefixes classes using negative syntax', () => {
+  expect(prefixNegativeModifiers('base', '-modifier')).toEqual('-base-modifier')
+})
+
+test('it prefixes classes and omits suffix using default negative syntax', () => {
+  expect(prefixNegativeModifiers('base', '-')).toEqual('-base')
+})