Show a vision's tint on its own page and on a pythiae

ae36961 · Gabriel Morell · 2026-08-11 20:12

20 files +79 -6
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/components/core_components.ex
+18 −0
@@ -884,4 +884,22 @@ defmodule RoomSanctumWeb.CoreComponents do
884 884 </div>
885 885 """
886 886 end
887 +
888 + @doc """
889 + The coloured dot marking a tinted source, query or vision.
890 +
891 + Renders nothing when there is no tint, so callers can hand it whatever a
892 + meta embed holds -- including a nil embed -- without guarding first.
893 +
894 + <.tint_dot tint={@vision.meta && @vision.meta.tint} />
895 + """
896 + attr :tint, :string, default: nil
897 + attr :class, :string, default: ""
898 +
899 + def tint_dot(assigns) do
900 + ~H"""
901 + <i :if={@tint not in [nil, ""]} class={"fa-solid fa-circle text-#{@tint}-500 #{@class}"}></i>
902 + """
903 + end
904 +
887 905 end
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/show.html.heex
+4 −2
@@ -37,8 +37,10 @@
37 37 >
38 38 <i class={"fas fa-fw fa-star #{if v == @pythiae.curr_vision, do: "text-accent"}"}></i>
39 39 </button>
40 − <a href={Routes.vision_show_path(@socket, :show, v)} class="text-sm hover:underline btn btn-sm btn-secondary">
41 − <%= get_by_id(v, @visions).name %>
40 + <a href={Routes.vision_show_path(@socket, :show, v)} class="text-sm hover:underline btn btn-sm btn-secondary gap-2">
41 + <% vision = get_by_id(v, @visions) %>
42 + <.tint_dot tint={vision.meta && vision.meta.tint} />
43 + <%= vision.name %>
42 44 </a>
43 45 </div>
44 46 <% end%>
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/show.html.heex
+2 −2
@@ -183,7 +183,7 @@
183 183 <%= for v <- @visions do %>
184 184 <div class="flex gap-2 mx-2">
185 185 <%= live_patch to: Routes.vision_show_path(@socket, :show, v.id), class: "btn btn-primary gap-2 grow" do %>
186 − <i :if={v.meta && v.meta.tint} class={"fa-solid fa-circle text-#{v.meta && v.meta.tint}-500"}></i>
186 + <.tint_dot tint={v.meta && v.meta.tint} />
187 187 <%= v.name %>
188 188 <% end %>
189 189 <%!-- Only while the panel is open, so the usual view of "the
@@ -207,7 +207,7 @@
207 207 way the visions this query already belongs to do. --%>
208 208 <%= for v <- @avail_visions do %>
209 209 <div class="btn btn-secondary gap-2 float-left mx-2" phx-click="add-to" phx-value-vision={v.id}>
210 − <i :if={v.meta && v.meta.tint} class={"fa-solid fa-circle text-#{v.meta && v.meta.tint}-500"}></i>
210 + <.tint_dot tint={v.meta && v.meta.tint} />
211 211 <%= v.name %>
212 212 </div>
213 213 <% end %>
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/index.html.heex
+1 −1
@@ -35,7 +35,7 @@
35 35 <%= for {id, vision} <- @streams.visions do %>
36 36 <tr id={"vision-#{vision.id}"}>
37 37 <td>
38 − <i :if={vision.meta && vision.meta.tint} class={"fa-solid fa-circle mr-2 text-#{vision.meta && vision.meta.tint}-500"}></i>
38 + <.tint_dot tint={vision.meta && vision.meta.tint} class="mr-2" />
39 39 <%= vision.name %>
40 40 </td>
41 41 <td>$query</td>
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/show.html.heex
+4 −1
@@ -18,7 +18,10 @@
18 18
19 19 <div class="mx-4 py-2">
20 20 <div class="flex">
21 − <h2 class="text-2xl font-bold text-accent-300"><%= @vision.name %></h2>
21 + <h2 class="text-2xl font-bold text-accent-300">
22 + <.tint_dot tint={@vision.meta && @vision.meta.tint} class="mr-2" />
23 + <%= @vision.name %>
24 + </h2>
22 25 </div>
23 26 <div class="flex flex-col flex-col-reverse sm:flex-col-reverse md:flex-col">
24 27 <div class="flex flex-wrap md:flex-nowrap flex-col-reverse md:flex-row">
added apps/room_sanctum/test/room_sanctum_web/components/tint_dot_test.exs
+35 −0
@@ -0,0 +1,35 @@
1 +defmodule RoomSanctumWeb.TintDotTest do
2 + @moduledoc """
3 + The dot marking a tinted source, query or vision. Every surface that shows a
4 + tint renders this, so it takes whatever a meta embed holds -- including a nil
5 + embed -- without the caller guarding first.
6 + """
7 + use ExUnit.Case, async: true
8 +
9 + import Phoenix.LiveViewTest
10 + import RoomSanctumWeb.CoreComponents
11 +
12 + test "renders the colour it is given" do
13 + html = render_component(&tint_dot/1, tint: "teal")
14 +
15 + assert html =~ "text-teal-500"
16 + assert html =~ "fa-circle"
17 + end
18 +
19 + test "renders nothing without a tint" do
20 + for tint <- [nil, ""] do
21 + assert render_component(&tint_dot/1, tint: tint) |> String.trim() == "",
22 + "rendered something for #{inspect(tint)}"
23 + end
24 + end
25 +
26 + test "takes extra classes for spacing" do
27 + assert render_component(&tint_dot/1, tint: "rose", class: "mr-2") =~ "mr-2"
28 + end
29 +
30 + test "every colour in the palette renders its own class" do
31 + for tint <- RoomSanctum.Tints.all() do
32 + assert render_component(&tint_dot/1, tint: tint) =~ "text-#{tint}-500"
33 + end
34 + end
35 +end
modified apps/room_sanctum/test/room_sanctum_web/live/vision_tint_test.exs
+15 −0
@@ -134,6 +134,21 @@ defmodule RoomSanctumWeb.VisionTintTest do
134 134 assert html =~ "text-teal-500"
135 135 end
136 136
137 + test "the vision's own page shows it beside the name", ctx do
138 + v = vision(ctx.user, %{meta: %{tint: "teal"}})
139 +
140 + {:ok, _live, html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :show, v))
141 +
142 + assert html =~ "text-teal-500"
143 + # beside the name, not somewhere else on the page
144 + assert html =~ ~r/text-teal-500.{0,120}Dash/s
145 + end
146 +
147 + # The pythiae page cannot be rendered from this app's tests: it reads
148 + # users_rabbit, which belongs to room_hermes and is not migrated into
149 + # room_sanctum's test database. Its dot comes from the same tint_dot
150 + # component covered below.
151 +
137 152 test "an untinted vision gets no dot", ctx do
138 153 vision(ctx.user)
139 154

Parents: fc5bc54