1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 23:45:21 +02:00

Add lookbook + viewcomponent, organize design system file

This commit is contained in:
Zach Gollwitzer 2025-04-24 18:34:51 -04:00
parent 210b89cd17
commit 6af50928e6
19 changed files with 656 additions and 372 deletions

View file

@ -19,9 +19,11 @@ gem "propshaft"
gem "tailwindcss-rails"
gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
# Hotwire + UI
gem "stimulus-rails"
gem "turbo-rails"
gem "view_component"
gem "lookbook", ">= 2.3.7"
gem "hotwire_combobox"

View file

@ -139,6 +139,8 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.21.1)
addressable
csv (3.3.4)
date (3.4.1)
debug (1.10.0)
@ -194,6 +196,8 @@ GEM
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
htmlbeautifier (1.4.3)
htmlentities (4.3.4)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.15)
@ -255,6 +259,18 @@ GEM
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lookbook (2.3.9)
activemodel
css_parser
htmlbeautifier (~> 1.3)
htmlentities (~> 4.3.4)
marcel (~> 1.0)
railties (>= 5.0)
redcarpet (~> 3.5)
rouge (>= 3.26, < 5.0)
view_component (>= 2.0)
yard (~> 0.9)
zeitwerk (~> 2.5)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@ -262,6 +278,7 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
method_source (1.1.0)
mini_magick (5.2.0)
benchmark
logger
@ -394,6 +411,7 @@ GEM
io-console (~> 0.5)
rexml (3.4.1)
rotp (6.3.0)
rouge (4.5.1)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@ -508,6 +526,10 @@ GEM
vcr (6.3.1)
base64
vernier (1.7.0)
view_component (3.21.0)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@ -524,6 +546,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.37)
zeitwerk (2.7.2)
PLATFORMS
@ -564,6 +587,7 @@ DEPENDENCIES
jwt
letter_opener
logtail-rails
lookbook (>= 2.3.7)
lucide-rails!
mocha
octokit
@ -596,6 +620,7 @@ DEPENDENCIES
tzinfo-data
vcr
vernier
view_component
web-console
webmock

View file

@ -5,6 +5,12 @@
One-off styling (3rd party overrides, etc.) should be done in the application.css file.
*/
@import './maybe-design-system/background-utils.css';
@import './maybe-design-system/foreground-utils.css';
@import './maybe-design-system/text-utils.css';
@import './maybe-design-system/border-utils.css';
@import './maybe-design-system/component-utils.css';
@custom-variant theme-dark (&:where([data-theme=dark], [data-theme=dark] *));
@theme {
@ -18,6 +24,7 @@
--color-success: var(--color-green-600);
--color-warning: var(--color-yellow-600);
--color-destructive: var(--color-red-600);
--color-shadow: --alpha(var(--color-black) / 6%);
/* Gray scale */
--color-gray-25: #FAFAFA;
@ -231,262 +238,19 @@
}
}
/* Custom shadow borders used for surfaces / containers */
@utility shadow-border-xs {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-sm {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-md {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-lg {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-xl {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50);
}
/* Design system color utilities */
@utility text-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility text-secondary {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility text-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility text-link {
@apply text-blue-600;
@variant theme-dark {
@apply text-blue-500;
}
}
@utility bg-surface {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-black;
}
}
@utility bg-surface-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-surface-inset {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-surface-inset-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-container-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility bg-inverse {
@apply bg-gray-800;
@variant theme-dark {
@apply bg-white;
}
}
@utility bg-inverse-hover {
@apply bg-gray-700;
@variant theme-dark {
@apply bg-gray-100;
}
}
@utility bg-overlay {
background-color: rgba(var(--color-gray-100), 0.5);
@variant theme-dark {
background-color: var(--color-alpha-black-900);
}
}
@utility border-primary {
@apply border-alpha-black-300;
@variant theme-dark {
@apply border-alpha-white-400;
}
}
@utility border-secondary {
@apply border-alpha-black-200;
@variant theme-dark {
@apply border-alpha-white-300;
}
}
@utility border-tertiary {
@apply border-alpha-black-100;
@variant theme-dark {
@apply border-alpha-white-200;
}
}
@utility border-subdued {
@apply border-alpha-black-50;
@variant theme-dark {
@apply border-alpha-white-100;
}
}
@utility border-solid {
@apply border-black;
@variant theme-dark {
@apply border-white;
}
}
@utility border-destructive {
@apply border-red-500;
@variant theme-dark {
@apply border-red-400;
}
}
/* Foreground Colors */
@utility fg-gray {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility fg-contrast {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
}
@utility fg-inverse {
@apply text-white;
@variant theme-dark {
@apply text-gray-900;
}
}
@utility fg-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility fg-primary-variant {
@apply text-gray-800;
@variant theme-dark {
@apply text-gray-50;
}
}
@utility fg-secondary {
@apply text-gray-50;
@variant theme-dark {
@apply text-gray-700;
}
}
@utility fg-secondary-variant {
@apply text-gray-100;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility fg-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
/* Specific override for strong tags in prose under dark mode */
.prose:where([data-theme=dark], [data-theme=dark] *) strong {
color: theme(colors.white) !important;
}
@layer base {
[data-theme="dark"] {
--color-success: var(--color-green-500);
--color-warning: var(--color-yellow-400);
--color-destructive: var(--color-red-400);
--color-shadow: --alpha(#000000 / 8%);
}
button {
@apply cursor-pointer focus-visible:outline-gray-900;
}
@ -733,120 +497,5 @@
}
}
@layer utilities {
/* Specific override for strong tags in prose under dark mode */
.prose:where([data-theme=dark], [data-theme=dark] *) strong {
color: theme(colors.white) !important;
}
}
/* Button Backgrounds */
@utility button-bg-primary {
@apply bg-gray-900;
/* Maps to fg-primary light */
@variant theme-dark {
@apply bg-white;
/* Maps to fg-primary dark */
}
}
@utility button-bg-primary-hover {
@apply bg-gray-800;
/* Maps to fg-primary-variant light */
@variant theme-dark {
@apply bg-gray-50;
/* Maps to fg-primary-variant dark */
}
}
@utility button-bg-secondary {
@apply bg-gray-50; /* Maps to fg-secondary light */
@variant theme-dark {
@apply bg-gray-700; /* Maps to fg-secondary dark */
}
}
@utility button-bg-secondary-hover {
@apply bg-gray-100; /* Maps to fg-secondary-variant light */
@variant theme-dark {
@apply bg-gray-600; /* Maps to fg-secondary-variant dark */
}
}
@utility button-bg-disabled {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility button-bg-destructive {
@apply bg-red-500;
@variant theme-dark {
@apply bg-red-400;
}
}
@utility button-bg-destructive-hover {
@apply bg-red-600;
@variant theme-dark {
@apply bg-red-500;
}
}
@utility button-bg-ghost-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800 fg-inverse;
}
}
@utility button-bg-outline-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
/* Tab Styles */
@utility tab-item-active {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility tab-item-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility tab-bg-group {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-alpha-black-700;
}
}
@utility bg-nav-indicator {
@apply bg-black;
@variant theme-dark {
@apply bg-white;
}
}

View file

@ -0,0 +1,87 @@
@utility bg-surface {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-black;
}
}
@utility bg-surface-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-surface-inset {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-surface-inset-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-container-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility bg-inverse {
@apply bg-gray-800;
@variant theme-dark {
@apply bg-white;
}
}
@utility bg-inverse-hover {
@apply bg-gray-700;
@variant theme-dark {
@apply bg-gray-100;
}
}
@utility bg-overlay {
background-color: --alpha(var(--color-gray-100) / 50%);
@variant theme-dark {
background-color: var(--color-alpha-black-900);
}
}

View file

@ -0,0 +1,68 @@
/* Custom shadow borders used for surfaces / containers */
@utility shadow-border-xs {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-sm {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-md {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-lg {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-xl {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility border-primary {
@apply border-alpha-black-300;
@variant theme-dark {
@apply border-alpha-white-400;
}
}
@utility border-secondary {
@apply border-alpha-black-200;
@variant theme-dark {
@apply border-alpha-white-300;
}
}
@utility border-tertiary {
@apply border-alpha-black-100;
@variant theme-dark {
@apply border-alpha-white-200;
}
}
@utility border-subdued {
@apply border-alpha-black-50;
@variant theme-dark {
@apply border-alpha-white-100;
}
}
@utility border-solid {
@apply border-black;
@variant theme-dark {
@apply border-white;
}
}
@utility border-destructive {
@apply border-red-500;
@variant theme-dark {
@apply border-red-400;
}
}

View file

@ -0,0 +1,109 @@
/* Button Backgrounds */
@utility button-bg-primary {
@apply bg-gray-900;
/* Maps to fg-primary light */
@variant theme-dark {
@apply bg-white;
/* Maps to fg-primary dark */
}
}
@utility button-bg-primary-hover {
@apply bg-gray-800;
/* Maps to fg-primary-variant light */
@variant theme-dark {
@apply bg-gray-50;
/* Maps to fg-primary-variant dark */
}
}
@utility button-bg-secondary {
@apply bg-gray-50; /* Maps to fg-secondary light */
@variant theme-dark {
@apply bg-gray-700; /* Maps to fg-secondary dark */
}
}
@utility button-bg-secondary-hover {
@apply bg-gray-100; /* Maps to fg-secondary-variant light */
@variant theme-dark {
@apply bg-gray-600; /* Maps to fg-secondary-variant dark */
}
}
@utility button-bg-disabled {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility button-bg-destructive {
@apply bg-red-500;
@variant theme-dark {
@apply bg-red-400;
}
}
@utility button-bg-destructive-hover {
@apply bg-red-600;
@variant theme-dark {
@apply bg-red-500;
}
}
@utility button-bg-ghost-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800 fg-inverse;
}
}
@utility button-bg-outline-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
/* Tab Styles */
@utility tab-item-active {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility tab-item-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility tab-bg-group {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-alpha-black-700;
}
}
@utility bg-nav-indicator {
@apply bg-black;
@variant theme-dark {
@apply bg-white;
}
}

View file

@ -0,0 +1,63 @@
@utility fg-gray {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility fg-contrast {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
}
@utility fg-inverse {
@apply text-white;
@variant theme-dark {
@apply text-gray-900;
}
}
@utility fg-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility fg-primary-variant {
@apply text-gray-800;
@variant theme-dark {
@apply text-gray-50;
}
}
@utility fg-secondary {
@apply text-gray-50;
@variant theme-dark {
@apply text-gray-700;
}
}
@utility fg-secondary-variant {
@apply text-gray-100;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility fg-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
}

View file

@ -0,0 +1,31 @@
@utility text-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility text-secondary {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility text-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility text-link {
@apply text-blue-600;
@variant theme-dark {
@apply text-blue-500;
}
}

View file

@ -0,0 +1,15 @@
<%= wrapper_tag do %>
<% if icon_only? %>
<%= lucide_icon(@icon, class: icon_classes) %>
<% else %>
<% if @leading_icon %>
<%= lucide_icon(@leading_icon, class: icon_classes) %>
<% end %>
<span class="<%= text_classes %>"><%= @text %></span>
<% if @trailing_icon %>
<%= lucide_icon(@trailing_icon, class: icon_classes) %>
<% end %>
<% end %>
<% end %>

View file

@ -0,0 +1,117 @@
# frozen_string_literal: true
class ButtonComponent < ViewComponent::Base
VARIANTS = {
primary: {
bg: "bg-gray-900 theme-dark:bg-white hover:bg-gray-800 theme-dark:hover:bg-gray-50 disabled:bg-gray-500 theme-dark:disabled:bg-gray-400",
fg: "text-white theme-dark:text-gray-900"
},
secondary: {
bg: "bg-gray-50 theme-dark:bg-gray-700 hover:bg-gray-100 theme-dark:hover:bg-gray-600 disabled:bg-gray-200 theme-dark:disabled:bg-gray-600",
fg: "text-gray-900 theme-dark:text-white"
},
outline: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
fg: "text-gray-900 theme-dark:text-white",
border: "border border-gray-900 theme-dark:border-white"
},
outline_destructive: {
bg: "bg-transparent hover:bg-red-100 theme-dark:hover:bg-red-700",
fg: "text-destructive",
border: "border border-red-500"
},
ghost: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
fg: "text-gray-900 theme-dark:text-white"
},
link_color: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
fg: "text-gray-900 theme-dark:text-white"
},
link_gray: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
fg: "text-gray-900 theme-dark:text-white"
},
icon: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700 rounded-lg",
fg: "fg-gray"
}
}.freeze
SIZES = {
sm: {
icon_container: "w-8 h-8",
container: "px-2 py-1 rounded-md",
text: "text-sm",
icon: "w-4 h-4"
},
md: {
icon_container: "w-10 h-10",
container: "px-3 py-2 rounded-lg",
text: "text-sm",
icon: "w-5 h-5"
},
lg: {
icon_container: "w-12 h-12",
container: "px-4 py-3 rounded-xl",
text: "text-base",
icon: "w-6 h-6"
}
}
def initialize(text:, variant: "primary", size: "md", href: nil, leading_icon: nil, trailing_icon: nil, icon: nil, **options)
@text = text
@variant = variant.underscore.to_sym
@size = size.to_sym
@href = href
@leading_icon = leading_icon
@trailing_icon = trailing_icon
@icon = icon
@options = options
end
def wrapper_tag(&block)
html_tag = @href ? "a" : "button"
if @href.present?
content_tag(html_tag, class: container_classes, href: @href, **@options, &block)
else
content_tag(html_tag, class: container_classes, **@options, &block)
end
end
def text_classes
[
size_meta[:text],
variant_meta[:fg]
].join(" ")
end
def icon_classes
[
size_meta[:icon],
variant_meta[:fg]
].join(" ")
end
def icon_only?
@variant == :icon
end
private
def container_classes
[
"inline-flex items-center justify-center gap-1",
@variant == :icon ? size_meta[:icon_container] : size_meta[:container],
variant_meta[:bg]
].join(" ")
end
def size_meta
SIZES[@size]
end
def variant_meta
VARIANTS[@variant]
end
end

View file

@ -0,0 +1,61 @@
# frozen_string_literal: true
class IconComponent < ViewComponent::Base
erb_template <<~ERB
<%= tag.div class: container_classes do %>
<%= lucide_icon(@icon, class: icon_classes) %>
<% end %>
ERB
VARIANTS = {
default: {
icon: "fg-gray",
container: "bg-transparent"
}
}
SIZES = {
sm: {
icon: "w-4 h-4",
container: "w-8 h-8"
},
md: {
icon: "w-5 h-5",
container: "w-10 h-10"
},
lg: {
icon: "w-6 h-6",
container: "w-12 h-12"
}
}
def initialize(icon, variant: "default", size: "md")
@icon = icon
@variant = variant.to_sym
@size = size.to_sym
end
def icon_classes
[
size_meta[:icon],
variant_meta[:icon]
].join(" ")
end
def container_classes
[
"flex justify-center items-center",
size_meta[:container],
variant_meta[:container]
].join(" ")
end
private
def variant_meta
VARIANTS[@variant]
end
def size_meta
SIZES[@size]
end
end

View file

@ -0,0 +1,3 @@
class LookbooksController < Lookbook::PreviewController
layout "lookbooks"
end

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html data-theme="<%= params.dig(:lookbook, :display, :theme) %>">
<head>
<title>Component Preview</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= javascript_importmap_tags %>
</head>
<body class="p-4 bg-surface">
<%= yield %>
</body>
</html>

View file

@ -8,9 +8,9 @@
<%= link_to new_account_path(step: "method_select", classification: "asset"),
class: "btn btn--primary flex items-center justify-center gap-2 rounded-full w-9 h-9 md:hidden",
data: { turbo_frame: "modal" } do %>
<span class="flex items-center justify-center">
<%= lucide_icon("plus", class: "size-5") %>
</span>
<span class="flex items-center justify-center">
<%= lucide_icon("plus", class: "size-5") %>
</span>
<% end %>
</div>
<% end %>

View file

@ -33,5 +33,10 @@ module Maybe
if Rails.application.credentials.active_record_encryption.present?
config.active_record.encryption = Rails.application.credentials.active_record_encryption
end
config.view_component.preview_controller = "LookbooksController"
config.lookbook.preview_display_options = {
theme: [ "light", "dark" ] # available in view as params[:theme]
}
end
end

View file

@ -0,0 +1,3 @@
Rails.application.configure do
Rack::MiniProfiler.config.skip_paths = [ "/design-system" ]
end

View file

@ -8,6 +8,8 @@ Rails.application.routes.draw do
delete :disable
end
mount Lookbook::Engine, at: "/design-system"
# Uses basic auth - see config/initializers/sidekiq.rb
mount Sidekiq::Web => "/sidekiq"

View file

@ -0,0 +1,19 @@
class ButtonComponentPreview < ViewComponent::Preview
# @param variant select {{ ButtonComponent::VARIANTS.keys }}
# @param size select {{ ButtonComponent::SIZES.keys }}
# @param disabled toggle
# @param leading_icon text
# @param trailing_icon text
# @param icon text "This is only used for icon-only buttons"
def default(variant: "primary", size: "md", disabled: false, leading_icon: "plus", trailing_icon: nil, icon: "circle")
render ButtonComponent.new(
text: "Sample button",
variant: variant,
size: size,
disabled: disabled,
leading_icon: leading_icon,
trailing_icon: trailing_icon,
icon: icon
)
end
end

View file

@ -0,0 +1,11 @@
class IconComponentPreview < ViewComponent::Preview
# @param variant select {{ IconComponent::VARIANTS.keys }}
# @param size select {{ IconComponent::SIZES.keys }}
def default(variant: "default", size: "md")
render IconComponent.new(
"circle-user",
variant: variant,
size: size
)
end
end