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.
- checksums.yaml +4 -4
- data/README.md +349 -138
- data/app/assets/images/maquina.svg +1 -0
- data/app/assets/stylesheets/alert.css +143 -0
- data/app/assets/stylesheets/badge.css +145 -0
- data/app/assets/stylesheets/breadcrumbs.css +163 -0
- data/app/assets/stylesheets/card.css +128 -0
- data/app/assets/stylesheets/dropdown_menu.css +248 -0
- data/app/assets/stylesheets/empty.css +133 -0
- data/app/assets/stylesheets/form.css +617 -0
- data/app/assets/stylesheets/header.css +61 -0
- data/app/assets/stylesheets/maquina_components.css +143 -64
- data/app/assets/stylesheets/pagination.css +154 -0
- data/app/assets/stylesheets/sidebar.css +477 -0
- data/app/assets/stylesheets/table.css +205 -0
- data/app/assets/stylesheets/toggle_group.css +151 -0
- data/app/assets/tailwind/maquina_components_engine/engine.css +16 -0
- data/app/helpers/maquina_components/breadcrumbs_helper.rb +118 -0
- data/app/helpers/maquina_components/dropdown_menu_helper.rb +249 -0
- data/app/helpers/maquina_components/empty_helper.rb +102 -0
- data/app/helpers/{components → maquina_components}/icons_helper.rb +40 -3
- data/app/helpers/maquina_components/pagination_helper.rb +153 -0
- data/app/helpers/maquina_components/sidebar_helper.rb +63 -0
- data/app/helpers/maquina_components/table_helper.rb +144 -0
- data/app/helpers/maquina_components/toggle_group_helper.rb +172 -0
- data/app/javascript/controllers/breadcrumb_controller.js +71 -0
- data/app/javascript/controllers/dropdown_menu_controller.js +203 -0
- data/app/javascript/controllers/menu_button_controller.js +59 -0
- data/app/javascript/controllers/sidebar_controller.js +316 -0
- data/app/javascript/controllers/sidebar_trigger_controller.js +32 -0
- data/app/javascript/controllers/toggle_group_controller.js +178 -0
- data/app/views/components/_alert.html.erb +11 -10
- data/app/views/components/_badge.html.erb +10 -0
- data/app/views/components/_breadcrumbs.html.erb +16 -0
- data/app/views/components/_card.html.erb +4 -8
- data/app/views/components/_dropdown.html.erb +25 -0
- data/app/views/components/_dropdown_menu.html.erb +9 -0
- data/app/views/components/_empty.html.erb +10 -0
- data/app/views/components/_header.html.erb +8 -0
- data/app/views/components/_menu_button.html.erb +44 -0
- data/app/views/components/_pagination.html.erb +12 -33
- data/app/views/components/_separator.html.erb +11 -0
- data/app/views/components/_sidebar.html.erb +30 -20
- data/app/views/components/_simple_table.html.erb +49 -0
- data/app/views/components/_table.html.erb +21 -0
- data/app/views/components/_toggle_group.html.erb +24 -0
- data/app/views/components/alert/_description.html.erb +6 -0
- data/app/views/components/alert/_title.html.erb +6 -0
- data/app/views/components/breadcrumbs/_ellipsis.html.erb +9 -0
- data/app/views/components/breadcrumbs/_item.html.erb +8 -0
- data/app/views/components/breadcrumbs/_link.html.erb +8 -0
- data/app/views/components/breadcrumbs/_list.html.erb +8 -0
- data/app/views/components/breadcrumbs/_page.html.erb +8 -0
- data/app/views/components/breadcrumbs/_separator.html.erb +17 -0
- data/app/views/components/card/_action.html.erb +6 -0
- data/app/views/components/card/_content.html.erb +9 -0
- data/app/views/components/card/_description.html.erb +6 -0
- data/app/views/components/card/_footer.html.erb +17 -0
- data/app/views/components/card/_header.html.erb +9 -0
- data/app/views/components/card/_title.html.erb +9 -0
- data/app/views/components/dropdown_menu/_content.html.erb +20 -0
- data/app/views/components/dropdown_menu/_group.html.erb +12 -0
- data/app/views/components/dropdown_menu/_item.html.erb +29 -0
- data/app/views/components/dropdown_menu/_label.html.erb +13 -0
- data/app/views/components/dropdown_menu/_separator.html.erb +11 -0
- data/app/views/components/dropdown_menu/_shortcut.html.erb +12 -0
- data/app/views/components/dropdown_menu/_trigger.html.erb +24 -0
- data/app/views/components/empty/_content.html.erb +8 -0
- data/app/views/components/empty/_description.html.erb +12 -0
- data/app/views/components/empty/_header.html.erb +8 -0
- data/app/views/components/empty/_media.html.erb +13 -0
- data/app/views/components/empty/_title.html.erb +12 -0
- data/app/views/components/pagination/_content.html.erb +8 -0
- data/app/views/components/pagination/_ellipsis.html.erb +28 -0
- data/app/views/components/pagination/_item.html.erb +8 -0
- data/app/views/components/pagination/_link.html.erb +23 -0
- data/app/views/components/pagination/_next.html.erb +57 -0
- data/app/views/components/pagination/_previous.html.erb +57 -0
- data/app/views/components/sidebar/_content.html.erb +8 -0
- data/app/views/components/sidebar/_footer.html.erb +8 -0
- data/app/views/components/sidebar/_group.html.erb +12 -0
- data/app/views/components/sidebar/_header.html.erb +8 -0
- data/app/views/components/sidebar/_inset.html.erb +8 -0
- data/app/views/components/sidebar/_menu.html.erb +8 -0
- data/app/views/components/sidebar/_menu_button.html.erb +14 -0
- data/app/views/components/sidebar/_menu_item.html.erb +7 -0
- data/app/views/components/sidebar/_menu_link.html.erb +32 -0
- data/app/views/components/sidebar/_provider.html.erb +16 -0
- data/app/views/components/sidebar/_trigger.html.erb +12 -0
- data/app/views/components/stats/_stats_card.html.erb +100 -0
- data/app/views/components/stats/_stats_grid.html.erb +38 -0
- data/app/views/components/table/_body.html.erb +5 -0
- data/app/views/components/table/_caption.html.erb +5 -0
- data/app/views/components/table/_cell.html.erb +5 -0
- data/app/views/components/table/_footer.html.erb +5 -0
- data/app/views/components/table/_head.html.erb +8 -0
- data/app/views/components/table/_header.html.erb +8 -0
- data/app/views/components/table/_row.html.erb +8 -0
- data/app/views/components/toggle_group/_item.html.erb +19 -0
- data/config/importmap.rb +1 -0
- data/lib/generators/maquina_components/install/USAGE +39 -0
- data/lib/generators/maquina_components/install/install_generator.rb +123 -0
- data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +68 -0
- data/lib/generators/maquina_components/install/templates/theme.css.tt +179 -0
- data/lib/maquina_components/engine.rb +10 -0
- data/lib/maquina_components/version.rb +1 -1
- metadata +116 -12
- data/app/helpers/components/pagination_helper.rb +0 -15
- data/app/views/components/_card_content.html.erb +0 -5
- data/app/views/components/_card_header.html.erb +0 -8
- data/app/views/components/_sidebar_content.html.erb +0 -8
- data/app/views/components/_sidebar_group.html.erb +0 -42
- data/app/views/components/_sidebar_header.html.erb +0 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module
|
|
1
|
+
module MaquinaComponents
|
|
2
2
|
module IconsHelper
|
|
3
3
|
def icon_for(name, options = {})
|
|
4
4
|
return nil unless name
|
|
5
5
|
|
|
6
|
-
svg = icon_svg_for(name.to_sym)
|
|
6
|
+
svg = icon_svg_for(name.to_sym) || main_icon_svg_for(name.to_sym)
|
|
7
7
|
return nil unless svg
|
|
8
8
|
|
|
9
9
|
css_classes = options[:class]
|
|
@@ -16,7 +16,8 @@ module Components
|
|
|
16
16
|
svg.html_safe
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
def main_icon_svg_for(name)
|
|
20
|
+
end
|
|
20
21
|
|
|
21
22
|
def icon_svg_for(name)
|
|
22
23
|
case name
|
|
@@ -118,6 +119,42 @@ module Components
|
|
|
118
119
|
<line x1="12" x2="12.01" y1="16" y2="16"/>
|
|
119
120
|
</svg>
|
|
120
121
|
SVG
|
|
122
|
+
when :logout
|
|
123
|
+
<<~SVG.freeze
|
|
124
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
125
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
126
|
+
<polyline points="16 17 21 12 16 7"></polyline>
|
|
127
|
+
<line x1="21" x2="9" y1="12" y2="12"></line>
|
|
128
|
+
</svg>
|
|
129
|
+
SVG
|
|
130
|
+
when :chevron_up_down
|
|
131
|
+
<<~SVG.freeze
|
|
132
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
133
|
+
<path d="m7 15 5 5 5-5"></path>
|
|
134
|
+
<path d="m7 9 5-5 5 5"></path>
|
|
135
|
+
</svg>
|
|
136
|
+
SVG
|
|
137
|
+
when :chevron_right
|
|
138
|
+
<<~SVG
|
|
139
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
140
|
+
<path d="m9 18 6-6-6-6"/>
|
|
141
|
+
</svg>
|
|
142
|
+
SVG
|
|
143
|
+
when :left_panel
|
|
144
|
+
<<~SVG.freeze
|
|
145
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
146
|
+
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
|
|
147
|
+
<path d="M9 3v18"></path>
|
|
148
|
+
</svg>
|
|
149
|
+
SVG
|
|
150
|
+
when :ellipsis
|
|
151
|
+
<<~SVG.freeze
|
|
152
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
153
|
+
<circle cx="12" cy="12" r="1"/>
|
|
154
|
+
<circle cx="19" cy="12" r="1"/>
|
|
155
|
+
<circle cx="5" cy="12" r="1"/>
|
|
156
|
+
</svg>
|
|
157
|
+
SVG
|
|
121
158
|
end
|
|
122
159
|
end
|
|
123
160
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaquinaComponents
|
|
4
|
+
# Pagination Helper
|
|
5
|
+
#
|
|
6
|
+
# Provides convenient methods for creating pagination components with Pagy integration.
|
|
7
|
+
#
|
|
8
|
+
# @example Using the helper with Pagy
|
|
9
|
+
# <%%= pagination_nav(@pagy, :users_path) %>
|
|
10
|
+
#
|
|
11
|
+
# @example With additional params
|
|
12
|
+
# <%%= pagination_nav(@pagy, :search_users_path, params: { q: params[:q] }) %>
|
|
13
|
+
#
|
|
14
|
+
# @example With Turbo options
|
|
15
|
+
# <%%= pagination_nav(@pagy, :users_path, turbo: { action: :replace, frame: "users" }) %>
|
|
16
|
+
#
|
|
17
|
+
# @example Using partials directly
|
|
18
|
+
# <%%= render "components/pagination" do %>
|
|
19
|
+
# <%%= render "components/pagination/content" do %>
|
|
20
|
+
# <%%= render "components/pagination/item" do %>
|
|
21
|
+
# <%%= render "components/pagination/previous", href: prev_path %>
|
|
22
|
+
# <%% end %>
|
|
23
|
+
# ...
|
|
24
|
+
# <%% end %>
|
|
25
|
+
# <%% end %>
|
|
26
|
+
#
|
|
27
|
+
module PaginationHelper
|
|
28
|
+
# Renders a complete pagination navigation from a Pagy object
|
|
29
|
+
#
|
|
30
|
+
# @param pagy [Pagy] The Pagy pagination object
|
|
31
|
+
# @param route_helper [Symbol] Route helper method name (e.g., :users_path)
|
|
32
|
+
# @param params [Hash] Additional params to pass to route helper
|
|
33
|
+
# @param turbo [Hash] Turbo-specific data attributes
|
|
34
|
+
# @param show_labels [Boolean] Whether to show Previous/Next text labels
|
|
35
|
+
# @param css_classes [String] Additional CSS classes for the nav
|
|
36
|
+
# @return [String] Rendered HTML
|
|
37
|
+
def pagination_nav(pagy, route_helper, params: {}, turbo: {action: :replace}, show_labels: true, css_classes: "", **html_options)
|
|
38
|
+
return if pagy.pages <= 1
|
|
39
|
+
|
|
40
|
+
render "components/pagination", css_classes: css_classes, **html_options do
|
|
41
|
+
render "components/pagination/content" do
|
|
42
|
+
safe_join([
|
|
43
|
+
pagination_previous_item(pagy, route_helper, params, turbo, show_labels),
|
|
44
|
+
pagination_page_items(pagy, route_helper, params, turbo),
|
|
45
|
+
pagination_next_item(pagy, route_helper, params, turbo, show_labels)
|
|
46
|
+
])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Simpler pagination with just Previous/Next (no page numbers)
|
|
52
|
+
#
|
|
53
|
+
# @param pagy [Pagy] The Pagy pagination object
|
|
54
|
+
# @param route_helper [Symbol] Route helper method name
|
|
55
|
+
# @param params [Hash] Additional params to pass to route helper
|
|
56
|
+
# @param turbo [Hash] Turbo-specific data attributes
|
|
57
|
+
# @return [String] Rendered HTML
|
|
58
|
+
def pagination_simple(pagy, route_helper, params: {}, turbo: {action: :replace}, css_classes: "", **html_options)
|
|
59
|
+
return if pagy.pages <= 1
|
|
60
|
+
|
|
61
|
+
render "components/pagination", css_classes: css_classes, **html_options do
|
|
62
|
+
render "components/pagination/content" do
|
|
63
|
+
safe_join([
|
|
64
|
+
pagination_previous_item(pagy, route_helper, params, turbo, true),
|
|
65
|
+
pagination_next_item(pagy, route_helper, params, turbo, true)
|
|
66
|
+
])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Build paginated path with page param
|
|
72
|
+
#
|
|
73
|
+
# @param route_helper [Symbol] Route helper method name
|
|
74
|
+
# @param pagy [Pagy] The Pagy pagination object
|
|
75
|
+
# @param page [Integer] Page number
|
|
76
|
+
# @param extra_params [Hash] Additional params
|
|
77
|
+
# @return [String] URL path
|
|
78
|
+
def paginated_path(route_helper, pagy, page, extra_params = {})
|
|
79
|
+
page_param = pagy.vars[:page_param] || Pagy::DEFAULT[:page_param]
|
|
80
|
+
query_params = request.query_parameters.except(page_param.to_s).merge(extra_params)
|
|
81
|
+
query_params[page_param] = page
|
|
82
|
+
|
|
83
|
+
send(route_helper, query_params)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def pagination_previous_item(pagy, route_helper, params, turbo, show_label)
|
|
89
|
+
render "components/pagination/item" do
|
|
90
|
+
if pagy.prev
|
|
91
|
+
render "components/pagination/previous",
|
|
92
|
+
href: paginated_path(route_helper, pagy, pagy.prev, params),
|
|
93
|
+
show_label: show_label,
|
|
94
|
+
data: turbo_data(turbo)
|
|
95
|
+
else
|
|
96
|
+
render "components/pagination/previous",
|
|
97
|
+
disabled: true,
|
|
98
|
+
show_label: show_label
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pagination_next_item(pagy, route_helper, params, turbo, show_label)
|
|
104
|
+
render "components/pagination/item" do
|
|
105
|
+
if pagy.next
|
|
106
|
+
render "components/pagination/next",
|
|
107
|
+
href: paginated_path(route_helper, pagy, pagy.next, params),
|
|
108
|
+
show_label: show_label,
|
|
109
|
+
data: turbo_data(turbo)
|
|
110
|
+
else
|
|
111
|
+
render "components/pagination/next",
|
|
112
|
+
disabled: true,
|
|
113
|
+
show_label: show_label
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pagination_page_items(pagy, route_helper, params, turbo)
|
|
119
|
+
pagy.series.map do |item|
|
|
120
|
+
render "components/pagination/item" do
|
|
121
|
+
case item
|
|
122
|
+
when Integer
|
|
123
|
+
render "components/pagination/link",
|
|
124
|
+
href: paginated_path(route_helper, pagy, item, params),
|
|
125
|
+
active: item == pagy.page,
|
|
126
|
+
data: turbo_data(turbo) do
|
|
127
|
+
item.to_s
|
|
128
|
+
end
|
|
129
|
+
when String
|
|
130
|
+
# Current page (string representation)
|
|
131
|
+
render "components/pagination/link",
|
|
132
|
+
href: paginated_path(route_helper, pagy, item.to_i, params),
|
|
133
|
+
active: true,
|
|
134
|
+
data: turbo_data(turbo) do
|
|
135
|
+
item
|
|
136
|
+
end
|
|
137
|
+
when :gap
|
|
138
|
+
render "components/pagination/ellipsis"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def turbo_data(turbo)
|
|
145
|
+
return {} if turbo.blank?
|
|
146
|
+
|
|
147
|
+
data = {}
|
|
148
|
+
data[:turbo_action] = turbo[:action] if turbo[:action]
|
|
149
|
+
data[:turbo_frame] = turbo[:frame] if turbo[:frame]
|
|
150
|
+
data
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module MaquinaComponents
|
|
2
|
+
module SidebarHelper
|
|
3
|
+
# Get sidebar state from cookie
|
|
4
|
+
#
|
|
5
|
+
# Reads the sidebar state cookie and returns a String.
|
|
6
|
+
# Use this to set the state value in the sidebar
|
|
7
|
+
# to ensure server-rendered state matches client state.
|
|
8
|
+
#
|
|
9
|
+
# @param cookie_name [String] The cookie name (default: "sidebar_state")
|
|
10
|
+
# @return [String] expanded if sidebar should be open, collapsed otherwise
|
|
11
|
+
#
|
|
12
|
+
# @example In layout
|
|
13
|
+
# <%= render "components/sidebar",
|
|
14
|
+
# state: sidebar_state do %>
|
|
15
|
+
# <!-- content -->
|
|
16
|
+
# <% end %>
|
|
17
|
+
#
|
|
18
|
+
# @example With custom cookie name
|
|
19
|
+
# <%= render "components/sidebar",
|
|
20
|
+
# state: sidebar_state("custom_sidebar_cookie") do %>
|
|
21
|
+
# <!-- content -->
|
|
22
|
+
# <% end %>
|
|
23
|
+
#
|
|
24
|
+
def sidebar_state(cookie_name = "sidebar_state")
|
|
25
|
+
# Read cookie value
|
|
26
|
+
cookie_value = cookies[cookie_name]
|
|
27
|
+
|
|
28
|
+
# Default to expanded when no cookie exists
|
|
29
|
+
return :expanded if cookie_value.nil?
|
|
30
|
+
|
|
31
|
+
# Return expanded if cookie says "true", otherwise collapsed
|
|
32
|
+
(cookie_value == "true") ? :expanded : :collapsed
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if sidebar is currently open
|
|
36
|
+
#
|
|
37
|
+
# @param cookie_name [String] The cookie name (default: "sidebar_state")
|
|
38
|
+
# @return [Boolean] true if sidebar is open
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# <% if sidebar_open? %>
|
|
42
|
+
# <!-- Show sidebar-specific content -->
|
|
43
|
+
# <% end %>
|
|
44
|
+
#
|
|
45
|
+
def sidebar_open?(cookie_name = "sidebar_state")
|
|
46
|
+
sidebar_state(cookie_name) == :expanded
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if sidebar is currently closed
|
|
50
|
+
#
|
|
51
|
+
# @param cookie_name [String] The cookie name (default: "sidebar_state")
|
|
52
|
+
# @return [Boolean] true if sidebar is closed
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# <% if sidebar_closed? %>
|
|
56
|
+
# <!-- Show expanded content when sidebar is closed -->
|
|
57
|
+
# <% end %>
|
|
58
|
+
#
|
|
59
|
+
def sidebar_closed?(cookie_name = "sidebar_state")
|
|
60
|
+
sidebar_state(cookie_name) == :collapsed
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaquinaComponents
|
|
4
|
+
# Table Component Helper
|
|
5
|
+
#
|
|
6
|
+
# Provides a simple helper for rendering basic tables from collections.
|
|
7
|
+
# For complex tables, use the partials directly for full control.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage with collection
|
|
10
|
+
# <%= simple_table @invoices,
|
|
11
|
+
# columns: [
|
|
12
|
+
# { key: :number, label: "Invoice" },
|
|
13
|
+
# { key: :status, label: "Status" },
|
|
14
|
+
# { key: :amount, label: "Amount", align: :right }
|
|
15
|
+
# ] %>
|
|
16
|
+
#
|
|
17
|
+
# @example With caption and bordered variant
|
|
18
|
+
# <%= simple_table @users,
|
|
19
|
+
# columns: [
|
|
20
|
+
# { key: :name, label: "Name" },
|
|
21
|
+
# { key: :email, label: "Email" },
|
|
22
|
+
# { key: :role, label: "Role" }
|
|
23
|
+
# ],
|
|
24
|
+
# caption: "Active users",
|
|
25
|
+
# variant: :bordered %>
|
|
26
|
+
#
|
|
27
|
+
module TableHelper
|
|
28
|
+
# Render a simple table from a collection
|
|
29
|
+
#
|
|
30
|
+
# @param collection [Array, ActiveRecord::Relation] The collection to render
|
|
31
|
+
# @param columns [Array<Hash>] Column definitions with :key, :label, and optional :align
|
|
32
|
+
# @param caption [String, nil] Optional table caption
|
|
33
|
+
# @param variant [Symbol, nil] Container variant (:bordered)
|
|
34
|
+
# @param table_variant [Symbol, nil] Table variant (:striped)
|
|
35
|
+
# @param empty_message [String] Message to show when collection is empty
|
|
36
|
+
# @param row_id [Symbol, nil] Method to call for row ID (e.g., :id)
|
|
37
|
+
# @param html_options [Hash] Additional HTML options for the table
|
|
38
|
+
# @return [String] Rendered HTML
|
|
39
|
+
def simple_table(collection, columns:, caption: nil, variant: nil, table_variant: nil, empty_message: "No data available", row_id: nil, **html_options)
|
|
40
|
+
render partial: "components/simple_table", locals: {
|
|
41
|
+
collection: collection,
|
|
42
|
+
columns: columns,
|
|
43
|
+
caption: caption,
|
|
44
|
+
variant: variant,
|
|
45
|
+
table_variant: table_variant,
|
|
46
|
+
empty_message: empty_message,
|
|
47
|
+
row_id: row_id,
|
|
48
|
+
html_options: html_options
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate data attributes for table elements
|
|
53
|
+
# Useful when composing tables with other Rails helpers
|
|
54
|
+
#
|
|
55
|
+
# @example Using with content_tag
|
|
56
|
+
# <%= content_tag :table, **table_data_attrs do %>
|
|
57
|
+
# ...
|
|
58
|
+
# <% end %>
|
|
59
|
+
#
|
|
60
|
+
# @param variant [Symbol, nil] Table variant (:striped)
|
|
61
|
+
# @return [Hash] Data attributes hash
|
|
62
|
+
def table_data_attrs(variant: nil)
|
|
63
|
+
attrs = { data: { component: "table" } }
|
|
64
|
+
attrs[:data][:variant] = variant.to_s if variant
|
|
65
|
+
attrs
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Generate data attributes for table container
|
|
69
|
+
#
|
|
70
|
+
# @param variant [Symbol, nil] Container variant (:bordered)
|
|
71
|
+
# @return [Hash] Data attributes hash
|
|
72
|
+
def table_container_data_attrs(variant: nil)
|
|
73
|
+
attrs = { data: { table_part: "container" } }
|
|
74
|
+
attrs[:data][:variant] = variant.to_s if variant
|
|
75
|
+
attrs
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Generate data attributes for table row
|
|
79
|
+
#
|
|
80
|
+
# @param selected [Boolean] Whether the row is selected
|
|
81
|
+
# @return [Hash] Data attributes hash
|
|
82
|
+
def table_row_data_attrs(selected: false)
|
|
83
|
+
attrs = { data: { table_part: "row" } }
|
|
84
|
+
attrs[:data][:state] = "selected" if selected
|
|
85
|
+
attrs
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generate data attributes for table header
|
|
89
|
+
#
|
|
90
|
+
# @param sticky [Boolean] Whether the header is sticky
|
|
91
|
+
# @return [Hash] Data attributes hash
|
|
92
|
+
def table_header_data_attrs(sticky: false)
|
|
93
|
+
attrs = { data: { table_part: "header" } }
|
|
94
|
+
attrs[:data][:sticky] = "true" if sticky
|
|
95
|
+
attrs
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Generate data attributes for table head cell
|
|
99
|
+
# @return [Hash] Data attributes hash
|
|
100
|
+
def table_head_data_attrs
|
|
101
|
+
{ data: { table_part: "head" } }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Generate data attributes for table cell
|
|
105
|
+
#
|
|
106
|
+
# @param empty [Boolean] Whether this is an empty state cell
|
|
107
|
+
# @return [Hash] Data attributes hash
|
|
108
|
+
def table_cell_data_attrs(empty: false)
|
|
109
|
+
attrs = { data: { table_part: "cell" } }
|
|
110
|
+
attrs[:data][:empty] = "true" if empty
|
|
111
|
+
attrs
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Generate data attributes for table body
|
|
115
|
+
# @return [Hash] Data attributes hash
|
|
116
|
+
def table_body_data_attrs
|
|
117
|
+
{ data: { table_part: "body" } }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Generate data attributes for table footer
|
|
121
|
+
# @return [Hash] Data attributes hash
|
|
122
|
+
def table_footer_data_attrs
|
|
123
|
+
{ data: { table_part: "footer" } }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Generate data attributes for table caption
|
|
127
|
+
# @return [Hash] Data attributes hash
|
|
128
|
+
def table_caption_data_attrs
|
|
129
|
+
{ data: { table_part: "caption" } }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Convert alignment symbol to CSS class
|
|
133
|
+
#
|
|
134
|
+
# @param align [Symbol, nil] Alignment (:left, :center, :right)
|
|
135
|
+
# @return [String, nil] CSS class name
|
|
136
|
+
def table_alignment_class(align)
|
|
137
|
+
case align&.to_sym
|
|
138
|
+
when :right then "text-right"
|
|
139
|
+
when :center then "text-center"
|
|
140
|
+
else nil
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaquinaComponents
|
|
4
|
+
# Toggle Group Helper
|
|
5
|
+
#
|
|
6
|
+
# Provides convenient methods for creating toggle group components.
|
|
7
|
+
#
|
|
8
|
+
# @example Using partials directly
|
|
9
|
+
# <%%= render "components/toggle_group", type: :single, variant: :outline do %>
|
|
10
|
+
# <%%= render "components/toggle_group/item", value: "bold", aria_label: "Toggle bold" do %>
|
|
11
|
+
# <%%= icon_for :bold %>
|
|
12
|
+
# <%% end %>
|
|
13
|
+
# <%% end %>
|
|
14
|
+
#
|
|
15
|
+
# @example Using the helper with builder
|
|
16
|
+
# <%%= toggle_group type: :multiple, variant: :outline do |group| %>
|
|
17
|
+
# <%% group.item value: "bold", icon: :bold, aria_label: "Toggle bold" %>
|
|
18
|
+
# <%% group.item value: "italic", icon: :italic, aria_label: "Toggle italic" %>
|
|
19
|
+
# <%% end %>
|
|
20
|
+
#
|
|
21
|
+
# @example Simple data-driven helper
|
|
22
|
+
# <%%= toggle_group_simple type: :single, items: [
|
|
23
|
+
# { value: "left", icon: :align_left, aria_label: "Align left" },
|
|
24
|
+
# { value: "center", icon: :align_center, aria_label: "Align center" },
|
|
25
|
+
# { value: "right", icon: :align_right, aria_label: "Align right" }
|
|
26
|
+
# ] %>
|
|
27
|
+
#
|
|
28
|
+
module ToggleGroupHelper
|
|
29
|
+
# Renders a toggle group with builder pattern
|
|
30
|
+
#
|
|
31
|
+
# @param type [Symbol] Selection mode (:single, :multiple)
|
|
32
|
+
# @param variant [Symbol] Visual style (:default, :outline)
|
|
33
|
+
# @param size [Symbol] Size variant (:default, :sm, :lg)
|
|
34
|
+
# @param value [String, Array, nil] Initially selected value(s)
|
|
35
|
+
# @param disabled [Boolean] Disable all items
|
|
36
|
+
# @param css_classes [String] Additional CSS classes
|
|
37
|
+
# @param html_options [Hash] Additional HTML attributes
|
|
38
|
+
# @yield [ToggleGroupBuilder] Builder for adding items
|
|
39
|
+
# @return [String] Rendered HTML
|
|
40
|
+
def toggle_group(type: :single, variant: :default, size: :default, value: nil, disabled: false, css_classes: "", **html_options, &block)
|
|
41
|
+
builder = ToggleGroupBuilder.new(self, type: type, variant: variant, size: size, value: value, disabled: disabled)
|
|
42
|
+
|
|
43
|
+
if block && block.arity == 1
|
|
44
|
+
capture { yield(builder) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
render "components/toggle_group",
|
|
48
|
+
type: type,
|
|
49
|
+
variant: variant,
|
|
50
|
+
size: size,
|
|
51
|
+
value: value,
|
|
52
|
+
disabled: disabled,
|
|
53
|
+
css_classes: css_classes,
|
|
54
|
+
**html_options do
|
|
55
|
+
if block && block.arity == 1
|
|
56
|
+
builder.to_html
|
|
57
|
+
elsif block
|
|
58
|
+
capture(&block)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Renders a simple data-driven toggle group
|
|
64
|
+
#
|
|
65
|
+
# @param type [Symbol] Selection mode (:single, :multiple)
|
|
66
|
+
# @param items [Array<Hash>] Array of item configurations
|
|
67
|
+
# @param variant [Symbol] Visual style
|
|
68
|
+
# @param size [Symbol] Size variant
|
|
69
|
+
# @param value [String, Array, nil] Initially selected value(s)
|
|
70
|
+
# @return [String] Rendered HTML
|
|
71
|
+
def toggle_group_simple(items:, type: :single, variant: :default, size: :default, value: nil, disabled: false, css_classes: "", **html_options)
|
|
72
|
+
selected_values = normalize_value(value)
|
|
73
|
+
|
|
74
|
+
render "components/toggle_group",
|
|
75
|
+
type: type,
|
|
76
|
+
variant: variant,
|
|
77
|
+
size: size,
|
|
78
|
+
value: value,
|
|
79
|
+
disabled: disabled,
|
|
80
|
+
css_classes: css_classes,
|
|
81
|
+
**html_options do
|
|
82
|
+
safe_join(items.map do |item|
|
|
83
|
+
item_value = item[:value].to_s
|
|
84
|
+
is_pressed = selected_values.include?(item_value)
|
|
85
|
+
|
|
86
|
+
render "components/toggle_group/item",
|
|
87
|
+
value: item_value,
|
|
88
|
+
pressed: is_pressed,
|
|
89
|
+
disabled: item[:disabled] || disabled,
|
|
90
|
+
aria_label: item[:aria_label] do
|
|
91
|
+
parts = []
|
|
92
|
+
parts << icon_for(item[:icon]) if item[:icon] && respond_to?(:icon_for)
|
|
93
|
+
parts << item[:label] if item[:label]
|
|
94
|
+
safe_join(parts)
|
|
95
|
+
end
|
|
96
|
+
end)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Builder class for toggle group
|
|
101
|
+
class ToggleGroupBuilder
|
|
102
|
+
def initialize(view_context, type:, variant:, size:, value:, disabled:)
|
|
103
|
+
@view = view_context
|
|
104
|
+
@type = type
|
|
105
|
+
@variant = variant
|
|
106
|
+
@size = size
|
|
107
|
+
@value = value
|
|
108
|
+
@disabled = disabled
|
|
109
|
+
@items = []
|
|
110
|
+
@selected_values = normalize_value(value)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Add an item to the toggle group
|
|
114
|
+
def item(value:, label: nil, icon: nil, disabled: false, aria_label: nil, **options, &block)
|
|
115
|
+
is_pressed = @selected_values.include?(value.to_s)
|
|
116
|
+
|
|
117
|
+
@items << {
|
|
118
|
+
value: value,
|
|
119
|
+
label: label,
|
|
120
|
+
icon: icon,
|
|
121
|
+
disabled: disabled || @disabled,
|
|
122
|
+
aria_label: aria_label,
|
|
123
|
+
pressed: is_pressed,
|
|
124
|
+
options: options,
|
|
125
|
+
block: block
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def to_html
|
|
130
|
+
@view.safe_join(@items.map { |item| render_item(item) })
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def render_item(item)
|
|
136
|
+
@view.render "components/toggle_group/item",
|
|
137
|
+
value: item[:value],
|
|
138
|
+
pressed: item[:pressed],
|
|
139
|
+
disabled: item[:disabled],
|
|
140
|
+
aria_label: item[:aria_label],
|
|
141
|
+
**item[:options] do
|
|
142
|
+
if item[:block]
|
|
143
|
+
@view.capture(&item[:block])
|
|
144
|
+
else
|
|
145
|
+
parts = []
|
|
146
|
+
parts << @view.icon_for(item[:icon]) if item[:icon] && @view.respond_to?(:icon_for)
|
|
147
|
+
parts << item[:label] if item[:label]
|
|
148
|
+
@view.safe_join(parts)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def normalize_value(value)
|
|
154
|
+
case value
|
|
155
|
+
when Array then value.map(&:to_s)
|
|
156
|
+
when nil then []
|
|
157
|
+
else [value.to_s]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def normalize_value(value)
|
|
165
|
+
case value
|
|
166
|
+
when Array then value.map(&:to_s)
|
|
167
|
+
when nil then []
|
|
168
|
+
else [value.to_s]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["item", "ellipsis", "ellipsisSeparator"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.windowResizeHandler = this.handleResize.bind(this)
|
|
8
|
+
window.addEventListener('resize', this.windowResizeHandler)
|
|
9
|
+
this.handleResize()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
disconnect() {
|
|
13
|
+
window.removeEventListener('resize', this.windowResizeHandler)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
handleResize() {
|
|
17
|
+
// Get visible width of container
|
|
18
|
+
const containerWidth = this.element.clientWidth
|
|
19
|
+
const items = this.itemTargets
|
|
20
|
+
const ellipsis = this.hasEllipsisTarget ? this.ellipsisTarget : null
|
|
21
|
+
const ellipsisSeparator = this.hasEllipsisSeparatorTarget ? this.ellipsisSeparatorTarget : null
|
|
22
|
+
|
|
23
|
+
// Always show first and last items
|
|
24
|
+
if (items.length < 3 || !ellipsis) {
|
|
25
|
+
return; // Not enough items to collapse or no ellipsis element
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Reset visibility
|
|
29
|
+
if (ellipsis) ellipsis.classList.add('hidden')
|
|
30
|
+
if (ellipsisSeparator) ellipsisSeparator.classList.add('hidden')
|
|
31
|
+
|
|
32
|
+
items.forEach(item => {
|
|
33
|
+
item.classList.remove('hidden')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Check if we need to collapse items
|
|
37
|
+
let totalWidth = 0
|
|
38
|
+
items.forEach(item => {
|
|
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')
|
|
45
|
+
if (ellipsisSeparator) ellipsisSeparator.classList.remove('hidden')
|
|
46
|
+
|
|
47
|
+
// Start hiding middle items until we fit
|
|
48
|
+
for (let i = items.length - 2; i > 0; i--) {
|
|
49
|
+
if (i !== 0 && i !== items.length - 1) {
|
|
50
|
+
items[i].classList.add('hidden')
|
|
51
|
+
|
|
52
|
+
// Recalculate total width
|
|
53
|
+
totalWidth = 0
|
|
54
|
+
|
|
55
|
+
if (ellipsis) totalWidth += ellipsis.offsetWidth
|
|
56
|
+
if (ellipsisSeparator) totalWidth += ellipsisSeparator.offsetWidth
|
|
57
|
+
|
|
58
|
+
items.forEach(item => {
|
|
59
|
+
if (!item.classList.contains('hidden')) {
|
|
60
|
+
totalWidth += item.offsetWidth
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (totalWidth <= containerWidth) {
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|