daisy_components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.cursor/rules/how-to-write-tests.mdc +67 -0
- data/.cursor/rules/preview-file-stucture.mdc +95 -0
- data/.cursorrules +64 -0
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +13 -0
- data/.vscode/launch.json +55 -0
- data/.vscode/settings.json +44 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +32 -0
- data/Gemfile.lock +335 -0
- data/MIT-LICENSE +20 -0
- data/README.md +64 -0
- data/Rakefile +7 -0
- data/app/assets/images/daisy_components/.keep +0 -0
- data/app/assets/stylesheets/daisy_ui/application.css +15 -0
- data/app/components/daisy_ui/actions/button.rb +262 -0
- data/app/components/daisy_ui/actions/dropdown.rb +125 -0
- data/app/components/daisy_ui/actions/swap.rb +126 -0
- data/app/components/daisy_ui/base_component.rb +29 -0
- data/app/components/daisy_ui/data_display/accordion.rb +128 -0
- data/app/components/daisy_ui/data_display/accordion_item.rb +121 -0
- data/app/components/daisy_ui/data_display/avatar.rb +131 -0
- data/app/components/daisy_ui/data_display/avatar_group.rb +102 -0
- data/app/components/daisy_ui/data_display/badge.rb +126 -0
- data/app/components/daisy_ui/data_display/card/actions.rb +94 -0
- data/app/components/daisy_ui/data_display/card/body.rb +113 -0
- data/app/components/daisy_ui/data_display/card/figure.rb +44 -0
- data/app/components/daisy_ui/data_display/card/title.rb +56 -0
- data/app/components/daisy_ui/data_display/card.rb +157 -0
- data/app/components/daisy_ui/data_display/chat.rb +92 -0
- data/app/components/daisy_ui/data_display/chat_bubble/metadata.rb +71 -0
- data/app/components/daisy_ui/data_display/chat_bubble.rb +166 -0
- data/app/components/daisy_ui/divider.rb +9 -0
- data/app/components/daisy_ui/item.rb +20 -0
- data/app/components/daisy_ui/title.rb +9 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/daisy_ui/application_controller.rb +6 -0
- data/app/helpers/daisy_ui/application_helper.rb +6 -0
- data/app/helpers/daisy_ui/icons_helper.rb +296 -0
- data/app/views/layouts/daisyui/application.html.erb +17 -0
- data/bin/parse_coverage.rb +59 -0
- data/bin/rails +57 -0
- data/bin/rubocop +10 -0
- data/bin/scrape_component +86 -0
- data/daisy_components.gemspec +33 -0
- data/docs/assets/2025-01_screeshot_1.png +0 -0
- data/docs/assets/2025-01_screeshot_2.png +0 -0
- data/docs/assets/2025-01_screeshot_3.png +0 -0
- data/lib/daisy_components.rb +5 -0
- data/lib/daisy_ui/engine.rb +51 -0
- data/lib/daisy_ui/version.rb +5 -0
- data/lib/daisy_ui.rb +13 -0
- data/lib/tasks/daisy_ui_tasks.rake +6 -0
- metadata +112 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Dropdown component implementing DaisyUI's dropdown styles
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage with text trigger
|
|
7
|
+
# <%= render(DropdownComponent.new) do |d| %>
|
|
8
|
+
# <% d.with_trigger(text: "Click me") %>
|
|
9
|
+
# <% d.with_item(href: "#") { "Item 1" } %>
|
|
10
|
+
# <% d.with_item(href: "#") { "Item 2" } %>
|
|
11
|
+
# <% end %>
|
|
12
|
+
#
|
|
13
|
+
# @example With custom trigger content
|
|
14
|
+
# <%= render(DropdownComponent.new) do |d| %>
|
|
15
|
+
# <% d.with_trigger do %>
|
|
16
|
+
# <%= helpers.cog_icon("h-5 w-5") %> Settings
|
|
17
|
+
# <% end %>
|
|
18
|
+
# <% d.with_item(href: "#") { "Item 1" } %>
|
|
19
|
+
# <% d.with_item(href: "#") { "Item 2" } %>
|
|
20
|
+
# <% d.with_divider %>
|
|
21
|
+
# <% d.with_item(href: "#", class: "text-error") { "Delete" } %>
|
|
22
|
+
# <% end %>
|
|
23
|
+
class Dropdown < BaseComponent
|
|
24
|
+
renders_one :trigger, lambda { |**kwargs, &block|
|
|
25
|
+
return block if block
|
|
26
|
+
|
|
27
|
+
defaults = { tag_type: :div, tabindex: '0', role: :button, class: 'm-1' }
|
|
28
|
+
args = defaults.merge(kwargs)
|
|
29
|
+
render(Button.new(**args))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
renders_many :items, types: {
|
|
33
|
+
item: { renders: DaisyUI::Item, as: :item },
|
|
34
|
+
divider: { renders: DaisyUI::Divider, as: :divider },
|
|
35
|
+
title: { renders: DaisyUI::Title, as: :title }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
renders_one :custom_content
|
|
39
|
+
|
|
40
|
+
# Available dropdown positions from DaisyUI
|
|
41
|
+
POSITIONS = {
|
|
42
|
+
top: 'dropdown-top',
|
|
43
|
+
top_end: 'dropdown-top-end',
|
|
44
|
+
top_center: 'dropdown-top-center',
|
|
45
|
+
bottom: 'dropdown-bottom',
|
|
46
|
+
bottom_end: 'dropdown-bottom-end',
|
|
47
|
+
bottom_center: 'dropdown-bottom-center',
|
|
48
|
+
left: 'dropdown-left',
|
|
49
|
+
left_end: 'dropdown-left-end',
|
|
50
|
+
left_center: 'dropdown-left-center',
|
|
51
|
+
right: 'dropdown-right',
|
|
52
|
+
right_end: 'dropdown-right-end',
|
|
53
|
+
right_center: 'dropdown-right-center'
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
ALIGNMENTS = {
|
|
57
|
+
start: 'dropdown-start',
|
|
58
|
+
end: 'dropdown-end',
|
|
59
|
+
center: 'dropdown-center'
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# @param position [Symbol] Position of the dropdown content relative to the trigger
|
|
63
|
+
# @param hover [Boolean, String] When true or 'content', opens the dropdown on hover instead of click
|
|
64
|
+
# @param open [Boolean] When true, forces the dropdown to stay open
|
|
65
|
+
# @param align [Symbol] When :start, :end, or :center, aligns the dropdown content
|
|
66
|
+
# @param menu_class [String] Additional classes for the menu
|
|
67
|
+
# @param menu_tabindex [Integer, nil] Tabindex for the menu (defaults to 0)
|
|
68
|
+
# @param system_arguments [Hash] Additional HTML attributes
|
|
69
|
+
def initialize(position: nil, hover: false, open: false, align: nil,
|
|
70
|
+
menu_class: nil, menu_tabindex: 0, **system_arguments)
|
|
71
|
+
@position = build_argument(position, POSITIONS, 'position')
|
|
72
|
+
@hover = hover
|
|
73
|
+
@open = open
|
|
74
|
+
@align = build_argument(align, ALIGNMENTS, 'align')
|
|
75
|
+
@menu_class = menu_class
|
|
76
|
+
@menu_tabindex = menu_tabindex
|
|
77
|
+
super(**system_arguments)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def call
|
|
81
|
+
tag.div(**dropdown_arguments) do
|
|
82
|
+
safe_join([
|
|
83
|
+
trigger,
|
|
84
|
+
render_menu
|
|
85
|
+
].compact)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def dropdown_arguments
|
|
92
|
+
{
|
|
93
|
+
class: computed_dropdown_classes,
|
|
94
|
+
**system_arguments.except(:class)
|
|
95
|
+
}.compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def computed_dropdown_classes
|
|
99
|
+
modifiers = ['dropdown']
|
|
100
|
+
modifiers << @position if @position
|
|
101
|
+
modifiers << 'dropdown-hover' if @hover == true
|
|
102
|
+
modifiers << 'dropdown-hover-content' if @hover == 'content'
|
|
103
|
+
modifiers << 'dropdown-open' if @open
|
|
104
|
+
modifiers << @align if @align
|
|
105
|
+
|
|
106
|
+
class_names(modifiers, system_arguments[:class])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_menu
|
|
110
|
+
return custom_content if custom_content
|
|
111
|
+
return unless items.any?
|
|
112
|
+
|
|
113
|
+
tag.ul(tabindex: @menu_tabindex, class: computed_menu_classes) do
|
|
114
|
+
safe_join(items)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def computed_menu_classes
|
|
119
|
+
class_names(
|
|
120
|
+
'dropdown-content menu',
|
|
121
|
+
@menu_class || 'bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm'
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Swap component implementing DaisyUI's swap styles
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# <%= render(SwapComponent.new(
|
|
8
|
+
# states: { on: 'ON', off: 'OFF' }
|
|
9
|
+
# )) %>
|
|
10
|
+
#
|
|
11
|
+
# @example Theme toggle with icons
|
|
12
|
+
# <%= render(SwapComponent.new(
|
|
13
|
+
# states: {
|
|
14
|
+
# on: helpers.sun_icon('h-6 w-6'),
|
|
15
|
+
# off: helpers.moon_icon('h-6 w-6')
|
|
16
|
+
# },
|
|
17
|
+
# button: true,
|
|
18
|
+
# effect: :rotate
|
|
19
|
+
# )) %>
|
|
20
|
+
class Swap < DaisyUI::BaseComponent
|
|
21
|
+
# Available variants from DaisyUI
|
|
22
|
+
VARIANTS = {
|
|
23
|
+
primary: 'text-primary',
|
|
24
|
+
secondary: 'text-secondary',
|
|
25
|
+
accent: 'text-accent',
|
|
26
|
+
info: 'text-info',
|
|
27
|
+
success: 'text-success',
|
|
28
|
+
warning: 'text-warning',
|
|
29
|
+
error: 'text-error',
|
|
30
|
+
ghost: 'text-base-content',
|
|
31
|
+
neutral: 'text-neutral'
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Available sizes
|
|
35
|
+
SIZES = {
|
|
36
|
+
xs: 'text-xs',
|
|
37
|
+
sm: 'text-sm',
|
|
38
|
+
md: 'text-base',
|
|
39
|
+
lg: 'text-lg'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# Available effects
|
|
43
|
+
EFFECTS = {
|
|
44
|
+
rotate: 'swap-rotate',
|
|
45
|
+
flip: 'swap-flip',
|
|
46
|
+
flip_active: 'swap-flip-active'
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# @param states [Hash] Required hash with :on and :off states content
|
|
50
|
+
# @param value [Boolean] Initial state of the swap
|
|
51
|
+
# @param variant [Symbol] Color variant (primary/secondary/accent/etc)
|
|
52
|
+
# @param size [Symbol] Size variant (xs/sm/md/lg)
|
|
53
|
+
# @param effect [Symbol] Animation effect (rotate/flip/flip-active)
|
|
54
|
+
# @param active [Boolean] When true, gives the swap an active appearance
|
|
55
|
+
# @param button [Boolean] When true, renders as a button
|
|
56
|
+
# @param system_arguments [Hash] Additional HTML attributes
|
|
57
|
+
def initialize(states:, value: false, variant: nil, size: nil, effect: nil, active: false, button: false,
|
|
58
|
+
**system_arguments)
|
|
59
|
+
@states = validate_states!(states)
|
|
60
|
+
@value = ActiveModel::Type::Boolean.new.cast(value)
|
|
61
|
+
@variant = build_argument(variant, VARIANTS, 'variant')
|
|
62
|
+
@size = build_argument(size, SIZES, 'size')
|
|
63
|
+
@effect = build_argument(effect, EFFECTS, 'effect')
|
|
64
|
+
@active = active
|
|
65
|
+
@button = button
|
|
66
|
+
super(**system_arguments)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def call
|
|
70
|
+
tag.label(**label_html_attributes) do
|
|
71
|
+
safe_join([
|
|
72
|
+
tag.input(**input_html_attributes),
|
|
73
|
+
render_state(:on),
|
|
74
|
+
render_state(:off)
|
|
75
|
+
].compact)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def validate_states!(states)
|
|
82
|
+
raise ArgumentError, 'states cannot be nil' if states.nil?
|
|
83
|
+
raise ArgumentError, 'states cannot be empty' if states.empty?
|
|
84
|
+
raise ArgumentError, 'states must have both :on and :off keys' unless states.key?(:on) && states.key?(:off)
|
|
85
|
+
|
|
86
|
+
states
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def input_html_attributes
|
|
90
|
+
{
|
|
91
|
+
type: 'checkbox',
|
|
92
|
+
# class: 'hidden',
|
|
93
|
+
# role: 'switch',
|
|
94
|
+
checked: @value ? 'checked' : nil
|
|
95
|
+
}.compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def label_html_attributes
|
|
99
|
+
{
|
|
100
|
+
class: computed_classes,
|
|
101
|
+
**system_arguments.slice(:aria)
|
|
102
|
+
}.compact
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def computed_classes
|
|
106
|
+
modifiers = ['swap']
|
|
107
|
+
modifiers << @effect if @effect
|
|
108
|
+
modifiers << 'swap-active' if @active
|
|
109
|
+
modifiers << 'btn btn-ghost btn-circle' if @button
|
|
110
|
+
modifiers << @size if @size
|
|
111
|
+
modifiers << @variant if @variant
|
|
112
|
+
|
|
113
|
+
class_names(modifiers, system_arguments[:class])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def render_state(state)
|
|
117
|
+
return unless @states[state]
|
|
118
|
+
|
|
119
|
+
tag.div(@states[state], class: class_names(
|
|
120
|
+
"swap-#{state}",
|
|
121
|
+
{ 'swap-on': state == :on },
|
|
122
|
+
{ 'swap-off': state == :off }
|
|
123
|
+
))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class BaseComponent < ViewComponent::Base
|
|
5
|
+
def initialize(classes: nil, **system_arguments)
|
|
6
|
+
super
|
|
7
|
+
@classes = classes
|
|
8
|
+
@system_arguments = system_arguments
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def build_argument(key, valid_values, attr_name)
|
|
14
|
+
return unless key
|
|
15
|
+
|
|
16
|
+
class_name = valid_values[key.to_sym]
|
|
17
|
+
|
|
18
|
+
return class_name if class_name
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, "Invalid #{attr_name}: #{key}. Must be one of: #{valid_values.keys.join(', ')}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def classes
|
|
24
|
+
class_names(@classes)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :system_arguments
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Renders an accordion component that allows users to show and hide content sections.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# <%= render(AccordionComponent.new) do |component| %>
|
|
8
|
+
# <% component.with_item(title: "Item 1") do %>
|
|
9
|
+
# Content for item 1
|
|
10
|
+
# <% end %>
|
|
11
|
+
# <% component.with_item(title: "Item 2") do %>
|
|
12
|
+
# Content for item 2
|
|
13
|
+
# <% end %>
|
|
14
|
+
# <% end %>
|
|
15
|
+
#
|
|
16
|
+
# @example With arrow indicator
|
|
17
|
+
# <%= render(AccordionComponent.new(indicator: :arrow)) do |component| %>
|
|
18
|
+
# <% component.with_item(title: "Item 1") do %>
|
|
19
|
+
# Content for item 1
|
|
20
|
+
# <% end %>
|
|
21
|
+
# <% end %>
|
|
22
|
+
#
|
|
23
|
+
# @example With plus/minus indicator
|
|
24
|
+
# <%= render(AccordionComponent.new(indicator: :plus)) do |component| %>
|
|
25
|
+
# <% component.with_item(title: "Item 1") do %>
|
|
26
|
+
# Content for item 1
|
|
27
|
+
# <% end %>
|
|
28
|
+
# <% end %>
|
|
29
|
+
#
|
|
30
|
+
# @example Radio group behavior
|
|
31
|
+
# <%= render(AccordionComponent.new(input_type: :radio)) do |component| %>
|
|
32
|
+
# <% component.with_item(title: "Item 1", checked: true) do %>
|
|
33
|
+
# Content for item 1
|
|
34
|
+
# <% end %>
|
|
35
|
+
# <% component.with_item(title: "Item 2") do %>
|
|
36
|
+
# Content for item 2
|
|
37
|
+
# <% end %>
|
|
38
|
+
# <% end %>
|
|
39
|
+
#
|
|
40
|
+
# @example Joined items without gaps
|
|
41
|
+
# <%= render(AccordionComponent.new(join: true)) do |component| %>
|
|
42
|
+
# <% component.with_item(title: "Item 1") do %>
|
|
43
|
+
# Content for item 1
|
|
44
|
+
# <% end %>
|
|
45
|
+
# <% component.with_item(title: "Item 2") do %>
|
|
46
|
+
# Content for item 2
|
|
47
|
+
# <% end %>
|
|
48
|
+
# <% end %>
|
|
49
|
+
#
|
|
50
|
+
# @example Custom colors
|
|
51
|
+
# <%= render(AccordionComponent.new(
|
|
52
|
+
# bg_color: "bg-primary",
|
|
53
|
+
# text_color: "text-primary-content",
|
|
54
|
+
# border_color: "border-primary-focus"
|
|
55
|
+
# )) do |component| %>
|
|
56
|
+
# <% component.with_item(title: "Item 1") do %>
|
|
57
|
+
# Content for item 1
|
|
58
|
+
# <% end %>
|
|
59
|
+
# <% end %>
|
|
60
|
+
class Accordion < BaseComponent
|
|
61
|
+
INDICATORS = %i[arrow plus].freeze
|
|
62
|
+
INPUT_TYPES = %i[radio checkbox].freeze
|
|
63
|
+
|
|
64
|
+
renders_many :items, lambda { |title:, name: nil, checked: false|
|
|
65
|
+
AccordionItem.new(
|
|
66
|
+
title:,
|
|
67
|
+
name: name || @input_name,
|
|
68
|
+
checked:,
|
|
69
|
+
indicator: @indicator,
|
|
70
|
+
input_type: @input_type,
|
|
71
|
+
join: @join,
|
|
72
|
+
bg_color: @join ? nil : @bg_color,
|
|
73
|
+
text_color: @text_color,
|
|
74
|
+
border_color: @border_color,
|
|
75
|
+
padding: @padding,
|
|
76
|
+
**@system_arguments
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
attr_reader :join, :indicator, :input_type, :bg_color, :text_color, :border_color, :padding
|
|
81
|
+
|
|
82
|
+
# @param join [Boolean] Join items together without gaps
|
|
83
|
+
# @param indicator [Symbol] Type of indicator to show (:arrow, :plus)
|
|
84
|
+
# @param input_type [Symbol] Type of input to use (:radio, :checkbox)
|
|
85
|
+
# @param bg_color [String] Background color class
|
|
86
|
+
# @param text_color [String] Text color class
|
|
87
|
+
# @param border_color [String] Border color class
|
|
88
|
+
# @param padding [String] Padding class
|
|
89
|
+
# @param name [String, nil] Name for radio/checkbox inputs (auto-generated if nil)
|
|
90
|
+
# @param system_arguments [Hash] Additional HTML attributes
|
|
91
|
+
def initialize(join: false, indicator: nil, input_type: :checkbox,
|
|
92
|
+
bg_color: nil, text_color: nil, border_color: nil,
|
|
93
|
+
padding: nil, name: nil, **system_arguments)
|
|
94
|
+
@join = join
|
|
95
|
+
@indicator = indicator
|
|
96
|
+
@input_type = input_type
|
|
97
|
+
@bg_color = bg_color || 'bg-base-100'
|
|
98
|
+
@text_color = text_color
|
|
99
|
+
@border_color = border_color || 'border border-base-300'
|
|
100
|
+
@padding = padding
|
|
101
|
+
@input_name = name || "accordion-#{SecureRandom.uuid}"
|
|
102
|
+
super(**system_arguments)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def call
|
|
106
|
+
return safe_join(items || []) unless @join
|
|
107
|
+
|
|
108
|
+
tag.div(**html_attributes) do
|
|
109
|
+
safe_join(items || [])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def html_attributes
|
|
116
|
+
system_arguments.merge(
|
|
117
|
+
class: computed_classes
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def computed_classes
|
|
122
|
+
modifiers = ['join join-vertical']
|
|
123
|
+
modifiers << @bg_color if @bg_color
|
|
124
|
+
|
|
125
|
+
class_names(modifiers, system_arguments[:class])
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Renders a single accordion item within an AccordionComponent.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# <%= render(AccordionItemComponent.new(title: "Item 1")) do %>
|
|
8
|
+
# Content for item 1
|
|
9
|
+
# <% end %>
|
|
10
|
+
#
|
|
11
|
+
# @example With arrow indicator
|
|
12
|
+
# <%= render(AccordionItemComponent.new(
|
|
13
|
+
# title: "Item 1",
|
|
14
|
+
# indicator: :arrow
|
|
15
|
+
# )) do %>
|
|
16
|
+
# Content for item 1
|
|
17
|
+
# <% end %>
|
|
18
|
+
#
|
|
19
|
+
# @example With plus/minus indicator
|
|
20
|
+
# <%= render(AccordionItemComponent.new(
|
|
21
|
+
# title: "Item 1",
|
|
22
|
+
# indicator: :plus
|
|
23
|
+
# )) do %>
|
|
24
|
+
# Content for item 1
|
|
25
|
+
# <% end %>
|
|
26
|
+
#
|
|
27
|
+
# @example As part of a radio group
|
|
28
|
+
# <%= render(AccordionItemComponent.new(
|
|
29
|
+
# title: "Item 1",
|
|
30
|
+
# input_type: :radio,
|
|
31
|
+
# name: "group1",
|
|
32
|
+
# checked: true
|
|
33
|
+
# )) do %>
|
|
34
|
+
# Content for item 1
|
|
35
|
+
# <% end %>
|
|
36
|
+
#
|
|
37
|
+
# @example With custom colors
|
|
38
|
+
# <%= render(AccordionItemComponent.new(
|
|
39
|
+
# title: "Item 1",
|
|
40
|
+
# bg_color: "bg-primary",
|
|
41
|
+
# text_color: "text-primary-content",
|
|
42
|
+
# border_color: "border-primary-focus"
|
|
43
|
+
# )) do %>
|
|
44
|
+
# Content for item 1
|
|
45
|
+
# <% end %>
|
|
46
|
+
class AccordionItem < BaseComponent
|
|
47
|
+
INDICATORS = %i[arrow plus].freeze
|
|
48
|
+
INPUT_TYPES = %i[radio checkbox].freeze
|
|
49
|
+
|
|
50
|
+
# @param title [String] The title text for the accordion item
|
|
51
|
+
# @param name [String] Input name for radio/checkbox behavior
|
|
52
|
+
# @param indicator [Symbol] Type of indicator to show (:arrow, :plus)
|
|
53
|
+
# @param input_type [Symbol] Type of input to use (:radio, :checkbox)
|
|
54
|
+
# @param checked [Boolean] Whether the item is initially expanded
|
|
55
|
+
# @param join [Boolean] Style for joined items
|
|
56
|
+
# @param bg_color [String] Background color class
|
|
57
|
+
# @param text_color [String] Text color class
|
|
58
|
+
# @param border_color [String] Border color class
|
|
59
|
+
# @param padding [String] Padding class
|
|
60
|
+
# @param system_arguments [Hash] Additional HTML attributes
|
|
61
|
+
def initialize(title:, name:, text: nil, checked: false, indicator: nil,
|
|
62
|
+
input_type: :checkbox, join: false, bg_color: nil, text_color: nil,
|
|
63
|
+
border_color: nil, padding: nil, **system_arguments)
|
|
64
|
+
@title = title
|
|
65
|
+
@name = name
|
|
66
|
+
@text = text
|
|
67
|
+
@checked = checked
|
|
68
|
+
@indicator = indicator
|
|
69
|
+
@input_type = input_type
|
|
70
|
+
@join = join
|
|
71
|
+
@bg_color = bg_color
|
|
72
|
+
@text_color = text_color
|
|
73
|
+
@border_color = border_color
|
|
74
|
+
@padding = padding
|
|
75
|
+
super(**system_arguments)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def call
|
|
79
|
+
tag.div(**html_attributes) do
|
|
80
|
+
safe_join([render_input, render_title, render_content].compact)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def html_attributes
|
|
87
|
+
system_arguments.merge(
|
|
88
|
+
class: computed_classes
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def computed_classes
|
|
93
|
+
modifiers = ['collapse']
|
|
94
|
+
modifiers << "collapse-#{@indicator}" if @indicator
|
|
95
|
+
modifiers << 'join-item' if @join
|
|
96
|
+
modifiers << @bg_color if @bg_color
|
|
97
|
+
modifiers << @border_color if @border_color
|
|
98
|
+
modifiers << @padding if @padding
|
|
99
|
+
|
|
100
|
+
class_names(modifiers, system_arguments[:class])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_input
|
|
104
|
+
tag.input(
|
|
105
|
+
type: @input_type,
|
|
106
|
+
name: @name,
|
|
107
|
+
checked: @checked
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_title
|
|
112
|
+
tag.div(class: class_names('collapse-title font-semibold', @text_color)) do
|
|
113
|
+
@title
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_content
|
|
118
|
+
tag.div(class: class_names('collapse-content text-sm', @text_color)) { content || @text }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Avatar component implementing DaisyUI's avatar styles
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage with image
|
|
7
|
+
# <%= render(AvatarComponent.new(img_src: "user.jpg", img_alt: "User")) %>
|
|
8
|
+
#
|
|
9
|
+
# @example With placeholder text
|
|
10
|
+
# <%= render(AvatarComponent.new(placeholder_text: "JD")) %>
|
|
11
|
+
#
|
|
12
|
+
# @example Custom size
|
|
13
|
+
# <%= render(AvatarComponent.new(size: :w32, img_src: "user.jpg")) %>
|
|
14
|
+
#
|
|
15
|
+
# @example Different shapes
|
|
16
|
+
# <%= render(AvatarComponent.new(
|
|
17
|
+
# img_src: "user.jpg",
|
|
18
|
+
# shape: :squircle
|
|
19
|
+
# )) %>
|
|
20
|
+
#
|
|
21
|
+
# @example With online status
|
|
22
|
+
# <%= render(AvatarComponent.new(
|
|
23
|
+
# img_src: "user.jpg",
|
|
24
|
+
# status: :online
|
|
25
|
+
# )) %>
|
|
26
|
+
#
|
|
27
|
+
# @example With custom placeholder content
|
|
28
|
+
# <%= render(AvatarComponent.new) do |c|
|
|
29
|
+
# c.with_placeholder do
|
|
30
|
+
# helpers.user_icon
|
|
31
|
+
# end
|
|
32
|
+
# end %>
|
|
33
|
+
class Avatar < DaisyUI::BaseComponent
|
|
34
|
+
# Available avatar sizes in pixels
|
|
35
|
+
SIZES = {
|
|
36
|
+
w8: 'w-8',
|
|
37
|
+
w12: 'w-12',
|
|
38
|
+
w10: 'w-10',
|
|
39
|
+
w16: 'w-16',
|
|
40
|
+
w20: 'w-20',
|
|
41
|
+
w24: 'w-24',
|
|
42
|
+
w32: 'w-32'
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# Text sizes mapped to avatar sizes
|
|
46
|
+
TEXT_SIZES = {
|
|
47
|
+
w8: 'text-xs',
|
|
48
|
+
w12: nil, # default text size
|
|
49
|
+
w16: 'text-xl',
|
|
50
|
+
w20: 'text-2xl',
|
|
51
|
+
w24: 'text-3xl',
|
|
52
|
+
w32: 'text-4xl'
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
# Available avatar shapes
|
|
56
|
+
SHAPES = {
|
|
57
|
+
circle: 'rounded-full',
|
|
58
|
+
squircle: 'mask mask-squircle',
|
|
59
|
+
hexagon: 'mask mask-hexagon',
|
|
60
|
+
triangle: 'mask mask-triangle'
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
# Available status indicators
|
|
64
|
+
STATUSES = {
|
|
65
|
+
online: 'avatar-online',
|
|
66
|
+
offline: 'avatar-offline'
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# @param size [Symbol] Size of the avatar (w8/w12/w16/w20/w24/w32)
|
|
70
|
+
# @param shape [Symbol] Shape of the avatar (circle/squircle/hexagon/triangle)
|
|
71
|
+
# @param status [Symbol] Status indicator (online/offline)
|
|
72
|
+
# @param img_src [String] URL of the avatar image
|
|
73
|
+
# @param img_alt [String] Alt text for the avatar image
|
|
74
|
+
# @param placeholder_text [String] Text to show when no image is provided
|
|
75
|
+
# @param system_arguments [Hash] Additional HTML attributes
|
|
76
|
+
def initialize(size: nil, shape: nil, status: nil, img_src: nil, img_alt: nil, placeholder_text: nil,
|
|
77
|
+
inner_class: nil,
|
|
78
|
+
**system_arguments)
|
|
79
|
+
@size = build_argument(size, SIZES, 'size')
|
|
80
|
+
@shape = build_argument(shape, SHAPES, 'shape')
|
|
81
|
+
@status = build_argument(status, STATUSES, 'status')
|
|
82
|
+
@img_src = img_src
|
|
83
|
+
@img_alt = img_alt
|
|
84
|
+
@placeholder_text = placeholder_text
|
|
85
|
+
@size_key = size # Keep original size key for text size lookup
|
|
86
|
+
@inner_class = inner_class
|
|
87
|
+
super(**system_arguments)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def call
|
|
91
|
+
tag.div(**html_attributes) do
|
|
92
|
+
tag.div(class: inner_classes) do
|
|
93
|
+
if @img_src.present?
|
|
94
|
+
tag.img(src: @img_src, alt: @img_alt)
|
|
95
|
+
elsif @placeholder_text.present?
|
|
96
|
+
tag.span(@placeholder_text, class: text_size_class)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def text_size_class
|
|
105
|
+
return unless @size_key
|
|
106
|
+
|
|
107
|
+
TEXT_SIZES[@size_key]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def default_classes
|
|
111
|
+
modifiers = ['avatar']
|
|
112
|
+
modifiers << @status
|
|
113
|
+
modifiers << 'avatar-placeholder' if @placeholder_text.present?
|
|
114
|
+
class_names(modifiers, system_arguments[:class])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def inner_classes
|
|
118
|
+
class_names(
|
|
119
|
+
@size,
|
|
120
|
+
@shape || @inner_class || 'rounded',
|
|
121
|
+
{ 'bg-neutral text-neutral-content': @placeholder_text.present? }
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def html_attributes
|
|
126
|
+
attrs = system_arguments.except(:class)
|
|
127
|
+
attrs[:class] = default_classes
|
|
128
|
+
attrs
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|