wilday_ui 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/wilday_ui/index.js +214 -8
- data/app/assets/builds/wilday_ui/index.js.map +3 -3
- data/app/assets/stylesheets/wilday_ui/button.css +245 -0
- data/app/helpers/wilday_ui/components/button_helper.rb +219 -19
- data/app/javascript/wilday_ui/controllers/dropdown_controller.js +270 -0
- data/app/javascript/wilday_ui/controllers/index.js +7 -6
- data/app/javascript/wilday_ui/index.js +1 -1
- data/app/views/wilday_ui/_button.html.erb +16 -14
- data/app/views/wilday_ui/button/_content.html.erb +22 -0
- data/app/views/wilday_ui/button/_dropdown_menu.html.erb +26 -0
- data/lib/wilday_ui/version.rb +1 -1
- metadata +5 -2
@@ -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,
|
17
|
-
method: :get,
|
18
|
-
target: nil,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|