petal_components icon indicating copy to clipboard operation
petal_components copied to clipboard

Custom classes override existing classes (help avoid "!important")

Open mplatts opened this issue 2 years ago • 13 comments

My first naive approach would be something like this:

defmodule Class do
  @doc """
  Builds a class based on a set of default classes and specified overrides for them
  """
  def build_class(defaults, overrides) when is_binary(defaults),
    do: build_class(String.split(defaults), overrides)

  def build_class(defaults, overrides) when is_binary(overrides),
    do: build_class(defaults, String.split(overrides))

  def build_class(defaults, overrides) when is_list(defaults) and is_list(overrides) do
    defaults_map = get_class_map(defaults)
    overrides_map = get_class_map(overrides)

    Map.merge(defaults_map, overrides_map)
    |> Map.values()
    |> Enum.join(" ")
  end

  # Converts a list of classes to a map identified by their class name prefix
  defp get_class_map(classes) do
    classes
    |> Enum.map(fn class -> {get_class_id(class), class} end)
    |> Map.new()
  end

  # Gets the prefix of a class name (without the part after the last `-` character)
  defp get_class_id(class) do
    class
    |> String.reverse()
    |> String.split("-", parts: 2, trim: true)
    |> case do
      [class] -> class
      [_ | [prefix]] -> prefix
    end
    |> String.reverse()
  end
end

Since the class order is not important, we can split the classnames by whitespace and then put them into a map, identified by their class prefix (everything before the last -, e.g. "p-5 place-items-start" => %{"p" => "p-5", "place-items" => "place-items-start"}).

Then we just merge the overrides map over the defaults map and get the remaining values.

I am not 100% sure if this would always work with tailwindcss and the naming scheme used there.

This also would override class names like table with table-auto during merging, because they share the same prefix, which could also be undesirable.

Originally posted by @moogle19 in https://github.com/petalframework/petal_components/issues/34#issuecomment-1112892876

mplatts avatar May 01 '22 23:05 mplatts

One example where the approach above wouldn't work (from https://tailwindcss.com/docs/reusing-styles):

<div>
  <div class="flex items-center space-x-2 text-base">
    <h4 class="font-semibold text-slate-900">Contributors</h4>
    <span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700">204</span>
  </div>
  <div class="mt-3 flex -space-x-2 overflow-hidden">
    <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
    <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
    <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" alt=""/>
    <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
    <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
  </div>
  <div class="mt-3 text-sm font-medium">
    <a href="#" class="text-blue-500">+ 198 others</a>
  </div>
</div>

ring-2 and ring-white would override each other.

moogle19 avatar May 02 '22 07:05 moogle19

This could be a good starting point: https://github.com/dcastil/tailwind-merge I quickly tested the merging of ring-2 and ring-white and they seem to handle it correctly.

moogle19 avatar May 09 '22 14:05 moogle19

what about taking dasiyui approach i know maybe it is a lot work but writing one class instead of write huge chunks of classes could help to avoid this problem not just that it helps maintain you will have just logic of build the component and one class

mohammedzeglam-pg avatar Jul 01 '22 19:07 mohammedzeglam-pg

Yeah unfortunately that might too much work for a relatively small gain. I haven't had too much of a problem in my projects with overriding styles. Is there any obvious culprits we can try and do a small fix for? Guessing padding/margin/colors is the main thing people want to override?

mplatts avatar Jul 04 '22 22:07 mplatts

Rules we should try and support (taken from the tailwind-merge project mentioned above)

Last conflicting class wins

twMerge('p-5 p-2 p-4') // → 'p-4'

Allows refinements

twMerge('p-3 px-5') // → 'p-3 px-5'
twMerge('inset-x-4 right-4') // → 'inset-x-4 right-4'

Resolves non-trivial conflicts

twMerge('inset-x-px -inset-1') // → '-inset-1'
twMerge('bottom-auto inset-y-6') // → 'inset-y-6'
twMerge('inline block') // → 'block'

Supports modifiers and stacked modifiers

twMerge('p-2 hover:p-4') // → 'p-2 hover:p-4'
twMerge('hover:p-2 hover:p-4') // → 'hover:p-4'
twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4'

Supports arbitrary values

twMerge('bg-black bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--mystery-var)]'
twMerge('grid-cols-[1fr,auto] grid-cols-2') // → 'grid-cols-2'

Supports arbitrary properties

twMerge('[mask-type:luminance] [mask-type:alpha]') // → '[mask-type:alpha]'
twMerge('[--scroll-offset:56px] lg:[--scroll-offset:44px]')
// → '[--scroll-offset:56px] lg:[--scroll-offset:44px]'

// Don't do this!
twMerge('[padding:1rem] p-8') // → '[padding:1rem] p-8'

Watch out for mixing arbitrary properties which could be expressed as Tailwind classes. tailwind-merge does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep the bundle size small.

Supports arbitrary variants

twMerge('[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4') // → '[&:nth-child(3)]:py-4'
twMerge('dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4')
// → 'hover:dark:[&:nth-child(3)]:py-4'

// Don't do this!
twMerge('[&:focus]:ring focus:ring-4') // → '[&:focus]:ring focus:ring-4'

Similarly to arbitrary properties, tailwind-merge does not resolve conflicts between arbitrary variants and their matching predefined modifiers for bundle size reasons.

Supports important modifier

twMerge('!p-3 !p-4 p-5') // → '!p-4 p-5'
twMerge('!right-2 !-inset-x-1') // → '!-inset-x-1'

Preserves non-Tailwind classes

twMerge('p-5 p-2 my-non-tailwind-class p-4') // → 'my-non-tailwind-class p-4'

Supports custom colors out of the box

twMerge('text-red text-secret-sauce') // → 'text-secret-sauce'

Ignores undefined, null and false values

twMerge('some-class', undefined, null, false) // → 'some-class'

mplatts avatar Jul 20 '22 23:07 mplatts

https://stitches.dev/ could also provide inspiration

mplatts avatar Aug 05 '22 04:08 mplatts

How about doing something based on css variables instead of having an interface to manually pass classes? You could use this both for layout-technical customisations as well as aesthetics (colours and such). I'm not sure to want extent Tailwind can then still be used though...

dvic avatar Aug 05 '22 07:08 dvic

Hmm yeah that might be too fine grained and moving away from the power of Tailwind. Actually thinking @mohammedzeglam-pg might be right in following a daisy approach. eg.

@layer components {
  .btn {
    @apply inline-block px-5 py-3 rounded-lg focus:outline-none focus:ring focus:ring-offset-2 uppercase tracking-wider font-semibold text-sm sm:text-base;
  }
  .btn-primary {
    @apply bg-indigo-500 text-white hover:bg-indigo-400 focus:ring-indigo-500 focus:ring-opacity-50 active:bg-indigo-600;
  }
  .btn-secondary {
    @apply bg-gray-300 text-gray-800 hover:bg-gray-200 focus:ring-gray-300 focus:ring-opacity-50 active:bg-gray-400;
  }
}

Devs will pull in Petal Components CSS into their app.css... and hopefully can override them if need be (yet to test this).

From this video it seems you can then add classes like px-10 inline and it will override what's in the @apply collection.

https://www.youtube.com/watch?v=TrftauE2Vyk&ab_channel=TailwindLabs

This way you can do inline modifications, or if you want to drastically change a component for the whole application, you redefine the @apply block for that component in your app.css.

mplatts avatar Aug 07 '22 20:08 mplatts

https://www.youtube.com/watch?v=TrftauE2Vyk&ab_channel=TailwindLabs

i think now before this major update maybe add this to default config in docs will be better solution than manual, and your idea it will be making petal component fit to any design could thing about it. i was thinking about file that just imported like tailwind to app.css. https://tailwindcss.com/docs/configuration#important

mohammedzeglam-pg avatar Oct 10 '22 12:10 mohammedzeglam-pg

https://stitches.dev/

awesome job already done by @zachdaniel in his new project https://github.com/zachdaniel/tails

mohammedzeglam-pg avatar Oct 11 '22 13:10 mohammedzeglam-pg

It's definitely not "done" but it's a work in progress :) contributions welcome!

zachdaniel avatar Oct 11 '22 14:10 zachdaniel

It's definitely not "done" but it's a work in progress :) contributions welcome!

Nice! Seems like a great start and I think the majority of cases are just padding/margin/color merges.

mplatts avatar Oct 11 '22 18:10 mplatts

I'd also eventually like to add stuff there to turn at least some of the simpler classes into in-line styles so that you can write an email template in tailwind. Not sure how well that will work, but would be pretty useful IMO.

zachdaniel avatar Oct 11 '22 18:10 zachdaniel