From 57d71cd55edf09d4189e96843a30d9102ce9e03a Mon Sep 17 00:00:00 2001
From: Guillem Arias Fauste
Date: Sun, 10 May 2026 17:14:06 +0200
Subject: [PATCH] refactor(design-system): extend DS::Alert and migrate 9
inline alert blocks (#1731)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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
---
.../sure-design-system/_generated.css | 2 +
app/components/DS/alert.html.erb | 12 ++++-
app/components/DS/alert.rb | 17 ++++---
app/helpers/application_helper.rb | 2 +-
app/views/lunchflow_items/_api_error.html.erb | 12 +++--
.../lunchflow_items/_setup_required.html.erb | 12 +++--
app/views/settings/api_keys/created.html.erb | 18 +++-----
.../api_keys/created.turbo_stream.erb | 18 +++-----
app/views/settings/api_keys/new.html.erb | 18 +++-----
.../hostings/_alpha_vantage_settings.html.erb | 13 ++----
.../hostings/_eodhd_settings.html.erb | 9 +---
.../hostings/_provider_selection.html.erb | 9 +---
.../hostings/_twelve_data_settings.html.erb | 44 +++++++++----------
.../hostings/_yahoo_finance_settings.html.erb | 14 +++---
design/tokens/sure.tokens.json | 1 +
.../previews/alert_component_preview.rb | 31 ++++++++++++-
16 files changed, 112 insertions(+), 120 deletions(-)
diff --git a/app/assets/tailwind/sure-design-system/_generated.css b/app/assets/tailwind/sure-design-system/_generated.css
index fe92a6a29..105b788e4 100644
--- a/app/assets/tailwind/sure-design-system/_generated.css
+++ b/app/assets/tailwind/sure-design-system/_generated.css
@@ -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);
diff --git a/app/components/DS/alert.html.erb b/app/components/DS/alert.html.erb
index e56c9fc40..8f5c0fd41 100644
--- a/app/components/DS/alert.html.erb
+++ b/app/components/DS/alert.html.erb
@@ -1,7 +1,15 @@
<%= helpers.icon icon_name, size: "sm", color: icon_color, class: "shrink-0" %>
-
- <%= message %>
+
+ <% if title.present? %>
+
<%= title %>
+ <% end %>
+
+ <% if content.present? %>
+ <%= content %>
+ <% elsif message.present? %>
+ <%= message %>
+ <% end %>
diff --git a/app/components/DS/alert.rb b/app/components/DS/alert.rb
index 22241133f..b507111ff 100644
--- a/app/components/DS/alert.rb
+++ b/app/components/DS/alert.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d46415e57..f00d876ea 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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",
diff --git a/app/views/lunchflow_items/_api_error.html.erb b/app/views/lunchflow_items/_api_error.html.erb
index cec9db68c..b1d6cee13 100644
--- a/app/views/lunchflow_items/_api_error.html.erb
+++ b/app/views/lunchflow_items/_api_error.html.erb
@@ -4,13 +4,11 @@
<% dialog.with_header(title: "Lunch Flow Connection Error") %>
<% dialog.with_body do %>
-
- <%= icon("alert-circle", class: "text-destructive w-5 h-5 shrink-0 mt-0.5") %>
-
-
Unable to connect to Lunch Flow
-
<%= error_message %>
-
-
+ <%= render DS::Alert.new(
+ title: "Unable to connect to Lunch Flow",
+ message: error_message,
+ variant: :error
+ ) %>
Common Issues:
diff --git a/app/views/lunchflow_items/_setup_required.html.erb b/app/views/lunchflow_items/_setup_required.html.erb
index b205ef4bd..afd03df41 100644
--- a/app/views/lunchflow_items/_setup_required.html.erb
+++ b/app/views/lunchflow_items/_setup_required.html.erb
@@ -3,13 +3,11 @@
<% dialog.with_header(title: "Lunch Flow Setup Required") %>
<% dialog.with_body do %>
-
- <%= icon("alert-circle", class: "text-warning w-5 h-5 shrink-0 mt-0.5") %>
-
-
API Key Not Configured
-
Before you can link Lunch Flow accounts, you need to configure your Lunch Flow API key.
-
-
+ <%= 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
+ ) %>
Setup Steps:
diff --git a/app/views/settings/api_keys/created.html.erb b/app/views/settings/api_keys/created.html.erb
index 05f7eceb0..d37189000 100644
--- a/app/views/settings/api_keys/created.html.erb
+++ b/app/views/settings/api_keys/created.html.erb
@@ -62,18 +62,12 @@
-
-
- <%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
-
-
Important Security Note
-
- 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.
-
-
-
-
+ <%= render DS::Alert.new(title: "Important Security Note", variant: :warning) do %>
+
+ 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.
+
+ <% end %>
How to use your API key
diff --git a/app/views/settings/api_keys/created.turbo_stream.erb b/app/views/settings/api_keys/created.turbo_stream.erb
index 89dab090b..dec5baeb9 100644
--- a/app/views/settings/api_keys/created.turbo_stream.erb
+++ b/app/views/settings/api_keys/created.turbo_stream.erb
@@ -67,18 +67,12 @@
-
-
- <%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
-
-
Important Security Note
-
- 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.
-
-
-
-
+ <%= render DS::Alert.new(title: "Important Security Note", variant: :warning) do %>
+
+ 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.
+
+ <% end %>
How to use your API key
diff --git a/app/views/settings/api_keys/new.html.erb b/app/views/settings/api_keys/new.html.erb
index 20e322981..34def86b0 100644
--- a/app/views/settings/api_keys/new.html.erb
+++ b/app/views/settings/api_keys/new.html.erb
@@ -30,18 +30,12 @@
-
-
- <%= icon("alert-triangle", class: "w-5 h-5 text-warning-600 mt-0.5") %>
-
-
Security Warning
-
- 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.
-
-
-
-
+ <%= render DS::Alert.new(title: "Security Warning", variant: :warning) do %>
+
+ 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.
+
+ <% end %>
<%= render DS::Link.new(
diff --git a/app/views/settings/hostings/_alpha_vantage_settings.html.erb b/app/views/settings/hostings/_alpha_vantage_settings.html.erb
index 47c67eff5..8a9249541 100644
--- a/app/views/settings/hostings/_alpha_vantage_settings.html.erb
+++ b/app/views/settings/hostings/_alpha_vantage_settings.html.erb
@@ -34,13 +34,8 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
-
-
- <%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
-
-
<%= t(".rate_limit_warning") %>
-
<%= t(".no_health_check_note") %>
-
-
-
+ <%= render DS::Alert.new(variant: :warning) do %>
+
<%= t(".rate_limit_warning") %>
+
<%= t(".no_health_check_note") %>
+ <% end %>
diff --git a/app/views/settings/hostings/_eodhd_settings.html.erb b/app/views/settings/hostings/_eodhd_settings.html.erb
index e9ff03bba..0636da678 100644
--- a/app/views/settings/hostings/_eodhd_settings.html.erb
+++ b/app/views/settings/hostings/_eodhd_settings.html.erb
@@ -35,12 +35,5 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
-
-
- <%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
-
- <%= t(".rate_limit_warning") %>
-
-
-
+ <%= render DS::Alert.new(message: t(".rate_limit_warning"), variant: :warning) %>
diff --git a/app/views/settings/hostings/_provider_selection.html.erb b/app/views/settings/hostings/_provider_selection.html.erb
index 147d082ab..4e38bb38f 100644
--- a/app/views/settings/hostings/_provider_selection.html.erb
+++ b/app/views/settings/hostings/_provider_selection.html.erb
@@ -71,13 +71,6 @@
<% if ENV["EXCHANGE_RATE_PROVIDER"].present? || ENV["SECURITIES_PROVIDERS"].present? || ENV["SECURITIES_PROVIDER"].present? %>
-
-
- <%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5 shrink-0") %>
-
- <%= t(".env_configured_message") %>
-
-
-
+ <%= render DS::Alert.new(message: t(".env_configured_message"), variant: :warning) %>
<% end %>
diff --git a/app/views/settings/hostings/_twelve_data_settings.html.erb b/app/views/settings/hostings/_twelve_data_settings.html.erb
index 91ee30aca..9dbb77289 100644
--- a/app/views/settings/hostings/_twelve_data_settings.html.erb
+++ b/app/views/settings/hostings/_twelve_data_settings.html.erb
@@ -57,30 +57,26 @@
<% if @plan_restricted_securities.present? %>
-
-
- <%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5") %>
-
-
<%= t(".plan_upgrade_warning_title") %>
-
<%= t(".plan_upgrade_warning_description") %>
-
- <% @plan_restricted_securities.each do |security| %>
- -
- <%= security[:ticker] %>
- <% if security[:name].present? %>
- (<%= security[:name] %>)
- <% end %>
- — <%= t(".requires_plan", plan: security[:required_plan]) %>
-
- <% end %>
-
-
-
- <%= t(".view_pricing") %>
-
-
-
-
+
+ <%= render DS::Alert.new(title: t(".plan_upgrade_warning_title"), variant: :warning) do %>
+
<%= t(".plan_upgrade_warning_description") %>
+
+ <% @plan_restricted_securities.each do |security| %>
+ -
+ <%= security[:ticker] %>
+ <% if security[:name].present? %>
+ (<%= security[:name] %>)
+ <% end %>
+ — <%= t(".requires_plan", plan: security[:required_plan]) %>
+
+ <% end %>
+
+
+
+ <%= t(".view_pricing") %>
+
+
+ <% end %>
<% end %>
diff --git a/app/views/settings/hostings/_yahoo_finance_settings.html.erb b/app/views/settings/hostings/_yahoo_finance_settings.html.erb
index 2d9c0bb0e..1ed8044b6 100644
--- a/app/views/settings/hostings/_yahoo_finance_settings.html.erb
+++ b/app/views/settings/hostings/_yahoo_finance_settings.html.erb
@@ -20,15 +20,11 @@
<%= t(".status_inactive") %>
-
-
- <%= icon("alert-triangle", size: "sm", class: "text-amber-600 mt-0.5 shrink-0") %>
-
-
<%= t(".connection_failed") %>
-
<%= t(".troubleshooting") %>
-
-
-
+ <%= render DS::Alert.new(
+ title: t(".connection_failed"),
+ message: t(".troubleshooting"),
+ variant: :warning
+ ) %>
<% end %>
diff --git a/design/tokens/sure.tokens.json b/design/tokens/sure.tokens.json
index 8803068e6..5d899749b 100644
--- a/design/tokens/sure.tokens.json
+++ b/design/tokens/sure.tokens.json
@@ -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": {
diff --git a/test/components/previews/alert_component_preview.rb b/test/components/previews/alert_component_preview.rb
index ddd91183c..3ae3d36e4 100644
--- a/test/components/previews/alert_component_preview.rb
+++ b/test/components/previews/alert_component_preview.rb
@@ -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