refactor(design-system): extend DS::Alert and migrate 9 inline alert blocks (#1731)

* feat(design-system): add info semantic color token

Mirrors success/warning/destructive: --color-info maps to blue-600 in
light mode, blue-500 in dark mode. Unblocks the DS::Alert info variant
from carrying a raw 'blue-600' literal in icon_color and lets surface
tokens use bg-info/N alpha modifiers like the rest of the system.

Refs #1715

* refactor(design-system): adopt semantic tokens and add body slot in DS::Alert

Replaces the bg-{blue,green,yellow,red}-50 / text-{...}-700 / border-{...}-200
palette block in DS::Alert with semantic alpha-modifier surfaces
(bg-{info,success,warning,destructive}/10 + matching /20 borders).
Drops the 'blue-600' literal that icon_color was returning for the
info variant; helpers#icon now accepts color: :info backed by the
new --color-info token.

Adds an optional title: kwarg and an opt-in block-content slot so
rich alerts (title + paragraph, lists, embedded actions) can render
without callers reaching for a hand-rolled flex layout. The existing
message: API stays backward-compatible — nothing in the codebase that
already calls DS::Alert.new(message: ..., variant: ...) needs to change.

Lookbook gains with_title and with_body_slot examples covering the
new shapes.

Refs #1715

* refactor(views): migrate api_keys, hostings, lunchflow alerts to DS::Alert

Cleans up nine bespoke alert blocks that hand-rolled the same
flex + icon + bordered-surface shape DS::Alert already provides:

- settings/api_keys/{new,created,created.turbo_stream}.html.erb — three
  near-identical 'Security Warning' / 'Important Security Note' boxes
  using the broken bg-warning-50 / text-warning-700 raw-palette pair.
- settings/hostings/{_alpha_vantage,_eodhd,_yahoo_finance,_twelve_data,_provider_selection}_settings.html.erb —
  five amber-50 / amber-200 warning boxes covering rate-limit notes,
  health-check failure messaging, and the env-configured override
  banner. The twelve_data plan-restriction block keeps its bullet
  list and pricing link inside the new DS::Alert body slot.
- lunchflow_items/{_api_error,_setup_required}.html.erb — two modal
  alert headers whose flex+icon scaffolding now collapses onto
  DS::Alert. The surrounding bg-surface 'Common issues' / 'Setup
  steps' info cards stay as-is; this PR only touches the alert
  shape itself.

No functional or behavioural changes. Locale keys preserved.
amber-* palette uses on the alerts disappear; remaining bg-amber-*
hits in the codebase live outside the alert pattern and stay for
follow-up sub-PRs of #1715.

Refs #1715
This commit is contained in:
Guillem Arias Fauste
2026-05-10 17:14:06 +02:00
committed by GitHub
parent 712d6baca9
commit 57d71cd55e
16 changed files with 112 additions and 120 deletions

View File

@@ -12,6 +12,7 @@
--color-success: var(--color-green-600);
--color-warning: var(--color-yellow-600);
--color-destructive: var(--color-red-600);
--color-info: var(--color-blue-600);
--color-shadow: --alpha(var(--color-black) / 6%);
--color-gray-25: #FAFAFA;
--color-gray-50: #F7F7F7;
@@ -199,6 +200,7 @@
--color-success: var(--color-green-500);
--color-warning: var(--color-yellow-400);
--color-destructive: var(--color-red-400);
--color-info: var(--color-blue-500);
--color-shadow: --alpha(var(--color-white) / 8%);
--budget-unused-fill: var(--color-gray-500);
--budget-unallocated-fill: var(--color-gray-700);

View File

@@ -1,7 +1,15 @@
<div class="<%= container_classes %>">
<%= helpers.icon icon_name, size: "sm", color: icon_color, class: "shrink-0" %>
<div class="flex-1 text-sm">
<%= message %>
<div class="flex-1 text-sm text-primary space-y-1">
<% if title.present? %>
<p class="font-medium text-primary"><%= title %></p>
<% end %>
<% if content.present? %>
<%= content %>
<% elsif message.present? %>
<%= message %>
<% end %>
</div>
</div>

View File

@@ -1,24 +1,27 @@
class DS::Alert < DesignSystemComponent
def initialize(message:, variant: :info)
VARIANTS = %i[info success warning error destructive].freeze
def initialize(message: nil, title: nil, variant: :info)
@message = message
@title = title
@variant = variant
end
private
attr_reader :message, :variant
attr_reader :message, :title, :variant
def container_classes
base_classes = "flex items-start gap-3 p-4 rounded-lg border"
variant_classes = case variant
when :info
"bg-blue-50 text-blue-700 border-blue-200 theme-dark:bg-blue-900/20 theme-dark:text-blue-400 theme-dark:border-blue-800"
"bg-info/10 border-info/20"
when :success
"bg-green-50 text-green-700 border-green-200 theme-dark:bg-green-900/20 theme-dark:text-green-400 theme-dark:border-green-800"
"bg-success/10 border-success/20"
when :warning
"bg-yellow-50 text-yellow-700 border-yellow-200 theme-dark:bg-yellow-900/20 theme-dark:text-yellow-400 theme-dark:border-yellow-800"
"bg-warning/10 border-warning/20"
when :error, :destructive
"bg-red-50 text-red-700 border-red-200 theme-dark:bg-red-900/20 theme-dark:text-red-400 theme-dark:border-red-800"
"bg-destructive/10 border-destructive/20"
end
"#{base_classes} #{variant_classes}"
@@ -46,7 +49,7 @@ class DS::Alert < DesignSystemComponent
when :error, :destructive
"destructive"
else
"blue-600"
"info"
end
end
end

View File

@@ -17,7 +17,7 @@ module ApplicationHelper
def icon(key, size: "md", color: "default", custom: false, as_button: false, **opts)
extra_classes = opts.delete(:class)
sizes = { xs: "w-3 h-3", sm: "w-4 h-4", md: "w-5 h-5", lg: "w-6 h-6", xl: "w-7 h-7", "2xl": "w-8 h-8" }
colors = { default: "text-secondary", white: "text-inverse", success: "text-success", warning: "text-warning", destructive: "text-destructive", current: "text-current" }
colors = { default: "text-secondary", white: "text-inverse", success: "text-success", warning: "text-warning", destructive: "text-destructive", info: "text-info", current: "text-current" }
icon_classes = class_names(
"shrink-0",

View File

@@ -4,13 +4,11 @@
<% dialog.with_header(title: "Lunch Flow Connection Error") %>
<% dialog.with_body do %>
<div class="space-y-4">
<div class="flex items-start gap-3">
<%= icon("alert-circle", class: "text-destructive w-5 h-5 shrink-0 mt-0.5") %>
<div class="text-sm">
<p class="font-medium text-primary mb-2">Unable to connect to Lunch Flow</p>
<p class="text-secondary"><%= error_message %></p>
</div>
</div>
<%= render DS::Alert.new(
title: "Unable to connect to Lunch Flow",
message: error_message,
variant: :error
) %>
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
<p class="font-medium text-primary">Common Issues:</p>

View File

@@ -3,13 +3,11 @@
<% dialog.with_header(title: "Lunch Flow Setup Required") %>
<% dialog.with_body do %>
<div class="space-y-4">
<div class="flex items-start gap-3">
<%= icon("alert-circle", class: "text-warning w-5 h-5 shrink-0 mt-0.5") %>
<div class="text-sm text-secondary">
<p class="font-medium text-primary mb-2">API Key Not Configured</p>
<p>Before you can link Lunch Flow accounts, you need to configure your Lunch Flow API key.</p>
</div>
</div>
<%= render DS::Alert.new(
title: "API Key Not Configured",
message: "Before you can link Lunch Flow accounts, you need to configure your Lunch Flow API key.",
variant: :warning
) %>
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
<p class="font-medium text-primary">Setup Steps:</p>

View File

@@ -62,18 +62,12 @@
</div>
</div>
<div class="bg-warning-50 border border-warning-200 rounded-xl p-4">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
<div>
<h4 class="font-medium text-warning-800 text-sm">Important Security Note</h4>
<p class="text-warning-700 text-sm mt-1">
This is the only time your API key will be displayed. Make sure to copy it now and store it securely.
If you lose this key, you'll need to generate a new one.
</p>
</div>
</div>
</div>
<%= render DS::Alert.new(title: "Important Security Note", variant: :warning) do %>
<p>
This is the only time your API key will be displayed. Make sure to copy it now and store it securely.
If you lose this key, you'll need to generate a new one.
</p>
<% end %>
<div class="bg-surface-inset rounded-xl p-4">
<h4 class="font-medium text-primary mb-3">How to use your API key</h4>

View File

@@ -67,18 +67,12 @@
</div>
</div>
<div class="bg-warning-50 border border-warning-200 rounded-xl p-4">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
<div>
<h4 class="font-medium text-warning-800 text-sm">Important Security Note</h4>
<p class="text-warning-700 text-sm mt-1">
This is the only time your API key will be displayed. Make sure to copy it now and store it securely.
If you lose this key, you'll need to generate a new one.
</p>
</div>
</div>
</div>
<%= render DS::Alert.new(title: "Important Security Note", variant: :warning) do %>
<p>
This is the only time your API key will be displayed. Make sure to copy it now and store it securely.
If you lose this key, you'll need to generate a new one.
</p>
<% end %>
<div class="bg-surface-inset rounded-xl p-4">
<h4 class="font-medium text-primary mb-3">How to use your API key</h4>

View File

@@ -30,18 +30,12 @@
</div>
</div>
<div class="bg-warning-50 border border-warning-200 rounded-xl p-4">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
<div>
<h4 class="font-medium text-warning-800 text-sm">Security Warning</h4>
<p class="text-warning-700 text-sm mt-1">
Your API key will be displayed only once after creation. Make sure to copy and store it securely.
Anyone with access to this key can access your data according to the permissions you select.
</p>
</div>
</div>
</div>
<%= render DS::Alert.new(title: "Security Warning", variant: :warning) do %>
<p>
Your API key will be displayed only once after creation. Make sure to copy and store it securely.
Anyone with access to this key can access your data according to the permissions you select.
</p>
<% end %>
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
<%= render DS::Link.new(

View File

@@ -34,13 +34,8 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
<div class="text-sm text-amber-700">
<p><%= t(".rate_limit_warning") %></p>
<p class="mt-1"><%= t(".no_health_check_note") %></p>
</div>
</div>
</div>
<%= render DS::Alert.new(variant: :warning) do %>
<p><%= t(".rate_limit_warning") %></p>
<p><%= t(".no_health_check_note") %></p>
<% end %>
</div>

View File

@@ -35,12 +35,5 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
<p class="text-sm text-amber-700">
<%= t(".rate_limit_warning") %>
</p>
</div>
</div>
<%= render DS::Alert.new(message: t(".rate_limit_warning"), variant: :warning) %>
</div>

View File

@@ -71,13 +71,6 @@
</div>
<% if ENV["EXCHANGE_RATE_PROVIDER"].present? || ENV["SECURITIES_PROVIDERS"].present? || ENV["SECURITIES_PROVIDER"].present? %>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5 shrink-0") %>
<p class="text-sm text-amber-700">
<%= t(".env_configured_message") %>
</p>
</div>
</div>
<%= render DS::Alert.new(message: t(".env_configured_message"), variant: :warning) %>
<% end %>
</div>

View File

@@ -57,30 +57,26 @@
</div>
<% if @plan_restricted_securities.present? %>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 mt-4">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
<div class="text-sm">
<p class="font-medium text-amber-800"><%= t(".plan_upgrade_warning_title") %></p>
<p class="text-amber-700 mt-1"><%= t(".plan_upgrade_warning_description") %></p>
<ul class="mt-2 space-y-1">
<% @plan_restricted_securities.each do |security| %>
<li class="text-amber-700">
<span class="font-medium"><%= security[:ticker] %></span>
<% if security[:name].present? %>
<span class="text-amber-600">(<%= security[:name] %>)</span>
<% end %>
<span class="text-amber-600">— <%= t(".requires_plan", plan: security[:required_plan]) %></span>
</li>
<% end %>
</ul>
<p class="mt-2">
<a href="https://twelvedata.com/pricing" target="_blank" rel="noopener noreferrer" class="text-amber-800 underline font-medium">
<%= t(".view_pricing") %>
</a>
</p>
</div>
</div>
<div class="mt-4">
<%= render DS::Alert.new(title: t(".plan_upgrade_warning_title"), variant: :warning) do %>
<p><%= t(".plan_upgrade_warning_description") %></p>
<ul class="space-y-1">
<% @plan_restricted_securities.each do |security| %>
<li>
<span class="font-medium text-primary"><%= security[:ticker] %></span>
<% if security[:name].present? %>
<span class="text-secondary">(<%= security[:name] %>)</span>
<% end %>
<span class="text-secondary">— <%= t(".requires_plan", plan: security[:required_plan]) %></span>
</li>
<% end %>
</ul>
<p>
<a href="https://twelvedata.com/pricing" target="_blank" rel="noopener noreferrer" class="text-link underline font-medium">
<%= t(".view_pricing") %>
</a>
</p>
<% end %>
</div>
<% end %>
</div>

View File

@@ -20,15 +20,11 @@
<%= t(".status_inactive") %>
</p>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
<div class="flex items-start gap-2">
<%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5 shrink-0") %>
<div>
<h3 class="text-sm font-medium text-amber-700"><%= t(".connection_failed") %></h3>
<p class="text-sm text-amber-700 mt-1"><%= t(".troubleshooting") %></p>
</div>
</div>
</div>
<%= render DS::Alert.new(
title: t(".connection_failed"),
message: t(".troubleshooting"),
variant: :warning
) %>
</div>
<% end %>
</div>

View File

@@ -21,6 +21,7 @@
"success": { "$value": "{color.green.600}", "$type": "color", "$extensions": { "sure.dark": "{color.green.500}" } },
"warning": { "$value": "{color.yellow.600}", "$type": "color", "$extensions": { "sure.dark": "{color.yellow.400}" } },
"destructive": { "$value": "{color.red.600}", "$type": "color", "$extensions": { "sure.dark": "{color.red.400}" } },
"info": { "$value": "{color.blue.600}", "$type": "color", "$extensions": { "sure.dark": "{color.blue.500}" } },
"shadow": { "$value": "{color.black|6%}", "$type": "color", "$extensions": { "sure.dark": "{color.white|8%}" } },
"gray": {

View File

@@ -1,7 +1,34 @@
class AlertComponentPreview < Lookbook::Preview
# @param message text
# @param title text
# @param variant select [info, success, warning, error]
def default(message: "This is an alert message.", variant: :info)
render DS::Alert.new(message: message, variant: variant.to_sym)
def default(message: "This is an alert message.", title: nil, variant: :info)
render DS::Alert.new(message: message, title: title.presence, variant: variant.to_sym)
end
# @param variant select [info, success, warning, error]
def with_title(variant: :warning)
render DS::Alert.new(
message: "Heads up — this account hasn't synced in 7 days.",
title: "Stale connection",
variant: variant.to_sym
)
end
# @param variant select [info, success, warning, error]
def with_body_slot(variant: :error)
render DS::Alert.new(title: "We couldn't process this request", variant: variant.to_sym) do
tag.div do
safe_join([
tag.p("Verify the values you submitted and try again. If the issue persists, contact support.", class: "text-secondary"),
tag.ul(class: "list-disc list-inside text-secondary") do
safe_join([
tag.li("Check that all required fields are populated."),
tag.li("Confirm the dates fall within an open period.")
])
end
])
end
end
end
end