rails-active-ui 0.3.6 → 0.3.8

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: 6cba8d2c41659f546855317af82ec86ae112504f089e404038aa23183eae09ba
4
- data.tar.gz: 98d49cc0d8ca25088e4d3f2bd7dd2052f09205e6aeaef45aa4e491b3ce90ea5a
3
+ metadata.gz: ecbf2cf056cd56dbcac4a80553455b3a6b303ef8439bd38b5be513652835985f
4
+ data.tar.gz: 07c80687e4e01edb2050fe9e8f277dedbb7472f410f36e703948f791d4196104
5
5
  SHA512:
6
- metadata.gz: 5f9e8ec8ea0dbf90ecd9f2f730d65eb540174087146ee84329060358dc135dd8540522290a2325b15cdef8ca2d471c784bd136f869602f3089b620de3c121f4d
7
- data.tar.gz: 97074f59b5f1f879b2bc452d7e3eb539f3fd24089e1378587ddd6f36661d779b457b8c83e7836c0f6c551f0e319cf4723bcce3ed9f9f82847d0db0b14f13e83b
6
+ metadata.gz: 03b90d0da8e67d306bb9e59946264a1d931118179b35dfbcb6f80f16bd07a4b39cd7772453dffd7faba31bdc675aa9a620898f4cf545bc4ef8b2d633ffcaf08c
7
+ data.tar.gz: e60026a65dc4c3fcd3bfdfe97c56b36dc8a6fd9586da21122a20374122b273b38eb7b9333273fa749ad665b151e6b755bf0b667dc487bd7ef567d3445f473c20
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
+ ```
@@ -25,6 +25,7 @@ class MenuItemComponent < Component
25
25
  attribute :value, :string, default: nil
26
26
  attribute :target, :string, default: nil
27
27
  attribute :rel, :string, default: nil
28
+ attribute :tab, :string, default: nil
28
29
 
29
30
  def to_s
30
31
  classes = class_names(
@@ -45,6 +46,7 @@ class MenuItemComponent < Component
45
46
 
46
47
  opts = merge_html_options(class: classes)
47
48
  opts["data-value"] = value if value
49
+ opts["data-tab"] = tab if tab
48
50
  opts[:target] = target if target
49
51
  opts[:rel] = rel if rel
50
52
 
@@ -1,24 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Tab — tab navigation with content panes.
3
+ # Tab — a single content pane within a TabGroup.
4
4
  #
5
- # Usage:
6
- # Tab(active: true) { text "Tab pane content" }
5
+ # Usage (inside TabGroup):
6
+ # Tab(active: true, path: "first", attached: true, segment: true) {
7
+ # text "First tab content"
8
+ # }
9
+ #
10
+ # Usage (standalone, no TabGroup):
11
+ # Tab(active: true, path: "first") { text "Pane content" }
7
12
 
8
13
  class TabComponent < Component
9
- attribute :active, :boolean, default: false
10
- attribute :path, :string, default: nil
14
+ include Attachable
15
+
16
+ attribute :active, :boolean, default: false
17
+ attribute :path, :string, default: nil
18
+ attribute :segment, :boolean, default: false
11
19
 
12
20
  def to_s
13
21
  classes = class_names(
14
22
  "ui",
15
- { "active" => active },
23
+ { "active" => active,
24
+ "bottom attached" => attached,
25
+ "segment" => segment },
16
26
  "tab"
17
27
  )
18
28
 
19
- data = { controller: "fui-tab" }
29
+ data = {}
20
30
  data[:tab] = path if path
21
31
 
22
- tag.div(class: classes, data: data) { @content }
32
+ tag.div(**merge_html_options(class: classes, data: data)) { @content }
23
33
  end
24
34
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TabGroup — wrapper for tab navigation with content panes.
4
+ #
5
+ # Renders a <div data-controller="fui-tab"> that wraps a Menu and Tab panes.
6
+ # The fui-tab Stimulus controller finds .item[data-tab] elements within
7
+ # and initializes Fomantic-UI's $.fn.tab plugin for click-based switching.
8
+ #
9
+ # Usage:
10
+ # TabGroup {
11
+ # Menu(tabular: true, attached: "top") {
12
+ # MenuItem(active: true, tab: "first") { text "First" }
13
+ # MenuItem(tab: "second") { text "Second" }
14
+ # }
15
+ # Tab(active: true, path: "first", attached: true, segment: true) {
16
+ # text "First tab content"
17
+ # }
18
+ # Tab(path: "second", attached: true, segment: true) {
19
+ # text "Second tab content"
20
+ # }
21
+ # }
22
+
23
+ class TabGroupComponent < Component
24
+ attribute :history, :boolean, default: false
25
+ attribute :history_type, :string, default: "hash"
26
+
27
+ def to_s
28
+ data = { controller: "fui-tab" }
29
+ data[:fui_tab_history_value] = history if history
30
+ data[:fui_tab_history_type_value] = history_type if history
31
+
32
+ tag.div(**merge_html_options(data: data)) { @content }
33
+ end
34
+ end
@@ -94,6 +94,7 @@ module ComponentHelper
94
94
  Sidebar: "SidebarComponent",
95
95
  Sticky: "StickyComponent",
96
96
  Tab: "TabComponent",
97
+ TabGroup: "TabGroupComponent",
97
98
  Toast: "ToastComponent",
98
99
  Transition: "TransitionComponent",
99
100
 
@@ -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
@@ -587,37 +587,33 @@ Header(size: :h3) { text "Tab" }
587
587
  text "Tab content panels used with a menu for switching views."
588
588
 
589
589
  Segment {
590
- Menu(pointing: true, secondary: true) {
591
- MenuItem(active: true) {
592
- concat tag.a("data-tab": "tab-1") { "Tab 1" }
590
+ TabGroup {
591
+ Menu(tabular: true, attached: "top") {
592
+ MenuItem(active: true, tab: "tab-1") { text "Tab 1" }
593
+ MenuItem(tab: "tab-2") { text "Tab 2" }
593
594
  }
594
- MenuItem {
595
- concat tag.a("data-tab": "tab-2") { "Tab 2" }
595
+ Tab(active: true, path: "tab-1", attached: true, segment: true) {
596
+ text "Content for Tab 1"
597
+ }
598
+ Tab(path: "tab-2", attached: true, segment: true) {
599
+ text "Content for Tab 2"
596
600
  }
597
- }
598
- Tab(active: true, path: "tab-1") {
599
- text "Content for Tab 1"
600
- }
601
- Tab(path: "tab-2") {
602
- text "Content for Tab 2"
603
601
  }
604
602
  }
605
603
 
606
604
  Segment(secondary: true) {
607
605
  concat tag.pre { tag.code(
608
- 'Menu(pointing: true, secondary: true) {
609
- MenuItem(active: true) {
610
- concat tag.a("data-tab": "tab-1") { "Tab 1" }
606
+ 'TabGroup {
607
+ Menu(tabular: true, attached: "top") {
608
+ MenuItem(active: true, tab: "tab-1") { text "Tab 1" }
609
+ MenuItem(tab: "tab-2") { text "Tab 2" }
611
610
  }
612
- MenuItem {
613
- concat tag.a("data-tab": "tab-2") { "Tab 2" }
611
+ Tab(active: true, path: "tab-1", attached: true, segment: true) {
612
+ text "Content for Tab 1"
613
+ }
614
+ Tab(path: "tab-2", attached: true, segment: true) {
615
+ text "Content for Tab 2"
614
616
  }
615
- }
616
- Tab(active: true, path: "tab-1") {
617
- text "Content for Tab 1"
618
- }
619
- Tab(path: "tab-2") {
620
- text "Content for Tab 2"
621
617
  }'
622
618
  )}
623
619
  }
data/lib/ui/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ui
2
- VERSION = "0.3.6"
2
+ VERSION = "0.3.8"
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.6
4
+ version: 0.3.8
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
@@ -112,6 +113,7 @@ files:
112
113
  - app/components/sub_header_component.rb
113
114
  - app/components/sub_menu_component.rb
114
115
  - app/components/tab_component.rb
116
+ - app/components/tab_group_component.rb
115
117
  - app/components/table_cell_component.rb
116
118
  - app/components/table_component.rb
117
119
  - app/components/table_row_component.rb
@@ -125,6 +127,7 @@ files:
125
127
  - app/controllers/ui/examples_controller.rb
126
128
  - app/helpers/component_helper.rb
127
129
  - app/helpers/fui_helper.rb
130
+ - app/helpers/ui/fomantic_form_builder.rb
128
131
  - app/javascript/accordion.js
129
132
  - app/javascript/accordion.min.js
130
133
  - app/javascript/api.js