Skip to content

Commit

Permalink
Merge pull request #84 from bluzky/feature/tailwind-merge
Browse files Browse the repository at this point in the history
Replace Tails by turboprop
  • Loading branch information
bluzky authored Nov 15, 2024
2 parents befb9e7 + 63292e7 commit c0d36d4
Show file tree
Hide file tree
Showing 35 changed files with 1,960 additions and 251 deletions.
31 changes: 9 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,20 @@

## Installation

1. **Using `salad_ui` as part of your project:**

> This way you can install only components that you want to use or you want to edit SaladUI's component source code to fit your need.
> If you just want to use SaladUI's components, see **Using as library** below.
- Adding `salad_ui` to your list of dependencies in `mix.exs`:

1. Add `salad_ui` to your `mix.exs`
```elixir
def deps do
[
{:salad_ui, "~> 0.13.0", only: [:dev]},
{:tails, "~> 0.1"}
{:salad_ui, "~> 0.13.0"},
]
end
```

2. **Using `salad_ui` as part of your project:**

> This way you can install only components that you want to use or you want to edit SaladUI's component source code to fit your need.
> If you just want to use SaladUI's components, see **Using as library** below.
- Init Salad UI in your project
```
#> cd your_project
Expand All @@ -52,18 +50,7 @@ end
#> mix salad.add label button
```

2. **Using `salad_ui` as a library:**

- Adding `salad_ui` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:salad_ui, "~> 0.13.0", only: [:dev]}
]
end
```

3. **Using `salad_ui` as a library:**
- Init Salad UI in your project with option `--as-lib`
```
#> cd your_project
Expand Down Expand Up @@ -169,6 +156,6 @@ To run the failing tests only, just run `mix test.watch --stale`.
This project could not be available without these awesome works:

- `tailwind css` an awesome css utility project
- `tails` for merging tailwind class
- `turboprop` I borrow code from here for merging tailwinds classes
- `shadcn/ui` which this project is inspired from
- `Phoenix Framework` of course
2 changes: 0 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,3 @@ config :tailwind,
),
cd: Path.expand("../assets", __DIR__)
]

config :tails, colors_file: Path.join(File.cwd!(), "assets/tailwind.colors.json")
14 changes: 2 additions & 12 deletions docs/manual_install.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,7 @@ npm i -D tailwindcss-animate
yarn add -D tailwindcss-animate
```

3. Configure `tails`
SaladUI use `tails` to properly merge Tailwindcss classes

```elixir
# config/config.exs

config :tails, colors_file: Path.join(File.cwd!(), "assets/tailwind.colors.json")
```


4. **Add javascript to handle event from server**
3. **Add javascript to handle event from server**
This add ability to execute client action from server. It's similar to `JS.exec/2`. Thanks to [this post](https://fly.io/phoenix-files/server-triggered-js/) from fly.io.

Add this code snippet to the end of `app.js`
Expand All @@ -106,7 +96,7 @@ Then from server side, you can close an opening sheet like this.
end
```

5. Some tweaks
4. Some tweaks
Thanks to @ahacking

- To make dark and light mode work correctly, add following to your `app.css`
Expand Down
18 changes: 1 addition & 17 deletions lib/mix/tasks/salad.init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ defmodule Mix.Tasks.Salad.Init do
end

defp write_config(component_path) do
with :ok <- write_dev_config(component_path) do
write_tails_config()
end
write_dev_config(component_path)
end

defp write_dev_config(component_path) do
Expand All @@ -110,20 +108,6 @@ defmodule Mix.Tasks.Salad.Init do
patch_config(dev_config_path, components_config)
end

defp write_tails_config do
Mix.shell().info("Writing tails config to config.exs")
config_path = Path.join(File.cwd!(), "config/config.exs")

tails_config = [
tails: %{
description: "SaladUI use tails to properly merge Tailwind CSS classes",
values: [colors_file: "Path.join(File.cwd!(), \"assets/tailwind.colors.json\")"]
}
]

patch_config(config_path, tails_config)
end

defp patch_config(config_path, config) do
if File.exists?(config_path) do
Patcher.patch_config(config_path, config)
Expand Down
4 changes: 3 additions & 1 deletion lib/salad_ui.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ defmodule SaladUI do
use Phoenix.Component

import SaladUI.Helpers
import Tails, only: [classes: 1]

# alias OrangeCmsWeb.Components.LadUI.LadJS
alias Phoenix.LiveView.JS
defp classes(input) do
SaladUI.Merge.merge(input)
end
end
end

Expand Down
44 changes: 44 additions & 0 deletions lib/utils/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule SaladUI.Cache do
@moduledoc false
use GenServer

alias SaladUI.Merge.ClassTree

@default_table_name :turboprop_cache

@doc false
def default_table_name, do: @default_table_name

def start_link(default) when is_list(default) do
GenServer.start_link(__MODULE__, default)
end

@impl true
def init(opts \\ []) do
table_name = Keyword.get(opts, :cache_table_name, @default_table_name)
create_table(table_name)

insert(:class_tree, ClassTree.generate())

{:ok, []}
end

def create_table(table_name \\ @default_table_name) do
:ets.new(table_name, [:set, :public, :named_table, read_concurrency: true])
end

def insert(key, value, table_name \\ @default_table_name) do
:ets.insert(table_name, {key, value})
end

def retrieve(key, table_name \\ @default_table_name) do
case :ets.lookup(table_name, key) do
[{^key, value}] -> value
[] -> nil
end
end

def purge(table_name \\ @default_table_name) do
:ets.delete_all_objects(table_name)
end
end
141 changes: 141 additions & 0 deletions lib/utils/merge.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
defmodule SaladUI.Merge do
@moduledoc """
SaladUI Merge adds efficient class joining and merging of TailwindCSS classes to Elixir.
TailwindCSS class names are composable and allow specifying an infinite amount of different styles. Most components allow overriding class
names, like as passing `class` attribute that then gets merged with the existing styles. This can result in class lists such as
`text-white bg-red-500 bg-blue-300` where `text-white bg-red-500` is the preset style, and `bg-blue-300` is the override for that one
specific button that needs to look slightly different.
Styles based on class are applied according to the order _they are defined at in the stylesheet_. In this example, because TailwindCSS
orders color definitions alphabetically, the override does not work. `blue` is defined before `red`, so the `bg-red-500` class takes
precedence since it was defined later.
In order to still allow overriding styles, SaladUI Merge traverses the entire class list, creates a list of all classes and which
conflicting groups of styles exist in them and gives precedence to the ones that were defined last _in the class list_, which, unlike the
stylesheet, is in control of the user.
## Example
```elixir
iex> merge("text-white bg-red-500 bg-blue-300")
"text-white bg-blue-300"
iex> merge(["px-2 py-1 bg-red hover:bg-dark-red", "p-3 bg-[#B91C1C]"])
"hover:bg-dark-red p-3 bg-[#B91C1C]"
```
## Configuration
SaladUI Merge does not currently support full theme configuration - that's on the roadmap!
The limited configuration at the moment is adding Tailwind's `prefix` option.
```elixir
config :turboprop,
prefix: "tw-"
```
"""

alias SaladUI.Cache
alias SaladUI.Merge.Class
alias SaladUI.Merge.Config

@doc """
Joins and merges a list of classes.
Passes the input to `join/1` before merging.
"""
@spec merge(list(), term()) :: binary()
def merge(input, config \\ Config.config()) do
input
|> join()
|> retrieve_from_cache_or_merge(config)
end

@doc """
Joins a list of classes.
"""
@spec merge(binary() | list()) :: binary()
def join(input) when is_binary(input), do: input
def join(input) when is_list(input), do: do_join(input, "")
def join(_), do: ""

defp do_join("", result), do: result
defp do_join(nil, result), do: result
defp do_join([], result), do: result

defp do_join(string, result) when is_binary(string), do: do_join([string], result)

defp do_join([head | tail], result) do
case to_value(head) do
"" -> do_join(tail, result)
value when result == "" -> do_join(tail, value)
value -> do_join(tail, result <> " " <> value)
end
end

defp to_value(value) when is_binary(value), do: value

defp to_value(values) when is_list(values) do
Enum.reduce(values, "", fn v, acc ->
case to_value(v) do
"" -> acc
resolved_value when acc == "" -> resolved_value
resolved_value -> acc <> " " <> resolved_value
end
end)
end

defp to_value(_), do: ""

defp retrieve_from_cache_or_merge(classes, config) do
case Cache.retrieve("merge:#{classes}") do
nil ->
merged_classes = do_merge(classes, config)
Cache.insert("merge:#{classes}", merged_classes)
merged_classes

merged_classes ->
merged_classes
end
end

defp do_merge(classes, config) do
classes
|> String.trim()
|> String.split(~r/\s+/)
|> Enum.map(&Class.parse/1)
|> Enum.reverse()
|> Enum.reduce(%{classes: [], groups: []}, fn class, acc ->
handle_class(class, acc, config)
end)
|> Map.get(:classes)
|> Enum.join(" ")
end

defp handle_class(%{raw: raw, tailwind?: false}, acc, _config), do: Map.update!(acc, :classes, fn classes -> [raw | classes] end)

defp handle_class(%{conflict_id: conflict_id} = class, acc, config) do
if Enum.member?(acc.groups, conflict_id), do: acc, else: add_class(acc, class, config)
end

defp add_class(acc, %{raw: raw, group: group, conflict_id: conflict_id, modifier_id: modifier_id}, config) do
conflicting_groups =
group
|> conflicting_groups(config)
|> Enum.map(&"#{modifier_id}:#{&1}")
|> then(&[conflict_id | &1])

acc
|> Map.update!(:classes, fn classes -> [raw | classes] end)
|> Map.update!(:groups, fn groups -> groups ++ conflicting_groups end)
end

defp conflicting_groups(group, config) do
conflicts = Map.get(config.conflicting_groups, group, [])
modifier_conflicts = Map.get(config.conflicting_group_modifiers, group, [])

conflicts ++ modifier_conflicts
end
end
Loading

0 comments on commit c0d36d4

Please sign in to comment.