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.
@@ -0,0 +1,74 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module FeatureEngine
5
+ include WildayUi::Components::Button::Features::Loading
6
+ include WildayUi::Components::Button::Features::Dropdown
7
+ include WildayUi::Components::Button::Features::CopyToClipboard
8
+ include WildayUi::Components::Button::Features::ConfirmDialog
9
+ include WildayUi::Components::Button::Features::Tooltip
10
+ include WildayUi::Components::Button::Features::Animation
11
+
12
+ BUTTON_FEATURES = begin
13
+ {}
14
+ .merge(Features::Loading.feature_config)
15
+ .merge(Features::Tooltip.feature_config)
16
+ .merge(Features::Dropdown.feature_config)
17
+ .merge(Features::CopyToClipboard.feature_config)
18
+ .merge(Features::ConfirmDialog.feature_config)
19
+ .merge(Features::Animation.feature_config)
20
+ .freeze
21
+ end
22
+
23
+ def self.button_features
24
+ BUTTON_FEATURES
25
+ end
26
+
27
+ def determine_active_features(loading, dropdown, loading_text = nil, copy_to_clipboard = nil, confirm = nil, tooltip = nil, animation = nil, use_default_controller = true)
28
+ features = {}
29
+ features[:loading] = true if (loading || loading_text.present?) && use_default_controller
30
+ features[:dropdown] = true if dropdown && use_default_controller
31
+ features[:copy_to_clipboard] = true if copy_to_clipboard.present? && use_default_controller
32
+ features[:confirm] = true if confirm.present? && use_default_controller
33
+ features[:tooltip] = true if tooltip.present? && use_default_controller
34
+ features[:animation] = true if animation.present? && use_default_controller
35
+ features
36
+ end
37
+
38
+ def setup_features(active_features, options, use_default_controller)
39
+ return unless use_default_controller && active_features.any?
40
+
41
+ active_features.each do |feature, _value|
42
+ feature_config = BUTTON_FEATURES[feature]
43
+ next unless feature_config
44
+
45
+ # Skip adding controller for dropdown feature since it's handled by wrapper
46
+ if feature_config[:wrapper_required]
47
+ # For dropdown, only set the action, not the controller
48
+ options[:data][:action] = feature_config[:default_stimulus_action]
49
+ else
50
+ setup_feature_controller(options, feature_config)
51
+ end
52
+ end
53
+ end
54
+
55
+ def setup_feature_controller(options, feature_config)
56
+ options[:data] ||= {}
57
+
58
+ existing_controller = options.dig(:data, :controller)
59
+ existing_action = options.dig(:data, :action)
60
+
61
+ options[:data][:controller] = [
62
+ existing_controller,
63
+ feature_config[:stimulus_controller]
64
+ ].compact.join(" ")
65
+
66
+ options[:data][:action] = [
67
+ existing_action,
68
+ feature_config[:default_stimulus_action]
69
+ ].compact.join(" ")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,97 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module Animation
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: false,
8
+ stimulus_controller: "animation",
9
+ default_stimulus_action: "animation#animate animation#disableAfterAnimation"
10
+ }.freeze
11
+
12
+ def self.feature_config
13
+ { animation: FEATURE_CONFIG }
14
+ end
15
+
16
+ def setup_animation_options(options, additional_classes, animation, wrapper_data)
17
+ return unless animation.present?
18
+
19
+ animation_config = normalize_animation_options(animation)
20
+
21
+ # Add animation data attributes
22
+ options[:data] ||= {}
23
+ options[:data][:animation] = animation_config.to_json
24
+ options[:data][:controller] = "animation"
25
+ options[:data][:animation_target] = "button"
26
+ options[:data][:action] = FEATURE_CONFIG[:default_stimulus_action]
27
+
28
+ additional_classes = [
29
+ additional_classes,
30
+ "w-button-animated",
31
+ "w-button-animation-#{animation_config[:name]}",
32
+ "w-button-animation-trigger-#{animation_config[:trigger]}"
33
+ ].compact.join(" ")
34
+ end
35
+
36
+ private
37
+
38
+ def normalize_animation_options(options)
39
+ if options.is_a?(Symbol)
40
+ {
41
+ name: options,
42
+ trigger: :hover,
43
+ timing: :ease,
44
+ duration: 0.3,
45
+ iteration: 1,
46
+ direction: :normal,
47
+ fill_mode: :none,
48
+ disabled: false
49
+ }
50
+ else
51
+ config = {
52
+ name: options[:name]&.to_sym,
53
+ trigger: options[:trigger]&.to_sym || :hover,
54
+ timing: normalize_timing(options[:timing]),
55
+ direction: options[:direction]&.to_sym || :normal,
56
+ iteration: normalize_iteration(options[:iteration]),
57
+ fill_mode: options[:fill_mode]&.to_sym || :none,
58
+ duration: options[:duration] || 0.3,
59
+ delay: options[:delay] || 0,
60
+ disabled: options[:disabled] || false
61
+ }
62
+
63
+ # Handle properties based on animation type
64
+ if options[:properties]
65
+ if config[:name] == :custom
66
+ config[:properties] = options[:properties] # Pass through all properties for custom animations
67
+ elsif config[:timing] == :cubic_bezier
68
+ config[:properties] = { cubic_bezier: options[:properties][:cubic_bezier] } # Handle cubic-bezier timing
69
+ end
70
+ end
71
+
72
+ config.compact
73
+ end
74
+ end
75
+
76
+ def normalize_timing(timing)
77
+ return :ease unless timing
78
+
79
+ timing = timing.to_sym if timing.is_a?(String)
80
+ case timing
81
+ when :linear, :ease, :ease_in, :ease_out, :ease_in_out, :cubic_bezier
82
+ timing
83
+ else
84
+ :ease # default if invalid timing provided
85
+ end
86
+ end
87
+
88
+ def normalize_iteration(value)
89
+ return value if value.is_a?(Integer)
90
+ return :infinite if value.to_s.downcase == "infinite"
91
+ value&.to_sym
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,70 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module ConfirmDialog
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: true,
8
+ stimulus_controller: "confirmation",
9
+ default_stimulus_action: "click->confirmation#showDialog"
10
+ }.freeze
11
+
12
+ def self.feature_config
13
+ { confirm: FEATURE_CONFIG }
14
+ end
15
+
16
+ def setup_confirmation_options(options, additional_classes, confirm, wrapper_data)
17
+ return unless confirm.present?
18
+
19
+ confirm_config = normalize_confirmation_options(confirm)
20
+
21
+ # Use the same theme processing as regular buttons
22
+ confirm_theme_styles = process_theme(:solid, { name: confirm_config[:variant] })
23
+ cancel_theme_styles = process_theme(:subtle, { name: :secondary })
24
+
25
+ wrapper_data.merge!(
26
+ controller: FEATURE_CONFIG[:stimulus_controller],
27
+ confirmation_title_value: confirm_config[:title],
28
+ confirmation_message_value: confirm_config[:message],
29
+ confirmation_icon_color_value: confirm_config[:variant],
30
+ confirmation_confirm_text_value: confirm_config[:confirm_text],
31
+ confirmation_cancel_text_value: confirm_config[:cancel_text],
32
+ confirmation_confirm_styles_value: confirm_theme_styles,
33
+ confirmation_cancel_styles_value: cancel_theme_styles
34
+ )
35
+
36
+ # Only add loading state if enabled
37
+ if confirm_config[:loading]
38
+ wrapper_data.merge!(
39
+ confirmation_loading_value: "true",
40
+ confirmation_loading_text_value: confirm_config[:loading_text]
41
+ )
42
+ end
43
+ end
44
+
45
+ def normalize_confirmation_options(options)
46
+ if options.is_a?(String)
47
+ {
48
+ title: "Confirm Action",
49
+ message: options,
50
+ variant: :info,
51
+ confirm_text: "Confirm",
52
+ cancel_text: "Cancel"
53
+ }
54
+ else
55
+ {
56
+ title: options[:title] || "Confirm Action",
57
+ message: options[:message],
58
+ variant: options[:variant] || :info,
59
+ confirm_text: options[:confirm_text] || "Confirm",
60
+ cancel_text: options[:cancel_text] || "Cancel",
61
+ loading: options[:loading] || false,
62
+ loading_text: options[:loading_text] || "Processing..."
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,56 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module CopyToClipboard
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: true,
8
+ stimulus_controller: "clipboard button",
9
+ default_stimulus_action: "click->clipboard#copy click->button#toggleLoading"
10
+ }.freeze
11
+
12
+ def self.feature_config
13
+ { copy_to_clipboard: FEATURE_CONFIG }
14
+ end
15
+
16
+ def setup_clipboard_options(options, additional_classes, copy_to_clipboard, wrapper_data)
17
+ return unless copy_to_clipboard.present?
18
+
19
+ clipboard_config = normalize_clipboard_options(copy_to_clipboard)
20
+
21
+ wrapper_data.merge!(
22
+ controller: FEATURE_CONFIG[:stimulus_controller],
23
+ clipboard_text_value: clipboard_config[:text],
24
+ clipboard_feedback_text_value: clipboard_config[:feedback_text],
25
+ clipboard_feedback_position_value: clipboard_config[:position],
26
+ clipboard_feedback_duration_value: clipboard_config[:duration]
27
+ )
28
+
29
+ options[:data][:clipboard_target] = "button"
30
+ options[:data][:button_target] = "button"
31
+ end
32
+
33
+ private
34
+
35
+ def normalize_clipboard_options(options)
36
+ if options.is_a?(Hash)
37
+ {
38
+ text: options[:text],
39
+ feedback_text: options[:feedback_text] || "Copied!",
40
+ position: options[:position] || "top",
41
+ duration: options[:duration] || 2000
42
+ }
43
+ else
44
+ {
45
+ text: options.to_s,
46
+ feedback_text: "Copied!",
47
+ position: "top",
48
+ duration: 2000
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,74 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module Dropdown
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: true,
8
+ stimulus_controller: "dropdown",
9
+ default_stimulus_action: "click->dropdown#toggle"
10
+ }.freeze
11
+
12
+ def self.feature_config
13
+ { dropdown: FEATURE_CONFIG }
14
+ end
15
+
16
+ def setup_dropdown_options(options, additional_classes, dropdown, dropdown_items, wrapper_data)
17
+ additional_classes = "#{additional_classes} w-button-dropdown"
18
+
19
+ options[:data][:dropdown_target] = "button"
20
+
21
+ wrapper_data.merge!(
22
+ controller: FEATURE_CONFIG[:stimulus_controller],
23
+ dropdown_id: "dropdown-#{SecureRandom.hex(4)}"
24
+ )
25
+
26
+ if dropdown.is_a?(Hash)
27
+ wrapper_data.merge!(
28
+ dropdown_position_value: dropdown[:position]&.to_s || "bottom",
29
+ dropdown_align_value: dropdown[:align]&.to_s || "start",
30
+ dropdown_trigger_value: dropdown[:trigger]&.to_s || "click"
31
+ )
32
+ else
33
+ wrapper_data.merge!(
34
+ dropdown_position_value: "bottom",
35
+ dropdown_align_value: "start",
36
+ dropdown_trigger_value: "click"
37
+ )
38
+ end
39
+
40
+ normalize_dropdown_items(dropdown_items)
41
+ end
42
+
43
+ private
44
+
45
+ def normalize_dropdown_items(items, parent_id = nil)
46
+ return [] unless items
47
+
48
+ items.map.with_index do |item, index|
49
+ item_id = generate_item_id(parent_id, index)
50
+
51
+ normalized_item = {
52
+ id: item_id,
53
+ text: item[:text],
54
+ href: item[:href],
55
+ divider: item[:divider]
56
+ }
57
+
58
+ if item[:children].present?
59
+ normalized_item[:children] = normalize_dropdown_items(item[:children], item_id)
60
+ end
61
+
62
+ normalized_item.compact
63
+ end
64
+ end
65
+
66
+ def generate_item_id(parent_id, index)
67
+ base = parent_id ? "#{parent_id}-" : "dropdown-item-"
68
+ "#{base}#{index}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,32 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module Loading
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: false,
8
+ stimulus_controller: "button",
9
+ default_stimulus_action: "click->button#toggleLoading"
10
+ }.freeze
11
+
12
+ def self.feature_config
13
+ { loading: FEATURE_CONFIG }
14
+ end
15
+
16
+ def setup_loading_options(options, loading_text)
17
+ return unless loading_text.present?
18
+
19
+ feature_config = FEATURE_CONFIG
20
+ setup_feature_controller(options, feature_config, loading_text)
21
+ end
22
+
23
+ def setup_loading_data_attributes(options, loading_text)
24
+ return unless loading_text.present?
25
+ options[:data] ||= {}
26
+ options[:data][:button_loading_text] = loading_text
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,138 @@
1
+ module WildayUi
2
+ module Components
3
+ module Button
4
+ module Features
5
+ module Tooltip
6
+ FEATURE_CONFIG = {
7
+ wrapper_required: true,
8
+ stimulus_controller: "tooltip",
9
+ default_stimulus_action: {
10
+ hover: "mouseenter->tooltip#show mouseleave->tooltip#hide focusin->tooltip#show focusout->tooltip#hide",
11
+ click: "click->tooltip#toggle"
12
+ }
13
+ }.freeze
14
+
15
+ def self.feature_config
16
+ { tooltip: FEATURE_CONFIG }
17
+ end
18
+
19
+ def setup_tooltip_options(options, additional_classes, tooltip, wrapper_data)
20
+ tooltip_config = normalize_tooltip_options(tooltip)
21
+
22
+ # Check if dropdown is present
23
+ has_dropdown = wrapper_data[:controller]&.include?("dropdown")
24
+ has_clipboard = wrapper_data[:controller]&.include?("clipboard")
25
+
26
+ # Get the appropriate action based on trigger type
27
+ # Force hover behavior if dropdown is present
28
+ trigger_type = (has_dropdown || has_clipboard) ? :hover : tooltip_config[:trigger].to_sym
29
+ tooltip_action = FEATURE_CONFIG[:default_stimulus_action][trigger_type]
30
+
31
+ # Merge controllers
32
+ existing_controller = wrapper_data[:controller]
33
+ wrapper_data[:controller] = [
34
+ existing_controller,
35
+ FEATURE_CONFIG[:stimulus_controller]
36
+ ].compact.join(" ")
37
+
38
+ # Merge actions
39
+ existing_action = wrapper_data[:action]
40
+ if has_dropdown
41
+ # Keep the dropdown toggle action and add tooltip hover actions
42
+ wrapper_data[:action] = [
43
+ "click->dropdown#toggle", # Ensure dropdown action comes first
44
+ tooltip_action
45
+ ].compact.join(" ")
46
+ elsif has_clipboard
47
+ # Keep the clipboard copy action and add tooltip hover actions
48
+ wrapper_data[:action] = [
49
+ "click->clipboard#copy click->button#toggleLoading",
50
+ tooltip_action
51
+ ].compact.join(" ")
52
+ else
53
+ wrapper_data[:action] = [
54
+ existing_action,
55
+ tooltip_action
56
+ ].compact.join(" ")
57
+ end
58
+
59
+ # Handle theme data
60
+ theme = tooltip_config[:theme]
61
+ theme_name = theme.is_a?(Hash) ? theme[:name] : theme
62
+
63
+ wrapper_data.merge!(
64
+ tooltip_content_value: tooltip_config[:content],
65
+ tooltip_position_value: tooltip_config[:position],
66
+ tooltip_align_value: tooltip_config[:align],
67
+ tooltip_trigger_value: trigger_type,
68
+ tooltip_show_delay_value: tooltip_config[:delay][:show],
69
+ tooltip_hide_delay_value: tooltip_config[:delay][:hide],
70
+ tooltip_offset_value: tooltip_config[:offset],
71
+ tooltip_theme_value: theme_name,
72
+ tooltip_size_value: tooltip_config[:size],
73
+ tooltip_arrow_value: tooltip_config[:arrow]
74
+ )
75
+
76
+ # Add custom theme styles if present
77
+ if theme.is_a?(Hash) && theme[:custom]
78
+ custom_styles = []
79
+ custom_styles << "--tooltip-text-color: #{theme[:custom][:color]}" if theme[:custom][:color]
80
+ custom_styles << "--tooltip-bg-color: #{theme[:custom][:background]}" if theme[:custom][:background]
81
+ wrapper_data[:tooltip_custom_style_value] = custom_styles.join(";")
82
+ end
83
+
84
+ options[:data][:tooltip_target] = "trigger"
85
+ options[:aria] ||= {}
86
+ options[:aria][:describedby] = "tooltip-#{SecureRandom.hex(4)}"
87
+ end
88
+
89
+ private
90
+
91
+ def normalize_tooltip_options(options)
92
+ if options.is_a?(String)
93
+ {
94
+ content: options,
95
+ position: "top",
96
+ align: "center",
97
+ trigger: "hover",
98
+ delay: { show: 0, hide: 0 },
99
+ offset: 8,
100
+ theme: "light",
101
+ size: "md",
102
+ arrow: false
103
+ }
104
+ else
105
+ theme = options[:theme]
106
+ theme_data = if theme.is_a?(Hash) && theme[:custom]
107
+ {
108
+ name: "custom",
109
+ custom: {
110
+ color: theme.dig(:custom, :color),
111
+ background: theme.dig(:custom, :background)
112
+ }
113
+ }
114
+ else
115
+ { name: theme || "light" }
116
+ end
117
+
118
+ {
119
+ content: options[:content],
120
+ position: options[:position] || "top",
121
+ align: options[:align] || "center",
122
+ trigger: options[:trigger] || "hover",
123
+ delay: {
124
+ show: options.dig(:delay, :show) || 0,
125
+ hide: options.dig(:delay, :hide) || 0
126
+ },
127
+ offset: options[:offset] || 8,
128
+ theme: theme_data,
129
+ size: options[:size] || "md",
130
+ arrow: options[:arrow] || false
131
+ }
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,97 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("AnimationController connected");
6
+ this.setupAnimation();
7
+ }
8
+
9
+ setupAnimation() {
10
+ const animationData = JSON.parse(this.element.dataset.animation || "{}");
11
+ if (!animationData.name) return;
12
+
13
+ const {
14
+ name,
15
+ trigger,
16
+ duration = 0.3,
17
+ timing = "ease",
18
+ delay = 0, // Add default value here
19
+ iteration = 1,
20
+ direction = "normal",
21
+ fill_mode = "none",
22
+ properties,
23
+ } = animationData;
24
+
25
+ // Set up CSS custom properties
26
+ this.element.style.setProperty("--animation-name", name);
27
+ this.element.style.setProperty("--animation-duration", `${duration}s`);
28
+ this.element.style.setProperty("--animation-delay", `${delay}s`); // This will now be "0s" instead of "undefineds"
29
+ this.element.style.setProperty(
30
+ "--animation-timing",
31
+ this.getTimingFunction(timing, properties)
32
+ );
33
+ this.element.style.setProperty(
34
+ "--animation-iteration",
35
+ iteration === "infinite" ? "infinite" : iteration || 1
36
+ );
37
+ this.element.style.setProperty(
38
+ "--animation-direction",
39
+ direction.replace(/_/g, "-")
40
+ );
41
+ this.element.style.setProperty(
42
+ "--animation-fill-mode",
43
+ fill_mode.replace(/_/g, "-")
44
+ );
45
+
46
+ // Handle custom properties
47
+ if (properties && name === "custom") {
48
+ Object.entries(properties).forEach(([key, value]) => {
49
+ this.element.style.setProperty(`--animation-custom-${key}`, value);
50
+ });
51
+ }
52
+
53
+ if (trigger === "click") {
54
+ this.element.addEventListener("click", () => this.animate());
55
+ } else if (trigger === "load") {
56
+ this.animate();
57
+ }
58
+ }
59
+
60
+ animate() {
61
+ const animationData = JSON.parse(this.element.dataset.animation || "{}");
62
+ if (!animationData.name) return;
63
+
64
+ // Remove existing animation class to allow re-triggering
65
+ this.element.classList.remove("is-animating");
66
+ // Force a reflow to ensure the animation runs again
67
+ void this.element.offsetWidth;
68
+ // Add animation class
69
+ this.element.classList.add("is-animating");
70
+ }
71
+
72
+ getTimingFunction(timing, properties) {
73
+ if (!timing) return "ease";
74
+
75
+ if (timing === "cubic_bezier" && properties?.cubic_bezier) {
76
+ const [x1, y1, x2, y2] = properties.cubic_bezier;
77
+ return `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`;
78
+ }
79
+ return timing.replace(/_/g, "-");
80
+ }
81
+
82
+ disableAfterAnimation() {
83
+ const animationConfig = JSON.parse(this.element.dataset.animation || "{}");
84
+ if (!animationConfig.disabled) return; // Only disable if config says so
85
+
86
+ const duration =
87
+ parseFloat(this.element.style.getPropertyValue("--animation-duration")) *
88
+ 1000;
89
+ const delay =
90
+ parseFloat(this.element.style.getPropertyValue("--animation-delay")) *
91
+ 1000;
92
+
93
+ setTimeout(() => {
94
+ this.element.disabled = true;
95
+ }, duration + delay);
96
+ }
97
+ }
@@ -4,6 +4,7 @@ import DropdownController from "./dropdown_controller";
4
4
  import ClipboardController from "./clipboard_controller";
5
5
  import ConfirmationController from "./confirmation_controller";
6
6
  import TooltipController from "./tooltip_controller";
7
+ import AnimationController from "./animation_controller";
7
8
  // Initialize Stimulus
8
9
  const application = Application.start();
9
10
  window.Stimulus = application;
@@ -14,6 +15,7 @@ application.register("dropdown", DropdownController);
14
15
  application.register("clipboard", ClipboardController);
15
16
  application.register("confirmation", ConfirmationController);
16
17
  application.register("tooltip", TooltipController);
18
+ application.register("animation", AnimationController);
17
19
  // Debug check to ensure Stimulus is loaded
18
20
  // if (window.Stimulus) {
19
21
  // console.log("✅ Stimulus is loaded and initialized.");