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.
@@ -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