Skip to content

Commit

Permalink
Improve components apis (#3)
Browse files Browse the repository at this point in the history
This PR introduces several updates and enhancements to the component
library, focusing on improved functionality, testing, and documentation.
Key changes include:

- Added icon support for buttons with `icon_start` and `icon_end`
parameters
- Updated DropdownComponent with new position, hover, and trigger
configurations
 - Enhanced SwapComponent with states, value, and effect attributes
 - Improved AccordionComponent with `html_attributes` handling
 - Updated AvatarComponent with size, shape, and status validation
 - Added AvatarGroupComponent for grouped avatar display
 - Enhanced CardComponent with new attributes and rendering logic
 - Added coverage reporting and parsing functionality
- Updated tests for all components to include new features and HTML
attributes handling
- Improved Lookbook previews with playground methods and detailed
documentation
  • Loading branch information
Nittarab authored Jan 26, 2025
1 parent 626ea2b commit 00efb91
Show file tree
Hide file tree
Showing 55 changed files with 2,147 additions and 957 deletions.
11 changes: 6 additions & 5 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use Gemfile to manage dependencies no gemspec
Use Minitest and Fixtures for testing
Use Capybara for testing

Create SVG for icons
Create SVG for icons, and use the IconHelper to render them
The IconHelper should not be used in the component, but in the test and the preview, Icon are passed, to the component

DONT delete documentation comments in the code, update them instead

Expand Down Expand Up @@ -46,14 +47,14 @@ For every component create:
- Ruy class
- html template
- lookbook preview with a playground method and the description as Lookbook description with group and label
- test
- TEST: in the test we test the component behavior and the preview (just the fact that they render). Don't duplicate tests to much, but give precedence to the preview but be sure to test the behavior)
- documentation

For images, use placehold.co to generate placeholder images.

After you make a change run the test with Coverage:

When you want to check the coverage, run:
COVERAGE=true bin/rails test
Read the coverage report by using ./bin/parse_coverage.rb

and read the /coverage/coverage.xml file to see if you missed something.
Every component preview should be organized into groups that separate HTML and Ruby source rendering.

3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ end

# Start debugger with binding.b [https://github.com/ruby/debug]
# gem "debug", ">= 1.0.0"

gem 'nokogiri', '~> 1.18', group: :development
gem 'terminal-table', '~> 1.6', group: :development
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ GEM
simplecov_json_formatter (0.1.4)
sorbet-runtime (0.5.11766)
stringio (3.1.2)
terminal-table (1.6.0)
thor (1.3.2)
timeout (0.4.3)
tzinfo (2.0.6)
Expand Down Expand Up @@ -311,6 +312,7 @@ DEPENDENCIES
daisy_components!
debug
lookbook (= 2.3.4)
nokogiri (~> 1.18)
puma
rubocop (= 1.69.2)
rubocop-capybara (= 2.19.0)
Expand All @@ -319,6 +321,7 @@ DEPENDENCIES
ruby-lsp
simplecov
simplecov-cobertura
terminal-table (~> 1.6)
view_component (= 3.21.0)

BUNDLED WITH
Expand Down
74 changes: 61 additions & 13 deletions app/components/daisy_components/actions/button_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ module Actions
# @example Basic usage
# <%= render(ButtonComponent.new(text: "Click me")) %>
#
# @example With icons
# <%= render(ButtonComponent.new(
# text: "Submit",
# icon_start: helpers.check_icon("h-5 w-5"),
# variant: "primary"
# )) %>
#
# @example With both icons
# <%= render(ButtonComponent.new(
# text: "Next",
# icon_start: helpers.sync_icon("h-5 w-5"),
# icon_end: helpers.arrow_right_icon("h-5 w-5")
# )) %>
#
# @example Icon only button
# <%= render(ButtonComponent.new(
# icon_start: helpers.search_icon("h-6 w-6"),
# class: "btn-square"
# )) %>
#
# @example With block content
# <%= render(ButtonComponent.new) do %>
# Complex <strong>content</strong>
Expand All @@ -33,7 +53,10 @@ module Actions
# method: :delete,
# variant: "error"
# )) %>
class ButtonComponent < BaseComponent
class ButtonComponent < BaseComponent # rubocop:disable Metrics/ClassLength
renders_one :start_icon
renders_one :end_icon

# Available button variants from DaisyUI
VARIANTS = %w[neutral primary secondary accent info success warning error ghost link].freeze

Expand All @@ -55,6 +78,8 @@ class ButtonComponent < BaseComponent
# @param rel [String] Link relationship attribute (e.g., noopener, noreferrer)
# @param loading [Boolean] When true, shows a loading spinner and disables the button
# @param active [Boolean] When true, gives the button a pressed appearance
# @param icon_start [String] SVG icon to display before the text
# @param icon_end [String] SVG icon to display after the text
# @param system_arguments [Hash] Additional HTML attributes to be applied to the button
def initialize(
text: nil,
Expand All @@ -68,6 +93,8 @@ def initialize(
rel: nil,
loading: false,
active: false,
icon_start: nil,
icon_end: nil,
**system_arguments
)
@text = text
Expand All @@ -81,6 +108,10 @@ def initialize(
@rel = rel
@loading = loading
@active = active

with_start_icon { icon_start } if icon_start
with_end_icon { icon_end } if icon_end

super(**system_arguments)
end

Expand All @@ -95,25 +126,24 @@ def call
private

def button_tag
tag.button(**button_arguments) { content || @text }
tag.button(**button_arguments) { button_content }
end

def link_tag
tag.a(**link_arguments) { content || @text }
tag.a(**link_arguments) { button_content }
end

def shared_arguments
classes = class_names(
'btn',
"btn-#{@variant}" => @variant,
"btn-#{@size}" => @size,
'btn-disabled' => @disabled || @loading,
'loading' => @loading,
'btn-active' => @active
)
def button_content
parts = []
parts << start_icon if start_icon
parts << (content || @text)
parts << end_icon if end_icon
safe_join(parts)
end

def shared_arguments
{
class: [classes, system_arguments[:class]].compact.join(' '),
class: computed_classes,
disabled: @disabled || @loading,
'aria-disabled': @disabled || @loading,
'aria-busy': @loading,
Expand All @@ -122,6 +152,24 @@ def shared_arguments
}
end

def computed_classes
base_classes = class_names(
'btn',
"btn-#{@variant}" => @variant,
"btn-#{@size}" => @size,
'btn-disabled' => @disabled || @loading,
'loading' => @loading,
'btn-active' => @active,
'gap-2' => icon_and_content?
)

[base_classes, system_arguments[:class]].compact.join(' ')
end

def icon_and_content?
(start_icon || end_icon) && (@text || content)
end

def button_arguments
shared_arguments.merge(
type: @type,
Expand Down
118 changes: 78 additions & 40 deletions app/components/daisy_components/actions/dropdown_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,65 @@ module Actions
# Dropdown component implementing DaisyUI's dropdown styles
#
# @example Basic usage
# <%= render(DropdownComponent.new) do %>
# <%= render(ButtonComponent.new(text: "Click me")) %>
# <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
# <li><a>Item 1</a></li>
# <li><a>Item 2</a></li>
# </ul>
# <% end %>
# <%= render(DropdownComponent.new(
# trigger: { text: "Click me" },
# items: [
# { text: "Item 1", href: "#" },
# { text: "Item 2", href: "#" }
# ]
# )) %>
#
# @example With position
# <%= render(DropdownComponent.new(position: "top")) do %>
# <%= render(ButtonComponent.new(text: "Dropdown")) %>
# <ul class="dropdown-content menu">
# <li><a>Item</a></li>
# </ul>
# <% end %>
#
# @example With hover
# <%= render(DropdownComponent.new(hover: true)) do %>
# <%= render(ButtonComponent.new(text: "Hover me")) %>
# <ul class="dropdown-content menu">
# <li><a>Item</a></li>
# </ul>
# <% end %>
#
# @example Forced open state
# <%= render(DropdownComponent.new(open: true)) do %>
# <%= render(ButtonComponent.new(text: "Always open")) %>
# <ul class="dropdown-content menu">
# <li><a>Item</a></li>
# </ul>
# <% end %>
#
# @example Right aligned
# <%= render(DropdownComponent.new(align_end: true)) do %>
# <%= render(ButtonComponent.new(text: "Right aligned")) %>
# <ul class="dropdown-content menu">
# <li><a>Item</a></li>
# </ul>
# <% end %>
# @example With position and hover
# <%= render(DropdownComponent.new(
# position: :top,
# hover: true,
# trigger: {
# text: "Settings",
# icon: helpers.cog_icon("h-5 w-5"),
# variant: :ghost
# },
# items: [
# { text: "Profile", href: "/profile", icon: helpers.user_icon("h-5 w-5") },
# { text: "Settings", href: "/settings", icon: helpers.cog_icon("h-5 w-5") },
# { type: :divider },
# { text: "Logout", href: "/logout", variant: :error }
# ]
# )) %>
class DropdownComponent < BaseComponent
# Available dropdown positions from DaisyUI
POSITIONS = %w[top bottom left right].freeze
VARIANTS = %w[primary secondary accent info success warning error ghost neutral].freeze
SIZES = %w[xs sm md lg].freeze

# @param position [String] Position of the dropdown content relative to the trigger (top/bottom/left/right)
# @param hover [Boolean] When true, opens the dropdown on hover instead of click
# @param open [Boolean] When true, forces the dropdown to stay open
# @param align_end [Boolean] When true, aligns the dropdown content to the end (right) of the trigger
# @param variant [String] Button variant for the trigger (primary/secondary/accent/etc)
# @param size [String] Size of the trigger button (xs/sm/md/lg)
# @param trigger [Hash] Configuration for the trigger button
# @param items [Array<Hash>] Array of menu items
# @param system_arguments [Hash] Additional HTML attributes to be applied to the dropdown container
def initialize(position: nil, hover: false, open: false, align_end: false, **system_arguments)
def initialize(position: nil, hover: false, open: false, align_end: false,
variant: nil, size: nil, trigger: nil, items: nil, **system_arguments)
@position = position if POSITIONS.include?(position.to_s)
@hover = hover
@open = open
@align_end = align_end
@variant = variant if VARIANTS.include?(variant.to_s)
@size = size if SIZES.include?(size.to_s)
@trigger = trigger || {}
@items = items || []
super(**system_arguments)
end

def call
tag.div(**dropdown_arguments) { content }
tag.div(**dropdown_arguments) do
safe_join([
render_trigger,
render_menu
])
end
end

private
Expand All @@ -81,6 +82,43 @@ def dropdown_arguments
**system_arguments.except(:class)
}.compact
end

def render_trigger
trigger_classes = class_names(
'btn',
"btn-#{@variant}" => @variant,
"btn-#{@size}" => @size,
@trigger[:class] => @trigger[:class]
)

tag.button(class: trigger_classes) do
safe_join([
@trigger[:icon],
@trigger[:text]
].compact)
end
end

def render_menu
return unless @items.any?

tag.ul(class: 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52') do
safe_join(@items.map { |item| render_menu_item(item) })
end
end

def render_menu_item(item)
return tag.li(class: 'divider') if item[:type] == :divider

tag.li do
tag.a(href: item[:href], class: item[:variant] ? "text-#{item[:variant]}" : nil) do
safe_join([
item[:icon],
item[:text]
].compact)
end
end
end
end
end
end
Loading

0 comments on commit 00efb91

Please sign in to comment.