JNavith / tailwindcss-theme-variants

Media-query- or JavaScript-based theme variants with fallback for Tailwind CSS
https://tailwindcss-theme-variants.web.app/
MIT License
188 stars 5 forks source link

"Semantics" API design thoughts #4

Closed JNavith closed 3 years ago

JNavith commented 3 years ago

Classes like primary=gray-900 will set the custom property --primary: theme("colors.gray-900"); so all the children of that node can use bg-primary or text-primary

Candidates for configuration in the plugin:

  1. Explain what type it can be and require the plugin to look it up. This means there will have to be pre-determined, finite types.

    // other options
    semantics: {
    "primary": { type: "color" },
    "secondary": { type: "color" },
    },
  2. Provide a (nested) object of all values. This sounds flexible, but now there is no connection between the CSS property(ies) being / that could be set and the values they can be set to. (Because Tailwind config now needs to be extended by the plugin to create the named utilities)

    // other options
    semantics: {
    "primary": theme("colors"), // need a reference to theme somehow
    "secondary": theme("colors"),
    },
  3. colorProperties can be an array listing off borderColor, textColor, backgroundColor, ..., so the plugin knows to look up each of those things and (1) make it possible to do primary=blue-500 to assign a semantic name to one of the values in those (merged?) theme values and (2) make it possible to use bg-primary. Maybe call it colorUtilities?

    import { colorProperties } from "tailwindcss-theme-variants";
    // other options
    semantics: {
    "primary": colorProperties, 
    "secondary": colorProperties,
    },
  4. Mirror Tailwind CSS theme configuration both in structure and functionality as closely as possible (excluding extend because... how??). This is also going to require hard-coded utilities, unfortunately.

    themes: {
    light: {
    // current config options
    semantics: {
      colors: {
         primary: "gray-100", // reference to the value currently in `theme.colors.gray.100` above
         // don't need the exact value because we take the `@apply` approach 
         // (but when we implement the custom properties override technique it'll be easy to get)
         // when calling `theme` try it like `theme("colors.gray-100", theme("colors.gray.100", undefined))` then error if it's undefined
      },
      // if backgroundColor were to be here, it would override `colors` for just `bg-{color}`classes
    },
    },
    dark: {
    semantics: {
      colors: {
        // error: no `primary` here even though it was listed in `light`
        // similarly:  error in the other place because no `on-primary` was listed there despite being here
        "on-primary": "gray-200",
      }
    }
    }
    }
JNavith commented 3 years ago

Implementation idea rather than configuration idea

Now that we can @apply arbitrary classes, maybe make semantics work such that each theme has the semantics key and the plugin aggregates all the things and does

.text-primary { 
  @apply light:text-gray-900 dark:text-gray-200;
}

In that case, make the addUtilities call copy the variants of textColor. This works like custom properties but doesn't need them! This is comparable to the way tailwindcss-theming and tailwindcss-theme-swapper do it, rather than something new—but that's okay, because I think we can extend this approach. Let's try starting with that as the backbone / what can be fallen back to, but when custom properties are both supported in the browser and configured by the primary=blue-800 class, then those will take precedence. Is that a good idea or is it too complex to implement AND document?

JNavith commented 3 years ago

Rejected features that were starting to be implemented:

semantic-name=value syntax: while it's inventive, I don't see the point anymore.

TODO. Variables can be set with utility classes that follow the format `semantic-name=value`; reminds you of assigning variables in regular programming languages, doesn't it? This new assignment will cascade down the CSS / DOM tree, because, ***surprise***, variables are implemented with CSS custom properties despite me describing semantics as an alternative to custom properties earlier! [This is because constants don't need custom properties, but variables do.]

                    const themeValues = lookupTheme(utility, {}) ?? {};

                    const flattenedThemeValues = Object.entries(themeValues).reduce((themeValuesAccumulating, [key, value]) => {
                        if (typeof value === "string") {
                            themeValuesAccumulating[key] = value;
                        } else {
                            Object.entries(value).forEach(([nestedKey, nestedValue]) => {
                                themeValuesAccumulating[`${key}-${nestedKey}`] = nestedValue as string;
                            });
                        }
                        return themeValuesAccumulating;
                    }, {} as Record<string, string>);

                    Object.entries(flattenedThemeValues).forEach(([themeKey, themeValue]) => {
                        // addBase here?

                        const variableDeclaration = `.${e(`${semanticName}=${themeKey}`)}`;
                        if (!onlyie11) {
                            addUtilities({
                                [variableDeclaration]: {
                                    [`--${semanticName}`]: themeValue,
                                },
                            },
                            // TODO: ummmm
                            [group ?? "", ...lookupVariants("semanticVariables", [])]);
                        }

                        const variableUser = `.${e(className({ name: semanticName }))}`;
                        if (!noie11) {
                            addUtilities({
                                // Set both for parity with custom properties (i.e. they can apply to the current element in addition to its children)
                                [`${variableDeclaration}${variableUser}, ${variableDeclaration} ${variableUser}`]: {
                                    [`@apply ${className({ name: themeKey })}`]: "",
                                },
                            },
                            // TODO: how can we support variants like above?
                            // the modifySelectors transformation isn't what I expected
                            []);
                        }
                    });
JNavith commented 3 years ago

Custom utilities

Because I'm currently implementing approach 4, this is based on that:

tailwindcssThemeVariants({
  themes: {
    light: {
      ...
      semantics: { ... },
    },
    dark: {
      ...
      semantics: { ... },
    },
  },
  variants: { ... },
  // New
  utilities: { ... },
})

utilities has the same merge logic as variants

JNavith commented 3 years ago

Fully implemented as of 1.6 🤯