wilday_ui 0.2.3 → 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.
@@ -90,3 +90,281 @@
90
90
  display: inline-flex;
91
91
  align-items: center;
92
92
  }
93
+
94
+ /* Loading ButtonSpinner */
95
+ .w-button-spinner {
96
+ width: 1rem;
97
+ height: 1rem;
98
+ border: 2px solid transparent;
99
+ border-top-color: #ffffff;
100
+ border-radius: 50%;
101
+ animation: w-button-spin 0.6s linear infinite;
102
+ }
103
+
104
+ @keyframes w-button-spin {
105
+ from {
106
+ transform: rotate(0deg);
107
+ }
108
+ to {
109
+ transform: rotate(360deg);
110
+ }
111
+ }
112
+
113
+ /* Loading Button State */
114
+ .w-button.w-button-loading {
115
+ pointer-events: none;
116
+ opacity: 0.8;
117
+ }
118
+
119
+ .w-button.w-button-loading .w-button-spinner {
120
+ display: inline-block;
121
+ }
122
+
123
+ .w-button.w-button-loading > *:not(.w-button-spinner) {
124
+ display: none;
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,
@@ -8,29 +27,229 @@ module WildayUi
8
27
  radius: :rounded,
9
28
  icon: nil,
10
29
  icon_position: :left,
30
+ loading: false,
31
+ loading_text: nil,
11
32
  disabled: false,
12
33
  additional_classes: "",
34
+ use_default_controller: true,
35
+ href: nil,
36
+ method: :get,
37
+ target: nil,
38
+ dropdown: false,
39
+ dropdown_items: nil,
40
+ dropdown_icon: nil,
13
41
  **options
14
42
  )
15
43
  content_for(:head) { stylesheet_link_tag "wilday_ui/button", media: "all" }
16
- 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
+ {
17
106
  primary: "w-button-primary",
18
107
  secondary: "w-button-secondary",
19
108
  outline: "w-button-outline"
20
109
  }[variant] || "w-button-primary"
110
+ end
21
111
 
22
- size_class = {
112
+ def get_size_class(size)
113
+ {
23
114
  small: "w-button-small",
24
115
  medium: "w-button-medium",
25
116
  large: "w-button-large"
26
117
  }[size] || "w-button-medium"
118
+ end
27
119
 
28
- radius_class = {
120
+ def get_radius_class(radius)
121
+ {
29
122
  rounded: "w-button-rounded",
30
123
  pill: "w-button-pill",
31
124
  square: "w-button-square"
32
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)
157
+
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?
170
+ options[:data][:button_loading_text] = loading_text
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
209
+
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
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
33
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)
34
253
  render partial: "wilday_ui/button",
35
254
  locals: {
36
255
  content: content,
@@ -39,9 +258,16 @@ module WildayUi
39
258
  radius_class: radius_class,
40
259
  icon: icon,
41
260
  icon_position: icon_position,
261
+ loading: loading,
262
+ loading_text: loading_text,
42
263
  additional_classes: additional_classes,
43
264
  disabled: disabled,
44
- attributes: options.map { |key, value| "#{key}='#{value}'" }.join(" ")
265
+ html_options: options,
266
+ href: href,
267
+ dropdown: dropdown,
268
+ dropdown_items: dropdown_items,
269
+ dropdown_icon: dropdown_icon,
270
+ wrapper_options: wrapper_options
45
271
  }
46
272
  end
47
273
  end
@@ -1,8 +1,10 @@
1
1
  document.addEventListener("DOMContentLoaded", () => {
2
+ console.log("Button component loaded heah");
2
3
  document.querySelectorAll(".w-button").forEach((button) => {
3
4
  button.addEventListener("click", (event) => {
4
5
  if (button.disabled) {
5
6
  event.preventDefault();
7
+ return;
6
8
  }
7
9
  });
8
10
  });
@@ -0,0 +1,56 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class ButtonController extends Controller {
4
+ static values = {
5
+ loadingDuration: { type: Number, default: 2000 },
6
+ };
7
+
8
+ initialize() {}
9
+
10
+ connect() {}
11
+
12
+ toggleLoading(event) {
13
+ const button = this.element;
14
+ const isLink = button.tagName.toLowerCase() === "a";
15
+ const hasLoadingText = button.dataset.buttonLoadingText;
16
+
17
+ // Always prevent default if we have loading text
18
+ if (hasLoadingText) {
19
+ event.preventDefault();
20
+
21
+ if (button.disabled || button.classList.contains("w-button-loading")) {
22
+ return;
23
+ }
24
+
25
+ this.startLoading(button);
26
+
27
+ setTimeout(() => {
28
+ this.stopLoading(button);
29
+
30
+ // Only navigate if it's a link with href
31
+ if (isLink && button.href) {
32
+ window.location.href = button.href;
33
+ }
34
+ }, this.loadingDurationValue);
35
+ }
36
+ }
37
+
38
+ startLoading(button) {
39
+ this.originalContent = button.innerHTML;
40
+ const loadingText = button.dataset.buttonLoadingText;
41
+
42
+ button.classList.add("w-button-loading");
43
+ button.setAttribute("aria-busy", "true");
44
+ button.style.pointerEvents = "none";
45
+ button.disabled = true;
46
+ button.innerHTML = `<span class="w-button-spinner"></span> ${loadingText}`;
47
+ }
48
+
49
+ stopLoading(button) {
50
+ button.classList.remove("w-button-loading");
51
+ button.removeAttribute("aria-busy");
52
+ button.style.pointerEvents = "";
53
+ button.disabled = false;
54
+ button.innerHTML = this.originalContent;
55
+ }
56
+ }