diff --git a/mix.exs b/mix.exs index a6657b8..8fe7fc5 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,7 @@ defmodule Shadix.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:phoenix_live_view, "~> 1.1"}, + {:phoenix_live_view, "~> 1.2"}, {:jason, "~> 1.4"}, # Dev/test harness + docs deployment: assertion helpers and TS hook builds. diff --git a/mix.lock b/mix.lock index 35b40a3..61274ab 100644 --- a/mix.lock +++ b/mix.lock @@ -22,7 +22,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.32", "4977ad4cfab7868f3d2ba390a2cd6c62412614d48f798e85785602281a0a3827", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c9d8e76373414259ec5699a809991d1324808bee1d297730599e273282cee34"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.3", "e8abe7f2acb96d98695e9290b35cb5a01a696cc2086d4ff2e0b3aa8b9c74675f", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "449affd6aea24daaa2f6b43748fc1e2c6a87610df996cc1f54e7b19a7a18e638"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phosphor_live_view": {:hex, :phosphor_live_view, "2.1.0", "41a433b3a9493896ce085460c1e67ab23979b8b7ce82406a63029b8cc638c348", [:mix], [{:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "723f2d92765e1b096b2b117651c4564576d3e693d155273026c0f556de87811a"}, -- 2.51.2 From f6ef260c8e5b83af7e4aa573ad0a144a89faa971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Sat, 20 Jun 2026 00:49:03 +0100 Subject: [PATCH 02/12] feat: ColocatedCSS module, namespace rewrite, :shadix config, render_file Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/shadix/colocated_css.ex | 17 ++++++++++ lib/shadix/generator.ex | 54 ++++++++++++++++++++++++++++++ test/shadix/colocated_css_test.exs | 8 +++++ test/shadix/generator_test.exs | 53 +++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 lib/shadix/colocated_css.ex create mode 100644 test/shadix/colocated_css_test.exs diff --git a/lib/shadix/colocated_css.ex b/lib/shadix/colocated_css.ex new file mode 100644 index 0000000..0d3da36 --- /dev/null +++ b/lib/shadix/colocated_css.ex @@ -0,0 +1,17 @@ +defmodule Shadix.ColocatedCSS do + @moduledoc """ + HEEx `:type` for shadix's colocated component CSS. + + Extracts the contents of a component's `") + + :error -> + content + end + end + + defp inject_style(content, _styles, _style), do: content + @doc """ Returns the default target namespace for the current Mix project. diff --git a/test/shadix/colocated_css_test.exs b/test/shadix/colocated_css_test.exs new file mode 100644 index 0000000..49603e6 --- /dev/null +++ b/test/shadix/colocated_css_test.exs @@ -0,0 +1,8 @@ +defmodule Shadix.ColocatedCSSTest do + use ExUnit.Case, async: true + + test "transform/4 returns the css unchanged with no root attrs" do + assert Shadix.ColocatedCSS.transform("style", %{}, ".x{color:red}", %{}) == + {:ok, ".x{color:red}", []} + end +end diff --git a/test/shadix/generator_test.exs b/test/shadix/generator_test.exs index e0b6b98..6effe89 100644 --- a/test/shadix/generator_test.exs +++ b/test/shadix/generator_test.exs @@ -45,6 +45,11 @@ defmodule Shadix.GeneratorTest do assert out == "import Demo.UI.Form\n Demo.UI.Form.field_attrs(x)" end + test "rewrites Shadix.ColocatedCSS to the target namespace" do + assert Generator.rewrite_namespace(":type={Shadix.ColocatedCSS}", "Demo.UI") == + ":type={Demo.UI.ColocatedCSS}" + end + test "leaves unrelated text unchanged" do source = "defmodule MyApp.Something do\n @doc \"unrelated\"\nend" result = Generator.rewrite_namespace(source, "Demo.UI") @@ -52,6 +57,54 @@ defmodule Shadix.GeneratorTest do end end + describe "configured_style/1 + configured_namespace/1 + configured_dir/2" do + test "configured_style prefers opts, then app env, then nova" do + assert Generator.configured_style(style: "vega") == "vega" + Application.put_env(:shadix, :style, "vega") + assert Generator.configured_style([]) == "vega" + Application.delete_env(:shadix, :style) + assert Generator.configured_style([]) == "nova" + end + + test "configured_namespace prefers opts, then app env, then default" do + assert Generator.configured_namespace(namespace: "A.B") == "A.B" + Application.put_env(:shadix, :namespace, "C.D") + assert Generator.configured_namespace([]) == "C.D" + Application.delete_env(:shadix, :namespace) + assert Generator.configured_namespace([]) == "ShadixWeb.Components.UI" + end + + test "configured_dir prefers opts, then app env, then default_dir" do + assert Generator.configured_dir([dir: "x/y"], "A.B") == "x/y" + Application.put_env(:shadix, :dir, "z") + assert Generator.configured_dir([], "A.B") == "z" + Application.delete_env(:shadix, :dir) + assert Generator.configured_dir([], "Demo") == "lib/demo" + end + end + + describe "render_file/4" do + test "injects chosen style at the marker and rewrites namespace" do + content = ~s(<%!-- shadix:colocated-css --%>\n) + styles = %{"nova" => "@layer components { [data-slot=button]{height:2rem} }"} + out = Generator.render_file(content, styles, "nova", "Demo.UI") + assert out =~ ":type={Demo.UI.ColocatedCSS}" + assert out =~ "height:2rem" + refute out =~ "shadix:colocated-css" + end + + test "leaves legacy (markerless) content to namespace rewrite only" do + assert Generator.render_file("Shadix.Cn.cn([])", nil, "nova", "Demo.UI") == + "Demo.UI.Cn.cn([])" + end + + test "passes through when the style key is absent from styles map" do + content = "<%!-- shadix:colocated-css --%>\nx" + out = Generator.render_file(content, %{"nova" => "css"}, "vega", "Demo.UI") + assert out =~ "shadix:colocated-css" + end + end + describe "default_namespace/0" do test "returns a camelize+Web.Components.UI namespace derived from app name" do # This calls Mix.Project.config()[:app], which in test context is :shadix -- 2.51.2 From 682ec2be86a343384e43cf7251f698141bb04c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Sat, 20 Jun 2026 00:51:59 +0100 Subject: [PATCH 03/12] feat(gen.registry,add): per-style manifests + --style colocated injection - gen.registry: manifest_for/2 carries styles map + default_style + colocated_css dep for marker components; reads priv/styles/` block for the chosen style (then namespace-rewrites), + so the consumer's component is self-contained with exactly one style. Style/namespace/dir are + persisted in a `config :shadix` block by `shadix.init` (the `components.json` analog); precedence + is `--style` flag > config > `default_style`. +- **`Shadix.ColocatedCSS`** (`lib/shadix/colocated_css.ex`) is the LiveView 1.2 `:type` module; + it's installed into the consumer (namespace-rewritten) like the `cn` helper. +- **Deps:** colocated CSS needs **LiveView ≥ 1.2** + **Tailwind ≥ 4.2.3**; the skins `@apply` + utilities from **`tw-animate-css`** (consumers `npm i tw-animate-css`) and a `no-scrollbar` + `@utility` — both wired by `theme.css`. +- **Editing a component's look:** change `priv/styles/