fix(goals): theme-aware avatar text contrast; compact picker popup

Avatar letter/icon now uses `--avatar-color` CSS variable + the new
`.goal-avatar` class. Light mode darkens the text to 55% color + 45%
black so pale palette entries (cyan-300, green-300) stay readable on
the 10%-mix tint over white (~4.5:1). Dark mode reverts to the full
brand color via [data-theme="dark"] .goal-avatar override so the text
doesn't disappear against the near-black tinted surface. Verified
live: #805dee renders as a darker oklab in light mode and full
rgb(128,93,238) in dark mode.

Picker popup compacted:
- 80 (320px) wide, max-h-[60vh] overflow-y-auto so it never spills
  off-screen.
- Anchored below the avatar + horizontally centered to it (top-full
  left-1/2 -translate-x-1/2) so it doesn't drift off to the right
  edge of the form on narrow modals.
- Icon grid max-h-40 (160px, ~5 rows) with the in-house `scrollbar`
  utility for a thin gray thumb that works in both themes.
- Section headers (Color / Icon) styled `uppercase tracking-wide`
  for visual hierarchy.

Verified popup at 320x310px in edit modal, no vertical overflow.
This commit is contained in:
Guillem Arias
2026-05-11 21:36:24 +02:00
parent 41ffe10a7d
commit 7e50feeca4
3 changed files with 26 additions and 11 deletions

View File

@@ -188,6 +188,20 @@
scrollbar-width:none
}
/* Tinted-bg + colored-content avatar used by Goals::AvatarComponent and
the goals color/icon picker. Theme-aware text color: light mode darkens
the letter/icon so pale palette entries (cyan-300, green-300, etc.) keep
~4.5:1 contrast against the 10%-mix tint over white. Dark mode reverts
to the full color so the letter doesn't disappear against the near-black
surface. */
.goal-avatar {
background-color: color-mix(in oklab, var(--avatar-color) 10%, transparent);
color: color-mix(in oklab, var(--avatar-color) 55%, black);
}
[data-theme="dark"] .goal-avatar {
color: var(--avatar-color);
}
.invite_code [data-clipboard-target="iconDefault"],
.invite_code [data-clipboard-target="iconSuccess"] {
transition: opacity 0.2s;

View File

@@ -1,5 +1,5 @@
<span class="inline-flex items-center justify-center font-semibold <%= box_classes %> <%= text_classes %> <%= radius_classes %>"
style="background-color: color-mix(in oklab, <%= color %> 10%, transparent); color: <%= color %>;"
<span class="goal-avatar inline-flex items-center justify-center font-semibold <%= box_classes %> <%= text_classes %> <%= radius_classes %>"
style="--avatar-color: <%= color %>;"
aria-hidden="true"
data-testid="goal-avatar">
<% if icon.present? %>

View File

@@ -1,8 +1,8 @@
<%# locals: (form:, colors:, icons:) %>
<div data-controller="color-icon-picker" data-color-icon-picker-preset-colors-value="<%= colors %>">
<div class="w-fit relative">
<span class="inline-flex items-center justify-center w-11 h-11 rounded-xl font-semibold text-base"
style="background-color: color-mix(in oklab, <%= form.object.color %> 10%, transparent); color: <%= form.object.color %>;"
<span class="goal-avatar inline-flex items-center justify-center w-11 h-11 rounded-xl font-semibold text-base"
style="--avatar-color: <%= form.object.color %>;"
data-color-icon-picker-target="avatar">
<% if form.object.icon.present? %>
<%= icon(form.object.icon, color: "current", size: "md") %>
@@ -16,11 +16,12 @@
<%= icon("pen", size: "xs") %>
</summary>
<div class="absolute left-0 sm:left-auto sm:right-0 mt-2 z-50 bg-container p-4 border border-alpha-black-25 rounded-2xl shadow-xs h-fit w-87 max-w-[calc(100vw-2rem)]" data-color-icon-picker-target="popup">
<div class="flex gap-2 flex-col mb-4" data-color-icon-picker-target="selection">
<div class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 bg-container p-3 border border-alpha-black-25 rounded-2xl shadow-xs w-80 max-w-[calc(100vw-2rem)] max-h-[60vh] overflow-y-auto"
data-color-icon-picker-target="popup">
<div class="flex gap-2 flex-col mb-3" data-color-icon-picker-target="selection">
<div data-color-icon-picker-target="pickerSection"></div>
<h4 class="text-secondary text-sm">Color</h4>
<div class="flex flex-wrap md:flex-nowrap gap-2 items-center" data-color-icon-picker-target="colorsSection">
<h4 class="text-secondary text-xs uppercase tracking-wide">Color</h4>
<div class="flex flex-wrap gap-2 items-center" data-color-icon-picker-target="colorsSection">
<% colors.each do |c| %>
<label class="relative">
<%= form.radio_button :color, c, class: "sr-only peer", data: { action: "change->color-icon-picker#handleColorChange" } %>
@@ -45,9 +46,9 @@
</div>
</div>
<div class="flex flex-wrap gap-2 justify-center flex-col">
<h4 class="text-secondary text-sm">Icon</h4>
<div class="flex flex-wrap gap-0.5 max-h-52 overflow-auto">
<div class="flex flex-col gap-2">
<h4 class="text-secondary text-xs uppercase tracking-wide">Icon</h4>
<div class="flex flex-wrap gap-0.5 max-h-40 overflow-y-auto scrollbar">
<% icons.each do |icon_name| %>
<label class="relative">
<%= form.radio_button :icon, icon_name, class: "sr-only peer", data: { action: "change->color-icon-picker#handleIconChange change->color-icon-picker#handleIconColorChange", color_icon_picker_target: "icon" } %>