refactor(contacts): two-column edit dialog — wider, shorter, roomier grid

The edit dialog was a tall, narrow single column that wasted ~half the
horizontal space and cramped the icon grid into ~2.5 clipped rows crowding
the footer. Rework it into the portfolio-editor two-column shape:

- Card widened 560 → 880 logical.
- Full-width live preview stays on top (now shows more of the address).
- Body is two fixed-height columns: form (label / address+paste / notes /
  global) on the left, avatar picker (segmented + icon grid / image / badge
  hint) on the right, each filling its column so the grid gets ~6 columns and
  ~4 rows instead of clipping mid-row.
- Overall modal is much shorter, so Save/Cancel no longer crowd the grid.
- Badge-mode hint re-centered relative to the current cursor (it now sits
  below the segmented control inside the shared column, not a fresh child).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 07:23:19 -05:00
parent 3d2b734541
commit 19dea53ef8

View File

@@ -228,14 +228,13 @@ void RenderContactsTab(App* app)
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
const float dp = Layout::dpiScale();
const bool light = material::IsLightTheme();
float formW = -1.0f; // stretch inputs to the card content width
float notesH = (notesInput.height > 0 ? notesInput.height : 56.0f) * dp;
ImFont* btnFont = S.resolveFont(actionBtn.font);
material::OverlayDialogSpec ov;
ov.title = title; ov.p_open = open;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 560.0f; ov.idSuffix = id; // wider than a plain form — holds the icon grid
ov.cardWidth = 880.0f; ov.idSuffix = id; // two-column body: form (left) + avatar picker (right)
ov.cardBottomViewportRatio = 0.92f;
if (material::BeginOverlayDialog(ov)) {
// End-truncate a string to fit maxW (whole UTF-8 code points), appending an ellipsis.
@@ -289,15 +288,24 @@ void RenderContactsTab(App* app)
hasAddr ? material::OnSurfaceMedium() : material::OnSurfaceDisabled(), ad.c_str());
ImGui::Dummy(ImVec2(pw, ph));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// ================= Two-column body: form (left) | avatar picker (right) =================
const float bodyH = 300.0f * dp;
const float colGap = Layout::spacingLg();
const float bodyAvail = ImGui::GetContentRegionAvail().x;
const float colW = (bodyAvail - colGap) * 0.5f;
// ---- LEFT: label / address / notes / global ----------------------------------------
ImGui::BeginChild("##contactFormCol", ImVec2(colW, bodyH), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Focus the first field the frame the dialog opens so it's keyboard-ready.
if (s_focus_edit_field) {
ImGui::SetKeyboardFocusHere();
s_focus_edit_field = false;
}
material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel",
s_edit_label, sizeof(s_edit_label), formW);
s_edit_label, sizeof(s_edit_label), -1.0f);
ImGui::Spacing();
@@ -320,14 +328,18 @@ void RenderContactsTab(App* app)
ImGui::Spacing();
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
s_edit_notes, sizeof(s_edit_notes), ImVec2(-1.0f, notesH));
ImGui::Spacing();
ImGui::Checkbox(TR("contact_global"), &s_edit_global);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_tt"));
ImGui::EndChild(); // ##contactFormCol
// ---- Avatar picker: Badge / Icon / Image -------------------------------------------
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SameLine(0, colGap);
// ---- RIGHT: avatar picker (Badge / Icon / Image), fills the column height -----------
ImGui::BeginChild("##contactAvatarCol", ImVec2(colW, bodyH), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("contact_avatar"));
{
const char* segs[3] = { TR("contact_avatar_badge"), TR("contact_avatar_icon"),
@@ -349,18 +361,18 @@ void RenderContactsTab(App* app)
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Fixed-height area so the modal height doesn't jump when switching avatar modes.
const float avatarAreaH = 176.0f * dp;
ImGui::BeginChild("##avArea", ImVec2(ImGui::GetContentRegionAvail().x, avatarAreaH), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// The mode content fills the rest of the avatar column (fixed height → no jump on switch).
if (s_edit_avatar_mode == 0) {
// Badge: centered explanatory hint.
// Badge: explanatory hint, centered in the remaining space below the segmented control.
const char* msg = TR("contact_avatar_badge_hint");
ImFont* f = material::Type().caption();
ImVec2 a = ImGui::GetContentRegionAvail();
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, a.x - 16.0f * dp, 0, msg);
ImGui::SetCursorPos(ImVec2(std::max(0.0f, (a.x - ts.x) * 0.5f), a.y * 0.4f));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ts.x, a.x));
ImVec2 a = ImGui::GetContentRegionAvail(); // remaining w,h from the current cursor
float wrapW = a.x - 16.0f * dp;
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, wrapW, 0, msg);
float x0 = ImGui::GetCursorPosX(), y0 = ImGui::GetCursorPosY();
ImGui::SetCursorPosX(x0 + std::max(0.0f, (a.x - ts.x) * 0.5f));
ImGui::SetCursorPosY(y0 + std::max(0.0f, (a.y - ts.y) * 0.4f));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ts.x, wrapW));
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), msg);
ImGui::PopTextWrapPos();
} else if (s_edit_avatar_mode == 1) {
@@ -435,8 +447,9 @@ void RenderContactsTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), TR("contact_avatar_image_hint"));
}
ImGui::EndChild(); // ##avArea
ImGui::EndChild(); // ##contactAvatarCol
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
float actionW = std::max(110.0f * dp,
btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("save")).x