rails-active-ui 0.3.8 → 0.4.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +195 -6
  3. data/Rakefile +1 -1
  4. data/app/components/accordion_component.rb +41 -24
  5. data/app/components/accordion_item_component.rb +40 -0
  6. data/app/components/ad_component.rb +1 -1
  7. data/app/components/api_component.rb +1 -1
  8. data/app/components/breadcrumb_component.rb +1 -1
  9. data/app/components/button_group_component.rb +36 -0
  10. data/app/components/button_to_component.rb +1 -1
  11. data/app/components/calendar_component.rb +24 -20
  12. data/app/components/card_component.rb +5 -3
  13. data/app/components/comment_component.rb +5 -3
  14. data/app/components/comment_group_component.rb +27 -0
  15. data/app/components/comment_reply_component.rb +47 -0
  16. data/app/components/comment_reply_group_component.rb +25 -0
  17. data/app/components/dimmer_component.rb +1 -1
  18. data/app/components/divider_component.rb +1 -1
  19. data/app/components/dropdown_component.rb +6 -2
  20. data/app/components/embed_component.rb +1 -1
  21. data/app/components/emoji_component.rb +1 -1
  22. data/app/components/feed_component.rb +16 -4
  23. data/app/components/feed_item_component.rb +85 -0
  24. data/app/components/field_component.rb +42 -0
  25. data/app/components/flag_component.rb +1 -1
  26. data/app/components/flyout_component.rb +3 -2
  27. data/app/components/h_stack_component.rb +1 -1
  28. data/app/components/header_component.rb +2 -2
  29. data/app/components/image_component.rb +1 -1
  30. data/app/components/input_component.rb +22 -41
  31. data/app/components/item_component.rb +3 -2
  32. data/app/components/item_group_component.rb +1 -1
  33. data/app/components/list_component.rb +2 -2
  34. data/app/components/list_content_component.rb +27 -0
  35. data/app/components/list_description_component.rb +17 -0
  36. data/app/components/list_header_component.rb +28 -0
  37. data/app/components/list_item_component.rb +37 -0
  38. data/app/components/loader_component.rb +1 -1
  39. data/app/components/menu_component.rb +1 -1
  40. data/app/components/modal_component.rb +2 -1
  41. data/app/components/nag_component.rb +1 -1
  42. data/app/components/overlay_component.rb +1 -1
  43. data/app/components/placeholder_component.rb +37 -13
  44. data/app/components/popup_component.rb +1 -1
  45. data/app/components/progress_component.rb +1 -1
  46. data/app/components/pusher_component.rb +1 -1
  47. data/app/components/rail_component.rb +1 -1
  48. data/app/components/rating_component.rb +1 -1
  49. data/app/components/reveal_component.rb +3 -2
  50. data/app/components/row_component.rb +4 -0
  51. data/app/components/search_component.rb +1 -1
  52. data/app/components/segment_group_component.rb +1 -1
  53. data/app/components/shape_component.rb +1 -1
  54. data/app/components/sidebar_component.rb +1 -1
  55. data/app/components/site_component.rb +1 -1
  56. data/app/components/slider_component.rb +1 -1
  57. data/app/components/state_component.rb +1 -1
  58. data/app/components/statistic_component.rb +3 -2
  59. data/app/components/step_component.rb +2 -2
  60. data/app/components/step_group_component.rb +1 -1
  61. data/app/components/sticky_component.rb +1 -1
  62. data/app/components/style_component.rb +1 -1
  63. data/app/components/sub_accordion_component.rb +25 -0
  64. data/app/components/sub_header_component.rb +1 -1
  65. data/app/components/sub_menu_component.rb +1 -1
  66. data/app/components/table_cell_component.rb +2 -2
  67. data/app/components/table_component.rb +2 -2
  68. data/app/components/tag_component.rb +57 -0
  69. data/app/components/tag_group_component.rb +33 -0
  70. data/app/components/text_component.rb +1 -1
  71. data/app/components/toast_component.rb +1 -1
  72. data/app/components/transition_component.rb +2 -2
  73. data/app/components/v_stack_component.rb +1 -1
  74. data/app/components/visibility_component.rb +1 -1
  75. data/app/helpers/component_helper.rb +23 -6
  76. data/app/helpers/ui/fomantic_form_builder.rb +155 -201
  77. data/app/javascript/ui/controllers/fui_calendar_controller.js +18 -8
  78. data/lib/ui/version.rb +1 -1
  79. metadata +15 -3
  80. data/app/components/checkbox_component.rb +0 -41
  81. data/app/components/label_component.rb +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecbf2cf056cd56dbcac4a80553455b3a6b303ef8439bd38b5be513652835985f
4
- data.tar.gz: 07c80687e4e01edb2050fe9e8f277dedbb7472f410f36e703948f791d4196104
3
+ metadata.gz: 847077ab92e13f582035970b65c03270e249b0c07d231aaa24ec9b8bd67b3747
4
+ data.tar.gz: 19289c0f34216d56bb1445ddba8a2458e53907f4609c7cd0daf9f725384e12f6
5
5
  SHA512:
6
- metadata.gz: 03b90d0da8e67d306bb9e59946264a1d931118179b35dfbcb6f80f16bd07a4b39cd7772453dffd7faba31bdc675aa9a620898f4cf545bc4ef8b2d633ffcaf08c
7
- data.tar.gz: e60026a65dc4c3fcd3bfdfe97c56b36dc8a6fd9586da21122a20374122b273b38eb7b9333273fa749ad665b151e6b755bf0b667dc487bd7ef567d3445f473c20
6
+ metadata.gz: 11ecf07a55ce294c3551c1a736d588aab2f02faf7e8fa53a20e44e11e1b4200d5996c867eb5bd77c6e6d99a1078269b73f2c735f5e34a59e14799b39bceca93f
7
+ data.tar.gz: c63697323a25b300048da0744a79fcc81b2d952bebf36983bf31ecb6810c90ff754b1cb887f419ad5baf5e5009e39e744588cabc53cd9a8eb45ffccecab1bf11
data/README.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # rails-active-ui
2
2
 
3
- A Fomantic-UI component system for Rails. Views use `.html.ruby` files with PascalCase component calls.
3
+ A Fomantic-UI component system for Rails. Views use `.html.ruby` files with PascalCase component calls built on `ActiveModel::Attributes`.
4
4
 
5
5
  ## Setup
6
6
 
7
7
  Add to your Gemfile:
8
8
 
9
9
  ```ruby
10
- gem "rails-active-ui"
10
+ gem "rails-active-ui", path: "engines/rails-active-ui"
11
11
  ```
12
12
 
13
+ Requires Ruby >= 3.2 and Rails ~> 8.1.
14
+
13
15
  ### Engine initializers
14
16
 
15
17
  The gem's engine (`Ui::Engine`) registers the following automatically:
@@ -48,7 +50,7 @@ const application = Application.start()
48
50
  registerFuiControllers(application)
49
51
  ```
50
52
 
51
- These are thin jQuery bridge controllers that initialize Fomantic-UI widgets in `connect()` and tear them down in `disconnect()`, making them Turbo-compatible.
53
+ These are thin jQuery bridge controllers that initialize Fomantic-UI widgets in `connect()` and tear them down in `disconnect()`, making them Turbo-compatible. There are 28 Fomantic bridge controllers covering accordion, api, calendar, checkbox, datatable, dimmer, dropdown, embed, emoji-picker, flyout, form, item-list, modal, nag, popup, progress, rating, search, shape, sidebar, site, slider, state, sticky, tab, toast, transition, and visibility.
52
54
 
53
55
  ### Rails engine usage
54
56
 
@@ -64,15 +66,157 @@ class Engine < ::Rails::Engine
64
66
  end
65
67
  ```
66
68
 
69
+ ## View DSL
70
+
71
+ Views use `.html.ruby` files with a pure-Ruby DSL. Every PascalCase call renders a component:
72
+
73
+ ```ruby
74
+ Header(size: :h2) { "Hello World" }
75
+ Segment(inverted: true) {
76
+ Paragraph { "Some content here" }
77
+ }
78
+ ```
79
+
80
+ ### Text content
81
+
82
+ There are two ways to output text inside a block:
83
+
84
+ - **Block return value** -- when the block contains only a string, just return it directly:
85
+ ```ruby
86
+ Header { "Page Title" }
87
+ Button(color: "green") { "Save" }
88
+ ```
89
+
90
+ - **`Text` component** -- when you need to mix text with other components in the same block, use `Text { "..." }` for each piece of text and `NbSpace()` for whitespace between components:
91
+ ```ruby
92
+ Button(color: "blue") {
93
+ Icon(name: "edit")
94
+ NbSpace()
95
+ Text { "Add Reply" }
96
+ }
97
+ ```
98
+
99
+ Prefer `{ "string" }` over `{ Text { "string" } }` when the block contains only text.
100
+
101
+ Never use lowercase `text` to emit inline content alongside components -- use `Text { "..." }` instead. Never embed leading/trailing spaces into text strings -- use `NbSpace()`. Every HTML element must be rendered through a component call.
102
+
103
+ ### Inline elements
104
+
105
+ Common HTML elements that appear inside components:
106
+
107
+ | HTML | Ruby DSL |
108
+ |------|----------|
109
+ | `<a>Text</a>` | `LinkTo { "Text" }` |
110
+ | `<a class="user">Name</a>` | `LinkTo(class: "user") { "Name" }` |
111
+ | `<a href="/path">Link</a>` | `LinkTo(href: "/path") { "Link" }` |
112
+ | `<span class="ui red text">` | `Text(color: "red") { "..." }` |
113
+ | `<div>` (generic) | `Wrapper(class: "...") { ... }` |
114
+ | `&nbsp;` | `NbSpace()` |
115
+
116
+ ### Utility helpers
117
+
118
+ | Helper | Output |
119
+ |--------|--------|
120
+ | `text "string"` | Appends plain text to the output buffer |
121
+ | `NbSpace()` | Appends `&nbsp;` |
122
+ | `Partial("path/to/partial")` | Delegates to `render` |
123
+ | `DocType()` | Outputs `<!DOCTYPE html>` |
124
+ | `StylesheetLink("file.css")` | Wraps `stylesheet_link_tag` |
125
+ | `JavascriptImportmap()` | Wraps `javascript_importmap_tags` |
126
+ | `CsrfMetaTags()` | Wraps `csrf_meta_tags` |
127
+ | `CspMetaTag()` | Wraps `csp_meta_tag` |
128
+ | `ContentFor(:name) { ... }` | Wraps `content_for` |
129
+ | `Style("css string")` | Renders an inline `<style>` tag |
130
+
131
+ ## Components
132
+
133
+ All components are registered in `ComponentHelper::COMPONENT_MAP` (`app/helpers/component_helper.rb`). Every PascalCase call in a `.html.ruby` view resolves through this map.
134
+
135
+ ### Base class
136
+
137
+ All components inherit from `Component` (`app/lib/component.rb`), which provides:
138
+
139
+ - **`ActiveModel::Attributes`** for declarative attribute definitions with types and defaults
140
+ - **Named slots** via the `slot` class macro -- yields `self` so callers fill regions:
141
+ ```ruby
142
+ Card { |c|
143
+ c.header { "Title" }
144
+ c.description { "Body text" }
145
+ }
146
+ ```
147
+ - **HTML pass-through** -- `id`, `class`, `data`, `style`, `role`, `tabindex`, `title`, `aria`, `target`, `rel` bypass attributes and merge into the rendered tag
148
+ - **`merge_html_options`** -- merges component-computed options with user-provided HTML options (classes concatenate, data hashes deep-merge)
149
+ - **Dev annotations** -- in development, output is wrapped in `<!-- BEGIN ClassName -->` / `<!-- END ClassName -->` comments
150
+
151
+ ### Component inventory
152
+
153
+ **Layout Primitives:** VStack, HStack, Column, Row, Pusher, Overlay, LinkTo, SubHeader
154
+
155
+ **Globals:** Reset, Site, Wrapper, Template, BackButton
156
+
157
+ **Elements:** Button, ButtonGroup, ButtonTo, Paragraph, Container, Divider, Emoji, Flag, Header, Icon, Image, Input, Tag, TagGroup, List, ListItem, ListContent, ListHeader, ListDescription, Loader, Placeholder, Rail, Reveal, Segment, SegmentGroup, Step, StepGroup, Text
158
+
159
+ **Collections:** Breadcrumb, Form, Grid, Menu, MenuItem, SubMenu, Message, Table, TableRow, TableCell
160
+
161
+ **Views:** Ad, ItemGroup, Card, Comment, CommentGroup, CommentReplyGroup, CommentReply, Feed, FeedItem, Item, Statistic
162
+
163
+ **Modules:** Accordion, AccordionItem, SubAccordion, Calendar, Dimmer, Dropdown, Embed, Flyout, Modal, Nag, Popup, Progress, Slider, Rating, Search, Shape, Sidebar, Sticky, Tab, TabGroup, Toast, Transition
164
+
165
+ **Behaviors:** Api, State, Visibility
166
+
167
+ **Blocks:** ResourceListBlock
168
+
169
+ ### Component patterns
170
+
171
+ **Simple component** -- attributes map to CSS class modifiers:
172
+ ```ruby
173
+ Button(color: "red", size: "large") { "Delete" }
174
+ # => <button class="ui red large button">Delete</button>
175
+ ```
176
+
177
+ **Component with slots** -- child regions are filled via yielded self:
178
+ ```ruby
179
+ FeedItem { |e|
180
+ e.label { Image(src: "/images/avatar/small/elliot.jpg") }
181
+ e.summary {
182
+ LinkTo(class: "user") { "Elliot Fu" }
183
+ NbSpace()
184
+ Text { "added you as a friend" }
185
+ }
186
+ e.date_inline { "1 Hour Ago" }
187
+ e.meta {
188
+ LinkTo(class: "like") {
189
+ Icon(name: "like")
190
+ NbSpace()
191
+ Text { "4 Likes" }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ **CSS class passthrough** -- for one-off Fomantic classes not covered by component attributes:
198
+ ```ruby
199
+ Menu(class: "equal width") { ... }
200
+ ```
201
+
202
+ ### method_missing behavior
203
+
204
+ PascalCase calls not in `COMPONENT_MAP` are handled by `method_missing`:
205
+
206
+ 1. If inside a `Form()` block, the call is tried as an underscored form builder method (e.g. `TextField` -> `f.text_field`)
207
+ 2. Otherwise, it falls through to `tag.public_send(tag_name)`, generating an arbitrary HTML element
208
+
209
+ This means typos in component names will not raise errors -- they silently generate unexpected HTML tags.
210
+
67
211
  ## Form Builder
68
212
 
69
- rails-active-ui ships with `FomanticFormBuilder`, a drop-in `ActionView::Helpers::FormBuilder` subclass that wraps every field helper in Fomantic-UI markup.
213
+ rails-active-ui ships with `Ui::FomanticFormBuilder`, a drop-in `ActionView::Helpers::FormBuilder` subclass that wraps every field helper in Fomantic-UI markup.
70
214
 
71
215
  Set it as the default in your `ApplicationController`:
72
216
 
73
217
  ```ruby
74
218
  class ApplicationController < ActionController::Base
75
- ActionView::Base.default_form_builder = FomanticFormBuilder
219
+ default_form_builder Ui::FomanticFormBuilder
76
220
  end
77
221
  ```
78
222
 
@@ -97,7 +241,18 @@ Form(url: users_path, method: :post) {
97
241
  | `EmailField(:email)` | `f.email_field :email` | Email input |
98
242
  | `PasswordField(:password)` | `f.password_field :password` | Password input |
99
243
  | `NumberField(:age)` | `f.number_field :age` | Number input |
244
+ | `UrlField(:website)` | `f.url_field :website` | URL input |
245
+ | `TelephoneField(:phone)` | `f.telephone_field :phone` | Telephone input |
246
+ | `SearchField(:q)` | `f.search_field :q` | Search input |
247
+ | `ColorField(:color)` | `f.color_field :color` | Color picker |
248
+ | `RangeField(:volume)` | `f.range_field :volume` | Range slider |
249
+ | `DateField(:birthday)` | `f.date_field :birthday` | Date picker (Fomantic calendar) |
250
+ | `DatetimeLocalField(:starts_at)` | `f.datetime_local_field :starts_at` | Datetime picker |
251
+ | `TimeField(:alarm)` | `f.time_field :alarm` | Time picker |
252
+ | `MonthField(:month)` | `f.month_field :month` | Month picker |
253
+ | `WeekField(:week)` | `f.week_field :week` | Week picker |
100
254
  | `TextArea(:bio)` | `f.text_area :bio` | Textarea |
255
+ | `EmojiField(:icon)` | `f.emoji_field :icon` | Emoji picker (Stimulus-powered) |
101
256
  | `Select(:role, choices)` | `f.select :role, choices` | Select dropdown |
102
257
  | `CheckBox(:terms)` | `f.check_box :terms` | Checkbox with Fomantic styling |
103
258
  | `RadioButton(:plan, "pro")` | `f.radio_button :plan, "pro"` | Radio button |
@@ -112,9 +267,11 @@ All field helpers accept these options:
112
267
  - `label:` -- override label text (`nil` to suppress)
113
268
  - `required:` -- adds "required" class and asterisk
114
269
  - `disabled:` -- adds "disabled" class
270
+ - `readonly:` -- adds "read-only" class
115
271
  - `inline:` -- label sits beside the input
116
272
  - `width:` -- Fomantic grid column word (e.g. `"six"`, `"three"`)
117
- - `error:` -- error message string
273
+ - `error:` -- error message string, adds "error" class
274
+ - `warning:` -- warning message string, adds "warning" class
118
275
  - `hint:` -- grey note beneath the input
119
276
  - `field_class:` -- extra classes on the wrapping `.field` div
120
277
  - `input_class:` -- extra classes on the input element
@@ -125,6 +282,15 @@ All field helpers accept these options:
125
282
  - `size:` -- Fomantic size (e.g. `"tiny"`, `"large"`)
126
283
  - `basic:` -- basic button style
127
284
  - `icon:` -- icon name (e.g. `"checkmark"`)
285
+ - `inverted:` -- inverted style
286
+
287
+ ### Checkbox / Radio options
288
+
289
+ - `kind:` -- `:checkbox` (default), `:slider`, or `:toggle`
290
+ - `size:` -- Fomantic size
291
+ - `inverted:` -- inverted style
292
+ - `fitted:` -- removes label padding
293
+ - `right_aligned:` -- label appears on the left
128
294
 
129
295
  ### Field groups
130
296
 
@@ -144,5 +310,28 @@ Form(url: users_path) {
144
310
  ErrorMessage("Something went wrong", ["Email is taken"])
145
311
  SuccessMessage("All done!", "Profile updated.")
146
312
  WarningMessage("Heads up", ["Verify your email"])
313
+ InfoMessage("Note", ["This is informational"])
147
314
  }
148
315
  ```
316
+
317
+ ## FuiHelper
318
+
319
+ `FuiHelper` is auto-included into `ActionView::Base` and provides:
320
+
321
+ - **`fui_javascript_tags`** -- emits `<script>` tags for jQuery and all 23 Fomantic-UI component scripts in the correct load order (site and transition first)
322
+ - **`datatable(columns:, options:, &block)`** -- renders a DataTables-powered table wrapped in a `fui-datatable` Stimulus controller div
323
+
324
+ ## Example Browser
325
+
326
+ The repository includes a full Rails application that serves as a live example browser for all components. Example views live under `app/views/examples/` organized by Fomantic-UI category:
327
+
328
+ ```
329
+ app/views/examples/
330
+ collections/ # breadcrumb, form, grid, menu, message, table
331
+ elements/ # button, container, divider, emoji, flag, header, icon, image, input, list, etc.
332
+ examples/ # attached, bootstrap, dashboard, fixed, grid, homepage, login, etc.
333
+ modules/ # accordion, calendar, checkbox, dimmer, dropdown, tab, etc.
334
+ views/ # advertisement, card, comment, feed, item, statistic
335
+ ```
336
+
337
+ Run `bin/rails server` and visit `http://localhost:3000` to browse all examples. See `CONVERTING_EXAMPLES.md` for the guide on converting Fomantic-UI HTML docs into `.html.ruby` example views.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler/setup"
2
2
 
3
- APP_RAKEFILE = File.expand_path("example/Rakefile", __dir__)
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  require "bundler/gem_tasks"
@@ -1,43 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Accordion — collapsible content panel using <details>/<summary>.
3
+ # Accordion — collapsible content panels.
4
+ #
5
+ # Renders a Fomantic-UI accordion container wired to the fui-accordion
6
+ # Stimulus controller. Block content should contain raw `.title` and
7
+ # `.content` div pairs (built with `tag.div`).
4
8
  #
5
9
  # Usage:
6
- # Accordion(attached: true) { |a|
7
- # a.title { text "Section" }
8
- # text "Panel content"
10
+ # Accordion(styled: true) {
11
+ # tag.div(class: "active title") {
12
+ # safe_join([tag.i(class: "dropdown icon"), "What is a dog?"])
13
+ # }
14
+ # tag.div(class: "active content") {
15
+ # tag.p { "A dog is a type of domesticated animal." }
16
+ # }
9
17
  # }
10
18
 
11
19
  class AccordionComponent < Component
12
20
  include Attachable
13
21
 
14
- attribute :raised, :boolean, default: false
15
- attribute :inverted, :boolean, default: false
16
- attribute :basic, :boolean, default: false
17
- attribute :compact, :boolean, default: false
18
- attribute :color, :string, default: nil
19
- attribute :secondary, :boolean, default: false
20
- attribute :open, :boolean, default: false
21
-
22
- slot :title
22
+ attribute :styled, :boolean, default: false
23
+ attribute :fluid, :boolean, default: false
24
+ attribute :inverted, :boolean, default: false
25
+ attribute :basic, :boolean, default: false
26
+ attribute :compact, :boolean, default: false
27
+ attribute :very_compact, :boolean, default: false
28
+ attribute :tree, :boolean, default: false
29
+ attribute :color, :string, default: nil
30
+ attribute :exclusive, :boolean, default: true
31
+ attribute :collapsible, :boolean, default: true
32
+ attribute :duration, :integer, default: 350
23
33
 
24
34
  def to_s
25
35
  classes = class_names(
26
36
  "ui",
27
37
  color,
28
- { "attached" => attached, "raised" => raised, "inverted" => inverted,
29
- "basic" => basic, "compact" => compact, "secondary" => secondary },
30
- "segment"
38
+ {
39
+ "attached" => attached,
40
+ "styled" => styled,
41
+ "fluid" => fluid,
42
+ "inverted" => inverted,
43
+ "basic" => basic && styled,
44
+ "compact" => compact,
45
+ "very compact" => very_compact,
46
+ "tree" => tree
47
+ },
48
+ "accordion"
31
49
  )
32
50
 
33
- opts = merge_html_options(class: classes)
34
- opts[:open] = "" if open
51
+ data = { controller: "fui-accordion" }
52
+ data[:fui_accordion_exclusive_value] = "false" unless exclusive
53
+ data[:fui_accordion_collapsible_value] = "false" unless collapsible
54
+ data[:fui_accordion_duration_value] = duration.to_s if duration != 350
55
+
56
+ opts = merge_html_options(class: classes, data: data)
35
57
 
36
- tag.details(**opts) {
37
- safe_join([
38
- tag.summary { @slots[:title] || "" },
39
- tag.div { @content }
40
- ])
41
- }
58
+ tag.div(**opts) { @content }
42
59
  end
43
60
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AccordionItem — a single panel inside an Accordion.
4
+ #
5
+ # Renders two sibling divs (.title + .content) with no wrapper,
6
+ # exactly the structure Fomantic-UI expects as direct children of
7
+ # .ui.accordion or a nested .accordion.
8
+ #
9
+ # Usage:
10
+ # Accordion(styled: true) {
11
+ # AccordionItem(active: true) { |i|
12
+ # i.title { "What is a dog?" }
13
+ # i.content {
14
+ # Paragraph { "A dog is a type of domesticated animal." }
15
+ # }
16
+ # }
17
+ # }
18
+
19
+ class AccordionItemComponent < Component
20
+ attribute :active, :boolean, default: false
21
+ attribute :icon, :string, default: "dropdown"
22
+
23
+ slot :title
24
+ slot :content
25
+
26
+ def to_s
27
+ title_classes = class_names({ "active" => active }, "title")
28
+ content_classes = class_names({ "active" => active }, "content")
29
+
30
+ title_el = @slots[:title] ? tag.div(class: title_classes) {
31
+ safe_join([ tag.i(class: "#{icon} icon"), @slots[:title] ])
32
+ } : nil
33
+
34
+ content_el = @slots[:content] ? tag.div(class: content_classes) {
35
+ @slots[:content]
36
+ } : nil
37
+
38
+ safe_join([ title_el, content_el ])
39
+ end
40
+ end
@@ -23,6 +23,6 @@ class AdComponent < Component
23
23
  opts = { class: classes }
24
24
  opts[:data] = { text: test } if test
25
25
 
26
- tag.div(**opts) { @content }
26
+ tag.div(**merge_html_options(**opts)) { @content }
27
27
  end
28
28
  end
@@ -19,6 +19,6 @@ class ApiComponent < Component
19
19
  data[:fui_api_action_value] = action_val if action_val
20
20
  data[:fui_api_state_context_value] = state_context if state_context
21
21
 
22
- tag.div(data: data) { @content }
22
+ tag.div(**merge_html_options(data: data)) { @content }
23
23
  end
24
24
  end
@@ -21,6 +21,6 @@ class BreadcrumbComponent < Component
21
21
  "breadcrumb"
22
22
  )
23
23
 
24
- tag.div(class: classes) { @content }
24
+ tag.div(**merge_html_options(class: classes)) { @content }
25
25
  end
26
26
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ButtonGroup — a group of buttons displayed together.
4
+ #
5
+ # Usage:
6
+ # ButtonGroup(color: "teal") {
7
+ # Button { "Save" }
8
+ # Dropdown(floating: true, button: true) { ... }
9
+ # }
10
+ # ButtonGroup(icon: true) {
11
+ # Button { Icon(name: "bold") }
12
+ # Button { Icon(name: "italic") }
13
+ # }
14
+
15
+ class ButtonGroupComponent < Component
16
+ include Sizeable
17
+
18
+ attribute :color, :string, default: nil
19
+ attribute :icon, :boolean, default: false
20
+ attribute :vertical, :boolean, default: false
21
+ attribute :basic, :boolean, default: false
22
+ attribute :labeled, :boolean, default: false
23
+
24
+ def to_s
25
+ classes = class_names(
26
+ "ui",
27
+ color,
28
+ size,
29
+ { "icon" => icon, "vertical" => vertical, "basic" => basic,
30
+ "labeled" => labeled },
31
+ "buttons"
32
+ )
33
+
34
+ tag.div(**merge_html_options(class: classes)) { @content }
35
+ end
36
+ end
@@ -36,7 +36,7 @@ class ButtonToComponent < Component
36
36
 
37
37
  @view_context.button_to(
38
38
  url,
39
- html_opts.merge(class: classes)
39
+ merge_html_options(**html_opts, class: classes)
40
40
  ) { @content }
41
41
  end
42
42
  end
@@ -1,34 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Calendar — date/time picker.
3
+ # Calendar — Fomantic-UI date/time picker container.
4
4
  #
5
- # Usage:
6
- # Calendar(type: :date, name: "start_date")
7
- # Calendar(type: :datetime, format: "YYYY-MM-DD HH:mm")
5
+ # Standalone usage (wraps any content — Input, Button, or nothing for inline):
6
+ # Calendar(type: "date") { Input(icon: "calendar", icon_position: "left", placeholder: "Date") }
7
+ # Calendar(type: "datetime") { Button { "Select date" } }
8
+ # Calendar(type: "datetime") # inline calendar (no input)
9
+ #
10
+ # Inside a Form, date/time fields render the calendar automatically:
11
+ # Form(url: "#") { DateField(:start_date) }
8
12
 
9
13
  class CalendarComponent < Component
10
- attribute :type, :string, default: "date"
11
- attribute :name, :string, default: nil
12
- attribute :format, :string, default: nil
13
- attribute :min_date, :string, default: nil
14
- attribute :max_date, :string, default: nil
14
+ attribute :type, :string, default: "date"
15
+ attribute :format, :string, default: nil
16
+ attribute :min_date, :string, default: nil
17
+ attribute :max_date, :string, default: nil
18
+ attribute :inverted, :boolean, default: false
19
+ attribute :disabled, :boolean, default: false
20
+ attribute :size, :string, default: nil
15
21
 
16
22
  def to_s
17
23
  data = { controller: "fui-calendar", fui_calendar_type_value: type }
18
- data[:fui_calendar_format_value] = format if format
24
+ data[:fui_calendar_format_value] = format if format
19
25
  data[:fui_calendar_min_date_value] = min_date if min_date
20
26
  data[:fui_calendar_max_date_value] = max_date if max_date
21
27
 
22
- input_opts = { type: "text", placeholder: type }
23
- input_opts[:name] = name if name
28
+ classes = class_names(
29
+ "ui",
30
+ size,
31
+ ("inverted" if inverted),
32
+ ("disabled" if disabled),
33
+ "calendar"
34
+ )
24
35
 
25
- tag.div(class: "ui calendar", data: data) {
26
- tag.div(class: "ui input left icon") {
27
- safe_join([
28
- tag.i(class: "calendar icon"),
29
- tag.input(**input_opts)
30
- ])
31
- }
32
- }
36
+ tag.div(**merge_html_options(class: classes, data: data)) { @content }
33
37
  end
34
38
  end
@@ -45,12 +45,14 @@ class CardComponent < Component
45
45
  content_el = content_parts.any? ? tag.div(class: "content") { safe_join(content_parts) } : nil
46
46
  extra_el = @slots[:extra] ? tag.div(class: "extra content") { @slots[:extra] } : nil
47
47
 
48
- inner = safe_join([ image_el, content_el, @content.presence, extra_el ])
48
+ # Only include @content when no slots are used (block-only mode)
49
+ loose_content = @slots.values.any? ? nil : @content.presence
50
+ inner = safe_join([ image_el, content_el, loose_content, extra_el ])
49
51
 
50
52
  if href
51
- tag.a(class: classes, href: href) { inner }
53
+ tag.a(**merge_html_options(class: classes, href: href)) { inner }
52
54
  else
53
- tag.div(class: classes) { inner }
55
+ tag.div(**merge_html_options(class: classes)) { inner }
54
56
  end
55
57
  end
56
58
  end
@@ -14,6 +14,7 @@
14
14
  class CommentComponent < Component
15
15
  attribute :collapsed, :boolean, default: false
16
16
  attribute :threaded, :boolean, default: false
17
+ attribute :disabled, :boolean, default: false
17
18
 
18
19
  slot :avatar
19
20
  slot :author
@@ -23,7 +24,7 @@ class CommentComponent < Component
23
24
 
24
25
  def to_s
25
26
  classes = class_names(
26
- { "collapsed" => collapsed },
27
+ { "collapsed" => collapsed, "disabled" => disabled },
27
28
  "comment"
28
29
  )
29
30
 
@@ -38,8 +39,9 @@ class CommentComponent < Component
38
39
 
39
40
  content_el = content_parts.any? ? tag.div(class: "content") { safe_join(content_parts) } : nil
40
41
 
41
- tag.div(class: classes) {
42
- safe_join([ avatar_el, content_el, @content.presence ])
42
+ loose_content = @slots.values.any? ? nil : @content.presence
43
+ tag.div(**merge_html_options(class: classes)) {
44
+ safe_join([ avatar_el, content_el, loose_content ])
43
45
  }
44
46
  end
45
47
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CommentGroup — wrapper for a group of comments.
4
+ #
5
+ # Usage:
6
+ # CommentGroup(threaded: true) {
7
+ # Comment { |c| ... }
8
+ # Comment { |c| ... }
9
+ # }
10
+
11
+ class CommentGroupComponent < Component
12
+ attribute :threaded, :boolean, default: false
13
+ attribute :minimal, :boolean, default: false
14
+ attribute :inverted, :boolean, default: false
15
+ attribute :size, :string, default: nil
16
+
17
+ def to_s
18
+ classes = class_names(
19
+ "ui",
20
+ size,
21
+ { "threaded" => threaded, "minimal" => minimal, "inverted" => inverted },
22
+ "comments"
23
+ )
24
+
25
+ tag.div(**merge_html_options(class: classes)) { @content }
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CommentReply — a reply comment nested inside a CommentReplyGroup.
4
+ #
5
+ # Usage:
6
+ # CommentReplyGroup {
7
+ # CommentReply { |c|
8
+ # c.avatar { Image(src: "avatar.png") }
9
+ # c.author { text "Jenny" }
10
+ # c.metadata { text "Just now" }
11
+ # c.text_slot { text "Great reply!" }
12
+ # c.actions { text "Reply" }
13
+ # }
14
+ # }
15
+
16
+ class CommentReplyComponent < Component
17
+ attribute :disabled, :boolean, default: false
18
+
19
+ slot :avatar
20
+ slot :author
21
+ slot :metadata
22
+ slot :text_slot
23
+ slot :actions
24
+
25
+ def to_s
26
+ classes = class_names(
27
+ { "disabled" => disabled },
28
+ "comment"
29
+ )
30
+
31
+ avatar_el = @slots[:avatar] ? tag.a(class: "avatar") { @slots[:avatar] } : nil
32
+
33
+ content_parts = [
34
+ @slots[:author] ? tag.a(class: "author") { @slots[:author] } : nil,
35
+ @slots[:metadata] ? tag.div(class: "metadata") { @slots[:metadata] } : nil,
36
+ @slots[:text_slot] ? tag.div(class: "text") { @slots[:text_slot] } : nil,
37
+ @slots[:actions] ? tag.div(class: "actions") { @slots[:actions] } : nil
38
+ ].compact
39
+
40
+ content_el = content_parts.any? ? tag.div(class: "content") { safe_join(content_parts) } : nil
41
+
42
+ loose_content = @slots.values.any? ? nil : @content.presence
43
+ tag.div(**merge_html_options(class: classes)) {
44
+ safe_join([ avatar_el, content_el, loose_content ])
45
+ }
46
+ end
47
+ end