wilday_ui 0.8.0 → 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.
@@ -1,672 +0,0 @@
1
- module WildayUi
2
- module Components
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
- copy_to_clipboard: {
16
- wrapper_required: true,
17
- stimulus_controller: "clipboard button",
18
- default_stimulus_action: "click->clipboard#copy click->button#toggleLoading"
19
- },
20
- confirm: {
21
- wrapper_required: true,
22
- stimulus_controller: "confirmation",
23
- default_stimulus_action: "click->confirmation#showDialog"
24
- },
25
- tooltip: {
26
- wrapper_required: true,
27
- stimulus_controller: "tooltip",
28
- default_stimulus_action: {
29
- hover: "mouseenter->tooltip#show mouseleave->tooltip#hide focusin->tooltip#show focusout->tooltip#hide",
30
- click: "click->tooltip#toggle"
31
- }
32
- }
33
- # Add more features here as needed
34
- # tooltip: {
35
- # wrapper_required: true,
36
- # stimulus_controller: "tooltip",
37
- # default_stimulus_action: "mouseenter->tooltip#show mouseleave->tooltip#hide"
38
- # }
39
- }.freeze
40
-
41
- def w_button(
42
- content,
43
- variant: :solid,
44
- size: :medium,
45
- radius: :rounded,
46
- gradient: nil,
47
- icon: nil,
48
- icon_position: :left,
49
- icon_only: false,
50
- loading: false,
51
- loading_text: nil,
52
- disabled: false,
53
- additional_classes: "",
54
- use_default_controller: true,
55
- href: nil,
56
- method: :get,
57
- target: nil,
58
- underline: true,
59
- dropdown: false,
60
- dropdown_items: nil,
61
- dropdown_icon: nil,
62
- theme: nil,
63
- copy_to_clipboard: nil,
64
- confirm: nil,
65
- tooltip: nil,
66
- **options
67
- )
68
- content_for(:head) { stylesheet_link_tag "wilday_ui/components/button/index", media: "all" }
69
-
70
- options[:data] ||= {}
71
- wrapper_data = {}
72
- wrapper_options = nil
73
-
74
- # Process gradient styles if present
75
- if gradient.present?
76
- gradient_styles = process_gradient(gradient)
77
- options[:style] = [ options[:style], gradient_styles ].compact.join(";") if gradient_styles.present?
78
- end
79
-
80
- # Process theme styles
81
- if theme.present?
82
- theme_styles = process_theme(variant, theme)
83
- options[:style] = theme_styles if theme_styles.present?
84
- end
85
-
86
- variant_class = get_variant_class(variant)
87
- size_class = get_size_class(size)
88
- radius_class = get_radius_class(radius)
89
- gradient_class = get_gradient_class(gradient)
90
-
91
- # Setup features that require Stimulus controllers
92
- active_features = determine_active_features(loading, dropdown, loading_text, copy_to_clipboard, confirm, tooltip, use_default_controller)
93
-
94
- setup_features(active_features, options, use_default_controller, loading_text)
95
-
96
- setup_link_options(options, href, target, method)
97
-
98
- if dropdown
99
- setup_dropdown_options(
100
- options,
101
- additional_classes,
102
- dropdown,
103
- dropdown_items,
104
- wrapper_data
105
- )
106
- end
107
-
108
- if copy_to_clipboard
109
- setup_clipboard_options(
110
- options,
111
- additional_classes,
112
- copy_to_clipboard,
113
- wrapper_data
114
- )
115
- end
116
-
117
- if confirm
118
- setup_confirmation_options(
119
- options,
120
- additional_classes,
121
- confirm,
122
- wrapper_data
123
- )
124
- end
125
-
126
- if tooltip
127
- setup_tooltip_options(
128
- options,
129
- additional_classes,
130
- tooltip,
131
- wrapper_data
132
- )
133
- end
134
-
135
- # Setup wrapper options if any feature requires it
136
- wrapper_options = setup_wrapper_options(
137
- active_features,
138
- additional_classes,
139
- wrapper_data
140
- )
141
-
142
- render_button(
143
- content,
144
- variant_class,
145
- size_class,
146
- radius_class,
147
- gradient_class,
148
- icon,
149
- icon_position,
150
- icon_only,
151
- loading,
152
- loading_text,
153
- additional_classes,
154
- disabled,
155
- options,
156
- href,
157
- underline,
158
- dropdown,
159
- dropdown_items,
160
- dropdown_icon,
161
- wrapper_options
162
- )
163
- end
164
-
165
- private
166
-
167
- # def get_variant_class(variant)
168
- # {
169
- # primary: "w-button-primary",
170
- # secondary: "w-button-secondary",
171
- # outline: "w-button-outline"
172
- # }[variant] || "w-button-primary"
173
- # end
174
-
175
- def get_variant_class(variant)
176
- {
177
- solid: "w-button-solid",
178
- subtle: "w-button-subtle",
179
- surface: "w-button-surface",
180
- outline: "w-button-outline",
181
- ghost: "w-button-ghost",
182
- plain: "w-button-plain"
183
- }[variant] || "w-button-solid"
184
- end
185
-
186
- def get_size_class(size)
187
- {
188
- small: "w-button-small",
189
- medium: "w-button-medium",
190
- large: "w-button-large"
191
- }[size] || "w-button-medium"
192
- end
193
-
194
- def get_radius_class(radius)
195
- {
196
- rounded: "w-button-rounded",
197
- pill: "w-button-pill",
198
- square: "w-button-square"
199
- }[radius] || "w-button-rounded"
200
- end
201
-
202
- def get_gradient_class(gradient)
203
- return nil unless gradient
204
-
205
- if gradient.is_a?(Hash) && gradient[:from] && gradient[:to]
206
- "w-button-gradient-custom"
207
- else
208
- "w-button-gradient-#{gradient}"
209
- end
210
- end
211
-
212
- def process_gradient(gradient)
213
- return nil unless gradient.is_a?(Hash) && gradient[:from] && gradient[:to]
214
-
215
- if gradient[:via]
216
- "background: linear-gradient(135deg, #{gradient[:from]}, #{gradient[:via]}, #{gradient[:to]})"
217
- else
218
- "background: linear-gradient(135deg, #{gradient[:from]}, #{gradient[:to]})"
219
- end
220
- end
221
-
222
- def process_theme(variant, theme)
223
- return nil unless theme[:name] || theme[:custom]
224
-
225
- Rails.logger.debug "[Wilday UI] Processing theme for variant: #{variant}"
226
- Rails.logger.debug "[Wilday UI] Theme config: #{theme.inspect}"
227
-
228
- styles = {}
229
-
230
- if theme[:name]
231
- theme_colors = get_theme_colors(variant, theme[:name])
232
- styles.merge!(theme_colors)
233
- end
234
-
235
- if theme[:custom]
236
- custom_styles = process_custom_theme(theme[:custom])
237
- styles.merge!(custom_styles)
238
- end
239
-
240
- generate_styles(styles)
241
- end
242
-
243
- def get_theme_colors(variant, theme_name)
244
- config = WildayUi::Config::Theme.configuration
245
- # Convert theme_name to string
246
- return {} unless config&.colors&.[](theme_name.to_s)
247
-
248
- colors = config.colors[theme_name.to_s]
249
-
250
- case variant
251
- when :solid
252
- {
253
- "--w-button-color": "#FFFFFF",
254
- "--w-button-bg": colors["500"],
255
- "--w-button-hover-bg": colors["600"],
256
- "--w-button-active-bg": colors["700"]
257
- }
258
- when :subtle
259
- {
260
- "--w-button-color": colors["700"],
261
- "--w-button-bg": colors["50"],
262
- "--w-button-hover-bg": colors["100"],
263
- "--w-button-hover-color": colors["800"]
264
- }
265
- when :surface
266
- {
267
- "--w-button-color": colors["700"],
268
- "--w-button-bg": colors["50"],
269
- "--w-button-hover-bg": colors["100"],
270
- "--w-button-border": colors["200"],
271
- "--w-button-hover-border": colors["300"]
272
- }
273
- when :outline
274
- {
275
- "--w-button-color": colors["600"],
276
- "--w-button-border": colors["300"],
277
- "--w-button-hover-bg": colors["50"],
278
- "--w-button-hover-border": colors["400"],
279
- "--w-button-hover-color": colors["700"]
280
- }
281
- when :ghost
282
- {
283
- "--w-button-color": colors["600"],
284
- "--w-button-hover-bg": colors["50"],
285
- "--w-button-hover-color": colors["700"]
286
- }
287
- when :plain
288
- {
289
- "--w-button-color": colors["600"],
290
- "--w-button-hover-color": colors["700"],
291
- "--w-button-active-color": colors["800"]
292
- }
293
- else
294
- {}
295
- end
296
- end
297
-
298
- def process_custom_theme(custom)
299
- return {} unless custom
300
-
301
- {
302
- "--w-button-color": custom[:color],
303
- "--w-button-bg": custom[:background],
304
- "--w-button-border": custom[:border],
305
- "--w-button-hover-color": custom.dig(:hover, :color),
306
- "--w-button-hover-bg": custom.dig(:hover, :background),
307
- "--w-button-hover-border": custom.dig(:hover, :border),
308
- "--w-button-active-color": custom.dig(:active, :color),
309
- "--w-button-active-bg": custom.dig(:active, :background),
310
- "--w-button-active-border": custom.dig(:active, :border)
311
- }.compact
312
- end
313
-
314
- def generate_styles(styles)
315
- styles.map { |k, v| "#{k}: #{v}" }.join(";")
316
- end
317
-
318
- def determine_active_features(loading, dropdown, loading_text = nil, copy_to_clipboard = nil, confirm = nil, tooltip = nil, use_default_controller = true)
319
- features = {}
320
- features[:loading] = true if (loading || loading_text.present?) && use_default_controller
321
- features[:dropdown] = true if dropdown && use_default_controller
322
- features[:copy_to_clipboard] = true if copy_to_clipboard.present? && use_default_controller
323
- features[:confirm] = true if confirm.present? && use_default_controller
324
- features[:tooltip] = true if tooltip.present? && use_default_controller
325
- features
326
- end
327
-
328
- def setup_features(active_features, options, use_default_controller, loading_text)
329
- return unless use_default_controller && active_features.any?
330
-
331
- active_features.each do |feature, _value|
332
- feature_config = BUTTON_FEATURES[feature]
333
- next unless feature_config
334
-
335
- # Skip adding controller for dropdown feature since it's handled by wrapper
336
- if feature_config[:wrapper_required]
337
- # For dropdown, only set the action, not the controller
338
- options[:data][:action] = feature_config[:default_stimulus_action]
339
- else
340
- setup_feature_controller(options, feature_config, loading_text)
341
- end
342
- end
343
- end
344
-
345
- def setup_feature_controller(options, feature_config, loading_text)
346
- options[:data] ||= {}
347
-
348
- existing_controller = options.dig(:data, :controller)
349
- existing_action = options.dig(:data, :action)
350
-
351
- options[:data][:controller] = [
352
- existing_controller,
353
- feature_config[:stimulus_controller]
354
- ].compact.join(" ")
355
-
356
- options[:data][:action] = [
357
- existing_action,
358
- feature_config[:default_stimulus_action]
359
- ].compact.join(" ")
360
-
361
- # Add feature-specific data attributes
362
- if feature_config[:stimulus_controller] == "button" && loading_text.present?
363
- options[:data][:button_loading_text] = loading_text
364
- end
365
- end
366
-
367
- def setup_link_options(options, href, target, method)
368
- return unless href.present?
369
-
370
- options[:href] = href
371
- options[:target] = target if target
372
- options[:data][:method] = method if method != :get
373
- options[:rel] = "noopener noreferrer" if target == "_blank"
374
- end
375
-
376
- def setup_dropdown_options(options, additional_classes, dropdown, dropdown_items, wrapper_data)
377
- additional_classes = "#{additional_classes} w-button-dropdown"
378
-
379
- options[:data][:dropdown_target] = "button"
380
-
381
- wrapper_data.merge!(
382
- controller: "dropdown",
383
- dropdown_id: "dropdown-#{SecureRandom.hex(4)}"
384
- )
385
-
386
- if dropdown.is_a?(Hash)
387
- wrapper_data.merge!(
388
- dropdown_position_value: dropdown[:position]&.to_s || "bottom",
389
- dropdown_align_value: dropdown[:align]&.to_s || "start",
390
- dropdown_trigger_value: dropdown[:trigger]&.to_s || "click"
391
- )
392
- else
393
- wrapper_data.merge!(
394
- dropdown_position_value: "bottom",
395
- dropdown_align_value: "start",
396
- dropdown_trigger_value: "click"
397
- )
398
- end
399
-
400
- normalize_dropdown_items(dropdown_items)
401
- end
402
-
403
- def setup_wrapper_options(active_features, additional_classes, wrapper_data)
404
- return nil unless needs_wrapper?(active_features)
405
-
406
- {
407
- class: [ "w-button-wrapper", additional_classes ].compact.join(" "),
408
- data: wrapper_data,
409
- role: active_features[:dropdown] ? "menu" : nil
410
- }.compact
411
- end
412
-
413
- def needs_wrapper?(active_features)
414
- active_features.any? { |feature, _| BUTTON_FEATURES[feature][:wrapper_required] }
415
- end
416
-
417
- def normalize_dropdown_items(items, parent_id = nil)
418
- return [] unless items
419
-
420
- items.map.with_index do |item, index|
421
- item_id = generate_item_id(parent_id, index)
422
-
423
- normalized_item = {
424
- id: item_id,
425
- text: item[:text],
426
- href: item[:href],
427
- divider: item[:divider]
428
- }
429
-
430
- if item[:children].present?
431
- normalized_item[:children] = normalize_dropdown_items(item[:children], item_id)
432
- end
433
-
434
- normalized_item.compact
435
- end
436
- end
437
-
438
- def generate_item_id(parent_id, index)
439
- base = parent_id ? "#{parent_id}-" : "dropdown-item-"
440
- "#{base}#{index}"
441
- end
442
-
443
- def setup_clipboard_options(options, additional_classes, copy_to_clipboard, wrapper_data)
444
- return unless copy_to_clipboard.present?
445
-
446
- clipboard_config = normalize_clipboard_options(copy_to_clipboard)
447
-
448
- wrapper_data.merge!(
449
- controller: "clipboard button",
450
- clipboard_text_value: clipboard_config[:text],
451
- clipboard_feedback_text_value: clipboard_config[:feedback_text],
452
- clipboard_feedback_position_value: clipboard_config[:position],
453
- clipboard_feedback_duration_value: clipboard_config[:duration]
454
- )
455
-
456
- options[:data][:clipboard_target] = "button"
457
- options[:data][:button_target] = "button"
458
- end
459
-
460
- def normalize_clipboard_options(options)
461
- if options.is_a?(Hash)
462
- {
463
- text: options[:text],
464
- feedback_text: options[:feedback_text] || "Copied!",
465
- position: options[:position] || "top",
466
- duration: options[:duration] || 2000
467
- }
468
- else
469
- {
470
- text: options.to_s,
471
- feedback_text: "Copied!",
472
- position: "top",
473
- duration: 2000
474
- }
475
- end
476
- end
477
-
478
- def setup_confirmation_options(options, additional_classes, confirm, wrapper_data)
479
- return unless confirm.present?
480
-
481
- confirm_config = normalize_confirmation_options(confirm)
482
-
483
- # Use the same theme processing as regular buttons
484
- confirm_theme_styles = process_theme(:solid, { name: confirm_config[:variant] })
485
- cancel_theme_styles = process_theme(:subtle, { name: :secondary })
486
-
487
- wrapper_data.merge!(
488
- controller: "confirmation",
489
- confirmation_title_value: confirm_config[:title],
490
- confirmation_message_value: confirm_config[:message],
491
- confirmation_icon_color_value: confirm_config[:variant],
492
- confirmation_confirm_text_value: confirm_config[:confirm_text],
493
- confirmation_cancel_text_value: confirm_config[:cancel_text],
494
- confirmation_confirm_styles_value: confirm_theme_styles,
495
- confirmation_cancel_styles_value: cancel_theme_styles
496
- )
497
-
498
- # Only add loading state if enabled
499
- if confirm_config[:loading]
500
- wrapper_data.merge!(
501
- confirmation_loading_value: "true",
502
- confirmation_loading_text_value: confirm_config[:loading_text]
503
- )
504
- end
505
- end
506
-
507
- def normalize_confirmation_options(options)
508
- if options.is_a?(String)
509
- {
510
- title: "Confirm Action",
511
- message: options,
512
- variant: :info,
513
- confirm_text: "Confirm",
514
- cancel_text: "Cancel"
515
- }
516
- else
517
- {
518
- title: options[:title] || "Confirm Action",
519
- message: options[:message],
520
- variant: options[:variant] || :info,
521
- confirm_text: options[:confirm_text] || "Confirm",
522
- cancel_text: options[:cancel_text] || "Cancel",
523
- loading: options[:loading] || false,
524
- loading_text: options[:loading_text] || "Processing..."
525
- }
526
- end
527
- end
528
-
529
- def setup_tooltip_options(options, additional_classes, tooltip, wrapper_data)
530
- tooltip_config = normalize_tooltip_options(tooltip)
531
-
532
- # Check if dropdown is present
533
- has_dropdown = wrapper_data[:controller]&.include?("dropdown")
534
- has_clipboard = wrapper_data[:controller]&.include?("clipboard")
535
-
536
- # Get the appropriate action based on trigger type
537
- # Force hover behavior if dropdown is present
538
- trigger_type = (has_dropdown || has_clipboard) ? :hover : tooltip_config[:trigger].to_sym
539
- tooltip_action = BUTTON_FEATURES[:tooltip][:default_stimulus_action][trigger_type]
540
-
541
- # Merge controllers
542
- existing_controller = wrapper_data[:controller]
543
- wrapper_data[:controller] = [
544
- existing_controller,
545
- "tooltip"
546
- ].compact.join(" ")
547
-
548
- # Merge actions
549
- existing_action = wrapper_data[:action]
550
- if has_dropdown
551
- # Keep the dropdown toggle action and add tooltip hover actions
552
- wrapper_data[:action] = [
553
- "click->dropdown#toggle", # Ensure dropdown action comes first
554
- tooltip_action
555
- ].compact.join(" ")
556
- elsif has_clipboard
557
- # Keep the clipboard copy action and add tooltip hover actions
558
- wrapper_data[:action] = [
559
- "click->clipboard#copy click->button#toggleLoading",
560
- tooltip_action
561
- ].compact.join(" ")
562
- else
563
- wrapper_data[:action] = [
564
- existing_action,
565
- tooltip_action
566
- ].compact.join(" ")
567
- end
568
-
569
- # Handle theme data
570
- theme = tooltip_config[:theme]
571
- theme_name = theme.is_a?(Hash) ? theme[:name] : theme
572
-
573
- wrapper_data.merge!(
574
- tooltip_content_value: tooltip_config[:content],
575
- tooltip_position_value: tooltip_config[:position],
576
- tooltip_align_value: tooltip_config[:align],
577
- tooltip_trigger_value: trigger_type,
578
- tooltip_show_delay_value: tooltip_config[:delay][:show],
579
- tooltip_hide_delay_value: tooltip_config[:delay][:hide],
580
- tooltip_offset_value: tooltip_config[:offset],
581
- tooltip_theme_value: theme_name,
582
- tooltip_size_value: tooltip_config[:size],
583
- tooltip_arrow_value: tooltip_config[:arrow]
584
- )
585
-
586
- # Add custom theme styles if present
587
- if theme.is_a?(Hash) && theme[:custom]
588
- custom_styles = []
589
- custom_styles << "--tooltip-text-color: #{theme[:custom][:color]}" if theme[:custom][:color]
590
- custom_styles << "--tooltip-bg-color: #{theme[:custom][:background]}" if theme[:custom][:background]
591
- wrapper_data[:tooltip_custom_style_value] = custom_styles.join(";")
592
- end
593
-
594
- options[:data][:tooltip_target] = "trigger"
595
- options[:aria] ||= {}
596
- options[:aria][:describedby] = "tooltip-#{SecureRandom.hex(4)}"
597
- end
598
-
599
- def normalize_tooltip_options(options)
600
- if options.is_a?(String)
601
- {
602
- content: options,
603
- position: "top",
604
- align: "center",
605
- trigger: "hover",
606
- delay: { show: 0, hide: 0 },
607
- offset: 8,
608
- theme: "light",
609
- size: "md",
610
- arrow: false
611
- }
612
- else
613
- theme = options[:theme]
614
- theme_data = if theme.is_a?(Hash) && theme[:custom]
615
- {
616
- name: "custom",
617
- custom: {
618
- color: theme.dig(:custom, :color),
619
- background: theme.dig(:custom, :background)
620
- }
621
- }
622
- else
623
- { name: theme || "light" }
624
- end
625
-
626
- {
627
- content: options[:content],
628
- position: options[:position] || "top",
629
- align: options[:align] || "center",
630
- trigger: options[:trigger] || "hover",
631
- delay: {
632
- show: options.dig(:delay, :show) || 0,
633
- hide: options.dig(:delay, :hide) || 0
634
- },
635
- offset: options[:offset] || 8,
636
- theme: theme_data,
637
- size: options[:size] || "md",
638
- arrow: options[:arrow] || false
639
- }
640
- end
641
- end
642
-
643
- def render_button(content, variant_class, size_class, radius_class, gradient_class, icon, icon_position, icon_only,
644
- loading, loading_text, additional_classes, disabled, options, href, underline,
645
- dropdown, dropdown_items, dropdown_icon, wrapper_options)
646
-
647
- render partial: "wilday_ui/button",
648
- locals: {
649
- content: content,
650
- variant_class: variant_class,
651
- size_class: size_class,
652
- radius_class: radius_class,
653
- gradient_class: gradient_class,
654
- icon: icon,
655
- icon_position: icon_position,
656
- icon_only: icon_only,
657
- loading: loading,
658
- loading_text: loading_text,
659
- additional_classes: additional_classes,
660
- disabled: disabled,
661
- html_options: options,
662
- href: href,
663
- underline: underline,
664
- dropdown: dropdown,
665
- dropdown_items: dropdown_items,
666
- dropdown_icon: dropdown_icon,
667
- wrapper_options: wrapper_options
668
- }
669
- end
670
- end
671
- end
672
- end