Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial discussion #1

Open
msaraiva opened this issue Feb 17, 2021 · 40 comments
Open

Initial discussion #1

msaraiva opened this issue Feb 17, 2021 · 40 comments

Comments

@msaraiva
Copy link
Member

From the slack channel:

harmon:

anyone make progress with a tailwind surface component library? I am finding tailwind a bit too flexible to easily abstract further - also need to keep the class names intact (not do string concat) to ensure purge css works when generating production css

also to do things like modals effectively - does need some js code - we should try to standardize the approach for shipping components with some hooks, or that require alpine..

lnr0626:

i've been working with tailwind ui, so I can't release the components i've built :-(

harmon:

hmm yea - is it just a tailwind plugin? or how does tailwind UI extend on tailwind? - Think i might pull the trigger on that also... would like the tailwind ui components...

suppose requiring alpinejs on a tailwind component lib would be sensible - it would probably be the recommended approach from the tailwind devs is tailwind UI really just some markup they give you? - they must mention not sharing that in the license agreement...

pretty thin line between creating some tailwind button component library (for example) and not stepping on the tailwind UI license

lnr0626:

they give you the markup, tell you which classes to modify on which elements, and a few other things (they might have some vue/react stuff, but i haven't paid attention to that)

the cost was worth it for me, but i also don't have anyone that's dedicated to doing design and so it just help relieve cognitive load

harmon:

it is otherwise a very fair license, stifling a bit for this type of idea though..

yea agree, value is there for sure

lnr0626:

yeah, hard to create a component lib once you've seen the actual markdown cause at the end of the day they aren't doing anything too crazy (i.e. there aren't too many ways to create a button that has an outline on focus and some changed emphasis on hover)

harmon:

Examples of use not allowed by the license:

  • Creating a React or Vue version of Tailwind UI and making it available either for sale or for free

straight from the license ^ - think that would apply to us also

best we could do is streamline getting tailwind setup in a phoenix/liveview/surface project

paulstatezny:

Yeah, It’s too bad, we’re doing the same thing with TailwindUI, which is really great stuff.

msaraiva:

it would be great to have our own surface_tailwind. Regarding the license which states that is not allowed to:

  • Creating a React or Vue version of Tailwind UI and making it available either for sale or for free

I believe they mention React and Vue because that's what they use to provide components for JS. Creating similar components using Surface would already be different in many ways as we're creating server-side components using another language. As far as I can see, the text above doesn't say we can't create server-side templates that use some of the same utility classes. That would be really hard to do since the utility classes are common and free and there are not too many different ways to create some of the simplest components. This drives me to a few question. Are there any other TailwindCSS based component suites out there? Are they free? If so, don't they use the same (or similar) set of utility classes to build their components?

harmon:

that is the complication really - some of the UI components are free, some are not - would be difficult to say if we did create a toggle for example - if it was the same utility classes as is used in the TailwindUI components (even by accident) - would that somehow be in breach of the tailwind UI license?

@msaraiva
Copy link
Member Author

@lnr0626, @paulstatezny and @harmon25 in case any of you knows anyone else that could be also interested in Tailwind components for Surface, please ping them so they can join us on this discussion.

@miguel-s and @Malian I'm not sure what CSS solutions you're using but since you're both very active contributors, in case you're using (or have plans to use) Tailwind, feel free to join us as well. BTW, in case you use Slack, it would be great to have you on the #surface channel ;)

Cheers.

@miguel-s
Copy link

I do use Tailwind CSS and UI, and also AlpineJS in my own projects :D

Don't now how I missed the #surface slack channel, will join you there soon!

@paulstatezny
Copy link

Ping @capitalist

@paulstatezny
Copy link

paulstatezny commented Feb 18, 2021

So I think it's established that we cannot use any markup from Tailwind UI.

That said, is the purpose of this project to cook up custom generic components from scratch? The Bulma version uses official Bulma markup instead of doing it from scratch, right?

@msaraiva
Copy link
Member Author

is the purpose of this project to cook up custom generic components from scratch?

If we can't find any free set of components that we can use, then yes.

The Bulma version uses official Bulma markup instead of doing it from scratch, right?

Yes. Both surface_bulma and surface_bootstrap use their official counterparts.

Just for the record, I think TailwindUI is great and it's more than fair that it has a commercial licence. What I think it's unfair is that Vue.js users get ready-to-use components while a Surface/LV user will have to roll its own suite of components, which cannot be shared to make the life of others easier, not even with those that also bought the licence. It seems like such a waste of resources.

An effort to provide an opensource alternative for Surface/LV would be fantastic for the ecosystem.

@paulstatezny
Copy link

100% agreed, and supportive of that. I was asking to make sure I understood the strategy/approach for this project.

@harmon25
Copy link

What I think it's unfair is that Vue.js users get ready-to-use components while a Surface/LV user will have to roll its own suite of components...

I don't think there are any official TailwindCSS/UI Vue or React component suites. Most component suites I.E semantic-ui, antd, materialize have a consistent style, or 'look' to them - the point of tailwind is that it could look however you want. (just tweak the classes)

I think this project is interesting - the alpine version would work nicely with LV, appears that is a WIP...

Here are the Tailwind Docs regarding JS integration

@miguel-s
Copy link

miguel-s commented Feb 18, 2021

I don't think there are any official TailwindCSS/UI Vue or React component suites.

TailwindLabs are working on something along these lines with HeadlessUI. It's still very early and only React and Vue (Alpine coming soon).

This project is under the MIT License, so maybe we could follow what they're doing?

@Malian
Copy link

Malian commented Feb 19, 2021

I do use Tailwind CSS, Tailwind UI, and also AlpineJS in my projects.

I will join the Surface community on slack!

@tmepple
Copy link

tmepple commented Feb 19, 2021

@msaraiva

a Surface/LV user will have to roll its own suite of components, which cannot be shared to make the life of others easier, not even with those that also bought the licence

Are you sure Adam wouldn't let us share our derived components with others who have already bought the license... maybe from their password protected site? It seems like most of us here have already bought the license and are all reinventing the wheel leveraging the same stack anyway.

@msaraiva
Copy link
Member Author

Are you sure Adam wouldn't let us share our derived components with others who have already bought the license... maybe from their password protected site?

@tmepple I'm not sure, either way, I believe the goal of this project shoud be to have an opensource suite of components that doesn't depend on TailwindUI and by the feedback so far, it looks like everyone using TailwindCSS is already using TailwindUI so I'm not sure I'll be able to bring enough people/companies to invest time and resources on this effort. Maybe the best thing to do is to wait for a free alternative to come up so we can build components on top of it.

I don't think there are any official TailwindCSS/UI Vue or React component suites.

@harmon25 there will be soon https://twitter.com/adamwathan/status/1362877785480564738?s=20

@BryanJBryce
Copy link

The reason I found this was because I thought I'd just take the Bulma components and redo them with Tailwind. Might be worth having a project here so that people can start adding components. Just don't copy TailwindUI.

@msaraiva
Copy link
Member Author

Hi @BryanJBryce!

@olivermt is working on a more complete suite of components based on bootstrap (https://github.com/surface-ui/surface_bootstrap/). It should be released in the next few days. After its release, maybe we could try to mimic the same API for Tailwind. I'm sure we'll not be able to have the exact same API but we might be able to keep it as similar as possible.

@Menkir
Copy link

Menkir commented Mar 24, 2021

There is https://tailwindcomponents.com/ where are free tailwindcss components. Doesn't help for the ui license problem but there is already a community making custom components that we can freely use.

@DEvil0000
Copy link

DEvil0000 commented Mar 24, 2021

Totally in the same boat - we also have the license and are in need to write own surface components.

From the license it is quite clear that you can only open source end products not component based libs. I however can imagine to get around this limitation without violation of the license. The library would need to use its own set of CSS classes which are intermediate classes (probably bulma like) and just inherit from other classes (bulma by default). They could however also inherit (or map) from(/to) CSS classes of tailwind without providing tailwind CSS classes, whole tailwind UI themes or a complete mapping.
So every user of this lib would need to map its CSS classes to some CSS classes of their desire when not using defaults.

This could possibly be done in some LESS like approach

.inputbase() {
  /* your base code */
}

.someInput {
  .inputbase;
  /*some input special code */
}

So in general more like plugging in CSS later by the user of the lib.

edit: which slack?

@type1fool
Copy link

type1fool commented May 2, 2021

Hey gang. I've been using TailwindCSS for a couple years now, and I wanted to start contributing to this Tailwind library for Surface.

I see a lot of concern about creating Tailwind components because of TailwindUI's license. I can't find the exact podcast episode, but I recall @adamwathan discussing his intent with the license around this time last year. From what I recall, the license is intended to prevent repackaging and redistributing TailwindUI components. To me, that means a TailwindCSS-based library for Surface could exist as long as it's not essentially copy/pasted from the TailwindUI source code.

From the FAQs:

Can I use Tailwind UI in open source projects?
Yep! As long as what you're building is some sort of actual website and not a derivative component library, theme builder, or other product where the primary purpose is clearly to repackage and redistribute our components, it's totally okay for that project to be open source.

For more information and examples of what is and isn't okay, read through our license.

TailwindUI License

So far, the approach has been to implement components based on documented examples (Bulma, Bootstrap). What if Surface component libraries were more standardized and CSS framework-agnostic? Components and their props would have implementations for each supported CSS framework, but the API for each component would be consistent. Kinda like Ecto has adapters for a few drivers. This way, the Surface components would have distinct appearance (and functionality?) from TailwindUI.

Could Surface pull off a unique design aesthetic this way? Is that what users want?

@type1fool
Copy link

For context, this is the episode where licensing is discussed (85:43)
https://fullstackradio.com/135

@BryanJBryce
Copy link

Hi @BryanJBryce!

@olivermt is working on a more complete suite of components based on bootstrap (https://github.com/surface-ui/surface_bootstrap/). It should be released in the next few days. After its release, maybe we could try to mimic the same API for Tailwind. I'm sure we'll not be able to have the exact same API but we might be able to keep it as similar as possible.

@msaraiva How do you feel this release came out as an example for a starting point for tailwind?

@olivermt
Copy link

olivermt commented May 6, 2021

As for my POV, I'd say my stuff might not be so relevant, as I'm simply implementing my way through the sandwhich list of components and features in the bootstrap 5 library.

@gdub01
Copy link

gdub01 commented May 26, 2021

https://dev.to/jameswallis/5-places-to-get-pre-crafted-tailwind-css-components-for-free-3jlg lists 5 open source/mit license tailwind component libs as well.

@haubie
Copy link

haubie commented May 27, 2021

Is the idea for the tailwind_surface library to have API parity with the surface_bootstrap library, or do things a little differently?

The reason I ask is I was looking at the differences as to how tailwind and boostrap approach things.

For example taking the button component in the surface_bootstrap library, the colors prop accepts one of these values ~w(primary secondary success danger warning info light dark). But tailwind's approach to colours isn't as semantic as this, and could be any one of the many colours which come with tailwind or custom ones.

Same with a prop like size. The surface_boostrap button takes ~w(small large), but there aren't really preset sizes like this in tailwind, although they could be made to work that way. The same for the rounded prop. It's a boolean in surface_boostrap, but in tailwind it could be rounded-( sm | md | lg | xl | 2xl | 3xl| full ) or any combination the top, right, left, bottom, top-left, top-right, bottom-right, bottom-left variants, etc.

I guess for both of the above, a 'sensible default' could be included in a component, maybe drawing inspiration from Tailwind UI?

But I'm also thinking of (1) how could there be a sensible starting point, and (2) not end up with lots of verboseness like adding the following to the class prop each time for a button because thats what suits the needs in a particular project: "inline-flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" !

I'm wondering if others were thinking of this too - how to find a balance with some nice defaults using tailwind in a flexible surface component library, maintaining the expressiveness of tailwind when needed, but not resulting in having everything to be on a components class prop?

@gdub01
Copy link

gdub01 commented May 27, 2021 via email

@haubie
Copy link

haubie commented May 28, 2021

I wonder about the idea of a theming component / aspect that could be used? So buttons would have class defaults, but inherit theme options. Then you would define things like borders, spacing, colors as part of the theme. https://material-ui.com/customization/default-theme/ has a pretty good theme object definition.

I like that approach @gdub01.
It gives flexibility to set styles globally, and I assume there could be prop(s) on a component to override the global theme if needed too?

@msaraiva
Copy link
Member Author

https://dev.to/jameswallis/5-places-to-get-pre-crafted-tailwind-css-components-for-free-3jlg lists 5 open source/mit license tailwind component libs as well.

@gdub01 thanks for the link. Cool stuff 👍 This will certainly be helpful.

Is the idea for the tailwind_surface library to have API parity with the surface_bootstrap library, or do things a little differently?

@haubie there's no requirement for keeping any parity. It can be completely different. Most of the time I'm sure it will be better to keep each one them following their own already established vocabulary and semantics.

But I'm also thinking of (1) how could there be a sensible starting point, and (2) not end up with lots of verboseness like adding the following to the class prop each time for a button because thats what suits the needs in a particular project: "inline-flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" !

Hard to tell now but the chances a project like this have to get bloated is pretty high. TailwindUI did a pretty good job on that regard. The components look good and are flexible enough, however, I didn't have a chance to take a look at the recently released set of official React components provided by TailwindUI. So I'm not sure how intuitive and flexible the API ended up.

One thing is creating pseudo-components that looks cool but have no behaviour, no state and no public API. Creating "real" components with all that is a totally different beast.

I'm wondering if others were thinking of this too - how to find a balance with some nice defaults using tailwind in a flexible surface component library, maintaining the expressiveness of tailwind when needed, but not resulting in having everything to be on a components class prop?

That's certainly the big question here. Has anyone already seen how TailwindUI's official React component suites handles that? Are there any other lib that do this nicely? What kind of abstractions they came up with?

@haubie
Copy link

haubie commented May 29, 2021

Thanks for the clarification @msaraiva and those thoughts as well.

I hadn't looked at the React or Vue components in Tailwind UI, but I've now had a peak into some that are in the free tier.

Lower-level components

As you suspected, there can be a lot of bloat on the public API of the lower-level components:

<Popover.Button className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">

In this current version of Tailwind UI, it doesn't looked like they have a global theming layer as colours, typography, etc, are hardcoded as above in each component.

Higher-level components

Higher level components look clean, and seem to have an API which focuses more on the content/data. e.g. the Features component below, takes an array of values on a features prop:

[
  {
    name: 'Competitive exchange rates',
    description:
      'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste dolor cupiditate blanditiis ratione.',
    icon: GlobeAltIcon,
  },
  {
    name: 'No hidden fees',
    description:
      'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste dolor cupiditate blanditiis ratione.',
    icon: ScaleIcon,
  }
]

image

I also get the impression that some of the Tailwind UI components aren't meant to be a simple drop in where most of the style and behaviour is configured through an API on the component. They seem more of a starting point that will require the dev to edit. Example below is a header component with menu items:

image

It all looks hard coded to the specific example, rather than exposing a generic 'menu' prop taking an array.

I'm thinking there is probably a nicer way which strikes the balance of a sensible starting point without too much verboseness. The material example @gdub01 looks like a nice way of defining customisable global styles, but also allowing overriding through props on an individual component when necessary? I guess this could still be done in Tailwind as per:

It might be nicer done in an Elixir module which could hold the theme config, but how will people who use tailwind on a regular basis feel about this? e.g. setting the palette something like the material example shared by @gdub01 :

[
        primary: [
          main: [
            background: "bg-indigo-500",
            text: "text-indigo-500",
            contrast_text: "text-white",
            border: "border-indigo-500",
            ring: "ring-pink-600"
          ],
          dark: [
            background: "bg-indigo-900",
            text: "text-indigo-900",
            contrast_text: "text-white",
            border: "border-indigo-900",
            ring: "ring-pink-600"
          ],
          light: [
            background: "bg-indigo-400",
            text: "text-indigo-400",
            contrast_text: "text-white",
            border: "border-indigo-400",
            ring: "ring-pink-600"
          ],
        ]
]

Setting the default global styles for the lower-level components like buttons something like this:

[
      button: [
        alignment: "inline-flex items-center justify-center",
        padding: "px-4 py-2",
        margin: "",
        border: "border border-transparent  rounded-md",
        text: "text-base font-medium #{color(:primary, :main, :contrast_text)} hover:#{color(:primary, :dark, :contrast_text)}",
        background: "#{color(:primary, :main, :background)} hover:#{color(:primary, :dark, :background)}",
        ring: "focus:outline-none focus:ring-4 focus:#{color(:primary, :main, :ring)}"
      ]
]

With an equivalent prop on the button component that could be pulled in by the component author as defaults, but overridden, e.g.:

defmodule SurfaceTailwind.Button do
  use Surface.Component
  alias SurfaceTailwind.Theme

 # ...other props, etc...

  prop alignment, :css_class, default: Theme.component(:button, :alignment)
  prop padding, :css_class, default: Theme.component(:button, :padding)
  prop margin, :css_class, default: Theme.component(:button, :margin)
  prop border, :css_class, default: Theme.component(:button, :border)
  prop text, :css_class, default: Theme.component(:button, :text)
  prop background, :css_class, default: Theme.component(:button, :background)
  prop ring, :css_class, default: Theme.component(:button, :ring)

  slot default

  def render(assigns) do
    ~H"""
    <button
      type={{@type}}
      aria-label={{@aria_label}}
      :on-click={{@click}}
      disabled={{@disabled}}
      value={{@value}}
      class={{[@alignment, @padding, @margin, @border, @text, @background, @ring, @class]}}>
      <slot>{{ @label }}</slot>
    </button>
    """
  end
end

And used like this:

<Button>Default style from theme</Button>

<Button background="bg-green-600 hover:bg-green-900">Backround overridden</Button>

Output:

image

@haubie
Copy link

haubie commented May 29, 2021

Little bit of a revision to the above idea, if supporting say primary, secondary and other named palettes defined in a theme, the above may need a tweak to remove the defaults defined in the props, e.g.:

defmodule SurfaceTailwind.Button do
  use Surface.Component
  alias SurfaceTailwind.Theme

  @component_name :button
  @themeable_props [:alignment, :padding, :margin, :border, :text, :background, :ring]

  # ...other props, etc...

  prop theme, :atom, default: :primary

  prop alignment, :css_class
  prop padding, :css_class
  prop margin, :css_class
  prop border, :css_class
  prop text, :css_class
  prop background, :css_class
  prop ring, :css_class

  slot default

  def render(assigns) do
    ~H"""
    <button
      type={{@type}}
      aria-label={{@aria_label}}
      :on-click={{@click}}
      disabled={{@disabled}}
      value={{@value}}
      class={{classes(assigns), @class}}>
      <slot>{{ @label }}</slot>
      <p class="text-xs ml-12">Theme: {{@theme}}</p>
    </button>
    """
  end

  defp classes(assigns) do
    {overridden_class_list, theme_class_list} =
      @themeable_props
      |> Enum.split_with(fn class_group -> if Map.get(assigns, class_group) != nil, do: true, else: false end)

    classes_from_theme =
      theme_class_list
      |> Enum.map(fn class_group -> Theme.component(@component_name, class_group, assigns.theme) end)

    classes_from_component_user =
      overridden_class_list
      |> Enum.map(fn class_group -> Map.get(assigns, class_group) end)

    classes_from_theme ++ classes_from_component_user
  end

end

.... or something like that. I guess with a little refactoring, that classes/1 function could be moved to SurfaceTailwind.Theme and aliased instead.

And that would allow:

  • Default theme: <Button>Hello</Button>
  • Selecting another theme or pallet defined in the theme module: <Button theme={{:secondary}}>Hello</Button>
  • Using a prop to override a themes value: <Button theme={{:secondary}} background="bg-green-900">Hello</Button>

@gdub01
Copy link

gdub01 commented May 29, 2021

I like the above a lot!

I do wonder if we can get away without having a specific button theme.

If we want the button border to look like this:

border: "border border-transparent rounded-md",

the button component could look like this:

border: "border border-transparent #{Theme.component(:spacing, :border_radius)}",

Just with the idea that the button component itself sets defaults that incorporates the global theme without the need for a separate button theme.

Then to switch between a primary and secondary button, it would go from:

background: "#{color(:primary, :main, :background)} hover:#{color(:primary, :dark, :background)}",

to:

`background: "#{color(@button_variant, :main, :background)} hover:#{color(@button_variant, :dark, :background)}",

where @button_variant is :primary or :secondary

or something like that?

@haubie
Copy link

haubie commented May 30, 2021

Just with the idea that the button component itself sets defaults that incorporates the global theme without the need for a separate button theme.

I really like that idea @gdub01 and can see the advantages of that approach.

I had a bit of a play around with the idea and got a prototype working for a couple of components as below. I think it feels pretty usable, flexible and tailwind-ish. Probably worth trying with a more complex component as at the moment the classes are just being applied to the top most HTML element, rather than reaching in any deeper.

Added to the component is a function which holds the theme information. That callout to SurfaceTailwind.Theme.build_class_list(assigns, &component_theme/1) is what applies the styles defined in the component and any values pulled from the global theme config, or overridden by a prop or in the class prop.

Button example

defmodule SurfaceTailwind.Button do
  use Surface.Component
  alias SurfaceTailwind.Theme, as: T

  # ... other props removed for brevity ... #

  prop theme, :atom, default: :primary

  prop alignment, :css_class
  prop padding, :css_class
  prop margin, :css_class
  prop border, :css_class
  prop text, :css_class
  prop text_size, :css_class
  prop background, :css_class
  prop ring, :css_class

  slot default

  def render(assigns) do
    ~H"""
    <button
      type={{@type}}
      aria-label={{@aria_label}}
      :on-click={{@click}}
      disabled={{@disabled}}
      value={{@value}}
      class={{classes(assigns)}}>
      <slot>{{ @label }}</slot>
    </button>
    """
  end

  def classes(assigns), do: T.build_class_list(assigns, &component_theme/1)

 # Theme definition for the component, embedded with the component.
  def component_theme(theme \\ :primary) do
    [
      alignment: "inline-flex items-center justify-center",
      padding: "px-4 py-2",
      margin: "",
      border: "border rounded-md #{T.value(theme, :main, :border)}",
      border_radius: T.value(:general, :style, :border_radius),
      text: "font-medium #{T.value(theme, :main, :contrast_text)} hover:#{T.value(theme, :dark, :contrast_text)}",
      text_size: "text-base",
      background: "#{T.value(theme, :main, :background)} hover:#{T.value(theme, :dark, :background)}",
      ring: "focus:outline-none focus:ring-4 focus:#{T.value(theme, :main, :ring)}"
    ]
  end

end

Examples of it being used:

  • Default (primary) theme: <Button>Default</Button>
  • Default with background and text overridden in prop: <Button background="bg-green-200 hover:bg-green-900" text="text-green-900 hover:text-white" class="my-4">Default + props</Button>
  • Secondary theme: <Button theme={{:secondary}} class="my-4">Secondary theme</Button>
  • Secondary theme with background and text overridden in prop: <Button theme={{:secondary}} background="bg-green-200 hover:bg-green-900" text="text-green-900 hover:text-white" class="my-4">Secondary theme + props</Button>
  • Secondary theme, overriding alignment and text size, adding an SVG:
      <Button theme={{:secondary}} alignment="inline-flex flex-col items-center justify-center" text_size="text-xs">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" viewBox="0 0 20 20" fill="currentColor">
          <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
        </svg>
        <div>Secondary theme</div>
      </Button>
  • Disabled button - if disabled = true, it will set the theme to :disabled <Button class="my-4" disabled={{true}}>Disabled theme</Button>
  • Applying a the :neutral theme: <Button theme={{:neutral}} class="my-4">Neutral theme</Button>

Sample output for the above:

image

Alert component example

In this component, the theme is set by the alert type, e.g. if it is a :info, :warning or :error alert.

Alert component code:

defmodule SurfaceTailwind.Alert do
  use Surface.Component
  alias SurfaceTailwind.Theme, as: T

  prop type, :string, default: :info, values: [:info, :error, :warning]

  # ...other props an icon fetching code edited out for brevity...

  prop size, :css_class
  prop alignment, :css_class
  prop padding, :css_class
  prop margin, :css_class
  prop border, :css_class
  prop text, :css_class
  prop text_size, :css_class
  prop background, :css_class
  prop ring, :css_class
  prop shadow, :css_class

  slot default

  def render(assigns) do
    ~H"""
    <div
      aria-label={{@aria_label}}
      :on-click={{@click}}
      value={{@value}}
      class={{classes(assigns)}}>
      <div class="w-12 h-12
                  rounded-full
                  bg-white bg-opacity-20
                  flex flex-col
                  justify-center items-center
                  mr-3">{{icon(@type)}}</div>
      <div class="flex-1">
      <slot>{{ @label }}</slot>
      </div>
      <div class="w-6 opacity-60">{{icon(:cross)}}</div>
    </div>
    """
  end

  def classes(assigns) do
    # Alert theme is selected based on the alert type parameter, rather than using the theme parameter
    # E.g. <Alert type={{:error}}>Error message text here.</Alert>
    assigns  = Map.put(assigns, :theme, assigns.type)

    T.build_class_list(assigns, &component_theme/1)
  end

  def component_theme(theme ) do
    [
      size: "w-full",
      alignment: "inline-flex items-center justify-left",
      padding: "px-3 py-3",
      margin: "",
      border: "border #{T.value(theme, :main, :border)}",
      border_radius: T.value(:general, :style, :border_radius),
      text: "font-semibold #{T.value(theme, :main, :contrast_text)}",
      text_size: "text-base",
      background: "#{T.value(theme, :main, :background)} hover:#{T.value(theme, :dark, :background)}",
      ring: "focus:outline-none focus:ring-4 focus:#{T.value(theme, :main, :ring)}",
      icon: T.value(theme, :main, :icon),
      shadow: "shadow-lg"
    ]
  end
end

Example uses:

  • Default (info): <Alert class="my-4">Default alert with the info styling.</Alert>
  • Warning: <Alert type={{:warning}} class="my-4">Warning alert styling.</Alert>
  • Error: <Alert type={{:error}} class="my-4">Error alert styling.</Alert>
  • Overriding the border prop: <Alert type={{:error}} class="my-4" border="border-8 border-blue-900">Error alert styling with overriding of Tailwind CSS border classes.</Alert>

Output for the above:

image

Global theme config

For the above, just a nested keyword list:

[
        general: [
          style: [
            border_radius: "rounded-md",
          ]
        ],
        primary: [
          main: [
            background: "bg-blue-700",
            text: "text-blue-700",
            contrast_text: "text-white",
            border: "border-transparent",
            ring: "ring-pink-600"
          ],
          dark: [
            background: "bg-blue-900",
            text: "text-blue-900",
            contrast_text: "text-white",
            border: "border-blue-800",
            ring: "ring-pink-600"
          ],
          light: [
            background: "bg-blue-50",
            text: "text-blue-400",
            contrast_text: "text-white",
            border: "border-blue-400",
            ring: "ring-pink-600"
          ]
        ],
        secondary: [
          main: [
            background: "bg-blue-50 seco",
            text: "text-blue-700",
            contrast_text: "text-blue-700",
            border: "border-blue-700",
            ring: "ring-pink-600"
          ],
          dark: [
            background: "bg-blue-500",
            text: "text-blue-800",
            contrast_text: "text-white",
            border: "border-blue-800",
            ring: "ring-pink-600"
          ],
          light: [
            background: "bg-blue-100",
            text: "text-blue-400",
            contrast_text: "text-blue-700",
            border: "border-blue-100",
            ring: "ring-pink-600"
          ]
        ],
        disabled: [
          main: [
            background: "bg-gray-50",
            text: "text-gray-700",
            contrast_text: "text-gray-400",
            border: "border-gray-200",
            ring: "ring-pink-600"
          ]
        ],
        neutral: [
          main: [
            background: "bg-gray-100",
            text: "text-gray-700",
            contrast_text: "text-gray-700",
            border: "border-gray-500",
            ring: "ring-pink-600"
          ],
          dark: [
            background: "bg-gray-500",
            text: "text-gray-800",
            contrast_text: "text-white",
            border: "border-gray-800",
            ring: "ring-pink-600"
          ],
          light: [
            background: "bg-gray-100",
            text: "text-gray-400",
            contrast_text: "text-gray-700",
            border: "border-gray-100",
            ring: "ring-pink-600"
          ]
        ],
        error: [
          main: [
            background: "bg-red-700",
            text: "bg-red-700",
            contrast_text: "text-white",
            border: "",
            ring: "ring-blue-500",
          ]
        ],
        info: [
          main: [
            background: "bg-blue-700",
            text: "bg-blue-700",
            contrast_text: "text-white",
            border: "",
            ring: "ring-pink-500",
          ]
        ],
        warning: [
          main: [
            background: "bg-yellow-600",
            text: "bg-yellow-700",
            contrast_text: "text-white",
            border: "border-transparent",
            ring: "ring-pink-500",
          ]
        ],
        success: [
          main: [
            background: "bg-green-600",
            text: "bg-green-700",
            contrast_text: "text-white",
            border: "border-transparent",
            ring: "ring-pink-500",
          ]
        ]
    ]

I think this could be moving in the right direction, what do you think?

@gdub01
Copy link

gdub01 commented May 30, 2021 via email

@Menkir
Copy link

Menkir commented Jun 1, 2021

Looks really cool @haubie ,
I had a quite similar approach for my reusable components, also using Tailwindcss/TailwindUi. I think this works perfect for small components. The biggest challenge for me was this approach for more complex components consisting of multiple configurable parts like e.g. a Dropdown. It consists of the actual menu and a button. The menu itself has entries. If you want to style it you end up with dozens of properties with similar names.
What I ended up was to separate configurable properties like color, size dimensions, fonts etc. to a css file. Things like Layout properties e.g. Flexbox, Grid etc. still remain within the component.

The Click Plugin injects prop click, :event and prop values, :list with the function phx_values/1 to convert the list of values to phx-value-* attributes. You can ignore the Subheadline and Text Components here.

For example I have the TailwindUi Alert:

defmodule Ui6Web.Components.Feedback.Alert do
  @moduledoc """
  Tailwind Feedback Alert
  Based on the following source: https://tailwindui.com/components/application-ui/feedback/alerts
  """
  use Surface.Component
  alias Ui6Web.Components.{Plugins, Custom}
  use Plugins.Click

  alias Custom.Typo.{SubHeadline, Text}

  @icons %{
    "default" => "",
    "info"    => "fas fa-exclamation-circle",
    "success" => "fas fa-check-circle",
    "warning" => "fas fa-exclamation-triangle",
    "error"   => "fas fa-times",
  }

  slot default

  @doc """
  Type of the Alert
  """
  prop type, :string, default: "default", values: ~w(default info warning error success)

  @doc """
  Title of the Alert
  """
  prop title, :string

  @doc """
  Set accent border to the left side of the Alert
  """
  prop accent, :boolean, default: false

  def render(assigns) do
  ~H"""
  <!-- This example requires Tailwind CSS v2.0+ -->
  <div class={{ "alert", accent: @accent}}
    data-type={{ @type }}
    :attrs={{ phx_values(@values) }}
    :on-click={{ @click }}>
    <div class="flex">
      <div class="flex-shrink-0">
        <span class="alert-icon" data-type={{ @type }}>
          <i class={{ get_icon(@type) }}></i>
        </span>
      </div>
      <div class="ml-3">
        <SubHeadline type={{ @type }}>
          {{ @title }}
        </SubHeadline>
        <div class="mt-2">
          <Text type={{ @type }}>
            <slot/>
          </Text>
        </div>
      </div>
    </div>
  </div>

  """
  end

  defp get_icon(type), do: Map.get(@icons, type)

The corresponding css looks like this:

.alert{
    @apply p-4 rounded-md;
}
.alert.accent{
    @apply border-l-4 rounded-none;
}
.alert[data-type="default"]{
    @apply bg-theme-soft border-theme;
}
.alert[data-type="warning"]{
    @apply border-yellow-400 bg-yellow-50;
}
.alert[data-type="success"]{
    @apply border-green-400 bg-green-50;
}
.alert[data-type="error"]{
    @apply border-red-400 bg-red-50;
}
.alert[data-type="info"]{
    @apply border-blue-400 bg-blue-50;
}

.alert-icon{
    @apply w-5 h-5 icon;
}
.alert-icon[data-type="default"] {
    @apply text-theme.softer;
}
.alert-icon[data-type="success"] {
    @apply text-green-400;
}
.alert-icon[data-type="error"] {
    @apply text-red-400;
}
.alert-icon[data-type="warning"] {
    @apply text-yellow-400;
}
.alert-icon[data-type="info"] {
    @apply text-blue-400;
}

The following example shows the usage:

<Alert title="Default">
...
</Alert>

<Alert title="Info" type="info">
  ...
</Alert>

<Alert title="Success" type="success">
  ...
</Alert>

<Alert title="Warning" type="warning">
  ...
</Alert>

<Alert title="Error" type="error">
  ...
</Alert>

image

The main reason to separate style and form was to have readable, smaller html. My approach is much less flexbile, but that was also intentional. A TailwindUI-Component-Library.

@haubie
Copy link

haubie commented Jun 1, 2021

What I ended up was to separate configurable properties like color, size dimensions, fonts etc. to a css file. Things like Layout properties e.g. Flexbox, Grid etc. still remain within the component.

That looks like a good approach too @Menkir. I wondered about that as well, using the @apply and creating styles in a css file instead. This maybe the best way to go to keep it as tailwind-ish as possible?

I haven't tried a more complex component as yet, but I will keep these ideas in mind. As component authors such as yourself explore this, I'm interested to learn about the benefits and trade-offs on the different approaches too! I think I'm rubbing against Tailwind CSS utility-first approach at times, when trying to maintain flexibility to override Tailwind's classes based on props.

Yesterday I started to author what I thought would be a trivial Grid component that all it does is layout any elements passed to it in the default prop in columns.

e.g. something like this:

<Grid cols="3" gap="4">
   <Card>1</Card>
   <Card>2</Card>
   <Card>3</Card>
   <Card>4</Card>
</Grid>

That looks really simple and neat, with a concise and understandable API. But then when I accomodate different breakpoints, going by that approach I'd be creating a cols prop for each breakpoint:

<Grid cols="3" sm_cols="2" md_cols="3" lg_cols="4" xl_cols="6" xxl_cols="8" gap="4">...

And potentially a similar number of breakpoint variations for the gap prop too.

I'm not sure if there is any advantage over just applying Tailwind's classes directly to a div:

<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 xxl:grid-cols-8
                     gap-4">...

The other thing which is a little annoying is that if I wanted to use interpolation like grid-cols-{{@cols}} in the component, where @cols is an integer like 3, the grid-col-3 class may never make it into the prod CSS due to Tailwind's JIT or PurgeCSS not recognising grid-cols-{{@cols}} as a valid tailwind class!

I really love using Tailwind, but when it comes to using it for completely flexible components in Surface, maybe I "can't have my cake and eat it" as the proverb goes! I'll keep trying though!

@rmayhue
Copy link

rmayhue commented Jul 8, 2021

I saw daisyUI - Tailwind CSS Components in a changelog.com newsletter a little while back. It's still a WIP as all it's components haven't been implemented yet, but it's off to a good start.

I'm also interested in a Surface Tailwind library and it's great to see so much interest in creating one.

@olivermt
Copy link

olivermt commented Jul 8, 2021

I dont understand how daisyUI can exist, it is literally one of the first thing they describe in what you can or cannot do with a Tailwind license.

@rmayhue
Copy link

rmayhue commented Jul 8, 2021

I don't think daisyUI is using or based on Tailwind UI code. It's just using the Tailwind CSS framework directly to create a component library, which is basically the same thing Tailwind UI is doing. The Tailwind CSS framework is using the MIT license.

@msaraiva
Copy link
Member Author

Hi everyone!

I think the time has come to try out some of the ideas discussed here.

Since I want to get back to work on the Surface Catalogue and make it an official supported tool (not only a prototype), it might be a good chance to redesign the UI using TailwindCSS.

In the process, I'll try to create some reusable components that could serve as the initial suite of components we want to provide in the context of this project.

In case anyone is interested in helping me on this journey, please let me know. Although I'll wait for Phoenix v1.6 before starting writing any code, I want to start discussing the design of the new UI straight away. I created a Replace Bulma with TailwindCSS issue on the surface_catalogue project so we can move on with the discussion there.

In case you haven't seen the catalogue in action, here's a short video demonstrating some of its features: https://twitter.com/MarlusSaraiva/status/1360254701808324615.

It requires some of the most common components, including Button, Table, Tabs, Menu, Modal, different types of form controls and many others. So I believe it's a suitable project for our goal.

Thank you all for the many useful ideas and the valuable information you brought to this discussion. They will be extremely important for us to decide the direction we should take.

❤️ ❤️ ❤️

@cvkmohan
Copy link

Hello @msaraiva, Have you considered https://uniformcss.com ? It is a scss based utility framework. That way, possibly we can avoid the postcss overhead also in the project. We can just move on with esbuild and esbuild-scss

@mayel
Copy link

mayel commented Oct 12, 2021

👋 I was just yesterday looking into DaisyUI as way to make using Tailwind a bit easier in our project (which uses Surface), so I'm quite excited discovering this repo!...

@wdiechmann
Copy link

@msaraiva what would be really cool is a totally agnostic Surface Catalogue - where all components are working with little to no css at all -

When we/community design components, we use whatever lib (like TailwindUI, Bulma, etc) is dearest to us, and we document that in the Surface Component Repo in some sort of way - but before we push, we .gitignore the lib 😄

In that way we get only the markup and then each develop can use the components that fits their use-case - by setting dependencies for surface_components and say tailwindcss, tailwindcss/forms.

I doubt it that a string of HTML markup is part of Tailwind Labs IP - and in fact I believe that Adam Wathan will sell more libs if more devs can easily utilize his work 😃

@lnr0626
Copy link

lnr0626 commented Oct 15, 2021

Tailwind released something along the lines of that which they called headless ui - that might be something to use as inspiration

@fodurrr
Copy link

fodurrr commented Aug 5, 2022

Hi, I am a beginner with Phoenix and Surface UI, coming from React and Vue.

Here is an idea: Why not use UnoCSS for styling? It can handle easily all Tailwind CSS classes easily.

Features: UnoCSS

It has many fantastic features, example pure css icons with more than 100K icons https://icones.js.org/
Attributify mode - group utilities in attributes.

Benchmark:
2022/7/2 08:38:12 PM
1656 utilities | x50 runs (min build time)

none 5.87 ms / delta. 0.00 ms
unocss v0.43.0 9.17 ms / delta. 3.30 ms (x1.00)
tailwindcss v3.1.4 497.24 ms / delta. 491.37 ms (x148.70)
windicss v3.5.5 869.47 ms / delta. 863.60 ms (x261.35)

It can be easily used instead of Tailwind and still configurable with presets to act like Tailwind or Boostrap etc. see UnoCSS presets and configurations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests