wilday_ui 0.2.3 → 0.4.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,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
- }