wilday_ui 0.2.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,270 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button", "menu", "submenu"];
5
+ static values = {
6
+ trigger: { type: String, default: "click" },
7
+ position: { type: String, default: "bottom" },
8
+ align: { type: String, default: "start" },
9
+ };
10
+
11
+ connect() {
12
+ const position = this.element.dataset.dropdownPositionValue;
13
+ const align = this.element.dataset.dropdownAlignValue;
14
+
15
+ if (position) this.positionValue = position;
16
+ if (align) this.alignValue = align;
17
+
18
+ // Set up hover events if trigger is hover
19
+ if (this.triggerValue === "hover") {
20
+ this.element.addEventListener("mouseenter", () => {
21
+ console.log("Mouse enter - showing menu");
22
+ this.handleHover(true);
23
+ });
24
+
25
+ this.element.addEventListener("mouseleave", () => {
26
+ console.log("Mouse leave - hiding menu");
27
+ this.handleHover(false);
28
+ });
29
+ }
30
+
31
+ // Set up keyboard navigation
32
+ this.element.addEventListener("keydown", this.handleKeydown.bind(this));
33
+
34
+ // Close on click outside
35
+ document.addEventListener("click", this.handleClickOutside.bind(this));
36
+
37
+ // Set up parent dropdown items to handle submenus
38
+ this.setupSubmenus();
39
+ }
40
+
41
+ disconnect() {
42
+ document.removeEventListener("click", this.handleClickOutside.bind(this));
43
+ }
44
+
45
+ handleHover(show) {
46
+ if (show) {
47
+ this.menuTarget.classList.add("show");
48
+ this.buttonTarget.classList.add("active");
49
+ this.buttonTarget.setAttribute("aria-expanded", "true");
50
+ } else {
51
+ this.menuTarget.classList.remove("show");
52
+ this.buttonTarget.classList.remove("active");
53
+ this.buttonTarget.setAttribute("aria-expanded", "false");
54
+ }
55
+ }
56
+
57
+ toggle(event) {
58
+ event.preventDefault();
59
+ event.stopPropagation();
60
+
61
+ if (this.isOpen) {
62
+ this.menuTarget.classList.remove("show");
63
+ } else {
64
+ this.menuTarget.classList.add("show");
65
+ }
66
+ }
67
+
68
+ show() {
69
+ const menuElement = this.element.querySelector(".w-button-dropdown-menu");
70
+ const buttonElement = this.element.querySelector(
71
+ "[data-dropdown-target='button']"
72
+ );
73
+
74
+ this.updatePosition();
75
+ menuElement.classList.add("show");
76
+ buttonElement.classList.add("active");
77
+ buttonElement.setAttribute("aria-expanded", "true");
78
+
79
+ // Apply position and alignment
80
+ menuElement.dataset.position = this.positionValue || "bottom";
81
+ menuElement.dataset.align = this.alignValue || "start";
82
+
83
+ // Focus first menu item
84
+ const firstItem = menuElement.querySelector(".w-button-dropdown-item");
85
+ if (firstItem) firstItem.focus();
86
+ }
87
+
88
+ hide() {
89
+ const menuElement = this.element.querySelector(".w-button-dropdown-menu");
90
+ const buttonElement = this.element.querySelector(
91
+ "[data-dropdown-target='button']"
92
+ );
93
+
94
+ menuElement.classList.remove("show");
95
+ buttonElement.classList.remove("active");
96
+ buttonElement.setAttribute("aria-expanded", "false");
97
+ }
98
+
99
+ handleClickOutside(event) {
100
+ if (!this.element.contains(event.target)) {
101
+ console.log("Click outside detected. Closing all dropdowns.");
102
+ this.closeAllSubmenus();
103
+ this.hide(); // Close the main dropdown
104
+ }
105
+ }
106
+
107
+ handleKeydown(event) {
108
+ if (!this.isOpen && event.key === "Enter") {
109
+ this.show();
110
+ return;
111
+ }
112
+
113
+ if (this.isOpen) {
114
+ switch (event.key) {
115
+ case "Escape":
116
+ this.hide();
117
+ this.buttonTarget.focus();
118
+ break;
119
+ case "ArrowDown":
120
+ event.preventDefault();
121
+ this.focusNextItem();
122
+ break;
123
+ case "ArrowUp":
124
+ event.preventDefault();
125
+ this.focusPreviousItem();
126
+ break;
127
+ case "ArrowRight":
128
+ this.openSubmenu(event.target);
129
+ break;
130
+ case "ArrowLeft":
131
+ this.closeSubmenu(event.target);
132
+ break;
133
+ case "Tab":
134
+ this.hide();
135
+ break;
136
+ }
137
+ }
138
+ }
139
+
140
+ setupSubmenus() {
141
+ this.element
142
+ .querySelectorAll(".w-button-dropdown-parent")
143
+ .forEach((parent) => {
144
+ const submenu = parent.querySelector(".w-button-dropdown-menu");
145
+ const arrow = parent.querySelector(".w-button-dropdown-arrow");
146
+
147
+ if (!submenu) {
148
+ return;
149
+ }
150
+
151
+ // Determine the trigger (hover or click)
152
+ const trigger =
153
+ parent.closest(".w-button-wrapper")?.dataset.dropdownTriggerValue ||
154
+ "click";
155
+
156
+ if (trigger === "hover") {
157
+ parent.addEventListener("mouseenter", () => {
158
+ this.showSubmenu(submenu, arrow);
159
+ });
160
+ parent.addEventListener("mouseleave", () => {
161
+ this.hideSubmenu(submenu, arrow);
162
+ });
163
+ } else if (trigger === "click") {
164
+ parent.addEventListener("click", (event) => {
165
+ event.stopPropagation(); // Prevent closing parent dropdown
166
+ this.toggleSubmenu(submenu, arrow);
167
+ });
168
+
169
+ // Close the submenu when clicking outside
170
+ document.addEventListener("click", (event) => {
171
+ if (!parent.contains(event.target)) {
172
+ this.hideSubmenu(submenu, arrow);
173
+ }
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ toggleSubmenu(submenu, arrow) {
180
+ if (submenu.classList.contains("show")) {
181
+ this.hideSubmenu(submenu, arrow);
182
+ } else {
183
+ this.showSubmenu(submenu, arrow);
184
+ }
185
+ }
186
+
187
+ showSubmenu(submenu, arrow) {
188
+ submenu.classList.add("show");
189
+ submenu.setAttribute("aria-expanded", "true");
190
+
191
+ // Rotate arrow if present
192
+ if (arrow) {
193
+ arrow.classList.add("active");
194
+ }
195
+ }
196
+
197
+ hideSubmenu(submenu, arrow) {
198
+ submenu.classList.remove("show");
199
+ submenu.setAttribute("aria-expanded", "false");
200
+
201
+ // Reset arrow rotation if present
202
+ if (arrow) {
203
+ arrow.classList.remove("active");
204
+ }
205
+ }
206
+
207
+ isParentOpen(parent) {
208
+ const parentMenu = parent.closest(".w-button-dropdown-menu");
209
+ return parentMenu && parentMenu.classList.contains("show");
210
+ }
211
+
212
+ closeAllSubmenus() {
213
+ this.element
214
+ .querySelectorAll(".w-button-dropdown-menu.show")
215
+ .forEach((menu) => {
216
+ menu.classList.remove("show");
217
+ menu.setAttribute("aria-expanded", "false");
218
+ });
219
+ }
220
+
221
+ openSubmenu(parentItem) {
222
+ const submenu = parentItem.querySelector(".w-button-dropdown-menu");
223
+ if (submenu) {
224
+ this.showSubmenu(submenu);
225
+ submenu.querySelector(".w-button-dropdown-item").focus();
226
+ }
227
+ }
228
+
229
+ closeSubmenu(parentItem) {
230
+ const submenu = parentItem.closest(".w-button-dropdown-menu");
231
+ if (submenu) {
232
+ this.hideSubmenu(submenu);
233
+ parentItem.closest(".w-button-dropdown-parent").focus();
234
+ }
235
+ }
236
+
237
+ focusNextItem() {
238
+ const items = this.getMenuItems();
239
+ const currentIndex = items.indexOf(document.activeElement);
240
+ const nextIndex = currentIndex + 1 < items.length ? currentIndex + 1 : 0;
241
+ items[nextIndex].focus();
242
+ }
243
+
244
+ focusPreviousItem() {
245
+ const items = this.getMenuItems();
246
+ const currentIndex = items.indexOf(document.activeElement);
247
+ const previousIndex =
248
+ currentIndex > 0 ? currentIndex - 1 : items.length - 1;
249
+ items[previousIndex].focus();
250
+ }
251
+
252
+ getMenuItems() {
253
+ return Array.from(
254
+ this.menuTarget.querySelectorAll(".w-button-dropdown-item")
255
+ );
256
+ }
257
+
258
+ updatePosition() {
259
+ const menuElement = this.element.querySelector(".w-button-dropdown-menu");
260
+ const position = this.hasPositionValue ? this.positionValue : "bottom";
261
+ const align = this.hasAlignValue ? this.alignValue : "start";
262
+
263
+ menuElement.setAttribute("data-position", position);
264
+ menuElement.setAttribute("data-align", align);
265
+ }
266
+
267
+ get isOpen() {
268
+ return this.menuTarget.classList.contains("show");
269
+ }
270
+ }
@@ -0,0 +1,17 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+ import ButtonController from "./button_controller";
3
+ import DropdownController from "./dropdown_controller";
4
+
5
+ // Initialize Stimulus
6
+ const application = Application.start();
7
+ window.Stimulus = application;
8
+
9
+ // Register the button controller
10
+ application.register("button", ButtonController);
11
+ application.register("dropdown", DropdownController);
12
+ // Debug check to ensure Stimulus is loaded
13
+ // if (window.Stimulus) {
14
+ // console.log("✅ Stimulus is loaded and initialized.");
15
+ // } else {
16
+ // console.error("❌ Stimulus failed to load.");
17
+ // }
@@ -1 +1,4 @@
1
+ import "./controllers";
1
2
  import "./components/button";
3
+
4
+ // console.log("JavaScript loaded");
@@ -8,6 +8,7 @@
8
8
  <%= yield :head %>
9
9
 
10
10
  <%= stylesheet_link_tag "wilday_ui/application", media: "all" %>
11
+ <%= javascript_include_tag "wilday_ui/index", "data-turbo-track": "reload", type: "module" %>
11
12
  </head>
12
13
  <body>
13
14
 
@@ -1,13 +1,38 @@
1
- <button
2
- class="w-button <%= variant_class %> <%= size_class %> <%= radius_class %> <%= additional_classes %>"
3
- <%= attributes %>
4
- <%= "disabled" if disabled %>
5
- >
6
- <% if icon_position == :left && icon.present? %>
7
- <i class="w-button-icon-left <%= icon %>"></i>
1
+ <%
2
+ # Prepare base HTML options
3
+ html_options = {
4
+ class: ["w-button", variant_class, size_class, radius_class, additional_classes, ("w-button-loading" if loading)].compact.join(" "),
5
+ disabled: disabled || loading,
6
+ "aria-busy": loading,
7
+ "aria-disabled": disabled || loading,
8
+ "aria-expanded": dropdown ? "false" : nil,
9
+ "aria-haspopup": dropdown ? "true" : nil
10
+ }
11
+
12
+ # Only add Stimulus data attributes if they're in the passed options
13
+ if local_assigns[:html_options]&.dig(:data, :controller)
14
+ html_options[:data] = (local_assigns[:html_options][:data] || {})
15
+ end
16
+
17
+ # Merge any remaining options
18
+ html_options.merge!(local_assigns[:html_options] || {})
19
+
20
+ tag_type = href.present? ? :a : :button
21
+
22
+ # Conditionally render wrapper for dropdowns or advanced features
23
+ if local_assigns[:wrapper_options].present?
24
+ %>
25
+ <div <%= tag.attributes(wrapper_options) %>>
26
+ <%= content_tag tag_type, html_options do %>
27
+ <%= render partial: "wilday_ui/button/content", locals: local_assigns %>
8
28
  <% end %>
9
- <%= content %>
10
- <% if icon_position == :right && icon.present? %>
11
- <i class="w-button-icon-right <%= icon %>"></i>
29
+
30
+ <% if dropdown && dropdown_items %>
31
+ <%= render partial: "wilday_ui/button/dropdown_menu", locals: { items: dropdown_items, dropdown: dropdown } %>
12
32
  <% end %>
13
- </button>
33
+ </div>
34
+ <% else %>
35
+ <%= content_tag tag_type, html_options do %>
36
+ <%= render partial: "wilday_ui/button/content", locals: local_assigns %>
37
+ <% end %>
38
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% if loading %>
2
+ <span class="w-button-spinner"></span>
3
+ <%= loading_text %>
4
+ <% else %>
5
+ <% if icon_position == :left && icon.present? %>
6
+ <i class="w-button-icon-left <%= icon %>"></i>
7
+ <% end %>
8
+
9
+ <%= content %>
10
+
11
+ <% if dropdown %>
12
+ <% if dropdown_icon.present? %>
13
+ <i class="w-button-dropdown-arrow <%= dropdown_icon %>"></i>
14
+ <% else %>
15
+ <span class="w-button-dropdown-arrow"></span>
16
+ <% end %>
17
+ <% end %>
18
+
19
+ <% if icon_position == :right && icon.present? %>
20
+ <i class="w-button-icon-right <%= icon %>"></i>
21
+ <% end %>
22
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <% if items.present? %>
2
+ <div class="w-button-dropdown-menu" data-dropdown-target="menu" role="menu">
3
+ <% items.each do |item| %>
4
+ <% if item[:divider] %>
5
+ <div class="w-button-dropdown-divider" role="separator"></div>
6
+ <% elsif item[:children].present? %>
7
+ <div class="w-button-dropdown-item w-button-dropdown-parent" role="menuitem">
8
+ <%= item[:text] %>
9
+ <span class="w-button-dropdown-arrow"></span>
10
+ <%= render partial: "wilday_ui/button/dropdown_menu",
11
+ locals: {
12
+ items: item[:children],
13
+ parent_id: item[:id]
14
+ } %>
15
+ </div>
16
+ <% else %>
17
+ <%= link_to item[:text],
18
+ item[:href],
19
+ class: "w-button-dropdown-item",
20
+ role: "menuitem",
21
+ tabindex: "-1"
22
+ %>
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
@@ -19,16 +19,66 @@ module WildayUi
19
19
  end
20
20
  end
21
21
 
22
+ # # Configure asset paths and automatic precompilation
23
+ # initializer "wilday_ui.assets" do |app|
24
+ # app.config.assets.paths << root.join("app/assets/stylesheets")
25
+ # app.config.assets.paths << root.join("app/javascript")
26
+ # app.config.assets.paths << root.join("app/assets/builds")
27
+
28
+ # Rails.logger.info "[Wilday UI] Asset paths added: #{app.config.assets.paths}"
29
+
30
+ # # Automatically precompile all CSS files in wilday_ui directory
31
+ # css_files = Dir[root.join("app/assets/stylesheets/wilday_ui/**/*.css")].map do |file|
32
+ # file.split("app/assets/stylesheets/").last
33
+ # end
34
+
35
+ # # Precompile only the bundled JavaScript file
36
+ # app.config.assets.precompile += css_files
37
+ # app.config.assets.precompile += %w[wilday_ui/index.js]
38
+
39
+ # Rails.logger.info "[Wilday UI] CSS files precompiled: #{css_files}"
40
+ # Rails.logger.info "[Wilday UI] JS files precompiled: wilday_ui/index.js"
41
+ # end
42
+
22
43
  # Configure asset paths and automatic precompilation
23
44
  initializer "wilday_ui.assets" do |app|
24
- app.config.assets.paths << root.join("app/assets/stylesheets")
45
+ # Add engine asset paths
46
+ engine_asset_paths = [
47
+ root.join("app/assets/stylesheets"),
48
+ root.join("app/javascript"),
49
+ root.join("app/assets/builds")
50
+ ]
51
+
52
+ # Add dummy app assets path in development
53
+ if Rails.env.development?
54
+ engine_asset_paths << root.join("test/dummy/app/assets/builds")
55
+
56
+ # Create symlink for dummy app assets if it doesn't exist
57
+ dummy_builds_path = root.join("test/dummy/app/assets/builds/wilday_ui")
58
+ engine_builds_path = root.join("app/assets/builds/wilday_ui")
59
+
60
+ unless File.exist?(dummy_builds_path)
61
+ FileUtils.mkdir_p(dummy_builds_path.parent)
62
+ FileUtils.ln_sf(engine_builds_path, dummy_builds_path)
63
+ Rails.logger.info "[Wilday UI] Created symlink for dummy app assets"
64
+ end
65
+ end
66
+
67
+ # Add all asset paths to Rails
68
+ engine_asset_paths.each { |path| app.config.assets.paths << path }
69
+ Rails.logger.info "[Wilday UI] Asset paths added: #{app.config.assets.paths}"
25
70
 
26
71
  # Automatically precompile all CSS files in wilday_ui directory
27
72
  css_files = Dir[root.join("app/assets/stylesheets/wilday_ui/**/*.css")].map do |file|
28
73
  file.split("app/assets/stylesheets/").last
29
74
  end
30
75
 
76
+ # Precompile only the bundled JavaScript file
31
77
  app.config.assets.precompile += css_files
78
+ app.config.assets.precompile += %w[wilday_ui/index.js]
79
+
80
+ Rails.logger.info "[Wilday UI] CSS files precompiled: #{css_files}"
81
+ Rails.logger.info "[Wilday UI] JS files precompiled: wilday_ui/index.js"
32
82
  end
33
83
  end
34
84
  end
@@ -1,3 +1,3 @@
1
1
  module WildayUi
2
- VERSION = "0.2.3"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wilday_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - davidwinalda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-10 00:00:00.000000000 Z
11
+ date: 2024-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -39,9 +39,8 @@ extra_rdoc_files: []
39
39
  files:
40
40
  - README.md
41
41
  - Rakefile
42
- - app/assets/builds/index.css
43
- - app/assets/builds/index.js
44
- - app/assets/builds/index.js.map
42
+ - app/assets/builds/wilday_ui/index.js
43
+ - app/assets/builds/wilday_ui/index.js.map
45
44
  - app/assets/config/wilday_ui_manifest.js
46
45
  - app/assets/stylesheets/wilday_ui/application.css
47
46
  - app/assets/stylesheets/wilday_ui/button.css
@@ -49,12 +48,17 @@ files:
49
48
  - app/helpers/wilday_ui/application_helper.rb
50
49
  - app/helpers/wilday_ui/components/button_helper.rb
51
50
  - app/javascript/wilday_ui/components/button.js
51
+ - app/javascript/wilday_ui/controllers/button_controller.js
52
+ - app/javascript/wilday_ui/controllers/dropdown_controller.js
53
+ - app/javascript/wilday_ui/controllers/index.js
52
54
  - app/javascript/wilday_ui/index.js
53
55
  - app/jobs/wilday_ui/application_job.rb
54
56
  - app/mailers/wilday_ui/application_mailer.rb
55
57
  - app/models/wilday_ui/application_record.rb
56
58
  - app/views/layouts/wilday_ui/application.html.erb
57
59
  - app/views/wilday_ui/_button.html.erb
60
+ - app/views/wilday_ui/button/_content.html.erb
61
+ - app/views/wilday_ui/button/_dropdown_menu.html.erb
58
62
  - config/routes.rb
59
63
  - lib/tasks/wilday_ui_tasks.rake
60
64
  - lib/wilday_ui.rb
@@ -1 +0,0 @@
1
- .wilday-button{display:inline-flex;align-items:center;justify-content:center;padding:.5rem 1rem;border-radius:.375rem;font-size:1rem;cursor:pointer;transition:all .2s ease-in-out}.wilday-button-primary{background-color:#007bff;color:#fff;border:none}.wilday-button-primary:hover{background-color:#0056b3}.wilday-button-secondary{background-color:#6c757d;color:#fff;border:none}.wilday-button-secondary:hover{background-color:#5a6268}.wilday-button-outline{background-color:transparent;color:#007bff;border:1px solid #007bff}.wilday-button-outline:hover{background-color:#e7f1ff}.wilday-button-small{font-size:.875rem;padding:.25rem .5rem}.wilday-button-medium{font-size:1rem;padding:.5rem 1rem}.wilday-button-large{font-size:1.25rem;padding:.75rem 1.5rem}.wilday-button:disabled{background-color:#e0e0e0;color:#6c757d;cursor:not-allowed}
@@ -1,13 +0,0 @@
1
- (() => {
2
- // app/javascript/wilday_ui/components/button.js
3
- document.addEventListener("DOMContentLoaded", () => {
4
- document.querySelectorAll(".w-button").forEach((button) => {
5
- button.addEventListener("click", (event) => {
6
- if (button.disabled) {
7
- event.preventDefault();
8
- }
9
- });
10
- });
11
- });
12
- })();
13
- //# sourceMappingURL=index.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../javascript/wilday_ui/components/button.js"],
4
- "sourcesContent": ["document.addEventListener(\"DOMContentLoaded\", () => {\n document.querySelectorAll(\".w-button\").forEach((button) => {\n button.addEventListener(\"click\", (event) => {\n if (button.disabled) {\n event.preventDefault();\n }\n });\n });\n});\n"],
5
- "mappings": ";;AAAA,WAAS,iBAAiB,oBAAoB,MAAM;AAClD,aAAS,iBAAiB,WAAW,EAAE,QAAQ,CAAC,WAAW;AACzD,aAAO,iBAAiB,SAAS,CAAC,UAAU;AAC1C,YAAI,OAAO,UAAU;AACnB,gBAAM,eAAe;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH,CAAC;",
6
- "names": []
7
- }