Put the vision tint picker where it actually renders

fc5bc54 · Gabriel Morell · 2026-08-11 20:04

15 files +80 -0
Message
{commit_body(@commit)}

Files changed

modified ?
+0 −0

Binary file — not rendered.

modified ?
+0 −0

Binary file — not rendered.

modified ?
+0 −0

Binary file — not rendered.

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/form_component.ex
+35 −0
@@ -25,6 +25,41 @@ defmodule RoomSanctumWeb.VisionLive.FormComponent do
25 25
26 26 <.input field={@form[:public]} type="checkbox" label="Make this vision public" />
27 27
28 + <.inputs_for :let={meta} field={@form[:meta]}>
29 + <div class="form-control mt-4">
30 + <label class="label label-text">Tint</label>
31 + <div class="flex flex-wrap gap-2">
32 + <%!-- The radio is sr-only; the swatch carries the whole state,
33 + the same way bulk paint does. --%>
34 + <label class="cursor-pointer" title="No tint">
35 + <input
36 + type="radio"
37 + name={meta[:tint].name}
38 + value=""
39 + class="sr-only"
40 + checked={meta[:tint].value in [nil, ""]}
41 + />
42 + <span class={"#{tint_swatch(meta[:tint].value in [nil, ""])} bg-base-300"}>
43 + <i class="fa-solid fa-ban text-xs opacity-60"></i>
44 + </span>
45 + </label>
46 +
47 + <label :for={tint <- RoomSanctum.Tints.all()} class="cursor-pointer" title={tint}>
48 + <input
49 + type="radio"
50 + name={meta[:tint].name}
51 + value={tint}
52 + class="sr-only"
53 + checked={to_string(meta[:tint].value) == tint}
54 + />
55 + <span class={"#{tint_swatch(to_string(meta[:tint].value) == tint)} bg-#{tint}-500"}>
56 + <i :if={to_string(meta[:tint].value) == tint} class="fa-solid fa-check text-xs text-white"></i>
57 + </span>
58 + </label>
59 + </div>
60 + </div>
61 + </.inputs_for>
62 +
28 63 <div class="mt-6">
29 64 <h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Queries</h3>
30 65
modified apps/room_sanctum/test/room_sanctum_web/live/vision_tint_test.exs
+45 −0
@@ -80,6 +80,51 @@ defmodule RoomSanctumWeb.VisionTintTest do
80 80 end
81 81 end
82 82
83 + describe "the editor" do
84 + # The picker first went into cfg_vision_live/form_component.html.heex,
85 + # which nothing renders: the component defines render/1 inline, and that
86 + # wins. Storage and display tests both passed while the picker was
87 + # invisible, so these drive the editor itself.
88 + test "offers the palette when editing an untinted vision", ctx do
89 + v = vision(ctx.user)
90 +
91 + {:ok, _live, html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :edit, v))
92 +
93 + assert html =~ ">Tint<"
94 +
95 + assert length(Regex.scan(~r/vision\[meta\]\[tint\]/, html)) ==
96 + length(RoomSanctum.Tints.all()) + 1
97 + end
98 +
99 + test "offers the palette for a brand new vision", ctx do
100 + {:ok, _live, html} = live(ctx.conn, Routes.vision_index_path(ctx.conn, :new))
101 +
102 + assert length(Regex.scan(~r/vision\[meta\]\[tint\]/, html)) ==
103 + length(RoomSanctum.Tints.all()) + 1
104 + end
105 +
106 + test "pre-selects the tint the vision already has", ctx do
107 + v = vision(ctx.user, %{meta: %{tint: "teal"}})
108 +
109 + {:ok, _live, html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :edit, v))
110 +
111 + assert html =~ ~s(value="teal" class="sr-only" checked)
112 + refute html =~ ~s(value="rose" class="sr-only" checked)
113 + end
114 +
115 + test "saving from the form keeps the tint", ctx do
116 + v = vision(ctx.user)
117 +
118 + {:ok, live, _html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :edit, v))
119 +
120 + live
121 + |> form("#vision-form", vision: %{name: "Dash", meta: %{tint: "orange"}})
122 + |> render_submit()
123 +
124 + assert Configuration.get_vision!(v.id).meta.tint == "orange"
125 + end
126 + end
127 +
83 128 describe "showing it" do
84 129 test "the vision list marks a tinted vision", ctx do
85 130 vision(ctx.user, %{meta: %{tint: "teal"}})

Parents: 319490f