maquina-components 0.4.1 → 0.4.3
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 +4 -4
- data/app/assets/stylesheets/breadcrumbs.css +23 -0
- data/app/helpers/maquina_components/icons_helper.rb +22 -5
- data/app/javascript/controllers/breadcrumb_controller.js +166 -35
- data/app/views/components/_date_picker.html.erb +1 -1
- data/app/views/components/_menu_button.html.erb +1 -1
- data/app/views/components/_toast.html.erb +2 -2
- data/app/views/components/breadcrumbs/_ellipsis.html.erb +1 -1
- data/app/views/components/calendar/_header.html.erb +2 -2
- data/app/views/components/combobox/_input.html.erb +1 -1
- data/app/views/components/combobox/_option.html.erb +1 -1
- data/app/views/components/combobox/_trigger.html.erb +1 -1
- data/app/views/components/dropdown_menu/_trigger.html.erb +1 -1
- data/lib/maquina_components/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b709941be68c552761043a38b7359796b7bc644af13827fbd47b2f40eab90fb9
|
|
4
|
+
data.tar.gz: 931d38949a68e47881e9dc417786a412e5fb16ed02a23539d44259f452249913
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b063e95e3ea3c7d4fd649b75cd00696ac33ac88025c3ebab14bea256f7456005a81b8e0c7f3b70f8904f73cc987c4d5cc22a707cbef48f3d10dafa21a7d9ec1
|
|
7
|
+
data.tar.gz: 80b31cd01a92351baedec4a87509b494c6dbfe09cb42bb71c7a59444cca0cc48e4b020684d9c450c1b02568a352f0b2781654c5333a84a0c5af7ea38940f91a6
|
|
@@ -107,6 +107,29 @@
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/* ===== Responsive Behavior ===== */
|
|
110
|
+
/* Force nowrap when responsive controller is active so scrollWidth detects overflow */
|
|
111
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="list"] {
|
|
112
|
+
@apply flex-nowrap overflow-hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Items must not shrink or wrap text, otherwise flex compresses them and scrollWidth never exceeds clientWidth */
|
|
116
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="item"],
|
|
117
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="separator"] {
|
|
118
|
+
@apply shrink-0 whitespace-nowrap;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Allow the last item (current page) to truncate gracefully instead of hard-clipping */
|
|
122
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child {
|
|
123
|
+
@apply shrink min-w-0 overflow-hidden;
|
|
124
|
+
text-overflow: ellipsis;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child [data-breadcrumb-part="page"],
|
|
128
|
+
[data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child [data-breadcrumb-part="link"] {
|
|
129
|
+
@apply overflow-hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
/* Items hidden by Stimulus controller */
|
|
111
134
|
[data-controller="breadcrumb"] [data-breadcrumb-target="item"].hidden,
|
|
112
135
|
[data-controller="breadcrumb"] [data-breadcrumb-target="ellipsisSeparator"].hidden {
|
|
@@ -3,11 +3,31 @@ module MaquinaComponents
|
|
|
3
3
|
def icon_for(name, options = {})
|
|
4
4
|
return nil unless name
|
|
5
5
|
|
|
6
|
-
svg =
|
|
6
|
+
svg = main_icon_svg_for(name.to_sym) || icon_svg_for(name.to_sym)
|
|
7
7
|
return nil unless svg
|
|
8
8
|
|
|
9
|
+
apply_icon_options(svg, options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Internal icon helper for engine components. Always uses built-in SVGs
|
|
13
|
+
# so components work reliably regardless of the app's icon configuration.
|
|
14
|
+
def builtin_icon_for(name, options = {})
|
|
15
|
+
return nil unless name
|
|
16
|
+
|
|
17
|
+
svg = icon_svg_for(name.to_sym)
|
|
18
|
+
return nil unless svg
|
|
19
|
+
|
|
20
|
+
apply_icon_options(svg, options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def main_icon_svg_for(name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def apply_icon_options(svg, options)
|
|
9
29
|
css_classes = options[:class]
|
|
10
|
-
svg = svg.gsub('class="', "class=\"#{css_classes} ")
|
|
30
|
+
svg = svg.gsub('class="', "class=\"#{css_classes} ") if css_classes
|
|
11
31
|
|
|
12
32
|
if options[:stroke_width]
|
|
13
33
|
svg = svg.gsub('stroke-width="2"', "stroke-width=\"#{options[:stroke_width]}\"")
|
|
@@ -16,9 +36,6 @@ module MaquinaComponents
|
|
|
16
36
|
svg.html_safe
|
|
17
37
|
end
|
|
18
38
|
|
|
19
|
-
def main_icon_svg_for(name)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
39
|
def icon_svg_for(name)
|
|
23
40
|
case name
|
|
24
41
|
when :dollar
|
|
@@ -4,6 +4,13 @@ export default class extends Controller {
|
|
|
4
4
|
static targets = ["item", "ellipsis", "ellipsisSeparator"]
|
|
5
5
|
|
|
6
6
|
connect() {
|
|
7
|
+
this._dropdown = null
|
|
8
|
+
this._clickOutsideHandler = this._closeDropdown.bind(this)
|
|
9
|
+
this._escapeHandler = this._handleEscape.bind(this)
|
|
10
|
+
this._teardownHandler = this._teardown.bind(this)
|
|
11
|
+
|
|
12
|
+
document.addEventListener("turbo:before-cache", this._teardownHandler)
|
|
13
|
+
|
|
7
14
|
this.windowResizeHandler = this.handleResize.bind(this)
|
|
8
15
|
window.addEventListener('resize', this.windowResizeHandler)
|
|
9
16
|
this.handleResize()
|
|
@@ -11,61 +18,185 @@ export default class extends Controller {
|
|
|
11
18
|
|
|
12
19
|
disconnect() {
|
|
13
20
|
window.removeEventListener('resize', this.windowResizeHandler)
|
|
21
|
+
this._teardown()
|
|
22
|
+
document.removeEventListener("turbo:before-cache", this._teardownHandler)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ellipsisTargetConnected(element) {
|
|
26
|
+
const trigger = element.querySelector('[data-breadcrumb-part="ellipsis"]')
|
|
27
|
+
if (trigger) {
|
|
28
|
+
this._ellipsisTrigger = trigger
|
|
29
|
+
this._toggleHandler = this._toggleDropdown.bind(this)
|
|
30
|
+
trigger.addEventListener('click', this._toggleHandler)
|
|
31
|
+
trigger.style.cursor = 'pointer'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ellipsisTargetDisconnected(_element) {
|
|
36
|
+
if (this._ellipsisTrigger && this._toggleHandler) {
|
|
37
|
+
this._ellipsisTrigger.removeEventListener('click', this._toggleHandler)
|
|
38
|
+
this._ellipsisTrigger = null
|
|
39
|
+
this._toggleHandler = null
|
|
40
|
+
}
|
|
14
41
|
}
|
|
15
42
|
|
|
16
43
|
handleResize() {
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
const list = this.element.querySelector('[data-breadcrumb-part="list"]')
|
|
45
|
+
if (!list) return
|
|
46
|
+
|
|
19
47
|
const items = this.itemTargets
|
|
20
48
|
const ellipsis = this.hasEllipsisTarget ? this.ellipsisTarget : null
|
|
21
49
|
const ellipsisSeparator = this.hasEllipsisSeparatorTarget ? this.ellipsisSeparatorTarget : null
|
|
22
50
|
|
|
23
|
-
|
|
24
|
-
if (items.length < 3 || !ellipsis) {
|
|
25
|
-
return; // Not enough items to collapse or no ellipsis element
|
|
26
|
-
}
|
|
51
|
+
if (items.length < 1 || !ellipsis) return
|
|
27
52
|
|
|
28
|
-
// Reset
|
|
29
|
-
|
|
53
|
+
// Reset all items and their adjacent separators to visible
|
|
54
|
+
ellipsis.classList.add('hidden')
|
|
30
55
|
if (ellipsisSeparator) ellipsisSeparator.classList.add('hidden')
|
|
31
|
-
|
|
32
56
|
items.forEach(item => {
|
|
33
57
|
item.classList.remove('hidden')
|
|
58
|
+
const sep = this._adjacentSeparator(item)
|
|
59
|
+
if (sep) sep.classList.remove('hidden')
|
|
34
60
|
})
|
|
35
61
|
|
|
36
|
-
// Check
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
totalWidth += item.offsetWidth
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
if (totalWidth > containerWidth) {
|
|
43
|
-
// We need to collapse items - show ellipsis
|
|
44
|
-
if (ellipsis) ellipsis.classList.remove('hidden')
|
|
62
|
+
// Check overflow using scrollWidth vs clientWidth
|
|
63
|
+
if (list.scrollWidth > list.clientWidth) {
|
|
64
|
+
ellipsis.classList.remove('hidden')
|
|
45
65
|
if (ellipsisSeparator) ellipsisSeparator.classList.remove('hidden')
|
|
46
66
|
|
|
47
|
-
//
|
|
48
|
-
for (let i = items.length -
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
// Hide middle items one at a time until it fits
|
|
68
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
69
|
+
items[i].classList.add('hidden')
|
|
70
|
+
const sep = this._adjacentSeparator(items[i])
|
|
71
|
+
if (sep) sep.classList.add('hidden')
|
|
72
|
+
if (list.scrollWidth <= list.clientWidth) break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
this._updateDropdown()
|
|
77
|
+
}
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
// Find the next sibling separator <li> (not the managed ellipsisSeparator)
|
|
80
|
+
_adjacentSeparator(item) {
|
|
81
|
+
const next = item.nextElementSibling
|
|
82
|
+
if (next && next.dataset.breadcrumbPart === "separator" && !next.dataset.breadcrumbTarget) {
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Collect hidden items and update dropdown content
|
|
89
|
+
_updateDropdown() {
|
|
90
|
+
const hiddenItems = this.itemTargets.filter(item => item.classList.contains('hidden'))
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
92
|
+
if (hiddenItems.length === 0) {
|
|
93
|
+
this._removeDropdown()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
// Build list of links from hidden items
|
|
98
|
+
this._hiddenLinks = hiddenItems.map(item => {
|
|
99
|
+
const link = item.querySelector('[data-breadcrumb-part="link"]')
|
|
100
|
+
if (link) {
|
|
101
|
+
return { href: link.getAttribute('href'), text: link.textContent.trim() }
|
|
68
102
|
}
|
|
103
|
+
return null
|
|
104
|
+
}).filter(Boolean)
|
|
105
|
+
|
|
106
|
+
// If dropdown is currently open, rebuild its content
|
|
107
|
+
if (this._dropdown && this._dropdown.dataset.state === "open") {
|
|
108
|
+
this._buildDropdownContent()
|
|
69
109
|
}
|
|
70
110
|
}
|
|
111
|
+
|
|
112
|
+
_toggleDropdown(event) {
|
|
113
|
+
event.stopPropagation()
|
|
114
|
+
|
|
115
|
+
if (this._dropdown && this._dropdown.dataset.state === "open") {
|
|
116
|
+
this._closeDropdown()
|
|
117
|
+
} else {
|
|
118
|
+
this._openDropdown()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_openDropdown() {
|
|
123
|
+
if (!this._hiddenLinks || this._hiddenLinks.length === 0) return
|
|
124
|
+
|
|
125
|
+
if (!this._dropdown) {
|
|
126
|
+
this._dropdown = document.createElement('div')
|
|
127
|
+
this._dropdown.setAttribute('role', 'menu')
|
|
128
|
+
this._dropdown.dataset.dropdownMenuPart = 'content'
|
|
129
|
+
this._dropdown.style.position = 'fixed'
|
|
130
|
+
this._dropdown.style.zIndex = '50'
|
|
131
|
+
document.body.appendChild(this._dropdown)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._buildDropdownContent()
|
|
135
|
+
this._positionDropdown()
|
|
136
|
+
this._dropdown.dataset.state = 'open'
|
|
137
|
+
|
|
138
|
+
if (this._ellipsisTrigger) {
|
|
139
|
+
this._ellipsisTrigger.dataset.state = 'open'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Defer listeners so the current click doesn't immediately close
|
|
143
|
+
requestAnimationFrame(() => {
|
|
144
|
+
document.addEventListener('click', this._clickOutsideHandler)
|
|
145
|
+
document.addEventListener('keydown', this._escapeHandler)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_closeDropdown() {
|
|
150
|
+
if (this._dropdown) {
|
|
151
|
+
this._dropdown.dataset.state = 'closed'
|
|
152
|
+
}
|
|
153
|
+
if (this._ellipsisTrigger) {
|
|
154
|
+
delete this._ellipsisTrigger.dataset.state
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.removeEventListener('click', this._clickOutsideHandler)
|
|
158
|
+
document.removeEventListener('keydown', this._escapeHandler)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_removeDropdown() {
|
|
162
|
+
this._closeDropdown()
|
|
163
|
+
if (this._dropdown) {
|
|
164
|
+
this._dropdown.remove()
|
|
165
|
+
this._dropdown = null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_buildDropdownContent() {
|
|
170
|
+
if (!this._dropdown || !this._hiddenLinks) return
|
|
171
|
+
|
|
172
|
+
this._dropdown.innerHTML = ''
|
|
173
|
+
this._hiddenLinks.forEach(({ href, text }) => {
|
|
174
|
+
const link = document.createElement('a')
|
|
175
|
+
link.setAttribute('href', href)
|
|
176
|
+
link.setAttribute('role', 'menuitem')
|
|
177
|
+
link.dataset.dropdownMenuPart = 'item'
|
|
178
|
+
link.setAttribute('tabindex', '-1')
|
|
179
|
+
link.textContent = text
|
|
180
|
+
this._dropdown.appendChild(link)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_positionDropdown() {
|
|
185
|
+
if (!this._dropdown || !this._ellipsisTrigger) return
|
|
186
|
+
|
|
187
|
+
const rect = this._ellipsisTrigger.getBoundingClientRect()
|
|
188
|
+
this._dropdown.style.top = `${rect.bottom + 4}px`
|
|
189
|
+
this._dropdown.style.left = `${rect.left}px`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_handleEscape(event) {
|
|
193
|
+
if (event.key === 'Escape') {
|
|
194
|
+
this._closeDropdown()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_teardown() {
|
|
199
|
+
this._removeDropdown()
|
|
200
|
+
this._hiddenLinks = null
|
|
201
|
+
}
|
|
71
202
|
}
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
<%= "disabled" if disabled %>
|
|
69
69
|
aria-haspopup="dialog"
|
|
70
70
|
aria-expanded="false">
|
|
71
|
-
<%=
|
|
71
|
+
<%= builtin_icon_for :calendar, class: "size-4" %>
|
|
72
72
|
<span data-date-picker-target="display">
|
|
73
73
|
<%= display_value || placeholder || default_placeholder %>
|
|
74
74
|
</span>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
**html_options do %>
|
|
27
27
|
<% if toast_icon %>
|
|
28
28
|
<div data-toast-part="icon">
|
|
29
|
-
<%=
|
|
29
|
+
<%= builtin_icon_for toast_icon, class: "size-5" %>
|
|
30
30
|
</div>
|
|
31
31
|
<% end %>
|
|
32
32
|
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
data-toast-part="close"
|
|
48
48
|
data-action="toast#dismiss"
|
|
49
49
|
aria-label="Dismiss notification">
|
|
50
|
-
<%=
|
|
50
|
+
<%= builtin_icon_for :x, class: "size-4" %>
|
|
51
51
|
</button>
|
|
52
52
|
<% end %>
|
|
53
53
|
<% end %>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
data-action="click->calendar#previousMonth"
|
|
7
7
|
data-calendar-target="prevButton"
|
|
8
8
|
aria-label="Previous month">
|
|
9
|
-
<%=
|
|
9
|
+
<%= builtin_icon_for :chevron_left, class: "size-4" %>
|
|
10
10
|
</button>
|
|
11
11
|
|
|
12
12
|
<div data-calendar-part="caption">
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
data-action="click->calendar#nextMonth"
|
|
18
18
|
data-calendar-target="nextButton"
|
|
19
19
|
aria-label="Next month">
|
|
20
|
-
<%=
|
|
20
|
+
<%= builtin_icon_for :chevron_right, class: "size-4" %>
|
|
21
21
|
</button>
|
|
22
22
|
<% end %>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
) %>
|
|
7
7
|
|
|
8
8
|
<div data-combobox-part="input-wrapper">
|
|
9
|
-
<%=
|
|
9
|
+
<%= builtin_icon_for :search, class: "size-4 shrink-0 opacity-50" %>
|
|
10
10
|
<%= tag.input type: "text",
|
|
11
11
|
placeholder: placeholder,
|
|
12
12
|
autocomplete: "off",
|
|
@@ -18,5 +18,5 @@
|
|
|
18
18
|
data: merged_data,
|
|
19
19
|
**html_options do %>
|
|
20
20
|
<span data-combobox-target="label"><%= placeholder %></span>
|
|
21
|
-
<%=
|
|
21
|
+
<%= builtin_icon_for :chevrons_up_down, class: "size-4 shrink-0 opacity-50" %>
|
|
22
22
|
<% end %>
|
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
aria: { haspopup: "menu", expanded: "false" },
|
|
20
20
|
**html_options do %>
|
|
21
21
|
<%= yield %>
|
|
22
|
-
<%=
|
|
22
|
+
<%= builtin_icon_for :chevron_down, data: { "dropdown-menu-target": "chevron" } %>
|
|
23
23
|
<% end %>
|
|
24
24
|
<% end %>
|