layered-ui-rails 0.16.1 → 0.17.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c6db00c8644f6d77f57044ddb722d85679aa1a212dcf97e343235dedc059aa6
4
- data.tar.gz: 427a18c3eddf0676e5c9c6b92b13ac651c01a8d73b964a4d5c4ea5c683102cdc
3
+ metadata.gz: e39aa0c82625f31531a7c634bd1e30233b7b0c8beddd349ad3bd36225e0f580d
4
+ data.tar.gz: 71bedafdf65c00a1808debbc949ef8560d40d02f150afe89f844bd345437d124
5
5
  SHA512:
6
- metadata.gz: c543aa25cc392198de7350249cc6e448d84513af39b3ca5e42f0e94058a94aa014fd05ceb8eaed7dc3427c88c791f704b86101bc7636c23ac9fdf42d5ad5c142
7
- data.tar.gz: '003035092eee2d1a1aa9994829da39e532cf067f6a8248478d09a3ec5167cb09835fae97c7c808bc00087da9f4a2a643bdd8d7c600e4afe374ed3195be3f8ec3'
6
+ metadata.gz: 4104d1ccd55bf440a323975b6a06aace5346179c8fc06ad51fa037050ff25bfad540a075582d9f1c80587b127dd9176fef3ed98f8462c52f93cfb780b6e11802
7
+ data.tar.gz: d7ddafdb90cd018c340274602eb730079809bd35d1e1f807a729ebd41c4e76ff19c4a717bc0e3b8da735f821d3a1eb353804514acf8cbfa116592ef7343aa85b
@@ -77,6 +77,28 @@ Populate layout regions with `content_for` (always above the render call):
77
77
  <% content_for :l_ui_logo_dark do %>
78
78
  <%= image_tag "my_logo_dark.svg", alt: "", class: "l-ui-header__logo l-ui-header__logo--dark" %>
79
79
  <% end %>
80
+
81
+ <%# Prepend or append items to the header actions group %>
82
+ <% content_for :l_ui_header_actions_start do %>
83
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
84
+ <% end %>
85
+ <% content_for :l_ui_header_actions_end do %>
86
+ <%= link_to "Help", help_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
87
+ <% end %>
88
+
89
+ <%# Or replace the default actions group entirely and compose with helpers %>
90
+ <% content_for :l_ui_header_actions do %>
91
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
92
+ <%= l_ui_theme_toggle %>
93
+ <%= l_ui_authentication %>
94
+ <%= l_ui_navigation_toggle %>
95
+ <% end %>
96
+
97
+ <%# Inline header links (alongside the logo) %>
98
+ <% content_for :l_ui_header_links do %>
99
+ <%= link_to "Pricing", pricing_path %>
100
+ <%= link_to "About", about_path %>
101
+ <% end %>
80
102
  ```
81
103
 
82
104
  Body class modifiers:
@@ -116,6 +138,9 @@ Quick reference:
116
138
  | `l_ui_normalise_field(record, config)` | Normalise a raw field config hash into canonical form |
117
139
  | `l_ui_user_signed_in?` | Check if user is authenticated |
118
140
  | `l_ui_current_user` | Current user object |
141
+ | `l_ui_theme_toggle` | Default header theme toggle button |
142
+ | `l_ui_authentication` | Default header login/register buttons (Devise) |
143
+ | `l_ui_navigation_toggle` | Default header sidebar toggle button |
119
144
 
120
145
  ## CSS classes
121
146
 
@@ -208,7 +208,7 @@ Formats a date/time value as `"%-d %b %Y, %H:%M"` (e.g. "15 Apr 2026, 10:30"). R
208
208
  ## Form
209
209
 
210
210
  ```ruby
211
- l_ui_form(record, fields:, url:, method: nil, submit: nil)
211
+ l_ui_form(record, fields:, url:, method: nil, submit: nil, multipart: nil)
212
212
  ```
213
213
 
214
214
  - `record` (ActiveRecord) - the model instance
@@ -216,16 +216,21 @@ l_ui_form(record, fields:, url:, method: nil, submit: nil)
216
216
  - `url` (String) - form action URL
217
217
  - `method` (Symbol, optional) - HTTP method override
218
218
  - `submit` (String, optional) - submit button text; defaults to "Create" for new records and "Save" for persisted records
219
+ - `multipart` (Boolean, optional) - override multipart encoding. Defaults to auto-detection: `true` when any field is `:file`, otherwise `false`. Pass `true`/`false` to force.
220
+
221
+ If you need control beyond these options, override the partial in your host app by creating `app/views/layered/ui/managed_resource/_form.html.erb` (Rails view lookup prefers the host's copy), or write the form directly with `form_with` and reuse `layered_ui/shared/form_errors`, `layered_ui/shared/label`, and `layered_ui/shared/field_error`.
219
222
 
220
223
  Renders a complete form with all fields, error summary, and submit button via the `layered/ui/managed_resource/form` partial.
221
224
 
222
225
  Field options:
223
226
  - `attribute` (Symbol) - model attribute
224
- - `as` (Symbol, optional) - field type; auto-detected from column type. Supported: `:string`, `:text`, `:email`, `:password`, `:number`, `:date`, `:datetime`, `:select`, `:checkbox`, `:hidden`
227
+ - `as` (Symbol, optional) - field type; auto-detected from column type. Supported: `:string`, `:text`, `:email`, `:password`, `:number`, `:tel`, `:url`, `:search`, `:date`, `:datetime`, `:time`, `:month`, `:week`, `:color`, `:range`, `:file`, `:select`, `:checkbox`, `:hidden`. Forms automatically become `multipart` when any field is `:file`.
225
228
  - `label` (String, optional) - custom label text; defaults to humanised attribute
226
229
  - `required` (Boolean, optional) - marks field as required; default false
227
230
  - `hint` (String, optional) - help text below the field
228
231
  - `collection` (Array, optional) - required for `:select` type; e.g. `[['Label', value], ...]`
232
+ - `include_blank` (Boolean or String, optional) - for `:select` fields; defaults to `true`. Pass a string to use as the blank option's label, or `false` to omit it. Suppressed when `prompt:` is set
233
+ - `prompt` (String, optional) - for `:select` fields; prompt text shown as the first option, only selectable when no value is set
229
234
  - `placeholder` (String, optional) - input placeholder text
230
235
 
231
236
  ```erb
@@ -287,6 +292,25 @@ The button does not need to be inside the helper's wrapper, and no `data-control
287
292
 
288
293
  Calling `dialog.showModal()` directly is not supported - it bypasses the `l-ui--modal` controller and skips scroll lock, focus restoration, open-count tracking, and the screen-reader announcement.
289
294
 
295
+ ## Header
296
+
297
+ ```ruby
298
+ l_ui_theme_toggle # Renders the dark/light theme toggle button
299
+ l_ui_authentication # Renders Devise login/register buttons (no-op when signed in or when Devise routes are absent)
300
+ l_ui_navigation_toggle # Renders the hamburger that toggles the sidebar (only when navigation items exist or a user is signed in)
301
+ ```
302
+
303
+ Use these when overriding the header actions group with `:l_ui_header_actions` to compose your own bar from the default building blocks. The header also exposes `:l_ui_header_actions_start` and `:l_ui_header_actions_end` yields to prepend or append items without replacing the defaults, and `:l_ui_header_links` for inline links beside the logo.
304
+
305
+ ```erb
306
+ <% content_for :l_ui_header_actions do %>
307
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
308
+ <%= l_ui_theme_toggle %>
309
+ <%= l_ui_authentication %>
310
+ <%= l_ui_navigation_toggle %>
311
+ <% end %>
312
+ ```
313
+
290
314
  ## Authentication
291
315
 
292
316
  ```ruby
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [0.17.0] - 2026-05-19
6
+
7
+ ### Added
8
+
9
+ - `header_actions` helper for inserting custom action buttons into the layout header, alongside the existing authentication/theme/navigation toggles
10
+ - Header partial split into `_authentication`, `_navigation_toggle`, and `_theme_toggle` sub-partials
11
+ - Expanded `l_ui_form` field types: `tel`, `url`, `search`, `time`, `month`, `week`, `color`, `range`, and `file`
12
+ - `multipart:` option on `l_ui_form` to override the form's multipart encoding (auto-detected from `:file` fields otherwise)
13
+ - `prompt:` field option on `:select` fields in `l_ui_form`, shown as the first option and only selectable when no value is set; suppresses the default blank option
14
+
5
15
  ## [0.16.1] - 2026-05-16
6
16
 
7
17
  ### Fixed
@@ -1,7 +1,12 @@
1
1
  module Layered
2
2
  module Ui
3
3
  module FormHelper
4
- FIELD_TYPES = %i[string text email password number date datetime select checkbox hidden].freeze
4
+ FIELD_TYPES = %i[
5
+ string text email password number tel url search
6
+ date datetime time month week
7
+ color range file
8
+ select checkbox hidden
9
+ ].freeze
5
10
 
6
11
  # Renders a complete form with all fields, error summary,
7
12
  # and submit button.
@@ -10,9 +15,10 @@ module Layered
10
15
  # fields: Post.l_managed_resource_fields,
11
16
  # url: managed_posts_path)
12
17
  #
13
- def l_ui_form(record, fields:, url:, method: nil, submit: nil)
18
+ def l_ui_form(record, fields:, url:, method: nil, submit: nil, multipart: nil)
14
19
  render partial: "layered/ui/managed_resource/form",
15
- locals: { record: record, fields: fields, url: url, method: method, submit: submit }
20
+ locals: { record: record, fields: fields, url: url, method: method,
21
+ submit: submit, multipart: multipart }
16
22
  end
17
23
 
18
24
  # Normalises a raw field config hash into a canonical form.
@@ -35,7 +41,7 @@ module Layered
35
41
  label = config[:label] || attribute.to_s.humanize
36
42
 
37
43
  extras = config.except(:attribute, :as, :label, :required, :hint,
38
- :collection, :placeholder)
44
+ :collection, :placeholder, :prompt, :include_blank)
39
45
 
40
46
  {
41
47
  attribute: attribute,
@@ -45,6 +51,8 @@ module Layered
45
51
  hint: config[:hint],
46
52
  collection: config[:collection],
47
53
  placeholder: config[:placeholder],
54
+ prompt: config[:prompt],
55
+ include_blank: config[:include_blank],
48
56
  extras: extras
49
57
  }
50
58
  end
@@ -0,0 +1,17 @@
1
+ module Layered
2
+ module Ui
3
+ module HeaderHelper
4
+ def l_ui_theme_toggle
5
+ render "layouts/layered_ui/theme_toggle"
6
+ end
7
+
8
+ def l_ui_authentication
9
+ render "layouts/layered_ui/authentication"
10
+ end
11
+
12
+ def l_ui_navigation_toggle
13
+ render "layouts/layered_ui/navigation_toggle"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -25,11 +25,15 @@
25
25
  <%
26
26
  collection = config[:collection]
27
27
  choices = collection.respond_to?(:call) ? collection.call : collection
28
- include_blank = extras.delete(:include_blank)
29
- include_blank = true if include_blank.nil?
28
+ include_blank = config[:include_blank]
29
+ prompt = config[:prompt]
30
+ include_blank = true if include_blank.nil? && prompt.nil?
31
+ select_options = {}
32
+ select_options[:include_blank] = include_blank unless include_blank.nil?
33
+ select_options[:prompt] = prompt unless prompt.nil?
30
34
  %>
31
35
  <div class="l-ui-select-container">
32
- <%= form.select(attribute, choices, { include_blank: include_blank },
36
+ <%= form.select(attribute, choices, select_options,
33
37
  class: field_class.call("l-ui-select"), **base_opts, **extras) %>
34
38
  </div>
35
39
  <% elsif config[:as] == :checkbox %>
@@ -38,7 +42,13 @@
38
42
  <%= form.label(attribute, config[:label], class: "l-ui-checkbox-container__label") %>
39
43
  </div>
40
44
  <% else %>
41
- <% method_name = config[:as] == :string ? :text_field : :"#{config[:as]}_field" %>
45
+ <%
46
+ method_name = case config[:as]
47
+ when :string then :text_field
48
+ when :tel then :telephone_field
49
+ else :"#{config[:as]}_field"
50
+ end
51
+ %>
42
52
  <%= form.public_send(method_name, attribute,
43
53
  class: field_class.call("l-ui-form__field"), **base_opts, **extras) %>
44
54
  <% end %>
@@ -1,5 +1,7 @@
1
+ <% multipart = local_assigns.fetch(:multipart, nil) %>
1
2
  <% form_opts = { url: url, class: "l-ui-form" } %>
2
3
  <% form_opts[:method] = method if method %>
4
+ <% form_opts[:multipart] = multipart.nil? ? fields.any? { |f| f[:as] == :file } : multipart %>
3
5
 
4
6
  <%= form_with(model: record, **form_opts) do |f| %>
5
7
  <%= render "layered_ui/shared/form_errors", item: record %>
@@ -0,0 +1,9 @@
1
+ <% unless l_ui_user_signed_in? %>
2
+ <% if respond_to?(:new_user_registration_path) %>
3
+ <%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
4
+ <% end %>
5
+
6
+ <% if respond_to?(:new_user_session_path) %>
7
+ <%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
8
+ <% end %>
9
+ <% end %>
@@ -14,42 +14,14 @@
14
14
  <% end %>
15
15
 
16
16
  <nav class="l-ui-header__navigation" aria-label="Header navigation">
17
- <button
18
- type="button"
19
- data-controller="l-ui--theme"
20
- data-action="click->l-ui--theme#toggle"
21
- data-l-ui--theme-target="button"
22
- class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
23
- aria-label="Toggle dark mode"
24
- aria-pressed="false"
25
- >
26
- <%= image_tag "layered_ui/icon_moon.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--light", aria: { hidden: true } %>
27
- <%= image_tag "layered_ui/icon_sun.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--dark", aria: { hidden: true } %>
28
- </button>
29
-
30
- <% unless l_ui_user_signed_in? %>
31
- <% if respond_to?(:new_user_registration_path) %>
32
- <%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
33
- <% end %>
34
-
35
- <% if respond_to?(:new_user_session_path) %>
36
- <%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
37
- <% end %>
38
- <% end %>
39
-
40
- <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
41
- <button
42
- type="button"
43
- class="l-ui-button l-ui-button--navigation-toggle"
44
- data-action="click->l-ui--navigation#toggle"
45
- data-l-ui--navigation-target="toggleButton"
46
- aria-label="Toggle navigation menu"
47
- aria-expanded="false"
48
- aria-controls="l-ui-navigation"
49
- >
50
- <%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon l-ui-icon--md", data: { "l-ui--navigation-target": "openIcon" }, aria: { hidden: true } %>
51
- <%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon l-ui-icon--md", style: "display: none;", data: { "l-ui--navigation-target": "closeIcon" }, aria: { hidden: true } %>
52
- </button>
17
+ <% if content_for?(:l_ui_header_actions) %>
18
+ <%= yield(:l_ui_header_actions) %>
19
+ <% else %>
20
+ <%= yield(:l_ui_header_actions_start) %>
21
+ <%= l_ui_theme_toggle %>
22
+ <%= l_ui_authentication %>
23
+ <%= l_ui_navigation_toggle %>
24
+ <%= yield(:l_ui_header_actions_end) %>
53
25
  <% end %>
54
26
  </nav>
55
27
  </div>
@@ -0,0 +1,14 @@
1
+ <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
2
+ <button
3
+ type="button"
4
+ class="l-ui-button l-ui-button--navigation-toggle"
5
+ data-action="click->l-ui--navigation#toggle"
6
+ data-l-ui--navigation-target="toggleButton"
7
+ aria-label="Toggle navigation menu"
8
+ aria-expanded="false"
9
+ aria-controls="l-ui-navigation"
10
+ >
11
+ <%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon l-ui-icon--md", data: { "l-ui--navigation-target": "openIcon" }, aria: { hidden: true } %>
12
+ <%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon l-ui-icon--md", style: "display: none;", data: { "l-ui--navigation-target": "closeIcon" }, aria: { hidden: true } %>
13
+ </button>
14
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <button
2
+ type="button"
3
+ data-controller="l-ui--theme"
4
+ data-action="click->l-ui--theme#toggle"
5
+ data-l-ui--theme-target="button"
6
+ class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
7
+ aria-label="Toggle dark mode"
8
+ aria-pressed="false"
9
+ >
10
+ <%= image_tag "layered_ui/icon_moon.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--light", aria: { hidden: true } %>
11
+ <%= image_tag "layered_ui/icon_sun.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--dark", aria: { hidden: true } %>
12
+ </button>
@@ -33,6 +33,7 @@ module Layered
33
33
  helper Layered::Ui::TableHelper
34
34
  helper Layered::Ui::TitleBarHelper
35
35
  helper Layered::Ui::FormHelper
36
+ helper Layered::Ui::HeaderHelper
36
37
  helper Layered::Ui::ModalHelper
37
38
  helper Layered::Ui::RansackHelper
38
39
  end
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.16.1"
3
+ VERSION = "0.17.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-ui-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -213,6 +213,7 @@ files:
213
213
  - app/helpers/layered/ui/authentication_helper.rb
214
214
  - app/helpers/layered/ui/breadcrumbs_helper.rb
215
215
  - app/helpers/layered/ui/form_helper.rb
216
+ - app/helpers/layered/ui/header_helper.rb
216
217
  - app/helpers/layered/ui/modal_helper.rb
217
218
  - app/helpers/layered/ui/navigation_helper.rb
218
219
  - app/helpers/layered/ui/pagy_helper.rb
@@ -253,10 +254,13 @@ files:
253
254
  - app/views/layered_ui/shared/_label.html.erb
254
255
  - app/views/layered_ui/shared/_search_field.html.erb
255
256
  - app/views/layered_ui/shared/_search_select.html.erb
257
+ - app/views/layouts/layered_ui/_authentication.html.erb
256
258
  - app/views/layouts/layered_ui/_header.html.erb
257
259
  - app/views/layouts/layered_ui/_navigation.html.erb
260
+ - app/views/layouts/layered_ui/_navigation_toggle.html.erb
258
261
  - app/views/layouts/layered_ui/_notice.html.erb
259
262
  - app/views/layouts/layered_ui/_panel.html.erb
263
+ - app/views/layouts/layered_ui/_theme_toggle.html.erb
260
264
  - app/views/layouts/layered_ui/application.html.erb
261
265
  - config/importmap.rb
262
266
  - lib/generators/layered/ui/copy_assets_generator.rb