Merge origin/main into feat/goals-v2-architecture

Pulls in #1857 (DS::Disclosure :card_inset), #1858 (:inline variant),
#1902 (DS::Pill marker:false + semantic tones + :red palette), #1903
(settings/debugs token fix), plus #1878 (entry.date guard) and other
minor fixes that landed.

Resolves one conflict in app/components/DS/pill.rb: takes main's new
extended API (marker: flag, SEMANTIC_TONE_ALIASES, :red tone, updated
docstring) and preserves the goals-branch color-mix(...30% black)
text treatment that was added for light-mode contrast. Applies the
same color-mix to the new :red tone for consistency.
This commit is contained in:
Guillem Arias
2026-05-22 08:53:10 +02:00
18 changed files with 280 additions and 93 deletions

View File

@@ -0,0 +1,59 @@
require "test_helper"
class DS::PillTest < ViewComponent::TestCase
test "marker mode (default) renders uppercase sub-12px chrome" do
render_inline(DS::Pill.new(label: "Beta", tone: :violet))
pill = page.find("span", text: "Beta")
assert_includes pill[:class], "uppercase"
# Marker keeps sub-12px text via arbitrary value (intentional — see component docs).
assert_match(/text-\[1[01]px\]/, pill[:class])
end
test "marker: false renders normal-case DS-scale chrome" do
render_inline(DS::Pill.new(label: "Active", tone: :success, marker: false))
pill = page.find("span", text: "Active")
refute_includes pill[:class], "uppercase"
# Badge mode snaps to text-xs / text-sm — no sub-12px arbitrary values.
assert_match(/text-(xs|sm)/, pill[:class])
refute_match(/text-\[1[01]px\]/, pill[:class])
end
test "semantic tone aliases resolve to visual palette tones" do
{
success: :green,
warning: :amber,
error: :red,
destructive: :red,
info: :indigo,
neutral: :gray
}.each do |alias_name, expected_visual|
pill = DS::Pill.new(label: "x", tone: alias_name)
assert_equal expected_visual, pill.tone, "Expected #{alias_name}#{expected_visual}, got #{pill.tone}"
end
end
test "unknown tone falls back to violet" do
pill = DS::Pill.new(label: "x", tone: :nonexistent)
assert_equal :violet, pill.tone
end
test "red tone palette resolves to red-* tokens" do
pill = DS::Pill.new(label: "Failed", tone: :error)
assert_includes pill.palette[:dot], "color-red-500"
assert_includes pill.palette[:bg], "color-red-50"
end
test "icon option renders glyph in place of dot" do
render_inline(DS::Pill.new(label: "Syncing", tone: :info, marker: false, icon: "loader"))
# Lucide icon helper renders the inline SVG; verifying we see at least one <svg>
# is enough — the icon helper is covered by its own tests.
assert_selector "svg"
# And the dot is suppressed when an icon takes its place. `refute_selector
# ..., count: N` only fails when there are exactly N matches, so use
# `assert_no_selector` to strictly assert zero dots.
assert_no_selector "span.rounded-full[style*='background-color']"
end
end

View File

@@ -1,26 +1,68 @@
class PillComponentPreview < ViewComponent::Preview
# @param tone select ["violet", "indigo", "fuchsia", "amber", "gray"]
# @param tone select ["violet", "indigo", "fuchsia", "amber", "green", "gray", "red", "success", "warning", "error", "info", "neutral"]
# @param style select ["soft", "filled", "outline"]
# @param size select ["sm", "md"]
# @param label text
# @param show_dot toggle
# @param dot_only toggle
def default(tone: "violet", style: "soft", size: "sm", label: "Preview", show_dot: true, dot_only: false)
# @param marker toggle
# @param icon text
def default(tone: "violet", style: "soft", size: "sm", label: "Preview", show_dot: true, dot_only: false, marker: true, icon: nil)
render DS::Pill.new(
label: label,
tone: tone.to_sym,
style: style.to_sym,
size: size.to_sym,
show_dot: show_dot,
dot_only: dot_only
dot_only: dot_only,
marker: marker,
icon: icon.presence
)
end
# @!group Stage markers (marker: true — original #1829 shape)
def canary
render DS::Pill.new(label: "Canary", tone: :fuchsia)
end
def beta
render DS::Pill.new(label: "Beta", tone: :violet)
end
def new_marker
render DS::Pill.new(label: "New", tone: :indigo)
end
def dot_only_collapsed_sidebar
render DS::Pill.new(dot_only: true, tone: :violet)
end
# @!endgroup
# @!group Status badges (marker: false, semantic tones)
def status_active
render DS::Pill.new(label: "Active", tone: :success, marker: false)
end
def status_pending
render DS::Pill.new(label: "Pending", tone: :warning, marker: false)
end
def status_failed
render DS::Pill.new(label: "Failed", tone: :error, marker: false, icon: "circle-alert")
end
def status_archived
render DS::Pill.new(label: "Archived", tone: :neutral, marker: false)
end
def status_info
render DS::Pill.new(label: "Syncing", tone: :info, marker: false, icon: "loader")
end
# @!endgroup
# @!group Sizes (md)
def status_md
render DS::Pill.new(label: "Past due", tone: :error, marker: false, size: :md)
end
# @!endgroup
end