wilday_ui 0.6.0 → 0.8.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.
@@ -11,6 +11,24 @@ module WildayUi
11
11
  wrapper_required: false,
12
12
  stimulus_controller: "button",
13
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
+ }
14
32
  }
15
33
  # Add more features here as needed
16
34
  # tooltip: {
@@ -42,6 +60,9 @@ module WildayUi
42
60
  dropdown_items: nil,
43
61
  dropdown_icon: nil,
44
62
  theme: nil,
63
+ copy_to_clipboard: nil,
64
+ confirm: nil,
65
+ tooltip: nil,
45
66
  **options
46
67
  )
47
68
  content_for(:head) { stylesheet_link_tag "wilday_ui/components/button/index", media: "all" }
@@ -68,7 +89,7 @@ module WildayUi
68
89
  gradient_class = get_gradient_class(gradient)
69
90
 
70
91
  # Setup features that require Stimulus controllers
71
- active_features = determine_active_features(loading, dropdown, loading_text, use_default_controller)
92
+ active_features = determine_active_features(loading, dropdown, loading_text, copy_to_clipboard, confirm, tooltip, use_default_controller)
72
93
 
73
94
  setup_features(active_features, options, use_default_controller, loading_text)
74
95
 
@@ -84,6 +105,33 @@ module WildayUi
84
105
  )
85
106
  end
86
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
+
87
135
  # Setup wrapper options if any feature requires it
88
136
  wrapper_options = setup_wrapper_options(
89
137
  active_features,
@@ -267,10 +315,13 @@ module WildayUi
267
315
  styles.map { |k, v| "#{k}: #{v}" }.join(";")
268
316
  end
269
317
 
270
- def determine_active_features(loading, dropdown, loading_text = nil, use_default_controller = true)
318
+ def determine_active_features(loading, dropdown, loading_text = nil, copy_to_clipboard = nil, confirm = nil, tooltip = nil, use_default_controller = true)
271
319
  features = {}
272
320
  features[:loading] = true if (loading || loading_text.present?) && use_default_controller
273
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
274
325
  features
275
326
  end
276
327
 
@@ -389,6 +440,206 @@ module WildayUi
389
440
  "#{base}#{index}"
390
441
  end
391
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
+
392
643
  def render_button(content, variant_class, size_class, radius_class, gradient_class, icon, icon_position, icon_only,
393
644
  loading, loading_text, additional_classes, disabled, options, href, underline,
394
645
  dropdown, dropdown_items, dropdown_icon, wrapper_options)
@@ -0,0 +1,76 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button", "feedback"];
5
+ static values = {
6
+ text: String,
7
+ feedbackText: { type: String, default: "Copied!" },
8
+ feedbackPosition: { type: String, default: "top" },
9
+ feedbackDuration: { type: Number, default: 2000 },
10
+ };
11
+
12
+ connect() {
13
+ // Optional: Initialize any necessary setup
14
+ }
15
+
16
+ async copy(event) {
17
+ event.preventDefault();
18
+
19
+ try {
20
+ await navigator.clipboard.writeText(this.textValue);
21
+ this.showFeedback();
22
+ } catch (err) {
23
+ console.error("Failed to copy text:", err);
24
+ // Fallback for older browsers
25
+ this.fallbackCopy();
26
+ }
27
+ }
28
+
29
+ fallbackCopy() {
30
+ const textArea = document.createElement("textarea");
31
+ textArea.value = this.textValue;
32
+ textArea.style.position = "fixed";
33
+ textArea.style.left = "-9999px";
34
+ document.body.appendChild(textArea);
35
+ textArea.select();
36
+
37
+ try {
38
+ document.execCommand("copy");
39
+ this.showFeedback();
40
+ } catch (err) {
41
+ console.error("Fallback: Oops, unable to copy", err);
42
+ }
43
+
44
+ document.body.removeChild(textArea);
45
+ }
46
+
47
+ showFeedback() {
48
+ const feedback = this.hasFeedbackTarget
49
+ ? this.feedbackTarget
50
+ : this.createFeedbackElement();
51
+ feedback.textContent = this.feedbackTextValue;
52
+
53
+ // Remove any existing position classes
54
+ feedback.className = "w-button-feedback";
55
+
56
+ // Add position-specific classes
57
+ const positionClasses = this.feedbackPositionValue.split("-");
58
+ positionClasses.forEach((pos) => {
59
+ feedback.classList.add(`w-button-feedback-${pos}`);
60
+ });
61
+
62
+ feedback.classList.add("w-button-feedback-show");
63
+
64
+ setTimeout(() => {
65
+ feedback.classList.remove("w-button-feedback-show");
66
+ }, this.feedbackDurationValue);
67
+ }
68
+
69
+ createFeedbackElement() {
70
+ const feedback = document.createElement("div");
71
+ feedback.classList.add("w-button-feedback");
72
+ feedback.setAttribute("data-clipboard-target", "feedback");
73
+ this.element.appendChild(feedback);
74
+ return feedback;
75
+ }
76
+ }
@@ -0,0 +1,216 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["dialog", "confirmButton", "cancelButton"];
5
+ static values = {
6
+ title: String,
7
+ message: String,
8
+ iconColor: String,
9
+ confirmText: String,
10
+ cancelText: String,
11
+ confirmStyles: String,
12
+ cancelStyles: String,
13
+ };
14
+
15
+ // Store the original event to be used later
16
+ originalEvent = null;
17
+
18
+ connect() {
19
+ // Create and append dialog if it doesn't exist
20
+ if (!this.hasDialogTarget) {
21
+ this.element.insertAdjacentHTML("beforeend", this.dialogHTML);
22
+ }
23
+ }
24
+
25
+ disconnect() {
26
+ this.originalEvent = null;
27
+ this.isConfirmed = false;
28
+ }
29
+
30
+ showDialog(event) {
31
+ // Don't show dialog if already confirmed
32
+ if (this.isConfirmed) {
33
+ this.isConfirmed = false;
34
+ return;
35
+ }
36
+
37
+ // console.log("Show dialog triggered", event);
38
+ event.preventDefault();
39
+ // Store the original event and element
40
+ this.originalEvent = {
41
+ type: event.type,
42
+ element: event.currentTarget,
43
+ ctrlKey: event.ctrlKey,
44
+ metaKey: event.metaKey,
45
+ };
46
+ // console.log("Original event stored:", this.originalEvent);
47
+ this.dialogTarget.showModal();
48
+ this.focusConfirmButton();
49
+ }
50
+
51
+ confirm(event) {
52
+ // console.log("Confirm clicked");
53
+ event.preventDefault();
54
+ this.dialogTarget.close();
55
+
56
+ if (this.originalEvent?.element) {
57
+ const element = this.originalEvent.element;
58
+ // console.log("Processing element:", element);
59
+
60
+ // Let Turbo handle its own elements
61
+ if (
62
+ this.hasTurbo &&
63
+ !element.hasAttribute("data-turbo") &&
64
+ !element.hasAttribute("data-turbo-method")
65
+ ) {
66
+ this.resumeOriginalEvent();
67
+ return;
68
+ }
69
+
70
+ // Dispatch standard DOM custom event
71
+ const confirmEvent = new CustomEvent("confirm", {
72
+ bubbles: true,
73
+ cancelable: true,
74
+ detail: {
75
+ element: element,
76
+ originalEvent: this.originalEvent,
77
+ },
78
+ });
79
+
80
+ const wasHandled = !element.dispatchEvent(confirmEvent);
81
+ if (wasHandled) return;
82
+
83
+ // If not handled by custom event, resume original event
84
+ this.resumeOriginalEvent();
85
+ }
86
+ }
87
+
88
+ resumeOriginalEvent() {
89
+ if (!this.originalEvent) return;
90
+
91
+ const element = this.originalEvent.element;
92
+ // console.log("Resuming original event for:", element);
93
+
94
+ // Set flag before triggering the event
95
+ this.isConfirmed = true;
96
+
97
+ // Handle form submissions
98
+ if (element.closest("form")) {
99
+ const form = element.closest("form");
100
+ // console.log("Submitting form:", form);
101
+ const submitEvent = new Event("submit", {
102
+ bubbles: true,
103
+ cancelable: true,
104
+ });
105
+ form.dispatchEvent(submitEvent); // This will trigger the event listener
106
+ this.originalEvent = null;
107
+ return;
108
+ }
109
+
110
+ // Handle links
111
+ if (element.tagName === "A" || element.hasAttribute("href")) {
112
+ // console.log("Processing link click");
113
+ const click = new MouseEvent("click", {
114
+ bubbles: true,
115
+ cancelable: true,
116
+ ctrlKey: this.originalEvent.ctrlKey,
117
+ metaKey: this.originalEvent.metaKey,
118
+ });
119
+ element.dispatchEvent(click);
120
+ this.originalEvent = null;
121
+ return;
122
+ }
123
+
124
+ // Handle regular button click
125
+ if (
126
+ !element.closest("form") &&
127
+ !(element.tagName === "A" || element.hasAttribute("href"))
128
+ ) {
129
+ // console.log("Processing button click");
130
+ element.click();
131
+ this.originalEvent = null;
132
+ return;
133
+ }
134
+ }
135
+
136
+ cancel(event) {
137
+ event.preventDefault();
138
+ this.closeDialog();
139
+ }
140
+
141
+ closeDialog() {
142
+ this.dialogTarget.close();
143
+ this.originalEvent = null;
144
+ }
145
+
146
+ handleKeydown(event) {
147
+ if (event.key === "Escape") {
148
+ this.cancel(event);
149
+ }
150
+ }
151
+
152
+ handleClickOutside(event) {
153
+ if (event.target === this.dialogTarget) {
154
+ this.cancel(event);
155
+ }
156
+ }
157
+
158
+ focusConfirmButton() {
159
+ this.confirmButtonTarget.focus();
160
+ }
161
+
162
+ get hasTurbo() {
163
+ return typeof Turbo !== "undefined";
164
+ }
165
+
166
+ get dialogHTML() {
167
+ return `
168
+ <dialog class="w-button-confirmation-dialog"
169
+ data-confirmation-target="dialog"
170
+ data-action="click->confirmation#handleClickOutside keydown->confirmation#handleKeydown">
171
+ <div class="w-button-confirmation-dialog-content">
172
+ <div class="w-button-confirmation-dialog-icon ${this.iconColorValue}">
173
+ ${this.iconHTML}
174
+ </div>
175
+
176
+ <h3 class="w-button-confirmation-dialog-title">
177
+ ${this.titleValue}
178
+ </h3>
179
+
180
+ <div class="w-button-confirmation-dialog-message">
181
+ ${this.messageValue}
182
+ </div>
183
+
184
+ <div class="w-button-confirmation-dialog-actions">
185
+ <button data-confirmation-target="cancelButton"
186
+ data-action="click->confirmation#cancel"
187
+ class="w-button w-button-subtle w-button-medium w-button-rounded"
188
+ style="${this.cancelStylesValue}">
189
+ ${this.cancelTextValue}
190
+ </button>
191
+
192
+ <button data-confirmation-target="confirmButton"
193
+ data-action="click->confirmation#confirm"
194
+ class="w-button w-button-solid w-button-medium w-button-rounded"
195
+ style="${this.confirmStylesValue}">
196
+ ${this.confirmTextValue}
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </dialog>
201
+ `;
202
+ }
203
+
204
+ get iconHTML() {
205
+ const icons = {
206
+ info: '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
207
+ success:
208
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
209
+ warning:
210
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>',
211
+ danger:
212
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
213
+ };
214
+ return icons[this.iconColorValue] || icons.info;
215
+ }
216
+ }
@@ -1,7 +1,9 @@
1
1
  import { Application } from "@hotwired/stimulus";
2
2
  import ButtonController from "./button_controller";
3
3
  import DropdownController from "./dropdown_controller";
4
-
4
+ import ClipboardController from "./clipboard_controller";
5
+ import ConfirmationController from "./confirmation_controller";
6
+ import TooltipController from "./tooltip_controller";
5
7
  // Initialize Stimulus
6
8
  const application = Application.start();
7
9
  window.Stimulus = application;
@@ -9,6 +11,9 @@ window.Stimulus = application;
9
11
  // Register the button controller
10
12
  application.register("button", ButtonController);
11
13
  application.register("dropdown", DropdownController);
14
+ application.register("clipboard", ClipboardController);
15
+ application.register("confirmation", ConfirmationController);
16
+ application.register("tooltip", TooltipController);
12
17
  // Debug check to ensure Stimulus is loaded
13
18
  // if (window.Stimulus) {
14
19
  // console.log("✅ Stimulus is loaded and initialized.");