lesli_view 1.1.2 → 1.1.4
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/tabs.html.erb +59 -26
- data/lib/lesli_view/components/tabs.rb +7 -7
- data/lib/lesli_view/components/timeline.html.erb +28 -25
- 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/avatar.html.erb +3 -3
- data/lib/lesli_view/elements/avatar.rb +8 -8
- 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/items/activities.html.erb +4 -6
- data/lib/lesli_view/items/discussion.html.erb +6 -5
- data/lib/lesli_view/items/discussions.html.erb +8 -6
- data/lib/lesli_view/items/task.html.erb +16 -11
- data/lib/lesli_view/items/tasks.html.erb +8 -13
- data/lib/lesli_view/layout/container.html.erb +8 -2
- data/lib/lesli_view/partials/engine_spec.html.erb +28 -41
- 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/readme.md +185 -29
- metadata +6 -10
- data/lib/lesli_view/components/header.scss +0 -69
- data/lib/lesli_view/components/tabs.scss +0 -49
- data/lib/lesli_view/components/tabs_spec.rb +0 -0
- data/lib/lesli_view/components/timeline.scss +0 -216
- data/lib/lesli_view/components/toolbar.scss +0 -54
- data/lib/lesli_view/elements/avatar.scss +0 -50
- data/lib/lesli_view/elements/table.scss +0 -153
- data/lib/lesli_view/items/activities.scss +0 -39
- data/lib/lesli_view/items/discussions.scss +0 -47
|
@@ -30,9 +30,9 @@ Building a better future, one line of code at a time.
|
|
|
30
30
|
// ·
|
|
31
31
|
%>
|
|
32
32
|
|
|
33
|
-
<li
|
|
33
|
+
<li
|
|
34
34
|
id="<%= dom_id(@resource, @scope_key) %>"
|
|
35
|
-
class="lesli-items-tasks-item
|
|
35
|
+
class="lesli-items-tasks-item rounded-xl border transition-colors <%= @resource.done ? "lesli-items-tasks-task-done cursor-not-allowed border-slate-200 bg-slate-50" : "border-slate-200 hover:border-slate-300 hover:bg-slate-50" %>">
|
|
36
36
|
<%= form_with(
|
|
37
37
|
url: helpers.polymorphic_path(@resource),
|
|
38
38
|
method: :patch,
|
|
@@ -40,16 +40,21 @@ Building a better future, one line of code at a time.
|
|
|
40
40
|
local: true
|
|
41
41
|
) do |form| %>
|
|
42
42
|
<%= form.hidden_field :id, value: @resource.id %>
|
|
43
|
-
<%= form.button
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
<%= form.button(
|
|
44
|
+
type: "submit",
|
|
45
|
+
disabled: @resource.done?,
|
|
46
|
+
class: "flex w-full cursor-pointer items-center rounded-xl px-4 py-3 text-left text-sm font-medium text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--lesli-color-primary)] disabled:cursor-not-allowed"
|
|
47
|
+
) do %>
|
|
48
|
+
<span class="flex min-w-0 items-center gap-3">
|
|
49
|
+
<span
|
|
50
|
+
class="material-symbols-outlined shrink-0 text-xl leading-none <%= @resource.done ? "text-slate-400" : "text-[var(--lesli-color-primary)]" %>"
|
|
51
|
+
aria-hidden="true">
|
|
52
|
+
<%= @resource.done ? "check_box" : "check_box_outline_blank" %>
|
|
49
53
|
</span>
|
|
50
|
-
<span
|
|
51
|
-
|
|
54
|
+
<span class="min-w-0 break-words <%= "text-slate-500 line-through" if @resource.done %>">
|
|
55
|
+
<%= @resource.title %>
|
|
56
|
+
</span>
|
|
57
|
+
</span>
|
|
52
58
|
<% end %>
|
|
53
59
|
<% end %>
|
|
54
60
|
</li>
|
|
55
|
-
|
|
@@ -29,16 +29,9 @@ Building a better future, one line of code at a time.
|
|
|
29
29
|
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
30
|
// ·
|
|
31
31
|
%>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
text-decoration: line-through;
|
|
36
|
-
opacity: 0.6;
|
|
37
|
-
cursor: not-allowed;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
</style>
|
|
41
|
-
<article class="lesli-items-tasks box p-6" id="<%= dom_id(@resource, :tasks) %>">
|
|
32
|
+
<article
|
|
33
|
+
class="lesli-items-tasks rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
|
|
34
|
+
id="<%= dom_id(@resource, :tasks) %>">
|
|
42
35
|
<div class="lesli-items-tasks-form">
|
|
43
36
|
<% task_class = "#{@resource.class.name.deconstantize}::Items::Task".constantize %>
|
|
44
37
|
<% scope_key = ActiveSupport::Inflector.underscore(task_class.name).tr("/", "_") %>
|
|
@@ -54,13 +47,15 @@ Building a better future, one line of code at a time.
|
|
|
54
47
|
<%= form.field_control_submit "Add task" %>
|
|
55
48
|
<% end %>
|
|
56
49
|
</div>
|
|
57
|
-
<hr>
|
|
50
|
+
<hr class="my-6 border-0 border-t border-slate-200">
|
|
51
|
+
|
|
58
52
|
<div class="lesli-items-tasks-container">
|
|
59
|
-
<ul
|
|
53
|
+
<ul
|
|
54
|
+
id="<%= "#{scope_key}-lesli-items-tasks-list" %>"
|
|
55
|
+
class="m-0 list-none space-y-3 p-0">
|
|
60
56
|
<% @resource.tasks.each do |task| %>
|
|
61
57
|
<%= render(LesliView::Items::Task.new(task, scope_key)) %>
|
|
62
58
|
<% end %>
|
|
63
59
|
</ul>
|
|
64
60
|
</div>
|
|
65
61
|
</article>
|
|
66
|
-
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
<%
|
|
3
|
+
container_class = class_names(
|
|
4
|
+
"lesli-application-container mx-auto block w-full px-4 sm:px-6 lg:px-8",
|
|
5
|
+
dashboard ? "container" : "max-w-7xl"
|
|
6
|
+
)
|
|
7
|
+
%>
|
|
8
|
+
|
|
3
9
|
<turbo-frame
|
|
4
10
|
id="<%= turbo_frame_id %>"
|
|
5
|
-
class="
|
|
11
|
+
class="<%= container_class %>">
|
|
6
12
|
<%= content %>
|
|
7
13
|
</turbo-frame>
|
|
@@ -1,51 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
<style>
|
|
3
|
-
.partial-engine-spec div {
|
|
4
|
-
border-bottom: 1px solid silver;
|
|
5
|
-
}
|
|
6
|
-
.partial-engine-spec li {
|
|
7
|
-
font-size: 1.1rem;
|
|
8
|
-
margin-left: .4rem;
|
|
9
|
-
}
|
|
10
|
-
</style>
|
|
11
1
|
<div class="partial-engine-spec">
|
|
12
|
-
<div class="mb-
|
|
13
|
-
<h5 class="
|
|
14
|
-
<p class="
|
|
2
|
+
<div class="mb-4 border-b border-slate-300 pb-3">
|
|
3
|
+
<h5 class="text-xl font-semibold text-slate-800"><%= engine[:name] %></h5>
|
|
4
|
+
<p class="mt-1 text-xl leading-7 text-slate-600"><%= engine[:summary] %></p>
|
|
15
5
|
</div>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</span>
|
|
22
|
-
<span></span>
|
|
6
|
+
|
|
7
|
+
<ul class="m-0 list-none space-y-2 p-0">
|
|
8
|
+
<li class="ml-1.5 flex items-center gap-2 text-[1.1rem] text-slate-600">
|
|
9
|
+
<span class="material-symbols shrink-0 text-2xl leading-none text-slate-400" aria-hidden="true">
|
|
10
|
+
pest_control
|
|
23
11
|
</span>
|
|
24
|
-
Version: <%= engine[:version]
|
|
12
|
+
<span>Version: <%= engine[:version] %></span>
|
|
25
13
|
</li>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</span>
|
|
31
|
-
<span></span>
|
|
14
|
+
|
|
15
|
+
<li class="ml-1.5 flex items-center gap-2 text-[1.1rem] text-slate-600">
|
|
16
|
+
<span class="material-symbols shrink-0 text-2xl leading-none text-slate-400" aria-hidden="true">
|
|
17
|
+
schedule
|
|
32
18
|
</span>
|
|
33
|
-
Build: <%= LesliDate::Formatter.new(Time.at(engine[:build].to_i)).date_time
|
|
19
|
+
<span>Build: <%= LesliDate::Formatter.new(Time.at(engine[:build].to_i)).date_time %></span>
|
|
34
20
|
</li>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
21
|
+
|
|
22
|
+
<li class="ml-1.5 flex min-w-0 items-center gap-2 text-[1.1rem] text-slate-600">
|
|
23
|
+
<span class="material-symbols shrink-0 text-2xl leading-none text-slate-400" aria-hidden="true">
|
|
24
|
+
library_books
|
|
25
|
+
</span>
|
|
26
|
+
<span class="min-w-0 break-all">
|
|
27
|
+
<%= link_to_if(
|
|
28
|
+
engine.dig(:metadata, "documentation_uri"),
|
|
29
|
+
engine.dig(:metadata, "documentation_uri"),
|
|
30
|
+
engine.dig(:metadata, "documentation_uri"),
|
|
31
|
+
target: "_blank",
|
|
32
|
+
rel: "noopener noreferrer",
|
|
33
|
+
class: "font-sans text-[var(--lesli-color-primary)] underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--lesli-color-primary)]"
|
|
34
|
+
) %>
|
|
41
35
|
</span>
|
|
42
|
-
<%= link_to_if(
|
|
43
|
-
engine.dig(:metadata, "documentation_uri"),
|
|
44
|
-
engine.dig(:metadata, "documentation_uri"),
|
|
45
|
-
engine.dig(:metadata, "documentation_uri"),
|
|
46
|
-
target: "_blank",
|
|
47
|
-
class: "is-family-sans-serif"
|
|
48
|
-
) %>
|
|
49
36
|
</li>
|
|
50
37
|
</ul>
|
|
51
38
|
</div>
|
data/lib/lesli_view/version.rb
CHANGED
|
@@ -1,135 +1,44 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.lesli-view-widgets-calendar-content .weekday li,
|
|
13
|
-
.lesli-view-widgets-calendar-content .days li {
|
|
14
|
-
display: grid;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.lesli-view-widgets-calendar-content .weekday li a,
|
|
18
|
-
.lesli-view-widgets-calendar-content .days li a {
|
|
19
|
-
text-transform: uppercase;
|
|
20
|
-
display: grid;
|
|
21
|
-
place-items: center;
|
|
22
|
-
width: 100%;
|
|
23
|
-
height: 100%;
|
|
24
|
-
color: #747978;
|
|
25
|
-
font-size: 12px;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.lesli-view-widgets-calendar-content .weekday li a {
|
|
29
|
-
margin-bottom: 8px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.lesli-view-widgets-calendar-content .days li a {
|
|
33
|
-
border-radius: 5px;
|
|
34
|
-
font-weight: bold;
|
|
35
|
-
padding: 4px 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.lesli-view-widgets-calendar-content .days li .selected{
|
|
39
|
-
background-color: #F5A1A3!important;
|
|
40
|
-
color: #ffffff;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.lesli-view-widgets-calendar-content .days li a:hover{
|
|
44
|
-
background-color: #EEEDE9;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.lesli-view-widgets-calendar-content .days li .event{
|
|
48
|
-
color: #F5A1A3;
|
|
49
|
-
}
|
|
50
|
-
</style>
|
|
51
|
-
<div class="lesli-view-widgets-calendar">
|
|
52
|
-
<div class="box">
|
|
53
|
-
<div class="lesli-view-widgets-calendar-content">
|
|
54
|
-
<ul class="weekday">
|
|
55
|
-
<li><a href="#" title="Sun" data-value="7">Sun</a></li>
|
|
56
|
-
<li><a href="#" title="Mon" data-value="1">Mon</a></li>
|
|
57
|
-
<li><a href="#" title="Tue" data-value="2">Tue</a></li>
|
|
58
|
-
<li><a href="#" title="Wed" data-value="3">Wed</a></li>
|
|
59
|
-
<li><a href="#" title="Thu" data-value="4">Thu</a></li>
|
|
60
|
-
<li><a href="#" title="Fri" data-value="5">Fri</a></li>
|
|
61
|
-
<li><a href="#" title="Say" data-value="6">Sat</a></li>
|
|
62
|
-
</ul>
|
|
63
|
-
<ul class="days" id="lesli-view-widgets-calendar-days">
|
|
64
|
-
</ul>
|
|
65
|
-
</div>
|
|
1
|
+
<section
|
|
2
|
+
class="lesli-calendar-widget h-full rounded-2xl border border-slate-200 bg-white p-3 shadow-sm sm:p-4"
|
|
3
|
+
aria-label="<%= calendar_title %>">
|
|
4
|
+
|
|
5
|
+
<div class="grid grid-cols-7 gap-0.5">
|
|
6
|
+
<% weekdays.each do |abbreviated_name, full_name| %>
|
|
7
|
+
<div class="py-0.5 text-center text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
8
|
+
<abbr class="no-underline" title="<%= full_name %>"><%= abbreviated_name %></abbr>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
66
11
|
</div>
|
|
67
|
-
</div>
|
|
68
|
-
<script>
|
|
69
|
-
(() => {
|
|
70
|
-
|
|
71
|
-
const current = new Date();
|
|
72
|
-
|
|
73
|
-
const daysEl = document.getElementById('lesli-view-widgets-calendar-days');
|
|
74
|
-
|
|
75
|
-
// Example event days (YYYY-MM-DD)
|
|
76
|
-
const events = new Set([
|
|
77
|
-
'2026-01-08',
|
|
78
|
-
'2026-01-10',
|
|
79
|
-
'2026-01-27'
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
function renderCalendar(date) {
|
|
83
|
-
daysEl.innerHTML = '';
|
|
84
12
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
) {
|
|
118
|
-
a.classList.add('selected');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
a.addEventListener('click', e => {
|
|
122
|
-
e.preventDefault();
|
|
123
|
-
document
|
|
124
|
-
.querySelectorAll('.days a.selected')
|
|
125
|
-
.forEach(el => el.classList.remove('selected'));
|
|
126
|
-
a.classList.add('selected');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
li.appendChild(a);
|
|
130
|
-
daysEl.appendChild(li);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
renderCalendar(current);
|
|
134
|
-
})();
|
|
135
|
-
</script>
|
|
13
|
+
<div class="mt-0.5 grid grid-cols-7 gap-0.5">
|
|
14
|
+
<% calendar_days.each do |day| %>
|
|
15
|
+
<% if day.nil? %>
|
|
16
|
+
<span class="h-8 sm:h-9" aria-hidden="true"></span>
|
|
17
|
+
<% else %>
|
|
18
|
+
<% day_date = date_for(day) %>
|
|
19
|
+
<div
|
|
20
|
+
class="relative flex h-8 items-center justify-center rounded-lg text-sm font-medium sm:h-9
|
|
21
|
+
<%= if selected?(day)
|
|
22
|
+
"bg-sky-600 text-white shadow-sm"
|
|
23
|
+
elsif today?(day)
|
|
24
|
+
"bg-sky-50 text-sky-700 ring-1 ring-inset ring-sky-200"
|
|
25
|
+
else
|
|
26
|
+
"text-slate-600"
|
|
27
|
+
end %>">
|
|
28
|
+
|
|
29
|
+
<time
|
|
30
|
+
datetime="<%= day_date.iso8601 %>"
|
|
31
|
+
aria-label="<%= I18n.l(day_date, format: :long) %>"
|
|
32
|
+
<% if today?(day) %>aria-current="date"<% end %>><%= day %></time>
|
|
33
|
+
|
|
34
|
+
<% if event?(day) %>
|
|
35
|
+
<span
|
|
36
|
+
class="absolute bottom-1 h-1 w-1 rounded-full <%= selected?(day) ? 'bg-white' : 'bg-rose-400' %>"
|
|
37
|
+
aria-hidden="true"></span>
|
|
38
|
+
<span class="sr-only"><%= I18n.t("date.has_events", default: "Has events") %></span>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -33,11 +35,83 @@ Building a better future, one line of code at a time.
|
|
|
33
35
|
module LesliView
|
|
34
36
|
module Widgets
|
|
35
37
|
class Calendar < ViewComponent::Base
|
|
36
|
-
|
|
38
|
+
WEEK_STARTS = {
|
|
39
|
+
sunday: 0,
|
|
40
|
+
monday: 1
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :title, :number, :date, :selected_date, :week_starts_on
|
|
37
44
|
|
|
38
|
-
def initialize(
|
|
45
|
+
def initialize(
|
|
46
|
+
title = nil,
|
|
47
|
+
number = nil,
|
|
48
|
+
date: ::Date.current,
|
|
49
|
+
events: [],
|
|
50
|
+
selected_date: ::Date.current,
|
|
51
|
+
week_starts_on: :sunday
|
|
52
|
+
)
|
|
39
53
|
@title = title
|
|
40
|
-
@number = number
|
|
54
|
+
@number = number # Retained for compatibility with the original positional API.
|
|
55
|
+
@date = normalize_date(date) || ::Date.current
|
|
56
|
+
@selected_date = normalize_date(selected_date)
|
|
57
|
+
@week_starts_on = normalize_week_start(week_starts_on)
|
|
58
|
+
@event_dates = Array(events).each_with_object({}) do |event, dates|
|
|
59
|
+
event_date = normalize_date(event)
|
|
60
|
+
dates[event_date] = true if event_date
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def calendar_title
|
|
65
|
+
title.presence || I18n.l(date, format: "%B %Y")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def weekdays
|
|
69
|
+
abbreviated = Array(I18n.t("date.abbr_day_names"))
|
|
70
|
+
full = Array(I18n.t("date.day_names"))
|
|
71
|
+
offset = WEEK_STARTS.fetch(week_starts_on)
|
|
72
|
+
|
|
73
|
+
abbreviated.zip(full).rotate(offset)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def calendar_days
|
|
77
|
+
first_day = date.beginning_of_month
|
|
78
|
+
leading_blanks = (first_day.wday - WEEK_STARTS.fetch(week_starts_on)) % 7
|
|
79
|
+
days = Array.new(leading_blanks) + (1..date.end_of_month.day).to_a
|
|
80
|
+
days.concat(Array.new((7 - (days.length % 7)) % 7))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def date_for(day)
|
|
84
|
+
date.change(day: day)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def selected?(day)
|
|
88
|
+
selected_date == date_for(day)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def today?(day)
|
|
92
|
+
::Date.current == date_for(day)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def event?(day)
|
|
96
|
+
@event_dates.key?(date_for(day))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def normalize_date(value)
|
|
102
|
+
return if value.nil?
|
|
103
|
+
return value.to_date if value.respond_to?(:to_date)
|
|
104
|
+
|
|
105
|
+
::Date.parse(value.to_s)
|
|
106
|
+
rescue ArgumentError, TypeError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def normalize_week_start(value)
|
|
111
|
+
value = value.to_s.downcase.to_sym
|
|
112
|
+
return value if WEEK_STARTS.key?(value)
|
|
113
|
+
|
|
114
|
+
:sunday
|
|
41
115
|
end
|
|
42
116
|
end
|
|
43
117
|
end
|
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
</style>
|
|
1
|
+
<section
|
|
2
|
+
class="lesli-chart-widget h-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
3
|
+
<% if title.present? %>aria-labelledby="<%= title_id %>"<% else %>aria-label="<%= I18n.t('charts.chart', default: 'Chart') %>"<% end %>>
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
LesliView::Charts::Line
|
|
13
|
-
else
|
|
14
|
-
nil # Handle unknown types gracefully
|
|
15
|
-
end
|
|
16
|
-
%>
|
|
5
|
+
<div class="flex h-full min-h-40 flex-col">
|
|
6
|
+
<% if title.present? %>
|
|
7
|
+
<h2 id="<%= title_id %>" class="mb-3 text-sm font-medium leading-5 text-slate-500">
|
|
8
|
+
<%= title %>
|
|
9
|
+
</h2>
|
|
10
|
+
<% end %>
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
<% if data? %>
|
|
13
|
+
<div class="min-h-0 flex-1">
|
|
14
|
+
<%= render(chart_component.new(
|
|
15
|
+
labels: labels,
|
|
16
|
+
datasets: [{ label: title, data: dataset }],
|
|
17
|
+
height: "10rem",
|
|
18
|
+
compact: true
|
|
19
|
+
)) %>
|
|
20
|
+
</div>
|
|
21
|
+
<% else %>
|
|
22
|
+
<div class="flex flex-1 items-center justify-center text-sm text-slate-400">
|
|
23
|
+
<%= I18n.t("charts.empty", default: "No chart data") %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -33,12 +35,53 @@ Building a better future, one line of code at a time.
|
|
|
33
35
|
module LesliView
|
|
34
36
|
module Widgets
|
|
35
37
|
class Chart < ViewComponent::Base
|
|
38
|
+
CHART_COMPONENTS = {
|
|
39
|
+
bar: LesliView::Charts::Bar,
|
|
40
|
+
line: LesliView::Charts::Line
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
36
43
|
attr_reader :title, :data, :type
|
|
37
44
|
|
|
38
|
-
def initialize(title=nil, data=nil, type: :bar)
|
|
45
|
+
def initialize(title = nil, data = nil, type: :bar)
|
|
39
46
|
@title = title
|
|
40
|
-
@data = data
|
|
41
|
-
@type = type
|
|
47
|
+
@data = normalize_data(data)
|
|
48
|
+
@type = normalize_type(type)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def chart_component
|
|
52
|
+
CHART_COMPONENTS.fetch(type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def labels
|
|
56
|
+
data.map(&:first)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dataset
|
|
60
|
+
data.map(&:last)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def data?
|
|
64
|
+
data.any?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def title_id
|
|
68
|
+
@title_id ||= "lesli-chart-widget-title-#{object_id}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def normalize_data(value)
|
|
74
|
+
return [] if value.nil?
|
|
75
|
+
return value.to_a if value.respond_to?(:to_a)
|
|
76
|
+
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_type(value)
|
|
81
|
+
value = value.to_s.downcase.to_sym
|
|
82
|
+
return value if CHART_COMPONENTS.key?(value)
|
|
83
|
+
|
|
84
|
+
:bar
|
|
42
85
|
end
|
|
43
86
|
end
|
|
44
87
|
end
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
<section
|
|
2
|
+
class="lesli-count-widget h-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
3
|
+
<% if title.present? %>aria-labelledby="<%= title_id %>"<% else %>aria-label="<%= I18n.t('metrics.count', default: 'Count') %>"<% end %>>
|
|
4
|
+
|
|
5
|
+
<div class="flex h-full min-h-40 flex-col">
|
|
6
|
+
<% if title.present? %>
|
|
7
|
+
<h2 id="<%= title_id %>" class="text-sm font-medium leading-5 text-slate-500">
|
|
8
|
+
<%= title %>
|
|
9
|
+
</h2>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<div class="flex flex-1 items-center justify-center py-4">
|
|
13
|
+
<p class="break-all text-center text-5xl font-semibold leading-none tracking-tight text-sky-600 tabular-nums sm:text-6xl">
|
|
14
|
+
<%= display_number %>
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
15
17
|
</div>
|
|
16
|
-
</
|
|
18
|
+
</section>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
=begin
|
|
2
4
|
|
|
3
5
|
Lesli
|
|
@@ -35,10 +37,20 @@ module LesliView
|
|
|
35
37
|
class Count < ViewComponent::Base
|
|
36
38
|
attr_reader :title, :number
|
|
37
39
|
|
|
38
|
-
def initialize(title=nil, number=nil)
|
|
40
|
+
def initialize(title = nil, number = nil)
|
|
39
41
|
@title = title
|
|
40
42
|
@number = number
|
|
41
43
|
end
|
|
44
|
+
|
|
45
|
+
def display_number
|
|
46
|
+
return "—" if number.nil?
|
|
47
|
+
|
|
48
|
+
number
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def title_id
|
|
52
|
+
@title_id ||= "lesli-count-title-#{object_id}"
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
56
|
end
|