maquina-components 0.1.2 → 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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +349 -138
  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 +143 -64
  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/{components → maquina_components}/icons_helper.rb +40 -3
  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 +11 -10
  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 +4 -8
  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 +12 -33
  42. data/app/views/components/_separator.html.erb +11 -0
  43. data/app/views/components/_sidebar.html.erb +30 -20
  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 +116 -12
  108. data/app/helpers/components/pagination_helper.rb +0 -15
  109. data/app/views/components/_card_content.html.erb +0 -5
  110. data/app/views/components/_card_header.html.erb +0 -8
  111. data/app/views/components/_sidebar_content.html.erb +0 -8
  112. data/app/views/components/_sidebar_group.html.erb +0 -42
  113. data/app/views/components/_sidebar_header.html.erb +0 -3
@@ -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
+ }
@@ -1,11 +1,12 @@
1
- <%# locals: (title: nil) %>
2
- <% if notice || alert %>
3
- <div
4
- role="alert"
5
- class="<%= class_names('relative w-full rounded-lg border px-4 py-3 text-sm mx-auto mb-4 [&>svg]:size-4 [&>svg+div]:translate-y-(-3px) [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7 max-w-lg', '[&>svg]:text-foreground bg-background text-foreground': notice.present?, 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive': alert.present?) %>"
6
- >
7
- <%= icon_for(notice.present? ? :check : :circle_alert) %>
8
- <h5 class="mb-1 font-medium leading-none tracking-tight"><%= title || (notice.present? ? "Éxito" : "Error") %></h5>
9
- <div class="text-sm [&_p]:leading-relaxed"><%= notice %></div>
10
- </div>
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>
11
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>
@@ -1,10 +1,6 @@
1
- <%# locals: (custom_classes: "") -%>
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(component: :card) %>
2
3
 
3
- <div
4
- class="<%= class_names(
5
- "rounded-xl border border-border bg-card text-card-foreground shadow",
6
- custom_classes
7
- ) %>"
8
- >
4
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
9
5
  <%= yield %>
10
- </div>
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>
@@ -1,34 +1,13 @@
1
- <%# locals: (pagy:, route_helper:, param: nil, data: {turbo_action: :replace}) %>
2
- <nav class="mx-auto flex w-full justify-center mt-4">
3
- <ul class="flex flex-row items-center gap-1">
4
- <li>
5
- <% if pagy.prev.present? %>
6
- <%= link_to "Previo",
7
- paginated_path(route_helper, pagy, pagy.prev, param),
8
- class: "button-ghost",
9
- data: data %>
10
- <% else %>
11
- <button class="button-ghost" disabled>Previo</button>
12
- <% end %>
13
- </li>
14
- <% pagy.series.each do |page| %>
15
- <li>
16
- <%= link_to page,
17
- paginated_path(route_helper, pagy, page, param),
18
- class: class_names("button-ghost", "button-outline": page.is_a?(String)),
19
- data: data %>
20
- </li>
21
- <% end %>
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ component: :pagination
4
+ ) %>
22
5
 
23
- <li>
24
- <% if pagy.next.present? %>
25
- <%= link_to "Siguiente",
26
- paginated_path(route_helper, pagy, pagy.next, param),
27
- class: "button-ghost",
28
- data: data %>
29
- <% else %>
30
- <button class="button-ghost" disabled>Siguiente</button>
31
- <% end %>
32
- </li>
33
- </ul>
34
- </nav>
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 %>
@@ -0,0 +1,11 @@
1
+ <%# locals: (orientation: :horizontal) -%>
2
+
3
+ <div
4
+ data-orientation="<%= orientation %>"
5
+ role="none"
6
+ class="
7
+ bg-border shrink-0 data-[orientation=horizontal]:h-px
8
+ data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px mx-2
9
+ data-[orientation=vertical]:h-4
10
+ "
11
+ ></div>
@@ -1,29 +1,39 @@
1
+ <%# locals: (id: nil, state: :collapsed, collapsible: :offcanvas, variant: :inset, side: :left, css_classes: "", **html_options) %>
2
+ <% random_id = id || "sidebar-#{SecureRandom.hex(6)}"
3
+
4
+ merged_data = (html_options.delete(:data) || {}).merge(
5
+ sidebar_part: :root,
6
+ sidebar_target: "sidebar",
7
+ state: state,
8
+ variant: variant,
9
+ collapsible: collapsible,
10
+ side: side
11
+ ) %>
12
+
1
13
  <aside
2
- class="group peer hidden md:block text-sidebar-foreground"
3
- data-controller="sidebars"
4
- data-state="expanded"
5
- data-collapsible=""
6
- id="sidebar"
14
+ id="<%= random_id %>"
15
+ class="group peer sidebar-loading <%= css_classes %>"
16
+ <%= tag.attributes(data: merged_data, **html_options) %>
7
17
  >
8
- <!-- Mobile overlay -->
18
+ <%# Sidebar gap (creates space for sidebar on desktop) %>
19
+ <div id="<%= random_id %>-gap" data-sidebar-part="gap"></div>
20
+
21
+ <%# Mobile backdrop overlay %>
9
22
  <div
10
- class="
11
- inset-0 bg-black/80 lg:hidden transition-opacity duration-300 hidden
12
- "
13
- data-sidebar-target="overlay"
14
- data-action="click->sidebar#toggleMobile"
15
- >
16
- </div>
17
- <!-- Sidebar container -->
23
+ id="<%= random_id %>-overlay"
24
+ data-sidebar-part="backdrop"
25
+ data-sidebar-target="backdrop"
26
+ data-action="click->sidebar#backdropClick"
27
+ ></div>
28
+
29
+ <%# Sidebar container (fixed positioned) %>
18
30
  <div
19
- class="
20
- duration-200 fixed inset-y-0 z-10 h-svh w-(--sidebar-width)
21
- transition-[left,right,width] ease-linear md:flex left-0 border-r
22
- border-sidebar-border group-data-[collapsible=icon]:w-[--sidebar-width-icon]
23
- "
31
+ id="<%= random_id %>-container"
32
+ data-sidebar-part="container"
24
33
  data-sidebar-target="container"
25
34
  >
26
- <div class=" flex h-full w-full flex-col bg-sidebar">
35
+ <%# Inner sidebar content wrapper %>
36
+ <div id="<%= random_id %>-inner" data-sidebar-part="inner">
27
37
  <%= yield %>
28
38
  </div>
29
39
  </div>
@@ -0,0 +1,49 @@
1
+ <%# locals: (collection:, columns:, caption: nil, variant: nil, table_variant: nil, empty_message: "No data available", row_id: nil, html_options: {}) %>
2
+ <%= render "components/table", variant: variant, table_variant: table_variant, **html_options do %>
3
+ <% if caption.present? %>
4
+ <%= render "components/table/caption" do %><%= caption %><% end %>
5
+ <% end %>
6
+
7
+ <%= render "components/table/header" do %>
8
+ <%= render "components/table/row" do %>
9
+ <% columns.each do |column| %>
10
+ <%= render "components/table/head", css_classes: table_alignment_class(column[:align]) do %>
11
+ <%= column[:label] %>
12
+ <% end %>
13
+ <% end %>
14
+ <% end %>
15
+ <% end %>
16
+
17
+ <%= render "components/table/body" do %>
18
+ <% if collection.empty? %>
19
+ <%= render "components/table/row" do %>
20
+ <%= render "components/table/cell", colspan: columns.size, data: { empty: "true" } do %>
21
+ <%= empty_message %>
22
+ <% end %>
23
+ <% end %>
24
+ <% else %>
25
+ <% collection.each do |item| %>
26
+ <%
27
+ row_options = {}
28
+ row_options[:id] = "row-#{item.public_send(row_id)}" if row_id && item.respond_to?(row_id)
29
+ %>
30
+ <%= render "components/table/row", **row_options do %>
31
+ <% columns.each do |column| %>
32
+ <%
33
+ value = if column[:key].is_a?(Proc)
34
+ column[:key].call(item)
35
+ elsif item.is_a?(Hash)
36
+ item[column[:key]] || item[column[:key].to_s]
37
+ elsif item.respond_to?(column[:key])
38
+ item.public_send(column[:key])
39
+ end
40
+ %>
41
+ <%= render "components/table/cell", css_classes: table_alignment_class(column[:align]) do %>
42
+ <%= value %>
43
+ <% end %>
44
+ <% end %>
45
+ <% end %>
46
+ <% end %>
47
+ <% end %>
48
+ <% end %>
49
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%# locals: (css_classes: "", container: true, variant: nil, table_variant: nil, **html_options) %>
2
+ <%
3
+ # Table data attributes - merge user data with component defaults
4
+ table_data = (html_options.delete(:data) || {}).merge(component: :table)
5
+ table_data[:variant] = table_variant if table_variant
6
+
7
+ # Container data attributes
8
+ container_data = { table_part: :container }
9
+ container_data[:variant] = variant if variant
10
+ %>
11
+ <% if container %>
12
+ <div data-table-part="container"<%= " data-variant=\"#{variant}\"" if variant %>>
13
+ <%= content_tag :table, class: css_classes.presence, data: table_data, **html_options do %>
14
+ <%= yield %>
15
+ <% end %>
16
+ </div>
17
+ <% else %>
18
+ <%= content_tag :table, class: css_classes.presence, data: table_data, **html_options do %>
19
+ <%= yield %>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <%# locals: (type: :single, variant: :default, size: :default, value: nil, disabled: false, css_classes: "", **html_options) %>
2
+ <% selected_values = case value
3
+ when Array then value.map(&:to_s)
4
+ when nil then []
5
+ else [value.to_s]
6
+ end
7
+
8
+ merged_data = (html_options.delete(:data) || {}).merge(
9
+ controller: "toggle-group",
10
+ component: "toggle-group",
11
+ variant: variant,
12
+ size: size,
13
+ "toggle-group-type-value": type,
14
+ "toggle-group-selected-value": selected_values.to_json
15
+ ) %>
16
+
17
+ <%= content_tag :div,
18
+ role: "group",
19
+ class: css_classes.presence,
20
+ data: merged_data,
21
+ "aria-disabled": (disabled ? "true" : nil),
22
+ **html_options do %>
23
+ <%= yield %>
24
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%# locals: (text: nil, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(alert_part: :description) %>
3
+
4
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= text || yield %>
6
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%# locals: (text: nil, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(alert_part: :title) %>
3
+
4
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= text || yield %>
6
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :ellipsis
4
+ ) %>
5
+
6
+ <%= content_tag :span, role: "presentation", class: css_classes, data: merged_data, **html_options do %>
7
+ <%= icon_for(:ellipsis) %>
8
+ <span class="sr-only">More</span>
9
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :item
4
+ ) %>
5
+
6
+ <%= content_tag :li, class: css_classes, data: merged_data, **html_options do %>
7
+ <%= yield %>
8
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (href:, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :link
4
+ ) %>
5
+
6
+ <%= link_to href, class: css_classes, data: merged_data, **html_options do %>
7
+ <%= yield %>
8
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :list
4
+ ) %>
5
+
6
+ <%= content_tag :ol, class: css_classes, data: merged_data, **html_options do %>
7
+ <%= yield %>
8
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :page
4
+ ) %>
5
+
6
+ <%= content_tag :span, role: "link", aria: {current: "page", disabled: true}, class: css_classes, data: merged_data, **html_options do %>
7
+ <%= yield %>
8
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <%# locals: (icon: :chevron_right, css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(
3
+ breadcrumb_part: :separator
4
+ ) %>
5
+
6
+ <li
7
+ role="presentation"
8
+ aria-hidden="true"
9
+ class="<%= css_classes %>"
10
+ <%= tag.attributes(data: merged_data, **html_options) %>
11
+ >
12
+ <% if icon == :custom %>
13
+ <%= yield %>
14
+ <% else %>
15
+ <%= icon_for(icon) %>
16
+ <% end %>
17
+ </li>
@@ -0,0 +1,6 @@
1
+ <%# locals: (css_classes: "", **html_options) %>
2
+ <% merged_data = (html_options.delete(:data) || {}).merge(card_part: :action) %>
3
+
4
+ <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
5
+ <%= yield %>
6
+ <% end %>