maquina-components 0.1.1 → 0.2.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +410 -13
  3. data/app/assets/images/maquina.svg +1 -0
  4. data/app/assets/stylesheets/alert.css +143 -0
  5. data/app/assets/stylesheets/badge.css +145 -0
  6. data/app/assets/stylesheets/breadcrumbs.css +163 -0
  7. data/app/assets/stylesheets/card.css +128 -0
  8. data/app/assets/stylesheets/dropdown_menu.css +248 -0
  9. data/app/assets/stylesheets/empty.css +133 -0
  10. data/app/assets/stylesheets/form.css +617 -0
  11. data/app/assets/stylesheets/header.css +61 -0
  12. data/app/assets/stylesheets/maquina_components.css +178 -0
  13. data/app/assets/stylesheets/pagination.css +154 -0
  14. data/app/assets/stylesheets/sidebar.css +477 -0
  15. data/app/assets/stylesheets/table.css +205 -0
  16. data/app/assets/stylesheets/toggle_group.css +151 -0
  17. data/app/assets/tailwind/maquina_components_engine/engine.css +16 -0
  18. data/app/helpers/maquina_components/breadcrumbs_helper.rb +118 -0
  19. data/app/helpers/maquina_components/dropdown_menu_helper.rb +249 -0
  20. data/app/helpers/maquina_components/empty_helper.rb +102 -0
  21. data/app/helpers/maquina_components/icons_helper.rb +161 -0
  22. data/app/helpers/maquina_components/pagination_helper.rb +153 -0
  23. data/app/helpers/maquina_components/sidebar_helper.rb +63 -0
  24. data/app/helpers/maquina_components/table_helper.rb +144 -0
  25. data/app/helpers/maquina_components/toggle_group_helper.rb +172 -0
  26. data/app/javascript/controllers/breadcrumb_controller.js +71 -0
  27. data/app/javascript/controllers/dropdown_menu_controller.js +203 -0
  28. data/app/javascript/controllers/menu_button_controller.js +59 -0
  29. data/app/javascript/controllers/sidebar_controller.js +316 -0
  30. data/app/javascript/controllers/sidebar_trigger_controller.js +32 -0
  31. data/app/javascript/controllers/toggle_group_controller.js +178 -0
  32. data/app/views/components/_alert.html.erb +12 -0
  33. data/app/views/components/_badge.html.erb +10 -0
  34. data/app/views/components/_breadcrumbs.html.erb +16 -0
  35. data/app/views/components/_card.html.erb +6 -0
  36. data/app/views/components/_dropdown.html.erb +25 -0
  37. data/app/views/components/_dropdown_menu.html.erb +9 -0
  38. data/app/views/components/_empty.html.erb +10 -0
  39. data/app/views/components/_header.html.erb +8 -0
  40. data/app/views/components/_menu_button.html.erb +44 -0
  41. data/app/views/components/_pagination.html.erb +13 -0
  42. data/app/views/components/_separator.html.erb +11 -0
  43. data/app/views/components/_sidebar.html.erb +40 -0
  44. data/app/views/components/_simple_table.html.erb +49 -0
  45. data/app/views/components/_table.html.erb +21 -0
  46. data/app/views/components/_toggle_group.html.erb +24 -0
  47. data/app/views/components/alert/_description.html.erb +6 -0
  48. data/app/views/components/alert/_title.html.erb +6 -0
  49. data/app/views/components/breadcrumbs/_ellipsis.html.erb +9 -0
  50. data/app/views/components/breadcrumbs/_item.html.erb +8 -0
  51. data/app/views/components/breadcrumbs/_link.html.erb +8 -0
  52. data/app/views/components/breadcrumbs/_list.html.erb +8 -0
  53. data/app/views/components/breadcrumbs/_page.html.erb +8 -0
  54. data/app/views/components/breadcrumbs/_separator.html.erb +17 -0
  55. data/app/views/components/card/_action.html.erb +6 -0
  56. data/app/views/components/card/_content.html.erb +9 -0
  57. data/app/views/components/card/_description.html.erb +6 -0
  58. data/app/views/components/card/_footer.html.erb +17 -0
  59. data/app/views/components/card/_header.html.erb +9 -0
  60. data/app/views/components/card/_title.html.erb +9 -0
  61. data/app/views/components/dropdown_menu/_content.html.erb +20 -0
  62. data/app/views/components/dropdown_menu/_group.html.erb +12 -0
  63. data/app/views/components/dropdown_menu/_item.html.erb +29 -0
  64. data/app/views/components/dropdown_menu/_label.html.erb +13 -0
  65. data/app/views/components/dropdown_menu/_separator.html.erb +11 -0
  66. data/app/views/components/dropdown_menu/_shortcut.html.erb +12 -0
  67. data/app/views/components/dropdown_menu/_trigger.html.erb +24 -0
  68. data/app/views/components/empty/_content.html.erb +8 -0
  69. data/app/views/components/empty/_description.html.erb +12 -0
  70. data/app/views/components/empty/_header.html.erb +8 -0
  71. data/app/views/components/empty/_media.html.erb +13 -0
  72. data/app/views/components/empty/_title.html.erb +12 -0
  73. data/app/views/components/pagination/_content.html.erb +8 -0
  74. data/app/views/components/pagination/_ellipsis.html.erb +28 -0
  75. data/app/views/components/pagination/_item.html.erb +8 -0
  76. data/app/views/components/pagination/_link.html.erb +23 -0
  77. data/app/views/components/pagination/_next.html.erb +57 -0
  78. data/app/views/components/pagination/_previous.html.erb +57 -0
  79. data/app/views/components/sidebar/_content.html.erb +8 -0
  80. data/app/views/components/sidebar/_footer.html.erb +8 -0
  81. data/app/views/components/sidebar/_group.html.erb +12 -0
  82. data/app/views/components/sidebar/_header.html.erb +8 -0
  83. data/app/views/components/sidebar/_inset.html.erb +8 -0
  84. data/app/views/components/sidebar/_menu.html.erb +8 -0
  85. data/app/views/components/sidebar/_menu_button.html.erb +14 -0
  86. data/app/views/components/sidebar/_menu_item.html.erb +7 -0
  87. data/app/views/components/sidebar/_menu_link.html.erb +32 -0
  88. data/app/views/components/sidebar/_provider.html.erb +16 -0
  89. data/app/views/components/sidebar/_trigger.html.erb +12 -0
  90. data/app/views/components/stats/_stats_card.html.erb +100 -0
  91. data/app/views/components/stats/_stats_grid.html.erb +38 -0
  92. data/app/views/components/table/_body.html.erb +5 -0
  93. data/app/views/components/table/_caption.html.erb +5 -0
  94. data/app/views/components/table/_cell.html.erb +5 -0
  95. data/app/views/components/table/_footer.html.erb +5 -0
  96. data/app/views/components/table/_head.html.erb +8 -0
  97. data/app/views/components/table/_header.html.erb +8 -0
  98. data/app/views/components/table/_row.html.erb +8 -0
  99. data/app/views/components/toggle_group/_item.html.erb +19 -0
  100. data/config/importmap.rb +1 -0
  101. data/lib/generators/maquina_components/install/USAGE +39 -0
  102. data/lib/generators/maquina_components/install/install_generator.rb +123 -0
  103. data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +68 -0
  104. data/lib/generators/maquina_components/install/templates/theme.css.tt +179 -0
  105. data/lib/maquina_components/engine.rb +10 -0
  106. data/lib/maquina_components/version.rb +1 -1
  107. metadata +121 -5
@@ -0,0 +1,316 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Sidebar Controller
5
+ *
6
+ * Manages sidebar state, keyboard shortcuts, responsive behavior, and persistence.
7
+ * Works with sidebar_trigger_controller via Stimulus outlets.
8
+ *
9
+ * @example
10
+ * <div data-controller="sidebar" data-outlet="sidebar">
11
+ * <!-- sidebar content -->
12
+ * </div>
13
+ */
14
+ export default class extends Controller {
15
+ static values = {
16
+ open: { type: Boolean, default: true },
17
+ defaultOpen: { type: Boolean, default: true },
18
+ cookieName: { type: String, default: "sidebar_state" },
19
+ cookieMaxAge: { type: Number, default: 60 * 60 * 24 * 365 },
20
+ keyboardShortcut: { type: String, default: "b" }
21
+ }
22
+
23
+ static targets = ["sidebar", "container", "backdrop"]
24
+
25
+ initialize() {
26
+ // Load saved state from cookie or use default
27
+ // Note: Rails backend can set defaultOpen based on cookie value
28
+ // via sidebar_state helper
29
+ const cookieValue = this.getCookie(this.cookieNameValue)
30
+ this.openValue = cookieValue !== null
31
+ ? cookieValue === "true"
32
+ : this.defaultOpenValue
33
+
34
+ // Track if we're on mobile
35
+ this._isMobile = null
36
+ }
37
+
38
+ connect() {
39
+ // Detect initial screen size
40
+ this.checkScreenSize()
41
+
42
+ // Setup resize handler with debounce
43
+ this.resizeHandler = this.debounce(this.checkScreenSize.bind(this), 150)
44
+ window.addEventListener("resize", this.resizeHandler)
45
+
46
+ // Apply initial state without animation
47
+ this.updateStateImmediate()
48
+ }
49
+
50
+ disconnect() {
51
+ window.removeEventListener("resize", this.resizeHandler)
52
+ }
53
+
54
+ // ============================================================================
55
+ // Keyboard Shortcut Actions (Called by Stimulus declarative actions)
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Toggle sidebar via keyboard shortcut
60
+ * Called by data-action keyboard bindings (Cmd/Ctrl+B by default)
61
+ * @param {KeyboardEvent} event
62
+ */
63
+ toggleWithKeyboard(event) {
64
+ this.toggle()
65
+ }
66
+
67
+ // ============================================================================
68
+ // State Management
69
+ // ============================================================================
70
+
71
+ openValueChanged(new_value, old_value) {
72
+ if (new_value === old_value) return
73
+
74
+ this.updateState()
75
+ this.persistState()
76
+ this.dispatchStateChange()
77
+ }
78
+
79
+ updateState() {
80
+ if (!this.hasSidebarTarget) return
81
+
82
+ const isOpen = this.openValue
83
+ const isMobile = this.isMobile()
84
+ const state = isOpen ? "expanded" : "collapsed"
85
+
86
+ // Update sidebar state
87
+ this.sidebarTarget.setAttribute("data-state", state)
88
+
89
+ const collapsible = isOpen
90
+ ? "none"
91
+ : !isMobile
92
+ ? "icon"
93
+ : "offcanvas";
94
+
95
+ // Update sidebar visual state
96
+ this.sidebarTarget.setAttribute("data-collapsible", collapsible)
97
+
98
+ // Update backdrop (only visible on mobile when open)
99
+ if (this.hasBackdropTarget) {
100
+ const backdropState = (isOpen && isMobile) ? "visible" : "hidden"
101
+ this.backdropTarget.setAttribute("data-state", backdropState)
102
+
103
+ // Toggle hidden class for visibility
104
+ if (backdropState === "visible") {
105
+ this.backdropTarget.classList.remove("hidden")
106
+ } else {
107
+ // Wait for transition before hiding
108
+ setTimeout(() => {
109
+ if (this.backdropTarget.getAttribute("data-state") === "hidden") {
110
+ this.backdropTarget.classList.add("hidden")
111
+ }
112
+ }, 300)
113
+ }
114
+ }
115
+
116
+ // Lock/unlock scroll on mobile
117
+ if (isMobile) {
118
+ if (isOpen) {
119
+ this.lockScroll()
120
+ } else {
121
+ this.unlockScroll()
122
+ }
123
+ }
124
+ }
125
+
126
+ updateStateImmediate() {
127
+ // Update state without transitions (for initial render)
128
+ if (!this.hasSidebarTarget) return
129
+
130
+ this.updateState()
131
+
132
+ requestAnimationFrame(() => {
133
+ this.sidebarTarget.classList.remove("sidebar-loading")
134
+ })
135
+ }
136
+
137
+ // ============================================================================
138
+ // Public Actions (Called by triggers or other controllers)
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Toggle sidebar open/closed state
143
+ */
144
+ toggle() {
145
+ this.openValue = !this.openValue
146
+ }
147
+
148
+ /**
149
+ * Open the sidebar
150
+ */
151
+ open() {
152
+ this.openValue = true
153
+ }
154
+
155
+ /**
156
+ * Close the sidebar
157
+ */
158
+ close() {
159
+ this.openValue = false
160
+ }
161
+
162
+ /**
163
+ * Toggle only if on mobile (useful for touch gestures)
164
+ */
165
+ toggleMobile() {
166
+ if (this.isMobile()) {
167
+ this.toggle()
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handle backdrop click (close sidebar on mobile)
173
+ * @param {MouseEvent} event
174
+ */
175
+ backdropClick(event) {
176
+ if (this.isMobile() && this.openValue) {
177
+ this.close()
178
+ }
179
+ }
180
+
181
+ // ============================================================================
182
+ // Responsive Behavior
183
+ // ============================================================================
184
+
185
+ checkScreenSize() {
186
+ const wasMobile = this._isMobile
187
+ this._isMobile = window.innerWidth < 768
188
+
189
+ // First time detecting screen size
190
+ if (wasMobile === null) {
191
+ // On mobile, start closed; on desktop, use saved state
192
+ if (this._isMobile) {
193
+ this.openValue = false
194
+ }
195
+ return
196
+ }
197
+
198
+ // Screen size changed
199
+ if (wasMobile !== this._isMobile) {
200
+ this.handleScreenSizeChange(wasMobile, this._isMobile)
201
+ }
202
+ }
203
+
204
+ handleScreenSizeChange(wasMobile, isMobile) {
205
+ if (this.sidebarTarget.getAttribute("data-collapsible") === "offcanvas") {
206
+ // Mobile to desktop: restore saved state
207
+ if (wasMobile && !isMobile) {
208
+ const cookieValue = this.getCookie(this.cookieNameValue)
209
+ this.openValue = cookieValue !== null
210
+ ? cookieValue === "true"
211
+ : this.defaultOpenValue
212
+ }
213
+ // Desktop to mobile: close sidebar
214
+ else if (!wasMobile && isMobile) {
215
+ this.openValue = false
216
+ }
217
+ } else if (this.sidebarTarget.getAttribute("data-collapsible") === "icon") {
218
+ // Icon mode is always visible, just changes width
219
+ // Desktop to mobile: might want to close
220
+ if (!wasMobile && isMobile) {
221
+ this.openValue = false
222
+ }
223
+ } else if (this.sidebarTarget.getAttribute("data-collapsible") === "none") {
224
+ // Non-collapsible sidebars stay open
225
+ this.openValue = true
226
+ }
227
+
228
+ this.updateState()
229
+ }
230
+
231
+ isMobile() {
232
+ return this._isMobile ?? (window.innerWidth < 768)
233
+ }
234
+
235
+ // ============================================================================
236
+ // Persistence (Cookie Management)
237
+ // ============================================================================
238
+
239
+ persistState() {
240
+ // Only save desktop state to cookie
241
+ if (!this.isMobile()) {
242
+ this.setCookie(
243
+ this.cookieNameValue,
244
+ this.openValue.toString(),
245
+ this.cookieMaxAgeValue
246
+ )
247
+ }
248
+ }
249
+
250
+ getCookie(name) {
251
+ const value = `; ${document.cookie}`
252
+ const parts = value.split(`; ${name}=`)
253
+ if (parts.length === 2) {
254
+ return parts.pop().split(";").shift()
255
+ }
256
+ return null
257
+ }
258
+
259
+ setCookie(name, value, maxAge) {
260
+ document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax`
261
+ }
262
+
263
+ // ============================================================================
264
+ // Scroll Lock (for mobile overlay)
265
+ // ============================================================================
266
+
267
+ lockScroll() {
268
+ this.scrollPosition = window.pageYOffset
269
+ document.body.style.overflow = "hidden"
270
+ document.body.style.position = "fixed"
271
+ document.body.style.top = `-${this.scrollPosition}px`
272
+ document.body.style.width = "100%"
273
+ }
274
+
275
+ unlockScroll() {
276
+ document.body.style.overflow = ""
277
+ document.body.style.position = ""
278
+ document.body.style.top = ""
279
+ document.body.style.width = ""
280
+ if (this.scrollPosition !== undefined) {
281
+ window.scrollTo(0, this.scrollPosition)
282
+ }
283
+ }
284
+
285
+ // ============================================================================
286
+ // Events (Custom event dispatching)
287
+ // ============================================================================
288
+
289
+ dispatchStateChange() {
290
+ this.dispatch("stateChanged", {
291
+ detail: {
292
+ open: this.openValue,
293
+ mobile: this.isMobile(),
294
+ state: this.openValue ? "expanded" : "collapsed",
295
+ collapsible: this.sidebarTarget.getAttribute("data-collapsible")
296
+ },
297
+ bubbles: true
298
+ })
299
+ }
300
+
301
+ // ============================================================================
302
+ // Utilities
303
+ // ============================================================================
304
+
305
+ debounce(func, wait) {
306
+ let timeout
307
+ return function executedFunction(...args) {
308
+ const later = () => {
309
+ clearTimeout(timeout)
310
+ func(...args)
311
+ }
312
+ clearTimeout(timeout)
313
+ timeout = setTimeout(later, wait)
314
+ }
315
+ }
316
+ }
@@ -0,0 +1,32 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Sidebar Trigger Controller
5
+ *
6
+ * Triggers sidebar toggle via Stimulus outlets.
7
+ * Can be placed anywhere on the page - finds sidebar via outlet selector.
8
+ *
9
+ * @example
10
+ * <button
11
+ * data-controller="sidebar-trigger"
12
+ * data-action="click->sidebar-trigger#triggerClick"
13
+ * data-sidebar-trigger-sidebar-outlet="[data-outlet='sidebar']"
14
+ * >
15
+ * Toggle
16
+ * </button>
17
+ */
18
+ export default class extends Controller {
19
+ static outlets = ["sidebar"]
20
+
21
+ /**
22
+ * Toggle sidebar when trigger is clicked
23
+ * Works with multiple sidebars if multiple outlets are connected
24
+ */
25
+ triggerClick() {
26
+ if (this.hasSidebarOutlet) {
27
+ this.sidebarOutlets.forEach(outlet => {
28
+ outlet.toggle()
29
+ })
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,178 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Toggle Group Controller
5
+ *
6
+ * Manages a group of toggle buttons with single or multiple selection.
7
+ *
8
+ * @example Single selection
9
+ * <div data-controller="toggle-group"
10
+ * data-toggle-group-type-value="single"
11
+ * data-toggle-group-selected-value='["bold"]'>
12
+ * <button data-toggle-group-target="item" data-value="bold">Bold</button>
13
+ * <button data-toggle-group-target="item" data-value="italic">Italic</button>
14
+ * </div>
15
+ *
16
+ * @example Multiple selection
17
+ * <div data-controller="toggle-group"
18
+ * data-toggle-group-type-value="multiple"
19
+ * data-toggle-group-selected-value='["bold", "italic"]'>
20
+ * ...
21
+ * </div>
22
+ */
23
+ export default class extends Controller {
24
+ static targets = ["item"]
25
+
26
+ static values = {
27
+ type: { type: String, default: "single" },
28
+ selected: { type: Array, default: [] }
29
+ }
30
+
31
+ connect() {
32
+ this.syncItemStates()
33
+ }
34
+
35
+ /**
36
+ * Toggle an item's state
37
+ * @param {Event} event - Click event from toggle item
38
+ */
39
+ toggle(event) {
40
+ const item = event.currentTarget
41
+ if (item.disabled) return
42
+
43
+ const value = item.dataset.value
44
+ const isPressed = item.dataset.state === "on"
45
+
46
+ if (this.typeValue === "single") {
47
+ if (isPressed) {
48
+ this.selectedValue = []
49
+ } else {
50
+ this.selectedValue = [value]
51
+ }
52
+ } else {
53
+ if (isPressed) {
54
+ this.selectedValue = this.selectedValue.filter(v => v !== value)
55
+ } else {
56
+ this.selectedValue = [...this.selectedValue, value]
57
+ }
58
+ }
59
+
60
+ this.syncItemStates()
61
+ this.dispatchChange()
62
+ }
63
+
64
+ /**
65
+ * Handle keyboard navigation
66
+ * @param {KeyboardEvent} event
67
+ */
68
+ handleKeydown(event) {
69
+ const item = event.currentTarget
70
+ const items = this.itemTargets.filter(i => !i.disabled)
71
+ const currentIndex = items.indexOf(item)
72
+
73
+ let nextIndex = currentIndex
74
+
75
+ switch (event.key) {
76
+ case "ArrowRight":
77
+ case "ArrowDown":
78
+ event.preventDefault()
79
+ nextIndex = (currentIndex + 1) % items.length
80
+ break
81
+ case "ArrowLeft":
82
+ case "ArrowUp":
83
+ event.preventDefault()
84
+ nextIndex = (currentIndex - 1 + items.length) % items.length
85
+ break
86
+ case "Home":
87
+ event.preventDefault()
88
+ nextIndex = 0
89
+ break
90
+ case "End":
91
+ event.preventDefault()
92
+ nextIndex = items.length - 1
93
+ break
94
+ case " ":
95
+ case "Enter":
96
+ return
97
+ default:
98
+ return
99
+ }
100
+
101
+ items[nextIndex]?.focus()
102
+ }
103
+
104
+ /**
105
+ * Sync visual states with selectedValue
106
+ */
107
+ syncItemStates() {
108
+ this.itemTargets.forEach(item => {
109
+ const value = item.dataset.value
110
+ const isSelected = this.selectedValue.includes(value)
111
+
112
+ item.dataset.state = isSelected ? "on" : "off"
113
+ item.setAttribute("aria-pressed", isSelected)
114
+ })
115
+ }
116
+
117
+ /**
118
+ * Dispatch change event
119
+ */
120
+ dispatchChange() {
121
+ const detail = {
122
+ type: this.typeValue,
123
+ value: this.typeValue === "single"
124
+ ? (this.selectedValue[0] || null)
125
+ : this.selectedValue
126
+ }
127
+
128
+ this.dispatch("change", { detail })
129
+
130
+ this.element.dispatchEvent(new CustomEvent("toggle-group:change", {
131
+ bubbles: true,
132
+ detail
133
+ }))
134
+ }
135
+
136
+ /**
137
+ * Programmatically select a value
138
+ * @param {string} value - Value to select
139
+ */
140
+ select(value) {
141
+ if (this.typeValue === "single") {
142
+ this.selectedValue = [value]
143
+ } else if (!this.selectedValue.includes(value)) {
144
+ this.selectedValue = [...this.selectedValue, value]
145
+ }
146
+ this.syncItemStates()
147
+ this.dispatchChange()
148
+ }
149
+
150
+ /**
151
+ * Programmatically deselect a value
152
+ * @param {string} value - Value to deselect
153
+ */
154
+ deselect(value) {
155
+ this.selectedValue = this.selectedValue.filter(v => v !== value)
156
+ this.syncItemStates()
157
+ this.dispatchChange()
158
+ }
159
+
160
+ /**
161
+ * Clear all selections
162
+ */
163
+ clear() {
164
+ this.selectedValue = []
165
+ this.syncItemStates()
166
+ this.dispatchChange()
167
+ }
168
+
169
+ /**
170
+ * Get current value(s)
171
+ * @returns {string|string[]|null}
172
+ */
173
+ getValue() {
174
+ return this.typeValue === "single"
175
+ ? (this.selectedValue[0] || null)
176
+ : this.selectedValue
177
+ }
178
+ }
@@ -0,0 +1,12 @@
1
+ <%# locals: (variant: :default, icon: nil, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :alert,
4
+ variant: variant,
5
+ has_icon: icon.present? || nil
6
+ ).compact %>
7
+
8
+ <%= content_tag :div, role: :alert, class: css_classes.presence, data: merged_data, **html_options do %>
9
+ <%= icon_for(icon) if icon.present? %>
10
+
11
+ <div><%= yield %></div>
12
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (variant: :default, size: :md, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :badge,
4
+ variant: variant,
5
+ size: size
6
+ ) %>
7
+
8
+ <%= content_tag :span, class: css_classes, data: merged_data, **html_options do %>
9
+ <%= yield %>
10
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%# locals: (css_classes: "", responsive: false, **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :breadcrumbs
4
+ )
5
+
6
+ if responsive
7
+ merged_data[:controller] = "breadcrumb"
8
+ end %>
9
+
10
+ <nav
11
+ aria-label="Breadcrumb"
12
+ class="<%= css_classes %>"
13
+ <%= tag.attributes(data: merged_data, **html_options) %>
14
+ >
15
+ <%= yield %>
16
+ </nav>
@@ -0,0 +1,6 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(component: :card) %>
3
+
4
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= yield %>
6
+ <% end %>
@@ -0,0 +1,25 @@
1
+ <div
2
+ data-menu-button-target="content"
3
+ class="
4
+ absolute z-10 overflow-hidden border border-border p-1 text-popover-foreground
5
+ min-w-56 max-w-56 content-fit shadow-md bottom-full rounded-lg hidden
6
+ peer-data-[state=closed]/menu-button:animate-out
7
+ peer-data-[state=closed]/menu-button:fade-out-0
8
+ peer-data-[state=open]/menu-button:fade-in-0
9
+ peer-data-[state=closed]/menu-button:zoom-out-95
10
+ peer-data-[state=open]/menu-button:zoom-in-95
11
+ peer-data-[side=bottom]/menu-button:slide-in-from-top-2
12
+ peer-data-[side=left]/menu-button:slide-in-from-right-2
13
+ peer-data-[side=right]/menu-button:slide-in-from-left-2
14
+ peer-data-[side=top]/menu-button:slide-in-from-bottom-2
15
+ "
16
+ role="menu"
17
+ aria-orientation="vertical"
18
+ aria-labelledby="menu-button"
19
+ tabindex="-1"
20
+ >
21
+ <%= yield %>
22
+ </div>
23
+ <!--
24
+ <div role="separator" aria-orientation="horizontal" class="separator"></div>
25
+ -->
@@ -0,0 +1,9 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: "dropdown-menu",
4
+ controller: "dropdown-menu"
5
+ ) %>
6
+
7
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
8
+ <%= yield %>
9
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (variant: :default, size: :default, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :empty,
4
+ variant: variant,
5
+ size: size
6
+ ) %>
7
+
8
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
9
+ <%= yield %>
10
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :header
4
+ ) %>
5
+
6
+ <%= content_tag :header, class: css_classes.presence, data: merged_data, **html_options do %>
7
+ <div data-header-part="inner"><%= yield %></div>
8
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <%# locals: (title: "", subtitle: nil, icon: nil, text_icon: nil, icon_classes: "", submenu: false) %>
2
+
3
+ <ul class="flex w-full min-w-0 flex-col gap-1">
4
+ <li class="group/menu-item relative" data-controller="menu-button">
5
+ <button
6
+ data-state="closed"
7
+ class="
8
+ peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2
9
+ text-left outline-none ring-sidebar-ring transition-[width,height,padding]
10
+ focus-visible:ring-2 active:bg-sidebar-accent
11
+ active:text-sidebar-accent-foreground disabled:pointer-events-none
12
+ disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50
13
+ data-[state=open]:bg-sidebar-accent
14
+ data-[state=open]text-sidebar-accent-foreground
15
+ group-data-[collapsible=icon]:size-8! [&>span:last-child]:truncate
16
+ [&>svg]:size-4 [&>svg]:shrink-0 hover:bg-sidebar-accent
17
+ hover:text-sidebar-accent-foreground h-12 text-sm
18
+ group-data-[collapsible=icon]:p-0!
19
+ ", data-action="menu-button#toggle", data-menu-button-target="button" }
20
+ >
21
+ <% if icon.present? %>
22
+ <%= image_tag icon, alt: "maquina", class: "#{icon_classes}" %>
23
+ <% elsif text_icon.present? %>
24
+ <div
25
+ class="
26
+ flex aspect-square size-8 items-center justify-center rounded-lg
27
+ bg-sidebar-primary text-sidebar-primary-foreground
28
+ "
29
+ >
30
+ <span class="<%= icon_classes %>"><%= text_icon %></span>
31
+ </div>
32
+ <% end %>
33
+ <div class="grid flex-1 text-left text-sm leading-tight">
34
+ <span class="truncate font-semibold"><%= title %></span>
35
+ <% if subtitle.present? %>
36
+ <span class="truncate text-xs"><%= subtitle %></span>
37
+ <% end %>
38
+ </div>
39
+ <%= icon_for(:chevron_up_down, class: "ml-auto") if submenu %>
40
+ </button>
41
+
42
+ <%= yield %>
43
+ </li>
44
+ </ul>
@@ -0,0 +1,13 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :pagination
4
+ ) %>
5
+
6
+ <%= content_tag :nav,
7
+ role: "navigation",
8
+ "aria-label": html_options.delete(:"aria-label") || "Pagination",
9
+ class: css_classes.presence,
10
+ data: merged_data,
11
+ **html_options do %>
12
+ <%= yield %>
13
+ <% end %>