maquina-components 0.3.1.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b851df46a3afd618bc81e81319613ac295fb6b09fa5b68a3579f1508d3a07bda
4
- data.tar.gz: e90010af3a12f9ffd8930201bc5b07a697cafa10da533e4ffeaa546bca8d6ec9
3
+ metadata.gz: 5a1e90929a51b6023b0884911c00d7168804740389aa4b22594923f8ccf223c7
4
+ data.tar.gz: da1376c62eab19ac2145795de7bed3711ed4452574fd76c8659eedec5b933450
5
5
  SHA512:
6
- metadata.gz: 023b264cbe15f2d62aa508a35dc3919827130f1382e795b02a31afdf4c7160218f2f784eaf3c6521f75fce9ab5f3e7a0068b4b6064e9fe28a321b4d105955533
7
- data.tar.gz: 48606389939ad53c12e519e5d9d95b316cf083b492646c992c5a713eee5de1919fe218a860975350980619c2aba836d364ee10db32cccd75113045f159cdd5f3
6
+ metadata.gz: b2db7a9538eb06ae5b236cf02aabb77818a772a6df372ae439f64723615a68d9b4f4aa44c612c1e91c199758dddb4064d76bdaa1d616fcd64d48fcb68ec9e89e
7
+ data.tar.gz: 524381d71a7fdeb252e1ebd7e0627386e248c58da9abdcde5be359e0d2d65f3ae86eb6e19db953c2147a1e35aa4d4bf4fd345179120388136704740814aea68e
@@ -23,6 +23,18 @@
23
23
  transition: none !important;
24
24
  }
25
25
 
26
+ /* Prevent layout shift before Stimulus initializes.
27
+ The server renders collapsible="offcanvas" (gap=0) but JS immediately
28
+ sets collapsible="none" (gap=sidebar-width) for expanded desktop sidebars.
29
+ This rule keeps the gap at the correct width during loading so the main
30
+ content doesn't jump from left to right. Desktop-only — on mobile JS
31
+ forces the sidebar closed and the gap should stay at 0. */
32
+ @media (min-width: 768px) {
33
+ [data-sidebar-part="root"].sidebar-loading[data-state="expanded"] [data-sidebar-part="gap"] {
34
+ width: var(--sidebar-width);
35
+ }
36
+ }
37
+
26
38
  /* ===== Sidebar Gap (creates space on desktop) ===== */
27
39
  [data-sidebar-part="gap"] {
28
40
  @apply relative bg-transparent;
@@ -15,9 +15,9 @@ module MaquinaComponents
15
15
  # <%= toast :error, "Save failed", description: "Please check your connection." %>
16
16
  #
17
17
  # @example Toast with action
18
- # <%= toast :info, "New version available" do %>
18
+ # <%= toast :info, "New version available", content: capture { %>
19
19
  # <%= render "components/toast/action", label: "Refresh", href: root_path %>
20
- # <% end %>
20
+ # <% } %>
21
21
  #
22
22
  module ToastHelper
23
23
  # Flash type to toast variant mapping
@@ -55,15 +55,14 @@ module MaquinaComponents
55
55
  # @param title [String] Toast title
56
56
  # @param description [String, nil] Optional description
57
57
  # @param options [Hash] Additional options passed to the toast partial
58
- # @yield Optional block for custom content (e.g., action button)
58
+ # @param content [String, nil] HTML content via `capture` (e.g., action button)
59
59
  # @return [String] HTML-safe toast element
60
- def toast(variant, title, description: nil, **options, &block)
60
+ def toast(variant, title, description: nil, **options)
61
61
  render "components/toast",
62
62
  variant: variant,
63
63
  title: title,
64
64
  description: description,
65
- **options,
66
- &block
65
+ **options
67
66
  end
68
67
 
69
68
  # Render a success toast
@@ -71,8 +70,8 @@ module MaquinaComponents
71
70
  # @param title [String] Toast title
72
71
  # @param options [Hash] Additional options
73
72
  # @return [String] HTML-safe toast element
74
- def toast_success(title, **options, &block)
75
- toast(:success, title, **options, &block)
73
+ def toast_success(title, **options)
74
+ toast(:success, title, **options)
76
75
  end
77
76
 
78
77
  # Render an error toast
@@ -80,8 +79,8 @@ module MaquinaComponents
80
79
  # @param title [String] Toast title
81
80
  # @param options [Hash] Additional options
82
81
  # @return [String] HTML-safe toast element
83
- def toast_error(title, **options, &block)
84
- toast(:error, title, **options, &block)
82
+ def toast_error(title, **options)
83
+ toast(:error, title, **options)
85
84
  end
86
85
 
87
86
  # Render a warning toast
@@ -89,8 +88,8 @@ module MaquinaComponents
89
88
  # @param title [String] Toast title
90
89
  # @param options [Hash] Additional options
91
90
  # @return [String] HTML-safe toast element
92
- def toast_warning(title, **options, &block)
93
- toast(:warning, title, **options, &block)
91
+ def toast_warning(title, **options)
92
+ toast(:warning, title, **options)
94
93
  end
95
94
 
96
95
  # Render an info toast
@@ -98,8 +97,8 @@ module MaquinaComponents
98
97
  # @param title [String] Toast title
99
98
  # @param options [Hash] Additional options
100
99
  # @return [String] HTML-safe toast element
101
- def toast_info(title, **options, &block)
102
- toast(:info, title, **options, &block)
100
+ def toast_info(title, **options)
101
+ toast(:info, title, **options)
103
102
  end
104
103
 
105
104
  private
@@ -28,12 +28,35 @@ export default class extends Controller {
28
28
  }
29
29
 
30
30
  connect() {
31
+ this.boundTeardown = this.teardown.bind(this)
32
+
31
33
  this.setupPopoverEvents()
32
34
  this.updateDisplay()
35
+ document.addEventListener("turbo:before-cache", this.boundTeardown)
33
36
  }
34
37
 
35
38
  disconnect() {
36
39
  this.teardownPopoverEvents()
40
+ document.removeEventListener("turbo:before-cache", this.boundTeardown)
41
+ }
42
+
43
+ /**
44
+ * Reset to closed state before Turbo caches the page
45
+ */
46
+ teardown() {
47
+ if (this.hasPopoverTarget) {
48
+ try {
49
+ if (this.popoverTarget.matches(":popover-open")) {
50
+ this.popoverTarget.hidePopover()
51
+ }
52
+ } catch {
53
+ // Popover API not supported
54
+ }
55
+ }
56
+
57
+ if (this.hasTriggerTarget) {
58
+ this.triggerTarget.setAttribute("aria-expanded", "false")
59
+ }
37
60
  }
38
61
 
39
62
  /**
@@ -15,19 +15,25 @@ export default class extends Controller {
15
15
  static targets = ["trigger", "content", "chevron"]
16
16
 
17
17
  static values = {
18
- open: { type: Boolean, default: false }
18
+ open: { type: Boolean, default: false },
19
+ autoClose: { type: Boolean, default: false }
19
20
  }
20
21
 
21
22
  connect() {
22
23
  this.handleClickOutside = this.handleClickOutside.bind(this)
23
24
  this.handleKeydown = this.handleKeydown.bind(this)
25
+ this.boundTeardown = this.teardown.bind(this)
24
26
 
25
27
  // Set initial state on root element
26
28
  this.element.dataset.state = "closed"
29
+ this.element.addEventListener("click", this.handleItemClick)
30
+ document.addEventListener("turbo:before-cache", this.boundTeardown)
27
31
  }
28
32
 
29
33
  disconnect() {
30
34
  this.removeEventListeners()
35
+ this.element.removeEventListener("click", this.handleItemClick)
36
+ document.removeEventListener("turbo:before-cache", this.boundTeardown)
31
37
  }
32
38
 
33
39
  toggle(event) {
@@ -71,7 +77,8 @@ export default class extends Controller {
71
77
  // Wait for animation to complete
72
78
  const animationDuration = 100 // matches CSS animation duration
73
79
 
74
- setTimeout(() => {
80
+ this._closeTimeout = setTimeout(() => {
81
+ this._closeTimeout = null
75
82
  this.openValue = false
76
83
  this.element.dataset.state = "closed"
77
84
  this.contentTarget.dataset.state = "closed"
@@ -92,6 +99,39 @@ export default class extends Controller {
92
99
  }, animationDuration)
93
100
  }
94
101
 
102
+ // Turbo Cache Teardown
103
+
104
+ teardown() {
105
+ if (this._closeTimeout) {
106
+ clearTimeout(this._closeTimeout)
107
+ this._closeTimeout = null
108
+ }
109
+
110
+ this.openValue = false
111
+ this.element.dataset.state = "closed"
112
+
113
+ if (this.hasContentTarget) {
114
+ this.contentTarget.dataset.state = "closed"
115
+ this.contentTarget.hidden = true
116
+ }
117
+
118
+ if (this.hasTriggerTarget) {
119
+ this.triggerTarget.setAttribute("aria-expanded", "false")
120
+ }
121
+
122
+ this.removeEventListeners()
123
+ }
124
+
125
+ handleItemClick = (event) => {
126
+ if (!this.autoCloseValue || !this.openValue) return
127
+
128
+ const item = event.target.closest('[data-dropdown-menu-part="item"]')
129
+ if (!item) return
130
+ if (item.disabled || item.getAttribute("aria-disabled") === "true") return
131
+
132
+ this.teardown()
133
+ }
134
+
95
135
  // Event Handlers
96
136
 
97
137
  handleClickOutside(event) {
@@ -33,6 +33,9 @@ export default class extends Controller {
33
33
 
34
34
  // Track if we're on mobile
35
35
  this._isMobile = null
36
+
37
+ // Guard flag for morph-triggered attribute changes
38
+ this._morphing = false
36
39
  }
37
40
 
38
41
  connect() {
@@ -43,12 +46,62 @@ export default class extends Controller {
43
46
  this.resizeHandler = this.debounce(this.checkScreenSize.bind(this), 150)
44
47
  window.addEventListener("resize", this.resizeHandler)
45
48
 
49
+ this.boundTeardown = this.teardown.bind(this)
50
+ this.boundBeforeMorphElement = this.beforeMorphElement.bind(this)
51
+ this.boundHandleMorph = this.handleMorph.bind(this)
52
+ document.addEventListener("turbo:before-cache", this.boundTeardown)
53
+ document.addEventListener("turbo:before-morph-element", this.boundBeforeMorphElement)
54
+ document.addEventListener("turbo:morph", this.boundHandleMorph)
55
+
46
56
  // Apply initial state without animation
47
57
  this.updateStateImmediate()
48
58
  }
49
59
 
50
60
  disconnect() {
51
61
  window.removeEventListener("resize", this.resizeHandler)
62
+ document.removeEventListener("turbo:before-cache", this.boundTeardown)
63
+ document.removeEventListener("turbo:before-morph-element", this.boundBeforeMorphElement)
64
+ document.removeEventListener("turbo:morph", this.boundHandleMorph)
65
+ }
66
+
67
+ // ============================================================================
68
+ // Turbo Cache Teardown
69
+ // ============================================================================
70
+
71
+ teardown() {
72
+ if (this.isMobile()) {
73
+ this.openValue = false
74
+ }
75
+
76
+ if (this.hasBackdropTarget) {
77
+ this.backdropTarget.setAttribute("data-state", "hidden")
78
+ this.backdropTarget.classList.add("hidden")
79
+ }
80
+
81
+ this.unlockScroll()
82
+ }
83
+
84
+ beforeMorphElement(event) {
85
+ if (event.target === this.element) {
86
+ this._morphing = true
87
+ }
88
+ }
89
+
90
+ handleMorph() {
91
+ const cookieValue = this.getCookie(this.cookieNameValue)
92
+
93
+ if (this.isMobile()) {
94
+ this.openValue = false
95
+ } else if (cookieValue !== null) {
96
+ this.openValue = cookieValue === "true"
97
+ }
98
+
99
+ if (this.hasSidebarTarget) {
100
+ this.sidebarTarget.classList.remove("sidebar-loading")
101
+ }
102
+
103
+ this.updateState()
104
+ this._morphing = false
52
105
  }
53
106
 
54
107
  // ============================================================================
@@ -70,6 +123,7 @@ export default class extends Controller {
70
123
 
71
124
  openValueChanged(new_value, old_value) {
72
125
  if (new_value === old_value) return
126
+ if (this._morphing) return
73
127
 
74
128
  this.updateState()
75
129
  this.persistState()
@@ -1,8 +1,9 @@
1
- <%# locals: (css_classes: "", **html_options) %>
1
+ <%# locals: (auto_close: false, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  component: "dropdown-menu",
4
4
  controller: "dropdown-menu"
5
5
  ) %>
6
+ <% merged_data["dropdown-menu-auto-close-value"] = true if auto_close %>
6
7
 
7
8
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
8
9
  <%= yield %>
@@ -1,5 +1,5 @@
1
1
  <%# locals: (id: nil, state: :collapsed, collapsible: :offcanvas, variant: :inset, side: :left, css_classes: "", **html_options) %>
2
- <% random_id = id || "sidebar-#{SecureRandom.hex(6)}"
2
+ <% stable_id = id || "sidebar-#{side}"
3
3
 
4
4
  merged_data = (html_options.delete(:data) || {}).merge(
5
5
  sidebar_part: :root,
@@ -11,16 +11,16 @@
11
11
  ) %>
12
12
 
13
13
  <aside
14
- id="<%= random_id %>"
14
+ id="<%= stable_id %>"
15
15
  class="group peer sidebar-loading <%= css_classes %>"
16
16
  <%= tag.attributes(data: merged_data, **html_options) %>
17
17
  >
18
18
  <%# Sidebar gap (creates space for sidebar on desktop) %>
19
- <div id="<%= random_id %>-gap" data-sidebar-part="gap"></div>
19
+ <div id="<%= stable_id %>-gap" data-sidebar-part="gap"></div>
20
20
 
21
21
  <%# Mobile backdrop overlay %>
22
22
  <div
23
- id="<%= random_id %>-overlay"
23
+ id="<%= stable_id %>-overlay"
24
24
  data-sidebar-part="backdrop"
25
25
  data-sidebar-target="backdrop"
26
26
  data-action="click->sidebar#backdropClick"
@@ -28,12 +28,12 @@
28
28
 
29
29
  <%# Sidebar container (fixed positioned) %>
30
30
  <div
31
- id="<%= random_id %>-container"
31
+ id="<%= stable_id %>-container"
32
32
  data-sidebar-part="container"
33
33
  data-sidebar-target="container"
34
34
  >
35
35
  <%# Inner sidebar content wrapper %>
36
- <div id="<%= random_id %>-inner" data-sidebar-part="inner">
36
+ <div id="<%= stable_id %>-inner" data-sidebar-part="inner">
37
37
  <%= yield %>
38
38
  </div>
39
39
  </div>
@@ -1,4 +1,4 @@
1
- <%# locals: (variant: :default, title: nil, description: nil, icon: nil, duration: 5000, dismissible: true, css_classes: "", **html_options) %>
1
+ <%# locals: (variant: :default, title: nil, description: nil, icon: nil, duration: 5000, dismissible: true, content: nil, css_classes: "", **html_options) %>
2
2
  <%
3
3
  # Auto-select icon based on variant if not provided
4
4
  default_icons = {
@@ -39,7 +39,7 @@
39
39
  <%= render "components/toast/description", text: description %>
40
40
  <% end %>
41
41
 
42
- <%= yield if block_given? %>
42
+ <%= content if content %>
43
43
  </div>
44
44
 
45
45
  <% if dismissible %>
@@ -1,4 +1,4 @@
1
- <%# locals: (position: :bottom_right, css_classes: "", **html_options) %>
1
+ <%# locals: (position: :bottom_right, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  component: :toaster,
4
4
  controller: "toaster",
@@ -13,5 +13,5 @@
13
13
  class: css_classes.presence,
14
14
  data: merged_data,
15
15
  **html_options do %>
16
- <%= yield if block_given? %>
16
+ <%= content if content %>
17
17
  <% end %>
@@ -1,6 +1,6 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(alert_part: :description) %>
3
3
 
4
4
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
- <%= text || yield %>
5
+ <%= text || content %>
6
6
  <% end %>
@@ -1,6 +1,6 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(alert_part: :title) %>
3
3
 
4
4
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
- <%= text || yield %>
5
+ <%= text || content %>
6
6
  <% end %>
@@ -1,6 +1,6 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(card_part: :description) %>
3
3
 
4
4
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
- <%= text || yield %>
5
+ <%= text || content %>
6
6
  <% end %>
@@ -1,9 +1,9 @@
1
- <%# locals: (text: nil, size: :default, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, size: :default, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  card_part: :title,
4
4
  size: (size == :sm ? :sm : nil)
5
5
  ).compact %>
6
6
 
7
7
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
8
- <%= text || yield %>
8
+ <%= text || content %>
9
9
  <% end %>
@@ -1,8 +1,8 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  combobox_part: "label"
4
4
  ) %>
5
5
 
6
6
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
7
- <%= text || yield %>
7
+ <%= text || content %>
8
8
  <% end %>
@@ -1,4 +1,4 @@
1
- <%# locals: (default_open: true, variant: :inset, css_classes: "", cookie_name: "sidebar_state", keyboard_shortcut: "b", **html_options) %>
1
+ <%# locals: (id: nil, default_open: true, variant: :inset, css_classes: "", cookie_name: "sidebar_state", keyboard_shortcut: "b", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  component: :sidebar,
4
4
  variant: variant,
@@ -11,6 +11,6 @@
11
11
  action: "keydown.meta+#{keyboard_shortcut}@window->sidebar#toggleWithKeyboard keydown.ctrl+#{keyboard_shortcut}@window->sidebar#toggleWithKeyboard"
12
12
  ) %>
13
13
 
14
- <%= content_tag :div, class: css_classes, data: merged_data, **html_options do %>
14
+ <%= content_tag :div, id: id || "sidebar-provider", class: css_classes, data: merged_data, **html_options do %>
15
15
  <%= yield %>
16
16
  <% end %>
@@ -1,8 +1,8 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  toast_part: "description"
4
4
  ) %>
5
5
 
6
6
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
7
- <%= text || yield %>
7
+ <%= text || content %>
8
8
  <% end %>
@@ -1,8 +1,8 @@
1
- <%# locals: (text: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (text: nil, content: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = (html_options.delete(:data) || {}).merge(
3
3
  toast_part: "title"
4
4
  ) %>
5
5
 
6
6
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
7
- <%= text || yield %>
7
+ <%= text || content %>
8
8
  <% end %>
@@ -1,3 +1,3 @@
1
1
  module MaquinaComponents
2
- VERSION = "0.3.1.1"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maquina-components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chávez