wilday_ui 0.3.0 → 0.4.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.
@@ -123,3 +123,248 @@
123
123
  .w-button.w-button-loading > *:not(.w-button-spinner) {
124
124
  display: none;
125
125
  }
126
+
127
+ /* Dropdown Wrapper */
128
+ .w-button-wrapper {
129
+ position: relative !important;
130
+ display: inline-block !important;
131
+ padding: 0;
132
+ margin: 0;
133
+ overflow: visible !important;
134
+ }
135
+
136
+ /* Dropdown Button */
137
+ .w-button-dropdown {
138
+ display: inline-flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ gap: 0.5rem;
142
+ }
143
+
144
+ /* Dropdown Arrow */
145
+ .w-button-dropdown-arrow {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ margin-left: 0.5rem;
149
+ transition: transform 0.2s ease;
150
+ }
151
+
152
+ .w-button-dropdown-arrow:not(i) {
153
+ width: 0.5em;
154
+ height: 0.5em;
155
+ border: 0.15em solid currentColor;
156
+ border-top: 0;
157
+ border-left: 0;
158
+ transform: translateY(-25%) rotate(45deg);
159
+ }
160
+
161
+ /* Arrow Active State */
162
+ .w-button.active .w-button-dropdown-arrow:not(i) {
163
+ transform: translateY(25%) rotate(225deg);
164
+ }
165
+
166
+ .w-button.active .w-button-dropdown-arrow i {
167
+ transform: rotate(180deg);
168
+ }
169
+
170
+ /* Reset Arrow State */
171
+ .w-button-dropdown-arrow i:not(.active) {
172
+ transform: rotate(0);
173
+ }
174
+
175
+ /* Dropdown Menu */
176
+ .w-button-dropdown-menu {
177
+ position: absolute;
178
+ display: none;
179
+ width: max-content;
180
+ min-width: 100%;
181
+ background-color: white;
182
+ border: 1px solid rgba(0, 0, 0, 0.1);
183
+ border-radius: 0.5rem;
184
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
185
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
186
+ z-index: 1000;
187
+ overflow: visible;
188
+ margin: 0 !important;
189
+ padding: 0 !important;
190
+ }
191
+
192
+ .w-button-dropdown-menu.show {
193
+ display: block;
194
+ animation: fadeIn 0.2s ease-in-out;
195
+ }
196
+
197
+ /* Dropdown Items */
198
+ .w-button-dropdown-item {
199
+ display: block;
200
+ width: 100%;
201
+ padding: 0.5rem 1rem;
202
+ font-size: inherit;
203
+ line-height: inherit;
204
+ text-align: left;
205
+ white-space: nowrap;
206
+ background-color: transparent;
207
+ color: #374151;
208
+ text-decoration: none;
209
+ transition: all 0.15s ease-in-out;
210
+ box-sizing: border-box;
211
+ cursor: pointer;
212
+ }
213
+
214
+ .w-button-dropdown-item:hover,
215
+ .w-button-dropdown-item:focus {
216
+ background-color: #f3f4f6;
217
+ color: #111827;
218
+ outline: none;
219
+ }
220
+
221
+ /* Dropdown Divider */
222
+ .w-button-dropdown-divider {
223
+ height: 0;
224
+ margin: 0.5rem 0;
225
+ overflow: hidden;
226
+ border-top: 1px solid #e9ecef;
227
+ }
228
+
229
+ /* Animation */
230
+ @keyframes fadeIn {
231
+ from {
232
+ opacity: 0;
233
+ transform: translate(var(--translate-x, 0), var(--translate-y, 0));
234
+ }
235
+ to {
236
+ opacity: 1;
237
+ transform: translate(0, 0);
238
+ }
239
+ }
240
+
241
+ /* Position Variations */
242
+ .w-button-dropdown-menu[data-position="top"] {
243
+ bottom: 100%;
244
+ left: 0;
245
+ margin-bottom: 0.125rem;
246
+ --translate-y: -10px;
247
+ }
248
+
249
+ .w-button-dropdown-menu[data-position="bottom"] {
250
+ top: 100%;
251
+ left: 0;
252
+ margin-top: 0.125rem;
253
+ --translate-y: 10px;
254
+ }
255
+
256
+ .w-button-dropdown-menu[data-position="right"] {
257
+ left: 100%;
258
+ margin-left: 0.125rem;
259
+ top: 0;
260
+ --translate-x: 10px;
261
+ }
262
+
263
+ .w-button-dropdown-menu[data-position="left"] {
264
+ right: 100%;
265
+ margin-right: 0.125rem;
266
+ left: auto;
267
+ top: 0;
268
+ --translate-x: -10px;
269
+ }
270
+
271
+ .w-button-dropdown-menu[data-position="top"][data-align="start"],
272
+ .w-button-dropdown-menu[data-position="bottom"][data-align="start"] {
273
+ left: 0 !important;
274
+ right: auto !important;
275
+ }
276
+
277
+ .w-button-dropdown-menu[data-position="top"][data-align="end"],
278
+ .w-button-dropdown-menu[data-position="bottom"][data-align="end"] {
279
+ left: auto !important;
280
+ right: 0 !important;
281
+ }
282
+
283
+ .w-button-dropdown-menu[data-position="top"][data-align="center"],
284
+ .w-button-dropdown-menu[data-position="bottom"][data-align="center"] {
285
+ left: 50% !important;
286
+ transform: translateX(-50%) !important;
287
+ }
288
+
289
+ .w-button-dropdown-menu[data-position="top"][data-align="center"].show,
290
+ .w-button-dropdown-menu[data-position="bottom"][data-align="center"].show {
291
+ transform: translateX(-50%) translateY(0);
292
+ }
293
+
294
+ /* Submenu */
295
+ .w-button-dropdown-parent {
296
+ position: relative;
297
+ cursor: pointer;
298
+ }
299
+
300
+ .w-button-dropdown-parent .w-button-dropdown-menu {
301
+ position: absolute;
302
+ top: 0;
303
+ left: 100%;
304
+ z-index: 1100;
305
+ opacity: 0;
306
+ visibility: hidden;
307
+ transform: translateX(-10px);
308
+ transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
309
+ min-width: 10rem;
310
+ background-color: white;
311
+ padding: 0.5rem 0;
312
+ border: 1px solid rgba(0, 0, 0, 0.1);
313
+ border-radius: 0.375rem;
314
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
315
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
316
+ }
317
+
318
+ .w-button-dropdown-parent .w-button-dropdown-menu.show {
319
+ opacity: 1;
320
+ visibility: visible;
321
+ transform: translateX(0);
322
+ }
323
+
324
+ .w-button-dropdown-parent .w-button-dropdown-menu[data-position="left"] {
325
+ left: auto;
326
+ right: 100%;
327
+ }
328
+
329
+ /* Nested Submenu Styling */
330
+ .w-button-dropdown-menu .w-button-dropdown-parent .w-button-dropdown-menu {
331
+ top: 0;
332
+ left: 100%;
333
+ z-index: 1200;
334
+ opacity: 0;
335
+ visibility: hidden;
336
+ transform: translateX(-10px);
337
+ transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
338
+ }
339
+
340
+ .w-button-dropdown-menu .w-button-dropdown-parent .w-button-dropdown-menu.show {
341
+ opacity: 1;
342
+ visibility: visible;
343
+ transform: translateX(0);
344
+ pointer-events: auto;
345
+ }
346
+
347
+ /* Arrow State for Submenus */
348
+ .w-button-dropdown-parent .w-button-dropdown-arrow:not(i).active {
349
+ transform: translateY(25%) rotate(225deg);
350
+ }
351
+
352
+ .w-button-dropdown-arrow i.active {
353
+ transform: rotate(180deg);
354
+ }
355
+
356
+ /* Mobile responsiveness */
357
+ @media (max-width: 768px) {
358
+ .w-button-dropdown-menu[data-position="right"],
359
+ .w-button-dropdown-menu[data-position="left"] {
360
+ position: absolute;
361
+ left: 0;
362
+ right: 0;
363
+ top: 100%;
364
+ margin-top: 0.5rem;
365
+ margin-left: 0;
366
+ margin-right: 0;
367
+ --translate-x: 0;
368
+ --translate-y: 10px;
369
+ }
370
+ }
@@ -1,6 +1,25 @@
1
1
  module WildayUi
2
2
  module Components
3
3
  module ButtonHelper
4
+ BUTTON_FEATURES = {
5
+ dropdown: {
6
+ wrapper_required: true,
7
+ stimulus_controller: "dropdown",
8
+ default_stimulus_action: "click->dropdown#toggle"
9
+ },
10
+ loading: {
11
+ wrapper_required: false,
12
+ stimulus_controller: "button",
13
+ default_stimulus_action: "click->button#toggleLoading"
14
+ }
15
+ # Add more features here as needed
16
+ # tooltip: {
17
+ # wrapper_required: true,
18
+ # stimulus_controller: "tooltip",
19
+ # default_stimulus_action: "mouseenter->tooltip#show mouseleave->tooltip#hide"
20
+ # }
21
+ }.freeze
22
+
4
23
  def w_button(
5
24
  content,
6
25
  variant: :primary,
@@ -13,47 +32,224 @@ module WildayUi
13
32
  disabled: false,
14
33
  additional_classes: "",
15
34
  use_default_controller: true,
16
- href: nil, # href parameter
17
- method: :get, # method parameter
18
- target: nil, # target parameter
35
+ href: nil,
36
+ method: :get,
37
+ target: nil,
38
+ dropdown: false,
39
+ dropdown_items: nil,
40
+ dropdown_icon: nil,
19
41
  **options
20
42
  )
21
43
  content_for(:head) { stylesheet_link_tag "wilday_ui/button", media: "all" }
22
- variant_class = {
44
+
45
+ options[:data] ||= {}
46
+ wrapper_data = {}
47
+ wrapper_options = nil
48
+
49
+ variant_class = get_variant_class(variant)
50
+ size_class = get_size_class(size)
51
+ radius_class = get_radius_class(radius)
52
+
53
+ # Setup features that require Stimulus controllers
54
+ active_features = determine_active_features(loading, dropdown, loading_text, use_default_controller)
55
+
56
+ Rails.logger.debug "Active Features: #{active_features.inspect}"
57
+ Rails.logger.debug "Options before setup: #{options.inspect}"
58
+
59
+ setup_features(active_features, options, use_default_controller, loading_text)
60
+
61
+ Rails.logger.debug "Options after setup: #{options.inspect}"
62
+
63
+ setup_link_options(options, href, target, method)
64
+
65
+ if dropdown
66
+ setup_dropdown_options(
67
+ options,
68
+ additional_classes,
69
+ dropdown,
70
+ dropdown_items,
71
+ wrapper_data
72
+ )
73
+ end
74
+
75
+ # Setup wrapper options if any feature requires it
76
+ wrapper_options = setup_wrapper_options(
77
+ active_features,
78
+ additional_classes,
79
+ wrapper_data
80
+ )
81
+
82
+ render_button(
83
+ content,
84
+ variant_class,
85
+ size_class,
86
+ radius_class,
87
+ icon,
88
+ icon_position,
89
+ loading,
90
+ loading_text,
91
+ additional_classes,
92
+ disabled,
93
+ options,
94
+ href,
95
+ dropdown,
96
+ dropdown_items,
97
+ dropdown_icon,
98
+ wrapper_options
99
+ )
100
+ end
101
+
102
+ private
103
+
104
+ def get_variant_class(variant)
105
+ {
23
106
  primary: "w-button-primary",
24
107
  secondary: "w-button-secondary",
25
108
  outline: "w-button-outline"
26
109
  }[variant] || "w-button-primary"
110
+ end
27
111
 
28
- size_class = {
112
+ def get_size_class(size)
113
+ {
29
114
  small: "w-button-small",
30
115
  medium: "w-button-medium",
31
116
  large: "w-button-large"
32
117
  }[size] || "w-button-medium"
118
+ end
33
119
 
34
- radius_class = {
120
+ def get_radius_class(radius)
121
+ {
35
122
  rounded: "w-button-rounded",
36
123
  pill: "w-button-pill",
37
124
  square: "w-button-square"
38
125
  }[radius] || "w-button-rounded"
126
+ end
127
+
128
+ def determine_active_features(loading, dropdown, loading_text = nil, use_default_controller = true)
129
+ features = {}
130
+ features[:loading] = true if (loading || loading_text.present?) && use_default_controller
131
+ features[:dropdown] = true if dropdown && use_default_controller
132
+ features
133
+ end
134
+
135
+ def setup_features(active_features, options, use_default_controller, loading_text)
136
+ return unless use_default_controller && active_features.any?
137
+
138
+ active_features.each do |feature, _value|
139
+ feature_config = BUTTON_FEATURES[feature]
140
+ next unless feature_config
141
+
142
+ # Skip adding controller for dropdown feature since it's handled by wrapper
143
+ if feature_config[:wrapper_required]
144
+ # For dropdown, only set the action, not the controller
145
+ options[:data][:action] = feature_config[:default_stimulus_action]
146
+ else
147
+ setup_feature_controller(options, feature_config, loading_text)
148
+ end
149
+ end
150
+ end
151
+
152
+ def setup_feature_controller(options, feature_config, loading_text)
153
+ options[:data] ||= {}
154
+
155
+ existing_controller = options.dig(:data, :controller)
156
+ existing_action = options.dig(:data, :action)
39
157
 
40
- # Only add default Stimulus controller if requested and no custom controller specified
41
- if use_default_controller && !options.dig(:data, :controller) && loading_text.present?
42
- options[:data] ||= {}
43
- options[:data][:controller] = "button"
44
- options[:data][:action] = "click->button#toggleLoading"
158
+ options[:data][:controller] = [
159
+ existing_controller,
160
+ feature_config[:stimulus_controller]
161
+ ].compact.join(" ")
162
+
163
+ options[:data][:action] = [
164
+ existing_action,
165
+ feature_config[:default_stimulus_action]
166
+ ].compact.join(" ")
167
+
168
+ # Add feature-specific data attributes
169
+ if feature_config[:stimulus_controller] == "button" && loading_text.present?
45
170
  options[:data][:button_loading_text] = loading_text
46
171
  end
172
+ end
173
+
174
+ def setup_link_options(options, href, target, method)
175
+ return unless href.present?
176
+
177
+ options[:href] = href
178
+ options[:target] = target if target
179
+ options[:data][:method] = method if method != :get
180
+ options[:rel] = "noopener noreferrer" if target == "_blank"
181
+ end
182
+
183
+ def setup_dropdown_options(options, additional_classes, dropdown, dropdown_items, wrapper_data)
184
+ additional_classes = "#{additional_classes} w-button-dropdown"
185
+
186
+ options[:data][:dropdown_target] = "button"
187
+
188
+ wrapper_data.merge!(
189
+ controller: "dropdown",
190
+ dropdown_id: "dropdown-#{SecureRandom.hex(4)}"
191
+ )
192
+
193
+ if dropdown.is_a?(Hash)
194
+ wrapper_data.merge!(
195
+ dropdown_position_value: dropdown[:position]&.to_s || "bottom",
196
+ dropdown_align_value: dropdown[:align]&.to_s || "start",
197
+ dropdown_trigger_value: dropdown[:trigger]&.to_s || "click"
198
+ )
199
+ else
200
+ wrapper_data.merge!(
201
+ dropdown_position_value: "bottom",
202
+ dropdown_align_value: "start",
203
+ dropdown_trigger_value: "click"
204
+ )
205
+ end
206
+
207
+ normalize_dropdown_items(dropdown_items)
208
+ end
47
209
 
48
- # Link specific options
49
- if href.present?
50
- options[:href] = href
51
- options[:target] = target if target
52
- options[:data] ||= {}
53
- options[:data][:method] = method if method != :get
54
- options[:rel] = "noopener noreferrer" if target == "_blank"
210
+ def setup_wrapper_options(active_features, additional_classes, wrapper_data)
211
+ return nil unless needs_wrapper?(active_features)
212
+
213
+ {
214
+ class: [ "w-button-wrapper", additional_classes ].compact.join(" "),
215
+ data: wrapper_data,
216
+ role: active_features[:dropdown] ? "menu" : nil
217
+ }.compact
218
+ end
219
+
220
+ def needs_wrapper?(active_features)
221
+ active_features.any? { |feature, _| BUTTON_FEATURES[feature][:wrapper_required] }
222
+ end
223
+
224
+ def normalize_dropdown_items(items, parent_id = nil)
225
+ return [] unless items
226
+
227
+ items.map.with_index do |item, index|
228
+ item_id = generate_item_id(parent_id, index)
229
+
230
+ normalized_item = {
231
+ id: item_id,
232
+ text: item[:text],
233
+ href: item[:href],
234
+ divider: item[:divider]
235
+ }
236
+
237
+ if item[:children].present?
238
+ normalized_item[:children] = normalize_dropdown_items(item[:children], item_id)
239
+ end
240
+
241
+ normalized_item.compact
55
242
  end
243
+ end
244
+
245
+ def generate_item_id(parent_id, index)
246
+ base = parent_id ? "#{parent_id}-" : "dropdown-item-"
247
+ "#{base}#{index}"
248
+ end
56
249
 
250
+ def render_button(content, variant_class, size_class, radius_class, icon, icon_position,
251
+ loading, loading_text, additional_classes, disabled, options, href,
252
+ dropdown, dropdown_items, dropdown_icon, wrapper_options)
57
253
  render partial: "wilday_ui/button",
58
254
  locals: {
59
255
  content: content,
@@ -67,7 +263,11 @@ module WildayUi
67
263
  additional_classes: additional_classes,
68
264
  disabled: disabled,
69
265
  html_options: options,
70
- href: href
266
+ href: href,
267
+ dropdown: dropdown,
268
+ dropdown_items: dropdown_items,
269
+ dropdown_icon: dropdown_icon,
270
+ wrapper_options: wrapper_options
71
271
  }
72
272
  end
73
273
  end