maquina-components 0.2.0 → 0.3.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 +51 -0
- data/app/assets/stylesheets/combobox.css +218 -0
- data/app/assets/stylesheets/toast.css +433 -0
- data/app/assets/tailwind/maquina_components_engine/engine.css +2 -0
- data/app/helpers/maquina_components/combobox_helper.rb +300 -0
- data/app/helpers/maquina_components/toast_helper.rb +115 -0
- data/app/javascript/controllers/combobox_controller.js +325 -0
- data/app/javascript/controllers/toast_controller.js +115 -0
- data/app/javascript/controllers/toaster_controller.js +226 -0
- data/app/views/components/_combobox.html.erb +13 -0
- data/app/views/components/_toast.html.erb +53 -0
- data/app/views/components/_toaster.html.erb +17 -0
- data/app/views/components/combobox/_content.html.erb +17 -0
- data/app/views/components/combobox/_empty.html.erb +9 -0
- data/app/views/components/combobox/_group.html.erb +8 -0
- data/app/views/components/combobox/_input.html.erb +18 -0
- data/app/views/components/combobox/_label.html.erb +8 -0
- data/app/views/components/combobox/_list.html.erb +8 -0
- data/app/views/components/combobox/_option.html.erb +24 -0
- data/app/views/components/combobox/_separator.html.erb +6 -0
- data/app/views/components/combobox/_trigger.html.erb +22 -0
- data/app/views/components/toast/_action.html.erb +14 -0
- data/app/views/components/toast/_description.html.erb +8 -0
- data/app/views/components/toast/_title.html.erb +8 -0
- data/lib/maquina_components/version.rb +1 -1
- metadata +24 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaquinaComponents
|
|
4
|
+
# Combobox Helper
|
|
5
|
+
#
|
|
6
|
+
# Provides a builder pattern for creating combobox components with a clean API.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# <%= combobox placeholder: "Select framework..." do |cb| %>
|
|
10
|
+
# <% cb.trigger %>
|
|
11
|
+
# <% cb.content do %>
|
|
12
|
+
# <% cb.input placeholder: "Search..." %>
|
|
13
|
+
# <% cb.list do %>
|
|
14
|
+
# <% cb.option value: "nextjs" do %>Next.js<% end %>
|
|
15
|
+
# <% cb.option value: "remix" do %>Remix<% end %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
# <% cb.empty %>
|
|
18
|
+
# <% end %>
|
|
19
|
+
# <% end %>
|
|
20
|
+
#
|
|
21
|
+
# @example Simple data-driven combobox
|
|
22
|
+
# <%= combobox_simple placeholder: "Select framework...",
|
|
23
|
+
# options: [
|
|
24
|
+
# { value: "nextjs", label: "Next.js" },
|
|
25
|
+
# { value: "remix", label: "Remix" }
|
|
26
|
+
# ] %>
|
|
27
|
+
#
|
|
28
|
+
# @example With groups
|
|
29
|
+
# <%= combobox placeholder: "Select..." do |cb| %>
|
|
30
|
+
# <% cb.trigger %>
|
|
31
|
+
# <% cb.content do %>
|
|
32
|
+
# <% cb.input %>
|
|
33
|
+
# <% cb.list do %>
|
|
34
|
+
# <% cb.group do %>
|
|
35
|
+
# <% cb.label "Frontend" %>
|
|
36
|
+
# <% cb.option value: "react" do %>React<% end %>
|
|
37
|
+
# <% cb.option value: "vue" do %>Vue<% end %>
|
|
38
|
+
# <% end %>
|
|
39
|
+
# <% cb.separator %>
|
|
40
|
+
# <% cb.group do %>
|
|
41
|
+
# <% cb.label "Backend" %>
|
|
42
|
+
# <% cb.option value: "rails" do %>Rails<% end %>
|
|
43
|
+
# <% end %>
|
|
44
|
+
# <% end %>
|
|
45
|
+
# <% cb.empty %>
|
|
46
|
+
# <% end %>
|
|
47
|
+
# <% end %>
|
|
48
|
+
#
|
|
49
|
+
module ComboboxHelper
|
|
50
|
+
# Renders a combobox using the builder pattern
|
|
51
|
+
#
|
|
52
|
+
# @param id [String, nil] Explicit ID for the combobox
|
|
53
|
+
# @param name [String, nil] Form field name
|
|
54
|
+
# @param value [String, nil] Currently selected value
|
|
55
|
+
# @param placeholder [String] Placeholder text
|
|
56
|
+
# @param css_classes [String] Additional CSS classes
|
|
57
|
+
# @param html_options [Hash] Additional HTML attributes
|
|
58
|
+
# @yield [ComboboxBuilder] Builder instance for constructing the combobox
|
|
59
|
+
# @return [String] Rendered HTML
|
|
60
|
+
def combobox(id: nil, name: nil, value: nil, placeholder: "Select...", css_classes: "", **html_options, &block)
|
|
61
|
+
builder = ComboboxBuilder.new(self, placeholder: placeholder)
|
|
62
|
+
combobox_id = id || "combobox-#{SecureRandom.hex(4)}"
|
|
63
|
+
builder.combobox_id = combobox_id
|
|
64
|
+
|
|
65
|
+
capture(builder, &block)
|
|
66
|
+
|
|
67
|
+
render "components/combobox",
|
|
68
|
+
id: combobox_id,
|
|
69
|
+
name: name,
|
|
70
|
+
value: value,
|
|
71
|
+
placeholder: placeholder,
|
|
72
|
+
css_classes: css_classes,
|
|
73
|
+
**html_options do |_id|
|
|
74
|
+
builder.to_html
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Renders a simple combobox from data
|
|
79
|
+
#
|
|
80
|
+
# @param options [Array<Hash>] Array of option configurations with :value and :label keys
|
|
81
|
+
# @param placeholder [String] Placeholder text
|
|
82
|
+
# @param search_placeholder [String] Search input placeholder
|
|
83
|
+
# @param empty_text [String] Text shown when no results
|
|
84
|
+
# @param value [String, nil] Currently selected value
|
|
85
|
+
# @param name [String, nil] Form field name
|
|
86
|
+
# @param trigger_options [Hash] Options for the trigger button
|
|
87
|
+
# @param content_options [Hash] Options for the content container
|
|
88
|
+
# @return [String] Rendered HTML
|
|
89
|
+
def combobox_simple(
|
|
90
|
+
options:,
|
|
91
|
+
placeholder: "Select...",
|
|
92
|
+
search_placeholder: "Search...",
|
|
93
|
+
empty_text: "No results found.",
|
|
94
|
+
value: nil,
|
|
95
|
+
name: nil,
|
|
96
|
+
trigger_options: {},
|
|
97
|
+
content_options: {}
|
|
98
|
+
)
|
|
99
|
+
combobox(placeholder: placeholder, value: value, name: name) do |cb|
|
|
100
|
+
cb.trigger(**trigger_options)
|
|
101
|
+
|
|
102
|
+
cb.content(**content_options) do
|
|
103
|
+
cb.input(placeholder: search_placeholder)
|
|
104
|
+
cb.list do
|
|
105
|
+
options.each do |opt|
|
|
106
|
+
selected = value.present? && opt[:value].to_s == value.to_s
|
|
107
|
+
cb.option(value: opt[:value], selected: selected, disabled: opt[:disabled]) do
|
|
108
|
+
opt[:label] || opt[:value]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
cb.empty(text: empty_text)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Builder class for constructing comboboxes
|
|
118
|
+
class ComboboxBuilder
|
|
119
|
+
attr_accessor :combobox_id
|
|
120
|
+
|
|
121
|
+
def initialize(view_context, placeholder:)
|
|
122
|
+
@view = view_context
|
|
123
|
+
@placeholder = placeholder
|
|
124
|
+
@parts = []
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Defines the trigger button
|
|
128
|
+
#
|
|
129
|
+
# @param variant [Symbol] Button variant
|
|
130
|
+
# @param size [Symbol] Button size
|
|
131
|
+
# @param options [Hash] Additional options
|
|
132
|
+
def trigger(variant: :outline, size: :default, **options)
|
|
133
|
+
@parts << @view.render(
|
|
134
|
+
"components/combobox/trigger",
|
|
135
|
+
for_id: combobox_id,
|
|
136
|
+
placeholder: @placeholder,
|
|
137
|
+
variant: variant,
|
|
138
|
+
size: size,
|
|
139
|
+
**options
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Defines the content popover
|
|
144
|
+
#
|
|
145
|
+
# @param align [Symbol] Horizontal alignment
|
|
146
|
+
# @param width [Symbol] Width preset
|
|
147
|
+
# @param options [Hash] Additional options
|
|
148
|
+
# @yield Block containing combobox content
|
|
149
|
+
def content(align: :start, width: :default, **options, &block)
|
|
150
|
+
content_builder = ComboboxContentBuilder.new(@view)
|
|
151
|
+
@view.capture(content_builder, &block)
|
|
152
|
+
|
|
153
|
+
@parts << @view.render(
|
|
154
|
+
"components/combobox/content",
|
|
155
|
+
id: combobox_id,
|
|
156
|
+
align: align,
|
|
157
|
+
width: width,
|
|
158
|
+
**options
|
|
159
|
+
) { content_builder.to_html }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Shortcut to add input directly (delegates to content builder)
|
|
163
|
+
def input(placeholder: "Search...", **options)
|
|
164
|
+
@current_content_builder&.input(placeholder: placeholder, **options)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Shortcut to add list directly (delegates to content builder)
|
|
168
|
+
def list(**options, &block)
|
|
169
|
+
@current_content_builder&.list(**options, &block)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Shortcut to add empty directly (delegates to content builder)
|
|
173
|
+
def empty(text: "No results found.", **options)
|
|
174
|
+
@current_content_builder&.empty(text: text, **options)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Generates the final HTML
|
|
178
|
+
def to_html
|
|
179
|
+
@view.safe_join(@parts)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Builder for combobox content
|
|
184
|
+
class ComboboxContentBuilder
|
|
185
|
+
def initialize(view_context)
|
|
186
|
+
@view = view_context
|
|
187
|
+
@parts = []
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Adds the search input
|
|
191
|
+
#
|
|
192
|
+
# @param placeholder [String] Input placeholder
|
|
193
|
+
# @param options [Hash] Additional options
|
|
194
|
+
def input(placeholder: "Search...", **options)
|
|
195
|
+
@parts << @view.render(
|
|
196
|
+
"components/combobox/input",
|
|
197
|
+
placeholder: placeholder,
|
|
198
|
+
**options
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Adds the options list container
|
|
203
|
+
#
|
|
204
|
+
# @param options [Hash] Additional options
|
|
205
|
+
# @yield Block containing options
|
|
206
|
+
def list(**options, &block)
|
|
207
|
+
list_builder = ComboboxListBuilder.new(@view)
|
|
208
|
+
@view.capture(list_builder, &block)
|
|
209
|
+
|
|
210
|
+
@parts << @view.render(
|
|
211
|
+
"components/combobox/list",
|
|
212
|
+
**options
|
|
213
|
+
) { list_builder.to_html }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Adds the empty state
|
|
217
|
+
#
|
|
218
|
+
# @param text [String] Empty state text
|
|
219
|
+
# @param options [Hash] Additional options
|
|
220
|
+
def empty(text: "No results found.", **options)
|
|
221
|
+
@parts << @view.render(
|
|
222
|
+
"components/combobox/empty",
|
|
223
|
+
text: text,
|
|
224
|
+
**options
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Generates the final HTML
|
|
229
|
+
def to_html
|
|
230
|
+
@view.safe_join(@parts)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Builder for combobox list content
|
|
235
|
+
class ComboboxListBuilder
|
|
236
|
+
def initialize(view_context)
|
|
237
|
+
@view = view_context
|
|
238
|
+
@parts = []
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Adds an option
|
|
242
|
+
#
|
|
243
|
+
# @param value [String] Option value
|
|
244
|
+
# @param selected [Boolean] Whether selected
|
|
245
|
+
# @param disabled [Boolean] Whether disabled
|
|
246
|
+
# @param options [Hash] Additional options
|
|
247
|
+
# @yield Block for option content
|
|
248
|
+
def option(value:, selected: false, disabled: false, **options, &block)
|
|
249
|
+
content = @view.capture(&block) if block
|
|
250
|
+
@parts << @view.render(
|
|
251
|
+
"components/combobox/option",
|
|
252
|
+
value: value,
|
|
253
|
+
selected: selected,
|
|
254
|
+
disabled: disabled,
|
|
255
|
+
**options
|
|
256
|
+
) { content }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Adds a group
|
|
260
|
+
#
|
|
261
|
+
# @param options [Hash] Additional options
|
|
262
|
+
# @yield Block containing group items
|
|
263
|
+
def group(**options, &block)
|
|
264
|
+
group_builder = ComboboxListBuilder.new(@view)
|
|
265
|
+
@view.capture(group_builder, &block)
|
|
266
|
+
|
|
267
|
+
@parts << @view.render(
|
|
268
|
+
"components/combobox/group",
|
|
269
|
+
**options
|
|
270
|
+
) { group_builder.to_html }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Adds a label
|
|
274
|
+
#
|
|
275
|
+
# @param text [String, nil] Label text
|
|
276
|
+
# @param options [Hash] Additional options
|
|
277
|
+
# @yield Optional block for custom content
|
|
278
|
+
def label(text = nil, **options, &block)
|
|
279
|
+
@parts << @view.render(
|
|
280
|
+
"components/combobox/label",
|
|
281
|
+
text: text,
|
|
282
|
+
**options,
|
|
283
|
+
&block
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Adds a separator
|
|
288
|
+
#
|
|
289
|
+
# @param options [Hash] Additional options
|
|
290
|
+
def separator(**options)
|
|
291
|
+
@parts << @view.render("components/combobox/separator", **options)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Generates the final HTML
|
|
295
|
+
def to_html
|
|
296
|
+
@view.safe_join(@parts)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaquinaComponents
|
|
4
|
+
# Toast Helper
|
|
5
|
+
#
|
|
6
|
+
# Provides helpers for rendering toast notifications.
|
|
7
|
+
#
|
|
8
|
+
# @example Render flash messages as toasts
|
|
9
|
+
# <%= toast_flash_messages %>
|
|
10
|
+
#
|
|
11
|
+
# @example Render a single toast
|
|
12
|
+
# <%= toast :success, "Profile updated!" %>
|
|
13
|
+
#
|
|
14
|
+
# @example Toast with description
|
|
15
|
+
# <%= toast :error, "Save failed", description: "Please check your connection." %>
|
|
16
|
+
#
|
|
17
|
+
# @example Toast with action
|
|
18
|
+
# <%= toast :info, "New version available" do %>
|
|
19
|
+
# <%= render "components/toast/action", label: "Refresh", href: root_path %>
|
|
20
|
+
# <% end %>
|
|
21
|
+
#
|
|
22
|
+
module ToastHelper
|
|
23
|
+
# Flash type to toast variant mapping
|
|
24
|
+
FLASH_VARIANTS = {
|
|
25
|
+
notice: :success,
|
|
26
|
+
success: :success,
|
|
27
|
+
alert: :error,
|
|
28
|
+
error: :error,
|
|
29
|
+
warning: :warning,
|
|
30
|
+
warn: :warning,
|
|
31
|
+
info: :info
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Render all flash messages as toasts
|
|
35
|
+
#
|
|
36
|
+
# @param exclude [Array<Symbol>] Flash types to exclude
|
|
37
|
+
# @return [String] HTML-safe string of toast elements
|
|
38
|
+
def toast_flash_messages(exclude: [])
|
|
39
|
+
return "" if flash.empty?
|
|
40
|
+
|
|
41
|
+
toasts = flash.map do |type, message|
|
|
42
|
+
next if exclude.include?(type.to_sym)
|
|
43
|
+
next if message.blank?
|
|
44
|
+
|
|
45
|
+
variant = flash_to_variant(type)
|
|
46
|
+
render "components/toast", variant: variant, title: message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
safe_join(toasts.compact)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Render a single toast
|
|
53
|
+
#
|
|
54
|
+
# @param variant [Symbol] Toast variant (:default, :success, :info, :warning, :error)
|
|
55
|
+
# @param title [String] Toast title
|
|
56
|
+
# @param description [String, nil] Optional description
|
|
57
|
+
# @param options [Hash] Additional options passed to the toast partial
|
|
58
|
+
# @yield Optional block for custom content (e.g., action button)
|
|
59
|
+
# @return [String] HTML-safe toast element
|
|
60
|
+
def toast(variant, title, description: nil, **options, &block)
|
|
61
|
+
render "components/toast",
|
|
62
|
+
variant: variant,
|
|
63
|
+
title: title,
|
|
64
|
+
description: description,
|
|
65
|
+
**options,
|
|
66
|
+
&block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Render a success toast
|
|
70
|
+
#
|
|
71
|
+
# @param title [String] Toast title
|
|
72
|
+
# @param options [Hash] Additional options
|
|
73
|
+
# @return [String] HTML-safe toast element
|
|
74
|
+
def toast_success(title, **options, &block)
|
|
75
|
+
toast(:success, title, **options, &block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Render an error toast
|
|
79
|
+
#
|
|
80
|
+
# @param title [String] Toast title
|
|
81
|
+
# @param options [Hash] Additional options
|
|
82
|
+
# @return [String] HTML-safe toast element
|
|
83
|
+
def toast_error(title, **options, &block)
|
|
84
|
+
toast(:error, title, **options, &block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Render a warning toast
|
|
88
|
+
#
|
|
89
|
+
# @param title [String] Toast title
|
|
90
|
+
# @param options [Hash] Additional options
|
|
91
|
+
# @return [String] HTML-safe toast element
|
|
92
|
+
def toast_warning(title, **options, &block)
|
|
93
|
+
toast(:warning, title, **options, &block)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Render an info toast
|
|
97
|
+
#
|
|
98
|
+
# @param title [String] Toast title
|
|
99
|
+
# @param options [Hash] Additional options
|
|
100
|
+
# @return [String] HTML-safe toast element
|
|
101
|
+
def toast_info(title, **options, &block)
|
|
102
|
+
toast(:info, title, **options, &block)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Convert flash type to toast variant
|
|
108
|
+
#
|
|
109
|
+
# @param type [String, Symbol] Flash type
|
|
110
|
+
# @return [Symbol] Toast variant
|
|
111
|
+
def flash_to_variant(type)
|
|
112
|
+
FLASH_VARIANTS[type.to_sym] || :default
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|