From 675a2ab288fde0d358eb0bd038e6419a6000a544 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Mon, 4 May 2026 15:49:37 -0600 Subject: [PATCH 01/22] Phase 1 & 2: Remove prefix, bold, italic, and highlighted features Deletes the prefix field and bold/italic/highlighted formatting fields from the schema, API, toolbar, and templates. These features added complexity without being used in practice. Two reversible migrations drop the columns from the database. Co-Authored-By: Claude Sonnet 4.6 --- assets/css/app.css | 42 ----------- lib/elixdo/list_item.ex | 8 --- lib/elixdo/lists/db.ex | 6 +- lib/elixdo_web/controllers/api/item_json.ex | 4 -- lib/elixdo_web/live/list_live.ex | 18 ++--- lib/elixdo_web/live/list_live.html.heex | 26 ------- ...0504213717_drop_prefix_from_list_items.exs | 15 ++++ ...old_italic_highlighted_from_list_items.exs | 19 +++++ test/elixdo/lists_test.exs | 39 +++++------ .../controllers/api/list_controller_test.exs | 4 -- .../elixdo_web/live/list_live_phase5_test.exs | 16 ----- .../live/list_live_remove_formats_test.exs | 69 ++----------------- 12 files changed, 64 insertions(+), 202 deletions(-) create mode 100644 priv/repo/migrations/20260504213717_drop_prefix_from_list_items.exs create mode 100644 priv/repo/migrations/20260504214315_drop_bold_italic_highlighted_from_list_items.exs diff --git a/assets/css/app.css b/assets/css/app.css index 434d73e..8267fec 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -309,23 +309,6 @@ body { .swatch-purple { background: var(--c-purple); } .swatch-orange { background: var(--c-orange); } -/* Prefix input — matches toolbar height */ -.prefix-form { display: flex; align-items: center; } -.prefix-input { - width: 44px; - height: 40px; - padding: 0 8px; - background: var(--bg-4); - border: 1px solid var(--border-light); - border-radius: var(--radius-btn); - color: var(--text); - font-family: inherit; - font-size: 12px; - text-align: center; -} -.prefix-input::placeholder { color: var(--text-dim); } -.prefix-input:focus { outline: none; border-color: var(--accent); } - /* Today button */ .today-btn { display: flex; @@ -545,23 +528,6 @@ body { background: white; } -/* ─── Prefix badge ──────────────────────────────────────────────────── */ - -.prefix-badge { - display: flex; - align-items: center; - justify-content: center; - min-width: 28px; - height: 28px; - padding: 0 5px; - background: var(--bg-4); - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 15px; - line-height: 1; - flex-shrink: 0; -} - /* ─── Item body ─────────────────────────────────────────────────────── */ .item-body { @@ -573,14 +539,6 @@ body { line-height: 1.2; cursor: pointer; } -.item-body.bold { font-weight: 700; } -.item-body.italic { font-style: italic; } -.item-body.highlighted .item-text { - background: oklch(85% 0.16 90 / 0.28); - border-radius: 3px; - padding: 0 2px; -} - .item-body.color-red { color: oklch(72% 0.20 17); } .item-body.color-blue { color: oklch(72% 0.18 240); } .item-body.color-green { color: oklch(72% 0.16 145); } diff --git a/lib/elixdo/list_item.ex b/lib/elixdo/list_item.ex index 0e7256b..6fd43ea 100644 --- a/lib/elixdo/list_item.ex +++ b/lib/elixdo/list_item.ex @@ -12,10 +12,6 @@ defmodule Elixdo.ListItem do default: :active field :color, Ecto.Enum, values: [:red, :blue, :green, :purple, :orange] - field :bold, :boolean, default: false - field :italic, :boolean, default: false - field :highlighted, :boolean, default: false - field :prefix, :string field :arrowed_to_date, :date timestamps(type: :utc_datetime) @@ -29,10 +25,6 @@ defmodule Elixdo.ListItem do :body, :status, :color, - :bold, - :italic, - :highlighted, - :prefix, :arrowed_to_date ]) |> validate_required([:date, :position, :body]) diff --git a/lib/elixdo/lists/db.ex b/lib/elixdo/lists/db.ex index 6fdd104..9140666 100644 --- a/lib/elixdo/lists/db.ex +++ b/lib/elixdo/lists/db.ex @@ -93,11 +93,7 @@ defmodule Elixdo.Lists.DB do create_items(to_date, [ %{ body: item.body, - color: item.color, - bold: item.bold, - italic: item.italic, - highlighted: item.highlighted, - prefix: item.prefix + color: item.color } ]) end) diff --git a/lib/elixdo_web/controllers/api/item_json.ex b/lib/elixdo_web/controllers/api/item_json.ex index e25df92..085afe6 100644 --- a/lib/elixdo_web/controllers/api/item_json.ex +++ b/lib/elixdo_web/controllers/api/item_json.ex @@ -9,11 +9,7 @@ defmodule ElixdoWeb.Api.ItemJSON do body: item.body, status: to_string(item.status), position: item.position, - bold: item.bold, - italic: item.italic, - highlighted: item.highlighted, color: item.color && to_string(item.color), - prefix: item.prefix, arrowed_to_date: item.arrowed_to_date && Date.to_iso8601(item.arrowed_to_date), inserted_at: format_datetime(item.inserted_at), updated_at: format_datetime(item.updated_at) diff --git a/lib/elixdo_web/live/list_live.ex b/lib/elixdo_web/live/list_live.ex index 2ec2783..5770995 100644 --- a/lib/elixdo_web/live/list_live.ex +++ b/lib/elixdo_web/live/list_live.ex @@ -192,13 +192,9 @@ defmodule ElixdoWeb.ListLive do attrs = case field do - "bold" -> %{bold: setting == "true"} - "italic" -> %{italic: setting == "true"} - "highlighted" -> %{highlighted: setting == "true"} "color" when setting == "" -> %{color: nil} "color" when setting in @valid_color_strings -> %{color: String.to_existing_atom(setting)} "color" -> %{} - "prefix" -> %{prefix: if(setting == "", do: nil, else: setting)} _ -> %{} end @@ -215,7 +211,11 @@ defmodule ElixdoWeb.ListLive do Enum.filter(socket.assigns.items, &MapSet.member?(socket.assigns.selected, &1.id)) Enum.each(selected_items, fn item -> - Lists.update_item(item, %{bold: false, italic: false, highlighted: false, color: nil, status: :active, arrowed_to_date: nil}) + Lists.update_item(item, %{ + color: nil, + status: :active, + arrowed_to_date: nil + }) end) {:noreply, socket} @@ -321,13 +321,7 @@ defmodule ElixdoWeb.ListLive do defp item_class(_), do: "active" defp item_classes(item) do - [ - item_class(item), - if(item.bold, do: "bold", else: nil), - if(item.italic, do: "italic", else: nil), - if(item.highlighted, do: "highlighted", else: nil), - color_class(item.color) - ] + [item_class(item), color_class(item.color)] |> Enum.reject(&is_nil/1) |> Enum.join(" ") end diff --git a/lib/elixdo_web/live/list_live.html.heex b/lib/elixdo_web/live/list_live.html.heex index 14aeea4..acb4b06 100644 --- a/lib/elixdo_web/live/list_live.html.heex +++ b/lib/elixdo_web/live/list_live.html.heex @@ -44,22 +44,6 @@ - <%# ── Style group ───────────────────────────────────────── %> -
- - - -
- <%# ── Color swatches ────────────────────────────────────── %>
- <%# ── Prefix input ──────────────────────────────────────── %> -
- - -
- <%= if @date != @today do %> @@ -145,10 +123,6 @@ title={if MapSet.member?(@selected, item.id), do: "Deselect", else: "Select"} > - <%= if item.prefix do %> - {item.prefix} - <% end %> - <%= if @editing_id == item.id do %>
diff --git a/priv/repo/migrations/20260504213717_drop_prefix_from_list_items.exs b/priv/repo/migrations/20260504213717_drop_prefix_from_list_items.exs new file mode 100644 index 0000000..2cd43b7 --- /dev/null +++ b/priv/repo/migrations/20260504213717_drop_prefix_from_list_items.exs @@ -0,0 +1,15 @@ +defmodule Elixdo.Repo.Migrations.DropPrefixFromListItems do + use Ecto.Migration + + def up do + alter table(:list_items) do + remove :prefix + end + end + + def down do + alter table(:list_items) do + add :prefix, :string + end + end +end diff --git a/priv/repo/migrations/20260504214315_drop_bold_italic_highlighted_from_list_items.exs b/priv/repo/migrations/20260504214315_drop_bold_italic_highlighted_from_list_items.exs new file mode 100644 index 0000000..8566585 --- /dev/null +++ b/priv/repo/migrations/20260504214315_drop_bold_italic_highlighted_from_list_items.exs @@ -0,0 +1,19 @@ +defmodule Elixdo.Repo.Migrations.DropBoldItalicHighlightedFromListItems do + use Ecto.Migration + + def up do + alter table(:list_items) do + remove :bold + remove :italic + remove :highlighted + end + end + + def down do + alter table(:list_items) do + add :bold, :boolean, null: false, default: false + add :italic, :boolean, null: false, default: false + add :highlighted, :boolean, null: false, default: false + end + end +end diff --git a/test/elixdo/lists_test.exs b/test/elixdo/lists_test.exs index c42d52e..803d115 100644 --- a/test/elixdo/lists_test.exs +++ b/test/elixdo/lists_test.exs @@ -148,23 +148,26 @@ defmodule Elixdo.ListsTest do end test "allows same-status no-op active -> active (so format fields can be cleared)" do - item = insert_item(%{bold: true}) - assert {:ok, updated} = Lists.update_item(item, %{status: :active, bold: false}) + item = insert_item(%{color: :red}) + assert {:ok, updated} = Lists.update_item(item, %{status: :active, color: nil}) assert updated.status == :active - assert updated.bold == false + assert updated.color == nil end test "forbids invalid transition completed -> completed" do item = insert_item() {:ok, completed} = Lists.update_item(item, %{status: :completed}) - assert {:ok, updated} = Lists.update_item(completed, %{status: :completed, bold: false}) + assert {:ok, updated} = Lists.update_item(completed, %{status: :completed, color: nil}) assert updated.status == :completed end test "allows arrowed_out -> active (remove formats undoes arrow)" do item = insert_item() {:ok, arrowed, _copy} = Lists.arrow_item(item, ~D[2026-09-01]) - assert {:ok, restored} = Lists.update_item(arrowed, %{status: :active, arrowed_to_date: nil}) + + assert {:ok, restored} = + Lists.update_item(arrowed, %{status: :active, arrowed_to_date: nil}) + assert restored.status == :active assert restored.arrowed_to_date == nil end @@ -183,18 +186,19 @@ defmodule Elixdo.ListsTest do end test "allows clearing arrowed_to_date together with status: active (remove formats path)" do - item = insert_item(%{bold: true, italic: true}) + item = insert_item(%{color: :blue}) {:ok, arrowed, _copy} = Lists.arrow_item(item, ~D[2026-09-01]) + assert {:ok, restored} = Lists.update_item(arrowed, %{ status: :active, arrowed_to_date: nil, - bold: false, - italic: false + color: nil }) + assert restored.status == :active assert restored.arrowed_to_date == nil - assert restored.bold == false + assert restored.color == nil end test "forbids arrowed_out -> completed" do @@ -221,21 +225,10 @@ defmodule Elixdo.ListsTest do assert copy.status == :active end - test "preserves item formatting on copy" do - {:ok, [item]} = - Lists.create_items(@date, [ - %{ - body: "formatted", - bold: true, - italic: true, - highlighted: true - } - ]) - + test "preserves color on copy" do + {:ok, [item]} = Lists.create_items(@date, [%{body: "colored", color: :green}]) assert {:ok, _original, copy} = Lists.arrow_item(item, @date2) - assert copy.bold == true - assert copy.italic == true - assert copy.highlighted == true + assert copy.color == :green end test "forbids arrow on completed item" do diff --git a/test/elixdo_web/controllers/api/list_controller_test.exs b/test/elixdo_web/controllers/api/list_controller_test.exs index 17d84e5..2b74495 100644 --- a/test/elixdo_web/controllers/api/list_controller_test.exs +++ b/test/elixdo_web/controllers/api/list_controller_test.exs @@ -81,11 +81,7 @@ defmodule ElixdoWeb.Api.ListControllerTest do assert result["body"] == "test item" assert result["status"] == "active" assert result["position"] == 1 - assert result["bold"] == false - assert result["italic"] == false - assert result["highlighted"] == false assert result["color"] == nil - assert result["prefix"] == nil assert result["arrowed_to_date"] == nil assert String.ends_with?(result["inserted_at"], "Z") assert String.ends_with?(result["updated_at"], "Z") diff --git a/test/elixdo_web/live/list_live_phase5_test.exs b/test/elixdo_web/live/list_live_phase5_test.exs index 3e077bc..0be4458 100644 --- a/test/elixdo_web/live/list_live_phase5_test.exs +++ b/test/elixdo_web/live/list_live_phase5_test.exs @@ -96,22 +96,6 @@ defmodule ElixdoWeb.ListLivePhase5Test do refute html =~ ~s(class="select-btn selected") end - test "apply bold decoration to selected items", %{conn: conn} do - date = ~D[2026-05-21] - {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "make bold"}]) - item = List.first(items) - - {:ok, view, _} = live(conn, list_path("2026-05-21")) - view |> element("[phx-click='toggle_select'][phx-value-id='#{item.id}']") |> render_click() - - view - |> element("[phx-click='set_decoration'][phx-value-field='bold'][phx-value-setting='true']") - |> render_click() - - html = render(view) - assert html =~ "bold" - end - test "apply color to selected items", %{conn: conn} do date = ~D[2026-05-22] {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "color me"}]) diff --git a/test/elixdo_web/live/list_live_remove_formats_test.exs b/test/elixdo_web/live/list_live_remove_formats_test.exs index 1d119f3..7d85eba 100644 --- a/test/elixdo_web/live/list_live_remove_formats_test.exs +++ b/test/elixdo_web/live/list_live_remove_formats_test.exs @@ -24,50 +24,6 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do render(view) end - test "remove_formats clears bold on an active item", %{conn: conn} do - date = ~D[2026-07-01] - {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "bold item"}]) - item = List.first(items) - {:ok, _} = Elixdo.Lists.update_item(item, %{bold: true}) - refreshed = item |> Map.put(:bold, true) - - {:ok, view, _} = live(conn, list_path("2026-07-01")) - select_item(view, refreshed) - html = click_remove_formats(view) - - refute html =~ ~r/data-id="#{item.id}"[^>]*bold/ - db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) - assert db_item.bold == false - end - - test "remove_formats clears italic on an active item", %{conn: conn} do - date = ~D[2026-07-02] - {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "italic item"}]) - item = List.first(items) - {:ok, _} = Elixdo.Lists.update_item(item, %{italic: true}) - - {:ok, view, _} = live(conn, list_path("2026-07-02")) - select_item(view, item) - click_remove_formats(view) - - db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) - assert db_item.italic == false - end - - test "remove_formats clears highlighted on an active item", %{conn: conn} do - date = ~D[2026-07-03] - {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "highlighted item"}]) - item = List.first(items) - {:ok, _} = Elixdo.Lists.update_item(item, %{highlighted: true}) - - {:ok, view, _} = live(conn, list_path("2026-07-03")) - select_item(view, item) - click_remove_formats(view) - - db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) - assert db_item.highlighted == false - end - test "remove_formats clears color on an active item", %{conn: conn} do date = ~D[2026-07-04] {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "colored item"}]) @@ -90,7 +46,7 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "completed formatted"}]) item = List.first(items) {:ok, completed} = Elixdo.Lists.update_item(item, %{status: :completed}) - {:ok, _} = Elixdo.Lists.update_item(completed, %{bold: true, color: :blue}) + {:ok, _} = Elixdo.Lists.update_item(completed, %{color: :blue}) {:ok, view, _} = live(conn, list_path("2026-07-05")) select_item(view, item) @@ -99,7 +55,6 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do assert html =~ ~s(class="item active") db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) assert db_item.status == :active - assert db_item.bold == false assert db_item.color == nil end @@ -109,8 +64,7 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do date = ~D[2026-07-06] {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "wiggled formatted"}]) item = List.first(items) - {:ok, wiggled} = Elixdo.Lists.update_item(item, %{status: :wiggled_out}) - {:ok, _} = Elixdo.Lists.update_item(wiggled, %{italic: true}) + {:ok, _wiggled} = Elixdo.Lists.update_item(item, %{status: :wiggled_out}) {:ok, view, _} = live(conn, list_path("2026-07-06")) select_item(view, item) @@ -119,7 +73,6 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do assert html =~ ~s(class="item active") db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) assert db_item.status == :active - assert db_item.italic == false end test "remove_formats on an arrowed_out item restores it to active and clears arrow", %{ @@ -129,7 +82,7 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do target_date = ~D[2026-07-08] {:ok, items} = - Elixdo.Lists.create_items(date, [%{body: "arrowed formatted", bold: true, color: :green}]) + Elixdo.Lists.create_items(date, [%{body: "arrowed formatted", color: :green}]) item = List.first(items) {:ok, _original, _copy} = Elixdo.Lists.arrow_item(item, target_date) @@ -147,11 +100,12 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) assert db_item.status == :active assert db_item.arrowed_to_date == nil - assert db_item.bold == false assert db_item.color == nil # Copy on the target date must be untouched - copy = Elixdo.Lists.get_items_for_date(target_date) |> Enum.find(&(&1.body == "arrowed formatted")) + copy = + Elixdo.Lists.get_items_for_date(target_date) |> Enum.find(&(&1.body == "arrowed formatted")) + assert copy != nil assert copy.status == :active end @@ -161,22 +115,13 @@ defmodule ElixdoWeb.ListLiveRemoveFormatsTest do {:ok, items} = Elixdo.Lists.create_items(date, [%{body: "all formats"}]) item = List.first(items) - {:ok, _} = - Elixdo.Lists.update_item(item, %{ - bold: true, - italic: true, - highlighted: true, - color: :purple - }) + {:ok, _} = Elixdo.Lists.update_item(item, %{color: :purple}) {:ok, view, _} = live(conn, list_path("2026-07-09")) select_item(view, item) click_remove_formats(view) db_item = Elixdo.Lists.get_items_for_date(date) |> Enum.find(&(&1.id == item.id)) - assert db_item.bold == false - assert db_item.italic == false - assert db_item.highlighted == false assert db_item.color == nil assert db_item.status == :active end From f78a938ea1bded1cf74fecc1e74f9a169dd378da Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Mon, 4 May 2026 20:48:33 -0600 Subject: [PATCH 02/22] Phases 3 & 4: Priority handle decorations and sort-active-first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3: Add priority field (❶❷❸⭐🔥) to list items. Priority character replaces the drag handle and is always visible. Toolbar group lets users assign priority to selected items; remove-formats clears it. Arrow copies do not inherit priority. Phase 4: Sort-active button in toolbar reorders the current day so all active items come first, preserving within-group order, via the existing reorder_items/PubSub path. CSS fixes: priority characters always visible on desktop (specificity fix), correct sizing and tight spacing on both desktop and mobile. Co-Authored-By: Claude Sonnet 4.6 --- assets/css/app.css | 19 +- lib/elixdo/list_item.ex | 13 ++ lib/elixdo_web/controllers/api/item_json.ex | 1 + lib/elixdo_web/live/list_live.ex | 24 +++ lib/elixdo_web/live/list_live.html.heex | 20 ++- ...60504215723_add_priority_to_list_items.exs | 9 + test/elixdo/list_item_priority_test.exs | 39 +++++ .../api/item_json_priority_test.exs | 21 +++ .../live/list_live_priority_test.exs | 47 +++++ test/elixdo_web/live/list_live_sort_test.exs | 165 ++++++++++++++++++ 10 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 priv/repo/migrations/20260504215723_add_priority_to_list_items.exs create mode 100644 test/elixdo/list_item_priority_test.exs create mode 100644 test/elixdo_web/controllers/api/item_json_priority_test.exs create mode 100644 test/elixdo_web/live/list_live_priority_test.exs create mode 100644 test/elixdo_web/live/list_live_sort_test.exs diff --git a/assets/css/app.css b/assets/css/app.css index 8267fec..d8870a4 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -225,6 +225,7 @@ body { line-height: 1; } .toolbar-btn:hover { background: var(--bg-4); color: white; } +.priority-btn { color: var(--text); font-size: 22px; padding: 0 4px; min-width: unset; } /* Select-all button — ring matches item-select-btn */ .select-btn { @@ -264,8 +265,9 @@ body { pointer-events: none; } -/* Search button */ -.search-btn { +/* Search and sort buttons */ +.search-btn, +.sort-btn { display: flex; align-items: center; justify-content: center; @@ -278,7 +280,8 @@ body { cursor: pointer; transition: all 0.12s; } -.search-btn:hover { border-color: var(--accent); color: white; } +.search-btn:hover, +.sort-btn:hover { border-color: var(--accent); color: white; } /* Color swatches row — same 40px height */ .swatches-row { @@ -498,6 +501,14 @@ body { .drag-handle:active { cursor: grabbing; } .drag-ghost { opacity: 0.35; } +/* Priority character: always visible, full color, slightly larger than ⠿ */ +.drag-handle.drag-handle--priority { + opacity: 1; + color: var(--text); + font-size: 20px; + transition: none; +} + /* ─── Per-item select button ────────────────────────────────────────── */ .item-select-btn { @@ -775,9 +786,11 @@ body { .item { padding: 16px 14px 16px 16px; gap: 12px; } .item-body { font-size: 22px; line-height: 1.25; } .drag-handle { opacity: 0.4; font-size: 20px; padding: 0 0.6rem; min-width: 2rem; text-align: center; } + .drag-handle.drag-handle--priority { opacity: 1; font-size: 26px; } .item-select-btn { width: 32px; height: 32px; } .item-select-btn.selected::after { width: 8px; height: 8px; } .toolbar-btn { min-width: 40px; height: 38px; } + .priority-btn { font-size: 22px; padding: 0 4px; min-width: unset; } .add-item-input { font-size: 1.1rem; padding: 0.75rem; } .date-header { padding: 0.75rem; } .nav-arrow { font-size: 2rem; padding: 0.5rem 1.2rem; } diff --git a/lib/elixdo/list_item.ex b/lib/elixdo/list_item.ex index 6fd43ea..99edf3d 100644 --- a/lib/elixdo/list_item.ex +++ b/lib/elixdo/list_item.ex @@ -2,6 +2,8 @@ defmodule Elixdo.ListItem do use Ecto.Schema import Ecto.Changeset + @valid_priorities ["❶", "❷", "❸", "⭐", "🔥"] + schema "list_items" do field :date, :date field :position, :integer @@ -12,6 +14,7 @@ defmodule Elixdo.ListItem do default: :active field :color, Ecto.Enum, values: [:red, :blue, :green, :purple, :orange] + field :priority, :string field :arrowed_to_date, :date timestamps(type: :utc_datetime) @@ -25,13 +28,23 @@ defmodule Elixdo.ListItem do :body, :status, :color, + :priority, :arrowed_to_date ]) |> validate_required([:date, :position, :body]) |> validate_length(:body, min: 1) + |> validate_priority() |> validate_arrowed_to_date_consistency() end + defp validate_priority(changeset) do + case get_change(changeset, :priority) do + nil -> changeset + p when p in @valid_priorities -> changeset + _ -> add_error(changeset, :priority, "must be one of #{Enum.join(@valid_priorities, ", ")}") + end + end + # arrowed_to_date must be set iff status is arrowed_out defp validate_arrowed_to_date_consistency(changeset) do status = get_field(changeset, :status) diff --git a/lib/elixdo_web/controllers/api/item_json.ex b/lib/elixdo_web/controllers/api/item_json.ex index 085afe6..cae8d06 100644 --- a/lib/elixdo_web/controllers/api/item_json.ex +++ b/lib/elixdo_web/controllers/api/item_json.ex @@ -10,6 +10,7 @@ defmodule ElixdoWeb.Api.ItemJSON do status: to_string(item.status), position: item.position, color: item.color && to_string(item.color), + priority: item.priority, arrowed_to_date: item.arrowed_to_date && Date.to_iso8601(item.arrowed_to_date), inserted_at: format_datetime(item.inserted_at), updated_at: format_datetime(item.updated_at) diff --git a/lib/elixdo_web/live/list_live.ex b/lib/elixdo_web/live/list_live.ex index 5770995..a0e1e42 100644 --- a/lib/elixdo_web/live/list_live.ex +++ b/lib/elixdo_web/live/list_live.ex @@ -169,6 +169,20 @@ defmodule ElixdoWeb.ListLive do @valid_colors Ecto.Enum.values(Elixdo.ListItem, :color) @valid_color_strings Enum.map(@valid_colors, &Atom.to_string/1) + @valid_priorities ["❶", "❷", "❸", "⭐", "🔥"] + + def handle_event("set_priority", %{"priority" => p}, socket) + when p in @valid_priorities do + selected_items = + Enum.filter(socket.assigns.items, &MapSet.member?(socket.assigns.selected, &1.id)) + + Enum.each(selected_items, fn item -> + Lists.update_item(item, %{priority: p}) + end) + + {:noreply, socket} + end + def handle_event("set_status", %{"status" => status_str}, socket) when status_str in @valid_status_strings do status = String.to_existing_atom(status_str) @@ -213,6 +227,7 @@ defmodule ElixdoWeb.ListLive do Enum.each(selected_items, fn item -> Lists.update_item(item, %{ color: nil, + priority: nil, status: :active, arrowed_to_date: nil }) @@ -265,6 +280,15 @@ defmodule ElixdoWeb.ListLive do {:noreply, socket} end + def handle_event("sort_active", _, socket) do + {active, non_active} = + Enum.split_with(socket.assigns.items, &(&1.status == :active)) + + new_ids = Enum.map(active ++ non_active, & &1.id) + Lists.reorder_items(socket.assigns.date, new_ids) + {:noreply, socket} + end + # Search def handle_event("open_search", _, socket) do {:noreply, assign(socket, search_open: true, search_results: [])} diff --git a/lib/elixdo_web/live/list_live.html.heex b/lib/elixdo_web/live/list_live.html.heex index acb4b06..ef8dd88 100644 --- a/lib/elixdo_web/live/list_live.html.heex +++ b/lib/elixdo_web/live/list_live.html.heex @@ -14,6 +14,16 @@ + + <%# Sort active-first %> +
@@ -44,6 +54,14 @@
+ <%# ── Priority handle decorations ───────────────────────── %> +
+ <%= for p <- ["❶", "❷", "❸", "⭐", "🔥"] do %> + + <% end %> +
+ <%# ── Color swatches ────────────────────────────────────── %>
<%# ── Priority handle decorations ───────────────────────── %> -
+ +
<%= for p <- ["❶", "❷", "❸", "⭐", "🔥"] do %> <% end %>
- <%# ── Color swatches ────────────────────────────────────── %> + <%# ── Color swatches (desktop) + mobile trigger ─────────── %> +
@@ -185,6 +189,30 @@
<% end %> + <%# ── Priority bottom sheet (mobile) ──────────────────────────────── %> + <%= if @priority_sheet_open do %> +
+
+ <%= for p <- ["❶", "❷", "❸", "⭐", "🔥"] do %> + + <% end %> +
+
+ <% end %> + + <%# ── Color bottom sheet (mobile) ─────────────────────────────────── %> + <%= if @color_sheet_open do %> +
+
+ <%= for color <- [:red, :blue, :green, :purple, :orange] do %> + + <% end %> +
+
+ <% end %> + <%# ── Search modal ───────────────────────────────────────────────── %> <%= if @search_open do %> diff --git a/test/elixdo_web/live/list_live_phase7_test.exs b/test/elixdo_web/live/list_live_phase7_test.exs new file mode 100644 index 0000000..209e9be --- /dev/null +++ b/test/elixdo_web/live/list_live_phase7_test.exs @@ -0,0 +1,44 @@ +defmodule ElixdoWeb.ListLivePhase7Test do + use ElixdoWeb.ConnCase, async: false + import Phoenix.LiveViewTest + import Mox + + setup :verify_on_exit! + + setup do + Mox.stub(Elixdo.Clock.Mock, :today, fn -> ~D[2026-05-01] end) + :ok + end + + defp secret, do: System.get_env("SECRET_PATH", "dev-secret") + defp list_path(date), do: "/#{secret()}/list/#{date}" + + test "voice_input event creates a list item", %{conn: conn} do + date = ~D[2026-07-01] + {:ok, view, _} = live(conn, list_path("2026-07-01")) + + render_click(view, "voice_input", %{"text" => "buy milk"}) + + items = Elixdo.Lists.get_items_for_date(date) + assert Enum.any?(items, &(&1.body == "buy milk")) + end + + test "voice_input with blank text creates no item", %{conn: conn} do + date = ~D[2026-07-02] + {:ok, view, _} = live(conn, list_path("2026-07-02")) + + render_click(view, "voice_input", %{"text" => " "}) + + items = Elixdo.Lists.get_items_for_date(date) + assert items == [] + end + + test "voice_input item appears in the rendered list", %{conn: conn} do + {:ok, view, _} = live(conn, list_path("2026-07-03")) + + render_click(view, "voice_input", %{"text" => "call dentist"}) + + html = render(view) + assert html =~ "call dentist" + end +end From d3aceca79acd2b33d3ff10f53b4702e4e9b6b993 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 5 May 2026 10:05:01 -0600 Subject: [PATCH 13/22] cemoji tweaks --- lib/elixdo/emoji.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/elixdo/emoji.ex b/lib/elixdo/emoji.ex index 3d222e3..03f69ab 100644 --- a/lib/elixdo/emoji.ex +++ b/lib/elixdo/emoji.ex @@ -44,7 +44,7 @@ defmodule Elixdo.Emoji do "warning" => "⚠️", "tada" => "🎉", "rocket" => "🚀", - "bulb" => "💡", + "idea" => "💡", "memo" => "📝", "calendar" => "📅", "clock" => "🕐", @@ -56,7 +56,6 @@ defmodule Elixdo.Emoji do "dog" => "🐶", "cat" => "🐱", "pizza" => "🍕", - "coffee" => "☕", "lock" => "🔒", "key" => "🔑", "wrench" => "🔧", From 3ec9510a1f9bf95dc0fae6e2202fb6f002ea88b3 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 5 May 2026 12:20:20 -0600 Subject: [PATCH 14/22] Remove Select All button and supporting code Removes the toolbar select-all circle button, toggle_all event handler, .select-btn CSS block, and its single test. The per-item select circles and all toolbar actions are unaffected. Co-Authored-By: Claude Sonnet 4.6 --- assets/css/app.css | 37 ------------------- lib/elixdo_web/live/list_live.ex | 6 --- lib/elixdo_web/live/list_live.html.heex | 5 --- .../elixdo_web/live/list_live_phase5_test.exs | 12 ------ 4 files changed, 60 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 9b73d50..3e57d8d 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -227,43 +227,6 @@ body { .toolbar-btn:hover { background: var(--bg-4); color: white; } .priority-btn { color: var(--text); font-size: 22px; padding: 0 4px; min-width: unset; } -/* Select-all button — ring matches item-select-btn */ -.select-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: var(--bg-3); - border: 1px solid var(--border-light); - border-radius: var(--radius-btn); - cursor: pointer; - transition: all 0.12s; - position: relative; -} -.select-btn::before { - content: ""; - width: 22px; - height: 22px; - border-radius: 50%; - border: 2px solid oklch(72% 0.015 280); - transition: all 0.12s; - flex-shrink: 0; -} -.select-btn:hover::before { border-color: var(--accent); } -.select-btn.selected::before { - background: var(--accent); - border-color: var(--accent); -} -.select-btn.selected::after { - content: ""; - position: absolute; - width: 7px; - height: 7px; - border-radius: 50%; - background: white; - pointer-events: none; -} /* Search and sort buttons */ .search-btn, diff --git a/lib/elixdo_web/live/list_live.ex b/lib/elixdo_web/live/list_live.ex index b40cf9f..d12d8df 100644 --- a/lib/elixdo_web/live/list_live.ex +++ b/lib/elixdo_web/live/list_live.ex @@ -120,12 +120,6 @@ defmodule ElixdoWeb.ListLive do {:noreply, socket |> assign(:selected, selected) |> assign(:highlighted_item_id, nil)} end - def handle_event("toggle_all", _, socket) do - all_ids = socket.assigns.items |> Enum.map(& &1.id) |> MapSet.new() - selected = if socket.assigns.selected == all_ids, do: MapSet.new(), else: all_ids - {:noreply, assign(socket, :selected, selected)} - end - # Voice input def handle_event("voice_input", %{"text" => text}, socket) do text = text |> String.trim() |> Emoji.convert() diff --git a/lib/elixdo_web/live/list_live.html.heex b/lib/elixdo_web/live/list_live.html.heex index 21abb64..20449ca 100644 --- a/lib/elixdo_web/live/list_live.html.heex +++ b/lib/elixdo_web/live/list_live.html.heex @@ -2,11 +2,6 @@
- <% all_selected? = @items != [] and MapSet.size(@selected) == length(@items) %> - <%# Select-all — ring circle, no text content %> - - <%# Search — SVG magnifier %> + <% end %> + <%# Search — SVG magnifier %>
- <%= if @date != @today do %> - - <% end %>
<%# ── Date header ───────────────────────────────────────────── %> diff --git a/test/elixdo_web/controllers/api/mcp_controller_test.exs b/test/elixdo_web/controllers/api/mcp_controller_test.exs index 86a2f88..eaee88c 100644 --- a/test/elixdo_web/controllers/api/mcp_controller_test.exs +++ b/test/elixdo_web/controllers/api/mcp_controller_test.exs @@ -180,12 +180,12 @@ defmodule ElixdoWeb.Api.McpControllerTest do test "add_item converts shortcodes in body", %{conn: conn} do mcp(conn, "tools/call", %{ "name" => "add_item", - "arguments" => %{"date" => "2026-11-13", "body" => "buy :coffee: and :pizza:"} + "arguments" => %{"date" => "2026-11-13", "body" => "buy :star: and :rocket:"} }) |> json_response(200) [item] = Lists.get_items_for_date(~D[2026-11-13]) - assert item.body == "buy ☕ and 🍕" + assert item.body == "buy ⭐ and 🚀" end test "update_item converts shortcodes in body", %{conn: conn} do From cf6ada5b64d856f3a14227c1a44d149f49bdd84d Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 5 May 2026 15:34:34 -0600 Subject: [PATCH 16/22] tweaks --- assets/css/app.css | 8 ++++---- lib/elixdo_web/live/list_live.html.heex | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index cac3ac7..2580896 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -644,12 +644,12 @@ body { .add-item-input { flex: 1; padding: 10px 14px; - background: var(--bg-2); + background: #000; border: 1px solid var(--border-color); border-radius: var(--radius-card); color: var(--text); font-family: inherit; - font-size: 15px; + font-size: 20px; resize: none; min-height: 44px; transition: border-color 0.12s, box-shadow 0.12s; @@ -669,7 +669,7 @@ body { border: none; border-radius: var(--radius-card); font-family: inherit; - font-size: 24px; + font-size: 20px; font-weight: 600; cursor: pointer; letter-spacing: 0.02em; @@ -835,7 +835,7 @@ body { .item-select-btn.selected::after { width: 8px; height: 8px; } .toolbar-btn { min-width: 40px; height: 38px; } .priority-btn { font-size: 22px; padding: 0 4px; min-width: unset; } - .add-item-input { font-size: 1.1rem; padding: 0.75rem; } + .add-item-input { font-size: 20px; padding: 0.75rem; } .date-header { padding: 0.75rem; } .nav-arrow { font-size: 2rem; padding: 0.5rem 1.2rem; } .search-modal { min-width: unset; width: 90vw; } diff --git a/lib/elixdo_web/live/list_live.html.heex b/lib/elixdo_web/live/list_live.html.heex index 99edb40..4ec7876 100644 --- a/lib/elixdo_web/live/list_live.html.heex +++ b/lib/elixdo_web/live/list_live.html.heex @@ -162,7 +162,7 @@