rails-active-ui 0.3.5 → 0.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0058e3cc403ae342263ae7ed336067515a465e050c833071d582e2724fa0a451'
4
- data.tar.gz: 94956bddda4efa13eaf12fbed9de95fed87a999d33b871d80ae1ce992aa9de55
3
+ metadata.gz: dc0cc8da3df26f99e7a8cc99484217887250d5012cf7dabb3c226ce44a0c0dfa
4
+ data.tar.gz: fe3478bbc49e83fd764b9731d45938d3a211106c80c17efc93d7a084e5930db2
5
5
  SHA512:
6
- metadata.gz: b5fda493b980890c6a882563b8b04cb01f7e5b7b6a0df9dc567d5ccf6f565cd8d80fd78ab686b45cf7ed7e4ad43b2877269095730991cfb7b39c8d12cbd1ce91
7
- data.tar.gz: 937f9a991da1899949b425735a5926c7a2277c6769a77b39822c918e59afb0454f0f69fbebb9c4c85895ee058ee1e39948d5a8f6f29e0dcaf4d9760de10513f5
6
+ metadata.gz: c6a8ca84a74bd5d08eb650e612187dc09d7e5b11b1559b219a883e6846860018ae8e0238f5a653be4cd4d686f8dc34200dce6258f0e90d9e98511f66773c4808
7
+ data.tar.gz: 03ead102486b56deee8c6bd01db61a09324d5bfd25f150b33aaf144ad3d0823897fd0186e8b1211d919861a303d3df50cafa0b1f3666966449772655f634da3d
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # rails-active-ui
2
+
3
+ A Fomantic-UI component system for Rails. Views use `.html.ruby` files with PascalCase component calls.
4
+
5
+ ## Setup
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "rails-active-ui"
11
+ ```
12
+
13
+ ### Engine initializers
14
+
15
+ The gem's engine (`Ui::Engine`) registers the following automatically:
16
+
17
+ **Autoload paths** -- `app/lib` and `app/blocks` are added to the autoload paths so `Component` and block classes are available everywhere.
18
+
19
+ **Asset paths** -- `formantic-ui/` (Fomantic-UI CSS/JS distribution) and `app/javascript/` (Stimulus controllers) are added to Propshaft's asset paths. Reference them in your layout:
20
+
21
+ ```ruby
22
+ # CSS
23
+ StylesheetLink("stylesheets.css") # Fomantic-UI stylesheet
24
+
25
+ # jQuery + Fomantic-UI component JS (must come before importmap)
26
+ text fui_javascript_tags
27
+
28
+ # Importmap (loads Stimulus controllers)
29
+ JavascriptImportmap()
30
+ ```
31
+
32
+ **Importmap** -- the gem's `config/importmap.rb` is prepended to the app's importmap. It pins:
33
+ - `ui` -- the main entry point (`ui/index.js`)
34
+ - `ui/controllers/*` -- all Fomantic-UI Stimulus bridge controllers
35
+ - `emoji-picker-element` -- emoji picker from CDN
36
+
37
+ **Helpers** -- `ComponentHelper` and `FuiHelper` are included into `ActionView::Base` automatically.
38
+
39
+ ### Stimulus controllers
40
+
41
+ Register the Fomantic-UI Stimulus controllers in your app's `app/javascript/controllers/index.js`:
42
+
43
+ ```javascript
44
+ import { Application } from "@hotwired/stimulus"
45
+ import { registerFuiControllers } from "ui"
46
+
47
+ const application = Application.start()
48
+ registerFuiControllers(application)
49
+ ```
50
+
51
+ These are thin jQuery bridge controllers that initialize Fomantic-UI widgets in `connect()` and tear them down in `disconnect()`, making them Turbo-compatible.
52
+
53
+ ### Rails engine usage
54
+
55
+ If you're using rails-active-ui inside a Rails engine, your engine needs to register the gem's assets manually since engines don't inherit the host app's asset paths:
56
+
57
+ ```ruby
58
+ # lib/my_engine/engine.rb
59
+ class Engine < ::Rails::Engine
60
+ initializer "my_engine.assets" do |app|
61
+ ui_gem = Gem::Specification.find_by_name("rails-active-ui")
62
+ app.config.assets.paths << File.join(ui_gem.gem_dir, "app/assets")
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Form Builder
68
+
69
+ rails-active-ui ships with `FomanticFormBuilder`, a drop-in `ActionView::Helpers::FormBuilder` subclass that wraps every field helper in Fomantic-UI markup.
70
+
71
+ Set it as the default in your `ApplicationController`:
72
+
73
+ ```ruby
74
+ class ApplicationController < ActionController::Base
75
+ ActionView::Base.default_form_builder = FomanticFormBuilder
76
+ end
77
+ ```
78
+
79
+ Inside a `Form()` block, method_missing delegates to the form builder. Standard Rails form helpers become PascalCase calls:
80
+
81
+ ```ruby
82
+ Form(url: users_path, method: :post) {
83
+ TextField(:name, required: true)
84
+ EmailField(:email)
85
+ Select(:role, [["Admin", "admin"], ["User", "user"]], dropdown: true)
86
+ CheckBox(:terms, label: "I agree to the Terms")
87
+ HiddenField(:token)
88
+ Submit("Save", color: "green")
89
+ }
90
+ ```
91
+
92
+ ### Available form helpers
93
+
94
+ | `.html.ruby` call | Form builder method | Description |
95
+ |---|---|---|
96
+ | `TextField(:name)` | `f.text_field :name` | Text input wrapped in `.field` |
97
+ | `EmailField(:email)` | `f.email_field :email` | Email input |
98
+ | `PasswordField(:password)` | `f.password_field :password` | Password input |
99
+ | `NumberField(:age)` | `f.number_field :age` | Number input |
100
+ | `TextArea(:bio)` | `f.text_area :bio` | Textarea |
101
+ | `Select(:role, choices)` | `f.select :role, choices` | Select dropdown |
102
+ | `CheckBox(:terms)` | `f.check_box :terms` | Checkbox with Fomantic styling |
103
+ | `RadioButton(:plan, "pro")` | `f.radio_button :plan, "pro"` | Radio button |
104
+ | `HiddenField(:token)` | `f.hidden_field :token` | Hidden input (no wrapper) |
105
+ | `FileField(:avatar)` | `f.file_field :avatar` | File upload |
106
+ | `Submit("Save")` | `f.submit "Save"` | Submit button with Fomantic styling |
107
+
108
+ ### Field options
109
+
110
+ All field helpers accept these options:
111
+
112
+ - `label:` -- override label text (`nil` to suppress)
113
+ - `required:` -- adds "required" class and asterisk
114
+ - `disabled:` -- adds "disabled" class
115
+ - `inline:` -- label sits beside the input
116
+ - `width:` -- Fomantic grid column word (e.g. `"six"`, `"three"`)
117
+ - `error:` -- error message string
118
+ - `hint:` -- grey note beneath the input
119
+ - `field_class:` -- extra classes on the wrapping `.field` div
120
+ - `input_class:` -- extra classes on the input element
121
+
122
+ ### Submit button options
123
+
124
+ - `color:` -- Fomantic color (e.g. `"green"`, `"red"`, `"blue"`)
125
+ - `size:` -- Fomantic size (e.g. `"tiny"`, `"large"`)
126
+ - `basic:` -- basic button style
127
+ - `icon:` -- icon name (e.g. `"checkmark"`)
128
+
129
+ ### Field groups
130
+
131
+ ```ruby
132
+ Form(url: users_path) {
133
+ FieldsGroup(equal_width: true) {
134
+ TextField(:first_name)
135
+ TextField(:last_name)
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Form-level messages
141
+
142
+ ```ruby
143
+ Form(url: users_path) {
144
+ ErrorMessage("Something went wrong", ["Email is taken"])
145
+ SuccessMessage("All done!", "Profile updated.")
146
+ WarningMessage("Heads up", ["Verify your email"])
147
+ }
148
+ ```
@@ -0,0 +1,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FomanticFormBuilder
4
+ #
5
+ # A Rails FormBuilder wrapping every helper in Fomantic-UI markup.
6
+ #
7
+ # Usage in a view:
8
+ #
9
+ # <%= form_with model: @user, builder: FomanticFormBuilder do |f| %>
10
+ # <%= f.text_field :name %>
11
+ # <%= f.email_field :email, required: true %>
12
+ # <%= f.select :role, [['Admin', 'admin'], ['User', 'user']], dropdown: true %>
13
+ # <%= f.check_box :terms, label: 'I agree to the Terms and Conditions' %>
14
+ # <%= f.fields_group(equal_width: true) do %>
15
+ # <%= f.text_field :first_name %>
16
+ # <%= f.text_field :last_name %>
17
+ # <% end %>
18
+ # <%= f.submit 'Save', color: 'green' %>
19
+ # <% end %>
20
+ #
21
+ # Field options (shared across all helpers):
22
+ # label: String – override label text (nil to suppress label)
23
+ # required: Boolean – adds "required" class and asterisk
24
+ # disabled: Boolean – adds "disabled" class
25
+ # readonly: Boolean – adds "read-only" class
26
+ # inline: Boolean – label sits beside the input
27
+ # width: String – Fomantic grid column word, e.g. "six", "three"
28
+ # error: String – error message; adds "error" class + inline message
29
+ # warning: String – warning message; adds "warning" class + inline message
30
+ # hint: String – rendered as a small grey note beneath the input
31
+ # field_class: String – extra classes on the wrapping .field div
32
+ # input_class: String – extra classes on the input element itself
33
+ #
34
+ module Ui
35
+ class FomanticFormBuilder < ActionView::Helpers::FormBuilder
36
+ COLUMN_WORDS = %w[
37
+ one two three four five six seven eight nine ten
38
+ eleven twelve thirteen fourteen fifteen sixteen
39
+ ].freeze
40
+
41
+ # ──────────────────────────────────────────────────────────────────────────
42
+ # Text-like inputs
43
+ # ──────────────────────────────────────────────────────────────────────────
44
+
45
+ %i[
46
+ text_field email_field password_field
47
+ number_field url_field telephone_field phone_field
48
+ search_field color_field date_field datetime_local_field
49
+ month_field week_field time_field range_field
50
+ ].each do |method_name|
51
+ define_method(method_name) do |attribute, options = {}|
52
+ fomantic_field(attribute, options) do |attr, opts|
53
+ opts[:class] = class_names("", opts.delete(:input_class))
54
+ super(attr, opts)
55
+ end
56
+ end
57
+ end
58
+
59
+ # ──────────────────────────────────────────────────────────────────────────
60
+ # Text area
61
+ # ──────────────────────────────────────────────────────────────────────────
62
+
63
+ def text_area(attribute, options = {})
64
+ transparent = options.delete(:transparent)
65
+ fomantic_field(attribute, options) do |attr, opts|
66
+ input_class = class_names(("transparent" if transparent), opts.delete(:input_class))
67
+ opts[:class] = input_class.presence
68
+ super(attr, opts)
69
+ end
70
+ end
71
+
72
+ # ──────────────────────────────────────────────────────────────────────────
73
+ # Emoji picker
74
+ # ──────────────────────────────────────────────────────────────────────────
75
+
76
+ def emoji_field(attribute, options = {})
77
+ fomantic_field(attribute, options) do |attr, opts|
78
+ current = object&.public_send(attr) rescue nil
79
+ name = object_name ? "#{object_name}[#{attr}]" : attr.to_s
80
+
81
+ @template.tag.div(data: { controller: "fui-emoji-picker" }) {
82
+ @template.safe_join([
83
+ @template.hidden_field_tag(name, current, data: { fui_emoji_picker_target: "input" }),
84
+ @template.tag.button(
85
+ type: "button",
86
+ class: "ui basic button",
87
+ data: { fui_emoji_picker_target: "preview", action: "click->fui-emoji-picker#toggle" }
88
+ ) { (current.presence || "Pick emoji").html_safe },
89
+ @template.tag.div(
90
+ style: "display:none; position:absolute; z-index:1000;",
91
+ data: { fui_emoji_picker_target: "dropdown" }
92
+ )
93
+ ])
94
+ }
95
+ end
96
+ end
97
+
98
+ # ──────────────────────────────────────────────────────────────────────────
99
+ # Select / Dropdown
100
+ # ──────────────────────────────────────────────────────────────────────────
101
+
102
+ def select(attribute, choices = nil, options = {}, html_options = {}, &block)
103
+ use_dropdown = options.delete(:dropdown)
104
+ fomantic_field(attribute, options) do |attr, opts|
105
+ merged_html = html_options.merge(class: class_names(html_options[:class], opts.delete(:input_class)))
106
+ raw_select = super(attr, choices, opts, merged_html, &block)
107
+
108
+ use_dropdown ? dropdown_wrap(raw_select) : raw_select
109
+ end
110
+ end
111
+
112
+ # ──────────────────────────────────────────────────────────────────────────
113
+ # Check box
114
+ # ──────────────────────────────────────────────────────────────────────────
115
+
116
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
117
+ label_text = options.delete(:label) { label_for(attribute) }
118
+ kind = options.delete(:kind) { :checkbox } # :checkbox | :slider | :toggle
119
+
120
+ fomantic_field(attribute, options.merge(suppress_label: true)) do |attr, opts|
121
+ opts.delete(:input_class)
122
+ checkbox_html = super(attr, opts, checked_value, unchecked_value)
123
+ checkbox_ui(checkbox_html, label_text, kind)
124
+ end
125
+ end
126
+
127
+ # ──────────────────────────────────────────────────────────────────────────
128
+ # Radio button
129
+ # ──────────────────────────────────────────────────────────────────────────
130
+
131
+ def radio_button(attribute, value, options = {})
132
+ label_text = options.delete(:label) { value.to_s.humanize }
133
+
134
+ fomantic_field(attribute, options.merge(suppress_label: true)) do |attr, opts|
135
+ opts.delete(:input_class)
136
+ radio_html = super(attr, value, opts)
137
+ checkbox_ui(radio_html, label_text, :radio)
138
+ end
139
+ end
140
+
141
+ # ──────────────────────────────────────────────────────────────────────────
142
+ # File field
143
+ # ──────────────────────────────────────────────────────────────────────────
144
+
145
+ def file_field(attribute, options = {})
146
+ fomantic_field(attribute, options) do |attr, opts|
147
+ opts.delete(:input_class)
148
+ super(attr, opts)
149
+ end
150
+ end
151
+
152
+ # ──────────────────────────────────────────────────────────────────────────
153
+ # Hidden field (no wrapper)
154
+ # ──────────────────────────────────────────────────────────────────────────
155
+
156
+ def hidden_field(attribute, options = {})
157
+ super(attribute, options)
158
+ end
159
+
160
+ # ──────────────────────────────────────────────────────────────────────────
161
+ # Submit button
162
+ # ──────────────────────────────────────────────────────────────────────────
163
+
164
+ def submit(value = nil, options = {})
165
+ color = options.delete(:color)
166
+ basic = options.delete(:basic)
167
+ inverted = options.delete(:inverted)
168
+ size = options.delete(:size)
169
+ icon = options.delete(:icon)
170
+
171
+ button_class = class_names(
172
+ "ui", "button",
173
+ color,
174
+ size,
175
+ ("basic" if basic),
176
+ ("inverted" if inverted),
177
+ options.delete(:class)
178
+ )
179
+ options[:class] = button_class
180
+
181
+ icon_html = icon ? @template.tag.i(class: "#{icon} icon") : "".html_safe
182
+ icon_html + super(value, options)
183
+ end
184
+
185
+ # ──────────────────────────────────────────────────────────────────────────
186
+ # Label override — produces a plain <label> (called internally too)
187
+ # ──────────────────────────────────────────────────────────────────────────
188
+
189
+ def label(attribute, text = nil, options = {}, &block)
190
+ super(attribute, text, options, &block)
191
+ end
192
+
193
+ # ──────────────────────────────────────────────────────────────────────────
194
+ # fields_group — wraps children in <div class="fields ...">
195
+ #
196
+ # Options:
197
+ # equal_width: Boolean – adds "equal width"
198
+ # inline: Boolean – adds "inline"
199
+ # count: Integer – "N fields" (evenly divided)
200
+ # ──────────────────────────────────────────────────────────────────────────
201
+
202
+ def fields_group(options = {}, &block)
203
+ equal_width = options.delete(:equal_width)
204
+ inline = options.delete(:inline)
205
+ count = options.delete(:count)
206
+ extra = options.delete(:class)
207
+
208
+ count_word = COLUMN_WORDS[count.to_i - 1] if count
209
+
210
+ css = class_names(
211
+ count_word,
212
+ "fields",
213
+ ("equal width" if equal_width),
214
+ ("inline" if inline),
215
+ extra
216
+ )
217
+
218
+ @template.tag.div(class: css, &block)
219
+ end
220
+
221
+ # ──────────────────────────────────────────────────────────────────────────
222
+ # Form-level message helpers
223
+ # ──────────────────────────────────────────────────────────────────────────
224
+
225
+ def error_message(header, messages = [])
226
+ form_message("error", header, messages)
227
+ end
228
+
229
+ def success_message(header, body = nil)
230
+ form_message("success", header, [], body)
231
+ end
232
+
233
+ def warning_message(header, messages = [])
234
+ form_message("warning", header, messages)
235
+ end
236
+
237
+ def info_message(header, messages = [])
238
+ form_message("info", header, messages)
239
+ end
240
+
241
+ private
242
+
243
+ # ── Core field wrapper ─────────────────────────────────────────────────────
244
+
245
+ def fomantic_field(attribute, options, &block)
246
+ required = options.delete(:required)
247
+ disabled = options.delete(:disabled)
248
+ readonly_opt = options.delete(:readonly)
249
+ inline = options.delete(:inline)
250
+ width = options.delete(:width)
251
+ error_msg = options.delete(:error)
252
+ warning_msg = options.delete(:warning)
253
+ hint = options.delete(:hint)
254
+ field_class = options.delete(:field_class)
255
+ suppress_label = options.delete(:suppress_label)
256
+ custom_label = options.delete(:label)
257
+
258
+ has_error = error_msg.present? || object_has_error?(attribute)
259
+ has_warning = warning_msg.present?
260
+
261
+ div_class = class_names(
262
+ width,
263
+ "field",
264
+ ("required" if required),
265
+ ("disabled" if disabled),
266
+ ("read-only" if readonly_opt),
267
+ ("inline" if inline),
268
+ ("error" if has_error),
269
+ ("warning" if has_warning),
270
+ field_class
271
+ )
272
+
273
+ label_html = unless suppress_label
274
+ text = custom_label.nil? ? label_for(attribute) : custom_label
275
+ text ? label(attribute, text) : nil
276
+ end
277
+
278
+ input_html = block.call(attribute, options)
279
+
280
+ note_html = inline_note(error_msg || (has_error && first_error(attribute)), "red")
281
+ note_html = inline_note(warning_msg, "orange") if note_html.blank? && has_warning
282
+ note_html = inline_note(hint, "grey") if note_html.blank? && hint
283
+
284
+ @template.tag.div(class: div_class) do
285
+ safe_join([ label_html, input_html, note_html ].compact)
286
+ end
287
+ end
288
+
289
+ # ── Checkbox / Radio UI shell ──────────────────────────────────────────────
290
+
291
+ def checkbox_ui(input_html, label_text, kind)
292
+ modifier = { radio: "radio", slider: "slider", toggle: "toggle" }[kind]
293
+ css = class_names("ui", modifier, "checkbox")
294
+
295
+ @template.tag.div(class: css) do
296
+ safe_join([ input_html, @template.tag.label(label_text.is_a?(String) ? label_text : nil) ])
297
+ end
298
+ end
299
+
300
+ # ── Dropdown wrapper ───────────────────────────────────────────────────────
301
+
302
+ def dropdown_wrap(select_html)
303
+ @template.tag.div(class: "ui selection dropdown") do
304
+ safe_join([
305
+ @template.tag.i(class: "dropdown icon"),
306
+ select_html
307
+ ])
308
+ end
309
+ end
310
+
311
+ # ── Form-level messages ────────────────────────────────────────────────────
312
+
313
+ def form_message(type, header, messages = [], body = nil)
314
+ list_html = messages.any? ? @template.tag.ul(class: "list") {
315
+ safe_join(messages.map { |m| @template.tag.li(m) })
316
+ } : nil
317
+
318
+ body_html = body ? @template.tag.p(body) : nil
319
+
320
+ @template.tag.div(class: "ui #{type} message") do
321
+ safe_join([
322
+ @template.tag.div(class: "header") { header },
323
+ list_html,
324
+ body_html
325
+ ].compact)
326
+ end
327
+ end
328
+
329
+ # ── Inline note beneath input ──────────────────────────────────────────────
330
+
331
+ def inline_note(text, color = nil)
332
+ return nil if text.blank?
333
+
334
+ @template.tag.small(class: class_names("ui", color, "text")) { text }
335
+ end
336
+
337
+ # ── Humanise attribute name for label ─────────────────────────────────────
338
+
339
+ def label_for(attribute)
340
+ attr_str = attribute.to_s.delete_suffix("_id")
341
+ if object && object.class.respond_to?(:human_attribute_name)
342
+ object.class.human_attribute_name(attr_str)
343
+ else
344
+ attr_str.humanize
345
+ end
346
+ end
347
+
348
+ # ── ActiveModel error introspection ───────────────────────────────────────
349
+
350
+ def object_has_error?(attribute)
351
+ object.respond_to?(:errors) && object.errors[attribute].any?
352
+ end
353
+
354
+ def first_error(attribute)
355
+ object.errors[attribute].first if object.respond_to?(:errors)
356
+ end
357
+
358
+ # ── Safe join delegated to template ───────────────────────────────────────
359
+
360
+ def safe_join(parts)
361
+ @template.safe_join(parts)
362
+ end
363
+
364
+ # ── class_names helper (Rails 6.1+ ships this) ────────────────────────────
365
+
366
+ def class_names(*args)
367
+ args.flatten.compact.reject { |v| v == false || v.to_s.strip.empty? }.join(" ")
368
+ end
369
+ end
370
+
371
+ # ─────────────────────────────────────────────────────────────────────────────
372
+ # Usage examples (views)
373
+ # ─────────────────────────────────────────────────────────────────────────────
374
+
375
+ # ── 1. Basic user registration form ──────────────────────────────────────────
376
+ #
377
+ # <%= form_with model: @user, builder: FomanticFormBuilder, class: "ui form" do |f| %>
378
+ #
379
+ # <%# grouped equal-width row %>
380
+ # <%= f.fields_group(equal_width: true) do %>
381
+ # <%= f.text_field :first_name, required: true %>
382
+ # <%= f.text_field :last_name, required: true %>
383
+ # <% end %>
384
+ #
385
+ # <%= f.email_field :email, required: true %>
386
+ # <%= f.password_field :password, required: true,
387
+ # hint: "Minimum 8 characters" %>
388
+ #
389
+ # <%# Native select styled by Fomantic %>
390
+ # <%= f.select :role, [["Admin", "admin"], ["Member", "member"]],
391
+ # { prompt: "Select a role" } %>
392
+ #
393
+ # <%# Fomantic dropdown widget (adds JS .dropdown() wrapper) %>
394
+ # <%= f.select :country, country_options,
395
+ # { dropdown: true, required: true } %>
396
+ #
397
+ # <%= f.check_box :terms,
398
+ # label: "I agree to the Terms and Conditions",
399
+ # required: true %>
400
+ #
401
+ # <%# Error/success messages from model %>
402
+ # <%= f.error_message "We had some issues",
403
+ # @user.errors.full_messages if @user.errors.any? %>
404
+ #
405
+ # <%= f.submit "Sign up", color: "green" %>
406
+ # <% end %>
407
+
408
+
409
+ # ── 2. Inline field (label beside input) ─────────────────────────────────────
410
+ #
411
+ # <%= f.text_field :phone, label: "Phone Number", inline: true,
412
+ # width: "eight" %>
413
+
414
+
415
+ # ── 3. Width-constrained fields ───────────────────────────────────────────────
416
+ #
417
+ # <%= f.fields_group do %>
418
+ # <%= f.text_field :first_name, width: "six" %>
419
+ # <%= f.text_field :middle, width: "three" %>
420
+ # <%= f.text_field :last_name, width: "seven" %>
421
+ # <% end %>
422
+
423
+
424
+ # ── 4. Checkbox kinds ─────────────────────────────────────────────────────────
425
+ #
426
+ # <%= f.check_box :notifications, label: "Enable notifications", kind: :toggle %>
427
+ # <%= f.check_box :public, label: "Publicly visible", kind: :slider %>
428
+
429
+
430
+ # ── 5. Radio group ────────────────────────────────────────────────────────────
431
+ #
432
+ # <%= f.fields_group do %>
433
+ # <%= f.radio_button :plan, "basic", label: "Basic" %>
434
+ # <%= f.radio_button :plan, "pro", label: "Pro" %>
435
+ # <%= f.radio_button :plan, "enterprise", label: "Enterprise" %>
436
+ # <% end %>
437
+
438
+
439
+ # ── 6. Textarea ───────────────────────────────────────────────────────────────
440
+ #
441
+ # <%= f.text_area :bio, rows: 4 %>
442
+ # <%= f.text_area :description, rows: 2, transparent: true %>
443
+
444
+
445
+ # ── 7. Form-level state messages ──────────────────────────────────────────────
446
+ #
447
+ # <%= f.error_message "Action Forbidden", ["Email already registered"] %>
448
+ # <%= f.success_message "All done!", "Your profile has been updated." %>
449
+ # <%= f.warning_message "Heads up", ["Please verify your email"] %>
450
+ # <%= f.info_message "Password rules", ["Must be at least 8 characters"] %>
451
+
452
+
453
+ # ── 8. Submit variations ──────────────────────────────────────────────────────
454
+ #
455
+ # <%= f.submit "Save", color: "blue" %>
456
+ # <%= f.submit "Delete", color: "red", basic: true %>
457
+ # <%= f.submit "Go", color: "green", size: "large", icon: "checkmark" %>
458
+
459
+
460
+ # ── 9. Opt-in per form (when default builder is not set) ──────────────────────
461
+ #
462
+ # <%= form_with model: @post, builder: FomanticFormBuilder, class: "ui form" do |f| %>
463
+ # ...
464
+ # <% end %>
465
+
466
+
467
+ # ─────────────────────────────────────────────────────────────────────────────
468
+ # JavaScript initialisation (application.js or a Stimulus controller)
469
+ # ─────────────────────────────────────────────────────────────────────────────
470
+ #
471
+ # // Initialise all Fomantic dropdowns on the page
472
+ # document.addEventListener("DOMContentLoaded", () => {
473
+ # $(".ui.dropdown").dropdown();
474
+ # $(".ui.checkbox").checkbox();
475
+ # $(".ui.calendar").calendar({ type: "date" });
476
+ # });
477
+ #
478
+ # // Or with a Stimulus controller:
479
+ # //
480
+ # // import { Controller } from "@hotwired/stimulus"
481
+ # // export default class extends Controller {
482
+ # // connect() {
483
+ # // $(this.element).find(".ui.dropdown").dropdown()
484
+ # // $(this.element).find(".ui.checkbox").checkbox()
485
+ # // }
486
+ # // }
487
+ end
data/app/lib/component.rb CHANGED
@@ -19,8 +19,10 @@ class Component
19
19
  class_attribute :slot_names, default: []
20
20
 
21
21
  def initialize(**kwargs)
22
- @html_options = kwargs.extract!(*HTML_OPTIONS)
23
- super(**kwargs)
22
+ known = self.class.attribute_names.map(&:to_sym).to_set
23
+ attrs = kwargs.select { |k, _| known.include?(k) }
24
+ @html_options = kwargs.except(*attrs.keys)
25
+ super(**attrs)
24
26
  end
25
27
 
26
28
  def self.default(**overrides)
data/lib/ui/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ui
2
- VERSION = "0.3.5"
2
+ VERSION = "0.3.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-active-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - nathan
@@ -37,6 +37,7 @@ executables: []
37
37
  extensions: []
38
38
  extra_rdoc_files: []
39
39
  files:
40
+ - README.md
40
41
  - Rakefile
41
42
  - app/assets/datatables.css
42
43
  - app/assets/stylesheets.css
@@ -125,6 +126,7 @@ files:
125
126
  - app/controllers/ui/examples_controller.rb
126
127
  - app/helpers/component_helper.rb
127
128
  - app/helpers/fui_helper.rb
129
+ - app/helpers/ui/fomantic_form_builder.rb
128
130
  - app/javascript/accordion.js
129
131
  - app/javascript/accordion.min.js
130
132
  - app/javascript/api.js