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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/how-to-write-tests.mdc +67 -0
  3. data/.cursor/rules/preview-file-stucture.mdc +95 -0
  4. data/.cursorrules +64 -0
  5. data/.github/dependabot.yml +12 -0
  6. data/.github/workflows/ci.yml +65 -0
  7. data/.gitignore +13 -0
  8. data/.vscode/launch.json +55 -0
  9. data/.vscode/settings.json +44 -0
  10. data/CHANGELOG.md +39 -0
  11. data/Gemfile +32 -0
  12. data/Gemfile.lock +335 -0
  13. data/MIT-LICENSE +20 -0
  14. data/README.md +64 -0
  15. data/Rakefile +7 -0
  16. data/app/assets/images/daisy_components/.keep +0 -0
  17. data/app/assets/stylesheets/daisy_ui/application.css +15 -0
  18. data/app/components/daisy_ui/actions/button.rb +262 -0
  19. data/app/components/daisy_ui/actions/dropdown.rb +125 -0
  20. data/app/components/daisy_ui/actions/swap.rb +126 -0
  21. data/app/components/daisy_ui/base_component.rb +29 -0
  22. data/app/components/daisy_ui/data_display/accordion.rb +128 -0
  23. data/app/components/daisy_ui/data_display/accordion_item.rb +121 -0
  24. data/app/components/daisy_ui/data_display/avatar.rb +131 -0
  25. data/app/components/daisy_ui/data_display/avatar_group.rb +102 -0
  26. data/app/components/daisy_ui/data_display/badge.rb +126 -0
  27. data/app/components/daisy_ui/data_display/card/actions.rb +94 -0
  28. data/app/components/daisy_ui/data_display/card/body.rb +113 -0
  29. data/app/components/daisy_ui/data_display/card/figure.rb +44 -0
  30. data/app/components/daisy_ui/data_display/card/title.rb +56 -0
  31. data/app/components/daisy_ui/data_display/card.rb +157 -0
  32. data/app/components/daisy_ui/data_display/chat.rb +92 -0
  33. data/app/components/daisy_ui/data_display/chat_bubble/metadata.rb +71 -0
  34. data/app/components/daisy_ui/data_display/chat_bubble.rb +166 -0
  35. data/app/components/daisy_ui/divider.rb +9 -0
  36. data/app/components/daisy_ui/item.rb +20 -0
  37. data/app/components/daisy_ui/title.rb +9 -0
  38. data/app/controllers/concerns/.keep +0 -0
  39. data/app/controllers/daisy_ui/application_controller.rb +6 -0
  40. data/app/helpers/daisy_ui/application_helper.rb +6 -0
  41. data/app/helpers/daisy_ui/icons_helper.rb +296 -0
  42. data/app/views/layouts/daisyui/application.html.erb +17 -0
  43. data/bin/parse_coverage.rb +59 -0
  44. data/bin/rails +57 -0
  45. data/bin/rubocop +10 -0
  46. data/bin/scrape_component +86 -0
  47. data/daisy_components.gemspec +33 -0
  48. data/docs/assets/2025-01_screeshot_1.png +0 -0
  49. data/docs/assets/2025-01_screeshot_2.png +0 -0
  50. data/docs/assets/2025-01_screeshot_3.png +0 -0
  51. data/lib/daisy_components.rb +5 -0
  52. data/lib/daisy_ui/engine.rb +51 -0
  53. data/lib/daisy_ui/version.rb +5 -0
  54. data/lib/daisy_ui.rb +13 -0
  55. data/lib/tasks/daisy_ui_tasks.rake +6 -0
  56. 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