diff --git a/CHANGELOG.md b/CHANGELOG.md index b50586ad..47021a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- `ui_stat_card` takes `note:` to show a short note under its value, `plain: true` to draw a plain figure with no card border or background, and `fallback:` to show muted text such as "N/A" when its value is missing, and a stat card given none of them looks as it did. + ## [0.48.0] - 2026-10-15 ### Changed diff --git a/README.md b/README.md index 9ed34cd8..3958ea9d 100644 --- a/README.md +++ b/README.md @@ -939,6 +939,9 @@ Renders a metric card for dashboards. - `calculation:` (String) — how the metric is computed - `change:` (Number) — percent change against the previous period, shown as ▲/▼ - `href:` (String) — turns the value into a link to that URL; the card itself stays unlinked +- `note:` (String) — a short note shown under the value +- `plain:` (Boolean, default `false`) — draws a plain figure with no card border or background, for a row of figures +- `fallback:` (String) — shown in a muted colour when the value is missing, such as `"N/A"` When `definition:` or `calculation:` is given, an info button appears. Hovering or focusing it shows the details in a panel floating below the card, and tapping it toggles the panel on touch screens. The card never changes size. diff --git a/app/components/keystone/ui/stat_card_component.html.erb b/app/components/keystone/ui/stat_card_component.html.erb index 5ca97e6d..37a9cc19 100644 --- a/app/components/keystone/ui/stat_card_component.html.erb +++ b/app/components/keystone/ui/stat_card_component.html.erb @@ -12,7 +12,8 @@ <% end %>
- <% if link? %><%= value %><% else %><%= value %><% end %><% if suffix? %> <%= suffix %><% end %> + <% if missing? %><%= fallback %><% elsif link? %><%= value %><% else %><%= value %><% end %><% if suffix? %> <%= suffix %><% end %>
+ <% if note %><%= note %>
<% end %><%= change_label %>
diff --git a/app/components/keystone/ui/stat_card_component.rb b/app/components/keystone/ui/stat_card_component.rb index c7547390..0a010b21 100644 --- a/app/components/keystone/ui/stat_card_component.rb +++ b/app/components/keystone/ui/stat_card_component.rb @@ -6,6 +6,7 @@ class StatCardComponent < ViewComponent::Base EMPHASIS_CLASSES = "ks-stat-card-emphasis" HEADER_CLASSES = "ks-stat-card-header flex items-start justify-between" CARD_CLASSES = "ks-metric-card relative" + PLAIN_CLASSES = "ks-metric-plain relative" LABEL_CLASSES = "ks-stat-card-label text-sm" VALUE_BASE_CLASSES = "ks-stat-card-value text-3xl" SUFFIX_CLASSES = "ks-stat-card-suffix text-lg" @@ -13,6 +14,8 @@ class StatCardComponent < ViewComponent::Base CHANGE_ROW_CLASSES = "ks-stat-card-change h-5 text-sm" VALUE_LINK_CLASSES = "hover:underline focus:outline-none focus-visible:underline" INFO_BUTTON_CLASSES = "ks-stat-card-info peer shrink-0 transition" + NOTE_CLASSES = "ks-stat-card-note text-sm" + FALLBACK_CLASSES = "ks-stat-card-fallback" INFO_ICON = <<~SVG.freeze @@ -26,10 +29,13 @@ class StatCardComponent < ViewComponent::Base info: "ks-tone-info" }.freeze - attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href + attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href, :note - def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil) + def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil, note: nil, plain: false, fallback: nil) @label = label + @fallback = fallback + @note = note + @plain = plain @value = value @variant = variant @suffix = suffix @@ -39,8 +45,16 @@ def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, @href = href end + def missing? + @value.to_s.empty? && !@fallback.nil? + end + + def fallback + @fallback + end + def classes - CARD_CLASSES + @plain ? PLAIN_CLASSES : CARD_CLASSES end def label_classes diff --git a/keystone_ui.gemspec b/keystone_ui.gemspec index 67f74ab9..668c22e2 100644 --- a/keystone_ui.gemspec +++ b/keystone_ui.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "the_local/**/*"] + [ "MIT-LICENSE" ] spec.require_paths = [ "lib" ] - spec.add_dependency "keystone_ui-styles", ">= 0.22.0" + spec.add_dependency "keystone_ui-styles", ">= 0.25.0" spec.add_dependency "view_component", ">= 2.0", "< 5" spec.post_install_message = <<~MSG diff --git a/test/keystone_ui/packaging_test.rb b/test/keystone_ui/packaging_test.rb index 5b0fd5bb..138653ca 100644 --- a/test/keystone_ui/packaging_test.rb +++ b/test/keystone_ui/packaging_test.rb @@ -88,6 +88,12 @@ def test_requires_a_keystone_ui_styles_with_the_bar_chart_classes refute requirement.satisfied_by?(Gem::Version.new("0.21.0")) end + def test_requires_a_keystone_ui_styles_with_the_stat_note_and_plain_classes + requirement = gemspec.dependencies.find { |d| d.name == "keystone_ui-styles" }.requirement + + refute requirement.satisfied_by?(Gem::Version.new("0.24.0")) + end + private def gemspec diff --git a/test/render/stat_card_report_render_test.rb b/test/render/stat_card_report_render_test.rb new file mode 100644 index 00000000..fcb931f6 --- /dev/null +++ b/test/render/stat_card_report_render_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "render_helper" + +class Keystone::Ui::StatCardReportRenderTest < ViewComponent::TestCase + def test_shows_a_note_under_its_value + page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Commits", value: "553", note: "excluding merge commits")) + + assert_equal "excluding merge commits", page.css(".ks-stat-card-value + .ks-stat-card-note").text.strip + end + + def test_draws_a_plain_figure_without_the_card_border_when_asked + page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Commits", value: "553", plain: true)) + + assert_equal [ true, false ], [ page.css(".ks-metric-plain").any?, page.css(".ks-metric-card").any? ] + end + + def test_shows_its_fallback_muted_when_its_value_is_missing + page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Reviews", value: "", fallback: "N/A")) + + assert_equal "N/A", page.css(".ks-stat-card-value .ks-stat-card-fallback").text.strip + end +end diff --git a/the_local/agents/keystone_ui-develop.md b/the_local/agents/keystone_ui-develop.md index e5fe55ef..cab2aad5 100644 --- a/the_local/agents/keystone_ui-develop.md +++ b/the_local/agents/keystone_ui-develop.md @@ -420,7 +420,7 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as 10. Explain figures. - - `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil)` + - `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil, note: nil, plain: false, fallback: nil)` — `variant:` is `:neutral` `:success` `:danger` `:warning` `:info` and colours the value. `change:` is a signed number shown as `▲ 4.2%` in green, `▼` in red, or plain at zero. `href:` links the value only, so