layered-ui-rails 0.2.5 → 0.3.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 +4 -4
- data/AGENTS.md +0 -4
- data/CHANGELOG.md +15 -0
- data/app/assets/tailwind/layered/ui/styles.css +21 -0
- data/app/helpers/layered/ui/ransack_helper.rb +137 -0
- data/app/javascript/layered_ui/controllers/l_ui/search_form_controller.js +47 -0
- data/app/javascript/layered_ui/index.js +2 -0
- data/app/views/layered_ui/shared/_search_field.html.erb +6 -0
- data/app/views/layered_ui/shared/_search_select.html.erb +8 -0
- data/config/importmap.rb +1 -0
- data/lib/layered/ui/engine.rb +1 -0
- data/lib/layered/ui/version.rb +1 -1
- metadata +25 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8e4d9560a90e896fb66753e342a555c9ce281764e2c3c7ac9d326a4a52d5ff4
|
|
4
|
+
data.tar.gz: 3c1f55587df50d2f0def13536b0adb3c8869682246932fd73ddcd146a63a4e38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99a8dff88d32df2645427d4802d64294744cfc51d185e3b7df7b1fbf25db5887472b654293c6ce3af78440be2730a809c73f37bf282e6166d47b39ec80e85530
|
|
7
|
+
data.tar.gz: 13f9cc999716982874cfc3a6eaff48e63a5c7a06b608e321692de90d573c74e2879229dec97bd6250f3afb5b4bdfd222fef6150643b92e7f38d77b2b28fb85f3
|
data/AGENTS.md
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Guidance for AI agents working in this repository.
|
|
4
4
|
|
|
5
|
-
## Project
|
|
6
|
-
|
|
7
|
-
**layered-ui-rails** - Rails 8+ engine gem: design tokens, Tailwind CSS, Stimulus controllers (theme, mobile nav, panel, modal, tabs). Pure frontend, no server-side logic.
|
|
8
|
-
|
|
9
5
|
## Architecture
|
|
10
6
|
|
|
11
7
|
- **Entry:** `require "layered-ui-rails"` → `lib/layered/ui.rb` → `lib/layered/ui/engine.rb`
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.3.0] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Optional Ransack search helper (`l_ui_search_form`) with styled search form wrapping Ransack's `search_form_for`
|
|
10
|
+
- Sort link helper (`l_ui_sort_link`) with styled header cells wrapping Ransack's `sort_link`
|
|
11
|
+
- Stimulus controller (`l-ui--search-form`) for clear button and Turbo Frame targeting
|
|
12
|
+
- Support for multiple independent Ransack collections on a single page using Turbo Frames and `search_key`
|
|
13
|
+
- Graceful degradation when Ransack is not installed (visible warning in development, silent fallback in production)
|
|
14
|
+
- Ransack integration documentation and search helper API reference pages in the dummy app
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Updated gem description and summary to clarify that the engine is built on Tailwind CSS and integrates with Devise, Pagy, and Ransack
|
|
19
|
+
|
|
5
20
|
## [0.2.5] - 2026-04-09
|
|
6
21
|
|
|
7
22
|
### Changed
|
|
@@ -943,6 +943,17 @@ pre.l-ui-surface {
|
|
|
943
943
|
rounded-sm;
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
+
/* Search */
|
|
947
|
+
|
|
948
|
+
.l-ui-search__inline {
|
|
949
|
+
@apply flex items-center
|
|
950
|
+
gap-2;
|
|
951
|
+
|
|
952
|
+
.l-ui-form__field {
|
|
953
|
+
@apply mt-0;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
946
957
|
/* Switch */
|
|
947
958
|
|
|
948
959
|
.l-ui-switch {
|
|
@@ -1113,6 +1124,16 @@ pre.l-ui-surface {
|
|
|
1113
1124
|
text-right;
|
|
1114
1125
|
}
|
|
1115
1126
|
|
|
1127
|
+
.l-ui-table__sort-link {
|
|
1128
|
+
@apply inline-flex items-center gap-1
|
|
1129
|
+
no-underline
|
|
1130
|
+
text-foreground;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.l-ui-table__sort-indicator {
|
|
1134
|
+
@apply text-foreground-muted;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1116
1137
|
.l-ui-table__body {
|
|
1117
1138
|
@apply h-full
|
|
1118
1139
|
bg-background
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Ui
|
|
3
|
+
module RansackHelper
|
|
4
|
+
# Renders a styled Ransack search form.
|
|
5
|
+
#
|
|
6
|
+
# Simple usage (single input searching across fields):
|
|
7
|
+
# l_ui_search_form(@q, url: users_path, fields: [:name, :email])
|
|
8
|
+
#
|
|
9
|
+
# Custom usage (full control via block):
|
|
10
|
+
# l_ui_search_form(@q, url: users_path) do |f|
|
|
11
|
+
# render "layered_ui/shared/search_field", form: f, field: :name_cont, label: "Name"
|
|
12
|
+
# f.submit "Go", class: "l-ui-button--primary"
|
|
13
|
+
# end
|
|
14
|
+
def l_ui_search_form(query, url: nil, fields: [], predicate: :cont, combinator: :or, label: "Search", placeholder: nil, button: "Search", clear: nil, turbo_frame: nil, html: {}, &block)
|
|
15
|
+
result = require_ransack("l_ui_search_form") { |msg| tag.p(msg, class: "l-ui-notice--warning") }
|
|
16
|
+
return result unless result == true
|
|
17
|
+
|
|
18
|
+
scope = query.context&.search_key || :q
|
|
19
|
+
turbo_action = "advance"
|
|
20
|
+
html = html.merge(class: ["l-ui-form", html[:class]].compact.join(" "))
|
|
21
|
+
|
|
22
|
+
if turbo_frame
|
|
23
|
+
existing_data = (html[:data] || {}).symbolize_keys
|
|
24
|
+
existing_controller = existing_data[:controller]
|
|
25
|
+
controller = [existing_controller, "l-ui--search-form"].compact.join(" ")
|
|
26
|
+
existing_action = existing_data[:action]
|
|
27
|
+
action = [existing_action, "submit->l-ui--search-form#preserve"].compact.join(" ")
|
|
28
|
+
turbo_action = existing_data[:turbo_action] || turbo_action
|
|
29
|
+
|
|
30
|
+
html[:data] = existing_data.except(:controller, :action, :l_ui__search_form_scope_value).merge(
|
|
31
|
+
turbo_frame: turbo_frame, turbo_action: turbo_action,
|
|
32
|
+
controller: controller, action: action,
|
|
33
|
+
l_ui__search_form_scope_value: scope
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if block
|
|
38
|
+
search_form_for(query, url: url, html: html, as: scope, &block)
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "l_ui_search_form requires at least one field in simple mode (e.g. fields: [:name])" if fields.empty?
|
|
41
|
+
|
|
42
|
+
combined_field = fields.map(&:to_s).join("_#{combinator}_") + "_#{predicate}"
|
|
43
|
+
placeholder ||= "Search by #{fields.map { |f| f.to_s.humanize.downcase }.join(', ')}"
|
|
44
|
+
|
|
45
|
+
search_form_for(query, url: url, html: html, as: scope) do |f|
|
|
46
|
+
f.label(combined_field, label, class: "l-ui-sr-only") +
|
|
47
|
+
tag.div(class: "l-ui-search__inline") do
|
|
48
|
+
content = f.text_field(combined_field, class: "l-ui-form__field", placeholder: placeholder) +
|
|
49
|
+
f.submit(button, class: "l-ui-button--primary")
|
|
50
|
+
if clear
|
|
51
|
+
raise ArgumentError, "l_ui_search_form requires an explicit url: when clear: is set" unless url
|
|
52
|
+
clear_options = { class: "l-ui-button--outline" }
|
|
53
|
+
clear_options[:data] = { turbo_frame: turbo_frame, turbo_action: turbo_action, action: "click->l-ui--search-form#clear" } if turbo_frame
|
|
54
|
+
content += link_to(clear == true ? "Clear" : clear, url, **clear_options)
|
|
55
|
+
end
|
|
56
|
+
content
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
SORT_INDICATORS = {
|
|
63
|
+
"asc" => { symbol: "▲", label: ", sorted ascending", aria: "ascending" },
|
|
64
|
+
"desc" => { symbol: "▼", label: ", sorted descending", aria: "descending" }
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Renders a styled, accessible Ransack sort header cell.
|
|
68
|
+
#
|
|
69
|
+
# Returns a +<th>+ element containing a sort link and an accessible sort
|
|
70
|
+
# direction indicator. The +aria-sort+ attribute is set on the +<th>+
|
|
71
|
+
# so screen readers announce the current sort state.
|
|
72
|
+
#
|
|
73
|
+
# Usage:
|
|
74
|
+
# l_ui_sort_link(@q, :name)
|
|
75
|
+
# l_ui_sort_link(@q, :name, "Full name")
|
|
76
|
+
# l_ui_sort_link(@q, :created_at, "Joined", default_order: :desc)
|
|
77
|
+
# l_ui_sort_link(@q, :name, html: { data: { turbo_action: "replace" } })
|
|
78
|
+
def l_ui_sort_link(query, attribute, label = nil, default_order: nil, turbo_frame: nil, html: {})
|
|
79
|
+
label ||= attribute.to_s.humanize
|
|
80
|
+
link_class = ["l-ui-table__sort-link", html[:class]].compact.join(" ")
|
|
81
|
+
|
|
82
|
+
result = require_ransack("l_ui_sort_link") { |msg| tag.th(tag.span(label, title: msg), class: "l-ui-table__header-cell", scope: "col") }
|
|
83
|
+
return (result || tag.th(label, class: "l-ui-table__header-cell", scope: "col")) unless result == true
|
|
84
|
+
|
|
85
|
+
current_dir = sort_direction_for(query, attribute)
|
|
86
|
+
indicator = SORT_INDICATORS[current_dir]
|
|
87
|
+
aria_sort = indicator&.dig(:aria) || "none"
|
|
88
|
+
|
|
89
|
+
url = sort_url(query, attribute, { default_order: default_order }.compact)
|
|
90
|
+
link_html = html.except(:class)
|
|
91
|
+
if turbo_frame
|
|
92
|
+
existing_data = (link_html[:data] || {}).symbolize_keys
|
|
93
|
+
link_html[:data] = existing_data.merge(turbo_frame: turbo_frame, turbo_action: existing_data[:turbo_action] || "advance")
|
|
94
|
+
end
|
|
95
|
+
link = link_to(url, **link_html, class: link_class) do
|
|
96
|
+
parts = [label]
|
|
97
|
+
if indicator
|
|
98
|
+
parts << tag.span(indicator[:symbol], aria: { hidden: true }, class: "l-ui-table__sort-indicator")
|
|
99
|
+
parts << tag.span(indicator[:label], class: "l-ui-sr-only")
|
|
100
|
+
end
|
|
101
|
+
safe_join(parts)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
tag.th(link, class: "l-ui-table__header-cell l-ui-table__header-cell--sortable",
|
|
105
|
+
scope: "col", aria: { sort: aria_sort })
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def ransack_available?
|
|
111
|
+
defined?(Ransack)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns +true+ if Ransack is available. In development, returns the
|
|
115
|
+
# block's result so the caller can render a visible fallback. In
|
|
116
|
+
# production/test, logs and returns +nil+.
|
|
117
|
+
def require_ransack(helper_name)
|
|
118
|
+
return true if ransack_available?
|
|
119
|
+
|
|
120
|
+
message = "#{helper_name} requires the ransack gem. Add `gem \"ransack\"` to your Gemfile."
|
|
121
|
+
|
|
122
|
+
if Rails.env.development?
|
|
123
|
+
return yield(message)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Rails.logger.warn("[layered-ui-rails] #{message} The output has been hidden.")
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def sort_direction_for(query, attribute)
|
|
131
|
+
return unless query.respond_to?(:sorts)
|
|
132
|
+
sort = query.sorts.detect { |s| s.name == attribute.to_s }
|
|
133
|
+
sort&.dir
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Preserves query params from other search scopes when a scoped form submits
|
|
4
|
+
// or its clear link is clicked.
|
|
5
|
+
//
|
|
6
|
+
// When multiple Ransack collections share one page, each form only knows about
|
|
7
|
+
// its own fields. This controller reads the current URL at submit time and
|
|
8
|
+
// injects hidden inputs for params that belong to other scopes, so the
|
|
9
|
+
// resulting URL reflects the full state of all collections. The clear action
|
|
10
|
+
// rewrites the clear link's href to include the same preserved params.
|
|
11
|
+
export default class extends Controller {
|
|
12
|
+
static values = { scope: String }
|
|
13
|
+
|
|
14
|
+
preserve(event) {
|
|
15
|
+
this.element.querySelectorAll("[data-l-ui-preserved]").forEach(el => el.remove())
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of this.#otherParams()) {
|
|
18
|
+
const input = document.createElement("input")
|
|
19
|
+
input.type = "hidden"
|
|
20
|
+
input.name = key
|
|
21
|
+
input.value = value
|
|
22
|
+
input.setAttribute("data-l-ui-preserved", "")
|
|
23
|
+
this.element.appendChild(input)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
clear(event) {
|
|
28
|
+
const link = event.currentTarget
|
|
29
|
+
const preserved = this.#otherParams()
|
|
30
|
+
const base = link.href.split("?")[0]
|
|
31
|
+
const qs = preserved.toString()
|
|
32
|
+
link.href = qs ? `${base}?${qs}` : base
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#otherParams() {
|
|
36
|
+
const currentParams = new URLSearchParams(window.location.search)
|
|
37
|
+
const scope = this.scopeValue
|
|
38
|
+
const result = new URLSearchParams()
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of currentParams) {
|
|
41
|
+
if (key === scope || key.startsWith(scope + "[") || key === "commit") continue
|
|
42
|
+
result.append(key, value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -5,8 +5,10 @@ import PanelController from "layered_ui/controllers/l_ui/panel_controller"
|
|
|
5
5
|
import PanelResizeController from "layered_ui/controllers/l_ui/panel_resize_controller"
|
|
6
6
|
import PanelButtonController from "layered_ui/controllers/l_ui/panel_button_controller"
|
|
7
7
|
import ModalController from "layered_ui/controllers/l_ui/modal_controller"
|
|
8
|
+
import SearchFormController from "layered_ui/controllers/l_ui/search_form_controller"
|
|
8
9
|
import TabsController from "layered_ui/controllers/l_ui/tabs_controller"
|
|
9
10
|
|
|
11
|
+
application.register("l-ui--search-form", SearchFormController)
|
|
10
12
|
application.register("l-ui--theme", ThemeController)
|
|
11
13
|
application.register("l-ui--navigation", NavigationController)
|
|
12
14
|
application.register("l-ui--panel", PanelController)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<% include_blank = local_assigns.fetch(:include_blank, "Any") %>
|
|
2
|
+
|
|
3
|
+
<div class="l-ui-form__group">
|
|
4
|
+
<%= form.label field, label, class: "l-ui-label" %>
|
|
5
|
+
<div class="l-ui-select-wrapper">
|
|
6
|
+
<%= form.select field, options, { include_blank: include_blank }, class: "l-ui-select" %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
data/config/importmap.rb
CHANGED
|
@@ -11,5 +11,6 @@ pin "layered_ui/controllers/l_ui/navigation_controller", to: "layered_ui/control
|
|
|
11
11
|
pin "layered_ui/controllers/l_ui/panel_controller", to: "layered_ui/controllers/l_ui/panel_controller.js"
|
|
12
12
|
pin "layered_ui/controllers/l_ui/panel_resize_controller", to: "layered_ui/controllers/l_ui/panel_resize_controller.js"
|
|
13
13
|
pin "layered_ui/controllers/l_ui/panel_button_controller", to: "layered_ui/controllers/l_ui/panel_button_controller.js"
|
|
14
|
+
pin "layered_ui/controllers/l_ui/search_form_controller", to: "layered_ui/controllers/l_ui/search_form_controller.js"
|
|
14
15
|
pin "layered_ui/controllers/l_ui/tabs_controller", to: "layered_ui/controllers/l_ui/tabs_controller.js"
|
|
15
16
|
pin "layered_ui/controllers/l_ui/theme_controller", to: "layered_ui/controllers/l_ui/theme_controller.js"
|
data/lib/layered/ui/engine.rb
CHANGED
data/lib/layered/ui/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '7.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: ransack
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '4.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '4.0'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: sqlite3
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,9 +163,10 @@ dependencies:
|
|
|
149
163
|
- - "~>"
|
|
150
164
|
- !ruby/object:Gem::Version
|
|
151
165
|
version: '2.0'
|
|
152
|
-
description: An open source Rails 8+ engine
|
|
153
|
-
tokens,
|
|
154
|
-
navigation, slide-out panels, modals, and tabs.
|
|
166
|
+
description: An open source Rails 8+ engine built on Tailwind CSS, providing customisable
|
|
167
|
+
WCAG 2.2 AA compliant design tokens, utility classes, and Stimulus controllers for
|
|
168
|
+
theme switching, mobile navigation, slide-out panels, modals, and tabs. Integrates
|
|
169
|
+
with the gems you already use (Devise, Pagy, Ransack).
|
|
155
170
|
email:
|
|
156
171
|
- support@layered.ai
|
|
157
172
|
executables: []
|
|
@@ -192,11 +207,13 @@ files:
|
|
|
192
207
|
- app/helpers/layered/ui/authentication_helper.rb
|
|
193
208
|
- app/helpers/layered/ui/navigation_helper.rb
|
|
194
209
|
- app/helpers/layered/ui/pagination_helper.rb
|
|
210
|
+
- app/helpers/layered/ui/ransack_helper.rb
|
|
195
211
|
- app/javascript/layered_ui/controllers/l_ui/modal_controller.js
|
|
196
212
|
- app/javascript/layered_ui/controllers/l_ui/navigation_controller.js
|
|
197
213
|
- app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js
|
|
198
214
|
- app/javascript/layered_ui/controllers/l_ui/panel_controller.js
|
|
199
215
|
- app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
|
|
216
|
+
- app/javascript/layered_ui/controllers/l_ui/search_form_controller.js
|
|
200
217
|
- app/javascript/layered_ui/controllers/l_ui/tabs_controller.js
|
|
201
218
|
- app/javascript/layered_ui/controllers/l_ui/theme_controller.js
|
|
202
219
|
- app/javascript/layered_ui/index.js
|
|
@@ -218,6 +235,8 @@ files:
|
|
|
218
235
|
- app/views/layered_ui/shared/_field_error.html.erb
|
|
219
236
|
- app/views/layered_ui/shared/_form_errors.html.erb
|
|
220
237
|
- app/views/layered_ui/shared/_label.html.erb
|
|
238
|
+
- app/views/layered_ui/shared/_search_field.html.erb
|
|
239
|
+
- app/views/layered_ui/shared/_search_select.html.erb
|
|
221
240
|
- app/views/layouts/layered_ui/_header.html.erb
|
|
222
241
|
- app/views/layouts/layered_ui/_navigation.html.erb
|
|
223
242
|
- app/views/layouts/layered_ui/_notice.html.erb
|
|
@@ -272,6 +291,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
272
291
|
requirements: []
|
|
273
292
|
rubygems_version: 4.0.6
|
|
274
293
|
specification_version: 4
|
|
275
|
-
summary: Open source, minimalist
|
|
276
|
-
dark
|
|
294
|
+
summary: Open source, minimalist Tailwind-based UI system for Rails with responsive,
|
|
295
|
+
accessible components and light/dark themes.
|
|
277
296
|
test_files: []
|