crud_components 0.1.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 (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +23 -0
  4. data/LICENSE +21 -0
  5. data/README.md +511 -0
  6. data/RELEASING.md +44 -0
  7. data/Rakefile +12 -0
  8. data/app/assets/stylesheets/crud_components.css +35 -0
  9. data/app/views/crud_components/_action_button.html.erb +11 -0
  10. data/app/views/crud_components/_actions.html.erb +12 -0
  11. data/app/views/crud_components/_column_header.html.erb +24 -0
  12. data/app/views/crud_components/_column_picker.html.erb +66 -0
  13. data/app/views/crud_components/_filter.html.erb +34 -0
  14. data/app/views/crud_components/_form.html.erb +30 -0
  15. data/app/views/crud_components/_pager.html.erb +41 -0
  16. data/app/views/crud_components/_record.html.erb +15 -0
  17. data/app/views/crud_components/_row.html.erb +26 -0
  18. data/app/views/crud_components/_selection_action.html.erb +14 -0
  19. data/app/views/crud_components/_sort_link.html.erb +17 -0
  20. data/app/views/crud_components/_toolbar.html.erb +50 -0
  21. data/app/views/crud_components/fields/_asciidoc.html.erb +8 -0
  22. data/app/views/crud_components/fields/_association.html.erb +13 -0
  23. data/app/views/crud_components/fields/_association_list.html.erb +24 -0
  24. data/app/views/crud_components/fields/_attachment.html.erb +16 -0
  25. data/app/views/crud_components/fields/_attachment_thumb.html.erb +17 -0
  26. data/app/views/crud_components/fields/_boolean.html.erb +13 -0
  27. data/app/views/crud_components/fields/_date.html.erb +6 -0
  28. data/app/views/crud_components/fields/_datetime.html.erb +6 -0
  29. data/app/views/crud_components/fields/_email.html.erb +7 -0
  30. data/app/views/crud_components/fields/_enum.html.erb +14 -0
  31. data/app/views/crud_components/fields/_json.html.erb +10 -0
  32. data/app/views/crud_components/fields/_markdown.html.erb +9 -0
  33. data/app/views/crud_components/fields/_number.html.erb +8 -0
  34. data/app/views/crud_components/fields/_string.html.erb +8 -0
  35. data/app/views/crud_components/fields/_text.html.erb +9 -0
  36. data/app/views/crud_components/fields/_url.html.erb +11 -0
  37. data/app/views/crud_components/filters/_boolean.html.erb +12 -0
  38. data/app/views/crud_components/filters/_date_range.html.erb +11 -0
  39. data/app/views/crud_components/filters/_number_range.html.erb +13 -0
  40. data/app/views/crud_components/filters/_select.html.erb +8 -0
  41. data/app/views/crud_components/filters/_text.html.erb +5 -0
  42. data/app/views/crud_components/form_fields/_belongs_to.html.erb +3 -0
  43. data/app/views/crud_components/form_fields/_boolean.html.erb +12 -0
  44. data/app/views/crud_components/form_fields/_date.html.erb +2 -0
  45. data/app/views/crud_components/form_fields/_datetime.html.erb +2 -0
  46. data/app/views/crud_components/form_fields/_enum.html.erb +8 -0
  47. data/app/views/crud_components/form_fields/_file.html.erb +47 -0
  48. data/app/views/crud_components/form_fields/_habtm.html.erb +5 -0
  49. data/app/views/crud_components/form_fields/_number.html.erb +2 -0
  50. data/app/views/crud_components/form_fields/_string.html.erb +3 -0
  51. data/app/views/crud_components/form_fields/_text.html.erb +2 -0
  52. data/app/views/crud_components/layouts/_table.html.erb +143 -0
  53. data/config/locales/crud_components.de.yml +39 -0
  54. data/config/locales/crud_components.en.yml +40 -0
  55. data/crud_components.gemspec +48 -0
  56. data/docs/extending.md +308 -0
  57. data/docs/fields.md +442 -0
  58. data/docs/forms.md +253 -0
  59. data/docs/performance.md +90 -0
  60. data/docs/security.md +139 -0
  61. data/docs/views.md +405 -0
  62. data/lib/crud_components/action.rb +85 -0
  63. data/lib/crud_components/builder.rb +246 -0
  64. data/lib/crud_components/config.rb +128 -0
  65. data/lib/crud_components/dynamic_column.rb +68 -0
  66. data/lib/crud_components/engine.rb +25 -0
  67. data/lib/crud_components/errors.rb +9 -0
  68. data/lib/crud_components/fields/attachment_field.rb +22 -0
  69. data/lib/crud_components/fields/base.rb +260 -0
  70. data/lib/crud_components/fields/belongs_to_field.rb +91 -0
  71. data/lib/crud_components/fields/boolean_field.rb +31 -0
  72. data/lib/crud_components/fields/computed_field.rb +34 -0
  73. data/lib/crud_components/fields/date_field.rb +51 -0
  74. data/lib/crud_components/fields/dynamic_field.rb +44 -0
  75. data/lib/crud_components/fields/enum_field.rb +40 -0
  76. data/lib/crud_components/fields/has_many_field.rb +50 -0
  77. data/lib/crud_components/fields/json_field.rb +10 -0
  78. data/lib/crud_components/fields/numeric_field.rb +31 -0
  79. data/lib/crud_components/fields/path_field.rb +327 -0
  80. data/lib/crud_components/fields/string_field.rb +41 -0
  81. data/lib/crud_components/fields/text_field.rb +9 -0
  82. data/lib/crud_components/fieldset.rb +38 -0
  83. data/lib/crud_components/helpers.rb +259 -0
  84. data/lib/crud_components/like_spec.rb +113 -0
  85. data/lib/crud_components/markup.rb +36 -0
  86. data/lib/crud_components/model.rb +33 -0
  87. data/lib/crud_components/permission_context.rb +62 -0
  88. data/lib/crud_components/presenters/actions.rb +51 -0
  89. data/lib/crud_components/presenters/base.rb +95 -0
  90. data/lib/crud_components/presenters/cell_context.rb +28 -0
  91. data/lib/crud_components/presenters/cells.rb +160 -0
  92. data/lib/crud_components/presenters/collection.rb +498 -0
  93. data/lib/crud_components/presenters/column_selection.rb +91 -0
  94. data/lib/crud_components/presenters/filter.rb +38 -0
  95. data/lib/crud_components/presenters/form.rb +57 -0
  96. data/lib/crud_components/presenters/record.rb +57 -0
  97. data/lib/crud_components/query.rb +110 -0
  98. data/lib/crud_components/route_resolver.rb +123 -0
  99. data/lib/crud_components/structure.rb +343 -0
  100. data/lib/crud_components/version.rb +3 -0
  101. data/lib/crud_components/where_like.rb +13 -0
  102. data/lib/crud_components.rb +160 -0
  103. data/lib/generators/crud_components/install/install_generator.rb +43 -0
  104. data/lib/generators/crud_components/install/templates/crud_columns_controller.js +76 -0
  105. data/lib/generators/crud_components/install/templates/crud_filter_controller.js +32 -0
  106. data/lib/generators/crud_components/install/templates/crud_multiselect_controller.js +70 -0
  107. data/lib/generators/crud_components/install/templates/crud_select_controller.js +35 -0
  108. data/lib/generators/crud_components/install/templates/initializer.rb +56 -0
  109. data/lib/generators/crud_components/views/views_generator.rb +14 -0
  110. metadata +209 -0
data/docs/extending.md ADDED
@@ -0,0 +1,308 @@
1
+ # Extending & styling
2
+
3
+ The gem ships **no CSS** but is designed for **Bootstrap 5 by default**, and is built to
4
+ drop into an app that has its own design — even one on a CSS framework that works nothing
5
+ like Bootstrap. The class map and the partials cover overrides: swap cosmetic classes in
6
+ the map, override individual partials where the structure differs. For a whole different
7
+ framework, PRs are welcome — open an issue to talk it through first. Two facts make that
8
+ practical:
9
+
10
+ 1. **Everything visual is a partial**, and a file at the same path in your app wins
11
+ (standard Rails view-path precedence — the same mechanism as Devise or Kaminari
12
+ views). That one rule is the entire extension API.
13
+ 2. **The surfaces are decomposed**, so overriding one piece doesn't mean reimplementing
14
+ the others — you reuse the presenter and the sub-partials.
15
+
16
+ ## How far do you need to go?
17
+
18
+ | You want to… | Do this | Reach for |
19
+ | --------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------ |
20
+ | tweak colours / button styles | change CSS class names | the [class map](#styling) |
21
+ | restructure **one** surface (different table markup, your grid) | override **one** partial | `rails g crud_components:views`, then edit |
22
+ | add a whole new arrangement (cards, list, kanban) | add a layout partial | [Add a layout](#add-a-layout) |
23
+ | change a single field's display rendering | add a renderer partial | [renderers](#add-a-field-renderer) |
24
+ | move to a different CSS framework | override the partials (class map covers the easy bits) | this whole doc |
25
+
26
+ The class map is the *simplest* lever and deliberately covers only the common, cosmetic
27
+ cases — colours, sizes, button variants. It is **not** a full theming engine: structural
28
+ and utility classes (`d-flex`, `input-group`, `form-check`, `table-responsive`) live in
29
+ the partials, because pretending every framework shares Bootstrap's class vocabulary
30
+ would be a leaky abstraction. For a framework that works differently, you override the
31
+ relevant partials — and because they're small and decomposed, that stays cheap.
32
+ **When in doubt, copy the whole partial and rewrite it; that is a supported, first-class
33
+ path, not a failure.**
34
+
35
+ ```sh
36
+ bin/rails generate crud_components:views # copy the gem's partials into your app to edit
37
+ ```
38
+
39
+ ```
40
+ crud_components/
41
+ layouts/_table.html.erb # collection layouts (layout: :table, …)
42
+ _toolbar.html.erb # search box + reset + collection actions (reused by layouts)
43
+ _pager.html.erb # footer pager in the table (shown when the relation is paginated)
44
+ _actions.html.erb # a group of action buttons
45
+ fields/_string.html.erb … # value renderers (as: :string, …)
46
+ filters/_text.html.erb … # filter controls
47
+ _record.html.erb
48
+ _filter.html.erb
49
+ _form.html.erb # _form renders via simple_form
50
+ ```
51
+
52
+ ## Overriding one surface without rewriting the rest
53
+
54
+ Say you want a completely different collection table — your own `<table>` markup, your
55
+ framework's classes. Override `crud_components/layouts/_table.html.erb` and rewrite the
56
+ shell only. You do **not** reimplement search, filtering, sorting, cells or actions —
57
+ the `collection` presenter and the sub-partials hand them to you:
58
+
59
+ ```erb
60
+ <%# your app/views/crud_components/layouts/_table.html.erb %>
61
+ <%= render 'crud_components/toolbar', collection: collection %> <%# search + actions %>
62
+ <table class="my-table">
63
+ <thead><tr>
64
+ <% collection.fields.each do |field| %>
65
+ <th>
66
+ <% if collection.sortable_field?(field) %>
67
+ <a href="<%= collection.sort_url(field) %>">
68
+ <%= field.human_name %>
69
+ <%# sort_direction is :asc / :desc / nil (nil = not the active column).
70
+ sort_numeric? picks a numeric vs alphabetic icon; css.icon_prefix is
71
+ the library prefix. %>
72
+ <% if (dir = collection.sort_direction(field)) %>
73
+ <% family = collection.sort_numeric?(field) ? 'sort-numeric' : 'sort-alpha' %>
74
+ <i class="<%= collection.css.icon_prefix %><%= family %>-<%= dir == :desc ? 'up' : 'down' %>"></i>
75
+ <% else %>
76
+ <i class="<%= collection.css.icon_prefix %>arrow-down-up text-muted opacity-25"></i>
77
+ <% end %>
78
+ </a>
79
+ <% else %>
80
+ <%= field.human_name %>
81
+ <% end %>
82
+ </th>
83
+ <% end %>
84
+ </tr></thead>
85
+ <tbody>
86
+ <% collection.records.each do |record| %>
87
+ <tr id="<%= dom_id(record) %>">
88
+ <% collection.fields.each do |field| %>
89
+ <td><%= collection.cell(field, record) %></td> <%# type-aware cell, links, click-to-filter %>
90
+ <% end %>
91
+ <td><%= render 'crud_components/actions', actions: collection.row_actions(record) %></td>
92
+ </tr>
93
+ <% end %>
94
+ </tbody>
95
+ </table>
96
+ ```
97
+
98
+ The reusable building blocks the `collection` presenter exposes:
99
+
100
+ | Method | Returns |
101
+ | ---------------------------------------- | --------------------------------------------------------------------------------------------- |
102
+ | `fields` | the permitted fields (columns) to show, in order |
103
+ | `records` | the resolved, filtered, sorted rows (an array) |
104
+ | `cell(field, record)` | the type-aware cell HTML — value renderer, label link, click-to-filter |
105
+ | `sortable_field?(field)` | boolean: is this column sortable |
106
+ | `sort_url(field)` | the link that toggles/sets this column's sort |
107
+ | `sort_active?(field)` | boolean: is the result currently sorted by this column |
108
+ | `sort_direction(field)` | `:asc` / `:desc`, or `nil` when not the active sort column — turn it into a glyph yourself |
109
+ | `sort_numeric?(field)` | boolean: does this column sort numerically (vs alphabetically) — pick a numeric vs alpha icon |
110
+ | `filterable_field?(field)` | boolean: does this column have a filter control |
111
+ | `render_filter_control(field, query, …)` | the inline filter control HTML for a field |
112
+ | `row_actions(record)` | an `Actions` presenter for one row — feed to `_actions` |
113
+ | `collection_actions` | an `Actions` presenter for collection-level actions (e.g. "New") |
114
+ | `searchable?` | boolean: is there a free-text search (`?q=`) |
115
+ | `search_param_name` | the query-param name for the search box (respects `param_prefix:`) |
116
+ | `filtered?` | boolean: is any filter/search/sort currently active |
117
+ | `reset_url` | URL that clears *this* collection's filter/search/sort/page params |
118
+ | `filter_form_id` | the id of the external `<form>` the inline filter inputs bind to |
119
+ | `preserved_params` | params to re-emit as hidden inputs so the filter form keeps unrelated state |
120
+ | `paginated?` | boolean: was the relation handed in already `.page`-d (kaminari/will_paginate) |
121
+ | `page_scope` | the underlying (possibly paginated) relation, for driving your own pager |
122
+ | `page_url(n)` | a URL for page `n` that keeps this collection's state and others' params |
123
+ | `pager_pages(window:)` | page numbers to render, with `:gap` markers for elided ranges |
124
+
125
+ For pagination, either render the gem's `_pager` sub-partial, or feed `page_scope` (the underlying relation) to your own pager — e.g. `<%= paginate collection.page_scope %>` for kaminari (you style its markup, as always with kaminari). Sub-partials you can drop in: `_toolbar`, `_pager`, `_actions`. Filtering and the whitelist are never reimplemented in a layout — the presenter has already done that.
126
+
127
+ ## Add a field renderer
128
+
129
+ A renderer named `:stars` is the partial `crud_components/fields/_stars.html.erb`. It
130
+ receives `value`, `record`, `field`, `surface` (`:collection` or `:record`), and
131
+ `cell_context` (for click-to-filter; nil on surfaces without a query):
132
+
133
+ ```erb
134
+ <%# app/views/crud_components/fields/_stars.html.erb %>
135
+ <span title="<%= value %>/5"><%= '★' * value.to_i %><%= '☆' * (5 - value.to_i) %></span>
136
+ ```
137
+
138
+ ```ruby
139
+ attribute :rating, as: :stars
140
+ ```
141
+
142
+ `surface:` is how the built-in `:text` truncates in tables but not on record pages, and
143
+ `:image` sizes itself. Built-in renderers are the same kind of partial at the same paths —
144
+ **shadow one in your app to change it everywhere.**
145
+
146
+ ## Form inputs
147
+
148
+ Each input renders through a per-type partial,
149
+ `crud_components/form_fields/_<type>.html.erb`: **the partial decides *what* to render
150
+ (which `f.input`, its collection, blank options, …) and simple_form does the rest** (the
151
+ wrapper, label, hint and error markup, following your app's simple_form config). See
152
+ [Forms and your design system](#forms-and-your-design-system).
153
+
154
+ Two ways to customize:
155
+
156
+ * **Restyle a whole type** — shadow the partial, e.g. `form_fields/_enum.html.erb`, and it
157
+ changes everywhere that type appears.
158
+ * **Point one field at a different partial** — `attribute :slug, form_as: :string` renders
159
+ `slug` through `form_fields/_string.html.erb` (this mirrors `as:` for the display
160
+ renderer). There is no `form` facet.
161
+
162
+ To take over form rendering entirely, override `crud_components/_form.html.erb`.
163
+
164
+ ## Add a layout
165
+
166
+ A layout named `:cards` is the partial `crud_components/layouts/_cards.html.erb`,
167
+ receiving one `collection` presenter with resolved fields, rows, query state and sort
168
+ URLs — a custom layout never reimplements filtering or whitelisting:
169
+
170
+ ```erb
171
+ <%= crud_collection @books, layout: :cards %>
172
+ ```
173
+
174
+ ![A custom cards layout: the same collection presenter rendered as a responsive card grid (cover image pulled out, fields below), reusing the gem's search, filter sidebar and row actions](screenshots/cards.png)
175
+
176
+ A layout calls the same presenter interface [listed above](#overriding-one-surface-without-rewriting-the-rest)
177
+ — `fields`, `records`, `cell`, the sort/filter/action helpers, the pagination helpers — so
178
+ the built-in `_table` is a good starting point; copy it. For a worked example that pulls
179
+ an image field out as a card image, see the dummy app's
180
+ [`_cards.html.erb`](https://github.com/itadventurer/crud_components/blob/main/test/dummy/app/views/crud_components/layouts/_cards.html.erb).
181
+
182
+ ## Progressive enhancement
183
+
184
+ The progressive-enhancement story is deliberately **one mechanism, not a fork**: the
185
+ markup is *always* the plain, accessible, no-JS baseline, and JavaScript enhances that
186
+ same markup in place via Stimulus controllers attached with `data-controller`. There are
187
+ **no** parallel "raw" vs. "fancy" template trees to keep in sync, and Bootstrap-vs-other
188
+ lives in the [class map](#styling), not in template variants. A controller that isn't
189
+ loaded simply leaves the baseline as-is.
190
+
191
+ The gem ships **four** optional controllers, copied in by the install generator (you
192
+ register them with Stimulus; the gem depends on none):
193
+
194
+ ```sh
195
+ bin/rails generate crud_components:install
196
+ # initializer + crud-filter + crud-multiselect + crud-columns + crud-select
197
+ ```
198
+
199
+ - **`crud-filter`** strips empty params on submit (clean URLs) and auto-submits selects in
200
+ the inline filter row only (the standalone filter form never auto-submits — users
201
+ compose several filters there).
202
+ - **`crud-multiselect`** turns a habtm `<select multiple>` into a chips-list (each removable)
203
+ + an "add" dropdown. The select stays the hidden source of truth, so the form submits
204
+ identically with or without JS. Good up to a few hundred options; for thousands, render
205
+ an autocomplete against your own endpoint instead (see [forms.md](forms.md)).
206
+ - **`crud-columns`** lets the user drag the column-picker rows to reorder, and collapses
207
+ the submitted `?cols[]=a&cols[]=b` into a tidier `?cols=a,b`. Without it the picker still
208
+ works (tick + Apply is a plain GET); you just lose drag-reorder and the prettier URL.
209
+ - **`crud-select`** adds a "select all visible" / per-group master checkbox and a live
210
+ "N selected" count to selectable tables (bulk/selection actions). Without it the row
211
+ checkboxes still submit; you just tick them individually.
212
+
213
+ Each follows the same recipe, which is the whole pattern for any enhancement (a belongs_to
214
+ text input into an autocomplete, a date field into a range picker, …):
215
+
216
+ 1. The gem's partial renders the accessible baseline and, where useful, carries a
217
+ `data-controller` hook.
218
+ 2. Your Stimulus controller reads that markup and enhances it in place, manipulating the
219
+ underlying inputs so form submission is unchanged with or without JS.
220
+ 3. Ship the controller however you ship Stimulus (importmap pin, `app/javascript`, …).
221
+
222
+ ## Styling
223
+
224
+ The gem ships **no CSS** and produces markup meant to look native in the host app —
225
+ Bootstrap 5 class names by default, concentrated in one overridable class map for the
226
+ common cosmetic cases:
227
+
228
+ ```ruby
229
+ # config/initializers/crud_components.rb (created by `rails g crud_components:install`)
230
+ CrudComponents.configure do |config|
231
+ config.css.table = 'table table-sm table-hover'
232
+ config.css.button = 'btn btn-outline-dark'
233
+ config.css.badge = 'badge text-bg-secondary'
234
+ config.select_limit = 250 # belongs_to filter: select → text input threshold
235
+ end
236
+ ```
237
+
238
+ The full key list is `CrudComponents::Config::DEFAULT_CSS`. Each key feeds the `class="…"`
239
+ of one kind of element (table, button, badge, inputs, the toolbar, the filter row, …).
240
+
241
+ **Scope, honestly.** The class map is the *simplest* lever, not a theming engine. It
242
+ covers the elements whose class is a single configurable value. It does **not** abstract
243
+ away structure — utility classes like `d-flex`, `input-group`, `form-check` and
244
+ `table-responsive` live in the partials, because a class map that tried to model every
245
+ framework's layout primitives would be a leaky abstraction that helps no one.
246
+
247
+ Icons are rendered as `<i class="#{css.icon_prefix}#{name}">` — **Bootstrap Icons by
248
+ default** (`config.css.icon_prefix = 'bi bi-'`). Switch icon libraries by setting the
249
+ prefix, e.g. `config.css.icon_prefix = 'fa fa-'` for Font Awesome (the built-in icon
250
+ *names* are Bootstrap Icons, so a different library may need its own names — see below).
251
+
252
+ The icon **names** (the part after the prefix) live in two maps, so a different library is
253
+ a config change rather than a partial override:
254
+
255
+ ```ruby
256
+ config.action_icons[:destroy] = 'trash-fill' # per derived action; nil = no icon
257
+ config.file_icons['zip'] = 'file-earmark-zip' # attachment glyph by file extension
258
+ config.file_fallback_icon = 'file-earmark-text' # extension not in the map
259
+ ```
260
+
261
+ `config.action_icons` keys are the derived actions (`:new`/`:show`/`:edit`/`:destroy`);
262
+ `config.file_icons` maps a file extension to a full icon name (the whole Bootstrap
263
+ `filetype-*` family ships by default). Full lists: `Config::DEFAULT_ACTION_ICONS` /
264
+ `Config::DEFAULT_FILE_ICONS`.
265
+
266
+ So the real cost of a different framework is: **swap the class map for the cosmetic
267
+ classes, and override the few partials whose structure differs.** For a utility-first
268
+ framework like Tailwind (no semantic class names at all), you put the utility strings in
269
+ the map and override the structural partials:
270
+
271
+ ```ruby
272
+ config.css.button = 'inline-flex items-center rounded px-3 py-1.5 bg-gray-100 hover:bg-gray-200'
273
+ config.css.input = 'block w-full rounded border-gray-300'
274
+ config.css.toolbar = 'flex items-center justify-between gap-2 mb-2'
275
+ config.css.badge = 'inline-flex rounded-full bg-gray-100 px-2 text-xs'
276
+ # …then `rails g crud_components:views` and adjust _form / _toolbar / the filter
277
+ # controls where the *structure* (not just the class) needs to change.
278
+ ```
279
+
280
+ This is the intended path, not a fork: you keep all the derivation, the query layer and
281
+ the presenters; you rewrite only the markup that your framework shapes differently.
282
+
283
+ ## Forms and your design system
284
+
285
+ Forms are the **one** surface you mostly don't have to reskin by hand: they render through
286
+ [simple_form](https://github.com/heartcombo/simple_form), so they inherit your app's
287
+ simple_form wrapper config automatically — Bootstrap by default, and the community ships
288
+ Tailwind/Bulma/Foundation wrappers. Configure simple_form for your framework (its install
289
+ generator does this) and the gem's forms follow, including per-field error display (so
290
+ there's no `field_with_errors` wart to neutralize). The gem still derives *which* fields,
291
+ their types, the [permit list](forms.md) and the read-only/permission rules; simple_form
292
+ owns the markup.
293
+
294
+ Need to go further than wrappers allow? Override a per-type partial under
295
+ `crud_components/form_fields/` (see [Form inputs](#form-inputs)), or override
296
+ `crud_components/_form.html.erb` itself and render the fields however you like — the
297
+ `form` presenter hands you `fields`, `editable?(field)`, `form_options`, `summary_errors`
298
+ and `display(field)`, and each `field` knows its own `form_partial`.
299
+
300
+ ## i18n
301
+
302
+ Headers come from `human_attribute_name` (computed fields included), so your existing
303
+ ActiveRecord i18n applies. Every gem-generated string is looked up with a
304
+ `t(..., default:)` fallback, so the gem works with **zero** locale setup and is fully
305
+ translatable when you want it. Relevant keys live under `crud_components.*` (actions,
306
+ filter labels, "+n more", confirm dialogs, empty state).
307
+
308
+ See also: [Views](views.md) · [Fields](fields.md) · [Forms](forms.md).