lesli_view 1.1.1 → 1.1.3
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/lib/lesli_view/charts/bar.rb +10 -0
- data/lib/lesli_view/charts/general.html.erb +38 -52
- data/lib/lesli_view/charts/general.rb +69 -30
- data/lib/lesli_view/charts/line.rb +16 -0
- data/lib/lesli_view/components/header.html.erb +48 -23
- data/lib/lesli_view/components/header.rb +35 -2
- data/lib/lesli_view/components/toolbar.css +52 -0
- data/lib/lesli_view/components/toolbar.html.erb +36 -21
- data/lib/lesli_view/components/toolbar.rb +41 -4
- data/lib/lesli_view/elements/button.css +357 -0
- data/lib/lesli_view/elements/button.html.erb +15 -12
- data/lib/lesli_view/elements/button.rb +74 -34
- data/lib/lesli_view/elements/empty.html.erb +31 -6
- data/lib/lesli_view/elements/empty.rb +8 -1
- data/lib/lesli_view/elements/table-chart.html.erb +42 -0
- data/lib/lesli_view/elements/table.css +91 -0
- data/lib/lesli_view/elements/table.html.erb +24 -21
- data/lib/lesli_view/elements/table.rb +67 -17
- data/lib/lesli_view/forms/builder.rb +16 -3
- data/lib/lesli_view/forms/builder_horizontal.rb +34 -10
- data/lib/lesli_view/forms/fields.rb +103 -70
- data/lib/lesli_view/forms/fieldset.rb +3 -3
- data/lib/lesli_view/forms/form.css +171 -0
- data/lib/lesli_view/forms/form.scss +2 -54
- data/lib/lesli_view/forms/inputs.rb +27 -15
- data/lib/lesli_view/layout/container.html.erb +8 -2
- data/lib/lesli_view/version.rb +2 -2
- data/lib/lesli_view/widgets/calendar.html.erb +42 -133
- data/lib/lesli_view/widgets/calendar.rb +77 -3
- data/lib/lesli_view/widgets/chart.html.erb +25 -26
- data/lib/lesli_view/widgets/chart.rb +46 -3
- data/lib/lesli_view/widgets/count.html.erb +17 -15
- data/lib/lesli_view/widgets/count.rb +13 -1
- data/lib/lesli_view/widgets/date.html.erb +23 -21
- data/lib/lesli_view/widgets/date.rb +27 -2
- data/lib/lesli_view/widgets/weather.html.erb +40 -38
- data/lib/lesli_view/widgets/weather.rb +41 -2
- data/lib/lesli_view.rb +5 -1
- data/readme.md +158 -29
- metadata +12 -10
- data/lib/lesli_view/components/header.scss +0 -69
- data/lib/lesli_view/components/toolbar.scss +0 -54
- data/lib/lesli_view/elements/table.scss +0 -153
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c372c2ae1e73b645cbac17d23b504506b5c0a530de4ad3b9af37ac3f19ce338
|
|
4
|
+
data.tar.gz: d296585f3691d21398b81803cb3c616233b74e313df210c50434ec0adbc12418
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 906c140274216c77a9ff864dde9b2423919f947cadf4a02e0d327f5d5bd0c3bf9e97bacca7841f48256218a8f1394499ecb3febdd3936e3b665055ea378b3896
|
|
7
|
+
data.tar.gz: 7e914034819645b537b4b157eea35d847f4cb806ac544402cbe099be80dfda12ec4e2fa593bf0813d397df0ba412daa33c4624fcc6c9333304f95096b749b009
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -36,6 +38,14 @@ module LesliView
|
|
|
36
38
|
def type
|
|
37
39
|
"bar"
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def dataset_defaults
|
|
43
|
+
{
|
|
44
|
+
backgroundColor: "rgba(14, 165, 233, 0.72)",
|
|
45
|
+
borderColor: "rgb(2, 132, 199)",
|
|
46
|
+
borderWidth: 1
|
|
47
|
+
}
|
|
48
|
+
end
|
|
39
49
|
end
|
|
40
50
|
end
|
|
41
51
|
end
|
|
@@ -1,55 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Lesli · Ruby on Rails SaaS Development Framework.
|
|
21
|
-
|
|
22
|
-
Made with ♥ by LesliTech
|
|
23
|
-
Building a better future, one line of code at a time.
|
|
24
|
-
|
|
25
|
-
@contact hello@lesli.tech
|
|
26
|
-
@website https://www.lesli.tech
|
|
27
|
-
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
28
|
-
|
|
29
|
-
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
|
-
// ·
|
|
31
|
-
%>
|
|
32
|
-
<script>
|
|
33
|
-
document.addEventListener("turbo:load", () => {
|
|
34
|
-
new LesliChart({
|
|
35
|
-
id: "<%= id %>",
|
|
36
|
-
type: "<%= type %>",
|
|
37
|
-
datasets: <%= raw(datasets.to_json) %>,
|
|
38
|
-
labels: <%= raw(labels.to_json) %>,
|
|
39
|
-
compact: <%= compact ? "true" : "false" %>
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<div class="lesli-charts lesli-charts-<%= type %> section box m-0 p-2">
|
|
45
|
-
|
|
46
|
-
<% if title.present? %>
|
|
47
|
-
<%= tag.h3(title, class: (compact ? "is-size-7 has-text-centered" : "ml-2")) %>
|
|
1
|
+
<div
|
|
2
|
+
class="lesli-chart lesli-chart-<%= type %> flex h-full min-w-0 flex-col"
|
|
3
|
+
<% if title.present? %>aria-labelledby="<%= title_id %>"<% end %>>
|
|
4
|
+
|
|
5
|
+
<% if title.present? || subtitle.present? %>
|
|
6
|
+
<div class="mb-3">
|
|
7
|
+
<% if title.present? %>
|
|
8
|
+
<h3
|
|
9
|
+
id="<%= title_id %>"
|
|
10
|
+
class="<%= compact ? 'text-center text-xs font-medium text-slate-500' : 'text-base font-semibold text-slate-700' %>">
|
|
11
|
+
<%= title %>
|
|
12
|
+
</h3>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<% if subtitle.present? %>
|
|
16
|
+
<p class="mt-1 text-sm leading-5 text-slate-500"><%= subtitle %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
48
19
|
<% end %>
|
|
49
20
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div id="<%= id %>" style="min-height:<%= height %>"></div>
|
|
21
|
+
<div
|
|
22
|
+
id="<%= id %>"
|
|
23
|
+
class="min-w-0 flex-1"
|
|
24
|
+
style="min-height: <%= height %>"></div>
|
|
55
25
|
</div>
|
|
26
|
+
|
|
27
|
+
<script data-lesli-chart-config="<%= chart_config.to_json %>">
|
|
28
|
+
(() => {
|
|
29
|
+
const script = document.currentScript;
|
|
30
|
+
const renderChart = () => {
|
|
31
|
+
if (!window.LesliChart || !script) return;
|
|
32
|
+
new window.LesliChart(JSON.parse(script.dataset.lesliChartConfig));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (document.readyState === "loading") {
|
|
36
|
+
document.addEventListener("turbo:load", renderChart, { once: true });
|
|
37
|
+
} else {
|
|
38
|
+
renderChart();
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -33,48 +35,62 @@ Building a better future, one line of code at a time.
|
|
|
33
35
|
module LesliView
|
|
34
36
|
module Charts
|
|
35
37
|
class General < ViewComponent::Base
|
|
36
|
-
|
|
37
38
|
attr_reader(
|
|
38
|
-
:id,
|
|
39
|
-
:title,
|
|
40
|
-
:subtitle,
|
|
41
|
-
:labels,
|
|
42
|
-
:datasets,
|
|
43
|
-
:height,
|
|
39
|
+
:id,
|
|
40
|
+
:title,
|
|
41
|
+
:subtitle,
|
|
42
|
+
:labels,
|
|
43
|
+
:datasets,
|
|
44
|
+
:height,
|
|
44
45
|
:compact
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
def initialize(
|
|
48
|
-
id:nil,
|
|
49
|
-
title:nil,
|
|
50
|
-
subtitle:nil,
|
|
51
|
-
labels:nil,
|
|
52
|
-
dataset:nil,
|
|
53
|
-
datasets:nil,
|
|
54
|
-
database_to_dataset:nil,
|
|
55
|
-
database_to_datasets:nil,
|
|
56
|
-
height:"400px",
|
|
57
|
-
compact:false
|
|
49
|
+
id: nil,
|
|
50
|
+
title: nil,
|
|
51
|
+
subtitle: nil,
|
|
52
|
+
labels: nil,
|
|
53
|
+
dataset: nil,
|
|
54
|
+
datasets: nil,
|
|
55
|
+
database_to_dataset: nil,
|
|
56
|
+
database_to_datasets: nil,
|
|
57
|
+
height: "400px",
|
|
58
|
+
compact: false
|
|
58
59
|
)
|
|
59
|
-
@id = id || "lesli-chart-#{
|
|
60
|
+
@id = id.presence || "lesli-chart-#{object_id}"
|
|
60
61
|
@title = title
|
|
61
62
|
@subtitle = subtitle
|
|
62
|
-
@labels = labels
|
|
63
|
-
@
|
|
64
|
-
@
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
@labels = Array(labels)
|
|
64
|
+
@height = normalize_height(height)
|
|
65
|
+
@compact = !!compact
|
|
66
|
+
|
|
67
|
+
source_datasets = datasets
|
|
68
|
+
source_datasets = [{ label: title, data: dataset }] unless dataset.nil?
|
|
69
|
+
source_datasets = convert_database_to_dataset(database_to_dataset) if database_to_dataset
|
|
70
|
+
source_datasets = convert_database_to_datasets(database_to_datasets) if database_to_datasets
|
|
71
|
+
@datasets = normalize_datasets(source_datasets)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def type
|
|
73
75
|
nil
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
+
def chart_config
|
|
79
|
+
{
|
|
80
|
+
id: id,
|
|
81
|
+
type: type,
|
|
82
|
+
datasets: datasets,
|
|
83
|
+
labels: labels,
|
|
84
|
+
compact: compact
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def title_id
|
|
89
|
+
@title_id ||= "#{id}-title"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def convert_database_to_dataset(data)
|
|
93
|
+
[{ label: title, data: data.map { |record| { x: record["xaxiskey"], y: record["yaxiskey"] } } }]
|
|
78
94
|
end
|
|
79
95
|
|
|
80
96
|
# Transforms a database query result into a structure compatible with Chart.js
|
|
@@ -94,8 +110,7 @@ module LesliView
|
|
|
94
110
|
# 'sum(agent_count) as data'
|
|
95
111
|
# )
|
|
96
112
|
#
|
|
97
|
-
def
|
|
98
|
-
|
|
113
|
+
def convert_database_to_datasets(data)
|
|
99
114
|
# Group records by series name
|
|
100
115
|
# Example: group all "Chrome" rows together
|
|
101
116
|
data
|
|
@@ -118,6 +133,30 @@ module LesliView
|
|
|
118
133
|
}
|
|
119
134
|
end
|
|
120
135
|
end
|
|
136
|
+
|
|
137
|
+
# Preserve the original public conversion helpers.
|
|
138
|
+
alias_method :database_to_dataset, :convert_database_to_dataset
|
|
139
|
+
alias_method :database_to_datasets, :convert_database_to_datasets
|
|
140
|
+
|
|
141
|
+
def dataset_defaults
|
|
142
|
+
{}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def normalize_datasets(value)
|
|
148
|
+
Array(value).map do |dataset|
|
|
149
|
+
attributes = dataset.respond_to?(:to_h) ? dataset.to_h.symbolize_keys : { data: Array(dataset) }
|
|
150
|
+
dataset_defaults.merge(attributes)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def normalize_height(value)
|
|
155
|
+
value = value.to_s
|
|
156
|
+
return value if value.match?(/\A\d+(?:\.\d+)?(?:px|rem|em|vh|vw|%)\z/)
|
|
157
|
+
|
|
158
|
+
"400px"
|
|
159
|
+
end
|
|
121
160
|
end
|
|
122
161
|
end
|
|
123
162
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -36,6 +38,20 @@ module LesliView
|
|
|
36
38
|
def type
|
|
37
39
|
"line"
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def dataset_defaults
|
|
43
|
+
{
|
|
44
|
+
backgroundColor: "rgba(14, 165, 233, 0.12)",
|
|
45
|
+
borderColor: "rgb(2, 132, 199)",
|
|
46
|
+
borderWidth: 2,
|
|
47
|
+
fill: true,
|
|
48
|
+
pointBackgroundColor: "rgb(2, 132, 199)",
|
|
49
|
+
pointBorderColor: "rgb(255, 255, 255)",
|
|
50
|
+
pointBorderWidth: 2,
|
|
51
|
+
pointRadius: 3,
|
|
52
|
+
pointHoverRadius: 5
|
|
53
|
+
}
|
|
54
|
+
end
|
|
39
55
|
end
|
|
40
56
|
end
|
|
41
57
|
end
|
|
@@ -1,29 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
|
|
2
|
+
<section
|
|
3
|
+
class="mx-auto my-8 flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between"
|
|
4
|
+
<% if title.present? %>aria-labelledby="<%= title_id %>"<% end %>>
|
|
5
|
+
|
|
6
|
+
<div class="min-w-0">
|
|
7
|
+
<div class="flex items-start gap-4">
|
|
8
|
+
|
|
9
|
+
<% if back? %>
|
|
5
10
|
<%= link_to(
|
|
6
|
-
|
|
7
|
-
class: "
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
back_path,
|
|
12
|
+
class: "mt-1 inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-sky-600 bg-white text-sky-700 shadow-sm transition hover:bg-sky-50 hover:text-sky-800 focus:outline-none focus:ring-2 focus:ring-sky-500/30",
|
|
13
|
+
aria: { label: I18n.t("actions.back", default: "Go back") },
|
|
14
|
+
data: { turbo_frame: "_top" }
|
|
15
|
+
) do %>
|
|
16
|
+
<span class="material-symbols-rounded text-[26px] leading-none" aria-hidden="true">arrow_back</span>
|
|
10
17
|
<% end %>
|
|
11
18
|
<% end %>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
|
|
20
|
+
<div class="min-w-0">
|
|
21
|
+
<% if title.present? %>
|
|
22
|
+
<h1 id="<%= title_id %>" class="text-3xl tracking-tight text-slate-700 sm:text-4xl">
|
|
23
|
+
<%= title %>
|
|
24
|
+
</h1>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<% if subtitle.present? %>
|
|
28
|
+
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-500 sm:text-base">
|
|
29
|
+
<%= subtitle %>
|
|
30
|
+
</p>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
15
34
|
</div>
|
|
16
|
-
<% if subtitle.present? %>
|
|
17
|
-
<p><%= subtitle %></p>
|
|
18
|
-
<% end %>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="lesli-element-header-items is-flex is-flex-grow-1 is-justify-content-end">
|
|
21
|
-
<% if new_path %>
|
|
22
|
-
<%= render ::LesliView::Elements::Button.new(
|
|
23
|
-
"Nuevo", url:new_path, icon:"add", solid:true
|
|
24
|
-
) %>
|
|
25
|
-
<% end %>
|
|
26
|
-
<%= content %>
|
|
27
35
|
</div>
|
|
28
|
-
</section>
|
|
29
36
|
|
|
37
|
+
<% if actions? %>
|
|
38
|
+
<div
|
|
39
|
+
class="flex shrink-0 flex-wrap items-center gap-3 sm:justify-end"
|
|
40
|
+
role="group"
|
|
41
|
+
aria-label="<%= I18n.t('actions.label', default: 'Page actions') %>">
|
|
42
|
+
<% if new_path.present? %>
|
|
43
|
+
<%= render ::LesliView::Elements::Button.new(
|
|
44
|
+
new_button_label,
|
|
45
|
+
url: new_path,
|
|
46
|
+
icon: "add",
|
|
47
|
+
solid: true
|
|
48
|
+
) %>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<%= content if content? %>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
</section>
|
|
@@ -35,13 +35,46 @@ Building a better future, one line of code at a time.
|
|
|
35
35
|
module LesliView
|
|
36
36
|
module Components
|
|
37
37
|
class Header < ViewComponent::Base
|
|
38
|
-
attr_reader :title, :subtitle, :back, :new_path
|
|
38
|
+
attr_reader :title, :subtitle, :back, :new_path, :new_label
|
|
39
|
+
|
|
40
|
+
def initialize(title, subtitle = nil, new_path: nil, new_label: nil, back: nil)
|
|
41
|
+
# Support the legacy positional options hash:
|
|
42
|
+
# Header.new("Title", back: path)
|
|
43
|
+
if subtitle.is_a?(Hash)
|
|
44
|
+
legacy_options = subtitle.symbolize_keys
|
|
45
|
+
subtitle = legacy_options[:subtitle]
|
|
46
|
+
new_path ||= legacy_options[:new_path]
|
|
47
|
+
new_label ||= legacy_options[:new_label]
|
|
48
|
+
back ||= legacy_options[:back]
|
|
49
|
+
end
|
|
39
50
|
|
|
40
|
-
def initialize(title, subtitle="", new_path:nil, back:nil)
|
|
41
51
|
@title = title
|
|
42
52
|
@subtitle = subtitle
|
|
43
53
|
@back = back
|
|
44
54
|
@new_path = new_path
|
|
55
|
+
@new_label = new_label
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def back?
|
|
59
|
+
back.present?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def back_path
|
|
63
|
+
return back unless back == true
|
|
64
|
+
|
|
65
|
+
request.referer.presence || "/"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def new_button_label
|
|
69
|
+
new_label.presence || I18n.t("actions.new", default: "New")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def actions?
|
|
73
|
+
new_path.present? || content?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def title_id
|
|
77
|
+
@title_id ||= "lesli-header-title-#{object_id}"
|
|
45
78
|
end
|
|
46
79
|
end
|
|
47
80
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* Tailwind styles for LesliView::Components::Toolbar. */
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.lesli-toolbar {
|
|
5
|
+
@apply mb-6 flex w-full flex-col gap-2 sm:flex-row sm:items-center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.lesli-toolbar__form {
|
|
9
|
+
@apply flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-center;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lesli-toolbar__search {
|
|
13
|
+
@apply relative min-w-0 flex-1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.lesli-toolbar__input {
|
|
17
|
+
@apply h-10 w-full rounded-lg border border-slate-300 bg-white py-2 pl-10 pr-10 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 hover:border-slate-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500/10;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lesli-toolbar__search-icon {
|
|
21
|
+
@apply pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-lg leading-none text-slate-400;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.lesli-toolbar__clear {
|
|
25
|
+
@apply absolute right-1.5 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/30;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.lesli-toolbar__clear .material-symbols {
|
|
29
|
+
@apply text-lg leading-none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.lesli-toolbar__filters {
|
|
33
|
+
@apply flex flex-wrap items-center gap-2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.lesli-toolbar__filters select,
|
|
37
|
+
.lesli-toolbar__filters input {
|
|
38
|
+
@apply h-10 rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-700 outline-none transition hover:border-slate-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500/10;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.lesli-toolbar__submit {
|
|
42
|
+
@apply inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-500 transition hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/20;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lesli-toolbar__submit .material-symbols {
|
|
46
|
+
@apply text-xl leading-none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.lesli-toolbar__actions {
|
|
50
|
+
@apply flex shrink-0 flex-wrap items-center gap-2;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,25 +1,40 @@
|
|
|
1
|
-
<section class="lesli-toolbar
|
|
2
|
-
|
|
3
|
-
<div class="
|
|
4
|
-
<%=
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
<section class="lesli-toolbar" aria-label="<%= search_label %>">
|
|
2
|
+
<%= form_with(url: form_url, method: :get, local: true, class: "lesli-toolbar__form", role: "search") do %>
|
|
3
|
+
<div class="lesli-toolbar__search">
|
|
4
|
+
<%= label_tag(search_name, search_label, class: "sr-only") %>
|
|
5
|
+
|
|
6
|
+
<span class="lesli-toolbar__search-icon material-symbols" aria-hidden="true">search</span>
|
|
7
|
+
|
|
8
|
+
<%= text_field_tag(
|
|
9
|
+
search_name,
|
|
10
|
+
search_value,
|
|
11
|
+
placeholder: placeholder,
|
|
12
|
+
class: "lesli-toolbar__input",
|
|
13
|
+
autocomplete: "off"
|
|
14
|
+
) %>
|
|
15
|
+
|
|
16
|
+
<% if searching? %>
|
|
17
|
+
<%= link_to(clear_url, class: "lesli-toolbar__clear", aria: { label: clear_label }, title: clear_label) do %>
|
|
18
|
+
<span class="material-symbols" aria-hidden="true">close</span>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
17
21
|
</div>
|
|
18
|
-
|
|
19
|
-
<% if
|
|
20
|
-
<div class="
|
|
21
|
-
<%=
|
|
22
|
+
|
|
23
|
+
<% if filters? %>
|
|
24
|
+
<div class="lesli-toolbar__filters">
|
|
25
|
+
<%= filters %>
|
|
22
26
|
</div>
|
|
23
27
|
<% end %>
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
<button class="lesli-toolbar__submit" type="submit">
|
|
30
|
+
<span class="material-symbols" aria-hidden="true">search</span>
|
|
31
|
+
<span class="sr-only"><%= search_label %></span>
|
|
32
|
+
</button>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<% if actions? %>
|
|
36
|
+
<div class="lesli-toolbar__actions" role="group" aria-label="<%= I18n.t('lesli_view.toolbar.actions', default: 'Toolbar actions') %>">
|
|
37
|
+
<%= actions %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
25
40
|
</section>
|
|
@@ -35,12 +35,49 @@ Building a better future, one line of code at a time.
|
|
|
35
35
|
module LesliView
|
|
36
36
|
module Components
|
|
37
37
|
class Toolbar < ViewComponent::Base
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
renders_one :filters
|
|
39
|
+
renders_one :actions
|
|
40
|
+
|
|
41
|
+
attr_reader :search_placeholder, :initial_value, :url, :search_name
|
|
42
|
+
|
|
43
|
+
def initialize(search_placeholder = nil, initial_value: nil, url: nil, search_name: :search)
|
|
41
44
|
@search_placeholder = search_placeholder
|
|
42
45
|
@initial_value = initial_value
|
|
43
|
-
@
|
|
46
|
+
@url = url
|
|
47
|
+
@search_name = search_name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def form_url
|
|
51
|
+
url.presence || request.path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def search_value
|
|
55
|
+
return initial_value unless initial_value.nil?
|
|
56
|
+
|
|
57
|
+
request.query_parameters[search_name.to_s]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def searching?
|
|
61
|
+
search_value.present?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear_url
|
|
65
|
+
query = request.query_parameters.except(search_name.to_s)
|
|
66
|
+
return form_url if query.empty?
|
|
67
|
+
|
|
68
|
+
"#{form_url}?#{query.to_query}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def placeholder
|
|
72
|
+
search_placeholder.presence || I18n.t("lesli_view.toolbar.placeholder", default: "Search...")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def search_label
|
|
76
|
+
I18n.t("lesli_view.toolbar.search", default: "Search")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def clear_label
|
|
80
|
+
I18n.t("lesli_view.toolbar.clear", default: "Clear search")
|
|
44
81
|
end
|
|
45
82
|
end
|
|
46
83
|
end
|