better_ui 0.7.2 → 0.9.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 +4 -4
- data/README.md +86 -29
- data/app/components/better_ui/application_component.rb +34 -0
- data/app/components/better_ui/avatar_component/avatar_component.html.erb +15 -0
- data/app/components/better_ui/avatar_component.rb +227 -0
- data/app/components/better_ui/badge_component/badge_component.html.erb +16 -0
- data/app/components/better_ui/badge_component.rb +114 -0
- data/app/components/better_ui/breadcrumb/breadcrumb_component/breadcrumb_component.html.erb +12 -0
- data/app/components/better_ui/breadcrumb/breadcrumb_component.rb +148 -0
- data/app/components/better_ui/breadcrumb/item_component/item_component.html.erb +11 -0
- data/app/components/better_ui/breadcrumb/item_component.rb +78 -0
- data/app/components/better_ui/card_component.rb +45 -11
- data/app/components/better_ui/concerns/inline_label_styles.rb +137 -0
- data/app/components/better_ui/container_component/container_component.html.erb +3 -0
- data/app/components/better_ui/container_component.rb +143 -0
- data/app/components/better_ui/dialog/alert_component/alert_component.html.erb +61 -0
- data/app/components/better_ui/dialog/alert_component.rb +78 -0
- data/app/components/better_ui/dialog/confirm_component/confirm_component.html.erb +67 -0
- data/app/components/better_ui/dialog/confirm_component.rb +80 -0
- data/app/components/better_ui/dialog/dialog_component/dialog_component.html.erb +44 -0
- data/app/components/better_ui/dialog/dialog_component.rb +81 -0
- data/app/components/better_ui/divider_component/divider_component.html.erb +11 -0
- data/app/components/better_ui/divider_component.rb +344 -0
- data/app/components/better_ui/drawer/sidebar_component.rb +1 -0
- data/app/components/better_ui/dropdown/divider_component/divider_component.html.erb +1 -0
- data/app/components/better_ui/dropdown/divider_component.rb +20 -0
- data/app/components/better_ui/dropdown/dropdown_component/dropdown_component.html.erb +19 -0
- data/app/components/better_ui/dropdown/dropdown_component.rb +108 -0
- data/app/components/better_ui/dropdown/header_component/header_component.html.erb +3 -0
- data/app/components/better_ui/dropdown/header_component.rb +25 -0
- data/app/components/better_ui/dropdown/item_component/item_component.html.erb +7 -0
- data/app/components/better_ui/dropdown/item_component.rb +97 -0
- data/app/components/better_ui/fa_icon_component/fa_icon_component.html.erb +1 -0
- data/app/components/better_ui/fa_icon_component.rb +165 -0
- data/app/components/better_ui/forms/base_component.rb +3 -1
- data/app/components/better_ui/forms/select_component/select_component.html.erb +86 -0
- data/app/components/better_ui/forms/select_component.rb +347 -0
- data/app/components/better_ui/forms/text_input_component.rb +24 -2
- data/app/components/better_ui/heading_component/heading_component.html.erb +11 -0
- data/app/components/better_ui/heading_component.rb +259 -0
- data/app/components/better_ui/link_component/link_component.html.erb +5 -0
- data/app/components/better_ui/link_component.rb +169 -0
- data/app/components/better_ui/progress_component/progress_component.html.erb +15 -0
- data/app/components/better_ui/progress_component.rb +98 -0
- data/app/components/better_ui/spinner_component/spinner_component.html.erb +11 -0
- data/app/components/better_ui/spinner_component.rb +70 -0
- data/app/components/better_ui/table/cell_component/cell_component.html.erb +3 -0
- data/app/components/better_ui/table/cell_component.rb +84 -0
- data/app/components/better_ui/table/column_component.rb +75 -0
- data/app/components/better_ui/table/header_cell_component/header_cell_component.html.erb +18 -0
- data/app/components/better_ui/table/header_cell_component.rb +138 -0
- data/app/components/better_ui/table/header_component/header_component.html.erb +5 -0
- data/app/components/better_ui/table/header_component.rb +37 -0
- data/app/components/better_ui/table/row_component/row_component.html.erb +5 -0
- data/app/components/better_ui/table/row_component.rb +88 -0
- data/app/components/better_ui/table/table_component/table_component.html.erb +90 -0
- data/app/components/better_ui/table/table_component.rb +467 -0
- data/app/components/better_ui/tabs/container_component/container_component.html.erb +40 -0
- data/app/components/better_ui/tabs/container_component.rb +428 -0
- data/app/components/better_ui/tabs/panel_component/panel_component.html.erb +3 -0
- data/app/components/better_ui/tabs/panel_component.rb +105 -0
- data/app/components/better_ui/tabs/tab_component/tab_component.html.erb +9 -0
- data/app/components/better_ui/tabs/tab_component.rb +316 -0
- data/app/components/better_ui/tag_component/tag_component.html.erb +33 -0
- data/app/components/better_ui/tag_component.rb +114 -0
- data/app/components/better_ui/tooltip_component/tooltip_component.html.erb +11 -0
- data/app/components/better_ui/tooltip_component.rb +154 -0
- data/app/form_builders/better_ui/ui_form_builder.rb +90 -0
- data/app/helpers/better_ui/application_helper.rb +575 -0
- data/lib/better_ui/engine.rb +7 -0
- data/lib/better_ui/version.rb +1 -1
- metadata +63 -3
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
# A divider component for visually separating content sections.
|
|
5
|
+
#
|
|
6
|
+
# Renders a horizontal or vertical divider line with optional centered label text.
|
|
7
|
+
# Supports multiple border styles, color variants, sizes, and spacing options.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic horizontal divider
|
|
10
|
+
# <%= render BetterUi::DividerComponent.new %>
|
|
11
|
+
#
|
|
12
|
+
# @example Dashed divider with primary color
|
|
13
|
+
# <%= render BetterUi::DividerComponent.new(style: :dashed, variant: :primary) %>
|
|
14
|
+
#
|
|
15
|
+
# @example Divider with centered label
|
|
16
|
+
# <%= render BetterUi::DividerComponent.new(label: "OR") %>
|
|
17
|
+
#
|
|
18
|
+
# @example Divider with left-aligned label
|
|
19
|
+
# <%= render BetterUi::DividerComponent.new(label: "Section", label_position: :left) %>
|
|
20
|
+
#
|
|
21
|
+
# @example Vertical divider
|
|
22
|
+
# <%= render BetterUi::DividerComponent.new(orientation: :vertical) %>
|
|
23
|
+
#
|
|
24
|
+
# @example Thick divider with large spacing
|
|
25
|
+
# <%= render BetterUi::DividerComponent.new(size: :md, spacing: :xl) %>
|
|
26
|
+
class DividerComponent < ApplicationComponent
|
|
27
|
+
ORIENTATIONS = %i[horizontal vertical].freeze
|
|
28
|
+
STYLES = %i[solid dashed dotted].freeze
|
|
29
|
+
SIZES = %i[xs sm md].freeze
|
|
30
|
+
LABEL_POSITIONS = %i[left center right].freeze
|
|
31
|
+
SPACINGS = %i[xs sm md lg xl].freeze
|
|
32
|
+
|
|
33
|
+
# Initializes a new divider component.
|
|
34
|
+
#
|
|
35
|
+
# @param orientation [Symbol] the divider direction (:horizontal, :vertical)
|
|
36
|
+
# @param style [Symbol] the border style (:solid, :dashed, :dotted)
|
|
37
|
+
# @param variant [Symbol, nil] the color variant (nil for default gray)
|
|
38
|
+
# @param size [Symbol] the border thickness (:xs, :sm, :md)
|
|
39
|
+
# @param label [String, nil] centered text label for horizontal dividers
|
|
40
|
+
# @param label_position [Symbol] label alignment (:left, :center, :right)
|
|
41
|
+
# @param spacing [Symbol] outer margins (:xs, :sm, :md, :lg, :xl)
|
|
42
|
+
# @param container_classes [String, nil] additional CSS classes
|
|
43
|
+
# @param options [Hash] additional HTML attributes
|
|
44
|
+
#
|
|
45
|
+
# @raise [ArgumentError] if any parameter value is invalid
|
|
46
|
+
def initialize(
|
|
47
|
+
orientation: :horizontal,
|
|
48
|
+
style: :solid,
|
|
49
|
+
variant: nil,
|
|
50
|
+
size: :md,
|
|
51
|
+
label: nil,
|
|
52
|
+
label_position: :center,
|
|
53
|
+
spacing: :md,
|
|
54
|
+
container_classes: nil,
|
|
55
|
+
**options
|
|
56
|
+
)
|
|
57
|
+
@orientation = validate_orientation(orientation)
|
|
58
|
+
@style = validate_style(style)
|
|
59
|
+
@variant = validate_variant(variant) if variant
|
|
60
|
+
@size = validate_size(size)
|
|
61
|
+
@label = label
|
|
62
|
+
@label_position = validate_label_position(label_position)
|
|
63
|
+
@spacing = validate_spacing(spacing)
|
|
64
|
+
@container_classes = container_classes
|
|
65
|
+
@options = options
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_reader :orientation, :style, :variant, :size, :label, :label_position,
|
|
71
|
+
:spacing, :container_classes, :options
|
|
72
|
+
|
|
73
|
+
# Returns true if the divider is horizontal.
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
# @api private
|
|
77
|
+
def horizontal?
|
|
78
|
+
@orientation == :horizontal
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns true if the divider has a label and is horizontal.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
# @api private
|
|
85
|
+
def has_label?
|
|
86
|
+
@label.present? && horizontal?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns the complete CSS classes for a horizontal divider without label.
|
|
90
|
+
#
|
|
91
|
+
# @return [String] merged CSS class string
|
|
92
|
+
# @api private
|
|
93
|
+
def component_classes
|
|
94
|
+
if horizontal?
|
|
95
|
+
css_classes([
|
|
96
|
+
border_style_class,
|
|
97
|
+
horizontal_size_class,
|
|
98
|
+
variant_color_class,
|
|
99
|
+
horizontal_spacing_class,
|
|
100
|
+
@container_classes
|
|
101
|
+
].compact)
|
|
102
|
+
else
|
|
103
|
+
css_classes([
|
|
104
|
+
"inline-block",
|
|
105
|
+
"h-full",
|
|
106
|
+
"min-h-[1em]",
|
|
107
|
+
border_style_class,
|
|
108
|
+
vertical_size_class,
|
|
109
|
+
variant_color_class,
|
|
110
|
+
vertical_spacing_class,
|
|
111
|
+
@container_classes
|
|
112
|
+
].compact)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns the CSS classes for the flex wrapper when label is present.
|
|
117
|
+
#
|
|
118
|
+
# @return [String] merged CSS class string
|
|
119
|
+
# @api private
|
|
120
|
+
def wrapper_classes
|
|
121
|
+
css_classes([
|
|
122
|
+
"flex",
|
|
123
|
+
"items-center",
|
|
124
|
+
horizontal_spacing_class,
|
|
125
|
+
@container_classes
|
|
126
|
+
].compact)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns the CSS classes for the line segments in a labeled divider.
|
|
130
|
+
# Returns different classes based on label position and which line (first/second).
|
|
131
|
+
#
|
|
132
|
+
# @param position [Symbol] :first or :second to identify which line segment
|
|
133
|
+
# @return [String] merged CSS class string
|
|
134
|
+
# @api private
|
|
135
|
+
def line_classes(position = :both)
|
|
136
|
+
grow_class = case @label_position
|
|
137
|
+
when :left
|
|
138
|
+
position == :first ? "flex-grow-0 w-8" : "flex-grow"
|
|
139
|
+
when :right
|
|
140
|
+
position == :first ? "flex-grow" : "flex-grow-0 w-8"
|
|
141
|
+
else
|
|
142
|
+
"flex-grow"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
css_classes([
|
|
146
|
+
grow_class,
|
|
147
|
+
"border-t",
|
|
148
|
+
border_style_class,
|
|
149
|
+
horizontal_label_size_class,
|
|
150
|
+
variant_color_class
|
|
151
|
+
].compact)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns the CSS classes for the label text.
|
|
155
|
+
#
|
|
156
|
+
# @return [String] merged CSS class string
|
|
157
|
+
# @api private
|
|
158
|
+
def label_text_classes
|
|
159
|
+
"px-3 text-sm text-grayscale-500 whitespace-nowrap"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns the border style class.
|
|
163
|
+
#
|
|
164
|
+
# @return [String] border style class
|
|
165
|
+
# @api private
|
|
166
|
+
def border_style_class
|
|
167
|
+
case @style
|
|
168
|
+
when :solid then "border-solid"
|
|
169
|
+
when :dashed then "border-dashed"
|
|
170
|
+
when :dotted then "border-dotted"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Returns the border thickness class for horizontal dividers.
|
|
175
|
+
#
|
|
176
|
+
# @return [String] border size class
|
|
177
|
+
# @api private
|
|
178
|
+
def horizontal_size_class
|
|
179
|
+
case @size
|
|
180
|
+
when :xs then "border-t"
|
|
181
|
+
when :sm then "border-t-2"
|
|
182
|
+
when :md then "border-t-4"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the border thickness class for horizontal labeled dividers.
|
|
187
|
+
# Uses border-t classes since the lines are div elements with border-top.
|
|
188
|
+
#
|
|
189
|
+
# @return [String] border size class
|
|
190
|
+
# @api private
|
|
191
|
+
def horizontal_label_size_class
|
|
192
|
+
case @size
|
|
193
|
+
when :xs then ""
|
|
194
|
+
when :sm then "border-t-2"
|
|
195
|
+
when :md then "border-t-4"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns the border thickness class for vertical dividers.
|
|
200
|
+
#
|
|
201
|
+
# @return [String] border size class
|
|
202
|
+
# @api private
|
|
203
|
+
def vertical_size_class
|
|
204
|
+
case @size
|
|
205
|
+
when :xs then "border-l"
|
|
206
|
+
when :sm then "border-l-2"
|
|
207
|
+
when :md then "border-l-4"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns the color class for the border based on variant.
|
|
212
|
+
#
|
|
213
|
+
# @return [String] border color class
|
|
214
|
+
# @api private
|
|
215
|
+
def variant_color_class
|
|
216
|
+
case @variant
|
|
217
|
+
when nil then "border-grayscale-300"
|
|
218
|
+
when :primary then "border-primary-300"
|
|
219
|
+
when :secondary then "border-secondary-300"
|
|
220
|
+
when :accent then "border-accent-300"
|
|
221
|
+
when :success then "border-success-300"
|
|
222
|
+
when :danger then "border-danger-300"
|
|
223
|
+
when :warning then "border-warning-300"
|
|
224
|
+
when :info then "border-info-300"
|
|
225
|
+
when :light then "border-grayscale-200"
|
|
226
|
+
when :dark then "border-grayscale-600"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Returns the spacing (margin) class for horizontal dividers.
|
|
231
|
+
#
|
|
232
|
+
# @return [String] margin class
|
|
233
|
+
# @api private
|
|
234
|
+
def horizontal_spacing_class
|
|
235
|
+
case @spacing
|
|
236
|
+
when :xs then "my-1"
|
|
237
|
+
when :sm then "my-2"
|
|
238
|
+
when :md then "my-4"
|
|
239
|
+
when :lg then "my-6"
|
|
240
|
+
when :xl then "my-8"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns the spacing (margin) class for vertical dividers.
|
|
245
|
+
#
|
|
246
|
+
# @return [String] margin class
|
|
247
|
+
# @api private
|
|
248
|
+
def vertical_spacing_class
|
|
249
|
+
case @spacing
|
|
250
|
+
when :xs then "mx-1"
|
|
251
|
+
when :sm then "mx-2"
|
|
252
|
+
when :md then "mx-4"
|
|
253
|
+
when :lg then "mx-6"
|
|
254
|
+
when :xl then "mx-8"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Returns HTML attributes for the component element.
|
|
259
|
+
#
|
|
260
|
+
# @return [Hash] HTML attributes
|
|
261
|
+
# @api private
|
|
262
|
+
def html_attributes
|
|
263
|
+
@options
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Validates the orientation parameter.
|
|
267
|
+
#
|
|
268
|
+
# @param orientation [Symbol] the orientation to validate
|
|
269
|
+
# @return [Symbol] the validated orientation
|
|
270
|
+
# @raise [ArgumentError] if orientation is invalid
|
|
271
|
+
# @api private
|
|
272
|
+
def validate_orientation(orientation)
|
|
273
|
+
unless ORIENTATIONS.include?(orientation)
|
|
274
|
+
raise ArgumentError, "Invalid orientation: #{orientation}. Must be one of: #{ORIENTATIONS.join(', ')}"
|
|
275
|
+
end
|
|
276
|
+
orientation
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Validates the style parameter.
|
|
280
|
+
#
|
|
281
|
+
# @param style [Symbol] the style to validate
|
|
282
|
+
# @return [Symbol] the validated style
|
|
283
|
+
# @raise [ArgumentError] if style is invalid
|
|
284
|
+
# @api private
|
|
285
|
+
def validate_style(style)
|
|
286
|
+
unless STYLES.include?(style)
|
|
287
|
+
raise ArgumentError, "Invalid style: #{style}. Must be one of: #{STYLES.join(', ')}"
|
|
288
|
+
end
|
|
289
|
+
style
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Validates the variant parameter.
|
|
293
|
+
#
|
|
294
|
+
# @param variant [Symbol] the variant to validate
|
|
295
|
+
# @return [Symbol] the validated variant
|
|
296
|
+
# @raise [ArgumentError] if variant is invalid
|
|
297
|
+
# @api private
|
|
298
|
+
def validate_variant(variant)
|
|
299
|
+
unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
|
|
300
|
+
raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
|
|
301
|
+
end
|
|
302
|
+
variant
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Validates the size parameter.
|
|
306
|
+
#
|
|
307
|
+
# @param size [Symbol] the size to validate
|
|
308
|
+
# @return [Symbol] the validated size
|
|
309
|
+
# @raise [ArgumentError] if size is invalid
|
|
310
|
+
# @api private
|
|
311
|
+
def validate_size(size)
|
|
312
|
+
unless SIZES.include?(size)
|
|
313
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.join(', ')}"
|
|
314
|
+
end
|
|
315
|
+
size
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Validates the label_position parameter.
|
|
319
|
+
#
|
|
320
|
+
# @param label_position [Symbol] the label position to validate
|
|
321
|
+
# @return [Symbol] the validated label position
|
|
322
|
+
# @raise [ArgumentError] if label_position is invalid
|
|
323
|
+
# @api private
|
|
324
|
+
def validate_label_position(label_position)
|
|
325
|
+
unless LABEL_POSITIONS.include?(label_position)
|
|
326
|
+
raise ArgumentError, "Invalid label_position: #{label_position}. Must be one of: #{LABEL_POSITIONS.join(', ')}"
|
|
327
|
+
end
|
|
328
|
+
label_position
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Validates the spacing parameter.
|
|
332
|
+
#
|
|
333
|
+
# @param spacing [Symbol] the spacing to validate
|
|
334
|
+
# @return [Symbol] the validated spacing
|
|
335
|
+
# @raise [ArgumentError] if spacing is invalid
|
|
336
|
+
# @api private
|
|
337
|
+
def validate_spacing(spacing)
|
|
338
|
+
unless SPACINGS.include?(spacing)
|
|
339
|
+
raise ArgumentError, "Invalid spacing: #{spacing}. Must be one of: #{SPACINGS.join(', ')}"
|
|
340
|
+
end
|
|
341
|
+
spacing
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div role="separator" class="<%= component_classes %>"></div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Dropdown
|
|
5
|
+
class DividerComponent < ApplicationComponent
|
|
6
|
+
def initialize(container_classes: nil)
|
|
7
|
+
@container_classes = container_classes
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def component_classes
|
|
13
|
+
css_classes(
|
|
14
|
+
"border-t border-grayscale-200 my-1",
|
|
15
|
+
@container_classes
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%= tag.div(class: root_classes, data: controller_data) do %>
|
|
2
|
+
<% if trigger? %>
|
|
3
|
+
<div aria-haspopup="true"
|
|
4
|
+
aria-expanded="false"
|
|
5
|
+
data-action="click->better-ui--dropdown--dropdown#toggle"
|
|
6
|
+
data-better-ui--dropdown--dropdown-target="trigger">
|
|
7
|
+
<%= trigger %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<div role="menu"
|
|
12
|
+
hidden
|
|
13
|
+
data-better-ui--dropdown--dropdown-target="menu"
|
|
14
|
+
class="<%= menu_classes %>">
|
|
15
|
+
<% items.each do |item| %>
|
|
16
|
+
<%= item %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Dropdown
|
|
5
|
+
class DropdownComponent < ApplicationComponent
|
|
6
|
+
SIZES = {
|
|
7
|
+
sm: "w-40",
|
|
8
|
+
md: "w-56",
|
|
9
|
+
lg: "w-72"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
PLACEMENTS = {
|
|
13
|
+
bottom_start: "top-full left-0 mt-1",
|
|
14
|
+
bottom_end: "top-full right-0 mt-1",
|
|
15
|
+
top_start: "bottom-full left-0 mb-1",
|
|
16
|
+
top_end: "bottom-full right-0 mb-1"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
renders_one :trigger
|
|
20
|
+
renders_many :items, types: {
|
|
21
|
+
item: {
|
|
22
|
+
renders: lambda { |**args, &block|
|
|
23
|
+
ItemComponent.new(**args, &block)
|
|
24
|
+
},
|
|
25
|
+
as: :item
|
|
26
|
+
},
|
|
27
|
+
divider: {
|
|
28
|
+
renders: lambda { |**args|
|
|
29
|
+
DividerComponent.new(**args)
|
|
30
|
+
},
|
|
31
|
+
as: :divider
|
|
32
|
+
},
|
|
33
|
+
header: {
|
|
34
|
+
renders: lambda { |**args|
|
|
35
|
+
HeaderComponent.new(**args)
|
|
36
|
+
},
|
|
37
|
+
as: :header
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def initialize(
|
|
42
|
+
variant: :default,
|
|
43
|
+
size: :md,
|
|
44
|
+
placement: :bottom_start,
|
|
45
|
+
shadow: :lg,
|
|
46
|
+
auto_close: true,
|
|
47
|
+
close_on_item_click: true,
|
|
48
|
+
container_classes: nil,
|
|
49
|
+
menu_classes: nil
|
|
50
|
+
)
|
|
51
|
+
@variant = variant
|
|
52
|
+
@size = validate_size(size || :md)
|
|
53
|
+
@placement = validate_placement(placement || :bottom_start)
|
|
54
|
+
@shadow = normalize_shadow(shadow, default: :lg)
|
|
55
|
+
@auto_close = auto_close
|
|
56
|
+
@close_on_item_click = close_on_item_click
|
|
57
|
+
@container_classes = container_classes
|
|
58
|
+
@menu_classes = menu_classes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def controller_data
|
|
64
|
+
{
|
|
65
|
+
controller: "better-ui--dropdown--dropdown",
|
|
66
|
+
"better-ui--dropdown--dropdown-auto-close-value": @auto_close,
|
|
67
|
+
"better-ui--dropdown--dropdown-close-on-item-click-value": @close_on_item_click
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def root_classes
|
|
72
|
+
css_classes(
|
|
73
|
+
"relative inline-block",
|
|
74
|
+
@container_classes
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def menu_classes
|
|
79
|
+
css_classes(
|
|
80
|
+
"absolute z-50",
|
|
81
|
+
"bg-white rounded-md ring-1 ring-grayscale-200 ring-opacity-5",
|
|
82
|
+
"py-1 overflow-hidden",
|
|
83
|
+
"focus:outline-none",
|
|
84
|
+
"transition-all duration-150 ease-out",
|
|
85
|
+
"opacity-0 scale-95",
|
|
86
|
+
SIZES[@size],
|
|
87
|
+
PLACEMENTS[@placement],
|
|
88
|
+
SHADOWS[@shadow],
|
|
89
|
+
@menu_classes
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_size(size)
|
|
94
|
+
unless SIZES.key?(size)
|
|
95
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(', ')}"
|
|
96
|
+
end
|
|
97
|
+
size
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_placement(placement)
|
|
101
|
+
unless PLACEMENTS.key?(placement)
|
|
102
|
+
raise ArgumentError, "Invalid placement: #{placement}. Must be one of: #{PLACEMENTS.keys.join(', ')}"
|
|
103
|
+
end
|
|
104
|
+
placement
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Dropdown
|
|
5
|
+
class HeaderComponent < ApplicationComponent
|
|
6
|
+
def initialize(text: nil, container_classes: nil)
|
|
7
|
+
@text = text
|
|
8
|
+
@container_classes = container_classes
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def display_text
|
|
14
|
+
content.present? ? content : @text
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def component_classes
|
|
18
|
+
css_classes(
|
|
19
|
+
"px-3 py-2 text-xs font-semibold uppercase tracking-wider text-grayscale-500",
|
|
20
|
+
@container_classes
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Dropdown
|
|
5
|
+
class ItemComponent < ApplicationComponent
|
|
6
|
+
ITEM_VARIANTS = {
|
|
7
|
+
default: "text-grayscale-700",
|
|
8
|
+
danger: "text-danger-600"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
renders_one :icon
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
href: nil,
|
|
15
|
+
disabled: false,
|
|
16
|
+
method: nil,
|
|
17
|
+
active: false,
|
|
18
|
+
variant: :default,
|
|
19
|
+
container_classes: nil
|
|
20
|
+
)
|
|
21
|
+
@href = href
|
|
22
|
+
@disabled = disabled
|
|
23
|
+
@method = method
|
|
24
|
+
@active = active
|
|
25
|
+
@variant = validate_variant(variant)
|
|
26
|
+
@container_classes = container_classes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :href, :disabled, :method, :active, :variant, :container_classes
|
|
32
|
+
|
|
33
|
+
def link?
|
|
34
|
+
@href.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def component_classes
|
|
38
|
+
css_classes(
|
|
39
|
+
"flex items-center gap-2 w-full text-left px-3 py-2 text-sm",
|
|
40
|
+
"transition-colors duration-150",
|
|
41
|
+
ITEM_VARIANTS[@variant],
|
|
42
|
+
hover_classes,
|
|
43
|
+
active_classes,
|
|
44
|
+
disabled_classes,
|
|
45
|
+
@container_classes
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def hover_classes
|
|
50
|
+
return nil if @disabled
|
|
51
|
+
|
|
52
|
+
case @variant
|
|
53
|
+
when :danger then "hover:bg-danger-50 hover:text-danger-700"
|
|
54
|
+
else "hover:bg-grayscale-100 hover:text-grayscale-900"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def active_classes
|
|
59
|
+
@active ? "bg-grayscale-100" : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def disabled_classes
|
|
63
|
+
@disabled ? "opacity-50 cursor-not-allowed pointer-events-none" : "cursor-pointer"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def component_attributes
|
|
67
|
+
base = {
|
|
68
|
+
role: "menuitem",
|
|
69
|
+
tabindex: "-1",
|
|
70
|
+
class: component_classes,
|
|
71
|
+
data: {
|
|
72
|
+
"better-ui--dropdown--dropdown-target": "item"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if link?
|
|
77
|
+
base[:href] = @href
|
|
78
|
+
base[:data]["turbo-method"] = @method if @method
|
|
79
|
+
base[:"aria-disabled"] = "true" if @disabled
|
|
80
|
+
else
|
|
81
|
+
base[:type] = "button"
|
|
82
|
+
base[:disabled] = true if @disabled
|
|
83
|
+
base[:"aria-disabled"] = "true" if @disabled
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
base
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_variant(variant)
|
|
90
|
+
unless ITEM_VARIANTS.key?(variant)
|
|
91
|
+
raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{ITEM_VARIANTS.keys.join(', ')}"
|
|
92
|
+
end
|
|
93
|
+
variant
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<i class="<%= component_classes %>" aria-hidden="true"></i>
|