{"id":26517987,"url":"https://github.com/raulrpearson/yatm","last_synced_at":"2025-03-21T09:19:26.534Z","repository":{"id":283235799,"uuid":"944297155","full_name":"raulrpearson/yatm","owner":"raulrpearson","description":"A Tailwind class merging utility","archived":false,"fork":false,"pushed_at":"2025-03-19T07:20:25.000Z","size":15,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-19T07:42:00.562Z","etag":null,"topics":["css","elixir","tailwindcss","utility-classes"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/raulrpearson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-07T05:29:11.000Z","updated_at":"2025-03-19T07:27:10.000Z","dependencies_parsed_at":"2025-03-19T07:42:02.801Z","dependency_job_id":"6e0923f7-4a49-4eb5-bdb3-92910793f56b","html_url":"https://github.com/raulrpearson/yatm","commit_stats":null,"previous_names":["raulrpearson/yatm"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raulrpearson%2Fyatm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raulrpearson%2Fyatm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raulrpearson%2Fyatm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raulrpearson%2Fyatm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/raulrpearson","download_url":"https://codeload.github.com/raulrpearson/yatm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244759940,"owners_count":20505716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css","elixir","tailwindcss","utility-classes"],"created_at":"2025-03-21T09:19:26.067Z","updated_at":"2025-03-21T09:19:26.526Z","avatar_url":"https://github.com/raulrpearson.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Yatm\n\n[![Hex Version](https://img.shields.io/hexpm/v/yatm.svg)](https://hex.pm/packages/yatm)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/yatm)\n\n\u003e #### Warning {: .warning}\n\u003e\n\u003e This project is currently in an experimental proof-of-concept stage. It's\n\u003e still taking shape and many Tailwind features are not yet supported. You're\n\u003e welcome to try it out and encouraged to share your feedback, but don't expect\n\u003e it to serve your production needs just yet.\n\n## Usage\n\nAdd `yatm` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:yatm, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\nWherever you potentially need to resolve Tailwind class conflicts, call the\n`merge/1` function like so:\n\n```elixir\ndef button(assigns) do\n  ~H\"\"\"\n  \u003cbutton type={@type} class={merge([button_classes(), @class])} {@rest}\u003e\n    {render_slot(@inner_block)}\n  \u003c/button\u003e\n  \"\"\"\nend\n```\n\n## Motivation\n\nWhen using Tailwind with component abstractions, it's not uncommon for the\nauthors of components to provide some basic styling out of the box and for\nusers of those components to want to override some (but not all) of those\nclasses.\n\nWithout a Tailwind class merging utility, component authors usually choose to\neither fully overwrite their out of the box classes or just append the classes\nsupplied by the user to the ones provided in the component. See, for example,\nthe [button\ncomponent](https://github.com/phoenixframework/phoenix/blob/183eb76a88874729cdd5642da03ad7cebd3fa6d3/installer/templates/phx_web/components/core_components.ex#L164)\nincluded in new Phoenix projects:\n\n```elixir\ndef button(assigns) do\n  ~H\"\"\"\n  \u003cbutton type={@type} class={[button_classes(), @class]} {@rest}\u003e\n    {render_slot(@inner_block)}\n  \u003c/button\u003e\n  \"\"\"\nend\n```\n\nIf the user provides some additional classes with the `@class` attribute, these\nwill get appended to the existing ones. This can very easily lead to [class\nconflicts](https://tailwindcss.com/docs/styling-with-utility-classes#conflicting-utility-classes).\n\nFor example, if the user were to supply a `text-xl` class, there's no guarantee\n(in the general case) that Tailwind will build a CSS file where the `text-xl`\nis declared after the `text-sm` class (i.e. overriding the `font-size` property\nwith the CSS cascade). If it did, this might still be a problem if a `text-sm`\nwere intended to override a `text-xl` somewhere else in the codebase.\n\nThis is the problem that a Tailwind class merging utility is designed to\naddress. It works by parsing the list of concatenated classes and using\nknowledge of the CSS properties targeted by each Tailwind utility class to\nremove classes that are meant to be overriden by conflicting classes appearing\nlater in the string.\n\nCalling `Yatm.merge(\"text-sm text-xl\")` will return `\"text-xl\"` and calling\n`Yatm.merge(\"text-xl text-sm\")` will return `\"text-sm\"`.\n\n## Roadmap\n\n### Finish the `merge` utility\n\nThe first milestone is to finish building a `merge/1` function that can take\nthe same inputs as the regular `class` attribute in HEEx templates–a binary or\na list of binaries with nil and false values being discarded.\n\n### Concerns about (re)compilation\n\nI'm currently implementing this using\n[nimble_parsec](https://hex.pm/packages/nimble_parsec). I'm concerned that\ncompiling the parser is taking on the order of dozens of seconds and the\nresulting BEAM file is in the MBs. If this compilation only needs to run once,\nthat could be fine.\n\nThe problem is that the implementation will eventually have to take into\naccount the user's custom Tailwind theme, which could add new utility classes.\nIf the theme changes, then maybe that results in a parser recompilation, which\nis not ideal.\n\nI still need to explore the space of solutions and problems that might arise as\nthe implementation develops.\n\n### Concerns about runtime performance\n\nMy knowledge of the Phoenix rendering pipeline has some gaps but, as far as I\ncan tell, the code above would have to run the merge on every template\nre-render.\n\nMaybe the run time of the function is so small that there's not a big reason to\ntry to optimize it away but I'm not sure at this moment, I'd like to do some\nbenchmarking at some point.\n\nOther libraries memoize these computations. For example,\n[tw_merge](https://hex.pm/packages/tw_merge), which was forked from\n[turboprop](https://hex.pm/packages/turboprop), requires adding a cache process\nto your supervision tree.\n\nTurboprop's merge utility seems to have been implemented as a port of\n[tailwind-merge](https://github.com/dcastil/tailwind-merge), the canonical\nTailwind merge utility in the JavaScript ecosystem, which itself implements an\n[LRU\ncache](https://github.com/dcastil/tailwind-merge/blob/f4eacb6bc1800031147a153fcf20e586b277320e/src/lib/lru-cache.ts).\n\nIt could be that I'm just too early in my Elixir journey, but the idea of\nadding a process to the supervision tree just to merge strings together doesn't\nfeel super enticing. Could we find a better solution? Read on!\n\n### Building a custom EEx engine\n\nWhat if the templating engine was able to do this Tailwind class merging\nitself? No need to manually call the `merge` function every time. EEx provides\nthe [Engine behaviour](https://hexdocs.pm/eex/EEx.Engine.html) to enable this\nkind of extension. As far as I understand, EEx templates are usually used in a\nway in which they're precompiled, so maybe there's a way to \"memoize\" these\n(maybe expensive) class merging operations within the templates themselves.\n\nIf a component's class attribute takes a list of static string values known at\ncompile-time, we could run the merge and just inline the result into the\ncompiled template code, resulting in zero run-time cost.\n\nIf there's some run-time logic that makes the class value dynamic, it might\nstill be possible to transform the code and precompute the necessary merge\noperations at compile-time. For example, the code `class={[\"p-1 text-sm\", if\nsome_condition, do: \"text-xl\", else: \"text-2xl\"]}` could be transformed into\nsomething like `class={if some_condition, do: \"p-1 text-xl\", else: \"p-1\ntext-2xl\"}` so the minimum amount of work is left to be done at run-time.\n\nWe get the best of both worlds: no run-time cost (not even for cache misses)\nand no extra cache process and the latency, jitter and memory footprint that\ncould potentially have. The API is also better as merging happens without\nhaving to call the merge utility explicitly every time, the template engine\njust takes care of it according to a config value. The cost would be larger\ncompile times, obviously.\n\n### Integrating into Phoenix?\n\nWouldn't it be great if Phoenix shipped with this functionality out of the box?\nI think so. Given that Tailwind is so commonly used, it'd be nice to have\nTailwind class merging as one of the features of the templating engine shipped\nwith Phoenix. I assume there'd be an easy way to switch this feature on and off\nso that projects not using Tailwind wouldn't have to pay the compilation price.\n\n## Ecosystem\n\nA list of alternatives and other Tailwind-related packages:\n\n- [tailwind](https://hex.pm/packages/tailwind).\n- [tailwind_merge](https://hex.pm/packages/tailwind_merge).\n- [twix](https://hex.pm/packages/twix).\n- [tw_merge](https://hex.pm/packages/tw_merge).\n- [tailwind_formatter](https://hex.pm/packages/tailwind_formatter).\n- [tails](https://hex.pm/packages/tails).\n- [turboprop](https://hex.pm/packages/turboprop).\n\n## License\n\n[Blue Oak Model License 1.0.0](LICENSE.md), a [modern\nalternative](https://writing.kemitchell.com/2019/03/09/Deprecation-Notice.html)\nto the MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraulrpearson%2Fyatm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fraulrpearson%2Fyatm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraulrpearson%2Fyatm/lists"}